Skip to content

feat: a grid can be keyed on an operating system (ADR 0039) - #80

Merged
kelvin1295 merged 14 commits into
mainfrom
feat/os-grid-type
Sep 3, 2026
Merged

feat: a grid can be keyed on an operating system (ADR 0039)#80
kelvin1295 merged 14 commits into
mainfrom
feat/os-grid-type

Conversation

@kelvin1295

Copy link
Copy Markdown
Collaborator

Ships the public CLI's half of ADR 0039 — a grid can be keyed on an OS: a machine claims
its operating system when it signs in, and is admitted to the community grid for that OS.

What is in THIS repo

  • shared/system/os_grid.py — the closed token set (macos/windows/linux/omarchy) and
    os_token(). No env override, deliberately: the claim is derived from the machine, not typed.
  • remote/control_plane.py — sends os= on GET /v1/grid/tokens, and a second spelling of
    the same claim on the renewal body. The two are written in different places on both sides and
    drift independently, so both are pinned.
  • cli/os_grid_notice.py — three causes leave somebody with no OS grid; this says which, and stays
    quiet in the two cases that are not this feature's answer. os_served is read as a tri-state:
    None (an older control plane did not say) is not False (it said no).
  • remote/serve.pyname_chosen on the register meta. An os-community member runs THIS CLI's
    provider half, not grid-src's, so this is the half that decides whether a stranger's hostname is
    published on a public page.
  • fix: a malformed token answer no longer empties the credential store — latent on every grid
    type since the route was written, found because os-community reaches it on every refresh.
  • ADR 0039 itself (this repo is the cross-repo decision authority).

Cross-repo — read before deploying

Three repos, no code dependency, every seam is wire-level. Deploy order is part of the product:

  • grid-src BEFORE grid-apis. The control plane shells out grid network create --network-type os-community and that binary IS grid-src. Wrong order ⇒ argparse exit 2, a grid stuck pending
    retrying forever.
  • relay before the control plane for the denylist widening. Wrong order ⇒ a ban is exactly
    ineffective against the account it was written for
    , for up to a year.
  • This CLI has no ordering in either direction. os= is a new query parameter on an existing
    endpoint: an old control plane drops it, a new one reads an old CLI's silence as claiming nothing.

Verification

  • tests/test_os_grid_type_lockstep.py (new, 931 lines) compares the two repos' spellings
    against each other, not against a constant either side can move with its own tests.
  • Full suite green after the sync-down merge of main (v0.3.28).
  • Verified live on the dev VM, both halves deployed in ADR order, each proven from the running
    process rather than from git: admission by a non-owner; the same account on two machines getting
    two different OS grids and neither seeing the other's; task plane refused at its single gate;
    member-usage panel 403 against a 200 control; provider e-mail and machine name withheld against a
    control grid publishing both from the same relay build; denylist biting both halves and releasing
    again; a real completion served free.

Known open, deliberately

  • windows hostname derivation is unmeasured (issue 15's last box). It does not gate this:
    D-n withholds regardless of what any platform's default turns out to be.
  • Omarchy's own grid is a separate slice, blocked on a measurement nobody has taken.
  • grid-src's re-type door onto an os-community grid is a separate slice (issue 09), named in D-i.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CZ33AtDj2SctoLg37yYsy8

kelvin1295 and others added 14 commits August 28, 2026 16:55
ADR 0039 D-a adds one network type, `os-community`, whose two copies live in
grid-src (`network_runtime.py` and `private_server/grid_auth.py`) with no import
path to either. This adds the ADR and the cross-repo pin.

`tests/test_os_grid_type_lockstep.py` parses both grid-src modules with `ast`
and checks four things a rename or a dropped set member would otherwise break in
silence: the literal in each module, its presence in `VALID_NETWORK_TYPES`,
`VALID_NETWORK_TYPE_INPUTS` and `PUBLIC_NETWORK_TYPES`, and its presence in
`_requires_allowlist`'s open-type tuple. The last is the one no successful create
catches — the grid provisions, starts, appears in `grid ls`, and then 403s every
member, with no allowlist row anyone can add because D-e persists nothing about a
person's OS.

Every case carries a positive control (`private-domain` must be found by the same
helper), so a resolver that has quietly stopped reading anything fails as a
harness fault rather than reading as "the seam is fine". Six mutants applied to
grid-src, six killed.

`tests/grid_src_repo.py` extracts the sibling resolver `test_starter_engine_
lockstep.py` had inline, so the two suites cannot drift into two ideas of where
grid-src is — a resolver that drifts does not fail, it skips. It also now
validates `GRID_SRC_REPO` and RAISES on a bad one: `tests/e2e_cross_repo/
_harness.py` defaults that same variable to a different worktree, so exporting it
for the E2E would otherwise switch off every pin in both suites in silence.

Per this repository's rule for cross-repo assertions, the new suite skips unless
grid-src sits beside this worktree — it skips in CI, and a green CI proves
nothing about it.

Full suite: 3225 passed, 9 skipped, 0 failed.
Adds the public CLI's half of ADR 0039 (issue 02). `shared/system/os_grid
.os_token()` is the only place in this CLI that decides what OS this machine
runs: `macos`, `windows`, `linux`, or nothing. `remote/control_plane
.fetch_tokens` sends it as a new `os=` query parameter beside `device_id` —
the only channel there is, since the device-login start and poll calls carry
nothing about the machine and the browser approving a sign-in may be a
different device entirely.

The taxonomy is CLOSED, and that is what makes issue 03's auto-provisioning
safe: an unrecognised system resolves to None and sends no parameter at all,
where an open value space would turn every unrecognised string into a
permanent empty grid. Every Linux resolves to `linux` — an unusual choice of
distribution must not exclude somebody from the general Linux grid. `omarchy`
is deliberately absent (issue 04 owns taking the measurement first).

This is NOT `host.platform_kind()` and the two must not be merged: that one
answers "which binaries run here" and splits macOS by CPU generation, which
would put Apple Silicon and Intel users in different communities. A ⚠️ on
that function now says so.

The value is a CLAIM, never a fact — the same class as `device_id`. The gate
it feeds stops mistakes, not intent.

No rollout ordering in either direction: `os=` is a new key on an existing
endpoint, so an old control plane ignores it and a new one facing an old CLI
issues nobody an OS-grid token (D-j). Both directions are tested.

Also pins grid-apis `store.py` as the THIRD copy of the `os-community`
literal in the cross-repo lockstep suite, and records the amendments review
turned up in ADR 0039: D-i refuses project creation alone (not task creation
too), D-e's roster claim is narrower than "reports nobody", the re-type
residue is self-minted membership rather than access granted by hand, and
the refresh path's refusal is destructive rather than a clean gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017grXGJhrS2joydZFsWE9iU
…eter name

Two findings from the review of 8823e2b.

THE RECOVERY ADVICE WAS INVERTED ON EXACTLY ONE TYPE. `grid_credential`'s
403 branch told the user their refusal was about membership and that
`grid login` would not change it. True for every pre-existing network type,
and backwards for `os-community`: there membership is re-derived from the
`os=` claim that only `fetch_tokens` sends, so signing in again IS the only
recovery — and the CLI was steering people away from the one command that
would have fixed it.

THE `os` PARAMETER NAME IS A CROSS-REPO WIRE VALUE AND HAD NO PIN. This
feature added a second hand-duplicated value besides the type literal: the
query parameter's NAME, written independently as `params["os"]` here and
`alias="os"` in grid-apis. Rename it on one side alone and both repos stay
green — this repo's test asserts what the CLI sends, grid-apis' asserts what
it accepts, and neither notices they no longer agree. It degrades SILENTLY:
the parameter is ignored and nobody is issued a token, which is the class
CLAUDE.md's register says must be pinned rather than reasoned about. Now
pinned against grid-apis' decorator, with a positive control, and raising
rather than skipping when the sibling worktree is misconfigured.

⚠️ The pin matches BOTH `ast.FunctionDef` and `ast.AsyncFunctionDef`. `async
def` is a separate node type that is not a subclass of the sync one, and
grid-apis already spells 15 of its routes that way — so matching only the
sync kind made an ordinary `async def` refactor, which moves no route and
breaks no seam, report drift that had not happened. Verified with three
controls: without the fix an async refactor fails, with it there is no false
alarm, and moving the route's path still fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017grXGJhrS2joydZFsWE9iU
The CLI half of issue 10. `refresh_network_token` now reads
`os_grid.os_token()` itself and puts the claim in the refresh body, so a
machine on an `os-community` grid renews without a person running a command.
It omits the key entirely when this machine claims no OS — the same
discipline as the fetch, so the far end never has a second spelling of "no
claim" to recognise.

The serve loop needed no change and that is the design: `_ServeState.refresh`
passes no claim of its own and inherits one because the helper reads the
single decision point. Asserted there rather than at the helper, since the
loop is the caller a `network_epoch` bump actually reaches — a refactor that
threaded the claim through the loop's arguments would leave the helper's test
green and that one red, which is the right way round.

⚠️ THE FETCH'S PIN DOES NOT COVER THIS ONE. The claim rides the sign-in as a
query parameter and the renewal as a JSON body key: two spellings, written in
different places on both sides, drifting independently. Two new lockstep cases
— the renewal's key against grid-apis' model, and one asserting this CLI's own
two call sites agree with each other, because a machine announcing `os` on
sign-in and `os_token` on renewal would be admitted and then silently unable
to renew. Both verified by mutating each side separately.

⚠️ The agreement case takes a monkeypatch CONTEXT per call, and that is not
tidiness: both helpers patch `httpx.Client` and capture the real one first, so
under one patcher the second captures the first's replacement, its handler
never fires, and it reads an empty body. The helper's positive control caught
exactly that instead of letting the comparison report a verdict it had not
measured.

Also pins `test_control_plane_refresh_network_token_posts_refresh_unauthenticated`
to a fixed `platform.system()`. Its exact-body assertion had become
machine-dependent — it failed on a Mac and would have passed untouched on a
BSD — so it is pinned rather than loosened to a subset check, which is what
still catches an unconditional extra field.

ADR 0039 D-e records the decision, the rejected alternative and why it could
not work, and the measured both-ways degrade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017grXGJhrS2joydZFsWE9iU
D-l answers issue 08: `GET /relay/v1/grid/members/usage` is refused on
`os-community`, the same at-the-door shape as D-i.

Its gate is `inference:models` — the scope every consumer holds — so on a
grid that keeps no allowlist it reduces to "do you hold a token here". On
this type that is every stranger running the same operating system, and
the route returns each member's email alongside their spend.

D-e did not already cover it: the roster is empty by construction because
no OS membership is persisted, but this route's emails come from observed
traffic (`node_answered_query.member_snapshot()`), which no membership
model can empty. D-e now points at D-l so the next reader does not stop
there. The app being the route's only reader is not containment either —
the exposure is the route, and every member on this type can call it.

No rollout order and no half in this repo; grid-src carries the refusal.
D-m answers issue 14: `provider_email` is sent as `null` on `os-community`,
and the key stays.

`GET /relay/v1/grid/overview` is public and unauthenticated and names every
provider by email. On a private-domain grid that is a company publishing its
own people's machines; on this type it is the address of every stranger who
serves, readable by anyone who knows the URL.

This is the THIRD mechanism onto one exposure, and the door left open was the
WEAKEST-gated of the three: D-e empties the roster by construction, D-l refuses
an authenticated route, and this one has no gate at all. That ordering is not a
decision anybody made — it is the shape of finding the mechanisms one at a time,
so D-m also writes down the question to ask before the fourth.

The key stays and only the value goes: a payload shape that varies by network
type is the cost D-l already rejected. Overloading `null` is deliberate — it
already means "the owner could not be resolved", and a distinguishable
"withheld" would advertise that there is a name to ask for.

No app half and no rollout ordering, measured: `OverviewNode.providerEmail` is
already nullable and documented as null on an older relay, and `node_groups`
collects unattributed machines into one headless block. The app's own
`nodeHostHandle` already carried the comment that the overview "is
unauthenticated and carries the whole address" — the relay-side decision was
the missing half.

Rejected: a stable pseudonymous handle (stable across polls IS a durable
identifier), authenticating the overview (every member holds a token, so it
converts an unauthenticated leak into an authenticated one — what D-l refused),
and accepting it (running `grid join` says nothing about publishing an address).

D-l gains a forward pointer so it does not read as the last word.
…y found

D-m closed by saying nobody had enumerated every reader of UserRow.email on
the relay, so "no fourth mechanism found" was not yet "no fourth mechanism
exists". Issue 15 does that enumeration; this records its answer so the next
reader does not repeat it.

Six sites emit an address. Two are closed by decision (D-l, D-m), three are
unreachable behind D-i, and one is a token's own subject handed to its own
holder. There is no roster route on the relay at all — list_grid_members is
grid-apis'.

D-i's premise carries three of the six, so it was tested against the paths its
own AST scan admits it cannot see: apply_sync_snapshot writes no ProjectRow
(the most plausible one, being the writer driven from outside the relay), no
raw or bulk insert into projects exists outside tests, and no migration seeds
one.

⚠️ One residue D-m did not consider: the node `name` on the same public
payload is `opts.node_name or platform.node()` — the machine's hostname — and
consumer systems commonly derive a default machine name from the owner's. That
is weaker than an address and is NOT measured: the machine this audit ran on
does not show the pattern. So D-m is left as it stands rather than amended on
an unmeasured claim, and issue 15 carries the measurement to take on stock
macOS and stock Ubuntu plus three candidate answers.
ADR 0039 D-h decided the denylist is widened to `os-community`; issue 06 built
it. Neither half lives in this repository — grid-apis decides what a caller sees
and is issued, grid-src decides what a credential already in flight is served —
so what belongs here is the decision record and the pin that neither repo's own
suite can carry.

D-h gains an As-built note: the role condition moved into a named per-type
answer on each side (`store.denylist_governs`, `grid_auth._is_open_consumer`),
so "which types does the denylist reach" and "whom does it bite there" are asked
separately rather than one inferred from the other. It also records the two
consequences that are not obvious from the decision — the owner of an OS grid is
deniable rather than exempt, and every arm of `member_for_access` that
synthesizes a member has to ask (the owner arm was missed on the first pass and
found in review).

`test_the_denylist_reaches_an_os_grid_on_both_sides_of_the_call` parses
`DENYLIST_NETWORK_TYPES` out of BOTH sibling repos with `ast`, with a
permissioned-providers positive control on each. It reads the type SET only —
the role condition is a function body spelled differently on each side on
purpose, and each repo pins its own against a `both` member. Proven by dropping
`os-community` from either side, which kills the case for that side.

⚠️ Like every cross-repo check here it skips unless the sibling worktrees sit
beside this one, i.e. it skips in CI. Run locally with all four present: 13
passed. Full suite 3247 passed, 0 failed.
The public CLI's half of ADR 0039 D-k (issue 07). Three different things leave
somebody with no OS grid in their list, and until now they shared one symptom that
neither a support conversation nor the person in front of it could take apart.

`cli/os_grid_notice.py` is the new module. It answers cause 3 — a machine whose
operating system resolves to no token — LOCALLY, deciding it before `os_served` is
looked at at all. That independence is the point, not an optimisation: it is
parametrised over every answer the far end can give (`True`/`False`/`None`), so a
refactor folding it into the `os_served` branch is caught. Causes 1 and 2 become
one line from the control plane's new key.

`fetch_tokens` now returns a frozen `TokenFetch(networks, os_served)` rather than a
bare list — one door onto the request, not a second helper that guarantees nothing.
Every test stub therefore models the reply, which is a feature; the two in gated
tests that assert the call never happens were converted too rather than excused,
because a stub that cannot be copied safely is the next person's bug.

⚠️ `os_served` is a TRI-STATE and `None` is not `False`. Collapsing them — the
obvious tidy-up — puts "this control plane isn't serving one" in front of every
user of every deployment that has not shipped the key yet, the one direction D-k
exists to keep quiet. Compared by identity against the two booleans; `"false"` is
truthy in Python, so a truthiness test reads a refusal as a service.

⚠️ `--json` gained an always-present `os_grid` key. That IS a compatibility event —
the one part of this an older client can see — and it is recorded rather than
discovered later. Always-present rather than emitted-only-when-there-is-something-
to-say, because a key that comes and goes is one every script has to guard.

A fourth cross-repo pin joins the lockstep suite. `os_served` degrades silently in
BOTH directions (an unknown JSON key is dropped by everybody), so a rename on one
side leaves both repositories green with the feature explaining nothing. The pin
reads the keys grid-apis returns off its `return` dict literal — the route declares
no response model, so the literal is the contract — then probes which of them this
CLI actually reads by ANSWERING with each in turn. Verified by mutation on each side
separately, plus a case proving `OS_SERVED_KEY` has not drifted from the parser.

`os_token()` now resolves from `system_name()`, so the module makes a single
`platform.system()` read: the name a person is shown and the value the gate compares
can no longer come from two readings four lines apart.

Recorded rather than done, so nobody "fixes" them back:
- The line is NOT printed when the command itself fails. A failed `grid sync` should
  report its failure; burying the actionable sentence under a side fact serves
  nobody, and the line arrives on the next command that succeeds.
- `grid ls` gained nothing, though D-k's opening names it. The local cause is free
  there; the control-plane cause is not, because nothing persists `os_served`, and
  explaining one cause while silent on the other is worse than explaining neither.
- The claim is re-derived by `absence()` rather than threaded through `TokenFetch`:
  being answerable without a round trip is the property criterion 1 asks for.

Review found four defects, all in tests, none visible by reading — the two-reasons
test compared whole sentences (satisfied by the machine's name, since the two cases
can never share a machine); the fix for that over-blanked with `str.replace` and let
the mirrored mutant live; and criterion 4 keyed on the phrase "OS grid", which would
have gone vacuous on any reword of the sentence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuK1jcCXSjVwzNak87S4dw
D-m nulled the provider's address on the public overview and deliberately
left the machine described. Issue 15's enumeration found the machine's own
name on the same unauthenticated payload, and consumer systems derive a
default machine name from the owner's — so that field can carry a person's
given name with nobody having chosen to publish anything.

Measured 2026-09-03 on a never-renamed Mac (macOS 26.6): Setup Assistant
writes `<first word of the account's full name>'s <model>`, and the value
reaches `platform.node()` verbatim. A sweep of all 39 prod masters' own
overviews found one live node published under exactly such a default.

D-n: on `os-community` the overview publishes a name only when the provider
states its operator chose it (`name_chosen` on the register meta); otherwise
the anonymous label the route already computes. Withholding every name was
rejected on the same measurement — real operators do name their machines,
often after their people — and a relay-side heuristic was rejected because it
is wrong in both directions on the fleet actually observed.

Records two facts the audit did not have: the residue has TWO provider halves
(grid-src `lifecycle.py`, and the public CLI's `socket.gethostname()`), and
the app needs no half. The Linux measurement stays open and can only widen
this, not overturn it.
D-n was decided on the macOS measurement alone and said the Linux half could
only widen it. Reading the installers: Ubuntu desktop generates
`<username>-<product>` and leaks the same class; Omarchy sets no hostname at
all, so an Omarchy box inherits archinstall's constant `archlinux` and leaks
nothing; windows is still unmeasured and was never in issue 15's scope. The
fleet as deployed is provisioner-named throughout.

Marked as source reads rather than measurements, since that is what they are.
ADR 0039 D-n, issue 16. On an `os-community` grid the relay's public,
unauthenticated overview publishes a node's name only when the provider states
its operator chose it; every other network type is unchanged. This is the public
CLI's half of that statement — the binary an OS-grid member actually installs,
which is not the grid-src runtime that runs on the fleet — plus the cross-repo
pin holding all three copies of the key against each other.

- `--name` and the hostname stop being one string at the join. The run record
  gains `meta_name_chosen`, and `remote/serve._meta` states it on the wire as
  `name_chosen`, read by identity against `True` so a record written before this
  change reads as NOT chosen — the fail-closed direction, and the only thing
  standing in for a 404 nobody gets on a merged payload.
- The no-op gate compares the claim, but only when the record STATES one.
  `--name mybox` on a box already called `mybox` must act — same string,
  opposite fact — while a record from an older build must not turn every re-join
  that passed `--name` into a reload nobody asked for. Two questions, two
  readings of absent.
- The mirror direction is the only silent one, and it now speaks: dropping
  `--name` on a box whose hostname matches the chosen name says so on stderr,
  naming no network type (this repository holds no `os-community` constant).
- `tests/test_os_grid_type_lockstep.py` compares the key's spelling on both
  sides against each other — the relay's read off the `published_node_name`
  call, this CLI's measured off two metas that differ only in that fact — and
  against grid-src's other provider half. Verified by mutating all three copies
  separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZ33AtDj2SctoLg37yYsy8
Found reviewing issue 16; the defect is in issue 07's slice.

`fetch_tokens` fell back to `payload = {}` for a 200 whose body is not a JSON
object, so a malformed answer read as "zero grids" — and `cmd_sync` overwrites
`[[networks]]` authoritatively, discarding every grid's access AND refresh
token. Refresh rotation makes those unrecoverable, and the only signal was a
stderr line calling the wipe possibly transient.

`_validated` could never catch it: it is the trust boundary for bad ROWS and
iterates the list, so a non-object body yields nothing to validate and sails
through. The check now sits one level up, on the shape that holds the rows, and
raises `ControlPlaneError` — a `SystemExit` subclass, so "never a crash" (the
reason the fallback was added) still holds without the credential loss. The same
refusal covers a body that will not parse, an empty 200, and one nested past the
parser's limit; `RecursionError` is caught beside `ValueError` because
`json.loads` raises it and no `except ValueError` would see it.

The wording is pinned against `cli.auth._SESSION_EXPIRED_RE` so a broken 200
never sends somebody to `grid login` for a fault no credential can fix.

⚠️ This overturns the rule `test_fetch_tokens_survives_a_body_that_is_not_an
_object` encoded, so that test is rewritten with the reason rather than a second
one added beside it, and a negative control pins that an ordinary
`{"networks": []}` still clears the list.

Also, in the same review, the "no OS grid" line was reported as firing for every
user of a deployment with OS grids switched off. That is ADR 0039 D-k's decision
— the first of four states `false` deliberately collapses — not a defect. What
was wrong is `os_grid_notice`'s own "Quiet in the ordinary case" bullet, which
read as though that case were exempt. The bullet now says what D-k decided and
`test_a_deployment_serving_no_os_grid_at_all_still_says_so` pins it, because
suppressing the line there would also suppress it for a provision that failed
and left the row `pending`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZ33AtDj2SctoLg37yYsy8
Sync-down before the PR, so the feat x main integration lives on the feature
branch itself. Brings v0.3.28 and the Z-Image media route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZ33AtDj2SctoLg37yYsy8
@kelvin1295
kelvin1295 merged commit 1a00e19 into main Sep 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant