Files
goclaw/internal/tools/credential_context.go
Goon 75c570e951 feat(security): credentialed exec + HTTP RBAC + API key cache (#197)
- Secure CLI credential injection via AES-256-GCM encrypted env vars
- API key management with fine-grained RBAC scopes
- resolveAuth/requireAuth middleware across all 25+ HTTP handlers
- In-memory API key cache with TTL, negative caching, pubsub invalidation
- Sandbox-first execution (fails if unavailable, no silent fallback)
- Credential scrubbing, constant-time token comparison, Admin-only CLI creds
- SQL migration 000020: secure_cli_binaries + api_keys tables
- 14 unit tests for cache and RBAC with race detector

Closes #197
2026-03-15 20:13:18 +07:00

69 lines
2.6 KiB
Go

package tools
import (
"encoding/json"
"fmt"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// GenerateCredentialContext builds a TOOLS.md supplement from enabled secure CLI configs.
// This context is injected into the agent's system prompt so the LLM knows:
// - Which CLIs are available with pre-configured auth
// - That these CLIs run in Direct Exec Mode (no shell operators)
// - Which operations are blocked per CLI
// Returns empty string if no credentialed CLIs are configured.
func GenerateCredentialContext(creds []store.SecureCLIBinary) string {
if len(creds) == 0 {
return ""
}
var b strings.Builder
b.WriteString("\n\n## Credentialed CLI Tools\n\n")
b.WriteString("The following CLI tools have pre-configured authentication.\n")
b.WriteString("Credentials are injected automatically — do NOT attempt to provide or read credentials.\n\n")
b.WriteString("⚠️ CRITICAL: These tools run in DIRECT EXEC MODE (no shell).\n")
b.WriteString("- Do NOT use shell operators: ; && || | > >> < $() ``\n")
b.WriteString("- Do NOT use environment variables: $VAR, ${VAR}\n")
b.WriteString("- Each exec() call runs ONE command only\n")
b.WriteString("- Use --json or --format=json for structured output\n")
b.WriteString("- Parse JSON output directly — do NOT pipe to jq\n\n")
b.WriteString("### Available CLIs:\n\n")
for _, c := range creds {
b.WriteString(fmt.Sprintf("**%s** — %s\n", c.BinaryName, c.Description))
if blocked := summarizeDenyPatterns(c.DenyArgs); blocked != "" {
b.WriteString(fmt.Sprintf(" Blocked: %s\n", blocked))
}
if c.Tips != "" {
b.WriteString(fmt.Sprintf(" Tip: %s\n", c.Tips))
}
b.WriteString("\n")
}
b.WriteString("### When a command is blocked:\n")
b.WriteString("Tell the user: \"This operation requires admin approval and cannot be performed automatically.\"\n")
b.WriteString("Do NOT attempt workarounds to bypass blocked commands.\n")
return b.String()
}
// summarizeDenyPatterns converts regex deny patterns to a human-readable summary.
// E.g. ["auth\\s+", "ssh-key", "repo\\s+delete"] -> "auth, ssh-key, repo delete"
func summarizeDenyPatterns(patternsJSON json.RawMessage) string {
if len(patternsJSON) == 0 {
return ""
}
var patterns []string
if err := json.Unmarshal(patternsJSON, &patterns); err != nil || len(patterns) == 0 {
return ""
}
// Convert regex patterns to readable form by stripping common regex syntax
readable := make([]string, 0, len(patterns))
replacer := strings.NewReplacer(`\s+`, " ", `\s*`, " ", `\b`, "", `\w+`, "*")
for _, p := range patterns {
readable = append(readable, replacer.Replace(p))
}
return strings.Join(readable, ", ")
}