Hot-reload & capability requests

Hot-reload

wraptool watches the config file and reloads automatically. The reload steps:

  1. Parse and validate the new config (rejected reloads leave the running policy untouched and log the error).
  2. Atomically swap the policy used for new evaluations. In-flight commands keep running against the policy they started with.
  3. Remove the previously registered MCP tools and register a fresh set from the new config.
  4. Send notifications/tools/list_changed to every connected MCP client. Clients that honour the notification refresh their tool list without reconnecting.

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

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

Back to top