carlos / platform Public

harness: SSM stand-in, operator shell, bootstrap-box.sh unchanged, snapshot boot (step 6); TEARDOWN

carlos-edge active on all three boxes with nosymfollow mounts, cgroup
BPF enforced in the nested containers, status/boxes reports in MinIO
56 s after bootstrap, stdout truncated at 24,000 by the stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev efba3c4ec3fe312ed2dc9fcb5d3dd06fcdaa853c parent f434a42
14 files changed, +744 −0
  • harness/RESOURCES.md +1 −0
  • harness/RUNLOG.md +100 −0
  • harness/TEARDOWN.md +54 −0
  • harness/checks/check2.sh +39 −0
  • harness/checks/check5.sh +13 −0
  • harness/checks/check6.sh +37 −0
  • harness/checks/check6b.sh +27 −0
  • harness/checks/fix5.sh +6 −0
  • harness/checks/run6b.sh +9 −0
  • harness/checks/snap-base.sh +18 −0
  • harness/checks/snap-boot.sh +11 −0
  • harness/checks/ssmsmoke.sh +18 −0
  • harness/operator/operator-setup.sh +101 −0
  • harness/stand-ins/ssm.py +310 −0

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m

amadan: job cancelled — a newer push superseded it
diff --git a/harness/RESOURCES.md b/harness/RESOURCES.md
index 96cb866d9..fbfc845ce 100644
--- a/harness/RESOURCES.md
+++ b/harness/RESOURCES.md
@@ -14,3 +14,4 @@ Appended as each is created. Teardown commands: harness/TEARDOWN.md.
| 2026-10-20T00:50:44Z | ec2 instance (donor-2) | i-019177034b088459f | t4g.nano, profile carlos-harness, terminate on shutdown, shutdown -h +60; to be terminated once captured |
| 2026-10-20T00:51:50Z | (terminated) donor-1 | i-0d9175215cfcf63b8 | documents in s3://carlos-harness-640012953448/donors/donor-1/ |
| 2026-10-20T00:51:52Z | (terminated) donor-2 | i-019177034b088459f | documents in s3://carlos-harness-640012953448/donors/donor-2/ |
+| 2026-10-20T01:06:29Z | ebs volume (host root) | vol-035c92e5b506cc3db | 60 GiB gp3, DeleteOnTermination=true; goes with the host |
diff --git a/harness/RUNLOG.md b/harness/RUNLOG.md
index 212f8c99b..1f87c8910 100644
--- a/harness/RUNLOG.md
+++ b/harness/RUNLOG.md
@@ -270,3 +270,103 @@ MinIO 203.0.113.10:9000 connects. To reopen egress for re-provisioning (step 9's
`base-opt`): `nft add element inet harness egress_ports '{ 80, 443 }'; rm /etc/harness/egress-closed`.
**Step 5: GREEN** (2026-10-20T00:59:15Z).
+
+## Step 6: the SSM stand-in, `bootstrap-box.sh` unchanged, snapshot `boot`
+
+Harness binaries rebuilt at `f434a425` (`version.Build=harness-f434a425`, `-tags harness`,
+linux/arm64), sha256 carlos `f07a0c52...`, console `f1e85706...`, at
+`s3://carlos-harness-640012953448/artefacts/harness-f434a425/`.
+
+`harness/operator/operator-setup.sh` (on host, 01:02 UTC): awscli on the host; the harness
+`carlos` at `/usr/local/bin/carlos`; `/etc/carlos/harness-endpoint` on the host; the test
+**root release key** from `carlos release-keygen` at `/run/harness/keys/release.env` (root
+0600, tmpfs; pubkey `SN3K2ONY...`, sha256 `25b94d19adb8`); user `operator` (primary group
+Debian's existing `operator`, gid 37) with `~/.aws` profile `harness` = MinIO
+`harness-operator` and `~/.harness-env` = exactly `AWS_ENDPOINT_URL_SSM`,
+`AWS_ENDPOINT_URL_S3`, `AWS_REGION`, `AWS_PROFILE`; `~/infra/scripts/` = verbatim
+`bootstrap-box.sh` and `update-platform-binary.sh` from a0b3f5a. Then the SSM stand-in
+(`harness/stand-ins/ssm.py` -> `/opt/harness/ssm.py`, unit `harness-ssm.service`, bound
+203.0.113.1:8100; `/etc/harness/ssm.json`: leo = `i-0d9175215cfcf63b8` (donor-1), sophie =
+`i-019177034b088459f` (donor-2), dennis = `i-00000000000000023`, signer =
+`i-00000000000000030`, the last two harness-only ids). **Seeded:** the operator copied the
+harness `carlos` to `s3://harness-deployment/bootstrap/carlos` (the release recipe's job).
+
+Stand-in smoke test (`harness/checks/ssmsmoke.sh`), real aws CLI 2.23.6 from the operator shell:
+runs as `root`, `umask 0022`, `HOME` unset, cwd
+`/var/lib/amazon/ssm/<iid>/document/orchestration/<cid>/awsrunShellScript/0.awsrunShellScript`,
+env = agent PATH + the three injected variables; unknown instance -> `InvalidInstanceId`;
+Parameter Store put/get (SecureString)/`ParameterAlreadyExists`/delete/`ParameterNotFound`.
+
+Bootstrap (`harness/checks/run6b.sh`), as `operator`, script unchanged:
+
+```
+~/infra/scripts/bootstrap-box.sh harness i-0d9175215cfcf63b8 harness-deployment harness-data-ie ~/release.pub eu-west-1 # leo 01:03:04-01:03:11Z
+~/infra/scripts/bootstrap-box.sh harness i-019177034b088459f harness-deployment harness-data-ie ~/release.pub eu-west-1 # sophie 01:03:11-01:03:18Z
+~/infra/scripts/bootstrap-box.sh harness i-00000000000000023 harness-deployment harness-data-uk ~/release.pub eu-west-2 uk # dennis 01:03:18-01:03:25Z
+each: status: Success / bootstrap: units installed, carlos-edge.service + ... + carlos-instance-control.socket enabled / active / carlos harness-f434a425
+```
+
+Check (`harness/checks/check6.sh`, `check6b.sh`, 01:04-01:06 UTC):
+
+```
+status/boxes/ in MinIO, each 56 s after its box's bootstrap returned (01:03:11, :18, :25):
+ 01:04:07 status/boxes/kathleen-juggling.json (leo)
+ 01:04:14 status/boxes/ada-letterpress.json (sophie)
+ 01:04:21 status/boxes/noor-marquetry.json (dennis)
+every box: carlos-edge active, Result=success NRestarts=0
+ data-instances.mount active success /data/instances rw,nosuid,nodev,noexec,relatime,nosymfollow (btrfs subvol)
+ run-carlos-instances.mount active success /run/carlos/instances rw,nosuid,nodev,noexec,nosymfollow (tmpfs)
+ run-carlos-logs.mount active success /run/carlos/logs rw,nosuid,nodev,noexec,nosymfollow (tmpfs)
+ systemctl --failed: empty; is-system-running: running
+ journal this boot: [0] "Failed to set up mount namespacing" [0] IPAddressDeny [0] BPF [0] "not supported"
+ [0] AccessDenied [0] panic: [0] amazonaws.com [0] "Permission denied"
+cgroup BPF inside the nested container (functional):
+ systemd-run -p IPAddressDeny=any -p IPAddressAllow=localhost -> MinIO: blocked (timed out)
+ systemd-run (no deny) -> MinIO: CONNECTED
+ carlos-instance@.service IPAddressDeny covers 203.0.113.0/24? no (B2 holds)
+host.env (no endpoint variable; the units reach MinIO through the harness file fallback):
+ CARLOS_DATA_DIR=/data CARLOS_DEPLOYMENT_BUCKET=harness-deployment CARLOS_HIBERNATE_BUCKET=harness-data-ie
+ CARLOS_HIBERNATE_REGION=eu-west-1 CARLOS_HIBERNATE_PREFIX=instances AWS_REGION=eu-west-1 (+ CARLOS_RELEASE_PUBKEY;
+ dennis: harness-data-uk, eu-west-2, CARLOS_REGION=uk)
+layout after bootstrap: /opt/carlos carlos:carlos 755, /opt/carlos/carlos carlos:carlos 755, host.env carlos:carlos 600
+truncation: 30,000-byte stdout + 9,000-byte stderr -> Status Success, stdout 24000 chars, stderr 8000 chars
+```
+
+Box labels (from the fixed machine-ids): leo = `kathleen-juggling`, sophie = `ada-letterpress`,
+dennis = `noor-marquetry`.
+
+Snapshot `boot` (`harness/checks/snap-boot.sh`): all five stopped, `incus snapshot create <c> boot`,
+started; after restart every container `running`, `--failed` empty, `carlos-edge` active with
+NRestarts=0 on all three boxes; stop-snapshot-start plus checks took 10 s.
+
+Noted, not acted on: the boxes' tmpfs mounts size to 20% of the **host's** RAM (3.2 GB), not the
+container's 1 GiB (no lxcfs meminfo); dev boxes would size to their own 1 GiB.
+
+**Step 6: GREEN** (2026-10-20T01:06:11Z).
+
+## Handoff to steps 7 to 11
+
+State: host `i-06a1ec144aa5226fb` running (TTL 2026-10-20T12:40Z, self-terminates); containers
+infra, leo, sophie, dennis, signer running at snapshot `boot` (also `base`); provisioning
+egress closed; stand-ins `harness-imds` and `harness-ssm` active; test root release key at
+`/run/harness/keys/release.env` (pubkey in every box's host.env). Not yet made: the test
+**sums** key (write its public half to `/etc/carlos/harness-sums-pubkey`, root 0644, on
+every box and the signer, and on the host for the operator CLI), `/carlos/release-key` in the
+fake Parameter Store, the console, the Keymail stand-in.
+
+Get in:
+
+```bash
+# Non-interactive, from the worktree on DOCS-T1 (sandbox off): any script runs as root on the host.
+echo 'incus exec leo -- systemctl status carlos-edge --no-pager' | python3 harness/hssm.py -
+echo 'runuser -l operator -c "aws s3 ls s3://harness-deployment/status/boxes/"' | python3 harness/hssm.py -
+
+# Interactive:
+aws ssm start-session --profile kass --region eu-west-1 --target i-06a1ec144aa5226fb
+sudo -i # then:
+incus exec leo -- bash # a box (leo, sophie, dennis, signer, infra)
+runuser -l operator # the operator shell: harness carlos CLI, aws -> stand-in SSM + MinIO
+```
+
+Reset: `incus stop infra leo sophie dennis signer; for c in infra leo sophie dennis signer; do
+incus snapshot restore $c boot; done; incus start infra; incus start leo sophie dennis signer`.
diff --git a/harness/TEARDOWN.md b/harness/TEARDOWN.md
new file mode 100644
index 000000000..c64602f70
--- /dev/null
+++ b/harness/TEARDOWN.md
@@ -0,0 +1,54 @@
+# Harness teardown
+
+Every resource in `RESOURCES.md`, in dependency order. Run from DOCS-T1 with the
+sandbox off (AWS needs it). Account 640012953448, eu-west-1, profile `kass`.
+Each destructive step checks the `purpose=carlos-harness` tag first and prints
+what it is about to remove.
+
+The host also removes itself: it runs `shutdown -h 12:40` UTC (its TTL,
+2026-10-20T12:40Z) with `instance-initiated-shutdown-behavior=terminate`, and its
+root volume has `DeleteOnTermination=true`. Steps 3 to 5 are still needed after that.
+
+```bash
+A="aws --profile kass --region eu-west-1"
+HOST=i-06a1ec144aa5226fb
+SG=sg-0b9cf4c2a508b7cec
+BUCKET=carlos-harness-640012953448
+
+# 1. The host (and with it vol-035c92e5b506cc3db). Refuses an untagged instance.
+P=$($A ec2 describe-instances --instance-ids $HOST --query 'Reservations[0].Instances[0].Tags[?Key==`purpose`].Value|[0]' --output text)
+[ "$P" = carlos-harness ] && echo "terminating $HOST (purpose=$P)" && \
+ $A ec2 terminate-instances --instance-ids $HOST && $A ec2 wait instance-terminated --instance-ids $HOST
+
+# 2. Leak check: nothing tagged may be left running or attached. The donors
+# (i-0d9175215cfcf63b8, i-019177034b088459f) were terminated at 00:52Z.
+$A ec2 describe-instances --filters Name=tag:purpose,Values=carlos-harness \
+ Name=instance-state-name,Values=pending,running,stopping,stopped,shutting-down \
+ --query 'Reservations[].Instances[].[InstanceId,State.Name]' --output text # expect nothing
+$A ec2 describe-volumes --filters Name=tag:purpose,Values=carlos-harness \
+ --query 'Volumes[].[VolumeId,State]' --output text # expect nothing
+
+# 3. The security group (only once the host is gone).
+$A ec2 describe-security-groups --group-ids $SG --query 'SecurityGroups[0].Tags[?Key==`purpose`].Value|[0]' --output text # carlos-harness
+$A ec2 delete-security-group --group-id $SG
+
+# 4. The instance profile and role.
+$A iam remove-role-from-instance-profile --instance-profile-name carlos-harness --role-name carlos-harness
+$A iam delete-instance-profile --instance-profile-name carlos-harness
+$A iam detach-role-policy --role-name carlos-harness --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
+$A iam delete-role-policy --role-name carlos-harness --policy-name carlos-harness-bucket
+$A iam delete-role --role-name carlos-harness
+
+# 5. The bucket (unversioned; lifecycle already expires objects after 3 days).
+$A s3api get-bucket-tagging --bucket $BUCKET --query 'TagSet[?Key==`purpose`].Value|[0]' --output text # carlos-harness
+$A s3 rm --recursive --quiet s3://$BUCKET
+$A s3api delete-bucket --bucket $BUCKET
+```
+
+To tear down only the containers and keep the host (for example to rebuild a
+box from scratch), on the host as root: `incus delete --force leo sophie dennis signer infra`.
+To return to a known state instead: stop all five, `incus snapshot restore <c> base|boot`,
+start `infra` first, then the rest.
+
+Nothing harness-related lives on DOCS-T1 beyond this worktree: the scratch
+directory `$TMPDIR/harness-build` was deleted at the end of the phase 1 build.
diff --git a/harness/checks/check2.sh b/harness/checks/check2.sh
new file mode 100644
index 000000000..e5b2e6f5d
--- /dev/null
+++ b/harness/checks/check2.sh
@@ -0,0 +1,39 @@
+set -u
+echo "== incus list (before)"; incus list -f csv; echo "(end)"
+echo "== shutdown --show"; shutdown --show 2>&1
+echo "== nft guard"; nft list table inet harness | head -40
+incus launch images:debian/13/cloud scratch -p box -q
+for i in $(seq 1 30); do incus exec scratch -- python3 -c 1 2>/dev/null && incus exec scratch -- ip -4 -o addr show eth0 | grep -q 203.0.113 && break; sleep 2; done
+incus exec scratch -- ip -4 -o addr show eth0
+HOSTIP=$(ip -4 -o addr show ens5 | awk '{print $4}' | cut -d/ -f1)
+cat > /tmp/probe.py <<PY
+import socket, urllib.request, sys
+def tcp(h, p):
+ s = socket.socket(); s.settimeout(4)
+ try:
+ s.connect((h, p)); return "CONNECTED"
+ except ConnectionRefusedError: return "refused"
+ except socket.timeout: return "timeout (dropped)"
+ except OSError as e: return "error %s" % e
+ finally: s.close()
+try:
+ r = urllib.request.Request("http://169.254.169.254/latest/api/token", method="PUT", headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"})
+ tok = urllib.request.urlopen(r, timeout=5).read()
+ print("imds token PUT: GOT A TOKEN (len %d)" % len(tok))
+ r = urllib.request.Request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", headers={"X-aws-ec2-metadata-token": tok.decode()})
+ print("imds role list:", urllib.request.urlopen(r, timeout=5).read())
+except Exception as e:
+ print("imds token PUT:", type(e).__name__, e)
+print("tcp 169.254.169.254:80 ->", tcp("169.254.169.254", 80))
+print("tcp 169.254.169.253:53 ->", tcp("169.254.169.253", 53))
+print("tcp 169.254.170.2:80 ->", tcp("169.254.170.2", 80))
+print("tcp host VPC ip $HOSTIP:22 ->", tcp("$HOSTIP", 22))
+print("tcp 203.0.113.1:22 ->", tcp("203.0.113.1", 22))
+print("tcp 172.31.0.2:53 (VPC resolver) ->", tcp("172.31.0.2", 53))
+print("tcp deb.debian.org:443 (provisioning egress) ->", tcp("deb.debian.org", 443))
+PY
+incus file push /tmp/probe.py scratch/root/probe.py -q
+incus exec scratch -- python3 /root/probe.py
+rm -f /tmp/probe.py
+incus delete --force scratch
+echo "== incus list (after)"; incus list -f csv; echo "(end)"
diff --git a/harness/checks/check5.sh b/harness/checks/check5.sh
new file mode 100644
index 000000000..44a05727b
--- /dev/null
+++ b/harness/checks/check5.sh
@@ -0,0 +1,13 @@
+set -u
+for b in leo sophie dennis; do
+ echo "================ $b"
+ incus exec $b -- sh -c 'echo "cloud-init: $(cloud-init status 2>&1 | head -1)"; echo "motd: $(cat /etc/motd)"; echo "machine-id: $(cat /etc/machine-id)"; echo "ssm agent: $(systemctl is-enabled amazon-ssm-agent 2>&1) / $(systemctl is-active amazon-ssm-agent 2>&1)"; echo "harness-endpoint: $(stat -c "%U:%G %a" /etc/carlos/harness-endpoint) $(cat /etc/carlos/harness-endpoint)"'
+ echo "-- shims in cloud-init-output.log:"; incus exec $b -- grep -h "harness shim" /var/log/cloud-init-output.log
+ echo "-- cloud-init errors:"; incus exec $b -- sh -c 'grep -ci "error\|Traceback" /var/log/cloud-init.log | sed "s/^/cloud-init.log error-ish lines: /"; cloud-init status --format json 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(\"errors:\", d.get(\"errors\"), \"recoverable:\", list((d.get(\"recoverable_errors\") or {}).keys()))"'
+ echo "-- layout probe:"; incus exec $b -- sh < /root/harness/layout-probe.sh
+ echo "-- systemctl --failed:"; incus exec $b -- systemctl --failed --no-legend --plain; echo "(end)"
+ echo "-- system state: $(incus exec $b -- systemctl is-system-running 2>&1)"
+done
+echo "================ signer"
+incus exec signer -- sh -c 'echo "cloud-init: $(cloud-init status | head -1)"; command -v jq aws openssl; echo "endpoint: $(cat /etc/carlos/harness-endpoint)"; systemctl --failed --no-legend --plain; echo "(failed end)"'
+incus list -f csv -c ns4
diff --git a/harness/checks/check6.sh b/harness/checks/check6.sh
new file mode 100644
index 000000000..89e2f01c7
--- /dev/null
+++ b/harness/checks/check6.sh
@@ -0,0 +1,37 @@
+set -u
+T0=$(date -u +%s)
+echo "== waiting for status/boxes/ in MinIO (bootstrap finished ~01:03:25Z)"
+for i in $(seq 1 36); do
+ n=$(runuser -l operator -c 'aws s3 ls s3://harness-deployment/status/boxes/ 2>/dev/null' | grep -c json)
+ [ "$n" -ge 3 ] && break; sleep 5
+done
+echo "after $(( $(date -u +%s) - T0 ))s at $(date -u +%TZ):"
+runuser -l operator -c 'aws s3 ls --recursive s3://harness-deployment/status/boxes/'
+for b in leo sophie dennis; do
+ echo "================ $b"
+ incus exec $b -- sh -c '
+ echo "carlos-edge: $(systemctl is-active carlos-edge)"
+ systemctl show -p Id,ActiveState,Result,NRestarts carlos-edge data-instances.mount run-carlos-instances.mount run-carlos-logs.mount | paste -sd" " | sed "s/ Id=/\nId=/g"
+ echo "failed: [$(systemctl --failed --no-legend --plain)] state: $(systemctl is-system-running)"
+ echo "mounts:"; findmnt -no TARGET,FSTYPE,OPTIONS /data/instances /run/carlos/instances /run/carlos/logs 2>&1
+ echo "journal (this boot) hits:"
+ for pat in "Failed to set up mount namespacing" "IPAddressDeny" "BPF" "not supported" "AccessDenied" "panic:" "amazonaws.com" "Permission denied"; do
+ c=$(journalctl -b --no-pager -o cat | grep -c -- "$pat"); echo " [$c] $pat"
+ done
+ echo "IPAccounting/IPAddressDeny on carlos-edge: $(systemctl show -p IPAddressDeny carlos-edge | cut -c1-120)"
+ stat -c "%n %U:%G %a" /opt/carlos /opt/carlos/carlos /etc/carlos/host.env
+ grep -v PUBKEY /etc/carlos/host.env | tr "\n" " "; echo
+ '
+ echo "-- box label/report:"; incus exec $b -- sh -c 'journalctl -u carlos-edge --no-pager -o cat | grep -iE "status|report|publish" | head -3 | cut -c1-200'
+done
+echo "================ journal lines mentioning IPAddressDeny / bpf on leo"
+incus exec leo -- sh -c 'journalctl -b --no-pager -o cat | grep -iE "IPAddress|bpf" | sort | uniq -c | head -5'
+echo "================ truncation: 30,000-byte stdout through the stand-in"
+L=$(python3 -c 'import json; print([k for k,v in json.load(open("/etc/harness/ssm.json"))["instances"].items() if v=="leo"][0])')
+runuser -l operator -c "
+CID=\$(aws ssm send-command --instance-ids $L --document-name AWS-RunShellScript --parameters '{\"commands\":[\"head -c 30000 /dev/zero | tr \\\\\\\\0 x; head -c 9000 /dev/zero | tr \\\\\\\\0 y >&2\"]}' --query Command.CommandId --output text)
+for i in 1 2 3 4 5 6 7 8 9 10; do S=\$(aws ssm get-command-invocation --command-id \$CID --instance-id $L --query Status --output text); [ \$S = Success ] && break; sleep 1; done
+aws ssm get-command-invocation --command-id \$CID --instance-id $L --output json | python3 -c 'import json,sys; d=json.load(sys.stdin); print(\"status\", d[\"Status\"], \"stdout chars\", len(d[\"StandardOutputContent\"]), \"stderr chars\", len(d[\"StandardErrorContent\"]))'
+ls -l /dev/null >/dev/null; echo cid=\$CID
+"
+ls -l /var/log/harness/ssm-commands/*/ | tail -4
diff --git a/harness/checks/check6b.sh b/harness/checks/check6b.sh
new file mode 100644
index 000000000..a754019d0
--- /dev/null
+++ b/harness/checks/check6b.sh
@@ -0,0 +1,27 @@
+set -u
+for b in leo sophie dennis; do
+ echo "================ $b machine-id $(incus exec $b -- cat /etc/machine-id)"
+ incus exec $b -- sh -c '
+ for u in data-instances.mount run-carlos-instances.mount run-carlos-logs.mount; do
+ echo "$u: $(systemctl show -p ActiveState,Result,What,Where,Options $u | paste -sd" ")"
+ done
+ grep -E "^(/data|/run/carlos)" /proc/self/mountinfo >/dev/null; awk "\$5 ~ /^\/(data|run\/carlos)/ {print \" mountinfo:\", \$5, \$6}" /proc/self/mountinfo
+ # cgroup BPF inside the nested container: a transient unit with IPAddressDeny=any must not reach MinIO.
+ printf "IPAddressDeny=any -> MinIO: "
+ systemd-run --quiet --wait --pipe -p IPAddressDeny=any -p IPAddressAllow=localhost /usr/bin/python3 -c "
+import socket; s=socket.socket(); s.settimeout(3)
+try: s.connect((\"203.0.113.10\",9000)); print(\"CONNECTED (deny NOT enforced)\")
+except Exception as e: print(\"blocked:\", type(e).__name__, e)"
+ printf "no deny -> MinIO: "
+ systemd-run --quiet --wait --pipe /usr/bin/python3 -c "
+import socket; s=socket.socket(); s.settimeout(3)
+try: s.connect((\"203.0.113.10\",9000)); print(\"CONNECTED\")
+except Exception as e: print(\"blocked:\", type(e).__name__, e)"
+ echo "instance unit deny list includes 203.0.113? $(systemctl cat carlos-instance@.service 2>/dev/null | grep -m1 IPAddressDeny | grep -c 203.0.113) (0 = bridge allowed)"
+ echo "units: $(systemctl list-units "carlos*" --no-legend --plain | awk "{print \$1\"=\"\$3\"/\"\$4}" | paste -sd" ")"
+ '
+done
+echo "================ status/boxes labels -> box"
+for k in ada-letterpress kathleen-juggling noor-marquetry; do
+ runuser -l operator -c "aws s3 cp --quiet s3://harness-deployment/status/boxes/$k.json -" | python3 -c "import json,sys; d=json.load(sys.stdin); print('$k', {x: d.get(x) for x in ('instance_id','instance','region','public_ip','ip','version','build','addr') if x in d}, sorted(d)[:14])"
+done
diff --git a/harness/checks/fix5.sh b/harness/checks/fix5.sh
new file mode 100644
index 000000000..17758f742
--- /dev/null
+++ b/harness/checks/fix5.sh
@@ -0,0 +1,6 @@
+incus exec dennis -- journalctl -u amazon-ssm-agent --no-pager -n 6 -o cat | cut -c1-200
+for b in leo sophie dennis; do incus exec $b -- systemctl reset-failed amazon-ssm-agent.service 2>/dev/null; echo "$b: failed=[$(incus exec $b -- systemctl --failed --no-legend --plain)] state=$(incus exec $b -- systemctl is-system-running)"; done
+for b in leo sophie dennis; do echo "$b zram unit: $(incus exec $b -- systemctl show -p ActiveState,Result,FragmentPath systemd-zram-setup@zram0.service | tr '\n' ' ')"; done
+incus exec leo -- journalctl -u systemd-zram-setup@zram0 --no-pager -o cat | tail -2
+incus exec signer -- sh -c 'for t in jq aws openssl curl; do printf "%s: " $t; command -v $t || echo MISSING; done; aws --version'
+incus exec leo -- sh -c 'aws --version'
diff --git a/harness/checks/run6b.sh b/harness/checks/run6b.sh
new file mode 100644
index 000000000..06e916d39
--- /dev/null
+++ b/harness/checks/run6b.sh
@@ -0,0 +1,9 @@
+set -u
+id_of() { python3 -c "import json,sys; print([k for k,v in json.load(open('/etc/harness/ssm.json'))['instances'].items() if v=='$1'][0])"; }
+L=$(id_of leo); S=$(id_of sophie); D=$(id_of dennis)
+for spec in "leo $L harness-data-ie eu-west-1 " "sophie $S harness-data-ie eu-west-1 " "dennis $D harness-data-uk eu-west-2 uk"; do
+ set -- $spec
+ echo "================ $1: bootstrap-box.sh harness $2 harness-deployment $3 ~/release.pub $4 ${5:-} ($(date -u +%TZ))"
+ runuser -l operator -c "~/infra/scripts/bootstrap-box.sh harness $2 harness-deployment $3 ~/release.pub $4 ${5:-}"
+ echo "exit=$? ($(date -u +%TZ))"
+done
diff --git a/harness/checks/snap-base.sh b/harness/checks/snap-base.sh
new file mode 100644
index 000000000..343c11d9b
--- /dev/null
+++ b/harness/checks/snap-base.sh
@@ -0,0 +1,18 @@
+set -eu
+ALL="infra leo sophie dennis signer"
+incus stop $ALL
+for c in $ALL; do incus snapshot create $c base; done
+incus start infra; sleep 3; incus start leo sophie dennis signer
+# Provisioning is over: boxes reach nothing beyond 203.0.113.0/24 (spec §3.4).
+nft flush set inet harness egress_ports
+touch /etc/harness/egress-closed
+for c in $ALL; do for i in $(seq 1 60); do incus exec $c -- systemctl is-system-running --wait >/dev/null 2>&1 && break; sleep 2; done; done
+for c in $ALL; do echo "$c: $(incus snapshot list $c -f csv -c n | tr '\n' ' ') state=$(incus exec $c -- systemctl is-system-running) failed=[$(incus exec $c -- systemctl --failed --no-legend --plain)]"; done
+echo "egress set: $(nft list set inet harness egress_ports | grep -c elements) element lines (0 = closed)"
+incus exec leo -- python3 -c '
+import socket
+for h,p in (("deb.debian.org",443),("203.0.113.10",9000)):
+ s=socket.socket(); s.settimeout(4)
+ try: s.connect((h,p)); print(h,p,"CONNECTED")
+ except Exception as e: print(h,p,type(e).__name__)
+ s.close()'
diff --git a/harness/checks/snap-boot.sh b/harness/checks/snap-boot.sh
new file mode 100644
index 000000000..85ce9c04d
--- /dev/null
+++ b/harness/checks/snap-boot.sh
@@ -0,0 +1,11 @@
+set -eu
+ALL="infra leo sophie dennis signer"
+for b in leo sophie dennis; do echo "$b label: $(incus exec $b -- sh -c 'journalctl -u carlos-edge --no-pager -o cat | grep -oE "(ada-letterpress|kathleen-juggling|noor-marquetry)" | sort -u | head -1')"; done
+incus stop $ALL
+for c in $ALL; do incus snapshot create $c boot; done
+T0=$(date -u +%s)
+incus start infra; sleep 3; incus start leo sophie dennis signer
+for c in $ALL; do for i in $(seq 1 60); do incus exec $c -- systemctl is-system-running --wait >/dev/null 2>&1 && break; sleep 2; done; done
+for c in $ALL; do echo "$c: snapshots=[$(incus snapshot list $c -f csv -c n | paste -sd,)] state=$(incus exec $c -- systemctl is-system-running) failed=[$(incus exec $c -- systemctl --failed --no-legend --plain)]"; done
+for b in leo sophie dennis; do echo "$b carlos-edge: $(incus exec $b -- systemctl show -p ActiveState,Result,NRestarts carlos-edge | paste -sd' ')"; done
+echo "restart+checks took $(( $(date -u +%s) - T0 ))s"
diff --git a/harness/checks/ssmsmoke.sh b/harness/checks/ssmsmoke.sh
new file mode 100644
index 000000000..d8d75f009
--- /dev/null
+++ b/harness/checks/ssmsmoke.sh
@@ -0,0 +1,18 @@
+runuser -l operator -c '
+set -e
+L=$(python3 -c "import json; print([k for k,v in json.load(open(\"/dev/stdin\"))[\"instances\"].items() if v==\"leo\"][0])" < /etc/harness/ssm.json 2>/dev/null || true)
+' 2>/dev/null
+L=$(python3 -c 'import json; print([k for k,v in json.load(open("/etc/harness/ssm.json"))["instances"].items() if v=="leo"][0])')
+runuser -l operator -c "
+set -e
+CID=\$(aws ssm send-command --instance-ids $L --document-name AWS-RunShellScript --parameters '{\"commands\":[\"id -un; umask; pwd; env | sort\", \"echo HOME=[\$HOME]\"]}' --query Command.CommandId --output text)
+echo cid=\$CID
+for i in 1 2 3 4 5 6 7 8 9 10; do S=\$(aws ssm get-command-invocation --command-id \$CID --instance-id $L --query Status --output text); [ \$S = Success ] || [ \$S = Failed ] && break; sleep 1; done
+aws ssm get-command-invocation --command-id \$CID --instance-id $L --query '[Status,ResponseCode,StandardOutputContent]' --output text
+echo '-- bad instance:'; aws ssm send-command --instance-ids i-0123456789abcdef0 --document-name AWS-RunShellScript --parameters '{\"commands\":[\"true\"]}' 2>&1 | tail -1
+echo '-- parameter store:'; aws ssm put-parameter --name /harness/smoke --type SecureString --value hello --query Version --output text
+aws ssm get-parameter --name /harness/smoke --with-decryption --query Parameter.Value --output text
+aws ssm put-parameter --name /harness/smoke --type SecureString --value again 2>&1 | tail -1
+aws ssm delete-parameter --name /harness/smoke && aws ssm get-parameter --name /harness/smoke 2>&1 | tail -1
+"
+tail -4 /var/log/harness/ssm.jsonl
diff --git a/harness/operator/operator-setup.sh b/harness/operator/operator-setup.sh
new file mode 100644
index 000000000..9b5096285
--- /dev/null
+++ b/harness/operator/operator-setup.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+# Step 6 (host side): the operator shell (B3), the test release key, the SSM
+# stand-in, and the bootstrap object. Run as root on the harness host.
+#
+# CARLOS_URL=<presigned> CONSOLE_URL=<presigned> SUMS="<sha256 carlos> <sha256 console>" operator-setup.sh
+set -euo pipefail
+export DEBIAN_FRONTEND=noninteractive
+H=/root/harness
+
+command -v aws >/dev/null || apt-get install -qy awscli >/dev/null
+
+# The harness build (-tags harness), checked by sha256.
+curl -sf -o $H/bin/carlos.part "$CARLOS_URL"
+curl -sf -o $H/bin/console.part "$CONSOLE_URL"
+set -- $SUMS
+[ "$(sha256sum < $H/bin/carlos.part | cut -d' ' -f1)" = "$1" ] || { echo "carlos sha256 mismatch" >&2; exit 1; }
+[ "$(sha256sum < $H/bin/console.part | cut -d' ' -f1)" = "$2" ] || { echo "console sha256 mismatch" >&2; exit 1; }
+chmod 755 $H/bin/carlos.part $H/bin/console.part
+mv $H/bin/carlos.part $H/bin/carlos; mv $H/bin/console.part $H/bin/console
+install -m 0755 $H/bin/carlos /usr/local/bin/carlos
+
+# The harness build's endpoint fallback, for the operator's own CLI.
+install -d -m 0755 /etc/carlos
+printf 'http://203.0.113.10:9000\n' > /etc/carlos/harness-endpoint
+chmod 0644 /etc/carlos/harness-endpoint
+
+# The test ROOT release key (H5): generated here, on tmpfs, for the host's
+# lifetime. Only its public half leaves /run.
+install -d -m 0700 /run/harness/keys
+if [ ! -s /run/harness/keys/release.env ]; then
+ (umask 077; /usr/local/bin/carlos release-keygen > /run/harness/keys/release.env 2>/dev/null)
+fi
+sed -n 's/^CARLOS_RELEASE_PUBKEY=//p' /run/harness/keys/release.env > $H/release.pub
+chmod 0644 $H/release.pub
+
+# The operator: unprivileged, the harness CLI, awscli, jq, python3. Its AWS
+# environment is exactly the two endpoints, its MinIO keys and the region.
+# Debian ships a system group "operator" (gid 37, no privileges); the user joins it.
+id operator >/dev/null 2>&1 || useradd -m -s /bin/bash -g operator operator
+OH=/home/operator
+install -d -o operator -g operator -m 0700 $OH/.aws $OH/infra $OH/infra/scripts
+python3 - <<'PY'
+import json
+u = json.load(open("/root/harness/minio-users.json"))["operator"]
+creds = "".join(f"[{p}]\naws_access_key_id = {u['access_key']}\naws_secret_access_key = {u['secret_key']}\n\n" for p in ("default", "harness"))
+open("/home/operator/.aws/credentials", "w").write(creds)
+open("/home/operator/.aws/config", "w").write("[default]\nregion = eu-west-1\n\n[profile harness]\nregion = eu-west-1\n")
+PY
+cat > $OH/.harness-env <<'EOF'
+# The operator's AWS environment in the harness (spec §3.9): nothing else.
+export AWS_ENDPOINT_URL_SSM=http://203.0.113.1:8100
+export AWS_ENDPOINT_URL_S3=http://203.0.113.10:9000
+export AWS_REGION=eu-west-1
+export AWS_PROFILE=harness
+EOF
+grep -q harness-env $OH/.profile 2>/dev/null || echo '. "$HOME/.harness-env"' >> $OH/.profile
+grep -q harness-env $OH/.bashrc 2>/dev/null || echo '. "$HOME/.harness-env"' >> $OH/.bashrc
+cp $H/release.pub $OH/release.pub
+for f in bootstrap-box.sh update-platform-binary.sh; do install -m 0755 $H/infra-copies/$f $OH/infra/scripts/$f; done
+chown -R operator:operator $OH/.aws $OH/.harness-env $OH/release.pub $OH/infra
+chmod 0600 $OH/.aws/credentials
+
+# The SSM stand-in: instance ids (the donors' for leo and sophie; harness-only
+# ids for dennis and the signer, which have no donor) mapped to containers.
+install -d -m 0700 /var/lib/harness /var/log/harness/ssm-commands
+D1=$(python3 -c 'import json; print(json.load(open("/etc/harness/donors/donor-1/document"))["instanceId"])')
+D2=$(python3 -c 'import json; print(json.load(open("/etc/harness/donors/donor-2/document"))["instanceId"])')
+cat > /etc/harness/ssm.json <<EOF
+{"instances": {"$D1": "leo", "$D2": "sophie", "i-00000000000000023": "dennis", "i-00000000000000030": "signer"}}
+EOF
+cat > /etc/systemd/system/harness-ssm.service <<'EOF'
+[Unit]
+Description=harness: SSM stand-in (203.0.113.1:8100)
+After=incus.service harness-guard.service
+Wants=incus.service
+
+[Service]
+ExecStart=/usr/bin/python3 /opt/harness/ssm.py
+Restart=always
+RestartSec=2
+
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl daemon-reload
+systemctl enable harness-ssm.service >/dev/null 2>&1
+systemctl restart harness-ssm.service
+for i in $(seq 1 20); do ss -ltn | grep -q '203.0.113.1:8100' && break; sleep 0.5; done
+
+# Seeded for step 6: the release recipe puts the box binary at
+# <deployment bucket>/bootstrap/carlos; here the operator does, from the harness build.
+runuser -l operator -c "aws s3 cp --no-progress /usr/local/bin/carlos s3://harness-deployment/bootstrap/carlos" >/dev/null
+echo "== operator-setup"
+echo "instances: $(cat /etc/harness/ssm.json)"
+echo "release pubkey: $(cut -c1-8 $H/release.pub)... sha256:$(sha256sum < $H/release.pub | cut -c1-12)"
+echo "release.env: $(stat -c '%U:%G %a' /run/harness/keys/release.env) on $(findmnt -no FSTYPE --target /run/harness/keys)"
+echo "carlos: $(/usr/local/bin/carlos version 2>&1 | head -1)"
+echo "harness info (arm64): $(CARLOS_HARNESS_INFO=1 /usr/local/bin/carlos | tr '\n' ' ')"
+systemctl is-active harness-ssm harness-imds
+ss -ltnp | grep -E '8100|8169'
+runuser -l operator -c 'env | grep -E "^AWS_" | sort; aws s3 ls s3://harness-deployment/bootstrap/'
diff --git a/harness/stand-ins/ssm.py b/harness/stand-ins/ssm.py
new file mode 100644
index 000000000..edd436d04
--- /dev/null
+++ b/harness/stand-ins/ssm.py
@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+"""SSM stand-in (spec §3.9). Python 3 standard library only. Runs as root on
+the harness host (it needs the incus socket), bound to 203.0.113.1:8100.
+
+The aws CLI and SDKs reach it through AWS_ENDPOINT_URL_SSM. It speaks the
+JSON 1.1 protocol (X-Amz-Target: AmazonSSM.<Op>) and ignores signatures.
+
+Run Command: SendCommand (AWS-RunShellScript), GetCommandInvocation.
+Parameter Store: GetParameter, GetParameters, PutParameter, DeleteParameter,
+DescribeParameters.
+
+A command runs as root in its container through `incus exec`, with a cleared
+environment: PATH as the agent's unit has it, no HOME (the real agent sets
+none), plus the three variables the harness declares (B4):
+AWS_ENDPOINT_URL_S3, AWS_ENDPOINT_URL_SSM, AWS_REGION=eu-west-1. It runs
+under /bin/sh with umask 022, in a per-command directory shaped like the
+agent's (/var/lib/amazon/ssm/<iid>/document/orchestration/<cid>/...).
+Results keep the real limits: StandardOutputContent cut at 24,000 characters,
+StandardErrorContent at 8,000; Failed on a non-zero exit; TimedOut at the
+execution timeout.
+
+Config /etc/harness/ssm.json: {"instances": {"<instance-id>": "<container>"}}.
+Logs: /var/log/harness/ssm.jsonl (one line per API call: op, ids, parameter
+NAMES, never values) and /var/log/harness/ssm-commands/<cid>/<iid>.{sh,out,err,rc}
+(full transcripts, untruncated).
+"""
+import datetime, http.server, json, os, subprocess, sys, threading, time, uuid
+
+CONFIG = os.environ.get("SSM_CONFIG", "/etc/harness/ssm.json")
+PARAMS = os.environ.get("SSM_PARAMS", "/var/lib/harness/ssm-params.json")
+LOG = os.environ.get("SSM_LOG", "/var/log/harness/ssm.jsonl")
+CMDLOG = os.environ.get("SSM_CMDLOG", "/var/log/harness/ssm-commands")
+BIND = (os.environ.get("SSM_ADDR", "203.0.113.1"), int(os.environ.get("SSM_PORT", "8100")))
+STDOUT_LIMIT, STDERR_LIMIT = 24000, 8000
+AGENT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+INJECT = {"AWS_ENDPOINT_URL_S3": "http://203.0.113.10:9000",
+ "AWS_ENDPOINT_URL_SSM": "http://203.0.113.1:8100",
+ "AWS_REGION": "eu-west-1"}
+ACCOUNT, REGION = "640012953448", "eu-west-1"
+
+lock = threading.Lock()
+invocations = {} # (cid, iid) -> dict
+
+
+class APIError(Exception):
+ def __init__(self, typ, msg, status=400):
+ super().__init__(msg)
+ self.typ, self.msg, self.status = typ, msg, status
+
+
+def now_epoch():
+ return round(time.time(), 3)
+
+
+def log(rec):
+ rec["t"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+ try:
+ with open(LOG, "a") as f:
+ f.write(json.dumps(rec) + "\n")
+ except OSError:
+ pass
+
+
+def instances():
+ with open(CONFIG) as f:
+ return json.load(f)["instances"]
+
+
+# ---- Run Command -------------------------------------------------------------
+
+def run_invocation(cid, iid, container, commands, timeout):
+ key = (cid, iid)
+ wd = f"/var/lib/amazon/ssm/{iid}/document/orchestration/{cid}/awsrunShellScript/0.awsrunShellScript"
+ script = "\n".join(commands) + "\n"
+ os.makedirs(os.path.join(CMDLOG, cid), exist_ok=True)
+ base = os.path.join(CMDLOG, cid, iid)
+ with open(base + ".sh", "w") as f:
+ f.write(script)
+ with lock:
+ inv = invocations[key]
+ inv["Status"] = inv["StatusDetails"] = "InProgress"
+ inv["ExecutionStartDateTime"] = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")[:-4] + "Z"
+ try:
+ # Write the script into the container, then run it under a cleared env.
+ subprocess.run(["incus", "exec", container, "--", "mkdir", "-p", wd], check=True, capture_output=True, timeout=60)
+ subprocess.run(["incus", "file", "push", base + ".sh", f"{container}{wd}/_script.sh", "--mode", "0700", "-q"],
+ check=True, capture_output=True, timeout=60)
+ env = ["env", "-i", f"PATH={AGENT_PATH}"] + [f"{k}={v}" for k, v in INJECT.items()]
+ argv = ["incus", "exec", container, "--cwd", wd, "--", *env,
+ "/bin/sh", "-c", "umask 022; exec /bin/sh _script.sh"]
+ try:
+ p = subprocess.run(argv, capture_output=True, timeout=timeout)
+ out, err, rc, status = p.stdout, p.stderr, p.returncode, ("Success" if p.returncode == 0 else "Failed")
+ except subprocess.TimeoutExpired as e:
+ out, err, rc, status = e.stdout or b"", e.stderr or b"", -1, "TimedOut"
+ subprocess.run(["incus", "exec", container, "--", "pkill", "-f", f"{wd}"], capture_output=True)
+ except Exception as e: # the stand-in's own failure, reported as the agent would: Failed
+ out, err, rc, status = b"", f"harness ssm stand-in: {e}".encode(), 1, "Failed"
+ for ext, data in (("out", out), ("err", err), ("rc", str(rc).encode())):
+ with open(f"{base}.{ext}", "wb") as f:
+ f.write(data)
+ so, se = out.decode("utf-8", "replace"), err.decode("utf-8", "replace")
+ with lock:
+ inv = invocations[key]
+ inv.update({"Status": status, "StatusDetails": status, "ResponseCode": rc,
+ "StandardOutputContent": so[:STDOUT_LIMIT], "StandardErrorContent": se[:STDERR_LIMIT],
+ "ExecutionEndDateTime": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")[:-4] + "Z"})
+ log({"op": "CommandFinished", "cid": cid, "iid": iid, "container": container, "status": status, "rc": rc,
+ "stdout_bytes": len(out), "stderr_bytes": len(err)})
+
+
+def send_command(req):
+ doc = req.get("DocumentName")
+ if doc != "AWS-RunShellScript":
+ raise APIError("InvalidDocument", f"harness stand-in serves AWS-RunShellScript only, not {doc}")
+ ids = req.get("InstanceIds") or []
+ if not ids or req.get("Targets"):
+ raise APIError("InvalidInstanceId", "InstanceIds required (Targets are not served)")
+ inst = instances()
+ for iid in ids:
+ if iid not in inst:
+ raise APIError("InvalidInstanceId", f"Instances [[{iid}]] not in a valid state for account {ACCOUNT}")
+ params = req.get("Parameters") or {}
+ commands = params.get("commands") or []
+ if not commands:
+ raise APIError("InvalidParameters", "commands is required")
+ timeout = int((params.get("executionTimeout") or ["3600"])[0])
+ cid = str(uuid.uuid4())
+ t = now_epoch()
+ for iid in ids:
+ with lock:
+ invocations[(cid, iid)] = {"CommandId": cid, "InstanceId": iid, "Comment": req.get("Comment", ""),
+ "DocumentName": doc, "DocumentVersion": "$DEFAULT",
+ "PluginName": "aws:runShellScript", "ResponseCode": -1,
+ "Status": "Pending", "StatusDetails": "Pending",
+ "StandardOutputContent": "", "StandardErrorContent": "",
+ "StandardOutputUrl": "", "StandardErrorUrl": "",
+ "CloudWatchOutputConfig": {"CloudWatchLogGroupName": "", "CloudWatchOutputEnabled": False}}
+ threading.Thread(target=run_invocation, args=(cid, iid, inst[iid], commands, timeout), daemon=True).start()
+ log({"op": "SendCommand", "cid": cid, "iids": ids, "containers": [inst[i] for i in ids],
+ "lines": len(commands), "timeout": timeout})
+ return {"Command": {"CommandId": cid, "DocumentName": doc, "DocumentVersion": "$DEFAULT",
+ "Comment": req.get("Comment", ""), "InstanceIds": ids, "Targets": [],
+ "Parameters": params, "Status": "Pending", "StatusDetails": "Pending",
+ "RequestedDateTime": t, "ExpiresAfter": t + timeout + 60,
+ "TargetCount": len(ids), "CompletedCount": 0, "ErrorCount": 0,
+ "TimeoutSeconds": req.get("TimeoutSeconds", timeout)}}
+
+
+def get_command_invocation(req):
+ key = (req.get("CommandId"), req.get("InstanceId"))
+ with lock:
+ inv = invocations.get(key)
+ inv = dict(inv) if inv else None
+ if inv is None:
+ raise APIError("InvocationDoesNotExist", "")
+ log({"op": "GetCommandInvocation", "cid": key[0], "iid": key[1], "status": inv["Status"]})
+ return inv
+
+
+# ---- Parameter Store -----------------------------------------------------------
+
+def load_params():
+ try:
+ with open(PARAMS) as f:
+ return json.load(f)
+ except FileNotFoundError:
+ return {}
+
+
+def save_params(p):
+ os.makedirs(os.path.dirname(PARAMS), exist_ok=True)
+ tmp = PARAMS + ".tmp"
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+ with os.fdopen(fd, "w") as f:
+ json.dump(p, f)
+ os.replace(tmp, PARAMS)
+
+
+def arn(name):
+ return f"arn:aws:ssm:{REGION}:{ACCOUNT}:parameter{name if name.startswith('/') else '/' + name}"
+
+
+def shape(name, p, value=True):
+ out = {"Name": name, "Type": p["Type"], "Version": p["Version"], "LastModifiedDate": p["LastModifiedDate"],
+ "ARN": arn(name), "DataType": "text"}
+ if value:
+ out["Value"] = p["Value"]
+ return out
+
+
+def get_parameter(req):
+ name = req.get("Name", "")
+ with lock:
+ p = load_params().get(name)
+ log({"op": "GetParameter", "name": name, "decrypt": bool(req.get("WithDecryption")), "found": p is not None})
+ if p is None:
+ raise APIError("ParameterNotFound", "")
+ return {"Parameter": shape(name, p)}
+
+
+def get_parameters(req):
+ names = req.get("Names") or []
+ with lock:
+ ps = load_params()
+ log({"op": "GetParameters", "names": names, "decrypt": bool(req.get("WithDecryption"))})
+ return {"Parameters": [shape(n, ps[n]) for n in names if n in ps], "InvalidParameters": [n for n in names if n not in ps]}
+
+
+def put_parameter(req):
+ name, value = req.get("Name", ""), req.get("Value")
+ if not name or value is None:
+ raise APIError("ValidationException", "Name and Value are required")
+ with lock:
+ ps = load_params()
+ old = ps.get(name)
+ if old and not req.get("Overwrite"):
+ log({"op": "PutParameter", "name": name, "refused": "exists"})
+ raise APIError("ParameterAlreadyExists", "The parameter already exists. To overwrite this value, set the overwrite option in the request to true.")
+ ver = (old["Version"] + 1) if old else 1
+ ps[name] = {"Type": req.get("Type") or (old or {}).get("Type", "String"), "Value": value, "Version": ver,
+ "LastModifiedDate": now_epoch()}
+ save_params(ps)
+ log({"op": "PutParameter", "name": name, "type": ps[name]["Type"], "version": ver})
+ return {"Version": ver, "Tier": "Standard"}
+
+
+def delete_parameter(req):
+ name = req.get("Name", "")
+ with lock:
+ ps = load_params()
+ if name not in ps:
+ log({"op": "DeleteParameter", "name": name, "found": False})
+ raise APIError("ParameterNotFound", "")
+ del ps[name]
+ save_params(ps)
+ log({"op": "DeleteParameter", "name": name})
+ return {}
+
+
+def describe_parameters(req):
+ with lock:
+ ps = load_params()
+ names = sorted(ps)
+ for f in req.get("ParameterFilters") or []:
+ key, opt, vals = f.get("Key"), f.get("Option", "Equals"), f.get("Values") or []
+ if key == "Name":
+ if opt == "BeginsWith":
+ names = [n for n in names if any(n.startswith(v) for v in vals)]
+ else:
+ names = [n for n in names if n in vals]
+ elif key == "Path":
+ names = [n for n in names if any(n.startswith(v.rstrip("/") + "/") for v in vals)]
+ for f in req.get("Filters") or []:
+ if f.get("Key") == "Name":
+ names = [n for n in names if any(n.startswith(v) for v in f.get("Values") or [])]
+ log({"op": "DescribeParameters", "count": len(names)})
+ return {"Parameters": [shape(n, ps[n], value=False) for n in names]}
+
+
+OPS = {"SendCommand": send_command, "GetCommandInvocation": get_command_invocation,
+ "GetParameter": get_parameter, "GetParameters": get_parameters, "PutParameter": put_parameter,
+ "DeleteParameter": delete_parameter, "DescribeParameters": describe_parameters}
+
+
+class H(http.server.BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def send_json(self, status, obj):
+ body = json.dumps(obj).encode()
+ self.send_response(status)
+ self.send_header("Content-Type", "application/x-amz-json-1.1")
+ self.send_header("Content-Length", str(len(body)))
+ self.send_header("x-amzn-RequestId", str(uuid.uuid4()))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_POST(self):
+ n = int(self.headers.get("Content-Length") or 0)
+ raw = self.rfile.read(n) if n else b"{}"
+ target = self.headers.get("X-Amz-Target", "")
+ op = target.split(".", 1)[1] if target.startswith("AmazonSSM.") else ""
+ fn = OPS.get(op)
+ if fn is None:
+ log({"op": op or target, "refused": "not served"})
+ return self.send_json(400, {"__type": "UnknownOperationException", "message": f"harness stand-in does not serve {target}"})
+ try:
+ req = json.loads(raw or b"{}")
+ return self.send_json(200, fn(req))
+ except APIError as e:
+ return self.send_json(e.status, {"__type": e.typ, "message": e.msg})
+ except Exception as e:
+ log({"op": op, "error": repr(e)})
+ return self.send_json(500, {"__type": "InternalServerError", "message": str(e)})
+
+ def log_message(self, *a):
+ pass
+
+
+def main():
+ os.makedirs(os.path.dirname(LOG), exist_ok=True)
+ os.makedirs(CMDLOG, mode=0o700, exist_ok=True)
+ instances()
+ srv = http.server.ThreadingHTTPServer(BIND, H)
+ print(f"ssm stand-in on {BIND[0]}:{BIND[1]}", flush=True)
+ srv.serve_forever()
+
+
+if __name__ == "__main__":
+ sys.exit(main())