carlos / platform Public

docs: cohort-1 runbook: fifth session: step 7 PASS (floor 1a6a7e50, home ie, leo's key, console runtime), closed

Signer policy: ReadControl also reads exactly status/consoles/dev-console.json
(the check script asserts it). Step 7.4 is sophie-only; the enrollment is an
identifier, not a credential, and is redacted only for tidiness. 7.4 now says
to wait for the restarted console's own report before console-report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a5054aad8e062c6b1a13786197c3a9288baee994 parent c15ce69
2 files changed, +56 −7
  • docs/superpowers/notes/claim-trial-inputs/signing-host.md +4 −3
  • docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md +52 −4

CI passed — 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
ok  	github.com/carlosframework/platform	292.278s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.096s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.175s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.701s
ok  	github.com/carlosframework/platform/internal/activator	250.101s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.514s
ok  	github.com/carlosframework/platform/internal/apperr	0.076s
ok  	github.com/carlosframework/platform/internal/applog	0.129s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.578s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.694s
ok  	github.com/carlosframework/platform/internal/changelog	0.305s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.026s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	401.885s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.613s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.448s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	44.864s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.134s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.014s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.155s
ok  	github.com/carlosframework/platform/internal/host	171.855s
ok  	github.com/carlosframework/platform/internal/hostactors	0.666s
ok  	github.com/carlosframework/platform/internal/hostbox	2.670s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.596s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.013s
ok  	github.com/carlosframework/platform/internal/hostexec	4.669s
ok  	github.com/carlosframework/platform/internal/hostfence	0.033s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.340s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.007s
ok  	github.com/carlosframework/platform/internal/hostops	1.654s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.013s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.459s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.161s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.013s
ok  	github.com/carlosframework/platform/internal/infraview	0.014s
ok  	github.com/carlosframework/platform/internal/instancectl	5.208s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.736s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.011s
ok  	github.com/carlosframework/platform/internal/layout	1.549s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.002s
ok  	github.com/carlosframework/platform/internal/lease	9.824s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.169s
ok  	github.com/carlosframework/platform/internal/maintenance	0.079s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.489s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.073s
ok  	github.com/carlosframework/platform/internal/metrics	0.007s
ok  	github.com/carlosframework/platform/internal/moves	21.025s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.815s
ok  	github.com/carlosframework/platform/internal/objstore	0.168s
ok  	github.com/carlosframework/platform/internal/ops	0.178s
ok  	github.com/carlosframework/platform/internal/park	0.196s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	17.542s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.358s
ok  	github.com/carlosframework/platform/internal/relay	7.219s
ok  	github.com/carlosframework/platform/internal/release	21.260s
ok  	github.com/carlosframework/platform/internal/rollout	3.616s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.057s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.584s
ok  	github.com/carlosframework/platform/internal/s3compat	0.004s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.488s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	8.427s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.149s
ok  	github.com/carlosframework/platform/internal/status	0.035s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.195s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.555s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 48824482 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.021s
=== RUN   TestT4StorageProbeMinio
2026/10/19 20:03:56 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E006E6116E6AB5, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.019s
minio contract: PASS
diff --git a/docs/superpowers/notes/claim-trial-inputs/signing-host.md b/docs/superpowers/notes/claim-trial-inputs/signing-host.md
index e263e838a..117d3e488 100644
--- a/docs/superpowers/notes/claim-trial-inputs/signing-host.md
+++ b/docs/superpowers/notes/claim-trial-inputs/signing-host.md
@@ -57,7 +57,8 @@ Trust: `ec2.amazonaws.com`. Managed policy: `AmazonSSMManagedInstanceCore` (Sess
"Condition": {"StringLike": {"s3:prefix": ["control/*", "status/boxes/*", "claim-signer/*", "apps/bab/console/*"]}}},
{"Sid": "ReadControl", "Effect": "Allow", "Action": "s3:GetObject",
"Resource": ["arn:aws:s3:::carlos-dev-640012953448/control/*",
- "arn:aws:s3:::carlos-dev-640012953448/status/boxes/*"]},
+ "arn:aws:s3:::carlos-dev-640012953448/status/boxes/*",
+ "arn:aws:s3:::carlos-dev-640012953448/status/consoles/dev-console.json"]},
{"Sid": "ReadConsoleRelease", "Effect": "Allow", "Action": "s3:GetObject",
"Resource": "arn:aws:s3:::carlos-dev-640012953448/apps/bab/console/*"},
{"Sid": "WriteIdentity", "Effect": "Allow", "Action": "s3:PutObject",
@@ -79,7 +80,7 @@ Why each statement:
- **ListForNotFound:** without `s3:ListBucket`, S3 answers a missing key with 403 instead of 404, and the runner's first `initialize` reads a roster that doesn't exist yet.
- **WriteRosterAndFloor:** these are exactly the runner's write allowlist (`internal/claimactivation/runner.go:89-104`).
- **ReadControl:** covers the reads for invitations, box records and floor evidence. Confirm it with a `--dry-run` of every phase from the signer, and widen only on a refusal.
- **Refused, 2026-10-19 (fourth session, runbook step 7.2):** `publish-floor` needs `s3:GetObject` on `status/consoles/dev-console.json`. Its census reads the console's own report (`ExactEvidence.ConsoleReport`, `internal/hostactors/census.go:349` at `f3bf1214`), and this policy does not allow that read. The policy above is unchanged. A wider one (that key in `ReadControl`, and `status/consoles/*` in `ListForNotFound`) needs Paul's approval before the next session.
+ **Widened, 2026-10-19 (Paul's go for the fifth session):** `ReadControl` now also allows `s3:GetObject` on exactly `status/consoles/dev-console.json`, and nothing else changed. In the fourth session `publish-floor` was refused that read, because its census reads the console's own report (`ExactEvidence.ConsoleReport`, `internal/hostactors/census.go:349` at `f3bf1214`). A read-only trace of `f3bf1214` found nothing else that `publish-floor`, `home` or `bind-key` needs outside this policy, refreshes and dry runs included. `ListForNotFound` is unchanged and keeps `control/*`, which gives the 404s those phases expect: the floor pair and `control/box-fences/…`. Step 7.4 runs on sophie and needs nothing from this role.
- **ReadConsoleRelease and WriteIdentity** (added 2026-10-18 for step 6.1; on main 7c1beb2d, published (v0.28.2, tag on 7c1beb2d)): `publish-identity` reads the console's `canary/console` pointer, manifest and artefact under `apps/bab/console/`, and writes one object, the identity at `control/system/router/releases/v0.28.4.json` (second session; the first session's was `v0.28.2.json`, now published and permanent; no `v0.28.3.json` was ever published), create-only. `WriteIdentity` names that exact key, not a prefix: no wildcard, so the role can write no other release's identity, the v0.28.2 and v0.28.3 ones included. `sign-policy` writes nothing. The runner admits only that one identity key for the phase, whatever IAM allows. platform-1e accepted this scope as proposed on 2026-10-18; Paul approves it together with the signer host.
**The release-key deny doesn't block this role.** That deny is the inline policy `deny-release-key-reads` on the box roles only (`carlos-dev-host`, `carlos-dev-host-b`). Dev-uk's role is limited by its own eu-west-2 scope. There is no account-wide deny. A policy simulation of this `ReleaseKey` statement returns `allowed` (2026-10-18). Because any role granted `ssm:GetParameter` on that ARN can read the key, the role exists only for the trial and is deleted afterwards. CloudTrail is checked so that every `GetParameter` on `/carlos/release-key` comes from a `carlos-claim-signer` session.
@@ -142,7 +143,7 @@ Paul approves the signer host and its role policy (including the exact identity
`signer-policy.json`: the inline policy `claim-signer` exactly as in "IAM role `carlos-claim-signer`" above, with `ReadConsoleRelease` on `apps/bab/console/*` and `WriteIdentity` on exactly `control/system/router/releases/v0.28.4.json`. Check it before use:
```bash
-python3 -c 'import json,sys;d=json.load(open("signer-policy.json"));w=[s for s in d["Statement"] if s["Sid"]=="WriteIdentity"][0]["Resource"];assert w=="arn:aws:s3:::carlos-dev-640012953448/control/system/router/releases/v0.28.4.json", w;print("ok")'
+python3 -c 'import json,sys;d=json.load(open("signer-policy.json"));w=[s for s in d["Statement"] if s["Sid"]=="WriteIdentity"][0]["Resource"];assert w=="arn:aws:s3:::carlos-dev-640012953448/control/system/router/releases/v0.28.4.json", w;r=[s for s in d["Statement"] if s["Sid"]=="ReadControl"][0]["Resource"];assert r==["arn:aws:s3:::carlos-dev-640012953448/control/*","arn:aws:s3:::carlos-dev-640012953448/status/boxes/*","arn:aws:s3:::carlos-dev-640012953448/status/consoles/dev-console.json"], r;print("ok")'
```
`signer-user-data.sh`, the root runner's (Debian ships no SSM agent):
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 2a3ac022a..873930e83 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
@@ -38,7 +38,7 @@ Final, 2026-10-19. Every value below was read or verified read-only on DOCS-T1 b
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.
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. **Third session, 2026-10-19 15:18Z to 15:25Z** (after the ownership fix): 6.2 PASS (leo armed, `floor_guard_ready` reported); 6.3 P9, `install-console`, `guard-arm --console` and `console-config` PASS; **STOP at the console restart**: `203/EXEC` `Permission denied` on `/opt/carlos/system/guard/carlos`, because the 6.3 block's `umask 077` made sophie's guard directory `700` and the console runs as `carlos`. The dev console is down (502) and held; sophie's edge fails the same way on its next restart. Session closed, CloudTrail clean (record under "The third session").
-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`. **Fourth session, 2026-10-19 17:49Z to 17:55Z:** 7.1 PASS; `console-report` PASS; `publish-floor` refused by IAM (the signer role cannot read `status/consoles/dev-console.json`): STOP before any write. No floor, home or key binding. Session closed, CloudTrail clean (record under "The fourth session"). Resuming needs Paul's approval of the wider signer policy, then a fifth session.
+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`. **Fourth session, 2026-10-19 17:49Z to 17:55Z:** 7.1 PASS; `console-report` PASS; `publish-floor` refused by IAM (the signer role cannot read `status/consoles/dev-console.json`): STOP before any write. No floor, home or key binding. Session closed, CloudTrail clean (record under "The fourth session"). Resuming needs Paul's approval of the wider signer policy, then a fifth session. **Fifth session, 2026-10-19 19:29Z to 19:38Z: step 7 PASS** (the signer policy widened by exactly `status/consoles/dev-console.json`): floor `1a6a7e50` published, home `ie`, leo's key bound, `jq -e` `true`, console runtime on after one `console-report` retry. Roster revision 9. Session closed, CloudTrail clean (record under "The fifth session").
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.
**Open:** nothing. Every TO-CONFIRM is cleared.
@@ -1061,7 +1061,7 @@ Opening (`signing-host.md`, "Create"):
- Created: role and inline policy `carlos-claim-signer` / `claim-signer` (extracted verbatim from `signing-host.md`; the `WriteIdentity` check printed `ok`), instance profile `carlos-claim-signer`, security group `sg-08c374c8c31ef448a` (no inbound, egress TCP 443 only), instance `i-0ea2852ef727929bb` (launched 10:18:32Z, SSM online 10:19:11Z), exchange run-id `cohort1-20261018T101832Z`.
- On DOCS-T1, before upload: the v0.28.2 `SHA256SUMS.txt` signature printed "Signature Verified Successfully" with the README's pinned key. `carlos-linux-arm64` `7141e037...f492`, jq 1.7.1 `4dd2d8a0...b4a5`, `SHA256SUMS.txt` `9c29b3d3...` and `.sig` `0fdb8557...` all matched. `release.pub` was taken from leo's `CARLOS_RELEASE_PUBKEY`, identical on sophie, fingerprint `960d03ff`.
- On the signer: both `sha256sum -c` lines `OK`, `carlos v0.28.2`, `jq-1.7.1`. Session start `2026-10-18T10:19:37Z`, recorded before the key fetch. Key at `/run/claim-signer/release.key`: `root 600 1 45`, tmpfs, no swap.
-- Console enrollment token generated on the signer at approval (`od -N16 /dev/urandom`), never printed. Format `^[0-9a-f]{32}$` ok, sha256 prefix `a611795b`. It lives only in the signer's `/root/claim-trial` and in the roster.
+- Console enrollment token generated on the signer at approval (`od -N16 /dev/urandom`), never printed. Format `^[0-9a-f]{32}$` ok, sha256 prefix `a611795b`. (Corrected 2026-10-19: an enrollment is an identifier, not a credential. It sits in plain text in `control/host-actors.json`, in the console invitation, and in the console's and sophie's reports, which every roster reader can read. On its own it grants nothing: joining needs a root-signed roster write, and reporting needs IAM write on the report key. Enrollment values are still redacted in printed output and in this record, for tidiness, not because they are secret.)
How it ran: the signer has no attended shell. Each block went from DOCS-T1 as one SSM `send-command` that sources `/root/claim-trial/helpers.sh`, which holds this section's first helper block with `CARLOS=/usr/local/bin/carlos`, `KEY=/run/claim-signer/release.key` and no `AWS_PROFILE`. Each phase ran as a plain errexit statement with stdout to a file, which was printed with every `enrollment` value redacted. Run output carries the payload, enrollments included.
@@ -1427,7 +1427,7 @@ Opening, 17:48Z to 17:51Z: PASS.
7.2, from 17:52:29Z, when sophie's report moved:
1. `box_inspect "$SOPHIE"`, 17:52:47Z to 17:52:59Z: `staged` twice, `rev` `5`.
-2. `console-report` on sophie, the block exactly as quoted: SSM `Success`. The dry run and the apply printed key `status/boxes/sophie-knitting.json`, `expected_roster_revision` 5, console `dev-console`, `binary_sha256` `8d49199f…5e9b`, `guard_floor_sha256` `1a6a7e50…4ab3`, and `all_launch_paths_guarded` `false`. Its output also carries an `enrollment` value; future runs should print it redacted.
+2. `console-report` on sophie, the block exactly as quoted: SSM `Success`. The dry run and the apply printed key `status/boxes/sophie-knitting.json`, `expected_roster_revision` 5, console `dev-console`, `binary_sha256` `8d49199f…5e9b`, `guard_floor_sha256` `1a6a7e50…4ab3`, and `all_launch_paths_guarded` `false`. Its output also carries the console's `enrollment`, which was printed in the executor's transcript. It is not a secret (see the correction under 5.1), but future runs print it redacted for tidiness.
3. `publish-floor` on the signer, sent the moment `console-report` returned `Success` (nothing between them), 17:53:08Z: **refused, STOP.** SSM `Failed`, exit 1, in `phase`'s refresh or dry run, before any apply. The error was `AccessDenied` on `s3:GetObject` for `arn:aws:s3:::carlos-dev-640012953448/status/consoles/dev-console.json`, as `assumed-role/carlos-claim-signer/i-06dcff9bf6386e406`, "because no identity-based policy allows the s3:GetObject action". Afterwards (17:53:31Z): roster etag still `f3717d8d…`, `moves-protocol.json` and `moves-protocol-auth.json` 404. `status/consoles/dev-console.json` exists (687 bytes, 17:49:46Z, the console's own report).
Cause, read from `f3bf1214`: `publish-floor`'s census reads the console's own report through `ExactEvidence.ConsoleReport` (`internal/hostactors/census.go:349`), at `status/consoles/<id>.json`. The signer's `ReadControl` statement (`signing-host.md`) grants `GetObject` only on `control/*` and `status/boxes/*`, and `ListForNotFound` lists only `control/*`, `status/boxes/*`, `claim-signer/*` and `apps/bab/console/*`. The root-runner rehearsal ran the phase with other credentials, so it never met this. This is not the #312 race the runbook's one retry is for: a second `console-report` and `publish-floor` would be refused the same way. So the retry was not run, and the policy was not widened, because that is outside the go. Resuming needs Paul's approval of a `signing-host.md` policy that also allows `s3:GetObject` on `status/consoles/dev-console.json` (and `status/consoles/*` in `ListForNotFound`), then a fifth signing session. Every other phase's reads are untested from the signer: `home` and `bind-key` never ran.
@@ -1441,6 +1441,53 @@ After the stop, 17:55Z: on leo and sophie the guard directories are still `755 r
Left as they are ("Stop and hold"): both boxes armed and serving; sophie's console guarded; the roster at revision 5; no floor, home or key binding; the published v0.28.4 identity; every staged file. `box_inspect` rewrote sophie's `inspect.json`, `roster.json` and `rev` with the same bytes (`c9143bdf`, `6fc74d75`, `f0b5c2c2`). No host.env or flag changed, and no other box file. The `console-report` entry in sophie's box report is gone at the agent's next tick (#312) and needs nothing done.
+### The fifth session: step 7 (v0.28.4, 2026-10-19)
+
+Run by the executor for the controller, on Paul's go confirmed directly with the controller: a fifth signing session for step 7, with one policy change. `ReadControl` also allows `s3:GetObject` on exactly `status/consoles/dev-console.json` (`signing-host.md`). Commands went from DOCS-T1 (profile `kass`), one SSM `send-command` per block, the signer's blocks sourcing `/root/claim-trial/helpers.sh`. Every printed output went through a filter that redacts enrollment values; none is in this record.
+
+**Step 7 PASS: floor published, home `ie`, leo's key bound, `jq -e` `true`, console runtime on. The session is closed, CloudTrail clean.**
+
+Pre-checks, read-only, 19:27Z to 19:28Z, all PASS:
+- Bucket: the roster 1967 bytes, etag `f3717d8d…`, revision 5. `moves-protocol.json` and `moves-protocol-auth.json` 404. The identity `releases/v0.28.4.json` present (1076 bytes, etag `47c51f83…`, sha256 `995e3c85`). Nothing under `claim-signer/`. Both pointers v0.28.4, `736826e4`, untagged. `status/consoles/dev-console.json` 687 bytes, 19:24:46Z.
+- Reports: leo 19:25:48Z, sophie 19:27:19Z; both `v0.28.4`, `736826e4`, `floor_guard_ready` `True`, `1a6a7e50`.
+- Leo and sophie: guard directories `755 root:root` twice, `sudo -u carlos test -x /opt/carlos/system/guard/carlos` passing (on sophie also the protected console). Under `/opt/carlos`: 0 entries not `root:root`, 0 group- or other-writable, 0 set-id. `host.env` root `600`. All three slots `736826e4`, `carlos v0.28.4`. Edge and instance-control `active`, not restarted since the v0.28.4 roll. Sophie's console `active` since 17:34:45Z, running the protected console as `carlos`. No AccessDenied line since 19:00Z. Staged files as recorded in the fourth session.
+- Serving, 19:28:28Z: the console's `/` and `/healthz` 200 (`f3bf1214`), wakecheck 200, claimtrial 200 via `--resolve` to leo. No `carlos-claim-signer` resources.
+
+Opening, 19:29Z to 19:30Z: PASS. Tools verified as in the fourth session (signature `Verified`, sums `b2dcd93c`, carlos arm64 `736826e4`, amd64 `2d53162f`, jq `4dd2d8a0`). The policy file differs from the fourth session's by that one resource, and the check script (now also asserting `ReadControl`'s list) printed `ok`. The policy read back from IAM equals the file. Role `carlos-claim-signer`, profile, security group `sg-0b5910cb134fe4e10` (no inbound, egress TCP 443 only), instance `i-09e858e3c91a7e293` (launched 19:29:15Z on the first try, IMDSv2, hop limit 1, `DeleteOnTermination` true, SSM `Online` 19:29:41Z). Run-id `cohort1-20261019T192915Z`. On the signer: `carlos v0.28.4`, `jq-1.7.1`, every input `OK`, `helpers.sh` `208ee7d9`, `claims help` `a909f5ef`, no swap, `/run` tmpfs. Session start `2026-10-19T19:30:06Z`, before the key fetch. Key `root 600 1 45`, tmpfs.
+
+5.1, `refresh` only, 19:30Z: PASS. `REV=5`, the same listing and bytes as before (`c9143bdf`, `6fc74d75`).
+
+7.1, 19:30Z: PASS. `system status` (verified amd64 CLI): all three boxes `v0.28.4 current`. Console `f3bf1214`. Leo and sophie `carlos v0.28.4` in both slots; dennis `v0.28.4`, `.prev` `v0.28.3`. Dennis at 19:30:38Z: the exact pass (`Success`, both `absent`, `carlos v0.28.4`).
+
+7.2, report and floor, 19:33Z:
+1. Sophie's report moved at 19:33:20Z. `box_inspect "$SOPHIE"`: revision 5, 19:33:35Z.
+2. `console-report`: `Success`, dry run and apply. `status/boxes/sophie-knitting.json`, revision 5, console `dev-console`, `binary_sha256` `8d49199f`, `guard_floor_sha256` `1a6a7e50`.
+3. `publish-floor`, sent the moment `console-report` returned: `Success` at 19:33:49Z. The dry run and the apply each listed four writes: the roster at revision 6 (`publication` `preparing`, `floor_sha256` `1a6a7e50…4ab3`), `control/moves-protocol-auth.json` (create, `3bbd48a8`), `control/moves-protocol.json` (create, `1a6a7e50`, `{"min":1,"min_release":"v0.28.4"}`), and the roster at revision 7 (`published`). Read back from the bucket: the roster 2111 bytes, `50fbf7d7`, etag `d5b7f099…`; the floor 33 bytes, `1a6a7e50`; the auth 375 bytes, `3bbd48a8`; all 19:33:45Z.
+4. `home`, write-once, so its dry run ran on its own first (plan `expected_revision` 7, `home_region` `ie`): one roster write, revision 8, sha256 `8cbcd419`. A path-by-path comparison with the current roster showed exactly `+home_region` and `revision` changed. Then the apply (`apply_plan` with the same plan): revision 8, `home_region` `ie`, `published`, sha256 `8cbcd419`, equal to the dry run's.
+
+7.3, leo's key, 19:36Z: PASS.
+- `box_inspect "$LEO"` (revision 8), `le` staged.
+- `evidence-key` on leo, the block as quoted: the dry run printed an empty binding (with no key yet, the dry run returns before generating one, `installActivationEvidenceKey`); the apply wrote `/etc/carlos/host-evidence.key` (root `600`, one link, 45 bytes) and `key-proof.json` (`9b76e36f`). Afterwards `/etc/carlos` and the guard directories are still `755 root:root`, and the `test -x` still passes. `.claims-activation.lock` (root `600`, empty) is new in `/etc/carlos`.
+- On DOCS-T1, `key-proof.json`'s binding names bucket `carlos-dev-640012953448` and box `leo-climbing`, its enrollment equals the staged `le`, its public key is 44 characters, and the proof 88.
+- `bind-key` on the signer: revision 9, sha256 `20ebe512` (dry run equal to apply). `refresh`, then the `jq -e` printed `true`. `leo-climbing` now carries `census_public_key`; sophie has none (not activated).
+
+7.4, the console runtime, on sophie (nothing on the signer):
+1. `box_inspect "$SOPHIE"`: revision 9.
+2. The block as quoted, 19:35:48Z. `console-config --enable-console-runtime`, dry run and apply: `environment` `CARLOS_CONSOLE_ID=dev-console`, `CONSOLE_HOME_REGION=ie`, `CARLOS_HOST_CLAIM=1`; `reload` true, `start` false. The restart: `active` at 19:35:50Z. Then `console-report`'s dry run, in the same second, was **refused**: `claim activation: input or protected state refused`. Cause: `console-report` takes the console's process from the console's own report (`status/consoles/dev-console.json`, `activationConsolePID`), which still named the pre-restart PID (642098). The restarted console (PID 647490) wrote its new report at 19:35:51Z.
+3. The runbook's one retry, 19:36:35Z: `box_inspect "$SOPHIE"` (revision 9), then `console-report`, dry run and apply: `Success`, PID 647490, `binary_sha256` `8d49199f`, `guard_floor_sha256` `1a6a7e50`. PASS.
+4. Afterwards: `claim-console.env` root `600` (73 bytes, `CARLOS_HOST_CLAIM=1`), `91-claim-runtime.conf` root `644`, unchanged. The console is `active` since 19:35:50Z, 0 restarts, `/opt/carlos/system/console/console` as `carlos`, with `CARLOS_HOST_CLAIM=1` in its environment. Its journal says the run sweep and seeds pass are off on a host-claim console. Guard directories `755 root:root`, `test -x` passing.
+
+For the next run of 7.4: wait for the console's own report to move after the restart before `console-report`.
+
+Closing, 19:36Z to 19:38Z, in full:
+1. The key was shredded and `/run/claim-signer` removed, 19:36:53Z.
+2. `i-09e858e3c91a7e293` was `terminated` at 19:37:25Z. The 9 exchange objects (the 7 inputs, `summary.jq` (the redacting print filter) and `bind-phase.json`) were removed by exact key after the listing matched the recorded keys. Nothing remains under `claim-signer/`. The role, profile, inline policy, managed attachment and `sg-0b5910cb134fe4e10` were deleted (19:37:54Z).
+3. CloudTrail since `2026-10-19T19:30:06Z`, read at 19:38:00Z and again at 19:53:37Z: one `GetParameter` on `/carlos/release-key`, at 19:30:08Z, from `arn:aws:sts::640012953448:assumed-role/carlos-claim-signer/i-09e858e3c91a7e293`, 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.
+
+After the floor, 19:38Z: the console's `/` and `/healthz` 200 (`f3bf1214`); wakecheck 200 (sophie); claimtrial 200 via `--resolve` to leo. Edge and instance-control `active` on both boxes, not restarted. No AccessDenied line since 19:28Z on leo; on sophie 2, both Cost Explorer. Reports: leo 19:33:41Z and sophie 19:33:20Z, both `floor_guard_ready` `True`, `1a6a7e50`.
+
+State now: the floor `{1, v0.28.4}` is published (`1a6a7e50`) and binds every box's recovery. The roster is at revision 9, `published`, home `ie` (write-once), with leo's census key bound. Sophie's console runs the claim runtime. Leo's host claims are still off (`CARLOS_HOST_CLAIM` unset), as step 7 leaves them. No host.env or flag changed beyond what `console-config` wrote. Next are 7a, then step 8, each with its own go.
+
### 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).
@@ -1580,7 +1627,7 @@ Paul's go: once for step 7. `home_region` becomes write-once as soon as claims a
jq -e '.roster.home_region == "ie" and .roster.publication == "published" and .roster.request_limit_ns == 120000000000' inspect.json
```
-4. **Console runtime.** On the trusted host, `box_inspect "$SOPHIE"`. Then on sophie:
+4. **Console runtime.** There is no signer phase for this. 7.4 is `console-config --enable-console-runtime`, the restart and `console-report`, all run on sophie. `console-config` makes no AWS call (it reads only local files), and `console-report` runs under sophie's own role, so 7.4 needs nothing from the signer role. From DOCS-T1, `box_inspect "$SOPHIE"` (the box helpers). Then on sophie:
```bash
set -euo pipefail
@@ -1605,6 +1652,7 @@ Expected:
- the `jq -e` in 3 prints `true`;
- `/etc/carlos/host-evidence.key` exists on leo (`root`, `600`, one link);
- `console.service` is `active` and its report publishes; if publication loses a race with the agent's own report, run `box_inspect` again, report again and retry with the current revision;
+- `console-report` straight after the restart is refused (`claim activation: input or protected state refused`) until the restarted console has written its own report, `status/consoles/dev-console.json`, with the new PID (about a second after the start in the fifth session). Wait for that object to move, then `box_inspect` and run `console-report` again;
- this report, too, lasts only until the agent's next status tick (#312). Nothing in step 7 needs it after it publishes; anything later that needs the console's compute evidence (the console runtime's own census, step 9) sees it vanish within about 6 minutes. That is an open design question (prev-rehearsal report), not a step 7 stop.
On failure: hold. A torn publish is resumed with identical bytes. Never erase the floor or substitute another digest. Leo's claim flag stays off.