carlos / platform Public

Sidecar isolation spec, revision 2: answers Astra round 1

All nine round 1 findings accepted (R1-4 in part). New D19 orders a
worker's starts against its stops (the #292 fence, widened); D11 becomes a
workers protocol floor that install, rollback and the recovery unit obey;
D9 makes lease replication every worker's only replication, fixing a
double-replication defect live on main; D8 narrows the no-database move;
D15 judges registry inventory; D17 uses the reconciler's placement set;
D18 needs a complete process census. §12 is the disposition. Round 1
findings renamed to the brief's -R1 convention.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 730ba093dffd58338aac556737d5b0fbbe994fd9 parent 2a2517d
2 files changed, +593 −184
  • docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md +593 −184
  • docs/superpowers/specs/{CODEX-FINDINGS-SIDECAR-ISOLATION.md => CODEX-FINDINGS-SIDECAR-ISOLATION-R1.md} +0 −0

CI failed — run details

CI log
=== gate ===
.amadan/test-gate.sh
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/superfly/ltx v0.5.1
go: downloading golang.org/x/net v0.56.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading golang.org/x/crypto v0.53.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading golang.org/x/sys v0.46.0
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.0.1
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading golang.org/x/sync v0.21.0
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== launchctx (2 s)
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]
=== vet (43 s)
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading golang.org/x/tools v0.47.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading golang.org/x/text v0.38.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading golang.org/x/mod v0.37.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== gofmt (0 s)
gofmt -l .: clean
=== rest (190 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.936s
ok  	github.com/carlosframework/platform/cmd/moveprobe	12.209s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.837s
ok  	github.com/carlosframework/platform/internal/activator	59.464s
ok  	github.com/carlosframework/platform/internal/agentsock	0.035s
ok  	github.com/carlosframework/platform/internal/alerting	0.007s
ok  	github.com/carlosframework/platform/internal/analytics	0.022s
ok  	github.com/carlosframework/platform/internal/appconfig	3.287s
ok  	github.com/carlosframework/platform/internal/apperr	0.178s
ok  	github.com/carlosframework/platform/internal/applog	0.355s
ok  	github.com/carlosframework/platform/internal/awsinv	0.012s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.010s
ok  	github.com/carlosframework/platform/internal/boxid	1.804s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.052s
ok  	github.com/carlosframework/platform/internal/caps	0.036s
ok  	github.com/carlosframework/platform/internal/certwarm	0.008s
ok  	github.com/carlosframework/platform/internal/cgroup	0.928s
ok  	github.com/carlosframework/platform/internal/changelog	0.839s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.711s
ok  	github.com/carlosframework/platform/internal/clamd	0.094s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	8.135s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.042s
ok  	github.com/carlosframework/platform/internal/dbquery	0.011s
ok  	github.com/carlosframework/platform/internal/dbreads	2.143s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.006s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.006s
ok  	github.com/carlosframework/platform/internal/docsite	0.036s
ok  	github.com/carlosframework/platform/internal/domains	2.997s
ok  	github.com/carlosframework/platform/internal/doorkey	0.016s
ok  	github.com/carlosframework/platform/internal/economics	0.006s
ok  	github.com/carlosframework/platform/internal/edge	49.026s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.010s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.171s
ok  	github.com/carlosframework/platform/internal/gate	1.704s
ok  	github.com/carlosframework/platform/internal/genselect	0.017s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.038s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.221s
ok  	github.com/carlosframework/platform/internal/host	142.417s
ok  	github.com/carlosframework/platform/internal/hostactors	6.571s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.018s
ok  	github.com/carlosframework/platform/internal/hostbox	3.284s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.787s
ok  	github.com/carlosframework/platform/internal/hostenv	0.018s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.018s
ok  	github.com/carlosframework/platform/internal/hostexec	5.576s
ok  	github.com/carlosframework/platform/internal/hostfence	0.050s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.422s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.026s
ok  	github.com/carlosframework/platform/internal/hostguard	1.051s
ok  	github.com/carlosframework/platform/internal/hostops	2.014s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.017s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.016s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.609s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.151s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.017s
ok  	github.com/carlosframework/platform/internal/infraview	0.018s
ok  	github.com/carlosframework/platform/internal/instancectl	5.274s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.857s
ok  	github.com/carlosframework/platform/internal/instancefs	0.021s
ok  	github.com/carlosframework/platform/internal/instancekey	0.017s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.026s
ok  	github.com/carlosframework/platform/internal/layout	1.794s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.020s
ok  	github.com/carlosframework/platform/internal/lease	10.105s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.539s
ok  	github.com/carlosframework/platform/internal/ledger	0.017s
ok  	github.com/carlosframework/platform/internal/mail	0.241s
ok  	github.com/carlosframework/platform/internal/maintenance	0.098s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.553s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.108s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	29.051s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.992s
ok  	github.com/carlosframework/platform/internal/objstore	0.263s
ok  	github.com/carlosframework/platform/internal/ops	0.188s
ok  	github.com/carlosframework/platform/internal/park	0.269s
ok  	github.com/carlosframework/platform/internal/plans	0.010s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	50.141s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.718s
ok  	github.com/carlosframework/platform/internal/relay	7.206s
ok  	github.com/carlosframework/platform/internal/release	29.435s
--- FAIL: TestAPointerClearRequeuesBeforeTheNextChannelIsRead (0.00s)
    manager_test.go:1185: the cleared build was requeued only after every channel was read
FAIL
FAIL	github.com/carlosframework/platform/internal/rollout	4.122s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.067s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.563s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.834s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/engines	3.128s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.241s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.877s
ok  	github.com/carlosframework/platform/internal/status	0.094s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.311s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.005s
ok  	github.com/carlosframework/platform/internal/termfence	0.009s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.007s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.006s
ok  	github.com/carlosframework/platform/internal/vet	1.758s
FAIL
=== internal_console@0 (83 s)
ok  	github.com/carlosframework/platform/internal/console	81.554s
=== internal_console@1 (48 s)
ok  	github.com/carlosframework/platform/internal/console	46.657s
=== internal_console@2 (43 s)
ok  	github.com/carlosframework/platform/internal/console	41.152s
=== internal_console@3 (47 s)
ok  	github.com/carlosframework/platform/internal/console	44.869s
=== internal_console@4 (47 s)
ok  	github.com/carlosframework/platform/internal/console	45.604s
=== internal_console@5 (48 s)
ok  	github.com/carlosframework/platform/internal/console	45.771s
=== internal_console@6 (44 s)
ok  	github.com/carlosframework/platform/internal/console	41.813s
=== internal_console@7 (50 s)
ok  	github.com/carlosframework/platform/internal/console	48.232s
=== root@0 (113 s)
ok  	github.com/carlosframework/platform	111.163s
=== root@1 (84 s)
ok  	github.com/carlosframework/platform	82.459s
=== root@2 (94 s)
ok  	github.com/carlosframework/platform	92.127s

FAILED: rest
make: *** [Makefile:19: test] Error 1

amadan: step gate failed: exit status 2

amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md b/docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md
index 83da7c395..aa4f7c01c 100644
--- a/docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md
+++ b/docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md
@@ -1,11 +1,25 @@
# Sidecar isolation: native sidecars on the nested tier, and no flat exec by default
-**Date:** 2026-10-24 · **Status:** draft, revision 1, for an independent
+**Date:** 2026-10-24 · **Status:** draft, revision 2, for an independent
review (brief: `CODEX-REVIEW-BRIEF-SIDECAR-ISOLATION.md`) and then Paul ·
**Requested by:** security review finding B1, confirmed live 2026-10-24 ·
**Main at:** fef28dc5 (every `file:line` below is on that commit unless it
says otherwise)
+**Revision 2** answers Astra's round 1 (`CODEX-FINDINGS-SIDECAR-ISOLATION-R1.md`,
+reviewed at 1c029ee5, verdict reject: four P1s, five P2s). Every finding's
+citations were checked against main and held. Each accepted finding is
+marked **(R1-n)** where the text answers it; §12 is the disposition table,
+with the one part declined and why. The largest changes: worker starts are
+ordered against stops like the run unit's (D19, new); downgrade is held by a
+workers protocol floor that the recovery unit also obeys (D11, rewritten);
+every worker replicates through its lease only, which also fixes a
+double-replication defect live on main today (D9, rewritten); the move of a
+row with no database is narrowed (D8); the upgrade guard judges inventory,
+not spawn history (D15); the create check uses the reconciler's own
+placement set (D17); and fence retirement needs a complete process census
+(D18).
+
**Builds on:** the isolated-exec design
(`2026-09-11-isolated-exec-design.md`, revision 17; "the nested design"
below), its plan 4 amendments (`../plans/2026-09-25-isolated-exec-4-layout-amendments.md`,
@@ -45,11 +59,12 @@ one. This spec closes that in four moves:
time, from what the target region's boxes report, instead of writing a
record that lands flat or never runs (§5).
4. **The interim fence retires** on a box once nothing flat can run there:
- the policy refuses, the allowlist is empty, no flat child cgroup is
- populated, and the nested probe's metadata line is enforced (§6).
+ the policy refuses, the allowlist is empty, a complete census of the
+ edge's process tree finds nothing but the agent and its own children,
+ and the nested probe's metadata line is enforced (§6).
§7 is the threat model, step by step. §8 is the rollout, dev first. §11
-lists three questions that are Paul's.
+lists the questions that are Paul's. §12 is the review disposition.
The decisions, for scanning (each is argued where it is made):
@@ -60,19 +75,20 @@ The decisions, for scanning (each is argued where it is made):
| D3 | A nested worker's agent socket is a per-host listener in its own run directory | §3.3 |
| D4 | Every worker is told its database path (`CARLOS_SIDECAR_DB`), flat and nested | §3.4 |
| D5 | `StartWorkerStaged` dispatches on layout; the nested arm passes the staged build to root | §3.5 |
-| D6 | `unitProc` answers `Crashed()` from systemd's result, never from the exit record | §3.6 |
+| D19 | A worker's starts are ordered against its stops, and a failed wake keeps the lease until the worker unit is proven gone (rev 2) | §3.5.1 |
+| D6 | A nested worker's crash is any unsuccessful result its own invocation reports; the template marks no signal exit successful (rev 2) | §3.6 |
| D7 | The sidecar reconciler mints nested exactly when the instance reconciler does | §3.7 |
-| D8 | The layout pass moves worker rows; a row with nothing on this box moves by row alone | §3.8 |
-| D9 | An always-on worker's box-wide replica starts a new lineage at its nested path | §3.9 |
+| D8 | The layout pass moves worker rows with their full extras inventory; a row whose database and its companions are absent on both sides moves without it (rev 2) | §3.8 |
+| D9 | Every worker replicates through its lease only; the box-wide replicator and the restore drills exclude workers (rev 2) | §3.9 |
| D10 | A failed nested gate never reopens flat for a worker | §3.10 |
-| D11 | Downgrade below the worker layout is refused by unit set on the box and by floor at the console | §3.11 |
+| D11 | A workers protocol floor, armed before any worker moves, holds every install, rollback and recovery selection (rev 2) | §3.11 |
| D12 | `CARLOS_FLAT_EXEC=refuse\|report` plus an exact-host allowlist, read from `host.env` | §4.1 |
| D13 | The refusal sits beside the claim refusal in both spawn seams, with its own error | §4.2 |
-| D14 | Refusals are reported per host with closed reason words, like config refusals | §4.3 |
-| D15 | Ship `report`; operators switch boxes to `refuse`; the next release defaults to `refuse` behind a console guard | §4.4 |
+| D14 | Refusals and a registry inventory of what `refuse` would stop are reported per host (rev 2) | §4.3 |
+| D15 | Ship `report`; operators switch boxes; N+1 changes a default only on boxes where the change stops nothing, judged from inventory (rev 2) | §4.4 |
| D16 | A refused managed row nudges the layout pass for that host | §4.5 |
-| D17 | Creation is refused unless every fresh box in scope reports nested workers ready | §5 |
-| D18 | The fence retires per box on four observable conditions; an allowlisted host keeps it | §6 |
+| D17 | Creation is refused unless the reconciler's own placement set has a fresh box and every one reports nested workers runnable (rev 2) | §5 |
+| D18 | The fence retires per box on four observable conditions, the third a complete process census; an allowlisted host keeps it (rev 2) | §6 |
---
@@ -177,24 +193,26 @@ As uid `carlos` in the host namespace, with the edge's own view of the box:
- loopback listeners, the VPC and the instance metadata service, and
through it the box role.
-### 1.6 The interim fence, and why it is weaker than it looks
+### 1.6 The interim fence, and what it does not cover
`carloku-infrastructure scripts/fence-flat-imds.sh` (not in this
-repository; described from the finding) drops metadata traffic from the
-edge's cgroup except from the agent's `supervise` leaf and one exempt
-operator app. Two limits, the second not in the finding:
+repository; described from the finding and the round 1 review) drops
+metadata traffic from the edge's cgroup except from the agent's
+`supervise` leaf. Two limits:
- It does nothing for the `/data` reads and writes or the cgroup writes
above.
-- **If it keys on cgroup path, a flat child can step out of it.** Cgroup v2
- lets a process move a pid between two cgroups when it can write
- `cgroup.procs` in both and in their common ancestor. `Delegate=` chowns
- the edge unit's cgroup and its `cgroup.procs` to `carlos`, and every
- directory below it is made by the agent as `carlos`. A flat child can
- therefore write its own pid into `supervise/cgroup.procs` (or the exempt
- app's cgroup) and open a socket there. The plan's first box task
- verifies this on a dev box (§9, box level) before anyone relies on the
- fence for anything.
+- **It keys on cgroup path**, so it holds only while a flat child cannot
+ move its pid into an exempt cgroup. Revision 1 argued that it could
+ (cgroup v2 allows the move to a writer of `cgroup.procs` at both ends and
+ their common ancestor, and `Delegate=` hands the edge's subtree to
+ `carlos`). The round 1 review reports that the fence as hardened since
+ makes `supervise/cgroup.procs` unwritable by `carlos` and exempts nothing
+ else, which closes that move (the kernel's destination and
+ common-ancestor checks; systemd's delegation ownership). This spec does
+ not rely on the fence either way. The plan's first box task still records
+ the attempt on a dev box (§9, box level), because the fence is the only
+ control between now and step 7 of §8.2.
The repository already reads the box's metadata gate once:
`nftGateUIDs` in the root adopt pass (`internal/host/adopt_advice.go:179-199`)
@@ -232,9 +250,21 @@ native sidecar has ever started through it.
(`backend_exec.go:3049-3130`; `workerEnv`, `internal/activator/activator.go:3321-3328`).
A worker that opens a database must be computing `/data/<host>.db`
itself, which stops working the moment its row is nested (§3.4).
-- The fence may be bypassable by cgroup migration (§1.6).
+- The fence keys on cgroup path; whether a flat child can leave its cgroup
+ is the fence's own hardening, not this spec's (§1.6).
- The flat tier is not only a cross-tenant read of flat siblings: it is a
write into nested siblings (§1.5).
+- **Every always-on worker is replicated twice today (R1-3).** A worker's
+ wake trades its lease for running replication unconditionally
+ (`activator.go:2602`), and always-on supervision keeps that lease and
+ replicator for as long as the row exists (`activator.go:4599-4627`). The
+ box-wide generator excludes only hibernating rows (`litestream.go:356-357`),
+ so it also replicates the same file, under its path key. The same file
+ warns that two replicators on one WAL fight over checkpointing and can
+ corrupt the replica (`litestream.go:230-234`). The restore drill's
+ census has the same `Hibernate` test (`internal/ops/hibernate_verify.go:45-60`).
+ This is a live defect, independent of isolation; D9 fixes it and §11 Q4
+ asks whether to land that part first.
---
@@ -268,6 +298,18 @@ Reopened, each because this problem forces it:
at `internal/layout/pass.go:347-355`). It is code, not a spec ruling, but
the refusal policy turns it from a cosmetic skip into an outage, so it
is named here (D8).
+- **#292's scope, "for the original run unit only"**
+ (`internal/instancectl/legacystart.go:11-34`). The ordered start and stop
+ was the owner's ruling for the run unit, the one role whose start the
+ agent abandoned with the lease at stake. A nested worker is the second
+ (R1-1). Widened to the worker role, unchanged in mechanism (D19).
+- **The box-wide replicator's `Hibernate` test** (`litestream.go:356-357`)
+ and the restore census that mirrors it (`hibernate_verify.go:45-60`).
+ Both meant "the activator owns this file's replication", and for workers
+ they have been wrong since always-on workers existed (R1-3, D9).
+- **Revision 1's own D1 "four lines and nothing else"**: the worker
+ template also drops the signal statuses from `SuccessExitStatus=` (R1-7,
+ D6).
Not reopened, though it looks as if it might need to be: plan 4 R14's
"defer an awake host". Plan 5 already bounded it with an awake grace
@@ -284,10 +326,11 @@ Not reopened, though it looks as if it might need to be: plan 4 R14's
`carlos-instance-worker@.service`. The `sidecar` role stays the legacy
co-spawn's, untouched.
-The template is the sidecar template with four lines changed and nothing
+The template is the sidecar template with five lines changed and nothing
else: `Description=` (`(worker)`), `ExecStart=/opt/carlos/carlos
instance-exec worker %I`, `ExecStopPost=/opt/carlos/carlos instance-exit
-worker %I` and `StandardOutput=file:/run/carlos/logs/%I.worker.sock`. It
+worker %I`, `StandardOutput=file:/run/carlos/logs/%I.worker.sock`, and no
+`SuccessExitStatus=` line (D6, R1-7). It
binds the same executable sources as the other original templates
(`/run/carlos-control/exe/%I/app` and `live`): a worker's host has no run
or sidecar unit of its own, and its seed (which `Seed` already dispatches
@@ -320,7 +363,8 @@ Root-side changes, all small: `names.go` gains `RoleWorker`,
`UnitPrefixWorker = "carlos-instance-worker@"`, its `prefixFor`,
`ParseRole` and `ParseUnitName` cases; the control service admits `worker`
on `/start`, `/stop`, `/reset` and `/sweep` with the run role's tier rule
-(always set the slice's tier: a worker's slice is its own) and validates a
+(always set the slice's tier: a worker's slice is its own) and the run
+role's start fence (D19), and validates a
named staged build with `validateStagedExe` as it does today
(`internal/instancectl/stagedexe.go:45-78`, whose registry view is
Kind-agnostic, `internal/host/claimstop_run.go:121-141`);
@@ -328,6 +372,11 @@ Kind-agnostic, `internal/host/claimstop_run.go:121-141`);
(`internal/instanceexec/exec.go:44-53`). No new verb, no new socket, no new
root relay. The census implications are in §8.3.
+A worker has one unit per host and no generation units: its replacement
+(`ReplaceWorker`, `internal/activator/workerswap.go:13-40`) and its respawn
+are stop-then-start on the same entry, so `/start` for the worker role
+always has `gen == 0`.
+
### 3.2 Decision D2: a worker row's paths, and `Classify`
**Decision.** A nested worker row has `DBPath = NestedDBPath(dataDir, host)`
@@ -437,35 +486,100 @@ looks for descendants in the right place.
`TestKillHostProvesANestedLayoutWorkersHostCgroup`
(`worker_gen_test.go:50-63`) is rewritten: a nested worker's cgroup is its
-unit's, and `KillHost` proves the slice, as for every other nested role.
+unit's, and `KillHost` alone is not the proof for a nested worker (D19).
+
+#### 3.5.1 Decision D19: a worker's start is ordered against its stop (R1-1)
+
+The failure: a nested worker's `/start` times out on the agent's side while
+the control service's handler is still running. The failed wake's cleanup
+sees an empty slice and gives the lease back; another box takes it; the
+late handler then submits the first box's start. Two workers own one
+database. This is exactly the race #292 closed for the run unit
+(`legacystart.go:11-34`), and the root fence today covers `RoleRun` only
+(`server.go:470-480`). On the agent side, a failed wake keeps its lease
+only for an unproven generation or a legacy start marked uncertain
+(`activator.go:2543-2566`), and the proof is hard-coded to the run unit
+(`LegacyRunGone`, `backend_exec.go:2595-2640`).
+
+**Decision.** The worker role gets the run unit's ordering, end to end:
+
+- **Root.** `handleStart` registers a worker start as an attempt
+ (`beginLegacyStart`) exactly as it does a run start; `/stop` of the
+ worker role fences the host for that role, cancels and joins the attempts
+ in flight within `legacyJoinBudget`, applies the accepted-connection
+ watermark, and only then stops the unit. The fence is keyed by host and
+ role, so a worker's stop never fences an instance's start or the
+ reverse. One that cannot be joined answers an error, and the agent keeps
+ the lease, as for the run unit.
+- **Agent.** The nested worker arm marks a start error uncertain the way
+ the run arm does (`legacyStartUncertain`, read by `legacyStartMayRun`,
+ `containment.go:74-79`) for
+ every outcome short of an answered refusal that ran nothing
+ (`launchRefused`, `containment.go:55-71`). The failed
+ wake's cleanup then takes the same branch as an uncertain legacy start
+ (`activator.go:2552-2554`): the prover, generalised from `LegacyRunGone`
+ to `RoleGone(ctx, rt, role)`, stops the worker unit through `/stop`
+ (which, once answered, means no earlier request can still submit a
+ start) and then requires the unit `inactive` or `failed` with its cgroup
+ empty or gone. Only that proof releases the lease. Anything short of it
+ keeps the lease, and the host stays this box's until a later stop
+ proves it.
+- **Respawns.** `respawnWorkerLocked` and `restartWorkerLocked` stop then
+ start on an entry that keeps its lease throughout, so a respawn's
+ uncertain start needs no lease decision; the next stop of that entry, by
+ any path, goes through the fenced `/stop`, which orders it.
+- `KillHost` inspecting the slice alone is not the proof, and the rewrite
+ of `TestKillHostProvesANestedLayoutWorkersHostCgroup` says so.
### 3.6 Decision D6: crash versus finish under a unit
The activator tells a hibernating worker's crash (restart with backoff)
from its finish (park) by `procCrashed`, which asks the proc's optional
`Crashed()` and defaults to "not a crash" (`activator.go:404-418`).
-`childProc` answers from `ProcessState` (`backend_exec.go:3329-3335`).
-`unitProc` has no `Crashed` (`internal/activator/unitproc.go:87-203`), so
-every nested worker crash would read as a finish and park.
-
-**Decision.** `unitProc.Crashed()` is true only after the observer's two
-termination facts hold (nested design §6.5) **and** systemd reported the
-proc's own invocation as `failed` with `Result` other than `success`.
-The advisory exit record never decides it (§3.3 of the nested design: the
-record is the tenant's claim). The template's `SuccessExitStatus=129 130 141
-143` (the trampoline's signal convention,
-`carlos-instance-sidecar@.service:41-46`) means a stop by SIGTERM is a
-finish, as for a flat child stopped the same way. An `inactive` unit whose
-invocation was garbage-collected reads as "not a crash", the safe default
-`procCrashed` already documents; a failed unit is not collected until it
-is reset or started again, so a real crash keeps its `Result`.
+`childProc` answers from `ProcessState`: any unsuccessful exit, a signal
+death included, is a crash (`backend_exec.go:3329-3335`). `unitProc` has
+no `Crashed` (`internal/activator/unitproc.go:87-203`), so every nested
+worker crash would read as a finish and park.
+
+Revision 1 kept the sidecar template's `SuccessExitStatus=129 130 141 143`
+(`carlos-instance-sidecar@.service:41-46`). The trampoline reports a
+tenant's signal death as `128 + signal` (`initExitCode`,
+`internal/instanceexec/init_linux.go:135-141`), so a worker that died of
+SIGTERM or SIGPIPE on its own would have read as success and parked
+forever (R1-7). That is not the flat contract.
+
+**Decision.**
+
+- The worker template has **no** `SuccessExitStatus=` line. Exit 0 is the
+ only success, as for a flat child.
+- `unitProc.Crashed()` is true only after the observer's two termination
+ facts hold (nested design §6.5) **and** systemd reports the proc's own
+ invocation with `Result` other than `success`. The advisory exit record
+ never decides it (§3.3 of the nested design: the record is the tenant's
+ claim).
+- A stop the agent asked for is told apart by the agent, not by the exit
+ status. Every reader of `procCrashed` reads a child that exited on its
+ own: the sweep's unexpected-exit arm (`activator.go:4592-4627`), the
+ clean-park mark (`:4504`), and the two settle windows
+ (`wakeplan.go:579`, `workerswap.go:186`). None of them is reached for an
+ entry the agent is stopping (`beginStopLocked`). A requested stop of a worker may leave
+ its unit `failed` (the trampoline answers systemd's SIGTERM with 143);
+ the termination facts and D19's proof already accept `failed`, and the
+ next start of a failed unit needs no reset.
+- An `inactive` unit whose invocation was garbage-collected reads as "not
+ a crash", the safe default `procCrashed` documents. A `failed` unit is
+ not collected until it is reset or started again, so a real crash keeps
+ its `Result` until the observer has read it.
+- A worker that sends SIGTERM to its own PID 1 makes a crash read as
+ whatever the trampoline then reports. It can only mislabel its own
+ exit, which is its own availability.
### 3.7 Decision D7: minting
**Decision.** `sidecarRouteFor` takes the same `nested` answer the instance
mint takes (`mintLayoutReader`, `internal/provision/reconcile.go:471-490`;
`RouteFor`'s `nested` arm, `:456-462`) and, when it is true and
-`CARLOS_NESTED_WORKERS` is on (§8.1), mints `DBPath = NestedDBPath`. Every
+`CARLOS_NESTED_WORKERS` is on (§8.1) and the workers floor holds (D11), mints `DBPath = NestedDBPath`. Every
other answer mints flat, as today. `convergeSidecar`'s shape check
(`sidecar_reconcile.go:621-626`) accepts either the flat or the nested path
for the row's host, and reports anything else as today.
@@ -479,7 +593,7 @@ and an instance host can never end in `.sidecar.internal`.
**Decision.** The layout pass's eligibility (`pass.go:264`, `:529`, `:747`)
widens from `ActivatorManaged(rt)` to `ActivatorManaged(rt) ||
-WorkerManaged(rt)`, behind `CARLOS_NESTED_WORKERS` (§8.1). Everything else
+WorkerManaged(rt)`, behind `CARLOS_NESTED_WORKERS` (§8.1) and the workers floor (D11). Everything else
about the move is plan 4's:
- **Quiesce.** `FenceAndStop` on the worker's entry. A hibernating worker
@@ -498,16 +612,50 @@ about the move is plan 4's:
the suffix, or the journal is refused like any mismatch. The nine
artifacts keep their order (`plan.go:98-101`); `socket` is recorded
absent on both sides. The journal format does not change.
-- **A row with nothing on this box moves by row alone.** Today the pass
- refuses a host whose database is missing (`pass.go:347-355`), so a row
- minted flat on a box that never woke it stays flat forever. Under the
- refusal policy that becomes a box that refuses the row on its first wake
- here, which is exactly a failover. **Decision:** when every one of the
- nine artifacts is absent at both endpoints, the pass journals the
- absence and does the row CAS only. The next wake restores from the
- bucket into the nested path. This changes `Required` for the database
- from "must exist" to "must exist unless nothing exists", for instance
- rows too.
+- **The app's own entries move too.** A worker's move takes the full extras
+ inventory (`ScanExtras`, `internal/layout/extras.go:14-50`, called from
+ `presence`, `pass.go:805-830`) exactly as an instance's does. Nothing in
+ this spec narrows it (R1-4).
+- **A row whose database was never here moves without one** (R1-4, R1-5).
+ Today the pass refuses a host whose database is missing (`pass.go:347-355`),
+ so a row minted flat on a box that never woke it stays flat forever.
+ Under the refusal policy that becomes a box that refuses the row on its
+ first wake here, which is exactly a failover. Revision 1 allowed a
+ row-only move when all nine artifacts were absent. That missed the
+ ordinary standby: config delivery writes every sidecar's `.config.env`
+ whether or not it has run (`internal/appconfig/syncexec.go:879-890`,
+ `:1523`), so a box that has never held the lease has config and no
+ database.
+ **Decision:** the database stops being `Required` when, under the fence,
+ **the database and every one of its companions are absent at both
+ endpoints**: `db`, `db-wal`, `db-shm`, `litestream-shadow`, `corrupt`,
+ and the box-wide replicator's `<db>-litestream` directory. The pass then
+ journals the database absent and moves everything that is present (the
+ env files, the config bundle, every extra) under the journal as it moves
+ any artifact. The next wake restores from the bucket into the nested
+ path, which is what `restorePlain` does on any box whose database path
+ is empty (`backend_exec.go:795-807`). A companion without its database
+ (a WAL with no database, say) is evidence of local history that went
+ missing, and the move is refused, as today, with the names in the
+ reason; an operator decides. This applies to instance rows too.
+
+ Why filesystem absence is enough here, and the part of R1-4 this
+ declines (§12): the platform's own rule is already that an absent
+ database means "restore from the bucket". `restorePlain` takes an
+ absent path as a cold box on every wake, flat or nested, today. A flat
+ tenant that renames a stopped sibling's database (the attack R1-4
+ describes) gets the same restore on that sibling's next flat wake with
+ no layout pass involved, and it could delete the file outright. So the
+ move adds no loss the flat tier did not already allow, and the flat
+ tier is what this spec removes. A renamed copy under a name the host
+ owns (`<host>.db.old`) is an extra and moves with the host; one under a
+ name no host owns is reported as unattributed, as today
+ (`pass.go:280-284`). Asking for evidence a flat tenant cannot forge
+ would mean root-held evidence of every past materialisation on every
+ box, for a loss the flat tenant can cause anyway.
+- **Failover.** The plan's test runs two boxes where only one has ever
+ held the database: the other's row moves with config only, and the
+ host then fails over to it nested, restoring from the bucket (R1-5).
- **Config.** `ConfigEnvPath` decides nested by both columns
(`internal/appconfig/syncexec.go:66-79`), so a nested worker's
`.config.env` would keep landing flat. It learns the worker shape (D2's
@@ -519,28 +667,46 @@ about the move is plan 4's:
row, and the report's per-host key already admits any host
(`internal/status/snapshot.go:230-254`).
-### 3.9 Decision D9: an always-on worker's replica lineage
-
-Hibernating workers replicate through the lease (`.activator-<host>.db-litestream`,
-one of the nine artifacts), keyed by host in the bucket, so their move is
-path-blind. An always-on worker is not hibernating, so the box-wide
-replicator claims its database (`internal/ops/litestream.go:356-357`,
-`:393-398`), and that replicator keys a replica by the database's full path
-(`replicaKey`, `:139-162`). Moving the file therefore moves the replica.
-This is new: no always-on row has ever been moved by the pass, because
-`ActivatorManaged` requires `Hibernate`.
-
-**Decision.** Accept a new lineage at the nested path. The pass, for an
-always-on worker: fences (the generator then drops the host,
-`FleetDBsWithSkipped`'s `fenced`), waits until the regenerated config no
-longer names the flat path and the box-wide replicator has reloaded,
-moves, and releases; the generator then names the nested path. The old
-lineage stays in the bucket under its old key (retention as configured).
-`CoverageShortfall`'s shrink guard is taught that a journaled host's flat
-path leaving the config is expected. The plan must confirm on a dev box
-that litestream releases the file on reload before the rename; if it does
-not, the pass stops the replicator for the rename, which is a box-wide
-pause of seconds, and says so in the event line.
+### 3.9 Decision D9: one replication owner for every worker (R1-3)
+
+Revision 1 assumed the box-wide replicator owned an always-on worker's
+database and planned a new path-keyed lineage for the move. Main says
+otherwise (§1.8): every worker's wake starts lease replication
+(`activator.go:2602`), always-on supervision keeps it
+(`activator.go:4599-4627`), and the box-wide generator replicates the same
+file too, because its exclusion tests `Hibernate` alone
+(`litestream.go:356-357`). Two replicators on one WAL is the corruption
+case the file itself warns about (`litestream.go:230-234`).
+
+**Decision.** The activator's lease replication is a worker's only
+replication, hibernating or always-on, flat or nested:
+
+- **One predicate, three readers.** `ops` gains `ActivatorReplicated(rt)`:
+ `rt.Backing != BackingUnit && rt.DBPath != "" && (rt.Hibernate ||
+ rt.IsSidecar())`. The box-wide generator's exclusion
+ (`litestream.go:356-357`) and the restore census (`HibernatingHosts`,
+ `hibernate_verify.go:45-60`, renamed for what it now selects) both use
+ it, so the two cannot drift into a gap that belongs to neither, which is
+ what that census's own comment promises.
+- **The transition.** On the first generation after this change, each
+ always-on worker's path leaves the box-wide config. `CoverageShortfall`'s
+ shrink guard is taught that a path leaving because its row became
+ `ActivatorReplicated` is expected, and names each one in the event line.
+ The lease lineage has been running all along, keyed by host, so nothing
+ stops being replicated. The old path-keyed lineage stays in the bucket
+ under its old key, under the retention it has.
+- **The restore drill** now covers always-on workers' lease lineages. The
+ first drill after the change is the evidence that those lineages are
+ sound after a period of two writers. One that fails is a finding for
+ that host, reported as any drill failure is.
+- **The move is then path-blind** for every worker, as it already is for a
+ hibernating one: the lease lineage is keyed by host, so moving the file
+ moves nothing in the bucket, and no box-wide reload sits inside the
+ move. Revision 1's wait for a regenerated config, and the dev-box check
+ on litestream releasing the file, are gone.
+
+This part has no dependency on the rest of the spec and is not behind
+`CARLOS_NESTED_WORKERS`. It can land first (§11 Q4).
### 3.10 Decision D10: while a box's nested gate fails
@@ -568,21 +734,72 @@ A binary from before this change runs a nested worker row through the old
no database path, and the row's own nested `DBPath` replicated by the old
lease code. Two copies, and unisolated.
-**Decision.**
-
-- Box: `systemApplier.install` refuses a candidate whose unit set lacks
- `carlos-instance-worker@.service` while any row whose host carries the
- sidecar suffix has `NestedPaths` (`provision.go:3020-3023`), beside
- R9's guard (`systemapply.go:1273-1290`). The reason names the rows and
- says to revert the layout first (copy to review).
-- Console: `layoutDowngradeBlocked` (`layoutguard.go:24-69`) gains a
- second floor, `release.WorkerLayoutFloor`, judged on the same observed
- rows. With D8 reporting worker rows, a nested worker row blocks a
- rollback below that floor the way a nested instance row blocks one
- below `LayoutFloor` (`internal/release/layoutintent.go:211`).
+Revision 1 guarded `systemApplier.install` only. Round 1 showed three
+other ways an old binary gets in front of a nested worker row (R1-2):
+
+- the update starts the new edge before its health gate
+ (`systemapply.go:600-612`), so the new agent may already have moved
+ workers when the gate fails and `restoreFiles` (`:1784-1800`) puts
+ `carlos.prev` back, with no worker check;
+- the recovery unit can **exec `carlos.prev` itself**
+ (`internal/host/units/carlos-system-recover.service:41`), so a check
+ that only the new binary makes cannot hold that path;
+- a row-only predicate misses a move interrupted before its row CAS: the
+ row still reads flat while its files are half-way. R9's guard counts
+ journals, fenced rows and conflicts for exactly this reason
+ (`systemapply.go:1273-1310`).
+
+The repo already has the mechanism for "an old binary must not run once
+this box has state it cannot read": the moves protocol floor, and the seeds
+floor built on it (seeded-instances design §3.3; `internal/seeds/floor.go:41-42`,
+`internal/host/rootseedsfloor.go`). A floor, once armed on a box, makes
+root give a verdict on `carlos.prev`, and the recovery unit runs
+`carlos.prev` only when that verdict names the armed floor
+(`carlos-system-recover.service:9-30`, `:41`); `restoreFiles` checks the
+same floor before it changes anything (`systemapply.go:1790-1797`).
+
+**Decision.** A third floor, the **workers floor**, on that mechanism:
+
+- **Published and armed before anything moves.** An operator publishes
+ `control/workers-protocol.json` naming the first release that carries
+ this spec. A box arms it locally as it arms the seeds floor
+ (`armFloor`, `systemapply.go:908-925`), and gives its `carlos.prev`
+ verdict. The layout pass treats worker rows as eligible (D8) only on a
+ box whose workers floor is armed **and** whose `carlos.prev` is either
+ excluded or at or above the floor; until then `CARLOS_NESTED_WORKERS=on`
+ is reported as `blocked: workers floor` and moves nothing. The reconciler
+ mints nested worker rows (D7) under the same condition. So every nested
+ worker row, journal or fence on a box exists only after every binary that
+ box can select would refuse to run below it.
+- **Every activation path obeys it.** Install refuses a candidate below an
+ armed workers floor, beside the moves floor's refusal. `restoreFiles`
+ refuses a `carlos.prev` below it, so a failed gate leaves the box on the
+ new build, journal kept, as the moves floor does. The recovery unit's
+ `ExecStart` gains the third test, the same shape as the seeds floor's
+ (`/data/.moves/workers-floor.min-release`, verdict
+ `/opt/carlos/carlos.prev.workers-allowed`), so the shell never execs a
+ `carlos.prev` below it.
+- **The row test, for the console and for status,** is R9's, not a
+ row-only one: a worker host is named when its row has `NestedPaths`,
+ when `Classify` calls it fenced or in conflict, or when it has a
+ journal, whether or not the row exists (`layoutReversalIncomplete`,
+ `systemapply.go:1273-1310`, which already covers every host this way;
+ D8's status change makes the console see worker hosts too). The console's
+ `layoutDowngradeBlocked` (`layoutguard.go:24-69`) gains
+ `release.WorkerLayoutFloor`, judged on those observed hosts, and blocks a
+ channel move below it while any is named.
+- **No disarm.** Neither existing floor has one, and this one adds none: a
+ floor only ever rises. A deployment that must go below it reverts its
+ worker rows first and then has an operator remove the floor files by
+ hand, knowingly. The plan documents that step beside the revert.
- `carlos system layout revert` reverts worker rows with the rest (they
are in the pass).
+The unit-set test of revision 1 (candidate lacks
+`carlos-instance-worker@.service`) stays as a second net for a candidate
+whose version cannot be parsed, the way R9 pairs the mount unit with
+`LayoutFloor` (`systemapply.go:510-528`).
+
### 3.12 Every consumer of the shape
The rule this repo has paid for three times: the consumer list is
@@ -603,9 +820,13 @@ main; the plan's first task re-runs the sweep on the day it starts:
`:347-355` (D8)
- `provision/sidecar_reconcile.go:131-148`, `:621-626` (D7)
- `host/layoutleg.go:193-199` (D8)
-- `host/systemapply.go:1273-1290` (D11)
+- `host/systemapply.go:1273-1310`, `:1784-1800`, `armFloor` (D11)
+- `host/units/carlos-system-recover.service` (D11)
- `console/layoutguard.go` (D11)
-- `ops/litestream.go` (D9)
+- `ops/litestream.go`, `ops/hibernate_verify.go` (D9)
+- `instancectl/legacystart.go`, `server.go` (D19)
+- `provision/sidecar_reconcile.go` `poolServed`, `placeable` (D17, shared
+ with the console)
- the sampler's slice-to-host mapping (`internal/host/metrics_probes.go`,
the nested design §6.6) and `carlos status --instances`' unit matcher
(sidecar design §14.4), which must recognise `carlos-instance-worker@`
@@ -634,9 +855,15 @@ logged and taken as the safe reading):
- `report`: every spawn proceeds as today, and each flat or foreign spawn
of a host not on the allowlist is recorded as a would-be refusal (§4.3).
- unset: `report` in the release that carries this spec; `refuse` from the
- next (D15). With `CARLOS_LAYOUT_DEFAULT=flat` (a deployment that has
- chosen not to isolate), unset stays `report` in every release, because
- that deployment's every row is flat by its operator's choice.
+ next (D15), with one exception. On a box whose **effective intent is
+ flat by an operator's choice**, unset stays `report` in every release,
+ because that box's every row is flat on purpose. Flat by choice is
+ either `CARLOS_LAYOUT_DEFAULT=flat` with no recorded intent, or a
+ recorded `flat` intent, whatever the default says (R1-6). Flat by
+ fallback (the default is nested but the box is not ready,
+ `defaultLayoutIntent`, `layoutleg.go:115-146`) is not a choice, and
+ unset means `refuse` there (D10). An intent the agent cannot read counts
+ as not chosen.
- an unknown value: `refuse`, logged. An operator who typed the variable
meant to choose, and the reading that cannot expose a tenant is the one
to take. (This is the opposite of `CARLOS_LAYOUT_DEFAULT`'s unknown-value
@@ -700,6 +927,15 @@ these seams; nothing here changes them.
(`flat_refused`, `foreign_refused`, `flat_reported`, `foreign_reported`),
first and last time and a count, the shape `ConfigRefusal` already has
(`snapshot.go:256-274`). Error text never goes in it.
+- **Inventory (R1-6).** Beside the spawn record, every snapshot carries
+ what `refuse` would stop **from the registry, not from history**: each
+ row the agent could spawn through a seam (activator-managed, worker and
+ foreign exec rows, awake or asleep) that classifies `flat` or `foreign`
+ and is not allowlisted, by host and layout word; the effective intent
+ with its source (`nested`, `flat_choice`, `flat_fallback`, `unknown`);
+ and the count of flat worker rows. A dormant row is in it on the first
+ snapshot after this release, whether it has woken or not. The inventory
+ is what D15 judges; the spawn record is what an operator reads.
- **Wake failure.** An instance wake refused this way answers the visitor
as any refused wake does (the member-safe reason path); a worker's state
word in `carlos sidecars ls` is a new one (copy to review: "Held: this
@@ -717,19 +953,36 @@ these seams; nothing here changes them.
that runs; status starts showing every flat spawn a `refuse` box would
stop.
2. **Per box**, after `CARLOS_NESTED_WORKERS=on` has moved its sidecars
- and its status shows no would-be refusal for a stretch that covers the
- box's slowest hibernating workload's wake interval, an operator sets
+ and its inventory (D14) lists nothing, an operator sets
`CARLOS_FLAT_EXEC=refuse` (with an allowlist if one is genuinely
needed). Dev boxes first (§8.2).
-3. **Release N+1** makes `refuse` the unset default (and turns
- `CARLOS_NESTED_WORKERS` on by default, §8.1). The console's system
- channel door refuses to move a channel to N+1 while any box that will
- follow it has the variable unset and reports a would-be refusal in its
- last fresh snapshot, or is stale, naming the boxes and hosts (copy to
- review). That is `layoutDowngradeBlocked`'s pattern (judge observed
- state; unknown blocks), applied to an upgrade.
-4. A box that needs the old behaviour after N+1 says so explicitly:
- `CARLOS_FLAT_EXEC=report`.
+3. **Release N+1** changes two defaults, and the console guards each one
+ separately, because they are different questions (R1-6): whether a box
+ may still run flat, and whether its workers are ready to move.
+
+ | Default that changes in N+1 | It changes nothing on a box when | Otherwise |
+ |---|---|---|
+ | `CARLOS_FLAT_EXEC` unset → `refuse` | the variable is set explicitly, or the effective intent is flat by choice (§4.1), or the inventory lists no row and the intent is known | blocked |
+ | `CARLOS_NESTED_WORKERS` unset → `on` | the variable is set explicitly, or the box has no flat worker row | blocked |
+
+ The system channel door refuses to move a channel to N+1 while any box
+ that will follow it is blocked on either line, or is stale, or reports
+ an inventory it could not complete, naming the boxes, the line and the
+ hosts (copy to review). That is `layoutDowngradeBlocked`'s pattern
+ (judge observed state; unknown blocks), applied to an upgrade. A box's
+ history of spawns plays no part: a dormant row that never woke is in
+ the inventory all the same.
+4. A box that needs the old behaviour after N+1 says so explicitly, and
+ each opt-out covers only its own line: `CARLOS_FLAT_EXEC=report` keeps
+ flat spawns running; `CARLOS_NESTED_WORKERS=off` keeps workers minted
+ and kept flat. An operator who sets only the first still has workers
+ move, and status says so beside the setting.
+
+Whether a worker is ready to move is not something a box can observe (a
+worker that computes `/data/<host>.db` looks the same as one that reads
+`CARLOS_SIDECAR_DB` until it starts nested). So the second line never
+infers readiness: on a box with worker rows, only an operator's explicit
+setting clears it, after §8.2 step 6's check.
Claim-mode boxes are unaffected throughout: they already refuse every
legacy launch shape before this check is reached.
@@ -760,18 +1013,36 @@ before the plan cap and the claim branch (`:315`, `:353`), so it covers
the claim-mode create too:
- The console reads every box's snapshot the way `carlos system status`
- does (`systemBoxes`, as `layoutDowngradeBlocked` uses it), keeps the
- boxes in the resolved region (or, for a fleet-placed app, the fleet's
- boxes), and drops stale ones.
+ does (`systemBoxes`, as `layoutDowngradeBlocked` uses it) and drops
+ stale ones.
+- **The placement set is the reconciler's, computed by the reconciler's
+ own code (R1-8).** A box can run the record exactly when
+ `ReconcileSidecars` would mint it there: the record's region is the
+ box's, the box serves the record's pool (`poolServed`), and the record's
+ placement is one the box accepts (`placeable`, with `no-inbound`
+ against the box's `CARLOS_INBOUND`)
+ (`internal/provision/sidecar_reconcile.go:343-356`). The console builds
+ the record it is about to write (region, pool, placement, as
+ `apisidecars.go:268-300` resolves them) and asks those same functions,
+ exported from `provision`, about each fresh box. So the box reports its
+ pools and its inbound setting in its snapshot, beside the field below;
+ a box that does not report them is not in the set and does not pass.
+ Revision 1's region-wide set could pass a region whose only ready box
+ serves the wrong pool or accepts inbound work, and block on an
+ irrelevant box.
- Each box reports a new additive field, `Metrics.Layout.Workers`:
- `nested` when its binary carries this spec, `CARLOS_NESTED_WORKERS` is
- on, its effective intent is nested and its nested gate answers; `off` or
- `blocked: <reason>` otherwise. The reason is private (it may name a
- path), like the layout report's reasons.
-- The create is refused when no fresh box is in scope, or when any fresh
- box in scope reports anything but `nested`. Stale boxes neither pass
- nor block: the box-side refusal (§4) is the boundary, and this door is
- the early, honest answer in front of it.
+ `nested` when its binary carries this spec, its workers floor is armed
+ with a `carlos.prev` verdict (D11), `CARLOS_NESTED_WORKERS` is on, its
+ effective intent is nested, its nested gate answers, **and it can launch
+ a worker at all**: a box with claim control armed refuses every worker
+ launch (`workerbuild.go:111-113`) and reports `blocked: claim` (R1-8).
+ `off` or `blocked: <reason>` otherwise. The reason is private (it may
+ name a path), like the layout report's reasons.
+- The create is refused when the placement set has no fresh box, or when
+ any fresh box in it reports anything but `nested`. A box outside the
+ set never blocks. Stale boxes neither pass nor block: the box-side
+ refusal (§4) is the boundary, and this door is the early, honest answer
+ in front of it.
- The refusal is a 409 with `code: "isolation"`, so the CLI and an app's
own code can tell it from a cap or a duplicate. The text names the
region and says that sidecars there cannot run isolated yet (copy to
@@ -796,12 +1067,31 @@ weakest box that may run it.
hold on that box, and each is observable through the API (`carlos status`),
with no box access:
-1. `CARLOS_FLAT_EXEC` in force is `refuse` (explicitly or by default);
+1. `CARLOS_FLAT_EXEC` in force is `refuse` (explicitly or by default),
+ and has been since this agent process started;
2. the allowlist is empty;
-3. no flat child is running: the agent reports the number of populated
- `carlos-exec@*` cgroups under its delegated subtree (it already walks
- them, `listExecCgroups`, `internal/host/metrics_probes.go:253`), and it
- is zero;
+3. **no flat child is running, by a complete census (R1-9)**. Revision 1
+ counted populated `carlos-exec@*` cgroups. That misses what lives
+ outside them: a spawn with no cgroup manager starts in the agent's own
+ cgroup (`cgroup.go:392-395`), the PID-move fallback leaves any
+ grandchild already forked in the agent's cgroup (`cgroup.go:360-375`),
+ and `listExecCgroups` answers an empty list for a missing or unreadable
+ root and skips unreadable groups (`metrics_probes.go:253-280`). So:
+ - **Drain.** Condition 1 holds only for an agent that read `refuse` at
+ its own start. The edge unit has the default `KillMode=control-group`
+ (`carlos-edge.service`), so the stop before that start killed every
+ process in its subtree, flat children included. A box switched to
+ `refuse` reports the condition only after its next edge restart.
+ - **Census.** The agent reads `cgroup.procs` of **every** cgroup in the
+ edge unit's subtree, its own leaf included, and reports `clean` only
+ when every pid it finds is the agent itself or a platform child it
+ started outside the tenant seams and still holds (it keeps that pid
+ set). Any other pid, any cgroup it could not read, a subtree root it
+ could not open, or a box with no delegation where it cannot tell its
+ own cgroup's members apart, reports `unknown` with the reason, and
+ `unknown` blocks retirement exactly as a populated count would.
+ - The census is a snapshot field (`Metrics.FlatExec.Census`: `clean`,
+ `populated`, `unknown`, with the time), read through `carlos status`.
4. the nested probe's metadata line is enforced (`v4: enforced`; nested
design §8.5).
@@ -838,7 +1128,7 @@ boxes and private services on the network.
| After | The attacker can | The attacker cannot |
|---|---|---|
| **Today** | everything in §1.5: read flat siblings; write nested siblings' raw directories and so run code inside them; kill or freeze the agent through cgroupfs; steal other flat workers' tokens; reach loopback, the VPC and metadata, and through it the box role | nothing the edge itself cannot do, short of root |
-| **The fence** (today, interim) | all of the above except metadata from its own cgroup; and very likely metadata too, by moving its pid into the exempt leaf (§1.6) | reach metadata from `carlos-exec@<host>` while it stays there |
+| **The fence** (today, interim) | all of the above except metadata | reach metadata, for as long as it cannot leave its cgroup for an exempt one (§1.6; the fence's own hardening, recorded on a dev box by §9) |
| **Release N rolled** (§3 dark, `report`) | the same: nothing that runs has changed | (status now names every flat spawn) |
| **`CARLOS_NESTED_WORKERS=on`** on a box whose gate is ready | on that box, nothing from a new or moved sidecar: it sees its own directory only, no loopback, no private range, no metadata, no sibling pid. It can still get a flat process **if** the box mints flat (gate failing with no recorded intent, D10) or the row is foreign | read or write another tenant's files from its sidecar; call the agent socket for another host (D3) |
| **`CARLOS_FLAT_EXEC=refuse`**, empty allowlist | nothing flat on that box, by any of the four spawn paths, in any layout state: a flat or foreign row is refused, the gate failing included | start any process in the host namespace |
@@ -903,24 +1193,33 @@ source is unreachable"; another provider's adapter supplies its own line.
| Setting | Where | Release N | Release N+1 | Gone in |
|---|---|---|---|---|
-| `CARLOS_NESTED_WORKERS` (`on`\|`off`) | box `host.env` | off: workers mint flat, the pass skips them, dispatch still honours any nested row | on unless `off` | N+2 |
-| `CARLOS_FLAT_EXEC` (`refuse`\|`report`) | box `host.env` | `report` | `refuse` unless set, or unless `CARLOS_LAYOUT_DEFAULT=flat` | stays (it is the self-hoster's opt-out) |
+| `CARLOS_NESTED_WORKERS` (`on`\|`off`) | box `host.env` | off: workers mint flat, the pass skips them, dispatch still honours any nested row. On does nothing until the workers floor is armed (D11) | on unless `off`; the N+1 channel guard (D15) | N+2 |
+| `CARLOS_FLAT_EXEC` (`refuse`\|`report`) | box `host.env` | `report` | `refuse` unless set, or unless the intent is flat by choice (§4.1); the N+1 channel guard (D15) | stays (it is the self-hoster's opt-out) |
+| `control/workers-protocol.json` | deployment bucket | published before any box turns workers on | stays | stays (a floor only rises) |
| `CARLOS_FLAT_EXEC_ALLOW` | box `host.env` | empty | empty | stays |
| `CONSOLE_REQUIRE_ISOLATED_WORKERS` | console env | off | on unless `false` | N+2 |
-Dark in N: every behaviour change. The dispatch in `StartWorkerStaged` is
-live in N, but no nested worker row can exist until a box turns
-`CARLOS_NESTED_WORKERS` on, so it is inert.
+Dark in N: every behaviour change except two. The dispatch in
+`StartWorkerStaged` is live in N, but no nested worker row can exist until
+a box arms the workers floor and turns `CARLOS_NESTED_WORKERS` on, so it is
+inert. **D9 is live in N on purpose**: the box-wide replicator stops
+replicating workers on the first generation after the roll (§3.9), and
+the shrink guard says so per host. D19's root fence is live too, and
+inert until a worker unit exists.
### 8.2 Order
The house order: dev boxes, then the dev console, then a check, then
flagship boxes, then the flagship console.
-1. **Release N to dev boxes, then the dev console.** Nothing changes.
- `carlos status --layout` lists sidecar rows as `flat` and the flat-exec
- section lists each as `flat_reported`.
-2. **Dev boxes: `CARLOS_NESTED_WORKERS=on`.** Watch sidecar rows turn
+1. **Release N to dev boxes, then the dev console.** Nothing that runs
+ changes. `carlos status --layout` lists sidecar rows as `flat` and the
+ flat-exec inventory lists each. Each always-on worker's path leaves the
+ box-wide config, named in the shrink guard's event line, and the next
+ restore drill covers its lease lineage (D9).
+2. **Publish the workers floor** for the deployment, and wait until every
+ dev box reports it armed with a `carlos.prev` verdict (D11).
+3. **Dev boxes: `CARLOS_NESTED_WORKERS=on`.** Watch sidecar rows turn
`nested`, hibernating ones at once, always-on ones within the awake
grace. The check is from inside: a throwaway sidecar in a dev account
whose release lists `/data` and `/run/carlos`, reads `/proc/1/status`,
@@ -928,27 +1227,34 @@ flagship boxes, then the flagship console.
`/v1/ready` and `/v1/next-due` over `CARLOS_AGENT_SOCKET`, and prints
all of it to `carlos sidecars logs`; then sleeps, wakes on its timer,
wakes on `carlos sidecars wake`, crashes once (restarted with backoff),
- and finishes once (parked). An always-on twin is moved by the grace
- stop, and its replica appears under the nested key.
-3. **Dev boxes: `CARLOS_FLAT_EXEC=refuse`.** A foreign row made on a dev
- box with `carlos add --db` is refused, status says so, and `carlos add`
- with the defaults repairs it. Verify §1.6's cgroup migration on a dev
- box both before and after: before, a flat child can move itself into
- `supervise`; after, there is no flat child to try.
-4. **Dev console: `CONSOLE_REQUIRE_ISOLATED_WORKERS=true`.** A create in a
- dev region whose boxes report `nested` succeeds; with one box's
- `CARLOS_NESTED_WORKERS` off, it is refused with `code: "isolation"`.
-5. **The check before flagship:** establish, for every app with native
+ is killed by SIGTERM from inside its own process once (a crash,
+ restarted), and finishes once (parked). An always-on twin is moved by
+ the grace stop, and its lease lineage continues under its host key.
+ A standby box that has the twin's config and has never held its lease
+ moves its row with config only, and the twin then fails over to it.
+4. **Dev boxes: `CARLOS_FLAT_EXEC=refuse`,** then an edge restart so the
+ census can read `clean` (D18). A foreign row made on a dev box with
+ `carlos add --db` is refused, status says so, and `carlos add` with the
+ defaults repairs it. Record the §1.6 cgroup migration attempt on a dev
+ box before and after: before, whether a flat child can move itself into
+ `supervise` under the fence; after, there is no flat child to try.
+5. **Dev console: `CONSOLE_REQUIRE_ISOLATED_WORKERS=true`.** A create in a
+ dev region whose eligible boxes report `nested` succeeds; with one
+ eligible box's `CARLOS_NESTED_WORKERS` off, it is refused with
+ `code: "isolation"`; with only an ineligible box (another pool) off, it
+ succeeds.
+6. **The check before flagship:** establish, for every app with native
sidecars on the flagship, whether its worker opens its database by
computed path, uses the agent socket by a hard-coded path, or reaches
anything on loopback or a private address (§3.4, §7.3). Each one that
does ships a release that reads `CARLOS_SIDECAR_DB` and
`CARLOS_AGENT_SOCKET` before its boxes turn nested workers on (§11 Q3).
-6. **Flagship boxes,** region by region, console box last: steps 2 and 3.
- Then the **flagship console**: step 4.
-7. **Fence retirement** per box when §6's four conditions hold, dev first,
+7. **Flagship:** release N to flagship boxes and console as in step 1;
+ the workers floor as in step 2; then boxes region by region, console
+ box last, steps 3 and 4. Then the **flagship console**: step 5.
+8. **Fence retirement** per box when §6's four conditions hold, dev first,
under its own scoped approval.
-8. **Release N+1** cut and rolled in the same order, behind the console
+9. **Release N+1** cut and rolled in the same order, behind the console
guard of D15.
### 8.3 Census
@@ -957,18 +1263,28 @@ Root-run code this touches, each of which changes a digest the root-read
census pins (`internal/provision/plan14_root_reads_test.go`) and so needs
a census proposal through the #280 process in the same branch:
-- `internal/instancectl/names.go`, `server.go` (the worker role on the
- existing verbs; no new verb);
+- `internal/instancectl/names.go`, `server.go`, `legacystart.go` (the
+ worker role on the existing verbs and under the start fence, D19; no new
+ verb);
- `internal/host/units/carlos-instance-worker@.service` (a new embedded
unit; templates are never enabled, so `update-platform-binary.sh` needs
no new name);
-- `internal/host/systemapply.go` (D11's guard);
+- `internal/host/systemapply.go` (D11: install, `restoreFiles`, `armFloor`)
+ and the workers floor's root files beside `rootseedsfloor.go`
+ (`rootworkersfloor.go`, its verdict and its boot read in
+ `rootmovesfloor_boot.go`);
+- `internal/host/units/carlos-system-recover.service` (D11: the third
+ floor test in `ExecStart`) (R1-2);
+- the `floor-allows-prev` verb, if the workers floor's verdict file needs
+ a case of its own there;
- the probe, only if the plan gives it a worker-template line (it should
not need one: the namespace block is identical, and the identity test
holds that).
No new root pass, path unit or relay. `instance-exec` and `instance-exit`
-run as `carlos` inside the namespace.
+run as `carlos` inside the namespace. D9's files (`internal/ops`) are not
+pinned by the root-read census today; the plan's census task confirms that
+on the day it runs.
### 8.4 Docs and copy
@@ -1003,40 +1319,77 @@ Unit and package level:
- The per-host agent listener: bound before the start, closed after
`done`; a token for host A refused on B's listener and on the shared
socket alike; the uniform 401; mode 0600.
-- `unitProc.Crashed`: a failed own invocation with `exit-code` is a crash;
- `success` after SIGTERM is not; a forged exit record claiming either is
- ignored; an unloaded invocation is not a crash.
+- `unitProc.Crashed`: a failed own invocation with `exit-code` is a crash,
+ including status 143 and 141 from a worker that died of SIGTERM or
+ SIGPIPE on its own; exit 0 is not; a forged exit record claiming either
+ is ignored; an unloaded invocation is not a crash; an agent-requested
+ stop that leaves the unit `failed` never reaches a `procCrashed` reader.
- The activator: a nested worker's wake, ready, next-due, idle, sleep,
timer wake, crash restart and finish park, end to end over the real
control server and a fake systemd that runs the trampoline's stub.
+- D19, over the real control server: a worker `/start` whose client gives
+ up while the handler is held keeps the lease on the agent side; the
+ `/stop` that follows cancels and joins it, and the held handler never
+ submits; a start accepted before a stop is refused however late it
+ registers; a join past the budget answers an error and the lease stays;
+ a worker stop does not fence the same host's run unit, or the reverse.
- The control service: `worker` admitted on the four verbs, refused where
it does not belong; the worker's unit and slice names; the tier always
- set; a staged build validated with the worker host's own app.
+ set; a staged build validated with the worker host's own app; `gen` other
+ than 0 refused for the worker role.
- Templates: the identity test across five templates; the worker
- template's four differing lines and nothing else.
+ template's five differing lines and nothing else.
- Reconciler: mint nested exactly when the instance mint does and the flag
is on; converge accepts both paths; the co-spawn check unchanged.
- Layout pass: a hibernating worker moves asleep; an always-on worker is
deferred for the grace and then stopped once; every wake source of a
fenced worker refuses (timer, wake verb, boot pass, sweep); a journal
- whose host suffix and row `Kind` disagree is refused; a row with no
- artifacts anywhere moves by CAS alone, and a row with some artifacts but
- no database is still refused; revert brings a worker back byte for byte.
-- `ConfigEnvPath` and SyncExec for a nested worker; the litestream
- generator and shrink guard across an always-on worker's move.
+ whose host suffix and row `Kind` disagree is refused; a worker's
+ app-made extras move with it; a row whose database and companions are
+ absent on both sides moves its config and extras with the database
+ journaled absent; a row with a WAL, shadow or meta directory but no
+ database is refused; two boxes where only one has held the database,
+ and the other then wakes the host nested from the bucket; revert brings
+ a worker back byte for byte.
+- `ConfigEnvPath` and SyncExec for a nested worker.
+- D9: `ActivatorReplicated` is the one predicate; the box-wide generator
+ excludes an always-on worker and the restore census includes it; the
+ shrink guard accepts a path leaving for that reason and names it, and
+ still refuses any other shrink.
+- D11: the workers floor arms like the seeds floor; the pass and the
+ reconciler treat workers as flat until it is armed with a verdict;
+ install refuses a candidate below it; `restoreFiles` refuses a
+ `carlos.prev` below it and keeps the journal; the recovery unit's
+ `ExecStart`, run by the real shell against fixture files, execs the
+ current binary when the verdict does not name the floor (the existing
+ recover-unit tests' shape); the row test names a worker host with only a
+ journal.
- Flat-exec policy: each value, unset under both layout defaults, an
unknown value, an invalid allowlist entry; the refusal from both seams;
the enumeration test over `exec.Command` in `internal/activator`; the
swap gate and the respawn veto classify the new error; the nudge of
- D16 runs the pass for a refused managed row and not for a foreign one.
-- Status: the `FlatExec` section and `Layout.Workers` pass the contract
- allowlist (`internal/status` `contract_test.go`) and never reach
- `fleet.json`; worker rows in the layout report.
+ D16 runs the pass for a refused managed row and not for a foreign one;
+ unset under a recorded `flat` intent with a nested default is `report`,
+ and under a gate fallback is `refuse`.
+- Status: the `FlatExec` section (spawn record, inventory, census) and
+ `Layout.Workers` pass the contract allowlist (`internal/status`
+ `contract_test.go`) and never reach `fleet.json`; worker rows in the
+ layout report; a dormant flat row that has never woken is in the
+ inventory.
+- D18's census: a fixture subtree with only the agent's pid is `clean`; a
+ pid in the agent's own leaf that the agent did not start is
+ `populated`; an unreadable `cgroup.procs`, a missing root, and a box with
+ no delegation are `unknown`.
- Console: the create door's refusal order (after the region, before the
- cap and the claim branch), every case of D17, `code: "isolation"`; the
- downgrade guard's worker floor; the N+1 channel guard of D15.
-- `systemapply`: a candidate without the worker template is refused while
- a nested worker row exists, and installs when none does.
+ cap and the claim branch), every case of D17 including a ready box in
+ the wrong pool, an inbound box for a `no-inbound` record, an unready box
+ outside the set, and a claim-mode box (`blocked: claim`);
+ `code: "isolation"`; the downgrade guard's worker floor; the N+1 channel
+ guard of D15, each line separately, with a dormant row and with
+ `CARLOS_FLAT_EXEC=report` set but `CARLOS_NESTED_WORKERS` unset.
+- `systemapply`: a candidate without the worker template, or below the
+ workers floor, is refused while a worker host is named, and installs
+ when none is.
Kernel and box level (root runner or a dev box; these need real cgroup v2
and systemd):
@@ -1047,9 +1400,12 @@ and systemd):
- A real nested worker unit: namespace listing, the probe's datagrams,
`PrivatePIDs`, the per-host agent socket reachable from inside and the
shared one absent, the log stream in `carlos sidecars logs`, a crash
- read as a crash, `systemctl restart carlos-edge` stopping it and the
- boot pass starting an always-on one again.
-- The always-on move's litestream release (D9).
+ (including a self-inflicted SIGTERM) read as a crash, a requested stop
+ leaving the unit `failed` and the next start succeeding,
+ `systemctl restart carlos-edge` stopping it and the boot pass starting
+ an always-on one again.
+- D18's census on a real box: `clean` after an edge restart under
+ `refuse`; `populated` with a stray process left in the agent's leaf.
The full gate runs once before handoff; locally, only the changed
packages' tests.
@@ -1078,15 +1434,68 @@ packages' tests.
until nested workers roll. It stops new flat sidecars today at the cost
of the feature for that time. Existing sidecars keep running.
2. **Treat what the flat tier could reach as exposed?** B1 was confirmed
- live, and §1.6 suggests the fence may not have held against a
- deliberate tenant. That would put the box roles' reachable secrets
- (regional SSM, the deployment bucket, and on the console box the IAM
- user minting) in scope for rotation, and possibly a look at the
- bucket's write history. That is an incident call.
+ live. The console audit (#370) found every sidecar ever created was the
+ operator account's, so no tenant has used the sidecar path. Round 1
+ reports the hardened fence closes the cgroup escape (§1.6). Flat
+ instance rows (foreign rows, rows the pass refused or deferred) had the
+ same reach, though, and before the fence nothing stopped metadata. That
+ would put the box roles' reachable secrets (regional SSM, the deployment
+ bucket, and on the console box the IAM user minting) in scope for
+ rotation, and possibly a look at the bucket's write history. That is an
+ incident call; the inventory of D14, once release N is out, says which
+ tenant hosts ran flat.
3. **Who confirms the apps that use native sidecars are ready to move?**
- §8.2 step 5 needs each such app's owner to confirm, or ship, a worker
- that reads `CARLOS_SIDECAR_DB` and `CARLOS_AGENT_SOCKET` and does not
- rely on loopback, before its flagship boxes turn nested workers on. And
- does the operator app the fence exempts need the flat allowlist at all,
- or can it move to a unit or to the nested tier first, so that every
- box's allowlist can be empty?
+ §8.2 step 6 needs a worker that reads `CARLOS_SIDECAR_DB` and
+ `CARLOS_AGENT_SOCKET` and does not rely on loopback, before its flagship
+ boxes turn nested workers on. Every sidecar today is the operator's, so
+ this is our own check rather than a customer's. And does the operator
+ app the fence exempts need the flat allowlist at all, or can it move to
+ a unit or to the nested tier first, so that every box's allowlist can
+ be empty?
+4. **Land D9 ahead of the rest?** The double replication of always-on
+ workers (§1.8, R1-3) is live on main now and risks a corrupt replica
+ for every always-on sidecar. D9 is small (one predicate in `internal/ops`,
+ the shrink guard's exception, the restore census) and independent of
+ isolation. Recommendation: its own branch and release now, ahead of
+ release N, with a restore drill of each always-on worker's lease lineage
+ after it rolls.
+5. **The workers floor is a third protocol floor.** It adds a third test to
+ the recovery unit's `ExecStart` and a third published control object.
+ The alternative is weaker: R9's unit-set test cannot hold the recovery
+ unit's own `carlos.prev` exec (R1-2). Recommendation: accept the floor.
+
+---
+
+## 12. Review disposition
+
+### 12.1 Round 1 (Astra, `CODEX-FINDINGS-SIDECAR-ISOLATION-R1.md`, at 1c029ee5)
+
+Verdict: reject. Every citation was checked against main and held.
+
+| Finding | | Disposition | Where |
+|---|---|---|---|
+| R1-1 | P1: no start/stop ordering, two workers on one lease | **Accepted.** The run unit's #292 fence, widened to the worker role; the failed wake keeps the lease until `/stop` and an empty unit prove it gone | D19, §3.5.1; §2 |
+| R1-2 | P1: D11 holds install but not rollback or recovery | **Accepted.** A workers protocol floor on the moves/seeds floor mechanism, armed before anything moves, held by install, `restoreFiles` and the recovery unit; R9's journal-aware row test; recover unit added to the census | D11, §3.11; §8.3; §11 Q5 |
+| R1-3 | P1: always-on workers already use lease replication | **Accepted.** One predicate makes lease replication every worker's only replication; the box-wide generator and the restore census use it. Recorded as a live defect | D9, §3.9; §1.8; §11 Q4 |
+| R1-4 | P1: "nine artifacts absent" is no evidence of no local data | **Accepted in part.** The full extras inventory moves with every worker; the row-only exception is replaced by "database and every companion absent at both ends", and a companion without its database refuses as today. **Declined:** evidence a flat tenant cannot manufacture (below) | D8, §3.8 |
+| R1-5 | P2: standby boxes with config and no database are stranded | **Accepted.** The narrowed rule moves config and extras with the database journaled absent; a two-box failover test | D8, §3.8; §9; §8.2 step 3 |
+| R1-6 | P2: D15 judges spawn history; `report` does not cover the worker default | **Accepted.** A registry inventory, dormant rows included, judged per default; flat by choice covers a recorded `flat` intent; the two defaults are guarded separately and opt out separately | D14, §4.3; D15, §4.4; §4.1 |
+| R1-7 | P2: `SuccessExitStatus` turns real crashes into finishes | **Accepted.** No signal statuses in the worker template; requested stops are told apart by the agent; D1's "four lines" reopened to five | D6, §3.6; D1, §3.1 |
+| R1-8 | P2: D17 checks a different placement set; claim mode passes | **Accepted.** The reconciler's own `poolServed` and `placeable`, over fresh boxes, nonempty; readiness includes the workers floor and launch capability (`blocked: claim`) | D17, §5 |
+| R1-9 | P2: D18's cgroup count is not proof | **Accepted.** `refuse` read at this agent's start (the edge restart drains the subtree), and a pid census of the whole edge subtree where anything unreadable or unaccounted is `unknown` and blocks | D18, §6 |
+
+The review's closing note, that the hardened fence already closes §1.6's
+cgroup escape, is taken: §1.6 and §7.2 no longer claim the fence is
+bypassable, and the dev-box record of the attempt stays.
+
+**Declined: R1-4's demand for evidence a flat tenant cannot manufacture.**
+The platform's own rule already treats an absent database as "restore from
+the bucket": `restorePlain` does that on every wake, flat or nested
+(`backend_exec.go:795-807`). A flat tenant that renames a stopped sibling's
+database gets that restore on the sibling's next flat wake today, with no
+layout pass involved, and could delete the file instead. The row move adds
+no loss the flat tier does not already allow, and the flat tier is what
+this spec removes. Root-held evidence of every past materialisation on
+every box would be a new mechanism for a loss the attacker can cause
+anyway. What the finding rightly caught, the incomplete inventory and the
+companion files, is fixed.
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-SIDECAR-ISOLATION.md b/docs/superpowers/specs/CODEX-FINDINGS-SIDECAR-ISOLATION-R1.md
similarity index 100%
rename from docs/superpowers/specs/CODEX-FINDINGS-SIDECAR-ISOLATION.md
rename to docs/superpowers/specs/CODEX-FINDINGS-SIDECAR-ISOLATION-R1.md