-
Notifications
You must be signed in to change notification settings - Fork 957
docs(devlog): remote hub design unit — audited roadmap for hub/client mode (stack base) #2771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
4f527e6
docs(devlog): remote hub mode — research, design draft, phased roadmap
83dd53c
docs(devlog): fold 5 audit blockers — drop header-only trusted-tailne…
66dcb29
docs(devlog): interview record — full-scope stacked delivery, dogfood…
a7d49f3
docs(devlog): interview round 3 — per-client key auto-issue via token…
3920954
docs(devlog): interview round 4 — usage renders from whichever store …
d225b2d
docs(devlog): remote hub decade docs 030-080 — diff-level roadmap for…
fca0a7b
docs(devlog): fold roadmap audit r1 — 10 blockers closed across 010-0…
ca1114f
docs(devlog): fold roadmap audit r2 — 7 blockers closed (synthesis in…
a938448
docs(devlog): fold roadmap audit r3 — pairing form mount chain + orph…
5eb58b7
fix(design): close five trust-boundary defects in the remote hub cont…
2da2d73
fix(design): finish removing insecure-http pairing across the unit
4080bdb
fix(design): close the review findings the first D-fix pass left open
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # 000 — Research: remote hub mode (evidence base) | ||
|
|
||
| Unit: 260827_remote_hub · Branch: codex/remote-hub-design · Status: research | ||
|
|
||
| ## Motivation (user request, 2026-08-27) | ||
|
|
||
| Run one ocx as a central HUB (Oracle VM / Mac mini / Docker — any machine), keep every | ||
| provider key, OAuth credential, and shared config there, and let other machines connect | ||
| with only a pointer + token ("ocx connect <tailnet-dns>"). The dashboard on a client | ||
| machine must still work at localhost:10100 with a two-plane split: shared pages operate | ||
| the hub, machine pages operate local file integration. Explicit constraint from the user: | ||
| today a Tailscale-bound GUI is unusable for some operations even WITH the admin token — | ||
| the design must fix remote GUI operability without collapsing the consent boundary. | ||
|
|
||
| ## In-repo evidence (verified 2026-08-27 on dev @ 8b1b65b8d) | ||
|
|
||
| - Non-loopback bind forces the data token: `isApiAuthRequired` returns true whenever the | ||
| bind hostname is not loopback (src/server/auth-cors.ts:260-262), and startup refuses a | ||
| public bind without a configured data credential. | ||
| - The remote-GUI limitation is a deliberate restriction, not an unreported weakness, and | ||
| it is already visible in shipped public code: `issueGuiSession` returns null when | ||
| `isApiAuthRequired(config)` is true and additionally requires a loopback Host | ||
| (src/server/management-auth.ts, `issueGuiSession`). The published dashboard guide | ||
| states the same boundary in user terms. | ||
|
|
||
| The consequence is a capability gap rather than an exposure: on a remote bind the | ||
| principal `gui-session` is unobtainable, so consent-bearing routes requiring | ||
| `ctx.principal === "gui-session"` (src/server/management/sidebar-routes.ts:42, | ||
| src/server/management/codex-prompt-routes.ts:298) answer 403 even to the admin token. | ||
| That 403 is correct and stays correct — the admin token must never be able to spend the | ||
| user's consent (AGENTS.md user-consent boundary). What is missing is any path for a | ||
| *browser* to mint a session remotely, which is what this unit designs. | ||
|
|
||
| Stated precisely: the current behavior fails closed. Nothing here describes a way to | ||
| obtain authority one should not have, so this note is a design rationale rather than | ||
| pre-disclosure material, and `AGENTS.md`'s scratch-space rule for unfixed defects does | ||
| not apply to it. Anything in this unit that WOULD describe an unfixed exploitable | ||
| weakness belongs in scratch space, not in `devlog/`. | ||
| - `managementRequestOrigin` returns null for a non-loopback Host when apiAuth is NOT | ||
| required (src/server/auth-cors.ts:118-129); when apiAuth IS required it derives the | ||
| origin from the request, which a TLS terminator breaks (http observed vs https public). | ||
| - The GUI attaches credentials only same-origin: `needsApiAuth` refuses absolute | ||
| cross-origin URLs (gui/src/api.ts:53-60). A two-plane GUI therefore needs an explicit | ||
| multi-target API layer, not a base-URL swap. | ||
| - The GUI needs a secure context in places: `crypto.subtle.digest` at | ||
| gui/src/log-conversation-id.ts:26, `navigator.clipboard` at | ||
| gui/src/oauth-health-display.ts:133 (with execCommand fallback). | ||
| - Injector already supports non-loopback targets: dedicated provider block with | ||
| `env_key = "OPENCODEX_API_AUTH_TOKEN"` and `model_catalog_json` requiring a LOCAL | ||
| absolute path (src/codex/inject.ts:186-247, 622+). | ||
| - `GET /api/catalog` and `GET /api/client-config` already exist behind management auth | ||
| (src/server/management/model-routes.ts:334-420). | ||
| - Headless OAuth exists: `oauthOpenBrowser: false` (src/oauth/open-browser-choice.ts) and | ||
| `POST /api/oauth/login/code` (src/server/management/oauth-account-routes.ts:208). | ||
| - Allowlist-listener precedent: the unauthenticated loopback listener enumerates exactly | ||
| the routes it serves (src/server/index.ts, loopbackRouteAllowed) — the machine-plane | ||
| listener should copy this failure mode (default-404). | ||
| - Token-file delivery precedent: `OCX_API_TOKEN_FILE` (src/lib/service-secrets.ts, | ||
| src/service.ts:1571+). | ||
| - CLI already talks to the management API over HTTP with injectable baseUrl | ||
| (src/cli/runtime-api.ts, RuntimeApiDeps.baseUrl) — client-mode remote management | ||
| commands are a URL + credential change, not a new client. | ||
|
|
||
| ## External evidence (Luna swarm, 3 lanes, sources opened 2026-08-27) | ||
|
|
||
| Peer proxies separate UI sessions from master keys: | ||
| - LiteLLM: LITELLM_MASTER_KEY for API/admin, separate UI login minting expiring | ||
| virtual keys; per-user/per-device virtual keys with budgets, central key custody. | ||
| https://docs.litellm.com.cn/docs/proxy/ui , virtual_keys.md / access_control.md in | ||
| BerriAI/litellm-docs (opened 2026-08-27). | ||
| - sub2api: admin web UI uses JWT session; automation uses a separate global Admin API | ||
| Key (x-api-key). https://github.com/Wei-Shaw/sub2api (opened 2026-08-27). | ||
| - One API broken-access-control reports (#2410, #2423) show central key custody makes | ||
| route-level authz the main defense. | ||
|
|
||
| Tailscale transport facts (official docs, verified dates in page footers): | ||
| - `tailscale serve` = tailnet-only reverse proxy to a localhost backend; injects | ||
| Tailscale-User-* identity headers; backend must bind loopback or headers are | ||
| spoofable. https://tailscale.com/docs/features/tailscale-serve | ||
| - `tailscale cert` issues public CA certs only for the ts.net FQDN (not bare MagicDNS | ||
| short names); names land in Certificate Transparency logs. | ||
| https://tailscale.com/docs/how-to/set-up-https-certificates | ||
| - Funnel is public-internet exposure (ports 443/8443/10000) — out of scope here. | ||
|
|
||
| Browser platform facts (MDN/WHATWG/IETF, opened 2026-08-27): | ||
| - Plain-HTTP non-localhost origins are NOT secure contexts: no crypto.subtle, no | ||
| async clipboard, Secure cookies unavailable. http://localhost IS potentially | ||
| trustworthy. https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts | ||
| - Header-token SPAs avoid ambient-cookie CSRF but still need exact-origin allowlists | ||
| and Origin checks on mutations (WHATWG Fetch; RFC 9700 OAuth BCP). | ||
| - RFC 8628 device flow is the reference pattern for headless-hub OAuth; ocx's | ||
| oauthOpenBrowser:false + /api/oauth/login/code is already equivalent in shape. | ||
|
|
||
| ## Design consequences (carried into 010) | ||
|
|
||
| 1. Two credential worlds stay separate: data-plane admission (client machines) vs | ||
| management (admin token / gui-session). Peers (LiteLLM, sub2api) validate this split. | ||
| 2. Remote GUI needs a NEW session-issuance path, not a weakening of requireManagementAuth: | ||
| the loopback-only refusal in issueGuiSession is the single gate to generalize. | ||
| 3. HTTPS via tailscale serve against a loopback-only management ingress is the | ||
| recommended browser path; plain-HTTP tailnet operation must exist as a documented | ||
| opt-in because usability on a private tailnet was the user's explicit complaint. | ||
| 4. localhost:10100 client GUI + direct-to-hub shared plane is cross-origin; the hub | ||
| needs management CORS for an allowlisted client origin, or the client listener | ||
| relays. Both appear in 010 with the relay constrained to a fixed target. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # 001 — Interview record (2026-08-28) | ||
|
|
||
| Answers captured from the maintainer (session 01a0439a, I-phase round 2): | ||
|
|
||
| - **Scope: ALL 6 phases, full implementation including hardening (P6).** Delivery as a | ||
| stacked PR chain grown from this branch (codex/remote-hub-design is the stack base; | ||
| each phase PR targets the previous head; retarget to dev as parents land — | ||
| DEV-STACK / enforce-target child rules). | ||
| - Q2 (plain-HTTP pairing): accepted — rung 4 ships in Phase 2 with rung 3. | ||
| - Q3 (per-client keys): recommendation accepted BUT see new usage requirement below, | ||
| which pulls toward auto-issuing per-client keys at connect. | ||
| - Q4 (URL split): accepted — separate managementUrl allowed, /readyz advertises it. | ||
| - Q5 (remote session TTL): accepted — renewable long-lived remote sessions. | ||
| - Q6 (hub local integration): accepted — hub does not inject locally by default. | ||
| - Q7 (Claude): launcher-scope first confirmed; maintainer notes it is machine-local | ||
| anyway — clean separation is the requirement, not persistent integration. | ||
| - Q8 (deployment): **dogfood on clisu-oracle as part of this work**, AND the protocol | ||
| must tolerate release-build peers: a released client against a dev-build hub (and | ||
| the reverse) must interoperate "어느정도" — i.e. protocol-version negotiation in | ||
| /readyz is a hard requirement, not polish (Phase 1 scope). | ||
| - **NEW requirement (usage attribution):** the client GUI usage page should reflect | ||
| "my machine's usage" while connected, and after `ocx disconnect` the GUI (back in | ||
| standalone mode) shows the local proxy's own usage again. Feasibility confirmed in | ||
| code: usage attempts already persist `apiKeyId` for configured-key admissions | ||
| (src/server/management/api-key-usage.ts:78-89, admissionFields in | ||
| src/server/auth-cors.ts:369-375), so a per-client filtered usage view is a query | ||
| over existing data — it requires the machine to authenticate with its OWN key, | ||
| which is why connect should default to per-client key issuance. | ||
|
|
||
| Open contradiction (to resolve this round): shared-token-allowed (Q3 answer) vs | ||
| per-machine usage view (new requirement) — attribution is keyed on apiKeyId, so a | ||
| shared token collapses all machines into one bucket. | ||
|
|
||
| ## Round 3 answers (2026-08-28) | ||
|
|
||
| - **Q-A = a (auto-issue per-client key at connect).** Storage question resolved in | ||
| code: the key is NEVER written to config.toml (env_key contract); it lands in the | ||
| existing owner-only token file (serviceApiTokenFilePath, src/lib/service-secrets.ts:5, | ||
| 0600 + ACL hardening) which the shim already reads into OPENCODEX_API_AUTH_TOKEN when | ||
| the env is empty (src/codex/shim.ts:699-701 unix, :1000-1001 batch, :1043 ps). | ||
| disconnect deletes the file. The shared-token-vs-attribution contradiction is CLOSED: | ||
| per-client keys are the connect default, so per-machine usage attribution works. | ||
| - **Q-C = a.** Protocol v1 negotiated via /readyz; same-major interop with | ||
| feature-detection; guaranteed pair = dev hub ↔ latest release client; older peers get | ||
| an explicit "hub protocol too new/old, upgrade ocx" error. Phase 1 hard requirement. | ||
| - **Q-B: OPEN ASSUMPTION (low)** — usage page default while connected = "this machine" | ||
| slice with a toggle to hub-wide; not answered explicitly, adopting the recommended | ||
| default; reversible in Phase 4 GUI work. | ||
|
|
||
| ## Final contradiction rescan (round 3) | ||
|
|
||
| - Shared-token vs attribution: RESOLVED (per-client default; shared token remains a | ||
| degraded documented mode where usage collapses into one bucket). | ||
| - Pairing-grant issuance vs POST /api/keys authority: connect needs admin-class | ||
| authority ONCE — satisfied by pairing code (rung 3/4) or admin token; neither is | ||
| persisted on the client. No contradiction. | ||
| - Dogfood release-compat vs stacked delivery: protocol version lives in Phase 1 (stack | ||
| base), so every later phase rides it. No ordering conflict. | ||
| - Remaining OPEN ASSUMPTIONS: Q-B default; session TTL exact value (12h sliding, | ||
| tunable); relay streaming backpressure deferred to Phase 6. | ||
|
|
||
| Interview readiness: Goal/Constraint/Success/Ontology all covered by asked-and-answered | ||
| rounds 1-3. Ready for I -> P. | ||
|
|
||
| ## Round 4 answer (2026-08-28) — usage rendering settled | ||
|
|
||
| Maintainer's rule, adopted verbatim as the design: **connected → render the hub's | ||
| usage (my apiKeyId slice); not connected → render the local usage.jsonl.** No local | ||
| mirroring of the connect-period usage (option b rejected as unnecessary complexity); | ||
| the connect-period history lives on the hub and is visible there. Grounding: | ||
| usage persists where the serving proxy runs (appendUsageEntry → | ||
| ~/.opencodex/usage.jsonl, src/usage/log.ts:166-167, 521-523), so this rule is just | ||
| "render the store that actually recorded the traffic" — zero data duplication, | ||
| no schema change. Q-B default (this-machine slice with hub-wide toggle) stands as | ||
| the connected view's default. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # 002 — Audit synthesis, roadmap round 1 (FAIL, 10 blockers) — canonical decisions | ||
|
|
||
| Reviewer: Volta (same reviewer retained for re-audit). Per-blocker disposition: | ||
|
|
||
| 1 [fold] Fixture repair: hub-too-new = hub{p:2,min:2} vs client p1; hub-too-old = client p2 requiring min2 vs hub{p:1,min:1}. Zero/malformed rows move to the malformed-input test class (400), not the mismatch class. | ||
| 2 [fold] Chain completion: 030's readyz metadata builder signature becomes build(config, req) from Phase 1; Phase 2's file map adds src/remote/protocol.ts + the /readyz handler as consumers of hub.managementPublicOrigin (config wins over observed origin when set). | ||
| 3 [fold] Pairing end-to-end: the relay (060) and the mgmt ingress (070) BOTH allow POST /opencodex-session (exchange) in addition to GET bootstrap; ocx gui pair prints a code bound to a caller-supplied browser origin (default http://localhost:10100); dogfood config (070) adds corsAllowOrigins:["http://localhost:10100"]. | ||
| 4 [fold] Plane mapping is per-CALL, not per-page: Startup/Integrations keep their existing /api/* calls on the shared plane; only new machine sections call /api/machine/*. 060 file map adds gui/src/pages/Startup.tsx, Integrations.tsx, ApiKeys.tsx, Grok.tsx (call-site routing), and drops the page-level table. | ||
| 5 [fold] Canonical names, propagated everywhere: routes = exactly 060's /api/machine/{status,clients,sync,shim,disconnect,hub-relay} with GET/POST /api/machine/shim (no PUT clients/:id — 010 updated); connect flags = --pairing-code-stdin | --admin-token-stdin (050 drops --credential-*; 010 drops --token-env/--token-stdin). | ||
| 6 [fold] Phase 6 owners renamed to the real creators: src/client/hub-client.ts, src/client/hub-relay.ts, tests/client-connect.test.ts; tests/remote-catalog.test.ts either created BY Phase 6 (listed as Add) or folded into client-connect tests — 080 names it as Add. | ||
| 7 [fold] /v1/catalog gains authenticated-only response header x-opencodex-key-id echoing the admitted key's id (030 IN-scope; never on unauthenticated paths); 080's rotation probe consumes it. | ||
| 8 [fold] Remove impossible self-invalidation: pairing grants are NOT key-bound; disconnect revokes nothing on the hub by itself — key deletion is an operator action (hub GUI / ocx connect revoke WITH admin credential). 080 reworded; 040 grant contract loses boundKeyId. | ||
| 9 [fold] 050: transient admin credential is retained in memory until the connect transaction commits or rolls back, then zeroized. | ||
| 10 [fold] 080 rotation names src/cli/connect.ts (parser) + tests; src/client/state.ts pendingOperation {kind:"rotate", newKeyIssuedAt, oldKeyBackupPath} full chain; rotation writes old key to <tokenfile>.prev (0600) until verified commit, then deletes — crash recovery documented. | ||
|
|
||
| No rebuttals; all 10 folded. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # 003 — Audit synthesis, roadmap round 2 (FAIL, 7 blockers) — canonical decisions | ||
|
|
||
| Closed in r2: old-2 (managementPublicOrigin chain), old-4 (per-call planes), old-6 (phase-6 owners). | ||
| Decisions for the 7 remaining (all fold, no rebuttals): | ||
|
|
||
| 1 P3-A4 fixture: rejection row uses hub {protocol:2, minimumClientProtocol:2}; a p2/min1 hub | ||
| is COMPATIBLE and gets its own acceptance row. "protocol major 2" wording deleted. | ||
| 2 Pairing e2e, single truth: ocx gui pair --origin <browser-origin> REQUIRED argument, no | ||
| default (040+070 updated; dogfood runbook passes --origin http://localhost:10100). | ||
| 060 gains a pairing UI owner row: gui/src/connect-pairing.ts + i18n keys + activation | ||
| scenario (paste code → POST exchange via relay → session stored). Relay contract states | ||
| it forwards the browser Origin header verbatim on POST /opencodex-session and 060 test | ||
| plan adds the exact-POST-route case. | ||
| 3 Canonical relay spelling everywhere: POST /api/machine/hub-relay/* (prefix + suffix); | ||
| 010:179 updated to the wildcard form. | ||
| 4 x-opencodex-key-id: configured-key admission ONLY (environment/loopback/none → header | ||
| absent); value re-validated header-safe as ^[A-Za-z0-9._-]{1,64}$ at emission (mismatch → | ||
| omit header, log once); emitted on 200 AND 304; response gains Cache-Control: private, | ||
| no-cache; tests cover absence for environment/loopback and no key-id in logs. privacy:scan | ||
| claim removed — runtime-header privacy is proven by the log-absence test instead. | ||
| 5 Post-disconnect revoke: hub GUI is the SOLE post-disconnect revocation path. ocx connect | ||
| revoke exists only while connected (state carries apiKeyId from issuance response — 050 | ||
| state gains apiKeyId field, full chain issuance→state→revoke→display); disconnect prompts | ||
| a reminder naming the hub GUI page. No tombstones. | ||
| 6 Zeroization wording: "release references and overwrite the coordinator's Uint8Array copy; | ||
| the immutable argv/stdin string copies are best-effort GC" — OneTimeConnectCredential.value | ||
| becomes Uint8Array (decoded once at read), display never renders it. | ||
| 7 Rotation chain completed: OcxClientConnectionConfig gains pendingOperation?: { kind: | ||
| "rotate"; rotationId: string; newKeyIssuedAt: string; oldKeyBackupPath: string } with | ||
| validation in the client-config reader; recovery on doubly-accepted = COMMIT the new key | ||
| (delete .prev + clear pendingOperation) because new-key acceptance proves issuance | ||
| completed; .prev writer assigned to src/lib/service-secrets.ts (existing owner) as | ||
| writeTokenBackup/restoreTokenBackup; 080 focused commands add tests/client-connect.test.ts | ||
| and tests/service-secrets.test.ts. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Mark the r1 pairing command as superseded.
Line [7] still gives
ocx gui paira default browser origin.devlog/_plan/260827_remote_hub/003_audit_r2_synthesis.md:8-13changes this to a required--originargument with no default. Update this historical record or label the r1 wording as superseded so implementers do not select conflicting CLI behavior.🤖 Prompt for AI Agents