15 KiB
Run Sausage v3 on Citadel
Use the bootstrap web chat for conversations. Credential approvals remain on separate host pages.
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. The package supplies the native Rust builder and its tools.
The package entry point is:
nix run . -- run "$HOME/sausage-v3" </dev/null
The package sets SAUSAGE_USERLAND to its assembled bundle by default.
You can override it with --userland BUNDLE or the environment variable.
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 and creates target/userland/bundle by default.
It snapshots committed source, not uncommitted edits.
The source archive includes the complete agents/, skills/, and extensions/ directories.
The world copier preserves symlinks, including dangling links in the source tree.
A fresh development world needs no manual source copy.
The bundle includes /bin/chat.wasm and an enabled /etc/systemd/system/chat.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 chat 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 the same run WORLD --userland BUNDLE arguments.
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 --userland does not update an existing world.
Even a failed initial start can create WORLD/world. Do not assume another start will seed it.
Keep both world/ and host/ for recovery. Never copy secrets into the world.
Sources: main.rs, world.rs,
flake.nix, and bundle.nu.
Use the chat and stop the host
The chat selects the root conversation by default. Choose another conversation from the list.
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 chat never approves credential requests. Its links open the host's approval pages.
Agents can change the chat source at /src/sausage/userland/chat and rebuild it later.
The terminal console remains optional. Each nonempty console line submits a message to the durable root 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 chat listens on port 8080. The host listens on 127.0.0.1:8090 by default.
Port 8081 on Citadel belongs to Traefik.
From the laptop, create one SSH tunnel with both forwards:
ssh -N -L 8080:127.0.0.1:8080 -L 8090:127.0.0.1:8090 citadel.vpn.surma.technology
Open http://localhost:8080 for chat and http://localhost:8090/register for the first passkey.
Keep laptop port 8090 to match the host's configured browser origin.
The defaults are --api-rp-id localhost and --api-origin http://localhost:8090.
If the host port must change, use --api-listen and adjust the tunnel's remote port.
For direct VPN access, the default loopback listener is insufficient.
You need a reachable listener, HTTPS termination, and matching RP ID and origin options.
Plain HTTP on the VPN hostname does not provide the browser's secure context for passkeys.
The SSH tunnel avoids that extra setup for localhost tests.
To check with Surma: the laptop tunnel, browser passkey registration, secret entry, and request approval.
The planned production origin is https://sausage.apps.surma.technology, with RP ID sausage.apps.surma.technology.
Passkeys registered for localhost do not transfer to that RP ID. Plan a separate production registration.
Do not expose the listener before the first registration to people who must not approve requests.
Register, create an item, enter a secret
- Open
/registerand choose a label for the first passkey. - Complete the browser's passkey ceremony.
- Ask the agent to file an item creation request through
POST /cred/requests. - Review the request on
/approve/REQUEST_IDand approve it with your passkey. - Read the item's public metadata to find its item ID and stable field ID.
- Open
/entry/ITEM_ID/FIELD_IDon the host origin. - Enter the secret in the blank host 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 approval link on the host 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.
Sources: host API, pages.rs,
edits.rs, and app.js.
Models
The host requires an llm-proxy credential before the harness starts.
On an unlocked new store, it can bootstrap that item from these host environment variables:
PI_PROXY_BASE_URL: the proxy base URL, without the appended/v1.PI_PROXY_API_KEY: the proxy API key.
Supply them through your host secret mechanism. Do not print them or commit them. Do not put either value in a world configuration file. Once the item exists, the host uses it rather than replacing it from the environment. The bootstrap is not a general credential import interface for a locked store.
World model families live at /src/sausage/agents/models.json.
Agent definitions live in /src/sausage/agents/*.md.
Project overlays live in <repo>/.sausage/agents/.
The models extension reads these files at each run boundary. Edits affect the next run without a restart.
An explicit model override can take precedence over a family.
The initial console root has a host-defined model. Do not assume a family edit changes every existing conversation.
All real model requests retain the host proxy transport.
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 listener on port 8090 does not expose harness routes.
The seeded chat server forwards /api/... to /harness/... through WASI HTTP.
The bootstrap page polls entries?after= once per second rather than using a persistent stream.
Keep credential approval and secret entry on host pages.
| Method and route | Result or request |
|---|---|
GET /harness/conversations |
Conversation records, titles, ownership, ancestry, and state. |
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/families |
World model families. |
GET /harness/extensions |
Loaded extension status. |
GET /harness/credential-requests |
Pending conversation requests with host approval 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 proxy credentials. They do not verify a real model session or external credential injection.
- A usable
llm-proxycredential is mandatory even before the first chat. - 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 shrequires-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.