carlos / platform Public

Sidecar isolation spec, revision 3: answers Astra round 2, Paul's rulings

R2-1 (uncertain hold replacement) and R2-3 (running replicator, not the
config file) accepted; R2-2 answered with no new floor: workers move only
once carlos.prev is worker-capable, and install keeps both binaries above
the floor. R2-4 answered by Paul's Q6 ruling: unset CARLOS_FLAT_EXEC is a
per-box ratchet. D17 (and R2-5) and D18 deferred to follow-ups (§13).
Approved to build by Paul 2026-10-24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 99017de51f3e3a3af604adf596c6c41185ed2ea8 parent 6749a4d
1 file changed, +284 −249
  • docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md +284 −249

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
.amadan/test-gate.sh
go: downloading golang.org/x/net v0.56.0
go: downloading golang.org/x/crypto v0.53.0
go: downloading golang.org/x/sys v0.46.0
go: downloading github.com/pion/stun/v3 v3.0.1
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading golang.org/x/sync v0.21.0
go: downloading golang.org/x/text v0.38.0
go: downloading golang.org/x/crypto v0.53.0
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/pion/transport/v3 v3.0.8
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (26 s)
go: downloading golang.org/x/crypto v0.53.0
go: downloading golang.org/x/text v0.38.0
go: downloading golang.org/x/tools v0.47.0
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading golang.org/x/mod v0.37.0
=== gofmt (1 s)
gofmt -l .: clean
=== rest (178 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.627s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.954s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.761s
ok  	github.com/carlosframework/platform/internal/activator	59.981s
ok  	github.com/carlosframework/platform/internal/agentsock	0.020s
ok  	github.com/carlosframework/platform/internal/alerting	0.007s
ok  	github.com/carlosframework/platform/internal/analytics	0.074s
ok  	github.com/carlosframework/platform/internal/appconfig	2.885s
ok  	github.com/carlosframework/platform/internal/apperr	0.190s
ok  	github.com/carlosframework/platform/internal/applog	0.312s
ok  	github.com/carlosframework/platform/internal/awsinv	0.015s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.008s
ok  	github.com/carlosframework/platform/internal/boxid	0.892s
?   	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.042s
ok  	github.com/carlosframework/platform/internal/caps	0.036s
ok  	github.com/carlosframework/platform/internal/certwarm	0.006s
ok  	github.com/carlosframework/platform/internal/cgroup	0.768s
ok  	github.com/carlosframework/platform/internal/changelog	0.596s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.261s
ok  	github.com/carlosframework/platform/internal/clamd	0.068s
?   	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	7.994s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.049s
ok  	github.com/carlosframework/platform/internal/dbquery	0.036s
ok  	github.com/carlosframework/platform/internal/dbreads	2.114s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.014s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.024s
ok  	github.com/carlosframework/platform/internal/docsite	0.042s
ok  	github.com/carlosframework/platform/internal/domains	2.967s
ok  	github.com/carlosframework/platform/internal/doorkey	0.023s
ok  	github.com/carlosframework/platform/internal/economics	0.014s
ok  	github.com/carlosframework/platform/internal/edge	46.187s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.141s
ok  	github.com/carlosframework/platform/internal/gate	1.676s
ok  	github.com/carlosframework/platform/internal/genselect	0.032s
ok  	github.com/carlosframework/platform/internal/geo	0.013s
ok  	github.com/carlosframework/platform/internal/geoip	0.037s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.216s
ok  	github.com/carlosframework/platform/internal/host	142.370s
ok  	github.com/carlosframework/platform/internal/hostactors	5.592s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.016s
ok  	github.com/carlosframework/platform/internal/hostbox	3.030s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.762s
ok  	github.com/carlosframework/platform/internal/hostenv	0.016s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	5.350s
ok  	github.com/carlosframework/platform/internal/hostfence	0.050s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.378s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.022s
ok  	github.com/carlosframework/platform/internal/hostguard	1.026s
ok  	github.com/carlosframework/platform/internal/hostops	1.927s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.016s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.018s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.620s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.175s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.017s
ok  	github.com/carlosframework/platform/internal/infraview	0.020s
ok  	github.com/carlosframework/platform/internal/instancectl	5.241s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.804s
ok  	github.com/carlosframework/platform/internal/instancefs	0.015s
ok  	github.com/carlosframework/platform/internal/instancekey	0.019s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.023s
ok  	github.com/carlosframework/platform/internal/layout	1.744s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.022s
ok  	github.com/carlosframework/platform/internal/lease	10.134s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.556s
ok  	github.com/carlosframework/platform/internal/ledger	0.012s
ok  	github.com/carlosframework/platform/internal/mail	0.294s
ok  	github.com/carlosframework/platform/internal/maintenance	0.105s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.596s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.082s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	22.357s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.055s
ok  	github.com/carlosframework/platform/internal/objstore	0.289s
ok  	github.com/carlosframework/platform/internal/ops	0.229s
ok  	github.com/carlosframework/platform/internal/park	0.307s
ok  	github.com/carlosframework/platform/internal/plans	0.009s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	31.365s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.009s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.713s
ok  	github.com/carlosframework/platform/internal/relay	7.242s
ok  	github.com/carlosframework/platform/internal/release	26.079s
ok  	github.com/carlosframework/platform/internal/rollout	3.855s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.012s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.060s
ok  	github.com/carlosframework/platform/internal/rootstate	0.028s
ok  	github.com/carlosframework/platform/internal/runs	0.558s
ok  	github.com/carlosframework/platform/internal/s3compat	0.007s
ok  	github.com/carlosframework/platform/internal/safefile	0.009s
ok  	github.com/carlosframework/platform/internal/schedule	2.715s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.670s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.282s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.955s
ok  	github.com/carlosframework/platform/internal/status	0.067s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.299s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.004s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.703s
=== internal_console@0 (91 s)
ok  	github.com/carlosframework/platform/internal/console	89.395s
=== internal_console@1 (45 s)
ok  	github.com/carlosframework/platform/internal/console	43.130s
=== internal_console@2 (41 s)
ok  	github.com/carlosframework/platform/internal/console	39.617s
=== internal_console@3 (41 s)
ok  	github.com/carlosframework/platform/internal/console	39.225s
=== internal_console@4 (44 s)
ok  	github.com/carlosframework/platform/internal/console	42.116s
=== internal_console@5 (41 s)
ok  	github.com/carlosframework/platform/internal/console	39.996s
=== internal_console@6 (38 s)
ok  	github.com/carlosframework/platform/internal/console	36.399s
=== internal_console@7 (47 s)
ok  	github.com/carlosframework/platform/internal/console	45.305s
=== root@0 (110 s)
ok  	github.com/carlosframework/platform	108.369s
=== root@1 (84 s)
ok  	github.com/carlosframework/platform	82.674s
=== root@2 (91 s)
ok  	github.com/carlosframework/platform	89.359s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50528418 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.024s
=== RUN   TestT4StorageProbeMinio
2026/10/24 20:33:17 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E19166DC6D4E75, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.025s
=== RUN   TestS3StoreMinioContract
=== RUN   TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN   TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.03s)
    --- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
    --- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.02s)
PASS
ok  	github.com/carlosframework/platform/internal/release	0.032s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox

amadan: job cancelled — a newer push superseded it
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 aa4f7c01c..c48fdc073 100644
--- a/docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md
+++ b/docs/superpowers/specs/2026-10-24-sidecar-isolation-design.md
@@ -1,6 +1,8 @@
# Sidecar isolation: native sidecars on the nested tier, and no flat exec by default
-**Date:** 2026-10-24 · **Status:** draft, revision 2, for an independent
+**Date:** 2026-10-24 · **Status:** revision 3, approved to build by Paul
+2026-10-24 ("let's just do this and close the vulnerability and roll a
+release when done"), 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
@@ -20,6 +22,25 @@ not spawn history (D15); the create check uses the reconciler's own
placement set (D17); and fence retirement needs a complete process census
(D18).
+**Revision 3** answers Astra's round 2 (`CODEX-FINDINGS-SIDECAR-ISOLATION-R2.md`,
+at 730ba093, verdict reject: three P1s, two P2s), marked **(R2-n)**, and
+records Paul's rulings of 2026-10-24 (§11). It also cuts what ships to what
+closes the hole, in one release and its patch (§13):
+
+- **D11 needs no new floor.** Workers become nested on a box only once its
+ `carlos.prev` is itself a release that understands nested workers. Every
+ path that can put an older binary in front of a row (rollback, the
+ recovery unit, the root guard's copy) picks `carlos.prev` or the current
+ binary, so both are then worker-capable (R2-2).
+- **D15 needs no console guard and no `host.env` edit.** An unset
+ `CARLOS_FLAT_EXEC` turns to `refuse` on its own, on each box, the first
+ time that box's inventory is empty, and never turns back. An unset
+ `CARLOS_NESTED_WORKERS` is on. So no default change stops a workload that
+ was running when it changed (R2-4).
+- **Deferred to follow-ups:** the console's create-time check (D17, and so
+ R2-5) and the fence's retirement (D18). Neither is the boundary; the box's
+ refusal is.
+
**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`,
@@ -81,14 +102,14 @@ The decisions, for scanning (each is argued where it is made):
| 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 | A workers protocol floor, armed before any worker moves, holds every install, rollback and recovery selection (rev 2) | §3.11 |
+| D11 | Workers move only once `carlos.prev` is itself worker-capable; install keeps both binaries at or above the floor (rev 3) | §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 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 |
+| D15 | Unset `CARLOS_NESTED_WORKERS` is on; unset `CARLOS_FLAT_EXEC` is a per-box ratchet that turns to `refuse` once nothing would be refused (rev 3) | §4.4 |
| D16 | A refused managed row nudges the layout pass for that host | §4.5 |
-| 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 |
+| D17 | (deferred) Creation is refused unless the reconciler's own placement set has a fresh box and every one reports nested workers runnable | §5 |
+| D18 | (deferred) The fence retires per box on four observable conditions, the third a complete process census | §6 |
---
@@ -528,6 +549,19 @@ only for an unproven generation or a legacy start marked uncertain
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.
+- **Replacements (R2-1).** An uncertain start is not "nothing ran". The
+ nested worker arm's uncertain outcome also answers true to
+ `startMayHaveRun` (`containment.go:160-171`, which today knows only
+ `startUncertain` and an unproven generation), so it reaches
+ `ReplaceWorker` as "may have executed". `ReplaceWorker`'s
+ unchanged-incarnation branch (`workerswap.go:152-160`) then treats it as
+ the attempt's build having run: a hold attempt restores nothing and stops
+ the entry (the F2 rule for a hold build exec'd before a refusal), and an
+ overlap attempt falls back only after the attempted unit is proven gone
+ through the fenced `/stop` and `RoleGone`. The respawn's error arm
+ (`activator.go:3739`) keeps the obligation on the entry until that proof,
+ so no path restores the old build over a database the new one may have
+ migrated.
- `KillHost` inspecting the slice alone is not the proof, and the rewrite
of `TestKillHostProvesANestedLayoutWorkersHostCgroup` says so.
@@ -579,7 +613,7 @@ forever (R1-7). That is not the flat contract.
**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) and the workers floor holds (D11), mints `DBPath = NestedDBPath`. Every
+`CARLOS_NESTED_WORKERS` is on (§8.1) and D11's `carlos.prev` condition holds, 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.
@@ -593,7 +627,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) and the workers floor (D11). Everything else
+WorkerManaged(rt)`, behind `CARLOS_NESTED_WORKERS` (§8.1) and D11's `carlos.prev` condition. Everything else
about the move is plan 4's:
- **Quiesce.** `FenceAndStop` on the worker's entry. A hibernating worker
@@ -699,11 +733,24 @@ replication, hibernating or always-on, flat or nested:
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 running replicator, not the file, decides (R2-3).**
+ `/etc/litestream.yml` is regenerated by an operator (`carlos ops
+ litestream`, `internal/ops/ops.go:149-175`, which prints `changed` for
+ the caller to restart the replicator) and litestream reads it only at
+ start (`carlos-litestream.service:16`). A config that no longer names a
+ worker's database is therefore no evidence that the running daemon has
+ let go of it. So the layout pass moves an always-on worker only when the
+ box-wide replicator cannot be holding the file: `carlos-litestream` is
+ not active, or it is active and its current invocation started after
+ the config was last written (`ActiveEnterTimestamp` against the file's
+ mtime, read through `systemctl show`) **and** that config does not name
+ the file. Otherwise the move is deferred with the reason "the box-wide
+ replicator still names this database: regenerate its config and restart
+ it" (copy to review), and retried on every pass, so a failed restart is
+ never taken for a done one.
- **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.
+ moves nothing in the bucket.
This part has no dependency on the rest of the spec and is not behind
`CARLOS_NESTED_WORKERS`. It can land first (§11 Q4).
@@ -727,78 +774,64 @@ recorded (`layoutleg.go:127-146`). For workers:
loudly"). An operator who needs a specific host up through a gate
failure adds it to the allowlist, knowingly (§4.1).
-### 3.11 Decision D11: downgrade
+### 3.11 Decision D11: downgrade (rev 3: R1-2, R2-2)
A binary from before this change runs a nested worker row through the old
`StartWorkerStaged`: flat, with the env read from the flat directory and
no database path, and the row's own nested `DBPath` replicated by the old
lease code. Two copies, and unisolated.
-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`).
+Revision 1 guarded `systemApplier.install` only. Round 1 named the other
+ways an old binary reaches a row: the rollback after a failed gate
+(`restoreFiles`, `systemapply.go:1784-1800`), the recovery unit, which can
+exec `carlos.prev` itself (`carlos-system-recover.service:41`), and a move
+interrupted before its row CAS. Revision 2 answered with a third protocol
+floor; round 2 showed that the root guard's separately installed copy
+(`guardarm.go:148-166`, overriding the recovery unit's `ExecStart`) would
+not know it (R2-2), and on flagship no protocol floor is published at all.
+
+Revision 3 closes every path from one fact about them. **Each path that
+puts a binary in front of the rows picks one of two files: the current
+`carlos` or `carlos.prev`.** The recovery unit's shell runs one or the
+other (`:41`); the guard copy chooses `carlos.prev` when an apply's journal
+is present and the current binary otherwise (`guardexec.go:66-80`), and its
+floor route chooses between the same two (`:95-110`); `restoreFiles` puts
+`carlos.prev` back. And root records `carlos.prev`'s release beside it
+before every apply (`<optDir>/carlos.prev.version`, written in `notePrev`,
+`systemapply.go:818-830`) and keeps an older `carlos.prev` when the
+running build is not recorded known-good (`decidePrev`, `:792-815`).
+
+**Decision.**
+
+- **Workers move only when both files can run them.** The layout pass
+ treats worker rows as eligible (D8), and the reconciler mints nested
+ worker rows (D7), only on a box whose running build and whose
+ `carlos.prev.version` both parse as releases at or above
+ `release.WorkerLayoutFloor` (the first release carrying this spec). An
+ unreadable or unparseable `carlos.prev.version`, or none, reads as below.
+ Status says `workers: waiting for carlos.prev` (a closed word) while it
+ holds. In practice a box qualifies after its second roll at or above the
+ floor (§13).
+- **Once true, it stays true.** Neither file goes below the floor without
+ an install, and install refuses that:
+ - a candidate whose version parses below `WorkerLayoutFloor`, or whose
+ unit set lacks `carlos-instance-worker@.service` (for a candidate whose
+ version does not parse), is refused while any worker host is named,
+ beside R9's guard and on its row test (below);
+ - `decidePrev` never replaces `carlos.prev` with a binary below the
+ floor, because the running build is at or above it, and keeps the
+ existing `carlos.prev` otherwise.
+ So the guard copy, whichever of the two it picks, picks a worker-capable
+ binary. It needs no change, and neither does the recovery unit.
+- **The row test 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`). D8's status
+ change lets the console see worker hosts too, and `layoutDowngradeBlocked`
+ (`layoutguard.go:24-69`) gains `release.WorkerLayoutFloor`, judged on
+ those hosts, and blocks a channel move below it while any is named.
+- `carlos system layout revert` reverts worker rows with the rest (they are
+ in the pass); after it, install accepts an older candidate again.
### 3.12 Every consumer of the shape
@@ -854,8 +887,9 @@ logged and taken as the safe reading):
refused (§4.2).
- `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 one exception. On a box whose **effective intent is
+- unset: the ratchet of D15 (rev 3): `report` until the box's inventory is
+ first empty under a nested intent, `refuse` from then on, 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
@@ -946,46 +980,45 @@ these seams; nothing here changes them.
- **`carlos status --layout`** shows the policy and each refused or
allowlisted host beside its layout word.
-### 4.4 Decision D15: sequencing, so no box loses a workload on upgrade
-
-1. **Release N** carries §3, §4 and §5, with `CARLOS_FLAT_EXEC` defaulting
- to `report` and `CARLOS_NESTED_WORKERS` off. Rolling it changes nothing
- 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 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** 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.
+### 4.4 Decision D15: sequencing, so no box loses a workload on upgrade (rev 3: R1-6, R2-4)
+
+Revisions 1 and 2 had a later release change both defaults behind a
+console guard. Round 2 showed the guard is judged when the channel moves,
+while each box applies the release later, so a workload created in between
+gets both changes unassessed (R2-4). Paul's ruling (§11 Q6) is that a
+default change must not stop what was running. Revision 3 makes the
+change happen on the box, at the one moment it is known to stop nothing.
+
+**Decision.**
+
+- **`CARLOS_NESTED_WORKERS` unset means on**, from release N, subject to
+ D11's condition and the box's effective intent being nested. A worker
+ that opens `CARLOS_SIDECAR_DB` and uses `CARLOS_AGENT_SOCKET` moves
+ without noticing. Every native sidecar on our deployments today is the
+ operator's, and §8.2's check covers them before release N rolls. From
+ release N the documented contract is those two variables (D4).
+- **`CARLOS_FLAT_EXEC` unset is a ratchet.** It reads `report` until the
+ first time the agent finds the box's inventory (D14) empty and the
+ effective intent nested, and from then on `refuse`. The agent records
+ that moment in `/data/.carlos/flat-exec-refuse` (written once, atomically,
+ never removed by any code path) and reads it at start. So:
+ - nothing that runs when the ratchet turns is refused by it, because the
+ inventory was empty;
+ - a row that arrives later meets `refuse`, which is the property: a new
+ worker is minted nested, a new instance is nested by default, and a
+ foreign row an operator adds by hand is refused and named in status
+ (D14), with `carlos add` or the allowlist as the repair;
+ - a box whose intent is flat by choice (§4.1) never turns.
+ The file lives in the agent's data directory, which only `carlos`
+ processes in the host namespace can write. Once the ratchet has turned
+ there are none but the agent.
+- **An explicit setting always wins**: `refuse` or `report`, read at agent
+ start as before, and the ratchet file is then ignored but not removed.
+- The inventory, the ratchet's state and its time are in status, so an
+ operator can see which boxes have turned and why one has not.
+
+No console guard, no later release flipping a default, and no `host.env`
+edit on any box.
### 4.5 Decision D16: the stragglers
@@ -1004,7 +1037,11 @@ repair stays the box-local `carlos add`.
---
-## 5. Decision D17: creation-time refusal
+## 5. Decision D17: creation-time refusal (deferred, §13)
+
+**Revision 3:** not in release N (Paul, §11 Q1). The follow-up takes this
+section as written plus R2-5's fix: a stale or incomplete box that the
+placement set cannot exclude blocks creation.
**Decision.** `handleAPISidecarCreate` refuses before any write when the
app's sidecars could not run isolated where they would be placed. The
@@ -1031,8 +1068,8 @@ the claim-mode create too:
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, its workers floor is armed
- with a `carlos.prev` verdict (D11), `CARLOS_NESTED_WORKERS` is on, its
+ `nested` when its binary carries this spec, D11's `carlos.prev` condition holds,
+ `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).
@@ -1061,7 +1098,11 @@ weakest box that may run it.
---
-## 6. Decision D18: retiring the interim fence
+## 6. Decision D18: retiring the interim fence (deferred, §13)
+
+**Revision 3:** not in release N. The fence stays on every box until the
+follow-up builds the census below and an operator removes the fence under
+its own scoped approval.
**Decision.** `fence-flat-imds.sh` may be removed from a box when all four
hold on that box, and each is observable through the API (`carlos status`),
@@ -1131,9 +1172,9 @@ boxes and private services on the network.
| **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 |
-| **Console refusal on** | (no change in power) | create a record the platform would place unisolated or never run; this is honesty, the boundary is the line above |
-| **Fence retired** (§6) | nothing new: there is no flat child left for the fence to filter | |
+| **`refuse` in force** (explicit, or the ratchet turned), 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 |
+| **Console refusal on** (follow-up, D17) | (no change in power) | create a record the platform would place unisolated or never run; this is honesty, the boundary is the line above |
+| **Fence retired** (follow-up, §6) | nothing new: there is no flat child left for the fence to filter | |
### 7.3 What remains after the last step
@@ -1187,75 +1228,56 @@ source is unreachable"; another provider's adapter supplies its own line.
---
-## 8. Rollout
+## 8. Rollout (rev 3)
### 8.1 Flags and defaults
-| 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 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 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.
+| Setting | Where | Unset means | Explicit values |
+|---|---|---|---|
+| `CARLOS_NESTED_WORKERS` | box `host.env` | on (D15), effective only under D11's condition and a nested intent | `on`, `off` (keeps workers minted and kept flat) |
+| `CARLOS_FLAT_EXEC` | box `host.env` | the ratchet: `report` until the box's inventory is first empty under a nested intent, `refuse` from then on (D15); `report` forever under a flat-by-choice intent | `refuse`, `report` |
+| `CARLOS_FLAT_EXEC_ALLOW` | box `host.env` | empty | exact hosts (D12) |
+
+None of these needs setting on any of our boxes. Nothing is dark in
+release N except what D11 holds back until `carlos.prev` qualifies.
### 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 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
+1. **Before the cut: the sidecar check.** Every native sidecar on dev and
+ flagship (all the operator's, #370) is checked for a computed database
+ path, a hard-coded agent socket, and loopback or private-address use
+ (§3.4, §7.3). Each one that needs it ships a release reading
+ `CARLOS_SIDECAR_DB` and `CARLOS_AGENT_SOCKET` first. Where an always-on
+ worker's database is named in a box's `/etc/litestream.yml`, the
+ operator regenerates it (`carlos ops litestream`, which D9 now excludes
+ workers from) and restarts the replicator; until then D9's check holds
+ that worker's move (§3.9).
+2. **Release N to dev boxes, then the dev console.** On each box: status
+ shows the inventory; workers stay flat (`waiting for carlos.prev`); D9's
+ exclusion and D19's fence are live.
+3. **Release N's patch (N.1) to dev boxes and the dev console.** Now
+ `carlos.prev` is N, so D11's condition holds and dev boxes move their
+ workers: hibernating ones at once, always-on ones within the awake
+ grace. Verify from inside with a throwaway sidecar in a dev account
whose release lists `/data` and `/run/carlos`, reads `/proc/1/status`,
sends the probe's datagrams, opens `CARLOS_SIDECAR_DB`, calls
- `/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),
- 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).
-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.
-9. **Release N+1** cut and rolled in the same order, behind the console
- guard of D15.
+ `/v1/ready` and `/v1/next-due` over `CARLOS_AGENT_SOCKET`, and prints it
+ to `carlos sidecars logs`; then sleeps, wakes on its timer and on
+ `carlos sidecars wake`, crashes once (restarted), is killed by SIGTERM
+ from inside once (a crash, restarted), and finishes once (parked). Then
+ watch each dev box's inventory empty and its ratchet turn to `refuse`.
+ A foreign row made with `carlos add --db` is then refused and named.
+4. **Flagship:** N to boxes region by region, console box last, then the
+ flagship console; then N.1 the same way. Watch each box move its workers
+ and turn its ratchet. A box whose ratchet has not turned within the
+ awake grace after N.1 is named in status with what holds it.
+5. **The fence** stays until its retirement follow-up (§13).
+
+N.1 can be whatever patch is next on `main`; its only job here is to make
+N the box's `carlos.prev`.
### 8.3 Census
@@ -1269,14 +1291,10 @@ a census proposal through the #280 process in the same branch:
- `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: 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;
+- `internal/host/systemapply.go` (D11, rev 3: install's refusal of a
+ candidate below `WorkerLayoutFloor` while a worker host is named;
+ `decidePrev` unchanged). The recovery unit, the guard and the floor files
+ are not touched (R2-2);
- 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).
@@ -1356,14 +1374,23 @@ Unit and package level:
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.
+- D11 (rev 3): the pass and the reconciler treat workers as flat while
+ `carlos.prev.version` is missing, unparseable or below the floor, and
+ status says `waiting for carlos.prev`; install refuses a candidate below
+ the floor, or one lacking the worker template, while a worker host is
+ named, including a host with only a journal.
+- D15's ratchet: unset reads `report` with a non-empty inventory; turns
+ once on the first empty inventory under a nested intent, writes its file
+ once, and stays `refuse` across an agent restart and a later non-empty
+ inventory; never turns under a flat-by-choice intent; an explicit value
+ wins and leaves the file.
+- D19 replacement (R2-1): a hold `ReplaceWorker` whose worker start
+ answers uncertain restores nothing and stops the entry; an overlap one
+ falls back only after `RoleGone`.
+- D9's running-replicator check (R2-3): a move is deferred while
+ `carlos-litestream` is active and started before the config's mtime, or
+ its config names the file; it proceeds when the replicator is inactive or
+ restarted on a config without it.
- 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
@@ -1376,20 +1403,15 @@ Unit and package level:
`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
+- (follow-up) 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 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.
+- Console: the downgrade guard's worker floor. (D17's door tests go with
+ the follow-up.)
+- `systemapply`: a candidate without the worker template, or below
+ `WorkerLayoutFloor`, 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):
@@ -1404,7 +1426,7 @@ and systemd):
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
+- (follow-up) 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
@@ -1427,44 +1449,27 @@ packages' tests.
---
-## 11. Open questions for Paul
-
-1. **Pause sidecar creation now?** Turning `CONSOLE_REQUIRE_ISOLATED_WORKERS`
- on before any box reports nested workers refuses every sidecar create
- 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. 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 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.
-
----
+## 11. Questions for Paul, and his rulings (2026-10-24)
+
+1. **Pause sidecar creation now?** Ruled: no ("don't worry about pausing").
+ The console check (D17) is deferred to a follow-up (§13).
+2. **Treat what the flat tier could reach as exposed?** Still Paul's
+ incident call, outside this build. Every sidecar ever created was the
+ operator's (#370); flat instance rows had the same reach, and before the
+ fence nothing stopped metadata. Release N's inventory shows which tenant
+ hosts ran flat on each box.
+3. **The operator app the fence exempts:** moves with everything else; no
+ box carries an allowlist entry (recommendation taken under "just do
+ this").
+4. **Land D9 ahead of the rest?** Ruled by "just do this": D9 ships in
+ release N with the rest, as one release, rather than separately.
+5. **A third protocol floor?** Superseded: revision 3's D11 needs none
+ (R2-2).
+6. **How defaults reach existing boxes:** nothing that runs is stopped by a
+ default change (recommendation taken). Revision 3 does it with the
+ ratchet of D15 instead of keeping old defaults on established boxes,
+ because the ratchet needs no per-box `host.env` change and turns every
+ box once it is safe.
## 12. Review disposition
@@ -1499,3 +1504,33 @@ 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.
+
+
+### 12.2 Round 2 (Astra, `CODEX-FINDINGS-SIDECAR-ISOLATION-R2.md`, at 730ba093)
+
+Verdict: reject. Round 1's findings fixed or fixed in part; R1-4's decline
+judged reasonable. Citations spot-checked and held.
+
+| Finding | | Disposition | Where |
+|---|---|---|---|
+| R2-1 | P1: an uncertain hold replacement falls back over a migrated database | **Accepted.** The worker arm's uncertain start answers "may have run" to `startMayHaveRun` too; `ReplaceWorker` treats it as executed: a hold attempt restores nothing and stops the entry through the fenced stop and `RoleGone` | D19, §3.5.1 |
+| R2-2 | P1: the root guard's copy ignores a new floor | **Accepted, differently.** No new floor: workers move only once `carlos.prev` is itself worker-capable, and install keeps both files at or above the floor; the guard picks one of those two, so it needs no change | D11, §3.11 |
+| R2-3 | P1: a regenerated config is not proof the replicator let go | **Accepted.** An always-on worker's move waits until the running replicator started after a config that does not name the file | D9, §3.9 |
+| R2-4 | P2: defaults change during the rollout window | **Accepted, by Paul's ruling (Q6).** No release flips a default behind a console guard; the flat-exec default is a per-box ratchet that turns only when nothing would be refused | D15, §4.4 |
+| R2-5 | P2: D17 drops stale boxes that still run workers flat | **Deferred with D17.** The console check is not the boundary and is out of this release; the follow-up takes R2-5's fix (stale eligible boxes block) | §5, §13 |
+
+---
+
+## 13. What release N carries, and what follows
+
+**Release N (and its patch N.1, §8.2):** D1 to D14, D16 and D19, with D11
+and D15 as in revision 3. That closes B1: a native sidecar runs nested on
+every box whose `carlos.prev` qualifies, and once a box's ratchet turns,
+nothing on it runs in the host namespace unless an operator allowlists it.
+
+**Follow-ups, each its own branch, filed as amadan discussions:**
+
+- **D17**, the console's create-time check, with R2-5's fix.
+- **D18**, retiring the interim fence: the process census in status, then
+ the carloku-infrastructure change under its own scoped approval.
+- Copy and docs for anything those add.