carlos / platform Public

docs: cohort-1 runbook: second session step 6: 6.1 and 6.1a PASS, 6.2 STOP, closed

6.1: policy.v0.28.4.json 2114cdd0 (floor_sha256 1a6a7e50); identity
v0.28.4 published 09:17:58Z, identity.v0.28.4.json 995e3c85, staged on
leo and sophie. 6.1a: both boxes' carlos and carlos.prev now 736826e4.
6.2: guard-arm on leo refused (hostfloor: authenticated floor required):
/opt/carlos is carlos:carlos (host-init chown), and OpenProtected needs
root-owned parents. Session closed in full; CloudTrail clean (one key
read, from the signer). Also corrects 6.1's deb names to 0.28.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 1ddc6185bb60c50ed9a7a4fdbe8c481d7f282f94 parent 14727e5
1 file changed, +36 −4
  • docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md +36 −4

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m

amadan: job cancelled — a newer push superseded it
diff --git a/docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md b/docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md
index c8a08ead8..ec90ac427 100644
--- a/docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md
+++ b/docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md
@@ -36,8 +36,8 @@ Final, 2026-10-19. Every value below was read or verified read-only on DOCS-T1 b
2. **Dev roll, boxes only. DONE** (platform-1e: 0 refusing, no panics, wakecheck 200; verified above). Stop rule for any re-check: a box not `current` at v0.28.4, `refused` or `rolled-back`; instance-control not restarted (H4).
3. **5.0a, promote-only** (approved). **DONE 2026-10-19 07:58Z, PASS**: `x-carlos-version: f3bf1214`, digests `yes`, P9 PASS, no rollback (record under 5.0a). Ship and promote `f3bf1214` to `canary/console` with the v0.28.4 amd64 CLI, then re-run P9. Stop: the console's sha256 is not `8d49199f…5e9b`; a pointer row `written after digests were turned on`; the header or `readlink` not `f3bf1214`; any P9 `SET` line or console-mail role policy. Rollback: promote `7c1beb2d`.
4. **Open the session** (`signing-host.md`, "Create"). platform-1e confirms with Paul first. This puts the release key on a host a second time. Stop: a checksum mismatch; `carlos version` not `carlos v0.28.4`; `claims help` not as quoted; a key file that is not root `600`. **DONE 2026-10-19 09:12Z to 09:14Z, PASS**: signer `i-006e3c79dfdf5d814`, SG `sg-0a0f98545441cf295`, run-id `cohort1-20261019T091304Z`, session start `2026-10-19T09:14:14Z` (record under "5.1 in the second session").
-5. **5.1, resumed** ("5.1 in the second session"): the box re-check, then `refresh` only, never `initialize`. Stop: revision not 5, the listing not as recorded, or a staged file missing or changed. **DONE 2026-10-19 09:15Z, PASS**: revision 5, listing as recorded, every staged file unchanged. The signer is up with the key in tmpfs, waiting for step 6.
-6. **Step 6**: 6.1 `sign-policy` and `publish-identity` (new `--out` paths) and staging under new paths; 6.1a the `-h` check, then `install-channel --policy --identity` on leo, then sophie; 6.2 `guard-arm` on leo; 6.3 P9 (third run), `install-console`, `guard-arm --console`, `console-config`, the restart, and the wait for both boxes' `floor_guard_ready`. Stop: any refusal (6.1a lists its refusals, the `--identity` ones included); an identity value other than the expected ones above; any 6.1a output other than its expected lines; a P9 `SET` line or console-mail role policy; `console.service` not `active`.
+5. **5.1, resumed** ("5.1 in the second session"): the box re-check, then `refresh` only, never `initialize`. Stop: revision not 5, the listing not as recorded, or a staged file missing or changed. **DONE 2026-10-19 09:15Z, PASS**: revision 5, listing as recorded, every staged file unchanged.
+6. **Step 6**: 6.1 `sign-policy` and `publish-identity` (new `--out` paths) and staging under new paths; 6.1a the `-h` check, then `install-channel --policy --identity` on leo, then sophie; 6.2 `guard-arm` on leo; 6.3 P9 (third run), `install-console`, `guard-arm --console`, `console-config`, the restart, and the wait for both boxes' `floor_guard_ready`. Stop: any refusal (6.1a lists its refusals, the `--identity` ones included); an identity value other than the expected ones above; any 6.1a output other than its expected lines; a P9 `SET` line or console-mail role policy; `console.service` not `active`. **2026-10-19: 6.1 and 6.1a PASS; 6.2 STOP; the session is closed** (record under "The second session's step 6"). `guard-arm` on leo refused with `hostfloor: authenticated floor required` because `/opt/carlos` is owned by `carlos:carlos` (host-init's `chown -R carlos:carlos /data /opt/carlos`), and `OpenProtected` needs every parent of `/opt/carlos/carlos` root-owned. Sophie has the same ownership. Held for a ruling on #250.
7. **Step 7**: readers first (P3), then the dennis guard check (7.1), then **`console-report` on sophie, followed IMMEDIATELY by `publish-floor` on the signer** (the console's evidence lasts only until the agent's next status tick, about 5 minutes, #312), then home, leo's key, the `jq -e` check and the console runtime. Stop: a reader below v0.28.4; an armed policy on dennis; `publish-floor` refused after one fresh `console-report`; `jq -e` not `true`.
8. **Close the session**, always allowed and done at any stop: shred the key on the signer; terminate the instance; remove the exchange objects under `claim-signer/<run-id>/` by exact key; delete the role, instance profile, inline and managed policy attachments and security group. Then the CloudTrail check from DOCS-T1 ("Closing the signing session"): every `GetParameter` on `/carlos/release-key` since the session's start comes from `assumed-role/carlos-claim-signer/<instance id>`, one per key fetch; no instance tagged `purpose=claim-signer` pending or running; `NoSuchEntity` for the role. Anything else goes to #250 before any further step.
@@ -1120,7 +1120,7 @@ Block 5, 5.1 resumed, 09:14:41Z to 09:15:10Z, PASS:
- `refresh` only; no phase ran, and nothing was written to the bucket. `REV=5`; `inspect.json` keys `etag`, `roster`, `signed_roster`, etag `f3717d8d67f496ffd9525704c7a690b0`. Printed with every `enrollment` field redacted: region `ie` (namespace `platform`, pools `cohort1` and `console`); `leo-climbing` pool `cohort1`, `i-046c7fe43620944bc`, `active`; `sophie-knitting` pool `console`, `i-05498eada6f0afd23`, `active`; `dev-console` with compute `sophie-knitting`, namespaces `platform`; `holds` and `activations` empty. `inspect.json` `c9143bdf` and `roster.json` `6fc74d75` are the same bytes as the first session's.
- Afterwards the roster object's etag was still `f3717d8d…` (written 2026-10-18 10:21:54Z), `control/moves-protocol.json` 404, and `releases/` still only `v0.28.2.json`.
-The signer is up with the key in tmpfs for step 6. Nothing touched it between blocks.
+The signer stayed up with the key in tmpfs for step 6, and nothing touched it between blocks. It was closed at the 6.2 stop (see "The second session's step 6").
## Step 6. Arm leo's guard and start the guarded console
@@ -1168,7 +1168,7 @@ Expected: the dry run, then the apply, succeed; `writes` is empty, because the p
| `identity.release_tag` | `v0.28.4` | the release tag |
| `identity.moves_protocol` | `1` | `moves.ProtocolVersion` on main, the floor's `min` |
-Verbatim means the two sums files reach the plan as JSON strings holding their bytes unchanged. Download them on DOCS-T1 with the signer's CLI (`signing-host.md`), pass them to the signer through the exchange prefix with `aws s3 cp`, and read them with `jq --rawfile`. Never retype, trim or re-save them. The phase verifies `SHA256SUMS.txt` against its signature with the vendor release key the carlos binary embeds, so one changed byte is refused. It also checks that the list is v0.28.4's own: it must name `carlos_0.28.3_amd64.deb` and `carlos_0.28.3_arm64.deb`, and no other `carlos_*.deb`, so another release's genuine list (v0.28.2's and v0.28.3's included) is refused under this tag. The console values are copied from 5.0a's record, not from memory. No sha256 is typed anywhere: the router's comes from the signed sums (`carlos-linux-arm64`), and the console's from the channel pointer's signed manifest digest, the manifest and the artefact. The console's sha256 may not be any file listed in the sums.
+Verbatim means the two sums files reach the plan as JSON strings holding their bytes unchanged. Download them on DOCS-T1 with the signer's CLI (`signing-host.md`), pass them to the signer through the exchange prefix with `aws s3 cp`, and read them with `jq --rawfile`. Never retype, trim or re-save them. The phase verifies `SHA256SUMS.txt` against its signature with the vendor release key the carlos binary embeds, so one changed byte is refused. It also checks that the list is v0.28.4's own: it must name `carlos_0.28.4_amd64.deb` and `carlos_0.28.4_arm64.deb`, and no other `carlos_*.deb`, so another release's genuine list (v0.28.2's and v0.28.3's included) is refused under this tag. The console values are copied from 5.0a's record, not from memory. No sha256 is typed anywhere: the router's comes from the signed sums (`carlos-linux-arm64`), and the console's from the channel pointer's signed manifest digest, the manifest and the artefact. The console's sha256 may not be any file listed in the sums.
```bash
CV=f3bf1214
@@ -1347,6 +1347,38 @@ Leo's host claims stay off until step 8.
On failure: hold. Arming is one-way: keep the local armed policy, and never delete the enrollment or the guard. Restore only an image at or above the floor. Never delete or rewrite either published identity, any staged `policy*.json` or `identity*.json`, or the files in `/opt/carlos/system/console/`.
+### The second session's step 6 (v0.28.4, 2026-10-19)
+
+**2026-10-19: 6.1 PASS; 6.1a PASS on leo and sophie; 6.2 STOP. The session is closed.** Run by the executor for the controller, on Paul's go for step 6 (install-channel, guard-arm and the guarded console restart included), confirmed directly with the controller. Commands went over SSM from DOCS-T1, as in 5.1. No host.env or flag was changed. P9, `install-console`, `guard-arm --console`, `console-config` and the console restart were not run: the dev console is untouched (`f3bf1214` from 5.0a).
+
+6.1, the armed policy, 09:16Z: PASS. `--out /root/claim-trial/policy.v0.28.4.json`, checked absent first. `floor-phase.json` is `{"floor":{"min":1,"min_release":"v0.28.4"}}`. The dry run and the apply both gave revision 5, `writes: []` and `floor_sha256` `1a6a7e50f1081e1fdebe07026a9ad1cd0b27e9e36851176c5f510185832a4ab3`, as expected; keep it for step 7. `policy.v0.28.4.json` is sha256 `2114cdd09b31ccda20726e7007c49251dd2af5c2173bf804ae018a35fc4bda08`, root `600`, 507 bytes, equal to `output_sha256`.
+
+6.1, the identity, 09:17Z: PASS, published. `identity-phase.json`'s sums and signature re-read with `jq -j` hash to `b2dcd93c` and `480ecfb1`, and its other fields are `release_tag` `v0.28.4`, `moves_protocol` 1 and `console` `{account bab, app console, channel canary/console, version f3bf1214}`. The dry run went first, on its own (plan `expected_revision` 5, `--out` still absent after it), and was checked field by field. It showed one create-only put (`before_etag` empty) of `control/system/router/releases/v0.28.4.json`, with `identity_key` that key, `version` and `release_tag` `v0.28.4`, `moves_protocol` 1, router `bootstrap/router/v0.28.4/carlos-linux-arm64` `736826e4…4b21` and console `bootstrap/console/v0.28.4/carlos-linux-arm64` `8d49199f…5e9b`. The payload also holds `channel stable`, `spread 0s`, `source release:v0.28.4`, `released_at` zero and `promoted_at` (the publish time). The published `v0.28.2.json` carries `promoted_at` too, so it is part of the shape. The sums, verified on DOCS-T1, name only `carlos_0.28.4_amd64.deb` and `carlos_0.28.4_arm64.deb`. Then `phase_out` ran the dry run and the apply. The identity was published at 09:17:58Z, 1076 bytes, etag `47c51f83c813544600f6087022136e25`. `identity.v0.28.4.json` is sha256 `995e3c85aa430c3e098501554719dc674c1ec0b8f709e4049031bfef4c66adbf`, root `600`, 1076 bytes, equal to the write's sha256 and `output_sha256`.
+
+Transfer and staging, 09:18Z: PASS. Both files went to DOCS-T1 through `claim-signer/<run-id>/out/` with the same sha256 on both sides (`2114cdd0…`, `995e3c85…`). On DOCS-T1, `identity.v0.28.4.json` is `cmp`-identical to an `aws s3 cp` of the published key. Both paths were `absent` on leo and on sophie, then `staged` four times. Afterwards each box holds `policy.v0.28.4.json` (`2114cdd0`, 507) and `identity.v0.28.4.json` (`995e3c85`, 1076), root:root `600`, one link, beside the first session's untouched `policy.json` (`dfac87e6`) and `identity.json` (`2d22f05f`).
+
+6.1a, 09:18Z to 09:19Z: PASS on both boxes.
+- Precondition: `stable.json` and `canary.json` name v0.28.4, `bootstrap/router/v0.28.4/carlos-linux-arm64` `736826e4`, with no `release_tag` and no `moves_protocol`, as expected.
+- `/opt/carlos/carlos system install-channel -h` on leo matched the quoted help exactly.
+- Leo printed, in order: `carlos v0.28.4`, `carlos v0.28.3`, `replaced the rollback copy carlos.prev with v0.28.4 (v0.28.4) from the stable channel, the release carlos already runs. the release carlos.prev held before is no longer on this box`, `carlos v0.28.4` twice, `v0.28.4 (v0.28.4) from the stable channel is already installed`, `identity-equal`. Sophie printed the same, with `canary`. Neither printed the root AWS hardening line.
+- Afterwards, on both boxes, `/opt/carlos/carlos` and `carlos.prev` are both sha256 `736826e40d5c1de399678e8bf82bbd15c0a5123f766089d5b42d0539f2ea4b21`, root:root `755`, 49217698 bytes. Before, `.prev` was `6154afb4` (v0.28.3). `/opt/carlos/system/router/applied.json` and `seen.json` hash the same before and after (leo `3501b093`/`b18b0e14`, sophie `1090bb3c`/`27704c8e`). `channel-identity.json` (1076 bytes, root `644`) equals the staged identity. Neither box can go back to v0.28.3 now; that was the point of this step.
+
+6.2, arm leo, 09:19:49Z: **STOP.** Before: no `host-actor.json`, no `/opt/carlos/system/guard`, no `carlos-edge.service.d`, no `CARLOS_HOST_CLAIM`. `guard-arm` printed `hostfloor: authenticated floor required`, and SSM recorded `Failed`, exit 1. It wrote nothing: afterwards there was still no `host-actor.json`, guard directory or edge drop-in. `carlos-edge` and `carlos-instance-control` stayed `active`, and both slots were still `736826e4`.
+
+Cause, read from `f3bf1214` and checked read-only on the boxes:
+- `guard-arm` (`internal/host/guardarm.go`) opens `/opt/carlos/carlos` and `/opt/carlos/carlos.prev` with `hostfloor.OpenProtected` (`internal/hostfloor/image_linux.go`). That needs every parent directory owned by uid 0 and not group- or world-writable; otherwise it returns a bare `ErrFloor`, which `guard-arm` logs as exactly this line.
+- On leo and on sophie, `/opt/carlos` is `carlos:carlos 755` (`/opt` and `/opt/carlos/system` are root). `platform-infrastructure` `dev/host-init.sh:10` (and `flagship/host-init.sh:10`) run `chown -R carlos:carlos /data /opt/carlos`.
+- The inputs are not the cause. A local replay (`internal/hostfloor` at `f3bf1214`, a scratch test, nothing run on a box) of the same checks gave no error for the staged `policy.v0.28.4.json` and `identity.v0.28.4.json`, leo's public key, bucket `carlos-dev-640012953448`, build `v0.28.4` (the release binary's `-X …version.Build=v0.28.4`), `linux-arm64` and `736826e4…4b21`: the policy verifies (floor `{1, v0.28.4}`, digest `1a6a7e50`), the identity verifies, and `CheckImage` passes. `guardArmIdle` returns only wrapped messages, and leo's `CARLOS_DEPLOYMENT_BUCKET` is set in `host.env`.
+- The first session's 6.2 stop printed the same line. Its `.prev` was also wrong then, but this ownership check comes before the `.prev` image check, so the ownership was probably what refused it then too.
+- Sophie's `guard-arm --console` would refuse the same way.
+
+Closing the signing session, 09:20Z to 09:24Z, in full:
+1. The key was shredded on the signer and `/run/claim-signer` removed (`shredded`, then the directory gone).
+2. `i-006e3c79dfdf5d814` (root volume `DeleteOnTermination` true) was `terminated` at 09:20:50Z. All 9 exchange objects under `claim-signer/cohort1-20261019T091304Z/` were removed by exact key, once the listing was checked to equal the keys recorded when each was written; nothing remains under `claim-signer/`. Then the role was taken out of the profile, the profile deleted, the inline policy deleted, the managed policy detached, the role deleted, and `sg-0a0f98545441cf295` deleted.
+3. CloudTrail since `2026-10-19T09:14:14Z`, read at 09:21:30Z, 09:23:51Z and 09:24:53Z: one `GetParameter` on `/carlos/release-key`, at 09:14:16Z from `arn:aws:sts::640012953448:assumed-role/carlos-claim-signer/i-006e3c79dfdf5d814`, which is the session's one key fetch. No instance tagged `purpose=claim-signer`; `NoSuchEntity` for the role and the profile; no `carlos-claim-signer` security group. Nothing unexpected.
+
+Left as they are ("Stop and hold"): the roster at revision 5; the v0.28.4 identity (correct, and permanent under v0.28.4); `policy.v0.28.4.json` and `identity.v0.28.4.json` staged on leo and sophie, beside the first session's files; both boxes' `carlos.prev` now v0.28.4, and `channel-identity.json`; `/opt/carlos` ownership. No floor is published. Resuming needs a reviewed fix for the `/opt/carlos` ownership (in host-init and on the boxes, or in `guard-arm`'s check), then Paul's go for a third signing session. That session reuses the published identity: a rerun of `publish-identity` with a new `--out` writes nothing and gives the same bytes. `sign-policy` with a new `--out` gives the same `policy.v0.28.4.json` bytes (ed25519 is deterministic). The 6.1a step is already done on both boxes.
+
### The first session's step 6 (v0.28.2, 2026-10-18)
**2026-10-18: 6.1 PASS; 6.2 STOP. The session is closed.** Run by the executor on Paul's go for step 6, relayed by the controller. Commands went over SSM, as in 5.1.