santi is a standalone agent runtime.
It keeps the architecture deliberately small:
crates/
api/ # the `santi-api` server binary: config, bootstrap, and local ops
santi-core/ # soul runtime: sessions, turns, context assembly, objects, workspace
santi-estate/ # Keel resource graph, persistence ceremonies, and projections
santi-provider/ # provider-agnostic ProviderClient boundary (OpenAI Responses, chat-completions)
santi-api/ # HTTP/SSE + OpenAPI server library over santi-core
santi/ # the `santi` transport-only HTTP client binary
The runtime owns soul identity, per-session runtime state, turn execution with
streaming events (thinking / text / tool calls / tool results), context
assembly into provider input, a local object protocol (santi://), and
workspace/memory. The only way into the runtime is HTTP.
santi-core— runtime model and service. Turn execution, context assembly,santi://object store, soul/session workspaces and memory.santi-estate— the Keel-backed persistent resource graph. It owns durable facts, lifecycle ceremonies, projections, schema evolution, and the exact one-way transition from the retired v39 store.santi-provider— theProviderClienttrait and its OpenAI Responses / chat-completions implementations.santi-corestays provider-agnostic behind this boundary.santi-api— Axum HTTP server, SSE streaming, and OpenAPI export as a library. Owns the HTTP boundary and linkssanti-core.api— thesanti-apiserver executable. Owns config, bootstrap, serving, OpenAPI export, and local runtime operations.santi— the transport-only HTTP client. It reaches the runtime only over HTTP and does not link the runtime crates.
cp santi.example.toml santi.toml # fill in a provider api_key + model
cp .env.example .env # SANTI_PATHS_DATABASE / SANTI_LISTEN_HOST / SANTI_LISTEN_PORT
cargo run -p api -- bootstrap
cargo run -p api -- serveBootstrap is explicit and idempotent. It initializes the Keel estate and keeps
its sudo custody at $SANTI_HOME/runtime/sudo with owner-only permissions.
Keep that file with the estate in every backup or move: bootstrap refuses an
occupied estate whose sudo custody is absent. Ordinary serve only binds an
already initialized estate and never mints replacement custody.
With no .env/config at all, santi-api resolves paths from its home directory
(SANTI_HOME, default ~/.santi): it reads ~/.santi/santi.toml, while
bootstrap creates the required runtime directories.
Then, against a running server:
cargo run -p santi -- health
cargo run -p santi -- strand create
cargo run -p santi -- strand send <strand_id> "hello"
cargo run -p santi -- strand events <strand_id>Every accepted send returns a durable receipt.inbox_id. Query its obligation
state and state-transition evidence without replaying the message timeline:
santi receipt <inbox_id>Receipt completion means an assistant turn completed and was persisted. Driver
recovery or incident resolution alone never marks the receipt completed.
Migration-reconstructed transitions expose reconstructed_from; live
transitions leave it unset. A v24 drain is completed only when its linked turn
is durably completed, never merely because the inbox item was drained.
After the cause of a turn_failed receipt is cleared, an explicit
santi strand drive <strand_id> starts a recovery turn even when no new inbox
message exists. A context compact that resolves its incident does the same.
Ordinary boot/completion pokes do not retry failed receipts, and recovery reuses
durable confirmed effect results rather than replaying them automatically.
Shell commands also create a durable effect attempt. Receipt completion still only proves the assistant turn was persisted; inspect the linked effect before claiming that an external action occurred:
santi effect query <effect_id>
santi effect resolve <effect_id> \
--outcome applied \
--evidence "operator found the target marker"prepared means dispatch has not begun. An interrupted dispatching attempt
becomes unknown, because the runtime cannot prove whether the command took
effect, and is never replayed automatically. A mechanically rejected spawn is
not_dispatched; a durably captured command result is confirmed. Only an
unknown attempt accepts an explicit applied or not-applied operator
resolution, and resolution records evidence without retrying the command or
changing its turn/receipt state.
Long-running agent commands use the soul-owned job resource instead of teaching
the shell tool nohup/tmux conventions:
santi job create "compile release" "cargo build --release" \
--timeout-seconds 3600 \
--output-limit-bytes 16777216 \
--remind-every-seconds 300
santi job list
santi job get <job_id>
santi job logs <job_id> --stream stdout --cursor 0
santi job cancel <job_id>
santi job ack <job_id>job create is available from a Santi runtime shell invocation, which supplies
a short-lived, single-use capability bound to that soul/strand/turn/tool
origin. Success means the job specification is durable and the detached
sidecar has published its initial claimed state; it does not mean the payload
is running or complete. The creating shell may then end while the sidecar and
payload continue independently through the job's stateless unique stamp
convention.
--remind-every-seconds is optional; when present it must be greater than zero
and wakes the owning strand with a coalesced current job snapshot.
Cold start reconciles each active stamp independently and never automatically
replays an uncertain job. The launched process receives origin locators but
never inherits the create capability.
Export the OpenAPI document:
cargo run -p api -- export-openapiLocal runtime operations stay on the server entry:
santi-api doctor
SANTI_STRAND_ID=<strand_id> santi-api inbox seed "come look"A downstream owns one non-overlapping label zone such as stim:. Create a
high-entropy token, register only its SHA-256 digest through the trusted
management path, and retain the token in the downstream:
TOKEN=$(openssl rand -hex 32)
DIGEST=$(printf %s "$TOKEN" | sha256sum | cut -d ' ' -f1)
curl -X POST http://127.0.0.1:43307/api/v1/downstreams \
-H 'Content-Type: application/json' \
-d "{\"id\":\"stim\",\"prefix\":\"stim:\",\"digest\":\"$DIGEST\"}"The credential digest is stored but never returned by the management API. Registrations are idempotent when all three input fields match. Prefix overlap, credential reuse, or reuse of an id with different values is rejected. Upgrading a v31 database intentionally clears the old environment-variable registrations; register digest credentials before starting a remote consumer.
The downstream submits every request with a stable idempotency key. Repeating the
same key and payload returns the original receipt; changing the payload produces
409 Conflict:
curl -X POST https://santi.liberte.top/api/v1/ingest \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"soul":"soul_default","label":"stim:alice","text":"hello","request":"message-42"}'Completed turns are pulled with the same credential. The response is
{"cursor":...,"events":[...]} and includes only the registered zone. Persist
the returned cursor even when events is empty. The cursor is a global opaque
high-water mark, so it reveals aggregate activity volume but no other zone's
labels or payloads. The SSE endpoint is only a lossy wake-up signal; always use
cursor backfill as the authority:
curl -H "Authorization: Bearer $TOKEN" \
'https://santi.liberte.top/api/v1/turn-events?since=0'
curl -N -H "Authorization: Bearer $TOKEN" \
https://santi.liberte.top/api/v1/turn-events/streamsanti.toml (gitignored) holds real provider credentials. Start from
santi.example.toml.
Everything anchors on the santi home — SANTI_HOME, default ~/.santi — so the
runtime works with zero explicit configuration. Each path can be overridden by
its own variable (configuration precedence is --flag > environment > config
file > defaults):
| Variable | Default | Purpose |
|---|---|---|
SANTI_HOME |
~/.santi |
Anchor for the defaults below |
SANTI_CONFIG |
$SANTI_HOME/santi.toml |
Provider config file (--config overrides) |
SANTI_PATHS_DATABASE |
$SANTI_HOME/runtime/db |
Keel estate |
SANTI_PATHS_RUNTIME_ROOT |
$SANTI_HOME/runtime |
Soul/session memory, objects |
SANTI_PATHS_EXECUTION_ROOT |
$SANTI_HOME/execution |
Shell tool working area |
SANTI_PROVIDER |
openai |
Selected provider profile |
SANTI_LISTEN_HOST / SANTI_LISTEN_PORT |
127.0.0.1 / 43307 |
Bind address |
SANTI_API_KEY |
unset | Transitional static bearer sent by the CLI (--api-key overrides). The runtime has no global API-key gate; edge Authentik protects management paths, while downstream data paths use registered zone credentials. |
SANTI_API_URL |
http://127.0.0.1:43307 |
Client target (--base-url overrides) |
SANTI_CAPABILITY_ISSUER |
unset | Runtime capability issuer; configuring any capability field requires the complete authority |
SANTI_CAPABILITY_AUDIENCE |
unset | Exact downstream audience carried by each runtime capability |
SANTI_CAPABILITY_KEY_ID |
unset | Active Ed25519 key id |
SANTI_CAPABILITY_PRIVATE_KEY |
unset | Unpadded base64url Ed25519 32-byte private seed; never inherited by a shell |
SANTI_CAPABILITY_TTL_SECONDS |
120 |
Capability lifetime, from 1 through 300 seconds |
The sudo custody path is deliberately derived from the runtime root rather than
configured independently. The default pair is runtime/db plus runtime/sudo,
so the existing runtime snapshot remains the atomic recovery unit.
A .env in the working directory is loaded and overrides the process
environment (via dotenvy::dotenv_override).
Each synchronous turn shell starts from an explicit environment wall instead
of inheriting the server process wholesale. The small host allowlist is
overlaid, in order, by [environment] in santi.toml, soul declarations,
strand declarations, and Santi's reserved SANTI_* runtime variables.
Values can be literals or env://NAME references into the server process
environment. An unresolved reference is passed to the shell unchanged and
recorded as a SantiSystem message, so a missing value is visible without
writing it into the estate.
santi env set soul soul_default STIM_BASE_URL https://stim.example.com:43309
santi env list soul soul_defaultWhen [capability] is configured, Santi signs a fresh
SANTI_RUNTIME_CAPABILITY immediately before each shell effect. Its claims bind
issuer, audience, key id, soul, strand, turn, tool call, effect, issue time, and
expiry. The reserved value overlays declared environment and is not persisted.
The private key remains in typed server config and is excluded by the child
environment wall.
Generate a 32-byte seed outside the estate, configure the authority, and derive the public material for downstream trust without printing the private key:
export SANTI_CAPABILITY_PRIVATE_KEY="$(
openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n'
)"
santi-api capability publicRotation adds the new public key to downstream trust first, switches Santi's
key_id and private key second, then removes the retired public key after the
maximum TTL.
Soul and strand declarations are intentionally limited to the synchronous turn
shell in v1. Detached santi job create payloads retain their existing host
allowlist plus reserved engine variables; they receive neither these
declarations nor SANTI_RUNTIME_CAPABILITY.
The declared release shape supports Linux x86_64 only. plumb.toml names the
two binaries and the Debian payload root; stable Plumb owns target builds,
archives, package assembly, managers, and sealed delivery. Santi has not
entered formal publication yet. Once it does, the host path will resolve the
immutable Debian object from the candidate's exact seal.
curl -fsSL https://releases.santi.perish.uk/manage.sh | shCanonical stable is the only moving install and the only release admitted to the default seat. Beta validation is always exact and isolated:
curl -fsSL https://releases.santi.perish.uk/manage.sh | sh -s -- \
install \
--channel beta \
--version vX.Y.Z-beta.N \
--install-root "$HOME/.local/share/santi-beta" \
--bin-dir "$HOME/.local/santi-beta/bin"Forgejo PerishFire/santi is the canonical source and automation target. The
public GitHub repository is retained as historical context without reverse
synchronization.
MIT. See LICENSE.