Skip to content

docs(remote-hub): the one-port hub recipe, invite flow, and launchd semantics (en+ko, skill) - #4254

Merged
lidge-jun merged 7 commits into
devfrom
codex/260911-l7-hub-docs-skill
Sep 11, 2026
Merged

docs(remote-hub): the one-port hub recipe, invite flow, and launchd semantics (en+ko, skill)#4254
lidge-jun merged 7 commits into
devfrom
codex/260911-l7-hub-docs-skill

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Stack (hub single-port, #4236): 1 #4249 → 2 #4250 → 3 #4251 → 4 #4252 → 5 this PR. Targets #4252's branch; retarget to dev as the stack lands. Local suite deliberately not run (operator instruction); hosted CI on the pushed head is the proof.

Summary

The documentation and the ocx skill still described the hub the stack just replaced, and in one place they described something that had become actively harmful.

The guide told the operator to export a data-plane token by hand. export OPENCODEX_API_AUTH_TOKEN="$(openssl rand -hex 32)", immediately before ocx service install. That line is how the maintainer's own hub ended up with a management admin token in the data-plane variable and crash-looped — the token at hand went into the variable the command demanded. PR4 removed the demand; this PR removes the instruction. In its place: the real precedence (environment, then the existing owner-only service-api-token, then 32 fresh random bytes), that only the path is printed, that a foreground ocx start reads the same file, and that an admin token there is refused with unset OPENCODEX_API_AUTH_TOKEN and rerun.

The setup block is now the one-port recipe. hostname is the tailnet IP, ocx config set unauthenticatedLoopbackListener '{"enabled":true}' binds the companion socket on the same port, and hub.dataPublicOrigin sits beside hub.managementPublicOrigin — without it ocx hub invite falls back to http://<tailnet IP>:<port>, which is exactly the address a machine behind the TLS frontend cannot use, so the guide would have handed out a broken command. The ported form {"enabled":true,"port":10104} stays documented as the alternative, with PR3's restart caveat stated where an operator hits it rather than in a devlog.

Two sections that did not exist. The hub's own local clients records what PR3 shipped: ocx claude, Claude Desktop, Cursor, system-env and the vision helper work on a hub now, with the exact admitted wire list, count_tokens named as not admitted, and why /api/* stays 404 there. Inviting another machine is ocx hub invite → the copy-paste echo '<code>' | ocx connect … --pairing-code-stdin block, plus the corsAllowOrigins precondition with its exact command and the reason it exists (ocx connect presents Origin: http://localhost:<its own port>; grants are origin-bound).

Two corrections the code supports and the plan's own wording did not.

ocx service restart did not restart a healthy macOS job: serviceCommand mapped restartrepair, repairService calls installLaunchd, and after PR1 installLaunchd returns early — service is already loaded from the current plist; nothing to do. — whenever the rendered plist equals the file, the token file is unchanged, and launchctl print agrees. There was no stop/start in that path, so a restart had to be a hand-written launchctl command. #4249 then fixed the code instead (ee6a20a0e), and the last commit here follows it — see the restart bullet below. PR1's repair no-op is still right; what changed is that there is now a verb that does the other thing.

Tailscale Serve must not target the companion listener. It is a real socket on 127.0.0.1:<proxy port>, so the mapping is accepted and then fails: the companion runs with a policy view whose hostname is 127.0.0.1, which makes isApiAuthRequired false, and that branch of isAllowedRequestOrigin requires a loopback Host header. Serve forwards Host: hub-name.tailnet-name.ts.net, so the data routes answer 403 origin_rejected#4241's trap, reachable a second way. The forwarder section stays intact and its example port moved off 10100, because the companion owns that address now. #4241's loopback-bind trap table is unchanged and now says explicitly that it is about the bind, with the companion named as the sanctioned way to get a 127.0.0.1 socket on a hub.

Scope: English and Korean only. The Korean page existed but was a condensed pre-#4241 copy — it set hub.managementPublicOrigin before creating hub (the #4200 defect, in Korean), exported the token by hand, and offered the retired --allow-insecure-http. It is now a section-for-section mirror of English. The other five locales are a follow-up, matching #4241's own precedent; the devlog names exactly what they still claim.

reference/configuration/server.md (en + ko) gains hub.dataPublicOrigin, how the two advertised origins relate, why --management-url can only confirm while --data-url really overrides, and the companion refusal rule. Its hostname row and ## Remote access no longer claim a non-loopback bind refuses to start without the environment variable — that stopped being true in PR4, and a reference page that says the proxy will refuse to start is not a cosmetic inaccuracy. The Korean page had neither the loopback-listener section nor the Remote Hub key table; both are written.

skills/ocx gains the topology, the invite flow, the hub gate ("this machine is a hub; local sync is gated unless the loopback listener is enabled" — the exact sentence, with the note that it means the gate and not clientIntegrations), a section on the data token that says what not to do with it, the launchd repair/status semantics with a per-row "repair?" column, and recipe 10 for inviting a machine. references/01_management_surface.md was not hand-edited; skill:surface:check reports it current.

Help copy was read as a first-time hub operator and touched in exactly three places, each wrong or missing: the macOS restart/status semantics under ocx service, --management-url and --clients under ocx hub, and the banner's ocx status line, which was the one command that answers "what is this hub doing" and had nothing pointing at it.

Restacked mid-work, and it changed the copy. This branch was cut from the then-tip of #4252's branch; while it was being written that branch was rebased onto #4249 and grew a four-commit review round, so the original base left its history entirely. The rebase had one conflict (src/cli/registry.ts's hub details — both sides had edited the --data-url paragraph), resolved by keeping the base's new refusal text and appending this PR's --management-url / --clients lines. The review round was not only text, and four of its findings made sentences in the first draft false; the last commit reconciles all four across both locales, both reference pages and the skill:

  • ocx hub invite now refuses a loopback- or wildcard-derived data origin instead of advertising http://localhost:<port>. So the order is --data-urlhub.dataPublicOrigin → the bind address, and that last step only works when the bind is an address another machine can dial. "…or http://<bind>:<port> as a last resort" would have walked an operator on a loopback-bound hub into the refusal with no idea why.
  • The reused service-api-token file is re-checked for the admin token, with a different remedy — delete the file and run ocx service repair, because unsetting a variable says nothing about a file — and both checks now run ahead of the loopback short-circuit.
  • The ocx status token states changed: present (env) is gone, the state is always about the file (present (file) / unsafe (file) / admin-collision (file) / missing), and the shell's variable is a separate sub-line. Listing present (env) was exactly the honesty defect the review round fixed in the code.
  • Every successful invite prints a Bound browser origin: line on stderr that is not in the --json envelope, and it decides whether the exchange works. The skill's recipe now says to relay it, which an agent reading only --json would drop.
  • ocx service restart restarts a healthy launchd job now, so the docs stop telling operators to kickstart by hand. A fifth reconciliation, against ee6a20a0e on fix(service): macOS repair no longer evicts a healthy launchd job, and verifies the one it loads #4249, which landed after this unit was written. restart is no longer an alias of repair: it runs the same refresh and, when nothing was reloaded, runs launchctl kickstart -k gui/<uid>/com.opencodex.proxy in place, verifies with the launchd probe, and prints service restarted (launchctl kickstart -k …). repair keeps the no-op — a repair of a healthy service must not be an outage — and a bare ocx service still selects repair; Linux always restarted (systemctl --user restart) and Windows is unchanged. Every passage in both guides, both skill references and SKILL.md that said restart aliases repair / restarts nothing / "kick the job yourself" now names ocx service restart, with the kickstart line demoted to the manual fallback the failure path itself prints. The guides also distinguish ocx restart (the proxy process you started) from ocx service restart (the service the manager supervises) wherever a restart is prescribed. src/cli/registry.ts's service entry was already reconciled in the base — the docs were made to match it, not the reverse — and the docs-claims gate pins the new claim, forbids the old "is an alias of repair" sentence, and keeps the kickstart line pinned only alongside the words "manual fallback". A repo-wide grep then found two more pages outside this unit's original file list, both quoting code ee6a20a0e changed, and both fixed: reference/cli/lifecycle.md (en + ko) had | restart | Alias of repair. | in the ocx service table, and its ocx status paragraph still printed the advice src/cli/version-skew.ts stopped emitting (ocx service repair (ocx service restart is an alias)) — a skew leaves the definition byte-identical, so repair would no-op over the old process. The other five locales of that page are recorded in the devlog's Left over with the rest of the translation-parity follow-up.

No runtime behavior changes. The only src/ edits are help and registry strings.

Verification

  • tests/ci-workflows/docs-remote-hub-claims.test.ts extended from 7 to 12 tests, five new cases run over both locales: the companion form and the ported alternative are both present; ocx hub invite with its corsAllowOrigins precondition and --pairing-code-stdin; the kickstart line and the launchd state could not be verified state; and the Serve warning. The no-exported-token assertion is line-anchored (/^\s*export\s+OPENCODEX_API_AUTH_TOKEN/m) rather than a substring ban: prose has to stay free to name the variable, because the page must say the step is gone and that an admin token is refused there. The first draft used a substring and failed on its own page, which is how the anchor got chosen. docs(remote-hub): make the setup runnable on a fresh config and close the macOS data plane #4241's seven assertions are untouched — extended, never relaxed.
  • Focused tests, exact commands and counts:
    • bun test tests/ci-workflows/docs-remote-hub-claims.test.ts tests/ci-workflows/skill-ocx.test.ts tests/cli/cli-help.test.ts tests/cli/cli-registry.test.ts57 pass, 0 fail, 515 expect()
    • bun test tests/cli/cli-capabilities.test.ts17 pass
    • bun test tests/ci-workflows/docs-429-failover-claims.test.ts tests/ci-workflows/docs-provider-billing-claims.test.ts tests/ci-workflows/docs-readme-translation-parity.test.ts tests/ci-workflows/docs-bun-source-requirement.test.ts64 pass (the other docs-claims gates)
    • bun run typecheck → clean · bun run privacy:scan → Privacy scan passed · bun run skill:surface:check → current
  • docs-site/AGENTS.md requires a docs build for any docs-site/ change, and it passed: cd docs-site && bun install --frozen-lockfile && bun run build425 pages built, Complete! On top of that, every in-page anchor was checked against the generated HTML rather than by eye: all six new English ids exist in dist/guides/remote-hub/index.html, and a script compared every href="#…" against every id="…" in dist/ko/guides/remote-hub/index.html and both server/index.html pages — zero unresolved, including the percent-encoded Korean anchors.
  • After the restart-wording commit, re-run: bun test tests/ci-workflows/docs-remote-hub-claims.test.ts tests/ci-workflows/skill-ocx.test.ts tests/cli/cli-help.test.ts tests/cli/cli-registry.test.ts57 pass, 0 fail, 519 expect() (four new assertions on the launchd-semantics gate), bun run typecheck → clean, bun run privacy:scan → Privacy scan passed, and cd docs-site && bun install --frozen-lockfile && bun run build425 pages built, Complete! again. No src/ change in that commit.
  • Every command above was re-run after the restack and the four reconciliations; the counts are from the final tree, and the docs build passed three times (before the restack, after it, and after the restart-wording commit).
  • The repository-wide suite was deliberately not run (operator instruction). Hosted CI on the pushed head is the proof.
  • Read-only CLI checks on the maintainer's live hub: bun run src/cli/index.ts help hub and … help service, to read the rendered copy. No ocx service …, no ocx start/stop/ensure/sync/restore, and no launchctl command was run. The two corrections above were established by reading src/service.ts (serviceCommandrepair, repairServiceinstallLaunchd, the early return) and src/server/auth-cors.ts (isAllowedRequestOrigin's loopback Host requirement), not by probing the hub.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Part of #4236.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 11, 2026 03:11
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f04166b3-bed0-4b7b-988b-a69de936da2e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 11, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 72 / 80

이 PR은 #4236 허브 단일 포트 스택의 5번째(마지막) 조각입니다. 지금 dev HEAD는 babb76449(#4240 L4 client-catalog, 패키지 2.51.0)이고, 이 PR의 베이스는 dev가 아니라 codex/260911-l4-hub-token-ux(#4252)입니다. 그 아래는 #4251#4250#4249(베이스 dev)입니다. 스택 순서 1 repair → 2 companion → 3 local-clients → 4 token/invite → 5 이 docs/skill 입니다. 아래 네 장이 안착하기 전에는 dev에 바로 못 붙고, 부모 랜딩 후 리타깃이 필요합니다.

지금 devdocs-site/src/content/docs/guides/remote-hub.md는 아직도 export OPENCODEX_API_AUTH_TOKEN="$(openssl rand -hex 32)"ocx service install 바로 앞에 둡니다. #4252 본문이 말한 사고 그대로입니다. 손에 있는 관리(admin) 토큰을 데이터 플레인 변수에 넣으면 assertNotAdminToken에서 크래시 루프가 납니다. 코드 쪽(#4252)이 이미 “요구하지 말고 파일로 프로비저닝”으로 고쳤는데, 가이드가 예전 문장을 그대로 가르치면 운영자는 또 그 함정으로 갑니다. 이 PR은 그 문장을 지우고, 실제 우선순위(환경 → 기존 owner-only service-api-token → 없으면 32바이트)와 unset OPENCODEX_API_AUTH_TOKEN 후 재실행을 적습니다.

설정 블록도 한 포트 레시피로 바뀝니다. hostname은 테일넷 IP, unauthenticatedLoopbackListener '{"enabled":true}'(포트 없음)가 companion이고, hub.dataPublicOriginhub.managementPublicOrigin 옆에 둡니다. dataPublicOrigin이 없으면 ocx hub invitehttp://<테일넷 IP>:<port>로 떨어져서, TLS 프론트 뒤에 있는 머신이 쓸 수 없는 명령을 받게 됩니다. 포트 있는 형태(port: 10104)는 대안으로 남기고, companion을 켠 뒤 바운스가 필요할 때 ocx service restart가 아니라 launchctl kickstart -k gui/$(id -u)/com.opencodex.proxy(또는 stop→start)라고 말합니다. #4249 이후 macOS에서 restartrepair→건강한 job이면 nothing to do로 끝나는 동작과 맞춥니다.

새로 생긴 두 절도 코드와 맞습니다. 허브 자신의 로컬 클라이언트는 companion이 받는 와이어(POST /v1/messages, POST /v1/chat/completions)와 받지 않는 count_tokens·/api/*를 적고, 다른 머신 초대ocx hub inviteecho '<code>' | ocx connect … --pairing-code-stdincorsAllowOrigins 전제(연결 쪽이 Origin: http://localhost:<자기 포트>를 보내서)를 적습니다. Tailscale Serve를 companion 소켓에 붙이지 말라는 경고도 있습니다. companion은 127.0.0.1용 정책이라 Serve가 넘긴 Host: …ts.net에서 403 origin_rejected가 납니다(#4241 함정의 다른 길).

영어·한국어 가이드와 reference/configuration/server.md(en+ko), skills/ocx(SKILL.md, 03_recipes.md 레시피 10, 04_failure_semantics.md의 repair?/launchd 표, 05_remote_hub.md), src/cli/help.ts·registry.ts의 도움말 세 곳만 런타임 밖에서 손댑니다. tests/ci-workflows/docs-remote-hub-claims.test.ts는 7→12로 늘렸고, companion/invite/kickstart/Serve 경고·export 줄 금지를 양쪽 로케일에 걸었습니다. export 금지는 부분문자열 금지가 아니라 ^\s*export\s+OPENCODEX_API_AUTH_TOKEN 줄 앵커입니다. 본문이 변수 이름을 말해야 해서(없어진 단계·관리 토큰 거부) 그 선택이 맞습니다. 문서 빌드·앵커 검사·focused bun test 숫자는 본문에 적혀 있고, 전체 스위트는 운영자 지시로 생략·호스티드 CI가 증명입니다.

한 가지 합류 전에 꼭 볼 점: 이름 붙인 베이스 codex/260911-l4-hub-token-ux(현재 tip b26eee311, PR4 리뷰 라운드 기록)가 이 PR 헤드의 조상이 아닙니다. merge-base --is-ancestor가 실패하고, GitHub 파일 목록이 +4711로 스택 전체가 잡힙니다. 실제 docs 단위 커밋은 9fba8b458·dd1ad1f74·daa738b18·e044679ec(그리고 restack 잔여 fb1898e19) 정도입니다. 리뷰·머지 전에 #4252 tip 위로 다시 쌓아야 UI diff와 랜딩 범위가 맞습니다.

라인 단위로 보면 아래입니다.

docs-site/.../guides/remote-hub.md (en) 설정 블록 - 한 포트 companion + hub.dataPublicOrigin + export 줄 삭제가 사고 재발을 막는 핵심이다
docs-site/.../ko/guides/remote-hub.md - en과 절 대 절 미러로 #4200식 parent-before-nested·옛 --allow-insecure-http·수동 토큰 export가 빠졌다
tests/ci-workflows/docs-remote-hub-claims.test.ts - 12케이스·양 로케일·줄 앵커 export 금지가 #4241 가드를 풀지 않고 늘린다
skills/ocx/references/05_remote_hub.md - 토큰에 대해 “export 시키지 말 것 / 고치려고 재생성하지 말 것 / 다른 머신에 복사하지 말 것”을 적어 에이전트 오판을 막는다
skills/ocx/references/04_failure_semantics.md - nothing to do·launchd state could not be verified를 실패가 아니라고 하고 repair? 열을 둔다
src/cli/registry.ts (service/hub 도움말) - macOS restart no-op·kickstart·--management-url은 확인만·--clients 문서화가 첫 운영자 읽기와 맞다
docs-site/.../ja|fr|ru|tr|zh-cn|zh-tw/guides/remote-hub.md - 이번 범위 밖이라 ja 등은 아직 export OPENCODEX_API_AUTH_TOKEN·옛 레시피를 가르친다
베이스 브랜치 조상 관계 - 이름 베이스 tip이 헤드 조상이 아니라 GitHub diff가 스택 전체(+4711)로 보인다. 합류 전 #4252 tip으로 restack이 필요하다

메인테이너의 판단이 필요한 지점

너의 추천

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun force-pushed the codex/260911-l7-hub-docs-skill branch from e044679 to 55d0773 Compare September 11, 2026 03:16
@lidge-jun
lidge-jun force-pushed the codex/260911-l7-hub-docs-skill branch from 55d0773 to 1e792cb Compare September 11, 2026 03:24
@lidge-jun
lidge-jun force-pushed the codex/260911-l4-hub-token-ux branch 2 times, most recently from 2aa572d to 2e90005 Compare September 11, 2026 03:30
@lidge-jun
lidge-jun force-pushed the codex/260911-l7-hub-docs-skill branch from 1e792cb to 30d5984 Compare September 11, 2026 03:34
lidge-jun and others added 7 commits September 11, 2026 12:43
…tics (en+ko)

The guide told the operator to export a data-plane token by hand before
`ocx service install`, described the loopback listener only in its ported form, and
had no path for joining a second machine other than assembling `ocx connect` from
parts. All three stopped being true in this stack (#4236).

English and Korean now share one structure. The setup block is the one-port recipe:
hostname is the tailnet IP, `unauthenticatedLoopbackListener: {"enabled": true}`
binds the companion socket on the same port, and `hub.dataPublicOrigin` sits beside
`hub.managementPublicOrigin` so the printed invite is usable. The
`export OPENCODEX_API_AUTH_TOKEN="$(openssl rand -hex 32)"` line is gone, replaced by
the real precedence (env, then the existing owner-only file, then 32 fresh bytes) and
the fact that an ADMIN token there is refused. New sections cover the ported form as
the documented alternative, the hub's own local clients with the exact admitted wire
list and `count_tokens` named as absent, `ocx hub invite` with the `corsAllowOrigins`
precondition and the reason `--management-url` can only confirm, and macOS service
operations.

Two corrections the code supports and the plan's wording did not. `ocx service restart`
aliases `repair`, and after PR1 a repair of a healthy launchd job is a no-op, so it
restarts nothing -- `launchctl kickstart -k gui/$(id -u)/com.opencodex.proxy` is the
way to bounce it. And Tailscale Serve must not target the companion listener: it is a
real socket, so the mapping is accepted, and then the loopback admission policy's
`Host` check rejects the forwarded Host exactly as the plain loopback-bind trap does.
The forwarder section stays, with its example port moved off 10100 because the
companion owns that address now.

#4241's structure and every claim its guard test pins are intact. The guard was
extended, not relaxed: five new cases over both locales pin the companion form, the
ported alternative, `ocx hub invite` with its precondition, the kickstart line, the
four-state launchd wording, and the Serve warning. The no-exported-token assertion is
line-anchored so prose can still name the variable -- the page has to, to say the step
is gone and that an admin token is refused there.

`reference/configuration/server.md`: `hub.dataPublicOrigin`, how the two public
origins relate, and the companion refusal rule. The `hostname` row and `## Remote
access` no longer claim a non-loopback bind refuses to start without the environment
variable, which stopped being true in PR4. The Korean page had neither the
loopback-listener section nor the Remote Hub key table; both are written.

The other five translations are out of scope and still carry the old recipe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hd no-ops

The skill described a hub whose only interesting decisions were pairing and
disconnection. After #4236 three more things are wrong to guess, and each of them is a
thing an agent would otherwise report as a bug.

`SKILL.md` leads with the topology: one port, the companion listener, `ocx hub invite`
instead of a hand-built `ocx connect`, `--management-url` as a confirmation, and "read
the `Hub:` block in `ocx status` before asking the operator about ports or tokens".

`05_remote_hub.md` gains the listener's two forms and the collision refusal, the exact
list of admitted wires with an explicit "do not propose widening it to `/api/*`", the
hub gate on the hub's own clients quoted verbatim, the invite flow including the
`No loopback browser origin is admitted for pairing` refusal and the command that fixes
it, and a section on the data token that says what NOT to do with it: never tell an
operator to export one, never regenerate it to fix something, never copy the file to
another machine.

`04_failure_semantics.md` gains the two states that read as failures and are not. A
repair printing `nothing to do` is success. `ocx service restart` aliases `repair`, so
on a healthy macOS job it restarts nothing -- which matters precisely when a restart is
the requirement. The four launchd verdicts get a per-row "repair?" column, because
`launchd state could not be verified` is an unanswerable probe, not a down service, and
treating it as one is what used to send operators to repair a serving hub. A hub-gated
skip exits 0 having written nothing and has nothing to retry.

`03_recipes.md` gains recipe 10: read the hub block, `ocx hub invite --json`, hand over
`command`, and the two refusals that burn no code.

`01_management_surface.md` is generated and was left alone; `skill:surface:check`
reports it current.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…url only confirms

Read as a first-time hub operator, PR4's help text is accurate; three things were not.

`ocx service` had exactly one line about `restart`, and it was Windows-specific. An
operator reading it infers that `restart` restarts the service, which on macOS is false
after PR1: `restart` aliases `repair`, and a repair whose plist, token file and loaded
job already agree returns early without touching launchd. The details now say so and
name `launchctl kickstart -k gui/$(id -u)/com.opencodex.proxy` (or stop then start) for
the case where a bounce is the actual requirement -- a change to hostname, port or
`unauthenticatedLoopbackListener`. The same block now lists the four `ocx service
status` verdicts, so `launchd state could not be verified` is read as an unanswerable
probe rather than a down service.

`ocx hub`'s usage line advertises `--management-url`, and nothing said it is a
confirmation of `hub.managementPublicOrigin` rather than an override. An operator
reading only the help would pass a different value and hit a refusal. `--clients` was
undocumented too.

The top-level banner's `ocx status` line now mentions the hub block, because nothing in
`ocx --help` pointed at the one command that answers "what is this hub doing".

No behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Records the en+ko rewrite, the reference-config additions, the skill pages, the help
corrections, the extended guard test, and the two findings that changed the copy: a
macOS `ocx service restart` does not bounce a healthy job, and the loopback companion
listener must not be a Tailscale Serve target. Names the five translations left out of
scope and exactly what they still claim.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es and the skill

The base branch grew a four-commit review round while this unit was being written, and
four of its findings made sentences here false. Reading src/ at the new base rather than
trusting the first draft is what caught them.

`ocx hub invite` now REFUSES a loopback- or wildcard-derived data origin instead of
advertising `http://localhost:<port>`, which would tell the other machine to dial itself
and spend the single-use code. So the resolution order is `--data-url`, then
hub.dataPublicOrigin, then the bind address -- and that last step only works when the
bind is an address another machine can dial. "...or http://<bind>:<port> as a last
resort" would have walked an operator on a loopback-bound hub into the refusal with no
idea why. An explicit override is never second-guessed, because a loopback data origin is
legitimate over an SSH tunnel.

The reused `service-api-token` file is re-checked for the admin token now, with a
different remedy: delete the file and run `ocx service repair`, because unsetting a
variable says nothing about a file. Both collision checks run ahead of the loopback
short-circuit, since the wrapper reads that file into the variable whatever the hostname.
Every page that said the existing file is reused now says it is re-checked, not trusted.

The `ocx status` token states changed. `present (env)` is gone -- the state is always
about the file (`present (file)`, `unsafe (file)`, `admin-collision (file)`, `missing`)
and the shell's variable is a separate sub-line. Listing `present (env)` as a state was
exactly the honesty defect the review round fixed in the code.

Every successful invite prints a `Bound browser origin:` line on stderr, and it is NOT in
the --json envelope. A grant is bound to one origin while a remote `ocx connect` presents
`Origin: http://localhost:<its own port>`, so a non-default bound origin means the other
machine must already be on that port or the exchange is refused and the code is spent.
The skill's recipe now says to relay that line, which an agent reading only --json would
drop.

One smaller correction rode along: the `corsAllowOrigins` fix command preserves existing
entries, so all three places that quote it say to run the line `invite` prints rather
than a hand-written one-element array.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The base (#4249) gained `ee6a20a0e` after this unit was written, which inverts the
claim five of these files had just landed. `restart` no longer folds into `repair`: it
runs the same refresh and, when nothing was reloaded, runs
`launchctl kickstart -k gui/<uid>/com.opencodex.proxy` in place, verifies with the
launchd probe, and prints `service restarted (launchctl kickstart -k …)`. `repair` keeps
the no-op -- a repair of a healthy service must not be an outage -- and a bare
`ocx service` still selects `repair`. Linux always restarted
(`systemctl --user restart`); Windows is unchanged.

So every passage saying restart aliases repair / restarts nothing / telling the operator
to kickstart by hand now names `ocx service restart`, and `launchctl kickstart -k` is
demoted to the manual fallback the failure path itself prints. Both guides, the two skill
references and `SKILL.md` also distinguish `ocx restart` (the proxy process you started)
from `ocx service restart` (the service the manager supervises) wherever a restart is
prescribed; `src/cli/help.ts`'s `ocx restart` is a different verb and is untouched.
`src/cli/registry.ts`'s `service` entry was already reconciled in the base -- the docs
were made to match it, not the reverse.

The docs-claims gate pins the new claim and forbids the old "is an alias of `repair`"
sentence, while keeping the kickstart line pinned only alongside the words "manual
fallback", so the page cannot quietly promote it back to the recommended route.

No `src/` change. Verified: the four focused test files (57 pass, 0 fail),
`bun run typecheck` clean, `bun run privacy:scan` passed, and the docs-site build
(425 pages, Complete!). No `ocx service …` and no `launchctl` command was run on this
host, per the operator's instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pair

A repo-wide grep for the old claim found two pages outside the previous commit's file
list, and both quote code that `ee6a20a0e` changed:

- `reference/cli/lifecycle.md` (en + ko) had `| restart | Alias of repair. |` in the
  `ocx service` subcommand table. It now describes the two verbs separately, matching
  `src/cli/registry.ts`: `repair` reloads the manager only when something changed, so a
  healthy unchanged macOS job keeps running; `restart` is the same refresh but always
  restarts, kickstarting an unchanged loaded job in place.
- The `ocx status` version-skew paragraph on both pages still printed the
  pre-`ee6a20a0e` advice, `ocx service repair (ocx service restart is an alias)`, which
  `src/cli/version-skew.ts` no longer emits: a skew leaves the definition byte-identical,
  so repair reloads nothing and keeps the old process serving. Both now say
  `ocx service restart` and why.

The bare-`ocx service` row and the `repair` row also stop claiming an unconditional
restart — a gap PR1 left rather than one this round created. The other five locales of
`lifecycle.md` still carry both old rows and are recorded in the devlog's Left over,
with the same translation-parity follow-up as the rest of this unit.

Verified: the four focused test files (57 pass, 0 fail, 519 expect()),
`bun run privacy:scan` passed, and `cd docs-site && bun run build` → 425 pages,
Complete! No `src/` change. No `ocx service …` and no `launchctl` command was run on this
host.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@lidge-jun
lidge-jun force-pushed the codex/260911-l4-hub-token-ux branch from 2e90005 to 902fc02 Compare September 11, 2026 03:43
@lidge-jun
lidge-jun force-pushed the codex/260911-l7-hub-docs-skill branch from 1f765a5 to eff7204 Compare September 11, 2026 03:43
Base automatically changed from codex/260911-l4-hub-token-ux to dev September 11, 2026 04:17
@lidge-jun
lidge-jun merged commit 6d8ed37 into dev Sep 11, 2026
36 of 39 checks passed
@lidge-jun
lidge-jun deleted the codex/260911-l7-hub-docs-skill branch September 11, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant