feat: add benchmark tool-isolation guard (task 0023) #24
Reference in New Issue
Block a user
Delete Branch "task-0023-bench-tool-isolation-guard"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Task:
.claude/tasks/0023-bench-tool-isolation-guard.mdSummary
Adds the guard that confines each benchmark arm's agent to exactly one tool, so a result measures the tool rather than the agent's choice between tools (ADR 0016).
guardCommand(arm, command)— the authoritative guard. It inspects every binary a proposed shell command would reach (not just the leading token) via a hand-rolled shell parser handling pipelines,;/&&/||sequences, subshells,$(...)and backtick substitutions, process substitutions, redirections (incl.2>&1,&>), and leadingNAME=valueassignments. It permits only the active arm's one allow-listed binary plus a curated set of harmless read-only utilities; foreign binaries, absolute-path evasions (even of the arm's own binary), and interpreter-based fetch tricks are denied. Thegitea-mcparm runs with the shell disabled entirely.provisionArmBin(arm, binDir, locate?)— produces a curated per-arm bin directory exposing only that arm's binary (empty forgitea-mcp), as the convenience layer behind the authoritative guard.locateis injectable so tests stay host-independent.ARM_BINARY/HARMLESS_BINARIES— the per-arm allow-listed binary and the curated harmless-utility set.31 colocated tests in
bench/guard.test.ts, run vianpm run test:bench(kept out of thesrc/coverage tier).Deviations from the plan
Depth beyond the literal acceptance criteria, invited by ADR 0016 ("isolation strength rests on the completeness of the guard's deny rules"): the full command parser closes pipe-hiding and substitution-hiding evasions in addition to the named absolute-path and interpreter-fetch cases, and path-qualified invocations are refused even for the arm's own binary. No criteria were dropped.
Review
Risk
Overall: Medium
bench/plus one README line; only in-directory imports, no external callers.guard.test.tsthoroughly exercises both exports (own/foreign/abs-path/pipe/substitution/redirection/interpreter/mcp-off, provisioning success/empty/throw).extractCommandsis an intricate hand-rolled shell tokenizer.bench/is dev-only, excluded from the published package.Standards findings (unaddressed)
stringrather than a dedicated type. Left as-is: this matches the sibling files' de-facto convention (result.tsalso uses barestringfortaskId).extractCommandssize/complexity (judgement call) — ~185 lines of nested cursor-sharing closures. Left as-is: the complexity is deliberate and justified by the security intent, and extracting the closures wouldn't help since they share one cursor.Spec findings (unaddressed)