Repository navigation
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
| export const POLICY_PATH = "/sandbox/.saw/tool-actions.yaml"; | ||
| export const IDENTITY_PATH = "/sandbox/.saw/identity.json"; | ||
| export const TASK_PATH = "/sandbox/.saw/current-task.json"; | ||
| export const AUDIT_PATH = "/sandbox/.saw/audit/tool-actions.jsonl"; |
There was a problem hiding this comment.
The agent shares the sandbox filesystem. It can rewrite the policy to defaultAction: allow with zero rules. It can rewrite the identity to spoof the user. It can wipe the audit log. The read path checks no signature and no hash. This bypasses the whole gate.
Store policy and identity outside the agent-writable path. Mount them read-only. Check a hash or signature on each read. Record the policy hash in each audit line.
relevant also for charts/governance-policy/tool-actions.yaml
| # tool-gate code names these tools. defaultAction applies when no rule | ||
| # matches, and every call is still audited with the signed-in user and task. | ||
| version: 1 | ||
| defaultAction: allow |
There was a problem hiding this comment.
Only exec, message, and saw-denied-tool hold rules. All other tools (write, edit, shell, MCP tools) hit the default and run free. Audit does not stop damage.
Change the default to deny or approval. Allowlist safe read-only tools one by one.
| function ruleMatches(rule, tool, command) { | ||
| if (rule.tool !== tool) return false; | ||
| if (!rule.whenCommandMatches) return true; | ||
| return rule.whenCommandMatches.some((needle) => command.includes(needle)); |
There was a problem hiding this comment.
command.includes(needle)
The match is case-sensitive and space-exact. It parses no shell. git push, /bin/rm, rm with a tab, sh -c "git push", and python -c ... all evade it. echo rm foo trips it by accident.
Normalize case and spaces. Parse the shell or block wrappers (sh -c, eval, $(), ${}). Use anchored patterns.
There was a problem hiding this comment.
here too, relevant for tool-actions.yaml
| export function commandText(params) { | ||
| if (!params || typeof params !== "object") return ""; | ||
| const cmd = params.command ?? params.cmd ?? ""; | ||
| if (Array.isArray(cmd)) return cmd.map((part) => String(part)).join(" "); | ||
| if (typeof cmd === "string") return cmd; | ||
| return ""; | ||
| } |
There was a problem hiding this comment.
The reader knows only command and cmd. An exec call with script, args, or another key yields "". Empty text matches no rule. The call then hits defaultAction: allow.
List the full exec schema. Fail closed (deny) when the tool is exec but no command text is found.
| function userOf(identity) { | ||
| return { | ||
| subject: identity && typeof identity.subject === "string" ? identity.subject : "", | ||
| username: identity && typeof identity.username === "string" ? identity.username : "", | ||
| }; | ||
| } |
There was a problem hiding this comment.
The check accepts any object. {} and {"username":""} pass. The call then runs with an empty audit user. The installer also writes "" when config values are missing.
Require a non-empty subject and username. Reject the install when they are missing.
| for extra in tool-actions.yaml tool-gate.mjs tool-gate-plugin.mjs; do | ||
| [[ -f "${dir}/${extra}" ]] && files+=("$extra") | ||
| done |
There was a problem hiding this comment.
Make the three files mandatory in both places. Fail when they are missing. Re-sign the bundle.
Related Ticket:
APPENG-6600
Summary
Implements per-tool authorization, human-in-the-loop approval, and user/task-bound audit logging for OpenClaw and NemoClaw agent sandboxes.
While the control-plane governance interceptor handles coarse resource operations (e.g., sandbox creation), this PR introduces
saw-tool-gate—abefore_tool_callhook inside agent sandboxes—to enforce per-tool policies and approval rules defined intool-actions.yaml.Key Changes
1. Configurable Governance Policy
charts/governance-policy/tool-actions.yamland synchronized it to installer files and thegovernance-policyConfigMap.allow,deny, andapprovalactions:message(action:approval).whenCommandMatchessubstring checks forexeccommands (git push,gh pr merge,rm,kubectl delete,oc delete).saw-denied-tool(action:deny).2. Agent Sandbox Interceptor (
saw-tool-gate)saw-applyinto agent sandboxes (openclaw,nemoclaw) under/sandbox/.openclaw/extensions/saw-tool-gate/.before_tool_callto evaluate policies before any tool executes./approveflow when a tool requires approval, holding execution until resolved (approvedvs.rejected/timeout/cancel).3. Identity, Task Binding & Fail-Closed Strategy
username,subject) read from/sandbox/.saw/identity.json.sessionKeyorsessionId(whichever is present) astask.id.decision: deny) if policy configuration, identity data, or task session context (sessionKeyandsessionIdboth missing) is unavailable.4. Audit Trail Logging
/sandbox/.saw/audit/tool-actions.jsonl.usercontext,taskID, sandbox/workspace names, tool name, matchedruleID, args digest, redacted summary, and decision outcome (allow,deny,approval_required,approved,rejected).Test Coverage
Node Unit Tests (
tool-gate.test.mjs- 19 tests):Identity and task binding (
sessionKey/sessionIdfallback handling stored astask.id).Tool and
execsubstring matching logic (whenCommandMatches).Fail-closed behavior for missing policy, corrupted identity, or missing task context.
Approval path resolution callbacks (
onResolutionloggingapprovedvsrejected).Audit schema verification (
rulefield) and credential redaction.Pytest Suite (5 tests in
test_tool_gate.py, plus 3 chart tests intest_openshell_saw_chart.py):Verifies installer injects gate extensions only into agent sandboxes (ignoring generic
toolboxsandboxes).Evaluates
tool-actions.yamlpolicy parsing via Node intest_tool_gate.py.Verifies installer disk payloads, chart policy copies, and ConfigMap rendering in
test_openshell_saw_chart.py.Manual Verification
Verified on test environment (
notebookandcuda-sandbox):git status→ Executed immediately (allow).rm -rf /tmp/saw-tool-gate-probe→ Intercepted (approval_required).saw-denied-tool→ Blocked immediately (deny).Confirmed
/sandbox/.saw/audit/tool-actions.jsonlreceived correctly formatted entries tagged with usernamealiceand tasktask-readme-check.