Skip to content

API Types

All request/response types for the 17 control plane API endpoints, organized by endpoint group. All types use JSON struct tags matching the API specification.

Registration

POST /v1/register

Unauthenticated (security: []): the bootstrap token travels in the request body, not an Authorization header. Success is 201 Created; errors use RFC 9457 application/problem+json. See Control Plane API Endpoints for the wire examples and error taxonomy.

RegisterRequest

FieldTypeJSON TagConstraints
ProjectIDstring"project_id"Required. Platform project UUID.
ResourceHandlestring"resource_handle"Required. Platform Resource handle.
BootstrapTokenstring"bootstrap_token"Required. Format psb_<env>_<project>_<kind>_<random>, matching ^psb_[a-z]+_[a-z2-7]+_(node|bridge)_[a-z2-7]{20,}$.
Noncestring"nonce"Required. Fresh UUIDv4 generated per registration attempt (server-side replay protection).
PublicKeystring"public_key"Required. Curve25519 public key as 44-char standard base64, matching ^[A-Za-z0-9+/]{43}=$.
RequestedResourceIDstring"requested_resource_id,omitempty"Optional. Resource ID override when substrate naming differs from the handle.

hostname, metadata, and capabilities are no longer part of registration. Capabilities are published after registration via PUT /v1/nodes/{node_id}/capabilities.

RegisterResponse (201 Created)

FieldTypeJSON TagDescription
NodeIDstring"node_id"Assigned node identifier (UUID)
MeshIPstring"mesh_ip"Assigned mesh IP address
SigningPublicKeystring"signing_public_key"Control plane signing public key
SigningKeyIDstring"signing_key_id"Signing key id for rotation-aware signature verification (e.g. did:web:plexsphere.com#key-2026-04)
NSKstring"nsk"Node secret key, returned exactly once: 44-char standard-padded base64 that decodes to the 32-byte AES-256-GCM key. Sent verbatim as the bearer credential; decoded before use as a key
PeerSnapshot[]RegisterPeer"peer_snapshot"Initial peer snapshot
DomainMeshCIDRstring"domain_mesh_cidr"Domain mesh address range (e.g. 100.64.0.0/10)

RegisterPeer

FieldTypeJSON TagDescription
NodeIDstring"node_id"Peer node ID
MeshIPstring"mesh_ip"Peer mesh IP address
PublicKeystring"public_key"Peer Curve25519 public key
FallbackEndpointstring"fallback_endpoint,omitempty"Optional fallback WireGuard endpoint

RegisterPeer is deliberately narrow: it carries no psk, allowed_ips, or endpoint. The reconciliation peer shape is SnapshotPeer (see State), used by GET /v1/nodes/{node_id}/state.

Peer

Peer is the WireGuard peer shape used only for the SSE peer_* payloads; it no longer appears on the state pull. It remains until issue #25 migrates that contract.

FieldTypeJSON TagDescription
IDstring"id"Peer node ID
PublicKeystring"public_key"WireGuard public key
MeshIPstring"mesh_ip"Mesh IP address
Endpointstring"endpoint"WireGuard endpoint
AllowedIPs[]string"allowed_ips"Allowed IP ranges
PSKstring"psk"Pre-shared key

Heartbeat

POST /v1/nodes/{node_id}/heartbeat

HeartbeatRequest

FieldTypeJSON TagDescription
ClientNowtime.Time"client_now"RFC 3339 UTC send time, stamped fresh per request; the server rejects a skew above 60s
BinaryChecksumstring"binary_checksum"SHA-256 of the running binary (64-char hex or 44-char base64 of 32 bytes)
BinaryVersionstring"binary_version"Build version (dev when unset); never empty
NATSummarymap[string]any"nat_summary"Always a JSON object: {} before discovery, else {"endpoint", "nat_type"}

nat_summary is a map[string]any rather than a nil-able pointer so it always marshals to an object: a nil map would emit null, which the control plane rejects with malformed_heartbeat_request.

The in-memory NAT discovery result that feeds nat_summary is nat.DiscoveryResult; it is never serialized on its own.

HeartbeatResponse

FieldTypeJSON TagDescription
AcceptedAttime.Time"accepted_at"Server receive time (used for skew estimation)
Reconcilebool"reconcile"Whether to trigger reconciliation
RotateKeysbool"rotate_keys"Whether to rotate keys

State

GET /v1/nodes/{node_id}/state

NodeStateSnapshot

The desired-state envelope. Every block key is always present on the wire: a null value means "block not populated", never "field absent", so the differ distinguishes a nil pointer from a populated block. None of the eight fields carry omitempty.

FieldTypeJSON TagDescription
Peers[]SnapshotPeer"peers"Desired peers; [] when empty, node_id ascending, self excluded
Reachabilityjson.RawMessage"reachability"The control plane's verdict about this node, carried opaquely and decoded leniently by the reachability observer
Policy*PolicySnapshot"policy"Merged network policy block
Bridge*BridgeSnapshot"bridge"Bridge subtrees
State*NodeStateBlock"state"Node state buckets
Reports*NodeStateBlock"reports"Mirrors state today (forward-compat split)
Executions[]NodeStateExecution"executions"Pending action dispatches; [] when empty, never null
Sessions*[]NodeStateSession"sessions"Live mediated-access sessions; [] when empty, never null on the wire. A pointer, like the block fields above: a nil value is a block the control plane did not populate, which is not a teardown signal

Three members are consumed on the dispatch seam rather than by the differ. executions is not desired state at all — it is a delivery queue, documented under Executions below. sessions is desired state, but state the tunnel dispatcher converges on itself, documented under Sessions below. reachability is not state to converge on either: the reachability observer decodes it leniently and logs the verdict when it changes.

SnapshotPeer

Carries no psk, allowed_ips, or endpoint. plexd derives AllowedIPs locally as mesh_ip/32, programs fallback_endpoint as the WireGuard endpoint (relay target), and configures peers without a preshared key.

FieldTypeJSON TagDescription
NodeIDstring"node_id"Peer node ID
MeshIPstring"mesh_ip"Peer mesh IP address
PublicKeystring"public_key"Peer WireGuard public key
FallbackEndpointstring"fallback_endpoint,omitempty"Optional relay/fallback endpoint

ReachabilitySnapshot

The interior of the reachability block. NodeStateSnapshot.Reachability stays a json.RawMessage: decoding it inside FetchState would let a malformed diagnostic block abort the whole state pull, so the decode happens in the reachability observer instead.

State is a plain string and is never validated against the constants below. The verdict vocabulary belongs to the control plane and grows there — never_reported was added after plexd shipped — so a value this build does not know is logged verbatim rather than rejected.

FieldTypeJSON TagDescription
Statestring"state"ReachabilityHealthy (healthy), ReachabilityStale (stale), ReachabilityUnreachable (unreachable), or ReachabilityNeverReported (never_reported); any other value is carried through
LastHeartbeatAt*time.Time"last_heartbeat_at,omitempty"Absent until the control plane accepts the first heartbeat
ChangedAttime.Time"changed_at"When the verdict last changed; always present

PolicySnapshot

The single merged policy block.

FieldTypeJSON TagDescription
RevisionIDstring"revision_id"Policy revision identifier
Fingerprintstring"fingerprint"44-char base64 SHA-256 over the server's canonical rule stream; plexd compares it byte-for-byte and never re-derives it
Rules[]PolicyRule"rules"Ordered firewall rules

PolicyRule

A five-tuple firewall rule. Ports is present iff Protocol is tcp or udp.

FieldTypeJSON TagDescription
Actionstring"action"allow, deny, or log
Protocolstring"protocol"tcp, udp, icmp, or any
SourceCIDRstring"source_cidr"Source CIDR
DestinationCIDRstring"destination_cidr"Destination CIDR
Ports*PortRange"ports,omitempty"Inclusive destination port range

PortRange

A single inclusive destination port range (from <= to).

FieldTypeJSON TagDescription
Fromint"from"Range start port
Toint"to"Range end port

BridgeSnapshot

Carries the four bridge subtrees. Each child is present-but-nullable; there are no base fields on the wire. Inner shapes (RelayConfig, UserAccessConfig, IngressConfig, SiteToSiteConfig) are documented under Bridge Mode.

FieldTypeJSON TagDescription
Relay*RelayConfig"relay"Relay session assignments
UserAccess*UserAccessConfig"user_access"User access configuration
Ingress*IngressConfig"ingress"Public ingress configuration
SiteToSite*SiteToSiteConfig"site_to_site"Site-to-site VPN configuration

NodeStateBlock

A three-bucket state block. Each bucket is a required array (never null when the block is populated), with entries ordered by key ascending.

FieldTypeJSON TagDescription
Metadata[]StateEntry"metadata"Metadata entries
Data[]StateEntry"data"Data entries
Reports[]StateEntry"reports"Report entries

StateEntry

Value is an opaque string; WorkloadTag is absent/empty when the entry is unattributed.

FieldTypeJSON TagDescription
Keystring"key"Entry key
Valuestring"value"Opaque string value
WorkloadTagstring"workload_tag,omitempty"Owning workload, if any

Supporting types

These types are still part of the API but no longer ride the state snapshot. SigningKeyRotation is the payload of the signing_key_rotated SSE event; SecretRef is the shape of the local node API's secret index; DataEntry is the shape the local node API serves for cached data entries.

SigningKeyRotation

FieldTypeJSON TagDescription
KeyIDstring"key_id"Key id of the new current signing key
PublicKeystring"public_key"Base64 Ed25519 public key of the new current key
PreviousKeyIDstring"previous_key_id"Prior key id kept valid during the grace window
TransitionExpirestime.Time"transition_expires"When the previous key id stops being accepted

DataEntry

FieldTypeJSON TagDescription
Keystring"key"Entry key
ContentTypestring"content_type"MIME content type
Payloadjson.RawMessage"payload"Arbitrary JSON payload
Versionint"version"Entry version
UpdatedAttime.Time"updated_at"Last update timestamp

SecretRef

FieldTypeJSON TagDescription
Keystring"key"Secret key name
Versionint"version"Secret version

Secrets

GET /v1/nodes/{node_id}/secrets/{key}

SecretEnvelope

The raw AES-256-GCM envelope served by the endpoint. Unlike every other type on this page it carries no JSON tags: Data is read from the octet-stream body, Version and KID from response headers — the type is never JSON-marshaled or unmarshaled.

FieldTypeSourceDescription
Data[]byteResponse body (application/octet-stream)<12-byte nonce> || <ciphertext + 16-byte GCM tag>
VersionintX-Plexsphere-Secret-Version headerVersion of the returned secret
KIDstringX-Plexsphere-Secret-KID headerKey id of the NSK the envelope was sealed under

Reports

PUT / DELETE /v1/nodes/{node_id}/state/reports/{key}

Per-key node state reports. PUT upserts the report at {key}; DELETE removes it. {key} follows the report key grammar ^[a-z][a-z0-9._-]{0,127}$, and Value is capped at 4096 bytes. See Control Plane API Endpoints for the wire examples and error taxonomy.

NodeStateReportRequest (PUT body)

FieldTypeJSON TagDescription
Valuestring"value"Opaque report payload (at most 4096 bytes)
WorkloadTagstring"workload_tag,omitempty"Owning workload; absent when unattributed

NodeStateReportResponse (200 OK on PUT)

DELETE returns 204 No Content with no body (or 404 report_not_found when the key has no report).

FieldTypeJSON TagDescription
AcceptedAttime.Time"accepted_at"Server receive time
Keystring"key"Echoes the addressed report key

Executions

The executions block of the state snapshot and the single execution callback the node posts back to POST /v1/nodes/{node_id}/executions/{execution_id}.

executions block of GET /v1/nodes/{node_id}/state

NodeStateExecution

One pending action dispatch. The block is the delivery channel for action dispatches: it is always present ([] when empty, never null) and ordered by RequestedAt, then ExecutionID. An entry keeps reappearing on every pull until its execution reaches a terminal status through the execution callback, so a consumer must tolerate re-observing the same ExecutionID. The entry carries no callback URL and no hook checksum.

FieldTypeJSON TagDescription
ExecutionIDstring"execution_id"Execution identifier
Actionstring"action"Action name (builtin or hook)
Typestring"type"One of the ActionKind* values
Parametersmap[string]json.RawMessage"parameters"Action parameters, each held as raw JSON so a large integer keeps every digit; nullable, a JSON null decodes to a nil map
Statusstring"status"pending, ack, or started
RequestedAttime.Time"requested_at"When the control plane dispatched the execution
ExpiresAttime.Time"expires_at"Absolute UTC deadline, not a relative timeout

ActionKind constants

ConstantValueDescription
ActionKindBuiltinbuiltinDispatches an action built into plexd
ActionKindHookhookDispatches a hook registered by the node

Pull-only execution status

ConstantValueDescription
ExecutionStatusPendingpendingDispatched and not yet acknowledged; observed only on the pull block and never reported by a node on the callback

The other two statuses an entry can carry, ack and started, are the node-reportable constants listed below.

POST /v1/nodes/{node_id}/executions/{execution_id}

A single callback advances an execution through its lifecycle, posted once per transition: ackstartedsucceeded | failed | cancelled.

ExecutionCallbackRequest

FieldTypeJSON TagDescription
Statusstring"status"One of the ExecutionStatus* values
ExitCode*int"exit_code,omitempty"Process exit code on a terminal callback (explicit zero)
Errorstring"error,omitempty"Failure reason on a failed terminal
DeclaredOutputBytesint64"declared_output_bytes,omitempty"Captured output length; over 16 KiB drives the presign
Output*ExecutionOutput"output,omitempty"Captured output on a terminal callback

ExecutionOutput

FieldTypeJSON TagDescription
Inlinestring"inline,omitempty"Base64 output body, used only when at most 16 KiB
ObjectKeystring"object_key,omitempty"Object-store key of an uploaded over-ceiling output
SHA256string"sha256,omitempty"Lowercase-hex SHA-256 of the uploaded bytes

ExecutionCallbackResponse

FieldTypeJSON TagDescription
Statusstring"status"The new invocation status
OutputUploadURLstring"output_upload_url,omitempty"Presigned PUT URL, set only on the declaring callback

ExecutionStatus constants

ConstantValueDescription
ExecutionStatusAckackAcknowledges receipt of the action request
ExecutionStatusStartedstartedReports that the action has begun running
ExecutionStatusSucceededsucceededTerminal callback for a successful run
ExecutionStatusFailedfailedTerminal callback for a failed run
ExecutionStatusCancelledcancelledTerminal callback for a cancelled run

Observability

The control-plane leg of the three ingest endpoints sends the flattened wire types below and receives an IngestReceipt on 202 Accepted. The internal pipeline and the optional local endpoint keep the richer MetricPoint / LogEntry / AuditEntry shapes; see Metrics Collection, Log Forwarding, and Audit Forwarding.

POST /v1/nodes/{node_id}/metrics

Body is a JSON array of MetricSample.

MetricSample

FieldTypeJSON TagDescription
Groupstring"group"Wire group: node_resources, tunnel_health, peer_latency, or agent_stats
Namestring"name"Sample name
Valuefloat64"value"Sample value
Labelsmap[string]string"labels,omitempty"Dimensions (e.g. peer_id); absent when the sample has none
Timestamptime.Time"timestamp"Sample time

MetricPoint — internal pipeline and local-endpoint format; MetricBatch is a type alias for []MetricPoint

FieldTypeJSON TagDescription
Timestamptime.Time"timestamp"Measurement time
Groupstring"group"Metric group name
PeerIDstring"peer_id,omitempty"Optional peer ID
Datajson.RawMessage"data"Metric data payload

POST /v1/nodes/{node_id}/logs

Body is NDJSON: one LogLine per line.

LogLine

FieldTypeJSON TagDescription
Severitystring"severity"Syslog severity: emerg, alert, crit, err, warning, notice, info, debug
Unitstring"unit,omitempty"Systemd unit; absent when unknown
Hostnamestring"hostname,omitempty"Origin hostname; absent when unknown
Messagestring"message"Log message (non-empty)
Timestamptime.Time"timestamp"Log time

LogEntry — internal pipeline and local-endpoint format; LogBatch is a type alias for []LogEntry

FieldTypeJSON TagDescription
Timestamptime.Time"timestamp"Log timestamp
Sourcestring"source"Log source
Unitstring"unit"Systemd unit
Messagestring"message"Log message
Severitystring"severity"Log level
Hostnamestring"hostname"Origin hostname

POST /v1/nodes/{node_id}/audit

Body is NDJSON: one AuditEvent per line.

AuditEvent

FieldTypeJSON TagDescription
Sourcestring"source"Wire source: auditd, k8s, or plexd
Actionstring"action"Action performed (non-empty)
Outcomestring"outcome"Outcome (non-empty)
Timestamptime.Time"timestamp"Event time

AuditEntry — internal pipeline and local-endpoint format; AuditBatch is a type alias for []AuditEntry

FieldTypeJSON TagDescription
Timestamptime.Time"timestamp"Event timestamp
Sourcestring"source"Audit source
EventTypestring"event_type"Audit event type
Subjectjson.RawMessage"subject"Who performed it
Objectjson.RawMessage"object"What was affected
Actionstring"action"Action performed
Resultstring"result"Action result
Hostnamestring"hostname"Origin hostname
Rawstring"raw"Raw audit record

Ingest receipt

IngestReceipt (202 Accepted for all three ingest endpoints)

FieldTypeJSON TagDescription
AcceptedAttime.Time"accepted_at"When the control plane accepted the batch
Recordsint"records"Number of records accepted from the batch

Capabilities

PUT /v1/nodes/{node_id}/capabilities

CapabilityManifestRequest

FieldTypeJSON TagDescription
BinaryVersionstring"binary_version"Agent version, non-empty after trimming
BinaryChecksumstring"binary_checksum"Running binary's SHA-256: 32 bytes, standard-padded base64
SSHHostKeyFingerprintstring"ssh_host_key_fingerprint,omitempty"Optional SHA256:<base64> host-key fingerprint
DeclaredHooks[]DeclaredHook"declared_hooks,omitempty"Optional hook declarations, at most 128, unique names

DeclaredHook

FieldTypeJSON TagDescription
Namestring"name"Hook identifier, non-empty
Checksumstring"checksum"Hook payload SHA-256: 32 bytes, standard-padded base64

The handler decodes this body with DisallowUnknownFields, so the manifest carries these fields and nothing else. Two consequences are worth stating outright:

  • The digest is base64, never hex. Both checksum fields are declared format: byte, so a hex digest decodes to 48 bytes and is refused with binary_checksum_invalid. integrity.WireChecksum converts the hex form the integrity package works in into the wire form.
  • There is no field for the agent's builtin action list. An earlier payload sent one under builtin_actions, inside a binary object the contract also does not define; the whole manifest was rejected. The action inventory is served locally by the node API's GET /v1/actions.

BinaryInfo — the node API's local view of the running binary. The control plane receives the same two values as the manifest's flat fields.

FieldTypeJSON TagDescription
Versionstring"version"Binary version
Checksumstring"checksum"Binary checksum

ActionInfo

FieldTypeJSON TagDescription
Namestring"name"Action name
Descriptionstring"description"Action description
Parameters[]ActionParam"parameters"Action parameters

ActionParam

FieldTypeJSON TagDescription
Namestring"name"Parameter name
Typestring"type"Parameter type
Requiredbool"required"Whether required
Defaultstring"default,omitempty"Default value for optional parameters
Descriptionstring"description"Parameter description

HookInfo

FieldTypeJSON TagDescription
Namestring"name"Hook name
Descriptionstring"description"Hook description
Sourcestring"source"Hook source path
Checksumstring"checksum"Source checksum
Parameters[]ActionParam"parameters"Hook parameters
Timeoutstring"timeout"Execution timeout
Sandboxstring"sandbox"Sandbox type

NAT Endpoint

PUT /v1/nodes/{node_id}/endpoint

EndpointRequest

FieldTypeJSON TagDescription
Endpointstring"endpoint"Discovered public endpoint (ip:port)
NATTypestring"nat_type"Wire NAT type (full_cone, restricted, port_restricted, symmetric, unknown)
ReportedAttime.Time"reported_at"RFC 3339 UTC, stamped fresh per attempt

EndpointResponse

FieldTypeJSON TagDescription
AcceptedAttime.Time"accepted_at"Server receive time
StaleAftertime.Time"stale_after"Deadline after which the endpoint is stale; drives the re-report cadence

The response no longer carries peer endpoints. Inbound peer endpoint updates arrive via the peer_endpoint_changed SSE event.

Key Rotation

POST /v1/keys/rotate

The server identifies the rotating node from its NSK bearer credential, so the request carries no node id. The response is a receipt, not a peer list — the propagated peer and PSK changes arrive via the next state pull.

KeyRotateRequest

FieldTypeJSON TagDescription
NewPublicKeystring"new_public_key"New Curve25519 public key (44-char standard base64)

KeyRotateResponse

FieldTypeJSON TagDescription
RotationIDstring"rotation_id"Identifier for this rotation
KIDstring"kid"Key identifier for the rotated material
WrapKeyVersionint"wrap_key_version"Wrap-key version (monotonic, >= 0)

Sessions

The sessions block of the state snapshot and the activity records the node posts back to POST /v1/nodes/{node_id}/sessions/{session_id}.

sessions block of GET /v1/nodes/{node_id}/state

NodeStateSession

One live mediated-access session. The block is desired state, not a queue: it is always present on the wire ([] when empty, never null). A response that violates that contract by omitting or nulling the key decodes to a nilSessions pointer, which the tunnel dispatcher leaves alone rather than reading as an empty block — emptiness tears every live session down, so the two cases must stay distinguishable. An entry appears when the control plane issues the session and disappears on revocation or hard expiry — that disappearance is the teardown signal, so there is no separate teardown event, no revocation callback to answer, and no terminal status to report. A live entry is redelivered on every pull, so a consumer must tolerate re-observing the same SessionID.

FieldTypeJSON TagDescription
SessionIDstring"session_id"Session identifier
JTIstring"jti"Equals the session id; carried opaquely and never evaluated
Kindstring"kind"One of the SessionKind* values; selects the member of Target
TargetSessionTarget"target"Exactly one member set, matching Kind
ExpiresAttime.Time"expires_at"Absolute UTC deadline, not a relative timeout
IdleTimeoutSecondsint"idle_timeout_seconds,omitempty"Idle window in seconds; 0 or absent means no idle window

SessionTarget

FieldTypeJSON TagDescription
SSH*SessionTargetSSH"ssh,omitempty"Target of an ssh session
K8s*SessionTargetK8s"k8s,omitempty"Target of a k8s session
TCP*SessionTargetTCP"tcp,omitempty"Target of a tcp session

SessionTargetSSH

FieldTypeJSON TagDescription
Userstring"user"Local account the session logs in as
AllowedCommands[]string"allowed_commands,omitempty"Closed set of command lines the session may run

SessionTargetK8s

FieldTypeJSON TagDescription
Userstring"user"Impersonated Kubernetes user
ImpersonateGroups[]string"impersonate_groups,omitempty"Groups impersonated alongside the user

SessionTargetTCP

FieldTypeJSON TagDescription
Hoststring"host"Host the node forwards the session's connections to
Portint"port"Port on that host

SessionKind constants

ConstantValueDescription
SessionKindSSHsshMediates an SSH session; Target.SSH is set
SessionKindK8sk8sMediates a Kubernetes API session; Target.K8s is set
SessionKindTCPtcpMediates a plain TCP forward; Target.TCP is set

Only tcp entries are provisionable by this agent; ssh and k8s entries are decoded and settled as unsupported. See Secure Access Tunneling for how plexd consumes the block.

POST /v1/nodes/{node_id}/sessions/{session_id}

A one-of session activity record: exactly one of ssh, k8s, or tcp is set, selecting the session kind. plexd's tunnel subsystem is an opaque TCP forwarder, so it emits only tcp rows; the ssh and k8s variants are carried by the type and accepted by the server but not emitted by any current session type. Success is 204 No Content.

SessionActivityRequest

FieldTypeJSON TagDescription
SSH*SSHActivity"ssh,omitempty"Per-command SSH session row
K8s*K8sActivity"k8s,omitempty"Per-request Kubernetes API row
TCP*TCPActivity"tcp,omitempty"TCP session lifecycle row

SSHActivity

FieldTypeJSON TagDescription
Commandstring"command"Executed command line (capped at 1 KiB)
ExitCode*int"exit_code,omitempty"Command exit code
StartedAt*time.Time"started_at,omitempty"RFC 3339 start time
CompletedAt*time.Time"completed_at,omitempty"RFC 3339 completion time

K8sActivity

FieldTypeJSON TagDescription
Verbstring"verb"API verb
ResourceKindstring"resource_kind,omitempty"Target resource kind
Namespacestring"namespace,omitempty"Target namespace
Namestring"name,omitempty"Target object name
StatusCodeint"status_code,omitempty"Response status code
DurationMSint64"duration_ms,omitempty"Request duration in ms

TCPActivity

FieldTypeJSON TagDescription
Phasestring"phase"One of the TCPPhase* values
TargetHoststring"target_host,omitempty"Forward target host
TargetPortint"target_port,omitempty"Forward target port
ListenerEndpointstring"listener_endpoint,omitempty"Address the node's listener bound; set on session_started rows only
BytesIn*int64"bytes_in,omitempty"Operator→target bytes (explicit 0 on session_ended)
BytesOut*int64"bytes_out,omitempty"Target→operator bytes (explicit 0 on session_ended)
TerminatedBystring"terminated_by,omitempty"One of the TerminatedBy* values

TCPPhase constants

ConstantValueDescription
TCPPhaseSessionStartedsession_startedOpening of a TCP session
TCPPhaseSessionEndedsession_endedClose of a TCP session

TerminatedBy constants

ConstantValueDescription
TerminatedByTTLExpiredttl_expiredSession reached its time-to-live
TerminatedByIdleTimeoutidle_timeoutIdle window elapsed with no byte flow
TerminatedByPlexdCloseplexd_closeplexd closed the session locally
TerminatedByOperatorRevokeoperator_revokeOperator's access was revoked. Part of the wire enum; never produced by plexd, which cannot tell a revocation from a block the control plane failed to serve

Integrity

POST /v1/nodes/{node_id}/integrity-violations

A batch of the tamper-evidence divergences the agent detected on the node: a mismatched binary checksum, a tampered hook checksum, or a rotated SSH host key. The endpoint takes a batch even for a single finding, so a lone violation travels as a one-entry array rather than as a bare object. Success is 200 OK with a receipt, not 202: the violation rows and the integrity_alert outbox event are committed before the response is written.

The handler decodes with DisallowUnknownFields and caps the body at 32 KiB, so an unknown or renamed field refuses the whole request.

IntegrityViolationsRequest

FieldTypeJSON TagDescription
Violations[]IntegrityViolationReport"violations"1 to MaxIntegrityViolationsPerBatch (128) entries

An empty array is refused with 400 integrity_violations_empty and an oversized one with integrity_violations_too_many. ReportIntegrityViolations checks both bounds locally and returns ErrIntegrityViolationsEmpty or ErrIntegrityViolationsTooMany without sending, since a refused request would take every violation in it down.

IntegrityViolationReport

FieldTypeJSON TagDescription
KindIntegrityViolationKind"kind"Artifact class the divergence applies to
DetectedByIntegrityDetector"detected_by"On-node detector that surfaced it
ArtifactIDstring"artifact_id"Affected artifact; non-empty after trimming, at most 4096 bytes
ObservedChecksumstring"observed_checksum,omitempty"Computed SHA-256, 32 raw bytes in standard-padded base64
ExpectedChecksumstring"expected_checksum,omitempty"Baseline digest, same encoding
ObservedFingerprintstring"observed_fingerprint,omitempty"Observed host-key fingerprint, SHA256:<base64>
ExpectedFingerprintstring"expected_fingerprint,omitempty"Baseline fingerprint, same form

Kind decides which digest pair is legal: a checksum kind carries the checksums and no fingerprint, the host-key kind carries the fingerprints and no checksum. Crossing them is refused with 400 integrity_violation_kind_mismatch, which is why the four digest fields are omitempty — an unset one must be absent from the JSON rather than present and empty.

The checksum fields are declared format: byte, so the hex form the integrity package works in decodes to 48 bytes and is refused with integrity_violation_checksum_invalid. integrity.WireChecksum converts hex into the wire form.

IntegrityViolationKind constants

ConstantValueDescription
IntegrityKindBinaryChecksumbinary_checksumThe agent's own binary
IntegrityKindHookChecksumhook_checksumA hook script
IntegrityKindSSHHostKeyssh_host_keyThe mesh SSH host key

IntegrityDetector constants

ConstantValueDescription
IntegrityDetectorStartupScanstartup_scanThe agent's sweep of the artifacts it holds baselines for. The enum has no value for a periodic re-scan, so the interval-driven sweeps report this one too
IntegrityDetectorInotifyinotifyThe hooks-directory watcher reacting to a filesystem event
IntegrityDetectorPreDispatchpre_dispatchThe check that runs immediately before a hook executes

A value outside either set is refused with 400 integrity_violation_kind_invalid or integrity_violation_detected_by_invalid.

IntegrityViolationsResponse

FieldTypeJSON TagDescription
AcceptedAttime.Time"accepted_at"Server-side commit timestamp
ViolationCountint"violation_count"Rows persisted; matches the input length

SSE Events

GET /v1/nodes/{node_id}/events

Returns text/event-stream with signed event envelopes.

Envelope

FieldTypeJSON TagDescription
IDstring"id"Unique event identifier (required)
Typestring"type"Event type discriminator (required)
Scopestring"scope"Scope the event applies to
KeyIDstring"key_id"Key id that selects the verifying signing key
IssuedAttime.Time"issued_at"Event timestamp
Payloadjson.RawMessage"payload"Event-specific JSON payload
Signaturestring"signature"Base64 Ed25519 signature over the canonical form

ParseEnvelope requires id and type. See Event Verification for the canonical form and signature rules.

Event Types

The event set is organized in two tiers. Contract types are emitted today; documented-coming types are named for the platform's 14-type taxonomy. Reconcile-driving payloads are opaque — the state pull is authoritative.

ConstantValueTier
EventNodeStateUpdatednode_state_updatedcontract
EventPolicyUpdatedpolicy_updatedcontract
EventBridgeConfigUpdatedbridge_config_updatedcontract
EventActionRequestaction_requestcontract
EventSessionSetupsession_setupcontract
EventPeerRegisteredpeer_registereddocumented-coming
EventPeerPSKAssignedpeer_psk_assigneddocumented-coming
EventPeerDeregisteredpeer_deregistereddocumented-coming
EventPeerEndpointChangedpeer_endpoint_changeddocumented-coming
EventPeerKeyRotatedpeer_key_rotateddocumented-coming
EventRotateKeysrotate_keysdocumented-coming
EventSigningKeyRotatedsigning_key_rotateddocumented-coming
EventSessionRevokedsession_revokeddocumented-coming

action_request is a contract type with an opaque payload: action dispatches are delivered in the executions block of the state pull, so the event only triggers a reconcile and the resulting pull carries the dispatch. session_setup is the same shape for mediated access — the session is delivered in the sessions block of that pull. session_revoked is the same shape from the other end: a session leaving the sessions block is what tears it down, so the event only pulls the observing reconcile forward.