sequenceDiagram
participant Op as Operator
participant FS as filesystem
participant W as wraptool
participant MCP as MCP client
Op->>FS: save config.yaml
FS-->>W: fsnotify event(s)
W->>W: debounce 200ms
W->>W: Load + Validate
alt valid
W->>W: policy.Reload (atomic)
W->>W: DeleteTools(old) + RegisterAll(new)
W-->>MCP: notifications/tools/list_changed
MCP->>W: tools/list
W-->>MCP: new tool set
else invalid
W-->>Op: log error, keep old policy
end
Hot-reload & capability requests
Hot-reload
wraptool watches the config file and reloads automatically. The reload steps:
- Parse and validate the new config (rejected reloads leave the running policy untouched and log the error).
- Atomically swap the policy used for new evaluations. In-flight commands keep running against the policy they started with.
- Remove the previously registered MCP tools and register a fresh set from the new config.
- Send
notifications/tools/list_changedto every connected MCP client. Clients that honour the notification refresh their tool list without reconnecting.
Triggers
File save — parent directory is watched via fsnotify; editors that save via atomic rename (vim, helix, …) are handled. CREATE+WRITE+CHMOD bursts are coalesced with a 200 ms debounce.
SIGHUP — still supported for scripted reloads:
kill -HUP $(pidof wraptool)
If fsnotify fails to initialise (no inotify available, etc.) wraptool logs a warning and falls back to SIGHUP-only mode.
Capability requests
When the AI hits a denied command, wraptool returns a structured refusal:
DENIED: 'git push --force origin main' is not permitted.
Policy: flag '--force' is not in the allowlist for 'git push'.
Allowed flags for 'git push': --set-upstream
To request this capability, call the 'wraptool_request_capability' tool.
The AI calls wraptool_request_capability with tool, subcommand, flags, rationale, and optional enables. The request is appended to a JSONL log.
Discovering what to request
A denial names the capability to ask for, but a tool with no allow rules never produces a denial — it registers no MCP tools at all, so the AI has nothing to call and nothing to be refused. That is a legitimate config state (a tool added ahead of its rules), and it used to leave capability requests as guesswork.
wraptool_discover closes that gap. It lists every wrapped tool with the subcommands policy allows today and the subcommands the tool advertises in its own --help but policy does not yet permit:
{"tools": {
"fj": {
"allowed": [],
"available": {"pr": "manage pull requests", "issue": "manage issues"},
"note": "no allow rules: nothing is callable yet — request one of the available subcommands"
}
}}The AI reads the catalog, then names a real subcommand in its request. Appearing in available grants nothing: the subcommand is still denied until an operator approves it below. The catalog comes from cache-backed introspection. A missing or stale cache may run the tool with a fixed -h, --help, or help argv; no argument from the AI ever reaches that command line.
Operator workflow
wraptool requests list # pending requests
wraptool requests approve req_23ae3793 --apply # accept + merge into config
wraptool requests deny req_01a2b3c4 \
--reason "use --force-with-lease instead"The same review workflow is available in the browser via the Web UI — enable it with an admin block.
--apply
wraptool requests approve <id> --apply merges the approved capability into the config in place:
- The tool must already exist in
tools:(binary path must be set). Unknown tools are refused — operator must add the binary manually first. - If a deny rule covers the same subcommand, the apply is refused.
- An existing allow rule with the same subcommand gains the new flags; duplicates are dropped.
- Otherwise a new allow rule is appended.
- Comments and key ordering are preserved (the YAML node tree is edited; the file is not re-marshaled from typed structs).
- The edited config is validated before being written; on failure the original file is left untouched.
- The write is atomic (temp file + rename) and the running server picks the change up via the file watcher.
Auto-added flags have no flag_constraints — tighten them by hand if you want to bound their values.
sequenceDiagram participant AI participant W as wraptool participant Log as request log participant Op as Operator participant Cfg as config.yaml AI->>W: denied tool call W-->>AI: denial + hint AI->>W: wraptool_request_capability(...) W->>Log: append (status=pending) Op->>Log: requests list Op->>W: requests approve <id> --apply W->>Cfg: ApplyRequest (atomic, preserves comments) Cfg-->>W: fsnotify event W->>W: reload policy + tools W-->>AI: notifications/tools/list_changed