JSON output shapes
Every read/status command takes a global --json flag and prints one
pretty JSON value to stdout (status lines stay on stderr — see the
api output discipline). This page is the
machine-consumer’s reference: the send-outcome vocabulary you classify by, the
session-digest schema, the shell relay’s MAC-stamped frames, and the catalog
of --json shapes.
A JSON value on stdout, a status tag on stderr, the truth in the exit code. A program reads whichever it needs and never has to scrape a human line.
Send outcomes
The closed set of spt send outcome lines — SENT, SENT(WAN), QUEUED,
QUEUED(idle-only), DEFERRED, NO_PERCH, and the WAN failure tags — is
documented with its exact conditions in
Messaging → Send outcomes.
The one rule a caller must encode: classify by exit code (0 = every
delivered/spooled outcome, non-zero = every failure), and treat QUEUED as
success — the message is durably spooled and drains when the target next
comes online; never retry on it.
Session digest — endpoint digest --json
spt endpoint digest <id> --json prints the endpoint’s activity digest as a
projection of its session logs (not a PTY scrape). The top-level object:
{ "turns": [ /* Turn, oldest → newest */ ] }
A --after <seq> cursor that predated the retained window adds one top-level
field, "after_predates_window": true, so a consumer knows it missed rows.
Turn — one user-opened turn:
| Field | Type | Notes |
|---|---|---|
input | string | null | The input that opened the turn; null for a preamble turn. |
entries | array of entry | Agent/tool/boundary/context entries in stream order. |
input_seq | number | Omitted when absent. |
partial | bool | Omitted when false; true on the open trailing turn. |
Entry — an externally-tagged variant (the tag key names the kind):
| Variant | Fields |
|---|---|
Agent | text (string); seq, ts optional |
ToolSprint | tools (array of {name, arg}); seq, ts optional — consecutive tool uses collapse into one sprint |
Boundary | kind ∈ clear | compact | boot; ts optional |
Context | kind ∈ psyche_download | echo_mirror | owl_message; body (string); ts optional |
ts is an RFC3339-UTC ordering key. --follow --json streams per-update
deltas instead of the snapshot shape above.
This is the digest read shape. It is distinct from the digest record an adapter pushes via
spt api digest-entry/ a[digest]extractor — that ingest contract (role/text/tool/ts) is documented in the manifest digest-record reference.
The shell relay — MAC-stamped frames
Two poll surfaces authenticate differently:
spt api poll <id>— the agent hook-channel drain. Reads the caller’s own perch spool; the manifest[inject]set must include the hook method. Each row prints as one whole<EVENT …>envelope on stdout.spt api poll <shell-id> --link <token>— the shell relay drain. The link token is the credential: it resolves the(owner, shell)pair and is refused (exit 1,AUTH_REFUSED) if no instance holds it. Rows are emitted raw, one per line — deliberately not<EVENT>-wrapped (the shell child parses its own vocabulary).
Shell-relay frames are MAC-stamped. The on-wire form is:
<mac> <frame>
— a 64-hex-char HMAC-SHA256 over the frame bytes, one ASCII space, then the
raw frame. The key is SHA-256(link_token); a frame with no valid MAC is
dropped, never processed. Agent-perch surfaces (spt ready, api listen,
api poll <id>) never emit stamped frames — a consumer of agent traffic
only ever sees <EVENT> / <EVENT-PART> lines.
--json catalog
Commands that emit --json, and the top-level shape each prints. Fields marked
optional are omitted when empty.
| Command | Top-level shape |
|---|---|
endpoint list · whoami | { self, subnets[], local[] } — self: {id, status, ready, alive, unbound, description, psyche_host_error, translation_fault?}; subnets[]: {name, endpoints[]} where each endpoint is {id, node, node_label, status, resources, endpoint_type?, project?}; local[]: {id, state, address, ready, alive, unbound, project?} |
endpoint digest | { turns[] } — see above |
endpoint description show | { id, description } |
endpoint role | { id, role } |
api endpoint-info [<id>] | { id, endpoint_type, adapter, local_node:{label,key}, attached_node:{label,key}|null, controlled, project, cwd, subnets[] } (always JSON) |
daemon status | { running, pid, net_up, pump_heartbeat_ms, managed_by, managed_active, subnets[], local_endpoints[], broker_image?, broker_stale?, stall_evict_count?, stall_evict_last_ms? } |
subnet status [--nodes] | { daemon_running, subnets[] } — each {name, node_count, endpoint_count, nodes[]} |
subnet show-code | { subnet, code, otpauth_uri? } |
notif list | { notifs[] } — each {notif_id, subnet, kind, state, from_id, head} |
access list | { entries[] } — each {endpoint, nodes[], locked} |
grant list | { grants[] } — each {capability, agent, node, qualifier} |
adapter list | { adapters[] } — each {name, kind, mode, version, source_dir, active} |
adapter version <option> | { adapter, version } |
shell list | { owner, shells[], instantiable[] } — each shell {id, alias, adapter, status} |
All shapes are additive-forever: new keys may appear, existing keys keep their meaning. Parse tolerantly (ignore unknown fields) and a newer daemon never breaks an older consumer.