skip to content
$worker

approval-gate

v1.0.8

Policy and decision surface for human-held function calls — pre_trigger gate, pending inbox, per-session permission settings, and two notification trigger types.

iiiverified
73 installs45 in 7d2 today
install
$iii worker add approval-gate@1.0.8
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

agent-ready brief for v1.0.8
install + config + dependencies + readme + api reference, all in one place. fetch as agent-context.md for an llm to consume.
the same content rendered as discrete blocks below is exposed as a single markdown document at /workers/approval-gate.md?version=1.0.8. paste it into an llm prompt or pipe it through curl from a worker.

install

install
$iii worker add approval-gate@1.0.8

dependencies

readme

README.md

approval-gate

The policy and decision surface for human-held function calls (spec). Four surfaces, one worker:

  1. The gateapproval::gate, a pre_trigger hook the worker binds itself at startup on the harness's harness::hook::pre-trigger trigger type. It evaluates per-session mode, allow-lists, and inline config rules, and answers continue, deny, or hold.
  2. The grant watchapproval::grant-watch, a post_trigger hook bound on shell::* / coder::*. It watches for a jail-scope grant_hint on a dispatch failure and converts it into a folder_access hold instead of letting the raw rejection reach the model. See Folder-access grant watch below.
  3. The decision planeapproval::resolve plus the per-session settings RPCs (set_mode, add_always_allow, approve_always, …). Human/console only.
  4. The pending inbox — an ephemeral index of held calls (approval::list-pending / approval::get-pending) plus two trigger types (approval::pending-created / approval::pending-resolved) that notification workers and UIs bind to. PendingApprovalRecord.kind ("function" | "folder_access") tells a console which prompt to render.

The worker keeps no resolved-approval history: a record exists only while a call is held; every record has an explicit deletion path (resolve, turn abort, session delete). The transcript's function_result and the pending_resolved event are the audit trail. Holds do not expire — they wait until a human resolves or the turn/session is purged.

Standalone caveat

This worker codes against the greenfield harness contracts (harness::hook::pre-trigger, harness::hook::post-trigger, harness::function::resolve, harness::turn-completed, harness::workspace::grant/grants — see harness.md § Hooks / § API Reference), which are not implemented by the current harness yet. The harness/session trigger bindings are best-effort: on an engine without those trigger types the worker still boots, serves its RPCs, and logs trigger_type_not_found for the absent bindings (restart it after the sibling appears to re-bind). Lacking harness::workspace::grant/grants specifically degrades approval::grant-watch alone (see Folder-access grant watch) — it never holds, the rest of the worker is unaffected. The configuration worker is the one hard dependency — it is enabled by default in the engine, and a failed register/fetch aborts boot. The integration suite exercises the harness surface against in-process fakes until harness 1.0 lands.

Install

iii worker add approval-gate

Quickstart

cargo build
./target/debug/approval-gate --url ws://127.0.0.1:49134

The authoritative config lives in the engine's configuration worker (no committed config.yaml); pass --config .yaml only to seed the entry on its very first registration.

Hold → decide → release, from any client:

# A held call shows up in the inbox…
iii call approval::list-pending '{}'
# …a human allows it (the harness re-runs it through trigger)…
iii call approval::resolve '{"session_id": "s_1", "function_call_id": "c_1", "decision": "allow"}'
# …or denies it with a reason the model can adapt to.
iii call approval::resolve '{"session_id": "s_1", "function_call_id": "c_1", "decision": "deny", "reason": "not on prod"}'

Permission model

Per-session mode plus two allow-lists, evaluated in this order (ported unchanged from the proven implementation):

  1. approval::* / configuration::* target → deny (human_only_function, even under full — self-escalation defense)
  2. mode full → allow
  3. approved_always hit → allow (every mode — remembered human decisions)
  4. mode auto and always_allow hit → allow (dormant under manual)
  5. fall through to configuration rules (first match wins): allow → allow · deny → deny · no match → hold

When the configuration entry omits rules, the gate denies only this worker's own approval::* surface; every other call holds. On startup the worker seeds/backfills the stored entry so rules are editable in the console.

Custom trigger types

Type Fires Payload
approval::pending-created a call was held and its inbox record written (async, off the hot path) PendingApprovalRecord & { status: "pending" } — redacted args, session context: self-sufficient for notification copy
approval::pending-resolved a pending call left the inbox (exactly once per record) ids + outcome: "allow" | "deny" | "aborted", operator reason on deny

Binding config (both types): { session_id?, metadata? }metadata is a subset-equality match against the record's denormalized session_metadata, so a multi-tenant notification worker binds to only its own sessions. After a restart, reconcile with one approval::list-pending call.

Folder-access grant watch

When a shell::* / coder::* call fails with a jail-scope error (S215, S220, C215, C218) whose message carries a grant_hint= tail, approval::grant-watch (a post_trigger hook, bound on_error: "fail_open" so a crashed/absent watch never turns an already-decided result into a stuck call) converts the failure into a kind: "folder_access" pending record instead of letting the raw denial reach the model. The record's grant_request: { dir, offending_path, error_code } is what the console shows: "allow access to dir?".

Before holding, the watch stands down (delivers the original error, no prompt) whenever:

  • the offending dir is the session's own workspace (metadata.working_dir) or under it — that is a harness stamp hole, not something a grant fixes;
  • the harness already reports a durable grant covering the dir (harness::workspace::grants) — shell rejecting anyway is a store/stamp skew, not something a re-ask resolves;
  • the user already denied this dir earlier in the session (approval_grant_denied/);
  • the call has already been re-asked grant_reask_limit times (default 3, configurable — see below);
  • the harness doesn't implement the workspace-grant control plane at all (harness::workspace::grants fails) — the watch stands down entirely rather than looping.

Resolving a folder_access record's allow decision takes an additional grant_scope (once | session | always, default once):

Scope Effect
once The release carries extra_roots: [dir] — trusted for just this one call.
session harness::workspace::grant first (durable for the rest of the session), THEN release with no extra_roots — the harness stamps the grant on every subsequent call itself. Falls back to once-style extra_roots if the grant call fails, so the user's click still works.
always Same as session, PLUS a best-effort append of dir to the shell deployment configuration's fs.host_roots (configuration::get/configuration::set {id: "shell"}). A persist failure is logged and never blocks the release.

deny records the dir in approval_grant_denied/ (so the same session won't be re-asked about it again) before delivering a denial whose default reason mentions folder access specifically ("user denied folder access to

" unless the operator supplies their own).

Degraded modes — the watch is entirely additive and fails toward "do nothing, deliver the original error":

  • No shell grant_hint support (older shell/coder): the ladder never finds a hint, so grant-watch never triggers — behaves as if unbound.
  • No harness workspace-grant control plane (older harness): the "already granted?" check fails, so every candidate stands down — no holds, no prompts, just the original error.
  • Harness/console down when the hook fires: on_error: "fail_open" delivers the original error unchanged; nothing is stuck.

Configuration

The whole config — runtime wiring and deployment approval defaults — lives in the single engine configuration entry approval-gate (operator-edited via the console's Configuration screen; reactive reload, no polling). There is no committed config.yaml. On first boot the worker seeds the entry with the built-in defaults (including rules) so the editor is pre-filled; existing stored values are never overwritten except to add a missing rules field.

{
  "default_mode": "manual",        // manual | auto | full — sessions with no stored settings
  "rules": [                       // first match wins; no match → hold
    "!approval::*",
    { "function": "state::get", "action": "allow", "modes": ["auto"] }
  ],
  "grant_reask_limit": 3           // approval::grant-watch re-asks per held call before standing down
}

configuration is a required boot dependency: the worker registers the schema and fetches the authoritative value at startup, and a failed register/fetch aborts boot (the gate must run on a known, authoritative policy surface, never a guessed one). When no value is stored yet the built-in defaults above are seeded and used. Every field hot-reloads on configuration::set — nothing requires a restart.

Agent exposure

Deny all approval::* and configuration::* functions to in-run agents: resolve would let an agent approve its own held calls, and the settings RPCs are self-escalation (the gate's human_only_function rule is the in-depth backstop). list_pending / get_pending are read-only and redacted, but they enumerate held calls across sessions — keep them off agent allow-lists too.

Local development & testing

cargo test                                   # lib suites: pure unit + engine-backed handlers
cargo test --test integration               # engine-backed; self-skips without `iii`
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
./target/debug/approval-gate --manifest      # registry-publish manifest

The integration suite spawns a real engine (III_ENGINE_BIN or iii on PATH) with configuration + iii-state, registers the production surface in-process, and fakes sibling RPCs where noted (session::get). With the harness binary available, tests/harness_integration.rs additionally boots the real harness worker for cross-worker hold / sweep / resolve checks.

Architecture documentation

Deep documentation lives in architecture/: internals.md for maintainers (evaluation order, the emit-gate deletion mechanics, lazy seeding, engine facts the code depends on) and integration.md for consumers (the full function/trigger contract, the harness handoff, deployment notes, what not to do).

api reference (json)

agent-api-reference.json
{
  "functions": [
    {
      "description": "Add a function to the session's auto-mode trust list (idempotent). Human/console-only.",
      "metadata": {},
      "name": "approval::add-always-allow",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "function_id": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "function_id",
          "session_id"
        ],
        "title": "AlwaysAllowMutationRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AlwaysAllowEntry": {
            "properties": {
              "function_id": {
                "type": "string"
              },
              "granted_at": {
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              },
              "granted_by": {
                "$ref": "#/definitions/GrantedBy"
              }
            },
            "required": [
              "function_id",
              "granted_at",
              "granted_by"
            ],
            "type": "object"
          },
          "ApprovalSettings": {
            "description": "The stored per-session record (scope `approval_settings`). Reads compute the effective settings from configuration defaults when no record exists — see settings.rs.",
            "properties": {
              "always_allow": {
                "default": [],
                "description": "Consulted only in auto mode.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "approved_always": {
                "default": [],
                "description": "Consulted in every mode — remembered human decisions.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "mode": {
                "allOf": [
                  {
                    "$ref": "#/definitions/PermissionMode"
                  }
                ],
                "default": "manual"
              },
              "mode_set_at": {
                "default": 0,
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "GrantedBy": {
            "oneOf": [
              {
                "enum": [
                  "user_click"
                ],
                "type": "string"
              },
              {
                "description": "Copied from deployment auto-scoped allow rules on first mutation.",
                "enum": [
                  "seed"
                ],
                "type": "string"
              }
            ]
          },
          "PermissionMode": {
            "enum": [
              "manual",
              "auto",
              "full"
            ],
            "type": "string"
          }
        },
        "description": "Shared by every settings mutation RPC: the post-mutation record.",
        "properties": {
          "settings": {
            "$ref": "#/definitions/ApprovalSettings"
          }
        },
        "required": [
          "settings"
        ],
        "title": "SettingsResponse",
        "type": "object"
      }
    },
    {
      "description": "Record a per-session 'approve always' grant (honoured in every mode). Human/console-only.",
      "metadata": {},
      "name": "approval::approve-always",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "function_id": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "function_id",
          "session_id"
        ],
        "title": "ApproveAlwaysRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AlwaysAllowEntry": {
            "properties": {
              "function_id": {
                "type": "string"
              },
              "granted_at": {
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              },
              "granted_by": {
                "$ref": "#/definitions/GrantedBy"
              }
            },
            "required": [
              "function_id",
              "granted_at",
              "granted_by"
            ],
            "type": "object"
          },
          "ApprovalSettings": {
            "description": "The stored per-session record (scope `approval_settings`). Reads compute the effective settings from configuration defaults when no record exists — see settings.rs.",
            "properties": {
              "always_allow": {
                "default": [],
                "description": "Consulted only in auto mode.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "approved_always": {
                "default": [],
                "description": "Consulted in every mode — remembered human decisions.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "mode": {
                "allOf": [
                  {
                    "$ref": "#/definitions/PermissionMode"
                  }
                ],
                "default": "manual"
              },
              "mode_set_at": {
                "default": 0,
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "GrantedBy": {
            "oneOf": [
              {
                "enum": [
                  "user_click"
                ],
                "type": "string"
              },
              {
                "description": "Copied from deployment auto-scoped allow rules on first mutation.",
                "enum": [
                  "seed"
                ],
                "type": "string"
              }
            ]
          },
          "PermissionMode": {
            "enum": [
              "manual",
              "auto",
              "full"
            ],
            "type": "string"
          }
        },
        "description": "Shared by every settings mutation RPC: the post-mutation record.",
        "properties": {
          "settings": {
            "$ref": "#/definitions/ApprovalSettings"
          }
        },
        "required": [
          "settings"
        ],
        "title": "SettingsResponse",
        "type": "object"
      }
    },
    {
      "description": "Drop the session's stored settings record (revert to configuration defaults).",
      "metadata": {},
      "name": "approval::clear-settings",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "title": "ClearSettingsRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "cleared": {
            "type": "boolean"
          }
        },
        "required": [
          "cleared"
        ],
        "title": "ClearSettingsResponse",
        "type": "object"
      }
    },
    {
      "description": "post_trigger hook: watch shell::*/coder::* dispatch failures for a jail-scope filesystem_access_request and hold a filesystem_access pending approval for it. Called by the harness only.",
      "metadata": {},
      "name": "approval::filesystem-access-watch",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "HookCall": {
            "properties": {
              "arguments": {
                "default": null
              },
              "function_id": {
                "type": "string"
              },
              "id": {
                "type": "string"
              }
            },
            "required": [
              "function_id",
              "id"
            ],
            "type": "object"
          },
          "HookResult": {
            "description": "`post_trigger` payload: the function_result the harness is about to deliver to the model.",
            "properties": {
              "content": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/TextBlock"
                },
                "type": "array"
              },
              "details": {
                "$ref": "#/definitions/HookResultDetails"
              },
              "function_call_id": {
                "default": "",
                "type": "string"
              },
              "function_id": {
                "default": "",
                "type": "string"
              },
              "is_error": {
                "default": false,
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "HookResultDetails": {
            "properties": {
              "error": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/HookResultError"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "type": "object"
          },
          "HookResultError": {
            "description": "`result.details.error` on a dispatch failure. New harness versions fill `code`; older/plain remote errors can arrive without one, so consumers may recover it from the `filesystem_access_request` tail (contracts.md § post_trigger hook can now HOLD).",
            "properties": {
              "code": {
                "default": null,
                "type": [
                  "string",
                  "null"
                ]
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "message"
            ],
            "type": "object"
          },
          "TextBlock": {
            "properties": {
              "text": {
                "type": "string"
              },
              "type": {
                "type": "string"
              }
            },
            "required": [
              "text",
              "type"
            ],
            "type": "object"
          }
        },
        "properties": {
          "call": {
            "anyOf": [
              {
                "$ref": "#/definitions/HookCall"
              },
              {
                "type": "null"
              }
            ],
            "description": "pre_trigger payload."
          },
          "depth": {
            "default": 0,
            "description": "Sub-agent depth (hooks run for child turns too).",
            "format": "int64",
            "type": "integer"
          },
          "metadata": {
            "additionalProperties": true,
            "default": null,
            "description": "The per-send tracing metadata (`metadata.fs_scope.root` is the user-picked session workspace, absolute + canonical).",
            "type": [
              "object",
              "null"
            ]
          },
          "point": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/definitions/HookResult"
              },
              {
                "type": "null"
              }
            ],
            "description": "post_trigger payload: the function_result about to be delivered."
          },
          "session_id": {
            "type": "string"
          },
          "step": {
            "default": null,
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "turn_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id",
          "turn_id"
        ],
        "title": "HookInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "oneOf": [
          {
            "properties": {
              "decision": {
                "enum": [
                  "continue"
                ],
                "type": "string"
              }
            },
            "required": [
              "decision"
            ],
            "type": "object"
          },
          {
            "properties": {
              "decision": {
                "enum": [
                  "deny"
                ],
                "type": "string"
              },
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "decision",
              "reason"
            ],
            "type": "object"
          },
          {
            "properties": {
              "decision": {
                "enum": [
                  "hold"
                ],
                "type": "string"
              }
            },
            "required": [
              "decision"
            ],
            "type": "object"
          }
        ],
        "title": "HookOutput"
      }
    },
    {
      "description": "pre_trigger hook: evaluate the permission model and answer continue / deny / hold; writes the pending inbox record on hold. Called by the harness only.",
      "metadata": {},
      "name": "approval::gate",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "HookCall": {
            "properties": {
              "arguments": {
                "default": null
              },
              "function_id": {
                "type": "string"
              },
              "id": {
                "type": "string"
              }
            },
            "required": [
              "function_id",
              "id"
            ],
            "type": "object"
          },
          "HookResult": {
            "description": "`post_trigger` payload: the function_result the harness is about to deliver to the model.",
            "properties": {
              "content": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/TextBlock"
                },
                "type": "array"
              },
              "details": {
                "$ref": "#/definitions/HookResultDetails"
              },
              "function_call_id": {
                "default": "",
                "type": "string"
              },
              "function_id": {
                "default": "",
                "type": "string"
              },
              "is_error": {
                "default": false,
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "HookResultDetails": {
            "properties": {
              "error": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/HookResultError"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "type": "object"
          },
          "HookResultError": {
            "description": "`result.details.error` on a dispatch failure. New harness versions fill `code`; older/plain remote errors can arrive without one, so consumers may recover it from the `filesystem_access_request` tail (contracts.md § post_trigger hook can now HOLD).",
            "properties": {
              "code": {
                "default": null,
                "type": [
                  "string",
                  "null"
                ]
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "message"
            ],
            "type": "object"
          },
          "TextBlock": {
            "properties": {
              "text": {
                "type": "string"
              },
              "type": {
                "type": "string"
              }
            },
            "required": [
              "text",
              "type"
            ],
            "type": "object"
          }
        },
        "properties": {
          "call": {
            "anyOf": [
              {
                "$ref": "#/definitions/HookCall"
              },
              {
                "type": "null"
              }
            ],
            "description": "pre_trigger payload."
          },
          "depth": {
            "default": 0,
            "description": "Sub-agent depth (hooks run for child turns too).",
            "format": "int64",
            "type": "integer"
          },
          "metadata": {
            "additionalProperties": true,
            "default": null,
            "description": "The per-send tracing metadata (`metadata.fs_scope.root` is the user-picked session workspace, absolute + canonical).",
            "type": [
              "object",
              "null"
            ]
          },
          "point": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/definitions/HookResult"
              },
              {
                "type": "null"
              }
            ],
            "description": "post_trigger payload: the function_result about to be delivered."
          },
          "session_id": {
            "type": "string"
          },
          "step": {
            "default": null,
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "turn_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id",
          "turn_id"
        ],
        "title": "HookInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "oneOf": [
          {
            "properties": {
              "decision": {
                "enum": [
                  "continue"
                ],
                "type": "string"
              }
            },
            "required": [
              "decision"
            ],
            "type": "object"
          },
          {
            "properties": {
              "decision": {
                "enum": [
                  "deny"
                ],
                "type": "string"
              },
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "decision",
              "reason"
            ],
            "type": "object"
          },
          {
            "properties": {
              "decision": {
                "enum": [
                  "hold"
                ],
                "type": "string"
              }
            },
            "required": [
              "decision"
            ],
            "type": "object"
          }
        ],
        "title": "HookOutput"
      }
    },
    {
      "description": "Read one pending record; null when resolved or unknown.",
      "metadata": {},
      "name": "approval::get-pending",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "function_call_id": {
            "description": "The function call id of the held call.",
            "type": "string"
          },
          "session_id": {
            "description": "The session the pending record belongs to.",
            "type": "string"
          }
        },
        "required": [
          "function_call_id",
          "session_id"
        ],
        "title": "GetPendingRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "anyOf": [
          {
            "$ref": "#/definitions/GetPendingResponse"
          },
          {
            "type": "null"
          }
        ],
        "definitions": {
          "AccessRequest": {
            "description": "The filesystem access ask parsed from the shell/coder `filesystem_access_request` tail. Only present on `kind: \"filesystem_access\"` records.",
            "properties": {
              "attempted_path": {
                "description": "The raw offending path from the request, as sent by the caller.",
                "type": "string"
              },
              "error_code": {
                "description": "The jail-scope rejection code that produced this hint (`S215`, `S220`, `C215`, `C218`).",
                "type": "string"
              },
              "requested_root": {
                "description": "Canonical root a grant should target (existing directory, or nearest existing ancestor directory of the offending path).",
                "type": "string"
              }
            },
            "required": [
              "attempted_path",
              "error_code",
              "requested_root"
            ],
            "type": "object"
          },
          "GetPendingResponse": {
            "description": "`null` (handler returns `Option<GetPendingResponse>`) when resolved or unknown.",
            "properties": {
              "pending": {
                "$ref": "#/definitions/PendingApprovalRecord"
              }
            },
            "required": [
              "pending"
            ],
            "type": "object"
          },
          "PendingApprovalRecord": {
            "description": "The inbox payload — shared by both triggers, `list_pending`, and `get_pending`. Self-describing (all ids inside the value) and notification-safe (arguments pass through redaction).",
            "properties": {
              "access_request": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/AccessRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Present only when `kind == \"filesystem_access\"`."
              },
              "arguments_excerpt": {
                "default": null,
                "description": "Redacted — safe to forward to notification channels."
              },
              "assistant_excerpt": {
                "description": "First text block of the assistant message that contained this function_call. Best-effort; not derivable from `pre_trigger` `HookInput` in v1, so always omitted today.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "depth": {
                "default": 0,
                "description": "Sub-agent depth (0 = top-level), from `HookInput`.",
                "format": "int64",
                "type": "integer"
              },
              "function_call_id": {
                "type": "string"
              },
              "function_id": {
                "type": "string"
              },
              "kind": {
                "allOf": [
                  {
                    "$ref": "#/definitions/PendingKind"
                  }
                ],
                "description": "`\"function\"` or `\"filesystem_access\"`."
              },
              "pending_at": {
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              },
              "session_description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "session_id": {
                "type": "string"
              },
              "session_metadata": {
                "additionalProperties": true,
                "description": "Tenancy + routing (trigger config filter target).",
                "type": [
                  "object",
                  "null"
                ]
              },
              "session_title": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "turn_id": {
                "type": "string"
              }
            },
            "required": [
              "function_call_id",
              "function_id",
              "kind",
              "pending_at",
              "session_id",
              "turn_id"
            ],
            "type": "object"
          },
          "PendingKind": {
            "description": "What a pending record represents.",
            "oneOf": [
              {
                "description": "A function call held by `approval::gate` (the normal case).",
                "enum": [
                  "function"
                ],
                "type": "string"
              },
              {
                "description": "A function call held by `approval::filesystem-access-watch` after a jail-scope rejection carrying a `filesystem_access_request` — the console should ask \"allow access to `requested_root`?\" instead of a generic approve/deny prompt.",
                "enum": [
                  "filesystem_access"
                ],
                "type": "string"
              }
            ]
          }
        },
        "title": "Nullable_GetPendingResponse"
      }
    },
    {
      "description": "Read the session's effective settings (stored record or configuration defaults); never writes.",
      "metadata": {},
      "name": "approval::get-settings",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "title": "GetSettingsRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AlwaysAllowEntry": {
            "properties": {
              "function_id": {
                "type": "string"
              },
              "granted_at": {
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              },
              "granted_by": {
                "$ref": "#/definitions/GrantedBy"
              }
            },
            "required": [
              "function_id",
              "granted_at",
              "granted_by"
            ],
            "type": "object"
          },
          "ApprovalSettings": {
            "description": "The stored per-session record (scope `approval_settings`). Reads compute the effective settings from configuration defaults when no record exists — see settings.rs.",
            "properties": {
              "always_allow": {
                "default": [],
                "description": "Consulted only in auto mode.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "approved_always": {
                "default": [],
                "description": "Consulted in every mode — remembered human decisions.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "mode": {
                "allOf": [
                  {
                    "$ref": "#/definitions/PermissionMode"
                  }
                ],
                "default": "manual"
              },
              "mode_set_at": {
                "default": 0,
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "GrantedBy": {
            "oneOf": [
              {
                "enum": [
                  "user_click"
                ],
                "type": "string"
              },
              {
                "description": "Copied from deployment auto-scoped allow rules on first mutation.",
                "enum": [
                  "seed"
                ],
                "type": "string"
              }
            ]
          },
          "PermissionMode": {
            "enum": [
              "manual",
              "auto",
              "full"
            ],
            "type": "string"
          },
          "SettingsSource": {
            "enum": [
              "stored",
              "defaults"
            ],
            "type": "string"
          }
        },
        "properties": {
          "settings": {
            "$ref": "#/definitions/ApprovalSettings"
          },
          "source": {
            "allOf": [
              {
                "$ref": "#/definitions/SettingsSource"
              }
            ],
            "description": "Whether a per-session record exists."
          }
        },
        "required": [
          "settings",
          "source"
        ],
        "title": "GetSettingsResponse",
        "type": "object"
      }
    },
    {
      "description": "The pending inbox across sessions, with tenancy filters; the catch-up path for notification workers after a restart.",
      "metadata": {},
      "name": "approval::list-pending",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "cursor": {
            "default": null,
            "description": "Opaque.",
            "type": [
              "string",
              "null"
            ]
          },
          "limit": {
            "default": null,
            "description": "Default 50.",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "metadata": {
            "additionalProperties": true,
            "default": null,
            "description": "Equality match against `session_metadata` (tenancy).",
            "type": [
              "object",
              "null"
            ]
          },
          "session_id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "ListPendingRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AccessRequest": {
            "description": "The filesystem access ask parsed from the shell/coder `filesystem_access_request` tail. Only present on `kind: \"filesystem_access\"` records.",
            "properties": {
              "attempted_path": {
                "description": "The raw offending path from the request, as sent by the caller.",
                "type": "string"
              },
              "error_code": {
                "description": "The jail-scope rejection code that produced this hint (`S215`, `S220`, `C215`, `C218`).",
                "type": "string"
              },
              "requested_root": {
                "description": "Canonical root a grant should target (existing directory, or nearest existing ancestor directory of the offending path).",
                "type": "string"
              }
            },
            "required": [
              "attempted_path",
              "error_code",
              "requested_root"
            ],
            "type": "object"
          },
          "PendingApprovalRecord": {
            "description": "The inbox payload — shared by both triggers, `list_pending`, and `get_pending`. Self-describing (all ids inside the value) and notification-safe (arguments pass through redaction).",
            "properties": {
              "access_request": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/AccessRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Present only when `kind == \"filesystem_access\"`."
              },
              "arguments_excerpt": {
                "default": null,
                "description": "Redacted — safe to forward to notification channels."
              },
              "assistant_excerpt": {
                "description": "First text block of the assistant message that contained this function_call. Best-effort; not derivable from `pre_trigger` `HookInput` in v1, so always omitted today.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "depth": {
                "default": 0,
                "description": "Sub-agent depth (0 = top-level), from `HookInput`.",
                "format": "int64",
                "type": "integer"
              },
              "function_call_id": {
                "type": "string"
              },
              "function_id": {
                "type": "string"
              },
              "kind": {
                "allOf": [
                  {
                    "$ref": "#/definitions/PendingKind"
                  }
                ],
                "description": "`\"function\"` or `\"filesystem_access\"`."
              },
              "pending_at": {
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              },
              "session_description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "session_id": {
                "type": "string"
              },
              "session_metadata": {
                "additionalProperties": true,
                "description": "Tenancy + routing (trigger config filter target).",
                "type": [
                  "object",
                  "null"
                ]
              },
              "session_title": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "turn_id": {
                "type": "string"
              }
            },
            "required": [
              "function_call_id",
              "function_id",
              "kind",
              "pending_at",
              "session_id",
              "turn_id"
            ],
            "type": "object"
          },
          "PendingKind": {
            "description": "What a pending record represents.",
            "oneOf": [
              {
                "description": "A function call held by `approval::gate` (the normal case).",
                "enum": [
                  "function"
                ],
                "type": "string"
              },
              {
                "description": "A function call held by `approval::filesystem-access-watch` after a jail-scope rejection carrying a `filesystem_access_request` — the console should ask \"allow access to `requested_root`?\" instead of a generic approve/deny prompt.",
                "enum": [
                  "filesystem_access"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "pending": {
            "description": "Ordered by pending_at ascending.",
            "items": {
              "$ref": "#/definitions/PendingApprovalRecord"
            },
            "type": "array"
          }
        },
        "required": [
          "pending"
        ],
        "title": "ListPendingResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: hot-reload approval-gate from the authoritative configuration when it changes — swaps the per-call snapshot (timeouts + approval defaults).",
      "metadata": {},
      "name": "approval::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Internal `approval::on-config-change` trigger payload. The handler re-fetches the authoritative configuration, so this carries only the (advisory) configuration id; a struct (not `Value`) keeps the request schema concrete and unknown fields are ignored.",
        "properties": {
          "id": {
            "default": null,
            "description": "Configuration id that changed (advisory; the handler re-fetches the value).",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "OnConfigChangeEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Ack returned by the internal `approval::on-config-change` handler.",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "OnConfigChangeResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: session::deleted handler (purge the session's settings and pending records).",
      "metadata": {},
      "name": "approval::on-session-deleted",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "`session::deleted` payload (only the field we read).",
        "properties": {
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "title": "SessionDeletedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Acknowledgement returned by the internal trigger-bound handlers (`on-config-change`, `on-session-deleted`, `on-turn-completed`) whose result is not consumed by callers — kept typed so the response schema is concrete rather than the permissive `AnyValue` (\"unknown\") schema a `Value` return would emit.",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "EventAck",
        "type": "object"
      }
    },
    {
      "description": "Internal: harness::turn-completed handler (purge the turn's pending records).",
      "metadata": {},
      "name": "approval::on-turn-completed",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "`harness::turn-completed` payload (only the field we read).",
        "properties": {
          "turn_id": {
            "type": "string"
          }
        },
        "required": [
          "turn_id"
        ],
        "title": "TurnCompletedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Acknowledgement returned by the internal trigger-bound handlers (`on-config-change`, `on-session-deleted`, `on-turn-completed`) whose result is not consumed by callers — kept typed so the response schema is concrete rather than the permissive `AnyValue` (\"unknown\") schema a `Value` return would emit.",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "EventAck",
        "type": "object"
      }
    },
    {
      "description": "Remove a function from the session's auto-mode trust list (no-op when absent). Human/console-only.",
      "metadata": {},
      "name": "approval::remove-always-allow",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "function_id": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "function_id",
          "session_id"
        ],
        "title": "AlwaysAllowMutationRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AlwaysAllowEntry": {
            "properties": {
              "function_id": {
                "type": "string"
              },
              "granted_at": {
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              },
              "granted_by": {
                "$ref": "#/definitions/GrantedBy"
              }
            },
            "required": [
              "function_id",
              "granted_at",
              "granted_by"
            ],
            "type": "object"
          },
          "ApprovalSettings": {
            "description": "The stored per-session record (scope `approval_settings`). Reads compute the effective settings from configuration defaults when no record exists — see settings.rs.",
            "properties": {
              "always_allow": {
                "default": [],
                "description": "Consulted only in auto mode.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "approved_always": {
                "default": [],
                "description": "Consulted in every mode — remembered human decisions.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "mode": {
                "allOf": [
                  {
                    "$ref": "#/definitions/PermissionMode"
                  }
                ],
                "default": "manual"
              },
              "mode_set_at": {
                "default": 0,
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "GrantedBy": {
            "oneOf": [
              {
                "enum": [
                  "user_click"
                ],
                "type": "string"
              },
              {
                "description": "Copied from deployment auto-scoped allow rules on first mutation.",
                "enum": [
                  "seed"
                ],
                "type": "string"
              }
            ]
          },
          "PermissionMode": {
            "enum": [
              "manual",
              "auto",
              "full"
            ],
            "type": "string"
          }
        },
        "description": "Shared by every settings mutation RPC: the post-mutation record.",
        "properties": {
          "settings": {
            "$ref": "#/definitions/ApprovalSettings"
          }
        },
        "required": [
          "settings"
        ],
        "title": "SettingsResponse",
        "type": "object"
      }
    },
    {
      "description": "Apply a human decision to a held call: release it for execution (allow) or deliver a denial (deny). Human/console-only.",
      "metadata": {},
      "name": "approval::resolve",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AccessDuration": {
            "description": "How far an `allow` on a `filesystem_access` pending record should apply. Ignored (logged) when the target record's `kind` is not `\"filesystem_access\"`. Defaults to `once` when omitted.",
            "oneOf": [
              {
                "description": "One-shot: trust the directory for just the released call.",
                "enum": [
                  "once"
                ],
                "type": "string"
              },
              {
                "description": "Durable for the rest of the session (`harness::filesystem::grant`).",
                "enum": [
                  "session"
                ],
                "type": "string"
              },
              {
                "description": "Durable for the session AND persisted to the `shell` deployment configuration (`fs.host_roots`), best-effort.",
                "enum": [
                  "always"
                ],
                "type": "string"
              }
            ]
          },
          "ResolveDecision": {
            "enum": [
              "allow",
              "deny"
            ],
            "type": "string"
          }
        },
        "properties": {
          "access_duration": {
            "anyOf": [
              {
                "$ref": "#/definitions/AccessDuration"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Only meaningful when the target record is `kind: \"filesystem_access\"` and `decision: \"allow\"`. Defaults to `once`."
          },
          "decision": {
            "$ref": "#/definitions/ResolveDecision"
          },
          "function_call_id": {
            "type": "string"
          },
          "reason": {
            "default": null,
            "description": "Surfaced to the model on deny.",
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "decision",
          "function_call_id",
          "session_id"
        ],
        "title": "ResolveRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "resolved": {
            "description": "false: unknown/already-resolved pending call.",
            "type": "boolean"
          },
          "turn_resumed": {
            "description": "Passthrough from harness::function::resolve.",
            "type": [
              "boolean",
              "null"
            ]
          }
        },
        "required": [
          "resolved"
        ],
        "title": "ResolveResponse",
        "type": "object"
      }
    },
    {
      "description": "Set the session's permission mode (manual / auto / full). Human/console-only.",
      "metadata": {},
      "name": "approval::set-mode",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "PermissionMode": {
            "enum": [
              "manual",
              "auto",
              "full"
            ],
            "type": "string"
          }
        },
        "properties": {
          "mode": {
            "$ref": "#/definitions/PermissionMode"
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "mode",
          "session_id"
        ],
        "title": "SetModeRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AlwaysAllowEntry": {
            "properties": {
              "function_id": {
                "type": "string"
              },
              "granted_at": {
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              },
              "granted_by": {
                "$ref": "#/definitions/GrantedBy"
              }
            },
            "required": [
              "function_id",
              "granted_at",
              "granted_by"
            ],
            "type": "object"
          },
          "ApprovalSettings": {
            "description": "The stored per-session record (scope `approval_settings`). Reads compute the effective settings from configuration defaults when no record exists — see settings.rs.",
            "properties": {
              "always_allow": {
                "default": [],
                "description": "Consulted only in auto mode.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "approved_always": {
                "default": [],
                "description": "Consulted in every mode — remembered human decisions.",
                "items": {
                  "$ref": "#/definitions/AlwaysAllowEntry"
                },
                "type": "array"
              },
              "mode": {
                "allOf": [
                  {
                    "$ref": "#/definitions/PermissionMode"
                  }
                ],
                "default": "manual"
              },
              "mode_set_at": {
                "default": 0,
                "description": "ms epoch",
                "format": "int64",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "GrantedBy": {
            "oneOf": [
              {
                "enum": [
                  "user_click"
                ],
                "type": "string"
              },
              {
                "description": "Copied from deployment auto-scoped allow rules on first mutation.",
                "enum": [
                  "seed"
                ],
                "type": "string"
              }
            ]
          },
          "PermissionMode": {
            "enum": [
              "manual",
              "auto",
              "full"
            ],
            "type": "string"
          }
        },
        "description": "Shared by every settings mutation RPC: the post-mutation record.",
        "properties": {
          "settings": {
            "$ref": "#/definitions/ApprovalSettings"
          }
        },
        "required": [
          "settings"
        ],
        "title": "SettingsResponse",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "A function call was held for human approval and its inbox record written. Payload: PendingApprovalRecord (redacted args, session context, expiry). Bind notification workers here.",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "description": "Config accepted by both trigger types.",
        "properties": {
          "metadata": {
            "additionalProperties": true,
            "description": "Equality match against the record's `session_metadata` (every key given here must equal the stored value — subset match).",
            "type": [
              "object",
              "null"
            ]
          },
          "session_id": {
            "description": "Only deliver events for this session.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "BindingConfig",
        "type": "object"
      },
      "metadata": {},
      "name": "approval::pending-created",
      "return_schema": {}
    },
    {
      "description": "A pending approval left the inbox (outcome: allow | deny | timeout | aborted). Emitted exactly once per record; lets UIs clear badges.",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "description": "Config accepted by both trigger types.",
        "properties": {
          "metadata": {
            "additionalProperties": true,
            "description": "Equality match against the record's `session_metadata` (every key given here must equal the stored value — subset match).",
            "type": [
              "object",
              "null"
            ]
          },
          "session_id": {
            "description": "Only deliver events for this session.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "BindingConfig",
        "type": "object"
      },
      "metadata": {},
      "name": "approval::pending-resolved",
      "return_schema": {}
    }
  ]
}