# Tool-call Approval Policy

> Applies to: ChengOS v0.2.0+ ｜ Last verified: 2026-09-25 ｜ Source: `crates/cheng-core/src/services/tool_call_approval.rs`, `crates/cheng-core/src/services/trusted_automation.rs`, `crates/cheng-file-ops/src/undo.rs`, `crates/cheng-file-ops/src/protected_paths.rs`, `crates/cheng-api/src/services/preset_switch_approval.rs`

When an agent calls a tool that can change files or run processes, ChengOS decides — before
anything happens — whether the call runs automatically or waits for a person. This page is the
exact policy. The approval mechanism itself (cards, `~~y`, `/approve`, timeouts, the reaper) is
covered in [Approvals & Human Review](/docs/exec-approvals-reviews).

## One decision, three steps

Every agent tool path — the async and sync tool paths and the tool runtime — calls the same
policy function after the node's static configuration and the model's arguments have been
merged. Nothing else computes file-operation approval.

1. **Preflight.** The side where the files live observes the planned call without writing:
   in-process on the server, or over the workspace tool bridge in a client-local CLI or desktop
   client. It reports canonical paths, whether targets exist, sizes, and content hashes.
2. **Decision.** A pure function takes the merged arguments, the preflight facts, and the trusted
   environment (never the model's arguments about itself) and returns *run automatically* or
   *require approval*, with a reason. **Missing facts always mean review.**
3. **Checked execution.** The decision pins the facts it relied on as preconditions. The executor
   re-checks them immediately before any side effect and refuses on drift with
   `stale_precondition`, so neither an automatic call nor an approved one can land on a file that
   changed after it was checked.

## Tiers for file operations

These apply to `tools/file_ops_hub` (and the legacy `tools/file_ops` alias).

| Tier | Operations | Decision |
|---|---|---|
| Read-only | `view_file`, `list_directory`, `file_tree`, `search_files`, `file_info`, `diff_file`, `validate_file` | Never reviewed |
| Rehearsal | Any operation with `dry_run: true` where dry-run is side-effect-free | Never reviewed |
| **Adaptive** | `write_file`, `edit_file`, `patch_file`, `copy_file`, `create_directory`, `backup_file` | Automatic inside the low-impact envelope, otherwise reviewed |
| **Destructive** | `delete_file`, `move_file`, `rollback_file`, and every mutating `batch_file_ops` | Always reviewed |

An operation whose switch (`allow_write`, `allow_delete`, …) is off is refused by the hub itself,
so no card is shown for it.

**The low-impact envelope.** An adaptive call runs automatically only when *all* of these hold:

- a verified workspace root is bound to the run, and the call has one target;
- the target is not protected or flagged by the write-policy guard;
- the call turns off no safety net (`allow_sensitive`, `allow_ignored`, `force_overwrite`);
- it creates rather than overwrites, or it is an edit/patch that keeps its pre-image backup;
- backups go to the default location;
- the content or diff is at most **64 KiB**.

Every automatic call carries an **undo credential** (below).

## Why a call was reviewed

Each decision has a stable reason code, shown on the approval card and written to the audit log:

| Reason | Card says |
|---|---|
| `destructive_operation` | deletes, moves, and rollbacks always need your approval |
| `mutating_batch` | multi-file changes are approved as one change set |
| `protected_target` | the target is a protected path |
| `policy_guarded_target` | the target is a sensitive or ignored path |
| `safety_override` | the call turns off a safety check |
| `overwrite` | it replaces existing content |
| `change_too_large` | the change is larger than the automatic limit |
| `backup_disabled` | the edit turned off its backup |
| `multiple_targets` | it touches many files |
| `custom_backup_root` | backups are redirected to a custom location |
| `workspace_unbound` | no verified workspace root is bound to this run |
| `preflight_unavailable` | the workspace could not describe this change in advance |
| `planning_failed` | the change could not be planned in advance |
| `process_execution` | real process execution (workspace shell or tests) |

Automatic decisions record `low_impact_scoped_change`, `author_opt_out`, or `trusted_automation`.

`preflight_unavailable` usually means an older CLI or desktop client that cannot answer the
preflight request; upgrading the client restores automatic small edits.

## Approval identity and "always allow"

Reviews for destructive operations, batches, protected or guarded targets, safety overrides, and
process execution are **target-bound**: the approval identity names the operation and the exact
paths, ending in a digest of the operation, workspace root, and paths. A remembered
"always allow" therefore covers exactly that operation on exactly those paths, and
"always allow" rules recorded before this policy never cover them. A batch approval is one-shot:
it covers the listed operations and is never reusable.

An approved call is still checked: if the previewed file changed between the card and the
execution, the call fails as stale instead of landing.

## Protected paths

These always need a person, even for a tiny, undoable change. Patterns use gitignore syntax,
relative to the workspace root, and match the canonical path (a symlink or `./` spelling cannot
dodge them):

```text
.git   .env   .env.*   *.pem   *.key   id_rsa*   id_ed25519*   .ssh
/.github/workflows   /.gitlab-ci.yml
```

`CHENG_FILE_OPS_PROTECTED_PATTERNS` (comma-separated, same syntax) **adds** patterns for a
deployment. Nothing a workflow or a model supplies can remove one.

## Undo credentials

Skipping a prompt is only acceptable when the change can be taken back. Every automatic mutation
attaches an `undo_credential` to its tool output describing exactly what it did:

| Operation | Credential | Revert rule |
|---|---|---|
| Edit or patch (any backed-up rewrite) | Pre-image backup + post-write hash | Restore only if the file still has the post-write hash |
| New file (write or copy) | "Absent before" + post-write hash | Delete only if the file still has the post-write hash |
| `create_directory` | The directories actually created | Remove innermost first, each only while empty |
| `backup_file` | The snapshot created | Remove that snapshot |

Every revert refuses when the path has changed since, so an undo can never destroy later work.
In this release the credential is recorded in the tool output, and so in the execution trace; there is no one-click undo
button yet, and a revert is applied by the file-ops core (`cheng_file_ops::undo::revert`).

## Opting out, safely

**`require_approval = false`** on a file-ops node is an author-level switch. It skips review for
adaptive operations and non-destructive batches (reason `author_opt_out`) — but **not** for
destructive ones.

**Trusted automation** is the only way a delete, move, rollback, or destructive batch runs with
nobody watching. It is deployment configuration, never workflow configuration: the operator
writes a JSON file and points `CHENG_TRUSTED_AUTOMATION_POLICY_FILE` at it.

```json
{
  "policies": [
    {
      "id": "nightly-artifact-cleanup",
      "workflow_ids": ["3f1c…"],
      "operations": ["delete_file", "move_file"],
      "roots": ["/srv/workspaces/artifacts", "client:<root key>"]
    }
  ]
}
```

Each entry names specific workflows, operations, and roots. There is deliberately no
"all workflows" or "all operations" form. A server root covers itself and everything below it; a
client-local workspace is named by `client:` plus its root key (a digest, never a path). The node
must also have `require_approval = false`. Every call an entry authorizes is audited with the
entry's `id`.

## Process execution

`tools/workspace_shell` and running tests with `tools/workspace_tests` are **always** reviewed
(reason `process_execution`); the card states where the process will run, so a server run is
never mistaken for your laptop. Read-only workspace tools — git, markdown, preview, test
detection — are never gated.

No workflow field can switch this off. An operator can disable it for a whole deployment with
`CHENG_WORKSPACE_PROCESS_APPROVAL_DISABLED=true`, meant only for trusted automation servers.

## Preset switches

An agent may *request* a different preset for a tool node wired alongside it, but never apply one
itself:

- `tools/list_available_presets` returns only presets marked LLM-visible and valid for a sibling
  tool — one that feeds the same agent.
- `tools/request_preset_switch` builds an approval card with a parameter diff. The preset is
  applied, and the workflow saved, only after the user approves (web card, `~~y`, or CLI
  `/approve`).

A target that is not a sibling tool of the requesting node is refused. An agent can never
re-configure unrelated nodes.

## Audit

Every governed decision writes a structured record to the `cheng::audit::tool_policy` log
target: tool, decision (`auto_execute` or `require_user_approval`), reason, whether the review is
mandatory, the trusted-policy ID, whether preconditions were pinned, and the execution and
workflow IDs. Arguments and file content are never logged.

## Next steps

- [Approvals & Human Review](/docs/exec-approvals-reviews) — the gate, decisions, and the reaper.
- [File Operation Nodes](/docs/nodes-file-ops) — every operation and switch of the file-ops hub.
- [Remote & Cross-Device Connections](/docs/cli-remote-connect) — client-local execution and preflight.
