plans: row 87 round 2 answers Filbert's and Darkwing's reviews of the containerized install design (#1581)
The Console binds 0.0.0.0 in its own namespace behind a host-loopback publish (a decision 107 enforcement change, reach unchanged). It mounts businesses/ and its other mounts at their host paths, checks bus-host/ against an allowlist, turns the queue panel off, and answers 404 on every board route. The git socket's bundles and land logs move to git-host/. recover() still ends leftover runs host-lost. RESOLVED_VIA gets a human-socket entry, and every container kind gets mount-list and hardening tests. Spike citations are corrected, and the root case and two-network setup are marked as not spiked. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -3,7 +3,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`.
|
||||
`docs/plans/2026-10-18_containerized-install-rows.md`. Round 2 answers
|
||||
Filbert's review (comment 28851) and Darkwing's (comment 28852).
|
||||
|
||||
This doc turns decisions 105 to 108 into interfaces. It doesn't reopen
|
||||
them. Every claim about container behavior cites a spike in
|
||||
@@ -32,8 +33,10 @@ Every Mosaic container runs as container uid 0, which is the host user
|
||||
(S2: `uid_map 0 1000 1`), with `--cap-drop ALL` and
|
||||
`--security-opt no-new-privileges`.
|
||||
|
||||
Why not S2's recommended split, a non-root broker uid with a group per
|
||||
socket:
|
||||
Why not S2's recommended split? S2 recommends a non-root broker uid with
|
||||
one gid of its own, each client a non-root uid given `--group-add` with
|
||||
that gid, and the sockets kept apart by which directory each container
|
||||
mounts (S:166-169). The reasons against it:
|
||||
|
||||
- Files a non-root container uid creates are owned by `100999`-style
|
||||
uids that the host user can't read or change, and the host user can't
|
||||
@@ -44,19 +47,26 @@ socket:
|
||||
container.
|
||||
- The Credentials file rule (`packages/bus/src/credentials.mjs:60-72`)
|
||||
requires the file's uid to equal `process.getuid()` and the mode to be
|
||||
exactly 0600. In a root container, the host user's files show as uid 0,
|
||||
and the process is uid 0, so the rule passes unchanged.
|
||||
exactly 0600. In a root container, the host user's files show as owner
|
||||
0:0 (HP:120-121), and the process is uid 0, so the rule passes
|
||||
unchanged. Container root reads the host user's 0600 file as its owner
|
||||
(S:148-149).
|
||||
- With `--cap-drop ALL`, container root gets no DAC override (S2: a root
|
||||
client with cap-drop ALL is refused at a 0660 socket it neither owns
|
||||
nor is grouped on). Owner access still works, so a socket is reachable
|
||||
exactly where its directory is mounted.
|
||||
|
||||
The tradeoff, stated plainly: the mount list is the access control. A
|
||||
wrong mount isn't caught by a uid check behind it. I5 and I4 therefore
|
||||
test each container's mount list exactly (section 9), not just that the
|
||||
expected mounts exist. The host user can already reach everything these
|
||||
containers hold, so a container escape lands where a host-side agent
|
||||
already is (decision 95's limit), not higher.
|
||||
wrong mount isn't caught by a uid check behind it. So every container
|
||||
kind gets a test of its whole mount list (section 9), not just that the
|
||||
expected mounts exist: the Console in I4, sessions in I5, and the broker
|
||||
and notifier, both egress proxies and the pi worker in I6.
|
||||
|
||||
A container escape would land at the host user, because container uid 0
|
||||
is host uid 1000 (S:146-147) and the rootless daemon runs as that user.
|
||||
That's a reading of the uid map. No spike tried an escape. The host user
|
||||
can already reach everything these containers hold, so an escape lands
|
||||
where a host-side agent already is (decision 95's limit), not higher.
|
||||
|
||||
## 3. Components
|
||||
|
||||
@@ -87,6 +97,8 @@ replaces its reply path for the Console.
|
||||
(REPORT.md:19). It's pinned by digest, and a bump is a reviewed commit.
|
||||
- `mosaic-core`: the base plus the repository's packages at the release
|
||||
commit and `RELEASE`. The broker, notifier and Console run from it.
|
||||
It has no `git`, which the base image lacks, so the Console's queue
|
||||
panel is off in containerized mode (section 8).
|
||||
- `mosaic-session`: `mosaic-core` plus Claude Code and pi at the
|
||||
versions `package.json` and the session code pin today. The host's
|
||||
`MOSAIC_CLAUDE_BIN` isn't used in containerized mode.
|
||||
@@ -100,6 +112,11 @@ From the I1 review and S7 and S9:
|
||||
- `--read-only`, `--tmpfs /tmp:rw,noexec,nosuid`. Sessions also get
|
||||
`--tmpfs` for HOME (S7: Claude writes `.claude*` under HOME and
|
||||
`claude-1000/` and `cc-socks/` under /tmp, and pi writes `.pi/agent/`).
|
||||
- The root case is unmeasured. Sessions run as container root, but S6
|
||||
and S7 ran Claude and pi as `--user 1000:1000` (S:352-354, S:433). The
|
||||
tmpfs list above and the egress list in section 7 rest on those runs.
|
||||
I5 repeats S7's write check and S6's proxy check as container root
|
||||
before either list or the limits below are final.
|
||||
- `--cap-drop ALL`, `--security-opt no-new-privileges`, and the default
|
||||
seccomp profile.
|
||||
- `--pids-limit`, `--memory` with `--memory-swap` set to the same value
|
||||
@@ -113,8 +130,13 @@ From the I1 review and S7 and S9:
|
||||
| broker | 256 | 1g | 1 |
|
||||
| Console | 128 | 512m | 0.5 |
|
||||
| egress | 64 | 128m | 0.5 |
|
||||
- Sessions run with `--init`, so `docker stop` delivers SIGTERM and the
|
||||
container exits 143 (S8).
|
||||
- Sessions run with `--init`. S8 measured `docker stop` on an `--init`
|
||||
container with no SIGTERM handler: tini forwards the signal and the
|
||||
container exits 143 in 18 ms (S:493). The session runner has no handler
|
||||
today, so 143 is its clean stop. With a handler it would be 0 (S:494).
|
||||
S8 measured 137 after the grace period only without `--init` (S:491).
|
||||
That a runner which ignores the forwarded TERM also ends 137 under
|
||||
`--init` is inferred, and I5 measures it.
|
||||
|
||||
S9 didn't test io or bandwidth limits, ulimits or a custom seccomp
|
||||
profile, and this design adds none of them.
|
||||
@@ -146,9 +168,10 @@ rootless check runs before every use of the endpoint:
|
||||
docker --host <endpoint> info --format '{{json .SecurityOptions}}'
|
||||
```
|
||||
|
||||
The check passes only if the exit status is 0 and the output contains
|
||||
`name=rootless`. S1 says to test the exit status first, because a missing
|
||||
socket exits 1 and prints `null`. `--host` overrides `DOCKER_HOST` (S1).
|
||||
The check tests the exit status first, because a missing socket exits 1
|
||||
and prints `null`. Then it parses the output as a JSON array and passes
|
||||
only if one element is exactly `name=rootless` (S:100-103). A substring
|
||||
match isn't enough. `--host` overrides `DOCKER_HOST` (S1).
|
||||
Everything that runs docker in containerized mode goes through one helper
|
||||
that adds `--host <endpoint>` and runs this check first. A failure
|
||||
refuses with `endpoint-not-rootless` and starts nothing.
|
||||
@@ -181,9 +204,21 @@ directory).
|
||||
|
||||
`<dataRoot>/sock/` and each subdirectory are 0700, owned by the host
|
||||
user. Every path is checked against the 107-byte limit, the way
|
||||
`newRun` checks `tools.sock` (`packages/seat/src/session.mjs`). In
|
||||
containerized mode `bus-host/` holds no socket, which section 8 relies
|
||||
on.
|
||||
`newRun` checks `tools.sock` (`packages/seat/src/session.mjs`).
|
||||
|
||||
In containerized mode `bus-host/` holds no socket and nothing of the git
|
||||
socket's, which section 8 relies on. Today the git socket keeps its
|
||||
bundle files and its landing logs there (`dir` at
|
||||
`packages/cli/src/git-socket.mjs:277`, used at 349 for `land/<id>/` and
|
||||
409 for `<hex>.bundle`). In containerized mode that `dir` is
|
||||
`<dataRoot>/git-host/` (0700, host user), which no container mounts. I5
|
||||
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`).
|
||||
|
||||
The local install keeps its single `<store>/broker.sock` and
|
||||
`bus-host/launch.sock` and `bus-host/git.sock`, unchanged.
|
||||
@@ -193,17 +228,22 @@ The local install keeps its single `<store>/broker.sock` and
|
||||
In containerized mode the broker serves both sockets with the same
|
||||
verbs. What differs is the envelope each one accepts:
|
||||
|
||||
- **Agent listener.** Capability envelopes only. A `human` or `tester`
|
||||
envelope gets `human-required`, as the spike measured (S4, HP).
|
||||
- **Agent listener.** Capability envelopes only. A `human` envelope gets
|
||||
`human-required`, as S4 and HP measured (S:239-241, HP:150). A
|
||||
`tester` envelope gets the same answer by design. No spike sent one.
|
||||
- **Human listener.** `human` and `tester` envelopes. A capability
|
||||
envelope gets `invalid-envelope`. Reaching the socket is the proof
|
||||
(decision 105), so `authenticateHuman` here looks up the business and
|
||||
calls `bindHuman` with `via: "human-socket"`. It reads no `/proc`. HP
|
||||
measured that no container can read a host process's environ, even
|
||||
with `--pid=host`.
|
||||
measured that container root with `--cap-drop ALL` and `--pid=host`
|
||||
gets EACCES on a host process's environ, and so does the same with
|
||||
`--cap-add SYS_PTRACE`, and so does uid 1001 (HP:64-66, 149). That no
|
||||
container can read it is HP's reading of the kernel source, which HP
|
||||
marks as not spiked (HP:68).
|
||||
|
||||
`serve()` (`packages/bus/src/server.mjs:132-137`) takes the path and mode
|
||||
for each listener rather than hard-coding one. Today it refuses to start
|
||||
`serve()` (`packages/bus/src/server.mjs:13`, with the listen and the
|
||||
`chmodSync(path, 0o600)` at 132-137) takes the path and mode for each
|
||||
listener rather than hard-coding one. Today it refuses to start
|
||||
over an existing socket (14-19). After a kill -9 the old file remains and
|
||||
answers ECONNREFUSED (S3). So a broker started in containerized mode
|
||||
removes a socket at its own listener path only if the path is a socket
|
||||
@@ -220,8 +260,8 @@ date for that verb.
|
||||
### The new `via` value and what reads it
|
||||
|
||||
`human-socket`. Every place that writes or reads a human `via` at
|
||||
8517e5f45 (the working tree's broker.mjs carries another row's
|
||||
uncommitted edits, so these are HEAD lines):
|
||||
8d47d6184. The working tree's broker.mjs carries another row's
|
||||
uncommitted edits, so these are committed lines:
|
||||
|
||||
- `packages/bus/src/runtime.mjs:117` binds with `via: 'cli'`. The human
|
||||
listener's bind passes `human-socket`.
|
||||
@@ -240,11 +280,21 @@ uncommitted edits, so these are HEAD lines):
|
||||
- `packages/webui/src/public/bus.js:188` labels a decision event
|
||||
`human` only for `cli`. It accepts either. That's a change under
|
||||
`public/`, so decision 123 applies to I3's review.
|
||||
- `packages/cli/src/git-socket.mjs:252` maps `via` to the squash
|
||||
message's text (`RESOLVED_VIA`: `cli` is "via the CLI", `broker` is
|
||||
"as its arbiter, via the broker"), and 259 falls back to
|
||||
`via ${f.via}`. Without an entry, a merge Jason resolved over the human
|
||||
socket would say "via human-socket" in its squash message, the row 137
|
||||
r3 provenance problem. I3 adds `human-socket` with the text "via the
|
||||
human socket".
|
||||
- `packages/tasks/live/run.mjs:108` stays `cli`. It's the local live run.
|
||||
|
||||
I3 adds one exported helper, `isHumanVia(via)`, in `packages/bus`, and
|
||||
every reader above uses it, so a third value can't miss a reader. I3's
|
||||
tests cover each reader with `human-socket`.
|
||||
every reader above that decides whether the human acted uses it, so a
|
||||
third value can't miss a reader. `RESOLVED_VIA` is a text map, not a
|
||||
check, so it gets its own entry instead. I3's tests cover each reader
|
||||
with `human-socket`, and the squash message of a merge resolved over the
|
||||
human socket.
|
||||
|
||||
The trail doesn't distinguish the Console from the CLI in containerized
|
||||
mode. Both arrive on the human socket, and a field the client sets would
|
||||
@@ -258,7 +308,8 @@ broker, because the proof needs the host's `/proc` (HP). The relay, HP's
|
||||
option B, is a host process in the bus host:
|
||||
|
||||
- It listens on `<dataRoot>/sock/relay/human.sock` (0600).
|
||||
- It runs `verifyHuman` unchanged (`packages/bus/src/runtime.mjs`).
|
||||
- It runs `verifyHuman` unchanged (`packages/bus/src/human.mjs:52`,
|
||||
called today at `runtime.mjs:111`).
|
||||
`cliPath` is the host path of `human-cli.mjs` in the tree the bus host
|
||||
runs from (the path-bound proof, decision 102).
|
||||
- It adds the peer-pid check: the claimed pid must equal the connecting
|
||||
@@ -302,10 +353,15 @@ on the host. Decision 95's own case stays open for host processes. The
|
||||
Console is published on its configured loopback port, `127.0.0.1:7332`
|
||||
in the live install today. 7330 is only the bare `serve` default.
|
||||
Reaching it is the proof (decision 105), and the Authentik header only
|
||||
names the user. A loopback Host skips the header check entirely
|
||||
(`packages/webui/README.md`, "What the proxy must do"). So any host
|
||||
process that can reach that port can act as Jason, including a process
|
||||
under another host uid. Today's install already has that reach.
|
||||
names the user. A request with a loopback Host skips the header check
|
||||
(`packages/webui/README.md`, "What the proxy must do"), but in preview
|
||||
mode it is read-only, since every POST but the named ones is refused
|
||||
(`packages/webui/src/serve.mjs:288-302`). Acting as Jason takes the
|
||||
public Host plus exactly one header equal to the allowed user (270,
|
||||
283-287). Any local process can send both, because the Console can't
|
||||
tell the proxy from another client on the port. So any host process that
|
||||
can reach that port can act as Jason, including a process under another
|
||||
host uid. Today's install already has that reach.
|
||||
dragon-lin has no other login user. A process under Jason's uid has
|
||||
three more routes. It can connect to the human socket directly. HP
|
||||
measured that against 0666, and here the socket is 0600, but the host
|
||||
@@ -324,9 +380,9 @@ Mos ruled on this reading (mos-0657, 2026-10-18):
|
||||
accepted "any same-UID process" for the PoC only, and said it doesn't
|
||||
carry the limit into the containerized install. Decision 105 carries
|
||||
only the Jason-uid case. Accepting the other-uid reach is Jason's call.
|
||||
It goes to him in the PM report and as Vikunja P19 333, with the
|
||||
It goes to Jason in the PM report and as Vikunja P19 333, with the
|
||||
default that it stays a documented limit. It doesn't block this
|
||||
design or I2 to I5. Only I6's limits section waits on his answer.
|
||||
design or I2 to I5. Only I6's limits section waits on Jason's answer.
|
||||
|
||||
The cheapest fix if Jason doesn't accept it: the Console serves a 0600
|
||||
unix socket in a host directory instead of a published TCP port, so only
|
||||
@@ -405,19 +461,32 @@ instead of `{pid, startTime}`.
|
||||
reports `true` with the recorded start. That replaces
|
||||
`startTimeOf(runnerPid) === runnerStartTime`
|
||||
(`packages/seat/src/session.mjs:230-241`). A missing container counts
|
||||
as dead.
|
||||
as dead. Not spiked. I5 tests it.
|
||||
- **Stop.** `docker stop --time 30 <id>`. That's the 30 s `stopSession`
|
||||
gives today (247-268). With `--init` a clean stop exits 143, and a
|
||||
stop after the grace period exits 137 (S8). `close()` in the launcher
|
||||
(566-600) does the same, then `docker kill` if `stopTimeoutMs` passes.
|
||||
gives today (247-268). S8 measured, under `--init`, 143 in 18 ms for a
|
||||
process with no TERM handler (S:493), and 0 for one whose handler exits
|
||||
cleanly (S:494). The session runner has no TERM handler today, so 143
|
||||
is its clean stop. A stop after the grace period exits 137, which S8
|
||||
measured without `--init` (S:491). That it's 137 under `--init` too is
|
||||
inferred, and I5 measures it. `close()` in the launcher (566-600) does
|
||||
the same, then `docker kill` if `stopTimeoutMs` passes.
|
||||
- **Exit.** The `docker run` client is the launcher's child, and its
|
||||
exit status is the container's. Containers run without `--rm`, so the
|
||||
launcher can still read the status with inspect if the client died.
|
||||
The launcher removes the container once it has recorded the exit.
|
||||
- **Recovery.** `recover()` (489-513) lists containers labelled
|
||||
`mosaic.business=<business>`. It removes, with `docker rm -f`, every
|
||||
one whose run isn't live in the registry, and logs each one to the
|
||||
launch log.
|
||||
The launcher removes the container once it has recorded the exit. Not
|
||||
spiked. I5 tests it.
|
||||
- **Recovery.** `recover()` (`packages/cli/src/launcher.mjs:491-513`)
|
||||
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
|
||||
`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
|
||||
that the registry doesn't name, logging each to the launch log. The
|
||||
label filter and `rm -f` are not spiked. I5 tests all of it, including
|
||||
that a leftover run ends `host-lost` and its role is free again.
|
||||
|
||||
The local install keeps its bwrap path and pid record unchanged.
|
||||
|
||||
@@ -474,13 +543,14 @@ There are three networks:
|
||||
- **`mosaic-egress`**: a normal bridge. Only the two proxies join it.
|
||||
|
||||
Each proxy answers on its internal network under the alias `proxy`, port
|
||||
3128, and takes CONNECT only. An internal network has no DNS and no route
|
||||
out, CONNECT to a listed host returns 200, and any other host gets 403
|
||||
(S5).
|
||||
3128, and takes CONNECT only. On an internal network the alias `proxy`
|
||||
resolves (S:311), there is no external DNS and no route out (S:329),
|
||||
CONNECT to a listed host returns 200, and any other host gets 403 (S5).
|
||||
|
||||
Two containers run from one image so that each network gets its own
|
||||
list. An agent can't reach the services list, and the broker can't reach
|
||||
a provider host.
|
||||
a provider host. S5 ran one internal network. Two internal networks with
|
||||
a proxy each are not spiked, and I6 tests them.
|
||||
|
||||
The lists live in `contracts/egress.json`, which the egress image bakes
|
||||
in. A change is a reviewed commit and a new image, like any contract.
|
||||
@@ -489,8 +559,11 @@ The two keys:
|
||||
- **`agents`.** The provider hosts for the models in the model table.
|
||||
Claude Code needs `api.anthropic.com:443`. With
|
||||
`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` it doesn't need the
|
||||
datadog intake (S6). I2 measures pi's provider hosts with a real token,
|
||||
because S6 used a made-up one and left one `UPSTREAM-ERROR` unexplained.
|
||||
datadog intake (S6). I2 measures both CLIs' provider hosts with a real
|
||||
token. Both of S6's caveats are Claude's. S6 used a made-up token
|
||||
(S:410-413), and its `UPSTREAM-ERROR` run ended with Claude's "Not
|
||||
logged in" (S:416-418). pi didn't touch the network with no key
|
||||
(S:372), so its hosts are unmeasured.
|
||||
- **`services`.** The Vikunja host (tasks.woltje.com), the Gitea host and
|
||||
Discord's API and gateway hosts. I2 lists each with the code path that
|
||||
needs it.
|
||||
@@ -498,9 +571,12 @@ The two keys:
|
||||
Both CLIs honour `HTTPS_PROXY` in both cases, and pi ignores `ALL_PROXY`
|
||||
(S6).
|
||||
|
||||
Not spiked: whether the broker's and notifier's own `fetch` honours the
|
||||
proxy on node 24.21.0 (`NODE_USE_ENV_PROXY=1`). I2 measures it. If it
|
||||
doesn't, I2 stops and asks before adding a dependency.
|
||||
S5 measured node's `fetch` through the proxy on node 24.21.0 with
|
||||
`NODE_USE_ENV_PROXY=1` and `HTTPS_PROXY`, and got the provider's HTTP 401
|
||||
(S:320, image at S:19, cited by S6 at S:388-389). What isn't measured is
|
||||
whether the broker's and notifier's own code paths go through `fetch`
|
||||
with that variable set. I2 checks each path. If one doesn't, I2 stops and
|
||||
asks before adding a dependency.
|
||||
|
||||
The proxy writes one JSON line per CONNECT to stdout,
|
||||
`{at, list, host, port, allowed}`. The bus host appends those lines to
|
||||
@@ -519,34 +595,85 @@ request, a slow client and a connect to a port that isn't listed.
|
||||
a container only on an internal network accepts `-p` and publishes
|
||||
nothing, so the bridge is required, and I4 checks that the port
|
||||
answers. Authentik forwards to it as today.
|
||||
- **Mounts:**
|
||||
- `sock/human`;
|
||||
- `config.json` as a read-only file mount (the config is never
|
||||
rewritten, so S3's dead-inode problem doesn't arise; a config change
|
||||
needs a Console restart, as `loadSystem` at start already does);
|
||||
- `bus-host/` read-only, for `host.json` and `sessions.json`
|
||||
(`bus.mjs:65`, `reads.mjs:125-132,234`). This is safe only because
|
||||
`bus-host/` holds no socket in containerized mode (section 5). I4
|
||||
adds a check at Console start that refuses if `bus-host/` holds a
|
||||
socket or anything that passes for a credentials file.
|
||||
- **What it never gets:** the dataRoot, `bus/`, the relay, the agent
|
||||
socket, the secrets directory, or the notifier config file. Today
|
||||
`readNotifyConfig` (`reads.mjs:229`) reads the file. In containerized
|
||||
mode I4 replaces it with a human read verb on the broker, since the
|
||||
notifier lives there.
|
||||
- **Other reads.** `RELEASE` comes from the image. The Board's queue
|
||||
panel runs `queue-read.mjs` in a checkout (`reads.mjs:75-79`). I4 mounts
|
||||
the canonical checkout read-only for it, which is safe because the
|
||||
repository holds no secrets (invariant 3). If the config names no
|
||||
checkout, I4 turns the panel off.
|
||||
- **Reply path out of tmux.** Today POST `/api/reply` goes to the
|
||||
control board, which runs `tools/tmux/agent-send.sh`
|
||||
(`packages/control-board/src/serve.mjs:59,74-94,239-248`). In
|
||||
containerized mode the Console has no route to the board, and the
|
||||
route answers 404. A reply to a managed instance is a human
|
||||
`message.send` over the human socket, the path row 76 built. The
|
||||
board's tmux liveness check (`scan.mjs:196-209`) has no containerized
|
||||
counterpart, because the sessions list comes from `sessions.json`.
|
||||
- **Bind address, a decision 107 enforcement change.** `startServer`
|
||||
refuses a non-loopback bind (`packages/webui/src/serve.mjs:252-253`).
|
||||
Inside the container that can't work: Docker forwards a published port
|
||||
to the container's bridge address, so a server bound to the
|
||||
container's 127.0.0.1 is unreachable through `-p`. In containerized
|
||||
mode the Console binds `0.0.0.0` inside its own network namespace, and
|
||||
the publish stays on host `127.0.0.1`. The in-container port equals the
|
||||
published port, because the loopback-Host check compares the Host's
|
||||
port with `server.address().port` (277). The reach doesn't change: the
|
||||
only way in is still host loopback on that port. What changes is where
|
||||
that's enforced, from the bind check in the process to the publish in
|
||||
the engine. Local mode keeps the loopback refusal. Not spiked. I4 tests
|
||||
that the port answers on host 127.0.0.1 and not on the host's other
|
||||
addresses.
|
||||
- **Mounts.** Each lands at its host path, so `loadSystem`,
|
||||
`configDir()` and the reads resolve as they do today and no path code
|
||||
changes:
|
||||
- `<dataRoot>/sock/human/`, the directory;
|
||||
- `<configDir>/config.json`, a read-only file mount. The config is
|
||||
never rewritten, so S3's dead-inode problem doesn't arise, and a
|
||||
config change needs a Console restart, as `loadSystem` at start
|
||||
already does;
|
||||
- `<configDir>/businesses/`, read-only, for `loadBusiness` and
|
||||
`launchSettings` (`reads.mjs:187-195,234`,
|
||||
`packages/business/src/business.mjs:25-26`). Never the config
|
||||
directory itself, which holds `secrets/`;
|
||||
- `<dataRoot>/bus-host/`, read-only, for `host.json`, `sessions.json`
|
||||
and `boot-status.json` (`bus.mjs:65`, `reads.mjs:125-132,216,234`).
|
||||
|
||||
`MOSAIC_CONFIG` is set to the host path of `config.json`.
|
||||
`mosaic-config.mjs` requires a regular non-symlink file there, and a
|
||||
dataRoot that is absolute and doesn't contain the home directory or the
|
||||
config directory (`scripts/mosaic-config.mjs:41-44,50-54,76-100`). Its home
|
||||
check reads the container's HOME, which differs from the host's. That
|
||||
`loadSystem` passes in the container isn't spiked, and I4 tests it.
|
||||
`rolesDir` comes from the image (`packages/cli/src/config.mjs:35`). I4 changes
|
||||
`packages/webui/src/cli.mjs:58` so that in containerized mode the
|
||||
broker path is `<dataRoot>/sock/human/broker.sock` instead of
|
||||
`socketPath(dataRoot)`. The Console keeps `humanCall`, now pointed at
|
||||
the human socket. `human-cli` already carries `business` in its proof
|
||||
(`packages/bus/src/human-cli.mjs:28`).
|
||||
- **The `bus-host/` start check.** The mount is safe only because
|
||||
`bus-host/` holds no socket and nothing of the git socket's in
|
||||
containerized mode (section 5). At start, in containerized mode, the
|
||||
Console lists `bus-host/` and accepts only the five state files and
|
||||
their writers' temporary names: `host.json`, `host.lock`,
|
||||
`sessions.json`, `boot-status.json`, `boot-events.jsonl`, any of them
|
||||
followed by `.<suffix>.tmp`, and `host.lock.<suffix>.stale`. Each must
|
||||
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
|
||||
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
|
||||
directory, or the notifier config file. Today `readNotifyConfig`
|
||||
(`reads.mjs:229`) reads the file. In containerized mode I4 replaces it
|
||||
with a human read verb on the broker, since the notifier lives there.
|
||||
- **The queue panel is off.** `RELEASE` comes from the image. The
|
||||
queue panel runs `queue-read.mjs`, which spawns `git` in the checkout
|
||||
`--checkout` names (`reads.mjs:75-79`, `cli.mjs:30-35`,
|
||||
`packages/queue/src/store.mjs:51-90`). `mosaic-core` has no `git`
|
||||
(section 3), so in containerized mode the queue read answers a fixed
|
||||
refusal without spawning anything. A later row can add `git` to the
|
||||
image and a same-path checkout mount with `--checkout`. In local mode,
|
||||
if `--checkout` is absent the panel reads the Console's own copy, as
|
||||
today.
|
||||
- **No route to the board.** `/api/board`, `/api/conversations` and
|
||||
`/api/conversation` (GET), and `/api/seen` and `/api/reply` (POST), are
|
||||
proxied to the control board today (`serve.mjs:304-323`). In
|
||||
containerized mode each answers 404 and the Console opens no
|
||||
connection to the board. Local mode keeps all five. A reply to a
|
||||
managed instance is a human `message.send` over the human socket, the
|
||||
path row 76 built. The board's `/api/reply` runs
|
||||
`tools/tmux/agent-send.sh`
|
||||
(`packages/control-board/src/serve.mjs:59,74-94,239-248`), and its
|
||||
tmux liveness check (`scan.mjs:196-209`) has no containerized
|
||||
counterpart, because the sessions list comes from `sessions.json`. I4
|
||||
tests each route.
|
||||
- **The 107 limit.** A compromised Console holds human authority. Its
|
||||
route list and CSP limit what it can do. I4 lists every route in the
|
||||
README with what it can reach.
|
||||
@@ -555,30 +682,50 @@ request, a slow client and a connect to a port that isn't listed.
|
||||
|
||||
- **Mount lists, exactly.** For each container kind, a test reads
|
||||
`docker inspect` `.Mounts` and compares the whole list to the expected
|
||||
list. Section 2's tradeoff depends on it.
|
||||
list. Section 2's tradeoff depends on it. I4 tests the Console's, I5 a
|
||||
session's, and I6 the broker and notifier's, each egress proxy's and
|
||||
the pi worker's.
|
||||
- **Hardening, every kind.** `/proc/self/status` shows CapEff 0,
|
||||
NoNewPrivs 1 and Seccomp 2 (S9), and the root filesystem is read-only.
|
||||
I4 tests the Console, I5 a session, and I6 the broker and notifier,
|
||||
both egress proxies and the pi worker.
|
||||
- **Networks.** A session can't resolve or reach any host except
|
||||
through `proxy` (S5). The Console's port answers on 127.0.0.1. The
|
||||
broker can't reach a provider host.
|
||||
- **Hardening.** A session's `/proc/self/status` shows CapEff 0,
|
||||
NoNewPrivs 1 and Seccomp 2 (S9).
|
||||
through `proxy` (S5), and I5 repeats S6's proxy check as container
|
||||
root. I6 tests the two internal networks, each with its own proxy, and
|
||||
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 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
|
||||
file, a planted `launch.sock` or `git.sock`, a `land/` directory and a
|
||||
`.bundle`, naming the entry. Each board-proxied route answers 404, and
|
||||
each is unchanged in local mode. The queue read refuses without
|
||||
spawning. All in I4.
|
||||
- **Envelopes and `via`.** A human envelope on the agent socket gets
|
||||
`human-required`, and a capability on the human socket gets
|
||||
`invalid-envelope`. A decision resolved through the human socket
|
||||
records `via: human-socket`.
|
||||
records `via: human-socket`, and a merge it resolves says "via the
|
||||
human socket" in its squash message.
|
||||
- **The relay:**
|
||||
- a genuine CLI passes;
|
||||
- an agent marker in the CLI or an ancestor is refused;
|
||||
- a snooped nonce on another connection is refused, in both modes;
|
||||
- with no reader the relay refuses.
|
||||
- **The stop matrix.** `docker stop` gives 143, and a container that
|
||||
ignores TERM gives 137 after the grace period (S8). After a launcher
|
||||
restart, `recover()` removes an orphan.
|
||||
- **The stop matrix.** Under `--init`, `docker stop` on the runner gives
|
||||
143 (S:493). A container that ignores TERM gives 137 after the grace
|
||||
period, which I5 measures under `--init`. After a launcher restart,
|
||||
`recover()` removes the leftover container, ends its run `host-lost`
|
||||
and frees its role, then removes a labelled container the registry
|
||||
doesn't name.
|
||||
- **The root case.** I5 repeats S7's write check as container root
|
||||
before the tmpfs list is final.
|
||||
- **The token.** It is absent from inspect, top, logs and the container
|
||||
config. The adapter's child has it, and the runner's other children
|
||||
don't.
|
||||
- **The rootless refusal.** The rootful socket, a missing socket and a
|
||||
non-rootless daemon each refuse with `endpoint-not-rootless`. The
|
||||
missing-socket case covers S1's `null` output.
|
||||
- **The rootless refusal.** The rootful socket and a missing socket each
|
||||
refuse with `endpoint-not-rootless`, and the rootless endpoint passes,
|
||||
as in I2. The missing-socket case covers S1's `null` output.
|
||||
|
||||
## 10. Limits the docs state
|
||||
|
||||
@@ -589,14 +736,17 @@ writes them.
|
||||
loopback port passes as human through the Console, and one under
|
||||
Jason's uid also passes directly, through the relay or through the
|
||||
engine (decisions 95 and 105, section 5). The other-uid half of this
|
||||
line waits on Jason's answer, P19 333 (section 5). I6 writes what he
|
||||
rules. REQ-DEC-3 holds against
|
||||
agent containers and against accidental or inherited launches.
|
||||
line waits on Jason's answer, P19 333 (section 5). I6 writes what
|
||||
Jason rules. REQ-DEC-3 holds against agent containers and against
|
||||
accidental or inherited launches.
|
||||
- **The rootful daemon.** Jason is in the `docker` group, so any host
|
||||
process under his uid can reach the rootful daemon, which is
|
||||
process under Jason's uid can reach the rootful daemon, which is
|
||||
root-equivalent (decision 106).
|
||||
- **The Console.** A compromised Console holds human authority, within
|
||||
its route list and CSP (decision 107).
|
||||
its route list and CSP (decision 107). In containerized mode the
|
||||
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 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).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# The containerized install: rows I2 to I6 (2026-10-18)
|
||||
|
||||
Status: written by Sage, lead, as part of row 87 (#1581). Each section
|
||||
Status: written by Sage, lead, as part of row 87 (#1581). Round 2
|
||||
answers Filbert's review (comment 28851) and Darkwing's (comment 28852).
|
||||
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
|
||||
"the design" with its section number. A builder follows the design and
|
||||
@@ -33,9 +35,13 @@ Nothing builds the containerized install's images, the config can't name
|
||||
a docker endpoint, and every script reaches Docker through the ambient
|
||||
context (`scripts/hello.sh:24`, `build.sh:13`, `release.sh:39,122`,
|
||||
`agent.sh:208`, `verify.sh:36,39,45`, `land-work.sh:87-88`,
|
||||
`test-release.sh:58`, `mosaic-task.mjs:396`). S1 showed that the ambient
|
||||
context can be the rootful daemon. Decision 105's egress rule has no
|
||||
proxy that the repository owns, only the 43-line spike tool.
|
||||
`test-release.sh:58`, `test-task.sh:139,395,442,517`,
|
||||
`mosaic-task.mjs:396`, and `docker network rm` in `dropNetwork` at
|
||||
`packages/cli/src/git-socket.mjs:243`). The `test-release.sh` and
|
||||
`test-task.sh` lines are `docker info` probes. With `DOCKER_HOST` and the
|
||||
context unset, the default is the system socket, and S1 queried that
|
||||
socket directly and found it rootful (S:25, 84-87). Decision 105's egress
|
||||
rule has no proxy that the repository owns, only the 43-line spike tool.
|
||||
|
||||
### Owner and reviewer
|
||||
|
||||
@@ -69,10 +75,14 @@ proxy that the repository owns, only the 43-line spike tool.
|
||||
case asserts the exit code and that the config file is unchanged.
|
||||
- `scripts/lib/docker.sh`: reads the mode and endpoint through
|
||||
`scripts/load_config`. It adds `--host` and runs the rootless check
|
||||
before the first call in a process. On any failure it exits with
|
||||
before the first call in a process. The check tests the exit status,
|
||||
then parses the output as a JSON array and passes only if one element
|
||||
is exactly `name=rootless` (design section 4, S:100-103). A substring
|
||||
match isn't enough. On any failure it exits with
|
||||
`endpoint-not-rootless` on stderr. `test-install.sh` covers the rootful
|
||||
socket, a missing socket (S1's exit 1 and `null`), and the rootless
|
||||
endpoint passing.
|
||||
socket, a missing socket (S1's exit 1 and `null`), an output that holds
|
||||
`name=rootless` only as a substring, and the rootless endpoint
|
||||
passing.
|
||||
- `packages/egress`: the CONNECT proxy from design section 7, reading one
|
||||
list from `contracts/egress.json` by name. Tests cover allow, deny, a
|
||||
malformed request, a slow client, a port that isn't listed and the JSON
|
||||
@@ -82,11 +92,15 @@ proxy that the repository owns, only the 43-line spike tool.
|
||||
- `contracts/egress.json`, with `agents` and `services` lists. Each entry
|
||||
names the code path that needs it. The two measurements below settle
|
||||
the lists:
|
||||
- Whether node 24.21.0's `fetch` in the broker honours `HTTPS_PROXY`
|
||||
with `NODE_USE_ENV_PROXY=1`, measured with no credential against
|
||||
the services hosts. If it doesn't, stop and tell Sage before adding a
|
||||
dependency.
|
||||
- The provider hosts Claude Code and pi need with a real token. The row
|
||||
- S5 already measured node 24.21.0's `fetch` through the proxy with
|
||||
`NODE_USE_ENV_PROXY=1` and `HTTPS_PROXY` (S:320). Don't repeat it.
|
||||
Check that each of the broker's and notifier's outbound code paths
|
||||
goes through `fetch` with that variable set, with no credential,
|
||||
against the services hosts. If one doesn't, stop and tell Sage before
|
||||
adding a dependency.
|
||||
- The provider hosts Claude Code and pi need with a real token. S6's
|
||||
run used a made-up token, and pi made no network call without a key
|
||||
(design section 7). The row
|
||||
ships the script and the expected output. Mos runs it and posts the
|
||||
output on the row's issue. The list is final when that output
|
||||
matches.
|
||||
@@ -134,7 +148,9 @@ today's proof accepts a nonce snooped from another connection.
|
||||
native.
|
||||
- `packages/webui/src/public/bus.js` (line 188 only). Decision 123
|
||||
applies.
|
||||
- `packages/cli/src/` where the bus host starts the relay.
|
||||
- `packages/cli/src/` where the bus host starts the relay, and
|
||||
`packages/cli/src/git-socket.mjs` for the `RESOLVED_VIA` entry only,
|
||||
with `packages/cli/tests/git-socket.test.mjs`.
|
||||
- `packages/bus/tests/`, `packages/webui/tests/` for the line 188 case,
|
||||
`packages/bus/README.md`, `docs/TOOLS.md`.
|
||||
- `agents/dewey/work/queue-<row>/`.
|
||||
@@ -153,6 +169,10 @@ today's proof accepts a nonce snooped from another connection.
|
||||
- `isHumanVia`, and every reader in design section 5 uses it. Each
|
||||
reader has a test with `human-socket`. The landing test shows a merge
|
||||
Jason resolved over the human socket lands.
|
||||
- `RESOLVED_VIA` (`packages/cli/src/git-socket.mjs:252`, used at 259)
|
||||
gains `human-socket: "via the human socket"`. The git-socket test shows
|
||||
the squash message of a merge resolved over the human socket says "via
|
||||
the human socket", and the existing "via the CLI" case is unchanged.
|
||||
- The relay: `verifyHuman` and `verifyTester` unchanged, plus the
|
||||
peer-pid check, forwarding to the human socket. The peer-pid check also
|
||||
goes into the local install's proof. Tests: a genuine CLI passes; an
|
||||
@@ -211,22 +231,43 @@ the broker.
|
||||
### What ships
|
||||
|
||||
- The Console container from design section 8: its image, network,
|
||||
published port, mounts and limits. It reaches the broker only through
|
||||
the mounted human socket.
|
||||
- A start check that refuses when `bus-host/` holds a socket, or a file
|
||||
that passes the credentials file rule.
|
||||
published port, mounts at their host paths, `MOSAIC_CONFIG`, and
|
||||
limits. It reaches the broker only through the mounted human socket:
|
||||
`cli.mjs:58` uses `<dataRoot>/sock/human/broker.sock` in containerized
|
||||
mode and keeps `humanCall`.
|
||||
- The bind from design section 8. In containerized mode the Console
|
||||
binds `0.0.0.0` in its own network namespace on the published port,
|
||||
and the publish stays on host `127.0.0.1`. Local mode keeps the
|
||||
loopback refusal (`serve.mjs:252-253`).
|
||||
- The `bus-host/` start check from design section 8, an allowlist of the
|
||||
five state files and their writers' `.tmp` and `.stale` names. Any
|
||||
other entry refuses, and the refusal names it.
|
||||
- The notifier read through the new broker verb in containerized mode.
|
||||
The file read stays in local mode.
|
||||
- The queue panel through a read-only checkout mount, or off when the
|
||||
config names no checkout.
|
||||
- `/api/reply` answers 404 in containerized mode. A reply is a human
|
||||
`message.send` over the human socket.
|
||||
- The README lists every route with what it can reach (decision 107).
|
||||
- Tests: the exact mount list from inspect; the port answers on
|
||||
127.0.0.1 and not on another address; a signed-in decision resolves and
|
||||
records `via: human-socket`; the start check refuses each bad
|
||||
`bus-host/` case; `/api/reply` is 404 in containerized mode and
|
||||
unchanged in local mode.
|
||||
- The queue panel is off in containerized mode: the read answers a fixed
|
||||
refusal without spawning. In local mode, if `--checkout` is absent, it
|
||||
reads the Console's own copy as today.
|
||||
- `/api/board`, `/api/conversations`, `/api/conversation`, `/api/seen`
|
||||
and `/api/reply` answer 404 in containerized mode, and the Console
|
||||
opens no connection to the board. A reply is a human `message.send`
|
||||
over the human socket.
|
||||
- The README lists every route with what it can reach (decision 107),
|
||||
and states the bind change.
|
||||
- Tests:
|
||||
- the exact mount list from inspect;
|
||||
- CapEff 0, NoNewPrivs 1, Seccomp 2 and a read-only root filesystem
|
||||
in the Console container;
|
||||
- `loadSystem` passes in the container, with `MOSAIC_CONFIG` at the
|
||||
host path and the container's own HOME;
|
||||
- the port answers on host 127.0.0.1 and not on the host's other
|
||||
addresses;
|
||||
- a signed-in decision resolves and records `via: human-socket`;
|
||||
- the start check accepts the five files and a leftover `.tmp`, and
|
||||
refuses a socket, an unknown file, a planted `launch.sock` and
|
||||
`git.sock`, a `land/` directory and a `.bundle`;
|
||||
- each of the five board routes answers 404 in containerized mode and
|
||||
is unchanged in local mode;
|
||||
- the queue read refuses in containerized mode without spawning.
|
||||
- Suites: webui, bus, and every `scripts/test-*.sh` green.
|
||||
|
||||
### Out of scope
|
||||
@@ -263,7 +304,8 @@ token in a file and egress only through the proxy.
|
||||
|
||||
### Files owned
|
||||
|
||||
- `packages/cli/src/launcher.mjs`, `packages/seat/src/session.mjs`,
|
||||
- `packages/cli/src/launcher.mjs`, `packages/cli/src/git-socket.mjs`
|
||||
(the socket and `git-host/` paths only), `packages/seat/src/session.mjs`,
|
||||
`packages/seat/src/sandbox.mjs`, a new
|
||||
`packages/seat/src/container.mjs`, the session runner, and their tests.
|
||||
- `packages/seat/README.md`, `packages/cli/README.md`, `docs/TOOLS.md`.
|
||||
@@ -273,8 +315,17 @@ 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. In local mode, bwrap and the pid record
|
||||
stay unchanged.
|
||||
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.
|
||||
- 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/`
|
||||
(0700), which no container mounts. Local mode keeps `bus-host/`.
|
||||
- The root re-measure from design section 3. Before the tmpfs list and
|
||||
the limits are final, the row repeats S7's write check and S6's proxy
|
||||
check with the session running as container root, and records both in
|
||||
BUILD.md.
|
||||
- The token file check before launch, the runner's read, and the token
|
||||
only in the adapter's environment (design section 7). The `docker run`
|
||||
environment holds no provider variable.
|
||||
@@ -284,14 +335,22 @@ token in a file and egress only through the proxy.
|
||||
- Tests:
|
||||
- the exact mount list from inspect, including what a role without the
|
||||
launch or git grant doesn't get;
|
||||
- CapEff 0, NoNewPrivs 1 and Seccomp 2 in the session;
|
||||
- CapEff 0, NoNewPrivs 1, Seccomp 2 and a read-only root filesystem
|
||||
in the session;
|
||||
- a session reaches a listed host only through `proxy` and can't
|
||||
resolve any other host;
|
||||
- the token is absent from inspect, top, logs and the container
|
||||
config, present in the adapter's child and absent from the runner's
|
||||
other children;
|
||||
- the stop matrix (143 on stop, 137 after the grace period);
|
||||
- `recover()` removes an orphan after a launcher restart;
|
||||
- the stop matrix under `--init`: 143 on stop (S:493), and 137 after
|
||||
the grace period, which no spike measured under `--init`;
|
||||
- liveness from inspect, a missing container counted as dead, and the
|
||||
exit status read through the client and through inspect;
|
||||
- after a launcher restart, `recover()` removes the leftover
|
||||
container, ends its run `host-lost`, records the end and frees the
|
||||
role, then removes a labelled container the registry doesn't name;
|
||||
- in containerized mode nothing of the git socket's lands in
|
||||
`bus-host/`;
|
||||
- `mosaic stop <run>` stops a container session;
|
||||
- each launch refusal from design section 6, including
|
||||
`endpoint-not-rootless` and `no-session-credential`.
|
||||
@@ -335,6 +394,9 @@ and no document states the limits in design section 10.
|
||||
- `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`.
|
||||
`dropNetwork` (`packages/cli/src/git-socket.mjs:243`) is JavaScript and
|
||||
can't source the helper, so in containerized mode it passes `--host`
|
||||
with the configured endpoint after the same rootless check.
|
||||
- `scripts/bootstrap.sh` only to print the install steps. It still never
|
||||
writes the config.
|
||||
- `README.md` (the install section), `docs/TOOLS.md`, `LAYERS.md`,
|
||||
@@ -357,13 +419,22 @@ and no document states the limits in design section 10.
|
||||
- The install section of the README: the steps, the config key, and
|
||||
every limit in design section 10, each with its decision number. The
|
||||
other-uid half of the host-process limit says what Jason ruled on
|
||||
P19 333. If he hasn't ruled when I6 reaches that section, the row
|
||||
P19 333. If Jason hasn't ruled when I6 reaches that section, the row
|
||||
waits on it.
|
||||
`LAYERS.md` L4 and `CONDUCTOR.md:17,40` describe what is built.
|
||||
- Tests: the whole install starts and stops cleanly, and leaves no
|
||||
container or network. A pi worker task runs in containerized mode and
|
||||
writes its run record. Each moved script refuses `endpoint-not-rootless`
|
||||
against the rootful socket in containerized mode.
|
||||
- Tests:
|
||||
- the whole install starts and stops cleanly, and leaves no container
|
||||
or network;
|
||||
- the exact mount list from inspect for the broker and notifier
|
||||
container, each egress container and the pi worker;
|
||||
- CapEff 0, NoNewPrivs 1, Seccomp 2 and a read-only root filesystem
|
||||
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;
|
||||
- a pi worker task runs in containerized mode and writes its run
|
||||
record;
|
||||
- each moved script refuses `endpoint-not-rootless` against the
|
||||
rootful socket in containerized mode.
|
||||
- Suites: every `scripts/test-*.sh` and the node suites of each touched
|
||||
package green.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user