Manifest reference
The runtime manifest is the declarative half of the harness contract: one TOML file per adapter, declaring only what varies per harness or shell. This page is the complete field reference.
Machine-readable companion: manifest.schema.json
— generated from the exact code that parses manifests, so it never drifts.
Validate your manifest against it, then spt adapter add enforces the
cross-field rules listed at the bottom.
The principle
SPT is not a harness. Command templates are opaque strings — spt-core
never parses out a model, tool list, or flag; the adapter writes the full
command line and spt-core runs it with {key} substitution placeholders
filled. Anything spt-core owns is not in the manifest:
- Sentinels (idle markers, the echo gate) — managed via
spt api state/spt api echo-gate; adapters only call them. - Spool, registry, perch, and daemon-state schemas.
- The event-block vocabulary — the tags spt-core surfaces to agents are a fixed, documented constant. Adapters pass spt-core’s output through unchanged.
- File-drop filenames — statically
<endpoint_id>-commune.md/<endpoint_id>-signoff.md; only the watched directory is declared. - Config knobs (pulse period, summarizer windows, …) — global spt-core settings with per-endpoint overrides, never per-adapter.
[adapter] — header (required)
The only mandatory section, and it must be readable before any install or
update — min_spt_core_version is the compatibility gate.
[adapter]
name = "my-harness" # the adapter_name every api call carries
kind = "harness" # "harness" (default) | "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0" # lowest spt-core this adapter tolerates
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
| Field | Required | Meaning |
|---|---|---|
name | yes | Adapter id; --adapter <name> on every machinery call |
kind | no (default harness) | harness hosts agents; shell provides a driven surface |
version | yes | The adapter’s own version |
min_spt_core_version | yes | Compat gate, checked before install/update |
hostable_types | no | Endpoint types this adapter can host |
[hooks.<event>] — inbound hook table
One entry per harness event, declaring the spt api command it fires, the
input fields it maps in, and whether the hook can surface text into the
agent’s context.
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
reads = ["session_id", "parent_pid"]
can_inject = true
[hooks.Stop]
fires = "api state idle"
can_inject = false # no inject channel -> sentinel/relay fallback
| Field | Required | Meaning |
|---|---|---|
fires | yes | Opaque api … command line the harness invokes for this event |
reads | no | Input fields (e.g. from the hook’s stdin payload) mapped into the command |
can_inject | no (default false) | Whether this hook can inject context back to the agent. When false, spt-core falls back to its sentinel + relay/poll path instead of expecting injection |
can_inject is the single most load-bearing harness-varying fact — declare
it honestly per hook.
[session] — watched dirs + role templates
Two watched-directory keys sit directly on [session]; the file names are
fixed by spt-core, only the directory varies:
[session]
commune_dir = ".my-harness" # watched for <endpoint_id>-commune.md
signoff_dir = ".my-harness" # watched for <endpoint_id>-signoff.md
Commune and signoff are file-drops, not commands — an agent writes a markdown file; spt-core’s watcher does the rest.
[session.<role>] — outbound templates
One opaque command template per role. Model, tools, flags, permissions — all
live inside command, never as separate fields.
Roles: self (the agent’s own session) · psyche_init / psyche_resume
(the endpoint’s persistent-context companion) · echo_commune (the bounded
history summarizer for sessions that end without a signoff) · signoff
(final context save) · notif (endpoint-native notification render).
[session.psyche_init]
command = "my-harness run --agent psyche --name {session_name} --model cheap"
cwd = "{psyche_dir}"
env_remove = ["MY_HARNESS_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"
detach = true
keys = ["session_name", "psyche_dir"]
| Field | Required | Meaning |
|---|---|---|
command | yes | Opaque command line with {key} placeholders |
cwd | no | Working directory (substitutable) |
recursion_guard_env | no | Env var set on summarizer children so their hooks bail (no summarizer-of-summarizer loops) |
detach | no (default false) | Spawn detached |
env_remove | no | Env vars stripped from the child’s inherited environment |
keys | no | The substitution keys spt-core fills for this role |
notif is the endpoint-native notification render — an OS toast, a status
LED, anything the adapter can run. Spawned detached when a notification
surfaces at this endpoint. Keys spt-core fills: {notif_id}, {notif_from},
{notif_subnet}, {notif_body}.
[session.notif]
command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
[env.<VAR>] — env-var table
Vars to inject into (or read from) sessions, and how. The injection channel is asymmetric by hosting mode: spt-hosted sessions inherit env from the broker that spawned them (no channel needed); harness-hosted sessions need the harness’s declared channel.
[env.MY_HARNESS_SESSION_ID]
direction = "inject" # "inject" | "read"
value = "{session_id}" # required for inject
channel = "MY_ENV_FILE" # harness-hosted only
[history] — transcript access
How spt-core reads a session’s conversation history (it powers the echo-commune summarizer). Three strategies; pick exactly one:
[history]
strategy = "fetcher" # "fetcher" | "locate_normalize" | "native"
fetcher = "my-harness-history --session {session_id}"
| Strategy | Required fields | Meaning |
|---|---|---|
fetcher | fetcher | spt-core runs your binary; it emits normalized history |
locate_normalize | locate_template, normalize_command | spt-core locates the raw transcript, then runs your normalizer over it |
native | — | The adapter pushes via spt api history-log; spt-core stores it |
spt-core has no built-in transcript parser for any harness — the adapter always owns that knowledge.
[inject] — input-injection methods
How text can be put in front of the agent, per activity state. Any
combination of pty, hook, relay, http:
[inject]
activity = ["hook"] # non-disruptive while the agent is working
idle = ["pty", "hook"]
[identity] — session identity
How the harness’s session id is obtained:
[identity]
session_id_source = "post_spawn" # "post_spawn" | "uuid_inject"
parent_ancestor_name = "my-harness"
post_spawn: discovered after spawn (process tree / wrapper hand-off), with
parent_ancestor_name as the process-tree anchor. uuid_inject: spt-core
injects a UUID the harness echoes back.
[pty_digest] — live activity buffer patterns
For sessions spt-core hosts in its own terminal layer, the adapter may supply
regex patterns that segment the raw PTY byte stream into an at-a-glance
activity digest (spt digest <id>). The patterns are opaque to spt-core —
the same rule as command templates; spt-core compiles and runs them, never
understanding harness semantics.
[pty_digest]
input_pattern = "\\nUSER> " # user-turn boundary (required)
agent_pattern = "\\nAGENT> " # agent-turn boundary (required)
tool_patterns = [ # optional: one regex per known tool,
"(?P<name>Write)\\((?P<arg>[^)]*)\\)", # naming `arg` (+ optional `name`)
]
catchall_pattern = "(?P<name>[A-Z][a-zA-Z]+)\\((?P<arg>[^)]*)\\)" # optional
window_turns = 3 # optional presentation override
persist = false # opt-in coarse activity log (default off)
Both boundary markers are required when the section is present; an absent section just means the digest is unavailable for this adapter.
[update] — adapter self-update
How spt-core updates (and first installs — install is the first update) this adapter:
[update]
avenue = "delegated" # "delegated" | "file_pull"
command = "my-harness plugin update spt" # delegated: the updater to run
self_verifies = true # delegated: attests the updater verifies its content
version_check = true # check min_spt_core_version before/after
uninstall = "my-harness plugin uninstall spt" # optional inverse, run by `spt adapter remove`
| Avenue | Required fields | Meaning |
|---|---|---|
delegated | command | spt-core delegates to the harness’s own updater. Set self_verifies = true to attest that updater verifies what it installs — an unattested delegated update is skipped as unverifiable |
file_pull | repo, signing_key | spt-core pulls files from repo (optionally filtered by path_regex) and verifies them against the adapter author’s Ed25519 signing_key (64 hex chars) before applying |
With file_pull, you sign your releases with your own key; spt-core’s
release keys never extend to adapter content.
Shell adapters (kind = "shell")
A shell adapter provides a driven surface (notifier, robot, sensor)
instead of hosting agents: same file, different body — the [shell] section
is required for (and exclusive to) kind = "shell". See
Shells: getting started for a worked,
shipping example; the field reference:
[shell]
spawn = "my-shell --link {link_token}" # broker-launched; opaque template
ephemeral = false # true -> no offline perch, no history retention
broadcast = "subnet" # "subnet" | "same-node" | "none" (discovery scope)
command_receipt = "stdin" # "http" | "stdin" | "relay" (how commands arrive)
pre_close = "park-and-save" # optional instruction sent on link-break
close_timeout_ms = 3000 # graceful-termination window
persistent = true # auto-online whenever the owner endpoint is online
wake_command = "my-waker --link {link_token}" # offline wake-watcher; exit code 86 = wake
can_shutdown = false # may the shell fire `api owner-shutdown`?
require_approval = "none" # "none" | "remembered" | "always" (per-spawn gate)
max_instances_per_owner = 4 # optional cap (online + offline both count)
over_cap = "reject" # "reject" | "approve" at the cap
[shell.capabilities] # the agent->shell command vocabulary
notify = { args = ["title", "body"] }
clear = {}
[shell.sensory] # the shell->agent sensory vocabulary
types = ["event"]
The capability and sensory vocabularies live in the manifest — spt-core
resolves them by adapter name, validates agent commands against them, and
rejects anything outside the declared vocabulary. The shell binary binds with
spt api … bind-shell --link <token> (the link token is the credential)
and pushes sensory payloads with spt api … emit.
Cross-field rules (spt adapter add enforces these)
The schema validates structure; registration additionally enforces:
adapter.nameandadapter.versionmust be non-empty.kind = "shell"requires a[shell]section;kind = "harness"must not have one.[history] strategy = "fetcher"requiresfetcher;locate_normalizerequires bothlocate_templateandnormalize_command.[env.*] direction = "inject"requires avalue.[update] avenue = "delegated"requirescommand;file_pullrequiresrepoandsigning_key.[pty_digest], when present, requires non-emptyinput_patternandagent_pattern.
A violation is a one-line error naming the field — fix and re-add.