# Self-hosted Subrouter authority S1 — request

**Status:** COMPLETED

**Primary design:** `docs/specs/2026-08-17-subrouter-authority-design.md`

**Owner-obtainable evidence:** the installed credential-empty `authority-proof` command must report `Exact dark route: PASS`, `Proxy grant scope: PASS`, `Admin isolation: PASS`, `Gateway-only home: PASS`, and `Native launch unchanged: PASS` without printing a key, account identifier, route identifier, credential path, or provider request body.

**Goal:** Add a server-enforced exact-account route, route-scoped proxy grants, protected local administration, additive Systray bindings, gateway-only client homes, and dark health/launch resolution while leaving every real provider launch native and every provider credential outside Subrouter.

## Context

S0 is installed as a credential-empty singleton on `127.0.0.1:31415`. Its authority-mode handler intentionally exposes only health/readiness and has no route, grant, or account custody. This request is design slice S1 only. It creates one synthetic credentialless fixture through the supported local authority administration seam and proves policy at the installed entrypoint. It does not import, inspect, refresh, copy, or send a provider credential and must not begin the S2 live canary.

The pinned upstream already accepts `X-Subrouter-Account-ID`, but a client header is not an authorization boundary. S1 binds one opaque route to one provider/account in the authority store, authenticates every route request with a grant scoped to that route, rejects conflicting selectors, and injects the bound account server-side before existing account selection. Pooled scheduling must be unreachable from an exact route.

## Files

Modify only these Overdeck paths:

- `modules/subrouter/patches/0001-overdeck-authority-safety.patch` — extend the pinned patch with exact-route/grant storage, HTTP enforcement, protected administration, CLI administration/proof, and fixture tests.
- `modules/subrouter/bin/verify-candidate` — recognize the expanded bounded upstream path list and run the exact-route/grant focused tests.
- `modules/subrouter/bin/install-candidate` — atomically install schema-compatible S1 state/admin material and preserve rollback.
- `modules/subrouter/systemd/overdeck-subrouter.service` — pass only protected authority configuration required by S1; keep loopback and S0 isolation.
- `modules/subrouter/tests/test_candidate_pipeline.py` — pin the expanded patch scope and exact-revision receipt behavior.
- `modules/subrouter/tests/test_service_contract.py` — cover protected admin material, credentialless fixture installation, idempotence, rollback, and installed proof.
- `modules/subrouter/README.md` — document S1 dark operation and key-safe commands.
- `modules/systray/account_registry.py` — additive authority binding metadata and backward-compatible registry serialization.
- `modules/systray/command_router.py` — authority launch-plan builder, gateway-only homes, inherited credential stripping, and dark resolution without launch cutover.
- `modules/systray/provider_services.py` — gateway health adapter selected by binding mode; no provider-token inspection.
- A narrowly named helper under `modules/systray/` only if required to keep authority config/grant parsing independent of GTK.
- Focused tests under `modules/systray/tests/` for every registry, environment, dark-routing, health, and secret-redaction contract below.
- `docs/plans/2026-08-17-subrouter-authority.md` and `docs/plans/INDEX.md` — mark S0 completed and register this S1 request/receipt.

The expanded patch may modify only these additional pinned-upstream paths beyond the S0 allowlist:

- `internal/authority/routes.go` and `internal/authority/routes_test.go` — durable exact routes, hashed grants, expiry/revocation, locking, cardinality validation, and sanitized views.
- `internal/proxy/authority_routes.go` and `internal/proxy/authority_routes_test.go` — `/r/<route-id>` authentication, exact selector enforcement, bounded self-status, and data/control separation.
- `internal/proxy/proxy.go` and `internal/proxy/proxy_test.go` — the smallest seam needed to make a server-forced account immutable through existing selection and to reject a conflicting client selector.
- `cmd/subrouter/authority_routes.go` and `cmd/subrouter/authority_routes_test.go` — protected file-writing administration and non-secret installed proof.
- `cmd/subrouter/authority_status.go`, `cmd/subrouter/authority_status_test.go`, and `cmd/subrouter/main.go` — wire S1 only in authority mode and expose sanitized route/grant counts.

Any other upstream path is a scope blocker and requires this request to be corrected before code changes. Provider protocol, OAuth bodies, refresh logic, scheduling, and forwarding remain upstream-owned.

## Contracts

### Authority data

```text
AuthorityRoute
  route_id: opaque random non-secret identifier
  tenant_id: opaque internal identifier
  provider: claude | codex
  account_id: exact server-side account identifier
  enabled: boolean

ProxyGrant
  grant_id: opaque random non-secret identifier
  route_id: exact route scope
  key_hash: SHA-256 hash of the random bearer key
  audience: workstation | buildbox-run
  issued_at: RFC3339 timestamp
  expires_at: RFC3339 timestamp
  revoked_at: RFC3339 timestamp | null
```

The authority store persists only these fields. It never persists or returns a clear proxy key after grant creation. Writes are atomic, fsynced, mode 0600, protected by a cross-process mutation lock, and reread under lock before validation. Enabled routes must resolve to exactly one matching provider/account at every request and mutation; an invalid route is atomically disabled or the mutation fails without partial state.

### Route HTTP surface

```text
/r/<route-id>/<provider-path>
/r/<route-id>/_subrouter/status
```

A route request authenticates a normal bearer or `X-Api-Key` value by SHA-256 digest and constant-time comparison, then verifies grant existence, exact route scope, expiry, revocation, route enabled state, provider compatibility, and exact account existence. It strips the proxy credential and all client account-selection headers before forwarding. It injects the route-bound account through an internal context/value seam that cannot be overridden by headers. `/r/<route-id>/_subrouter/status` returns only readiness/migration state, provider, bounded route/grant fingerprints, expiry/revocation state, and sanitized account availability; it exposes no account ID, tenant ID, key/hash, credential path, provider credential, admin state, or list.

Missing, malformed, expired, revoked, unknown-route, and wrong-route grants return 401/403 without distinguishing key existence. A conflicting `X-Subrouter-Account-ID` or equivalent account selector returns 409 before account selection. A bound account that is absent, blocked, disabled, duplicated, or incompatible returns unavailable and never enters pooled scheduling.

Every `/_subrouter/*` mutation/list/admin endpoint remains unreachable through `/r/<route-id>` except the exact bounded status path. Existing S0 health/readiness remain loopback-only. Authority mode has no tenant account import, tenant mutation, global account list, drain, transcript, repair, or admin HTTP route reachable with a proxy grant.

### Local administration and key creation

```text
authority-route create|disable|status --state-dir <absolute> ...
authority-grant create --state-dir <absolute> --route-id <id> --audience <audience> --expires-at <time> --proxy-key-file <absolute-protected-path>
authority-grant revoke --state-dir <absolute> --grant-id <id>
authority-proof --state-dir <absolute> --origin <loopback-origin> --proxy-key-file <absolute-protected-path>
```

These commands require the protected local authority administration seam and refuse non-local origins, symlinked state/key paths, permissive parent/key modes, relative paths, stdout key output, and an occupied destination. Grant creation generates the key inside the command, atomically writes it directly to the requested mode-0600 file, fsyncs file and parent, and sends/stores only its hash and lifecycle metadata. Failure after file creation revokes/removes the unfinished grant and removes the exact run-owned file. The key, hash, route/account IDs, Authorization values, and full paths never appear in stdout/stderr/logs/receipts. Proxy grants cannot authenticate any administration command or endpoint.

The service/admin mechanism must be local-only and independently protected from route grants. S1 may use the dedicated service identity plus a root-owned local control file/socket; it must not distribute an admin bearer to Systray, buildboxes, clients, or fixture output. Installation creates no provider account and refuses authority state containing provider credential material.

### Systray binding

```text
AuthorityBinding
  mode: native | subrouter-dark | subrouter
  authority_name: configured endpoint reference
  route_id: opaque non-secret route identifier
  provider: claude | codex
  proxy_grant_ref: protected workstation grant file reference
```

`Account.authority_binding` is optional. Missing/legacy entries decode as `mode=native`. Registry serialization validates exact enum/provider values, rejects unknown fields and credential-like inline values, writes the binding additively, and round-trips native entries without manufacturing a binding. Renaming/rekeying an account preserves its binding; removal removes only metadata and never reads/deletes the referenced grant file.

No account slug, route, tenant, remote account ID, endpoint, or key is hardcoded in source. Authority endpoints are resolved by `authority_name` from protected configuration rather than embedded in registry entries.

### Command-router seam

```text
build_authority_launch(account, tool, argv) -> AuthorityLaunch
AuthorityLaunch = executable + argv + environment + gateway_home + sanitized_status
```

- `native`: preserve existing executable, argv, environment, credential home, sync behavior, and dispatch behavior byte-for-byte.
- `subrouter-dark`: resolve endpoint/binding/grant metadata, read bounded route self-status, build and validate the prospective gateway-only launch, then execute the unchanged native launch. Dark resolution failure reports `Gateway: migration required` to health/status but cannot block or alter native execution.
- `subrouter`: construct only the exact route base URL (`/r/<route-id>` for Claude; `/r/<route-id>/v1` for Codex), route-key auth environment, stable session identity/provider namespace, and dedicated gateway-only home. It removes inherited native provider auth variables and never supplies a native account home. Missing/expired/revoked/incompatible material fails visibly with no native fallback. S1 must keep this mode disabled for installed real accounts.
- Info/version invocations remain credential-free and local.

Gateway homes live under protected Overdeck runtime state, contain a manifest naming authority/route fingerprints and allowed files, and refuse provider auth files (`auth.json`, `.credentials.json`, `claude.json`, or token-bearing JSON). Creation is atomic and mode 0700. Tests inspect the materialized tree and prove no provider credential file or native-home link exists.

### Gateway health

`fetch_gateway_health(binding) -> AccountSnapshot` calls only the route's bounded status using its proxy grant. It distinguishes ready, migration-required/missing binding, unavailable authority, blocked chain, and exhausted account using existing owner-facing status vocabulary. On outage it preserves the last verified snapshot as stale and never invokes native provider health or opens native fallback. `native` retains current health behavior. `subrouter-dark` exposes gateway readiness alongside, but does not replace, native launch behavior.

## Credentialless fixture

Installation creates no fixture automatically. The installed S1 proof setup creates one synthetic authority account record containing only provider, opaque synthetic ID, and explicitly unavailable/no-credential state; one disabled-by-default exact route; and one short-lived fixture grant written to a run-owned protected path. Enabling the fixture route is allowed only inside the proof transaction. No request can reach a provider: the synthetic target must fail before transport, and tests install a transport that fails if called.

`authority-proof` exercises the installed daemon through HTTP without exposing its key. It proves exact self-status success; provider request refusal before transport; missing, malformed, expired, revoked, wrong-route, and conflicting-selector failures; every mutation/admin/list path denied; new streams denied after revocation; and sanitized output/logs. It then revokes the grant, disables/removes the fixture route/account through the supported local seam, and removes the run-owned key file. A failed proof performs the same cleanup and reports only a bounded failure class.

## Out of scope

- Any live provider login, token import/copy/read, refresh, OAuth POST, model request, or provider credential custody.
- S2 mode cutover, S3 tailnet exposure, S4 remote-seat grants, S5 account migration, or S6 native-path removal.
- Cloudflare, public listeners, pooled-route conventions, client-supplied exact-account trust, or a second compatibility proxy.
- Changes to provider refresh semantics already completed in S0.
- GTK layout redesign or new owner-facing UI primitives.

## Acceptance

Run in order with unique external temp roots and registered buildbox execution for Go/build work:

1. Focused: `modules/subrouter/bin/verify-candidate focused`
   - PASS: exact-route forcing, grant scope/expiry/revocation, selector conflict, sanitized self-status, protected grant-file generation, and credentialless cleanup tests pass alongside S0 refresh/store-lease tests.
2. Static: `modules/subrouter/bin/verify-candidate static`
   - PASS: expanded path allowlist, patch applicability, Go format/vet, Python compile/static checks, shell/systemd syntax, JSON, secret scans, and `git diff --check` pass.
3. Systray focused: `RUN_ID="$(date +%s)-$$"; ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/overdeck-private-tests/subrouter-s1-systray"; TMP_ROOT="$ROOT/$RUN_ID"; mkdir -p "$TMP_ROOT"; chmod 0700 "${XDG_CACHE_HOME:-$HOME/.cache}/overdeck-private-tests" "$ROOT" "$TMP_ROOT"; python3 -m pytest modules/systray/tests/test_account_registry.py modules/systray/tests/test_authority_client.py modules/systray/tests/test_command_router.py modules/systray/tests/test_command_router_authority.py modules/systray/tests/test_provider_services.py -q --basetemp "$TMP_ROOT"`
   - PASS: additive registry compatibility, native snapshot equality, dark resolution, gateway-only home, environment stripping, stale health, and log/error redaction contracts pass.
4. Module contract: `RUN_ID="$(date +%s)-$$"; ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/overdeck-private-tests/subrouter-s1-module"; TMP_ROOT="$ROOT/$RUN_ID"; mkdir -p "$TMP_ROOT"; chmod 0700 "${XDG_CACHE_HOME:-$HOME/.cache}/overdeck-private-tests" "$ROOT" "$TMP_ROOT"; python3 -m pytest modules/subrouter/tests/ -q --basetemp "$TMP_ROOT"`
   - PASS: candidate, installer, isolation, idempotence, rollback, fixture lifecycle, and exact receipt contracts pass without root/live credentials.
5. Broad/build once: `modules/subrouter/bin/verify-candidate full`
   - PASS: complete pinned patched `go test ./...`, binary build, exact artifact digest, and atomic exact-revision receipt succeed on a registered buildbox for identical bytes.
6. Install exact candidate: `CANDIDATE="$(modules/subrouter/bin/verify-candidate path)"; deck-sudo modules/subrouter/bin/install-candidate "$CANDIDATE"`
   - PASS: immutable candidate activates on loopback, remains provider-credential-empty, and rollback metadata remains valid.
7. Installed proof: invoke the installed binary's protected fixture setup/proof/cleanup wrapper through `deck-sudo`; do not expose the generated key path or value in shell history/output.
   - PASS output is exactly the five owner lines named at the top plus `Provider credentials: 0`; native Claude/Codex credential file fingerprints taken by the existing bounded fingerprint helper are unchanged.
8. Installed dark Systray proof: create a temporary synthetic registry/config root outside real account state, resolve one `subrouter-dark` fixture through the installed service, and invoke the real command-router status seam with execution stubbed.
   - PASS: status reports `Gateway: ready` or `Gateway: migration required`, prospective gateway home has no provider auth file, and captured native executable/argv/environment equal the native baseline.

The exact installed S1 receipt and proof output are linked from `docs/plans/INDEX.md` before S2 begins. Missing installed evidence keeps S1 active and blocks S2.

## Completion evidence — 2026-08-19

S1 is complete and remains dark for every real provider account. No live provider login, token import/copy/read, OAuth refresh, or model request was performed. S2 has not started.

Exact installed candidate is canonical only through the verifier/runtime pointers; a literal candidate digest is deliberately not committed because the receipt key includes Git `HEAD` and committing that literal would invalidate it. Use:

```text
repository receipt: modules/subrouter/bin/verify-candidate path
installed receipt: /var/lib/overdeck/subrouter/current/receipt.json
patch: 46c620505f5d372a1c44dc10ca8632423d22df704fce65a56ad3501a8937a3a1
upstream revision: 29c7ebb306ac54739206f4752449e437047dd150
source digest: 74ab8da37d467a7d36bb73b11d82d90fd5ea14f3b75c535026adb0e45d9529f6
binary sha256: b1cbfa35dc3dba2bf2ee455f7325dbde5c5f4015142705a5ab2815a071a70d21
```

Exact receipt phases are all exit 0: focused, static, and full ran on registered buildbox `debian3`; the module contract ran against the same input/diff and collected 42 tests. Final Systray focused acceptance is `237 passed in 14.63s`. The full phase includes the pinned patched `go test ./...` and binary build.

Installed owner evidence:

```text
Exact dark route: PASS
Proxy grant scope: PASS
Admin isolation: PASS
Gateway-only home: PASS
Native launch unchanged: PASS
Provider credentials: 0
```

After synthetic proof cleanup, installed `authority-status` reports `Store lease: held`, `Provider credentials: 0`, `Authority routes: 0`, `Proxy grants: 0`, `Unresolved attempts: 0`, and `Gateway: ready`. `overdeck-subrouter.service` is active and bound only to `127.0.0.1:31415`. A second authority process against the installed state exits nonzero with `authority store is already owned`.

Installed namespace isolation was proved against `/home/user/.codex`, `/home/user/.claude`, the Systray account runtime root, and `/home/user/.ssh`: the `overdeck-subrouter` UID inside the service mount namespace has neither read nor traversal access. A bounded metadata-only native-credential fingerprint covered 10 Claude/Codex credential files without opening their contents. Before and after an idempotent reinstall of this exact candidate the aggregate fingerprint was identical: `6b83f92dc34609ba4e8cf99e2842c462c680e40337a9ff300aa26a69a208ce9f`.

The installed dark-Systray proof used a temporary synthetic registry/config root and a short-lived synthetic exact route/grant. The real command-router authority seam resolved the installed loopback service, materialized only `gateway-manifest.json` and `config.toml`, and captured the native executable/argv/environment exactly equal to the native baseline. The synthetic authority route/grant and proof key were revoked/removed afterward; installed status returned to zero routes and zero grants.
