carlos / platform Public

docs: cohort-1 runbook folds in platform-1e's answers (jq, install-control)

- No new tools on the boxes: every jq line now runs on the trusted
  signing host. Steps 5 to 7 add SSM helpers there (ssm_run, box_get,
  box_put, box_inspect): the box writes raw JSON, the trusted host
  fetches it over SSM, parses it with jq and stages roster.json, rev and
  le back, each transfer checked by sha256. Box blocks read them with
  cat. Inputs for the boxes are staged the same way.
- install-control runs once, after host.env carries CARLOS_HOST_CLAIM=1
  and CARLOS_HOST_CLAIM_ORIGINS=1 and before journal-migrate; the unit
  depends only on readClaimUnitSettings, not the census.
- Removed the answered open items (jq, install-control, the usage and
  docs edits Paul already confirmed on main).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3bc941b316106a25404f580cb3c4416c820afd61 parent 64824ff
1 file changed, +92 −31
  • docs/superpowers/plans/2026-10-16-claim-activation-dev-cohort1.md +92 −31

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	301.781s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.755s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.228s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.681s
ok  	github.com/carlosframework/platform/internal/activator	250.491s
ok  	github.com/carlosframework/platform/internal/agentsock	0.027s
ok  	github.com/carlosframework/platform/internal/alerting	0.007s
ok  	github.com/carlosframework/platform/internal/analytics	0.024s
ok  	github.com/carlosframework/platform/internal/appconfig	2.502s
ok  	github.com/carlosframework/platform/internal/apperr	0.104s
ok  	github.com/carlosframework/platform/internal/applog	0.161s
ok  	github.com/carlosframework/platform/internal/awsinv	0.006s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.006s
ok  	github.com/carlosframework/platform/internal/boxid	0.760s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.019s
ok  	github.com/carlosframework/platform/internal/caps	0.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.004s
ok  	github.com/carlosframework/platform/internal/cgroup	0.727s
ok  	github.com/carlosframework/platform/internal/changelog	0.344s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.034s
ok  	github.com/carlosframework/platform/internal/clamd	0.057s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	402.555s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.130s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.007s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.382s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.003s
ok  	github.com/carlosframework/platform/internal/edge	44.986s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.133s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.010s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.155s
ok  	github.com/carlosframework/platform/internal/host	173.235s
ok  	github.com/carlosframework/platform/internal/hostactors	0.588s
ok  	github.com/carlosframework/platform/internal/hostbox	2.591s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.556s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.011s
ok  	github.com/carlosframework/platform/internal/hostexec	4.854s
ok  	github.com/carlosframework/platform/internal/hostfence	0.036s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.545s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.036s
ok  	github.com/carlosframework/platform/internal/hostops	1.607s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.002s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.474s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.167s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.005s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.217s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.736s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.003s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.008s
ok  	github.com/carlosframework/platform/internal/layout	1.529s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.002s
ok  	github.com/carlosframework/platform/internal/lease	9.817s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.006s
ok  	github.com/carlosframework/platform/internal/ledger	0.020s
ok  	github.com/carlosframework/platform/internal/mail	0.153s
ok  	github.com/carlosframework/platform/internal/maintenance	0.077s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.538s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.082s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.061s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.739s
ok  	github.com/carlosframework/platform/internal/objstore	0.178s
ok  	github.com/carlosframework/platform/internal/ops	0.167s
ok  	github.com/carlosframework/platform/internal/park	0.195s
ok  	github.com/carlosframework/platform/internal/privatefile	0.006s
ok  	github.com/carlosframework/platform/internal/provision	36.135s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.442s
ok  	github.com/carlosframework/platform/internal/relay	7.264s
ok  	github.com/carlosframework/platform/internal/release	22.379s
ok  	github.com/carlosframework/platform/internal/rollout	3.750s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.060s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.570s
ok  	github.com/carlosframework/platform/internal/s3compat	0.004s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.493s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.481s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.966s
ok  	github.com/carlosframework/platform/internal/status	0.039s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.342s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.572s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 48824482 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/17 20:26:37 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DF6AF9CDB00289, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.023s
minio contract: PASS
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 d20fb3b67..63d33223a 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
@@ -20,7 +20,8 @@ Sources:
- Plan 9 Task C2, milestones 2 to 4 (`docs/superpowers/plans/2026-10-08-host-lifecycle-claim-9-activation-readiness.md` on `design-replica-ownership`);
- `docs/host-claim-restore-runbook.md` on main, including "Home-region activation order";
- main's `internal/host/activation_command.go` and `activation_command_linux.go` (command flags and help), `internal/claimactivation/runner.go` (plan phases), `internal/host/rootmigration_production_linux.go` (`verifyOfflineRootMigration`, `rootMigrationOfflineUnits`) and `docs/site/cli.md` "carlos system claims";
-- platform-1e's `activation-tools-report.md`: run 11 (2026-10-17) for leo's attended window, and run 2 for the operator sequence of the retained commands, which run 11 states are byte-identical on main;
+- platform-1e's `activation-tools-report.md`: run 11 (2026-10-17) for leo's attended window, and run 2 for the `carlos` commands of steps 5 to 7, which run 11 states are byte-identical on main;
+- platform-1e's answers of 2026-10-17: no tools installed on the boxes, `jq` only on the trusted host; `install-control` runs once;
- the files seed runbook strings, which Paul approved unchanged on 2026-10-15 (copy review `files-seed-copy-review/result.json`, action `approve`);
- `docs/superpowers/notes/2026-10-16-empty-app-key-cleanup.bash` on main;
- amadan #250, #272, #280 and #286.
@@ -185,7 +186,7 @@ Pass: all twelve are root-owned regular files in `/etc/systemd/system`; the edge
2026-10-17:
- Units: PASS, re-run today. All twelve are `root 644` regular files. Edge and the five triggers `loaded active enabled`; the six services `loaded inactive` (`carlos-system-recover.service` enabled, the others static). The edge has no drop-in directory yet; `90-host-floor.conf` appears when step 6 arms the guard.
- These files live in `/etc/systemd/system`, so a runtime mask in `/run/systemd/system` loses to them (#272 round 3 saw this). Step 8 masks by moving each file aside and linking `/dev/null` in its place.
-- `jq`: absent on leo, sophie and dennis (re-run today). Steps 5 to 7 use `jq` on the boxes. See H9.
+- `jq`: absent on leo, sophie and dennis (re-run today). Nothing is installed for the trial; every `jq` line runs on the trusted signing host (H9).
- Kernel 6.12.105, `/data` on ext4, 17G free, 927 MB of memory, systemd 257 (2026-10-16 and today).
## Blockers
@@ -320,11 +321,13 @@ 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`. The command blocks below are quoted exactly from platform-1e's report (run 2, sections A to C, whose command branches run 11 confirms are byte-identical on main), and every flag was checked against main's code.
+`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.
+
+**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 root shell holding `/root/claim-trial` with `release.pub`, `release.key`, `regions.json`, `leo.json`, `sophie.json`, `console.json` and `floor.json`. The release signing key never goes on either box.
-- **Leo and sophie:** attended root shells. 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.
+- **The trusted signing host:** a root shell holding `/root/claim-trial` with `release.pub`, `release.key`, `regions.json`, `leo.json`, `sophie.json`, `console.json` and `floor.json`, plus `jq`, the AWS CLI with profile `kass`, `gzip` and `base64`. The release signing key never goes on either box.
+- **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.
The controller supplies, and Paul approves, the concrete inputs: the signed release, floor and guard artifacts; namespace, pool and label inputs (leo's pool `cohort1`, sophie's matching its real placement); the console identity; the fence role and credentials from step 3; and leo's reviewed host.env changes. Revalidate both instance IDs first.
@@ -334,7 +337,7 @@ Paul's go: once for step 5.
Each box's signed AWS identity record must already be published by the existing box identity mechanism. A missing record is a stop, not permission to make one up.
-On the trusted signing host, the helper:
+On the trusted signing host, the helpers. The first block is the report's; the second adds the SSM transfer helpers that replace `jq` on the boxes:
```bash
set -euo pipefail
@@ -364,6 +367,47 @@ phase() {
}
```
+```bash
+LEO=i-046c7fe43620944bc
+SOPHIE=i-05498eada6f0afd23
+# ssm_run <instance> <command>: run as root on the box; print stdout; fail unless the command succeeded.
+ssm_run() {
+ local id s
+ id=$(aws --region "$R" ssm send-command --instance-ids "$1" --document-name AWS-RunShellScript \
+ --parameters "$(python3 -c 'import json,sys;print(json.dumps({"commands":[sys.argv[1]]}))' "$2")" \
+ --query Command.CommandId --output text)
+ while s=$(aws --region "$R" ssm get-command-invocation --command-id "$id" --instance-id "$1" \
+ --query Status --output text 2>/dev/null); [ -z "$s" ] || [ "$s" = Pending ] || [ "$s" = InProgress ]; do sleep 2; done
+ test "$s" = Success
+ aws --region "$R" ssm get-command-invocation --command-id "$id" --instance-id "$1" \
+ --query StandardOutputContent --output text
+}
+# box_get <instance> <path on box> <local path>: copy a box file here, checked by sha256.
+box_get() {
+ ssm_run "$1" "gzip -c '$2' | base64 -w0; echo; sha256sum '$2' | cut -d' ' -f1" > get.out
+ sed -n 1p get.out | base64 -d | gunzip > "$3"
+ test "$(sha256sum "$3" | cut -d' ' -f1)" = "$(sed -n 2p get.out)"
+}
+# box_put <instance> <local path> <path on box>: stage a local file on the box (mode 0600), checked by sha256.
+box_put() {
+ local b h
+ b=$(gzip -c "$2" | base64 -w0)
+ h=$(sha256sum "$2" | cut -d' ' -f1)
+ ssm_run "$1" "umask 077; echo '$b' | base64 -d | gunzip > '$3' && test \"\$(sha256sum '$3' | cut -d' ' -f1)\" = $h && echo staged"
+}
+# box_inspect <instance>: the box reads the roster with its own credentials; parse it here; stage roster.json and rev on the box.
+box_inspect() {
+ ssm_run "$1" "cd /root/claim-trial && /opt/carlos/carlos system claims inspect --bucket carlos-dev-640012953448 --aws-region eu-west-1 --public-key \"\$(cat release.pub)\" > inspect.json" > /dev/null
+ box_get "$1" /root/claim-trial/inspect.json box-inspect.json
+ jq '.signed_roster' box-inspect.json > box-roster.json
+ jq -er '.roster.revision' box-inspect.json > box-rev
+ box_put "$1" box-roster.json /root/claim-trial/roster.json
+ box_put "$1" box-rev /root/claim-trial/rev
+}
+```
+
+SSM returns at most 24,000 characters of output per command. `box_get` compresses the file and checks its digest, so a truncated transfer fails rather than passing silently.
+
For a new roster only (dev has none, P2): initialize, configure, then invite and enroll both computes and the console. On resume, run `refresh` and inspect the actual state instead. Never turn a read error into permission to initialize or overwrite.
```bash
@@ -391,7 +435,16 @@ done
Expected: every dry run, then its apply, succeeds; `inspect.json` lists `leo-climbing` (pool `cohort1`, instance `i-046c7fe43620944bc`), `sophie-knitting` and the console.
-Then transfer `roster.json`, `release.pub`, the matching enrollment JSON, the approved armed `policy.json` and the signed `identity.json` to `/root/claim-trial` on each box. No private signing key is transferred.
+Then stage the box inputs, still on the trusted host. `policy.json` and `identity.json` are the approved armed policy and signed identity; `console-path` and `console-id` are the protected console executable path and the approved console ID (the same ID as in `console-bound.json`):
+
+```bash
+ssm_run "$LEO" "install -d -o root -g root -m 0700 /root/claim-trial && echo ok"
+ssm_run "$SOPHIE" "install -d -o root -g root -m 0700 /root/claim-trial && echo ok"
+for f in release.pub policy.json identity.json leo-climbing-enrollment.json; do box_put "$LEO" "$f" "/root/claim-trial/$f"; done
+for f in release.pub policy.json identity.json sophie-knitting-enrollment.json console-path console-id; do box_put "$SOPHIE" "$f" "/root/claim-trial/$f"; done
+```
+
+Expected: `staged` for each file. No private signing key is transferred.
On failure: hold. A partly applied phase is resumed from the latest authenticated revision, never recreated.
@@ -411,7 +464,13 @@ set +a
--identity /root/claim-trial/identity.json --enrollment /root/claim-trial/leo-climbing-enrollment.json
```
-On sophie, stage `console-path` (the protected absolute console executable path) and `console-id` (the approved ID from `console-bound.json`). `CARLOS_HOST_CLAIM` stays absent or empty in sophie's `/etc/carlos/host.env`; the installer refuses otherwise.
+On the trusted host, stage sophie's current roster and revision:
+
+```bash
+box_inspect "$SOPHIE"
+```
+
+On sophie. `CARLOS_HOST_CLAIM` stays absent or empty in sophie's `/etc/carlos/host.env`; the installer refuses otherwise.
```bash
set -euo pipefail
@@ -423,9 +482,7 @@ set +a
PUB=$(cat release.pub)
CID=$(cat console-id)
CONSOLE_BINARY=$(cat console-path)
-/opt/carlos/carlos system claims inspect --bucket carlos-dev-640012953448 --aws-region eu-west-1 --public-key "$PUB" > inspect.json
-REV=$(jq -er '.roster.revision' inspect.json)
-jq '.signed_roster' inspect.json > roster.json
+REV=$(cat rev)
/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"
@@ -444,7 +501,7 @@ systemctl is-active console.service
Expected:
- `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.
+- `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.
@@ -465,33 +522,40 @@ Paul's go: once for step 7. `home_region` becomes write-once as soon as claims a
refresh
```
-3. **Leo's evidence key.** On leo, keeping the applied response, not the dry-run one:
+3. **Leo's evidence key.** On the trusted host, stage leo's roster, revision and enrollment ID:
+
+ ```bash
+ box_inspect "$LEO"
+ jq -er '.roster.boxes["leo-climbing"].enrollment' box-inspect.json > le
+ box_put "$LEO" le /root/claim-trial/le
+ ```
+
+ On leo, keeping the applied response, not the dry-run one:
```bash
set -euo pipefail
umask 077
cd /root/claim-trial
PUB=$(cat release.pub)
- /opt/carlos/carlos system claims inspect --bucket carlos-dev-640012953448 --aws-region eu-west-1 --public-key "$PUB" > inspect.json
- REV=$(jq -er '.roster.revision' inspect.json)
- LE=$(jq -er '.roster.boxes["leo-climbing"].enrollment' inspect.json)
- jq '.signed_roster' inspect.json > roster.json
+ REV=$(cat rev)
+ LE=$(cat le)
/opt/carlos/carlos system claims evidence-key --roster /root/claim-trial/roster.json --expect-revision "$REV" \
--bucket carlos-dev-640012953448 --box leo-climbing --enrollment "$LE" --public-key "$PUB" --dry-run
/opt/carlos/carlos system claims evidence-key --roster /root/claim-trial/roster.json --expect-revision "$REV" \
--bucket carlos-dev-640012953448 --box leo-climbing --enrollment "$LE" --public-key "$PUB" > key-proof.json
```
- Transfer only `key-proof.json` to the trusted signing host. It holds the public binding and proof, never the private key. There:
+ On the trusted host, fetch `key-proof.json` (it holds the public binding and proof, never the private key), bind it and check the roster:
```bash
+ box_get "$LEO" /root/claim-trial/key-proof.json key-proof.json
jq '{binding:.key_proof.binding,proof:.key_proof.proof}' key-proof.json > bind-phase.json
phase bind-key bind-phase.json
refresh
jq -e '.roster.home_region == "ie" and .roster.publication == "published" and .roster.request_limit_ns == 120000000000' inspect.json
```
-4. **Console runtime.** On sophie:
+4. **Console runtime.** On the trusted host, `box_inspect "$SOPHIE"`. Then on sophie:
```bash
set -euo pipefail
@@ -499,9 +563,7 @@ Paul's go: once for step 7. `home_region` becomes write-once as soon as claims a
cd /root/claim-trial
PUB=$(cat release.pub)
CID=$(cat console-id)
- /opt/carlos/carlos system claims inspect --bucket carlos-dev-640012953448 --aws-region eu-west-1 --public-key "$PUB" > inspect.json
- REV=$(jq -er '.roster.revision' inspect.json)
- jq '.signed_roster' inspect.json > roster.json
+ REV=$(cat rev)
/opt/carlos/carlos system claims console-config --roster /root/claim-trial/roster.json --expect-revision "$REV" \
--bucket carlos-dev-640012953448 --console-id "$CID" --public-key "$PUB" --enable-console-runtime --dry-run
/opt/carlos/carlos system claims console-config --roster /root/claim-trial/roster.json --expect-revision "$REV" \
@@ -514,7 +576,7 @@ Paul's go: once for step 7. `home_region` becomes write-once as soon as claims a
--expect-revision "$REV" --console-id "$CID" --public-key "$PUB"
```
-5. **Fence credentials.** On leo, with `/root/claim-trial/fence-credentials.json` and `/root/claim-trial/fence-role-arn` staged from step 3:
+5. **Fence credentials.** On the trusted host, stage `fence-credentials.json` and `fence-role-arn` from step 3 on leo with `box_put` (to `/root/claim-trial/`). Then on leo:
```bash
set -euo pipefail
@@ -527,10 +589,12 @@ Paul's go: once for step 7. `home_region` becomes write-once as soon as claims a
--bucket carlos-dev-640012953448 --aws-region eu-west-1 --role "$FENCE_ROLE"
```
+ The credentials file holds a secret. `box_put` puts its compressed bytes in an SSM command, so they appear in SSM command history. If that is not acceptable, use the approved secret transport instead and check the file's sha256 on both sides.
+
Expected:
- the `jq -e` in 3 prints `true`;
- `/etc/carlos/host-evidence.key` exists on leo (`root`, `600`, one link);
-- `console.service` is `active` and its report publishes; if publication loses a race with the agent's own report, inspect again, report again and retry with the current revision;
+- `console.service` is `active` and its report publishes; if publication loses a race with the agent's own report, run `box_inspect` again, report again and retry with the current revision;
- `/etc/carlos/fence-writer/credentials.json` exists on leo (`root`, `600`, as the command prints).
On failure: hold. A torn publish is resumed with identical bytes. Never erase the floor or substitute another digest. Leo's claim flag stays off.
@@ -649,6 +713,8 @@ systemctl is-active carlos-instance-control.service
journalctl -u carlos-instance-control.service --since '-5min' --no-pager
```
+Run `install-control` once: after host.env carries `CARLOS_HOST_CLAIM=1` and `CARLOS_HOST_CLAIM_ORIGINS=1`, and before `journal-migrate`. The rendered unit depends only on the host.env settings (`readClaimUnitSettings`: enabled, the data and opt paths, the root retry directory), not on the census, so it is not run again afterwards (platform-1e, 2026-10-17).
+
Expected:
- `install-control` prints `"target": "/etc/systemd/system/carlos-instance-control.service"`, `"reload": true` and `"start": false`;
- the unit keeps its protected environment, `StateDirectory=carlos-claims` and `MemoryMax=256M`;
@@ -1057,7 +1123,7 @@ Leaving hold is a separate decision: a fix and resume, or a claims-off teardown
**H8. Memory.** Leo has 927 MB. Claim mode raises instance-control's cap to 256M. Watch `systemctl show carlos-instance-control.service -p MemoryCurrent` during step 10.
-**H9. `jq` is not installed on the boxes.** Steps 5 to 7 use `jq` on leo and sophie (`inspect.json`, `roster.json`, the enrollment and key files), and it is absent on all three dev boxes (2026-10-17). Either install it on leo and sophie under that step's go (a package change on the box), or run the `jq` lines on the trusted host and stage their outputs at the same paths. Open question 4.
+**H9. No new tools on the boxes.** `jq` is not installed on leo, sophie or dennis (2026-10-17), and nothing is installed for the trial (platform-1e). Every `jq` line runs on the trusted signing host, which fetches box JSON over SSM and stages parsed results back (steps 5 to 7). Staged values are checked by sha256, and the box commands verify the roster's signature and revision.
## Later option: carlos-dev-uk
@@ -1071,13 +1137,8 @@ Not part of this trial (Paul, 2026-10-16). dennis-stenciling is placement `uk`,
## Open questions
-Answered: dev-uk is out of this trial; the console is enrolled by the activation tools; the independent writer and key rotation are accepted trial gaps; box roles no longer read `/carlos/release-key`; the home name is `ie`; the `proof*` routes are removed; step 4 is approved; flagship does not need the release before dev's window; the automated offline window is replaced by the manual one.
+Answered: dev-uk is out of this trial; the console is enrolled by the activation tools; the independent writer and key rotation are accepted trial gaps; box roles no longer read `/carlos/release-key`; the home name is `ie`; the `proof*` routes are removed; step 4 is approved; flagship does not need the release before dev's window; the automated offline window is replaced by the manual one; `jq` runs only on the trusted host and nothing is installed on the boxes; `install-control` runs once, before `journal-migrate`; the usage and docs edits for the removed commands are confirmed by Paul and on main.
For platform-1e:
1. Should `hostops.Apps` skip an empty app segment instead of refusing, so one stray object cannot stop activation on a whole deployment?
2. For a later teardown out of hold: how do writes made under the claim reach the claims-off restore path?
-3. The run 2 report said to run `install-control` again after `journal-migrate`, so the census-dependent root-retry configuration is included. Run 11's window installs it once, before `journal-migrate`. Is a second install needed in 8.2?
-4. `jq` on leo and sophie (H9): install it, or run those lines on the trusted host?
-
-For Paul:
-5. Run 11 asks Paul to confirm three minimal usage and docs edits for the removed commands (`activation-tools-run-11-strings.json`). Main already carries the shortened usage.