Skip to content

Repository files navigation

santi

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.

Crates

  • 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 — the ProviderClient trait and its OpenAI Responses / chat-completions implementations. santi-core stays provider-agnostic behind this boundary.
  • santi-api — Axum HTTP server, SSE streaming, and OpenAPI export as a library. Owns the HTTP boundary and links santi-core.
  • api — the santi-api server 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.

Running locally

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 -- serve

Bootstrap 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-openapi

Local runtime operations stay on the server entry:

santi-api doctor
SANTI_STRAND_ID=<strand_id> santi-api inbox seed "come look"

Cross-host downstreams

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/stream

Configuration

santi.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).

Turn shell environment

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_default

When [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 public

Rotation 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.

Distribution

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 | sh

Canonical 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.

License

MIT. See LICENSE.

About

Customized personal agent service

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages