Runs: check file writes by real path and log every refused tool
Write(clients/**) rules alone let a competitor scan's report write be denied. Writes inside clients/, team/ and reports/ are now allowed by resolved path; anything refused is logged with the tool and path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2e0b28f2a2
commit
c8ed9870d0
@ -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/<slug>/ 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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user