| 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. |