carlos / platform Public

harness: window run 1 on cc90d542: H13-H15, pre-window seed, G2 and G3 PASS, G4 FAIL (armed-box candidate version refusal)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3a4595e6be533dc556ef11978bb4569ac172c707 parent f214813
16 files changed, +620 −1
  • harness/RUNLOG.md +116 −0
  • harness/apps/wakecheck/main.go +72 −0
  • harness/boxes/vendor-shims.sh +54 −0
  • harness/hrun.sh +15 −0
  • harness/release/release-window.sh +42 −0
  • harness/scenarios/cohort1/blocks/b1.sh +40 −0
  • harness/scenarios/cohort1/blocks/b2.box.sh +48 −0
  • harness/scenarios/cohort1/blocks/b5.sh +34 −0
  • harness/scenarios/cohort1/blocks/g4-roll.sh +19 −0
  • harness/scenarios/cohort1/blocks/pw-seed.sh +66 −0
  • harness/scenarios/cohort1/blocks/w-identity.sh +26 −0
  • harness/scenarios/cohort1/blocks/w-open-signer.sh +39 −0
  • harness/scenarios/cohort1/values.env +5 −0
  • harness/scenarios/sync.sh +8 −0
  • harness/snapshot.sh +6 −1
  • harness/stand-ins/certs.sh +30 −0
diff --git a/harness/RUNLOG.md b/harness/RUNLOG.md
index 9c2036bc3..bc878b0aa 100644
--- a/harness/RUNLOG.md
+++ b/harness/RUNLOG.md
@@ -753,3 +753,119 @@ If-Match on DELETE (step 3), so in the harness each of these deletes uncondition
Snapshots on every container: `base`, `boot`, `pre62`, `s7` (`s7` also on `hdir/sophie-console`).
The host is left at `s7`. No `pre-window` snapshot was taken: the window (8.1 onward) is not in this
phase's scope; take it with `harness/snapshot.sh create pre-window` right before 8.1.
+
+# Window (Option B), 2026-10-20 morning: gates G0 to G11
+
+Runbook: `claim-dev-cohort1-runbook` @ `a06f17ea`, "Window package" (top) and steps 8 to 10.
+Two runs: **run 1** on `claim-empty-cohort-done` @ `cc90d542` (stopped at G4, below) and **run 2** on
+the fixed tip `d940c09f` (next section). Paul's gos are pre-authorised inside the harness only.
+Times are the host's UTC.
+
+## Harness repairs before the window (from snapshot `pre-window`)
+
+- **`pre-window`** taken at 07:05Z from a full `s7` restore. Retaken at 07:24Z (below).
+- **snapshot.sh never restored the console volume.** Its test was `incus storage volume snapshot show
+ hdir sophie-console <name>`, which incus 6.0.4 refuses ("Invalid number of arguments"), so every
+ `restore` since step 10 restored the containers but left sophie's console DB (`hdir/sophie-console`)
+ live. Found when a re-seed after a restore met `apps create: that name is taken` (the console DB still
+ held the apps; the bucket did not). Fixed (`show hdir sophie-console/<name>`, and a WARNING when absent),
+ then `s7` restored in full and `pre-window` deleted and retaken (07:24Z), volume included.
+- **H14, census-neutral shim placement** (`harness/boxes/vendor-shims.sh`). The window's migration
+ census walks `/etc/systemd/system` and refuses any regular file or non-vendor link it cannot class.
+ Dev's leo has none of the harness's entries there (`internal/host/testdata/leo-step8/leo-tree-snapshot.tsv`
+ compared with the harness leo's tree): the zram no-op unit, the asset-tag unit (H12) and its
+ `sysinit.target.wants` link, and the `amazon-ssm-agent` `/dev/null` mask. Moved to
+ `/usr/local/lib/systemd/system` (a unit path outside the census roots): same effect, checked
+ (asset tag correct after restart, agent `inactive`, zram unit from `/usr/local/lib`).
+- **H15, read-only cgroupfs for tenant units.** `ProtectControlGroups=yes` is a no-op in the harness's
+ privileged LXC containers (measured: a `systemd-run -p ProtectControlGroups=yes` unit sees
+ `/sys/fs/cgroup rw` and `W_OK`). So instance-control's isolation probe failed `cgroup-ro` and
+ `cgroup-ro-mount`, `layout: no intent recorded; nested by default is not in force`, and wakecheck was
+ first minted **flat** on leo (`/data/wakecheck....db`), unlike dev. A `BindReadOnlyPaths=/sys/fs/cgroup`
+ drop-in on the probe and the five tenant units (`/usr/local/lib/systemd/system/<unit>.d/`) gives them
+ the read-only view (`ro`, `not writable`). After it: `layout: no intent recorded -> nested by default`,
+ and every host below is nested. The probe's three `unverified` lines (imds-v6, private-v6, io-weight)
+ remain, as they do not block.
+- **H13, certificate stand-in** (`harness/stand-ins/certs.sh`): no route to Let's Encrypt, so each host
+ gets a certificate under a harness test CA (CA key on host tmpfs), published at `control/certs/<host>`
+ as autocert's cache entry; the edges' mirror serves it; curl uses `--cacert /root/harness-ca.pem`.
+- **The tenant build** (`harness/apps/wakecheck`): dev's `wc4` artefact lives only in the dev bucket, so a
+ stand-in with the same contract (`--socket`/`--db`; one SQLite row per request; `build= host= hits=`)
+ is shipped as version `wc4`. sha256 `4a79c656...`.
+
+## Pre-window state: SEEDED BY PLATFORM COMMANDS
+
+`harness/scenarios/cohort1/blocks/pw-seed.sh` (07:26-07:30Z), from `pre-window` + H14/H15, every write
+through the ordinary doors with the console claims-off:
+- **sophie's pause** (dev's 2026-10-19 state): the saved copies in `/root/claim-trial/paused-20261019/`
+ (`cp -p`, `cmp`-equal: `5df17bac...` 73 bytes, `815226c3...` 56 bytes), then the runbook's pause recipe
+ verbatim: `active`, `CARLOS_HOST_CLAIM=` (empty), healthz 200. Its last line races (Runbook corrections, C1).
+- **wakecheck** (pool-less): `apps create`, `ship wc4`, `promote wc4 edge`, store-mode `instances enable`
+ (domain `dev.harness.test`, channel `edge`), signed-in `instances create`. Minted on leo and sophie;
+ woken on leo (`hits=2,3,4`), so leo holds its nested leftovers as dev's leo does.
+- **step 4.2 and 4.3** on leo, box commands verbatim: `CARLOS_POOLS=cohort1`, edge restart
+ (`instances loop armed ... pools [cohort1]`), `carlos remove ... wakecheck.dev.harness.test` -> `removed`.
+- **claimtrial** (step 4.4: pool `cohort1`, store-mode enable, signed-in create): minted on leo only,
+ woken (`hits=2,3,4`). **rollcheck-0261** (`--kind static`, pool-less): sophie and dennis.
+ **moverehearsal** (the offline-move rehearsal app; created claims-off with `--region uk`, so it sits on
+ dennis as the rehearsal's app does after its move; no move was run).
+- Result: four records, `apps/bab/{claimtrial,moverehearsal,rollcheck-0261,wakecheck}/instances/*.json`,
+ each without `acq`; leo `claimtrial` (pool cohort1) plus wakecheck's leftovers; sophie console,
+ rollcheck-0261, wakecheck; dennis moverehearsal, rollcheck-0261.
+
+## Run 1: cc90d542
+
+**Build (G0 here, G1's "tag and publish").** `git archive cc90d542` + the harness patch (6b905c11's
+code diff, seven files, sha256 `b2f1ad8f...`), `-tags harness`, `GOFLAGS=-buildvcs=false`,
+`version.Build=v0.28.6`. Harness tag **v0.28.6** plays dev's v0.28.5 (harness v0.28.5 is step 7's
+96db0efe and plays dev's v0.28.4). carlos `9afa2deb...`, console `4e686875...`. Tagged tests green
+(`./internal/s3compat ./internal/publicrelease`), and `-run 'Backfill|Census|Step8|EmptyLeo|Ruling6'`
+over claimactivation, hostops, hostbox: ok. No CI, merge or copy review in the harness (G1 n/a).
+`harness/release/release-window.sh` (07:30Z): signed sums (`4d9810d1...`, verified), operator CLI
+v0.28.6, then **the console ship and promote for v0.28.6** (`shipped console cc90d542`,
+`promoted console cc90d542 -> canary/console`): publish-identity needs it (Runbook corrections, C2).
+
+**G2, B5 (07:31Z): PASS** (with the known blocker). 1: `0` and no `PRE /` (vacuous: MinIO refuses the
+`apps/bab//` prefix with XMinioInvalidObjectName, so the `//` segment cannot exist or be listed here).
+2: the four records, each `MISSING` (the known blocker B1 resolves). 3: `1`, `null` (MinIO omits
+KeyCount for an empty listing; S3 says `0`), `0`. 4: `Success 9 ['cohort1', 'console']`. 5: `0 0` on
+sophie and dennis. 6: no counterpart. 7: `200`, `c1ae2900...c412`, `CARLOS_HOST_CLAIM=`, `0`.
+
+**G3, signing session 6 (07:31-07:32Z): PASS.** `w-open-signer.sh`: run-id
+`cohort1-20261020T073158Z`, signer CLI `carlos v0.28.6` (`OK` against the signed sums), helpers and
+release.pub `OK`, no swap, `/run` tmpfs, key `root 600 1 45`. `w-identity.sh`: one create-only write
+(`before_etag ""`) of `control/system/router/releases/v0.28.6.json`; router `9afa2deb...`, console
+`4e686875...`; identity file = published object. The P-B2 grants: **not exercised** (the signer's MinIO
+user is `readwrite`; only a hand IAM review, as step 11 did, would show a missing grant).
+
+**G4, leo on v0.28.6: FAIL** (07:32:50Z promote, read 07:42:52Z). D10's harness equivalent is runbook
+step 1 item 2's new road for `stable` only (`carlos system update --channel stable --version v0.28.6
+--file ... --yes`), which moves leo and dennis; sophie stays on canary at v0.28.5.
+```
+kathleen-juggling stable v0.28.5 behind -> v0.28.6 refused v0.28.6: refused: candidate reports
+ "2026/10/20 07:33:00 ALERT: root AWS file, profile, endpoint and trust-store environment overrides ignored\ncarlos v0.28.6", not v0.28.6
+noor-marquetry stable v0.28.6 current (dennis: unarmed, rolled)
+leo journal 07:33:00: system apply: stable v0.28.6: refused: refused: candidate reports "...ALERT...\ncarlos v0.28.6", not v0.28.6
+```
+Failure action applied: hold the window (leo claims-off, v0.28.5, serving). Snapshot
+**`g4-fail-cc90d542`** (07:44Z).
+
+**Root cause (code, present on main and on both window tips).** On an armed box the updater runs the
+candidate's `version` through the guard: `systemApplier.guardRun` -> `hostfloor.Disk.Run`
+(`internal/hostfloor/image_linux.go:72`, `cmd.CombinedOutput()`, environment inherited). The updater
+is root, so it has already run `rootaws.ScrubEnvironment`, which **sets** `AWS_CONFIG_FILE` and
+`AWS_SHARED_CREDENTIALS_FILE` to `/dev/null/carlos-root-aws` in its own environment
+(`internal/rootaws/environment.go:72-73`). The child inherits them, its own scrub counts any set
+variable in the list as an override (`environment.go:63-66`) and logs the ALERT to stderr
+(`environment.go:75-76`); CombinedOutput folds that line into the output, and
+`carlosVersionOf` (`internal/host/systemapply.go:2049`, a `carlos ` prefix on the whole trimmed
+output) fails, so `stageCandidate` refuses at `systemapply.go:2107-2108`. Reproduced by hand on leo:
+`AWS_CONFIG_FILE=/dev/null/carlos-root-aws /opt/carlos/carlos version` prints the ALERT then
+`carlos v0.28.5`; without the variable, only the version. The unarmed path (`ops.run`,
+`systemapply.go:184-197`) uses `Output()`, stdout only, which is why dennis rolled. **Every armed box
+refuses every router update this way**: on dev, leo and sophie (armed since 2026-10-19) cannot take
+v0.28.5 through `carlos system update`. Dev never met it because no armed box has been rolled yet. The
+refusing code runs in the **old** updater, so a fix in the new release only helps if it is in the
+candidate's side (the child must not print the ALERT when the two variables already hold the disabled
+path); a fix in `Disk.Run` (stdout only) reaches boxes only after a roll. `carlos.prev.allowed` was
+written at 07:34Z by the refused attempt.
diff --git a/harness/apps/wakecheck/main.go b/harness/apps/wakecheck/main.go
new file mode 100644
index 000000000..114af510b
--- /dev/null
+++ b/harness/apps/wakecheck/main.go
@@ -0,0 +1,72 @@
+// wakecheck is the harness's stand-in for dev's wakecheck build wc4, whose
+// bytes live only in the dev deployment bucket (the harness never reads it).
+// Same contract as testdata/echoapp (-socket, -addr or LISTEN_FDS; -db), plus
+// what the runbook's acceptance reads: every request to anything but /healthz
+// writes one SQLite row in the instance database and replies
+// "build=<BUILD> host=<hostname> hits=<rows>".
+package main
+
+import (
+ "database/sql"
+ "flag"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "strconv"
+
+ _ "modernc.org/sqlite"
+)
+
+func main() {
+ socket := flag.String("socket", "", "unix socket to listen on")
+ addr := flag.String("addr", "", "TCP host:port to listen on")
+ dbPath := flag.String("db", "", "instance database path")
+ flag.Parse()
+ if *dbPath == "" {
+ *dbPath = "wakecheck.db"
+ }
+ db, err := sql.Open("sqlite", *dbPath)
+ if err == nil {
+ _, err = db.Exec(`PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS hits (id INTEGER PRIMARY KEY, at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')))`)
+ }
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ host, _ := os.Hostname()
+ mux := http.NewServeMux()
+ mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "ok") })
+ mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
+ var n int
+ if _, err := db.Exec(`INSERT INTO hits DEFAULT VALUES`); err == nil {
+ err = db.QueryRow(`SELECT count(*) FROM hits`).Scan(&n)
+ }
+ fmt.Fprintf(w, "build=%s host=%s hits=%d\n", os.Getenv("BUILD"), host, n)
+ })
+ ln, err := listen(*socket, *addr)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ if err := http.Serve(ln, mux); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func listen(socket, addr string) (net.Listener, error) {
+ if n, _ := strconv.Atoi(os.Getenv("LISTEN_FDS")); n >= 1 {
+ if pid := os.Getenv("LISTEN_PID"); pid == "" || pid == strconv.Itoa(os.Getpid()) {
+ return net.FileListener(os.NewFile(3, "listener"))
+ }
+ }
+ if socket != "" {
+ _ = os.Remove(socket)
+ return net.Listen("unix", socket)
+ }
+ if addr == "" {
+ addr = "127.0.0.1:8080"
+ }
+ return net.Listen("tcp", addr)
+}
diff --git a/harness/boxes/vendor-shims.sh b/harness/boxes/vendor-shims.sh
new file mode 100644
index 000000000..b4c3c5a39
--- /dev/null
+++ b/harness/boxes/vendor-shims.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# H14 and H15 (on host, root), applied from snapshot pre-window before the seed.
+#
+# H14, census-neutral shim placement. The window's migration census walks
+# /etc/systemd/system and refuses any regular file or non-vendor link it
+# cannot class (internal/hostbox/migration_census_linux.go CheckMigrationRoots).
+# Dev's leo has none of the harness's three /etc/systemd/system entries
+# (internal/host/testdata/leo-step8/leo-tree-snapshot.tsv): the zram no-op
+# unit (step 5), the asset-tag unit and its sysinit.target.wants link (H12),
+# and the amazon-ssm-agent /dev/null mask (step 5). They move to
+# /usr/local/lib/systemd/system, a systemd unit path outside the census roots,
+# with the same effect: the zram unit there overrides /usr/lib's; the
+# asset-tag unit is wanted through a .wants link there; the SSM agent is kept
+# from starting by a drop-in condition instead of a mask.
+#
+# H15, read-only cgroupfs for tenant units. ProtectControlGroups=yes is a
+# no-op inside the harness's privileged LXC containers (measured: a
+# `systemd-run -p ProtectControlGroups=yes` unit still sees /sys/fs/cgroup rw),
+# so instance-control's isolation probe fails `cgroup-ro` and `cgroup-ro-mount`
+# and the box never puts "nested by default" in force; dev's leo runs nested.
+# A BindReadOnlyPaths=/sys/fs/cgroup drop-in on the probe and every tenant unit
+# gives those units the read-only cgroupfs ProtectControlGroups gives them on
+# EC2. Drop-ins live in /usr/local/lib/systemd/system/<unit>.d/.
+set -euo pipefail
+L=/usr/local/lib/systemd/system
+for b in leo sophie dennis; do
+ incus exec $b -- bash -s <<EOF
+set -euo pipefail
+install -d -m 0755 $L $L/sysinit.target.wants $L/amazon-ssm-agent.service.d
+# zram no-op: move, do not copy
+if [ -f /etc/systemd/system/systemd-zram-setup@.service ] && [ ! -L /etc/systemd/system/systemd-zram-setup@.service ]; then
+ mv -T /etc/systemd/system/systemd-zram-setup@.service $L/systemd-zram-setup@.service
+fi
+# asset tag unit: disable from /etc, move, want it from /usr/local
+if [ -f /etc/systemd/system/harness-dmi-instance.service ]; then
+ systemctl disable harness-dmi-instance.service 2>/dev/null || true
+ rm -f /etc/systemd/system/sysinit.target.wants/harness-dmi-instance.service
+ mv -T /etc/systemd/system/harness-dmi-instance.service $L/harness-dmi-instance.service
+ ln -sfn $L/harness-dmi-instance.service $L/sysinit.target.wants/harness-dmi-instance.service
+fi
+rmdir /etc/systemd/system/sysinit.target.wants 2>/dev/null || true
+# SSM agent: condition drop-in instead of the /etc mask
+printf '[Unit]\n# harness H14: the box agent never runs (it would register with real SSM)\nConditionPathExists=/nonexistent/harness-no-ssm-agent\n' > $L/amazon-ssm-agent.service.d/zz-harness-off.conf
+if [ "\$(readlink /etc/systemd/system/amazon-ssm-agent.service 2>/dev/null)" = /dev/null ]; then rm /etc/systemd/system/amazon-ssm-agent.service; fi
+# H15
+for u in carlos-instance-probe.service carlos-instance-gen@.service carlos-instance@.service carlos-instance-seed@.service carlos-instance-cmd@.service carlos-instance-sidecar@.service; do
+ install -d -m 0755 $L/\$u.d
+ printf '[Service]\n# harness H15: ProtectControlGroups is a no-op in LXC; a read-only bind gives the same view\nBindReadOnlyPaths=/sys/fs/cgroup\n' > $L/\$u.d/zz-harness-cgroup-ro.conf
+done
+systemctl daemon-reload
+echo "$b: \$(systemctl is-enabled harness-dmi-instance.service 2>&1) dmi=\$(systemctl is-active harness-dmi-instance.service) ssm=\$(systemctl show -p LoadState -p ActiveState --value amazon-ssm-agent.service | paste -sd/) zram=\$(systemctl show -p FragmentPath --value systemd-zram-setup@zram0.service)"
+systemd-run -q --wait -P -p BindReadOnlyPaths=/sys/fs/cgroup sh -c 'grep " /sys/fs/cgroup " /proc/self/mountinfo | cut -d" " -f5,6; test -w /sys/fs/cgroup/cgroup.procs && echo W_OK || echo "not writable"'
+EOF
+done
diff --git a/harness/hrun.sh b/harness/hrun.sh
new file mode 100755
index 000000000..2ee820250
--- /dev/null
+++ b/harness/hrun.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# hrun.sh FILE [ARGS...]: copy a harness script to the host as /root/harness/<basename>
+# (root 0700) and run it there as root with ARGS, through hssm.py (sandbox off).
+# HRUN_TIMEOUT sets the SSM timeout (default 1800 s).
+set -euo pipefail
+f=$1; shift
+n=$(basename "$f")
+d=$(dirname "$0")
+{
+ printf 'install -d -m 0700 /root/harness\ncat > /root/harness/%s <<'"'"'__HRUN_EOF__'"'"'\n' "$n"
+ cat "$f"
+ printf '__HRUN_EOF__\nchmod 0700 /root/harness/%s\nbash /root/harness/%s' "$n" "$n"
+ for a in "$@"; do printf ' %q' "$a"; done
+ printf '\n'
+} | python3 "$d/hssm.py" --timeout "${HRUN_TIMEOUT:-1800}" -
diff --git a/harness/release/release-window.sh b/harness/release/release-window.sh
new file mode 100644
index 000000000..c30e6b24a
--- /dev/null
+++ b/harness/release/release-window.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+# The window release (on host, root): harness tag v0.28.6 = claim-empty-cohort-done @ cc90d542
+# plus the harness patch (6b905c11's code diff), built on DOCS-T1 with -tags harness and
+# version.Build=v0.28.6. It plays the runbook's "tag and publish v0.28.5" (G1): the harness's
+# v0.28.5 is already step 7's release of 96db0efe, which plays dev's v0.28.4.
+# Same recipe as release-setup.sh, minus what step 7 already did once: the test sums key
+# (reused from /run/harness/keys/sums.key), its public half on every box, and the release
+# key in the fake Parameter Store.
+# SUMS="<carlos sha> <console sha>" CV=<console ship version> release-window.sh
+set -euo pipefail
+TAG=v0.28.6; V=${TAG#v}
+R=/root/harness/release/$TAG; K=/run/harness/keys
+test -s $K/sums.key
+mkdir -p $R
+aws s3 cp --quiet s3://carlos-harness-640012953448/artefacts/$TAG/carlos-linux-arm64 $R/carlos-linux-arm64
+aws s3 cp --quiet s3://carlos-harness-640012953448/artefacts/$TAG/console-linux-arm64 $R/console-linux-arm64
+set -- $SUMS
+[ "$(sha256sum < $R/carlos-linux-arm64 | cut -d' ' -f1)" = "$1" ] && [ "$(sha256sum < $R/console-linux-arm64 | cut -d' ' -f1)" = "$2" ] || { echo "artefact sha256 mismatch"; exit 1; }
+chmod 755 $R/carlos-linux-arm64 $R/console-linux-arm64
+openssl pkey -in $K/sums.key -pubout -outform DER | tail -c 32 | base64 -w0 > $R/sums.pub
+cmp $R/sums.pub /etc/carlos/harness-sums-pubkey
+for a in amd64 arm64; do printf 'harness placeholder: no deb is built for %s %s\n' "$TAG" "$a" > $R/carlos_${V}_$a.deb; done
+(cd $R && sha256sum carlos-linux-arm64 carlos_${V}_amd64.deb carlos_${V}_arm64.deb > SHA256SUMS.txt)
+openssl pkeyutl -sign -inkey $K/sums.key -rawin -in $R/SHA256SUMS.txt | base64 -w0 > $R/SHA256SUMS.txt.sig
+echo >> $R/SHA256SUMS.txt.sig
+install -m 0755 $R/carlos-linux-arm64 /usr/local/bin/carlos
+O=/home/operator/release-$TAG; install -d -o operator -g operator -m 0755 $O
+for f in carlos-linux-arm64 console-linux-arm64 SHA256SUMS.txt SHA256SUMS.txt.sig carlos_${V}_amd64.deb carlos_${V}_arm64.deb sums.pub; do install -o operator -g operator -m 0644 $R/$f $O/$f; done
+chmod 755 $O/carlos-linux-arm64 $O/console-linux-arm64
+echo "$CV" > $O/CV; chown operator:operator $O/CV
+echo "== release $TAG"; (cd $R && cat SHA256SUMS.txt && sha256sum SHA256SUMS.txt SHA256SUMS.txt.sig console-linux-arm64)
+P=$(mktemp -d); openssl pkey -in $K/sums.key -pubout > $P/p.pem; base64 -d $R/SHA256SUMS.txt.sig > $P/s.bin
+echo "verify sums: $(openssl pkeyutl -verify -pubin -inkey $P/p.pem -rawin -in $R/SHA256SUMS.txt -sigfile $P/s.bin)"; rm -rf $P
+echo "operator CLI: $(/usr/local/bin/carlos version)"; echo "harness info: $(CARLOS_HARNESS_INFO=1 /usr/local/bin/carlos | tr '\n' ' ')"
+# 5.0a item 4 for the window release (see RUNLOG "Runbook corrections"): ship and promote the
+# v0.28.6-stamped console to canary/console, as step7-operator.sh did for v0.28.5.
+runuser -l operator -c 'set -euo pipefail; cd ~/release-v0.28.6; CARLOS=/usr/local/bin/carlos; CONSOLE=~/release-v0.28.6/console-linux-arm64; CV=$(cat CV)
+export AWS_PROFILE=harness AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=harness-deployment CARLOS_RELEASE_PUBKEY="$(cat ~/release.pub)"
+echo "== 1a/1e"; "$CARLOS" channels --app console --account-id bab; "$CARLOS" channels digests --account-id bab
+echo "== 2"; sha256sum "$CONSOLE"; grep -c "$(sha256sum "$CONSOLE" | cut -d" " -f1)" SHA256SUMS.txt || true
+echo "== 4"; CARLOS_BIN=$CARLOS ~/infra/scripts/ship-app.sh harness harness-deployment console "$CV" canary/console binary "$CONSOLE"
+"$CARLOS" channels --app console --account-id bab'
diff --git a/harness/scenarios/cohort1/blocks/b1.sh b/harness/scenarios/cohort1/blocks/b1.sh
new file mode 100755
index 000000000..6a4fb4f07
--- /dev/null
+++ b/harness/scenarios/cohort1/blocks/b1.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+# Block B1 (gate G5), operator shell, signed in to the harness console. Runbook text with the harness
+# values: DEV -> http://127.0.0.1:8090, domain dev.harness.test, REHEARSAL_APP/HOST = moverehearsal
+# (seeded pre-window: created claims-off with --region uk, so it sits on dennis as the rehearsal's app
+# does after its move); R/B/S as B5; `leo` is ssm.sh to leo.
+set -euo pipefail
+. ~/cohort1/values.env
+R="--profile harness --region eu-west-1"
+B=harness-deployment
+S=~/cohort1/ssm.sh
+leo() { "$S" eu-west-1 "$LEO" "$1"; }
+DEV=http://127.0.0.1:8090
+REHEARSAL_APP='moverehearsal' # set at the rehearsal
+REHEARSAL_HOST='moverehearsal.dev.harness.test' # set at the rehearsal
+: "${REHEARSAL_APP:?set it from the rehearsal record}"
+: "${REHEARSAL_HOST:?set it from the rehearsal record}"
+echo "== B1 deletes $(date -u +%FT%TZ)"
+carlos instances delete --app claimtrial --account bab --host claimtrial.dev.harness.test --console $DEV --no-prompt
+carlos instances delete --app wakecheck --account bab --host wakecheck.dev.harness.test --console $DEV --no-prompt
+carlos instances delete --app rollcheck-0261 --account bab --host rollcheck-0261.dev.harness.test --console $DEV --no-prompt
+carlos instances delete --app "$REHEARSAL_APP" --account bab --host "$REHEARSAL_HOST" --console $DEV --no-prompt
+date -u +%FT%TZ
+echo "-- bucket"; aws $R s3 ls --recursive "s3://$B/apps/" | awk '{print $4}' | grep -cE '/(instances|sidecars)/[^/]+\.json$' || true
+sleep 45
+echo "-- registries $(date -u +%FT%TZ)"
+leo 'python3 -c "
+import sqlite3
+c=sqlite3.connect(\"file:/data/registry.db?mode=ro\",uri=True)
+ts=[r[0] for r in c.execute(\"select name from sqlite_master where type=\x27table\x27 and name not like \x27sqlite_%\x27 order by name\")]
+n=0
+for t in ts:
+ k=c.execute(\"select count(*) from %s\" % t).fetchone()[0]
+ n+=k
+ print(t,k)
+print(\"total\",n)
+for r in c.execute(\"select host,app,pool from routes order by host\"): print(r)"
+systemctl list-units --all --plain --no-legend "carlos-instance-gen@*" "carlos-instance@*"
+journalctl -u carlos-edge.service --since -30min --no-pager | grep "removed retired"'
+"$S" eu-west-1 "$SOPHIE" 'python3 -c "import sqlite3; c=sqlite3.connect(\"file:/data/registry.db?mode=ro\",uri=True); [print(r) for r in c.execute(\"select host,app,pool from routes order by host\")]"'
+"$S" eu-west-2 "$DENNIS" 'python3 -c "import sqlite3; c=sqlite3.connect(\"file:/data/registry.db?mode=ro\",uri=True); print(c.execute(\"select count(*) from routes\").fetchone()[0])"'
diff --git a/harness/scenarios/cohort1/blocks/b2.box.sh b/harness/scenarios/cohort1/blocks/b2.box.sh
new file mode 100644
index 000000000..3bdfbb6cc
--- /dev/null
+++ b/harness/scenarios/cohort1/blocks/b2.box.sh
@@ -0,0 +1,48 @@
+# Block B2, the move (runbook verbatim but for HOSTS' domain and REHEARSAL_HOST), then its check block.
+# Run in an attended root shell on each box (here: incus exec <box> -- bash).
+set -eu
+umask 077
+STAMP=$(date -u +%Y%m%dT%H%MZ)
+ASIDE="/var/lib/carlos-aside-$STAMP"
+REHEARSAL_HOST='moverehearsal.dev.harness.test' # set at the rehearsal: the same value as B1's
+: "${REHEARSAL_HOST:?set it from the rehearsal record}"
+HOSTS=(claimtrial.dev.harness.test wakecheck.dev.harness.test rollcheck-0261.dev.harness.test "$REHEARSAL_HOST")
+test "$(stat -c %d /data)" = "$(stat -c %d /var/lib)"
+test "$(stat -c %d /opt/carlos/live)" = "$(stat -c %d /var/lib)"
+mkdir -m 0700 -- "$ASIDE"
+test "$(stat -c '%a %U:%G' "$ASIDE")" = '700 root:root'
+aside() {
+ if [ -L "$1" ] || [ -e "$1" ]; then
+ mv -T -- "$1" "$ASIDE/$H/$2"
+ echo "moved $1"
+ else
+ echo "absent $1"
+ fi
+}
+for H in "${HOSTS[@]}"; do
+ test "$(python3 -c "import sqlite3,sys; c=sqlite3.connect('file:/data/registry.db?mode=ro',uri=True); print(c.execute('select count(*) from routes where host=?',(sys.argv[1],)).fetchone()[0])" "$H")" = 0
+ test -z "$(systemctl list-units --all --plain --no-legend "carlos-instance-gen@$H:*" "carlos-instance@$H.service")"
+ mkdir -m 0700 -- "$ASIDE/$H"
+ if [ -L "/opt/carlos/live/$H" ]; then readlink -- "/opt/carlos/live/$H"; fi
+ aside "/data/.instances/$H" instance
+ aside "/data/programs/$H.json" program.json
+ aside "/opt/carlos/live/$H" live-link
+ aside "/data/$H" key-dir
+ aside "/data/$H.config.env" config.env
+ aside "/data/$H.env" env
+done
+for H in "${HOSTS[@]}"; do
+ test ! -e "/data/.instances/$H"
+ test ! -L "/data/.instances/$H"
+ test ! -e "/data/instances/$H"
+ test ! -L "/data/instances/$H"
+ test ! -e "/data/programs/$H.json"
+ test ! -e "/opt/carlos/live/$H"
+ test ! -L "/opt/carlos/live/$H"
+ test ! -e "/data/$H"
+ test ! -e "/data/$H.config.env"
+ test ! -e "/data/$H.env"
+done
+ls -la /data/instances /data/.instances /data/programs /opt/carlos/live
+ls -laR "$ASIDE"
+echo "B2 OK $ASIDE"
diff --git a/harness/scenarios/cohort1/blocks/b5.sh b/harness/scenarios/cohort1/blocks/b5.sh
new file mode 100755
index 000000000..48ca1c5b8
--- /dev/null
+++ b/harness/scenarios/cohort1/blocks/b5.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# Block B5 (gate G2, read-only), operator shell. Runbook text verbatim but for the harness values:
+# R: --profile kass -> harness; B: carlos-dev-640012953448 -> harness-deployment; S: ~/cohort1/ssm.sh;
+# sophie i-05498eada6f0afd23 -> $SOPHIE, dennis i-009d593f451cfd562 -> $DENNIS; the inspect's bucket;
+# item 6 has no counterpart (the three stopped dev instances do not exist here); item 7's curl goes to
+# the console's loopback door (http://127.0.0.1:8090) instead of https://platform.dev.carlosframework.com.
+set -uo pipefail
+. ~/cohort1/values.env
+mkdir -p "$TMPDIR/cohort1"
+R="--profile harness --region eu-west-1"
+B=harness-deployment
+S=~/cohort1/ssm.sh
+echo "== B5 $(date -u +%FT%TZ)"
+# 1. The empty app segment: no current object, no PRE /
+echo "-- 1"; aws $R s3 ls --recursive "s3://$B/apps/bab//" | wc -l
+aws $R s3 ls "s3://$B/apps/bab/"
+# 2. Instance records, and each one's acq (backfill-collect refuses a record without one)
+echo "-- 2"; aws $R s3 ls --recursive "s3://$B/apps/" | awk '{print $4}' | grep -E '/(instances|sidecars)/[^/]+\.json$' > "$TMPDIR/cohort1/records" || true
+cat "$TMPDIR/cohort1/records"
+while read -r key; do aws $R s3 cp "s3://$B/$key" - | jq -r --arg k "$key" '[$k, (.acq // "MISSING")] | @tsv'; done < "$TMPDIR/cohort1/records"
+# 3. No backfill plan, no claims, no admission
+echo "-- 3"; aws $R s3api head-object --bucket "$B" --key control/hosts-backfill.json 2>&1 | grep -c 'Not Found'
+aws $R s3api list-objects-v2 --bucket "$B" --prefix control/hosts/ --query 'KeyCount'
+aws $R s3 ls --recursive "s3://$B/apps/" | awk '{print $4}' | grep -cE '/admission\.json$' || true
+# 4. The roster's pools, from a signature-verified inspect on sophie (stdout only, nothing staged)
+echo "-- 4"; "$S" eu-west-1 "$SOPHIE" 'cd /root/claim-trial && /opt/carlos/carlos system claims inspect --bucket harness-deployment --aws-region eu-west-1 --public-key "$(cat release.pub)" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[\"roster\"][\"revision\"], d[\"roster\"][\"regions\"][\"ie\"][\"pools\"])"'
+# 5. Sophie and dennis: no cohort1 row, no CARLOS_POOLS
+echo "-- 5"; "$S" eu-west-1 "$SOPHIE" 'python3 -c "import sqlite3; c=sqlite3.connect(\"file:/data/registry.db?mode=ro\",uri=True); print(c.execute(\"select count(*) from routes where pool=\x27cohort1\x27\").fetchone()[0])"; grep -c "^CARLOS_POOLS=" /etc/carlos/host.env || true'
+"$S" eu-west-2 "$DENNIS" 'python3 -c "import sqlite3; c=sqlite3.connect(\"file:/data/registry.db?mode=ro\",uri=True); print(c.execute(\"select count(*) from routes where pool=\x27cohort1\x27\").fetchone()[0])"; grep -c "^CARLOS_POOLS=" /etc/carlos/host.env || true'
+# 6. The three stopped instances carry no box role
+echo "-- 6: no counterpart in the harness (no stopped dev instances exist here)"
+# 7. The dev console is claims-off
+echo "-- 7"; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/healthz
+"$S" eu-west-1 "$SOPHIE" 'sha256sum /etc/carlos/claim-console.env | cut -c1-64; tr "\0" "\n" < /proc/$(systemctl show -p MainPID --value console.service)/environ | grep "^CARLOS_HOST_CLAIM="; journalctl -u console.service --since "$(systemctl show -p ActiveEnterTimestamp --value console.service)" --no-pager | grep -c "off on a host-claim console" || true'
diff --git a/harness/scenarios/cohort1/blocks/g4-roll.sh b/harness/scenarios/cohort1/blocks/g4-roll.sh
new file mode 100755
index 000000000..3924e48a6
--- /dev/null
+++ b/harness/scenarios/cohort1/blocks/g4-roll.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+# Gate G4, the harness equivalent of D10's roll: runbook step 1 item 2's new road for the stable
+# channel only (leo and dennis follow stable; sophie stays on canary at v0.28.5, as dev's sophie stays
+# on v0.28.4), with the harness's one change, --file (see system-update.sh).
+set -euo pipefail
+. ~/cohort1/values.env
+C=http://127.0.0.1:8090
+F=~/release-v0.28.6/carlos-linux-arm64
+carlos system status --console $C
+date -u +%FT%TZ
+carlos system update --console $C --channel stable --version v0.28.6 --file $F --platform linux-arm64 --yes
+date -u +%FT%TZ
+for i in $(seq 1 40); do
+ s=$(carlos system status --console $C 2>/dev/null)
+ echo "$s" | grep -q 'kathleen-juggling.*v0.28.6.*current' && echo "$s" | grep -q 'noor-marquetry.*v0.28.6.*current' && break
+ sleep 15
+done
+date -u +%FT%TZ; carlos system status --console $C
+for b in "$LEO" "$DENNIS"; do r=eu-west-1; [ "$b" = "$DENNIS" ] && r=eu-west-2; ./../ssm.sh $r "$b" '/opt/carlos/carlos version; /opt/carlos/carlos.prev version; systemctl is-active carlos-edge.service carlos-instance-control.socket; systemctl show carlos-edge.service -p ExecStart --value | grep -o "guard-exec[^;]*" | head -1; systemctl show carlos-instance-control.service -p ActiveEnterTimestamp; cat /data/system/router/applied.json 2>/dev/null | head -c 300; echo'; done
diff --git a/harness/scenarios/cohort1/blocks/pw-seed.sh b/harness/scenarios/cohort1/blocks/pw-seed.sh
new file mode 100755
index 000000000..0bb94d576
--- /dev/null
+++ b/harness/scenarios/cohort1/blocks/pw-seed.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+# Pre-window state, SEEDED BY PLATFORM COMMANDS (host root shell), from snapshot pre-window + H14/H15.
+# Recreates what the runbook assumes on the morning of the window, through the ordinary doors, claims off:
+# A sophie: the 2026-10-19 pause (saved copies, then the runbook's pause recipe, verbatim)
+# B wakecheck (pool-less) before leo's pool; woken on leo so leo holds its leftovers (as dev's leo does)
+# C runbook step 4.2 (CARLOS_POOLS=cohort1) and 4.3 (carlos remove on leo), box commands verbatim
+# D claimtrial (step 4.4, pool cohort1, store-mode enable, signed-in create), woken on leo (step 4.5)
+# E rollcheck-0261 (pool-less, --kind static) and the offline-move rehearsal app moverehearsal (--region uk)
+# Harness substitutions: console http://127.0.0.1:8090, domain dev.harness.test, build = the harness
+# wakecheck stand-in (harness/apps/wakecheck) shipped as version wc4, certificates from H13.
+set -euo pipefail
+OP() { runuser -l operator -c "set -euo pipefail; C=http://127.0.0.1:8090; F=~/cohort1/pw/linux-arm64; $1"; }
+echo "== A1 saved copies ($(date -u +%TZ))"
+incus exec sophie -- bash -c 'set -euo pipefail; umask 077
+D=/root/claim-trial/paused-20261019; install -d -m 0700 "$D"
+cp -p /etc/carlos/claim-console.env "$D/claim-console.env"; cp -p /etc/systemd/system/console.service.d/91-claim-runtime.conf "$D/91-claim-runtime.conf"
+cmp /etc/carlos/claim-console.env "$D/claim-console.env"; cmp /etc/systemd/system/console.service.d/91-claim-runtime.conf "$D/91-claim-runtime.conf"
+sha256sum "$D"/*'
+echo "== A2 pause recipe, verbatim but its last line (see Runbook corrections) ($(date -u +%TZ))"
+incus exec sophie -- bash -c 'set -euo pipefail
+umask 077
+[ "$(sha256sum /etc/carlos/claim-console.env | cut -c1-64)" = 5df17bac42a91a02d90584edc5476c045400af57e467b2ea0815ba50b7e31d03 ]
+printf '"'"'CARLOS_CONSOLE_ID=dev-console\nCONSOLE_HOME_REGION=ie\nCARLOS_HOST_CLAIM=\n'"'"' > /etc/carlos/.claim-console.env.pause
+[ "$(sha256sum /etc/carlos/.claim-console.env.pause | cut -c1-64)" = c1ae2900db31295090d2f4f2f9b5f6cdcca07e74545a6b1a9da039c3abf5c412 ]
+mv -T /etc/carlos/.claim-console.env.pause /etc/carlos/claim-console.env
+systemctl restart console.service
+systemctl is-active console.service
+for i in $(seq 1 40); do p=$(systemctl show -p MainPID --value console.service); [ "$(readlink /proc/$p/exe)" = /opt/carlos/system/console/console ] && break; sleep 0.25; done
+tr '"'"'\0'"'"' '"'"'\n'"'"' < "/proc/$p/environ" | grep '"'"'^CARLOS_HOST_CLAIM='"'"'
+sleep 3; curl -s -o /dev/null -w "healthz %{http_code}\n" http://127.0.0.1:8090/healthz'
+install -d -o operator -g operator -m 0755 /home/operator/cohort1/pw
+aws s3 cp --quiet s3://carlos-harness-640012953448/artefacts/v0.28.6/wakecheck-linux-arm64 /home/operator/cohort1/pw/linux-arm64
+chown operator:operator /home/operator/cohort1/pw/linux-arm64
+echo "== B wakecheck ($(date -u +%TZ))"
+OP 'sha256sum $F; carlos apps create --app wakecheck --account bab --console $C
+carlos ship --app wakecheck --account bab --console $C --version wc4 --label "harness wakecheck stand-in, wc4" --no-prompt $F
+carlos promote --app wakecheck --account bab --console $C --no-prompt wc4 edge
+AWS_PROFILE=harness CARLOS_DEPLOYMENT_BUCKET=harness-deployment carlos instances enable --app wakecheck --account bab --domain dev.harness.test --channel edge
+carlos instances create --app wakecheck --account bab --host wakecheck.dev.harness.test --console $C --no-prompt'
+bash /root/harness/certs.sh wakecheck.dev.harness.test claimtrial.dev.harness.test rollcheck-0261.dev.harness.test moverehearsal.dev.harness.test
+sleep 40
+for i in 1 2 3; do incus exec leo -- curl -s -m 40 --cacert /root/harness-ca.pem -w ' [%{http_code}]\n' --resolve wakecheck.dev.harness.test:443:127.0.0.1 https://wakecheck.dev.harness.test/; done
+incus exec leo -- sh -c 'journalctl -u carlos-edge --since -2min --no-pager | grep -E "layout|nested" | tail -3; ls /data/instances /data/.instances'
+echo "== C step 4.2, 4.3 ($(date -u +%TZ))"
+incus exec leo -- bash -c 'cp -p /etc/carlos/host.env /etc/carlos/host.env.bak-pool-$(date -u +%Y%m%d)
+printf "CARLOS_POOLS=cohort1\n" >> /etc/carlos/host.env
+systemctl restart carlos-edge.service && systemctl is-active carlos-edge.service'
+sleep 8
+incus exec leo -- bash -c 'runuser -u carlos -- /opt/carlos/carlos remove --registry /data/registry.db wakecheck.dev.harness.test; systemctl is-active carlos-edge.service'
+echo "== D, E ($(date -u +%TZ))"
+for app in claimtrial rollcheck-0261 moverehearsal; do
+ OP "carlos apps create --app $app --account bab --console \$C
+carlos ship --app $app --account bab --console \$C --version wc4 --label 'harness pre-window $app, wakecheck stand-in wc4' --no-prompt \$F
+carlos promote --app $app --account bab --console \$C --no-prompt wc4 edge"
+ sleep 30
+done
+OP 'AWS_PROFILE=harness CARLOS_DEPLOYMENT_BUCKET=harness-deployment carlos instances enable --app claimtrial --account bab --domain dev.harness.test --pool cohort1 --channel edge
+AWS_PROFILE=harness CARLOS_DEPLOYMENT_BUCKET=harness-deployment carlos instances enable --app rollcheck-0261 --account bab --domain dev.harness.test --channel edge
+AWS_PROFILE=harness CARLOS_DEPLOYMENT_BUCKET=harness-deployment carlos instances enable --app moverehearsal --account bab --domain dev.harness.test --channel edge
+carlos instances create --app claimtrial --account bab --host claimtrial.dev.harness.test --console $C --no-prompt
+carlos instances create --app rollcheck-0261 --account bab --host rollcheck-0261.dev.harness.test --kind static --console $C --no-prompt
+carlos instances create --app moverehearsal --account bab --host moverehearsal.dev.harness.test --region uk --console $C --no-prompt'
+sleep 40
+for i in 1 2 3; do incus exec leo -- curl -s -m 40 --cacert /root/harness-ca.pem -w ' [%{http_code}]\n' --resolve claimtrial.dev.harness.test:443:127.0.0.1 https://claimtrial.dev.harness.test/; done
+for b in leo sophie dennis; do echo "== $b"; incus exec $b -- python3 -c "import sqlite3; c=sqlite3.connect('file:/data/registry.db?mode=ro',uri=True); [print(r) for r in c.execute('select host,app,pool,region,channel,hibernate from routes order by host')]"; incus exec $b -- sh -c 'ls /data/instances /data/.instances /data/programs /opt/carlos/live 2>&1 | tr "\n" " "; echo'; done
+OP 'aws s3 ls --recursive s3://harness-deployment/apps/ | awk "{print \$4}" | grep -E "/(instances|sidecars)/[^/]+\.json$"'
diff --git a/harness/scenarios/cohort1/blocks/w-identity.sh b/harness/scenarios/cohort1/blocks/w-identity.sh
new file mode 100755
index 000000000..5c911a604
--- /dev/null
+++ b/harness/scenarios/cohort1/blocks/w-identity.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Gate G3: publish-identity for the window release (runbook 6.1's second phase; the floor is
+# unchanged, so no sign-policy). HARNESS: tag v0.28.6, console ship version cc90d542 (shipped and
+# promoted to canary/console by release-window.sh, the 5.0a step the window package lacks), bucket.
+set -euo pipefail
+. ~/cohort1/values.env
+cd ~/cohort1
+RUN=$(cat run-id); X=s3://harness-deployment/claim-signer/$RUN
+./ssm.sh eu-west-1 "$SIGNER" "
+. /root/claim-trial/helpers.sh
+CV=cc90d542
+jq -n --rawfile sums SHA256SUMS.txt --rawfile sig SHA256SUMS.txt.sig --arg cv \"\$CV\" \\
+ '{identity:{release_tag:\"v0.28.6\",moves_protocol:1,sha256sums:\$sums,sha256sums_sig:\$sig,
+ console:{account:\"bab\",app:\"console\",channel:\"canary/console\",version:\$cv}}}' > identity-phase.json
+trap 'tail -20 publish-identity-w.log' EXIT
+phase_out publish-identity identity-phase.json /root/claim-trial/identity.v0.28.6.json publish-identity-w-result.json > publish-identity-w.log 2>&1
+trap - EXIT
+grep -E '\"writes\"|before_etag|\"key\"' publish-identity-w.log | head -8
+echo \"publish-identity writes: \$(jq -c .writes publish-identity-w-result.json)\"
+jq '{identity_key, release}' publish-identity-w-result.json
+aws s3 cp --quiet identity.v0.28.6.json $X/out/identity.v0.28.6.json; sha256sum identity.v0.28.6.json | cut -c1-16
+echo REV=\$REV
+"
+mkdir -p out
+aws s3 cp --quiet "$X/out/identity.v0.28.6.json" out/identity.v0.28.6.json
+aws s3 cp --quiet s3://harness-deployment/control/system/router/releases/v0.28.6.json - | cmp - out/identity.v0.28.6.json && echo "identity file = published object"
diff --git a/harness/scenarios/cohort1/blocks/w-open-signer.sh b/harness/scenarios/cohort1/blocks/w-open-signer.sh
new file mode 100755
index 000000000..728690c69
--- /dev/null
+++ b/harness/scenarios/cohort1/blocks/w-open-signer.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Signing session 6 (window) opening, as 51-open-signer.sh, with the window release v0.28.6 (runbook "Order of work" 1; signing-host.md "Getting jq and the
+# carlos CLI there" and "Getting the key there"), from the operator shell.
+# HARNESS: the signer is the signer container (no EC2 instance, role or SG is created: its
+# MinIO user stands for carlos-claim-signer); the exchange is s3://harness-deployment/claim-signer/<run-id>/;
+# jq is Debian 13's packaged jq (installed at base), not the jq-1.7.1 GitHub binary.
+set -euo pipefail
+. ~/cohort1/values.env
+cd ~/cohort1
+RUN=cohort1-$(date -u +%Y%m%dT%H%M%SZ); echo "$RUN" > run-id; echo "run-id $RUN"
+X=s3://harness-deployment/claim-signer/$RUN
+for f in ~/release.pub ~/release-v0.28.6/SHA256SUMS.txt ~/release-v0.28.6/SHA256SUMS.txt.sig ~/release-v0.28.6/carlos-linux-arm64 \
+ inputs/floor.json inputs/regions.json inputs/leo.json inputs/sophie.json inputs/console.json signer-helpers.sh; do
+ aws s3 cp --quiet "$f" "$X/$(basename "$f")"
+done
+aws s3 ls "$X/"
+SUMS=$(cd ~/release-v0.28.6 && sha256sum carlos-linux-arm64 SHA256SUMS.txt SHA256SUMS.txt.sig | awk '{print $1}' | paste -sd' ')
+H=$(sha256sum signer-helpers.sh | cut -d' ' -f1); P=$(sha256sum ~/release.pub | cut -d' ' -f1)
+./ssm.sh eu-west-1 "$SIGNER" "
+set -euo pipefail
+set -- $SUMS
+aws s3 cp --quiet $X/carlos-linux-arm64 /usr/local/bin/carlos
+printf '%s %s\n' \$1 /usr/local/bin/carlos | sha256sum -c
+chmod 0755 /usr/local/bin/carlos
+/usr/local/bin/carlos version
+jq --version; command -v jq
+install -d -m 0700 -o root -g root /root/claim-trial
+cd /root/claim-trial
+for f in release.pub SHA256SUMS.txt SHA256SUMS.txt.sig floor.json regions.json leo.json sophie.json console.json signer-helpers.sh; do aws s3 cp --quiet $X/\$f \$f; done
+mv signer-helpers.sh helpers.sh; chmod 0600 *
+printf '%s %s\n' \$2 SHA256SUMS.txt \$3 SHA256SUMS.txt.sig $H helpers.sh $P release.pub | sha256sum -c
+swapon --show | wc -l | sed 's/^/swap devices: /'; findmnt -no FSTYPE --target /run | sed 's/^/\/run: /'
+/usr/local/bin/carlos system claims help 2>&1 | sed 's/^/help| /'
+echo session-start \$(date -u +%FT%TZ)
+install -d -m 0700 -o root -g root /run/claim-signer
+aws --region eu-west-1 ssm get-parameter --name /carlos/release-key --with-decryption \
+ --query Parameter.Value --output text | install -m 0600 -o root -g root /dev/stdin /run/claim-signer/release.key
+stat -c '%U %a %h %s' /run/claim-signer/release.key
+"
diff --git a/harness/scenarios/cohort1/values.env b/harness/scenarios/cohort1/values.env
index ce2c3c1c2..2c96b171d 100644
--- a/harness/scenarios/cohort1/values.env
+++ b/harness/scenarios/cohort1/values.env
@@ -10,3 +10,8 @@ BUCKET=harness-deployment # dev: carlos-dev-640012953448
PROFILE=harness # dev: kass
TAG=v0.28.5 # dev: v0.28.4
CV=1b635e61 # dev: f3bf1214 (the console's ship version)
+# Window (Option B) values, 2026-10-20 morning harness run:
+WTAG=v0.28.6 # dev: v0.28.5 (the window release, claim-empty-cohort-done); harness v0.28.6 = cc90d542 + harness patch
+WCV=cc90d542 # dev: the v0.28.5 console's ship version
+DOMAIN=dev.harness.test # dev: dev.carlosframework.com
+CONSOLE=http://127.0.0.1:8090 # dev: https://platform.dev.carlosframework.com
diff --git a/harness/scenarios/sync.sh b/harness/scenarios/sync.sh
new file mode 100755
index 000000000..c791fe5b8
--- /dev/null
+++ b/harness/scenarios/sync.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+# sync.sh: copy harness/scenarios/cohort1 (blocks, helpers, inputs; never out/) to the
+# operator's ~/cohort1 on the host, through hssm.py (DOCS-T1, sandbox off).
+set -euo pipefail
+d=$(cd "$(dirname "$0")" && pwd)
+b64=$(tar -C "$d/cohort1" -czf - --exclude=out . | base64 -w0)
+printf 'echo %s | base64 -d | tar -xzf - -C /home/operator/cohort1 --no-same-owner\nchown -R operator:operator /home/operator/cohort1\nls /home/operator/cohort1/blocks | wc -l\n' "$b64" \
+ | python3 "$d/../hssm.py" -
diff --git a/harness/snapshot.sh b/harness/snapshot.sh
index e5a3a411f..4542f0916 100755
--- a/harness/snapshot.sh
+++ b/harness/snapshot.sh
@@ -2,6 +2,8 @@
# Consistent harness snapshot / restore: every container stopped, plus sophie's
# ext4 console volume (H11), which container snapshots do not include.
# snapshot.sh create <name> | restore <name>
+# (Fixed 2026-10-20 07:3xZ: the volume test used "show hdir sophie-console <name>", which incus 6.0.4
+# rejects, so every restore before then skipped the console volume.)
set -euo pipefail
op=$1 name=$2
ALL="infra leo sophie dennis signer"
@@ -10,8 +12,11 @@ case $op in
create) for c in $ALL; do incus snapshot create $c "$name"; done
incus storage volume snapshot create hdir sophie-console "$name" >/dev/null ;;
restore) for c in $ALL; do incus snapshot restore $c "$name"; done
- if incus storage volume snapshot show hdir sophie-console "$name" >/dev/null 2>&1; then
+ if incus storage volume snapshot show hdir "sophie-console/$name" >/dev/null 2>&1; then
incus storage volume snapshot restore hdir sophie-console "$name"
+ echo "sophie-console volume restored to $name"
+ else
+ echo "WARNING: no sophie-console volume snapshot $name; the console DB was NOT restored"
fi ;;
*) echo "usage: snapshot.sh create|restore <name>"; exit 2 ;;
esac
diff --git a/harness/stand-ins/certs.sh b/harness/stand-ins/certs.sh
new file mode 100755
index 000000000..ebeaa205b
--- /dev/null
+++ b/harness/stand-ins/certs.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# H13, the certificate stand-in (on host, root): the harness has no route to
+# Let's Encrypt, so a host's certificate is minted under a harness test CA and
+# published where the housekeeper's issuance would publish it,
+# control/certs/<host> (autocert's cache entry: private key PEM, then the
+# certificate PEM; internal/edge/certsync.go parseCertEntry). Every edge's
+# mirror then serves it, as dev's edges serve sophie's Let's Encrypt copies.
+# The CA key lives on tmpfs (/run/harness/keys/ca.key); its certificate goes
+# to /home/operator/harness-ca.pem and each box's /root/harness-ca.pem for
+# curl --cacert.
+# certs.sh <host> [<host>...]
+set -euo pipefail
+K=/run/harness/keys; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+if [ ! -s $K/ca.key ]; then
+ (umask 077; openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out $K/ca.key)
+ openssl req -x509 -new -key $K/ca.key -subj "/CN=carlos harness test CA" -days 7 -out $K/ca.pem 2>/dev/null
+fi
+install -o operator -g operator -m 0644 $K/ca.pem /home/operator/harness-ca.pem
+for c in leo sophie dennis; do incus file push $K/ca.pem $c/root/harness-ca.pem --uid 0 --gid 0 --mode 0644 -q; done
+eval "$(python3 -c 'import json; u=json.load(open("/root/harness/minio-users.json"))["operator"]; print("export AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s" % (u["access_key"], u["secret_key"]))')"
+export AWS_ENDPOINT_URL_S3=http://203.0.113.10:9000 AWS_REGION=eu-west-1
+for h in "$@"; do
+ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out $W/$h.key
+ openssl req -new -key $W/$h.key -subj "/CN=$h" -out $W/$h.csr 2>/dev/null
+ printf 'subjectAltName=DNS:%s\nextendedKeyUsage=serverAuth\n' "$h" > $W/$h.ext
+ openssl x509 -req -in $W/$h.csr -CA $K/ca.pem -CAkey $K/ca.key -CAcreateserial -days 3 -extfile $W/$h.ext -out $W/$h.pem 2>/dev/null
+ cat $W/$h.key $W/$h.pem > $W/$h.entry
+ aws s3 cp --quiet $W/$h.entry "s3://harness-deployment/control/certs/$h"
+ echo "published control/certs/$h ($(openssl x509 -in $W/$h.pem -noout -enddate))"
+done