| 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. |