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

FlagDefaultMeaning
--issuerOIDC 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-issuerWorkload 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-audiencesagentmesh-audienceAudiences accepted in OIDC tokens, comma-separated.
--oidc-client-idfirst audienceOAuth client ID advertised on /info, for providers where it differs from the audience.
--insecure-skip-tls-verifyfalseSkip TLS verification when fetching issuer metadata and keys. For a cluster issuer served with the cluster CA, or a local development issuer.
--bind-address0.0.0.0:8080HTTP listen address.
--db-driversqlitesqlite or postgres.
--db-dsncontrol-plane.dbDatabase DSN. A PostgreSQL DSN contains a password, so the next two options are preferred for PostgreSQL.
--db-dsn-pathFile containing the DSN. Overrides --db-dsn. Can also be set with AGENTMESH_DB_DSN.
--admin-token-pathFile 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-enrollmentfalseIssue credentials for valid bootstrap-token enrollments immediately instead of queueing them for approval.
--biscuit-ttl24hLifetime of each credential. If the OIDC token expires sooner, the credential expires with it.
--oidc-session-ttl2160h (90 days)How long a human OIDC enrollment may keep refreshing before the identity must log in again.
--workload-session-ttl48hHow long a workload OIDC enrollment (--workload-issuer) may refresh without presenting a fresh platform JWT in TokenRefreshRequest.jwt.
--key-rotation-interval24hHow often a new signing key is generated. 0 disables rotation.
--key-grace-period1hHow 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-duration15mHow long a router lease lasts without renewal.
--node-retention720h (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-interval30sHow often the event publisher re-reads the router leases and dials any router it is not connected to.
--log-levelinfodebug, 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

RoutePurpose
GET /healthzLiveness.
GET /readyzReadiness. Returns 503 when the database is unreachable.
GET /metricsPrometheus metrics.
GET /infoControlPlaneInfoResponse: 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 /keysThe 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-serverOIDC Discovery and OAuth 2.1 Authorization Server metadata for outbound STS federation and MCP OAuth 2.1 clients.
GET /jwksJSON Web Key Set (ES256 public keys) for verifying border JWTs minted by POST /sts/token.
GET /oauth/authorize, POST /oauth/tokenOAuth 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.

RouteBodyPurpose
POST /registerEnrollRequest (OIDC token, public key, requested role, labels)OIDC enrollment. Returns EnrollResponse: the credential, the control plane public key, router addresses and expiry.
POST /enrollBootstrapEnrollRequest (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-SigPoll a pending enrollment. Any authentication failure answers 401, so the credential is released only to the enrollee.
POST /refreshTokenRefreshRequest (optional jwt), current credential as Authorization: Bearer <base64>Exchange a credential for a new one. When jwt is present and its `iss
POST /token/exchangeTokenExchangeRequest (subject_token, optional task_rule and seal, challenge signature), node credential as bearerStateless 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/tokenSTSTokenRequest (biscuit, destination, optional audience, challenge signature), node credential as bearerStateless 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 /revocationscredential as Authorization: Bearer <base64>RevocationsResponse: revoked root Biscuit revocation IDs (revocation_ids) and banned peer IDs (banned_peer_ids).
POST /routers/leaseRouterLeaseRequest (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 /policiescredential 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 /egresscredential 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/catalogNodeCatalogReport, credential as bearerA 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.

RoutePurpose
GET /admin/statusAdminStatusResponse: everything the console shows: routers, nodes, enrollment requests, tokens, users, the policy and the node service catalog.
GET /admin/policyThe mesh policy as PolicyConfig, the same document POST /policies takes.
POST /policies, PUT /policiesReplace the mesh policy. The body is PolicyConfig, so a misspelt grant fails instead of being dropped.
GET /admin/bootstrap-tokensBootstrapTokenListResponse: every token, including revoked and expired ones.
POST /admin/bootstrap-tokensMint 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/enrollmentsEnrollmentRequestListResponse: bootstrap enrollment requests, pending and decided.
POST /admin/enrollments/{id}/approveApprove. Spends a token usage. Checks the token again, and checks the labels against the role’s allowed_labels.
POST /admin/enrollments/{id}/rejectReject.
POST /admin/revokeBody {"peer_id": "..."}. Ban the node and, for an OIDC enrollment, the identity behind it.
POST /admin/nodes/{peer_id}/unbanLift both bans.
POST /admin/nodes/{peer_id}/autonomous-recoveryBody {"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.

RoutePurpose
GET /user/statusUserStatusResponse: the caller, their enrolled nodes and tokens. For an administrator it also carries the routers and the policy.
POST /user/bootstrap-tokensMint 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/revokeBan 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_usages holds 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. /info lists 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.