diff --git a/dashboard/server/jobs.js b/dashboard/server/jobs.js index 8a43965..c88ec36 100644 --- a/dashboard/server/jobs.js +++ b/dashboard/server/jobs.js @@ -55,6 +55,24 @@ const BLOCKED_TOOLS = [ "approve_scheduled", "cancel_scheduled", "schedule_from_library", "competitor_add", "comment_inbox", "direct_messages", "list_scheduled", ].map((t) => `mcp__social-manager__${t}`); +// Path-scoped rules like Write(clients/**) can miss (absolute paths, other spellings), so file writes are also +// checked here by real path. Everything else not on the allow-list is refused, with the reason in the job log. +const WRITE_DIRS = ["clients", "team", "reports"].map((d) => path.resolve(PROJECT_ROOT, d)); +const FILE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]); +function permissionCheck(job) { + return async (tool, input) => { + const file = input.file_path || input.notebook_path; + if (FILE_TOOLS.has(tool) && file) { + const full = path.resolve(PROJECT_ROOT, file); + if (WRITE_DIRS.some((d) => full === d || full.startsWith(d + path.sep))) return { behavior: "allow", updatedInput: input }; + log(job, "warn", `Refused ${tool} outside clients/, team/ and reports/: ${file}`); + return { behavior: "deny", message: `Only files inside clients/, team/ and reports/ can be written. Write to clients// instead.` }; + } + log(job, "warn", `Refused tool not allowed in runs: ${tool}`); + return { behavior: "deny", message: `${tool} isn't available in dashboard runs.` }; + }; +} + // Our own model servers (Ollama on a laptop) break on very long requests, so those runs send only what the job needs: // the built-in tools runs use, the job's own skill and the social-manager server (≈13k tokens instead of ≈25k). const LEAN_TOOLS = ["Read", "Glob", "Grep", "Edit", "Write", "Bash", "Skill", "TodoWrite"]; @@ -189,8 +207,9 @@ async function run(job) { model: AI.model, maxTurns: AI.maxTurns, env: runEnv(), - permissionMode: "dontAsk", + permissionMode: "default", allowedTools: READ_TOOLS, + canUseTool: permissionCheck(job), disallowedTools: BLOCKED_TOOLS, abortController: job.abort, systemPrompt: { type: "preset", preset: "claude_code", append: HEADLESS_RULES }, @@ -205,6 +224,7 @@ async function run(job) { } else if (msg.type === "result") { job.costUsd = msg.total_cost_usd ?? null; job.turns = msg.num_turns ?? null; + for (const d of msg.permission_denials || []) log(job, "warn", `Denied: ${d.tool_name} ${JSON.stringify(d.tool_input).slice(0, 200)}`); if (msg.subtype === "success" && !msg.is_error) { job.result = msg.result; job.status = "succeeded";