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..182520a --- /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 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 + +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. +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: + +```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. +**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 + +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 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. +- `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. + +A successful development start does not verify the Nix package, external services, or the production passkey origin.