From f8d04192c9b1b49b5e2f4b90c2dd1562e1f47b21 Mon Sep 17 00:00:00 2001 From: Surma Date: Sat, 7 Nov 2026 20:54:26 +0000 Subject: [PATCH 1/2] Document v3 console operation and cutover blockers --- README.md | 3 + docs/RUNNING.md | 260 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 docs/RUNNING.md diff --git a/README.md b/README.md index 5337d93..c23ecd4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ You can see how the sausage is made. +**v3:** Read [Run Sausage v3 on Citadel](docs/RUNNING.md) for the console, approvals, and current bootstrap gaps. +The rest of this README describes v2. + Sausage is a world of small WASI programs ("plugins") that you grow by talking to it. A plugin is a `.wasm` file with JSON-RPC methods. Each call to a plugin starts a new process. The agent is a plugin too: it writes new diff --git a/docs/RUNNING.md b/docs/RUNNING.md new file mode 100644 index 0000000..bbd246e --- /dev/null +++ b/docs/RUNNING.md @@ -0,0 +1,260 @@ +# Run Sausage v3 on Citadel + +The console is the UI. The browser handles credential approvals, not chat. + +**TL;DR:** Use a separate v3 world and the release host. Keep its complete folder between starts. +The current Nix package does not assemble a complete default world. A fresh checkout is not yet a complete cutover path. +Resolve the bootstrap gaps below before production cutover. +The rest of the root README describes v2. + +## 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: + +```sh +nix run . -- run "$HOME/sausage-v3" --userland "$SAUSAGE_USERLAND" +``` + +`SAUSAGE_USERLAND` must name an assembled bundle with `bin/` and `src/` directories. +It is not a world folder. The CLI also accepts this variable without `--userland`. +The package does not set it for you. + +The source-tree bundle builder is: + +```sh +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. +**Current gaps:** its source archive omits `agents/`, including `agents/models.json`. +It also includes a dangling esbuild symlink at `userland/esbuild/source/scripts/browser/esbuild.wasm.bagel`. +The world copier dereferences symlinks, so this source tree fails initial seeding with `No such file or directory`. +Do not treat the current bundle output as a verified startup path. + +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 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`](../host/src/main.rs), [`world.rs`](../host/src/world.rs), +[`flake.nix`](../flake.nix), and [`bundle.nu`](../userland/bundle.nu). + +## Use and stop the console + +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. +Keep the foreground terminal available if you want to continue chat. + +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. +The existing flake loop still contains v2 assumptions. Do not use its no-argument mode for v3. + +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 approval pages + +The host listens on `127.0.0.1:8081` by default. +If another service owns port 8081, choose another host port with `--api-listen`. +Adjust the tunnel's remote port, but retain laptop port 8081 for the default origin. +From the laptop, create an SSH tunnel to Citadel: + +```sh +ssh -N -L 8081:127.0.0.1:8081 citadel.vpn.surma.technology +``` + +Open **http://localhost:8081/register** on the laptop. +Keep the laptop port at 8081 to match the configured browser origin. +The defaults are `--api-rp-id localhost` and `--api-origin http://localhost:8081`. + +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. +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 + +1. Open `/register` and choose a label for the first passkey. +2. Complete the browser's passkey ceremony. +3. Ask the agent to file an item creation request through `POST /cred/requests`. +4. Review the request on `/approve/REQUEST_ID` and approve it with your passkey. +5. Read the item's public metadata to find its item ID and stable field ID. +6. Open `/entry/ITEM_ID/FIELD_ID` on the host origin. +7. 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`](../host/src/api/README.md), [`pages.rs`](../host/src/api/pages.rs), +[`edits.rs`](../host/src/cred/edits.rs), and [`app.js`](../host/src/api/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 `/.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`](../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 listener on port 8081 does **not** expose harness routes. +A browser UI needs a world server that forwards these requests. +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`](../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 + +- The Nix package wraps the host and builder tools but does not ship or select the default userland bundle. +- The bundle source archive omits `agents/`. A fresh bundle lacks model families and built-in agent definitions. +- The source archive includes a dangling esbuild symlink. The world copier cannot seed that tree. +- Fresh-world validation currently stops at that copy failure. The package start and full operational sequence remain unverified. +- A usable `llm-proxy` credential is mandatory even before the first chat. +- The browser has approval, passkey, and secret-entry pages, but no item editor, dashboard, or chat UI. +- The console has no shell commands, transcript navigation, or special quit command. +- `sausage sh` requires `-c`. Its interactive shell is not implemented. +- The flake's runtime-toolchain check and loop comments still describe v2 behavior. +- This guide does not migrate a v2 world. Keep the live v2 folder untouched. + +Do not treat a successful host compilation as evidence that these bootstrap gaps disappeared. From 63ac29d02677176cdd5d35ee5897931c44049a14 Mon Sep 17 00:00:00 2001 From: Surma Date: Sun, 8 Nov 2026 03:08:38 +0000 Subject: [PATCH 2/2] Update v3 guide after fresh development bundle checks --- docs/RUNNING.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/RUNNING.md b/docs/RUNNING.md index bbd246e..182520a 100644 --- a/docs/RUNNING.md +++ b/docs/RUNNING.md @@ -3,8 +3,8 @@ The console is the UI. The browser handles credential approvals, not chat. **TL;DR:** Use a separate v3 world and the release host. Keep its complete folder between starts. -The current Nix package does not assemble a complete default world. A fresh checkout is not yet a complete cutover path. -Resolve the bootstrap gaps below before production cutover. +The development bundle now seeds a complete source tree, including agents, skills, and extensions. +The Nix package does not yet supply that bundle. Verify the remaining cutover steps below before production use. The rest of the root README describes v2. ## Build and start @@ -30,10 +30,9 @@ 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. -**Current gaps:** its source archive omits `agents/`, including `agents/models.json`. -It also includes a dangling esbuild symlink at `userland/esbuild/source/scripts/browser/esbuild.wasm.bagel`. -The world copier dereferences symlinks, so this source tree fails initial seeding with `No such file or directory`. -Do not treat the current bundle output as a verified startup path. +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 no longer needs a manual source copy. For a release host from the checkout: @@ -104,6 +103,7 @@ 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. @@ -247,9 +247,9 @@ Source: [`host API contract`](../host/src/api/README.md), ## Known gaps before cutover - The Nix package wraps the host and builder tools but does not ship or select the default userland bundle. -- The bundle source archive omits `agents/`. A fresh bundle lacks model families and built-in agent definitions. -- The source archive includes a dangling esbuild symlink. The world copier cannot seed that tree. -- Fresh-world validation currently stops at that copy failure. The package start and full operational sequence remain unverified. +- The development bundle passes fresh-world startup checks. The Nix package start remains unverified until its bundle integration lands. +- 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-proxy` credential is mandatory even before the first chat. - The browser has approval, passkey, and secret-entry pages, but no item editor, dashboard, or chat UI. - The console has no shell commands, transcript navigation, or special quit command. @@ -257,4 +257,4 @@ Source: [`host API contract`](../host/src/api/README.md), - The flake's runtime-toolchain check and loop comments still describe v2 behavior. - This guide does not migrate a v2 world. Keep the live v2 folder untouched. -Do not treat a successful host compilation as evidence that these bootstrap gaps disappeared. +A successful development start does not verify the Nix package, external services, or the production passkey origin.