Skip to content

feat(channels): opt-in live tool approvals via the portal#53

Merged
Germey merged 1 commit into
mainfrom
feat/portal-tool-approvals
Jul 3, 2026
Merged

feat(channels): opt-in live tool approvals via the portal#53
Germey merged 1 commit into
mainfrom
feat/portal-tool-approvals

Conversation

@acedatacloud-dev

Copy link
Copy Markdown
Member

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 portal for your Approve / Deny
instead 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; clicking
Approve writes the decision the daemon then honors.

How it works (the two processes talk via the filesystem)

channels start (runs turns) and channels portal (the UI) are separate
processes, so approvals go through a small file-backed registry under
<config_dir>/approvals/:

  • channels/approvals.pyApprovalStore: 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 (0600
    files, 0700 dir); stale (>10 min) pendings are ignored.
  • SessionDispatcher gains approval_store + require_approval. On a
    PERMISSION_REQUEST event it publishes the request and polls the store for a
    decision (bounded by turn_timeout), then calls
    session.resolve_permission(...). The turn's finally cancels the pollers
    (which resolve deny + clean up as they unwind).
  • Portal: GET /api/approvals + POST /api/approvals {id, decision}
    (token + Host gated like every route); a panel polls every 2.5s and posts your
    verdict.
  • Config: require_approval: bool (default false) on each instance;
    channels start creates 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 path
is 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 → allow
    resolves and the turn completes; deny works; and with require_approval
    off nothing is published (broker-timeout deny).
  • config (require_approval parse/default/bad-type) + portal (service + HTTP
    GET/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-purpose subagent (Codex not installed), read-only.

VERDICT: CLEAN

Confirmed: opt-out is byte-for-byte unchanged (the emit hook + _await_approval
never run when require_approval is false); valid_id is enforced on every
store op so a hostile request_id can't escape the approvals dir (atomic writes,
0600/0700); the poller is deadline-bounded and its finally resolves deny +
cleans up on cancellation, and PermissionBroker.resolve is a no-op once the
future 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) is
0600-local and carries no gateway token / full message text; both approval
endpoints are token + Host gated with decision ∈ {allow,deny} validation and
the 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.

@Germey Germey merged commit bec4f17 into main Jul 3, 2026
3 checks passed
@Germey Germey deleted the feat/portal-tool-approvals branch July 3, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants