feat(channels): opt-in live tool approvals via the portal#53
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds opt-in live tool approvals: when a WeChat instance sets
require_approval = true, anything the agent wants to run on your machine (Bash,Edit, Write, …) is held in
channels portalfor your Approve / Denyinstead of running unattended (or hanging until the provider's permission
timeout denies it).
Verified end-to-end with Playwright: a pending request appears in the portal as
[instance] Tool+ title + a monospace input preview with Deny/Approve; clickingApprove writes the decision the daemon then honors.
How it works (the two processes talk via the filesystem)
channels start(runs turns) andchannels portal(the UI) are separateprocesses, so approvals go through a small file-backed registry under
<config_dir>/approvals/:channels/approvals.py—ApprovalStore:create/list_pending/decide/poll_decision/cleanup. Request ids are strictly validated(
^[A-Za-z0-9_-]{1,128}$) before use as filenames; writes are atomic (0600files, 0700 dir); stale (>10 min) pendings are ignored.
SessionDispatchergainsapproval_store+require_approval. On aPERMISSION_REQUESTevent it publishes the request and polls the store for adecision (bounded by
turn_timeout), then callssession.resolve_permission(...). The turn'sfinallycancels the pollers(which resolve deny + clean up as they unwind).
GET /api/approvals+POST /api/approvals {id, decision}(token + Host gated like every route); a panel polls every 2.5s and posts your
verdict.
require_approval: bool(defaultfalse) on each instance;channels startcreates one shared store and passes it per-instance.Opt-out is byte-for-byte safe
self._require_approval = bool(require_approval and approval_store is not None).With the default (
false), none of the approval code runs — the daemon pathis exactly as before. Zero regression risk for existing setups.
Tests
test_channels_approvals.py— store CRUD, invalid-id rejection, stale expiry,ordering.
test_channels_dispatcher.py— full flow: request is published → allowresolves and the turn completes; deny works; and with
require_approvaloff nothing is published (broker-timeout deny).
require_approvalparse/default/bad-type) + portal (service + HTTPGET/POST /api/approvals, 400/404/401 paths).Full suite: 426 passed, 1 skipped (the 3 Windows-only failures are pre-existing
and pass on CI Linux). ruff clean.
🤖 对抗评审
Reviewer: Claude
general-purposesubagent (Codex not installed), read-only.VERDICT: CLEAN
Confirmed: opt-out is byte-for-byte unchanged (the emit hook +
_await_approvalnever run when
require_approvalis false);valid_idis enforced on everystore op so a hostile
request_idcan't escape the approvals dir (atomic writes,0600/0700); the poller is deadline-bounded and its
finallyresolves deny +cleans up on cancellation, and
PermissionBroker.resolveis a no-op once thefuture is done (no double-resolve hang); sync file I/O is sub-ms on tiny local
files;
input_preview(a user-facing approval prompt, truncated to 400 chars) is0600-local and carries no gateway token / full message text; both approval
endpoints are token + Host gated with
decision ∈ {allow,deny}validation andthe 256 KB body cap; the shared store is corruption-safe via per-request atomic
files; the 2.5s poll is cleared on reload. Ruff clean, Python 3.10.