# Gateway usage-limit parity handoff — 2026-08-23

## Goal
Restore the normal Systray Codex usage-limit UI (5-hour / 7-day percentages, reset times, and named/model-specific limits) for accounts migrated to the local Subrouter Gateway, without moving provider credentials back into Systray and without introducing a second account registry.

## Current lane
- Worktree: `/home/user/Projects/overdeck/.worktrees/gateway-usage-limits`
- Branch: `wt/gateway-usage-limits`
- Implementation commit: `6c01c59998f93c40e866f6ef9499e098d5d8f6bd` — `Restore Gateway usage limits`
- Branch base when created: `origin/main` at `d1b5d1f21fcc34ff7f7d5d281db6cc8d46206b38`
- Do **not** merge or push to `main` automatically; this handoff branch is intentionally separate.

## Live symptom / root cause
`multideal` is already Gateway-active and healthy, but its tray usage limits disappeared after migration.

Live reproduction before this fix:
- `systray-gateway status --tool codex --account multideal` => active / Gateway ready.
- `cdx --account multideal --status --json` => Gateway ready but 5-hour and 7-day `used_percentage` values were `null`.

Root cause:
- `ProviderServiceMap.for_account()` correctly swaps an active Gateway account away from the native credential reader.
- The active account then uses `AuthorityHealthAdapter`.
- `AuthorityHealthAdapter` previously returned only `gateway_health_snapshot()` (route/readiness information).
- That snapshot intentionally contains no provider usage windows, so the existing tray usage UI had nothing to render after credential custody moved to Subrouter.

This is a UI/data-source regression, not a failure of Gateway routing.

## Implemented architecture
### Subrouter authority-admin usage endpoint
The pinned Subrouter patch now exposes an authenticated loopback-only authority-admin `usage` operation for the exact Gateway owner label.

The service-side callback:
- supports Codex Gateway accounts in this implementation;
- resolves the authority-owned Codex account under the dedicated `overdeck-subrouter` service identity;
- uses existing `AccountRef.ResolvedAccount(...)` plus `AccountRef.FetchUsageWindowsCached(...)`;
- therefore preserves Subrouter's existing usage caching semantics rather than creating another independent polling loop;
- returns only sanitized usage metadata:
  - `checked_at`
  - `fresh`
  - bounded `windows[]`
  - window `name`
  - `used_percent`
  - `limit_window_seconds`
  - `reset_after_seconds`
  - optional `feature`
- does **not** serialize access tokens, refresh tokens, provider credentials, route proxy keys, or raw provider responses.

The response schema is provider-generic, but only Codex authority-account resolution is wired in this slice. Native Claude usage behavior is unchanged.

### Systray Gateway usage client
`modules/systray/authority_client.py` now provides `fetch_gateway_usage(...)`.

Security / routing properties:
- only active `AuthorityMode.SUBROUTER` bindings are eligible;
- only the exact local `subrouter-primary` endpoint/origin is allowed to use the local authority-admin token;
- the workstation admin token is read from the existing protected token file;
- the route proxy key is not sent to the usage endpoint;
- provider OAuth credentials remain exclusively inside Subrouter custody.

### Existing tray snapshot/UI reused
`AuthorityHealthAdapter` now combines:
1. existing exact Gateway route/readiness health; and
2. sanitized Gateway usage windows.

It maps account-wide windows back into the existing fields:
- primary / 5-hour => `AccountSnapshot.primary_used_pct`, `primary_reset_at`
- secondary / 7-day => `AccountSnapshot.secondary_used_pct`, `secondary_reset_at`

Additional feature/model windows are converted to existing `NamedLimit` rows.

No new usage UI was added; the fix intentionally restores the pre-existing Systray quota rendering path.

### Cache / stale behavior preserved
The original authority cache identity is retained and remains bound to:
- account tray key
- authority name
- resolved endpoint origin
- route ID
- provider
- grant-file identity (path/device/inode/size/mtime)

Therefore quota snapshots cannot bleed across route/grant rebinding.

If route health is UNKNOWN, the existing stale-route contract remains in force.
If route health is healthy but the usage read transiently fails, the previous quota snapshot for that exact binding is retained and the detail reports limits unavailable instead of blanking the UI.

## Important diagnostic cleanup already completed
A diagnostic attempt tested whether Codex `app-server` could be launched directly against the immutable Gateway home to obtain rate limits. That approach was rejected because Codex app-server writes state into `CODEX_HOME`, which contaminates the protected Gateway home.

The probe-created entries were removed from the live `multideal` Gateway home:
- `.cache`
- `.tmp`
- `installation_id`
- `skills`
- `sqlite`
- `tmp`

The Gateway home was revalidated and left with only:
- `config.toml`
- `gateway-manifest.json`

After cleanup, `cdx --account multideal --status --json` again passed Gateway home validation. Its percentages remain null until this branch is deployed, which is expected.

## Verification completed
### Pinned Subrouter patch
- Scratch patched upstream: `/home/user/Projects/.subrouter-gateway-admin-src`
- Pinned upstream revision: `29c7ebb306ac54739206f4752449e437047dd150`
- Regenerated repository patch: `modules/subrouter/patches/0001-overdeck-authority-safety.patch`
- `git diff --check`: clean before commit.
- Fresh upstream `git apply --check --unidiff-zero --whitespace=error-all`: PASS.

### Go / Subrouter
On `debian1`:
- targeted authority-admin usage/provision/grant tests: PASS;
- `go vet ./cmd/subrouter`: PASS.

The usage endpoint regression verifies:
- sanitized usage windows are returned;
- correct provider/account label reaches the callback;
- no admin token/provider token/proxy-key fields appear in the response.

### Systray
On `debian1`, under the protected buildbox test-root policy:
- `modules/systray/tests/test_authority_client.py`
- `modules/systray/tests/test_provider_services.py`
- **38 passed**.

Regression coverage includes:
- Gateway usage request via authenticated local admin endpoint;
- clear route proxy key absent from the usage request;
- numeric 5h/7d percentages mapped into existing account snapshot fields;
- reset times mapped correctly;
- model-specific windows retained as named limits;
- existing authority route-rebind cache isolation still passing;
- existing stale-cache behavior still passing.

### Official Subrouter candidate verifier
Against committed HEAD `6c01c59998f93c40e866f6ef9499e098d5d8f6bd`:
- `modules/subrouter/bin/verify-candidate focused`: **PASS**.

The user requested handoff immediately after this phase.
**`static`, `module`, and `full` have not been run yet for this commit.**
No sealed candidate from this commit has been installed.

## Live system state at handoff
The live workstation still runs the previously accepted Gateway implementation, not this usage-limit branch.

Known live state immediately before this lane:
- `multideal` is Gateway-active.
- Exact route health is `Gateway: ready`.
- Persistent local grant renewal was already implemented and proven.
- Workstation has one Subrouter on `127.0.0.1:31415`; no `31416` listener.
- `overdeck-vm` Subrouter was inactive/dead in the last completed acceptance.
- Provider credentials remain service-owned by Subrouter.
- Systray usage percentages for Gateway-bound `multideal` remain null until this branch is landed/deployed/installed.

Do not assume any new runtime deployment from `6c01c5999`; none was performed.

## Remaining work — exact order
1. Inspect this worktree, `origin/main`, live services, and candidate artifact state before doing anything.
2. Re-run/continue official candidate verification from committed HEAD `6c01c5999`:
   - `modules/subrouter/bin/verify-candidate static`
   - `modules/subrouter/bin/verify-candidate module`
   - `modules/subrouter/bin/verify-candidate full`
3. If a phase fails, diagnose and repair, update this handoff/plan, commit again, and rerun all required phases against the new exact HEAD.
4. After all four phases pass, read `verify-candidate path` and confirm receipt `base_commit` matches exact branch HEAD and all exits are zero.
5. Re-fetch `origin/main`; land only if ancestry is clean and the repository's landing requirements are satisfied.
6. Deploy canonical workstation source.
7. Install only the exact sealed candidate bound to the landed commit. Trusted upgrade must preserve live authority state.
8. Restart only Systray if required so it loads the new usage client.
9. Final live acceptance for `multideal`:
   - `systray-gateway status --tool codex --account multideal` remains active;
   - `cdx --account multideal --status --json` shows non-null 5h/7d percentages when upstream usage is available;
   - tray UI visibly shows usage-limit bars/percentages again;
   - route remains `Gateway: ready`;
   - authority counts remain stable (`1 credential / 1 route / 1 grant / 0 unresolved attempts`, unless owner has intentionally added more accounts);
   - no provider re-login is requested;
   - no provider credentials appear in Systray material;
   - one workstation Subrouter only, `127.0.0.1:31415`, no `31416`;
   - VM Subrouter remains inactive/dead.
10. Append final installed/live UI acceptance to the durable Gateway plan and handoff/progress record.

## Durable plan
Primary authority/design record:
`docs/plans/2026-08-22-workstation-ai-account-gateway.md`

The plan already contains the `Gateway account usage-limit parity — 2026-08-23` section with the rationale and pre-commit verification evidence.

## Do not do
- Do not read/copy provider OAuth tokens into Systray.
- Do not reintroduce `deck-sudo`, `runuser`, or `migration-provision` into the runtime migration/usage path.
- Do not run a second Subrouter on the VM.
- Do not run substantive tests on the workstation; use debian1/2/3.
- Do not use Codex app-server directly against the immutable Gateway home for usage polling.
- Do not blank the usage UI merely because account custody is Gateway-bound.
