Slice 1 S6. packages/harness runs a role session under the pi or claude
adapter behind a tool gate, and packages/seat holds the session and its
process. The bus host gains a launch socket: `mosaic bus start
<business> --pm <harness>:<model>` runs the PM without a window, the PM
launches role sessions through the broker within the business file's
`launch.max`, and every launch, refusal and end is a launch-log line and
a broker event. `mosaic talk`, `mosaic launches off|on|list` and
`mosaic stop <run>` are new CLI verbs. packages/bus gains the IPC ops
identity, authorize, refuse, endLaunch and credentialStatus, plus
Broker.endLaunch (decision 77); no socket verb, event kind or schema
changed. The session environment is an allowlist, and the runner exits
before claiming if a founder credential reaches it.
Filbert built it (agents/filbert/work/s6). Darkwing reviewed on #1523:
round 1 changes (26991/26995), round 2 changes (27004/27010), round 3
changes (27012/27032), round 4 approve (27033/27034) on manifest
08a78972, 42 paths.
Sage's gate on 64b0f9f1 (12:32Z), sequential and teed: node suites
business 60, bus 74, cli 83, control-board 124, conversation 182,
discord 178, harness 56, ledger 78, mosaic 69, queue 149, runs 41,
seat 27, tasks 51, webui 39, every one 0 failed; test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98 and 26 without Docker, all 0 failed.
64b0f9f1..ee7f909b touches none of the 42 paths. It landed after the
Q14 hold ended at 2026-10-11T17:19:23.857Z. The recorded PM-to-coder
run on a scratch data root is still owed before row 41 moves to done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
212 lines
11 KiB
Markdown
212 lines
11 KiB
Markdown
# seat
|
|
|
|
`mosaic launch <seat>` starts a seat through its existing launch script,
|
|
unchanged, and leaves one registration record that the control board reads
|
|
instead of guessing. `mosaic seat task <seat> <text>` changes the task on
|
|
that record and notes who set it. Nothing else. No provider or auth registry, no roster schema
|
|
change, no stopping or killing of seats, one record per seat.
|
|
|
|
Issue #1504. Plain ESM, no dependencies, Node 24 or newer.
|
|
|
|
## Commands
|
|
|
|
The repository wrapper is `scripts/mosaic`. It is not on PATH and is not
|
|
the npm-global `mosaic` CLI from the estate tooling, which has no `launch`
|
|
or `seat` command; run it by path.
|
|
|
|
```
|
|
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]
|
|
```
|
|
|
|
- `<seat>` is a name under `<repo>/agents/` (`--repo` defaults to the
|
|
current directory) or a path to a seat directory such as
|
|
`~/.mosaic/fleet/agents/orch-01`. Either way the directory must hold an
|
|
executable `launch.sh`.
|
|
- Everything after `--` goes to the launch script untouched.
|
|
- `launch` writes the registration, then replaces itself with the launch
|
|
script (`process.execve`). The seat keeps the same pid, terminal and
|
|
process group as if you had run `launch.sh` yourself, the launch script's
|
|
exit code is yours, and the tmux pane's foreground command stays the
|
|
harness, which the control board's liveness check depends on. Node marks
|
|
`process.execve` experimental (present since 24); it is the only way to
|
|
keep the seat's pid, so the package accepts that and pins Node 24 or
|
|
newer. The seat tests exercise it directly (exit passthrough).
|
|
- The launch script receives `MOSAIC_LAUNCH_REGISTERED=<record path>`. A
|
|
launch script that sees this variable is already registered and must not
|
|
call `mosaic launch` again; `mosaic launch` refuses to run when it is set.
|
|
- `seat task` records who set the task in `taskSetBy` (#1511): `--by NAME`
|
|
if given, else `$MOSAIC_AGENT_NAME` if set and non-empty, else the word
|
|
`unknown`. An explicit value must be one token of 1 to 64 characters
|
|
(letters, digits, `.` `_` `@` `:` `-`, starting with a letter or digit);
|
|
anything else refuses with exit 4 before the record is touched. A set but
|
|
malformed `MOSAIC_AGENT_NAME` also refuses rather than being reported as
|
|
`unknown`; `--by` is the explicit way past it. The value is never echoed
|
|
back in a refusal. **This is what the caller said, not a verified
|
|
identity.** Nothing checks it, nothing grants on it; the board displays
|
|
it next to the task and does no more with it. `launch --task` does not
|
|
set the field.
|
|
|
|
The four repository seats (`agents/darkwing`, `agents/dewey`,
|
|
`agents/filbert`, `agents/rocko`) register themselves: their `launch.sh`
|
|
re-enters through `scripts/mosaic launch` unless already registered or
|
|
called with `--check`. So `agents/darkwing/launch.sh` and
|
|
`scripts/mosaic launch darkwing --task "..."` are the same path; the second
|
|
form is how you attach a task. A `--check` run never writes a record.
|
|
|
|
## The record
|
|
|
|
`<dataRoot>/seats/<layout>/<seat>/registration.json`, directory 0700, file 0600,
|
|
written atomically. `layout` is `repo`, `fleet` or `unknown` (see below);
|
|
it is part of the path because a seat name alone is not unique, and
|
|
`seat task` refuses a bare name that is registered in more than one layout
|
|
until `--layout` says which. `dataRoot` comes from `~/.config/mosaic-dev/config.json`
|
|
(or `$MOSAIC_CONFIG`); a missing or unreadable config refuses the launch.
|
|
|
|
The record is rewritten on every launch. It is written before the launch
|
|
script runs, so a launch the script itself refuses (a failed `--check`-style
|
|
precondition, a missing context file) still leaves a record with a pid that
|
|
is no longer running; the next launch replaces it, and the board treats a
|
|
record whose pid is gone as stale (shown, but the derived values win). It is a launch record,
|
|
not a run record: it is not evidence, it holds one seat's latest launch only, and it
|
|
lives outside `<dataRoot>/board/` because the board never writes here and a
|
|
scan never changes it.
|
|
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"seat": "darkwing",
|
|
"project": "mosaic-stack",
|
|
"task": "Control board: seat registration (#1504)",
|
|
"workspace": "/mnt/storage/src/mosaic-stack",
|
|
"tmux": { "socket": null, "session": "darkwing" },
|
|
"harness": "pi",
|
|
"startedAt": "2026-09-12T16:20:11.000Z",
|
|
"pid": 431734,
|
|
"sessionsDir": "/mnt/storage/src/mosaic-stack/.pi/state/darkwing/sessions",
|
|
"seatDir": "/mnt/storage/src/mosaic-stack/agents/darkwing",
|
|
"launchScript": "/mnt/storage/src/mosaic-stack/agents/darkwing/launch.sh",
|
|
"layout": "repo",
|
|
"updatedAt": null
|
|
}
|
|
```
|
|
|
|
After `scripts/mosaic seat task darkwing "Row 6 (#1511)" --by jason` the
|
|
same record also carries `"taskSetBy": "jason"` and a non-null
|
|
`updatedAt`; `startedAt` and every other field are unchanged.
|
|
|
|
Fields asked for in the brief: `seat`, `project`, `task` (empty unless
|
|
`--task` or a later `seat task`), `workspace`, `tmux` (session name and
|
|
socket, from the `TMUX` variable of the pane the launch ran in; null outside
|
|
tmux), `harness` (only what `--harness` says; the repository launch scripts
|
|
pass `pi` or `claude-code`), `startedAt`, `pid`.
|
|
|
|
Fields added, and why:
|
|
|
|
- `sessionsDir`: how the board matches a record to a row. Seat names are not
|
|
unique across layouts (there is a `darkwing` in this repository and a
|
|
`darkwing` in the fleet), so the record names the sessions directory the
|
|
board already scans, and only an exact match counts.
|
|
- `seatDir`, `launchScript`, `layout`: what was launched and how the paths
|
|
were derived, so a wrong record can be traced without re-running anything.
|
|
`layout` is `repo` (`<repo>/agents/<seat>` with `<repo>/.git`), `fleet`
|
|
(`<seatDir>/.pi` exists) or `unknown` (nothing derived, `sessionsDir` null,
|
|
the record is still written but the board cannot match it).
|
|
- `updatedAt`: set only by `seat task`, so a task change is distinguishable
|
|
from a relaunch.
|
|
- `taskSetBy`: set only by `seat task` (see above). Optional in the shape:
|
|
a record written before the field existed loads unchanged, the board shows
|
|
`unknown` for it, and nothing rewrites it until the next `seat task` or
|
|
launch. The record version stays 1; a value that is not one bounded token
|
|
makes the record unreadable (refused, not misread).
|
|
- `version`: so a later shape change can be refused rather than misread.
|
|
|
|
`project` and `workspace` are derived only for the repo layout (the
|
|
repository's basename and root). For the fleet layout they are null unless
|
|
`--project` and `--workspace` are given, because the roster has no project
|
|
field and all fleet seats run in `~/.mosaic`; the board then keeps its own
|
|
derived values for those rows.
|
|
|
|
## How the board uses it
|
|
|
|
`packages/control-board` reads every `<dataRoot>/seats/<layout>/<seat>/registration.json`
|
|
on each scan. A registered task, project or workspace wins over the derived
|
|
value, and the row's `taskSource`, `activeProjectSource` or
|
|
`workspaceSource` says `registration`. An empty task or a null project or
|
|
workspace in the record leaves the derived value in place. Rows without a
|
|
registration are unchanged. A malformed record is reported in
|
|
`registrationErrors` on the index and skipped; it never takes the board down
|
|
and it is never treated as absent silently. A record whose pid is no
|
|
longer running is stale: still shown on the Registered line, but it does
|
|
not override anything. The row's `taskSetBy` is the record's setter only
|
|
while the task shown is the registered one (`taskSource` `registration`);
|
|
otherwise it is null, so a stale or empty registration never attributes a
|
|
transcript-derived task, and a Discord connector's fixed task never borrows
|
|
a native record's setter.
|
|
|
|
The repository launch scripts are the only launchers that register.
|
|
Fleet seats under `~/.mosaic` stay on their own launchers (Jason's ruling,
|
|
2026-09-12); a fleet seat can still be registered by hand with
|
|
`scripts/mosaic launch <seat-dir>`.
|
|
|
|
## Managed role sessions (slice 1 S6, #1523)
|
|
|
|
Separate from seats: `src/session.mjs` holds what the bus host's launcher
|
|
(`packages/cli/src/launcher.mjs`) uses to start and stop a managed role
|
|
session, and `src/proc.mjs` the `/proc` reads (start time, command line,
|
|
children) that the host, the launcher and `mosaic stop` share. Nothing here
|
|
decides authority or capacity; `packages/cli/README.md` ("Sessions") has the
|
|
launch flow and the on-disk layout.
|
|
|
|
- `newRun` picks a run id (`r` and 12 hex digits) and makes its 0700
|
|
directory, refusing a data root too deep for the run's socket path.
|
|
- `sessionEnv` builds the session's environment from an allowlist
|
|
(`ENV_ALLOW`), the repository's `node_modules/.bin` on `PATH`, and
|
|
`MOSAIC_RUN_ID`. Founder credential variables never pass.
|
|
- `spawnSession` starts `packages/harness/src/runner.mjs` under
|
|
`unshare --user --map-current-user --pid --fork --kill-child --mount-proc`,
|
|
detached, with stdin open for the capability line and output to
|
|
`runner.log` (0600). `runnerOf` finds the runner inside it.
|
|
- `Registry` is the host's list of running sessions, mirrored to
|
|
`bus-host/sessions.json` (0600); `readSessions` reads it and marks each
|
|
entry live or stale by the runner's start time, and refuses a file it
|
|
can't read.
|
|
- `stopSession` is `mosaic stop`: SIGTERM to a live runner, resent every
|
|
second until it exits.
|
|
|
|
Limits of the PID namespace, which are limits and not a sandbox:
|
|
|
|
- The runner is pid 1 of its namespace. A detached child is reparented to
|
|
it, not to the user's init, so it stays under the launch's pid. Pid 1
|
|
ignores a signal it has no handler for; the runner installs its handlers
|
|
first, and the senders resend SIGTERM to cover Node's startup.
|
|
- The namespace isolates pids only. With its own `/proc`, a session sees
|
|
no process outside it, so it can't signal or `ptrace` another session or
|
|
the host by pid (checked on this host, Yama `ptrace_scope` 1). It still
|
|
runs as the host's user, with that user's files, `/tmp`, network and
|
|
sockets.
|
|
- A session can still ask a process outside its namespace (a systemd user
|
|
manager, an existing tmux server) to run a command for it.
|
|
- The runner doesn't reap zombies other than its own turns; they go when
|
|
the namespace does.
|
|
|
|
## Exit codes
|
|
|
|
`launch` exits with the launch script's own code once the script runs.
|
|
Before that: 1 the launch script could not be started (the record is
|
|
removed again), 2 invalid data or configuration (missing config, no such
|
|
seat, no executable `launch.sh`, bad record on disk), 4 usage.
|
|
`seat task`: 0 ok, 1 no registration for that seat, 2 as above, 4 usage.
|
|
|
|
## Tests
|
|
|
|
```
|
|
node --test packages/seat/tests/
|
|
```
|
|
|
|
The repository launch scripts are covered end to end by
|
|
`scripts/test-darkwing-launch.mjs` (darkwing, dewey, filbert) and
|
|
`scripts/test-rocko-launch.mjs`, which run each `launch.sh` in a fixture
|
|
root that is also its own data root, so no registration reaches a real one.
|