carlos / platform Public

docs: cohort-1 runbook takes platform-1e's step 6 artifacts review

Builder findings for steps 6 and 7 from the review of
claim-step6-artifacts @ c38a4ca9, drafted for Paul after v0.28.2:

- new step 5.0a, its own go, outside any roll: pointer digests on for
  bab, then a fresh promote of a console built with
  version.Build=v0.28.2; writers-before-readers checks and a rollback
- step 6 rewritten around sign-policy, publish-identity (--out) and
  install-console; the console path is fixed at
  /opt/carlos/system/console/console; publish-identity's verbatim
  inputs and their sources
- owed before production cohorts: a trusted build attestation tied to
  the console artefact's digest
- inputs README, templates and signing-host.md follow (signer role
  gains the console reads and the one identity write)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d8f56ebdb59361c44f16473dd7e279b6dd491ce7 parent e656905
5 files changed, +254 −44
  • docs/superpowers/notes/claim-trial-inputs/README.md +43 −17
  • docs/superpowers/notes/claim-trial-inputs/identity.template.json +11 −4
  • docs/superpowers/notes/claim-trial-inputs/policy.template.json +1 −1
  • docs/superpowers/notes/claim-trial-inputs/signing-host.md +9 −4
  • docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md +190 −18

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/notes/claim-trial-inputs/README.md b/docs/superpowers/notes/claim-trial-inputs/README.md
index 13cac6b6d..7dfb56b65 100644
--- a/docs/superpowers/notes/claim-trial-inputs/README.md
+++ b/docs/superpowers/notes/claim-trial-inputs/README.md
@@ -1,6 +1,8 @@
# Claim trial inputs, dev cohort 1 (draft for review)
-These are the concrete inputs the runbook's steps 5 to 7 say "the controller supplies and Paul approves" (`docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md`). They were drafted on 2026-10-18 from dev values read with read-only calls, then updated with platform-1e's rulings the same day. Nothing here is signed, and no secret is in any file. A value still open is marked `TO-CONFIRM`; one that waits on a platform fix is marked `BLOCKED`.
+These are the concrete inputs the runbook's steps 5 to 7 say "the controller supplies and Paul approves" (`docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md`). They were drafted on 2026-10-18 from dev values read with read-only calls, then updated with platform-1e's rulings the same day. Nothing here is signed, and no secret is in any file. A value still open is marked `TO-CONFIRM`; one that waits on a platform fix is marked `BLOCKED`, or `pending v0.28.2` once the fix is built and waits only for the release.
+
+Builder findings from the review of platform-1e's branch `claim-step6-artifacts` (`c38a4ca9`, census ruled, pending v0.28.2) were added on 2026-10-18, drafted for Paul after v0.28.2: the console built with `version.Build=v0.28.2`, pointer digests for the console's channel (runbook step 5.0a), the fixed console path, and `publish-identity`'s verbatim inputs.
Timing: step 5.0 (box identity records) is done. Everything after it, from 5.1 (roster and enrollment) on, waits for v0.28.2, which carries the fixes for gaps 1 and 2, and runs in one signing session (Paul's ruling, 2026-10-18).
@@ -15,8 +17,8 @@ Platform main at drafting: `b1118925` (region HA 2b). The box-record verb is on
| `sophie.json` | step 5, `invite-box` and `enroll-box` | `claimactivation.BoxInput` |
| `console.json` | step 5, `invite-console` and `enroll-console` (the runbook's `jq` adds `compute` after sophie enrolls) | `hostactors.Console` |
| `floor.json` | step 7, `publish-floor` | `hostfloor.Floor` |
-| `policy.template.json` | step 6, `guard-arm --policy` | `hostfloor.Policy`, unsigned template (shape only; the real file is byte-exact from `--out`) |
-| `identity.template.json` | step 6, `guard-arm --identity` | signed system pointer, unsigned template (shape only; the real file is byte-exact from the bucket) |
+| `policy.template.json` | step 6, `guard-arm --policy` | `hostfloor.Policy`, unsigned template (shape only; the real file is byte-exact from `sign-policy --out`) |
+| `identity.template.json` | step 6, `guard-arm --identity` and `install-console --identity` | signed system pointer, unsigned template (shape only; the real file is byte-exact from `publish-identity --out`, the published object as read back) |
| `signing-host.md` | steps 5 and 7, the trusted signing host | proposal |
## Values and where they come from
@@ -44,37 +46,61 @@ Platform main at drafting: `b1118925` (region HA 2b). The box-record verb is on
| Artifact | Value | Status |
|---|---|---|
| Release for step 5 | v0.28.1, dev only (box-record verb), `3b69c2bd`, published 2026-10-18; on leo, sophie, dennis and the dev console. `carlos-linux-arm64` sha256 `2ae174da29b1d10fdcb009172ba830db5460b62e838bcecc7d9e17535f35adea` (signed `SHA256SUMS.txt`, verified) | confirmed |
-| Release for step 6 onwards | v0.28.2 (armed policy signing phase, published system identity, protected console install) | TO-CONFIRM: being built by platform-1e |
-| Router linux-arm64 sha256 (v0.28.2) | from the signed `SHA256SUMS.txt` of the v0.28.2 release | TO-CONFIRM |
-| Console linux-arm64 sha256 (v0.28.2) | from the console app's signed release manifest in the deployment bucket (platform-1e ruling). Never typed by hand, and never taken from `SHA256SUMS.txt`, which does not cover the console | TO-CONFIRM |
+| Release for step 6 onwards | v0.28.2 (`claims run` phases `sign-policy` and `publish-identity`, `claims run --out`, `claims install-console`) | pending v0.28.2 (branch `claim-step6-artifacts` @ `c38a4ca9`) |
+| Router linux-arm64 sha256 (v0.28.2) | the `carlos-linux-arm64` line of the v0.28.2 release's signed `SHA256SUMS.txt`, read by `publish-identity` from the sums text in its plan. Never typed | TO-CONFIRM when published |
+| Dev console build for v0.28.2 | built from the v0.28.2 tag with `-ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=v0.28.2"`: the release tag, not the short sha. `publish-identity` reads the stamp and refuses any other; the console's floor report compares it with the identity's version, and the identity is create-only per tag. Shipped as version `<short sha of v0.28.2's commit>` and promoted to `canary/console` after pointer digests are on (runbook 5.0a). `roll-console.sh` can't build it: it stamps the short sha | TO-CONFIRM: built in 5.0a |
+| Console account, app and channel | `bab`, `console`, `canary/console` (pointer `apps/bab/console/channels/canary/console.json`) | TO-CONFIRM by 5.0a step 1's read-only checks. Flagship's console is `bab`, `canary/console`, and direct-bucket ships default to the ops account |
+| Pointer digests for `bab` | off today; must be on, and `canary/console`'s pointer must carry digests, before `publish-identity`. `carlos channels digests on --account-id bab`, then a fresh console promote (runbook 5.0a, its own Paul's go, outside any roll) | TO-DO: 5.0a |
+| Console linux-arm64 sha256 (v0.28.2) | read by `publish-identity`: the `canary/console` pointer's signed manifest digest for the console version, then that manifest's `linux-arm64` sha256, then the artefact itself, hashed and stamp-checked. Never typed by hand, and never taken from `SHA256SUMS.txt`: the phase refuses a console sha256 that is any file in the sums | TO-CONFIRM: equals the `sha256sum` of 5.0a's build |
| Router linux-arm64 sha256, v0.28.0 (current, for comparison) | `3ca7c74d5356a3995ecf677d142bc32fafaf790a2cc1bc0b628f3eec1b7ea731`, from the signed dev `control/system/router/stable.json` | confirmed |
-| Console executable on sophie | today `/opt/carlos/versions/26065378/console` (root-owned; `/opt/carlos/live/platform.dev.carlosframework.com` is a symlink to it in a carlos-owned directory), with no `.prev` beside it | BLOCKED on gap 2: no hand-made files; the protected path and its `.prev` come from v0.28.2's console install and must match the signed identity. `.prev` may equal current (platform-1e ruling) |
-| `policy.json` | an armed `hostfloor.Policy` with the floor bytes and a root-signed `moves-protocol` authorization, written byte-exact by v0.28.2's new phase with `--out` and read directly; no `jq` or other parser ever touches it | BLOCKED on gap 1 (v0.28.2) |
-| `identity.json` | the signed system pointer for v0.28.2, `control/system/router/releases/v0.28.2.json`, fetched byte-exact and read directly; no `jq` or other parser ever touches it | BLOCKED on gap 2 (v0.28.2) |
+| Console executable on sophie | fixed at `/opt/carlos/system/console/console`, with `console.prev` beside it (same bytes at arming), both root:root `0755`, created by `carlos system claims install-console` on sophie from the build 5.0a promoted (`--source /opt/carlos/versions/<CV>/console`). `guard-arm --console` takes this path. Replaces today's `/opt/carlos/versions/26065378/console`, which has no `.prev` | pending v0.28.2 (branch `claim-step6-artifacts` @ `c38a4ca9`) |
+| `policy.json` | an armed `hostfloor.Policy` with the floor bytes and a root-signed `moves-protocol` authorization, written byte-exact by `claims run` phase `sign-policy` with `--out` on the signer and read directly; no `jq` or other parser ever touches it | pending v0.28.2 (branch `claim-step6-artifacts` @ `c38a4ca9`) |
+| `identity.json` | the signed system identity for v0.28.2 at `control/system/router/releases/v0.28.2.json`, published create-only by `claims run` phase `publish-identity` on the signer and written byte-exact to `--out` as read back; read directly, no `jq` or other parser ever touches it | pending v0.28.2 (branch `claim-step6-artifacts` @ `c38a4ca9`) |
+
+## publish-identity's plan
+
+`publish-identity` (runbook step 6.1) builds the identity from signed inputs only. Its plan's `identity` object:
+
+| Field | Value | Verbatim | Source |
+|---|---|---|---|
+| `sha256sums` | the whole text of `SHA256SUMS.txt`, final newline included | yes, byte for byte | the v0.28.2 public release, `https://github.com/carlosframework/releases/releases/download/v0.28.2/SHA256SUMS.txt` |
+| `sha256sums_sig` | the whole text of `SHA256SUMS.txt.sig` | yes, byte for byte | the same release |
+| `console.account` | `bab` | yes | 5.0a step 1 |
+| `console.channel` | `canary/console` | yes | the channel 5.0a promoted to |
+| `console.version` | the console's ship version, the short sha of v0.28.2's commit | yes | 5.0a's record (`CV`) |
+| `console.app` | `console` | yes | the console app |
+| `release_tag` | `v0.28.2` | yes | the release tag |
+| `moves_protocol` | `1` | yes | `moves.ProtocolVersion` on main |
+
+The sums files are downloaded on DOCS-T1 with the signer's CLI, passed to the signer through the exchange prefix with `aws s3 cp`, and put into the plan with `jq --rawfile`, so the JSON strings hold their bytes unchanged. The phase verifies the sums against the vendor release key the carlos binary embeds, so one changed byte is refused. The identity is create-only per tag: a wrong one cannot be replaced under v0.28.2.
## Gaps found while drafting
-platform-1e confirmed gaps 1 and 2 on main on 2026-10-18 and is building the fixes, with Paul's go: (a) a `claims run` phase that signs the armed policy's authorization for a floor on the trusted host without publishing anything, and (b) the release workflow publishing the signed system identity (router and console) at `control/system/router/releases/<tag>.json`, plus the protected console install with its `.prev`. Both ship in v0.28.2, before step 6.
+platform-1e confirmed gaps 1 and 2 on main on 2026-10-18 and built the fixes, with Paul's go, on branch `claim-step6-artifacts` (`c38a4ca9`, census ruled): (a) the `claims run` phase `sign-policy`, which signs the armed policy for a floor on the trusted host and writes nothing to the bucket; (b) the `claims run` phase `publish-identity`, which publishes the signed system identity (router and console) at `control/system/router/releases/<tag>.json` from the trusted host (not the release workflow); and (c) `claims install-console`, the protected console install with its `.prev`. All ship in v0.28.2, before step 6.
1. **No tool signs the armed policy offline.** `guard-arm` (step 6) refuses anything but `mode: "armed"` with a floor and a valid authorization: a root envelope over `{schema 1, purpose "moves-protocol", deployment_bucket, key "control/moves-protocol.json", sha256 of the floor bytes}`. On main, that authorization is signed only inside `hostfloor.Publish`, which writes it to the bucket, and `PublishFloor` (step 7) refuses unless every actor already reports `FloorGuardReady` for that floor digest, which needs the armed policy. The only other producer, `joinroot.go`, reads an already published floor. So step 6 needs a reviewed way to sign the policy without publishing: for example, a `claims run` phase that signs and prints it on the trusted host. Fix: (a) above.
2. **No system identity for any release exists in the dev bucket.** `guard-arm --identity` takes a release-workflow signed pointer with `current.release_tag` and `current.moves_protocol >= 1`, stored at `control/system/router/releases/<tag>.json`. The dev bucket holds only `control/system/router/{canary,stable}.json`, whose payloads carry neither field, and `release.PublishSystemIdentity` has no production caller on main. Fix: (b) above.
-3. **The console needs a root-owned path and a `.prev`.** No hand-made files. Fix: (b) above.
+3. **The console needs a root-owned path and a `.prev`.** No hand-made files. Fix: (c) above, at the fixed path `/opt/carlos/system/console/console`.
+4. **The console's channel pointer has no digests, and the console is stamped with the short sha.** Found reviewing the branch. `publish-identity` takes the console's sha256 only through the `canary/console` pointer's signed manifest digest, and refuses a console whose `version.Build` is not the release tag. Fix: runbook 5.0a (digests on for `bab`, then a fresh promote of a console built with `version.Build=v0.28.2`), with its own Paul's go, outside any roll.
## Open items
TO-CONFIRM:
1. Console enrollment token, 32 lowercase hex characters (generated at approval).
-2. v0.28.2 published: the router sha256 from its signed `SHA256SUMS.txt`, the console sha256 from the console app's signed release manifest, and the carlos v0.28.2 CLI sha256 for the signer. This also fixes the floor's `min_release`.
-3. The bucket keys the signer needs, confirmed by a `--dry-run` of every phase from the signer.
+2. v0.28.2 published: the router sha256 from its signed `SHA256SUMS.txt`, the console sha256 (read by `publish-identity` through the digest-signed `canary/console` pointer), and the carlos v0.28.2 CLI sha256 for the signer. This also fixes the floor's `min_release`.
+3. The bucket keys the signer needs, confirmed by a `--dry-run` of every phase from the signer. `publish-identity` adds reads under `apps/bab/console/` and one write, `control/system/router/releases/v0.28.2.json` (`signing-host.md`).
+4. The console's account, app and channel (`bab`, `console`, `canary/console`) and its ship version `CV`, from 5.0a.
TO-CREATE (each with Paul's go): the `carlos-claim-signer` security group (its ID), role, instance profile and instance (`signing-host.md`).
Known now: v0.28.1 is published and on dev; the carlos v0.28.1 and jq 1.7.1 sha256 values, the AMI, the VPC and the subnet are in `signing-host.md`.
-BLOCKED (fixes ship in v0.28.2, built by platform-1e):
-4. `policy.json`: the signed armed policy (gap 1).
-5. `identity.json`: the signed v0.28.2 system identity (gap 2).
-6. Sophie's protected console path and its `.prev` (gap 2; `.prev` may equal current).
+TO-DO, its own Paul's go, after v0.28.2 and before the signing session: runbook 5.0a, pointer digests on for `bab` and a fresh promote of the console built with `version.Build=v0.28.2` (gap 4).
+
+Pending v0.28.2 (built by platform-1e on branch `claim-step6-artifacts` @ `c38a4ca9`):
+5. `policy.json`: the signed armed policy, from `sign-policy --out` (gap 1).
+6. `identity.json`: the signed v0.28.2 system identity, from `publish-identity --out` (gap 2).
+7. Sophie's protected console at `/opt/carlos/system/console/console` and its `.prev`, from `install-console` (gap 3; `.prev` equals current at arming).
Confirmed on 2026-10-18: namespace `platform`, sophie's pool `console`, console ID `dev-console`, operator `paul@keymail.dev`. Box identity records published (step 5.0): `control/boxes/leo-climbing.json` and `control/boxes/sophie-knitting.json`.
diff --git a/docs/superpowers/notes/claim-trial-inputs/identity.template.json b/docs/superpowers/notes/claim-trial-inputs/identity.template.json
index 59ffd71ba..1af62c740 100644
--- a/docs/superpowers/notes/claim-trial-inputs/identity.template.json
+++ b/docs/superpowers/notes/claim-trial-inputs/identity.template.json
@@ -2,7 +2,7 @@
"payload_before_signing": {
"schema": 1,
"component": "router",
- "channel": "TO-CONFIRM: stable (leo) / canary (sophie)",
+ "channel": "stable",
"current": {
"version": "v0.28.2",
"release_tag": "v0.28.2",
@@ -10,11 +10,18 @@
"artifacts": {
"linux-arm64": {
"key": "bootstrap/router/v0.28.2/carlos-linux-arm64",
- "sha256": "TO-CONFIRM: v0.28.2 linux-arm64 router sha256"
+ "sha256": "TO-CONFIRM: the carlos-linux-arm64 line of the v0.28.2 release's signed SHA256SUMS.txt"
+ }
+ },
+ "console_artifacts": {
+ "linux-arm64": {
+ "key": "bootstrap/console/v0.28.2/carlos-linux-arm64",
+ "sha256": "TO-CONFIRM: the console build from runbook 5.0a, read through the canary/console pointer's signed manifest digest"
}
},
"source": "release:v0.28.2"
- }
+ },
+ "spread": "0s"
},
- "note": "BLOCKED on gap 2 (fix ships in v0.28.2): the real file is the release-workflow signed pointer ({payload, sig}) that the v0.28.2 release workflow publishes at control/system/router/releases/v0.28.2.json, covering the router and the console"
+ "note": "Shape only, pending v0.28.2 (branch claim-step6-artifacts @ c38a4ca9). The real file is the root-signed identity ({payload, sig}) that the claims run phase publish-identity publishes create-only at control/system/router/releases/v0.28.2.json and writes byte-exact to --out. The channel is always stable and is read by nothing that checks an identity. No sha256 is typed: publish-identity reads both"
}
diff --git a/docs/superpowers/notes/claim-trial-inputs/policy.template.json b/docs/superpowers/notes/claim-trial-inputs/policy.template.json
index 825c7b3e0..7cf66dc56 100644
--- a/docs/superpowers/notes/claim-trial-inputs/policy.template.json
+++ b/docs/superpowers/notes/claim-trial-inputs/policy.template.json
@@ -6,5 +6,5 @@
"min": 1,
"min_release": "v0.28.2"
},
- "authorization": "BLOCKED on gap 1 (fix ships in v0.28.2): root-signed envelope of {schema: 1, purpose: \"moves-protocol\", deployment_bucket: \"carlos-dev-640012953448\", key: \"control/moves-protocol.json\", sha256: <sha256 of the exact floor bytes>}, signed on the trusted host by the new claims run phase without publishing"
+ "authorization": "Pending v0.28.2 (branch claim-step6-artifacts @ c38a4ca9): root-signed envelope of {schema: 1, purpose: \"moves-protocol\", deployment_bucket: \"carlos-dev-640012953448\", key: \"control/moves-protocol.json\", sha256: <sha256 of the exact floor bytes>}, signed on the trusted host by the claims run phase sign-policy, which writes nothing to the bucket and writes the policy byte-exact to --out"
}
diff --git a/docs/superpowers/notes/claim-trial-inputs/signing-host.md b/docs/superpowers/notes/claim-trial-inputs/signing-host.md
index ff05084d1..0b3ffb29c 100644
--- a/docs/superpowers/notes/claim-trial-inputs/signing-host.md
+++ b/docs/superpowers/notes/claim-trial-inputs/signing-host.md
@@ -23,7 +23,7 @@ The signer reuses the AMI, type, subnet and launch options. It gets its **own**
## Separation of duties
-- **The signer** runs only the signing work: `refresh`, `apply_plan` and `phase` from step 5, and the floor, home and bind-key phases of step 7. It has no `ssm:SendCommand`, so it can't reach the boxes.
+- **The signer** runs only the signing work: `refresh`, `apply_plan` and `phase` from step 5, `sign-policy` and `publish-identity` through `phase_out` in step 6, and the floor, home and bind-key phases of step 7. It has no `ssm:SendCommand`, so it can't reach the boxes.
- **DOCS-T1** (profile `kass`) runs every box helper (`ssm_run`, `box_get`, `box_put`, `box_inspect`) and the `jq` parsing for the boxes, as in steps 5 to 7 today.
- **Files move between them** through `s3://carlos-dev-640012953448/claim-signer/<run-id>/`: inputs (`regions.json`, `leo.json` and the rest, plus `release.pub`) down to the signer, and outputs (`inspect.json`, `roster.json`, the enrollment JSON files) back to DOCS-T1.
- On the signer, `CARLOS=/usr/local/bin/carlos`, and the runbook's `export AWS_PROFILE=kass` line is left out: the signer uses its instance role.
@@ -54,10 +54,14 @@ Trust: `ec2.amazonaws.com`. Managed policy: `AmazonSSMManagedInstanceCore` (Sess
"Resource": "arn:aws:ssm:eu-west-1:640012953448:parameter/carlos/release-key"},
{"Sid": "ListForNotFound", "Effect": "Allow", "Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::carlos-dev-640012953448",
- "Condition": {"StringLike": {"s3:prefix": ["control/*", "status/boxes/*", "claim-signer/*"]}}},
+ "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/*"]},
+ {"Sid": "ReadConsoleRelease", "Effect": "Allow", "Action": "s3:GetObject",
+ "Resource": "arn:aws:s3:::carlos-dev-640012953448/apps/bab/console/*"},
+ {"Sid": "WriteIdentity", "Effect": "Allow", "Action": "s3:PutObject",
+ "Resource": "arn:aws:s3:::carlos-dev-640012953448/control/system/router/releases/v0.28.2.json"},
{"Sid": "WriteRosterAndFloor", "Effect": "Allow", "Action": "s3:PutObject",
"Resource": ["arn:aws:s3:::carlos-dev-640012953448/control/host-actors.json",
"arn:aws:s3:::carlos-dev-640012953448/control/host-actor-invitations/*",
@@ -75,6 +79,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.
+- **ReadConsoleRelease and WriteIdentity** (added 2026-10-18 for step 6.1, pending v0.28.2, branch `claim-step6-artifacts` @ `c38a4ca9`): `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.2.json`, create-only. `sign-policy` writes nothing. The runner admits only that one identity key for the phase, whatever IAM allows.
**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.
@@ -99,9 +104,9 @@ Known values (2026-10-18):
- carlos v0.28.2: TO-CONFIRM when published. The signing session uses it.
On DOCS-T1, before launching:
-1. Download the carlos CLI `carlos-linux-arm64` (v0.28.2: the session runs 5.1 through 7, and step 6's policy signing needs v0.28.2), with its `SHA256SUMS` and `SHA256SUMS.sig`, from the public release. Verify the signature as the README's "Releasing the platform" verification does, then `sha256sum -c`. Record the sha256.
+1. Download the carlos CLI `carlos-linux-arm64` (v0.28.2: the session runs 5.1 through 7, and step 6's `sign-policy` and `publish-identity` need v0.28.2), with the release's `SHA256SUMS.txt` and `SHA256SUMS.txt.sig`, from the public release. Verify the signature as the README's "Releasing the platform" verification does, then `sha256sum -c`. Record the sha256. Keep both sums files exactly as downloaded: step 6.1's `publish-identity` plan carries them verbatim.
2. Download jq 1.7.1 `jq-linux-arm64` and the release's `sha256sum.txt` from `github.com/jqlang/jq/releases/tag/jq-1.7.1`, then check it with `sha256sum -c`. Record the sha256.
-3. Upload both to `s3://carlos-dev-640012953448/claim-signer/<run-id>/`.
+3. Upload the carlos CLI, jq, `SHA256SUMS.txt` and `SHA256SUMS.txt.sig` to `s3://carlos-dev-640012953448/claim-signer/<run-id>/` with `aws s3 cp`, which copies bytes unchanged.
On the signer, as root:
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 2f08bc6fc..79c31e593 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
@@ -1,6 +1,6 @@
# Host claim activation, dev cohort 1: mechanics trial runbook
-Draft, first written 2026-10-16, revised 2026-10-17 against main at 26065378. It authorizes nothing. Every change below waits for Paul's go at its own checkpoint. Neither the approved copy quoted in step 8 nor the activation tools' tests authorize running anything.
+Draft, first written 2026-10-16, revised 2026-10-17 against main at 26065378. Step 5.0a and step 6 were revised on 2026-10-18 against platform-1e's branch `claim-step6-artifacts` at `c38a4ca9` (pending v0.28.2). It authorizes nothing. Every change below waits for Paul's go at its own checkpoint. Neither the approved copy quoted in step 8 nor the activation tools' tests authorize running anything.
Dev cohort 1 is a mechanics trial (Paul, 2026-10-16, relayed by platform-1e):
- The scope is one box, `carlos-dev-ie-b` (leo-climbing). `carlos-dev-uk` is not part of this trial; it stays a later option.
@@ -191,13 +191,13 @@ Pass: all twelve are root-owned regular files in `/etc/systemd/system`; the edge
## Blockers
-1. **Release.** Resolved: v0.28.0, cut from 26065378 (Plan 14 Parts 2 and 3, the activation tools, e8e961dd), is fully rolled on dev (2026-10-18). Step 1. Two dev-only releases follow, each rolled to dev with Paul's go: v0.28.1 (the box-record verb), rolled 2026-10-18 and used by step 5.0 (done), and v0.28.2 (gap 1 and 2 fixes), which everything past 5.0 waits for.
+1. **Release.** Resolved: v0.28.0, cut from 26065378 (Plan 14 Parts 2 and 3, the activation tools, e8e961dd), is fully rolled on dev (2026-10-18). Step 1. Two dev-only releases follow, each rolled to dev with Paul's go: v0.28.1 (the box-record verb), rolled 2026-10-18 and used by step 5.0 (done), and v0.28.2 (gap 1 and 2 fixes), which everything past 5.0 waits for. The v0.28.2 dev roll moves the boxes only: leave out step 1's console item, because step 5.0a ships and promotes the dev console's v0.28.2 build after pointer digests are on.
2. **Empty app segment.** 267 objects under `apps/bab//` (P5). Source: leo's app-less `proof*` routes, removed (2026-10-16). Cleanup waits for the e8e961dd release to reach leo and for Paul's approval. Step 2.
3. **IAM.** Leo's object-version actions were applied on 2026-10-18 (targeted, `0 to add, 1 to change, 0 to destroy`): the claim's data store lists, reads and deletes object versions in `carlos-dev-ie-640012953448` on the seed and cold-wake paths. The fence-writer role (`carlos-dev-ie-b-fence-writer`, bootstrap user `carlos-dev-ie-b-fence-writer-root`, no access key yet) and the deployment bucket policy were applied on 2026-10-18 from the saved targeted plan (`5 added, 0 changed, 0 destroyed`) and merged as platform-infrastructure#46 (`beb346b`). The bucket policy names that role as the only fence writer. After the apply, wakecheck still served on sophie (200) and leo answered `waking elsewhere` (sophie held the lease), with no AccessDenied in either box's journals. Step 3.
4. **Box identity records.** Resolved 2026-10-18 (step 5.0 done: `control/boxes/leo-climbing.json` and `control/boxes/sophie-knitting.json`). Step 5's enrollment needs each box's signed AWS identity record at `control/boxes/<box>.json`. None exists, and none has ever been written: the publisher (`internal/host/boxrecord.go`, `boxRecordLoop`) runs only with `CARLOS_REGION_HA` on, which is off on every dev box. Leo's and sophie's records must be published before step 5. Paul ruled a reviewed one-off publish, with region HA staying off.
The verb `carlos system box publish-record` (branch `box-publish-record`, merging now) ships in v0.28.1, dev only, and is the supported way. It replaces the agent's region-HA-only publisher for this trial; step 5.0 runs it once per box. The record is AWS-signed (the EC2 instance identity document and AWS's signature over it), not signed by the box or the operator, and grants nothing until enrollment reads it.
-5. **Signed armed policy and system identity (everything past 5.0).** Step 6's `guard-arm` needs an armed `policy.json` (the floor plus a root-signed `moves-protocol` authorization) and a signed system identity at `control/system/router/releases/<tag>.json`. On main, nothing signs the policy without publishing it, and nothing publishes a system identity; the dev bucket has none. platform-1e confirmed both gaps on 2026-10-18 and is building the fixes, with Paul's go: (a) a `claims run` phase that signs the armed policy's authorization for a floor on the trusted host without publishing anything; (b) the release workflow publishing the signed system identity (router and console), and the protected console install with its `.prev`. Both ship in v0.28.2, so the floor's `min_release` is v0.28.2. By Paul's ruling the signing session (5.1 to 7) opens only after v0.28.2, so this blocker gates everything past 5.0. Details in `docs/superpowers/notes/claim-trial-inputs/README.md`.
+5. **Signed armed policy and system identity (everything past 5.0).** Step 6's `guard-arm` needs an armed `policy.json` (the floor plus a root-signed `moves-protocol` authorization) and a signed system identity at `control/system/router/releases/<tag>.json`. On main, nothing signs the policy without publishing it, and nothing publishes a system identity; the dev bucket has none. platform-1e confirmed both gaps on 2026-10-18 and built the fixes, with Paul's go, on branch `claim-step6-artifacts` (`c38a4ca9`, census ruled; pending v0.28.2): (a) the `claims run` phase `sign-policy`, which signs the armed policy for a floor on the trusted host and writes nothing to the bucket; (b) the `claims run` phase `publish-identity`, which publishes the release's signed identity (router and console) from the trusted host, not from the release workflow; (c) `claims install-console`, which puts the protected console and its `.prev` at `/opt/carlos/system/console/console` on sophie. All three ship in v0.28.2, so the floor's `min_release` is v0.28.2. `publish-identity` also needs the dev console's channel pointer to carry signed manifest digests and a console built with `version.Build=v0.28.2` (step 5.0a). By Paul's ruling the signing session (5.1 to 7) opens only after v0.28.2, so this blocker gates everything past 5.0. Details in `docs/superpowers/notes/claim-trial-inputs/README.md`.
## Accepted trial gaps
@@ -209,6 +209,7 @@ Paul deferred these for this trial only. They are not blockers here, and they mu
## Owed before production cohorts
- **Dev module drift.** A whole-root `tofu plan` of `platform-infrastructure/dev` on 2026-10-17 showed 15 adds and 1 change (13 adds after leo's `host_buckets` change and the box-records bucket policy were applied for the trial; whole-root plan from main `a0b3f5a` on 2026-10-18: `13 to add, 0 to change, 0 to destroy`) that come from the platform modules (pinned `?ref=main`) moving ahead of dev: termination SQS queues and EventBridge rules for both IE boxes, the deployment bucket's lifecycle rule and its box-records bucket policy, leo's object-version actions (`host_buckets` gains `PullDoorObjectVersions`, `s3:DeleteObjectVersion` and `PullDoorVersionList`; applied for the trial on 2026-10-18), and an SSM parameter scope policy on both IE roles (a deny on parameter reads outside `/carlos/*`). It is owed: its own plan and Paul's approval later, before production cohorts. It is not part of step 7a, whose plan is targeted at its own three resources. Step 3's IAM overlaps it (see open question 3).
+- **A trusted build attestation for the console.** `publish-identity` (step 6) accepts a console artefact only if its `version.Build` stamp is the release tag. It reads that stamp from the `-ldflags` the Go build info records (`release.ArtifactBuild`), because release binaries are stripped (`-s -w`) and the linked value itself cannot be read. So the phase checks what the linker was told, not what it linked. Production cohorts need an attestation from a trusted builder, tied to the console artefact's sha256, that says which source and which stamp produced those bytes. The trial accepts the gap because only the operator holds the release key, so only the operator can ship and promote a console build the phase would accept.
## Step 1. Release and roll, claims off
@@ -240,7 +241,7 @@ Hazards: H3 (#272, never the old road on leo) and H4 (the old-updater instance-c
On failure: `carlos system rollback --console https://platform.dev.carlosframework.com --channel stable` (and `canary`). This is an ordinary claims-off release rollback, and it works only while no actor is armed. From step 6 on, never run below the floor.
-Failure handling for the rest of the runbook: steps 1 to 4 change nothing claim-related, so their own undo applies. From step 5.1 on, a failure means stop and hold. Before step 8 there are no claim-mode units, so "hold" means: stop the runbook, change nothing more, and leave leo serving claims off as it is. From step 8 on, follow "Stop and hold".
+Failure handling for the rest of the runbook: steps 1 to 4, 5.0 and 5.0a change nothing claim-related, so their own undo applies. From step 5.1 on, a failure means stop and hold. Before step 8 there are no claim-mode units, so "hold" means: stop the runbook, change nothing more, and leave leo serving claims off as it is. From step 8 on, follow "Stop and hold".
## Step 2. Clean up the empty app segment
@@ -313,7 +314,7 @@ Why a pool: dev's two home boxes both mint every unregioned, pool-less record. A
## Step 5. Box identity records, roster and enrollment
-Step 5.0 is done. 5.1 (roster and enrollment) runs inside the signing session, after v0.28.2 (see "The signing session").
+Step 5.0 is done. 5.0a (pointer digests and the release-stamped console) runs after v0.28.2 is on dev and before the signing session opens. 5.1 (roster and enrollment) runs inside the signing session (see "The signing session").
### 5.0 Box identity records (done 2026-10-18)
@@ -375,12 +376,104 @@ the record at control/boxes/sophie-knitting.json is valid AWS-signed evidence fo
Any other answer is a stop. The `invite-box` dry run in 5.1 then checks the same evidence again, from the signer.
+### 5.0a Pointer digests and the release-stamped dev console
+
+Builder finding from the review of platform-1e's branch `claim-step6-artifacts` (`c38a4ca9`), drafted for Paul after v0.28.2.
+
+Runs after v0.28.2 is on dev's boxes and checked, and before the signing session opens. Paul's go: separate, for this step alone. It is not part of any roll. The roll notes from v0.26.0 to v0.28.0 say "don't run `carlos channels digests on`" while rolling, so this step runs on its own, with no roll in progress. It changes nothing claim-related: leo's claims stay off and the roster is not touched.
+
+Why it is needed: `publish-identity` (step 6) takes the console's sha256 only from the console channel's root-verified pointer. It reads the pointer's signed manifest digest for the exact console version, then that manifest, then the artefact, whose `version.Build` stamp must be `v0.28.2`. Dev's `canary/console` pointer carries no digests today, so the phase would refuse it. The console's own floor report compares its build stamp with the identity's version, and the identity is create-only per tag, so a console stamped with the short sha cannot be put right later under v0.28.2.
+
+Scope: the digest marker is per account. The console app lives in account `bab` (`apps/bab/console/`), alongside wakecheck, claimtrial and the other dev test apps. With digests on for `bab`, every upgraded writer signs digests into every `bab` pointer it writes. A box that has verified a pointer with digests refuses later pointers for that channel without them (README, "Pointer digests (#253): writers before readers").
+
+Values, on DOCS-T1:
+- `CARLOS`: the carlos v0.28.2 CLI for DOCS-T1, downloaded from the public release and checked against the release's signed `SHA256SUMS.txt`, the same way `signing-host.md` checks the signer's copy.
+- `CV`: the console's ship version, the short sha of v0.28.2's commit. `carlos ship` compares a re-ship with the stored manifest byte for byte and refuses different bytes, so `CV` must not have been shipped for `console` before. If it has, stop and ask. Never make up another version.
+- `release.pub`: the dev release public key (fingerprint `960d03ff`), the same file 5.1 stages.
+
+**1. Writers before readers (read-only).** Every writer that can promote into `bab` on dev must sign digests before the marker goes on. Digest signing first shipped in v0.26.0 (#253).
+
+The dev console is on v0.28.x:
+
+```bash
+curl -sI https://platform.dev.carlosframework.com/ | grep -i x-carlos-version
+$TMPDIR/cohort1/ssm.sh eu-west-1 i-05498eada6f0afd23 'readlink /proc/$(systemctl show -p MainPID --value console.service)/exe'
+```
+
+Pass: the header names a v0.28.x commit (`26065378` for v0.28.0, `3b69c2bd` for v0.28.1, or v0.28.2's), and the running executable is `/opt/carlos/versions/<that version>/console`. Expected today: `3b69c2bd`. Record it as the rollback version. Promotes made through the console are signed by the console, which reads the marker itself, so this check covers every console-mode CLI and CI job.
+
+Every CLI that promotes direct-bucket on dev understands digests:
+- On DOCS-T1: `"$CARLOS" version` prints `carlos v0.28.2`; it is the `CARLOS_BIN` for `ship-app.sh` below. `carlos version` (the copy on `PATH`) prints v0.26.0 or later (P3 saw v0.27.2 on 2026-10-17).
+- On the boxes: P3's box line shows v0.28.2 on leo, sophie and dennis after the roll. A box CLI is a writer only if someone promotes from the box.
+- Everyone else with dev bucket credentials who promotes direct-bucket into `bab`: platform-d8 (H2) and any other agent or CI job. Ask on #250 and wait for a clear answer from each: `carlos version` is v0.26.0 or later, or they will not promote on dev until this step is done.
+
+The state before:
+
+```bash
+AWS_PROFILE=kass AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=carlos-dev-640012953448 CARLOS_RELEASE_PUBKEY="$(cat release.pub)" \
+ "$CARLOS" channels digests --account-id bab
+aws --profile kass s3 ls s3://carlos-dev-640012953448/apps/bab/console/channels/canary/
+```
+
+Expected: `pointer digests: off for bab`, then a table of the `bab` channels without digests, including `bab console canary/console <rollback version> no`. The listing shows `console.json`. Any other account for the console is a stop: the inputs README's console release values assume `bab`.
+
+**2. Build the console (local only).** From a clean checkout at the v0.28.2 tag:
+
+```bash
+CV=$(git rev-parse --short 'v0.28.2^{commit}')
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
+ -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=v0.28.2" \
+ -o console-linux-arm64 ./cmd/console
+go version -m console-linux-arm64 | grep -- -ldflags
+sha256sum console-linux-arm64
+```
+
+Expected: the `-ldflags` line holds `-X github.com/carlosframework/platform/internal/version.Build=v0.28.2` exactly once, and no other `version.Build`. `publish-identity` refuses an unstamped console, one stamped twice, or one stamped with anything but the tag. Don't use `roll-console.sh`: it stamps the short sha.
+
+**3. Turn digests on for `bab`.** The step's one go covers 3 and 4 together.
+
+```bash
+export AWS_PROFILE=kass AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=carlos-dev-640012953448 CARLOS_RELEASE_PUBKEY="$(cat release.pub)"
+CARLOS_RELEASE_KEY=$(aws ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \
+ "$CARLOS" channels digests on --account-id bab
+```
+
+The key is read the way `ship-app.sh` reads it, and never printed. Expected, verbatim from main: `pointer digests on for bab. Upgraded writers sign digests from now on`, then `pointer digests: on for bab since <time>`, then the same table as in 1. Existing pointers get digests at their next promote.
+
+**4. Promote the release-stamped console.**
+
+```bash
+CARLOS_BIN=$CARLOS platform-infrastructure/scripts/ship-app.sh kass carlos-dev-640012953448 console "$CV" canary/console binary console-linux-arm64
+```
+
+Sophie stages, adopts and restarts `console.service` on its own, in about 20 seconds, so the dev console is briefly down.
+
+Expected:
+- `x-carlos-version` (as in 1) shows `$CV`;
+- the `readlink` from 1 shows `/opt/carlos/versions/$CV/console`. That path is `install-console`'s `--source` in step 6;
+- `channels digests --account-id bab` (as in 1) no longer lists `console canary/console`, and no row says `written after digests were turned on`. Such a row means an old writer is still in use: stop and report on #250;
+- wakecheck still answers 200 through DNS.
+
+Finish this step before the signing session opens. Its `GetParameter` reads come from `remote-agent`, and the CloudTrail check at the session's close counts from the session's start.
+
+Rollback:
+- **Before 4** (only the marker is written): run 3 again with `off` in place of `on`. No box has seen a digest yet, so this is a full undo. Expected: `pointer digests off for bab. Boxes that have already seen digests keep refusing pointers without them`.
+- **After 4:** leave digests on. Sophie has verified a `canary/console` pointer with digests and refuses later pointers for that channel without them, so with digests off the next console promote would not be adopted. To back out the console build, promote the recorded rollback version with the same v0.28.2 CLI while digests stay on. That pointer carries digests, so sophie accepts it:
+
+ ```bash
+ export AWS_PROFILE=kass AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=carlos-dev-640012953448
+ CARLOS_RELEASE_KEY=$(aws ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \
+ "$CARLOS" promote -app console <rollback version> canary/console
+ ```
+
+ Then check `x-carlos-version` and the `readlink` as in 1. `carlos channels digests off` is only for a broken writer (README), and it does not clear boxes that have already seen digests.
+
## The signing session: 5.1, 6 and 7
Paul's ruling, 2026-10-18: the trusted signing host is created once, after v0.28.2 is on dev, for one session that covers 5.1, step 6 and step 7, so the release key is on a host only once. The trial waits for v0.28.2 before anything past 5.0.
Order of work:
-1. **Open the session:** create the signer (TO-CREATE, Paul's go), install the verified tools, and fetch the key into tmpfs (`signing-host.md`, "Create" and "Getting the key there").
+1. **Open the session,** only once 5.0a has passed: create the signer (TO-CREATE, Paul's go), install the verified tools, and fetch the key into tmpfs (`signing-host.md`, "Create" and "Getting the key there").
2. **5.1** roster and enrollment.
3. **Step 6:** arm leo's guard and start the guarded console.
4. **Step 7:** readers, floor, home and leo's key.
@@ -405,12 +498,22 @@ For commands that write, add --dry-run to validate and print the planned operati
Run signing phases on the trusted operator host. Run box installers as root.
```
-`run` takes a JSON plan whose `phase` is one of `initialize`, `configure`, `invite-box`, `enroll-box`, `invite-console`, `enroll-console`, `publish-floor`, `home` or `bind-key`, with `bucket` and `expected_revision`. Every `carlos` command below is quoted from platform-1e's report (run 2, sections A to C; run 11 confirms these command branches are byte-identical on main), and every flag was checked against main's code.
+Pending v0.28.2 (branch `claim-step6-artifacts` @ `c38a4ca9`), the help changes in three places, quoted from that branch's `internal/host/activation_command.go`:
+
+```
+ run --plan FILE --signing-key FILE --public-key K --aws-region R [--out FILE]
+ install-console --identity FILE --source FILE --bucket B --aws-region R --public-key K
+The sign-policy and publish-identity phases write their file to --out, a new path. Pass it to guard-arm as it is.
+```
+
+`install-console` sits between `console-report` and `install-control`, and the last line follows "Run signing phases on the trusted operator host. Run box installers as root." Check these against `/usr/local/bin/carlos system claims help` on the signer before step 6; any difference is a stop.
+
+`run` takes a JSON plan whose `phase` is one of `initialize`, `configure`, `invite-box`, `enroll-box`, `invite-console`, `enroll-console`, `publish-floor`, `home` or `bind-key`, with `bucket` and `expected_revision`. v0.28.2 adds `sign-policy` and `publish-identity` (pending, same branch); these two must have `--out`, and every other phase refuses it. Every `carlos` command below is quoted from platform-1e's report (run 2, sections A to C; run 11 confirms these command branches are byte-identical on main), and every flag was checked against main's code.
**No new tools on the boxes (platform-1e, 2026-10-17).** `jq` is not installed on leo or sophie, and nothing is installed for the trial. Every `jq` line runs on the trusted signing host. A box writes raw JSON (`inspect.json`, `key-proof.json`) to a file; the trusted host fetches it over SSM, parses it there, and stages the results (`roster.json`, `rev`, `le`) back on the box over SSM. The box commands read those files with `cat`. A wrong or stale staged roster is refused, because `console-config` and `evidence-key` verify the roster's signature and its revision against `--expect-revision`.
Where they run:
-- **The trusted signing host:** a throwaway dev instance, `carlos-claim-signer`, proposed in `docs/superpowers/notes/claim-trial-inputs/signing-host.md` (root-runner pattern, SSM only, its own role that may read `/carlos/release-key`). It holds `/root/claim-trial` with the inputs, the key in a `0600` tmpfs file at `/run/claim-signer/release.key`, `jq` and the carlos v0.28.2 CLI, both checked by sha256. It runs only the signing helpers (`refresh`, `apply_plan`, `phase`) with its instance role; set `KEY=/run/claim-signer/release.key` and `CARLOS=/usr/local/bin/carlos`, and leave out `export AWS_PROFILE=kass`. The release signing key never goes on either box.
+- **The trusted signing host:** a throwaway dev instance, `carlos-claim-signer`, proposed in `docs/superpowers/notes/claim-trial-inputs/signing-host.md` (root-runner pattern, SSM only, its own role that may read `/carlos/release-key`). It holds `/root/claim-trial` with the inputs, the key in a `0600` tmpfs file at `/run/claim-signer/release.key`, `jq` and the carlos v0.28.2 CLI, both checked by sha256. It runs only the signing helpers (`refresh`, `apply_plan`, `phase`, and step 6's `phase_out`) with its instance role; set `KEY=/run/claim-signer/release.key` and `CARLOS=/usr/local/bin/carlos`, and leave out `export AWS_PROFILE=kass`. The release signing key never goes on either box.
- **The operator host (DOCS-T1, profile `kass`):** runs the box helpers below (`ssm_run`, `box_get`, `box_put`, `box_inspect`) and the `jq` parsing for the boxes. Files move between the signer and DOCS-T1 through `s3://carlos-dev-640012953448/claim-signer/<run-id>/`.
- **Leo and sophie:** attended root shells for the box blocks, which use only the platform binary, `systemctl` and the shell. Inputs are staged at the exact paths shown, root-owned, private files `0600`, no symlinks or extra hard links. Staging is a checkpoint, not permission to make up signatures.
@@ -527,24 +630,82 @@ for f in release.pub leo-climbing-enrollment.json; do box_put "$LEO" "$f" "/root
for f in release.pub sophie-knitting-enrollment.json console-id; do box_put "$SOPHIE" "$f" "/root/claim-trial/$f"; done
```
-Expected: `staged` for each file. No private signing key is transferred. `policy.json`, `identity.json` and `console-path` are staged in step 6, once v0.28.2 exists.
+Expected: `staged` for each file. No private signing key is transferred. `policy.json` and `identity.json` are made and staged in step 6.
On failure: hold. A partly applied phase is resumed from the latest authenticated revision, never recreated.
## Step 6. Arm leo's guard and start the guarded console
-Waits for v0.28.2 on dev: `policy.json` is signed on the trusted host by v0.28.2's new `claims run` phase (blocker 5a), `identity.json` is the v0.28.2 system identity the release workflow publishes (5b), and sophie's `--console` path and its `.prev` come from v0.28.2's console install (5b; `.prev` may equal current). Never hand-make any of them.
+Inside the signing session, after 5.1. Waits for v0.28.2 on dev and for 5.0a. Everything this step adds to main is pending v0.28.2 (branch `claim-step6-artifacts` @ `c38a4ca9`): the `claims run` phases `sign-policy` and `publish-identity`, `claims run --out`, and `claims install-console`. Never hand-make any of their files. The builder findings from the review of that branch (the release-tag stamp, 5.0a, the fixed console path and the verbatim identity inputs) are drafted here for Paul after v0.28.2.
+
+Paul's go: once for step 6. It restarts `console.service` on sophie, which briefly interrupts the dev console.
+
+### 6.1 Sign the policy and publish the identity (signer)
-`policy.json` and `identity.json` are byte-exact files, read directly. Nothing parses or rewrites them: no `jq`, no reformatting, no copy and paste. `policy.json` is written by the new phase with `--out policy.json` on the signer. `identity.json` is the object at `control/system/router/releases/v0.28.2.json`, fetched unchanged with `aws s3 cp`. Record each file's sha256 where it is made; `box_put` checks the same sha256 on arrival. Stage them, with sophie's protected `console-path`, from DOCS-T1:
+`policy.json` and `identity.json` are byte-exact files, read directly. Nothing parses or rewrites them: no `jq`, no reformatting, no copy and paste. Each phase writes its file to `--out`: a new absolute path in a root-owned directory that no group or other user can write. The phase creates it `0600`, reads it back, and refuses a path that already exists. The dry run needs `--out` too; it checks the path and writes nothing. `apply_plan` passes no `--out`, so add this helper beside `phase` on the signer:
```bash
-for f in policy.json identity.json; do box_put "$LEO" "$f" "/root/claim-trial/$f"; done
-for f in policy.json identity.json console-path; do box_put "$SOPHIE" "$f" "/root/claim-trial/$f"; done
+# phase_out <phase> <input> <out file> <result file>: phase, for the two phases that write a file to --out.
+phase_out() {
+ refresh
+ jq -n --arg phase "$1" --arg bucket "$B" --argjson rev "$REV" \
+ --slurpfile input "$2" '{phase:$phase,bucket:$bucket,expected_revision:$rev} + $input[0]' > plan.json
+ "$CARLOS" system claims run --plan /root/claim-trial/plan.json --signing-key "$KEY" --public-key "$PUB" --aws-region "$R" --out "$3" --dry-run
+ "$CARLOS" system claims run --plan /root/claim-trial/plan.json --signing-key "$KEY" --public-key "$PUB" --aws-region "$R" --out "$3" > "$4"
+ test "$(sha256sum "$3" | cut -d' ' -f1)" = "$(jq -er .output_sha256 "$4")"
+ refresh
+}
```
-The exact command for the new phase comes with v0.28.2.
+**The armed policy:**
-Paul's go: once for step 6. It restarts `console.service` on sophie, which briefly interrupts the dev console.
+```bash
+jq -n --slurpfile x floor.json '{floor:$x[0]}' > floor-phase.json
+phase_out sign-policy floor-phase.json /root/claim-trial/policy.json sign-policy-result.json
+jq -er .floor_sha256 sign-policy-result.json
+```
+
+Expected: the dry run, then the apply, succeed; `writes` is empty, because the phase writes nothing to the bucket; `floor_sha256` is the floor's digest, kept for step 7. Step 7's `publish-floor` writes the same floor bytes and authorization from the same `floor.json` (the branch's tests pin that). `sign-policy` refuses a floor other than the one the deployment is already bound to.
+
+**The identity.** `publish-identity`'s plan carries these inputs, the first four verbatim:
+
+| Plan field | Value | Where it comes from |
+|---|---|---|
+| `identity.sha256sums` | the exact text of `SHA256SUMS.txt`, final newline included | the v0.28.2 public release: `https://github.com/carlosframework/releases/releases/download/v0.28.2/SHA256SUMS.txt` |
+| `identity.sha256sums_sig` | the exact text of `SHA256SUMS.txt.sig` | the same release, same path with `.sig` |
+| `identity.console.account` | `bab` | the console app's account, checked in 5.0a step 1 |
+| `identity.console.channel` | `canary/console` | the channel 5.0a promoted to |
+| `identity.console.version` | `$CV` | the console's ship version from 5.0a (the short sha of v0.28.2's commit) |
+| `identity.console.app` | `console` | the console app |
+| `identity.release_tag` | `v0.28.2` | 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. 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=<the console ship version recorded in 5.0a>
+jq -n --rawfile sums SHA256SUMS.txt --rawfile sig SHA256SUMS.txt.sig --arg cv "$CV" \
+ '{identity:{release_tag:"v0.28.2",moves_protocol:1,sha256sums:$sums,sha256sums_sig:$sig,
+ console:{account:"bab",app:"console",channel:"canary/console",version:$cv}}}' > identity-phase.json
+phase_out publish-identity identity-phase.json /root/claim-trial/identity.json publish-identity-result.json
+jq '{identity_key, release}' publish-identity-result.json
+```
+
+Expected:
+- the dry run, then the apply, succeed, and `writes` lists one create-only put of `control/system/router/releases/v0.28.2.json`;
+- `identity_key` is that key, and `release` shows `version` and `release_tag` `v0.28.2`, `moves_protocol` `1`, `artifacts.linux-arm64.sha256` equal to the `carlos-linux-arm64` line of `SHA256SUMS.txt`, and `console_artifacts.linux-arm64.sha256` equal to the `sha256sum` of 5.0a's build;
+- `identity.json` is the published object's bytes as read back, the same file `aws s3 cp` of that key would give.
+
+The identity is create-only per tag. Publishing the same identity again writes nothing; a different one under v0.28.2 is refused and can never replace it. So check the console values against 5.0a before the apply. A wrong identity, once published, is a stop, and putting it right needs a new release tag. A rerun after a successful publish needs a new `--out` path (for example `identity.2.json`); it writes nothing to the bucket, and its file must have the same sha256.
+
+Copy both files to DOCS-T1 through `s3://carlos-dev-640012953448/claim-signer/<run-id>/` with `aws s3 cp`, and record each sha256 on both sides. `box_put` checks the same sha256 on arrival. Stage them from DOCS-T1:
+
+```bash
+for f in policy.json identity.json; do box_put "$LEO" "$f" "/root/claim-trial/$f"; done
+for f in policy.json identity.json; do box_put "$SOPHIE" "$f" "/root/claim-trial/$f"; done
+```
+
+### 6.2 Arm leo
On leo, after the release and protected current and previous binaries are installed:
@@ -558,6 +719,10 @@ set +a
--identity /root/claim-trial/identity.json --enrollment /root/claim-trial/leo-climbing-enrollment.json
```
+### 6.3 Install the protected console, arm sophie and start the guarded console
+
+The console path is fixed: `/opt/carlos/system/console/console`, with `console.prev` beside it, both created by `carlos system claims install-console` on sophie (pending v0.28.2, branch `claim-step6-artifacts` @ `c38a4ca9`). Its `--source` is the console build 5.0a promoted, at `/opt/carlos/versions/<CV>/console`, the path 5.0a's `readlink` showed.
+
On the trusted host, stage sophie's current roster and revision:
```bash
@@ -575,8 +740,13 @@ set -a
set +a
PUB=$(cat release.pub)
CID=$(cat console-id)
-CONSOLE_BINARY=$(cat console-path)
REV=$(cat rev)
+CONSOLE_SOURCE=/opt/carlos/versions/<CV>/console
+CONSOLE_BINARY=/opt/carlos/system/console/console
+/opt/carlos/carlos system claims install-console --identity /root/claim-trial/identity.json --source "$CONSOLE_SOURCE" \
+ --bucket carlos-dev-640012953448 --aws-region eu-west-1 --public-key "$PUB" --dry-run
+/opt/carlos/carlos system claims install-console --identity /root/claim-trial/identity.json --source "$CONSOLE_SOURCE" \
+ --bucket carlos-dev-640012953448 --aws-region eu-west-1 --public-key "$PUB"
/opt/carlos/carlos system guard-arm --policy /root/claim-trial/policy.json \
--identity /root/claim-trial/identity.json --enrollment /root/claim-trial/sophie-knitting-enrollment.json \
--console "$CONSOLE_BINARY"
@@ -593,13 +763,14 @@ systemctl is-active console.service
```
Expected:
+- `install-console`'s dry run, then its apply, print `release_tag` `v0.28.2`, a `sha256` equal to the identity's `console_artifacts.linux-arm64.sha256`, `path` `/opt/carlos/system/console/console`, `previous` `/opt/carlos/system/console/console.prev` and `mode` `0755`. Both copies are the same bytes, owned by root (`.prev` equals current at arming). It does not touch `console.service`. It refuses a staged identity that differs from the published one, an identity that does not name sophie's running build (sophie's `/opt/carlos/carlos` must be v0.28.2), and a source with a symlink on its path, more than one link, or bytes other than the identity's console sha256. A refusal is a stop.
- `guard-arm` succeeds on both. On leo it writes `/etc/carlos/host-actor.json` and installs `/etc/systemd/system/carlos-edge.service.d/90-host-floor.conf`, which step 8 has to handle.
- `console-config` writes `/etc/carlos/claim-console.env` and `/etc/systemd/system/console.service.d/91-claim-runtime.conf`, reloads systemd and does not start anything.
- `console.service` is `active` and the report publishes. If reporting refuses because a just-started console or box has no fresh telemetry yet, hold, read the journal, wait for the normal report and rerun the reporting pair. Never refresh timestamps by hand. If a command refuses the revision because the roster moved, run `box_inspect "$SOPHIE"` again and repeat from `REV=$(cat rev)`.
Leo's host claims stay off until step 8. Get real fresh box evidence from both enrolled computes before step 7.
-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.
+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 the published identity, `policy.json`, `identity.json` or the files in `/opt/carlos/system/console/`.
## Step 7. Readers, floor, home and leo's key
@@ -1458,6 +1629,7 @@ What must not be touched while leo is held:
- `/etc/carlos/host-actor.json`, `/etc/carlos/host-evidence.key`, `/etc/carlos/fence-writer/` and `/opt/carlos/system/guard`;
- `/opt/carlos/carlos`, `/opt/carlos/carlos.prev` and anything staged: no `carlos system update`, rollback or old-road roll, and no `stable`-channel roll while leo is held, because it is on `stable` with dennis;
- `/data`, including `registry.db`, `/data/instances` and any parked database;
+- sophie's protected console (`/opt/carlos/system/console/console` and `console.prev`), the published identity (`control/system/router/releases/v0.28.2.json`) and the `bab` pointer-digest marker;
- the roster, floor and enrollments, and the claim's S3 objects: `control/hosts/`, `control/host-actors.json`, `control/host-origins/`, admitted prefixes, and `instances/` in `carlos-dev-ie-640012953448`;
- no `systemctl reset-failed` before the failed results are recorded, no reboot unless the edge is persistently masked, no rerun of `journal-migrate`, `install-control` or the seed, and no console `reset-wake-retry`.
@@ -1502,6 +1674,6 @@ For platform-1e:
2. For a later teardown out of hold: how do writes made under the claim reach the claims-off restore path?
3. Answered 2026-10-18: leo's object-version actions are applied for the trial; the box-records bucket policy goes with the fence-writer role in platform-infrastructure#46.
4. Box identity records: `carlos system box publish-record` ships in v0.28.1, dev only (step 5.0). The v0.28.1 dev roll comes before step 5.
-6. Answered 2026-10-18: platform-1e builds the armed policy signing phase and the published system identity with the protected console install, in v0.28.2 (blocker 5). Step 6 waits for it; step 5 does not.
+6. Answered 2026-10-18: platform-1e builds the armed policy signing phase and the published system identity with the protected console install, in v0.28.2 (blocker 5). Built on branch `claim-step6-artifacts` (`c38a4ca9`, census ruled): `sign-policy`, `publish-identity` and `install-console`. Step 6 waits for it, and for 5.0a; step 5.0 did not.
7. Still open in `docs/superpowers/notes/claim-trial-inputs/README.md`: the console enrollment token, the v0.28.1 and v0.28.2 artifacts, and the signing host values. Namespace `platform`, sophie's pool `console`, console ID `dev-console` and operator `paul@keymail.dev` are confirmed.
5. The console's mutation classifier lets it delete under `control/boxes/` and `control/box-volumes/`, which the new box-records bucket policy refuses: amadan carlos/platform#302.