Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
180 commits
Select commit Hold shift + click to select a range
3e0f99a
chore(release): move dev to 2.40.0 after the v2.39.0 release (#3127)
lidge-jun Sep 1, 2026
9c8bbbf
docs(devlog): record the v2.39.0 release train (#3126)
lidge-jun Sep 1, 2026
33d32b6
test(auth): pin the websocket refresh account to stop a cross-platfor…
lidge-jun Sep 1, 2026
6f415ba
fix(release): call the dev version bump instead of listening for an e…
lidge-jun Sep 1, 2026
132b557
docs(devlog): record the post-release automation repairs (#3130)
lidge-jun Sep 1, 2026
abcda8e
docs(devlog): record the 2026-08-31 non-priority-70 bug triage round …
lidge-jun Sep 1, 2026
0dc01cd
fix(openai): allow canonical fake-IP addresses on provider PATCH (#3133)
lidge-jun Sep 1, 2026
b14b741
fix(service): Windows cold-start budget and code-page-mangled schedul…
lidge-jun Sep 1, 2026
c8c8dc3
test(auth): close the startup-prime window that rotates the credentia…
lidge-jun Sep 1, 2026
b81c435
fix(minimax): align split-reasoning wire schema with the official rea…
lidge-jun Sep 1, 2026
58be3c5
test: probe for a free pid instead of assuming 4242 is dead (#3137)
lidge-jun Sep 1, 2026
15b0f70
docs(devlog): close merge train round 3 (#3140)
lidge-jun Sep 1, 2026
4086526
test(auth): seed the pool quota after the clock is pinned (#3147)
lidge-jun Sep 1, 2026
4f527e6
docs(devlog): remote hub mode — research, design draft, phased roadmap
Aug 27, 2026
83dd53c
docs(devlog): fold 5 audit blockers — drop header-only trusted-tailne…
Aug 27, 2026
66dcb29
docs(devlog): interview record — full-scope stacked delivery, dogfood…
Aug 27, 2026
a7d49f3
docs(devlog): interview round 3 — per-client key auto-issue via token…
Aug 27, 2026
3920954
docs(devlog): interview round 4 — usage renders from whichever store …
Aug 27, 2026
d225b2d
docs(devlog): remote hub decade docs 030-080 — diff-level roadmap for…
Aug 27, 2026
fca0a7b
docs(devlog): fold roadmap audit r1 — 10 blockers closed across 010-0…
Aug 27, 2026
d25cbc0
feat(remote): add protocol metadata and runtime role
Aug 27, 2026
ca1114f
docs(devlog): fold roadmap audit r2 — 7 blockers closed (synthesis in…
Aug 27, 2026
3d983d5
feat(remote): serve authenticated catalog snapshots
Aug 27, 2026
a938448
docs(devlog): fold roadmap audit r3 — pairing form mount chain + orph…
Aug 27, 2026
1bb9828
fix(remote): derive management origin from request host
Aug 27, 2026
5eb58b7
fix(design): close five trust-boundary defects in the remote hub cont…
Sep 1, 2026
825800e
test(remote): cover phase one protocol and catalog contract
Aug 27, 2026
2da2d73
fix(design): finish removing insecure-http pairing across the unit
Sep 1, 2026
8980430
fix(remote): type catalog bytes over ArrayBuffer and scope the key-id…
Aug 27, 2026
4080bdb
fix(design): close the review findings the first D-fix pass left open
Sep 1, 2026
4fdd54d
fix(remote): reconcile the phase-one catalog contract with the landed…
Sep 1, 2026
863a88e
feat(connect): persist fail-closed client ownership
Aug 27, 2026
6d9aed2
feat(connect): add durable remote routing target
Aug 27, 2026
91a4f6c
feat(connect): add remote hub CLI and sync
Aug 27, 2026
db95f8f
test(connect): cover transaction and client routing
Aug 27, 2026
9d46ca3
fix(connect): narrow client type boundaries
Aug 27, 2026
6fdaaf8
fix(connect): preserve catalog conditional fetch
Aug 27, 2026
abf0f81
feat(remote-gui): add remote session issuance and pairing
Aug 27, 2026
3d9184d
fix(connect): preserve sync wiring contract
Aug 27, 2026
a68e95c
fix(remote-gui): harden identity and capability replay checks
Aug 27, 2026
753f097
fix(connect): preserve malformed client state fail-closed
Aug 27, 2026
8906173
test(remote-gui): cover remote session consent boundaries
Aug 27, 2026
fd27c1e
test(connect): align audited phase three matrix
Aug 27, 2026
e6c6e78
fix(remote-gui): enforce exact bootstrap destination
Aug 27, 2026
4ae8fa4
fix(connect): reconcile client journal before lifecycle
Aug 27, 2026
5397019
test(remote-gui): lock replay and expiry negatives
Aug 27, 2026
886c26e
test(connect): cover sync and disconnect conflicts
Aug 27, 2026
bf382c0
fix(remote-gui): preserve renewal and mutation origin checks
Aug 27, 2026
acdac3f
fix(connect): a process-owned journal is ours to unwind, not a conflict
Sep 1, 2026
dbec900
fix(remote-gui): drop plaintext pairing and bound the unauthenticated…
Sep 1, 2026
af95d59
fix(connect): preserve the sync handler exit code on the connected br…
Sep 1, 2026
dad1712
test(remote-gui): stub the prompt happy-dom does not implement
Sep 1, 2026
20f3c11
fix(connect): restore the catalog the user had, and stop calling a st…
Sep 1, 2026
1d99dd5
feat(two-plane): filter hub usage by client key
Aug 27, 2026
872b945
feat(two-plane): add client machine and hub GUI planes
Aug 27, 2026
90c7e8f
test(two-plane): cover machine relay and GUI routing
Aug 27, 2026
fcd79c4
feat(deploy): add loopback hub management ingress
Aug 27, 2026
ccf319c
test(two-plane): align shared-base shell assertions
Aug 27, 2026
6999656
test(deploy): cover hub management ingress boundaries
Aug 27, 2026
4f27ee8
feat(two-plane): harden relay and offline target states
Aug 27, 2026
a8fdcf6
feat(deploy): harden management ingress allowlist
Aug 27, 2026
e3f6cf8
fix(two-plane): repair remote verification failures
Aug 27, 2026
7d8b878
docs(deploy): add remote hub deployment guide
Aug 27, 2026
a3fe759
fix(two-plane): unblock client start and GUI verification
Aug 27, 2026
5cdef72
fix(client): a hub without client state is disconnected, not mismatch…
Aug 27, 2026
61710ef
docs(devlog): phase-4 gui screenshot evidence
Aug 27, 2026
bf221bc
fix(hub): the hub role never rewrites its host client configs on star…
Aug 27, 2026
fedbe0a
fix(two-plane): declare the machine plane, enable relay, and finish t…
Sep 1, 2026
0a73858
fix(connect): seed default config on a fresh machine instead of refus…
Aug 27, 2026
16ddd3e
fix(two-plane): authenticate the relayed pairing exchange and isolate…
Sep 1, 2026
7bf2823
docs(devlog): clisu-oracle dogfood record — full connect lifecycle pr…
Aug 27, 2026
95639f0
fix(two-plane): a standalone install neither probes nor announces the…
Sep 1, 2026
76257cc
test(deploy): assert the ingress role rule where the message is actua…
Sep 1, 2026
25c784f
feat(hardening): add recoverable data key rotation API
Aug 27, 2026
b829320
feat(hardening): bound pairing attempts and add session logout
Aug 27, 2026
10a3198
feat(hardening): validate remote protocol catalog and relay
Aug 27, 2026
2ea6313
feat(hardening): recover client key rotation through token backup
Aug 27, 2026
b12093b
feat(hardening): add api key rotation controls
Aug 27, 2026
be2ee7d
test(hardening): cover rotation pairing catalog and relay negatives
Aug 27, 2026
913d2a5
docs(i18n): synchronize remote hub hardening across locales
Aug 27, 2026
88d9889
fix(hardening): gate startup on rotation recovery state
Aug 27, 2026
9bded9c
feat(hardening): expose remote session logout control
Aug 27, 2026
ad7acdb
fix(hardening): keep one-time rotation secret visible
Aug 27, 2026
c693700
fix(hardening): reject mismatched catalog validators
Aug 27, 2026
e5bca8d
test(hardening): cover subprocess protocol skew matrix
Aug 27, 2026
bd0064d
test(hardening): reject rotation secrets and revoke ids in argv
Aug 27, 2026
9088d60
fix(hardening): bound remote session logout request
Aug 27, 2026
54d799c
test(hardening): keep rotation evidence secret-free
Aug 27, 2026
c22792e
fix(hardening): drop a stray import fragment and guard an undefined r…
Aug 27, 2026
5da9f17
fix(hardening): repair phase six full-suite regressions
Aug 27, 2026
6a71a41
fix(hardening): confirm the abort before rewinding, and never delete …
Sep 1, 2026
0e443d8
fix(hardening): declare the four routes this phase adds
Sep 1, 2026
dd8c3e6
feat(cli): declare ocx connect rotate as a capability
Sep 1, 2026
efefe36
fix(hardening): keep the remaining two-plane UI off a standalone install
Sep 1, 2026
278fd61
Merge pull request #2771 from lidge-jun/codex/remote-hub-design
lidge-jun Sep 1, 2026
87459f8
Merge pull request #2772 from lidge-jun/codex/remote-hub-p1
lidge-jun Sep 1, 2026
39e5aef
Merge pull request #2776 from lidge-jun/codex/remote-hub-p2
lidge-jun Sep 1, 2026
fd8b6b8
Merge pull request #2777 from lidge-jun/codex/remote-hub-p3
lidge-jun Sep 1, 2026
163feb6
Merge pull request #2781 from lidge-jun/codex/remote-hub-p4
lidge-jun Sep 1, 2026
6d732d3
Merge pull request #2786 from lidge-jun/codex/remote-hub-p5
lidge-jun Sep 1, 2026
9232df0
Merge pull request #2789 from lidge-jun/codex/remote-hub-p6
lidge-jun Sep 1, 2026
3275b5a
docs(devlog): remote hub restack planning unit (#3149)
lidge-jun Sep 1, 2026
b27bab0
docs(devlog): close out the remote hub merge train (#3159)
lidge-jun Sep 1, 2026
c692831
docs(devlog): record the objective closeout and worktree realignment …
lidge-jun Sep 1, 2026
75d9ada
docs(devlog): verify the four review fixes reached dev (#3161)
lidge-jun Sep 1, 2026
7ac4c9f
docs(devlog): close the remote hub restack unit (#3162)
lidge-jun Sep 1, 2026
2f2629f
docs(skill): teach ocx the remote hub roles, pairing boundary, and di…
lidge-jun Sep 1, 2026
e236c36
fix(catalog): read Copilot context window limits (#3163)
Ingwannu Sep 1, 2026
75090d4
fix(codex): preserve request-owned main pins (#3166)
Ingwannu Sep 1, 2026
e40245e
fix(client): tell the dashboard it is a client (#3169)
lidge-jun Sep 1, 2026
e92aa33
fix(cli): route ocx models new-policy and new-arrivals to the runtime…
lidge-jun Sep 1, 2026
7386b52
fix(combo): apply the combo default effort the target can actually re…
lidge-jun Sep 1, 2026
0d8147c
docs(reference): document the readyz protocol fields and the remote-h…
lidge-jun Sep 1, 2026
22a643a
test(combo): update the failover e2e assertion the effort fix invalid…
lidge-jun Sep 1, 2026
e582aee
fix(gui): stop the mobile topbar and integration cards overflowing (#…
lidge-jun Sep 1, 2026
2e2da87
fix(codex): rotate accounts on wrapped quota failures (#3176)
Vadevious Sep 1, 2026
0d6424f
fix(responses): surface provider 413 as terminal context overflow (#3…
Ingwannu Sep 1, 2026
51c4917
fix(export): preserve Hermes vision capabilities (#3178)
lidge-jun Sep 1, 2026
eceb02d
fix(remote): restore authenticated GUI health (#3179)
lidge-jun Sep 1, 2026
634d9e5
fix(codex): retry caller main after pool rejection (carry of #3135) (…
lidge-jun Sep 1, 2026
865a36e
fix(claude): keep proxy admission keys out of subscription launches (…
lidge-jun Sep 1, 2026
3b9cddf
docs(devlog): close the multiplatform QA and GUI unit (#3181)
lidge-jun Sep 1, 2026
fecb77a
fix(codex): serialize native-main refresh on the CODEX_HOME claim (re…
lidge-jun Sep 1, 2026
afd5b46
fix(compact): route combo compact requests through the failover path …
lidge-jun Sep 1, 2026
fe766e1
fix(codex): throttle repeated failed pool quota primes (rebase of #30…
lidge-jun Sep 1, 2026
ea29e25
fix(service): report the wait that was actually spent, not the budget…
lidge-jun Sep 1, 2026
d335570
fix duplicate Codex restore after graceful stop (#3187)
lidge-jun Sep 1, 2026
5ccf7c8
fix(cli): let an explicit different --port start a sibling (rebase of…
lidge-jun Sep 1, 2026
5557772
fix(openai): exclude user-owned alias overlays from canonical seed va…
lidge-jun Sep 1, 2026
c870714
fix(usage): price aggregator models spelled vendor/model (#3194)
lidge-jun Sep 1, 2026
f3bcc67
fix(responses): strip ChatGPT citation control markers before the cli…
lidge-jun Sep 1, 2026
4be4326
docs(devlog): drop remote home-path citations the privacy scanner fla…
lidge-jun Sep 1, 2026
ef6a163
fix(capacity): count uncalibrated Codex plans instead of excluding th…
lidge-jun Sep 1, 2026
88c4275
feat(combo): adapt reasoning effort to target capabilities (#2731) (#…
lidge-jun Sep 1, 2026
c17bc94
fix(codex): verify auth.json identity, not just content, before publi…
lidge-jun Sep 1, 2026
52d9416
feat(responses): opt-in ceiling for oversized outbound passthrough bo…
lidge-jun Sep 1, 2026
c7f3f6f
feat(router): summarize compaction on the default provider when no ca…
lidge-jun Sep 1, 2026
59449fa
docs(codex): explain why routed models are gated during reserve mode …
lidge-jun Sep 1, 2026
55400ef
feat(anthropic): add claude-fable-5-1 with official Fable 5.1 pricing…
lidge-jun Sep 1, 2026
fcf0da2
test(quota): move the malformed-plan pool test onto the #3198 contrac…
olddonkey Sep 1, 2026
53c09a2
fix(server): allow POST /v1/alpha/search on the loopback listener (#3…
lidge-jun Sep 1, 2026
6a6efa9
feat(catalog): per-provider retainModels opt-in (#1690, carries #2860…
lidge-jun Sep 1, 2026
0d73d65
feat(images): relay Codex image_gen to xAI Imagine with Grok OAuth (c…
lidge-jun Sep 1, 2026
4a382be
feat(codex): opt-in authless Codex Desktop routing mode (#1107) (#3207)
lidge-jun Sep 1, 2026
e1eb8c5
docs(cli): explain shim-free Codex token injection boundaries (#2713)…
lidge-jun Sep 1, 2026
5bc6939
feat(proxy): startup Windows system-proxy discovery behind proxy "aut…
lidge-jun Sep 1, 2026
941cb45
feat(providers): opt-in OS keychain storage for provider API keys (#1…
lidge-jun Sep 1, 2026
5fc7d07
feat(cursor): seed claude-fable-5-1 at 1M ahead of the Cursor lineup …
lidge-jun Sep 1, 2026
d975fea
feat(catalog): durable display names for discovered models (#2201, ca…
lidge-jun Sep 1, 2026
ef7b3c9
feat(quota): per-account Gem/Cla quota for Google Antigravity (#1082)…
lidge-jun Sep 1, 2026
356e495
feat(oauth): generic pool-settings contract for OAuth providers (#695…
lidge-jun Sep 1, 2026
f84dbf9
fix(ci): declare the provider keychain capability and refresh the Cur…
lidge-jun Sep 1, 2026
6fe4631
feat(responses): opt-in upstream Responses WebSocket transport (#2816…
lidge-jun Sep 1, 2026
519bba7
feat(codex): opt-in reset-credit auto-redemption before expiry (#822)…
lidge-jun Sep 1, 2026
85f7ef9
devlog: wp14 carry record and wp15 disposition (#3221)
lidge-jun Sep 1, 2026
d23eab4
fix(responses): keep the reserved functions group intact for codex-sp…
lidge-jun Sep 1, 2026
b732b0d
fix(responses): scope the self-named namespace scrub to declared bare…
lidge-jun Sep 2, 2026
261b7e0
fix(cli): stop a sibling start from persisting its port into config.p…
lidge-jun Sep 2, 2026
86dee69
docs(readme): stack the four demo gifs one per row so they stop getti…
lidge-jun Sep 2, 2026
1c8278b
fix: fail over zero-output incomplete combo streams (#3236)
lidge-jun Sep 2, 2026
98444a5
docs(readme): shrink the stacked demo gifs to 560px (#3237)
lidge-jun Sep 2, 2026
e4d4815
docs(readme): orca-style feature rows — caption left, gif right (#3238)
lidge-jun Sep 2, 2026
744d12d
fix(subagents): auto-fallback encrypted V2 spawns to native Codex wit…
lidge-jun Sep 2, 2026
7f00d0e
fix(subagents): let encrypted-task recovery run before the synthesize…
lidge-jun Sep 2, 2026
b54508c
fix(agents): allow Codexless V2 task recovery (#3241)
lidge-jun Sep 2, 2026
2cb5921
revert(subagents): drop the synthesized native chain for encrypted sp…
lidge-jun Sep 2, 2026
7aa64bb
feat(cursor): derive the picker seed from the capability table and la…
lidge-jun Sep 2, 2026
f607233
feat(server): advertise api_types and capabilities on the raw /v1/mod…
lidge-jun Sep 2, 2026
72a7c4a
docs: Cursor Private Inference connector guide (#3231)
lidge-jun Sep 2, 2026
83838e7
feat(cursor): let the Codex Fast toggle reach Cursor's fast variant (…
lidge-jun Sep 2, 2026
8a7d003
docs(devlog): bug-label drawdown campaign record and main->dev regres…
lidge-jun Sep 2, 2026
8d2dd66
feat(cursor): expose -fast identities to clients without a Fast toggl…
lidge-jun Sep 2, 2026
21416a7
docs(cursor): record the stack landing SHAs and close residual R5 (#3…
lidge-jun Sep 2, 2026
8fb4e6e
docs(cursor): closeout verification on the landed dev (#3244)
lidge-jun Sep 2, 2026
fd5ccab
feat(cursor): read-only Private Inference status route for the dashbo…
lidge-jun Sep 2, 2026
879d748
feat(gui): Cursor integration tab, overview card, DSH label (#3248)
lidge-jun Sep 2, 2026
7d25f99
docs(cursor): dashboard section and read-only surface note (#3249)
lidge-jun Sep 2, 2026
19b0157
fix(windows): server.stop drains the config-dir ACL flight; retrying …
lidge-jun Sep 2, 2026
a6ee24f
fix(windows): writable fsync handles, hermetic ACL test lanes, teardo…
lidge-jun Sep 2, 2026
84b48d6
merge dev into preview for v2.40.0-preview.20260902
Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
33 changes: 25 additions & 8 deletions .github/workflows/dev-version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,35 @@ name: Dev version bump
# sign-off that a bot cannot supply. Until that merge the red persists. This converts a
# forgotten chore into a queued, reviewable change - not into an automatic repair.
#
# A `release` event resolves this workflow file from the repository DEFAULT branch
# (`main`), not from `dev` - the same trap documented in cleanup-closed-pr-branches.yml.
# So merging this file to `dev` installs it but arms nothing; it first fires after an
# ordinary dev -> main promotion carries it there.
# WHY THIS IS CALLED, NOT TRIGGERED. It used to listen for `release: published`, and in
# that form it ran ZERO times across v2.37.0, v2.38.0 and v2.39.0 - every one of those
# bumps was still opened by hand (#3045, #3076, #3127). The workflow was not broken; the
# event never existed. `release.yml` creates the GitHub release with
# `GH_TOKEN: ${{ github.token }}`, and GitHub does not start workflow runs from events
# raised by the default `GITHUB_TOKEN`. A `release: published` listener therefore cannot
# observe a release this repository publishes itself, no matter which branch it sits on.
#
# The fix keeps the credential surface unchanged: no PAT, no app token, no
# `contents: write` on the release job. `release.yml` CALLS this workflow directly after
# a successful publish, so the run is a child of the release run instead of a reaction to
# an event that is never delivered.
#
# A `workflow_call` body resolves from the CALLER's ref, and `release.yml` only ever runs
# on `main` or `preview` (its own branch gate). So this file must be on `main` to take
# effect - the same promotion requirement the old comment described, now for a different
# reason.
#
# There is deliberately no `workflow_dispatch`: a branch-selected manual run executes
# THAT branch body with `contents: write`. Re-drive a missed run by running
# `bun scripts/bump-dev-version.ts <released> package.json` locally and opening the pull
# request normally.
on:
release:
types: [published]
workflow_call:
inputs:
released-version:
description: "The tag that just published, e.g. v2.39.0"
required: true
type: string

permissions: {}

Expand Down Expand Up @@ -69,7 +86,7 @@ jobs:
- name: Decide the version dev should carry
id: decide
env:
RELEASED_VERSION: ${{ github.event.release.tag_name }}
RELEASED_VERSION: ${{ inputs.released-version }}
run: |
set -euo pipefail
bun scripts/bump-dev-version.ts "${RELEASED_VERSION}" package.json
Expand All @@ -88,7 +105,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
NEXT_VERSION: ${{ steps.decide.outputs.version }}
RELEASED_VERSION: ${{ github.event.release.tag_name }}
RELEASED_VERSION: ${{ inputs.released-version }}
run: |
set -euo pipefail

Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,41 @@ concurrency:
cancel-in-progress: false

jobs:
# Move `dev` past the version that just published.
#
# This is a CALL, not a `release: published` listener. The release is created with
# `github.token`, and GitHub does not start workflow runs from events that token
# raises - so a listener cannot observe a release this repository publishes itself. In
# that form it ran ZERO times across v2.37.0, v2.38.0 and v2.39.0 while every one of
# those bumps was opened by hand (#3045, #3076, #3127).
#
# `needs: publish` means this is skipped unless the publish job succeeded, so a failed
# publish or a failed release creation never opens a bump pull request; the explicit
# condition only adds the dry-run case. The called workflow declares its own
# `contents: write` / `pull-requests: write` for its own job, so nothing here gains
# write access.
#
# Both channels call this, and the double-call is safe because `bump-dev-version.ts`
# compares against what `dev` already carries. In the usual train `dev` is already at
# the stable core when the preview publishes, so that call returns `changed=false`
# ("dev already carries 2.40.0, which is ahead of the published 2.40.0-preview.*") and
# every later step is gated on that output. The stable call returns `changed=true` and
# opens the one pull request. A preview publishing while `dev` is genuinely behind
# still bumps it, which is the point.
#
# It is declared FIRST in this file, ahead of the jobs it depends on, because
# tests/ci-workflows.test.ts splits the workflow on `- name:` and reads each `run:`
# block to the start of the next one when it checks that dispatch inputs never
# interpolate into shell source. A job declared between two steps lands inside that
# window and reads as shell. Job order in YAML carries no execution meaning - `needs`
# does - so declaring it before its own dependency costs nothing.
bump-dev-version:
needs: publish
if: ${{ inputs.dry-run != true }}
uses: ./.github/workflows/dev-version-bump.yml
with:
released-version: v${{ inputs.version }}

validate-dispatch:
runs-on: ubuntu-latest
permissions:
Expand Down
70 changes: 49 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,55 @@ npm install -g @bitkyc08/opencodex
ocx start # proxy + dashboard on localhost:10100
```

<table align="center">
<tr>
<td width="50%" align="center">
<img src="assets/claude-code-models.gif" alt="Claude Code running a routed model through opencodex — the status bar shows gpt-5.6-luna-medium as the active model" width="410"><br>
<sub><b>Claude Code, running any model.</b><br>The picker is stock Claude Code. The brain behind it isn't.</sub>
</td>
<td width="50%" align="center">
<img src="https://raw.githubusercontent.com/lidge-jun/opencodex/main/assets/demo.gif" alt="opencodex demo — running a task in the Codex app on a routed non-OpenAI model" width="410"><br>
<sub><b>Codex, running any model.</b><br>Pick a provider and go — same workflow, different brain.</sub>
</td>
</tr>
<tr>
<td width="50%" align="center">
<img src="https://raw.githubusercontent.com/lidge-jun/opencodex/main/assets/claude-desktop-subagent.gif" alt="Claude Desktop answering as Claude Opus 4.8, then dispatching a GPT-5.6 Sol subagent through opencodex" width="410"><br>
<sub><b>Claude Desktop, running any model.</b><br>Opus answers, then hands the task to a GPT-5.6 Sol subagent.</sub>
</td>
<td width="50%" align="center">
<img src="https://raw.githubusercontent.com/lidge-jun/opencodex/main/assets/grok-build-subagent.gif" alt="Grok Build running GPT-5.6 Sol through opencodex and calling a Kimi K3 subagent" width="410"><br>
<sub><b>Grok Build, running any model.</b><br>Sol drives the session and calls a Kimi K3 subagent.</sub>
</td>
</tr>
<table>
<tr>
<td width="50%" valign="middle">

### Claude Code, running any model

The picker is stock Claude Code. The brain behind it isn't.

</td>
<td width="50%">
<img src="assets/claude-code-models.gif" alt="Claude Code running a routed model through opencodex — the status bar shows gpt-5.6-luna-medium as the active model" width="100%">
</td>
</tr>
<tr>
<td width="50%" valign="middle">

### Codex, running any model

Pick a provider and go — same workflow, different brain.

</td>
<td width="50%">
<img src="https://raw.githubusercontent.com/lidge-jun/opencodex/main/assets/demo.gif" alt="opencodex demo — running a task in the Codex app on a routed non-OpenAI model" width="100%">
</td>
</tr>
<tr>
<td width="50%" valign="middle">

### Claude Desktop, running any model

Opus answers, then hands the task to a GPT-5.6 Sol subagent.

</td>
<td width="50%">
<img src="https://raw.githubusercontent.com/lidge-jun/opencodex/main/assets/claude-desktop-subagent.gif" alt="Claude Desktop answering as Claude Opus 4.8, then dispatching a GPT-5.6 Sol subagent through opencodex" width="100%">
</td>
</tr>
<tr>
<td width="50%" valign="middle">

### Grok Build, running any model

Sol drives the session and calls a Kimi K3 subagent.

</td>
<td width="50%">
<img src="https://raw.githubusercontent.com/lidge-jun/opencodex/main/assets/grok-build-subagent.gif" alt="Grok Build running GPT-5.6 Sol through opencodex and calling a Kimi K3 subagent" width="100%">
</td>
</tr>
</table>

<p align="center">
Expand Down
105 changes: 105 additions & 0 deletions devlog/_plan/260827_remote_hub/000_research.md
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.
75 changes: 75 additions & 0 deletions devlog/_plan/260827_remote_hub/001_interview.md
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.
Loading
Loading