Skip to content

feat(governance): tie tool actions to user identity and task with configurable approval gate - #77

Open
mtalvi wants to merge 5 commits into
validatedpatterns-sandbox:mainfrom
mtalvi:gate-tool
Open

mtalvi wants to merge 5 commits into
validatedpatterns-sandbox:mainfrom
mtalvi:gate-tool

Conversation

@mtalvi

@mtalvi mtalvi commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

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—a before_tool_call hook inside agent sandboxes—to enforce per-tool policies and approval rules defined in tool-actions.yaml.


Key Changes

1. Configurable Governance Policy
  • Added charts/governance-policy/tool-actions.yaml and synchronized it to installer files and the governance-policy ConfigMap.
  • Configurable rule engine supporting allow, deny, and approval actions:
  • Exact tool match for message (action: approval).
  • whenCommandMatches substring checks for exec commands (git push, gh pr merge, rm , kubectl delete, oc delete).
  • Exact tool match for saw-denied-tool (action: deny).
2. Agent Sandbox Interceptor (saw-tool-gate)
  • Installed via saw-apply into agent sandboxes (openclaw, nemoclaw) under /sandbox/.openclaw/extensions/saw-tool-gate/.
  • Hooks into before_tool_call to evaluate policies before any tool executes.
  • Triggers OpenClaw's /approve flow when a tool requires approval, holding execution until resolved (approved vs. rejected/timeout/cancel).
3. Identity, Task Binding & Fail-Closed Strategy
  • Records user identity (username, subject) read from /sandbox/.saw/identity.json.
  • Binds the task context using sessionKey or sessionId (whichever is present) as task.id.
  • Fail-Closed Design: Blocks execution (decision: deny) if policy configuration, identity data, or task session context (sessionKey and sessionId both missing) is unavailable.
4. Audit Trail Logging
  • Records every decision to /sandbox/.saw/audit/tool-actions.jsonl.
  • Captures: timestamp, user context, task ID, sandbox/workspace names, tool name, matched rule ID, args digest, redacted summary, and decision outcome (allow, deny, approval_required, approved, rejected).
  • Redacts raw credentials and sensitive tokens from audit logs.

Test Coverage

  • Node Unit Tests (tool-gate.test.mjs - 19 tests):

  • Identity and task binding (sessionKey / sessionId fallback handling stored as task.id).

  • Tool and exec substring matching logic (whenCommandMatches).

  • Fail-closed behavior for missing policy, corrupted identity, or missing task context.

  • Approval path resolution callbacks (onResolution logging approved vs rejected).

  • Audit schema verification (rule field) and credential redaction.

  • Pytest Suite (5 tests in test_tool_gate.py, plus 3 chart tests in test_openshell_saw_chart.py):

  • Verifies installer injects gate extensions only into agent sandboxes (ignoring generic toolbox sandboxes).

  • Evaluates tool-actions.yaml policy parsing via Node in test_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 (notebook and cuda-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.jsonl received correctly formatted entries tagged with username alice and task task-readme-check.

Comment on lines +8 to +11
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";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too, relevant for tool-actions.yaml

Comment on lines +135 to +141
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 "";
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +154 to +159
function userOf(identity) {
return {
subject: identity && typeof identity.subject === "string" ? identity.subject : "",
username: identity && typeof identity.username === "string" ? identity.username : "",
};
}

@Hadar301 Hadar301 Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +144 to +146
for extra in tool-actions.yaml tool-gate.mjs tool-gate-plugin.mjs; do
[[ -f "${dir}/${extra}" ]] && files+=("$extra")
done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the three files mandatory in both places. Fail when they are missing. Re-sign the bundle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants