Skip to content

fix(cli): keep a second-home proxy off a live proxy's client config - #6108

Closed
lidge-jun wants to merge 1 commit into
devfrom
codex/t4-bug-hardening-sibling-home
Closed

lidge-jun wants to merge 1 commit into
devfrom
codex/t4-bug-hardening-sibling-home

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Starting a second proxy with its own OPENCODEX_HOME while another proxy was already running rewrote the other proxy's client routing. Example: the default home runs on 10100, and OPENCODEX_HOME=/tmp/x ocx start --port 10200 changes the managed base_url in ~/.grok/config.toml to 10200 (and the same class of Codex and Claude startup sync). When the second proxy exits, it strips the block or leaves it pointing at a dead port. The cause is that owner discovery (findProxyOwnerBeforeJournalRecovery) only reads the second home's records and configured port. It never finds the primary, so the existing one-way sibling mark (src/codex/sibling-start.ts), which already keeps a same-home sibling off shared client files, is never set.

After this change, ocx start runs a cross-home owner check (src/cli/cross-home-owner.ts) when same-home discovery finds no owner. It runs before journal recovery and before any client write, and again under the bind lease.

  • Hints: the default home's runtime-port.json (read only when the resolved OPENCODEX_HOME differs from <homedir>/.opencodex), the managed Grok [model_providers.opencodex].base_url, and the managed Codex provider base_url. Reads are bounded, and only loopback URLs with an explicit port count.
  • Identity: each candidate port is probed with the existing probePortOwner. A hit requires an OpenCodex health identity whose PID is a positive integer different from this process. A foreign service, a stale port, a remote or malformed URL, or a legacy pid: null answer grants nothing, so a lone custom-home install keeps its automatic sync.
  • Effect: a hit sets the existing sibling mark, so Codex sync/inject, Grok startup sync and exit strip, catalog refresh, launchd env and the shell hook all stay off shared files, as they already do for a same-home sibling. Two writers that did not consult the mark now do: the Claude agent roster startup sync and the ensure-time Grok fence (ON and OFF branches, reported as a sibling skip).
  • Journal recovery moves out of the probe helper and runs only after the full owner decision, and never for a sibling.
  • Explicit ocx sync and ocx grok apply are unchanged.

Docs: structure/runtime.md, structure/codex-home.md, structure/clients/integrations.md, and the CLI lifecycle reference (en, ko, ja, zh-cn, zh-tw) describe the cross-home condition. src/cli/index.ts stays at 1,999 lines (ratchet threshold 2,000).

Known limits: two homes that start before either has published a record or a client URL can still both sync, and so can a sibling that starts while the primary is briefly down during its own restart. This change fixes the observed case (a running primary and a later sibling) without adding a cross-home lock. A hint that points only at the data-only listener, which has no /healthz, is treated as unknown. The Claude intercept settings migration in src/claude/intercept/runtime.ts (run during startServer when intercept is enabled) does not consult the mark yet. It is owned by a different area and is tracked separately.

Verification

  • bun test tests/cli/sibling-home-client-sync.test.ts tests/cli/cli-start-journal-order.test.ts tests/cli/hub-gated-local-clients.test.ts tests/providers/xai/grok-lifecycle.test.ts tests/claude-integration/claude-agent-startup-sync.test.ts tests/codex-integration/codex-desired-state.test.ts tests/cli/cli-dispatch.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts: 174 pass, 0 fail.
  • The new tests/cli/sibling-home-client-sync.test.ts (registered in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json) uses fake loopback /healthz identity servers for: another PID (sibling), the same PID, pid: null, a foreign service, a closed port, and remote/malformed URLs. It also spawns a real secondary ocx start with isolated HOME, OPENCODEX_HOME, CODEX_HOME, GROK_HOME and CLAUDE_CONFIG_DIR, and checks that shared client files are left byte-identical while a lone custom home still injects.
  • Red-green: the two new writer-guard regressions failed before the patch (27 pass, 2 fail) and pass after. The cross-home discovery module is new, so its tests could not run against the old code; the start-path test covers the behavior change.
  • bun run typecheck, bun run structure:check, git diff --check: pass. bun run test:changed and privacy:scan from a same-commit throwaway checkout: added below when complete. The local full suite was not run because seven release lanes share this machine and its test lock. Hosted CI shards are the full-suite evidence.
  • No test or QA run touched the real home: tests pin every client home to a temp directory.

Security review

Assets: user client routing files (~/.grok/config.toml, CODEX_HOME/config.toml, the Claude agent roster) and the Codex journal. Entrypoint: a second local ocx start/ocx ensure. Trust boundary: one isolated proxy home reaching user-global client state. The change only removes writes. Client-file URLs are treated as untrusted hints: loopback only, explicit port, bounded reads, and an OpenCodex identity plus a different positive PID required. No credential, token or request content is read or logged, and no auth check changes.

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.

Starting a second proxy with its own OPENCODEX_HOME while another proxy was
running (for example the default home on 10100) rewrote the shared
~/.grok/config.toml opencodex base_url, and the same class of Codex and Claude
startup sync, to the second proxy's port, then stripped or left it on exit.
Owner discovery only read the second home's own records and configured port,
so the existing one-way sibling mark was never set.

ocx start now runs a cross-home owner check when same-home discovery finds no
owner, before journal recovery and before any client write, and again under
the bind lease. It reads the default home's runtime-port.json (only when the
resolved home differs), the managed Grok base_url and the managed Codex
provider base_url, keeps loopback URLs with an explicit port, and identity
probes each port. A live OpenCodex whose reported PID is a positive integer
other than this process sets the existing sibling mark, so every writer that
honors it stays off the shared files. A foreign, stale, remote or pid-less
answer grants nothing, so a lone custom-home instance still syncs. Journal
recovery now runs only after that decision and never for a sibling. The
Claude agent roster startup sync and the ensure-time Grok fence, which did
not consult the mark, now skip for a sibling. Explicit ocx sync and ocx grok
apply are unchanged.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 27, 2026 16:18
@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b1d5a87b-959f-4148-beaf-ab50eb80d997

📥 Commits

Reviewing files that changed from the base of the PR and between 24b2f39 and 4bfae6f.

📒 Files selected for processing (18)
  • docs-site/src/content/docs/ja/reference/cli/lifecycle.md
  • docs-site/src/content/docs/ko/reference/cli/lifecycle.md
  • docs-site/src/content/docs/reference/cli/lifecycle.md
  • docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
  • docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md
  • scripts/test-layout/layout.json
  • src/cli/claude-agent-startup-sync.ts
  • src/cli/cross-home-owner.ts
  • src/cli/ensure-desired-integrations.ts
  • src/cli/index.ts
  • structure/clients/integrations.md
  • structure/codex-home.md
  • structure/runtime.md
  • tests/claude-integration/claude-agent-startup-sync.test.ts
  • tests/cli/cli-dispatch.test.ts
  • tests/cli/hub-gated-local-clients.test.ts
  • tests/cli/sibling-home-client-sync.test.ts
  • tests/fixtures/test-layout-expected.json

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T16:22:46.456713Z 4bfae6f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the bug Something isn't working label Sep 27, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bfae6f055

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/index.ts
Comment on lines 750 to +751
const owner = await findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true });
if (!owner.live) reconcileStartupJournal();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mark the ensure parent as a cross-home sibling

When ocx ensure runs under a second OPENCODEX_HOME, this path never calls findCrossHomeOwner before recovering the journal. Although the spawned start child detects the live owner and marks itself as a sibling, that mark is process-local; after waitForProxy, the unmarked ensure parent still calls reconcileEnsureDesiredIntegrations, syncModelsToCodex, and syncClaudeAgentDefsAtProxyStartup. It can therefore replay the second home's journal and repoint or remove the live owner's shared Codex, Grok, and Claude configuration—the exact failure this change is meant to prevent. Apply the cross-home ownership decision in handleEnsure and suppress all parent-side shared-client writes when an owner is found.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the batch commit ca24aaf (#6113). handleEnsure now calls markCrossHomeSibling() before journal recovery, so the ensure parent marks itself, and every parent-side shared-client call honors the mark: Codex sync through the desired-state gate, the Grok fence, Claude agent sync, system-env injection, and the shell-hook and ensure-integration call sites. A repeated ensure that finds this home's sibling already live also defers to the other owner. The new regression in tests/cli/sibling-home-client-sync.test.ts reproduced the roster deletion before the fix.

Comment on lines +22 to +24
const bytes = Buffer.alloc(MAX_HINT_BYTES + 1);
const count = readSync(fd, bytes, 0, bytes.length, 0);
return count > MAX_HINT_BYTES ? null : bytes.toString("utf8", 0, count);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve owner discovery for large managed configs

When the live owner uses another custom home and discovery depends on the shared Codex or Grok hint, any valid config.toml larger than 256 KiB is returned as null here, even if its managed provider block is present near the beginning. With no default-home runtime record to fall back to, the later start is treated as an ordinary owner and rewrites the shared client configuration. Large Codex configs are otherwise supported by the ownership paths, so the bounded discovery should retain enough evidence to identify a managed loopback target, or fail safely rather than equating an oversized file with an absent hint.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ca24aaf (#6113). Hints are now read the way their writers read them: Codex through readBoundedCodexConfig (1 MiB, the same cap the Codex injector enforces, so a larger config is refused by the writer too), and Grok in full, as the Grok injector does. Tests cover a Grok config over 256 KiB and a Codex config between 256 KiB and 1 MiB.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 66 / 80

이 PR은 프록시를 하나 더 켜도, 먼저 켜 둔 쪽의 클라이언트 설정을 건드리지 않게 해요. 베이스는 dev예요.

기본 홈이 10100에서 이미 돌아가는데, 다른 폴더를 OPENCODEX_HOME으로 두고 ocx start --port 10200을 하면 예전에는 ~/.grok/config.toml의 주소가 10200으로 바뀌었어요. Codex와 Claude도 시작하면서 같이 바뀌었어요. 나중 쪽이 꺼지면 그 블록을 지우거나, 이미 죽은 포트를 남겨요.

주인 찾기가 나중 홈의 기록과 포트만 봤어요. 먼저 켜 둔 쪽을 못 찾아서, 공유 파일을 건너뛰는 표시가 안 켜졌어요. 그 표시는 원래 같은 홈의 형제에게만 있었어요.

이제는 ocx start가 같은 홈에서 주인을 못 찾으면 다른 홈도 봐요. Codex 사용 기록을 다시 맞추기 전이고, 클라이언트 파일을 쓰기 전이에요. 포트를 잡은 뒤에도 한 번 더 봐요. 보는 곳은 기본 홈의 runtime-port.json(지금 홈이 ~/.opencodex가 아닐 때만), Grok 관리 블록의 base_url, Codex 관리 블록의 base_url이에요. 이 컴퓨터의 주소이고 포트가 적혀 있을 때만 후보예요. /healthz가 OpenCodex이고, PID가 이 프로세스와 다른 양수일 때만 형제로 표시해요. 다른 프로그램이거나, 포트가 죽었거나, 먼 주소이거나, pid가 없으면 주인이 아니에요. 혼자 쓰는 커스텀 홈은 예전처럼 동기화해요.

표시가 켜지면 Codex 동기화, Grok을 시작할 때 맞추는 일, 끌 때 지우는 일, 카탈로그, launchd, 셸 훅은 공유 파일을 안 건드려요. 이번에 그 표시를 새로 보는 곳은 Claude 에이전트 명단과, ocx ensure가 Grok 관리 블록을 맞출 때예요. 사용 기록은 주인을 정한 뒤에만 다시 맞추고, 형제는 안 해요. 사용자가 직접 하는 ocx sync와 ocx grok apply는 그대로예요.

PR이 적어 둔 한계도 코드와 같아요. 둘 다 기록이나 주소를 남기기 전에 켜지면 둘 다 쓸 수 있어요. 본체가 재시작으로 잠깐 내려간 사이에 형제가 켜져도 그래요. /healthz가 없는 리스너는 모름으로 둬요. Claude intercept가 설정을 옮기는 코드는 아직 이 표시를 안 봐요. 그 부분은 다른 일로 남겨 두었다고 적혀 있어요.

src/cli/index.ts:751 - ocx ensure는 다른 홈 주인을 안 봐요. 같은 홈에 살아 있는 프록시가 없으면 바로 사용 기록을 다시 맞춰요. 그다음 자식을 띄워요. 자식 ocx start는 형제로 표시하지만, 그 표시는 자식 프로세스 안에만 있어요. 부모는 표시가 없는 채로 809행에서 Grok을 맞추고, 812행에서 Codex 모델을 동기화하고, 824행에서 Claude 명단을 동기화해요. 나중 홈의 ocx ensure가 먼저 켜 둔 쪽의 Codex, Grok, Claude를 자기 포트로 바꾸거나, 공유 사용 기록을 자기 기준으로 다시 써요. 이번 PR이 막으려던 길이에요. Grok과 Claude에 넣은 건너뛰기는 siblingOfLivePort()를 보는데, 부모는 그 값이 비어 있어요.

src/cli/cross-home-owner.ts:24 - 힌트 파일이 256KiB보다 크면 내용을 버리고 힌트가 없는 것으로 봐요. 관리 블록이 파일 앞쪽에 있어도요. 기본 홈의 runtime-port.json이 없고 Codex나 Grok 주소만으로 주인을 찾아야 할 때, 나중 시작은 평범한 주인이 되어 공유 설정을 다시 써요. 크다는 이유만으로 힌트를 지우면, 떠 있는 주인을 놓쳐요.

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

ocx ensure 부모도 형제로 막을지예요. 자식 start만 막으면 부모가 곧이어 다시 써요. 이번 설명은 들어가는 곳을 ocx start와 ocx ensure로 적고 있어요.

256KiB를 넘는 관리 설정은 힌트가 없는 것으로 둘지, 그 경우 쓰기를 멈출지예요. 혼자 쓰는 커스텀 홈은 계속 동기화해야 하고, 이미 떠 있는 주인은 놓치면 안 돼요.

너의 추천

ocx start만 보면 방향이 맞아요. 가짜 /healthz와 실제 두 번째 start로, 공유 파일이 바이트 그대로인 것을 확인해요. 합치기 전에 handleEnsure에서 같은 홈 주인이 없으면 findCrossHomeOwner를 먼저 해 주세요. 주인이 있으면 부모의 사용 기록 복구와 Codex, Grok, Claude 쓰기를 건너뛰게 하고, 그 경우를 테스트에 넣어 주세요. 256KiB를 넘으면 읽은 앞부분에서 관리 주소를 찾거나, 못 판단하면 동기화를 멈추세요.

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

@lidge-jun

Copy link
Copy Markdown
Owner Author

This slice is integrated through the batch PR #6113 on current dev (6d64ea2), one commit per slice, so the three fixes need one CI cycle instead of three serial rebase cycles. The commit is tree-identical to this PR's head, plus the two Codex review fixes for #6108. I will close this PR once #6113 merges.

@lidge-jun

Copy link
Copy Markdown
Owner Author

Superseded by #6113, merged into dev as 2275680ab3. This slice landed as commit f1f903bf0d, including the fixes for both Codex findings here and the later #6113 review fixes: Design B root hint, per-ID budget lease, ensure sibling record, and nonblocking bounded hint reads.

@lidge-jun lidge-jun closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant