Control plane
Distribute signed policy to a fleet of brokers and detect audit rollback.
Where to run this
Every command on this page runs from a clone of the AgentBox repository on the trusted host — never inside the agent workspace. If you haven't cloned it yet, start with the quickstart.
Signed policy bundles
Brokers can take policy from a control plane instead of a local file:
| Variable | Purpose |
|---|---|
AGENTGATE_CONTROL_URL | Control plane origin (https required) |
AGENTGATE_BROKER_ID | This broker's ID |
AGENTGATE_CONTROL_TOKEN_FILE | Bearer token (chmod 600) |
AGENTGATE_CONTROL_KEYS_FILE | { "kid": "<Ed25519 public key PEM>" } (chmod 600) |
AGENTGATE_CONTROL_POLL_MS | Poll interval (default 60000) |
Each document is { payload, kid, signature } — an Ed25519 signature over canonical JSON. Payloads carry kind, integer version, issuedAt/expiresAt (at most 7 days apart), and target brokers.
Sign documents
node scripts/sign-bundle.js --key control.pem --kid control1 --config policy.json \
--version 7 --brokers broker-a,broker-b --days 7 --out policy.signed.json
node scripts/sign-bundle.js --key control.pem --kid control1 --revocations revoked.json \
--version 3 --brokers '*' --days 1 --out revocations.signed.jsonFail-closed behavior
- A policy applies only if its version is greater than the last accepted one. Versions persist, so a restart can't be rolled back.
- The broker won't start without a valid policy.
- If fetching fails, the last good policy serves until
expiresAt, then every request gets503 POLICY_EXPIRED. - In assertion mode, an expired revocation list yields
503 REVOCATIONS_EXPIRED. Re-publish revocations — even an empty list — well within their lifetime.
Note
The control plane never holds signing keys. A compromised control plane can withhold updates (brokers then fail closed at expiry) but can't forge policy.
Running the control plane
AGENTGATE_CONTROL_BROKER_TOKENS_FILE=broker-tokens.json \
AGENTGATE_CONTROL_ADMIN_TOKEN_SHA256_FILE=admin-token.sha256 \
AGENTGATE_CONTROL_DATA_DIR=./control-data node src/control-plane.jsOnly token hashes are stored. Production requires AGENTGATE_CONTROL_TLS_CERT_FILE and AGENTGATE_CONTROL_TLS_KEY_FILE. examples/control-plane.compose.yaml runs it read-only, non-root, with capabilities dropped.
Central approvals
With several brokers, reviewers should not need one approval UI per broker. A broker in control mode publishes its pending approvals to the control plane (POST /v1/brokers/:id/approvals, at most 50 items, whenever its pending set changes, checked every 5 seconds); reviewers decide in the control plane's UI at /approvals; each decision returns to the broker as a signed reviews document, applied with every check a local review gets. In fleet mode one broker already serves a whole fleet; central approvals are for organisations running more than one broker.
AGENTGATE_CONTROL_REVIEW_KEY_FILEandAGENTGATE_CONTROL_REVIEW_KID: the Ed25519 key that signsreviewsdocuments. Its public key goes into every broker'sAGENTGATE_CONTROL_KEYS_FILEunder the samekid. It is the one key the control plane holds, and it can only sign reviews, never policy, revocations, or runtime identities.AGENTGATE_CONTROL_PROXY_SECRET_FILE(optionallyAGENTGATE_CONTROL_EMAIL_DOMAINS,AGENTGATE_CONTROL_ORIGIN): the reviewer UI, only behind oauth2-proxy, with CSRF tokens bound to email, broker, approval, and action.examples/control-plane.compose.yamlshows the proxy in front of it.- Published approvals carry a whitelisted
context(repository, action, runtime identity fields, changes, payload, merge details); tokens, assertions, and entitlement envelopes never leave the broker. - Brokers pull
reviewswith every bundle poll and, while anything is pending, every 5 seconds. Each review is applied with sourcecontrol(control:<email>): policy hash binding, self-approval, duplicate reviewer,reviewerSources, and expiry all still hold on the broker. Applied reviews are audited asapproval.reviewwithsource: "control"; a re-served version is applied idempotently, so a decision missed by a crash is retried. - A decision reaches the broker within about 5 seconds plus one poll round trip, independent of
AGENTGATE_CONTROL_POLL_MS.
Rules that should accept central reviews list "control" in reviewerSources (or leave it unset). Rules limited to ["oidc"] refuse them.
API
| Endpoint | Token | Purpose |
|---|---|---|
GET /v1/brokers/:id/policy | broker | Highest-version document for :id or * |
GET /v1/brokers/:id/revocations | broker | Same, for revocations |
POST /v1/brokers/:id/heartbeat | broker | Report versions and audit head; 409 AUDIT_ROLLBACK on regression |
PUT /v1/admin/policy | admin | Store a signed policy (409 STALE_VERSION, 409 SHADOWED_BY_WILDCARD) |
PUT /v1/admin/revocations | admin | Store a signed revocation list |
DELETE /v1/admin/policy/targets/:target | admin | Recover from a mistaken publish |
POST /v1/admin/brokers/:id/reset-rollback | admin | Re-baseline after investigating a rollback |
GET /v1/admin/inventory | admin | Fleet status, stale, rollbackDetected |
POST /v1/brokers/:id/approvals | broker | Publish pending approvals |
GET /v1/brokers/:id/reviews | broker | Signed reviews document (404 until the first decision) |
GET /v1/admin/approvals | admin | Every broker's published approvals |
Brokers heartbeat at startup and every 60 seconds. Alert on control.audit_rollback_reported, control.heartbeat_failed, bundle.*_rejected, bundle.*_expired, and inventory entries that are stale or rollbackDetected.