carlos / platform Public

sec-sidecar-create-gate

Squashed from sec-sidecar-create-gate into main.
claude agent, operated by paul@keymail.dev e3fd2e0256b92c29e6f4aca7a4ba7b6bb7fde156 parent 9ccf6b4
14 files changed, +491 −2
  • README.md +7 −0
  • cmd/console/main.go +54 −0
  • cmd/console/sidecaraccounts_serve_test.go +55 −0
  • cmd/console/sidecaraccounts_test.go +44 −0
  • docs/site/cli.md +6 −0
  • docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md +13 −0
  • internal/console/apisidecars.go +13 −1
  • internal/console/server.go +7 −0
  • internal/console/sidecarsgate.go +64 −0
  • internal/console/sidecarsgate_test.go +193 −0
  • internal/provision/plan9_b8_census_test.go +1 −1
  • internal/provision/sec_sidecar_create_gate_census_test.go +27 −0
  • internal/provision/testdata/plan14-root-reads-chain.tsv +1 −0
  • internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv +6 −0

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading github.com/superfly/ltx v0.5.1
go: downloading golang.org/x/net v0.60.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading golang.org/x/crypto v0.57.0
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading golang.org/x/sys v0.48.0
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.1.5
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/pion/dtls/v3 v3.1.4
go: downloading github.com/pion/transport/v4 v4.0.2
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/pion/dtls/v3 v3.1.4
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading golang.org/x/text v0.42.0
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading golang.org/x/sync v0.23.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading golang.org/x/text v0.42.0
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.23.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading modernc.org/memory v1.11.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== launchctx (3 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (46 s)
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading golang.org/x/tools v0.49.0
go: downloading github.com/pion/dtls/v3 v3.1.4
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading golang.org/x/text v0.42.0
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.23.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading golang.org/x/mod v0.41.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== gofmt (0 s)
gofmt -l .: clean
=== rest (198 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.434s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.691s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.735s
ok  	github.com/carlosframework/platform/internal/activator	59.821s
ok  	github.com/carlosframework/platform/internal/agentsock	0.023s
ok  	github.com/carlosframework/platform/internal/alerting	0.006s
ok  	github.com/carlosframework/platform/internal/analytics	0.028s
ok  	github.com/carlosframework/platform/internal/appconfig	2.660s
ok  	github.com/carlosframework/platform/internal/apperr	0.102s
ok  	github.com/carlosframework/platform/internal/applog	0.164s
ok  	github.com/carlosframework/platform/internal/awsinv	0.006s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.006s
ok  	github.com/carlosframework/platform/internal/boxid	0.864s
?   	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.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.006s
ok  	github.com/carlosframework/platform/internal/cgroup	0.765s
ok  	github.com/carlosframework/platform/internal/changelog	0.469s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.094s
ok  	github.com/carlosframework/platform/internal/clamd	0.063s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	8.141s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.011s
ok  	github.com/carlosframework/platform/internal/dbquery	0.008s
ok  	github.com/carlosframework/platform/internal/dbreads	2.142s
?   	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.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.007s
ok  	github.com/carlosframework/platform/internal/domains	2.530s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	48.720s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.017s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.232s
ok  	github.com/carlosframework/platform/internal/gate	1.727s
ok  	github.com/carlosframework/platform/internal/genselect	0.042s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.048s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.283s
ok  	github.com/carlosframework/platform/internal/host	148.615s
ok  	github.com/carlosframework/platform/internal/hostactors	6.308s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.016s
ok  	github.com/carlosframework/platform/internal/hostbox	3.204s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.783s
ok  	github.com/carlosframework/platform/internal/hostenv	0.016s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.016s
ok  	github.com/carlosframework/platform/internal/hostexec	5.698s
ok  	github.com/carlosframework/platform/internal/hostfence	0.040s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.570s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.016s
ok  	github.com/carlosframework/platform/internal/hostguard	1.053s
ok  	github.com/carlosframework/platform/internal/hostops	2.020s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.015s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.019s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.614s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.205s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.015s
ok  	github.com/carlosframework/platform/internal/infraview	0.017s
ok  	github.com/carlosframework/platform/internal/instancectl	5.254s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.818s
ok  	github.com/carlosframework/platform/internal/instancefs	0.020s
ok  	github.com/carlosframework/platform/internal/instancekey	0.019s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.025s
ok  	github.com/carlosframework/platform/internal/layout	1.872s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.021s
ok  	github.com/carlosframework/platform/internal/lease	10.086s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.564s
ok  	github.com/carlosframework/platform/internal/ledger	0.037s
ok  	github.com/carlosframework/platform/internal/mail	0.265s
ok  	github.com/carlosframework/platform/internal/maintenance	0.098s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.583s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.098s
ok  	github.com/carlosframework/platform/internal/metrics	0.010s
ok  	github.com/carlosframework/platform/internal/moves	29.917s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.010s
ok  	github.com/carlosframework/platform/internal/objstore	0.254s
ok  	github.com/carlosframework/platform/internal/ops	0.295s
ok  	github.com/carlosframework/platform/internal/park	0.338s
ok  	github.com/carlosframework/platform/internal/plans	0.021s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	51.228s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.008s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.779s
ok  	github.com/carlosframework/platform/internal/relay	7.200s
ok  	github.com/carlosframework/platform/internal/release	30.456s
ok  	github.com/carlosframework/platform/internal/rollout	4.136s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.006s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.059s
ok  	github.com/carlosframework/platform/internal/rootstate	0.032s
ok  	github.com/carlosframework/platform/internal/runs	0.617s
ok  	github.com/carlosframework/platform/internal/s3compat	0.008s
ok  	github.com/carlosframework/platform/internal/safefile	0.008s
ok  	github.com/carlosframework/platform/internal/schedule	2.758s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/engines	3.145s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.035s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.911s
ok  	github.com/carlosframework/platform/internal/status	0.086s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.311s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.007s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.704s
=== internal_console@0 (82 s)
ok  	github.com/carlosframework/platform/internal/console	80.490s
=== internal_console@1 (49 s)
ok  	github.com/carlosframework/platform/internal/console	47.370s
=== internal_console@2 (44 s)
ok  	github.com/carlosframework/platform/internal/console	42.708s
=== internal_console@3 (50 s)
ok  	github.com/carlosframework/platform/internal/console	48.315s
=== internal_console@4 (48 s)
ok  	github.com/carlosframework/platform/internal/console	46.388s
=== internal_console@5 (49 s)
ok  	github.com/carlosframework/platform/internal/console	47.320s
=== internal_console@6 (44 s)
ok  	github.com/carlosframework/platform/internal/console	43.167s
=== internal_console@7 (52 s)
ok  	github.com/carlosframework/platform/internal/console	50.259s
=== root@0 (113 s)
ok  	github.com/carlosframework/platform	111.696s
=== root@1 (84 s)
ok  	github.com/carlosframework/platform	83.076s
=== root@2 (94 s)
ok  	github.com/carlosframework/platform	92.576s
=== 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: 50659490 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: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
  minio: downloaded, digest verified
  mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
  mc: downloaded, 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.023s
=== RUN   TestT4StorageProbeMinio
2026/10/24 21:02:08 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E192F9C7DF8F77, 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.032s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox
=== RUN   TestDBForwardMinio
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 21:02:10 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 21:02:10 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 21:02:10 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 21:02:10 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 21:02:10 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 21:02:10 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 21:02:10 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%3A37141: could not determine target mailbox
    agent.go:135: dbreads: dbr_rdstz6a6es2ai5xngmk5dseydq: planned 3 files (1544 bytes) and restored to 3 in 10ms
    agent.go:135: dbreads: dbr_rdstz6a6es2ai5xngmk5dseydq: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 21:02:11 console: db read dbr_rdstz6a6es2ai5xngmk5dseydq: relayed 1 chunks (102 bytes) in 1.004s, ended done
    agent.go:135: dbreads: dbr_tnfh633gdfr3aeszsqvzzpslii: planned 1 files (720 bytes) and restored to 1 in 8ms
    agent.go:135: dbreads: dbr_tnfh633gdfr3aeszsqvzzpslii: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 21:02:12 console: db read dbr_tnfh633gdfr3aeszsqvzzpslii: relayed 1 chunks (102 bytes) in 1.004s, ended done
    agent.go:135: dbreads: dbr_ewod2ldt3obr5eqjwldee3ukva: planned 3 files (1544 bytes) and held through 3 in 10ms
    agent.go:135: dbreads: dbr_ewod2ldt3obr5eqjwldee3ukva: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 21:02:13 console: db read dbr_ewod2ldt3obr5eqjwldee3ukva: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN   TestDBForwardMinio/by_disconnect
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_hciqdfodgf6harpi2bscuifhcy: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 21:02:13 console: db read dbr_hciqdfodgf6harpi2bscuifhcy: relayed 0 chunks (0 bytes) in 12ms, ended client gone
    agent.go:135: dbreads: dbr_hciqdfodgf6harpi2bscuifhcy: output 5 bytes in 1 confirmed chunks in 45ms
=== RUN   TestDBForwardMinio/by_timeout
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_ze7ypwd6ls6lk2uymw5mqd3hvq: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 21:02:33 console: db read dbr_ze7ypwd6ls6lk2uymw5mqd3hvq: relayed 0 chunks (0 bytes) in 19.206s, ended no-result
    agent.go:135: dbreads: dbr_ze7ypwd6ls6lk2uymw5mqd3hvq: output 5 bytes in 1 confirmed chunks in 19.187s
--- PASS: TestDBForwardMinio (22.57s)
    --- PASS: TestDBForwardMinio/by_disconnect (0.16s)
    --- PASS: TestDBForwardMinio/by_timeout (19.31s)
PASS
ok  	github.com/carlosframework/platform/internal/console	22.590s
minio contract: PASS

landed from sec-sidecar-create-gate

diff --git a/README.md b/README.md
index 7a4b80895..c6ec2a3ac 100644
--- a/README.md
+++ b/README.md
@@ -644,6 +644,13 @@ publish an inflated instance count.
`carlos system geoip publish` instead (docs/site/cli.md, "Platform GeoIP").
Uploads are staged in `geoip-staging/` next to `CONSOLE_DB`, which the
console clears at startup.
+- `CARLOS_SIDECAR_ACCOUNTS`: comma-separated account sqids (not names) that
+ may still enable and create native sidecars. Set, every other account gets
+ 403 `sidecars-paused` from `carlos sidecars enable` and `carlos sidecars
+ create`. List, get, wake and delete still work, and existing sidecars keep
+ running. Native sidecars run on the flat tier, outside the nested tenants'
+ isolation, until nested sidecars land (discussion #370). **Unset or empty =
+ no gate.** An entry that is not a sqid stops the console at boot.
**Canonical-host caveat**, stated honestly: an app that pins its own host
(e.g. keymail's `--host keymail.dev`) will redirect or 404 on its platform
diff --git a/cmd/console/main.go b/cmd/console/main.go
index c95cfa05f..067f138c9 100644
--- a/cmd/console/main.go
+++ b/cmd/console/main.go
@@ -51,6 +51,14 @@
// CARLOS_TEMPORARY_APPS=1 turns on temporary apps (carlos deploy
// --temporary); off by default, see the temporary static sites rollout.
//
+// CARLOS_SIDECAR_ACCOUNTS (comma-separated account sqids) pauses native
+// sidecars for every account it does not name: enabling sidecars and
+// creating one answer 403 sidecars-paused, while list, get, wake and delete
+// keep working and existing sidecars keep running. Native sidecars run on
+// the flat tier, outside the nested tenants' isolation, until discussion
+// #370 lands. Unset or empty is no gate. An entry that is not a sqid, or a
+// value that names no sqid at all, stops the console at startup.
+//
// Tenant email (task 17) reads CARLOS_MAIL_OPERATORS (comma-separated
// addresses; narrows the fleet-operator bit further for the pause/cap
// door only — see mailOperators' doc comment), CARLOS_MAIL_REGION,
@@ -107,6 +115,7 @@ import (
"github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/console"
"github.com/carlosframework/platform/internal/dnsrec"
+ "github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/geo"
"github.com/carlosframework/platform/internal/geoip"
"github.com/carlosframework/platform/internal/lease"
@@ -291,6 +300,12 @@ func serve() {
} else {
log.Print("console: CARLOS_RELEASE_KEY unset — promotions disabled, running read-only")
}
+ // Before anything is opened: a typo in the allowlist must stop the
+ // console rather than quietly pause, or unpause, the wrong accounts.
+ sidecarAccts, err := sidecarAccounts(os.Getenv)
+ if err != nil {
+ log.Fatalf("console: %v", err)
+ }
hostClaim := hostClaimEnabled(os.Getenv)
st := openServeDB(hostClaim)
storeProvisioner, storeDeployment := storeBackend()
@@ -457,6 +472,9 @@ func serve() {
// owners (discussion #176). Unset is the owner-only console.
SystemOperators: systemOperators(),
PlatformDomain: platformDomain(),
+ // The sidecar pause (internal/console/sidecarsgate.go). Unset is no
+ // gate; set, only these accounts may enable or create a sidecar.
+ SidecarAccounts: sidecarAccts,
}
// The Phase 1 moves switch (CARLOS_MOVES_PHASE1, off by default, index
// D13): applied after the literal so a test can prove it reaches the
@@ -875,6 +893,42 @@ func systemOperators() []string {
return out
}
+// sidecarAccounts parses CARLOS_SIDECAR_ACCOUNTS (comma-separated account
+// sqids): the accounts that may still enable and create native sidecars
+// while every other account is paused (internal/console/sidecarsgate.go,
+// discussion #370). Each entry is trimmed and lower-cased, and must be a
+// sqid (domains.ValidSqid), never an account name: "ops" is not one, and a
+// name that matched nothing would pause the very account it meant to keep.
+// An invalid entry is an error for serve to stop on.
+//
+// Unset or the empty string is nil, and nil is no gate: every account may
+// enable and create sidecars, exactly as before the list existed. This is
+// the opposite direction to the operator lists above on purpose, so a
+// deployment that never sets it is unaffected. Any other value that names
+// no sqid at all ("," or " , ") is an error too: it reads as a list, and
+// parsing it to nil would quietly turn the gate off.
+func sidecarAccounts(getenv func(string) string) ([]string, error) {
+ raw := getenv("CARLOS_SIDECAR_ACCOUNTS")
+ if raw == "" {
+ return nil, nil
+ }
+ var out []string
+ for _, a := range strings.Split(raw, ",") {
+ a = strings.ToLower(strings.TrimSpace(a))
+ if a == "" {
+ continue
+ }
+ if !domains.ValidSqid(a) {
+ return nil, fmt.Errorf("CARLOS_SIDECAR_ACCOUNTS: %q is not an account sqid (the account's id, e.g. bab, never its name)", a)
+ }
+ out = append(out, a)
+ }
+ if len(out) == 0 {
+ return nil, fmt.Errorf("CARLOS_SIDECAR_ACCOUNTS: %q names no account sqid; unset it for no gate, or list at least one sqid", raw)
+ }
+ return out, nil
+}
+
// platformDomain reads CARLOS_PLATFORM_DOMAIN — the domain the relay's own
// hostnames hang off (relay.<region>.<domain>), which an enabled app is
// handed as its ICE server URLs.
diff --git a/cmd/console/sidecaraccounts_serve_test.go b/cmd/console/sidecaraccounts_serve_test.go
new file mode 100644
index 000000000..066138fba
--- /dev/null
+++ b/cmd/console/sidecaraccounts_serve_test.go
@@ -0,0 +1,55 @@
+package main_test
+
+import (
+ "context"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+// TestServeStopsOnABadSidecarAccount: serve reads CARLOS_SIDECAR_ACCOUNTS
+// and stops before it opens anything on an entry that is not a sqid, or on a
+// value set to a list that names no sqid at all ("," would otherwise parse
+// to nil and quietly turn the gate off). The timeout is the failure mode it
+// guards: a serve that ignored the variable would carry on and listen.
+func TestServeStopsOnABadSidecarAccount(t *testing.T) {
+ bin := buildConsole(t)
+ for _, c := range []struct{ val, want string }{
+ {"bab,ops", `"ops"`},
+ {",", "names no account sqid"},
+ {" , ", "names no account sqid"},
+ } {
+ tmp := t.TempDir()
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ cmd := exec.CommandContext(ctx, bin, "serve")
+ cmd.Dir = tmp
+ cmd.Env = append(os.Environ(),
+ "CONSOLE_ORIGIN=http://127.0.0.1:1",
+ "CONSOLE_ADDR=127.0.0.1:0",
+ "CONSOLE_DB="+filepath.Join(tmp, "console.db"),
+ // 32 zero bytes: a well-formed Ed25519 public key, so serve gets
+ // past the key check to the variable under test.
+ "CARLOS_RELEASE_PUBKEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "CARLOS_RELEASE_KEY=",
+ "CARLOS_SIDECAR_ACCOUNTS="+c.val,
+ )
+ out, err := cmd.CombinedOutput()
+ timedOut := ctx.Err() != nil
+ cancel()
+ if timedOut {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: serve did not stop: %s", c.val, out)
+ }
+ if err == nil {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: serve exited 0: %s", c.val, out)
+ }
+ if !strings.Contains(string(out), "CARLOS_SIDECAR_ACCOUNTS") || !strings.Contains(string(out), c.want) {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: the refusal does not name the variable and %s: %s", c.val, c.want, out)
+ }
+ if _, err := os.Stat(filepath.Join(tmp, "console.db")); err == nil {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: serve opened its database before refusing", c.val)
+ }
+ }
+}
diff --git a/cmd/console/sidecaraccounts_test.go b/cmd/console/sidecaraccounts_test.go
new file mode 100644
index 000000000..cba72c2bd
--- /dev/null
+++ b/cmd/console/sidecaraccounts_test.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "slices"
+ "testing"
+)
+
+// CARLOS_SIDECAR_ACCOUNTS reaches Config.SidecarAccounts through
+// sidecarAccounts: trimmed, lower-cased, each a sqid. Unset and the empty
+// string are nil, which is no gate; any entry that is not a sqid, and any
+// other value that names no sqid, is an error serve stops on (TestServeStopsOnABadSidecarAccount runs that through the binary).
+func TestSidecarAccountsParsesTheList(t *testing.T) {
+ for _, tc := range []struct {
+ val string
+ want []string
+ wantErr bool
+ }{
+ {"", nil, false},
+ {" ", nil, true}, // set, but names no sqid: refused, never "no gate"
+ {",", nil, true}, // the same, and the review's case
+ {" , ", nil, true}, // ditto
+ {"bab", []string{"bab"}, false},
+ {" BAB , cdef ,", []string{"bab", "cdef"}, false},
+ {"bab,ops", nil, true}, // a name, not a sqid: o is not in the alphabet
+ {"bab,ab", nil, true}, // too short
+ {"bab,b-b", nil, true}, // not a letter
+ {"bab;cdef", nil, true}, // wrong separator reads as one bad entry
+ {"x1234567890abcdefg", nil, true},
+ } {
+ got, err := sidecarAccounts(func(k string) string {
+ if k != "CARLOS_SIDECAR_ACCOUNTS" {
+ t.Fatalf("read %s", k)
+ }
+ return tc.val
+ })
+ if (err != nil) != tc.wantErr {
+ t.Errorf("CARLOS_SIDECAR_ACCOUNTS=%q: err %v, want error %v", tc.val, err, tc.wantErr)
+ continue
+ }
+ if !slices.Equal(got, tc.want) {
+ t.Errorf("CARLOS_SIDECAR_ACCOUNTS=%q: %v, want %v", tc.val, got, tc.want)
+ }
+ }
+}
diff --git a/docs/site/cli.md b/docs/site/cli.md
index 0a8e384f2..3d0dd287a 100644
--- a/docs/site/cli.md
+++ b/docs/site/cli.md
@@ -1016,6 +1016,12 @@ it back from, so `carlos sidecars ls` shows the build its box last started
for each sidecar, beside the version its channel points at. One that has not
started on any box reads as unknown rather than as current.
+A console operator can pause sidecars for every account not named in
+`CARLOS_SIDECAR_ACCOUNTS`, a comma-separated list of account sqids. A paused
+account gets a 403 with the code `sidecars-paused` from `enable` and
+`create`. Its existing sidecars keep running, and `ls`, `wake` and `rm` still
+work. Unset, nothing is paused.
+
### carlos steering
Decides whether one instance's DNS answer varies by where the request comes
diff --git a/docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md b/docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md
new file mode 100644
index 000000000..7fe04527e
--- /dev/null
+++ b/docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md
@@ -0,0 +1,13 @@
+# Census approval: sec-sidecar-create-gate
+
+Ruling `census-ruling-sec-sidecar-create-gate`. Every change below keeps the disposition it already has, so it needs an approval, not a decision.
+
+| unit | change | read sites | what changed |
+|---|---|---|---|
+| `cmd/console/main.go` `serve` | body | unchanged (3) | serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites. |
+
+Envelopes re-pinned: `cmd/console`, `internal/console`.
+
+Seal: `945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a` → `0ca3baafa26f59c0b7d8c6d6a39237d074afb71f885b7fbd50debebab5bf89f2`.
+
+Overlay: `internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv`, appended to `internal/provision/testdata/plan14-root-reads-chain.tsv`.
diff --git a/internal/console/apisidecars.go b/internal/console/apisidecars.go
index 698b7daa4..17c8aa12d 100644
--- a/internal/console/apisidecars.go
+++ b/internal/console/apisidecars.go
@@ -198,7 +198,8 @@ func (s *Server) handleAPISidecarGet(w http.ResponseWriter, r *http.Request) {
// handleAPISidecarCreate is the point of the feature: one create-only write
// to the bucket, in the family's refusal order — gate, budget, readiness
-// (instanceGate), manifest, name, region, channel, shape, write.
+// (instanceGate), manifest, name, region, channel, shape, pause
+// (sidecarsgate.go), write.
//
// Every field policy governs is stamped from the MANIFEST, never from the
// request. The body has no pool field to lie in, no host field (the host is
@@ -311,6 +312,12 @@ func (s *Server) handleAPISidecarCreate(w http.ResponseWriter, r *http.Request)
jsonError(w, http.StatusBadRequest, err.Error())
return
}
+ // The sidecar pause (sidecarsgate.go): after every check above, so a
+ // stranger still meets the 404 and a bad request its 400, and before
+ // the cap's listing, the leftover rail and either write arm.
+ if s.refuseSidecarsPaused(w, acct, app, "create") {
+ return
+ }
// Plans 3a: the plan caps workers per app, under the account lock.
defer s.planLock(acct)()
if err := s.checkSidecars(r.Context(), acct, app); err != nil {
@@ -586,6 +593,11 @@ func (s *Server) handleAPISidecarEnable(w http.ResponseWriter, r *http.Request)
return
}
}
+ // The sidecar pause (sidecarsgate.go): after authentication, membership
+ // and validation, before the manifest read and its write.
+ if s.refuseSidecarsPaused(w, acct, app, "enable") {
+ return
+ }
ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout)
defer cancel()
// Read WITH the etag and write guarded by it below: a read-modify-write
diff --git a/internal/console/server.go b/internal/console/server.go
index e7c940087..0c186db2f 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -855,6 +855,13 @@ type Config struct {
// wherever the two differ by anything but that label; the relay's DNS
// names are operator-managed and this console cannot discover them.
PlatformDomain string
+ // SidecarAccounts is the operator allowlist of account sqids that may
+ // still enable and create native sidecars (CARLOS_SIDECAR_ACCOUNTS,
+ // normalized by cmd/console: trimmed, lower-cased, each a valid sqid).
+ // Empty is no gate at all, every deployment's shape before the list
+ // existed. Set, every other account meets sidecarsPaused on the two
+ // doors that bring a sidecar into being (sidecarsgate.go).
+ SidecarAccounts []string
}
// Server is the console's http.Handler.
diff --git a/internal/console/sidecarsgate.go b/internal/console/sidecarsgate.go
new file mode 100644
index 000000000..002d4c8e5
--- /dev/null
+++ b/internal/console/sidecarsgate.go
@@ -0,0 +1,64 @@
+// The sidecar pause: an operator allowlist in front of the two doors that
+// bring a native sidecar into being.
+//
+// Why it exists: a native sidecar runs on the flat tier
+// (activator.ExecBackend.StartWorkerStaged) as the box's carlos uid with no
+// mount namespace, so on its box it can read and write every nested
+// tenant's /data/instances/<iid>. The fix is nested sidecars (amadan
+// discussion #370). Until that lands, an operator lists the accounts that
+// may still enable or create one (CARLOS_SIDECAR_ACCOUNTS) and every other
+// account is refused.
+//
+// The doors, and why there are only two. A sidecar runs only from a record
+// at apps/<sqid>/<app>/sidecars/<name>.json, which the box's sidecar
+// reconcile pass (or, under host claim, the root claim path) turns into a
+// Kind "sidecar" route, and it takes its argv and ceilings from
+// apps/<sqid>/<app>/sidecars.json. The console writes the record in exactly
+// one place (handleAPISidecarCreate, both the CreateSidecarRecord and the
+// host-claim beginClaimCreate arms) and the manifest in exactly one place
+// (handleAPISidecarEnable). Both call refuseSidecarsPaused. Ship and promote
+// publish releases and channel pointers, never a sidecar manifest; restore
+// clears an app's trash marker and brings back only sidecars that already
+// existed; carlos run needs a nested instance. Store mode (`carlos sidecars
+// enable` against the bucket) is the operator's own bucket credential and
+// is not a console door.
+//
+// What it leaves alone, on purpose: list, get, wake and delete. Existing
+// sidecars keep running, and an account taken off the list must still be
+// able to retire its own.
+package console
+
+import (
+ "log"
+ "net/http"
+ "slices"
+)
+
+// sidecarsPausedMsg is the one sentence a refused account reads.
+// copy: approved by Paul 2026-10-24 (copy review).
+const sidecarsPausedMsg = "sidecars are paused for this account. existing sidecars keep running, and you can still delete them"
+
+// sidecarsPausedCode is the refusal's machine-readable code, beside the
+// sentence the way plan caps carry "cap".
+const sidecarsPausedCode = "sidecars-paused"
+
+// sidecarsPaused reports whether acct is refused new sidecars: the
+// allowlist is set and does not name the account's sqid.
+func (s *Server) sidecarsPaused(acct *Account) bool {
+ return len(s.cfg.SidecarAccounts) > 0 && !slices.Contains(s.cfg.SidecarAccounts, acct.Sqid)
+}
+
+// refuseSidecarsPaused answers 403 sidecars-paused and reports true when
+// acct may not enable or create a sidecar. Call it AFTER the door's
+// authentication, membership and validation checks, so a non-member still
+// gets the byte-identical 404 and the gate never confirms that an account
+// exists, and BEFORE any bucket write. Request-path refusals are not
+// audited in this console (the plan cap's is not either), so it logs.
+func (s *Server) refuseSidecarsPaused(w http.ResponseWriter, acct *Account, app, verb string) bool {
+ if !s.sidecarsPaused(acct) {
+ return false
+ }
+ log.Printf("console: api sidecars %s %s/%s: refused, account not in CARLOS_SIDECAR_ACCOUNTS", verb, acct.Sqid, app)
+ writeJSON(w, http.StatusForbidden, map[string]any{"error": sidecarsPausedMsg, "code": sidecarsPausedCode})
+ return true
+}
diff --git a/internal/console/sidecarsgate_test.go b/internal/console/sidecarsgate_test.go
new file mode 100644
index 000000000..7f0f90dd0
--- /dev/null
+++ b/internal/console/sidecarsgate_test.go
@@ -0,0 +1,193 @@
+package console
+
+// The sidecar pause (sidecarsgate.go), through the real Server, its router
+// and the cliRoute auth stack the rest of apisidecars_test.go uses. The
+// properties, in the order they would hurt if lost: with the allowlist set,
+// an account it does not name cannot enable or create a sidecar by any
+// credential that reaches those doors, and nothing is written to the bucket
+// when it is refused; a stranger still gets the byte-identical 404, so the
+// gate never confirms an account exists; existing sidecars stay listable,
+// wakeable and deletable; a named account is unaffected; and with the
+// allowlist unset nothing changes at all.
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "slices"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/svcrole"
+)
+
+// sidecarAppKeys is every bucket key under the fixture app's prefix, sorted: the
+// refused calls must leave it exactly as it was.
+func sidecarAppKeys(t *testing.T, fx *consoleFixture) []string {
+ t.Helper()
+ keys, err := fx.rel.List(context.Background(), release.AppPrefix("bab", "titogo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ slices.Sort(keys)
+ return keys
+}
+
+func assertSidecarsPaused(t *testing.T, what string, status int, body string) {
+ t.Helper()
+ if status != http.StatusForbidden {
+ t.Fatalf("%s = %d %s, want 403", what, status, body)
+ }
+ var got struct{ Error, Code string }
+ if err := json.Unmarshal([]byte(body), &got); err != nil {
+ t.Fatalf("%s body %q: %v", what, body, err)
+ }
+ if got.Code != "sidecars-paused" || got.Error != sidecarsPausedMsg {
+ t.Fatalf("%s body = %+v, want code sidecars-paused and the paused sentence", what, got)
+ }
+}
+
+// TestSidecarsPausedRefusesEnableAndCreate: the fixture's account (bab) is
+// not on the list, so enable and create are refused for the member, the
+// legacy app token and an instance-role service principal, and the bucket
+// is untouched.
+func TestSidecarsPausedRefusesEnableAndCreate(t *testing.T) {
+ fx, member := instanceFixture(t, "code-side-paused")
+ fx.srv.cfg.SidecarAccounts = []string{"cdef"}
+ u := sidecarsURL(fx, "bab", "titogo")
+
+ // Enable with no manifest yet: refused, and no manifest appears.
+ before := sidecarAppKeys(t, fx)
+ status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta"}`)
+ assertSidecarsPaused(t, "paused enable, no manifest", status, out)
+ if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) {
+ t.Fatalf("a refused enable changed the bucket: %v -> %v", before, after)
+ }
+
+ // An operator-written manifest (store mode, not a console door) is
+ // there; enable still may not rewrite it.
+ enableSidecars(t, fx, "titogo", titoSidecarManifest())
+ manifest, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ status, _, out = rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta","args":["evil"]}`)
+ assertSidecarsPaused(t, "paused enable over a manifest", status, out)
+ if now, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo")); err != nil || string(now) != string(manifest) {
+ t.Fatalf("a refused enable rewrote the manifest: %s -> %s (%v)", manifest, now, err)
+ }
+
+ // Create, by every credential the create door admits.
+ appTok, err := fx.st.NewAppToken(OpsAccountID, "titogo", "alice@keymail.dev", "signup", time.Now())
+ if err != nil {
+ t.Fatal(err)
+ }
+ svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
+ before = sidecarAppKeys(t, fx)
+ for _, c := range []struct{ what, token string }{
+ {"member create", member},
+ {"app token create", appTok},
+ {"instance service principal create", svc},
+ } {
+ status, _, out := rawReq(t, http.MethodPost, u, c.token, `{"name":"bot","region":"ie"}`)
+ assertSidecarsPaused(t, c.what, status, out)
+ }
+ if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) {
+ t.Fatalf("a refused create changed the bucket: %v -> %v", before, after)
+ }
+ if keys, err := fx.rel.List(context.Background(), provision.SidecarsPrefix("bab", "titogo")); err != nil || len(keys) != 0 {
+ t.Fatalf("refused creates left records %v (%v)", keys, err)
+ }
+
+ // The gate comes after membership: a stranger and an unknown account
+ // still get the byte-identical 404, never the 403 that would confirm
+ // the account exists and is paused.
+ want := canonicalNotFound(t)
+ eve := cliToken(t, fx, "eve@keymail.dev", "code-side-paused-eve")
+ for _, c := range []struct{ what, method, url, token, body string }{
+ {"non-member enable", http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), eve, `{}`},
+ {"non-member create", http.MethodPost, u, eve, `{"name":"bot","region":"ie"}`},
+ {"unknown account create", http.MethodPost, sidecarsURL(fx, "zzzz", "titogo"), member, `{"name":"bot","region":"ie"}`},
+ {"unclaimed app create", http.MethodPost, sidecarsURL(fx, "bab", "ghost-app"), member, `{"name":"bot","region":"ie"}`},
+ } {
+ status, h, out := rawReq(t, c.method, c.url, c.token, c.body)
+ assertNotFound(t, c.what, want, status, h, out)
+ }
+ // And validation still answers first for a member.
+ if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"-nope-","region":"ie"}`); status != http.StatusBadRequest {
+ t.Fatalf("paused create with a bad name = %d %s, want the 400 first", status, out)
+ }
+}
+
+// TestSidecarsPausedLeavesExistingSidecarsAlone: a sidecar created before the
+// pause is still listed, read, woken and deleted by the paused account, by
+// the member and by the instance service principal.
+func TestSidecarsPausedLeavesExistingSidecarsAlone(t *testing.T) {
+ fx, member := instanceFixture(t, "code-side-paused-existing")
+ enableSidecars(t, fx, "titogo", titoSidecarManifest())
+ u := sidecarsURL(fx, "bab", "titogo")
+ for _, name := range []string{"bot", "bot2"} {
+ if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"`+name+`","region":"ie"}`); status != http.StatusCreated {
+ t.Fatalf("create %s before the pause = %d %s", name, status, out)
+ }
+ }
+ fx.srv.cfg.SidecarAccounts = []string{"cdef"}
+ svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
+
+ for _, c := range []struct{ what, method, url string }{
+ {"list", http.MethodGet, u},
+ {"get", http.MethodGet, u + "/bot"},
+ {"wake", http.MethodPost, u + "/bot/wake"},
+ } {
+ if status, _, out := rawReq(t, c.method, c.url, member, ""); status != http.StatusOK {
+ t.Fatalf("paused %s = %d %s, want 200", c.what, status, out)
+ }
+ }
+ for _, c := range []struct{ name, token string }{{"bot", member}, {"bot2", svc}} {
+ if status, _, out := rawReq(t, http.MethodDelete, u+"/"+c.name, c.token, ""); status != http.StatusOK {
+ t.Fatalf("paused delete of %s = %d %s, want 200", c.name, status, out)
+ }
+ if _, _, err := fx.rel.Get(context.Background(), provision.SidecarRecordKey("bab", "titogo", c.name)); err == nil {
+ t.Fatalf("the record for %s survived its delete", c.name)
+ }
+ }
+}
+
+// TestSidecarsPauseAllowlistAndUnset: an account the list names, and a
+// console with no list at all, enable and create exactly as before.
+func TestSidecarsPauseAllowlistAndUnset(t *testing.T) {
+ for _, c := range []struct {
+ what string
+ accts []string
+ }{
+ {"allowlisted", []string{"cdef", "bab"}},
+ {"unset", nil},
+ } {
+ t.Run(c.what, func(t *testing.T) {
+ fx, member := instanceFixture(t, "code-side-allow-"+c.what)
+ fx.srv.cfg.SidecarAccounts = c.accts
+ status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"stable"}`)
+ if status != http.StatusOK {
+ t.Fatalf("enable = %d %s, want 200", status, out)
+ }
+ // The pool and regions are operator policy the enable door never
+ // sets; put them as the operator would so create can place.
+ enableSidecars(t, fx, "titogo", titoSidecarManifest())
+ u := sidecarsURL(fx, "bab", "titogo")
+ if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"bot","region":"ie"}`); status != http.StatusCreated {
+ t.Fatalf("member create = %d %s, want 201", status, out)
+ }
+ svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
+ if status, _, out := rawReq(t, http.MethodPost, u, svc, `{"name":"bot2","region":"ie"}`); status != http.StatusCreated {
+ t.Fatalf("service principal create = %d %s, want 201", status, out)
+ }
+ for _, name := range []string{"bot", "bot2"} {
+ if _, err := provision.GetSidecarRecord(context.Background(), fx.rel, "bab", "titogo", name); err != nil {
+ t.Fatalf("record %s: %v", name, err)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 525edfb2c..ec0176aee 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -366,7 +366,7 @@ func TestPlan9B8RecordWriteCensus(t *testing.T) {
if len(got) < 100 {
t.Fatalf("census found only %d units under %s; the walk is broken", len(got), root)
}
- inventory := dbRegionForward3WriteCensusInventory()
+ inventory := secSidecarCreateGateWriteCensusInventory()
problems := append(censusDiff(inventory, got), censusPolicy(inventory)...)
if len(problems) > 0 {
t.Fatalf("record-write census rejected %d site(s); update the explicit inventory only after reviewing the source:\n %s", len(problems), strings.Join(problems, "\n "))
diff --git a/internal/provision/sec_sidecar_create_gate_census_test.go b/internal/provision/sec_sidecar_create_gate_census_test.go
new file mode 100644
index 000000000..c4ecffc32
--- /dev/null
+++ b/internal/provision/sec_sidecar_create_gate_census_test.go
@@ -0,0 +1,27 @@
+package provision
+
+// Record-write census entries for the sidecar pause (sec-sidecar-create-gate),
+// layered on dbRegionForward3WriteCensusInventory (main's last at 65ca4aca).
+// handleAPISidecarCreate gains one early return, refuseSidecarsPaused
+// (internal/console/sidecarsgate.go), after the shape check and before the
+// plan cap, the leftover rail and both write arms. Its write is unchanged:
+// the same CreateSidecarRecord create-only write, or the host-claim create.
+// No write site is added; the refusal writes nothing.
+func secSidecarCreateGateWriteCensusInventory() []censusEntry {
+ entries := append([]censusEntry(nil), dbRegionForward3WriteCensusInventory()...)
+ replacements := []censusEntry{
+ {"internal/console/apisidecars.go", "Server.handleAPISidecarCreate", "record-writer", "key:CreateSidecarRecord", "72f4d44028fbc46ed2ed1adca6154f385b62e143f47e597a49b6e28570b2ddab"},
+ }
+ for _, r := range replacements {
+ found := false
+ for i, e := range entries {
+ if e.File == r.File && e.Unit == r.Unit {
+ entries[i], found = r, true
+ }
+ }
+ if !found {
+ entries = append(entries, r)
+ }
+ }
+ return entries
+}
diff --git a/internal/provision/testdata/plan14-root-reads-chain.tsv b/internal/provision/testdata/plan14-root-reads-chain.tsv
index a67ca45fd..b55f07f47 100644
--- a/internal/provision/testdata/plan14-root-reads-chain.tsv
+++ b/internal/provision/testdata/plan14-root-reads-chain.tsv
@@ -3,3 +3,4 @@
# make census-propose appends here; nothing else edits this file.
plan14-root-reads-sec-host-imdsv2-module.tsv 945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a
plan14-root-reads-sec-deps-dos-bumps.tsv 945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a
+plan14-root-reads-sec-sidecar-create-gate.tsv 0ca3baafa26f59c0b7d8c6d6a39237d074afb71f885b7fbd50debebab5bf89f2
diff --git a/internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv b/internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv
new file mode 100644
index 000000000..ff9e1e70c
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv
@@ -0,0 +1,6 @@
+# Census overlay sec-sidecar-create-gate, written by census-propose (#369). census-ruling-sec-sidecar-create-gate, approved by the census owner before the merge; the approval card is docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md.
+# 1 body, 0 sites, 2 envelope: each changed unit re-frozen at its existing disposition with its claim appended.
+@legacy dd17558eb17031da9bd9c517c68fd97ac5456969222be618e4b24c74128813fd cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-sidecar-create-gate;basis=serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites. data-literal@4319,data-literal@5420,data-literal@5657 1f1aa5c40845b20922a7740ec15254a47ff5856e1ef14252a189e1af56ddd406
+cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-sidecar-create-gate;basis=serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites. data-literal@4319,data-literal@5420,data-literal@5657 1f1aa5c40845b20922a7740ec15254a47ff5856e1ef14252a189e1af56ddd406
+@coverage cmd/console 4865ecf0f8de501d62bcfbd10cc9a4244c3e5c5ee53c6c2ee476b5dba7fd36b2
+@coverage internal/console 3ab92802ced1e88b3b841f122fa64b201d576e9dfd5db7662da0028caf7c78ec