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",
|
"approve_scheduled", "cancel_scheduled", "schedule_from_library", "competitor_add", "comment_inbox", "direct_messages", "list_scheduled",
|
||||||
].map((t) => `mcp__social-manager__${t}`);
|
].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:
|
// 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).
|
// 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"];
|
const LEAN_TOOLS = ["Read", "Glob", "Grep", "Edit", "Write", "Bash", "Skill", "TodoWrite"];
|
||||||
@ -189,8 +207,9 @@ async function run(job) {
|
|||||||
model: AI.model,
|
model: AI.model,
|
||||||
maxTurns: AI.maxTurns,
|
maxTurns: AI.maxTurns,
|
||||||
env: runEnv(),
|
env: runEnv(),
|
||||||
permissionMode: "dontAsk",
|
permissionMode: "default",
|
||||||
allowedTools: READ_TOOLS,
|
allowedTools: READ_TOOLS,
|
||||||
|
canUseTool: permissionCheck(job),
|
||||||
disallowedTools: BLOCKED_TOOLS,
|
disallowedTools: BLOCKED_TOOLS,
|
||||||
abortController: job.abort,
|
abortController: job.abort,
|
||||||
systemPrompt: { type: "preset", preset: "claude_code", append: HEADLESS_RULES },
|
systemPrompt: { type: "preset", preset: "claude_code", append: HEADLESS_RULES },
|
||||||
@ -205,6 +224,7 @@ async function run(job) {
|
|||||||
} else if (msg.type === "result") {
|
} else if (msg.type === "result") {
|
||||||
job.costUsd = msg.total_cost_usd ?? null;
|
job.costUsd = msg.total_cost_usd ?? null;
|
||||||
job.turns = msg.num_turns ?? 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) {
|
if (msg.subtype === "success" && !msg.is_error) {
|
||||||
job.result = msg.result;
|
job.result = msg.result;
|
||||||
job.status = "succeeded";
|
job.status = "succeeded";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user