Repository navigation
fix(cli): prove cross-home ownership before deferring to a hinted port #6198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 5 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ad85a80
fix(cli): prove cross-home ownership before deferring to a hinted port
luvs01 6299fc0
fix(cli): probe every loopback family for the recorded home owner
luvs01 89be657
fix(cli): retry transient fenced-identity attestation failures (#6198)
luvs01 49ed717
fix(cli): prove cross-home owners via the shared registry and fail cl…
luvs01 b3cb717
test(codex): allow the shared owner-registry pointer in foreign-home …
luvs01 f3454e7
Revert "test(codex): allow the shared owner-registry pointer in forei…
luvs01 3cff601
fix(cli): anchor the owner registry to the default home and fail clos…
luvs01 d0ec2e9
test(cli): anchor the owner registry behind a test-only env seam (lid…
luvs01 18546fd
fix(cli): prune dead registry pointers before the cap and fail closed…
luvs01 8dbb264
fix(cli): refuse unlocated cross-home ownership before shared writes
luvs01 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| /** | ||
| * The cross-home owner registry. | ||
| * | ||
| * findCrossHomeOwner proves an owner through a home's protected runtime-port.json, | ||
| * but it used to know exactly one home: the default ~/.opencodex. A custom-home | ||
| * owner therefore stayed invisible to every other home (#6198): a second custom home | ||
| * saw the shared clients' managed URL, could not prove the answering process, and | ||
| * started as a competing owner that re-pointed shared Codex/Grok/Claude routing. | ||
| * | ||
| * This registry is the protected stable locator for those homes. Every runtime that | ||
| * publishes runtime-port.json also drops one tiny pointer file here so another home | ||
| * can find the record to attest against. The entries carry the home path only - the | ||
| * attestation secret stays inside the home's own record - so a forged or stale entry | ||
| * can never grant ownership; it can only send the reader to a record that still has | ||
| * to prove itself. | ||
| * | ||
| * The anchor is CODEX_HOME because that is the shared state this protects: the | ||
| * managed Codex client config lives there, and a sibling exists to leave that write | ||
| * alone. Entries are written atomically and never read back as truth - they only | ||
| * nominate a home for the caller's own record + liveness verification. | ||
| */ | ||
|
|
||
| import { createHash } from "node:crypto"; | ||
| import { mkdirSync, readdirSync, readFileSync, statSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join, resolve } from "node:path"; | ||
| import { atomicWriteFile } from "./atomic-write"; | ||
| import { getConfigDir } from "./paths"; | ||
| import { assertNotRealCodexHomeUnderTest } from "../lib/test-home-guard"; | ||
|
|
||
| const REGISTRY_DIR_NAME = "ocx-homes"; | ||
| const REGISTRY_ENTRY_SUFFIX = ".json"; | ||
| const MAX_REGISTRY_ENTRIES = 64; | ||
| const MAX_ENTRY_BYTES = 4096; | ||
| const HOME_KEY_LENGTH = 24; | ||
|
|
||
| function registryBaseDir(): string { | ||
| const raw = process.env.CODEX_HOME?.trim(); | ||
| // Without an env override the Codex client home defaults beside the user profile, | ||
| // the same location the managed config writers use. | ||
| return raw ? resolve(raw) : join(homedir(), ".codex"); | ||
| } | ||
|
|
||
| /** The shared directory the registry lives under. */ | ||
| export function ownerRegistryDir(): string { | ||
| return join(registryBaseDir(), REGISTRY_DIR_NAME); | ||
| } | ||
|
|
||
| function registryEntryPath(dir: string, home: string): string { | ||
| const key = createHash("sha256").update(resolve(home)).digest("hex").slice(0, HOME_KEY_LENGTH); | ||
| return join(dir, key + REGISTRY_ENTRY_SUFFIX); | ||
| } | ||
|
|
||
| /** | ||
| * Record 'home' in the shared registry. Best-effort and never throws: a failed write | ||
| * only degrades cross-home discovery, it must not break the publish that owns state. | ||
| */ | ||
| export function registerOwnerRegistryHome(home: string): void { | ||
| try { | ||
| assertNotRealCodexHomeUnderTest(registryBaseDir()); | ||
| const dir = ownerRegistryDir(); | ||
| mkdirSync(dir, { recursive: true }); | ||
| atomicWriteFile( | ||
| registryEntryPath(dir, home), | ||
| JSON.stringify({ home: resolve(home), v: 1 }) + "\n", | ||
| ); | ||
| } catch { /* discovery aid only */ } | ||
| } | ||
|
|
||
| /** | ||
| * Every registered home path, including this process's own (the caller filters it | ||
| * out). Entries are pointers, not facts: malformed, oversized, or unreadable entries | ||
| * are skipped rather than trusted. Bounded so a cluttered directory cannot stall | ||
| * startup discovery. | ||
| */ | ||
| export function readOwnerRegistryHomes(): string[] { | ||
| const dir = ownerRegistryDir(); | ||
| let names: string[]; | ||
| try { | ||
| names = readdirSync(dir) | ||
| .filter(name => name.endsWith(REGISTRY_ENTRY_SUFFIX)) | ||
| .slice(0, MAX_REGISTRY_ENTRIES); | ||
| } catch { | ||
| return []; | ||
| } | ||
| const homes: string[] = []; | ||
| for (const name of names) { | ||
| const path = join(dir, name); | ||
| try { | ||
| const stat = statSync(path); | ||
| if (!stat.isFile() || stat.size === 0 || stat.size > MAX_ENTRY_BYTES) continue; | ||
| const parsed: unknown = JSON.parse(readFileSync(path, "utf8")); | ||
| const home = parsed && typeof parsed === "object" | ||
| ? (parsed as Record<string, unknown>).home | ||
| : undefined; | ||
| if (typeof home === "string" && home.length > 0) homes.push(home); | ||
| } catch { /* a bad entry names nothing */ } | ||
| } | ||
| return homes; | ||
| } | ||
|
|
||
| /** Register this process's own home after its runtime record is published. */ | ||
| export function registerOwnHome(): void { | ||
| registerOwnerRegistryHome(getConfigDir()); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.