diff --git a/8-Roadmap.md b/8-Roadmap.md index 959d195..d217ef8 100644 --- a/8-Roadmap.md +++ b/8-Roadmap.md @@ -232,9 +232,9 @@ commits** have landed on `main` with **no tag**. So the stage tables above are t the obvious question unanswered: what is actually left before the single-host story can be called finished? -**The answer, as of 2026-08-17: one implementation unit, CB-596.** Everything else in 1.1 is merged -and deployed. CB-596 looked like a single operator command; running that command is what showed it is -not. See *Open* below — the measurement was step 1 of six. +**The answer, as of 2026-08-17: no code. Two operator actions.** Every ticket in 1.1, CB-596 +included, is merged and running on the live daemon. What is left is the half of CB-596 that lives in +the operator's own secret store, plus the probe that proves it. See *Open* below. Gitea milestone: **`1.1 — single-host close-out`**. The admission rule is one sentence — **if it would still be broken with exactly one host, it belongs in 1.1.** Read strictly, that rule sent four @@ -268,9 +268,10 @@ flowchart LR class op,fed,defer todo ``` -*Figure: the 1.1 milestone by theme, as of 2026-08-16 night. Green is merged; amber is open. One -amber box is left, and it is the one no session can close: CB-596 needs the operator to run a probe -the command classifier refuses. Everything on the left is single-host work; the right is release 2.* +*Figure: the 1.1 milestone by theme, as of 2026-08-17. Green is merged and deployed; amber is open. +The one amber box on the left is CB-596, and its code is green — what is amber is the half that lives +in the operator's secret store, which no session may edit. Everything on the left is single-host +work; the right is release 2.* ### Done — merged and verified @@ -288,11 +289,11 @@ the command classifier refuses. Everything on the left is single-host work; the ### Open -One — and it grew when it was measured. +One — and its code has landed. What is open is a file no session may edit. | Theme | Ticket | The point | |---|---|---| -| **Credential scope** | CB-596 (#82) | CB-592 blocks one credential name. A member's pane runs a login shell that re-sources the operator's whole secret store, so the other names come straight back. **Measured 2026-08-17 inside a live member: 31 names, all set.** Criterion 1 is done; criteria 2–6 are an implementation unit, now delegated. | +| **Credential scope** | CB-596 (#82) | CB-592 blocks one credential name. A member's pane runs a login shell that re-sources the operator's whole secret store, so the other names come straight back. **Measured 2026-08-17 inside a live member: 31 names, all set** — later 34 once the gap detector ran. The fix merged (`ac40de1`) and is deployed (jar `e11160695fbe`): `memberCredentials:` in config, deny-by-default, 34 known / 7 allowed / 29 blocked, with a startup summary and a per-spawn gap warning. **The remaining half is the operator's:** the launcher writes the member's environment, and then the pane's login shell overwrites it, so the same block must be re-applied by a `BRIDGED_MEMBER`-guarded export at the end of the secret store. | **What the measurement actually said.** Two results, and they point in opposite directions. @@ -318,6 +319,20 @@ safe way to compare two readings on one machine and an unsafe thing to publish: has length 5 and hashes to `8c6976e5b541`, which is SHA-256 of `admin`. A truncated hash of a low-entropy value is not an anonymiser. The finding was written up with names and lengths only. +**What the fix found that no list contained.** The gap detector warns, on every spawn, about any +credential-shaped variable on neither `known:` nor `allow:`. On its very first spawn it named two: +`CLAUDE_CODE_MESSAGING_TOKEN` and `SSH_AUTH_SOCK`. Neither is in the secret store, so no list written +by reading that file could ever have held them. `SSH_AUTH_SOCK` is the serious one — a member needs +it to push, because worktree remotes are `ssh://`, but it lets a member sign with every key the +operator's agent holds. That is strictly broader than the repo-scoped forge token CB-302 built to +avoid exactly this. Allowed for now, with the reasoning written into the config; filed as **CB-607 +(#110)** on 2.0, fixed by pushing over HTTPS with `WORKER_GITEA_TOKEN`. + +The general lesson is worth more than the instance: **the enumeration was of a file, and the exposure +is of an environment.** Anything granted by a handle rather than by a value is invisible to a list +built by reading `secrets.sh` — which is the argument for having the daemon report the gap instead of +trusting the list. + ### Deferred to 2.0 — the cut decision Made 2026-08-16, by reading the admission rule strictly. @@ -332,15 +347,27 @@ Made 2026-08-16, by reading the admission rule strictly. ### Before the tag 1. ~~Land or defer CB-606 and CB-586.~~ Both merged and deployed. -2. **Land CB-596.** Its measurement is done and it is delegated; criteria 2–6 still have to land and - be verified. This is the last code in 1.1. -3. ~~Redeploy the daemon.~~ Done 2026-08-16: `scripts/redeploy-bridged.sh --yes`, 860 tests, BUILD - SUCCESS, jar `64908de1cb56`. **CB-596's fix will need another redeploy after it merges** — a merge - is not a deployment, and this one changes what a member's environment contains. -4. ~~Confirm `bridge_whoami` still answers `primary`.~~ Done: `primary`, leader `opus`. Note the trap - — the redeploy cuts the lead's own bridge MCP mount and it does not reconnect by itself, so this - check usually needs the operator to run `/mcp` first. -5. Then tag `v1.1.0` and mark this section closed. +2. ~~Land CB-596.~~ Merged `ac40de1`, 870 tests, BUILD SUCCESS. +3. ~~Redeploy the daemon.~~ Done twice. The second, 2026-08-17, put CB-596 live: jar + `e11160695fbe`, and the startup line reads + `memberCredentials: 34 known name(s), 5 allowed — blocking 29 on every spawn`. A merge is not a + deployment, and this change alters what a member's environment contains. +4. ~~Confirm `bridge_whoami` still answers `primary`.~~ Done: `primary`. Note the trap — the redeploy + cuts the lead's own bridge MCP mount. It reconnected by itself the second time and did not the + first, so do not rely on either; if the tools are gone, ask the operator to run `/mcp`. +5. **Two operator actions, in this order.** Neither can be done by any session. + 1. **Rotate `GITEA_ACCESS_TOKEN`.** A lead leaked about 31 characters of it into a transcript + while dumping the structure of the secret store: the redaction covered `export NAME=…` lines, + and the token is set on a line that begins with a guard, so it fell through. Reported at once; + the operator chose to rotate later. + 2. **Append the CB-596 block to the secret store.** A `BRIDGED_MEMBER`-guarded loop that exports + the same sentinel over the 29 blocked names. It is a drop-in block that moves, copies and + retypes no value; the daemon's blocked set and that block were diffed name-by-name and agree. +6. **Then run the verification probe:** spawn a member and run `scripts/probe-member-credentials.sh`. + The pass condition is that the 29 blocked names come back at the sentinel's length and hash, and + the 7 allowed keep their real lengths. That measurement is what lets #82 close honestly — the + config half alone does not prove anything, because the login shell runs after it. +7. Then tag `v1.1.0` and mark this section closed. **On "Stage 5 ✅ production shape".** That row was never wrong — CB-504 did build a launchd agent and a systemd unit. But a unit in `deploy/` that no host has loaded is not supervision. CB-594 and CB-600