294 lines
17 KiB
Markdown
294 lines
17 KiB
Markdown
# 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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`](../host/src/main.rs), [`world.rs`](../host/src/world.rs),
|
|
[`flake.nix`](../flake.nix), and [`bundle.nu`](../userland/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`](../host/src/harness/input.rs), [`main.ts`](../host/js/main.ts),
|
|
and [`harness/mod.rs`](../host/src/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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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`](../host/src/api/README.md), [`credential views`](../userland/web/credentials-view.js),
|
|
[`edits.rs`](../host/src/cred/edits.rs), and [`browser cryptography`](../userland/web/credentials.js).
|
|
|
|
## 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`](../host/src/harness/credentials.rs), [`main.ts`](../host/js/main.ts),
|
|
and [`models`](../extensions/models/README.md).
|
|
|
|
## 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:
|
|
|
|
```sh
|
|
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`](../extensions/README.md) and [`extensions.ts`](../host/js/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`](../host/src/builder/README.md), [`box`](../userland/box/README.md),
|
|
[`undo`](../userland/undo/README.md), [`init`](../userland/init/README.md),
|
|
[`git`](../userland/git/README.md), and [`ssh`](../userland/ssh/README.md).
|
|
|
|
## 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`](../host/src/api/README.md),
|
|
[`harness-api.ts`](../host/js/harness-api.ts), and [`harness/http.rs`](../host/src/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.
|