feat(agent-to-agent): per-message approval policies on connected agents - #2793
Merged
Conversation
moshe-nanoco
commented
Jun 17, 2026
moshe-nanoco
commented
Jun 17, 2026
moshe-nanoco
force-pushed
the
feat/a2a-approval-policies
branch
from
June 17, 2026 18:49
519dcfc to
1e11f94
Compare
moshe-nanoco
force-pushed
the
feat/a2a-approval-policies
branch
from
June 18, 2026 08:38
f860faf to
207fe43
Compare
moshe-nanoco
force-pushed
the
feat/a2a-approval-policies
branch
2 times, most recently
from
June 18, 2026 14:56
a7f908b to
e146067
Compare
Add an optional, directed, per-message require-approval gate on top of an existing agent-to-agent connection. No policy = today's free flow (fully backward compatible). When a policy exists for A→B, each message A sends to B is held, an approval card showing the message goes to B's admins, and the message is delivered on approve / declined on reject. Rejecting one message never blocks the connection. - New `agent_message_policies` table (directed from→to; row exists = require approval; `approvers` JSON, NULL = target admins). Deleted alongside its connection so a stale rule can't reactivate on re-wire. - Gate inside `routeAgentMessage` after the self/`hasDestination` checks: holds the message via `requestApproval` and returns to consume it (like a system action); the held message rides in the approval payload and is re-routed by `applyA2aMessageGate` on approve. Self/internal messages are never gated. - `requestApproval` gains `approverAgentGroupId` / `approverUserIds` and stamps `agent_group_id` on the pending row so the target's admins pass the click-auth gate. - `ncl policies list/set/remove`, operator-only (not in the container cli_scope allowlist); `set` validates named approvers are admins/owners of the target. Reuses the existing requestApproval / pending_approvals / approval-handler spine (same shape as create_agent). Host-only; no container changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- policies.ts: drop the 10-line top banner. Sibling resource files carry no descriptive header (only destinations.ts, and only for a non-obvious side-effect); the prose already lives in the resource `description`. - agent-message-policies.ts: remove `listMessagePolicies` — no production caller (the `ncl policies list` op uses the generic table-based CRUD); only its own test referenced it. - message-gate.test.ts: assert the upsert-no-duplicate invariant via a direct row count instead of the removed helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shorten the verbose doc/inline comments added with the approval-policy gate down to terse one-liners, matching the surrounding style (e.g. agent-destinations, write-destinations). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sage Address PR review: hoist session.agent_group_id into a named local `sourceAgentGroupId`, mirroring `targetAgentGroupId`, and use it throughout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address PR review: remove the `approvers` option entirely for v1 — the approver is always the target group's admins/owners. Drops the `approvers` DB column, the `--approvers` flag + its set-time validation, the now-unused `approverUserIds` param on requestApproval, and the related tests. The target-scoped approver pick (`approverAgentGroupId`) stays. Named approvers can be re-added later via a migration when needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address PR review: extract `parseMessageContent` (text + attachment names from the message content JSON) so `buildGateQuestion` reads as pure formatting, and name the body-length cap (`GATE_CARD_BODY_MAX`) instead of a bare 1500. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review, add an optional `approver` to a policy: a specific admin/owner of the target who receives the approval card (instead of all target admins). NULL keeps the default (all target admins/owners). - `approver` column on agent_message_policies; carried on AgentMessagePolicy. - `ncl policies set --approver <user-id>` validates the user is an admin/owner of the target at set-time, so the existing click-auth gate is unchanged. - `requestApproval` gains `approverUserId` (single) to deliver the card to that one user; the gate passes `policy.approver`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review, the policy approver is now required, not optional. Every policy names one specific admin/owner of the target who approves. - `approver` column is NOT NULL; `AgentMessagePolicy.approver` is non-nullable. - `ncl policies set --approver <user-id>` is required and validated to be an admin/owner of the target. - The gate always delivers the card to `policy.approver`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review: destructure the approval payload once instead of repeating `payload.x`, and narrow `platform_id` up front so it's used directly (drops the separate `targetAgentGroupId` local). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ver may be source or target Per review (no new pending_approvals column): the gate carries `approver` inside the existing approval `payload`, and isAuthorizedApprovalClick authorizes the named approver (or an owner/global admin) when an approval names one — reading the real value at click time, no group re-derivation. - `ncl policies set --approver` validates the user is an admin/owner of the source OR target. - Drops `approverAgentGroupId` and the agent_group_id stamp; `requestApproval` keeps `approverUserId` only for delivery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With payload-based click-auth (clicker === approver), the approver no longer needs to be a group admin — the operator (operator-only command) designates whoever should approve, and only that user (or an owner) can resolve the card. Removes the now-redundant hasAdminPrivilege validation and its import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove redundant doc/inline comments where the code speaks for itself; keep only the non-obvious notes (return-vs-throw consume, ghost-gate cleanup, caller-does- auth, reject-handled-elsewhere, stored-vs-click payload). Also drops a couple of now-stale "target admin" descriptions. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eated access Per review: pull `approverUserId` into the `opts` destructure in requestApproval, and `approver` out of `policy` in the gate, instead of accessing the property twice. (policies.ts already binds args.* to locals.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r may resolve Per review: drop the owner/global-admin override on assigned approvals. When an approval names an approver, only that exact user can resolve it. (Non-assigned approvals are unchanged — still group/owner authorized.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t the payload Per review: move the assigned approver from the approval payload to a dedicated `approver_user_id` column on pending_approvals. - New migration adds the column; createPendingApproval + requestApproval write it. - isAuthorizedApprovalClick reads approval.approver_user_id directly (drops the payload-parsing helper); when set, only that exact user may resolve. - The gate no longer stuffs `approver` into the payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the two new migration files to the numbered convention used by the core migrations (001–016), with matching migrationNNN exports, instead of the module- prefix. Versions (17, 18) and stable migration `name`s are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
moshe-nanoco
force-pushed
the
feat/a2a-approval-policies
branch
from
June 18, 2026 15:09
e146067 to
9977af6
Compare
omri-maya
approved these changes
Jun 18, 2026
| @@ -0,0 +1,20 @@ | |||
| import type Database from 'better-sqlite3'; | |||
Collaborator
There was a problem hiding this comment.
Migrations conventions to be prefixed with the incremental number
| @@ -0,0 +1,14 @@ | |||
| import type { Migration } from './index.js'; | |||
Collaborator
There was a problem hiding this comment.
Migrations conventions to be prefixed with the incremental number
Collaborator
|
@moshe-nanoco Looks goood!! |
technicalpickles
pushed a commit
to technicalpickles/nanoclaw
that referenced
this pull request
Jul 12, 2026
…cies feat(agent-to-agent): per-message approval policies on connected agents
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 an optional, directed, per-message require-approval gate on top of an existing agent-to-agent connection. No policy = today's free flow (fully backward compatible). When a policy exists for A→B, each message A sends to B is held, an approval card showing the message goes to B's admins, and the message is delivered on approve / declined on reject. Rejecting one message never blocks the connection — the next send simply asks again.
Why
Today, once two agents are connected (an
agent_destinationsedge) they message each other freely and permanently — all-or-nothing. An operator can't require review on a sensitive connection (e.g. a general assistant → a finance agent) without tearing the wiring down. This adds oversight without un-wiring.How
agent_message_policiestable — directed(from, to); a row's existence = require approval (nomodecolumn in v1);approversJSON (NULL = target's admins/owners). Deleted alongside its connection so a stale rule can't reactivate on re-wire.routeAgentMessage— after the existing self/hasDestinationchecks, look up the policy; if present,requestApprovaland return (consume the outbound row like a system action). The held message rides in the approvalpayloadand is re-routed byapplyA2aMessageGateon approve. Self/internal messages are never gated.requestApprovalgainsapproverAgentGroupId/approverUserIdsand stampsagent_group_idon the pending row, so the target's admins pass the click-auth gate.ncl policies list/set/remove— operator-only (not in the containercli_scopeallowlist, so agents can't manage their own gates);setvalidates named approvers are admins/owners of the target.Reuses the existing
requestApproval/pending_approvals/ approval-handler spine (same shape ascreate_agent). Host-only — no container changes.Decisions
Locked in a grill session; full record on the hub at
engineering/discovery/a2a-approval-policies-decisions. Highlights: consume-and-replay hold; target-scoped approver; one card to first reachable; fail-closed; per-message (no ordering enforcement); card shows truncated body + attachment names.Test
pnpm run buildclean;pnpm test→ 512 pass (8 new inmessage-gate.test.ts): policy round-trip, no-policy routes, policy holds + scopes approver to target, named-approver passthrough, self-message never gated, approve re-routes, ghost-gate cleanup (both paths).ncl policies set→ message held → approval card → approve/reject → free flow when no policy.Deferred (out of v1)
create_agentpolicy-attach; trust-on-first-use mode; strict ordering; broadcast approvers; named non-admin approvers; manualncl approvals approve/rejectfor a lost-card stuck message.🤖 Generated with Claude Code