diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 271db94..028934d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -100,8 +100,9 @@ Protecciones contra prompt injection y abuso de tools (issue 0019): - **Rate limiting** — por room en `tools/registry.go` via `security.tool_rate_limit` - **System prompts** — seccion anti-injection obligatoria (template en `.claude/templates/security-prompt.md`) - **`storage.base_path`** — permite aislar datos de runtime fuera del arbol del proyecto +- **`claude_code.working_dir`** — aislamiento del subproceso `claude -p` fuera del repo (default: tmpdir) -Config YAML relevante: `security.sanitize.*`, `security.tool_rate_limit.*`, `storage.base_path` +Config YAML relevante: `security.sanitize.*`, `security.tool_rate_limit.*`, `storage.base_path`, `claude_code.working_dir` Documentacion completa: `docs/security.md` ## Preferencias diff --git a/.claude/rules/create_agent.md b/.claude/rules/create_agent.md index 4578467..bdd2e70 100644 --- a/.claude/rules/create_agent.md +++ b/.claude/rules/create_agent.md @@ -84,6 +84,18 @@ llm: api_key_env: ANTHROPIC_API_KEY # o OPENAI_API_KEY (default) ``` +**Claude-code provider** (si usa `claude-code` como provider): +```yaml +llm: + primary: + provider: claude-code + claude_code: + working_dir: "/tmp/claude-agents/" # SIEMPRE configurar, nunca dejar vacío + permission_mode: "bypassPermissions" +``` + +**Importante**: `working_dir` debe apuntar fuera del repositorio para evitar que el subproceso `claude -p` acceda al código fuente. Si se deja vacío, se usará un directorio temporal (con WARN en logs). + **Tool use** (si el agente necesita herramientas): ```yaml llm: diff --git a/agents/asistente-2/config.yaml b/agents/asistente-2/config.yaml index ff2a4dd..98affbe 100644 --- a/agents/asistente-2/config.yaml +++ b/agents/asistente-2/config.yaml @@ -54,7 +54,7 @@ llm: disable_tools: true # no ejecuta herramientas internas de claude allowed_tools: [] disallowed_tools: [] - working_dir: "" + working_dir: "/tmp/claude-agents/asistente-2" permission_mode: "bypassPermissions" model: "sonnet" fallback_model: "" diff --git a/agents/assistant-bot/config.yaml b/agents/assistant-bot/config.yaml index e555f12..f0c6436 100644 --- a/agents/assistant-bot/config.yaml +++ b/agents/assistant-bot/config.yaml @@ -54,7 +54,7 @@ llm: disable_tools: true # no ejecuta herramientas internas de claude allowed_tools: [] disallowed_tools: [] - working_dir: "" + working_dir: "/tmp/claude-agents/assistant-bot" permission_mode: "bypassPermissions" model: "sonnet" # modelo interno de claude -p fallback_model: "" diff --git a/dev/issues/README.md b/dev/issues/README.md index 95efce4..63f0e9c 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -24,4 +24,4 @@ afectados y notas de implementacion. | 17 | MCP client tools | [0017-mcp-client-tools.md](0017-mcp-client-tools.md) | pendiente | | 18 | Shared knowledge | [0018-shared-knowledge.md](0018-shared-knowledge.md) | pendiente | | 19 | Prompt injection hardening | [0019-prompt-injection-hardening.md](completed/0019-prompt-injection-hardening.md) | completado | -| 20 | Aislar claude -p del repo | [0020-claude-code-sandbox.md](0020-claude-code-sandbox.md) | pendiente | +| 20 | Aislar claude -p del repo | [0020-claude-code-sandbox.md](completed/0020-claude-code-sandbox.md) | completado | diff --git a/dev/issues/0020-claude-code-sandbox.md b/dev/issues/completed/0020-claude-code-sandbox.md similarity index 100% rename from dev/issues/0020-claude-code-sandbox.md rename to dev/issues/completed/0020-claude-code-sandbox.md diff --git a/docs/security.md b/docs/security.md index 13b2873..ae3d26f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -115,6 +115,28 @@ Prioridad: config `base_path` > `$AGENTS_DATA_DIR/` > `agents//data/` (d Esto previene que tools como `read_file` accedan accidentalmente a codigo fuente, `.env`, o configs del proyecto. +## 6. Aislamiento de claude -p (provider claude-code) + +Cuando un agente usa el provider `claude-code`, el subproceso `claude -p` se ejecuta en un directorio de trabajo aislado, no en la raiz del repositorio. + +**Configuracion:** + +```yaml +llm: + primary: + claude_code: + working_dir: "/tmp/claude-agents/mi-bot" # directorio aislado +``` + +**Comportamiento:** +- Si `working_dir` esta configurado: se crea el directorio automaticamente con `MkdirAll` y se usa como CWD del subproceso +- Si `working_dir` esta vacio: se crea un directorio temporal (`os.MkdirTemp`) y se loguea un WARN para que el operador lo note +- **Nunca** se hereda el CWD del launcher (raiz del repo) + +Esto evita que el subproceso `claude -p` tenga acceso de lectura/escritura al codigo fuente del proyecto, incluso con `permission_mode: bypassPermissions`. + +Implementado en `shell/llm/claudecode.go` → `resolveWorkDir()`. + ## Activacion Para activar todas las protecciones, añadir al `config.yaml` del agente: @@ -133,3 +155,4 @@ Y asegurarse de que: - Las tools tienen allowlists configuradas (no vacias si se quieren usar) - El system prompt incluye la seccion de seguridad - `storage.base_path` apunta fuera del proyecto en produccion +- `claude_code.working_dir` apunta fuera del repo si se usa el provider claude-code diff --git a/shell/llm/claudecode.go b/shell/llm/claudecode.go index f410f5c..50d8217 100644 --- a/shell/llm/claudecode.go +++ b/shell/llm/claudecode.go @@ -58,6 +58,9 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes timeout = defaultClaudeTimeout } + // Resolve working directory once at init time. + workDir := resolveWorkDir(cfg.WorkingDir, log) + return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -70,11 +73,12 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes "binary", binary, "args", strings.Join(args, " "), "prompt_len", len(prompt), + "working_dir", workDir, ) cmd := exec.CommandContext(ctx, binary, args...) - if cfg.WorkingDir != "" { - cmd.Dir = cfg.WorkingDir + if workDir != "" { + cmd.Dir = workDir } // Build clean env: inherit parent but remove ANTHROPIC_API_KEY // so claude uses its own OAuth auth instead of a potentially invalid key. @@ -120,6 +124,29 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes } } +// resolveWorkDir determines the working directory for the claude subprocess. +// If configured is empty, it creates a temporary directory to avoid inheriting the launcher's CWD. +// If configured is non-empty, it ensures the directory exists. +func resolveWorkDir(configured string, log *slog.Logger) string { + if configured == "" { + tmp, err := os.MkdirTemp("", "claude-agent-*") + if err != nil { + log.Error("claude-code: failed to create temp working dir", "err", err) + return "" // Fall through — cmd.Dir will remain empty (inherits CWD). + } + log.Warn("claude-code working_dir is empty, using temporary directory", + "dir", tmp, + ) + return tmp + } + + // Ensure configured directory exists. + if err := os.MkdirAll(configured, 0o755); err != nil { + log.Error("claude-code: failed to create working dir", "dir", configured, "err", err) + } + return configured +} + // buildClaudeArgs constructs the CLI arguments for claude -p. func buildClaudeArgs(cfg config.ClaudeCodeCfg, req coretypes.CompletionRequest) []string { args := []string{"--print", "--output-format", "json"} diff --git a/shell/llm/claudecode_test.go b/shell/llm/claudecode_test.go index a8d414d..07c87d0 100644 --- a/shell/llm/claudecode_test.go +++ b/shell/llm/claudecode_test.go @@ -5,6 +5,9 @@ import ( "errors" "io" "log/slog" + "os" + "path/filepath" + "strings" "testing" "time" @@ -318,6 +321,56 @@ func TestFilterEnv_PrefixSafety(t *testing.T) { } } +// ── resolveWorkDir ────────────────────────────────────────────────────── + +func TestResolveWorkDir_EmptyCreatesTempDir(t *testing.T) { + dir := resolveWorkDir("", discardLog) + if dir == "" { + t.Fatal("expected a temp directory, got empty string") + } + defer os.RemoveAll(dir) + + if !strings.Contains(dir, "claude-agent-") { + t.Errorf("temp dir %q should contain 'claude-agent-' prefix", dir) + } + + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("temp dir should exist: %v", err) + } + if !info.IsDir() { + t.Error("temp dir should be a directory") + } +} + +func TestResolveWorkDir_ConfiguredValueUsed(t *testing.T) { + want := filepath.Join(t.TempDir(), "custom-workdir") + + got := resolveWorkDir(want, discardLog) + + if got != want { + t.Errorf("got %q, want %q", got, want) + } + + info, err := os.Stat(got) + if err != nil { + t.Fatalf("configured dir should be created: %v", err) + } + if !info.IsDir() { + t.Error("configured dir should be a directory") + } +} + +func TestResolveWorkDir_ConfiguredAlreadyExists(t *testing.T) { + want := t.TempDir() // already exists + + got := resolveWorkDir(want, discardLog) + + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + // ── helpers ────────────────────────────────────────────────────────────── func contains(s, substr string) bool {