1368 lines
82 KiB
Markdown
1368 lines
82 KiB
Markdown
# TOOLS.md — command and tool reference
|
||
|
||
On-demand reference for agent sessions (conductors, bootstrapping agents,
|
||
reviewers). `AGENTS.md` routes here; this file carries the depth: usage,
|
||
inputs/outputs, exit codes, and safety notes for every entry point.
|
||
|
||
Reading guide: system entry points are `scripts/*.sh` (bash) or invoked via
|
||
`node scripts/mosaic-task.mjs` (node). Host-side helpers under `tools/`
|
||
(tmux messaging, watchers, prose checker) are covered under Tools
|
||
(host-side) below. Every script fails closed — missing
|
||
or invalid configuration/policy refuses the operation with a nonzero exit
|
||
and changes nothing.
|
||
|
||
## Lifecycle
|
||
|
||
| Command | Purpose | Notes |
|
||
|---|---|---|
|
||
| `scripts/bootstrap.sh [--instance NAME]` | Create `~/.config/mosaic-dev/config.json` if absent; with `--instance NAME`, `~/.config/mosaic-NAME/config.json` and the claimed root `~/.mosaic-NAME` | Idempotent; existing config validated, never rewritten. See [Stack instances](#stack-instances---instance-name-row-190) |
|
||
| `scripts/build.sh` | Build the release image | Tag derived from `RELEASE` + pinned pi version |
|
||
| `scripts/hello.sh` | One-shot startup request | Prints model response on stdout |
|
||
| `scripts/verify.sh` | Full gated test | Exit 0 only on exact `MOSAIC_HELLO_OK`; `EXPECTED_MARKER` overrides for negative drills |
|
||
|
||
## Tasks (missions, runs, evidence)
|
||
|
||
| Command | Purpose | Notes |
|
||
|---|---|---|
|
||
| `scripts/run-task.sh run <task.json>` | Execute a task | Immutable run record under `<dataRoot>/runs/` |
|
||
| `scripts/run-task.sh validate <task.json>` | Strict validation | Writes nothing |
|
||
| `node scripts/mosaic-task.mjs show <runId>` | Inspect a run | Full record + snapshots + artifacts |
|
||
| `node scripts/mosaic-task.mjs list` | List runs | task/workspace/session columns |
|
||
| `node scripts/mosaic-task.mjs retry <runId>` | Re-execute a run's snapshot | New run dir; `retriedFrom` lineage recorded |
|
||
| `node scripts/mosaic-task.mjs prune [--keep=N] [--yes]` | Retention | Dry-run default; receipt in `runs/.pruned.log` |
|
||
| `node scripts/mosaic-task.mjs resolve-role <roleFile>` | Validate a role file, version 1 or 2 | Prints `MOSAIC_ROLE_TOOLS` / `MOSAIC_ROLE_NETWORK`, and `MOSAIC_ROLE_CONTRACT` for version 2; config-free |
|
||
|
||
Task fields: `prompt` (required), `mission` (path), `expectExact`,
|
||
`timeoutSeconds` (5–600), `workspace` (`:run` or named), `capabilities.tools`
|
||
(allowlist: read write edit bash grep find ls), `session`,
|
||
`sessionForkFrom` (requires `session`). Mission fields: `objective`,
|
||
`directives[]`, optional governing `capabilities.tools`. Policy: a task may
|
||
narrow a mission's tools, never widen; empty intersection = tool-free run.
|
||
|
||
## Agent (interactive TUI)
|
||
|
||
```bash
|
||
scripts/agent.sh <name> [--mission <file>] [--workspace <ws>] [--session <s>] [--tools <list>]
|
||
```
|
||
|
||
Launches an interactive pi TUI inside the container with the four immutable
|
||
contracts + optional mission + agent identity as its system prompt,
|
||
persistent named session, optional workspace. Exit with `/quit`.
|
||
|
||
A seat role (`agent.json` `role`) binds to `roles/<role>.json` (M18): the
|
||
contract's tools are a ceiling the seat definition or `--tools` may narrow,
|
||
never escalate past. Missing/invalid contract refuses the launch; empty
|
||
intersection = loud tool-free seat. An explicit `MOSAIC_AGENTS_DIR` override
|
||
that cannot resolve the named seat also refuses (#46) — unset the override
|
||
for the M13 plain governed TUI. `--auth <account>` injects
|
||
`auth.<account>.json` (beside the active credential file) as the launch's
|
||
`PI_AUTH_FILE`; a missing/invalid account refuses (M19).
|
||
|
||
For native repository development, opt in with a **leading** `--host-dev`:
|
||
|
||
```bash
|
||
scripts/agent.sh --host-dev darkwing [--fresh] [--check] [--soul FILE] [--constitution FILE] [--user FILE] [--provider NAME] [--model ID] [--thinking LEVEL]
|
||
```
|
||
|
||
This mode delegates to `scripts/agent-host-dev.sh`, uses host Pi and repository
|
||
tools/skills plus the development goal extension, and keeps its own sessions
|
||
under `.pi/state/<name>/`. It uses native Pi authentication and does not run
|
||
container release alignment or apply managed seat role ceilings. It is a host
|
||
development session, not a sandboxed worker. Container-only flags such as
|
||
`--auth`, `--mission`, and `--tools` are rejected in this mode. Omitting
|
||
`--host-dev` retains the existing container lifecycle and policy checks;
|
||
container failures never trigger a host fallback. Darkwing's agent-local shim
|
||
selects host development explicitly. See `agents/darkwing/README.md`.
|
||
|
||
## Auth (credentials)
|
||
|
||
Credential checkpoint over pi's auth model (one `auth.json` keyed by
|
||
provider; resolution order `--api-key` → `auth.json` → env → models.json).
|
||
No credential material is ever printed — provider names, credential types,
|
||
and env var NAMES only.
|
||
|
||
Ownership rule (#48): `~/.pi` is read-only to the stack, permanently. The
|
||
only interaction is the existing read-only container mount of the default
|
||
credential (`PI_AUTH_FILE`, default `~/.pi/agent/auth.json`). Mosaic-managed
|
||
accounts live under the data root: `<dataRoot>/auth/<account>.json`, perms
|
||
0600 (mirroring `scripts/gitea-api.sh` hygiene — loose perms are flagged in
|
||
listings and refused by `--auth`).
|
||
|
||
| Command | Purpose | Notes |
|
||
|---|---|---|
|
||
| `scripts/auth.sh status` | Report both credential sources | Default harness credential (read-only) + mosaic-managed accounts; never prints material |
|
||
| `scripts/auth.sh accounts` | List mosaic-managed accounts | Under the data root; marks the active one; flags non-0600 |
|
||
|
||
`agent.sh --auth <account>` injects `<dataRoot>/auth/<account>.json` as the
|
||
launch's `PI_AUTH_FILE`; missing/symlinked/non-0600 accounts refuse.
|
||
Headless task runs keep the default credential.
|
||
|
||
## Release
|
||
|
||
| Command | Purpose | Notes |
|
||
|---|---|---|
|
||
| `scripts/release.sh package` | Build + tag the release image | Tag: `mosaic-poc-agent:<pi>-r<release>` |
|
||
| `scripts/release.sh activate` | Health gate → atomic pointer swap | `--fault-injection` proves the refusal path |
|
||
| `scripts/release.sh rollback` | Health-gated return to previous | Refuses if image missing |
|
||
| `scripts/release.sh status` | Release, tag, active pointer, log | Safe on empty state |
|
||
| `scripts/release.sh ensure` | Self-determination: align active pointer to `RELEASE` | Fast path restores a missing/mismatched pointer without a gate; slow path packages + health-gates first. Invoked automatically at launch |
|
||
|
||
## Conductor (worker patches)
|
||
|
||
```bash
|
||
scripts/conductor-apply.sh <runId> [--dry-run]
|
||
```
|
||
|
||
Auto-applies a worker's patch under `roles/conductor-policy.json`:
|
||
succeeded run → clean target tree → path allowlist → syntax gates →
|
||
apply → policy suites → attribution commit. Any failure reverts.
|
||
Push is never automatic.
|
||
|
||
## Conductor (landing approved work)
|
||
|
||
```bash
|
||
scripts/land-work.sh <decision-id> [--base refactor] [--dry-run] [--subject TEXT]
|
||
[--attribution LINE] [--business ID] [--project ID]
|
||
```
|
||
|
||
Turns a merge decision the human resolved into one gated local commit on
|
||
`refactor` (#1610). It reads `<dataRoot>/bus/bus.sqlite` read-only through
|
||
`scripts/land-work/read.mjs` and checks that the human picked the raise's
|
||
approval option, that the question names one `work/vikunja-<p>-<t>`, and
|
||
that the task's latest `review.verdict` approves a commit C that the work
|
||
ref in the working repository still points at. It then applies
|
||
`git diff $(merge-base refactor C) C` in a detached worktree, checks the
|
||
patch-id, runs every package suite except `queue` and every
|
||
`scripts/test-*.sh`, and commits. The subject is the approval option's
|
||
text unless `--subject` overrides it, and `--attribution` replaces the last
|
||
line, `Applied-under: scripts/land-work.sh (conductor)`. `--business` and
|
||
`--project` are needed only when the config has more than one business file
|
||
or the business more than one project. The tool prints the line for the PM
|
||
and doesn't send it. `--dry-run` runs every check and the gate and writes
|
||
no ref. A refusal names its reason and exits 2; the reasons are listed at
|
||
the top of the script. The worktree, its `node_modules` link and its Docker
|
||
network are removed on every exit. The gate log directory stays and the
|
||
tool names it. Keep `TMPDIR` short: the gate inherits it, the cli suite
|
||
puts unix sockets under it, and a 46-byte `TMPDIR` failed 8 of those tests.
|
||
Push is a separate act.
|
||
|
||
Since #1631 the gate, its steps and the root-file rule live in
|
||
`packages/seat/src/landing.mjs`, and the decision checks in
|
||
`packages/bus/src/landing.mjs`. The bus host's `land` op uses both to land
|
||
an agent's task on its working repository's `main`, never on `refactor`
|
||
(`packages/cli/README.md`, "Git socket"). Since #1651 the host starts the
|
||
holder's landing run itself ("Landing runs"). A decision raised for one target
|
||
can't land the other. land-work still accepts only a decision the human
|
||
resolved through the CLI.
|
||
|
||
## Maintenance
|
||
|
||
| Command | Purpose | Notes |
|
||
|---|---|---|
|
||
| `scripts/reset.sh` | Delete the data root | Triple-safety-checked (path, symlink, ownership marker) |
|
||
| `scripts/test-config.sh` | Config selftests (no Docker) | 24 cases |
|
||
| `scripts/test-task.sh` | Task selftests + live cases | 90 cases |
|
||
| `scripts/test-release.sh` | Release selftests | 14 cases |
|
||
| `scripts/test-conductor.sh` | Auto-apply selftests (sandboxed) | 17 cases |
|
||
| `scripts/test-auth.sh` | Auth checkpoint selftests (no Docker) | 13 cases |
|
||
| `scripts/test-land-work.sh` | Land-work selftests (sandboxed, fake Docker) | 84 cases |
|
||
| `scripts/gitea-api.sh <METHOD> <path> [body]` | Gitea API helper; reads the JSON credential file or a raw per-seat token file | Token never on argv/stdout |
|
||
|
||
## Seat launch with registration (`scripts/mosaic`)
|
||
|
||
```bash
|
||
scripts/mosaic launch <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH] [--harness NAME] [--repo PATH] [--config PATH] [-- args...]
|
||
scripts/mosaic seat task <seat> <text> [--by NAME] [--layout repo|fleet|unknown] [--config PATH]
|
||
```
|
||
|
||
Runs a seat's `launch.sh` unchanged (arguments after `--` pass through) and
|
||
leaves one registration record at `<dataRoot>/seats/<layout>/<seat>/registration.json`
|
||
that the control board reads: seat, project, task, workspace, tmux session,
|
||
harness, start time, pid. Rewritten on every launch. `seat task` changes the
|
||
task field only. The four repository seats (`agents/*/launch.sh`) register
|
||
themselves through this path unless called with `--check`. Run it by
|
||
path from the checkout. On a pinned host, `mosaic` on PATH is a wrapper
|
||
around a pinned copy's `scripts/mosaic`
|
||
([Stack copies](#stack-copies-scriptsstack-copysh)) and carries the human
|
||
commands; on Jason's host the estate CLI it replaced is `mosaic-old`.
|
||
Queue commands stay on `scripts/mosaic queue` in the canonical checkout,
|
||
because a copy refuses them ([One pinned
|
||
copy](../packages/cli/README.md#one-pinned-copy-row-85-1579)). Exit
|
||
codes: the launch script's own once it runs; before that 1 could not
|
||
start, 2 invalid config or seat, 4 usage. Details and the record's
|
||
fields: `packages/seat/README.md`.
|
||
|
||
## Work queue (`scripts/mosaic queue`)
|
||
|
||
```bash
|
||
scripts/mosaic queue list | show ID | next [SEAT]
|
||
scripts/mosaic queue add|move|release|assign|note|set ... --op ID [--by NAME]
|
||
scripts/mosaic queue verify [--current] | render [--check] | sync | unlock [--check-gate]
|
||
scripts/mosaic queue review request|resolve|abandon|record ... --op ID [--by NAME]
|
||
scripts/mosaic queue review verify-commit ID REF
|
||
scripts/queue-commit.sh -m MSG
|
||
```
|
||
|
||
`docs/plans/queue.json` holds the rows and an append-only log; the table in
|
||
`docs/plans/QUEUE.md` between the `mosaic-queue` markers is rendered from it.
|
||
Canonical checkout only. Every change needs an `--op ID` chosen before the
|
||
first attempt and reused on retry; only an op whose `ok <op> rev N` receipt
|
||
printed is done. The lead commits queue changes with `scripts/queue-commit.sh`;
|
||
the pre-commit guard refuses any other commit that stages the two queue files.
|
||
Moving a row with reviewers to in-review posts one review request comment
|
||
on its issue as the acting seat, with the seat's own token file in
|
||
`MOSAIC_GITEA_CREDENTIAL_FILE`; reviewers then run `review record`.
|
||
Exit codes: `0` ok · `1` failed · `2` invalid or refused · `3` uncertain,
|
||
retry the same op (for a review request, the retry sends nothing; resolve or
|
||
abandon it) · `4` usage. Details: `packages/queue/README.md`.
|
||
|
||
## Business files and role instances (`scripts/mosaic business`)
|
||
|
||
```bash
|
||
scripts/mosaic business validate <business>
|
||
scripts/mosaic business resolve <business> <instance> [--project <id>]
|
||
scripts/mosaic business instances <business> [--json]
|
||
scripts/mosaic business instance add <business> <instance> --definition <def> [--vars <file>] [--launch] --revision <rev>
|
||
scripts/mosaic business instance set <business> <instance> [--definition <def>] [--vars <file>] [--launch | --no-launch] --revision <rev>
|
||
```
|
||
|
||
Reads `businesses/<business>.json` next to the system config
|
||
(`$MOSAIC_CONFIG` or `~/.config/mosaic-dev/`), the version 2 role files in
|
||
`roles/` (or `$MOSAIC_ROLES_DIR`) and each project's `.mosaic/project.json`.
|
||
Writes nothing. `validate` checks the whole business, stat-checks every
|
||
credential reference without opening it and prints one digest per role
|
||
instance. `resolve` prints one instance's merged variables, their sources,
|
||
its narrowed tools, network and authority, and its credential references.
|
||
The business file must belong to you and not be group or other writable.
|
||
Exit codes: `0` ok · `2` invalid file or credential reference · `3` system
|
||
config problem · `4` usage or a required file missing. Details:
|
||
`packages/business/README.md`.
|
||
|
||
`mosaic credentials status|issue|rotate|revoke|delete` (row 199) goes to
|
||
`packages/cli` and the bus's human-only credential verbs: an agent's own
|
||
token in `<configDir>/secrets/agents/<business>/<agent>/<kind>`, read on
|
||
stdin, never printed. The tester may run `status` and `revoke`. Details:
|
||
`packages/cli/README.md`.
|
||
|
||
`instances` and `instance add|set` (row 165) go to `packages/cli` and the
|
||
bus's human-only `instances` and `instances.set` verbs, so they need a live
|
||
bus host and refuse inside an agent session. `instances` prints the revision,
|
||
each instance and the definitions `add` and `set` accept. `add` and `set`
|
||
write the business file under that revision and log the change with you as
|
||
the actor. They bind no credentials: a definition that declares credentials,
|
||
or a definition change on an instance that holds them, is refused with
|
||
`organization-credentials-out-of-scope` and stays a hand edit. After an add
|
||
or a definition change the output says a bus restart is needed; the restart
|
||
needs Mos's go or Jason's hand, and the command doesn't run it. Exit codes:
|
||
`0` ok · `2` refused, nothing written · `3` human proof refused · `4` usage.
|
||
The Console's Settings page has the same two forms. Details:
|
||
`packages/cli/README.md`.
|
||
|
||
## Inbox, decisions and the bus host (`scripts/mosaic`)
|
||
|
||
```bash
|
||
scripts/mosaic # no argument: the full-screen app
|
||
scripts/mosaic inbox | tasks | agents [--business <id>] [--json]
|
||
scripts/mosaic inbox --read <id>... | --read-all [--business <id>]
|
||
scripts/mosaic decide <decision> <option> [--note <text>] [--yes]
|
||
scripts/mosaic trail <task|decision> [--json]
|
||
scripts/mosaic bus start <business> [--pm] | stop | status [--json]
|
||
scripts/mosaic talk <role> <text> [--wait SECONDS] [--business <id>]
|
||
scripts/mosaic project create <name> [--repo <repo>] | list [--json]
|
||
scripts/mosaic threads [--project <project>] [--json] [--tester]
|
||
scripts/mosaic thread create <project> <name> | open <thread> | read <thread> [--json] [--tester]
|
||
scripts/mosaic talk --thread <thread> <text> [--wait SECONDS | --follow] [--tester]
|
||
scripts/mosaic follow --thread <thread> [--tester]
|
||
scripts/mosaic stop <run>
|
||
scripts/mosaic launches off | on [--business <id>]
|
||
scripts/mosaic launches list [--json]
|
||
scripts/mosaic halt | resume [--business <id>]
|
||
scripts/mosaic processing status [--business <id>] [--json]
|
||
scripts/bus-service.sh [--instance NAME] render [--copy ID [--copies DIR]] [--pm] [--claude PATH]
|
||
scripts/bus-service.sh [--instance NAME] install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH]
|
||
scripts/bus-service.sh [--instance NAME] uninstall | status <business>
|
||
```
|
||
|
||
The instance processing switch is separate from per-business launches off/on.
|
||
`mosaic halt` durably stops managed processing across every business in the
|
||
selected configuration's data root. The bus stays up for reads and human
|
||
recovery. `mosaic processing status --json` reports settled, pending or unknown
|
||
stops; `mosaic resume` checks the current generation and refuses pending stops.
|
||
Only the existing human proof can write it, independently of channel settings.
|
||
A missing/invalid state is never off: use halt, then resume; an unsafe control
|
||
parent needs operator repair. Unknown task/remote outcomes are not retried.
|
||
Native diagnostic seats remain available. First activation must stop old
|
||
processing before trusted initialization and align the image/read-only control
|
||
mount. This is a separate approved window, never a build step.
|
||
|
||
The human commands read and resolve through the broker's human transport
|
||
(`packages/bus/src/human-cli.mjs`), never the SQLite file. They refuse inside
|
||
an agent run, except the forms that take `--tester` (rows 110, 175 and 184), which
|
||
run as the business's tester; `packages/cli/README.md` lists them. `bus start` is the host the systemd user unit
|
||
`mosaic-bus@<business>` runs. It boots the broker, starts the Discord
|
||
notifier, and needs `<dataRoot>/notify/<business>/notify.json`. That
|
||
directory must be 0700: it also holds the notifier's journal `sent.jsonl`
|
||
(0600, never a symlink). On open the notifier copies a torn final line to
|
||
`torn-<UTC stamp>.bin` and truncates the journal to its last newline; a
|
||
malformed complete line refuses with exit 3.
|
||
|
||
`bus-service.sh` pins the Claude Code binary managed sessions run as the
|
||
unit's `MOSAIC_CLAUDE_BIN`, from `--claude PATH` or else `command -v claude`
|
||
(`packages/cli/README.md`, "Unit").
|
||
|
||
`bus start` also serves the session launcher (slice 1 S6): a PM session's
|
||
`launch` tool starts role sessions through it, each a runner under its own
|
||
PID namespace with a generated bundle, and `--pm` launches the business's
|
||
`launch.by` instance at start. A host that starts ends the launches a
|
||
crashed host left as `host-lost` and refuses with exit 3 on a
|
||
`sessions.json` it can't read. Every 2 s it also starts each launch-block
|
||
instance that holds an approved, unused merge decision, once per decision,
|
||
with a landing turn that calls `land_work`; a holder run that started
|
||
before the approval is stopped between turns first (`land-relaunch` in the
|
||
launch log; row 156, cli README "Landing runs"). `talk` sends a REQUEST and prints only the
|
||
reply to it, marking only that read (polls every 2 s, `--wait` default 900;
|
||
`0` only sends); other messages stay unread for `mosaic inbox`, and
|
||
`--follow` and `follow` print and read everything. `inbox --read <id>...`
|
||
marks unread messages read by the ids `inbox` prints, and `--read-all` marks
|
||
every one it lists (row 150); the cli README, "Human commands", says how
|
||
refusals print. When no run holds the
|
||
role, a line after `sent` says whether `launch.by` was told, or that the role
|
||
isn't in the launch block and `launch.by` can't launch it (row 152); the cli
|
||
README, "Human commands", lists the lines. `stop` sends SIGTERM to
|
||
a run's runner after checking it in
|
||
`sessions.json`. `launches off|on` revokes and restores `role.launch` for
|
||
the business; running sessions keep running. `launches list` reads
|
||
`sessions.json` and needs no broker. `talk`, `stop` and `launches off|on`
|
||
refuse inside an agent run.
|
||
With no argument on a terminal, `scripts/mosaic` opens the full-screen app:
|
||
Inbox, Tasks, Agents, Trail and Help, deciding through the same flow and
|
||
strings as `decide`. In an agent run it exits 3; without a terminal on
|
||
stdin and stdout it exits 4 with the usage.
|
||
Exit codes: `0` ok · `1` failed or outcome unknown · `2` invalid input ·
|
||
`3` refused or config problem · `4` usage. Details: `packages/cli/README.md`.
|
||
|
||
## Stack instances (`--instance NAME`, row 190)
|
||
|
||
Several stacks can run on one host, each with its own config, data root,
|
||
units, Console port and CLI wrapper. The default instance is today's stack
|
||
and keeps every name it had. A named instance is created once:
|
||
|
||
```
|
||
scripts/bootstrap.sh --instance NAME
|
||
```
|
||
|
||
NAME matches `^[a-z][a-z0-9]{1,15}$`; anything else exits 4
|
||
(`instance-name-invalid`), and `--instance dev` is the default. Bootstrap
|
||
writes `~/.config/mosaic-NAME/config.json` (0644) and claims `~/.mosaic-NAME`
|
||
with the marker `instance.json`, `{"instanceVersion":1,"name":"NAME"}`. It
|
||
creates either directory 0700; one that already exists keeps its mode. It refuses with exit 3 and writes
|
||
nothing when the root exists, is not empty and holds no marker naming NAME.
|
||
A second run changes nothing. `~` is the home of the OS user that runs the
|
||
instance.
|
||
|
||
`--instance NAME` is taken, once and in any position, by `bootstrap.sh`,
|
||
`bus-service.sh`, `webui-service.sh` (all but `copy`), `discord-service.sh`,
|
||
`stack-copy.sh` (`wrapper` and `check`) and `stack-release.sh`. Copies serve
|
||
every instance, so both `copy` verbs refuse it with exit 4. Inside a process the config
|
||
travels as `MOSAIC_CONFIG`. Selection, checked before any read or write:
|
||
|
||
| Given | Config | Names |
|
||
|---|---|---|
|
||
| neither | `~/.config/mosaic-dev/config.json` | the default's |
|
||
| `MOSAIC_CONFIG` only | that file | the default's |
|
||
| `--instance NAME` only | `~/.config/mosaic-NAME/config.json` | NAME's |
|
||
| both | must be one file (a symlink to it agrees), else exit 4 `instance-config-mismatch` | NAME's |
|
||
|
||
Every selection checks the data root's marker. A named instance whose root
|
||
lacks its own marker refuses `instance-root-unclaimed`; a root whose marker
|
||
names another instance refuses `instance-root-mismatch`, also when a config
|
||
reaches it with no flag. Both exit 3 before anything is read or changed. A
|
||
missing config exits 3 with the bootstrap command that creates it.
|
||
|
||
| | Default | Instance NAME |
|
||
|---|---|---|
|
||
| bus unit | `[email protected]` | `[email protected]` |
|
||
| Console unit | `mosaic-webui.service` | `mosaic-webui-NAME.service` |
|
||
| Discord unit | `[email protected]` | `[email protected]` |
|
||
| CLI wrapper | `$BIN/mosaic` (Q81 marker) | `$BIN/mosaic-NAME`, marker `# mosaic-cli wrapper, instance NAME (row 190)`, exports `MOSAIC_CONFIG` |
|
||
| bus session env | `%h/.config/mosaic-dev/secrets/claude-session.env` | `~/.config/mosaic-NAME/secrets/claude-session.env` |
|
||
| Console port | `--port`, 7332 when absent | `--port` required; 7330, 7332 and 7333 exit 4 `port-reserved` |
|
||
|
||
A named instance's units add `Environment=MOSAIC_CONFIG=<its config>` and
|
||
` [NAME]` after the Description, and the Console unit's comment on the
|
||
variables it sets names `MOSAIC_CONFIG`. The default's renders are
|
||
byte-identical to the templates' renders before row 190. `stack-copy.sh
|
||
wrapper --instance NAME` creates `$BIN/mosaic-NAME` or replaces one that
|
||
carries NAME's marker, and refuses any other file. `check --instance NAME`
|
||
needs `--business ID`, and fails with `CLI: <copy> (MOSAIC_CONFIG mismatch)`
|
||
when the wrapper does not export NAME's config.
|
||
|
||
For every instance, a Console `--port` or `--tester-port` that another
|
||
installed Console unit in the unit directory already takes refuses
|
||
`port-claimed`, naming that unit: `webui-service.sh install` exits 1, and
|
||
`stack-release.sh activate` and `rollback` exit 3 before anything stops
|
||
(`--plan` lists it). A unit with no `--port` holds 7332.
|
||
|
||
`stack-release.sh --instance NAME` reads only NAME's pointer, log, store and
|
||
backups. Its busy check sees only NAME's bus units. Its drop-in scan adds
|
||
`mosaic-bus-NAME@*.service.d` and the shared `mosaic-bus-.service.d`. Its
|
||
rollback set holds NAME's two unit files, their own `.d` directories and
|
||
`$BIN/mosaic-NAME`; it never captures or restores the shared prefix
|
||
directories (`mosaic-bus-.service.d`, `mosaic-webui-.service.d`,
|
||
`mosaic-discord-.service.d`). The live-session guard refuses a path inside
|
||
any named instance's config directory or `~/.mosaic-NAME`, even when that
|
||
instance's config is unreadable.
|
||
|
||
All instances run under one OS user for now. That user's processes can
|
||
read and control every instance; a separate user per instance is row 195.
|
||
A real second instance on this host is a Q173 window, and tunnel or Traefik
|
||
exposure of a named Console waits for Jason after Q175 SSO.
|
||
|
||
## Stack copies (`scripts/stack-copy.sh`)
|
||
|
||
```
|
||
scripts/stack-copy.sh copy COMMIT [--copies DIR] [--allow-worktree]
|
||
scripts/stack-copy.sh copy --bundle FILE SHA [--copies DIR]
|
||
scripts/stack-copy.sh wrapper ID [--instance NAME] [--copies DIR] [--bin DIR] [--create]
|
||
scripts/stack-copy.sh check [--instance NAME] [--copies DIR] [--bin DIR] [--business ID] [--units user|external]
|
||
```
|
||
|
||
One read-only tree supplies the bus, Console and human CLI (row 85,
|
||
#1579). Copies default to `${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies/<full id>`.
|
||
`copy` requires a canonical checkout (.git directory); --allow-worktree
|
||
is a test override. COMMIT must be reachable from both local refactor and
|
||
origin/refactor. It archives the runtime/import closure, adapters, roles,
|
||
`tools/unslop-hook` (the unslop check both harness modules mount, row 66),
|
||
RELEASE, package.json and package-lock.json; refuses missing manifests;
|
||
runs npm ci inside the temporary copy; removes write permissions; and
|
||
renames beside the target. A published existing copy stays unchanged.
|
||
Dependencies use the exact lock and carry Pi's installed lock record.
|
||
Failure publishes nothing and removes the temporary tree.
|
||
|
||
`copy --bundle FILE SHA` (row 232) is for a host with no refactor checkout.
|
||
SHA is a full 40-character id. The bundle must pass `git bundle verify`,
|
||
carry `refs/heads/refactor`, and reach SHA from it; each miss refuses and
|
||
writes nothing. The copy is then made from SHA as `copy` makes it, and the
|
||
script prints `release:` with the RELEASE value at SHA and `bundle sha256:`
|
||
with the bundle's digest. That pair is the release identity on such a host.
|
||
Without `--bundle`, `copy`'s reachability rule is unchanged.
|
||
|
||
`wrapper` is a live switching step, reserved for Jason or Mos. It accepts
|
||
only an existing copy and a regular non-symlink mosaic wrapper with the
|
||
Q81 marker on line 2 and its exact lease line to mosaic-old. It preserves
|
||
both lines, adds comments naming the copy root and Q81's mosaic-old lease
|
||
probe, and atomically replaces the wrapper's final exec with the
|
||
copy's scripts/mosaic. --bin is a fixture override; the default is
|
||
~/.npm-global/bin. Neither the Q81 worktree nor mosaic-old is removed.
|
||
`wrapper ID --create` (row 232) writes the first wrapper on a new host, the
|
||
same file `wrapper` writes. It refuses when anything is at the wrapper path,
|
||
a symlink included, and never replaces it. Without `--create`, a missing
|
||
wrapper still refuses.
|
||
`check` changes nothing: it reads effective systemd WorkingDirectory and
|
||
ExecStart (including drop-ins) for the bus and Console, plus the wrapper
|
||
exec. It prints all three paths and succeeds only on the same existing
|
||
copy; absent/failed reads and a mismatched ExecStart fail. Business defaults
|
||
to mosaic-stack. `check --units external` (row 232) reads root's system units
|
||
with `systemctl show`, without `--user`. Queue work continues on the canonical scripts/mosaic.
|
||
|
||
Bus render/install accept --copy ID, --copies DIR, --pm and the unchanged
|
||
--claude pin. Without --copy the rendered unit stays the checkout unit.
|
||
--pm adds the PM startup; a refusal exits 3, and RestartPreventExitStatus
|
||
prevents a retry. Install refuses any busy bus instance, even with
|
||
--no-reload; a failed state/drop-in read also refuses. Any template or
|
||
instance drop-in, or inherited mosaic-.service.d/ or service.d/ drop-in,
|
||
assigning ExecStart or WorkingDirectory refuses,
|
||
naming the file. Status reads effective roots, --pm and DropInPaths,
|
||
reports checkout or copy identity and refactor drift, and writes nothing.
|
||
|
||
### Pinned stack update
|
||
|
||
```
|
||
scripts/stack-release.sh adopt BUSINESS
|
||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]
|
||
[--proof-header NAME --proof-file PATH | --no-proof | --sso | --no-sso] [--send | --no-send] [--timeout S] [--plan]
|
||
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
|
||
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||
scripts/stack-release.sh status BUSINESS
|
||
scripts/stack-release.sh confirm ID BUSINESS --units external [--rollback OLD --cause A3|A4|A5] [--timeout S]
|
||
scripts/stack-release.sh revert ID BUSINESS --units external
|
||
scripts/stack-release.sh gate ID BUSINESS --units external --since EPOCH [--timeout S]
|
||
common options: --copies DIR, --bin DIR, --instance NAME, --units user|external
|
||
--events N is the total of the per-table counts the post-backup-events refusal prints: events and
|
||
every table in COUNTED.
|
||
--send needs --sso in the run's Console flags, given or carried. --no-sso needs --sso in the
|
||
pointer's, and takes the Console flags from the rollback set of the activation that turned it on.
|
||
--no-sso drops the pointer's --send, and so does a rollback to a Console without --sso.
|
||
```
|
||
|
||
This is an operator plan, not an action agents run. Both reviews and the
|
||
landing come before it. Mos grants each live run first. A bus restart ends
|
||
running sessions, so run it only when no PM turn is in flight. `--plan` and
|
||
`status` change nothing and need no go.
|
||
|
||
The script keeps `<dataRoot>/state/stack-active.json` (the live copy, business,
|
||
PM flag, claude pin, Console flags, schema and activation) and appends every
|
||
run to `<dataRoot>/state/stack-activation-log.jsonl`: a `started` line, then
|
||
one final line with `result` `ok`, `refused`, `gate-failed`,
|
||
`rollback-failed`, `pm-on-failed` or `pm-on-down`. A refusal exits 3, a gate
|
||
failure that rolled back exits 1, a failed rollback exits 2, usage exits 4. Only one run at a time holds
|
||
`state/stack-release.lock` (`run-locked`).
|
||
|
||
An activation id is the UTC second and the target copy's first 12 characters,
|
||
`20261020T013708Z-89af38a83afa`. A second run in the same second adds `-1`,
|
||
then `-2` up to `-9`, the first one with no rollback set, backup or
|
||
post-attempt store yet, so ids still sort by time. With all ten taken the run
|
||
refuses `capture-failed` before it stops anything. Adopt uses the same rule.
|
||
|
||
1. Once per host, `adopt BUSINESS` records the running stack as the pointer:
|
||
the copy `stack-copy.sh check` reports, the bus unit's `--pm` and
|
||
`MOSAIC_CLAUDE_BIN`, the Console's flags without `--checkout`, and the
|
||
copy's schema version. It refuses when the check fails, the bus unit has no
|
||
`MOSAIC_CLAUDE_BIN` or a pointer exists.
|
||
2. Make the pushed target copy with `stack-copy.sh copy COMMIT` and note its
|
||
full id.
|
||
3. Run `activate ID BUSINESS --plan` and take the output to Mos. It names the
|
||
store's schema and the target's, any migration with `--migrate` and the
|
||
decision 96 condition 2 line (Jason present), the backup, the commits the
|
||
run adds, and the three install commands. The `backup:` line gives the
|
||
directory the run backs the stopped store up to, or why it takes none:
|
||
there is no store file, or the run rolls back with `store-newer` or
|
||
`migration-needs-flag` before the backup. It refuses `copies-unrelated`
|
||
when neither copy is an ancestor of the other.
|
||
4. Mos removes any bus drop-in that overrides ExecStart or WorkingDirectory;
|
||
`activate` refuses `drop-in-override` and names the file.
|
||
5. With Mos's go, `activate ID BUSINESS`, plus `--migrate` when the plan
|
||
says so. Before it stops anything, it refuses a missing, unpushed or
|
||
unreadable copy, a busy `mosaic-bus@` instance, unit flags that differ
|
||
from the pointer, and a business file that names a plug-in kind the
|
||
target's `KINDS` lacks (`unknown-kind-for-target`, naming each kind). It
|
||
refuses `business-unreadable` when the target's code can't read the
|
||
business file: a copy older than eaeb5ebf2, which has no
|
||
`plugins/src/kinds.mjs`, or a file the target's loader refuses. Then it
|
||
saves the unit files, drop-ins and wrapper under
|
||
`state/stack-rollback/<activation>/`, stops the Console then the bus, and
|
||
reads the stopped store. A `-wal` or `-shm` beside it refuses
|
||
`store-unclean` and leaves the store alone. Every activate then takes an
|
||
owner-only backup of the stopped store under
|
||
`<dataRoot>/backups/stack/<activation>/`, before any migration (row 185),
|
||
and the log line names it. A missing store file takes no backup. A store
|
||
outside the target's chain (`store-newer`) or a migration without
|
||
`--migrate` rolls back before the backup.
|
||
It installs the bus, the Console and the wrapper from the pointer's flags,
|
||
starts the bus then the Console, and runs the gate: both units active, a
|
||
live bus host for this start, Console `/healthz`, `stack-copy.sh check`
|
||
and, with `--pm`, exactly one `launched` journal line for a live run of
|
||
this start. A pass writes the pointer.
|
||
6. A gate failure puts back the saved units and wrapper. The backup is
|
||
taken on every activate, but it is restored only when a migration ran.
|
||
A restore keeps the store it replaced as `bus.post-attempt-<stamp>`, where
|
||
the stamp is the activation's second and its suffix if it has one. After
|
||
a same-schema activate the store stays as the failed run left it. Then it
|
||
starts the old copy and gates it. If that gate
|
||
fails too, both units stay stopped, the log says `rollback-failed`, and
|
||
Jason or Mos settles it from the rollback set the message names.
|
||
7. `rollback BUSINESS` returns to the copy before the pointer's, taken from
|
||
the log. When the schemas differ it needs `--restore ACTIVATION`, with or
|
||
without a `-1` to `-9` suffix. A same-schema rollback can take
|
||
`--restore` too, with the backup the activate took. Rows written after that backup need
|
||
`--events N`, and Jason decides first. The count covers events and every
|
||
table in `COUNTED` (row 181), the tables a bus writer fills without an
|
||
event. The `post-backup-events` refusal prints one count per table with
|
||
rows, in that order, for example `1 event(s), 1 instance_names row(s)`.
|
||
N is the total of those printed counts, read off the refusal. It refuses
|
||
`business-unreadable` and `unknown-kind-for-target` like `activate`. For the second, set each kind
|
||
it names to `null` through the live bus first, with the `plugins set`
|
||
command in step 2 of the next section.
|
||
8. Jason's plain `mosaic talk pm "..."` must print sent and a reply. Keep the
|
||
`status` output with the receipts.
|
||
|
||
The Console's row 178 proof pair gets into the pointer only through `activate`
|
||
(row 187). `--proof-header NAME --proof-file PATH` puts the pair in the
|
||
pointer's Console flags and the unit after the preview flags and before `--send`
|
||
and the tester flag (`--tester-socket`, or `--tester-port` for legacy copies),
|
||
where `webui-service.sh` renders it, replacing any pair
|
||
already there. `--no-proof` drops it. Either may name the live copy, which a
|
||
plain activate refuses with `already-active`, so the proof goes on or off
|
||
without changing copies; `--no-proof` with no pair in the pointer still refuses
|
||
there. Later activates, `pm-on` and rollbacks carry the pair unchanged. A
|
||
rollback to a copy whose Console lacks `--proof-header` drops the pair and says
|
||
so in the steps log. An activate to such a copy refuses `proof-target` unless
|
||
`--no-proof` is given, so the proof goes off only when asked. Before anything
|
||
stops, the run also refuses `proof-needs-preview` when the flags it would
|
||
install lack `--public-host`, `--auth-header` or `--allow-user`,
|
||
`proof-header-invalid` on the Console's own header rules, and
|
||
`proof-file-invalid` for a path that isn't absolute, holds a character outside
|
||
`[A-Za-z0-9/._+-]`, is a symlink or not a regular file, isn't mode 600 or 400,
|
||
isn't owned by the uid the script runs as, or sits inside the repository or the
|
||
copies. It checks the file with `stat` and `realpath` and never reads the value.
|
||
`--plan` prints one `proof:` line: `on`, `carried`, `dropped` with the reason,
|
||
or `off`. On the live copy it also notes that the run re-pins the pointer's copy
|
||
and only the Console flags change.
|
||
|
||
`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's
|
||
Console flags and the unit, so the Console serves reads and no human sends. It
|
||
may name the live copy while the pointer holds `--send`; once `--send` is gone a
|
||
same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and
|
||
rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
|
||
`--plan` prints one `send:` line: `dropped`, `off` with or without the
|
||
`--no-send` note, or `carried`.
|
||
|
||
`activate ... --sso` (row 200, #1698) starts the Console with Q175 sign-in
|
||
(row 192). It drops `--public-host`, `--auth-header`, `--allow-user` and any
|
||
proof pair from the pointer's Console flags and the unit, and puts `--sso` after
|
||
the other flags, before `--send` and `--tester-port`. It may name the live copy
|
||
once, so a copy already at row 192 or later turns sign-in on without a copy
|
||
change. Later activates, `pm-on` and rollbacks to a copy with `--sso` carry it.
|
||
`--sso` with a proof pair or `--no-proof`, or on any other verb, is a usage
|
||
error. Before anything stops, a run whose Console flags hold `--sso` refuses
|
||
`sso-target` for a target Console without it, `sso-config` when the target's own
|
||
`checkConsole` refuses the config's `console` key (or there is none, in which
|
||
case, from another copy, it names the first step: activate the target without
|
||
`--sso`, then write `console.sso`; on a named instance both commands carry
|
||
`--instance NAME`), and `sso-slot` for each provider's slot file
|
||
that is missing, a symlink, not a regular file, not owned by this uid, has group
|
||
or other bits, resolves inside the repository or the copies, or is empty. It
|
||
uses `lstat`, `stat` and `realpath` on the slot and never reads it. `--plan`
|
||
prints one `sso:` line: `on` with what it drops and carries, `carried`, or
|
||
`dropped` on a rollback.
|
||
|
||
A rollback from an `--sso` pointer to a copy whose Console lacks `--sso` takes
|
||
the Console flags from the rollback set of the activation that moved the stack
|
||
onto the pointer's copy from the target, past any same-copy re-pin, checked
|
||
against the set's `SHA256SUMS`. `--send` and `--tester-port` follow the pointer.
|
||
When that set is missing, fails its sums, holds no readable argv or already ran
|
||
`--sso`, the run refuses `sso-rollback-flags`. Any activate or rollback also
|
||
refuses `config-console-key` before anything stops when the target's
|
||
`scripts/mosaic-config.mjs validate` refuses the config, including a non-console
|
||
key. Every existing parseable config is checked; a missing file passes. A target
|
||
without a loader accepts only the oldest top-level keys: `configVersion`,
|
||
`environment`, `dataRoot`, and `execution`; any later key refuses. The refusal
|
||
names the rejected key and says to restore the config.json backup first. A
|
||
`console` or `federation` key keeps row 203's message, which says the config
|
||
holds a console or federation key. So a
|
||
window re-pins to row 192 or later before it writes the key, and a rollback past
|
||
row 192 removes the key first.
|
||
|
||
Two more optional system config keys bound the host's compaction collections
|
||
(row 270, #1775, DECISIONS Q222). Only a human writes them, like `console`.
|
||
`compactionReadBytes` is the stream bytes one collection reads, a whole number
|
||
from 1048576 to 1073741824, default 67108864 (64 MiB). `compactionStreams` is the
|
||
stream files one collection opens, a whole number from 32 to 65536, default 1024.
|
||
The loader passes either on as written. The launcher takes the default for a
|
||
missing or bad value and logs one `system config:` line. A collection that hits
|
||
either bound counts as `not-collected`, and the next one reads on. A copy before
|
||
row 270 refuses both keys, and the release's config check doesn't catch that
|
||
(it runs the target's loader only for a `console` key). So any re-pin or
|
||
rollback plan whose OLD copy is before row 270 removes both keys first.
|
||
|
||
#### A target past c216f47db: the PM after the harness kind
|
||
|
||
From c216f47db on, the bus launches the PM only with a `harness` kind in the
|
||
business file, and no copy before it can set one. So `activate --pm` to such a
|
||
copy fails its gate and rolls back. Use two phases instead (row 169, Gitea
|
||
#1666, mos-0848):
|
||
|
||
1. `activate ID BUSINESS --pm-after-harness`, in place of `--pm`. It needs a
|
||
pointer with `pm` true. Before anything stops, it refuses
|
||
`pm-after-harness-target` when the target's `KINDS` has no `harness`, and
|
||
`pm-after-harness-set` when the business file already sets `harness`;
|
||
use `--pm` in both cases. It installs the bus without `--pm`, runs every
|
||
gate check but the PM's, and writes the pointer with `pm` false and
|
||
`pmPending` true. The log's final line is `result ok` with `pm pending`.
|
||
`status` and `--plan` print the pending state and the next step.
|
||
2. Jason sets `harness` with `plugins set` through the live bus. Copies from
|
||
before 98d29769e use `node <copy>/packages/cli/src/cli.mjs plugins set --file P
|
||
--business ID`; later copies dispatch `mosaic plugins set --file P --business ID`.
|
||
On the Q173 test system, the tester can run the latter with `--tester`.
|
||
`<copy>` is the pointer's copy, the one the bus runs from; human commands from
|
||
any other tree refuse `human-required`. P holds one patch,
|
||
`{"revision":"<64 hex>","plugins":{"<kind>":<value>|null}}`. Read the revision
|
||
with the same copy's `plugins --business ID` command (`--tester` for the tester).
|
||
A value sets the kind, null removes it, and an unnamed kind keeps its setting.
|
||
3. `pm-on BUSINESS` refuses `not-pending` without a pending pointer and
|
||
`harness-unset` unless `harness` is a non-empty list the pointer's copy
|
||
loads. It restarts the bus alone, with `--pm` on the same copy, and runs
|
||
the whole gate. A pass writes `pm` true and drops `pmPending`. A failure
|
||
puts the phase-1 bus unit back and reruns the phase-1 gate. If that
|
||
passes, the log says `pm-on-failed`, the pointer stays pending and the run
|
||
exits 1; fix `harness` and run `pm-on` again. If it fails too, both units
|
||
stop, the log says `pm-on-down`, and the run exits 2. `pm-on` and a
|
||
`pm-on-failed` run leave the Console running; `pm-on-down` stops it. None
|
||
of them touches the copy or the store.
|
||
|
||
While the pointer is pending, `activate` refuses `pm-pending`. Run `pm-on`
|
||
or `rollback`; a rollback from pending installs the old copy with `--pm` and
|
||
gates its launch.
|
||
|
||
A `started` line with no final line is an interrupted run. `status` shows
|
||
it, and `adopt`, `activate` and `rollback` refuse until Jason or Mos settles
|
||
it. Nothing prunes copies, rollback sets, backups or post-attempt stores.
|
||
|
||
After a switch, the human commands and `--tester` work only through `mosaic`
|
||
on PATH; the checkout's `scripts/mosaic` exits 3 with `human-required` or
|
||
`tester-required`, and queue writes stay on it ([packages/cli/README.md, One
|
||
pinned copy](../packages/cli/README.md#one-pinned-copy-row-85-1579)).
|
||
|
||
#### Rehearsal on a scratch root (D2, row 163)
|
||
|
||
```
|
||
scripts/stack-release.sh rehearse ID BUSINESS --backup ACTIVATION|PATH|last --port N [--timeout S]
|
||
```
|
||
|
||
`rehearse` starts the target copy's bus host and Console on a copy of a
|
||
backup and runs `activate`'s gate checks on them, all but the PM's, while the
|
||
live stack keeps running. It reads the live config and a backup of the live
|
||
store, so Jason or Mos runs it, as a window's pre-step. No development seat
|
||
runs it against live data. It stops and starts no unit and writes no pointer
|
||
or log line.
|
||
|
||
`--backup` takes an activation id under `<dataRoot>/backups/stack/` or an
|
||
absolute path. `--backup last` (row 233) takes the newest backup the
|
||
activation log records and prints `PASS backup-last` naming it. With none in
|
||
the log it fails `no-backup`, exits 3 and makes no directory. After the
|
||
backup's checks it prints `BACKUP_STORE <activation id> schema <version>`.
|
||
`--port` is the scratch Console's loopback port, 1024 to
|
||
65535. The run refuses `port-reserved` for 7332, 7333 and the pointer's
|
||
`--port` and `--tester-port`, and `port-in-use` for a port something holds;
|
||
it never picks another port. `--timeout` (default 60 seconds) bounds the
|
||
gate and each stop.
|
||
|
||
A copy marked for the private tester receives `--tester-socket` during rehearsal;
|
||
a legacy copy receives no tester flag. The socket is under the rehearsal's
|
||
scratch dataRoot, at `data/console/tester.sock`. That path is longer than the live
|
||
one. A long dataRoot can pass activation's pathname preflight and still make the
|
||
rehearsal Console refuse its socket path; the rehearsal then fails closed.
|
||
|
||
Before it makes anything, it checks the pointer, the copy (present,
|
||
reachable from refactor, a readable schema, a bus host with `--no-launch`),
|
||
the port and the backup's `SHA256SUMS`. Then it makes
|
||
`<dataRoot>/rehearsals/<UTC second>-<copy's first 12>/`, mode 0700:
|
||
|
||
- `data/bus/` holds a copy of the backup's store files, and
|
||
`data/notify/BUSINESS/notify.json` is `{"notifyVersion":1,"binding":null}`.
|
||
- `config/config.json` is the live config with `dataRoot` set to `data/`.
|
||
- `config/businesses/BUSINESS.json` is the live business file with every
|
||
`tracker.*` variable removed and every credential reference, the roles'
|
||
and `tracker.sync`'s, pointed at its own 0600 file under `placeholders/`
|
||
holding `rehearsal-placeholder-token`. `business.diff` shows the changes.
|
||
Roles come from the target copy's `roles/`, as the live bus reads them.
|
||
- `steps.log` holds the same PASS and FAIL lines the run prints, with times;
|
||
`host.log`, `console.log` and, after a migration, `migrate.log` sit beside it.
|
||
|
||
A guard then loads the scratch config with the target's code and refuses
|
||
with a named check: `data-root-live` when the scratch data root resolves to
|
||
the live one, `sessions-live` when a `sessions.json` there names a running
|
||
pid (the run never deletes the file), `data-root-scratch`, `notify-off`,
|
||
`tracker-configured` when `bootConfig` still sets a tracker, and
|
||
`credential-ref-live` for an env reference or a file outside `placeholders/`.
|
||
When the backup's schema differs from the target's, the run checks that the
|
||
target refuses the store, migrates it, opens it, migrates it again as a
|
||
no-op, and that the pointer's copy refuses the migrated store.
|
||
|
||
It starts `scripts/mosaic bus start BUSINESS --no-launch` and the Console's
|
||
`serve --port N --business BUSINESS` from the target copy, without `--send`.
|
||
The gate needs the host's `launches disabled` line, a live `bus status` host
|
||
with this run's pid and a boot from this start, the Console's address line,
|
||
`/healthz` 200, and both command lines on the target copy. Then it checks
|
||
that the host wrote no `sessions.json` and no `launches`, stops the Console
|
||
and the host (SIGTERM, then SIGKILL and a FAIL after the timeout), checks that
|
||
the host released `writer.lock`, and compares the backup and the live config
|
||
and business files with sums it took before. The last line is
|
||
`REHEARSAL_PASS` (exit 0) or `REHEARSAL_FAIL` (exit 1), then the directory.
|
||
Usage exits 4. Nothing prunes rehearsal directories.
|
||
|
||
`bus start --no-launch` refuses every launch with `launches-disabled`,
|
||
starts no thread ticker, and leaves an earlier host's `sessions.json` and
|
||
its runs alone. No unit passes it, and `--pm` with it is a usage error.
|
||
|
||
#### Root-owned units (`--units external`, row 232)
|
||
|
||
On a host whose bus and Console are root's system units, the stack user runs
|
||
`stack-release.sh` with `--units external` from its own clone of the release
|
||
bundle (sage-1561). The script then calls `systemctl show`, `is-active` and
|
||
`list-units` without `--user` and reads no journal. It starts, stops and
|
||
installs nothing. Root does those steps, and the I3 driver calls the verbs
|
||
below in order. Without `--units`, every command line and every byte of
|
||
output is what it was before row 232. `confirm`, `revert`, `gate`, `--to`
|
||
and `--expect-from` are usage errors without `--units external`.
|
||
|
||
- `adopt`, `status`, `activate ID` and `rollback --to ID` take the flag.
|
||
`rollback` then needs `--to`, and `--to` must be the copy `rollback` alone
|
||
would pick from the log, or it refuses `to-not-previous`.
|
||
- `--plan --expect-from OLD` refuses `pointer-differs` when the pointer
|
||
names a copy other than OLD.
|
||
- `pm-on` and `activate --pm-after-harness` refuse `external-pm`; the PM
|
||
flag is in root's bus unit. `--pm`, `--tester-port`, the proof flags,
|
||
`--no-proof`, `--no-send` and `--sso` are usage errors; root's Console
|
||
unit carries the Console flags.
|
||
- `activate` and `rollback` refuse `units-running` unless both units read
|
||
inactive or failed and the store is closed. They make the rollback set
|
||
with an exclusive `mkdir`, write `AWAITING-UNITS` (the target copy) first,
|
||
capture the pointer, the wrapper and both units' show output and unit file
|
||
sums, back up and migrate or restore the store, and write the wrapper.
|
||
They write no pointer and start nothing. The last line says the run is
|
||
staged and names `confirm`. A failure after the capture writes `FAILED`
|
||
with its code, logs nothing more and exits 1; the activation stays open.
|
||
- `confirm ID BUSINESS` needs exactly one `AWAITING-UNITS` for copy ID whose
|
||
activation the log holds open, or it refuses `open-activation`. It gates
|
||
the start root made and writes the pointer and an `ok` line with the
|
||
started line's op, from, to and backup.
|
||
`confirm --rollback OLD --cause A3|A4|A5` gates OLD after root put the old
|
||
units back and logs `failed` or `gate-failed` with `rollback ok` (exit 1)
|
||
or `rollback-failed` (exit 2). Either removes `AWAITING-UNITS` and `FAILED`.
|
||
- `revert ID BUSINESS` puts the saved wrapper back and, when the run had
|
||
started a migration, restores the backup taken before it. It writes no log
|
||
line and can run again.
|
||
- `gate ID BUSINESS --since EPOCH` gates a copy with no open activation,
|
||
takes no lock and writes nothing: 0 on a pass, 1 otherwise. EPOCH is root's
|
||
clock before root started the units.
|
||
|
||
The gate under `--units external` replaces the journal read. The bus unit's
|
||
`ActiveEnterTimestamp`, in whole seconds, must be at or after the started
|
||
line's second (`confirm`) or `--since` (`gate`); an older start fails
|
||
`stale-start`, so a unit that never stopped can't pass. With the PM on, the
|
||
gate reads `launches list --json` for exactly one live launch of the PM's
|
||
instance (the business's `launch.by`), for this business, whose run started at
|
||
or after the floor. `status` adds an
|
||
`awaiting units:` line for an open activation.
|
||
|
||
Under `--units external`, `activate --plan` also refuses `open-activation`
|
||
while any rollback set holds `AWAITING-UNITS`, and `migrate-required` when
|
||
the target migrates the store and the run lacks `--migrate` (row 233). Under
|
||
`--units user` the plan's bytes are unchanged.
|
||
|
||
#### Root's upgrade driver (`scripts/stack-upgrade.sh`, row 233)
|
||
|
||
```
|
||
scripts/stack-upgrade.sh upgrade --instance dev --bundle FILE --sha SHA [--migrate] [--no-rehearsal]
|
||
[--rehearse-port N] [--timeout S]
|
||
scripts/stack-upgrade.sh rollback --instance dev --to ID [--restore ACT [--events N]] [--timeout S]
|
||
```
|
||
|
||
Root runs it from a root-owned checkout of the reviewed commit, never from a
|
||
copy, because the stack's user can write the copies. Each run needs Mos's go,
|
||
and `--migrate` needs Jason present (decision 96). The host must be prepared
|
||
first (`docs/guides/host-run-sheet.md`). Only the default instance, `dev`, is
|
||
accepted; a named instance's units are a later row.
|
||
|
||
The unit values come from `/etc/mosaic-stack/dev.args`, which I2's install
|
||
writes: one `case2-unit.sh` argument per line, the serve flags after `--`,
|
||
owned by root with no group or other write, exactly one `--copy`, and that
|
||
copy the one both installed units run. Otherwise the run refuses
|
||
`args-unreadable` or `args-differ`. The driver writes the new copy id into
|
||
that file only after `confirm` passes.
|
||
|
||
`upgrade`, in order. Every stack user step runs as
|
||
`sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from the
|
||
user's clone `<home>/src/mosaic-stack`:
|
||
|
||
1. Takes `flock -n` on `/run/mosaic-stack/upgrade-dev.lock` and holds it to
|
||
the end, rollback included. A second run refuses `upgrade-running`, and
|
||
a lock it can't open refuses `lock-failed`.
|
||
2. Fetches the bundle's `refactor` into the clone and fast-forwards it to
|
||
SHA (`fetch-failed`), then `stack-copy.sh copy --bundle FILE SHA`
|
||
(`copy-failed`).
|
||
3. A1: this checkout's HEAD is SHA (`head-mismatch`), the copy exists
|
||
(`copy-missing`), `/usr/local/lib/mosaic-stack/hostcheck.sh --check`
|
||
exits 0 (`profile-drift`, row 240), both units name one copy
|
||
(`units-unreadable`, `units-disagree`) and it is the args file's
|
||
(`args-differ`), and
|
||
`activate SHA BUSINESS --units external --plan --expect-from OLD` exits 0
|
||
(`plan-refused`, which covers `open-activation`, `migrate-required` and
|
||
`pointer-differs`).
|
||
4. The rehearsal, `rehearse SHA BUSINESS --backup last --port 7339`
|
||
(`--rehearse-port`). Exit 0 goes on. Exit 3 means no backup in the log
|
||
and goes on only with `--no-rehearsal`, logging `rehearsal-skipped`;
|
||
without the flag it refuses `no-backup`. Exit 0 with `--no-rehearsal`
|
||
refuses `rehearsal-required`; any other exit refuses `rehearsal-failed`.
|
||
5. A2 stops the Console, then the bus, and waits for both to read inactive
|
||
or failed. A3 is `activate --units external`, A4 the render with the new
|
||
`--copy` (which also starts the bus, then the Console), A5 `confirm`.
|
||
The driver writes the new args file beside the old one before A2
|
||
(`args-failed`, exit 3) and moves it into place after A5. A failed move
|
||
exits 5 `args-failed`: the new copy is live and confirmed, the args file
|
||
still names the old one, and root moves the new file (the result line
|
||
names it) into place by hand.
|
||
|
||
A failure in A2 or a stage that exits 3 starts the old units again and gates
|
||
them (A2f). A later failure stops both units, runs `revert`, renders the old
|
||
copy and runs `confirm --rollback OLD --cause A3|A4|A5`. `rollback` is the
|
||
same from A1 on, with `rollback BUSINESS --to ID`, no fetch, no copy and no
|
||
rehearsal; `--restore` and `--events` pass through.
|
||
|
||
Root reads no output the stack user wrote except exit codes and the
|
||
rehearsal's `BACKUP_STORE` line, which it copies into its log after a match
|
||
against a fixed pattern (`BACKUP_STORE none` otherwise). The step log
|
||
`/var/log/mosaic-stack/upgrade-dev.log`, 0640, gets one
|
||
`<time> <step> rc=N argv: ...` line per step and a last
|
||
`<time> result exit=N <code> <text>` line.
|
||
|
||
Exit 0 ok, 1 failed with the old stack running and gated, 2 rollback-failed
|
||
with both units stopped, 3 refused with nothing stopped, 4 usage, 5
|
||
args-failed with the new stack live and confirmed. A refusal
|
||
after step 2 leaves the user's clone fast-forwarded and the new copy made. After exit
|
||
2 the activation stays open, so the next run's plan refuses until root
|
||
finishes it.
|
||
|
||
#### Root's host check (`scripts/case1-hostcheck.sh`, row 240)
|
||
|
||
```
|
||
/usr/local/lib/mosaic-stack/hostcheck.sh --check | --enforce | --upstream
|
||
```
|
||
|
||
Installed from `scripts/case1-hostcheck.sh` as `hostcheck.sh`, with
|
||
`scripts/case1-pins.sh` beside it (`docs/guides/host-run-sheet.md` section 4).
|
||
The pins are the profile's sha256, the `apparmor-profiles` version it came
|
||
from and the bubblewrap install pin; changing one is a reviewed commit.
|
||
Seven items, one `ok N` or `FAIL N` line each: the profile file matches the
|
||
pin and no package owns it; `aa-status` has `bwrap` and `unpriv_bwrap` in
|
||
enforce mode and no `mosaic-bwrap`; no other file under `/etc/apparmor.d`
|
||
names `bwrap`; a bwrap child under the units' no-new-privs reads
|
||
`bwrap//&unpriv_bwrap (enforce)`; `/usr/bin/bwrap` is `root mosaic 0750` by
|
||
the statoverride and by `stat`; the user-namespace restriction and
|
||
`fs.protected_hardlinks` are 1; `net.ipv4.ip_unprivileged_port_start` is
|
||
above the Console unit's `--port`, or above 1023 with no unit. A `note` line
|
||
says whether bubblewrap is the install pin and never fails.
|
||
|
||
`--check` exits 0 or 1; `stack-upgrade.sh` refuses `profile-drift` on 1.
|
||
`--enforce` exits 0. On a failure, or while `/etc/mosaic-stack/DRIFT` exists,
|
||
it unloads the profile if loaded, sets `/usr/bin/bwrap` to `root:root` 0700,
|
||
appends the failed items to `DRIFT` and one line to
|
||
`/var/log/mosaic-stack/hostcheck.log`. It never writes the statoverride. The
|
||
apt hook runs it after every package run, and `mosaic-hostcheck.service`
|
||
(`packages/cli/systemd/`) at boot before the bus. `--upstream` compares the
|
||
`apparmor-profiles` candidate's profile to the pin, prints any difference
|
||
and exits 0, or 1 when the download fails. Exit 2 is usage or a refusal:
|
||
root takes no `--root` (the test seam), and other users run only with it.
|
||
|
||
#### Root's acceptance run (`scripts/case1-accept.sh`, row 195)
|
||
|
||
```
|
||
case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
|
||
```
|
||
|
||
Root runs it on the Case 1 host after the install window, from a root-owned
|
||
checkout. It reads the user, home, copy, node, claude and both ports from the
|
||
args file (`/etc/mosaic-stack/dev.args` on .53), which must pass
|
||
`stack-upgrade.sh`'s rules, say `--profile factory` and name `--business`.
|
||
Every command that runs the stack's code runs as
|
||
`sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from `/`.
|
||
Root itself reads `ss`, `/proc`, the account database and file metadata. It
|
||
runs `systemctl show` and `is-enabled`, `curl` on the Console's loopback
|
||
`/healthz`, `sudo -l`, `sudo -u jarvis bwrap --version`, `hostcheck.sh
|
||
--check`, and node to parse `managed-settings.json` for F9. F9 runs node only
|
||
when `--node` is the `--node-pin` path, and the file and every directory above
|
||
it are root's with no group or other write. The one write is F3's message and
|
||
its reply, and it never opens `claude-session.env`.
|
||
|
||
It prints one line per check, `<id> <path> <mark> <evidence>`, then
|
||
`count <mark> N` for each mark and a result line. The functional checks
|
||
F1-F11 are `pass` or `FAIL`: both units active as the user, the Console on
|
||
127.0.0.1 only, a tester message to chief answered, `case2-precheck.sh
|
||
--profile factory`, the bwrap labels of a live role session, the claude and
|
||
node pins, one live PM launch, the credential file's owner and mode, the
|
||
autoupdater off, no Discord binding. The host paths P1-P11 are `closed`,
|
||
`accepted <ruling>` (Q182, Q183, mos-1096, mos-1101) or `unnamed`, which means
|
||
no ruling names the path; each `unnamed` line goes to Mos. P9 runs
|
||
`hostcheck.sh --check`, never `--enforce`.
|
||
|
||
Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal:
|
||
a bad args file, or `--root` misused. A P check never changes the exit.
|
||
`--root DIR` is the test seam for `packages/cli/tests/case1-accept.test.mjs`;
|
||
root takes none, and other users run only with it. Design: row 195 r7
|
||
section 5 (`agents/darkwing/work/queue-195/DESIGN.md`).
|
||
|
||
## Console service (`scripts/webui-service.sh`)
|
||
|
||
```bash
|
||
scripts/webui-service.sh copy COMMIT [--copies DIR] # make a pinned copy
|
||
# render, install, uninstall and status also take --instance NAME (row 190); copy refuses it (exit 4)
|
||
scripts/webui-service.sh render [--copy ID [--copies DIR]] [FLAGS] # print the unit
|
||
scripts/webui-service.sh install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [FLAGS]
|
||
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
|
||
scripts/webui-service.sh status [--dir DIR]
|
||
# FLAGS: [--port N] [--business ID] [--board URL]
|
||
# [--public-host NAME --auth-header HEADER --allow-user ID [--send]]
|
||
```
|
||
|
||
The systemd user unit `mosaic-webui.service` runs the Console,
|
||
`node packages/webui/src/cli.mjs serve`, with serve's flags as given:
|
||
`--port` defaults to 7332 (7330 is the operator's own Console), and the
|
||
preview set goes all three or none. `--send` (row 83) goes only with the
|
||
preview set and is refused otherwise (exit 1, nothing written); ExecStart
|
||
then ends with `--send`, and the Console needs the system config at start
|
||
(`--send needs the bus configured`, exit 2). Without `--send` the unit is
|
||
read-only. The template is
|
||
`packages/webui/systemd/mosaic-webui.service.in`.
|
||
|
||
Under `--send` the Console's Docs page (row 148) can also save an existing
|
||
spec/north-star.md or docs/NAME.md of a project: one local commit authored by the
|
||
business's human, refused `stale-base` if the file changed since the editor opened it.
|
||
It is the human bus verb `project.file.save` through the human transport; an agent
|
||
refuses it. Nothing is pushed. Without `--send` the page reads and previews only. See
|
||
`packages/webui/README.md` ("Spec and docs editor"). The page opens in a visual editor
|
||
built from reviewable source (row 154): `third_party/editor/` is unmodified upstream
|
||
source with its licenses and `PROVENANCE.json`, and `npm run build:editor` in
|
||
`packages/webui` rebuilds `src/public/editor.js` and `editor.css` with esbuild 0.28.2;
|
||
the save, the verb and the routes are unchanged. See "Visual editor" there. A save of spec/north-star.md also
|
||
sends the business's pm and cto one notice naming the commit (row 143); a BLOCK from
|
||
either goes to the chief of staff and on to the assistant. See `packages/bus/README.md`
|
||
("Spec review notices").
|
||
|
||
The unit runs a pinned copy, not the working checkout (row 77, #1571).
|
||
`copy COMMIT` resolves COMMIT to its full id, refuses one not reachable
|
||
from both `refs/heads/refactor` and `refs/remotes/origin/refactor`, and
|
||
delegates to stack-copy.sh for the archive and pinned npm install into `DIR/<id>/`, by default
|
||
`${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies/<id>/`. It writes a
|
||
temp directory beside the target, makes every file and directory read-only
|
||
(so a writer that replaces by rename can't change it either), then renames
|
||
it, so the copy's path is always whole. A copy that exists is left alone
|
||
(`exists:`). `install --copy ID` and `render --copy ID` point
|
||
`WorkingDirectory` and ExecStart at the copy and pass serve
|
||
`--checkout <this checkout>`, where the queue read still runs; `RELEASE`,
|
||
the roles, the config check and the bus child come from the copy. The bus
|
||
child is the human transport, `packages/bus/src/human-cli.mjs`, and the bus
|
||
checks that path against its own, so the bus must run from the same copy
|
||
(below). `install` refuses without `--copy`, or when the copy is missing.
|
||
`render` without `--copy` prints the unit for the checkout, as before.
|
||
|
||
`install --copy ID` also refuses unless the bus runs from the same copy
|
||
(round 2, #225). The bus accepts a human proof only from its own
|
||
`packages/bus/src/human-cli.mjs`, so a Console on a copy whose bus runs
|
||
from the checkout would fail every bus call with `human-required`. With
|
||
`--business ID`, install reads `[email protected]`; without it, every
|
||
loaded `mosaic-bus@` unit. It reads effective WorkingDirectory and ExecStart from
|
||
`systemctl --user show -p LoadState -p WorkingDirectory -p ExecStart`, so a drop-in counts,
|
||
and every ExecStart path must lie inside the copy. It refuses, naming both
|
||
paths, when one doesn't; when the unit is not installed or none is loaded;
|
||
when ExecStart has no path; and when the systemctl call fails. The check
|
||
runs before the unit state read and changes nothing.
|
||
|
||
`install` writes a temp file, renames it and runs
|
||
`systemctl --user daemon-reload`. `install` and `uninstall` refuse while the
|
||
unit is in any state but inactive or failed (`systemctl --user list-units`),
|
||
and say Jason or Mos stops it first; `--no-reload` skips only the
|
||
daemon-reload, never that check. If the state read itself fails they refuse
|
||
with one line naming the call, and change nothing. `copy` and `install`
|
||
refuse outside the canonical checkout (a `.git` directory, not a worktree's
|
||
`.git` file); `--allow-worktree` overrides that for the tests only. `status`
|
||
prints the unit state, the copy id it runs, whether `refactor` points at
|
||
that commit or another, whether the installed unit sends, and
|
||
`GET /healthz` on the installed port. It changes nothing.
|
||
|
||
Each value is held to a plain character set the unit can carry (no space,
|
||
`%`, `$`, quote, backslash, `&` or `|`), and the preview values must start
|
||
with a letter or digit; anything else is refused. With `--auth-header
|
||
X-authentik-uid`, in any letter case, `--allow-user` must be the Authentik
|
||
uid, `^[0-9a-f]{64}$` (lead decision 101). The default's unit sets no `MOSAIC_*`,
|
||
`CLAUDE*`, `CODEX*` or `PI_*` variable; a named instance's sets only
|
||
`MOSAIC_CONFIG` (row 190). Serve's exit 2 (usage, config, or a
|
||
port already in use) is not restarted.
|
||
|
||
The update/rollback sequence is [Pinned stack update](#pinned-stack-update)
|
||
above. The queue view reads the checkout and stays current. Copies remain
|
||
until an operator removes them; nothing here prunes or migrates copies.
|
||
|
||
There is no start, stop, enable or restart verb. The Console reads the bus
|
||
through the human transport, so whoever starts it holds the human's
|
||
capability. Jason starts, stops and restarts it
|
||
(`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
|
||
decision 102, which accepts that sends from a Console Mos started enter the
|
||
trail as Jason (`human-cli`). Any other agent starting it, even through
|
||
systemd, works around the human proof (lead decisions 62 and 94).
|
||
Exit codes: `0` ok · `1` failed or refused · `4` usage.
|
||
|
||
## Discord connector (`scripts/discord.sh`)
|
||
|
||
```bash
|
||
scripts/discord.sh check <binding>
|
||
scripts/discord.sh run <binding> [--supervised]
|
||
scripts/discord.sh stop <binding>
|
||
scripts/discord.sh unlock <binding>
|
||
scripts/discord.sh recover <binding>
|
||
scripts/discord.sh reload <binding>
|
||
scripts/discord-service.sh [--instance NAME] render | install [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status <binding>
|
||
```
|
||
|
||
One seat's conversation reachable from listed channels of one Discord
|
||
server, chat only. `<binding>` names `<dataRoot>/discord/<binding>.json`
|
||
(0600, never committed; shape in `packages/discord/fixtures/binding.example.json`).
|
||
`check` validates the binding, token file mode, context files and pi, reads
|
||
the bot, guild and channels over REST, opens and closes one gateway
|
||
connection; it sends nothing. `run` serves turns until `stop`, SIGTERM or
|
||
SIGINT and refuses to start when `STOP` exists or a delivery cannot be
|
||
reconciled. `stop` writes `STOP` and signals the running process. `unlock`
|
||
writes `STOP` and removes a `run.lock` left by a dead process; it refuses
|
||
while the owner is alive with an unverifiable identity or its record cannot be
|
||
parsed. `run` refuses a stale lock rather than reclaiming it, and releases a
|
||
claim that meets `STOP`. `recover` is the supervised pre-start, and
|
||
`run --supervised` does it first in the same process: it refuses (exit 3)
|
||
while `STOP` is present or the binding is held, clears a lock whose owner
|
||
is gone and removes only the `STOP` it wrote for that. `reload` validates
|
||
the binding file and sends SIGHUP to the running connector, which applies
|
||
`guildName`, `channels`, `users` and `limits` in place and refuses anything
|
||
else, keeping the old binding; attempts are journaled in `reloads.jsonl`. A
|
||
user entry may carry `channels`, an allowlist of listed channel ids.
|
||
`scripts/discord-service.sh install` renders and writes the systemd user
|
||
unit `[email protected]` (one instance per binding, restart on
|
||
failure, exit 3 never retried, SIGTERM on `systemctl --user stop`, SIGHUP on
|
||
`systemctl --user reload`). With
|
||
`install --copy ID [--copies DIR]`, a full id selects an existing shared
|
||
copy holding the Discord CLI and pi; ExecStart runs Node on that CLI with
|
||
--repo and WorkingDirectory in the copy. Without --copy the checkout unit
|
||
is unchanged. Status names its effective cwd/copy id, including drop-ins.
|
||
`check --offline` validates local binding/context/tool roots/pi/token mode
|
||
and reports journal/STOP without REST or gateway calls; run refuses this
|
||
flag. For a Mos-approved re-pin after the SetSpark freeze: make the copy,
|
||
run `node <copy>/packages/discord/src/cli.mjs check <binding> --repo <copy> --offline`,
|
||
save the installed unit, install --copy, then restart on Mos's go. Context
|
||
files resolve inside the copy and missing files refuse. Rollback restores
|
||
the saved unit and daemon-reloads/restarts on Mos's go; keep both copies.
|
||
The connector's pin is independent of stack-copy.sh check. An optional `tools` key in the binding declares
|
||
file roots; pi then runs with its own tools off and the package's
|
||
extension providing `list_dir`, `read_file` and `search` confined to those
|
||
roots, plus `write_file` and `edit_file` for roots marked `"write": true`
|
||
(temp file and rename, no folder creation, no dot-prefixed path, no
|
||
credential shapes), and `web_fetch` and `web_search` when `tools.web`
|
||
names a SearXNG instance (https only, public addresses only, three
|
||
redirects, capped body, html to text), with each call in the turn record.
|
||
A writable root that is a git work tree may carry `git` (`branch`,
|
||
`identity`, `tokenFile`, `author`, optional `protocol: "vault"`) and
|
||
gains `git_status`, `git_commit` (explicit paths, seat author,
|
||
`Requested-by:` trailer, push at once), `git_pull` (ff-only) and
|
||
`git_push` (one branch, never force), with `reserve_id` under the vault
|
||
protocol; git runs with no host config and the package's own credential
|
||
helper reading a 0600 token file, never printed. `tools.setspark` names the
|
||
SetSpark record service and adds `record_list`, `record_get`,
|
||
`record_create`, `record_update`, `resolve_id`, `open_approval_request`,
|
||
`get_approval_request` and `create_document`, one fixed path each, writes
|
||
keyed per turn and call; approvals of its proposals are an Approve button
|
||
or an exact `approve` reply, checked by the connector against the
|
||
request's approvers, never by the model, with one Discord message per
|
||
approval as evidence. `tools` is a fixed key: changing
|
||
it needs a stop and start. Records
|
||
under `<dataRoot>/discord/<binding>/`: `inbox.jsonl`, `outbox.jsonl`,
|
||
`drops.jsonl`, `admissions.jsonl`, `notices.jsonl`, `reloads.jsonl`, `approvals.jsonl`, write-once `turns/<id>.json`. Suite:
|
||
`scripts/test-discord.sh`.
|
||
Exit codes: 0 ok, 1 operation failed, 2 invalid data or configuration, 3
|
||
refused by a brake (a supervisor must not retry), 4 usage. Details:
|
||
`packages/discord/README.md`.
|
||
|
||
## Tools (host-side)
|
||
|
||
Host-side helpers under `tools/`, outside the `scripts/` command surface.
|
||
Per-tool READMEs: `tools/tmux/README.md` and `tools/unslop-hook/README.md`.
|
||
|
||
| Command | Purpose | Notes |
|
||
|---|---|---|
|
||
| `tools/tmux/agent-send.sh` | Inter-agent tmux message with addressing preamble | Reliable submit (bracketed paste, Enter flush, draft detection); ships `send-message.sh` over ssh for remote panes (remote needs only bash + tmux + base64) |
|
||
| `tools/agent-watch/agent-watch.sh` | Condition watcher per agent seat | One transient systemd `--user` timer + service per watch; fires `agent-send.sh` when the condition command exits 0 |
|
||
| `node tools/unslop-hook/unslop-check.js <file>` | Mechanical AI-tell prose check | Dependency-free node CLI + module driven by `lists.json`; `extension.ts` is the pi extension wrapper |
|
||
|
||
`agent-send.sh` prepends the preamble
|
||
`[<src_host>:<src_session> -> <dst_host>:<dst_session>]`; `-C`/`--class` adds
|
||
a ` class=<CLASS>` token (`terminal-log`, `actionable`, `human`, `reaction`,
|
||
`digest`; consumers treat an absent class as `actionable`). Flags: `-s` dst
|
||
session (required) · `-H` ssh target for a remote pane · `-L` named tmux
|
||
socket · `-n` dst hostname for the preamble · `-m`/`-f`/stdin message body ·
|
||
`-S` source-label override · `-r N` Enter-flush attempts (default 2) · `-v`
|
||
verbose · `-h` help. Exit codes: `0` delivered/queued · `1` target not found ·
|
||
`2` still draft · `3` usage error · `4` ambiguous socket (the session exists
|
||
on more than one tmux server; disambiguate with `-L` or `MOSAIC_TMUX_SOCKET`).
|
||
|
||
`agent-watch.sh` subcommands: `start --name <id> --session <session> --when
|
||
'<shell command; exit 0 = met>' --message <text>` with `--class`,
|
||
`--interval` (default 30), `--timeout` (default 3600), `--repeat`,
|
||
`--quiet-timeout`, `--socket` · `list` · `status [--json]` · `stop <name>` ·
|
||
`log <name>` · `meta-install [--interval 300] [--unit-name <unit>]` ·
|
||
`meta-remove [--unit-name <unit>]`. Interval floor is 10s (a watcher is a
|
||
fallback cadence, never a tight poll); hidden `_tick`/`_scan` subcommands run
|
||
inside the systemd services. `status` exit codes: `0` clean · `3` any stale
|
||
watch or dead meta-watch · `6` systemd user bus unreachable. Delivery goes
|
||
through `agent-send.sh`; rc `2` means the text reached the pane as an
|
||
unsubmitted draft, which counts as delivered and is not retried (other
|
||
failures retry twice, then the watch gives up). Watches are one-shot by
|
||
default; `--repeat` re-arms. Notices carry a `[watch:<name>]` prefix.
|
||
|
||
`unslop-check.js` checks a file (or stdin) against the word, phrase,
|
||
punctuation-density, and pattern lists in `lists.json`, stripping fenced and
|
||
inline code first so a quoted mention never flags. Invocation:
|
||
`node tools/unslop-hook/unslop-check.js <file>`; `UNSLOP_LISTS=<path>`
|
||
overrides the lists location. Exit codes: `0` clean · `1` violations (findings
|
||
printed as JSON on stdout) · `2` gate broken (invalid lists or unreadable
|
||
input; error on stderr, never a clean verdict).
|
||
|
||
## Exit-code convention
|
||
|
||
`0` success · `1` operation failed · `2` invalid data/configuration ·
|
||
`3` configuration missing for a read operation · `4` usage/file/environment
|
||
problem. Scripts print diagnostics on stderr; model responses (and only
|
||
model responses) on stdout.
|
||
|
||
## Agent limits (`scripts/mosaic limits`)
|
||
|
||
View: `scripts/mosaic limits --business ID --json`. Set: `scripts/mosaic limits
|
||
set --business ID --file PATCH`, with the current revision and replacement
|
||
`limits` and/or `models`, or `instances` mapping role names to model-table keys,
|
||
or `assistants` mapping an assistant instance to any of its `vars`, `tracker`
|
||
(`null` removes it) and `definition` (row 155, #1649, Jason's ruling Q169). The
|
||
definition stays an assistant one; `holder` and `credentials` stay hand edits.
|
||
Both use the human bus proof. Settings offers the same edits in signed-in send
|
||
mode. Unset means unlimited, zero blocks new launches, and running sessions
|
||
remain alive when a limit is lowered. Total, provider and exact model limits
|
||
are checked for each launch; counts have no hard-coded policy ceiling.
|
||
|
||
Token limits (row 125) are `limits.tokens.instances.NAME`, `providers.NAME`,
|
||
`models.PROVIDER.MODEL` and `keys.NAME`, each `{ "limit": N }`. The writer adds
|
||
and keeps `since`. `scripts/mosaic limits reset WORD... [--business ID]` (for
|
||
example `reset tokens providers anthropic`) moves one limit's `since` to now,
|
||
logged with its actor. A token limit is enforced only by a switched-on
|
||
`limit-check` plug-in (`token-limit` ships): it refuses a launch at the limit
|
||
and stops the session at the end of the turn that reaches it. Otherwise the
|
||
read shows it as `not enforced`; a failing check refuses and stops (fail
|
||
closed). Without `--json` the read prints JSON without `tokens` followed by
|
||
`token limits:` text lines, so scripts must pass `--json`.
|
||
|
||
The Console's Assistant section (signed-in send mode) also sets an instance's
|
||
seat name, as `scripts/mosaic name` does, and stops the assistant's runs in the
|
||
human's threads. The chief of staff makes the same changes with its
|
||
`organization_revision`, `change_organization`, `set_name` and `end_run` tools;
|
||
`organization_revision` reads the revision that `change_organization` sends.
|
||
|
||
Convert an old organization file with `scripts/mosaic limits migrate --file
|
||
ORGANIZATION --models MAPPING --roles-dir roles > CONVERTED`. It needs no bus,
|
||
never edits the input or system config and has no write flag. Inherited models
|
||
require a valid system config and an exact matching provider/model mapping.
|
||
Mapping JSON contains `models`,
|
||
`families` and optional per-role `instances` selections. Jason checks and installs
|
||
the output in Mos's window with the PM run idle; re-pin in that same window.
|
||
Never migrate the live file from an agent session.
|
||
|
||
|
||
### Local project records (`mosaic project`)
|
||
|
||
`mosaic project create` and `mosaic project list` work on the bus record of a
|
||
business's projects; `mosaic project init` and `mosaic project check` work on
|
||
the repository on disk. Threads (`mosaic threads`, `mosaic thread create|open|read`,
|
||
`mosaic talk --thread`, `mosaic follow --thread`) are described in
|
||
`packages/cli/README.md`; a plain `mosaic talk` to the threaded instance exits 2
|
||
and asks for `--thread`.
|
||
|
||
`mosaic project init <business> <project>` creates the declared project's
|
||
separate local record repository, with layout version 1, package-validated JSON
|
||
templates, Markdown spec/docs and its first commit authored as the business's
|
||
human. Init refuses an agent environment, an undeclared project or a non-empty
|
||
directory. Failed init cleans up its own layout and allows retry, preserving a
|
||
pre-existing empty target and unrelated files. A changed owned path refuses
|
||
`project-init-cleanup` for inspection. Records may not overlap any declared
|
||
project's code root. No bus or remote is involved.
|
||
|
||
`mosaic project check <business> <project>` validates project metadata, every
|
||
record, filename/ID agreement and the fixed layout, naming each problem. It
|
||
refuses `project-no-commit` if the layout has no readable HEAD commit. It
|
||
exits 0 when valid, 2 for project/record problems (including an undeclared
|
||
project), 3 for configuration loading problems and 4 for usage. The optional business project `repo` is a normalized absolute
|
||
record-repository path; the default is
|
||
`<dataRoot>/projects/<business>/<project>/`. `root` remains the code checkout.
|
||
The package README describes schemas and the shared agent record writer; agent
|
||
writes never enter `spec/`, never absorb pending edits and refuse remotes.
|
||
|
||
### Token usage (row 124)
|
||
|
||
`mosaic usage [--business ID] [--json]` reads token accounting from the bus.
|
||
Each total, run, instance, model, provider and account group shows input,
|
||
output, cache write, cache read and unknown turns. A malformed or unavailable
|
||
transcript contributes an unknown turn, never a zero-token reading. Accounts
|
||
are unassigned until account tracking ships. Exact totals larger than the
|
||
safe integer range are decimal strings in JSON. Settings has the same
|
||
read-only Token usage section. Counts are a best reading, not proof: the
|
||
session can edit its own transcript. This command changes no setting and
|
||
performs no token-limit enforcement.
|
||
|
||
### Console Chat (row 129, #1623)
|
||
|
||
The Console's **Chat** section uses `#/chat`, `#/chat/projects/UUID` and
|
||
`#/chat/threads/UUID`. Create and name a project and its threads, then send a
|
||
message in that thread's conversation. Names are creation-only. The project
|
||
catalog is business-wide; thread lists and histories belong to the current
|
||
human. This is separate from the Board's seat reply route.
|
||
|
||
`GET/POST /api/chat/projects` lists/creates projects; `GET/POST /api/chat/threads`
|
||
lists/creates threads (`project=UUID` optionally filters the GET).
|
||
`GET /api/chat/threads/UUID/messages?limit=100&after=UUID` pages history forward;
|
||
`POST` on that messages route sends `{body}` as a REQUEST.
|
||
`POST /api/chat/threads/UUID/open` takes `{}` and explicitly resumes the assistant.
|
||
GET never writes, opens a thread or marks messages delivered/read. No browser
|
||
owner, tenant, proof or arbitrary bus verb is accepted. A same-business thread
|
||
owned by someone else is 403 `thread-not-owned`; foreign/missing ids are 404.
|
||
|
||
The four chat POSTs use the existing signed-in `--send` boundary: named Host,
|
||
one exact proxy user header, exact HTTPS Origin, same-origin Fetch Metadata,
|
||
strict JSON and 16 KiB. Read-only mode disables them. Bodies are inert text;
|
||
drafts are memory-only per thread and cleared on sign-out. History starts
|
||
oldest-first in pages of 100, with a 200-message display window. Sending does
|
||
not claim an assistant answer; an uncertain receipt is never retried. Read the
|
||
current list/history before trying again. Activation still needs Mos's separate
|
||
window; changing these files does not authorize a live Console action.
|
||
|
||
### Console tester listener (`serve --tester-socket`, row 196)
|
||
|
||
`node packages/webui/src/cli.mjs serve --tester-socket` adds private HTTP at
|
||
`<dataRoot>/console/tester.sock` (0600) under console/ (0700). The path must fit
|
||
107 bytes. Unsafe ownership, modes, symlinks and pre-existing entries refuse.
|
||
The main Console remains on its configured TCP port. New copies refuse --tester-port.
|
||
|
||
The tester's fixed Host is mosaic-tester.invalid and its Origin is
|
||
http://mosaic-tester.invalid. No DNS is used. POST also needs same-origin Fetch
|
||
Metadata and bounded JSON. The endpoint uses only TESTER_CLI. It admits the
|
||
existing instance, route, name and tester-chat writes, never owner settings,
|
||
document saves, assistant stop or role messages. bus.tester stays Jason's switch.
|
||
Authorization, login/session cookies and configured proxy headers refuse.
|
||
|
||
```sh
|
||
node packages/cli/src/tester-http.mjs GET /api/bus/agents
|
||
printf '%s' '{"name":"fixture"}' | node packages/cli/src/tester-http.mjs POST /api/chat/projects
|
||
```
|
||
|
||
The helper loads the selected instance config and reports HTTP status/text/body
|
||
as JSON, exiting 2 for refusal. It never retries writes. Direct browser access
|
||
is removed with Mos's acceptance (mos-1015). A future authenticated browser relay
|
||
behind SSO is a separate row. No TCP compatibility bridge is provided.
|
||
|
||
`webui-service.sh render|install --tester-socket` checks target support and carries
|
||
the flag into ExecStart. `stack-release.sh activate ID BUSINESS --tester-socket`
|
||
explicitly replaces an inherited --tester-port pair in both unit and pointer.
|
||
Without explicit conversion a new target refuses tester-transport-migration before
|
||
stopping anything. Unsupported targets refuse tester-socket-target. Rollback drops
|
||
unsupported socket enablement, visibly in plan/steps, without enabling TCP. Legacy
|
||
flags remain supported by the release tool for legacy target copies only. Failed
|
||
activation recovery can restore the previous TCP unit and its old limits.
|
||
|
||
Activation of a new copy preflights the pinned root-owned system Python ELF,
|
||
records its resolved path/version/identity/hash and checks the bus's fresh startup
|
||
receipt. Restarts revalidate and append actual interpreter identity to
|
||
`<dataRoot>/bus/peercred-starts.jsonl`, without comparing an old activation hash.
|
||
After an OS interpreter update, the next authorized re-pin uses a different reviewed
|
||
copy and refreshes activation evidence. Do not bypass already-active or use a PATH shim.
|
||
All live operations keep their existing window authority.
|
||
|
||
Console proxy proof (row 178, #1676): `serve`, `webui-service.sh render` and
|
||
`install` accept `--proof-header NAME --proof-file PATH` together, under preview
|
||
mode only. The header must differ from Host, Origin, the auth header and
|
||
x-mosaic-tester. Serve exits 2 unless PATH is a non-symlink regular file owned by
|
||
its uid, mode 0600 or 0400, containing at least 32 base64url characters in one
|
||
line (an optional final LF is allowed). Render checks metadata only and refuses
|
||
paths inside the checkout or copies; it never opens the file. Keep the value
|
||
out of argv, logs and messages. Serve reads it once, and prints only `proxy
|
||
proof on` or `proxy proof off`. The public Host needs exactly one matching proof
|
||
before sign-in is checked; bad or absent proof gets 401 and no bus or board
|
||
call. The proof header on another Host or the tester listener gets 403.
|
||
`stack-release.sh` preserves both flags unchanged on re-pin and rollback.
|
||
|
||
This row installs no proof file, middleware or live flags. Those belong to a
|
||
separate Mos plan-checked window: middleware first, Console flags second, then a
|
||
public sign-in smoke and a forged-loopback 401 check; rollback drops flags
|
||
before middleware. Same-uid callers can still read the file; separate service
|
||
uid is Jason's question on #385. The Authentik allowed-uid file remains an
|
||
identity input and is never reused for proof.
|