plans: row 87 round 3 adds the host channel and keeps bindLaunch, answering Darkwing's and Filbert's round 2 reviews (#1581)
Darkwing L (comment 28863): section 3 gains "The host channel, a boundary change". The bus host starts the core container with `docker run -i` as its own child; core-entry.mjs forks process.mjs and notifier-process.mjs with IPC inside it and relays tagged JSON lines over the client's stdin and stdout. The core container runs with --log-driver none, since replies carry caps; I6 tests that. The channel adds no authentication of its own, so it is boundary change B2. Darkwing M: the registry keeps the docker run client's pid and startTime, so bindLaunch, runtime.mjs and broker.mjs are unchanged and I5 changes no file in packages/bus. The human proof's ancestry check never meets the client, so the mount list carries that boundary and I5 tests that a session can't reach sock/human or sock/relay. recover() removes the container, kills a leftover client and binds as today. Darkwing nits: the start check's temp names are patterns from their writers, five state files, and the git-socket.mjs split across I3, I5 and I6. Filbert (comment 28864): section 10 states the Console's outbound reach on the mosaic-console bridge, no wider than today, with the --network none forwarder as the not-spiked follow-up named on P19 333. Section 7 says four networks and lists mosaic-console. Section 11 lists B1 (the Console bind, before I4) and B2 (the host channel, before I6); I4's and I6's gates name their ruling. Doc-only: no test reads either file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -4,7 +4,8 @@ Status: written by Sage, lead, as row 87 (#1581). Reviewers: Darkwing and
|
||||
Filbert. Brief: `docs/plans/2026-10-12_i1-containerized-install.md`, § I1
|
||||
design. The I2 to I6 briefs are in
|
||||
`docs/plans/2026-10-18_containerized-install-rows.md`. Round 2 answers
|
||||
Filbert's review (comment 28851) and Darkwing's (comment 28852).
|
||||
Filbert's review (comment 28851) and Darkwing's (comment 28852). Round 3
|
||||
answers their round 2 reviews (Darkwing 28863, Filbert 28864).
|
||||
|
||||
This doc turns decisions 105 to 108 into interfaces. It doesn't reopen
|
||||
them. Every claim about container behavior cites a spike in
|
||||
@@ -86,6 +87,46 @@ trust as today's bus host. The notifier moves into the broker's
|
||||
container, because decision 105 lets only the broker hold service
|
||||
credentials, and the Discord token is one.
|
||||
|
||||
### The host channel, a boundary change
|
||||
|
||||
Today the bus host forks the broker and the notifier with an IPC channel
|
||||
(`packages/cli/src/host.mjs:215-217`, `242-244`). Everything the host
|
||||
trusts them with goes over it: the boot config, `bindLaunch`, the launch
|
||||
ops (`packages/bus/src/process.mjs:5-12`) and the notifier's reader
|
||||
capability (`host.mjs:244`). Without the channel the broker refuses to
|
||||
run, `trusted-host-required` (`process.mjs:57-59`). A container can't be
|
||||
forked, so containerized mode needs another channel.
|
||||
|
||||
The channel is the `docker run` client's stdin and stdout. The bus host
|
||||
starts the core container as its own child with `docker run -i`, without
|
||||
`-t`, the way the launcher starts a session's client (section 6). Inside,
|
||||
a new entry, `packages/cli/src/core-entry.mjs`, runs under `--init`. It
|
||||
forks `process.mjs` and `notifier-process.mjs` with IPC, as the host does
|
||||
today, so neither file changes and `trusted-host-required` still holds.
|
||||
The entry reads one JSON line per message from stdin, tagged `broker` or
|
||||
`notifier`, and passes it to that fork. It writes each reply as one JSON
|
||||
line on stdout with the same tag. The forks' stdout and stderr go to the
|
||||
container's stderr, so stdout carries only replies. EOF on stdin closes
|
||||
both forks, as a disconnect does today (`process.mjs:107`).
|
||||
|
||||
Replies carry capabilities: the boot reply holds the reader caps
|
||||
(`host.mjs:239`) and each `bindLaunch` reply a launch cap. The engine
|
||||
keeps a container's stdout in its log driver, so the core container runs
|
||||
with `--log-driver none`. S8 measured that a stdin line appears nowhere
|
||||
in inspect, top, logs, `/proc/1/environ`, cmdline or ps, but stdout
|
||||
wasn't part of it. That the client still gets stdout under
|
||||
`--log-driver none`, and that no cap reaches `docker logs` or the
|
||||
daemon's container directory, are not spiked. I6 tests both.
|
||||
|
||||
The boundary change is that the channel adds no authentication of its
|
||||
own. The writer on stdin is whoever holds the client's pipe, and that's
|
||||
the bus host. The other ways in are `docker attach` and `docker exec`
|
||||
through the engine endpoint. A caller with the endpoint can already run
|
||||
anything in the container, so the channel is no weaker than the
|
||||
container it reaches. Decision 106 lets only a host process hold the
|
||||
endpoint, and decision 105 accepts the Jason-uid routes to it. This
|
||||
change needs a ruling before I6 is queued (section 11).
|
||||
|
||||
The control board (`packages/control-board`) isn't part of the
|
||||
containerized install. It drives tmux seats on the host, and section 8
|
||||
replaces its reply path for the Console.
|
||||
@@ -216,9 +257,11 @@ makes the move with the socket paths. What stays in `bus-host/` is
|
||||
`host.json`, `host.lock`, `sessions.json`, `boot-status.json` and
|
||||
`boot-events.jsonl` (`packages/cli/src/host.mjs:35-37`,
|
||||
`boot-status.mjs:13-15`, `packages/seat/src/session.mjs:47`), plus the
|
||||
temporary names their writers make and remove: `<file>.<suffix>.tmp`
|
||||
(`host.mjs:83,122`, `boot-status.mjs:34`, `session.mjs:222`) and
|
||||
`host.lock.<suffix>.stale` (`host.mjs:84`).
|
||||
temporary names their writers make and remove. As patterns:
|
||||
`<file>\.[0-9]+\.tmp` (`host.mjs:122`, `boot-status.mjs:34`,
|
||||
`session.mjs:222`), and `host\.lock\.[0-9]+\.[0-9a-f]{8}\.tmp` and
|
||||
`host\.lock\.[0-9]+\.[0-9a-f]{8}\.stale` (`host.mjs:82-84`), where
|
||||
`<file>` is one of the five names. The number is the writer's pid.
|
||||
|
||||
The local install keeps its single `<store>/broker.sock` and
|
||||
`bus-host/launch.sock` and `bus-host/git.sock`, unchanged.
|
||||
@@ -445,17 +488,31 @@ out of the environment it builds for the adapter, and I5 tests that.
|
||||
|
||||
### The session record
|
||||
|
||||
In containerized mode the registry entry (`launcher.mjs:326-328`)
|
||||
replaces the four pid fields: pid, startTime, runnerPid and
|
||||
runnerStartTime. In their place it records:
|
||||
In containerized mode the registry entry (`launcher.mjs:326-328`) keeps
|
||||
`pid` and `startTime`, which are now the `docker run` client's, the
|
||||
launcher's child. It replaces `runnerPid` and `runnerStartTime`, since
|
||||
the runner lives in the container's pid namespace. In their place it
|
||||
records:
|
||||
|
||||
- `container`: the name;
|
||||
- `containerId`;
|
||||
- `endpoint`;
|
||||
- `containerStartedAt`: `.State.StartedAt` from inspect.
|
||||
|
||||
`bindLaunch` binds the run to `{containerId, containerStartedAt}`
|
||||
instead of `{pid, startTime}`.
|
||||
`bindLaunch` is unchanged and binds the run to the client's
|
||||
`{pid, startTime}` (`launcher.mjs:323`). The broker requires an integer
|
||||
pid and a numeric start time (`packages/bus/src/runtime.mjs:84-91`) and
|
||||
refuses unknown keys (`packages/bus/src/broker.mjs:379-383`), and both
|
||||
stay as they are. I5 changes no file in `packages/bus`.
|
||||
|
||||
The human proof's launch check walks the caller's host ancestry and
|
||||
refuses a process that matches a launch record
|
||||
(`packages/bus/src/human.mjs:80`). A container's processes descend from
|
||||
the engine's shim, not from the client, so that walk never meets the
|
||||
client's pid. In containerized mode the boundary is the mount list
|
||||
instead: no session gets `sock/human` or `sock/relay`, so a session
|
||||
process can't reach either. The ancestry claim is not spiked. I5 tests
|
||||
that a session process can't connect to either socket.
|
||||
|
||||
- **Liveness.** `docker --host <endpoint> inspect -f '{{.State.Running}} {{.State.StartedAt}}' <id>`
|
||||
reports `true` with the recorded start. That replaces
|
||||
@@ -479,8 +536,9 @@ instead of `{pid, startTime}`.
|
||||
today kills each leftover runner, binds its launch again, ends the run
|
||||
`host-lost`, records the end and logs it. That ending is what releases
|
||||
the role. In containerized mode only the kill changes. For each
|
||||
leftover entry it runs `docker rm -f <containerId>`, then calls
|
||||
`bindLaunch` with `{containerId, containerStartedAt}`, then
|
||||
leftover entry it runs `docker rm -f <containerId>` and kills the
|
||||
leftover client if `startTimeOf(pid)` still matches, then calls
|
||||
`bindLaunch` with the entry's `{pid, startTime}` as today, then
|
||||
`endRun(run, "host-lost")`, the `end` record and the log line, as
|
||||
501-510 do today. After the leftovers, it lists containers labelled
|
||||
`mosaic.business=<business>` and removes, with `docker rm -f`, any
|
||||
@@ -534,13 +592,16 @@ get that directory.
|
||||
|
||||
### Egress
|
||||
|
||||
There are three networks:
|
||||
There are four networks:
|
||||
|
||||
- **`mosaic-agents`** (`internal: true`): sessions, the pi worker, and
|
||||
the agents proxy.
|
||||
- **`mosaic-services`** (`internal: true`): the broker and the services
|
||||
proxy.
|
||||
- **`mosaic-egress`**: a normal bridge. Only the two proxies join it.
|
||||
- **`mosaic-console`**: a normal bridge for the Console alone, because
|
||||
an internal network publishes nothing (S5, section 8). It has no
|
||||
proxy, and section 10 states the outbound reach that leaves.
|
||||
|
||||
Each proxy answers on its internal network under the alias `proxy`, port
|
||||
3128, and takes CONNECT only. On an internal network the alias `proxy`
|
||||
@@ -646,7 +707,7 @@ request, a slow client and a connect to a port that isn't listed.
|
||||
be a regular file. It refuses any other entry, including a socket, a
|
||||
directory, a symlink and a fifo, and the refusal names the entry. A
|
||||
rule like "refuse anything that passes for a credentials file" can't
|
||||
work, because the four state files are small 0600 files owned by the
|
||||
work, because the five state files are small 0600 files owned by the
|
||||
host user and pass the credentials rule (`credentials.mjs:60-72`).
|
||||
- **What it never gets:** the dataRoot as a whole, `bus/`, `git-host/`,
|
||||
the relay, the agent socket, the config directory, the secrets
|
||||
@@ -695,6 +756,13 @@ request, a slow client and a connect to a port that isn't listed.
|
||||
that the broker can't reach a provider host. I4 tests that the
|
||||
Console's port answers on host 127.0.0.1 and not on the host's other
|
||||
addresses.
|
||||
- **The host channel.** I6 tests boot, `bindLaunch` and the notifier's
|
||||
start over the client's stdio, that `process.mjs` started without the
|
||||
entry still refuses `trusted-host-required`, that EOF stops both
|
||||
forks, and that no cap reaches `docker logs`, inspect or the daemon's
|
||||
container directory (section 3).
|
||||
- **The human sockets from a session.** I5 tests that a session process
|
||||
can't connect to `sock/human` or `sock/relay` (section 6).
|
||||
- **The Console's start and routes.** In containerized mode `loadSystem`
|
||||
passes in the container. The `bus-host/` start check accepts the five
|
||||
state files and a leftover `.tmp`, and refuses a socket, an unknown
|
||||
@@ -747,6 +815,16 @@ writes them.
|
||||
Console binds all addresses inside its own network namespace, and the
|
||||
host-loopback publish, not the process's bind check, keeps it off
|
||||
other addresses (section 8).
|
||||
- **The Console's outbound reach.** `mosaic-console` is a normal bridge
|
||||
with no proxy, so a compromised Console can open a connection to any
|
||||
host. That's no wider than today, where the Console is a host process
|
||||
with the host's network. No measured option narrows it: an internal
|
||||
network publishes nothing (S5). A later row could run the Console with
|
||||
`--network none` behind a host forwarder from 127.0.0.1:7332 to a unix
|
||||
socket, which is also the cheapest fix named on P19 333. Not spiked.
|
||||
- **The host channel.** Anything with the engine endpoint can attach to
|
||||
the core container's stdin and act as the bus host, as it can already
|
||||
exec in that container (section 3).
|
||||
- **The session token.** A session can read and copy its token. The
|
||||
egress list limits where it can send it, and a copy can land in the
|
||||
workspace on the host disk (decision 108).
|
||||
@@ -770,6 +848,14 @@ writes them.
|
||||
Decision 96's order puts I2 to I6 after D1 and D2, and I4 lands before
|
||||
M3 retires T3 Mos.
|
||||
|
||||
Two boundary changes need a ruling before their row is queued:
|
||||
- B1, the Console's bind (section 8), before I4;
|
||||
- B2, the host channel (section 3), before I6.
|
||||
|
||||
`packages/cli/src/git-socket.mjs` is split three ways: I3 owns the
|
||||
`RESOLVED_VIA` entry, I5 the socket and `git-host/` paths, and I6
|
||||
`dropNetwork`'s `--host`.
|
||||
|
||||
The follow-ups from the I1 review that this doc doesn't own:
|
||||
- the worker's whole-dataRoot mount at `compose.yaml:40`, which I6
|
||||
replaces;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Status: written by Sage, lead, as part of row 87 (#1581). Round 2
|
||||
answers Filbert's review (comment 28851) and Darkwing's (comment 28852).
|
||||
Round 3 answers their round 2 reviews (Darkwing 28863, Filbert 28864).
|
||||
Each section
|
||||
follows `docs/plans/BRIEF-TEMPLATE.md`. The design they build is
|
||||
`docs/plans/2026-10-18_containerized-install-design.md`, cited below as
|
||||
@@ -19,6 +20,9 @@ separate re-pin plan with Mos's go.
|
||||
Rules every row keeps:
|
||||
- Never `docker` without `--host`. In containerized mode every call goes
|
||||
through the helper from I2.
|
||||
- `packages/cli/src/git-socket.mjs` is split three ways: I3 owns the
|
||||
`RESOLVED_VIA` entry, I5 the socket and `git-host/` paths, and I6
|
||||
`dropNetwork`'s `--host`. Each row touches only its part.
|
||||
- No Docker volume, daemon or restart change. A test's networks and
|
||||
containers carry a name prefix of the row, and its cleanup removes them
|
||||
and only them.
|
||||
@@ -282,7 +286,8 @@ Authentik's configuration.
|
||||
|
||||
Filbert and Dewey each approve on the row's issue naming the candidate
|
||||
manifest. Sage runs the full gate green on the landing tree. Needs I2 and
|
||||
I3 done. Lands before M3.
|
||||
I3 done, and the B1 ruling on the Console's bind (design section 11)
|
||||
before the row is queued. Lands before M3.
|
||||
|
||||
---
|
||||
|
||||
@@ -315,9 +320,11 @@ token in a file and egress only through the proxy.
|
||||
### What ships
|
||||
|
||||
- The launch from design section 6, with the record, liveness, stop,
|
||||
exit and recovery rules there. `recover()` still ends each leftover run
|
||||
`host-lost` after removing its container. In local mode, bwrap and the
|
||||
pid record stay unchanged.
|
||||
exit and recovery rules there. The registry keeps the `docker run`
|
||||
client's `pid` and `startTime`, and `bindLaunch` binds to them
|
||||
unchanged, so this row changes no file in `packages/bus`. `recover()`
|
||||
still ends each leftover run `host-lost` after removing its container.
|
||||
In local mode, bwrap and the pid record stay unchanged.
|
||||
- The socket move from design section 5: in containerized mode the
|
||||
launch and git sockets live in `sock/launch/` and `sock/git/`, and the
|
||||
git socket's bundle files and `land/` logs live in `<dataRoot>/git-host/`
|
||||
@@ -351,6 +358,7 @@ token in a file and egress only through the proxy.
|
||||
role, then removes a labelled container the registry doesn't name;
|
||||
- in containerized mode nothing of the git socket's lands in
|
||||
`bus-host/`;
|
||||
- a session process can't connect to `sock/human` or `sock/relay`;
|
||||
- `mosaic stop <run>` stops a container session;
|
||||
- each launch refusal from design section 6, including
|
||||
`endpoint-not-rootless` and `no-session-credential`.
|
||||
@@ -390,7 +398,9 @@ and no document states the limits in design section 10.
|
||||
|
||||
- `packages/cli/src/` where `mosaic bus start` starts the broker,
|
||||
notifier and egress containers in containerized mode, with
|
||||
`scripts/bus-service.sh`.
|
||||
`scripts/bus-service.sh`, and a new `packages/cli/src/core-entry.mjs`
|
||||
for the host channel (design section 3). `packages/bus/src/process.mjs`
|
||||
and `packages/cli/src/notifier-process.mjs` don't change.
|
||||
- `compose.yaml`, `scripts/run-task.sh` and the worker adapter files it
|
||||
uses.
|
||||
- The scripts listed in I2's problem, moved to `scripts/lib/docker.sh`.
|
||||
@@ -431,6 +441,12 @@ and no document states the limits in design section 10.
|
||||
in each of those;
|
||||
- the two internal networks, each with its own proxy: an agent can't
|
||||
reach a services host, and the broker can't reach a provider host;
|
||||
- the host channel: boot, `bindLaunch` and the notifier's start work
|
||||
over the client's stdin and stdout; `process.mjs` started in the
|
||||
container without the entry still refuses `trusted-host-required`;
|
||||
EOF on stdin stops both forks; under `--log-driver none` the client
|
||||
still gets every reply, and no cap appears in `docker logs`,
|
||||
inspect or the daemon's container directory;
|
||||
- a pi worker task runs in containerized mode and writes its run
|
||||
record;
|
||||
- each moved script refuses `endpoint-not-rootless` against the
|
||||
@@ -448,4 +464,5 @@ Jason's time. Changing the rootful daemon or the `docker` group (decision
|
||||
|
||||
Filbert and Darkwing each approve on the row's issue naming the
|
||||
candidate manifest. Sage runs the full gate green on the landing tree.
|
||||
Needs I4 and I5 done.
|
||||
Needs I4 and I5 done, and the B2 ruling on the host channel (design
|
||||
section 11) before the row is queued.
|
||||
|
||||
Reference in New Issue
Block a user