feat(hub): provision the data-plane token, add ocx hub invite, and one hub block in ocx status - #4252
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Deterministic PR hygiene checks passed. |
fb1898e to
8c27729
Compare
07cdc54 to
5ea847d
Compare
리뷰 · 우선순위 75 / 80이 PR은 #4236 허브 단일 포트 스택의 4번째 조각(토큰 UX + invite + status 허브 블록)입니다. 지금 고치는 사고는 본문이 아주 분명하게 말합니다. 비루프백 호스트에서 코드로 보면 세 축이 같이 움직입니다.
검증은 서비스/시크릿/invite/status/config/레이아웃 등 파일 단위로 숫자가 적혀 있고, 라이브 허브에서는 라인 - 메인테이너의 판단이 필요한 지점
너의 추천 이 댓글은 grok-bot이 작성했습니다 |
b26eee3 to
2aa572d
Compare
1218e08 to
ea2b3a5
Compare
2aa572d to
2e90005
Compare
…ing it `assertServiceAuthEnvironment` threw for any non-loopback hostname with no OPENCODEX_API_AUTH_TOKEN, even when ~/.opencodex/service-api-token already held a perfectly good one. That demand is what produced the incident in #4236: the operator exported the ADMIN token because `install` asked for a token, the hub crash-looped on `assertNotAdminToken`, and `repair` asked for the same variable again -- so the only remembered way to make the command proceed was the thing that had broken it. `writeServiceApiTokenFile` stays the single chokepoint every backend funnels through, and now resolves by precedence: the env token (still refused when it is an admin token), else an existing owner-only token file, else 32 fresh random bytes written 0600 through the same hardened writer. It returns the origin and logs the PATH, never the value. A loopback install with no env token still gets nothing -- admission is not required there, and on a connected client that file holds the hub's issued key. The preflight keeps exactly two refusals: the admin-token collision, and a token file that exists but cannot be used (reported where the operator can still act rather than failing mid-install). The admin-token message now says to `unset` and rerun, instead of offering "or set it to a distinct data-plane key". A foreground `ocx start` had no wrapper to cat the file into its environment, so `assertServerAuthConfig` refused to bind a non-loopback hostname the installed service was serving happily. `startupDataPlaneToken` applies the wrappers' own precedence in one place -- env, then OCX_API_TOKEN_FILE, then the installed token when admission is required. `assertServerAuthConfig` itself is unchanged. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…art binds The launchd plist and the systemd unit cat `service-api-token` into the environment before exec, and WinSW names it through OCX_API_TOKEN_FILE, so under a service the proxy has always seen OPENCODEX_API_AUTH_TOKEN regardless of the calling shell. A foreground `ocx start` had neither source, so a non-loopback hostname the installed service serves happily was refused at bind. `handleStart` now hydrates the environment through `startupDataPlaneToken` with the same precedence, gated on `isApiAuthRequired` so a loopback bind keeps ignoring the file -- on a machine connected to a hub it holds that hub's issued client key, not this proxy's admission secret. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rigin A hub's management and data planes are different sockets on a real deployment: management is a loopback-only ingress published by Tailscale Serve on 443, while the data listener is bound to the node's tailnet address and fronted on its own port. Deriving one from the other produced an origin that answered /readyz and nothing else, so the advertised data origin is its own field. Same canonical-origin transform as managementPublicOrigin (http(s), no credentials, path, query or fragment) and deliberately not `.catch`ed: silently dropping a typo would make `ocx hub invite` fall back to http://<hostname>:<port>, which is the value the field exists to replace. It is advisory -- the origin the hub advertises, never a bind address. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>]
[--clients codex,claude]` prints the exact line to run on the other machine:
# Run on the other machine:
echo '<code>' | ocx connect <data-origin> --management-url <mgmt-origin> --pairing-code-stdin
No new management route was needed. A "connect pairing code" IS a GUI pairing
grant -- `ocx connect --pairing-code-stdin` exchanges it at POST
/opencodex-session -- so invite reuses `requestBoundGuiPairingGrant`, the attested
local mint `ocx gui pair` already drives, authorized by an HMAC over the running
proxy's own attestation secret. It therefore needs no admin token and nothing
exported in the shell, and src/server/* is untouched.
Everything that cannot work is refused before a single-use code is minted: a
non-hub runtimeRole, a missing hub.managementPublicOrigin, a non-loopback
plaintext management origin, a malformed --data-url, no running attested proxy,
and a hub whose allow-list admits no loopback browser origin. That last one is
the non-obvious constraint: `ocx connect` sends Origin:
http://localhost:<its own port>, so only managementPublicOrigin itself or a
loopback corsAllowOrigins entry can match the grant, and the refusal names the
`ocx config set` line that fixes it. --management-url is a confirmation rather
than an override, because the grant is bound to the configured origin and
advertising anything else hands out a code the hub then rejects.
`ocx status` gains one Hub block on a hub: advertised data origin, the loopback
listener in its companion/ported/off forms, the management ingress and public
origin, the data-token SOURCE (present (env) / present (file) / unsafe (file) /
missing, env first because that is the service's own precedence), and the invite
hint. `collectHubStatus` and `hubStatusLines` live in status.ts so the sentences
are testable without spawning the CLI; no token value reaches either.
Refs #4236
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oning `ocx hub` gets a registry entry whose details are the one place that states the topology end to end: one port, the companion loopback listener local processes dial without a credential, the separate loopback-only management plane, the owner-only token file that must never be copied to another machine, and how an invite's code is bound. `ocx help hub` renders it. `ocx service` details gain the token-provisioning precedence and the admin-token refusal, so an operator reading help before installing learns that nothing has to be exported by hand -- the absence of that sentence is what made the incident in #4236 reachable from the documentation alone. `ocx hub invite` is a declared capability and `bun run skill:surface` regenerated references/01_management_surface.md (39 capabilities, 18 state-changing). Its `routes` are deliberately empty with the reason in a comment: the mint it drives is answered in the composition root ahead of handleManagementAPI and so is absent from MANAGEMENT_ROUTES, and declaring it would fail the capability/registry reconciliation rather than inform anyone. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…refusals Service: a non-loopback install no longer demands the env token; provisioning generates 64 hex chars at mode 0600 and then REUSES the same value on repair and reinstall (regenerating would invalidate every client key exchange already performed); an env token still wins and a loopback install creates nothing; an unusable token file is reported by the preflight rather than failing mid-install; and the admin-token refusal tells the operator to unset, reaching the chokepoint so nothing is written. `startupDataPlaneToken`: the environment wins, OCX_API_TOKEN_FILE still beats the installed path, the installed token is used only when admission is required, and an absent or unusable file resolves to null instead of throwing at boot. `ocx hub invite`: argument parsing, the origin helpers, the exact printed command, the --json envelope, and every refusal path -- including that a --management-url differing from the configured origin is refused rather than printed. The mint is injected, so no proxy, socket or real grant is involved. `ocx status` hub block: companion vs ported vs off, the configured vs derived data origin, env-over-file token precedence, missing vs unusable, the invite hint, and -- the assertion that matters for a security boundary -- that no token value appears in the JSON or the rendered lines. `hub.dataPublicOrigin` normalizes like the management origin and rejects the same five malformed shapes at write time. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What shipped, why the admin-token demand was the defect rather than the refusal, why no new management route was needed, the base discrepancy (PR1 is not in this branch's ancestry despite the assignment), exact verification commands with counts, the one pre-existing server-auth failure, and what the docs PR still owes. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… binds
`derivedHubDataOrigin` sends 0.0.0.0, :: and 127.0.0.1 all through `probeHostname`,
which spells every one of them as loopback -- so a hub bound to loopback, or to a
wildcard with no `hub.dataPublicOrigin`, printed `ocx connect http://localhost:<port>`
under "# Run on the other machine". That tells the other machine to dial ITSELF, and
the pairing code is single-use, so it was spent on an exchange that cannot succeed.
The maintainer's live hub is exactly that shape.
`resolveHubDataOrigin` returns either a usable origin (tagged flag/config/derived) or
`loopback-derived`, and the latter is refused BEFORE the mint, naming which shape this
hub has. A wildcard bind is refused rather than resolved on purpose: nothing in this
repo derives a tailnet or LAN address (`probeHostname` deliberately collapses wildcards
to loopback, and there is no `networkInterfaces` caller anywhere), so deriving one here
would advertise an interface the operator never chose. An explicit `--data-url` or
`hub.dataPublicOrigin` is never second-guessed -- loopback is legitimate over an SSH
tunnel.
The bound browser origin is now printed on every successful invite, in both modes, on
stderr beside the single-use warning so the --json envelope is unchanged.
`selectInviteBrowserOrigin` silently falls back to the first admitted loopback origin,
and a remote `ocx connect` only ever sends `http://localhost:<its own configured port>`
-- so a grant bound to anything else was refused at the exchange with nothing printed
to explain it. When the bound origin is not the default, the warning names the port the
connecting machine must be configured with, and offers admitting the default instead.
Both surfaced `ocx config set` lines now actually run.
`ocx config set hub.managementPublicOrigin …` exits `config parent path not found: hub`
when the `hub` object is absent -- exactly the config being advised -- so
`configSetHubLines` prefixes `ocx config set hub '{}'`, as guides/remote-hub.md does,
and only when it is genuinely missing. `ocx config set corsAllowOrigins '[…]'` replaces
the array, so `appendCorsAllowOriginsCommand` emits the entries already configured plus
the new one instead of telling the operator to delete them.
Reuse: `canonicalHttpOrigin` is exported once from `src/lib/gui-pair-capability.ts`,
beside `canonicalGuiBrowserOrigin`, and the byte-identical private copies in `hub.ts`
and `gui-pair-client.ts` are gone. The copies in `src/config.ts` (zod schema path) and
`src/server/gui-session.ts` stay for now, so `src/server/*` keeps a zero diff.
Refs #4236
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ken file The `origin: "file"` branch of `writeServiceApiTokenFile` returned early without calling `assertNotAdminToken`, so a `service-api-token` that already held the MANAGEMENT token -- hand-pasted before #2696, or written by the very incident this unit closes -- was silently accepted: `ocx status` said `present (file)` and the hub crash-looped at boot with nothing naming the cause. That is the #4236 incident shape, still reachable. `assertNotAdminToken` takes the source now, because the remedy differs rather than the rule: `unset OPENCODEX_API_AUTH_TOKEN` is meaningless advice about a file, so the file message says the file holds the management token and to delete it and rerun `ocx service repair` (or `install`). It deliberately does not call `serviceRetryCommand()`, which would drag `diagnoseService()` -- and `launchctl` -- into an error path that runs during install. Both collision checks moved ahead of the loopback short-circuit in `assertServiceAuthEnvironment`: `buildServiceShellCommand` cats the token file into OPENCODEX_API_AUTH_TOKEN whenever the file exists, WHATEVER the hostname, so a loopback install with an admin-token file fences its management plane closed at boot just the same. On a machine connected to a hub that file holds the hub's issued per-client key, never a management token, so the check is a no-op there. The reused file is `chmod 0600`'d best-effort on the way through. `readServiceApiTokenState` accepts any bounded regular file, so a reused token could be group- or world-readable while install printed "owner-only"; best-effort because a non-owner cannot chmod and failing the install over a loose mode would be worse. `ocx status`: `HubDataTokenState` gains `admin-collision (file)` and the hub block adds two lines naming the consequence and the fix, because that is what a crash-looping hub looks like from status output and nothing else in the report said so. And the token state is now always about the FILE: `present (env)` was reported whenever the calling shell exported the variable, but the plist and the unit overwrite it from the file before exec, so the label described the operator's terminal rather than the hub. The shell's variable survives as its own field, `dataTokenEnvInShell`, rendered as a sub-line -- it does decide what a foreground `ocx start` in that same shell admits. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ion state
`hub-invite`: every loopback and wildcard spelling of the bind address resolves to
`loopback-derived` and is refused with nothing minted (the injected mint records no
call), while `--data-url` and `hub.dataPublicOrigin` each unblock it; the surfaced
config commands carry `ocx config set hub '{}'` only when `hub` is absent; the
corsAllowOrigins suggestion keeps the existing entries and never duplicates; the bound
browser origin reaches stderr in both human and --json mode, and a non-default one
names the port the client needs.
`service`: an admin token in the REUSED token file is refused by the preflight and by
the writer, the file is left untouched by the refusal, `unset` is NOT the advice given,
and a loopback install is refused too (the wrapper cats the file whatever the hostname).
A reused world-readable file comes back at mode 0600 with its bytes unchanged.
`cli-status-json`: the token state is about the file and never about the calling shell,
the shell's variable is reported separately, and a file holding the admin token reads
`admin-collision (file)` with the consequence and the fix in the lines -- reached
through the minted prefix and through byte-equality with the configured admin token,
the same comparison doctor and the service chokepoint use. Both cases still assert no
token value appears in the JSON or the rendered lines.
Refs #4236
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Six findings, all accepted, each with the reasoning that made it a fix rather than a preference: why a wildcard bind is refused instead of resolved, why the admin-token remedy differs by source, why both collision checks had to move ahead of the loopback short-circuit, why `present (env)` was backwards, why the `--json` envelope did not grow, and why two of the four `canonicalHttpOrigin` copies stay. The superseded paragraph in the first round's section 5 is marked rather than rewritten. Also records that `tests/service/service.test.ts` is 209 on this machine with the working tree reverted, not the 212 the first round reported, and that no live-hub command was run this round -- `ocx status` reaches `launchctl` through `diagnoseService`. Refs #4236 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2e90005 to
902fc02
Compare
ea2b3a5 to
0fd205f
Compare
Summary
Nobody should have to export a token by hand to run a hub, and the fact that they did is what
produced the incident in #4236.
The refusal was right; the demand was the defect.
ocx service installon a non-loopbackhostname threw unless
OPENCODEX_API_AUTH_TOKENwas set, so the operator exported the token athand — the admin token.
assertNotAdminTokencorrectly refused it, the hub crash-looped, andocx service repairthen demanded the same environment variable again. The only remembered wayto make the command proceed was the exact thing that had broken it. Worse, the preflight threw
even when
~/.opencodex/service-api-tokenalready held a perfectly good token, so a repair couldnot succeed from a healthy on-disk state.
writeServiceApiTokenFilestays the single chokepoint every backend funnels through (launchd,systemd, the Windows scheduler wrapper, WinSW native) and now resolves by precedence: the env
token, still refused outright when it is an admin token; else an existing owner-only
service-api-token; else 32 fresh random bytes, hex, written0600through the same hardenedwriter. It returns the origin and logs the path, never the value. Reusing an existing token
rather than regenerating is what makes repair, reinstall and restart idempotent — a new hub secret
would silently invalidate every client key exchange already performed. A loopback install with no
env token still creates nothing: admission is not required there, and on a machine connected to a
hub that same file holds the hub's issued client key, which a local install must not clobber. The
preflight keeps exactly two refusals: the admin-token collision, and a token file that exists but
is unusable — reported where the operator can still act instead of failing mid-install. The
admin-token message now tells them to
unsetand rerun, where it previously offered "or set it toa distinct data-plane key", which is how they got there.
The collision check is not only about the environment, and review caught that it had to run on
the reuse branch too. A
service-api-tokenthat already holds the admin token — hand-pastedpre-#2696, or written by this very incident — was silently accepted by the
origin: "file"path:ocx statussaidpresent (file)and the hub crash-looped at boot with nothing naming the cause.assertNotAdminTokennow takes the source, because the remedy differs (unsetis meaninglessadvice about a file; deleting it so a data token is generated is the fix), the writer and the
preflight both check the file, and both checks run ahead of the loopback short-circuit —
buildServiceShellCommandcats that file intoOPENCODEX_API_AUTH_TOKENwhenever it exists,whatever the hostname, so a loopback install is fenced closed at boot just the same. On a
connected client the file holds the hub's issued per-client key, never a management token, so the
check is a no-op there. The reused file is also
chmod 0600'd best-effort on the way through:readServiceApiTokenStateaccepts any bounded regular file, so "owner-only" was a claim nobodyhad checked.
A foreground
ocx startwas the mirror-image gap.assertServerAuthConfigreads the environment,and under a service the environment always has the token because the plist and the unit
catthefile into it before exec — so
ocx startrefused to bind a non-loopback hostname the installedservice on the same machine was serving happily.
startupDataPlaneTokenapplies the wrappers' ownprecedence in one place (env, then
OCX_API_TOKEN_FILE, then the installed token when admissionis required).
assertServerAuthConfigitself is untouched: the fix belongs at the env-hydrationlayer the wrappers already occupy, not in a per-request admission helper that would then read a
file on every request.
ocx hub invitereplaces the remaining manual step. It prints the exact line to run on theother machine:
No new management route was needed and none was added. A "connect pairing code" is a GUI
pairing grant —
ocx connect --pairing-code-stdinexchanges it atPOST /opencodex-session— soinvite reuses
requestBoundGuiPairingGrant, the attested local mintocx gui pairalreadydrives, authorized by an HMAC over the running proxy's own attestation secret rather than by the
admin token. It therefore needs no admin token and nothing exported in the shell, which is
better than the admin-token path originally sketched, and
src/server/*is untouched by this PR.Everything that cannot work is refused before a single-use code is minted: a non-hub
runtimeRole, a missinghub.managementPublicOrigin, a non-loopback plaintext management origin,a malformed
--data-url, no running attested proxy, a hub whose allow-list admits no loopbackbrowser origin, and — from review — a data origin that would resolve to this machine's own
loopback.
ocx connectsendsOrigin: http://localhost:<its own port>, so onlyhub.managementPublicOriginitself or a loopbackcorsAllowOriginsentry can ever match thegrant, and the refusal names the
ocx config setline that fixes it.--management-urlis aconfirmation rather than an override, because the grant is bound to the configured origin and
advertising anything else would hand out a code the hub then rejects.
The loopback-data-origin refusal is the one that mattered most.
probeHostnamespells0.0.0.0,::and127.0.0.1all as loopback, so a hub bound to any of them with nohub.dataPublicOriginprinted
ocx connect http://localhost:10100— an instruction for the other machine to dialitself, paid for with a single-use code.
resolveHubDataOriginnow returns either a usableorigin (
flag/config/derived) orloopback-derived, and the latter is a refusal namingwhich shape this hub has. A wildcard bind is refused rather than resolved on purpose: nothing in
this repo derives a tailnet or LAN address, and guessing one from
os.networkInterfaces()wouldadvertise an interface the operator never chose. An explicit
--data-urlorhub.dataPublicOriginis never second-guessed — loopback is legitimate over an SSH tunnel.The bound browser origin is now printed on every successful invite, on stderr beside the
single-use warning, and when it is not
http://localhost:10100the warning names the port theconnecting machine must be configured with.
selectInviteBrowserOriginsilently falls back to thefirst admitted loopback origin, the printed command carries no trace of which, and a grant bound
to anything the client does not send is refused at the exchange — burning the code with no hint.
Every
ocx config setline the command surfaces is now one that actually runs.ocx config set hub.managementPublicOrigin …exitsconfig parent path not found: hubwhen thehubobject is absent — exactly the config being advised — so the parent-creatingocx config set hub '{}'is prefixed, and only when it is genuinely missing.ocx config set corsAllowOrigins '[…]'replaces the array, so the suggestion now carries theentries the hub already has instead of telling the operator to delete them.
New optional
hub.dataPublicOrigincarries the advertised data origin. It is its own fieldrather than a derivation because on a real deployment the two planes are different sockets:
management is a loopback-only ingress published by Tailscale Serve on 443, data is the tailnet
bind fronted on its own port. Same canonical-origin transform as
managementPublicOrigin, anddeliberately not
.catched — silently dropping a typo would make invite fall back tohttp://<hostname>:<port>, the value the field exists to replace.ocx statusgains one Hub block, so the question a hub operator actually asks is answered in oneplace instead of four:
The listener line distinguishes the companion and ported forms from #4239's change through
effectiveLoopbackListenerPort. The token line reports the state of the file the servicereads —
present (file)/unsafe (file)/admin-collision (file)/missing— and a testasserts no token value appears in the JSON or the rendered lines.
Two review fixes live here.
present (env)used to win whenever the calling shell exportedOPENCODEX_API_AUTH_TOKEN, but the plist and the unit overwrite that variable from the filebefore exec, so the label described the operator's terminal rather than the hub; the shell's
variable is now its own field, rendered as "the installed service reads the file, not this", kept
because it does decide what a foreground
ocx startin that shell would admit. Andadmin-collision (file)is the incident shape made visible: a file holding the management tokenused to read
present (file)while the hub crash-looped, so the block now names the consequenceand the command that fixes it.
ocx help hubis the one place that states the topology end to end (one port; the companionlistener local processes dial without a credential; the separate loopback-only management plane;
the owner-only token file that must never be copied to another machine; how an invite's code is
bound).
ocx servicedetails gained the provisioning precedence and the admin-token refusal — theabsence of those sentences is what made this incident reachable from the documentation alone.
The guide still tells operators to
export OPENCODEX_API_AUTH_TOKEN="$(openssl rand -hex 32)"before
ocx service install. That step is now optional; rewritingguides/remote-hub.md(en + ko)around
ocx hub inviteand documentinghub.dataPublicOrigininreference/configuration/server.mdis the next PR in this stack.This PR is stacked on #4239 and targets its head branch. Retarget to
devonce that lands.Verification
bun x tsc --noEmitclean.bun run privacy:scanpassed.bun run skill:surface:checkreportsthe committed map current (39 capabilities, 18 state-changing).
bun test tests/service/service.test.ts211 pass;tests/service/service-secrets.test.ts10pass;
tests/service/winsw.test.ts25 pass;tests/cli/hub-invite.test.ts22 pass;tests/cli/cli-status-json.test.ts54 pass;tests/gui/gui-pair-client.test.ts4 pass;tests/gui/gui-pair-capability.test.ts2 pass;tests/cli/cli-registry.test.ts12 pass;tests/cli/cli-capabilities.test.ts17 pass;tests/cli/cli-help.test.ts17 pass;tests/cli/cli-dispatch.test.ts39 pass;tests/cli/cli-transport-honesty.test.ts22 pass;tests/ci-workflows/skill-ocx.test.ts16 pass;tests/server/config.test.ts196 pass;tests/test-layout.test.ts+tests/test-layout-tooling.test.ts17 pass. Also green:tests/cli/cli-json-contract.test.ts8,tests/cli/cli-start-journal-order.test.ts3,tests/config/config-user-edits.test.ts46,tests/providers/opencode-cli.test.ts51.tests/server/server-auth.test.tsis 111 pass / 1 fail on this branch and on the base withthe working tree stashed:
native passthrough upstream reset still logs 502 and penalizes the poolis pre-existing, not a regression.winsw.test.tsin thesame
bun testcall ascli-transport-honesty.test.tstrips the real-home guard throughcross-file
OPENCODEX_HOMEleakage; that is true on the base too.tests/cli/hub-invite.test.tsis new and registered in bothscripts/test-layout/layout.jsonand
tests/fixtures/test-layout-expected.json. It injects the mint, so no proxy, socket or realgrant is involved, and it pins every refusal path — including that a
--management-urldifferingfrom the configured origin is refused rather than printed. The hub status block, the startup
token precedence and the
hub.dataPublicOriginschema went into the existingcli-status-json.test.ts,service-secrets.test.tsandserver/config.test.ts.characters at mode
0600, that a second call returnsorigin: "file"with the same bytes, thata loopback install creates no file, and that the admin-token path writes nothing.
--data-url/hub.dataPublicOriginescapes, the bound-origin notes in both output modes, theocx config set hub '{}'prefix whenhubis absent, the appendingcorsAllowOriginsform,an admin token in the reused token FILE refused by both the preflight (non-loopback and
loopback) and the writer with the file left untouched, the
0600tightening of a reusedworld-readable file, and
admin-collision (file)in the status block — including that it isreached through the same byte-equality comparison doctor uses, not only the minted prefix.
tests/service/service.test.tsis 209 on this branch with the working tree reverted on thismachine, not the 212 reported in the first round; 211 is 209 plus the two new cases.
ocx statusrendered the block quoted above.ocx hub invite --jsonexited 1 withNo loopback browser origin is admitted for pairing…, minting nothing — that hub hashostname: 127.0.0.1and no
corsAllowOrigins, so the refusal is correct, and it is also the exact shape the newloopback-data-origin refusal exists for. No live command was run in the review round, not
even a read-only one:
ocx statusreachesdiagnoseService()→probeLaunchdLoadState()→launchctl, and the instruction for this machine is to run nolaunchctlat all. The liveconfig was not modified and no
ocx service,ocx start,ocx ensureorocx syncwas run.proof.
Checklist
Security-boundary notes for that last box. No admission path widened:
assertServerAuthConfig,configuredApiAuthToken,resolveDataPlaneAdmissionSecretand the loopback route allowlist areuntouched, and
src/server/*has no diff. The generated token israndomBytes(32)through thepre-existing hardened writer (
mkdir 0700→writeFileSync mode 0600→chmod→hardenSecretPathon Windows), so no new file-permission code was written. The admin-tokenchokepoint is preserved and now covered by a test that asserts nothing reaches disk when it
fires. Token values are never logged, never placed in a plist, unit file or argv, and the status
projection carries only which state the file is in — asserted, not merely intended; the
admin-token comparison added to
ocx statusderives a boolean and nothing else. The review roundtightened two things in this box's favour: the reuse branch can no longer accept a management
token from disk, and a reused token file is
chmod 0600'd rather than assumed owner-only. The pairing codeocx hub inviteprints goes to stdout exactly asocx gui pairalready prints its grant, withthe single-use warning on stderr — and now the bound browser origin beside it, which is operator
advice rather than envelope contract, so
--jsonstill emits exactly{ code, expiresAt, dataUrl, managementUrl, command }.One reuse cleanup from review:
canonicalHttpOriginis exported once fromsrc/lib/gui-pair-capability.ts, besidecanonicalGuiBrowserOrigin, and the private copies insrc/cli/hub.tsandsrc/cli/gui-pair-client.tsare gone. The copies insrc/config.ts(zodschema path) and
src/server/gui-session.tsstay, sosrc/server/*keeps a zero diff; collapsingthose two is its own change.
Refs #4236