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:

VariablePurpose
AGENTGATE_CONTROL_URLControl plane origin (https required)
AGENTGATE_BROKER_IDThis broker's ID
AGENTGATE_CONTROL_TOKEN_FILEBearer token (chmod 600)
AGENTGATE_CONTROL_KEYS_FILE{ "kid": "<Ed25519 public key PEM>" } (chmod 600)
AGENTGATE_CONTROL_POLL_MSPoll 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.json

Fail-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 gets 503 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.js

Only 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_FILE and AGENTGATE_CONTROL_REVIEW_KID: the Ed25519 key that signs reviews documents. Its public key goes into every broker's AGENTGATE_CONTROL_KEYS_FILE under the same kid. 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 (optionally AGENTGATE_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.yaml shows 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 reviews with every bundle poll and, while anything is pending, every 5 seconds. Each review is applied with source control (control:<email>): policy hash binding, self-approval, duplicate reviewer, reviewerSources, and expiry all still hold on the broker. Applied reviews are audited as approval.review with source: "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

EndpointTokenPurpose
GET /v1/brokers/:id/policybrokerHighest-version document for :id or *
GET /v1/brokers/:id/revocationsbrokerSame, for revocations
POST /v1/brokers/:id/heartbeatbrokerReport versions and audit head; 409 AUDIT_ROLLBACK on regression
PUT /v1/admin/policyadminStore a signed policy (409 STALE_VERSION, 409 SHADOWED_BY_WILDCARD)
PUT /v1/admin/revocationsadminStore a signed revocation list
DELETE /v1/admin/policy/targets/:targetadminRecover from a mistaken publish
POST /v1/admin/brokers/:id/reset-rollbackadminRe-baseline after investigating a rollback
GET /v1/admin/inventoryadminFleet status, stale, rollbackDetected
POST /v1/brokers/:id/approvalsbrokerPublish pending approvals
GET /v1/brokers/:id/reviewsbrokerSigned reviews document (404 until the first decision)
GET /v1/admin/approvalsadminEvery 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.