carlos / platform Public

rootfs apps: copy deck approved by delegation (Luna pass applied, 372 rows)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4e33daea857da852e41ad230296365cce4341f63 parent 1db3264
8 files changed, +495 −491
  • docs/superpowers/plans/2026-10-24-rootfs-apps-0-index.md +1 −1
  • docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.md +89 −87
  • docs/superpowers/plans/2026-10-24-rootfs-apps-2-box-isolation.md +3 −3
  • docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.md +1 −1
  • docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.md +16 −16
  • docs/superpowers/plans/2026-10-24-rootfs-apps-5-egress-allow.md +7 −7
  • docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.md +3 −3
  • docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md +375 −373

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.013s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (1 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (146 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.821s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.964s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.749s
ok  	github.com/carlosframework/platform/internal/activator	60.022s
ok  	github.com/carlosframework/platform/internal/agentsock	0.027s
ok  	github.com/carlosframework/platform/internal/alerting	0.008s
ok  	github.com/carlosframework/platform/internal/analytics	0.020s
ok  	github.com/carlosframework/platform/internal/appconfig	2.993s
ok  	github.com/carlosframework/platform/internal/apperr	0.147s
ok  	github.com/carlosframework/platform/internal/applog	0.217s
ok  	github.com/carlosframework/platform/internal/awsinv	0.010s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.008s
ok  	github.com/carlosframework/platform/internal/boxid	0.840s
?   	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.009s
ok  	github.com/carlosframework/platform/internal/caps	0.018s
ok  	github.com/carlosframework/platform/internal/certwarm	0.006s
ok  	github.com/carlosframework/platform/internal/cgroup	0.786s
ok  	github.com/carlosframework/platform/internal/changelog	0.558s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.240s
ok  	github.com/carlosframework/platform/internal/clamd	0.056s
?   	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.981s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.012s
ok  	github.com/carlosframework/platform/internal/dbquery	0.006s
ok  	github.com/carlosframework/platform/internal/dbreads	2.101s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.007s
ok  	github.com/carlosframework/platform/internal/docsite	0.007s
ok  	github.com/carlosframework/platform/internal/domains	2.953s
ok  	github.com/carlosframework/platform/internal/doorkey	0.012s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	45.609s
?   	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.196s
ok  	github.com/carlosframework/platform/internal/gate	1.935s
ok  	github.com/carlosframework/platform/internal/genselect	0.017s
ok  	github.com/carlosframework/platform/internal/geo	0.020s
ok  	github.com/carlosframework/platform/internal/geoip	0.048s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.315s
ok  	github.com/carlosframework/platform/internal/host	143.198s
ok  	github.com/carlosframework/platform/internal/hostactors	5.525s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.008s
ok  	github.com/carlosframework/platform/internal/hostbox	3.239s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.862s
ok  	github.com/carlosframework/platform/internal/hostenv	0.006s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.007s
ok  	github.com/carlosframework/platform/internal/hostexec	5.627s
ok  	github.com/carlosframework/platform/internal/hostfence	0.046s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.281s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.017s
ok  	github.com/carlosframework/platform/internal/hostguard	1.048s
ok  	github.com/carlosframework/platform/internal/hostops	2.250s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.618s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.199s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.003s
ok  	github.com/carlosframework/platform/internal/infraview	0.005s
ok  	github.com/carlosframework/platform/internal/instancectl	5.237s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.793s
ok  	github.com/carlosframework/platform/internal/instancefs	0.006s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.010s
ok  	github.com/carlosframework/platform/internal/layout	1.943s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.004s
ok  	github.com/carlosframework/platform/internal/lease	10.118s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.699s
ok  	github.com/carlosframework/platform/internal/ledger	0.031s
ok  	github.com/carlosframework/platform/internal/mail	0.222s
ok  	github.com/carlosframework/platform/internal/maintenance	0.087s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.554s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.087s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.462s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.130s
ok  	github.com/carlosframework/platform/internal/objstore	0.271s
ok  	github.com/carlosframework/platform/internal/ops	0.185s
ok  	github.com/carlosframework/platform/internal/park	0.300s
ok  	github.com/carlosframework/platform/internal/plans	0.006s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	14.694s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.011s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.687s
ok  	github.com/carlosframework/platform/internal/relay	7.228s
ok  	github.com/carlosframework/platform/internal/release	25.709s
ok  	github.com/carlosframework/platform/internal/rollout	3.818s
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.061s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.549s
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.711s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.884s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.118s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.107s
ok  	github.com/carlosframework/platform/internal/status	0.111s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.177s
ok  	github.com/carlosframework/platform/internal/svcrole	0.003s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.005s
ok  	github.com/carlosframework/platform/internal/unixdial	0.006s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.690s
=== internal_console@0 (87 s)
ok  	github.com/carlosframework/platform/internal/console	84.703s
=== internal_console@1 (44 s)
ok  	github.com/carlosframework/platform/internal/console	41.847s
=== internal_console@2 (42 s)
ok  	github.com/carlosframework/platform/internal/console	39.865s
=== internal_console@3 (41 s)
ok  	github.com/carlosframework/platform/internal/console	38.319s
=== internal_console@4 (42 s)
ok  	github.com/carlosframework/platform/internal/console	40.389s
=== internal_console@5 (41 s)
ok  	github.com/carlosframework/platform/internal/console	38.800s
=== internal_console@6 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.550s
=== internal_console@7 (46 s)
ok  	github.com/carlosframework/platform/internal/console	43.787s
=== root@0 (110 s)
ok  	github.com/carlosframework/platform	107.892s
=== root@1 (84 s)
ok  	github.com/carlosframework/platform	82.373s
=== root@2 (92 s)
ok  	github.com/carlosframework/platform	89.836s
=== 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 19:52:59 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E18F33D024C768, 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.022s
=== 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.033s
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
=== RUN   TestDBForwardMinio
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 19:53:01 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 19:53:01 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 19:53:01 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 19:53:01 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 19:53:01 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 19:53:01 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 19:53:01 console: could not notify member@example.com of a CLI token issue: keymail: lookup member@example.com: keymail: GET /api/federation/lookup?addr=member%40example.com&from=console%40127.0.0.1%3A41181: could not determine target mailbox
    agent.go:135: dbreads: dbr_rl7cw2yweqk6vewtuobqpbgvca: planned 3 files (1544 bytes) and restored to 3 in 10ms
    agent.go:135: dbreads: dbr_rl7cw2yweqk6vewtuobqpbgvca: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 19:53:02 console: db read dbr_rl7cw2yweqk6vewtuobqpbgvca: relayed 1 chunks (102 bytes) in 1.004s, ended done
    agent.go:135: dbreads: dbr_xhfivwdhkzv336obzcltulzvpu: planned 1 files (720 bytes) and restored to 1 in 8ms
    agent.go:135: dbreads: dbr_xhfivwdhkzv336obzcltulzvpu: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 19:53:03 console: db read dbr_xhfivwdhkzv336obzcltulzvpu: relayed 1 chunks (102 bytes) in 1.003s, ended done
    agent.go:135: dbreads: dbr_tbiwvodpruxptgzz5m5nhv2he4: planned 3 files (1544 bytes) and held through 3 in 10ms
    agent.go:135: dbreads: dbr_tbiwvodpruxptgzz5m5nhv2he4: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 19:53:04 console: db read dbr_tbiwvodpruxptgzz5m5nhv2he4: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN   TestDBForwardMinio/by_disconnect
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_rilrk4qbj64ilai7krxhm76j6i: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 19:53:04 console: db read dbr_rilrk4qbj64ilai7krxhm76j6i: relayed 0 chunks (0 bytes) in 13ms, ended client gone
    agent.go:135: dbreads: dbr_rilrk4qbj64ilai7krxhm76j6i: output 5 bytes in 1 confirmed chunks in 19ms
=== RUN   TestDBForwardMinio/by_timeout
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_txlu2ca5biqa6673tgdy4fvzu4: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 19:53:25 console: db read dbr_txlu2ca5biqa6673tgdy4fvzu4: relayed 0 chunks (0 bytes) in 19.982s, ended no-result
    agent.go:135: dbreads: dbr_txlu2ca5biqa6673tgdy4fvzu4: output 5 bytes in 1 confirmed chunks in 19.958s
--- PASS: TestDBForwardMinio (23.34s)
    --- PASS: TestDBForwardMinio/by_disconnect (0.14s)
    --- PASS: TestDBForwardMinio/by_timeout (20.09s)
PASS
ok  	github.com/carlosframework/platform/internal/console	23.357s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-0-index.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-0-index.md
index 8c873a582..a906ac561 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-0-index.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-0-index.md
@@ -23,7 +23,7 @@ plans cite the section beside the task that answers it.
**Copy:** every user-facing string of the series goes in one deck,
`2026-10-24-rootfs-apps-copy.md`, assembled from the string tables each plan
carries (IDs `RFS-<plan>-C<nn>`). Plan 1 Task 1 puts the gate in place and
-gets Paul's review before any task writes a string. Copy voice: no em
+gets Paul's review before any task writes a string (the deck was approved by delegation on 2026-10-24). Copy voice: no em
dashes; neutral names only (`shop`, `example.com`), never a tenant's, in
tests too. IDs match `^RFS-[1-6]-C(0[1-9]|[1-9][0-9]{1,2})$`: two digits
to C99, three from C100 (Plan 4 runs to C138), no leading zero (so Plan 2's
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.md
index 869ba7d77..9f951d140 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.md
@@ -57,7 +57,7 @@ None renames or changes a contract name. These are additions the later plans mus
## Global Constraints
-- Every user-facing string (CLI output, refusals, flag help, console messages) is copy-reviewed by Paul before it is written into code. The deck is `docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md`; its gate is `approved_rootfs_copy_test.go` (Task 1). A task uses only approved rows from this plan's table (RFS-1-C01 to RFS-1-C85) and adds their ids to `rootfsCopyBuilt` in the same commit. Texts in the tests below are the pending texts; write Paul's approved text instead.
+- Every user-facing string (CLI output, refusals, flag help, console messages) is copy-reviewed by Paul before it is written into code. The deck is `docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md`; its gate is `approved_rootfs_copy_test.go` (Task 1). A task uses only approved rows from this plan's table (RFS-1-C01 to RFS-1-C85) and adds their ids to `rootfsCopyBuilt` in the same commit. The deck was approved by delegation on 2026-10-24 (Paul asked for a Luna copy pass instead of a string-by-string review), and the texts in this plan's table and tests are the approved texts.
- A deck string appears in Go as one string literal, or one `fmt.Sprintf`/`fmt.Errorf`/`errors.New` format, equal to the row with `%s`/`%d`/`%q` where the row has a `<placeholder>`, no trailing newline inside the literal.
- Copy voice: no em dashes (a full stop and a new sentence, lower case after it). Neutral names only: `shop`, `example.com`, `bab` for the ops account in tests. Never a tenant's name, in tests too.
- Wire shapes are additive (AGENTS.md): the new manifest fields and meta keys are `omitempty`; a binary or static manifest marshals byte for byte as before.
@@ -127,91 +127,91 @@ Placeholders: `<app>` an app name; `<name>` a process name; `<var>` an environme
| id | file | text | status |
|---|---|---|---|
-| RFS-1-C01 | internal/release/rootfs.go | the processes declare no web process. A rootfs release needs exactly one, named web. | pending |
-| RFS-1-C02 | internal/release/rootfs.go | <name> is not a process name. Use web, release, console, or a worker name: lower-case letters, digits and hyphens, up to 16 characters, starting with a letter. | pending |
-| RFS-1-C03 | internal/release/rootfs.go | <name> is reserved, so no process can use that name. | pending |
-| RFS-1-C04 | internal/release/rootfs.go | a rootfs release can declare at most 4 worker processes, and this one declares <n>. | pending |
-| RFS-1-C05 | internal/release/rootfs.go | the <name> process needs a command: a list of words whose first word is not empty. | pending |
-| RFS-1-C06 | internal/release/rootfs.go | the <name> process sets <var> in env, which is not a variable name. | pending |
-| RFS-1-C07 | internal/release/rootfs.go | the <name> process sets <var> in env. Names that start with CARLOS_ belong to the platform. | pending |
-| RFS-1-C08 | internal/release/rootfs.go | the <name> process's working_dir must be an absolute path, and <dir> is not. | pending |
-| RFS-1-C09 | internal/release/rootfs.go | only the release process takes timeout_sec. | pending |
-| RFS-1-C10 | internal/release/rootfs.go | the release process's timeout_sec must be between 1 and <max> seconds. | pending |
-| RFS-1-C11 | internal/release/rootfs.go | the <name> process contains a NUL byte, which no command or variable can hold. | pending |
-| RFS-1-C12 | internal/release/rootfs.go | the processes are <n> bytes, over the limit of <max>. | pending |
-| RFS-1-C13 | internal/release/rootfs.go | processes, hibernate and jit belong to rootfs releases only. | pending |
-| RFS-1-C14 | internal/release/rootfs.go | a rootfs release carries one artifact, rootfs-linux-arm64, and nothing else. | pending |
-| RFS-1-C15 | internal/release/rootfs.go | a rootfs release cannot declare capabilities. | pending |
-| RFS-1-C16 | internal/release/rootfs.go | malformed processes block. Want the processes object: web, any workers, release and console. | pending |
-| RFS-1-C17 | internal/release/rootfs.go | processes: null. A rootfs release needs its processes, starting with web. | pending |
-| RFS-1-C18 | internal/release/rootfs_ship.go | <app> ships <kind> releases (<version> is one), so it cannot take a rootfs release. Ship rootfs releases to an app of their own, made with carlos apps create. | pending |
-| RFS-1-C19 | internal/release/rootfs_ship.go | <app> ships rootfs releases (<version> is one), so it cannot take a <kind> release. Ship that release to another app. | pending |
-| RFS-1-C20 | internal/release/rootfs_ship.go | <app>'s rootfs releases are always on (<version> is one), so this release cannot opt into hibernation. Ship it without --hibernate, or use a new app. | pending |
-| RFS-1-C21 | internal/release/rootfs_ship.go | <app>'s rootfs releases hibernate (<version> is one), so this release must too. Ship it with --hibernate, or use a new app. | pending |
-| RFS-1-C22 | internal/console/apicli.go | invalid kind (want "binary", "static" or "rootfs") | pending |
-| RFS-1-C23 | internal/console/apicli.go (a fragment of the temporary-app refusal) | ship a rootfs release to it | pending |
-| RFS-1-C24 | internal/console/apiship_rootfs.go | <app> is a control-plane app, so its releases are binaries only. | pending |
-| RFS-1-C25 | internal/console/apiship_rootfs.go | the request does not say how large the rootfs image is, so nothing was read. Update carlos and ship again. | pending |
-| RFS-1-C26 | internal/console/apiship_rootfs.go | the rootfs image is over 2 GiB, the most one release can carry. Make the image smaller and ship again. | pending |
-| RFS-1-C27 | internal/console/apiship_rootfs.go | a rootfs release ships exactly one file part, named "rootfs-linux-arm64" | pending |
-| RFS-1-C28 | internal/console/apiship_rootfs.go | the rootfs part does not match the size the request declared, so nothing was shipped. | pending |
-| RFS-1-C29 | internal/console/apiship_rootfs.go | version <version> was already shipped with different processes, hibernation or jit. Versions are immutable, so ship under a new version. | pending |
-| RFS-1-C30 | cli.go | an OCI image (a layout directory, a docker save archive or a local image name) or a directory, shipped as a rootfs release | pending |
-| RFS-1-C31 | cli.go | JSON file holding the rootfs release's processes: web, any workers, release and console | pending |
-| RFS-1-C32 | cli.go | let this rootfs app hibernate when idle. Rootfs apps are always on unless their first release opts in | pending |
-| RFS-1-C33 | cli.go | run the rootfs image here with Docker or Podman, the way a box would, and ship nothing | pending |
-| RFS-1-C34 | ship_rootfs.go | usage: carlos ship --app <a> --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] [--version <sha>] [--label <one line>] [--notes <text> \| --notes-file <file>] [--swap hold] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]\n carlos ship --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] --check | pending |
-| RFS-1-C35 | ship_rootfs.go | --rootfs cannot be combined with --<flag>. | pending |
-| RFS-1-C36 | ship_rootfs.go | --rootfs takes the image or directory as its value, so leave the other arguments off. | pending |
-| RFS-1-C37 | ship_rootfs.go | --rootfs needs --processes, a JSON file holding the release's processes. | pending |
-| RFS-1-C38 | ship_rootfs.go | --<flag> works only with --rootfs. | pending |
-| RFS-1-C39 | ship_rootfs.go | --processes <file>: <reason> | pending |
-| RFS-1-C40 | ship_rootfs.go | filled in console: <command>, because this looks like a <framework> app. Declare console in --processes to choose another. | pending |
-| RFS-1-C41 | ship_rootfs.go | left out <n> device files, sockets and pipes: a rootfs image cannot carry them. | pending |
-| RFS-1-C42 | ship_rootfs.go | built the rootfs image: <size>, <n> files. | pending |
-| RFS-1-C43 | ship_rootfs.go | the rootfs image is <size>, over the 2 GiB one release can carry. Make the image smaller and ship again. | pending |
-| RFS-1-C44 | apimode_rootfs.go | this console does not take rootfs releases yet. Update the console, then ship again. | pending |
-| RFS-1-C45 | internal/rootfsimage/errors.go | this image is built for <platform>, and boxes run linux/arm64. Rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64. | pending |
-| RFS-1-C46 | internal/rootfsimage/errors.go | this image has no linux/arm64 variant (it has <platforms>). Rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64. | pending |
-| RFS-1-C47 | internal/rootfsimage/errors.go | <path> is a program built for <platform>, and boxes run linux/arm64. Build the directory's programs for linux/arm64. | pending |
-| RFS-1-C48 | internal/rootfsimage/errors.go | the <name> process runs <command>, which the image does not have. Commands are looked up in the process's PATH, or from its working_dir when they contain a slash. | pending |
-| RFS-1-C49 | ship_runtime.go | <source> is not a file, a directory or a local image. | pending |
-| RFS-1-C50 | internal/rootfsimage/errors.go | <source> is not an OCI image, a docker save archive or a directory. | pending |
-| RFS-1-C51 | internal/rootfsimage/errors.go | the image has an entry outside its root (<path>), so it was not shipped. | pending |
-| RFS-1-C52 | internal/rootfsimage/errors.go | the image has <path> as a <type>, and boxes mount a directory there. Remove it from the image. | pending |
-| RFS-1-C53 | internal/rootfsimage/errors.go | the image links <path> to <target>, which is not a file in the image. | pending |
-| RFS-1-C54 | internal/rootfsimage/errors.go | the image sets <var> in its environment. Names that start with CARLOS_ belong to the platform, so remove it from the image. | pending |
-| RFS-1-C55 | internal/rootfsimage/errors.go | the archive holds <n> images. Save one image and ship that. | pending |
-| RFS-1-C56 | internal/rootfsimage/dir_other.go | a directory can be shipped as a rootfs only from Linux or macOS, which keep file modes. Ship an OCI image instead. | pending |
-| RFS-1-C57 | ship_runtime.go | --check runs the image with Docker or Podman, and neither is installed here. | pending |
-| RFS-1-C58 | ship_runtime.go | <source> is not a file or a directory, and there is no Docker or Podman here to export it as an image. | pending |
-| RFS-1-C59 | ship_runtime.go | could not export <image> with <runtime>: <reason> | pending |
-| RFS-1-C60 | ship_check.go | checking the image with <runtime>: read-only root, <memory> of memory, <seconds> s for web to answer. | pending |
-| RFS-1-C61 | ship_check.go | this machine is <arch>, so the arm64 image runs under emulation and starts slower than on a box. | pending |
-| RFS-1-C62 | ship_check.go | the arm64 image cannot run here. Install emulation for arm64 (qemu and binfmt), or run the check on an arm64 machine. | pending |
-| RFS-1-C63 | ship_check.go | release finished in <duration>. | pending |
-| RFS-1-C64 | ship_check.go | release exited with status <code>. Its last lines: | pending |
-| RFS-1-C65 | ship_check.go | release did not finish within <duration>. | pending |
-| RFS-1-C66 | ship_check.go | web answered GET / with <status> after <duration>. | pending |
-| RFS-1-C67 | ship_check.go | web did not answer GET / on CARLOS_SOCKET within <duration>. | pending |
-| RFS-1-C68 | ship_check.go | web listens on port <ports> instead of the socket. Serve HTTP on the unix socket at CARLOS_SOCKET. | pending |
-| RFS-1-C69 | ship_check.go | web answered GET / with <status>. Health needs an answer below 500. | pending |
-| RFS-1-C70 | ship_check.go | <name> exited with status <code>. Its last lines: | pending |
-| RFS-1-C71 | ship_check.go | <name> ran out of memory. A rootfs instance's tier allows <memory>. | pending |
-| RFS-1-C72 | ship_check.go | <name> tried to write outside its instance directory. The root is read-only, so write under <dir> or /tmp. | pending |
-| RFS-1-C73 | ship_check.go | <name> is still running after <duration>. | pending |
-| RFS-1-C74 | ship_check.go | the image passed the local check. A box also applies its egress fence and its own limits, so this is a convenience, not a guarantee. | pending |
-| RFS-1-C75 | ship_check.go | the image failed the local check. | pending |
-| RFS-1-C76 | ship_check_probe.go | --check needs a linux build of carlos to probe the socket from inside the container, and this one is a development build. Use a released carlos, or set CARLOS_CHECK_PROBE to a linux carlos binary. | pending |
-| RFS-1-C77 | ship_check_probe.go | could not fetch the linux build of carlos for the check: <reason> | pending |
-| RFS-1-C78 | internal/rootfsimage/errors.go | the image's layer <digest> does not match its digest, so it was not shipped. | pending |
-| RFS-1-C79 | internal/console/apicli.go | could not check this app's releases. Nothing was shipped. Try again. | pending |
-| RFS-1-C80 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for <kind> releases, so it cannot take a rootfs release. Ship rootfs releases to an app of their own, made with carlos apps create. | pending |
-| RFS-1-C81 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for rootfs releases, so it cannot take a <kind> release. Ship that release to another app. | pending |
-| RFS-1-C82 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases always on, so this release cannot opt into hibernation. Ship it without --hibernate, or use a new app. | pending |
-| RFS-1-C83 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases hibernate, so this release must too. Ship it with --hibernate, or use a new app. | pending |
-| RFS-1-C84 | cli.go | let this rootfs release's processes run a JIT compiler, as Node, Bun, Deno, the JVM and Ruby's YJIT need. Without it a box never lets memory be writable and executable at once | pending |
-| RFS-1-C85 | ship_check.go | the <name> process runs <program>, which needs a JIT, and this release does not declare one. It passes here but stops at start on a box. Ship it with --jit. | pending |
+| RFS-1-C01 | internal/release/rootfs.go | the processes declare no web process. a rootfs release needs exactly one, named web | approved |
+| RFS-1-C02 | internal/release/rootfs.go | <name> is not a process name. use web, release, console, or a worker name: lower-case letters, digits and hyphens, up to 16 characters, starting with a letter | approved |
+| RFS-1-C03 | internal/release/rootfs.go | <name> is reserved, so no process can use that name | approved |
+| RFS-1-C04 | internal/release/rootfs.go | a rootfs release can declare at most 4 worker processes, and this one declares <n> | approved |
+| RFS-1-C05 | internal/release/rootfs.go | the <name> process needs a command: a list of words whose first word is not empty | approved |
+| RFS-1-C06 | internal/release/rootfs.go | the <name> process sets <var> in env, which is not a variable name | approved |
+| RFS-1-C07 | internal/release/rootfs.go | the <name> process sets <var> in env. names that start with CARLOS_ belong to the platform | approved |
+| RFS-1-C08 | internal/release/rootfs.go | the <name> process's working_dir must be an absolute path, and <dir> is not | approved |
+| RFS-1-C09 | internal/release/rootfs.go | only the release process takes timeout_sec | approved |
+| RFS-1-C10 | internal/release/rootfs.go | the release process's timeout_sec must be between 1 and <max> seconds | approved |
+| RFS-1-C11 | internal/release/rootfs.go | the <name> process contains a NUL byte, which no command or variable can hold | approved |
+| RFS-1-C12 | internal/release/rootfs.go | the processes are <n> bytes, over the limit of <max> | approved |
+| RFS-1-C13 | internal/release/rootfs.go | processes, hibernate and jit belong to rootfs releases only | approved |
+| RFS-1-C14 | internal/release/rootfs.go | a rootfs release carries one artifact, rootfs-linux-arm64, and nothing else | approved |
+| RFS-1-C15 | internal/release/rootfs.go | a rootfs release cannot declare capabilities | approved |
+| RFS-1-C16 | internal/release/rootfs.go | malformed processes block. want the processes object: web, any workers, release and console | approved |
+| RFS-1-C17 | internal/release/rootfs.go | processes: null. a rootfs release needs its processes, starting with web | approved |
+| RFS-1-C18 | internal/release/rootfs_ship.go | <app> ships <kind> releases (<version> is one), so it cannot take a rootfs release. ship rootfs releases to an app of their own, made with carlos apps create | approved |
+| RFS-1-C19 | internal/release/rootfs_ship.go | <app> ships rootfs releases (<version> is one), so it cannot take a <kind> release. ship that release to another app | approved |
+| RFS-1-C20 | internal/release/rootfs_ship.go | <app>'s rootfs releases are always on (<version> is one), so this release cannot opt into hibernation. ship it without --hibernate, or use a new app | approved |
+| RFS-1-C21 | internal/release/rootfs_ship.go | <app>'s rootfs releases hibernate (<version> is one), so this release must too. ship it with --hibernate, or use a new app | approved |
+| RFS-1-C22 | internal/console/apicli.go | invalid kind (want "binary", "static" or "rootfs") | approved |
+| RFS-1-C23 | internal/console/apicli.go (a fragment of the temporary-app refusal) | ship a rootfs release to it | approved |
+| RFS-1-C24 | internal/console/apiship_rootfs.go | <app> is a control-plane app, so its releases are binaries only | approved |
+| RFS-1-C25 | internal/console/apiship_rootfs.go | the request does not say how large the rootfs image is, so nothing was read. update carlos and ship again | approved |
+| RFS-1-C26 | internal/console/apiship_rootfs.go | the rootfs image is over 2 GiB, the most one release can carry. make the image smaller and ship again | approved |
+| RFS-1-C27 | internal/console/apiship_rootfs.go | a rootfs release ships exactly one file part, named "rootfs-linux-arm64" | approved |
+| RFS-1-C28 | internal/console/apiship_rootfs.go | the rootfs part does not match the size the request declared, so nothing was shipped | approved |
+| RFS-1-C29 | internal/console/apiship_rootfs.go | version <version> was already shipped with different processes, hibernation or jit. versions are immutable, so ship under a new version | approved |
+| RFS-1-C30 | cli.go | an OCI image (a layout directory, a docker save archive or a local image name) or a directory, shipped as a rootfs release | approved |
+| RFS-1-C31 | cli.go | JSON file holding the rootfs release's processes: web, any workers, release and console | approved |
+| RFS-1-C32 | cli.go | let this rootfs app hibernate when idle. Rootfs apps are always on unless their first release opts in | approved |
+| RFS-1-C33 | cli.go | run the rootfs image here with Docker or Podman, the way a box would, and ship nothing | approved |
+| RFS-1-C34 | ship_rootfs.go | usage: carlos ship --app <a> --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] [--version <sha>] [--label <one line>] [--notes <text> \| --notes-file <file>] [--swap hold] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]\n carlos ship --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] --check | approved |
+| RFS-1-C35 | ship_rootfs.go | --rootfs cannot be combined with --<flag> | approved |
+| RFS-1-C36 | ship_rootfs.go | --rootfs takes the image or directory as its value, so leave the other arguments off | approved |
+| RFS-1-C37 | ship_rootfs.go | --rootfs needs --processes, a JSON file holding the release's processes | approved |
+| RFS-1-C38 | ship_rootfs.go | --<flag> works only with --rootfs | approved |
+| RFS-1-C39 | ship_rootfs.go | --processes <file>: <reason> | approved |
+| RFS-1-C40 | ship_rootfs.go | filled in console: <command>, because this looks like a <framework> app. declare console in --processes to choose another | approved |
+| RFS-1-C41 | ship_rootfs.go | left out <n> device files, sockets and pipes: a rootfs image cannot carry them | approved |
+| RFS-1-C42 | ship_rootfs.go | built the rootfs image: <size>, <n> files. | approved |
+| RFS-1-C43 | ship_rootfs.go | the rootfs image is <size>, over the 2 GiB one release can carry. make the image smaller and ship again | approved |
+| RFS-1-C44 | apimode_rootfs.go | this console does not take rootfs releases yet. update the console, then ship again | approved |
+| RFS-1-C45 | internal/rootfsimage/errors.go | this image is built for <platform>, and boxes run linux/arm64. rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64 | approved |
+| RFS-1-C46 | internal/rootfsimage/errors.go | this image has no linux/arm64 variant (it has <platforms>). rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64 | approved |
+| RFS-1-C47 | internal/rootfsimage/errors.go | <path> is a program built for <platform>, and boxes run linux/arm64. build the directory's programs for linux/arm64 | approved |
+| RFS-1-C48 | internal/rootfsimage/errors.go | the <name> process runs <command>, which the image does not have. commands are looked up in the process's PATH, or from its working_dir when they contain a slash | approved |
+| RFS-1-C49 | ship_runtime.go | <source> is not a file, a directory or a local image | approved |
+| RFS-1-C50 | internal/rootfsimage/errors.go | <source> is not an OCI image, a docker save archive or a directory | approved |
+| RFS-1-C51 | internal/rootfsimage/errors.go | the image has an entry outside its root (<path>), so it was not shipped | approved |
+| RFS-1-C52 | internal/rootfsimage/errors.go | the image has <path> as a <type>, and boxes mount a directory there. remove it from the image | approved |
+| RFS-1-C53 | internal/rootfsimage/errors.go | the image links <path> to <target>, which is not a file in the image | approved |
+| RFS-1-C54 | internal/rootfsimage/errors.go | the image sets <var> in its environment. names that start with CARLOS_ belong to the platform, so remove it from the image | approved |
+| RFS-1-C55 | internal/rootfsimage/errors.go | the archive holds <n> images. save one image and ship that | approved |
+| RFS-1-C56 | internal/rootfsimage/dir_other.go | a directory can be shipped as a rootfs only from Linux or macOS, which keep file modes. ship an OCI image instead | approved |
+| RFS-1-C57 | ship_runtime.go | --check runs the image with Docker or Podman, and neither is installed here | approved |
+| RFS-1-C58 | ship_runtime.go | <source> is not a file or a directory, and there is no Docker or Podman here to export it as an image | approved |
+| RFS-1-C59 | ship_runtime.go | could not export <image> with <runtime>: <reason> | approved |
+| RFS-1-C60 | ship_check.go | checking the image with <runtime>: read-only root, <memory> of memory, <seconds> s for web to answer | approved |
+| RFS-1-C61 | ship_check.go | this machine is <arch>, so the arm64 image runs under emulation and starts slower than on a box | approved |
+| RFS-1-C62 | ship_check.go | the arm64 image cannot run here. install emulation for arm64 (qemu and binfmt), or run the check on an arm64 machine | approved |
+| RFS-1-C63 | ship_check.go | release finished in <duration> | approved |
+| RFS-1-C64 | ship_check.go | release exited with status <code>. its last lines: | approved |
+| RFS-1-C65 | ship_check.go | release did not finish within <duration> | approved |
+| RFS-1-C66 | ship_check.go | web answered GET / with <status> after <duration> | approved |
+| RFS-1-C67 | ship_check.go | web did not answer GET / on CARLOS_SOCKET within <duration> | approved |
+| RFS-1-C68 | ship_check.go | web listens on port <ports> instead of the socket. serve HTTP on the unix socket at CARLOS_SOCKET | approved |
+| RFS-1-C69 | ship_check.go | web answered GET / with <status>. health needs an answer below 500 | approved |
+| RFS-1-C70 | ship_check.go | <name> exited with status <code>. its last lines: | approved |
+| RFS-1-C71 | ship_check.go | <name> ran out of memory. a rootfs instance's tier allows <memory> | approved |
+| RFS-1-C72 | ship_check.go | <name> tried to write outside its instance directory. the root is read-only, so write under <dir> or /tmp | approved |
+| RFS-1-C73 | ship_check.go | <name> is still running after <duration> | approved |
+| RFS-1-C74 | ship_check.go | the image passed the local check. a box also applies its egress fence and its own limits, so this is a convenience, not a guarantee | approved |
+| RFS-1-C75 | ship_check.go | the image failed the local check | approved |
+| RFS-1-C76 | ship_check_probe.go | --check needs a linux build of carlos to probe the socket from inside the container, and this one is a development build. use a released carlos, or set CARLOS_CHECK_PROBE to a linux carlos binary | approved |
+| RFS-1-C77 | ship_check_probe.go | could not fetch the linux build of carlos for the check: <reason> | approved |
+| RFS-1-C78 | internal/rootfsimage/errors.go | the image's layer <digest> does not match its digest, so it was not shipped | approved |
+| RFS-1-C79 | internal/console/apicli.go | could not check this app's releases. nothing was shipped. try again | approved |
+| RFS-1-C80 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for <kind> releases, so it cannot take a rootfs release. ship rootfs releases to an app of their own, made with carlos apps create | approved |
+| RFS-1-C81 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for rootfs releases, so it cannot take a <kind> release. ship that release to another app | approved |
+| RFS-1-C82 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases always on, so this release cannot opt into hibernation. ship it without --hibernate, or use a new app | approved |
+| RFS-1-C83 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases hibernate, so this release must too. ship it with --hibernate, or use a new app | approved |
+| RFS-1-C84 | cli.go | let this rootfs release's processes run a JIT compiler, as Node, Bun, Deno, the JVM and Ruby's YJIT need. without it a box never lets memory be writable and executable at once | approved |
+| RFS-1-C85 | ship_check.go | the <name> process runs <program>, which needs a JIT, and this release does not declare one. it passes here but stops at start on a box. ship it with --jit | approved |
RFS-1-C78's `<digest>` is a `sha256:<hex>` digest. RFS-1-C80 to C83 are the first-ship forms of C18 to C21: the app has no other release, and its kind marker (deviation 10) names the ship that fixed its kind, which may not have published, so they never say "is one". RFS-1-C85 is a warning: it does not fail the check. Kept out of the table on purpose (diagnostics, not human text): the probe's one-line JSON on stdout, `validateManifest`'s existing terse messages, store errors wrapped for logs.
@@ -341,6 +341,8 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
- [ ] **Step 4: Paul's copy review**
+Note: the deck was approved by delegation on 2026-10-24 (Paul asked for a Luna copy pass instead of a string-by-string review; see the deck header), so the gate's approval step is satisfied. Do not launch the review again for this deck; Step 5's edits are already applied and every row is `approved`, so skip to the Run line under Step 5. A row added after 2026-10-24 still goes through the review.
+
Invoke the `copy-review` skill with every live row of the deck (every row not `struck`; none is at this point), grouped by plan and, within this plan, by the file column, with the placeholder key above as context. Paul reviews the whole series in one pass. **Stop here until Paul has submitted the review.** Read the review's `result.json` before restarting the review server for any reason: a relaunch deletes it.
- [ ] **Step 5: Apply the review verbatim**
@@ -429,7 +431,7 @@ func TestValidateProcesses(t *testing.T) {
{"empty command", map[string]Process{"web": {}}, "the web process needs a command"},
{"empty first word", map[string]Process{"web": {Command: []string{"", "x"}}}, "the web process needs a command"},
{"bad env name", map[string]Process{"web": {Command: web.Command, Env: map[string]string{"1X": "y"}}}, `the web process sets "1X" in env, which is not a variable name`},
- {"platform env", map[string]Process{"web": {Command: web.Command, Env: map[string]string{"CARLOS_DB": "/x"}}}, "the web process sets CARLOS_DB in env. Names that start with CARLOS_"},
+ {"platform env", map[string]Process{"web": {Command: web.Command, Env: map[string]string{"CARLOS_DB": "/x"}}}, "the web process sets CARLOS_DB in env. names that start with CARLOS_"},
{"relative dir", map[string]Process{"web": {Command: web.Command, WorkingDir: "app"}}, `the web process's working_dir must be an absolute path, and "app" is not`},
{"unclean dir", map[string]Process{"web": {Command: web.Command, WorkingDir: "/app/../x"}}, "the web process's working_dir must be an absolute path"},
{"timeout on web", map[string]Process{"web": {Command: web.Command, TimeoutSec: 5}}, "only the release process takes timeout_sec"},
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-2-box-isolation.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-2-box-isolation.md
index 189f6d4f9..96b2071d2 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-2-box-isolation.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-2-box-isolation.md
@@ -78,7 +78,7 @@ Each is decided here, once, and should be copied into the index's "Where the cod
- The index's names, exactly: `Kind == "rootfs"`, artifact key `"rootfs-linux-arm64"`, `release.ProcessWeb`, `ProcessRelease`, `ProcessConsole`, `release.ValidWorkerName`, `Manifest.Processes map[string]Process`, `Process{Command, Env, WorkingDir, TimeoutSec}`, `instancectl.RoleWorker`, `WorkerInstance`, `ParseWorkerInstance`, `host.RootfsPlacementAllowed(f RootfsBoxFacts, rt registry.Route) error` with its two halves `host.RootfsBoxReady(f RootfsBoxFacts) error` and `host.OwnBoxAllowed(f RootfsBoxFacts, rt registry.Route) error` (deviation 7), `rootfs_ready`, `CARLOS_DEPLOYMENT_SINGLE_TENANT`, templates `carlos-instance-rootfs@.service`, `carlos-instance-rootfs-gen@.service`, `carlos-instance-rootfs-worker@.service`, `carlos-instance-rootfs-cmd@.service`, `carlos-instance-rootfs-probe.service`.
- Health bound for a rootfs `web`: 120 s (`rootfsHealthTimeout`, Task 9, initialised from Plan 1's `release.RootfsHealthTimeout`). Memory tier for a rootfs row: `cgroup.TierChildren`, 1 GiB (R9, deviation 9). `MemoryDenyWriteExecute=yes` stays in the shared block and in both probes; only a release whose root-staged manifest says `"jit": true` runs without it, through a per-start drop-in root writes and checks (R11, spec §5.11, C10; Task 5).
- The box's own addresses are denied for every nested unit, binary and rootfs: `IPAddressDeny=` on `carlos-instances.slice` from `localPrefixes`, in place before the first start and before `rootfs_ready` (Task 7a, spec §5.9, C6, amadan #367). Systemd merges a unit's lists with its slices' and an allow wins, so nothing in this plan or Plan 5 may render an allow that contains a box address.
-- Copy: every human-readable string this plan introduces is in the string table at the end (IDs `RFS-2-C01` onward); no em dashes; neutral names only (`shop`, `shop.example.com`, `blog.example.com`); nothing is written into a file until the copy deck `2026-10-24-rootfs-apps-copy.md` has Paul's review (Plan 1 Task 1's gate).
+- Copy: every human-readable string this plan introduces is in the string table at the end (IDs `RFS-2-C01` onward); no em dashes; neutral names only (`shop`, `shop.example.com`, `blog.example.com`); nothing is written into a file until the copy deck `2026-10-24-rootfs-apps-copy.md` has Paul's review (Plan 1 Task 1's gate; the deck was approved by delegation on 2026-10-24 (Paul asked for a Luna copy pass instead of a string-by-string review; see the deck header), so the gate's approval step is satisfied).
- Unit files are the source of truth in `internal/host/units/`, embedded by `bootstrap.go:36`'s glob; nothing is applied by drop-in except the per-start runtime drop-ins the control service already writes for generation units (`internal/instancectl/gen.go:41`).
- Production-wired witnesses: refusal and launch tests go through `ExecBackend.Start`, the control service's HTTP handlers and the trampoline's `execLaunch`, never through a fixture that re-implements the rule.
- Commit after every task with the session's attribution trailer. Never push to `main`; land through amadan (AGENTS.md).
@@ -3090,8 +3090,8 @@ Human-readable text this plan introduces, for the series copy deck. Diagnostics,
|---|---|---|
| RFS-2-C01 | `RootfsPlacementAllowed`, flat or not nested-ready; the activator's flat-row refusal | rootfs instances need nested isolation, and this box does not have it yet |
| RFS-2-C02 | `RootfsPlacementAllowed`, probe not passed; the control service's 503 | this box has not passed the rootfs isolation probe on this boot (<check>) |
-| RFS-2-C03 | `RootfsPlacementAllowed`, platform box | rootfs instances run only on a customer's own boxes. This box is shared: it is not a fleet box and the deployment is not single-tenant |
-| RFS-2-C12 | `RootfsPlacementAllowed`, platform box serving a pool | rootfs instances run only on a customer's own boxes. A dedicated pool (<pool>) does not make this box one customer's |
+| RFS-2-C03 | `RootfsPlacementAllowed`, platform box | rootfs instances run only on a customer's own boxes. this box is shared: it is not a fleet box and the deployment is not single-tenant |
+| RFS-2-C12 | `RootfsPlacementAllowed`, platform box serving a pool | rootfs instances run only on a customer's own boxes. a dedicated pool (<pool>) does not make this box one customer's |
| RFS-2-C04 | `RootfsPlacementAllowed`, fleet owner unknown or unreadable | this fleet box cannot confirm which account owns its fleet, so it will not start a rootfs instance |
| RFS-2-C05 | `RootfsPlacementAllowed`, another account | this instance belongs to an account that does not own this fleet |
| RFS-2-C06 | `RootfsPlacementAllowed`, claim armed | rootfs instances are not started under host lifecycle claim control yet, and this box has it armed |
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.md
index c160ece7b..479238c72 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.md
@@ -138,7 +138,7 @@ Plan 1 Task 1 assembles every plan's table into the series deck and gets Paul's
- [ ] **Step 1:** `grep -c '^| RFS-3-' docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md`. Expected: 29, every one `approved`. If so, skip to Step 4 with nothing to commit.
- [ ] **Step 2:** For each row of this plan's table the deck lacks (a row added after the series review), append it in the deck's format, status `pending`, add its id to `rootfsCopyIDs`, and run `go test . -run 'TestRootfsCopy' -count=1` (PASS).
-- [ ] **Step 3:** Invoke the `copy-review` skill on those rows only, wait for Paul's edits, apply them verbatim to the deck and to this plan's string table, and mark each row `approved`. Read `result.json` before any restart of the review server.
+- [ ] **Step 3:** (Note: the deck was approved by delegation on 2026-10-24 (Paul asked for a Luna copy pass instead of a string-by-string review; see the deck header), so the gate's approval step is satisfied; this step is for rows added after that date.) Invoke the `copy-review` skill on those rows only, wait for Paul's edits, apply them verbatim to the deck and to this plan's string table, and mark each row `approved`. Read `result.json` before any restart of the review server.
- [ ] **Step 4:** Run the gate again (PASS); commit the deck and the gate if Step 2 changed them.
```bash
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.md
index 71da1b436..383ff4544 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.md
@@ -1174,17 +1174,17 @@ Placeholders: `<app>` an app; `<fleet>` a fleet name; `<region>` a region's name
| id | text |
|---|---|
-| RFS-4-C01 | `<app> is a rootfs app, and a rootfs app runs only on boxes your account owns. place it on one of your fleets first with carlos apps place.` |
-| RFS-4-C02 | `<app> is a rootfs app, and a rootfs app does not run in a dedicated pool yet. create the instance without a pool.` |
-| RFS-4-C03 | `<app> is a rootfs app, so it cannot move to the shared boxes. a rootfs app runs only on boxes your account owns.` |
-| RFS-4-C04 | `<app> is a rootfs app with instances, and a rootfs app's instances cannot move to other boxes yet. delete its instances first, or create a new app where you want it to run.` |
-| RFS-4-C05 | `none of fleet <fleet>'s boxes can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check.` |
-| RFS-4-C06 | `no box in <region> can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check.` |
-| RFS-4-C07 | `could not check which boxes can run a rootfs app. try again.` |
-| RFS-4-C08 | `could not read this app's releases. try again.` |
-| RFS-4-C09 | `<app> is a rootfs app, and a new rootfs instance cannot start from a copy of another database yet. create it empty, then load the data from your app.` |
-| RFS-4-C10 | `<app> is a rootfs app, and a rootfs app has no replicas yet.` |
-| RFS-4-C11 | `<app> already has <n> instances made before its first release, such as <host>. an app's first release decides its kind, and those instances were made for a binary app. delete them, ship again, then create them again.` |
+| RFS-4-C01 | `<app> is a rootfs app, and a rootfs app runs only on boxes your account owns. place it on one of your fleets first with carlos apps place` |
+| RFS-4-C02 | `<app> is a rootfs app, and a rootfs app does not run in a dedicated pool yet. create the instance without a pool` |
+| RFS-4-C03 | `<app> is a rootfs app, so it cannot move to the shared boxes. a rootfs app runs only on boxes your account owns` |
+| RFS-4-C04 | `<app> is a rootfs app with instances, and a rootfs app's instances cannot move to other boxes yet. delete its instances first, or create a new app where you want it to run` |
+| RFS-4-C05 | `none of fleet <fleet>'s boxes can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check` |
+| RFS-4-C06 | `no box in <region> can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check` |
+| RFS-4-C07 | `could not check which boxes can run a rootfs app. try again` |
+| RFS-4-C08 | `could not read this app's releases. try again` |
+| RFS-4-C09 | `<app> is a rootfs app, and a new rootfs instance cannot start from a copy of another database yet. create it empty, then load the data from your app` |
+| RFS-4-C10 | `<app> is a rootfs app, and a rootfs app has no replicas yet` |
+| RFS-4-C11 | `<app> already has <n> instances made before its first release, such as <host>. an app's first release decides its kind, and those instances were made for a binary app. delete them, ship again, then create them again` |
### CLI (instances_rootfs.go, instances_cli.go)
@@ -1196,8 +1196,8 @@ Placeholders: `<app>` an app; `<fleet>` a fleet name; `<region>` a region's name
| RFS-4-C15 | `rootfs` |
| RFS-4-C16 | `rootfs, hibernates` |
| RFS-4-C17 | `<running>/<total>, <name> restarting` |
-| RFS-4-C18 | `rootfs, always on. it starts as soon as its box picks it up, and does not wait for a request.` |
-| RFS-4-C19 | `rootfs, hibernates when idle.` |
+| RFS-4-C18 | `rootfs, always on. it starts as soon as its box picks it up, and does not wait for a request` |
+| RFS-4-C19 | `rootfs, hibernates when idle` |
### The sample app (examples/rails-shop)
@@ -1220,7 +1220,7 @@ Placeholders: `<app>` an app; `<fleet>` a fleet name; `<region>` a region's name
|---|---|
| RFS-4-C30 | `Rootfs apps` |
| RFS-4-C31 | `A rootfs app is an app shipped as a read-only Linux image instead of a single binary. You build the image on your own machine, from a Dockerfile or anything else that makes an OCI image or a directory, and carlos ship --rootfs uploads it. The platform never builds anything and never pulls from a registry.` (fact) |
-| RFS-4-C32 | `A rootfs app runs only on boxes your account owns: a fleet of yours on a deployment that runs apps for other people, such as Carloku, or any box of a deployment run for one customer. The box must be on the nested layout and must have passed its rootfs check. The console refuses anywhere else, and so does the box.` |
+| RFS-4-C32 | `A rootfs app runs only on boxes your account owns: a fleet of yours on a deployment that runs apps for other people, or any box of a deployment run for one customer. The box must be on the nested layout and must have passed its rootfs check. The console refuses anywhere else, and so does the box.` |
| RFS-4-C33 | `What fixes an app's kind` |
| RFS-4-C34 | `An app's first release makes it a binary app or a rootfs app, and it stays that kind. A release of the other kind is refused when you ship it. To change kinds, create a new app.` |
| RFS-4-C35 | `Each instance takes its hibernation setting from the release it is created with, and keeps it. A rootfs app is always on unless that release was shipped with carlos ship --hibernate. A later release that asks for the other setting is refused when you ship it.` (fact, D4) |
@@ -1261,7 +1261,7 @@ Placeholders: `<app>` an app; `<fleet>` a fleet name; `<region>` a region's name
|---|---|
| RFS-4-C63 | `Run a Rails app on your own boxes` |
| RFS-4-C64 | `This guide takes a Rails 8 app from a Dockerfile to a running instance on boxes your account owns. The app runs from a read-only image you build on your own machine. Its web process and its job worker share one SQLite database, which the platform replicates, and uploads go to the app's own bucket.` |
-| RFS-4-C65 | `You need Docker with buildx, the carlos CLI signed in to your console, and a fleet with at least one box. On a deployment that runs apps for other people, such as Carloku, a rootfs app runs only on a fleet your account owns. [carlos fleets](/docs/cli#carlos-fleets) sets one up, and [Rootfs apps](/docs/rootfs) has the full contract.` |
+| RFS-4-C65 | `You need Docker with buildx, the carlos CLI signed in to your console, and a fleet with at least one box. On a deployment that runs apps for other people, a rootfs app runs only on a fleet your account owns. [carlos fleets](/docs/cli#carlos-fleets) sets one up, and [Rootfs apps](/docs/rootfs) has the full contract.` |
| RFS-4-C66 | `The finished app is in the platform repository under examples/rails-shop. Every file this guide shows is taken from it.` |
| RFS-4-C67 | `The app` |
| RFS-4-C68 | `Start from rails new with SQLite. Rails 8 gives you Solid Queue, Solid Cache and Solid Cable, each with a database of its own. On CARLOS an instance has one database, so all three share it.` |
@@ -1333,7 +1333,7 @@ Placeholders: `<app>` an app; `<fleet>` a fleet name; `<region>` a region's name
| id | file | text |
|---|---|---|
-| RFS-4-C127 | `cli.md` (carlos instances) | `For a rootfs app, RUNTIME says rootfs, or rootfs, hibernates for an instance that sleeps when idle. WORKERS counts the instance's running workers out of the ones it declares, and names a worker that keeps crashing. A binary instance shows binary and a dash.` |
+| RFS-4-C127 | `cli.md` (carlos instances) | For a rootfs app, RUNTIME says `rootfs`, or `rootfs, hibernates` for an instance that sleeps when idle. WORKERS counts the instance's running workers out of the ones it declares, and names a worker that keeps crashing. A binary instance shows `binary` and a dash. |
| RFS-4-C128 | `cli.md` (carlos apps) | `A rootfs app stays on boxes your account owns. On a deployment that runs apps for other people, --target "" is refused for it, and so is moving it to another fleet while it has instances. [Rootfs apps](/docs/rootfs) says why.` |
| RFS-4-C129 | `nav.json` label | `Rootfs apps` |
| RFS-4-C130 | `nav.json` blurb | `What a rootfs app's image is started with, what it can write, and where it may run.` |
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-5-egress-allow.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-5-egress-allow.md
index 24c397b5d..c8650b204 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-5-egress-allow.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-5-egress-allow.md
@@ -32,7 +32,7 @@ Names this plan needs from Plan 2 (its review-fix round owns them; if Plan 2 pic
- **Composite drop-ins and start paths (finding 14).** `RootfsGenDropIn(host string, n uint64, tier cgroup.Limit) string` and `WorkerDropIn(host, name string, tier cgroup.Limit) string` (Plan 2 Task 5), each with exactly one `Slice=<SliceName(host)>` line. The rootfs gen and worker start paths check `show -p Slice --value` against `SliceName(host)` before `start` (Plan 2 Task 5 step 5). Templates `carlos-instance-rootfs@`, `-rootfs-cmd@` carry `Slice=carlos-instances-%i.slice`; `-rootfs-gen@` and `-rootfs-worker@` carry no `Slice=`; `carlos-instance-rootfs-probe.service` carries `Slice=carlos-instances.slice` (Plan 2's per-template table).
- Unchanged from before: `host.OwnBoxAllowed`, `RootfsBoxFacts`, `(*agentRootfs).facts()`.
-Copy deck (`2026-10-24-rootfs-apps-copy.md`, not edited here): rows RFS-5-C36, C40, C41 change text and RFS-5-C42 changes its "Where" column (R8). RFS-5-C44 is new. All are `pending`.
+Copy deck (`2026-10-24-rootfs-apps-copy.md`, not edited here): rows RFS-5-C36, C40, C41 change text and RFS-5-C42 changes its "Where" column (R8). RFS-5-C44 is new. All were `pending` and are now `approved` (by delegation, 2026-10-24).
## Review dispositions (Astra R2)
@@ -1074,15 +1074,15 @@ Human text only. `%s`/`%q`/`%d` are filled by the code. No em dashes. These go i
| RFS-5-C33 | CLI show, per name | `%s did not resolve` |
| RFS-5-C34 | CLI show, stale box | `this box has not reported since %s` |
| RFS-5-C35 | CLI show, operator | `box %s` |
-| RFS-5-C36 | CLI usage | `usage: carlos egress grant --app <a> <destination>...` / ` carlos egress revoke --app <a> (<destination>... \| --all)` / ` carlos egress show --app <a>` / flags `--account`, `--account-id`, `--console` as in door-key usage / `A destination is an address range such as 10.0.0.0/16, a single address, or a hostname such as db.example.com.` / `on a deployment that runs apps for other people, only an owner of the app's account can grant, for an app on one of the account's own fleets, and anyone in the account can revoke. on a deployment run for one customer, only its operators can grant or revoke. anyone in the account can run show.` |
-| RFS-5-C37 | CLI usage | `An app reaches a destination only when its live build declares it too, and only on the customer's own boxes: a fleet box, or any box of a single-tenant deployment.` |
-| RFS-5-C38 | CLI usage | `Egress opens addresses, not ports. a granted address is reachable on every port, so keep the database's own firewall in place.` |
-| RFS-5-C39 | CLI usage | `Hostnames are looked up every few seconds, and an address stays open for ten minutes after the name stops returning it. for endpoints that return a different address on each lookup, grant the subnet instead.` |
-| RFS-5-C40 | README, App capabilities | `egress lets a nested instance reach private addresses its build names: a database or cache in the customer's own network. The build declares the destinations, as "egress": ["10.0.0.0/16", "db.example.com"], and an owner of the account that owns the fleet grants them to the app with carlos egress grant (on a deployment run for one customer, its operator does). A box opens only what both name, only for nested instances, and only on the customer's own boxes. On a box shared with other customers it opens nothing, because there a private range is the platform's own network. Whatever the list says, egress never opens the cloud metadata service, the box's loopback or the box's own addresses. It opens addresses, not ports: a granted address is reachable on every port, so the database's own firewall stays the port fence.` |
+| RFS-5-C36 | CLI usage | `usage: carlos egress grant --app <a> <destination>...` / ` carlos egress revoke --app <a> (<destination>... \| --all)` / ` carlos egress show --app <a>` / flags `--account`, `--account-id`, `--console` as in door-key usage / `a destination is an address range such as 10.0.0.0/16, a single address, or a hostname such as db.example.com` / `on a deployment that runs apps for other people, only an owner of the app's account can grant, for an app on one of the account's own fleets, and anyone in the account can revoke. on a deployment run for one customer, only its operators can grant or revoke. anyone in the account can run show` |
+| RFS-5-C37 | CLI usage | `an app reaches a destination only when its live build declares it too, and only on the customer's own boxes: a fleet box, or any box of a single-tenant deployment` |
+| RFS-5-C38 | CLI usage | `egress opens addresses, not ports. a granted address is reachable on every port, so keep the database's own firewall in place` |
+| RFS-5-C39 | CLI usage | `hostnames are looked up every few seconds, and an address stays open for ten minutes after the name stops returning it. for endpoints that return a different address on each lookup, grant the subnet instead` |
+| RFS-5-C40 | README, App capabilities | `egress lets a nested instance reach private addresses its build names: a database or cache in the customer's own network. The build declares the destinations as "egress": ["10.0.0.0/16", "db.example.com"], and an owner of the account that owns the fleet grants them to the app with carlos egress grant (on a deployment run for one customer, its operator does). A box opens only what the build names and the account grants, only for nested instances, and only on the customer's own boxes. On a box shared with other customers it opens nothing, because a private range there is the platform's own network. Whatever the list says, egress never opens the cloud metadata service, the box's loopback or the box's own addresses. It opens addresses, not ports: a granted address is reachable on every port, so the database's own firewall stays the port fence.` |
| RFS-5-C42 | grant door 409 (shared deployment; revoke needs no placement, R8) | `%s/%s does not run on a fleet your account owns, so egress cannot be granted to it. place it on one of your fleets first with carlos apps place` |
| RFS-5-C43 | ship refusal (rootfs release), replaces RFS-1-C15 | `a rootfs release can declare only the egress capability` |
| RFS-5-C44 | grant door 403 (shared deployment, a member who is not an owner; R8), printed by the CLI | `only an owner of the account can grant egress. anyone in the account can revoke it` |
-| RFS-5-C41 | docs/site/cli.md, `### carlos egress` | `Lets an app's nested instances reach private destinations, such as a managed database in your own network. Your app's build declares the destinations, and an owner of your account grants them to the app for its own fleets (on a deployment run for one customer, the deployment's operator grants them). An instance gets only what both name, and only on your own boxes: a fleet box, or any box of a single-tenant deployment.` / C37, C38, C39 as paragraphs / `show lists what is granted and, for each instance, whether its box applied it and why not.` |
+| RFS-5-C41 | docs/site/cli.md, `### carlos egress` | `Lets an app's nested instances reach private destinations, such as a managed database in your own network. Your app's build declares the destinations, and an owner of your account grants them to the app for its own fleets (on a deployment run for one customer, the deployment's operator grants them). An instance gets only destinations both the build names and the account grants, and only on your own boxes: a fleet box or any box of a single-tenant deployment.` / `Egress opens addresses, not ports. A granted address is reachable on every port, so keep the database's own firewall in place.` / `Hostnames are looked up every few seconds, and an address stays open for ten minutes after the name stops returning it. For endpoints that return a different address on each lookup, grant the subnet instead.` / `Show lists what is granted and, for each instance, whether its box applied it and why not.` |
String count: **44 IDs** (C04 carries seven class names; C36 and C41 are multi-line blocks). Changed in the Astra R1 round for R8: C36, C40 and C41 (text) and C42 (where it is used); added: C44.
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.md
index 199119350..9891d7813 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.md
@@ -147,7 +147,7 @@ Read `runClassRefusal` (`internal/console/apiruns.go:55-70` at 64a002c8) as Plan
- [ ] **Step 3: Check the copy rows**
-Confirm that rows RFS-6-C01 to RFS-6-C64 (the table at the end of this plan) are in the deck and marked approved. If they are missing, add them as pending, exactly as written here, and stop until Paul has reviewed them. Every later task adds the ids it writes to the series gate's built list (Plan 1 Task 1's test) in the same commit.
+Confirm that rows RFS-6-C01 to RFS-6-C64 (the table at the end of this plan) are in the deck and marked approved. If they are missing, add them as pending, exactly as written here, and stop until Paul has reviewed them. (Note: the deck was approved by delegation on 2026-10-24 (Paul asked for a Luna copy pass instead of a string-by-string review; see the deck header), so the gate's approval step is satisfied; the step applies only to rows added after that date.) Every later task adds the ids it writes to the series gate's built list (Plan 1 Task 1's test) in the same commit.
---
@@ -1387,12 +1387,12 @@ Every string is human text the plan introduces. Existing approved strings it reu
| RFS-6-C38 | setting door | `only an owner of the account can turn interactive sessions on` |
| RFS-6-C39 | setting door | `say whether interactive sessions are on or off` |
| RFS-6-C40 | setting door audit detail | `turned interactive sessions %s for %s (%s)` |
-| RFS-6-C41 | `apps_swap.go` usage | ` --interactive on\|off whether members may open a terminal session on the app's instances, with carlos run -it or carlos console. It is off until an owner turns it on. A session can read and change everything the instance can.` |
+| RFS-6-C41 | `apps_swap.go` usage | ` --interactive on\|off whether members may open a terminal session on the app's instances, with carlos run -it or carlos console. it is off until an owner turns it on. a session can read and change everything the instance can` |
| RFS-6-C42 | flag `--interactive` | `on or off: whether members may open terminal sessions on the app's instances` |
| RFS-6-C43 | `apps_swap.go` | `apps set: --interactive is on or off, not %q` |
| RFS-6-C44 | `apps_swap.go` | `%s: interactive sessions are on. any member can open a terminal on its instances with carlos run -it` |
| RFS-6-C45 | `apps_swap.go` | `%s: interactive sessions are off` |
-| RFS-6-C46 | `apps_swap.go` | `apps set: the change did not stick (the console stored interactive %s). Run it again` |
+| RFS-6-C46 | `apps_swap.go` | `apps set: the change did not stick (the console stored interactive %s). run it again` |
| RFS-6-C47 | `apps_swap.go:39`, replaces `appsSetNothing` | `apps set: nothing to set. Pass --swap, --swap-concurrency, --interactive or one of the update settings` |
| RFS-6-C48 | `carlos logs` | `interactive session %s opened by %s` |
| RFS-6-C49 | `carlos logs` | `interactive session %s ended with exit %d after %s` |
diff --git a/docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md b/docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md
index b4f52b762..d3141e57b 100644
--- a/docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md
+++ b/docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md
@@ -1,4 +1,4 @@
-# Rootfs apps: copy deck (pending Paul's review)
+# Rootfs apps: copy deck (approved)
Every user-facing string of the rootfs apps series (#366), assembled verbatim
from the string tables of plans 1 to 6 (`2026-10-24-rootfs-apps-[1-6]-*.md`).
@@ -8,6 +8,8 @@ Plan 1 Task 1 stops for that review. The gate is `approved_rootfs_copy_test.go`
status. Ids are canonical: `RFS-<plan>-C01` to `C99`, then `C100` onward
(Plan 4 runs to C138).
+Approved 2026-10-24 by delegation: Paul asked for a Luna copy pass instead of a string-by-string review (LUNA-COPY-REVIEW-ROOTFS.md); RFS-4-C127 was edited by Claude.
+
The rules:
- A row is `pending` until Paul approves it in the copy review. His edits are
@@ -37,91 +39,91 @@ From the plan: Placeholders: `<app>` an app name; `<name>` a process name; `<var
| id | file | text | status |
|---|---|---|---|
-| RFS-1-C01 | internal/release/rootfs.go | the processes declare no web process. A rootfs release needs exactly one, named web. | pending |
-| RFS-1-C02 | internal/release/rootfs.go | <name> is not a process name. Use web, release, console, or a worker name: lower-case letters, digits and hyphens, up to 16 characters, starting with a letter. | pending |
-| RFS-1-C03 | internal/release/rootfs.go | <name> is reserved, so no process can use that name. | pending |
-| RFS-1-C04 | internal/release/rootfs.go | a rootfs release can declare at most 4 worker processes, and this one declares <n>. | pending |
-| RFS-1-C05 | internal/release/rootfs.go | the <name> process needs a command: a list of words whose first word is not empty. | pending |
-| RFS-1-C06 | internal/release/rootfs.go | the <name> process sets <var> in env, which is not a variable name. | pending |
-| RFS-1-C07 | internal/release/rootfs.go | the <name> process sets <var> in env. Names that start with CARLOS_ belong to the platform. | pending |
-| RFS-1-C08 | internal/release/rootfs.go | the <name> process's working_dir must be an absolute path, and <dir> is not. | pending |
-| RFS-1-C09 | internal/release/rootfs.go | only the release process takes timeout_sec. | pending |
-| RFS-1-C10 | internal/release/rootfs.go | the release process's timeout_sec must be between 1 and <max> seconds. | pending |
-| RFS-1-C11 | internal/release/rootfs.go | the <name> process contains a NUL byte, which no command or variable can hold. | pending |
-| RFS-1-C12 | internal/release/rootfs.go | the processes are <n> bytes, over the limit of <max>. | pending |
-| RFS-1-C13 | internal/release/rootfs.go | processes, hibernate and jit belong to rootfs releases only. | pending |
-| RFS-1-C14 | internal/release/rootfs.go | a rootfs release carries one artifact, rootfs-linux-arm64, and nothing else. | pending |
-| RFS-1-C15 | internal/release/rootfs.go (live until Plan 5 Task 2a strikes it, replaced by RFS-5-C43) | a rootfs release cannot declare capabilities. | pending |
-| RFS-1-C16 | internal/release/rootfs.go | malformed processes block. Want the processes object: web, any workers, release and console. | pending |
-| RFS-1-C17 | internal/release/rootfs.go | processes: null. A rootfs release needs its processes, starting with web. | pending |
-| RFS-1-C18 | internal/release/rootfs_ship.go | <app> ships <kind> releases (<version> is one), so it cannot take a rootfs release. Ship rootfs releases to an app of their own, made with carlos apps create. | pending |
-| RFS-1-C19 | internal/release/rootfs_ship.go | <app> ships rootfs releases (<version> is one), so it cannot take a <kind> release. Ship that release to another app. | pending |
-| RFS-1-C20 | internal/release/rootfs_ship.go | <app>'s rootfs releases are always on (<version> is one), so this release cannot opt into hibernation. Ship it without --hibernate, or use a new app. | pending |
-| RFS-1-C21 | internal/release/rootfs_ship.go | <app>'s rootfs releases hibernate (<version> is one), so this release must too. Ship it with --hibernate, or use a new app. | pending |
-| RFS-1-C22 | internal/console/apicli.go | invalid kind (want "binary", "static" or "rootfs") | pending |
-| RFS-1-C23 | internal/console/apicli.go (a fragment of the temporary-app refusal) | ship a rootfs release to it | pending |
-| RFS-1-C24 | internal/console/apiship_rootfs.go | <app> is a control-plane app, so its releases are binaries only. | pending |
-| RFS-1-C25 | internal/console/apiship_rootfs.go | the request does not say how large the rootfs image is, so nothing was read. Update carlos and ship again. | pending |
-| RFS-1-C26 | internal/console/apiship_rootfs.go | the rootfs image is over 2 GiB, the most one release can carry. Make the image smaller and ship again. | pending |
-| RFS-1-C27 | internal/console/apiship_rootfs.go | a rootfs release ships exactly one file part, named "rootfs-linux-arm64" | pending |
-| RFS-1-C28 | internal/console/apiship_rootfs.go | the rootfs part does not match the size the request declared, so nothing was shipped. | pending |
-| RFS-1-C29 | internal/console/apiship_rootfs.go | version <version> was already shipped with different processes, hibernation or jit. Versions are immutable, so ship under a new version. | pending |
-| RFS-1-C30 | cli.go | an OCI image (a layout directory, a docker save archive or a local image name) or a directory, shipped as a rootfs release | pending |
-| RFS-1-C31 | cli.go | JSON file holding the rootfs release's processes: web, any workers, release and console | pending |
-| RFS-1-C32 | cli.go | let this rootfs app hibernate when idle. Rootfs apps are always on unless their first release opts in | pending |
-| RFS-1-C33 | cli.go | run the rootfs image here with Docker or Podman, the way a box would, and ship nothing | pending |
-| RFS-1-C34 | ship_rootfs.go | usage: carlos ship --app <a> --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] [--version <sha>] [--label <one line>] [--notes <text> \| --notes-file <file>] [--swap hold] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]\n carlos ship --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] --check | pending |
-| RFS-1-C35 | ship_rootfs.go | --rootfs cannot be combined with --<flag>. | pending |
-| RFS-1-C36 | ship_rootfs.go | --rootfs takes the image or directory as its value, so leave the other arguments off. | pending |
-| RFS-1-C37 | ship_rootfs.go | --rootfs needs --processes, a JSON file holding the release's processes. | pending |
-| RFS-1-C38 | ship_rootfs.go | --<flag> works only with --rootfs. | pending |
-| RFS-1-C39 | ship_rootfs.go | --processes <file>: <reason> | pending |
-| RFS-1-C40 | ship_rootfs.go | filled in console: <command>, because this looks like a <framework> app. Declare console in --processes to choose another. | pending |
-| RFS-1-C41 | ship_rootfs.go | left out <n> device files, sockets and pipes: a rootfs image cannot carry them. | pending |
-| RFS-1-C42 | ship_rootfs.go | built the rootfs image: <size>, <n> files. | pending |
-| RFS-1-C43 | ship_rootfs.go | the rootfs image is <size>, over the 2 GiB one release can carry. Make the image smaller and ship again. | pending |
-| RFS-1-C44 | apimode_rootfs.go | this console does not take rootfs releases yet. Update the console, then ship again. | pending |
-| RFS-1-C45 | internal/rootfsimage/errors.go | this image is built for <platform>, and boxes run linux/arm64. Rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64. | pending |
-| RFS-1-C46 | internal/rootfsimage/errors.go | this image has no linux/arm64 variant (it has <platforms>). Rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64. | pending |
-| RFS-1-C47 | internal/rootfsimage/errors.go | <path> is a program built for <platform>, and boxes run linux/arm64. Build the directory's programs for linux/arm64. | pending |
-| RFS-1-C48 | internal/rootfsimage/errors.go | the <name> process runs <command>, which the image does not have. Commands are looked up in the process's PATH, or from its working_dir when they contain a slash. | pending |
-| RFS-1-C49 | ship_runtime.go | <source> is not a file, a directory or a local image. | pending |
-| RFS-1-C50 | internal/rootfsimage/errors.go | <source> is not an OCI image, a docker save archive or a directory. | pending |
-| RFS-1-C51 | internal/rootfsimage/errors.go | the image has an entry outside its root (<path>), so it was not shipped. | pending |
-| RFS-1-C52 | internal/rootfsimage/errors.go | the image has <path> as a <type>, and boxes mount a directory there. Remove it from the image. | pending |
-| RFS-1-C53 | internal/rootfsimage/errors.go | the image links <path> to <target>, which is not a file in the image. | pending |
-| RFS-1-C54 | internal/rootfsimage/errors.go | the image sets <var> in its environment. Names that start with CARLOS_ belong to the platform, so remove it from the image. | pending |
-| RFS-1-C55 | internal/rootfsimage/errors.go | the archive holds <n> images. Save one image and ship that. | pending |
-| RFS-1-C56 | internal/rootfsimage/dir_other.go | a directory can be shipped as a rootfs only from Linux or macOS, which keep file modes. Ship an OCI image instead. | pending |
-| RFS-1-C57 | ship_runtime.go | --check runs the image with Docker or Podman, and neither is installed here. | pending |
-| RFS-1-C58 | ship_runtime.go | <source> is not a file or a directory, and there is no Docker or Podman here to export it as an image. | pending |
-| RFS-1-C59 | ship_runtime.go | could not export <image> with <runtime>: <reason> | pending |
-| RFS-1-C60 | ship_check.go | checking the image with <runtime>: read-only root, <memory> of memory, <seconds> s for web to answer. | pending |
-| RFS-1-C61 | ship_check.go | this machine is <arch>, so the arm64 image runs under emulation and starts slower than on a box. | pending |
-| RFS-1-C62 | ship_check.go | the arm64 image cannot run here. Install emulation for arm64 (qemu and binfmt), or run the check on an arm64 machine. | pending |
-| RFS-1-C63 | ship_check.go | release finished in <duration>. | pending |
-| RFS-1-C64 | ship_check.go | release exited with status <code>. Its last lines: | pending |
-| RFS-1-C65 | ship_check.go | release did not finish within <duration>. | pending |
-| RFS-1-C66 | ship_check.go | web answered GET / with <status> after <duration>. | pending |
-| RFS-1-C67 | ship_check.go | web did not answer GET / on CARLOS_SOCKET within <duration>. | pending |
-| RFS-1-C68 | ship_check.go | web listens on port <ports> instead of the socket. Serve HTTP on the unix socket at CARLOS_SOCKET. | pending |
-| RFS-1-C69 | ship_check.go | web answered GET / with <status>. Health needs an answer below 500. | pending |
-| RFS-1-C70 | ship_check.go | <name> exited with status <code>. Its last lines: | pending |
-| RFS-1-C71 | ship_check.go | <name> ran out of memory. A rootfs instance's tier allows <memory>. | pending |
-| RFS-1-C72 | ship_check.go | <name> tried to write outside its instance directory. The root is read-only, so write under <dir> or /tmp. | pending |
-| RFS-1-C73 | ship_check.go | <name> is still running after <duration>. | pending |
-| RFS-1-C74 | ship_check.go | the image passed the local check. A box also applies its egress fence and its own limits, so this is a convenience, not a guarantee. | pending |
-| RFS-1-C75 | ship_check.go | the image failed the local check. | pending |
-| RFS-1-C76 | ship_check_probe.go | --check needs a linux build of carlos to probe the socket from inside the container, and this one is a development build. Use a released carlos, or set CARLOS_CHECK_PROBE to a linux carlos binary. | pending |
-| RFS-1-C77 | ship_check_probe.go | could not fetch the linux build of carlos for the check: <reason> | pending |
-| RFS-1-C78 | internal/rootfsimage/errors.go | the image's layer <digest> does not match its digest, so it was not shipped. | pending |
-| RFS-1-C79 | internal/console/apicli.go | could not check this app's releases. Nothing was shipped. Try again. | pending |
-| RFS-1-C80 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for <kind> releases, so it cannot take a rootfs release. Ship rootfs releases to an app of their own, made with carlos apps create. | pending |
-| RFS-1-C81 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for rootfs releases, so it cannot take a <kind> release. Ship that release to another app. | pending |
-| RFS-1-C82 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases always on, so this release cannot opt into hibernation. Ship it without --hibernate, or use a new app. | pending |
-| RFS-1-C83 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases hibernate, so this release must too. Ship it with --hibernate, or use a new app. | pending |
-| RFS-1-C84 | cli.go | let this rootfs release's processes run a JIT compiler, as Node, Bun, Deno, the JVM and Ruby's YJIT need. Without it a box never lets memory be writable and executable at once | pending |
-| RFS-1-C85 | ship_check.go | the <name> process runs <program>, which needs a JIT, and this release does not declare one. It passes here but stops at start on a box. Ship it with --jit. | pending |
+| RFS-1-C01 | internal/release/rootfs.go | the processes declare no web process. a rootfs release needs exactly one, named web | approved |
+| RFS-1-C02 | internal/release/rootfs.go | <name> is not a process name. use web, release, console, or a worker name: lower-case letters, digits and hyphens, up to 16 characters, starting with a letter | approved |
+| RFS-1-C03 | internal/release/rootfs.go | <name> is reserved, so no process can use that name | approved |
+| RFS-1-C04 | internal/release/rootfs.go | a rootfs release can declare at most 4 worker processes, and this one declares <n> | approved |
+| RFS-1-C05 | internal/release/rootfs.go | the <name> process needs a command: a list of words whose first word is not empty | approved |
+| RFS-1-C06 | internal/release/rootfs.go | the <name> process sets <var> in env, which is not a variable name | approved |
+| RFS-1-C07 | internal/release/rootfs.go | the <name> process sets <var> in env. names that start with CARLOS_ belong to the platform | approved |
+| RFS-1-C08 | internal/release/rootfs.go | the <name> process's working_dir must be an absolute path, and <dir> is not | approved |
+| RFS-1-C09 | internal/release/rootfs.go | only the release process takes timeout_sec | approved |
+| RFS-1-C10 | internal/release/rootfs.go | the release process's timeout_sec must be between 1 and <max> seconds | approved |
+| RFS-1-C11 | internal/release/rootfs.go | the <name> process contains a NUL byte, which no command or variable can hold | approved |
+| RFS-1-C12 | internal/release/rootfs.go | the processes are <n> bytes, over the limit of <max> | approved |
+| RFS-1-C13 | internal/release/rootfs.go | processes, hibernate and jit belong to rootfs releases only | approved |
+| RFS-1-C14 | internal/release/rootfs.go | a rootfs release carries one artifact, rootfs-linux-arm64, and nothing else | approved |
+| RFS-1-C15 | internal/release/rootfs.go (live until Plan 5 Task 2a strikes it, replaced by RFS-5-C43) | a rootfs release cannot declare capabilities | approved |
+| RFS-1-C16 | internal/release/rootfs.go | malformed processes block. want the processes object: web, any workers, release and console | approved |
+| RFS-1-C17 | internal/release/rootfs.go | processes: null. a rootfs release needs its processes, starting with web | approved |
+| RFS-1-C18 | internal/release/rootfs_ship.go | <app> ships <kind> releases (<version> is one), so it cannot take a rootfs release. ship rootfs releases to an app of their own, made with carlos apps create | approved |
+| RFS-1-C19 | internal/release/rootfs_ship.go | <app> ships rootfs releases (<version> is one), so it cannot take a <kind> release. ship that release to another app | approved |
+| RFS-1-C20 | internal/release/rootfs_ship.go | <app>'s rootfs releases are always on (<version> is one), so this release cannot opt into hibernation. ship it without --hibernate, or use a new app | approved |
+| RFS-1-C21 | internal/release/rootfs_ship.go | <app>'s rootfs releases hibernate (<version> is one), so this release must too. ship it with --hibernate, or use a new app | approved |
+| RFS-1-C22 | internal/console/apicli.go | invalid kind (want "binary", "static" or "rootfs") | approved |
+| RFS-1-C23 | internal/console/apicli.go (a fragment of the temporary-app refusal) | ship a rootfs release to it | approved |
+| RFS-1-C24 | internal/console/apiship_rootfs.go | <app> is a control-plane app, so its releases are binaries only | approved |
+| RFS-1-C25 | internal/console/apiship_rootfs.go | the request does not say how large the rootfs image is, so nothing was read. update carlos and ship again | approved |
+| RFS-1-C26 | internal/console/apiship_rootfs.go | the rootfs image is over 2 GiB, the most one release can carry. make the image smaller and ship again | approved |
+| RFS-1-C27 | internal/console/apiship_rootfs.go | a rootfs release ships exactly one file part, named "rootfs-linux-arm64" | approved |
+| RFS-1-C28 | internal/console/apiship_rootfs.go | the rootfs part does not match the size the request declared, so nothing was shipped | approved |
+| RFS-1-C29 | internal/console/apiship_rootfs.go | version <version> was already shipped with different processes, hibernation or jit. versions are immutable, so ship under a new version | approved |
+| RFS-1-C30 | cli.go | an OCI image (a layout directory, a docker save archive or a local image name) or a directory, shipped as a rootfs release | approved |
+| RFS-1-C31 | cli.go | JSON file holding the rootfs release's processes: web, any workers, release and console | approved |
+| RFS-1-C32 | cli.go | let this rootfs app hibernate when idle. Rootfs apps are always on unless their first release opts in | approved |
+| RFS-1-C33 | cli.go | run the rootfs image here with Docker or Podman, the way a box would, and ship nothing | approved |
+| RFS-1-C34 | ship_rootfs.go | usage: carlos ship --app <a> --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] [--version <sha>] [--label <one line>] [--notes <text> \| --notes-file <file>] [--swap hold] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]\n carlos ship --rootfs <image-or-dir> --processes <file.json> [--hibernate] [--jit] --check | approved |
+| RFS-1-C35 | ship_rootfs.go | --rootfs cannot be combined with --<flag> | approved |
+| RFS-1-C36 | ship_rootfs.go | --rootfs takes the image or directory as its value, so leave the other arguments off | approved |
+| RFS-1-C37 | ship_rootfs.go | --rootfs needs --processes, a JSON file holding the release's processes | approved |
+| RFS-1-C38 | ship_rootfs.go | --<flag> works only with --rootfs | approved |
+| RFS-1-C39 | ship_rootfs.go | --processes <file>: <reason> | approved |
+| RFS-1-C40 | ship_rootfs.go | filled in console: <command>, because this looks like a <framework> app. declare console in --processes to choose another | approved |
+| RFS-1-C41 | ship_rootfs.go | left out <n> device files, sockets and pipes: a rootfs image cannot carry them | approved |
+| RFS-1-C42 | ship_rootfs.go | built the rootfs image: <size>, <n> files. | approved |
+| RFS-1-C43 | ship_rootfs.go | the rootfs image is <size>, over the 2 GiB one release can carry. make the image smaller and ship again | approved |
+| RFS-1-C44 | apimode_rootfs.go | this console does not take rootfs releases yet. update the console, then ship again | approved |
+| RFS-1-C45 | internal/rootfsimage/errors.go | this image is built for <platform>, and boxes run linux/arm64. rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64 | approved |
+| RFS-1-C46 | internal/rootfsimage/errors.go | this image has no linux/arm64 variant (it has <platforms>). rebuild it for linux/arm64, for example with docker buildx build --platform linux/arm64 | approved |
+| RFS-1-C47 | internal/rootfsimage/errors.go | <path> is a program built for <platform>, and boxes run linux/arm64. build the directory's programs for linux/arm64 | approved |
+| RFS-1-C48 | internal/rootfsimage/errors.go | the <name> process runs <command>, which the image does not have. commands are looked up in the process's PATH, or from its working_dir when they contain a slash | approved |
+| RFS-1-C49 | ship_runtime.go | <source> is not a file, a directory or a local image | approved |
+| RFS-1-C50 | internal/rootfsimage/errors.go | <source> is not an OCI image, a docker save archive or a directory | approved |
+| RFS-1-C51 | internal/rootfsimage/errors.go | the image has an entry outside its root (<path>), so it was not shipped | approved |
+| RFS-1-C52 | internal/rootfsimage/errors.go | the image has <path> as a <type>, and boxes mount a directory there. remove it from the image | approved |
+| RFS-1-C53 | internal/rootfsimage/errors.go | the image links <path> to <target>, which is not a file in the image | approved |
+| RFS-1-C54 | internal/rootfsimage/errors.go | the image sets <var> in its environment. names that start with CARLOS_ belong to the platform, so remove it from the image | approved |
+| RFS-1-C55 | internal/rootfsimage/errors.go | the archive holds <n> images. save one image and ship that | approved |
+| RFS-1-C56 | internal/rootfsimage/dir_other.go | a directory can be shipped as a rootfs only from Linux or macOS, which keep file modes. ship an OCI image instead | approved |
+| RFS-1-C57 | ship_runtime.go | --check runs the image with Docker or Podman, and neither is installed here | approved |
+| RFS-1-C58 | ship_runtime.go | <source> is not a file or a directory, and there is no Docker or Podman here to export it as an image | approved |
+| RFS-1-C59 | ship_runtime.go | could not export <image> with <runtime>: <reason> | approved |
+| RFS-1-C60 | ship_check.go | checking the image with <runtime>: read-only root, <memory> of memory, <seconds> s for web to answer | approved |
+| RFS-1-C61 | ship_check.go | this machine is <arch>, so the arm64 image runs under emulation and starts slower than on a box | approved |
+| RFS-1-C62 | ship_check.go | the arm64 image cannot run here. install emulation for arm64 (qemu and binfmt), or run the check on an arm64 machine | approved |
+| RFS-1-C63 | ship_check.go | release finished in <duration> | approved |
+| RFS-1-C64 | ship_check.go | release exited with status <code>. its last lines: | approved |
+| RFS-1-C65 | ship_check.go | release did not finish within <duration> | approved |
+| RFS-1-C66 | ship_check.go | web answered GET / with <status> after <duration> | approved |
+| RFS-1-C67 | ship_check.go | web did not answer GET / on CARLOS_SOCKET within <duration> | approved |
+| RFS-1-C68 | ship_check.go | web listens on port <ports> instead of the socket. serve HTTP on the unix socket at CARLOS_SOCKET | approved |
+| RFS-1-C69 | ship_check.go | web answered GET / with <status>. health needs an answer below 500 | approved |
+| RFS-1-C70 | ship_check.go | <name> exited with status <code>. its last lines: | approved |
+| RFS-1-C71 | ship_check.go | <name> ran out of memory. a rootfs instance's tier allows <memory> | approved |
+| RFS-1-C72 | ship_check.go | <name> tried to write outside its instance directory. the root is read-only, so write under <dir> or /tmp | approved |
+| RFS-1-C73 | ship_check.go | <name> is still running after <duration> | approved |
+| RFS-1-C74 | ship_check.go | the image passed the local check. a box also applies its egress fence and its own limits, so this is a convenience, not a guarantee | approved |
+| RFS-1-C75 | ship_check.go | the image failed the local check | approved |
+| RFS-1-C76 | ship_check_probe.go | --check needs a linux build of carlos to probe the socket from inside the container, and this one is a development build. use a released carlos, or set CARLOS_CHECK_PROBE to a linux carlos binary | approved |
+| RFS-1-C77 | ship_check_probe.go | could not fetch the linux build of carlos for the check: <reason> | approved |
+| RFS-1-C78 | internal/rootfsimage/errors.go | the image's layer <digest> does not match its digest, so it was not shipped | approved |
+| RFS-1-C79 | internal/console/apicli.go | could not check this app's releases. nothing was shipped. try again | approved |
+| RFS-1-C80 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for <kind> releases, so it cannot take a rootfs release. ship rootfs releases to an app of their own, made with carlos apps create | approved |
+| RFS-1-C81 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made it an app for rootfs releases, so it cannot take a <kind> release. ship that release to another app | approved |
+| RFS-1-C82 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases always on, so this release cannot opt into hibernation. ship it without --hibernate, or use a new app | approved |
+| RFS-1-C83 | internal/release/rootfs_ship.go | <app>'s first ship, <version>, made its rootfs releases hibernate, so this release must too. ship it with --hibernate, or use a new app | approved |
+| RFS-1-C84 | cli.go | let this rootfs release's processes run a JIT compiler, as Node, Bun, Deno, the JVM and Ruby's YJIT need. without it a box never lets memory be writable and executable at once | approved |
+| RFS-1-C85 | ship_check.go | the <name> process runs <program>, which needs a JIT, and this release does not declare one. it passes here but stops at start on a box. ship it with --jit | approved |
From the plan: RFS-1-C78's `<digest>` is a `sha256:<hex>` digest. RFS-1-C80 to C83 are the first-ship forms of C18 to C21: the app has no other release, and its kind marker (deviation 10) names the ship that fixed its kind, which may not have published, so they never say "is one". RFS-1-C85 is a warning: it does not fail the check. Kept out of the table on purpose (diagnostics, not human text): the probe's one-line JSON on stdout, `validateManifest`'s existing terse messages, store errors wrapped for logs.
@@ -131,18 +133,18 @@ From the plan: Human-readable text this plan introduces, for the series copy dec
| id | file | text | status |
|---|---|---|---|
-| RFS-2-C01 | `RootfsPlacementAllowed`, flat or not nested-ready; the activator's flat-row refusal | rootfs instances need nested isolation, and this box does not have it yet | pending |
-| RFS-2-C02 | `RootfsPlacementAllowed`, probe not passed; the control service's 503 | this box has not passed the rootfs isolation probe on this boot (<check>) | pending |
-| RFS-2-C03 | `RootfsPlacementAllowed`, platform box | rootfs instances run only on a customer's own boxes. This box is shared: it is not a fleet box and the deployment is not single-tenant | pending |
-| RFS-2-C04 | `RootfsPlacementAllowed`, fleet owner unknown or unreadable | this fleet box cannot confirm which account owns its fleet, so it will not start a rootfs instance | pending |
-| RFS-2-C05 | `RootfsPlacementAllowed`, another account | this instance belongs to an account that does not own this fleet | pending |
-| RFS-2-C06 | `RootfsPlacementAllowed`, claim armed | rootfs instances are not started under host lifecycle claim control yet, and this box has it armed | pending |
-| RFS-2-C07 | control service, manifest not rootfs; agent form for a binary staged under a rootfs row | the staged release for this instance is a binary, not a rootfs image | pending |
-| RFS-2-C08 | control service, undeclared process | <process> is not a process this release declares | pending |
-| RFS-2-C09 | activator, health bound | the web process did not answer on its socket within 120 s | pending |
-| RFS-2-C10 | control service, mount setup failed | the image has no <path>, which the instance needs; rebuild it with carlos ship | pending |
-| RFS-2-C11 | `RootfsBoxReady`, overlap swaps not configured | rootfs instances need overlap deploys, and this box does not run them: set CARLOS_SWAP=overlap in its host.env | pending |
-| RFS-2-C12 | `RootfsPlacementAllowed`, platform box serving a pool | rootfs instances run only on a customer's own boxes. A dedicated pool (<pool>) does not make this box one customer's | pending |
+| RFS-2-C01 | `RootfsPlacementAllowed`, flat or not nested-ready; the activator's flat-row refusal | rootfs instances need nested isolation, and this box does not have it yet | approved |
+| RFS-2-C02 | `RootfsPlacementAllowed`, probe not passed; the control service's 503 | this box has not passed the rootfs isolation probe on this boot (<check>) | approved |
+| RFS-2-C03 | `RootfsPlacementAllowed`, platform box | rootfs instances run only on a customer's own boxes. this box is shared: it is not a fleet box and the deployment is not single-tenant | approved |
+| RFS-2-C04 | `RootfsPlacementAllowed`, fleet owner unknown or unreadable | this fleet box cannot confirm which account owns its fleet, so it will not start a rootfs instance | approved |
+| RFS-2-C05 | `RootfsPlacementAllowed`, another account | this instance belongs to an account that does not own this fleet | approved |
+| RFS-2-C06 | `RootfsPlacementAllowed`, claim armed | rootfs instances are not started under host lifecycle claim control yet, and this box has it armed | approved |
+| RFS-2-C07 | control service, manifest not rootfs; agent form for a binary staged under a rootfs row | the staged release for this instance is a binary, not a rootfs image | approved |
+| RFS-2-C08 | control service, undeclared process | <process> is not a process this release declares | approved |
+| RFS-2-C09 | activator, health bound | the web process did not answer on its socket within 120 s | approved |
+| RFS-2-C10 | control service, mount setup failed | the image has no <path>, which the instance needs; rebuild it with carlos ship | approved |
+| RFS-2-C11 | `RootfsBoxReady`, overlap swaps not configured | rootfs instances need overlap deploys, and this box does not run them: set CARLOS_SWAP=overlap in its host.env | approved |
+| RFS-2-C12 | `RootfsPlacementAllowed`, platform box serving a pool | rootfs instances run only on a customer's own boxes. a dedicated pool (<pool>) does not make this box one customer's | approved |
## Plan 3: lifecycle (`2026-10-24-rootfs-apps-3-lifecycle.md`)
@@ -150,35 +152,35 @@ From the plan: Every row goes to the deck in Task 1 and reaches code only once a
| id | file | text | status |
|---|---|---|---|
-| RFS-3-C01 | `internal/rolloutwords/words.go` (`reasons["release"]`, read after "because") | its release command failed | pending |
-| RFS-3-C02 | `internal/rolloutwords/words.go` (`reasons["release-timeout"]`) | its release command did not finish in time | pending |
-| RFS-3-C03 | `internal/rolloutwords/words.go` (`EventReleaseStarted`) | running the release command of build %s | pending |
-| RFS-3-C04 | `internal/rolloutwords/words.go` (`EventReleaseDone`) | the release command of build %s finished in %s | pending |
-| RFS-3-C05 | `internal/rolloutwords/words.go` (`EventReleaseFailed`; also `ReleaseError.Error`) | the release command of build %s exited with status %d | pending |
-| RFS-3-C06 | `internal/rolloutwords/words.go` (`EventReleaseSignal`; also `ReleaseError.Error`) | the release command of build %s was ended by signal %s | pending |
-| RFS-3-C07 | `internal/rolloutwords/words.go` (`EventReleaseTimedOut`; also `ReleaseError.Error`) | the release command of build %s did not finish within %s, so it was stopped | pending |
-| RFS-3-C08 | `internal/rolloutwords/words.go` (`EventWorkerStarted`) | worker %s started | pending |
-| RFS-3-C09 | `internal/rolloutwords/words.go` (`EventWorkerExited`) | worker %s exited with status %d. starting it again in %s | pending |
-| RFS-3-C10 | `internal/rolloutwords/words.go` (`EventWorkerCrashLoop`) | worker %s keeps exiting. it is started again once a minute | pending |
-| RFS-3-C11 | `internal/rolloutwords/words.go` (`EventResidentExited`) | instance exited. starting it again in %s | pending |
-| RFS-3-C12 | `internal/rolloutwords/words.go` (`EventRootfsRefused`) | this box cannot run the instance: %s | pending |
-| RFS-3-C13 | `internal/rolloutwords/words.go` (`RootfsRefusedClaims`, fills C12) | host claims are armed here | pending |
-| RFS-3-C14 | `internal/rolloutwords/words.go` (`RootfsRefusedUnarmed`, fills C12) | overlap swaps are not armed here | pending |
-| RFS-3-C15 | `internal/activator/rootfs_service.go` (`errRootfsReplicaMissing`, the member's wake reason) | the instance has served before and its database replica is missing, so it was not started with an empty database | pending |
-| RFS-3-C16 | `internal/activator/activator.go` (`serveWakeError`, the 503 body for `errResidentBackoff`) | instance is restarting; retry shortly | pending |
-| RFS-3-C17 | `run.go` (the `--version` flag's help) | a build of the app to run instead of the one the instance serves | pending |
-| RFS-3-C18 | `run.go` (`runUsageRun`, replacing today's approved line at `:28`) | usage: carlos run --app <app> --instance <host> [--file <path>]... [--timeout 30m] [--version <build>] -- <arg>... | pending |
-| RFS-3-C19 | `internal/console/apiruns.go` (400) | %s is not a build name | pending |
-| RFS-3-C20 | `internal/console/apiruns.go` (409) | %s has no build %s | pending |
-| RFS-3-C21 | `run.go` (`runExit`, refused version) | %s: build %s is not on the instance's box, so the run did not start | pending |
-| RFS-3-C22 | `internal/host/runevents.go` (`runRefusedWords`, version) | the build is not on the box | pending |
-| RFS-3-C23 | `internal/console/seedmove.go` (`refuseRootfsMove`, 409) | %s is a rootfs instance, and a rootfs instance does not move. create a new instance where you want it | pending |
-| RFS-3-C24 | `internal/console/seedmove.go` (`rootfsMoveConflict`, `carlos apps place`) | this app's instances are rootfs instances, which do not move. create new instances where you want them | pending |
-| RFS-3-C25 | `internal/host/systemapply.go` (`systemApplyRootfsRefusal`) | %s cannot run rootfs instances, and this box still has: %s. delete those instances before installing it | pending |
-| RFS-3-C26 | `docs/site/cli.md` (`carlos run`, a paragraph) | --version runs another build of the app against the instance, for example the build a deploy is about to bring in. The build must already be on the instance's box. The command still gets the environment the instance serves with. | pending |
-| RFS-3-C27 | `docs/site/cli.md` (`carlos run`, a paragraph) | For a rootfs app the command after -- runs inside the instance's image and is found on the image's PATH. | pending |
-| RFS-3-C28 | `docs/site/cli.md` (`carlos run`, the same paragraph) (live until Plan 6 Task 16 strikes it, replaced by RFS-6-C62) | carlos run does not reach instances on a customer fleet yet. | pending |
-| RFS-3-C29 | `internal/rolloutwords/words.go` (`EventReleaseLeaseLost`) | the release command of build %s was stopped because this box no longer holds the instance. it runs again on the next attempt | pending |
+| RFS-3-C01 | `internal/rolloutwords/words.go` (`reasons["release"]`, read after "because") | its release command failed | approved |
+| RFS-3-C02 | `internal/rolloutwords/words.go` (`reasons["release-timeout"]`) | its release command did not finish in time | approved |
+| RFS-3-C03 | `internal/rolloutwords/words.go` (`EventReleaseStarted`) | running the release command of build %s | approved |
+| RFS-3-C04 | `internal/rolloutwords/words.go` (`EventReleaseDone`) | the release command of build %s finished in %s | approved |
+| RFS-3-C05 | `internal/rolloutwords/words.go` (`EventReleaseFailed`; also `ReleaseError.Error`) | the release command of build %s exited with status %d | approved |
+| RFS-3-C06 | `internal/rolloutwords/words.go` (`EventReleaseSignal`; also `ReleaseError.Error`) | the release command of build %s was ended by signal %s | approved |
+| RFS-3-C07 | `internal/rolloutwords/words.go` (`EventReleaseTimedOut`; also `ReleaseError.Error`) | the release command of build %s did not finish within %s, so it was stopped | approved |
+| RFS-3-C08 | `internal/rolloutwords/words.go` (`EventWorkerStarted`) | worker %s started | approved |
+| RFS-3-C09 | `internal/rolloutwords/words.go` (`EventWorkerExited`) | worker %s exited with status %d. starting it again in %s | approved |
+| RFS-3-C10 | `internal/rolloutwords/words.go` (`EventWorkerCrashLoop`) | worker %s keeps exiting. it is started again once a minute | approved |
+| RFS-3-C11 | `internal/rolloutwords/words.go` (`EventResidentExited`) | instance exited. starting it again in %s | approved |
+| RFS-3-C12 | `internal/rolloutwords/words.go` (`EventRootfsRefused`) | this box cannot run the instance: %s | approved |
+| RFS-3-C13 | `internal/rolloutwords/words.go` (`RootfsRefusedClaims`, fills C12) | host claims are armed here | approved |
+| RFS-3-C14 | `internal/rolloutwords/words.go` (`RootfsRefusedUnarmed`, fills C12) | overlap swaps are not armed here | approved |
+| RFS-3-C15 | `internal/activator/rootfs_service.go` (`errRootfsReplicaMissing`, the member's wake reason) | the instance has served before and its database replica is missing, so it was not started with an empty database | approved |
+| RFS-3-C16 | `internal/activator/activator.go` (`serveWakeError`, the 503 body for `errResidentBackoff`) | instance is restarting; retry shortly | approved |
+| RFS-3-C17 | `run.go` (the `--version` flag's help) | a build of the app to run instead of the one the instance serves | approved |
+| RFS-3-C18 | `run.go` (`runUsageRun`, replacing today's approved line at `:28`) | usage: carlos run --app <app> --instance <host> [--file <path>]... [--timeout 30m] [--version <build>] -- <arg>... | approved |
+| RFS-3-C19 | `internal/console/apiruns.go` (400) | %s is not a build name | approved |
+| RFS-3-C20 | `internal/console/apiruns.go` (409) | %s has no build %s | approved |
+| RFS-3-C21 | `run.go` (`runExit`, refused version) | %s: build %s is not on the instance's box, so the run did not start | approved |
+| RFS-3-C22 | `internal/host/runevents.go` (`runRefusedWords`, version) | the build is not on the box | approved |
+| RFS-3-C23 | `internal/console/seedmove.go` (`refuseRootfsMove`, 409) | %s is a rootfs instance, and a rootfs instance does not move. create a new instance where you want it | approved |
+| RFS-3-C24 | `internal/console/seedmove.go` (`rootfsMoveConflict`, `carlos apps place`) | this app's instances are rootfs instances, which do not move. create new instances where you want them | approved |
+| RFS-3-C25 | `internal/host/systemapply.go` (`systemApplyRootfsRefusal`) | %s cannot run rootfs instances, and this box still has: %s. delete those instances before installing it | approved |
+| RFS-3-C26 | `docs/site/cli.md` (`carlos run`, a paragraph) | --version runs another build of the app against the instance, for example the build a deploy is about to bring in. The build must already be on the instance's box. The command still gets the environment the instance serves with. | approved |
+| RFS-3-C27 | `docs/site/cli.md` (`carlos run`, a paragraph) | For a rootfs app the command after -- runs inside the instance's image and is found on the image's PATH. | approved |
+| RFS-3-C28 | `docs/site/cli.md` (`carlos run`, the same paragraph) (live until Plan 6 Task 16 strikes it, replaced by RFS-6-C62) | carlos run does not reach instances on a customer fleet yet. | approved |
+| RFS-3-C29 | `internal/rolloutwords/words.go` (`EventReleaseLeaseLost`) | the release command of build %s was stopped because this box no longer holds the instance. it runs again on the next attempt | approved |
From the plan: Count: 29 (RFS-3-C29 added in the Astra R1 fix round; Task 1 Step 2 takes it to the deck and Paul). (RFS-3-C28 is live until Plan 6 Task 16 marks it struck when runs reach fleet boxes, R7.)
@@ -190,179 +192,179 @@ From the plan: Placeholders: `<app>` an app; `<fleet>` a fleet name; `<region>`
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C01 | internal/console/rootfs.go | <app> is a rootfs app, and a rootfs app runs only on boxes your account owns. place it on one of your fleets first with carlos apps place. | pending |
-| RFS-4-C02 | internal/console/rootfs.go | <app> is a rootfs app, and a rootfs app does not run in a dedicated pool yet. create the instance without a pool. | pending |
-| RFS-4-C03 | internal/console/rootfs.go | <app> is a rootfs app, so it cannot move to the shared boxes. a rootfs app runs only on boxes your account owns. | pending |
-| RFS-4-C04 | internal/console/rootfs.go | <app> is a rootfs app with instances, and a rootfs app's instances cannot move to other boxes yet. delete its instances first, or create a new app where you want it to run. | pending |
-| RFS-4-C05 | internal/console/rootfs.go | none of fleet <fleet>'s boxes can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check. | pending |
-| RFS-4-C06 | internal/console/rootfs.go | no box in <region> can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check. | pending |
-| RFS-4-C07 | internal/console/rootfs.go | could not check which boxes can run a rootfs app. try again. | pending |
-| RFS-4-C08 | internal/console/rootfs.go | could not read this app's releases. try again. | pending |
-| RFS-4-C09 | internal/console/rootfs.go | <app> is a rootfs app, and a new rootfs instance cannot start from a copy of another database yet. create it empty, then load the data from your app. | pending |
-| RFS-4-C10 | internal/console/rootfs.go | <app> is a rootfs app, and a rootfs app has no replicas yet. | pending |
-| RFS-4-C11 | internal/console/rootfs.go | <app> already has <n> instances made before its first release, such as <host>. an app's first release decides its kind, and those instances were made for a binary app. delete them, ship again, then create them again. | pending |
+| RFS-4-C01 | internal/console/rootfs.go | <app> is a rootfs app, and a rootfs app runs only on boxes your account owns. place it on one of your fleets first with carlos apps place | approved |
+| RFS-4-C02 | internal/console/rootfs.go | <app> is a rootfs app, and a rootfs app does not run in a dedicated pool yet. create the instance without a pool | approved |
+| RFS-4-C03 | internal/console/rootfs.go | <app> is a rootfs app, so it cannot move to the shared boxes. a rootfs app runs only on boxes your account owns | approved |
+| RFS-4-C04 | internal/console/rootfs.go | <app> is a rootfs app with instances, and a rootfs app's instances cannot move to other boxes yet. delete its instances first, or create a new app where you want it to run | approved |
+| RFS-4-C05 | internal/console/rootfs.go | none of fleet <fleet>'s boxes can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check | approved |
+| RFS-4-C06 | internal/console/rootfs.go | no box in <region> can run a rootfs app yet. a box can once it runs a platform release with rootfs support, on the nested layout, and has passed its rootfs check | approved |
+| RFS-4-C07 | internal/console/rootfs.go | could not check which boxes can run a rootfs app. try again | approved |
+| RFS-4-C08 | internal/console/rootfs.go | could not read this app's releases. try again | approved |
+| RFS-4-C09 | internal/console/rootfs.go | <app> is a rootfs app, and a new rootfs instance cannot start from a copy of another database yet. create it empty, then load the data from your app | approved |
+| RFS-4-C10 | internal/console/rootfs.go | <app> is a rootfs app, and a rootfs app has no replicas yet | approved |
+| RFS-4-C11 | internal/console/rootfs.go | <app> already has <n> instances made before its first release, such as <host>. an app's first release decides its kind, and those instances were made for a binary app. delete them, ship again, then create them again | approved |
### CLI (instances_rootfs.go, instances_cli.go)
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C12 | instances_rootfs.go, instances_cli.go | RUNTIME | pending |
-| RFS-4-C13 | instances_rootfs.go, instances_cli.go | WORKERS | pending |
-| RFS-4-C14 | instances_rootfs.go, instances_cli.go | binary | pending |
-| RFS-4-C15 | instances_rootfs.go, instances_cli.go | rootfs | pending |
-| RFS-4-C16 | instances_rootfs.go, instances_cli.go | rootfs, hibernates | pending |
-| RFS-4-C17 | instances_rootfs.go, instances_cli.go | <running>/<total>, <name> restarting | pending |
-| RFS-4-C18 | instances_rootfs.go, instances_cli.go | rootfs, always on. it starts as soon as its box picks it up, and does not wait for a request. | pending |
-| RFS-4-C19 | instances_rootfs.go, instances_cli.go | rootfs, hibernates when idle. | pending |
+| RFS-4-C12 | instances_rootfs.go, instances_cli.go | RUNTIME | approved |
+| RFS-4-C13 | instances_rootfs.go, instances_cli.go | WORKERS | approved |
+| RFS-4-C14 | instances_rootfs.go, instances_cli.go | binary | approved |
+| RFS-4-C15 | instances_rootfs.go, instances_cli.go | rootfs | approved |
+| RFS-4-C16 | instances_rootfs.go, instances_cli.go | rootfs, hibernates | approved |
+| RFS-4-C17 | instances_rootfs.go, instances_cli.go | <running>/<total>, <name> restarting | approved |
+| RFS-4-C18 | instances_rootfs.go, instances_cli.go | rootfs, always on. it starts as soon as its box picks it up, and does not wait for a request | approved |
+| RFS-4-C19 | instances_rootfs.go, instances_cli.go | rootfs, hibernates when idle | approved |
### The sample app (examples/rails-shop)
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C20 | app/views/pages/show.html.erb | Tables in its database: <tables>. Cable messages: <n>. | pending |
-| RFS-4-C21 | `app/views/layouts/application.html.erb`, `app/views/pages/show.html.erb` | shop | pending |
-| RFS-4-C22 | app/views/pages/show.html.erb | Enqueue a job | pending |
-| RFS-4-C23 | app/views/pages/show.html.erb | Jobs done: <done> of <total> | pending |
-| RFS-4-C24 | app/views/pages/show.html.erb | Heartbeats in the last hour: <n> | pending |
-| RFS-4-C25 | app/views/pages/show.html.erb | Upload a file | pending |
-| RFS-4-C26 | app/views/pages/show.html.erb | This page was cached at <time>. | pending |
-| RFS-4-C27 | v2.patch | Latest note: <note> | pending |
-| RFS-4-C28 | README.md | A small Rails 8 app that runs on CARLOS as a rootfs app. The guide Run a Rails app on your own boxes is built from it. | pending |
-| RFS-4-C29 | README.md | Build it with docker buildx for linux/arm64, check it with carlos ship --check, and ship it with carlos ship --rootfs. The guide has every step. | pending |
+| RFS-4-C20 | app/views/pages/show.html.erb | Tables in its database: <tables>. Cable messages: <n>. | approved |
+| RFS-4-C21 | `app/views/layouts/application.html.erb`, `app/views/pages/show.html.erb` | shop | approved |
+| RFS-4-C22 | app/views/pages/show.html.erb | Enqueue a job | approved |
+| RFS-4-C23 | app/views/pages/show.html.erb | Jobs done: <done> of <total> | approved |
+| RFS-4-C24 | app/views/pages/show.html.erb | Heartbeats in the last hour: <n> | approved |
+| RFS-4-C25 | app/views/pages/show.html.erb | Upload a file | approved |
+| RFS-4-C26 | app/views/pages/show.html.erb | This page was cached at <time>. | approved |
+| RFS-4-C27 | v2.patch | Latest note: <note> | approved |
+| RFS-4-C28 | README.md | A small Rails 8 app that runs on CARLOS as a rootfs app. The guide Run a Rails app on your own boxes is built from it. | approved |
+| RFS-4-C29 | README.md | Build it with docker buildx for linux/arm64, check it with carlos ship --check, and ship it with carlos ship --rootfs. The guide has every step. | approved |
### docs/site/rootfs.md
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C30 | docs/site/rootfs.md | Rootfs apps | pending |
-| RFS-4-C31 | docs/site/rootfs.md (fact) | A rootfs app is an app shipped as a read-only Linux image instead of a single binary. You build the image on your own machine, from a Dockerfile or anything else that makes an OCI image or a directory, and carlos ship --rootfs uploads it. The platform never builds anything and never pulls from a registry. | pending |
-| RFS-4-C32 | docs/site/rootfs.md | A rootfs app runs only on boxes your account owns: a fleet of yours on a deployment that runs apps for other people, such as Carloku, or any box of a deployment run for one customer. The box must be on the nested layout and must have passed its rootfs check. The console refuses anywhere else, and so does the box. | pending |
-| RFS-4-C33 | docs/site/rootfs.md | What fixes an app's kind | pending |
-| RFS-4-C34 | docs/site/rootfs.md | An app's first release makes it a binary app or a rootfs app, and it stays that kind. A release of the other kind is refused when you ship it. To change kinds, create a new app. | pending |
-| RFS-4-C35 | docs/site/rootfs.md (fact, D4) | Each instance takes its hibernation setting from the release it is created with, and keeps it. A rootfs app is always on unless that release was shipped with carlos ship --hibernate. A later release that asks for the other setting is refused when you ship it. | pending |
-| RFS-4-C36 | docs/site/rootfs.md | Processes | pending |
-| RFS-4-C37 | docs/site/rootfs.md (fact) | The JSON file you pass to carlos ship --processes names the commands the image runs. Each is an argv array, either an absolute path or a name found on the image's PATH, with an optional env of plain values (never secrets) and an optional working_dir. | pending |
-| RFS-4-C38 | docs/site/rootfs.md | web is required, exactly one. It serves HTTP on CARLOS_SOCKET. | pending |
-| RFS-4-C39 | docs/site/rootfs.md (fact) | release is optional, at most one. It runs on every deploy, the first included, before the new web starts. It may run for ten minutes unless it sets timeout_sec. | pending |
-| RFS-4-C40 | docs/site/rootfs.md | console is optional, at most one. It is the command an interactive session starts. The platform never starts it on its own. | pending |
-| RFS-4-C41 | docs/site/rootfs.md | Any other name is a worker, up to four of them. A name is lowercase letters, digits and hyphens, starts with a letter, and is at most 16 characters long. | pending |
-| RFS-4-C42 | docs/site/rootfs.md | Commands are not run through a shell, so a $CARLOS_SOCKET written in a command is passed as it is. Read the variable in your server's config file instead, or run the command with sh -c if your image has a shell. | pending |
-| RFS-4-C43 | docs/site/rootfs.md | What every process gets | pending |
-| RFS-4-C44 | docs/site/rootfs.md (fact) | Every process of the instance gets the same environment: web, the workers, release and carlos run alike. That is the app's config bundle, the instance's own variables and secrets, its store's coordinates, and these: | pending |
-| RFS-4-C45 | docs/site/rootfs.md | CARLOS_SOCKET is the unix socket web serves HTTP on. | pending |
-| RFS-4-C46 | docs/site/rootfs.md | CARLOS_DB is the instance's SQLite database, the one file the platform replicates. An app that keeps its data somewhere else can ignore it. | pending |
-| RFS-4-C47 | docs/site/rootfs.md (fact) | CARLOS_HOST, CARLOS_REGION, CARLOS_CONTROL_SOCKET and CARLOS_PURGE mean what they mean for a binary instance, as [What an instance is started with](/docs/cli#what-an-instance-is-started-with) says. | pending |
-| RFS-4-C48 | docs/site/rootfs.md | Health | pending |
-| RFS-4-C49 | docs/site/rootfs.md | web must answer GET / on CARLOS_SOCKET with a status below 500 within 120 seconds of starting. Until it does, the instance does not serve and its workers do not start. | pending |
-| RFS-4-C50 | docs/site/rootfs.md | What it can write | pending |
-| RFS-4-C51 | docs/site/rootfs.md | The image is the instance's root filesystem, and it is read-only. An instance writes in two places: the directory CARLOS_DB is in, which is kept across restarts and deploys, and /tmp, which is empty at every start. /tmp is held in memory, so what is in it counts against the instance's memory. | pending |
-| RFS-4-C52 | docs/site/rootfs.md | Memory and CPU | pending |
-| RFS-4-C53 | docs/site/rootfs.md (fact, D10) | All of an instance's processes share one memory and CPU limit, the same tiers a binary instance gets, 1 GB by default. A process that goes over it is stopped and started again. | pending |
-| RFS-4-C54 | docs/site/rootfs.md | JIT compilers | pending |
-| RFS-4-C55 | docs/site/rootfs.md (fact, R11) | Memory is never writable and executable at once, unless the release is shipped with carlos ship --jit. A JIT compiler needs that: Node, Bun and Deno, the JVM, Ruby's YJIT and PHP's opcache JIT. Ruby, PHP and Python run well without one. | pending |
-| RFS-4-C56 | docs/site/rootfs.md | Network | pending |
-| RFS-4-C57 | docs/site/rootfs.md | An instance can reach public addresses, such as an object store or a managed database's public endpoint. It cannot reach private addresses, the box it runs on, or the cloud's metadata service. | pending |
-| RFS-4-C58 | docs/site/rootfs.md | Deployments run for one customer | pending |
-| RFS-4-C59 | docs/site/rootfs.md | A deployment whose boxes all belong to one customer says so with CARLOS_DEPLOYMENT_SINGLE_TENANT=1, in the console's environment and in every box's host.env. A rootfs app can then run on any of its boxes, not only on a fleet. Leave it unset on a deployment that runs apps for other people. The console does not start if the value is anything but 0 or 1. | pending |
-| RFS-4-C60 | docs/site/rootfs.md | Dedicated pools do not count as a customer's own boxes yet, because nothing ties a pool to one account. On a deployment that runs apps for other people, a rootfs app cannot run in one. | pending |
-| RFS-4-C61 | docs/site/rootfs.md | Not in this version | pending |
-| RFS-4-C62 | docs/site/rootfs.md | More than one copy of an app across boxes, more than one SQLite database per instance, moving a rootfs instance to another fleet or region, and starting a new rootfs instance from a copy of another database. | pending |
+| RFS-4-C30 | docs/site/rootfs.md | Rootfs apps | approved |
+| RFS-4-C31 | docs/site/rootfs.md (fact) | A rootfs app is an app shipped as a read-only Linux image instead of a single binary. You build the image on your own machine, from a Dockerfile or anything else that makes an OCI image or a directory, and carlos ship --rootfs uploads it. The platform never builds anything and never pulls from a registry. | approved |
+| RFS-4-C32 | docs/site/rootfs.md | A rootfs app runs only on boxes your account owns: a fleet of yours on a deployment that runs apps for other people, or any box of a deployment run for one customer. The box must be on the nested layout and must have passed its rootfs check. The console refuses anywhere else, and so does the box. | approved |
+| RFS-4-C33 | docs/site/rootfs.md | What fixes an app's kind | approved |
+| RFS-4-C34 | docs/site/rootfs.md | An app's first release makes it a binary app or a rootfs app, and it stays that kind. A release of the other kind is refused when you ship it. To change kinds, create a new app. | approved |
+| RFS-4-C35 | docs/site/rootfs.md (fact, D4) | Each instance takes its hibernation setting from the release it is created with, and keeps it. A rootfs app is always on unless that release was shipped with carlos ship --hibernate. A later release that asks for the other setting is refused when you ship it. | approved |
+| RFS-4-C36 | docs/site/rootfs.md | Processes | approved |
+| RFS-4-C37 | docs/site/rootfs.md (fact) | The JSON file you pass to carlos ship --processes names the commands the image runs. Each is an argv array, either an absolute path or a name found on the image's PATH, with an optional env of plain values (never secrets) and an optional working_dir. | approved |
+| RFS-4-C38 | docs/site/rootfs.md | web is required, exactly one. It serves HTTP on CARLOS_SOCKET. | approved |
+| RFS-4-C39 | docs/site/rootfs.md (fact) | release is optional, at most one. It runs on every deploy, the first included, before the new web starts. It may run for ten minutes unless it sets timeout_sec. | approved |
+| RFS-4-C40 | docs/site/rootfs.md | console is optional, at most one. It is the command an interactive session starts. The platform never starts it on its own. | approved |
+| RFS-4-C41 | docs/site/rootfs.md | Any other name is a worker, up to four of them. A name is lowercase letters, digits and hyphens, starts with a letter, and is at most 16 characters long. | approved |
+| RFS-4-C42 | docs/site/rootfs.md | Commands are not run through a shell, so a $CARLOS_SOCKET written in a command is passed as it is. Read the variable in your server's config file instead, or run the command with sh -c if your image has a shell. | approved |
+| RFS-4-C43 | docs/site/rootfs.md | What every process gets | approved |
+| RFS-4-C44 | docs/site/rootfs.md (fact) | Every process of the instance gets the same environment: web, the workers, release and carlos run alike. That is the app's config bundle, the instance's own variables and secrets, its store's coordinates, and these: | approved |
+| RFS-4-C45 | docs/site/rootfs.md | CARLOS_SOCKET is the unix socket web serves HTTP on. | approved |
+| RFS-4-C46 | docs/site/rootfs.md | CARLOS_DB is the instance's SQLite database, the one file the platform replicates. An app that keeps its data somewhere else can ignore it. | approved |
+| RFS-4-C47 | docs/site/rootfs.md (fact) | CARLOS_HOST, CARLOS_REGION, CARLOS_CONTROL_SOCKET and CARLOS_PURGE mean what they mean for a binary instance, as [What an instance is started with](/docs/cli#what-an-instance-is-started-with) says. | approved |
+| RFS-4-C48 | docs/site/rootfs.md | Health | approved |
+| RFS-4-C49 | docs/site/rootfs.md | web must answer GET / on CARLOS_SOCKET with a status below 500 within 120 seconds of starting. Until it does, the instance does not serve and its workers do not start. | approved |
+| RFS-4-C50 | docs/site/rootfs.md | What it can write | approved |
+| RFS-4-C51 | docs/site/rootfs.md | The image is the instance's root filesystem, and it is read-only. An instance writes in two places: the directory CARLOS_DB is in, which is kept across restarts and deploys, and /tmp, which is empty at every start. /tmp is held in memory, so what is in it counts against the instance's memory. | approved |
+| RFS-4-C52 | docs/site/rootfs.md | Memory and CPU | approved |
+| RFS-4-C53 | docs/site/rootfs.md (fact, D10) | All of an instance's processes share one memory and CPU limit, the same tiers a binary instance gets, 1 GB by default. A process that goes over it is stopped and started again. | approved |
+| RFS-4-C54 | docs/site/rootfs.md | JIT compilers | approved |
+| RFS-4-C55 | docs/site/rootfs.md (fact, R11) | Memory is never writable and executable at once, unless the release is shipped with carlos ship --jit. A JIT compiler needs that: Node, Bun and Deno, the JVM, Ruby's YJIT and PHP's opcache JIT. Ruby, PHP and Python run well without one. | approved |
+| RFS-4-C56 | docs/site/rootfs.md | Network | approved |
+| RFS-4-C57 | docs/site/rootfs.md | An instance can reach public addresses, such as an object store or a managed database's public endpoint. It cannot reach private addresses, the box it runs on, or the cloud's metadata service. | approved |
+| RFS-4-C58 | docs/site/rootfs.md | Deployments run for one customer | approved |
+| RFS-4-C59 | docs/site/rootfs.md | A deployment whose boxes all belong to one customer says so with CARLOS_DEPLOYMENT_SINGLE_TENANT=1, in the console's environment and in every box's host.env. A rootfs app can then run on any of its boxes, not only on a fleet. Leave it unset on a deployment that runs apps for other people. The console does not start if the value is anything but 0 or 1. | approved |
+| RFS-4-C60 | docs/site/rootfs.md | Dedicated pools do not count as a customer's own boxes yet, because nothing ties a pool to one account. On a deployment that runs apps for other people, a rootfs app cannot run in one. | approved |
+| RFS-4-C61 | docs/site/rootfs.md | Not in this version | approved |
+| RFS-4-C62 | docs/site/rootfs.md | More than one copy of an app across boxes, more than one SQLite database per instance, moving a rootfs instance to another fleet or region, and starting a new rootfs instance from a copy of another database. | approved |
### docs/site/rails.md
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C63 | docs/site/rails.md | Run a Rails app on your own boxes | pending |
-| RFS-4-C64 | docs/site/rails.md | This guide takes a Rails 8 app from a Dockerfile to a running instance on boxes your account owns. The app runs from a read-only image you build on your own machine. Its web process and its job worker share one SQLite database, which the platform replicates, and uploads go to the app's own bucket. | pending |
-| RFS-4-C65 | docs/site/rails.md | You need Docker with buildx, the carlos CLI signed in to your console, and a fleet with at least one box. On a deployment that runs apps for other people, such as Carloku, a rootfs app runs only on a fleet your account owns. [carlos fleets](/docs/cli#carlos-fleets) sets one up, and [Rootfs apps](/docs/rootfs) has the full contract. | pending |
-| RFS-4-C66 | docs/site/rails.md | The finished app is in the platform repository under examples/rails-shop. Every file this guide shows is taken from it. | pending |
-| RFS-4-C67 | docs/site/rails.md | The app | pending |
-| RFS-4-C68 | docs/site/rails.md | Start from rails new with SQLite. Rails 8 gives you Solid Queue, Solid Cache and Solid Cable, each with a database of its own. On CARLOS an instance has one database, so all three share it. | pending |
-| RFS-4-C69 | docs/site/rails.md | Puma | pending |
-| RFS-4-C70 | docs/site/rails.md | The platform gives the web process a unix socket to serve on, in CARLOS_SOCKET. Bind Puma to it and to nothing else. A process that listens on a TCP port instead never answers its health check. | pending |
-| RFS-4-C71 | docs/site/rails.md | puma.rb reads the socket from the environment because commands are not run through a shell. Without CARLOS_SOCKET, on your laptop, it listens on port 3000 as usual. | pending |
-| RFS-4-C72 | docs/site/rails.md | One database | pending |
-| RFS-4-C73 | docs/site/rails.md | CARLOS_DB is the path of the instance's database. It is the one file the platform replicates, and it is kept across restarts and deploys. | pending |
-| RFS-4-C74 | docs/site/rails.md | Point Solid Queue, Solid Cache and Solid Cable at the same database. Remove config.solid_queue.connects_to from config/environments/production.rb, remove database: from the production section of config/cache.yml, and remove connects_to from the production section of config/cable.yml. Each then uses the primary connection. | pending |
-| RFS-4-C75 | docs/site/rails.md | Their tables then need migrations of their own. Copy the table definitions from db/queue_schema.rb, db/cache_schema.rb and db/cable_schema.rb into three migrations, and delete the three schema files. | pending |
-| RFS-4-C76 | docs/site/rails.md | SQLite runs in WAL mode, so the web process and the worker can both write to the file. | pending |
-| RFS-4-C77 | docs/site/rails.md | Uploads | pending |
-| RFS-4-C78 | docs/site/rails.md | Keep uploads out of the image and out of the database. carlos store create gives the app a bucket and puts its coordinates in the app's environment. Point Active Storage at them: | pending |
-| RFS-4-C79 | docs/site/rails.md | Then set config.active_storage.service = :carlos in production.rb, and add the aws-sdk-s3 gem. | pending |
-| RFS-4-C80 | docs/site/rails.md | YJIT | pending |
-| RFS-4-C81 | docs/site/rails.md (fact, R11) | Instances run with memory that cannot be written and executed at once, unless the release is shipped with carlos ship --jit, so YJIT cannot start without it. Rails 7.2 and later turn YJIT on by default. This app turns it off in production.rb with config.yjit = false. To keep YJIT, ship with --jit instead. | pending |
-| RFS-4-C82 | docs/site/rails.md | The image | pending |
-| RFS-4-C83 | docs/site/rails.md | Build the image for arm64, which is what the boxes run. buildx does that on any machine, an Intel laptop included. When the image runs, its root filesystem is read-only, and only /tmp and the directory CARLOS_DB is in can be written. | pending |
-| RFS-4-C84 | docs/site/rails.md | tmp is a link to /tmp, because Rails writes pid and cache files there. | pending |
-| RFS-4-C85 | docs/site/rails.md | Bootsnap's cache is built into the image, and BOOTSNAP_READONLY keeps it from writing at run time. | pending |
-| RFS-4-C86 | docs/site/rails.md | Every file is readable by any user, because an instance does not run as root. | pending |
-| RFS-4-C87 | docs/site/rails.md (fact, D4) | There is no ENTRYPOINT or CMD. The platform runs the commands processes.json names, and nothing else. | pending |
-| RFS-4-C88 | docs/site/rails.md | The processes | pending |
-| RFS-4-C89 | docs/site/rails.md (fact, D4) | processes.json, beside the Dockerfile, names the processes. carlos ship --processes reads it when it makes the release: | pending |
-| RFS-4-C90 | docs/site/rails.md | web serves requests. worker runs beside it, from the same image, with the same environment and the same database. | pending |
-| RFS-4-C91 | docs/site/rails.md | release runs on every deploy, before the new web starts. Here it runs the migrations. | pending |
-| RFS-4-C92 | docs/site/rails.md | console is the command an interactive session starts. The platform never starts it on its own. | pending |
-| RFS-4-C93 | docs/site/rails.md (fact, D4) | A rootfs app is always on. To let an instance sleep when nobody uses it, ship the app's first release with carlos ship --hibernate. An instance keeps the setting it was created with. | pending |
-| RFS-4-C94 | docs/site/rails.md | Check it on your machine | pending |
-| RFS-4-C95 | docs/site/rails.md (fact) | carlos ship --check runs the image the way a box would: a read-only root, a private /tmp, a socket, a database, and the default memory limit. It needs Docker or Podman, and says what went wrong, such as a process that listened on a port, a write outside the instance's directory, or an image built for amd64. | pending |
-| RFS-4-C96 | docs/site/rails.md | Ship it | pending |
-| RFS-4-C97 | docs/site/rails.md | Create the app on your fleet, give it instances, a bucket and a secret key base. Then ship the image, promote it, and create the instance. | pending |
-| RFS-4-C98 | docs/site/rails.md | Ship before you create the instance. An app's first release makes it a rootfs app, and an instance takes that, and whether it hibernates, from the release when it is created. | pending |
-| RFS-4-C99 | docs/site/rails.md | The first start runs release, which migrates the empty database, then starts web, then the worker once web answers. carlos instances list shows the instance's runtime and its workers, and carlos logs shows all three processes. | pending |
-| RFS-4-C100 | docs/site/rails.md (fact, D8) | carlos run runs a one-off command in the instance, from its current image. It does not reach instances on a fleet yet. On a deployment run for one customer it works like this: | pending |
-| RFS-4-C101 | docs/site/rails.md | Deploys and migrations | pending |
-| RFS-4-C102 | docs/site/rails.md | A deploy starts the new web beside the old one and moves traffic once the new one answers. release runs first, from the new image, while the old version is still serving. So a migration has to work with the code that is already running: add a column, deploy, use it, and remove the old one in a later deploy. | pending |
-| RFS-4-C103 | docs/site/rails.md | When a migration cannot work that way, ship that release with --swap hold. The old version stops, release runs, and the new version starts. Requests wait for a few seconds while that happens. | pending |
-| RFS-4-C104 | docs/site/rails.md (fact) | If release fails or runs past its time limit, the old version keeps serving and the deploy stops for that instance. carlos instances list says so, and the next deploy tries again. | pending |
-| RFS-4-C105 | docs/site/rails.md | Rolling back a release does not roll back the database. carlos rollback brings the previous image back, and it runs against the schema the newer release left. This is the other reason a migration should work with the code before it. | pending |
-| RFS-4-C106 | docs/site/rails.md | During a deploy the old worker stops once the new web is healthy, and then the new worker starts. Two workers never run at once. | pending |
-| RFS-4-C107 | docs/site/rails.md | When something crashes | pending |
-| RFS-4-C108 | docs/site/rails.md (fact) | The platform starts the app when its box starts, starts web or a worker again when it crashes, and never waits for a visitor to do it. A worker that keeps crashing is started again with a growing delay, and never takes web down with it. | pending |
-| RFS-4-C109 | docs/site/rails.md | Limits | pending |
-| RFS-4-C110 | docs/site/rails.md | [Rootfs apps](/docs/rootfs) lists the limits. The ones a Rails app meets first: one memory limit for all its processes together, 120 seconds for web to answer after it starts, and at most four workers. | pending |
+| RFS-4-C63 | docs/site/rails.md | Run a Rails app on your own boxes | approved |
+| RFS-4-C64 | docs/site/rails.md | This guide takes a Rails 8 app from a Dockerfile to a running instance on boxes your account owns. The app runs from a read-only image you build on your own machine. Its web process and its job worker share one SQLite database, which the platform replicates, and uploads go to the app's own bucket. | approved |
+| RFS-4-C65 | docs/site/rails.md | You need Docker with buildx, the carlos CLI signed in to your console, and a fleet with at least one box. On a deployment that runs apps for other people, a rootfs app runs only on a fleet your account owns. [carlos fleets](/docs/cli#carlos-fleets) sets one up, and [Rootfs apps](/docs/rootfs) has the full contract. | approved |
+| RFS-4-C66 | docs/site/rails.md | The finished app is in the platform repository under examples/rails-shop. Every file this guide shows is taken from it. | approved |
+| RFS-4-C67 | docs/site/rails.md | The app | approved |
+| RFS-4-C68 | docs/site/rails.md | Start from rails new with SQLite. Rails 8 gives you Solid Queue, Solid Cache and Solid Cable, each with a database of its own. On CARLOS an instance has one database, so all three share it. | approved |
+| RFS-4-C69 | docs/site/rails.md | Puma | approved |
+| RFS-4-C70 | docs/site/rails.md | The platform gives the web process a unix socket to serve on, in CARLOS_SOCKET. Bind Puma to it and to nothing else. A process that listens on a TCP port instead never answers its health check. | approved |
+| RFS-4-C71 | docs/site/rails.md | puma.rb reads the socket from the environment because commands are not run through a shell. Without CARLOS_SOCKET, on your laptop, it listens on port 3000 as usual. | approved |
+| RFS-4-C72 | docs/site/rails.md | One database | approved |
+| RFS-4-C73 | docs/site/rails.md | CARLOS_DB is the path of the instance's database. It is the one file the platform replicates, and it is kept across restarts and deploys. | approved |
+| RFS-4-C74 | docs/site/rails.md | Point Solid Queue, Solid Cache and Solid Cable at the same database. Remove config.solid_queue.connects_to from config/environments/production.rb, remove database: from the production section of config/cache.yml, and remove connects_to from the production section of config/cable.yml. Each then uses the primary connection. | approved |
+| RFS-4-C75 | docs/site/rails.md | Their tables then need migrations of their own. Copy the table definitions from db/queue_schema.rb, db/cache_schema.rb and db/cable_schema.rb into three migrations, and delete the three schema files. | approved |
+| RFS-4-C76 | docs/site/rails.md | SQLite runs in WAL mode, so the web process and the worker can both write to the file. | approved |
+| RFS-4-C77 | docs/site/rails.md | Uploads | approved |
+| RFS-4-C78 | docs/site/rails.md | Keep uploads out of the image and out of the database. carlos store create gives the app a bucket and puts its coordinates in the app's environment. Point Active Storage at them: | approved |
+| RFS-4-C79 | docs/site/rails.md | Then set config.active_storage.service = :carlos in production.rb, and add the aws-sdk-s3 gem. | approved |
+| RFS-4-C80 | docs/site/rails.md | YJIT | approved |
+| RFS-4-C81 | docs/site/rails.md (fact, R11) | Instances run with memory that cannot be written and executed at once, unless the release is shipped with carlos ship --jit, so YJIT cannot start without it. Rails 7.2 and later turn YJIT on by default. This app turns it off in production.rb with config.yjit = false. To keep YJIT, ship with --jit instead. | approved |
+| RFS-4-C82 | docs/site/rails.md | The image | approved |
+| RFS-4-C83 | docs/site/rails.md | Build the image for arm64, which is what the boxes run. buildx does that on any machine, an Intel laptop included. When the image runs, its root filesystem is read-only, and only /tmp and the directory CARLOS_DB is in can be written. | approved |
+| RFS-4-C84 | docs/site/rails.md | tmp is a link to /tmp, because Rails writes pid and cache files there. | approved |
+| RFS-4-C85 | docs/site/rails.md | Bootsnap's cache is built into the image, and BOOTSNAP_READONLY keeps it from writing at run time. | approved |
+| RFS-4-C86 | docs/site/rails.md | Every file is readable by any user, because an instance does not run as root. | approved |
+| RFS-4-C87 | docs/site/rails.md (fact, D4) | There is no ENTRYPOINT or CMD. The platform runs the commands processes.json names, and nothing else. | approved |
+| RFS-4-C88 | docs/site/rails.md | The processes | approved |
+| RFS-4-C89 | docs/site/rails.md (fact, D4) | processes.json, beside the Dockerfile, names the processes. carlos ship --processes reads it when it makes the release: | approved |
+| RFS-4-C90 | docs/site/rails.md | web serves requests. worker runs beside it, from the same image, with the same environment and the same database. | approved |
+| RFS-4-C91 | docs/site/rails.md | release runs on every deploy, before the new web starts. Here it runs the migrations. | approved |
+| RFS-4-C92 | docs/site/rails.md | console is the command an interactive session starts. The platform never starts it on its own. | approved |
+| RFS-4-C93 | docs/site/rails.md (fact, D4) | A rootfs app is always on. To let an instance sleep when nobody uses it, ship the app's first release with carlos ship --hibernate. An instance keeps the setting it was created with. | approved |
+| RFS-4-C94 | docs/site/rails.md | Check it on your machine | approved |
+| RFS-4-C95 | docs/site/rails.md (fact) | carlos ship --check runs the image the way a box would: a read-only root, a private /tmp, a socket, a database, and the default memory limit. It needs Docker or Podman, and says what went wrong, such as a process that listened on a port, a write outside the instance's directory, or an image built for amd64. | approved |
+| RFS-4-C96 | docs/site/rails.md | Ship it | approved |
+| RFS-4-C97 | docs/site/rails.md | Create the app on your fleet, give it instances, a bucket and a secret key base. Then ship the image, promote it, and create the instance. | approved |
+| RFS-4-C98 | docs/site/rails.md | Ship before you create the instance. An app's first release makes it a rootfs app, and an instance takes that, and whether it hibernates, from the release when it is created. | approved |
+| RFS-4-C99 | docs/site/rails.md | The first start runs release, which migrates the empty database, then starts web, then the worker once web answers. carlos instances list shows the instance's runtime and its workers, and carlos logs shows all three processes. | approved |
+| RFS-4-C100 | docs/site/rails.md (fact, D8) | carlos run runs a one-off command in the instance, from its current image. It does not reach instances on a fleet yet. On a deployment run for one customer it works like this: | approved |
+| RFS-4-C101 | docs/site/rails.md | Deploys and migrations | approved |
+| RFS-4-C102 | docs/site/rails.md | A deploy starts the new web beside the old one and moves traffic once the new one answers. release runs first, from the new image, while the old version is still serving. So a migration has to work with the code that is already running: add a column, deploy, use it, and remove the old one in a later deploy. | approved |
+| RFS-4-C103 | docs/site/rails.md | When a migration cannot work that way, ship that release with --swap hold. The old version stops, release runs, and the new version starts. Requests wait for a few seconds while that happens. | approved |
+| RFS-4-C104 | docs/site/rails.md (fact) | If release fails or runs past its time limit, the old version keeps serving and the deploy stops for that instance. carlos instances list says so, and the next deploy tries again. | approved |
+| RFS-4-C105 | docs/site/rails.md | Rolling back a release does not roll back the database. carlos rollback brings the previous image back, and it runs against the schema the newer release left. This is the other reason a migration should work with the code before it. | approved |
+| RFS-4-C106 | docs/site/rails.md | During a deploy the old worker stops once the new web is healthy, and then the new worker starts. Two workers never run at once. | approved |
+| RFS-4-C107 | docs/site/rails.md | When something crashes | approved |
+| RFS-4-C108 | docs/site/rails.md (fact) | The platform starts the app when its box starts, starts web or a worker again when it crashes, and never waits for a visitor to do it. A worker that keeps crashing is started again with a growing delay, and never takes web down with it. | approved |
+| RFS-4-C109 | docs/site/rails.md | Limits | approved |
+| RFS-4-C110 | docs/site/rails.md | [Rootfs apps](/docs/rootfs) lists the limits. The ones a Rails app meets first: one memory limit for all its processes together, 120 seconds for web to answer after it starts, and at most four workers. | approved |
### docs/site/laravel-and-django.md
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C111 | docs/site/laravel-and-django.md | Laravel and Django | pending |
-| RFS-4-C112 | docs/site/laravel-and-django.md | The Rails guide's shape works for any framework that runs on Linux. These notes cover what is different for Laravel and for Django. Read [the Rails guide](/docs/rails) first. | pending |
-| RFS-4-C113 | docs/site/laravel-and-django.md | Laravel | pending |
-| RFS-4-C114 | docs/site/laravel-and-django.md | Serve Laravel with FrankenPHP, which is one HTTP server process. php-fpm speaks FastCGI, not HTTP, so it cannot be web on its own. Bind FrankenPHP to the socket in its Caddyfile: | pending |
-| RFS-4-C115 | docs/site/laravel-and-django.md | Point the sqlite connection at the instance's database in config/database.php, and set DB_CONNECTION=sqlite with carlos env set: | pending |
-| RFS-4-C116 | docs/site/laravel-and-django.md | The processes: web runs FrankenPHP, a worker runs the queue, release runs the migrations, and console runs tinker. With the database queue driver the jobs live in the same database. | pending |
-| RFS-4-C117 | docs/site/laravel-and-django.md | Caddy keeps state in its data and config directories. Point them at /tmp with XDG_DATA_HOME and XDG_CONFIG_HOME in web's env, and turn Caddy's admin endpoint off. | pending |
-| RFS-4-C118 | docs/site/laravel-and-django.md | For uploads, Laravel's s3 disk takes the bucket, region and keys from CARLOS_STORE_BUCKET, CARLOS_STORE_REGION, CARLOS_STORE_ACCESS_KEY and CARLOS_STORE_SECRET_KEY in config/filesystems.php. | pending |
-| RFS-4-C119 | docs/site/laravel-and-django.md | Leave the opcache JIT off, as it is by default. opcache itself is fine. | pending |
-| RFS-4-C120 | docs/site/laravel-and-django.md | Django | pending |
-| RFS-4-C121 | docs/site/laravel-and-django.md | Serve Django with Gunicorn, which binds a unix socket itself. Give it the path in gunicorn.conf.py, since commands are not run through a shell: | pending |
-| RFS-4-C122 | docs/site/laravel-and-django.md | In settings.py, use the sqlite3 engine with NAME set to os.environ["CARLOS_DB"]. | pending |
-| RFS-4-C123 | docs/site/laravel-and-django.md | The processes: web runs gunicorn -c gunicorn.conf.py with your WSGI module, release runs python manage.py migrate --noinput, and console runs python manage.py shell. A worker is whatever your task queue runs. Pick one that keeps its jobs in the database, so it shares the instance's database. | pending |
-| RFS-4-C124 | docs/site/laravel-and-django.md | Collect static files when you build the image, and serve them with WhiteNoise. They only need to be read, so the read-only image is fine. | pending |
-| RFS-4-C125 | docs/site/laravel-and-django.md | For uploads, django-storages' S3 backend takes the bucket, region and keys from CARLOS_STORE_BUCKET, CARLOS_STORE_REGION, CARLOS_STORE_ACCESS_KEY and CARLOS_STORE_SECRET_KEY in settings.py. | pending |
-| RFS-4-C126 | docs/site/laravel-and-django.md | Python's own JIT is off unless you build Python with it. Leave it off. | pending |
+| RFS-4-C111 | docs/site/laravel-and-django.md | Laravel and Django | approved |
+| RFS-4-C112 | docs/site/laravel-and-django.md | The Rails guide's shape works for any framework that runs on Linux. These notes cover what is different for Laravel and for Django. Read [the Rails guide](/docs/rails) first. | approved |
+| RFS-4-C113 | docs/site/laravel-and-django.md | Laravel | approved |
+| RFS-4-C114 | docs/site/laravel-and-django.md | Serve Laravel with FrankenPHP, which is one HTTP server process. php-fpm speaks FastCGI, not HTTP, so it cannot be web on its own. Bind FrankenPHP to the socket in its Caddyfile: | approved |
+| RFS-4-C115 | docs/site/laravel-and-django.md | Point the sqlite connection at the instance's database in config/database.php, and set DB_CONNECTION=sqlite with carlos env set: | approved |
+| RFS-4-C116 | docs/site/laravel-and-django.md | The processes: web runs FrankenPHP, a worker runs the queue, release runs the migrations, and console runs tinker. With the database queue driver the jobs live in the same database. | approved |
+| RFS-4-C117 | docs/site/laravel-and-django.md | Caddy keeps state in its data and config directories. Point them at /tmp with XDG_DATA_HOME and XDG_CONFIG_HOME in web's env, and turn Caddy's admin endpoint off. | approved |
+| RFS-4-C118 | docs/site/laravel-and-django.md | For uploads, Laravel's s3 disk takes the bucket, region and keys from CARLOS_STORE_BUCKET, CARLOS_STORE_REGION, CARLOS_STORE_ACCESS_KEY and CARLOS_STORE_SECRET_KEY in config/filesystems.php. | approved |
+| RFS-4-C119 | docs/site/laravel-and-django.md | Leave the opcache JIT off, as it is by default. opcache itself is fine. | approved |
+| RFS-4-C120 | docs/site/laravel-and-django.md | Django | approved |
+| RFS-4-C121 | docs/site/laravel-and-django.md | Serve Django with Gunicorn, which binds a unix socket itself. Give it the path in gunicorn.conf.py, since commands are not run through a shell: | approved |
+| RFS-4-C122 | docs/site/laravel-and-django.md | In settings.py, use the sqlite3 engine with NAME set to os.environ["CARLOS_DB"]. | approved |
+| RFS-4-C123 | docs/site/laravel-and-django.md | The processes: web runs gunicorn -c gunicorn.conf.py with your WSGI module, release runs python manage.py migrate --noinput, and console runs python manage.py shell. A worker is whatever your task queue runs. Pick one that keeps its jobs in the database, so it shares the instance's database. | approved |
+| RFS-4-C124 | docs/site/laravel-and-django.md | Collect static files when you build the image, and serve them with WhiteNoise. They only need to be read, so the read-only image is fine. | approved |
+| RFS-4-C125 | docs/site/laravel-and-django.md | For uploads, django-storages' S3 backend takes the bucket, region and keys from CARLOS_STORE_BUCKET, CARLOS_STORE_REGION, CARLOS_STORE_ACCESS_KEY and CARLOS_STORE_SECRET_KEY in settings.py. | approved |
+| RFS-4-C126 | docs/site/laravel-and-django.md | Python's own JIT is off unless you build Python with it. Leave it off. | approved |
### docs/site/cli.md and docs/site/nav.json
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C127 | `cli.md` (carlos instances) | For a rootfs app, RUNTIME says rootfs, or rootfs, hibernates for an instance that sleeps when idle. WORKERS counts the instance's running workers out of the ones it declares, and names a worker that keeps crashing. A binary instance shows binary and a dash. | pending |
-| RFS-4-C128 | `cli.md` (carlos apps) | A rootfs app stays on boxes your account owns. On a deployment that runs apps for other people, --target "" is refused for it, and so is moving it to another fleet while it has instances. [Rootfs apps](/docs/rootfs) says why. | pending |
-| RFS-4-C129 | `nav.json` label | Rootfs apps | pending |
-| RFS-4-C130 | `nav.json` blurb | What a rootfs app's image is started with, what it can write, and where it may run. | pending |
-| RFS-4-C131 | `nav.json` label | Run a Rails app | pending |
-| RFS-4-C132 | `nav.json` blurb | Build a Rails 8 app into an image, ship it, and run it with a worker on your own boxes. | pending |
-| RFS-4-C133 | `nav.json` label | Laravel and Django | pending |
-| RFS-4-C134 | `nav.json` blurb | What changes from the Rails guide for a Laravel or a Django app. | pending |
-| RFS-4-C135 | `nav.json` section | Guides | pending |
+| RFS-4-C127 | `cli.md` (carlos instances) | For a rootfs app, RUNTIME says `rootfs`, or `rootfs, hibernates` for an instance that sleeps when idle. WORKERS counts the instance's running workers out of the ones it declares, and names a worker that keeps crashing. A binary instance shows `binary` and a dash. | approved |
+| RFS-4-C128 | `cli.md` (carlos apps) | A rootfs app stays on boxes your account owns. On a deployment that runs apps for other people, --target "" is refused for it, and so is moving it to another fleet while it has instances. [Rootfs apps](/docs/rootfs) says why. | approved |
+| RFS-4-C129 | `nav.json` label | Rootfs apps | approved |
+| RFS-4-C130 | `nav.json` blurb | What a rootfs app's image is started with, what it can write, and where it may run. | approved |
+| RFS-4-C131 | `nav.json` label | Run a Rails app | approved |
+| RFS-4-C132 | `nav.json` blurb | Build a Rails 8 app into an image, ship it, and run it with a worker on your own boxes. | approved |
+| RFS-4-C133 | `nav.json` label | Laravel and Django | approved |
+| RFS-4-C134 | `nav.json` blurb | What changes from the Rails guide for a Laravel or a Django app. | approved |
+| RFS-4-C135 | `nav.json` section | Guides | approved |
### docs/site/rootfs.md, JIT rows
| id | file | text | status |
|---|---|---|---|
-| RFS-4-C136 | docs/site/rootfs.md (fact, R11) | A release shipped with --jit has jit set in its manifest, and every process of its instances runs with that protection off: web, the workers, release, console, carlos run and interactive sessions. Nothing else about the instance changes. The setting belongs to the release, so a later release can turn it on or off. | pending |
-| RFS-4-C137 | docs/site/rootfs.md (fact, R11) | Node stops at start without --jit. node --jitless does run, but only as an interpreter and without WebAssembly, which makes most apps much slower. Ship a Node app with --jit instead. | pending |
-| RFS-4-C138 | docs/site/rootfs.md (fact, R11) | carlos ship --check warns when a process runs Node, Bun or Deno and the release is not shipped with --jit, because the check passes on your machine and the box then stops the process at start. | pending |
+| RFS-4-C136 | docs/site/rootfs.md (fact, R11) | A release shipped with --jit has jit set in its manifest, and every process of its instances runs with that protection off: web, the workers, release, console, carlos run and interactive sessions. Nothing else about the instance changes. The setting belongs to the release, so a later release can turn it on or off. | approved |
+| RFS-4-C137 | docs/site/rootfs.md (fact, R11) | Node stops at start without --jit. node --jitless does run, but only as an interpreter and without WebAssembly, which makes most apps much slower. Ship a Node app with --jit instead. | approved |
+| RFS-4-C138 | docs/site/rootfs.md (fact, R11) | carlos ship --check warns when a process runs Node, Bun or Deno and the release is not shipped with --jit, because the check passes on your machine and the box then stops the process at start. | approved |
From the plan: (The WORKERS count `<running>/<total>` alone is a number, not copy, so it has no row. RFS-4-C20, unused in the first draft, is the sample page's database line.)
@@ -372,50 +374,50 @@ From the plan: Human text only. `%s`/`%q`/`%d` are filled by the code. No em das
| id | file | text | status |
|---|---|---|---|
-| RFS-5-C01 | ship and grant refusal (the grant door and CLI drop the `capabilities: egress: ` prefix) | capabilities: egress: %q is not an address range or a hostname. use a range such as 10.0.0.0/16, a single address, or a name such as db.example.com | pending |
-| RFS-5-C02 | ship and grant refusal | capabilities: egress: %q has host bits set. write it as %s | pending |
-| RFS-5-C03 | ship and grant refusal | capabilities: egress: %q includes %s, which is %s. egress never opens it | pending |
-| RFS-5-C04 | class names in C03 and show | `this host` · `loopback` · `link-local addresses, where the cloud metadata service lives` · `the cloud metadata service` · `multicast` · `reserved addresses` · `an address translation range` | pending |
-| RFS-5-C05 | ship and grant refusal | capabilities: egress: %q is wider than /%d. grant the database's own subnet, not a whole network | pending |
-| RFS-5-C06 | ship and grant refusal | capabilities: egress: %d destinations, at most %d | pending |
-| RFS-5-C07 | ship and grant refusal | capabilities: egress: %q is listed twice | pending |
-| RFS-5-C08 | ship refusal | capabilities: egress and doc_engine contradict. doc_engine fences a conversion engine away from private networks, and egress opens them. drop one | pending |
-| RFS-5-C09 | grant door 409 | this app already has %d egress destinations. revoke one before you grant another | pending |
-| RFS-5-C10 | grant and revoke door 502 | could not publish the egress grant. try again | pending |
-| RFS-5-C11 | grant and revoke door 500 | could not save the egress grant. try again | pending |
-| RFS-5-C12 | grant door 400 | name at least one destination to grant | pending |
-| RFS-5-C13 | revoke door 400, CLI | name the destinations to revoke, or use --all | pending |
-| RFS-5-C14 | CLI after grant | granted %s to %s/%s. it applies on the app's own boxes within a minute, to destinations its live build also declares | pending |
-| RFS-5-C15 | CLI after revoke | revoked %s from %s/%s. boxes close it within a minute | pending |
-| RFS-5-C16 | CLI, revoke of nothing granted | %s/%s had none of those destinations. nothing to revoke | pending |
-| RFS-5-C17 | CLI show | granted: %s | pending |
-| RFS-5-C18 | CLI show | nothing granted | pending |
-| RFS-5-C19 | CLI show | applied: %s (%d address ranges on this instance) | pending |
-| RFS-5-C20 | CLI show | not applied: %s | pending |
-| RFS-5-C21 | reason `not-own-box`, `box` | egress applies only on the customer's own boxes, and this box is not one | pending |
-| RFS-5-C22 | reason `not-nested` | the instance is not nested on this box | pending |
-| RFS-5-C23 | reason `manifest-unreadable` | the box could not read the live build's manifest | pending |
-| RFS-5-C24 | reason `not-declared` | the live build does not declare egress | pending |
-| RFS-5-C25 | reason `not-granted` | nothing it declares is granted | pending |
-| RFS-5-C26 | reason `record-unreadable` | the box has not read the grant yet | pending |
-| RFS-5-C27 | reason `record-expired` | the grant on this box has expired. the console renews it, so check that the console is running | pending |
-| RFS-5-C28 | reason `local-addrs` | the box could not list its own addresses, so it opened nothing | pending |
-| RFS-5-C29 | reason `forbidden` | a destination named an address egress never opens | pending |
-| RFS-5-C30 | reason `too-many` | more than %d address ranges after resolution | pending |
-| RFS-5-C31 | reason `malformed` | a destination was not a valid range | pending |
-| RFS-5-C32 | reason `systemctl`, `control-unreachable` | the box's instance control service did not apply it. it retries within seconds | pending |
-| RFS-5-C33 | CLI show, per name | %s did not resolve | pending |
-| RFS-5-C34 | CLI show, stale box | this box has not reported since %s | pending |
-| RFS-5-C35 | CLI show, operator | box %s | pending |
-| RFS-5-C36 | CLI usage | `usage: carlos egress grant --app <a> <destination>...` / ` carlos egress revoke --app <a> (<destination>... \| --all)` / ` carlos egress show --app <a>` / flags `--account`, `--account-id`, `--console` as in door-key usage / `A destination is an address range such as 10.0.0.0/16, a single address, or a hostname such as db.example.com.` / `on a deployment that runs apps for other people, only an owner of the app's account can grant, for an app on one of the account's own fleets, and anyone in the account can revoke. on a deployment run for one customer, only its operators can grant or revoke. anyone in the account can run show.` | pending |
-| RFS-5-C37 | CLI usage | An app reaches a destination only when its live build declares it too, and only on the customer's own boxes: a fleet box, or any box of a single-tenant deployment. | pending |
-| RFS-5-C38 | CLI usage | Egress opens addresses, not ports. a granted address is reachable on every port, so keep the database's own firewall in place. | pending |
-| RFS-5-C39 | CLI usage | Hostnames are looked up every few seconds, and an address stays open for ten minutes after the name stops returning it. for endpoints that return a different address on each lookup, grant the subnet instead. | pending |
-| RFS-5-C40 | README, App capabilities | egress lets a nested instance reach private addresses its build names: a database or cache in the customer's own network. The build declares the destinations, as "egress": ["10.0.0.0/16", "db.example.com"], and an owner of the account that owns the fleet grants them to the app with carlos egress grant (on a deployment run for one customer, its operator does). A box opens only what both name, only for nested instances, and only on the customer's own boxes. On a box shared with other customers it opens nothing, because there a private range is the platform's own network. Whatever the list says, egress never opens the cloud metadata service, the box's loopback or the box's own addresses. It opens addresses, not ports: a granted address is reachable on every port, so the database's own firewall stays the port fence. | pending |
-| RFS-5-C41 | docs/site/cli.md, `### carlos egress` | `Lets an app's nested instances reach private destinations, such as a managed database in your own network. Your app's build declares the destinations, and an owner of your account grants them to the app for its own fleets (on a deployment run for one customer, the deployment's operator grants them). An instance gets only what both name, and only on your own boxes: a fleet box, or any box of a single-tenant deployment.` / C37, C38, C39 as paragraphs / `show lists what is granted and, for each instance, whether its box applied it and why not.` | pending |
-| RFS-5-C42 | grant door 409 (shared deployment; revoke needs no placement, R8) | %s/%s does not run on a fleet your account owns, so egress cannot be granted to it. place it on one of your fleets first with carlos apps place | pending |
-| RFS-5-C43 | ship refusal (rootfs release), replaces RFS-1-C15 | a rootfs release can declare only the egress capability | pending |
-| RFS-5-C44 | grant door 403 (shared deployment, a member who is not an owner; R8), printed by the CLI | only an owner of the account can grant egress. anyone in the account can revoke it | pending |
+| RFS-5-C01 | ship and grant refusal (the grant door and CLI drop the `capabilities: egress: ` prefix) | capabilities: egress: %q is not an address range or a hostname. use a range such as 10.0.0.0/16, a single address, or a name such as db.example.com | approved |
+| RFS-5-C02 | ship and grant refusal | capabilities: egress: %q has host bits set. write it as %s | approved |
+| RFS-5-C03 | ship and grant refusal | capabilities: egress: %q includes %s, which is %s. egress never opens it | approved |
+| RFS-5-C04 | class names in C03 and show | `this host` · `loopback` · `link-local addresses, where the cloud metadata service lives` · `the cloud metadata service` · `multicast` · `reserved addresses` · `an address translation range` | approved |
+| RFS-5-C05 | ship and grant refusal | capabilities: egress: %q is wider than /%d. grant the database's own subnet, not a whole network | approved |
+| RFS-5-C06 | ship and grant refusal | capabilities: egress: %d destinations, at most %d | approved |
+| RFS-5-C07 | ship and grant refusal | capabilities: egress: %q is listed twice | approved |
+| RFS-5-C08 | ship refusal | capabilities: egress and doc_engine contradict. doc_engine fences a conversion engine away from private networks, and egress opens them. drop one | approved |
+| RFS-5-C09 | grant door 409 | this app already has %d egress destinations. revoke one before you grant another | approved |
+| RFS-5-C10 | grant and revoke door 502 | could not publish the egress grant. try again | approved |
+| RFS-5-C11 | grant and revoke door 500 | could not save the egress grant. try again | approved |
+| RFS-5-C12 | grant door 400 | name at least one destination to grant | approved |
+| RFS-5-C13 | revoke door 400, CLI | name the destinations to revoke, or use --all | approved |
+| RFS-5-C14 | CLI after grant | granted %s to %s/%s. it applies on the app's own boxes within a minute, to destinations its live build also declares | approved |
+| RFS-5-C15 | CLI after revoke | revoked %s from %s/%s. boxes close it within a minute | approved |
+| RFS-5-C16 | CLI, revoke of nothing granted | %s/%s had none of those destinations. nothing to revoke | approved |
+| RFS-5-C17 | CLI show | granted: %s | approved |
+| RFS-5-C18 | CLI show | nothing granted | approved |
+| RFS-5-C19 | CLI show | applied: %s (%d address ranges on this instance) | approved |
+| RFS-5-C20 | CLI show | not applied: %s | approved |
+| RFS-5-C21 | reason `not-own-box`, `box` | egress applies only on the customer's own boxes, and this box is not one | approved |
+| RFS-5-C22 | reason `not-nested` | the instance is not nested on this box | approved |
+| RFS-5-C23 | reason `manifest-unreadable` | the box could not read the live build's manifest | approved |
+| RFS-5-C24 | reason `not-declared` | the live build does not declare egress | approved |
+| RFS-5-C25 | reason `not-granted` | nothing it declares is granted | approved |
+| RFS-5-C26 | reason `record-unreadable` | the box has not read the grant yet | approved |
+| RFS-5-C27 | reason `record-expired` | the grant on this box has expired. the console renews it, so check that the console is running | approved |
+| RFS-5-C28 | reason `local-addrs` | the box could not list its own addresses, so it opened nothing | approved |
+| RFS-5-C29 | reason `forbidden` | a destination named an address egress never opens | approved |
+| RFS-5-C30 | reason `too-many` | more than %d address ranges after resolution | approved |
+| RFS-5-C31 | reason `malformed` | a destination was not a valid range | approved |
+| RFS-5-C32 | reason `systemctl`, `control-unreachable` | the box's instance control service did not apply it. it retries within seconds | approved |
+| RFS-5-C33 | CLI show, per name | %s did not resolve | approved |
+| RFS-5-C34 | CLI show, stale box | this box has not reported since %s | approved |
+| RFS-5-C35 | CLI show, operator | box %s | approved |
+| RFS-5-C36 | CLI usage | `usage: carlos egress grant --app <a> <destination>...` / ` carlos egress revoke --app <a> (<destination>... \| --all)` / ` carlos egress show --app <a>` / flags `--account`, `--account-id`, `--console` as in door-key usage / `a destination is an address range such as 10.0.0.0/16, a single address, or a hostname such as db.example.com` / `on a deployment that runs apps for other people, only an owner of the app's account can grant, for an app on one of the account's own fleets, and anyone in the account can revoke. on a deployment run for one customer, only its operators can grant or revoke. anyone in the account can run show` | approved |
+| RFS-5-C37 | CLI usage | an app reaches a destination only when its live build declares it too, and only on the customer's own boxes: a fleet box, or any box of a single-tenant deployment | approved |
+| RFS-5-C38 | CLI usage | egress opens addresses, not ports. a granted address is reachable on every port, so keep the database's own firewall in place | approved |
+| RFS-5-C39 | CLI usage | hostnames are looked up every few seconds, and an address stays open for ten minutes after the name stops returning it. for endpoints that return a different address on each lookup, grant the subnet instead | approved |
+| RFS-5-C40 | README, App capabilities | egress lets a nested instance reach private addresses its build names: a database or cache in the customer's own network. The build declares the destinations as "egress": ["10.0.0.0/16", "db.example.com"], and an owner of the account that owns the fleet grants them to the app with carlos egress grant (on a deployment run for one customer, its operator does). A box opens only what the build names and the account grants, only for nested instances, and only on the customer's own boxes. On a box shared with other customers it opens nothing, because a private range there is the platform's own network. Whatever the list says, egress never opens the cloud metadata service, the box's loopback or the box's own addresses. It opens addresses, not ports: a granted address is reachable on every port, so the database's own firewall stays the port fence. | approved |
+| RFS-5-C41 | docs/site/cli.md, `### carlos egress` | `Lets an app's nested instances reach private destinations, such as a managed database in your own network. Your app's build declares the destinations, and an owner of your account grants them to the app for its own fleets (on a deployment run for one customer, the deployment's operator grants them). An instance gets only destinations both the build names and the account grants, and only on your own boxes: a fleet box or any box of a single-tenant deployment.` / `Egress opens addresses, not ports. A granted address is reachable on every port, so keep the database's own firewall in place.` / `Hostnames are looked up every few seconds, and an address stays open for ten minutes after the name stops returning it. For endpoints that return a different address on each lookup, grant the subnet instead.` / `Show lists what is granted and, for each instance, whether its box applied it and why not.` | approved |
+| RFS-5-C42 | grant door 409 (shared deployment; revoke needs no placement, R8) | %s/%s does not run on a fleet your account owns, so egress cannot be granted to it. place it on one of your fleets first with carlos apps place | approved |
+| RFS-5-C43 | ship refusal (rootfs release), replaces RFS-1-C15 | a rootfs release can declare only the egress capability | approved |
+| RFS-5-C44 | grant door 403 (shared deployment, a member who is not an owner; R8), printed by the CLI | only an owner of the account can grant egress. anyone in the account can revoke it | approved |
From the plan: String count: **44 IDs** (C04 carries seven class names; C36 and C41 are multi-line blocks). Changed in the Astra R1 round for R8: C36, C40 and C41 (text) and C42 (where it is used); added: C44.
@@ -425,70 +427,70 @@ From the plan: Every string is human text the plan introduces. Existing approved
| id | file | text | status |
|---|---|---|---|
-| RFS-6-C01 | `run.go` usage | carlos run --app <app> --instance <host> -it -- <arg>... | pending |
-| RFS-6-C02 | `run.go` flag `-it`, `--interactive` | run the command in a terminal session. what you type goes to it, and its screen comes back | pending |
-| RFS-6-C03 | run.go | -it takes no --file. a session reads from your keyboard instead | pending |
-| RFS-6-C04 | run.go | -it takes no --timeout. a session ends after 30 minutes without a keystroke, and after 6 hours at most | pending |
-| RFS-6-C05 | run.go | -it takes no --follow. a session cannot be picked up again once its connection is gone | pending |
-| RFS-6-C06 | interactive_cli.go | carlos run -it needs a terminal. run it from an interactive shell, or leave out -it to run the command without one | pending |
-| RFS-6-C07 | interactive_term_other.go | terminal sessions do not work on Windows yet. use carlos run without -it, or a macOS or Linux terminal | pending |
-| RFS-6-C08 | `interactive_cli.go` usage | usage: carlos console --app <app> --instance <host> | pending |
-| RFS-6-C09 | flag `--app` | the app whose console to open | pending |
-| RFS-6-C10 | flag `--instance` | the instance to open it on | pending |
-| RFS-6-C11 | interactive_cli.go | carlos console takes no command. it opens the console process the app's manifest declares. to run another command: carlos run -it --app <app> --instance <host> -- <command> | pending |
-| RFS-6-C12 | interactive_cli.go | carlos console works only through a signed-in console, and this shell is in direct bucket mode. unset CARLOS_DEPLOYMENT_BUCKET and CARLOS_DEPLOYMENT_DIR, then run carlos auth login --console <url> | pending |
-| RFS-6-C13 | status | %s: waiting for its box to open the session | pending |
-| RFS-6-C14 | status | %s: starting the session. a sleeping instance wakes first | pending |
-| RFS-6-C15 | status | %s: connected. the session ends when the command exits | pending |
-| RFS-6-C16 | interactive_cli.go | could not open a session connection to %s: %v | pending |
-| RFS-6-C17 | interactive_cli.go | lost the connection to %s. the box closes the session within two minutes | pending |
-| RFS-6-C18 | sessionExit | %s: no box opened the session within 60 seconds. its box may be running an older release, or may not reach the console | pending |
-| RFS-6-C19 | sessionExit | %s: the session ended after 30 minutes without a keystroke | pending |
-| RFS-6-C20 | sessionExit | %s: the session ended at its 6 hour limit | pending |
-| RFS-6-C21 | sessionExit | %s: the session was interrupted: %s | pending |
-| RFS-6-C22 | sessionExit | %s: the connection to its box dropped, so the session was closed | pending |
-| RFS-6-C23 | sessionExit | %s: the session was cancelled before it started | pending |
-| RFS-6-C24 | sessionExit | %s: the box is not on the nested layout yet, so it cannot open a session | pending |
-| RFS-6-C25 | sessionExit | %s: the box has host claims armed. it cannot open a session yet | pending |
-| RFS-6-C26 | sessionExit | %s: the instance did not wake. %s | pending |
-| RFS-6-C27 | console door and `sessionExit` | interactive sessions are off for %s. an owner can turn them on with carlos apps set %s --interactive on | pending |
-| RFS-6-C28 | sessionExit | %s was deleted and created again after this session was requested. the session did not start | pending |
-| RFS-6-C29 | sessionExit | %s is an import that is not committed yet. the session did not start | pending |
-| RFS-6-C30 | sessionExit | %s: its box is still closing the last session. try again in a minute | pending |
-| RFS-6-C31 | sessionExit | carlos run: the box did not open the session: %s | pending |
-| RFS-6-C32 | console door | %s already has an interactive session open, started by %s | pending |
-| RFS-6-C33 | console door | %s runs a release that declares no console process. add a console entry to processes in the app's manifest, or name the command: carlos run -it --app %s --instance %s -- <command> | pending |
-| RFS-6-C34 | console door | interactive sessions need this console to hold the release signing key | pending |
-| RFS-6-C35 | console doors, host-claim | interactive sessions are not available on this console yet. they need the host-claim launch protocol | pending |
-| RFS-6-C36 | console door | the session request is malformed | pending |
-| RFS-6-C37 | console door | could not open the session. try again | pending |
-| RFS-6-C38 | setting door | only an owner of the account can turn interactive sessions on | pending |
-| RFS-6-C39 | setting door | say whether interactive sessions are on or off | pending |
-| RFS-6-C40 | setting door audit detail | turned interactive sessions %s for %s (%s) | pending |
-| RFS-6-C41 | `apps_swap.go` usage | --interactive on\|off whether members may open a terminal session on the app's instances, with carlos run -it or carlos console. It is off until an owner turns it on. A session can read and change everything the instance can. | pending |
-| RFS-6-C42 | flag `--interactive` | on or off: whether members may open terminal sessions on the app's instances | pending |
-| RFS-6-C43 | apps_swap.go | apps set: --interactive is on or off, not %q | pending |
-| RFS-6-C44 | apps_swap.go | %s: interactive sessions are on. any member can open a terminal on its instances with carlos run -it | pending |
-| RFS-6-C45 | apps_swap.go | %s: interactive sessions are off | pending |
-| RFS-6-C46 | apps_swap.go | apps set: the change did not stick (the console stored interactive %s). Run it again | pending |
-| RFS-6-C47 | `apps_swap.go:39`, replaces `appsSetNothing` | apps set: nothing to set. Pass --swap, --swap-concurrency, --interactive or one of the update settings | pending |
-| RFS-6-C48 | carlos logs | interactive session %s opened by %s | pending |
-| RFS-6-C49 | carlos logs | interactive session %s ended with exit %d after %s | pending |
-| RFS-6-C50 | carlos logs | interactive session %s ended after %s: %s | pending |
-| RFS-6-C51 | `carlos logs` reason | 30 minutes passed without a keystroke | pending |
-| RFS-6-C52 | `carlos logs` reason | it reached its 6 hour limit | pending |
-| RFS-6-C53 | `carlos logs` reason | the person's connection closed | pending |
-| RFS-6-C54 | `carlos logs` reason | the box lost its connection to the console | pending |
-| RFS-6-C55 | carlos logs | interactive session %s did not start: %s | pending |
-| RFS-6-C56 | `carlos logs` refusal | interactive sessions are off for this app | pending |
-| RFS-6-C57 | `carlos logs` refusal | the box was still closing the last session | pending |
-| RFS-6-C58 | `cli.md`, `### carlos run` | With -it, the command runs in a terminal session instead: what you type goes to it and its screen comes back, so a REPL or a shell works. The app must allow it first, with carlos apps set <app> --interactive on, which only an owner can do. A session ends when the command exits, after 30 minutes without a keystroke, after 6 hours, or soon after your connection drops. A deploy, a restart or a move of the instance ends it too. Ctrl-C and Ctrl-D go to the command. One session at a time per instance, beside any run. Who opened it, where, when it ended and how are in the audit log. What you type and what it prints are not kept. | pending |
-| RFS-6-C59 | `cli.md`, `### carlos console` | Opens the console process your app's manifest declares, in a terminal session, as carlos run -it would. carlos ship fills one in for Rails, Laravel and Django apps when the manifest has none. An app without one is refused, and the message says how to name a command instead. | pending |
-| RFS-6-C60 | `cli.md`, `carlos apps set` | --interactive on lets any member open a terminal session on the app's instances with carlos run -it or carlos console. A session can read and change everything the instance can, so it is off until an owner turns it on. Any member can turn it off. | pending |
-| RFS-6-C61 | `cli.md`, exit codes | A session exits with the command's own status, 124 when it ended for lack of keystrokes or at 6 hours, 125 when it was interrupted or its box's connection dropped, 130 when you cancelled it before it started, 3 when the box refused it or none opened it within 60 seconds, 2 when the console refused it, and 1 when carlos lost its connection to the console. | pending |
-| RFS-6-C62 | `cli.md`, `### carlos run`, replaces RFS-3-C28 | On a customer fleet, the instance's box reaches the console for the run itself, so carlos run works there as it does on any other box. | pending |
-| RFS-6-C63 | `carlos logs` (box, a run grant that did not verify) | run %s was not started on this box: its grant could not be verified | pending |
-| RFS-6-C64 | `carlos logs` (box, the relay dropped mid-run) | run %s lost its connection to the console, so it was stopped | pending |
+| RFS-6-C01 | `run.go` usage | carlos run --app <app> --instance <host> -it -- <arg>... | approved |
+| RFS-6-C02 | `run.go` flag `-it`, `--interactive` | run the command in a terminal session. what you type goes to it, and its screen comes back | approved |
+| RFS-6-C03 | run.go | -it takes no --file. a session reads from your keyboard instead | approved |
+| RFS-6-C04 | run.go | -it takes no --timeout. a session ends after 30 minutes without a keystroke, and after 6 hours at most | approved |
+| RFS-6-C05 | run.go | -it takes no --follow. a session cannot be picked up again once its connection is gone | approved |
+| RFS-6-C06 | interactive_cli.go | carlos run -it needs a terminal. run it from an interactive shell, or leave out -it to run the command without one | approved |
+| RFS-6-C07 | interactive_term_other.go | terminal sessions do not work on Windows yet. use carlos run without -it, or a macOS or Linux terminal | approved |
+| RFS-6-C08 | `interactive_cli.go` usage | usage: carlos console --app <app> --instance <host> | approved |
+| RFS-6-C09 | flag `--app` | the app whose console to open | approved |
+| RFS-6-C10 | flag `--instance` | the instance to open it on | approved |
+| RFS-6-C11 | interactive_cli.go | carlos console takes no command. it opens the console process the app's manifest declares. to run another command: carlos run -it --app <app> --instance <host> -- <command> | approved |
+| RFS-6-C12 | interactive_cli.go | carlos console works only through a signed-in console, and this shell is in direct bucket mode. unset CARLOS_DEPLOYMENT_BUCKET and CARLOS_DEPLOYMENT_DIR, then run carlos auth login --console <url> | approved |
+| RFS-6-C13 | status | %s: waiting for its box to open the session | approved |
+| RFS-6-C14 | status | %s: starting the session. a sleeping instance wakes first | approved |
+| RFS-6-C15 | status | %s: connected. the session ends when the command exits | approved |
+| RFS-6-C16 | interactive_cli.go | could not open a session connection to %s: %v | approved |
+| RFS-6-C17 | interactive_cli.go | lost the connection to %s. the box closes the session within two minutes | approved |
+| RFS-6-C18 | sessionExit | %s: no box opened the session within 60 seconds. its box may be running an older release, or may not reach the console | approved |
+| RFS-6-C19 | sessionExit | %s: the session ended after 30 minutes without a keystroke | approved |
+| RFS-6-C20 | sessionExit | %s: the session ended at its 6 hour limit | approved |
+| RFS-6-C21 | sessionExit | %s: the session was interrupted: %s | approved |
+| RFS-6-C22 | sessionExit | %s: the connection to its box dropped, so the session was closed | approved |
+| RFS-6-C23 | sessionExit | %s: the session was cancelled before it started | approved |
+| RFS-6-C24 | sessionExit | %s: the box is not on the nested layout yet, so it cannot open a session | approved |
+| RFS-6-C25 | sessionExit | %s: the box has host claims armed. it cannot open a session yet | approved |
+| RFS-6-C26 | sessionExit | %s: the instance did not wake. %s | approved |
+| RFS-6-C27 | console door and `sessionExit` | interactive sessions are off for %s. an owner can turn them on with carlos apps set %s --interactive on | approved |
+| RFS-6-C28 | sessionExit | %s was deleted and created again after this session was requested. the session did not start | approved |
+| RFS-6-C29 | sessionExit | %s is an import that is not committed yet. the session did not start | approved |
+| RFS-6-C30 | sessionExit | %s: its box is still closing the last session. try again in a minute | approved |
+| RFS-6-C31 | sessionExit | carlos run: the box did not open the session: %s | approved |
+| RFS-6-C32 | console door | %s already has an interactive session open, started by %s | approved |
+| RFS-6-C33 | console door | %s runs a release that declares no console process. add a console entry to processes in the app's manifest, or name the command: carlos run -it --app %s --instance %s -- <command> | approved |
+| RFS-6-C34 | console door | interactive sessions need this console to hold the release signing key | approved |
+| RFS-6-C35 | console doors, host-claim | interactive sessions are not available on this console yet. they need the host-claim launch protocol | approved |
+| RFS-6-C36 | console door | the session request is malformed | approved |
+| RFS-6-C37 | console door | could not open the session. try again | approved |
+| RFS-6-C38 | setting door | only an owner of the account can turn interactive sessions on | approved |
+| RFS-6-C39 | setting door | say whether interactive sessions are on or off | approved |
+| RFS-6-C40 | setting door audit detail | turned interactive sessions %s for %s (%s) | approved |
+| RFS-6-C41 | `apps_swap.go` usage | --interactive on\|off whether members may open a terminal session on the app's instances, with carlos run -it or carlos console. it is off until an owner turns it on. a session can read and change everything the instance can | approved |
+| RFS-6-C42 | flag `--interactive` | on or off: whether members may open terminal sessions on the app's instances | approved |
+| RFS-6-C43 | apps_swap.go | apps set: --interactive is on or off, not %q | approved |
+| RFS-6-C44 | apps_swap.go | %s: interactive sessions are on. any member can open a terminal on its instances with carlos run -it | approved |
+| RFS-6-C45 | apps_swap.go | %s: interactive sessions are off | approved |
+| RFS-6-C46 | apps_swap.go | apps set: the change did not stick (the console stored interactive %s). run it again | approved |
+| RFS-6-C47 | `apps_swap.go:39`, replaces `appsSetNothing` | apps set: nothing to set. Pass --swap, --swap-concurrency, --interactive or one of the update settings | approved |
+| RFS-6-C48 | carlos logs | interactive session %s opened by %s | approved |
+| RFS-6-C49 | carlos logs | interactive session %s ended with exit %d after %s | approved |
+| RFS-6-C50 | carlos logs | interactive session %s ended after %s: %s | approved |
+| RFS-6-C51 | `carlos logs` reason | 30 minutes passed without a keystroke | approved |
+| RFS-6-C52 | `carlos logs` reason | it reached its 6 hour limit | approved |
+| RFS-6-C53 | `carlos logs` reason | the person's connection closed | approved |
+| RFS-6-C54 | `carlos logs` reason | the box lost its connection to the console | approved |
+| RFS-6-C55 | carlos logs | interactive session %s did not start: %s | approved |
+| RFS-6-C56 | `carlos logs` refusal | interactive sessions are off for this app | approved |
+| RFS-6-C57 | `carlos logs` refusal | the box was still closing the last session | approved |
+| RFS-6-C58 | `cli.md`, `### carlos run` | With -it, the command runs in a terminal session instead: what you type goes to it and its screen comes back, so a REPL or a shell works. The app must allow it first, with carlos apps set <app> --interactive on, which only an owner can do. A session ends when the command exits, after 30 minutes without a keystroke, after 6 hours, or soon after your connection drops. A deploy, a restart or a move of the instance ends it too. Ctrl-C and Ctrl-D go to the command. One session at a time per instance, beside any run. Who opened it, where, when it ended and how are in the audit log. What you type and what it prints are not kept. | approved |
+| RFS-6-C59 | `cli.md`, `### carlos console` | Opens the console process your app's manifest declares, in a terminal session, as carlos run -it would. carlos ship fills one in for Rails, Laravel and Django apps when the manifest has none. An app without one is refused, and the message says how to name a command instead. | approved |
+| RFS-6-C60 | `cli.md`, `carlos apps set` | --interactive on lets any member open a terminal session on the app's instances with carlos run -it or carlos console. A session can read and change everything the instance can, so it is off until an owner turns it on. Any member can turn it off. | approved |
+| RFS-6-C61 | `cli.md`, exit codes | A session exits with the command's own status, 124 when it ended for lack of keystrokes or at 6 hours, 125 when it was interrupted or its box's connection dropped, 130 when you cancelled it before it started, 3 when the box refused it or none opened it within 60 seconds, 2 when the console refused it, and 1 when carlos lost its connection to the console. | approved |
+| RFS-6-C62 | `cli.md`, `### carlos run`, replaces RFS-3-C28 | On a customer fleet, the instance's box reaches the console for the run itself, so carlos run works there as it does on any other box. | approved |
+| RFS-6-C63 | `carlos logs` (box, a run grant that did not verify) | run %s was not started on this box: its grant could not be verified | approved |
+| RFS-6-C64 | `carlos logs` (box, the relay dropped mid-run) | run %s lost its connection to the console, so it was stopped | approved |
## Conflicts