Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions docs/runbooks/tool-approval-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ channel.

## Overview

Tool invocations pass through three layers:

1. **Hard deny** — commands that are always blocked (e.g., `netclaw daemon stop`,
`rm -rf /`). Never approvable. Checked first.
2. **Tool access** — per-audience allowlists (`AllowedTools`,
Tool invocations pass through four layers:

1. **Operation hard deny** — shell commands that are always blocked
(e.g., `netclaw daemon stop`, `rm -rf /`). Never approvable. Checked first.
2. **Resource hard deny** — protected files and directories (secrets, keys,
lifecycle/control-plane files) that are blocked for file tools and shell
path references. Never approvable.
3. **Tool access** — per-audience allowlists (`AllowedTools`,
`AllowedMcpServers`). Binary: the tool is available or it isn't.
3. **Approval gate** — for tools that pass layers 1 and 2, does this specific
4. **Approval gate** — for tools that pass layers 1-3, does this specific
invocation need user sign-off?

The approval gate is transparent to the LLM — it never knows approval is
Expand Down Expand Up @@ -132,8 +135,14 @@ For **compound commands** (`&&`, `||`, `;`, `|`), each segment is checked
independently. If any segment is unapproved, all unapproved patterns are
batched into one prompt.

For **non-shell tools** (MCP tools, `file_write`, etc.), approval is at the
tool-name level — either the tool is approved or it isn't.
For most **non-shell tools** (MCP tools, `file_read`, etc.), approval is at the
tool-name level.

For `file_write` and `file_edit`, approval is path-aware for Netclaw
control-plane targets. Writes under the control-plane root use mode keys like
`file_write:control-plane` / `file_edit:control-plane` and persist approvals as
path-scoped patterns (for example,
`file_write:control-plane:netclaw.json`).

### Persistent approvals

Expand Down Expand Up @@ -165,6 +174,11 @@ mode:

The hard deny check runs even in `Auto` mode (no approval configured).

In addition to command hard deny, Netclaw enforces path hard deny for protected
resources (for example `secrets.json`, key material, webhook secrets, and
control-plane lifecycle files). Those accesses are blocked for file tools and
for shell commands that reference those paths.

### Custom hard deny patterns

Add patterns via `HardDenyPatterns` in `netclaw.json`:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-12
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
## Context

`tool-approval-gates` added approval interception across `ToolAccessPolicy`,
`DispatchingToolExecutor`, and session approval retry handling. Follow-on
testing exposed composition edge cases:

- File mutation gating now uses argument-aware matcher keys for control-plane
paths, but key resolution needs explicit precedence when both path-specific
and base tool overrides exist.
- Approve-once currently depends on one-time context state and matcher patterns;
retry matching must align with the filtered unapproved set returned by
`IToolApprovalService`, not the pre-filter candidate set.
- Shell deny checks are split between operation-level command hard deny and
resource-level path denial; precedence and user-visible deny semantics need a
single contract.

The implementation spans `Netclaw.Actors.Tools`, `Netclaw.Security`, and
session retry flow state. Actor boundary remains unchanged: session actor owns
approval decisions and temporary one-time retry grants.

## Goals / Non-Goals

**Goals:**

- Make approval mode resolution deterministic when matcher-derived keys and base
tool keys both exist.
- Ensure approve-once retry acceptance checks use the same filtered unapproved
pattern set shown in the interaction prompt.
- Define shell hard-deny composition semantics between operation hard-deny and
resource hard-deny, including precedence for deny reasons.
- Add regression scenarios to the capability spec so future refactors preserve
these compositions.

**Non-Goals:**

- Redesign of approval UI/options or interaction protocol.
- New persistence model for approvals.
- Changes to trust audiences, grant categories, or Slack channel UX.

## Decisions

### Decision 1: Approval mode key precedence uses most-specific to least-specific

**Choice:** Resolve approval mode in this order:

1. Matcher-derived key override (for example `file_write:control-plane`)
2. Base tool key override (`file_write`)
3. Matcher fail-closed behavior for Personal audience
4. Audience `DefaultMode`

**Alternatives considered:**

- Matcher key only with no base fallback: rejected because adding path-specific
matchers unintentionally bypasses existing tool-level policy intent.
- Base key first: rejected because it prevents finer-grained overrides from
taking effect.

**Rationale:** This preserves backward compatibility for existing overrides while
letting operators tighten high-risk subsets without broadening unrelated calls.

### Decision 2: Approve-once matching is evaluated after unapproved filtering

**Choice:** For approval-gated calls, first compute unapproved patterns via
`IToolApprovalService`; then evaluate one-time retry bypass against that filtered
set.

**Alternatives considered:**

- Check one-time bypass against pre-filter matcher patterns: rejected because it
can reprompt even when the user just approved the exact prompt set.
- Persist approve-once to shared approval service: rejected because it breaks
one-shot scope guarantees.

**Rationale:** Prompt set and retry set must be identical to avoid UX/security
drift. One-time state remains in-memory and call-retry scoped.

### Decision 3: Shell deny composition is fail-closed with operation precedence

**Choice:** Shell invocation remains denied if either operation hard-deny
(`ShellCommandPolicy`) or resource hard-deny (`ToolPathPolicy`) matches.
Operation hard-deny is evaluated first; if it matches, resource checks are not
consulted for the result reason.

**Alternatives considered:**

- Resource deny first: rejected because known self-destructive operations should
short-circuit early and return stable hard-deny categorization.
- Merge both policies into one matcher: rejected for now to keep policy modules
independently testable.

**Rationale:** This keeps a strict deny floor while preserving diagnosable,
deterministic denial reasons.

## Risks / Trade-offs

- **[Risk] Key fallback could broaden approval-gated scope unexpectedly** ->
Mitigation: explicit spec scenarios for matcher-key override and base-key
fallback; add policy tests for both branches.
- **[Risk] Retry-path reordering may miss existing one-time checks** ->
Mitigation: add executor and pipeline tests that assert no reprompt on the
immediate retry but prompt on later calls.
- **[Risk] Deny precedence can hide secondary violations** -> Mitigation:
preserve first-deny reason in user result and audit log while keeping
independent tests for both operation and resource deny paths.

## Migration Plan

1. Update approval-mode resolution logic and unit tests.
2. Update executor retry matching order to use filtered unapproved patterns.
3. Update shell policy composition checks and denial reason assertions.
4. Update OpenSpec delta scenarios and run targeted test suites.

Rollback is straightforward: revert this change set to restore prior composition
behavior.

## Open Questions

- None for this scope; behavior is constrained to composition clarifications and
regression coverage.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Why

Tool approval behavior in the shipped pipeline has three composition gaps that can
produce surprising security outcomes: control-plane file mutations can miss
intended approval overrides, approve-once retries can reprompt because matching
uses pre-filter patterns, and shell policy layering between operation-level and
resource-level hard denies is under-specified. This follow-up closes those gaps
to keep approval gates deterministic and auditable under PRD-002 security
constraints.

## What Changes

- Define deterministic approval-mode key precedence for matcher-derived keys
(for example `file_write:control-plane`) versus base tool keys (`file_write`)
and default mode fallback.
- Align approve-once retry matching with the same filtered unapproved pattern
set that was shown to the user in the prompt, including path-aware matcher
patterns for control-plane file mutations.
- Clarify shell policy composition so operation hard-deny and resource hard-deny
are both enforced with explicit precedence and denial reasons.
- Add targeted behavior scenarios in the capability spec for key precedence,
approve-once retry behavior, and shell deny composition.
- In scope: tool approval composition and requirement/test updates in the
existing approval capability.
- Out of scope: new approval UX options, non-tool interaction types, sandbox
shell implementation, and broad ACL model redesign.

## Capabilities

### New Capabilities

- None.

### Modified Capabilities

- `tool-approval-gates`: Refine approval key resolution precedence,
approve-once retry matching semantics, and shell hard-deny composition
semantics; extend normative scenarios for these behaviors.

## Impact

- **Security / policy surface**: `ToolAccessPolicy`, `DispatchingToolExecutor`,
matcher implementations, and shell deny checks gain explicit composition
rules (PRD-002: SEC-003, SEC-006, SEC-009).
- **Behavioral consistency**: Approval prompts and immediate retries use the
same pattern identity set, reducing false reprompts.
- **Operational clarity**: Deny reason precedence is documented for actor logs,
tool audit entries, and troubleshooting.
- **Validation impact**: Update capability scenarios and matching tests in
approval gate and executor suites; no config schema changes expected.
Loading
Loading