Files
sausage/docs/RUNNING.md
T
2026-11-12 10:20:49 +00:00

17 KiB

Run Sausage v3 on Citadel

Use the web UI for conversations, passkeys, approvals, and secret entry. The host serves only JSON through /api/.

TL;DR: Start a separate v3 world, create the SSH tunnel below, and open http://localhost:8080. The Nix package supplies the userland bundle, source tree, and enabled chat service. Keep the complete world folder between starts.

Build and start

Run host commands from a checkout of the v3 branch on Citadel. Use Nix with flakes and the pipe-operators experimental feature. The package supplies the native Rust builder and its tools.

nix build builds the host and userland through separate Crane pipelines. The userland pipeline targets wasm32-wasip2. Separate Go derivations build awk and esbuild for WASI preview1. QuickJS builds from its vendored C sources with WASI clang. Nix assembles the final bundle without the development bundle script. Use sausage run --no-builder-sandbox WORLD only when the host must run the builder without namespace isolation.

The package entry point is:

nix run . -- init "$HOME/sausage-v3"
nix run . -- run "$HOME/sausage-v3" </dev/null

The package sets SAUSAGE_USERLAND to its assembled bundle by default. For init, you can override it with --userland BUNDLE or the environment variable. init asks nothing and refuses an existing world. run refuses a folder without a world. A bundle contains bin/, src/, and the seeded etc/ tree. It is not a world folder. Closing stdin only detaches the optional terminal console. The host and chat service continue.

The source-tree bundle builder is:

nix develop -c nu userland/bundle.nu

It builds the world commands incrementally with the configured Cargo cache. Pass a destination to place the bundle under your shared executor target directory. The script snapshots the current source files, including uncommitted edits. The source archive includes the complete config/, skills/, and extensions/ directories. The bundle contains plain source files. sausage init creates their repository and its first commit on main. Both bundle builders read userland/tools.json for the package and command names. Coreutils, diffutils, procps, and brush each occupy one real binary in bin/. Their command names are relative symlinks to those binaries. The world copier preserves these symlinks and dangling links in the source tree. A fresh development world needs no manual source copy. The bundle includes /bin/web.wasm and an enabled /etc/systemd/system/web.service. World init starts that service on 0.0.0.0:8080. To change its address, edit ExecStart to add --listen ADDR, then run systemctl restart web inside the world.

For a release host from the checkout:

nix develop -c cargo build -p sausage --release --locked

Cargo puts the host under target/release/sausage, unless CARGO_TARGET_DIR selects another directory. Use that binary with init WORLD --userland BUNDLE, then run WORLD. Use the repository's build slots and shared executor target directory for agent validation builds.

Choose $HOME/sausage-v3, not the live v2 ~/world. For the actual cutover, let v2 finish its work before you stop it. Do not run two production services. This guide does not stop or replace the deployed service. The host creates this layout:

Host path Purpose
sausage-v3/world/ The world root /, including programs and editable source.
sausage-v3/host/ Private host state, including credentials, harness storage, undo, and builder caches.

The host creates /run in memory. It generates /proc at runtime. It copies a bundle only when WORLD/world does not exist. A later run does not replace the world's tools, source, or config. Keep both world/ and host/ for recovery. Provider API keys in /etc/sausage/config.yaml are the explicit exception to host-only secret storage.

Sources: main.rs, world.rs, flake.nix, and bundle.nu.

Use the chat and stop the host

The chat selects the configured main agent by default. Choose another conversation from the list. Root #1 remains only as history. A main agent rename keeps the same conversation. The page polls for new entries and shows text, collapsed tools, and pending credential requests. Use Send to submit a message and Stop to stop the selected conversation's active turn. The pending-requests banner opens the approval view in the page. The host verifies the passkey assertion. Agents edit /srv/www/index.html. Each request reads the file fresh without a build or restart.

The terminal console remains optional. Each nonempty console line submits a message to the main agent conversation. Assistant text appears on stdout. This is not a shell or the v2 command dispatcher. Text such as quit, help, and core.restart has no special host meaning. Ask the agent to execute world commands when you need tools.

Press Ctrl-C to stop the host. SIGTERM also requests orderly shutdown. EOF only detaches the console input. It does not stop the host. The web chat remains available after console input closes.

After a crash, repeat the same start command with the same world folder. The host opens host/harness.sqlite, restores extensions, and resumes durable work. It does not replay the historical transcript into the console. It cannot restore ordinary process memory or reverse completed network effects. There is no automatic restart supervisor in this guide.

Sources: input.rs, main.ts, and harness/mod.rs.

Reach the chat and approval pages

The world web server listens on port 8080. The host opens no separate port. From the laptop, create one SSH tunnel:

ssh -N -L 8080:127.0.0.1:8080 citadel.vpn.surma.technology

Open http://localhost:8080. The first-passkey notice opens the Passkeys view. The host derives the passkey RP ID and origin from the browser request Origin header. For direct VPN access, passkeys need HTTPS because non-localhost HTTP lacks a secure context. Passkeys registered for localhost do not transfer to a production hostname. Do not expose the server to other people before the first registration.

Register, create an item, enter a secret

  1. Select Register your first passkey in the chat notice and choose a label.
  2. Complete the browser's passkey ceremony.
  3. Ask the agent to file an item creation request through POST /cred/requests.
  4. Select the request in the pending-requests banner and approve it with your passkey.
  5. Read the item's public metadata to find its item ID and stable field ID.
  6. Select Enter a secret, then select the item's secret field.
  7. Enter the secret in the blank form and complete its passkey ceremony.

Until the first passkey exists, D53 permits unlocked decisions and first registration. After registration, decisions require a passkey. Additional registrations require an existing passkey. Secret entry requires a passkey even during bootstrap. The browser encrypts the secret before submission. The world never receives its plaintext. Do not paste secrets into chat or world files.

There is no item editor or credential dashboard yet. Item creation uses kind: "edit" and a stable request key. Its request has item: null, base_version: null, and a change array. The creation operation is {op: "create_item", config: ITEM}. ITEM must contain the complete public item configuration, including its fields, rules, and transformation. Further edits add fields and rules. Inspect the typed operations before you construct those edits. Public reads use /cred/items and /cred/items/ITEM_ID. They do not return stored secret values.

Approve a request

The agent or world UI obtains pending requests from /harness/credential-requests or /cred/requests?state=pending. Follow the /#approve/ID link on the web origin through the tunnel. Review the stored summary and public details, choose limits when applicable, then approve or deny. The world can file a request. It cannot approve its own request.

For a manual Chrome check against a fresh world on port 8080, run:

node userland/web/browser-check.mjs

The script uses agent-browser and a CDP virtual authenticator. It registers a passkey and approves an item creation request. Set AGENT_BROWSER_EXECUTABLE_PATH if Chrome needs an explicit path.

Sources: host API, credential views, edits.rs, and browser cryptography.

Models

The world config is /etc/sausage/config.yaml, at WORLD/world/etc/sausage/config.yaml on the host. It contains providers, main_agent_name, and the global prompt. It contains no roster. Definitions live in /etc/sausage/roster/<name>.md. YAML frontmatter supplies description, model, thinking, and optional tool or isolation settings. The body supplies the role prompt. The filename supplies the name. Project definitions use <repo>/.sausage/roster/*.md. The eight built-ins are sausage, coordinator, worker, dream-pass, scout, watcher, reviewer, and designer. Every agent receives the global prompt before its role prompt. The shared prompt explains how agents inspect their own model and the live harness state.

Run sausage init. Set providers.<name>.api_key in the config, then run sausage run. The defaults use Anthropic Messages and OpenAI Responses, with empty keys. The config also shows a commented OpenAI-compatible Chat Completions endpoint. Each provider has type, base_url, api_key, and models. References use <provider>/<model>. An optional catalog selects the models.dev provider ID. It defaults to the configured provider's name. The host refreshes models.dev metadata at startup and caches it in host/models-dev.json. If the refresh fails, the host uses that cache or the packaged snapshot. Model objects can override context_window, max_tokens, reasoning, and cost. Cost fields are input, output, cache_read, and cache_write, in USD per million tokens. An unknown model needs all metadata fields. Its error names the missing fields and config path. Pi-ai stores estimated call costs in assistant usage.

The host and chat start with empty keys. A model request with missing settings names the config file. Provider API keys in the config are intentionally visible to agents. Do not print or commit them. Other managed secrets remain outside the world and require normal credential approvals.

Edit main_agent_name to select a definition. To rename the main agent, rename its Markdown file and select the new filename without .md. The same conversation adopts the new name, model, and role prompt. The default is sausage, with anthropic/claude-opus-5-5 and high thinking. Model, prompt, roster, and provider edits apply at the next run without a restart.

Sources: credentials.rs, main.ts, and models.

Change extensions

The world checkout is /src/sausage. The loader follows its main branch, not the host checkout's v3 branch. Each extension lives at extensions/NAME/server.ts inside that checkout. Commit extension edits to main with world git. A saved, uncommitted edit does not reload an extension. Use --no-gpg-sign for commits.

The loader imports a detached snapshot under /run/sausage/extensions/COMMIT. It validates the entire candidate before it replaces the live registry. A broken candidate leaves the previous good commit active. An active tool call retains its implementation. The next call uses the new implementation.

Read /proc/extensions through the agent's file tools to inspect current, good, and error. To restore the last good branch ref, ask the agent to execute:

extensions-revert

The command preserves editable files. It changes refs/heads/main, which triggers the normal reload. At startup, the loader tries the tip and then the last good commit before it resumes work.

Source: extensions and extensions.ts.

Everyday tools inside the world

These are world programs, not console commands. Ask the agent to run them.

Tool Use and limits
cargo Build, run, and test Rust programs. The host copies the workspace through the VFS, fetches dependencies, then builds offline in namespaces. It publishes WASI artifacts into the world. Other targets are unsupported.
box Build non-Rust projects or run browser tests in disposable containers. The default service is citadel.vpn.surma.technology:8093. It requires network access and service availability. Remove boxes after use.
undo Inspect tags and restore filesystem changes. Later edits can conflict. Network effects and native builds are not reversible. Retention is seven days or 4 GiB of unique content.
systemctl Control world init services, not Citadel's systemd. Persistent units live under /etc/systemd/system. Init observes edits without daemon reload.
journalctl Read world service logs under /var/log/journal. Each unit retains a 1 MiB log and one rotated file.
git Manage world repositories. HTTPS credential rules can inject authorization through the host HTTP hook. SSH remotes use host signing.
ssh Execute a remote command without a PTY. Public identities and signatures come from the host. Private keys never enter the world. Host keys live in the world's known_hosts file.

Init starts when the host starts the runtime, if /bin/init.wasm exists. Transient units under /run/systemd/transient disappear after restart. A missing credential grant causes a pending host request, not an automatic approval. A changed SSH host key fails even with the accept-new policy.

Command reference and implementation evidence: builder, box, undo, init, git, and ssh.

Build a world UI with the host API

World programs call http://host.sausage through standard WASI HTTP. The world web server forwards /api/PATH to http://host.sausage/PATH through WASI HTTP. It streams response chunks instead of buffering the entire NDJSON body. The bootstrap page polls entries?after= once per second rather than using a persistent stream. Use the credential views in the web UI. They call only /api/cred/... JSON routes.

Method and route Result or request
GET /harness/conversations Conversation records and main_agent: {name, conversationId} for the default selection.
GET /harness/conversations/ID/entries?after=0 {entries:[...]} in ascending order.
GET /harness/conversations/ID/stream?after=0 NDJSON entries, replay first, then live commits.
POST /harness/conversations/ID/submit {content:"text",requestId:"stable-id"} returns 202 and a submission ID. Reuse the request ID for retries.
POST /harness/conversations/ID/stop Stops active turns and owned work. Queued input remains.
GET /harness/events?after=0 NDJSON events, replay first, then live commits.
POST /harness/extensions/NAME/actions/ACTION {args:{...},conversationId?:number} invokes an exported user action.
GET /harness/models Public model catalog without authentication data.
GET /harness/extensions Loaded extension status.
GET /harness/credential-requests Pending conversation requests with /#approve/ID links.

The after cursor is exclusive. Reconnect with the last record ID. Streams close when the client disconnects or the harness stops. Do not render a stream as one JSON response. An unavailable harness returns 503. Invalid input returns 400. Missing resources return 404.

Source: host API contract, harness-api.ts, and harness/http.rs.

Known gaps before cutover

  • Laptop access, passkey ceremonies, secret entry, and browser approval remain to check with Surma.
  • Startup checks use fake provider credentials. They do not verify a real model session or external credential injection.
  • The bootstrap chat is deliberately plain. There is no item editor or credential dashboard.
  • The console has no shell commands, transcript navigation, or special quit command.
  • sausage sh requires -c. Its interactive shell is not implemented.
  • This guide does not migrate a v2 world. Keep the live v2 folder untouched.

A successful local start does not verify external services or the production passkey origin.