merge: issue/0020-claude-code-sandbox — aislamiento de claude -p del repositorio
Default seguro con tmpdir cuando working_dir esta vacio, configuracion explicita para ambos agentes, tests unitarios y documentacion completa.
This commit is contained in:
+2
-1
@@ -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`
|
- **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`)
|
- **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
|
- **`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`
|
Documentacion completa: `docs/security.md`
|
||||||
|
|
||||||
## Preferencias
|
## Preferencias
|
||||||
|
|||||||
@@ -84,6 +84,18 @@ llm:
|
|||||||
api_key_env: ANTHROPIC_API_KEY # o OPENAI_API_KEY (default)
|
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/<agent-id>" # 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):
|
**Tool use** (si el agente necesita herramientas):
|
||||||
```yaml
|
```yaml
|
||||||
llm:
|
llm:
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ llm:
|
|||||||
disable_tools: true # no ejecuta herramientas internas de claude
|
disable_tools: true # no ejecuta herramientas internas de claude
|
||||||
allowed_tools: []
|
allowed_tools: []
|
||||||
disallowed_tools: []
|
disallowed_tools: []
|
||||||
working_dir: ""
|
working_dir: "/tmp/claude-agents/asistente-2"
|
||||||
permission_mode: "bypassPermissions"
|
permission_mode: "bypassPermissions"
|
||||||
model: "sonnet"
|
model: "sonnet"
|
||||||
fallback_model: ""
|
fallback_model: ""
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ llm:
|
|||||||
disable_tools: true # no ejecuta herramientas internas de claude
|
disable_tools: true # no ejecuta herramientas internas de claude
|
||||||
allowed_tools: []
|
allowed_tools: []
|
||||||
disallowed_tools: []
|
disallowed_tools: []
|
||||||
working_dir: ""
|
working_dir: "/tmp/claude-agents/assistant-bot"
|
||||||
permission_mode: "bypassPermissions"
|
permission_mode: "bypassPermissions"
|
||||||
model: "sonnet" # modelo interno de claude -p
|
model: "sonnet" # modelo interno de claude -p
|
||||||
fallback_model: ""
|
fallback_model: ""
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ afectados y notas de implementacion.
|
|||||||
| 17 | MCP client tools | [0017-mcp-client-tools.md](0017-mcp-client-tools.md) | pendiente |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|||||||
@@ -115,6 +115,28 @@ Prioridad: config `base_path` > `$AGENTS_DATA_DIR/<id>` > `agents/<id>/data/` (d
|
|||||||
|
|
||||||
Esto previene que tools como `read_file` accedan accidentalmente a codigo fuente, `.env`, o configs del proyecto.
|
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
|
## Activacion
|
||||||
|
|
||||||
Para activar todas las protecciones, añadir al `config.yaml` del agente:
|
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)
|
- Las tools tienen allowlists configuradas (no vacias si se quieren usar)
|
||||||
- El system prompt incluye la seccion de seguridad
|
- El system prompt incluye la seccion de seguridad
|
||||||
- `storage.base_path` apunta fuera del proyecto en produccion
|
- `storage.base_path` apunta fuera del proyecto en produccion
|
||||||
|
- `claude_code.working_dir` apunta fuera del repo si se usa el provider claude-code
|
||||||
|
|||||||
+29
-2
@@ -58,6 +58,9 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes
|
|||||||
timeout = defaultClaudeTimeout
|
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) {
|
return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -70,11 +73,12 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes
|
|||||||
"binary", binary,
|
"binary", binary,
|
||||||
"args", strings.Join(args, " "),
|
"args", strings.Join(args, " "),
|
||||||
"prompt_len", len(prompt),
|
"prompt_len", len(prompt),
|
||||||
|
"working_dir", workDir,
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, binary, args...)
|
cmd := exec.CommandContext(ctx, binary, args...)
|
||||||
if cfg.WorkingDir != "" {
|
if workDir != "" {
|
||||||
cmd.Dir = cfg.WorkingDir
|
cmd.Dir = workDir
|
||||||
}
|
}
|
||||||
// Build clean env: inherit parent but remove ANTHROPIC_API_KEY
|
// Build clean env: inherit parent but remove ANTHROPIC_API_KEY
|
||||||
// so claude uses its own OAuth auth instead of a potentially invalid 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.
|
// buildClaudeArgs constructs the CLI arguments for claude -p.
|
||||||
func buildClaudeArgs(cfg config.ClaudeCodeCfg, req coretypes.CompletionRequest) []string {
|
func buildClaudeArgs(cfg config.ClaudeCodeCfg, req coretypes.CompletionRequest) []string {
|
||||||
args := []string{"--print", "--output-format", "json"}
|
args := []string{"--print", "--output-format", "json"}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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 ──────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
func contains(s, substr string) bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user