Appearance
plexd agent contract
The plexd agent is the first external integration of the platform API. It predates this control plane — it was developed against a mock — and is being ported onto the binding contract, which is the OpenAPI document at api/openapi/plexsphere-v1.yaml. This page is the consolidated agent-eye view of that contract: which operations make up the node-agent surface, how each is authenticated, which integration gaps are open on the control-plane side, and which mock-era surfaces were deliberately not adopted.
The machine-checked twin of this page is the workspace gate in tests/workspace/plexd_agent_contract_test.go, which pins the operation inventory, the per-operation auth posture, the absence of the retired paths, and the bootstrap/liveness/rotation/ snapshot wire shapes. Contract movement shows up there as an explicit table edit.
Credential model
A node holds exactly one long-lived credential: the Node Secret Key (NSK) returned once by POST /v1/register. The bootstrap token redeemed at registration is minted by an operator via the bootstrap-tokens surface and consumed on first use. After registration the NSK rides in Authorization: Bearer on every agent call and doubles as the AES-256-GCM key under which the secrets surface rewraps payloads.
Operation inventory
| Operation | Auth | Purpose |
|---|---|---|
POST /v1/register | bootstrap token (body) | redeem token, receive identity, NSK, signing key, peer snapshot, mesh CIDR |
POST /v1/nodes/{id}/heartbeat | NSK | liveness; returns reconcile and rotate_keys hints |
PUT /v1/nodes/{id}/endpoint | NSK | NAT endpoint observation; expires at stale_after |
GET /v1/nodes/{id}/state | NSK or operator | reconciliation pull — the baseline delivery channel; carries the pending-dispatch executions and sessions blocks |
GET /v1/nodes/{id}/events | NSK or operator | signed event stream (currently descoped, answers 501) |
GET /v1/nodes/{id}/reachability | NSK or operator | the node's own health projection |
PUT /v1/nodes/{id}/capabilities | NSK | capability manifest snapshot |
POST /v1/nodes/{id}/integrity-violations | NSK | tamper-evidence batches |
POST /v1/nodes/{id}/metrics | NSK | metric samples (JSON array) |
POST /v1/nodes/{id}/logs | NSK | log lines (NDJSON) |
POST /v1/nodes/{id}/audit | NSK | audit events (NDJSON) |
GET /v1/nodes/{id}/secrets/{name} | NSK | NSK-rewrapped secret envelope |
GET /v1/nodes/{id}/peers/{peer_node_id}/psk | NSK | NSK-rewrapped pairwise edge-PSK envelope |
PUT / DELETE /v1/nodes/{id}/state/reports/{key} | NSK | node-authored state reports |
POST /v1/keys/rotate | NSK | mesh key-rotation completion |
POST /v1/nodes/{id}/executions/{execution_id} | NSK | execution status callback (ack → started → terminal) |
POST /v1/nodes/{id}/sessions/{session_id} | NSK | per-kind session activity callback; a tcp session_started row also reports the listener endpoint the node bound |
GET /v1/artifacts/plexd/{version} | NSK or operator | indexed release record with per-architecture checksums and verdict |
GET /v1/artifacts/plexd/{version}/sigstore | NSK or operator | verbatim Sigstore bundle for independent verification |
Per-operation details live on the tag pages: nodes, node-events, capabilities, integrity, observability, secrets, actions, access, artifacts.
Known integration gaps
No control-plane gap currently blocks a real agent from operating end to end. The three surfaces that once did — the node-credential read surfaces, the dispatch channel, and PSK delivery — have all closed; each is recorded below with the shape it landed in:
- Node-credential read surfaces (closed). The five reads marked "NSK or operator" above are now dual-credential. A node authenticates
GET /v1/nodes/{id}/state,GET /v1/nodes/{id}/events, andGET /v1/nodes/{id}/reachabilitywith its NSK: the NSK arm applies an equality gate — the authenticated node must equal the addressed{id}— with no ReBAC round-trip, so a cross-node NSK is403 node_id_mismatchand a revoked one401 nsk_revoked. The two plexd artifact reads accept a registered node's valid, non-revoked NSK directly — the release registry is platform-global and read-only, so the NSK arm makes no authorization check. The operator arm (session cookie / API token / OIDC plus the operator-shaped relation) is unchanged on all five. - Dispatch channel (closed). Execution dispatches and session setups now ride the reconciliation pull:
GET /v1/nodes/{id}/statecarries anexecutionsblock of the Node's pending action dispatches and asessionsblock of the live mediated sessions it must serve (both always present,[]when empty). The agent acknowledges an execution through the NSK callbackPOST /v1/nodes/{id}/executions/{execution_id}and reports session activity throughPOST /v1/nodes/{id}/sessions/{session_id}, deriving both callback paths from its configured base — the pull blocks carry no callback URL. For atcpsession the agent also reports the listener it bound: thesession_startedrow carries an optionallistener_endpoint, thehost:porton the node's mesh address. The control plane never invents that coordinate. It persists the reported value as the session's authoritative dial target and serves it on the session read, so an operator's client dials the listener the agent actually opened, and a session no report has landed for is refused409 session_listener_pendingat the browser-attach gateway. Because the platform dials what it stores, the reported host must be the mesh address the platform allocated that node — the value is checked againstplexsphere.nodes.mesh_ipfor the node whose NSK authenticated the call, and a coordinate on any other address (a loopback, a metadata endpoint, a sibling node's mesh address) is refused with400 invalid_session_activity. An agent that binds on a wildcard address therefore reports its mesh address, not0.0.0.0. A repeatedsession_startedoverwrites the stored value, so the latest report wins. Asession_endedrow must not carry the key: a coordinate is admitted only on thesession_startedrow of atcpsession, and the platform refuses the whole callback with400 invalid_session_activityotherwise. The pull is the baseline delivery channel; the Signed Event Bus slice that would push the same dispatches with lower latency is a later optimisation and stays descoped for now. - PSK delivery (closed). Peer projections still carry no preshared keys, but a node now fetches the pairwise WireGuard edge PSK over the NSK-authenticated
GET /v1/nodes/{id}/peers/{peer_node_id}/psk. The control plane persists one wrapped PSK per Node; the server derives the per-tunnel edge key from the two endpoints' per-Node PSKs via HKDF-SHA256 (both ends derive a byte-identical value) and serves it rewrapped under the calling Node's NSK as a 60-byte AES-256-GCM envelope, so only NSK-wrapped ciphertext crosses the wire. Because the projections stay key-free by design, the agent runs a fixed re-fetch rule: fetch the edge PSK for a peer P when P first appears in a projection, when P'spublic_keychanges, and after the agent completes its own key rotation. An unknown, soft-deregistered, or foreign-Domain peer is404 peer_not_found; an endpoint whose per-Node PSK has not landed yet is409 psk_not_readyand the agent retries on its next reconcile. A429 per_node_rate_limitedmeans the agent exceeded its per-Node fetch budget; it waits theRetry-Afterinterval and resumes, and must not hot-loop. A403is terminal and never retried —node_deregisteredin particular says the agent's own Node has been revoked, so retrying would poll the control plane forever after an operator decommissioned it. The agent must open the envelope with the additional authenticated dataplexsphere/mesh/edge-psk/v1|<id>|<peer_node_id>|<epoch>, where<epoch>is theX-Plexsphere-PSK-Epochresponse header used verbatim, and must refuse an envelope whose epoch is older than the one already in force for that edge — a mesh key rotation does not rotate the NSK, so a captured pre-rotation envelope would otherwise still open and reinstall the retired key. See nodes for the full response contract.
Retired mock-era surfaces
The agent's mock-era contract contained surfaces that were not adopted into the binding API. They stay out unless a recorded disposition brings one back:
| Mock-era surface | Disposition |
|---|---|
GET /v1/ping | probe GET /v1/health instead |
POST /v1/nodes/{id}/deregister | decommission is operator-driven; no node-initiated removal |
POST /v1/nodes/{id}/drift | kept out; the agent applies corrections locally and nothing consumes a drift feed — a node-authored state report key (PUT /v1/nodes/{id}/state/reports/{key}) is the adoption path if visibility is ever needed |
GET /v1/nodes/{id}/metadata | never consumed; dropped |
POST /v1/nodes/{id}/report (bulk) | replaced by per-key state reports |
POST /v1/nodes/{id}/tunnels/{sid}/ready / closed | replaced by the session activity callback |
POST /v1/nodes/{id}/executions/{eid}/ack / result | replaced by the single execution callback |
GET /v1/artifacts/plexd/{version}/{os}/{arch} (binary) | replaced by the release-verdict record plus Sigstore bundle; binaries come from the release channel |
Dev-stack and test posture
The confidentiality direction is one-way: the public agent is consumed into this repository (pinned by /PLEXD_VERSION, deployed by the dev stack), and nothing from this repository ever runs in or flows into the public agent repository — the agent's own CI tests exclusively against its contract-faithful mock.
The pinned release runs against the real control plane in the golden-flow e2e suite (tests/e2e/dev/golden-flow/chainsaw-test.yaml). The dev stack deploys two agents against two seeded Resources in one Project, so each of them reads the other in its peer projection. The suite asserts both registrations and one capability manifest per node carrying the binary version, then each node's reconciliation pull carrying its sibling as a peer alongside the agent-authored status.mesh state report. It also asserts both reachability projections reaching healthy under the agents' own heartbeat cadence, an operator-triggered mesh key rotation one agent completes and its peer picks up in the next snapshot, and a metrics batch the agent logs a receipt for once the platform has acknowledged it.
Two legs are not exercised against the real agent. The execution and session callbacks wait on an upstream release that consumes the executions and sessions blocks of the reconciliation pull; the control-plane side is complete. The NAT endpoint report hard-depends on a reachable STUN server and would flake in CI, so PUT /v1/nodes/{id}/endpoint carries no e2e assertion. Edge-PSK fetch has no client in the pinned release, so its stub suite (tests/e2e/mesh/psk-fetch/) stays that surface's coverage until an upstream release adopts the endpoint.