Skip to content

fix(server): relay the Remote Control bridge with the client's own credential - #499

Merged
MagicalTux merged 2 commits into
KarpelesLab:masterfrom
ianberdin:fix/remote-control-bridge-client-credential
Oct 3, 2026
Merged

MagicalTux merged 2 commits into
KarpelesLab:masterfrom
ianberdin:fix/remote-control-bridge-client-credential

Conversation

@ianberdin

Copy link
Copy Markdown
Contributor

Claude Code 2.1.287: "Fixed claude remote-control failing to register behind an HTTP proxy" (anthropics/claude-code#97352). With HTTPS_PROXY pointing at TeamClaude, the bridge's control calls now reach the request listener. CLIENT_CREDENTIAL_PATHS relays only /v1/code/ and /api/oauth/ with the client's own credential, so everything else the bridge sends gets a rotated fleet token. A daemon cannot opt out per process: settings.json env is applied over the process environment, and remote-control refuses a root --settings.

Symptom - one machine running claude remote-control under systemd, in the 4h after the update:

  • POST /v1/environments/bridge registered the machine under whichever account rotation picked: five organization_uuid values in round-robin order over ~22 restarts. The login's own org appeared only when rotation landed on it.
  • Creating the session then answered 404 Not found.
  • The work poll drew a 401 from every account in turn. Its bearer is the environment secret that registration returned, not an OAuth token, so a fleet token in its place can only be refused; ack and heartbeat carry the work's session-ingress token the same way. TeamClaude logged 1286 "token rejected" lines in those 4h, against 31 in the 24h before.
  • The daemon dropped ~5s after "Ready", retried for ~11 min, exited, and was restarted. The phone could not reach the machine.

Change - add /v1/environments/, /v1/sessions/, /v2/session_ingress/ and /v2/ccr-sessions/ to CLIENT_CREDENTIAL_PATHS, the whole prefix as with /api/oauth/ in #245. The paths are from the 2.1.287 bundle: /v1/environments/bridge, /bridge/{id}/offline, /{id}/bridge/reconnect, /{id}/work/poll, /{id}/work/{work}/ack|heartbeat|stop; POST /v1/sessions, /v1/sessions/{id}, /events; the session MCP endpoints /v2/session_ingress/shttp/mcp/{id} and /v2/ccr-sessions/-/meta/mcp.

  • Each entry ends in /, so it stops at a segment boundary (/v1/sessionsX still rotates). The entry without that slash, the collection itself, matches too: a session is created with a bare POST /v1/sessions, and classificationPath() has already dropped the ?beta=true. The rule moves into isClientCredentialPath(). It also takes the bare /v1/code and /api/oauth, which no client sends.
  • WebSocket upgrades (/v2/session_ingress/mcp/ws/{id}) already keep the client's headers through relayUpgrade; this covers the plain requests.
  • As with /api/oauth/* since fix(server): relay all /api/oauth/* with the client's own credential #245, a client with no login of its own now gets a 401 on these paths instead of a rotated token. The two /v1 prefixes also serve the public Managed Agents API (managed-agents-2026-04-01). Its environments and sessions belong to the organization that created them, so rotation could not serve them across a multi-org pool anyway.
  • fix(server): relay /api/frame/* with the client's own credential #498 edits the same CLIENT_CREDENTIAL_PATHS line. Whichever lands second rebases: keep both comments and the union of entries. /api/frame/ works unchanged under the new predicate.

Tested - npm test: 2592 pass, 0 fail, 5 skipped (as on master). 15 new tests in test/remote-control-bridge-relay.test.js:

  • 13 assert that a bridge path keeps the client's credential, its path and its body, with an account manager that throws if consulted. All 13 fail on master.
  • 2 assert that /v1/sessionsX and /v1/environmentsX/bridge still rotate. Both fail if the entries are spelled without the trailing slash.

Inference rotating is already covered in test/oauth-client-credential.test.js. npm run lint, npm run typecheck and the strict ratchet (1703, unchanged) are clean on Node 22.12.0.

Live, the same prefixes as a hotfix on 1.1.22 on the affected machine (spelled there as plain /v1/environments and /v1/sessions): registration landed in the login's own org while the proxy's active account was a different one. Reconnect, poll, ack and heartbeat all answered 200, with no 401s.

🤖 Generated with Claude Code

…edential

Claude Code 2.1.287 fixed `claude remote-control` failing to register
behind an HTTP proxy (anthropics/claude-code#97352). With HTTPS_PROXY set
to this proxy, the bridge's control calls now reach the request listener,
where only /v1/code/ and /api/oauth/ kept the client's credential. The
rest went out with a rotated fleet token. Observed on a live machine:

- POST /v1/environments/bridge registered the machine under whichever
  account rotation picked: five organization ids, in round-robin order,
  over ~22 restarts in 4h.
- Creating the session then answered 404.
- The work poll drew a 401 from every account in turn. Its bearer is the
  environment secret that registration returned, not an OAuth token, so a
  fleet token in its place can only be refused; ack and heartbeat carry
  the work's session-ingress token the same way. The proxy logged 1286
  "token rejected" lines in the 4h after the update, against 31 in the
  24h before.
- The daemon dropped ~5s after "Ready", retried for ~11 min and exited.
  The phone could not reach the machine.

Add /v1/environments/, /v1/sessions/, /v2/session_ingress/ and
/v2/ccr-sessions/ to CLIENT_CREDENTIAL_PATHS, the whole prefix as with
/api/oauth/ in KarpelesLab#245. An entry ends in '/', so it stops at a segment
boundary (/v1/sessionsX still rotates), and the entry without that slash,
the collection itself, matches too: a session is created with a bare
POST /v1/sessions, and the classified path carries no query string. The
rule moves into isClientCredentialPath; it also takes the bare /v1/code
and /api/oauth, which no client sends.

The two /v1 prefixes also serve the public Managed Agents API. Its
environments and sessions belong to the organization that created them,
so rotation could not serve them across a multi-org pool; a client that
calls it through the proxy now sends its own key, as on /api/oauth/*.
@MagicalTux
MagicalTux merged commit 15b33ff into KarpelesLab:master Oct 3, 2026
5 checks passed
MagicalTux added a commit that referenced this pull request Oct 3, 2026
test/server-warmup-schedule.test.js carried its own bind-a-port-then-spawn
harness, the shape #486 replaced everywhere else: it lost the port race to a
neighbouring test on a loaded runner ("Port 39521 is already in use", node 20
on #499's run). It now uses test-helpers/spawn-server.js, which verifies the
port by the child's pid and respawns on a lost race, and the one-second
AbortSignal on the post-reload quota fetch is gone — the runner's timeout is
the bound for a wedged endpoint.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@MagicalTux MagicalTux mentioned this pull request Oct 4, 2026
MagicalTux added a commit that referenced this pull request Oct 4, 2026
Fifteen commits since 1.1.22. Two change what an existing install does
without an opt-in (#514, #503); the rest is additive or display.

Behaviour changes
  #514 the TUI quota-bar percentage (`quotaBarPercent`) is off unless set:
       a bar carries its countdown, and its fill is the percentage. The
       switch (g → Bar percentage) is unchanged; a config without the key
       now reads as off
  #503 with Codex accounts in the pool, a request on the intercepted
       chatgpt.com that is not Codex inference (codex-cli 0.156's workspace
       discovery, plugin, MCP and settings calls) is passed through to
       chatgpt.com with the client's own login instead of reaching the
       Anthropic pool and a 404; the Codex Responses WebSocket is refused
       (501) so the CLI falls back to HTTPS, where the pool serves it (#492)
  #502 `login --api` adds a key at priority 100, a last resort behind the
       subscriptions, with `--priority <n>` to place it; existing entries
       are untouched (#497)

Fixes
  #498 artifacts (`/api/frame/*`) are relayed with the client's own
       credential, so publishing and reading them works behind a rotated pool
  #499 the Remote Control bridge (`/v1/environments/*`, `/v1/sessions/*`,
       `/v2/session_ingress/*`, `/v2/ccr-sessions/*`), which Claude Code
       2.1.287 sends through HTTPS_PROXY, is relayed with the client's own
       credential; registration no longer lands in a rotated account's org
  #488 keep-warm works on Windows: the warm-up client is spawned through a
       shell, so npm's `claude.cmd` shim is found
  #489 Node 24.6's undefined HTTP/2 keep-alive buffer no longer crashes the
       MITM tunnel with a NaN timeout
  #490 on a very wide terminal the two provider panes sit together and their
       bars grow to the list's cap instead of padding half the screen

Features
  #496 `accountSort` orders the TUI account list by the soonest reset of a
       window (session, weekly, S7, F7) inside each provider group; cycled
       from the settings screen, display only
  #494 a z.ai GLM Coding Plan backend account shows its 5-hour and weekly
       windows in `teamclaude status`
  #495 a NanoGPT backend account shows its daily and weekly windows and
       NanoGPT's own billing advice (`billing balance`, `balance not allowed`)
  #500 each dashboard account card lists the models the account served in
       the last 15 minutes

Docs
  #505 what a cross-organization switch costs a Sonnet 5.5 conversation:
       the API drops the earlier thinking blocks silently; same-org pools are
       unaffected (#491)

Tests
  #501 #504 two tests that raced the wall clock or a shared port now assert
       the mechanism, and the last private server-spawn harness is gone

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants