agentmesh-control-plane
agentmesh-control-plane admits nodes, mints credentials, holds the mesh policy
and tracks routers. It is an HTTP server over SQLite or PostgreSQL.
agentmesh-control-plane [flags] run the server
agentmesh-control-plane admin ban --peer <id> ban a node directly in the database
agentmesh-control-plane admin unban --peer <id> lift a ban
Flags
| Flag | Default | Meaning |
|---|---|---|
--issuer | OIDC issuer URL(s), comma-separated. At least one of --issuer or --workload-issuer is required. The first issuer is advertised to enrolling nodes on /info. | |
--workload-issuer | Workload OIDC issuer(s), comma-separated (<issuer> or <issuer>=<email-suffix>, such as https://accounts.google.com=.gserviceaccount.com). Automatically added to --issuer. Workload tokens can enroll, refresh, and exchange credentials (/register, /refresh, /token/exchange), and are refused at /user/* and /oauth/authorize. | |
--allowed-audiences | agentmesh-audience | Audiences accepted in OIDC tokens, comma-separated. |
--oidc-client-id | first audience | OAuth client ID advertised on /info, for providers where it differs from the audience. |
--insecure-skip-tls-verify | false | Skip TLS verification when fetching issuer metadata and keys. For a cluster issuer served with the cluster CA, or a local development issuer. |
--bind-address | 0.0.0.0:8080 | HTTP listen address. |
--db-driver | sqlite | sqlite or postgres. |
--db-dsn | control-plane.db | Database DSN. A PostgreSQL DSN contains a password, so the next two options are preferred for PostgreSQL. |
--db-dsn-path | File containing the DSN. Overrides --db-dsn. Can also be set with AGENTMESH_DB_DSN. | |
--admin-token-path | File containing the bearer token for /admin/* and POST /policies. Can also be set with AGENTMESH_ADMIN_TOKEN. Without a token, the admin API cannot be used. | |
--auto-approve-enrollment | false | Issue credentials for valid bootstrap-token enrollments immediately instead of queueing them for approval. |
--biscuit-ttl | 24h | Lifetime of each credential. If the OIDC token expires sooner, the credential expires with it. |
--oidc-session-ttl | 2160h (90 days) | How long a human OIDC enrollment may keep refreshing before the identity must log in again. |
--workload-session-ttl | 48h | How long a workload OIDC enrollment (--workload-issuer) may refresh without presenting a fresh platform JWT in TokenRefreshRequest.jwt. |
--key-rotation-interval | 24h | How often a new signing key is generated. 0 disables rotation. |
--key-grace-period | 1h | How long a rotated-out key stays accepted. Credentials signed by a retired key cannot be verified or refreshed. Nodes and routers must pull /keys well within this window (agentmesh-node --control-plane-sync-interval, agentmesh-router --keys-sync-interval). |
--lease-duration | 15m | How long a router lease lasts without renewal. |
--node-retention | 720h (30 days) | How long the record of an enrolled node is kept after its session expires. Banned nodes are kept forever. 0 keeps every record. |
--mesh-reconnect-interval | 30s | How often the event publisher re-reads the router leases and dials any router it is not connected to. |
--log-level | info | debug, info, warn, error. LOG_FORMAT=json selects JSON output. |
HTTP API
Every request and response is a message in api/agentmesh.proto, in one of two
encodings. Routes that mesh components call use binary protobuf
(application/x-protobuf). Routes for operators and the console use
protojson of the same messages, with proto field names; unknown fields are
rejected. POST /policies accepts either and answers in the encoding of the
request.
Responses that carry credentials are sent with Cache-Control: no-store.
Unauthenticated
| Route | Purpose |
|---|---|
GET /healthz | Liveness. |
GET /readyz | Readiness. Returns 503 when the database is unreachable. |
GET /metrics | Prometheus metrics. |
GET /info | ControlPlaneInfoResponse: the OIDC issuer, client ID and audience, the addresses of routers with live leases, and the list of banned peer IDs. This is what a node needs before it can enroll. |
GET /keys | The current set of signing public keys, signed by every key in the set. A caller accepts the set only if one signature verifies under a key it already trusts. |
GET /.well-known/openid-configuration, GET /.well-known/oauth-authorization-server | OIDC Discovery and OAuth 2.1 Authorization Server metadata for outbound STS federation and MCP OAuth 2.1 clients. |
GET /jwks | JSON Web Key Set (ES256 public keys) for verifying border JWTs minted by POST /sts/token. |
GET /oauth/authorize, POST /oauth/token | OAuth 2.1 Authorization Code + PKCE endpoints (honouring RFC 8707 resource indicators to scope the issued Task Biscuit). Tokens from --workload-issuer are refused at /oauth/authorize with 403. |
Enrollment, refresh, and STS
Every request below carries a challenge_unix_ms and a challenge_signature.
The enrollee signs mesh:<endpoint>:<peer_id>:<challenge_unix_ms> with its
key, and the control plane accepts the signature within five minutes of that
instant. This proves that the caller holds the key behind the peer ID it
names. Every instant in a response (expire_time and the like) is a
google.protobuf.Timestamp, an RFC 3339 string in JSON.
Enrollment is admitted at 10 requests a second for the whole mesh, with a
burst of 20. A request over that is answered 429 with a Retry-After
header. agentmesh-node waits for it and sends the request again, with
jitter, for about a minute before it reports the failure, so a fleet of a
few hundred members that starts at once joins over its first minute instead
of losing the members the limiter turned away. A client of your own should
do the same.
| Route | Body | Purpose |
|---|---|---|
POST /register | EnrollRequest (OIDC token, public key, requested role, labels) | OIDC enrollment. Returns EnrollResponse: the credential, the control plane public key, router addresses and expiry. |
POST /enroll | BootstrapEnrollRequest (bootstrap token, public key, requested role, labels) | Bootstrap enrollment. Returns BootstrapEnrollResponse with status APPROVED and the credential, or with status PENDING. For a peer that is already approved, a new credential is minted directly. |
GET /enroll/status?peer_id= | headers X-Mesh-Challenge-Ts, X-Mesh-Challenge-Sig | Poll a pending enrollment. Any authentication failure answers 401, so the credential is released only to the enrollee. |
POST /refresh | TokenRefreshRequest (optional jwt), current credential as Authorization: Bearer <base64> | Exchange a credential for a new one. When jwt is present and its `iss |
POST /token/exchange | TokenExchangeRequest (subject_token, optional task_rule and seal, challenge signature), node credential as bearer | Stateless JWT-to-Biscuit exchange. Mints a Delegated Session Biscuit bound to the calling node (actor_node, client_peer_id) with zero database writes. |
POST /sts/token | STSTokenRequest (biscuit, destination, optional audience, challenge signature), node credential as bearer | Stateless Biscuit-to-JWT minting. Verifies the Biscuit and tar_block chain against egress://<destination> and mints a short-lived ES256 border JWT (sub, act.sub, aud, mesh_roles, mesh_task). |
GET /revocations | credential as Authorization: Bearer <base64> | RevocationsResponse: revoked root Biscuit revocation IDs (revocation_ids) and banned peer IDs (banned_peer_ids). |
POST /routers/lease | RouterLeaseRequest (credential, addresses, telemetry) | Register or renew a router lease. Requires role("mesh:role:router"). Announced addresses must end in the router’s own peer ID. |
GET /policies | credential as Authorization: Bearer <base64> | The mesh policy as PolicyConfigGetResponse: the Datalog rules a member adds to its authorizer, one per entry. Operators read the document at GET /admin/policy. |
GET /egress | credential as Authorization: Bearer <base64> | EgressAssignmentsResponse: the egress destinations whose served_by selects the calling node, by its roles or labels. A node registers and serves what it receives here. |
POST /nodes/catalog | NodeCatalogReport, credential as bearer | A node’s report of the services it publishes, for the console. Display only. Never used for authorization. |
Admin
All routes require Authorization: Bearer <admin-token>. Request and
response bodies are protojson of the messages named below, with proto field
names; unknown fields are rejected.
| Route | Purpose |
|---|---|
GET /admin/status | AdminStatusResponse: everything the console shows: routers, nodes, enrollment requests, tokens, users, the policy and the node service catalog. |
GET /admin/policy | The mesh policy as PolicyConfig, the same document POST /policies takes. |
POST /policies, PUT /policies | Replace the mesh policy. The body is PolicyConfig, so a misspelt grant fails instead of being dropped. |
GET /admin/bootstrap-tokens | BootstrapTokenListResponse: every token, including revoked and expired ones. |
POST /admin/bootstrap-tokens | Mint a token. Body BootstrapTokenCreateRequest: role (required), ttl_hours (default 24), max_usages (default 1), description, autonomous_recovery (default false). Returns 201 with BootstrapTokenCreateResponse: id, token (shown once), role, expire_time. |
DELETE /admin/bootstrap-tokens/{id} | Revoke a token. Can be repeated. The token stays in the list with revoke_time set. |
GET /admin/enrollments | EnrollmentRequestListResponse: bootstrap enrollment requests, pending and decided. |
POST /admin/enrollments/{id}/approve | Approve. Spends a token usage. Checks the token again, and checks the labels against the role’s allowed_labels. |
POST /admin/enrollments/{id}/reject | Reject. |
POST /admin/revoke | Body {"peer_id": "..."}. Ban the node and, for an OIDC enrollment, the identity behind it. |
POST /admin/nodes/{peer_id}/unban | Lift both bans. |
POST /admin/nodes/{peer_id}/autonomous-recovery | Body {"enabled": true}. Toggle the flag on an enrolled node. |
User
For a human OIDC user acting on their own nodes, authenticated with an ID token
from a configured human issuer (tokens matching --workload-issuer are refused
with 403). The console uses these routes.
| Route | Purpose |
|---|---|
GET /user/status | UserStatusResponse: the caller, their enrolled nodes and tokens. For an administrator it also carries the routers and the policy. |
POST /user/bootstrap-tokens | Mint a token owned by the caller; body and response as on the admin route. role defaults to mesh:role:node. Banning the owner disables the tokens they minted. |
POST /user/revoke | Ban one of the caller’s own nodes. |
Notes
- A new control plane grants nothing. With no policy in the database, enrollment succeeds only for identities bound to the requested role, and with no bindings there are none. A policy must be posted first.
- Bootstrap tokens are spent atomically.
max_usagesholds under concurrent enrollments, and a pending request is resolved exactly once. - An approved peer that enrolls again gets a new credential without approval, as long as the token is valid, the role matches the record and the peer is not banned. This is how a bootstrap node recovers from a retired signing key.
- Bans propagate in two ways.
/infolists banned peer IDs for nodes and routers that start or restart. A signed mesh event reaches the ones that are already running. - Node records are garbage-collected. A node whose session has expired is
deleted after
--node-retention. A node without a persistent data directory enrolls a new identity on every restart, so without this the node table would only grow.
Metrics
/metrics exposes agentmesh_control_plane_* counters and gauges: enrolled nodes
by role and state, active routers, mesh-connected peers as reported by
router leases, per-route request counts and latencies, and the Go runtime.