carlos / platform Public

plans: brief for the instance policy plans (sizes and egress); spec marked approved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b06ce419781775668f1123f8e7465c35c71243e9 parent 14b48af
2 files changed, +95 −1
  • docs/superpowers/plans/2026-10-21-instance-policy-PLAN-BRIEF.md +94 −0
  • docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md +1 −1
diff --git a/docs/superpowers/plans/2026-10-21-instance-policy-PLAN-BRIEF.md b/docs/superpowers/plans/2026-10-21-instance-policy-PLAN-BRIEF.md
new file mode 100644
index 000000000..53fd8abac
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-21-instance-policy-PLAN-BRIEF.md
@@ -0,0 +1,94 @@
+# Plan brief: instance sizes and egress allows (#328, #329)
+
+For the three plan writers. The spec is
+`docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md`
+(rev 7, Astra ACCEPT round 7, approved by Paul 2026-10-21). Its §0 rulings
+and §8 calls are settled; do not reopen them. Its §9 dispositions record
+why each mechanism is shaped the way it is: read them before simplifying
+anything.
+
+## The three plans
+
+| # | File | Covers |
+|---|---|---|
+| 1 | `2026-10-21-instance-policy-1-foundation.md` | The policy object (spec §3), its validation, the operator door and audit (§6), the CLI (`apps set --egress-allow/--size`, `instances set --size`, `apps show`), the purge sweep and create-time clear (§3), the store door's read grant (§4.3 Transport), and the agent converging the policy into registry columns (pre-claim delivery). |
+| 2 | `2026-10-21-instance-policy-2-egress.md` | The kernel proof first (§4.3), address validation in root (§4.1), root writing and re-asserting `IPAddressAllow=` on every start path and on a 30 s loop (§4.3), the claim-mode store read with the `app_id` binding (§3, §4.3), the not-wholly-nested rail, `addrs` publication and the grant-time check and report (§4.2), read-back and display (§4.4). |
+| 3 | `2026-10-21-instance-policy-3-sizes.md` | The size table (§5.1), every validator (§5.3), the effective size as the box's tier source (§5.2, §5.3), live growth and the captured tier (§5.4), the cold-start-only memory shrink and swap rule (§5.4), `mem_total_mib` and the capacity check (§5.5), read-back and display (§5.6). |
+
+Build order: 1, then 2 (Tito is blocked on egress), then 3. Plans 2 and 3
+consume plan 1's interfaces below and nothing else from each other, except
+that both touch `internal/instancectl/server.go`'s start paths: plan 3
+must say which of plan 2's additions it sits beside.
+
+## Cross-plan interfaces (pinned here so the three plans agree)
+
+Plan 1 produces these; plans 2 and 3 use them by these exact names.
+
+- **Package `internal/apppolicy`** (new):
+ - `const Prefix = "control/app-policy/"`; `func Key(sqid, app string) string`
+ returns `Prefix + sqid + "/" + app + ".json"`.
+ - `type Policy struct { V int \`json:"v"\`; AppID string \`json:"app_id,omitempty"\`; EgressAllow []string \`json:"egress_allow,omitempty"\`; DefaultSize string \`json:"default_size,omitempty"\`; Sizes map[string]SizeOverride \`json:"sizes,omitempty"\` }`
+ - `type SizeOverride struct { Size string \`json:"size"\`; CreatedAt time.Time \`json:"created_at"\` }`
+ - `func Get(ctx context.Context, s release.Store, sqid, app string) (*Policy, string, error)`: nil policy and `os.ErrNotExist` when absent.
+ - `func PutIfMatch(ctx context.Context, s release.Store, sqid, app string, p Policy, etag string) error`
+ - `func Delete(ctx context.Context, s release.Store, sqid, app string) error`
+ - `func (p Policy) Validate() error`: shape only (version, size names known to `cgroup.SizeByName`, at most 8 allows, each parsing under `egressaddr.Parse`).
+- **Package `internal/egressaddr`** (new, plan 1 creates it because the door validates; plan 2 extends nothing in it, only calls it):
+ - `func Parse(s string) (netip.Addr, error)`: single address, no CIDR, zone, port or hostname; unmaps v4-in-v6.
+ - `func Allowed(a netip.Addr) error`: the spec §4.1 refusals and the "must be inside the fence" rule, against `FenceDeny`.
+ - `var FenceDeny []netip.Prefix`: the instance templates' `IPAddressDeny=` ranges, one table; `internal/host/units_hardening_test.go` is changed to assert the templates and this table agree.
+ - `func Normalize(in []string) ([]string, error)`: parse, refuse, dedupe, sort; the stored form.
+- **Package `internal/cgroup`** (plan 1 adds only the names, so the door can validate; plan 3 owns the behaviour):
+ - `type Size struct { Name string; Limit Limit; Shares Shares }`
+ - `var Sizes []Size` in order small, standard, large, xlarge, with the spec §5.1 numbers. `standard` is exactly `TierDefault` and its current shares; `large` exactly `TierChildren` and its current shares.
+ - `func SizeByName(name string) (Size, bool)`, `func SizeOf(l Limit) (Size, bool)`.
+- **Registry columns** (plan 1 migrates and converges; plans 2 and 3 read):
+ - `routes.egress_allow TEXT NOT NULL DEFAULT ''`: the normalized list, space-separated.
+ - `routes.size TEXT NOT NULL DEFAULT ''`: the effective size name, `''` meaning the build's tier.
+ - `func (r *Registry) SetPolicy(host, egressAllow, size string) error`, and both fields on `registry.Route` as `EgressAllow string` and `Size string`.
+- **Door:** `PUT /api/cli/apps/{account}/{app}/policy` with body `{"egress_allow": [...] | null, "default_size": "..." | null, "size": {"host": "...", "value": "..."} | null}`, `If-Match` the policy ETag; reply carries the new policy, its ETag, and (plan 2 fills it) `not_checked` boxes. Gate: `apiSystemRoller`, empty `svcrole.Set{}`. Audit before write; refuse if the audit write fails.
+
+Any name a plan needs that is not here is that plan's to choose and pin in
+its own Interfaces blocks.
+
+## Rules every plan carries in its Global Constraints
+
+- Go; module `github.com/carlosframework/platform`. Leave `GOCACHE` at its
+ default. Scratch under `$TMPDIR`. Socket tests: `TMPDIR=/tmp/g`.
+- **Local tests are targeted:** only touched packages, with `-run`. Never
+ `make test`, never the whole console or host suite locally; CI runs the
+ full gate (`.amadan/test-gate.sh`). Exceptions: the privileged kernel
+ test (plan 2 Task 1) runs on the root runner.
+- **Parallel tests** (main as of 4f55bfb): new tests in `internal/host`,
+ `internal/activator`, `internal/console`, `moveprobe` call `t.Parallel()`
+ unless they set env (`t.Setenv`), cwd, umask, log output or package vars,
+ reach `edge.SecureUnixListen`, or listen on TCP (activator). No top-level
+ test may depend on another having run first. A new test slower than a
+ few seconds in console or root refreshes `.amadan/test-weights.tsv` with
+ `.amadan/test-weights.sh`.
+- **Cross-compile:** CI builds darwin and windows. Linux-only syscalls in
+ `internal/host` or `internal/instancectl` need `//go:build linux` and a
+ `!linux` stub.
+- **Root code** (`internal/instancectl`, root's paths in `internal/host`):
+ the plan ends with a census overlay task (see existing
+ `docs/superpowers/notes/census-proposal-*.md` and the `#280` thread); the
+ overlay loads after `plan14-root-reads-ci-speed-activator`.
+- **Production-wired witnesses:** claim and root behaviour is tested
+ through the real control service, units and wiring, not fixtures or
+ sleeps.
+- **Privileged tests** run on the root runner:
+ `//go:build linux && rootrunner`, run with
+ `~/.cache/tmp/claude-1001/own/after236/root-runner.py --worktree DIR --pkg ./internal/x --run REGEX`
+ with the sandbox off; read `root-runner-contract.md` beside the script.
+- **User-facing strings** (CLI output, door error messages a person
+ reads, alert text) are collected in the plan's last task for copy review
+ (the `copy-review` skill) before merge. Plain English, no em dashes, a
+ full stop and a new sentence instead. Internal error fragments and JSON
+ keys are not copy.
+- **No tenant names** in code, tests or comments: neutral examples
+ (`shop-a.example.com`, `10.0.5.20`).
+- **Commits** per task, ending with
+ `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
+- Nothing is on by default: with no policy object, every unit, slice and
+ probe line is byte-identical to main. Each plan has a task that proves
+ it for its part.
diff --git a/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md b/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md
index 35d4d276b..17f48dda1 100644
--- a/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md
+++ b/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md
@@ -4,7 +4,7 @@ Amadan #328 (an isolated instance reaching a private service its operator
runs) and #329 (operator-set CPU and memory per instance). Revision 7,
2026-10-21. Revisions 2 to 7 answer the builder check, Astra rounds
1 to 6, and Paul's ruling on fleet addresses (`CODEX-FINDINGS-instance-sizes-egress-R<n>.md`); the
-dispositions are §9.
+dispositions are §9. **Approved by Paul, 2026-10-21.**
Both are the same mechanism: **an operator-set policy for an app and its
instances, kept in one operator-only object, delivered to the box, and