Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions src/cli/cross-home-owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { markSiblingStart, siblingOfLivePort } from "../codex/sibling-start";
import { readClientConnectionState } from "../client/state";
import { findManagedRegion, resolveGrokHome } from "../grok/inject";
import { providerTableString } from "../codex/injected-marker";
import { probePortOwner, START_OWNERSHIP_LIVENESS } from "../server/proxy-liveness";
import { isLocalAttestationSecret } from "../lib/local-management-attestation";
import { loopbackProbeHosts, proveLiveProxyOwnedByHome, proxyIdentityAt, START_OWNERSHIP_LIVENESS } from "../server/proxy-liveness";
import type { RuntimePortState } from "../config/process-state";

const MAX_HINT_BYTES = 256 * 1024;
// Far above any real Grok config; discovery must not stall startup when Grok sync is off.
Expand Down Expand Up @@ -62,14 +64,21 @@ function loopbackPort(raw: string | null): number | null {
/** Returns only a different process with an identity-checked /healthz response. */
export async function findCrossHomeOwner(options: { homeDir?: string } = {}): Promise<number | null> {
const candidates = new Set<number>();
let defaultRuntime: RuntimePortState | null = null;
const defaultHome = join(options.homeDir ?? homedir(), ".opencodex");
if (resolve(getConfigDir()) !== resolve(defaultHome)) {
const raw = readBoundedRegularFile(join(defaultHome, "runtime-port.json"), MAX_HINT_BYTES);
if (raw) {
try {
const record: unknown = JSON.parse(raw);
if (record && typeof record === "object" && validPort((record as { port?: unknown }).port)) {
candidates.add((record as { port: number }).port);
if (record && typeof record === "object") {
const state = record as Record<string, unknown>;
if (Number.isSafeInteger(state.pid) && Number(state.pid) > 0 && validPort(state.port)
&& isLocalAttestationSecret(state.attestationSecret)
&& (state.hostname === undefined || typeof state.hostname === "string")) {
defaultRuntime = state as RuntimePortState;
candidates.add(defaultRuntime.port);
}
}
} catch { /* stale or malformed hint */ }
}
Expand Down Expand Up @@ -97,8 +106,21 @@ export async function findCrossHomeOwner(options: { homeDir?: string } = {}): Pr
} catch { /* an absent or invalid client home is not owner evidence */ }

for (const port of candidates) {
const owner = await probePortOwner(port, {}, START_OWNERSHIP_LIVENESS);
if (owner && Number.isSafeInteger(owner.pid) && owner.pid! > 0 && owner.pid !== process.pid) return port;
// A managed client URL is only a location hint. The default home's protected runtime
// record supplies the identity and proof key that make it ownership evidence.
if (!defaultRuntime || defaultRuntime.port !== port || defaultRuntime.pid === process.pid) continue;
// IPv4 and IPv6 loopback listeners are independent on this port, so a pid mismatch
// (or a dead answer) on one family does not prove the recorded owner absent. The
// recorded hostname is tried first and every loopback candidate gets an identity
// and attestation check before the port reports no owner.
for (const hostname of loopbackProbeHosts(defaultRuntime.hostname)) {
const identity = await proxyIdentityAt(port, { hostname, expectedPid: defaultRuntime.pid }, START_OWNERSHIP_LIVENESS);
if (identity?.pid !== defaultRuntime.pid) continue;
if (await proveLiveProxyOwnedByHome(
{ ...identity, hostname, port, source: "runtime" },
{ ...START_OWNERSHIP_LIVENESS, readRuntimeFn: () => defaultRuntime },
)) return port;
}
}
return null;
}
Expand Down
40 changes: 28 additions & 12 deletions src/server/proxy-liveness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,35 @@ async function attestFencedIdentity(
const secret: unknown = record ? Reflect.get(record, "attestationSecret") : undefined;
if (!record || record.pid !== pid || record.port !== port || typeof secret !== "string") return false;
const challenge = (io.createChallengeFn ?? createLocalAttestationChallenge)();
try {
const res = await fetchFn(url, {
headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: challenge },
signal: AbortSignal.timeout(timeoutMs),
});
const body = (await res.json().catch(() => null)) as HealthzIdentity | null;
// The second answer must still be the same fenced (or by now healthy) process.
if (!isOpencodexHealthz(body) && !isPackageTreeFencedHealthz(body)) return false;
if (body?.pid !== pid) return false;
return verifyLocalAttestationProof(secret, challenge, pid, port, res.headers.get(LOCAL_ATTESTATION_PROOF_HEADER));
} catch {
return false;
// One proof failure is definitive and never retried; a transport failure only means
// the listener did not answer yet, so it gets the same bounded retry the identity
// probe uses ??"did not answer" is not "not ours" (#6198). The challenge is minted
// once: a retried attempt proves the same fresh nonce, not a replayed proof.
const sleepFn = io.sleepFn ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
const nowFn = io.nowFn ?? Date.now;
const requestedAttempts = Math.trunc(io.attempts ?? 1);
const attempts = Number.isNaN(requestedAttempts)
? 1
: Math.max(1, Math.min(requestedAttempts, 5));
for (let attempt = 1; attempt <= attempts; attempt++) {
const remainingMs = io.deadlineAt === undefined ? timeoutMs : Math.min(timeoutMs, io.deadlineAt - nowFn());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (remainingMs <= 0) return false;
try {
const res = await fetchFn(url, {
headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: challenge },
signal: AbortSignal.timeout(remainingMs),
});
const body = (await res.json().catch(() => null)) as HealthzIdentity | null;
// The second answer must still be the same fenced (or by now healthy) process.
if (!isOpencodexHealthz(body) && !isPackageTreeFencedHealthz(body)) return false;
if (body?.pid !== pid) return false;
return verifyLocalAttestationProof(secret, challenge, pid, port, res.headers.get(LOCAL_ATTESTATION_PROOF_HEADER));
} catch {
if (attempt >= attempts) return false;
await sleepFn(100);
}
}
return false;
}

/** A bounded version string safe to carry beyond the untrusted health response. */
Expand Down
2 changes: 1 addition & 1 deletion structure/codex-home.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ A sibling instance is `ocx start --port <other>` while a live proxy serves the c
lease only with its own `OPENCODEX_HOME`, and still shares this Codex home, `~/.claude`, `~/.grok` and
the launchd domain with the live owner. `handleStart` marks the process through
`src/codex/sibling-start.ts` before the server binds, and the mark is one-way for the process's
lifetime. The cross-home check follows same-home discovery and precedes journal reconciliation. It reads the default home's runtime record only for a custom home, plus managed Grok and Codex loopback URLs. It accepts only an identity-checked positive PID different from this process; a sole custom-home start still syncs. The mark closes `localClientSyncAllowed` in `src/codex/desired-state.ts` with its own skip reason
lifetime. The cross-home check follows same-home discovery and precedes journal reconciliation. It reads the default home's protected runtime record only for a custom home, plus managed Grok and Codex loopback URLs as location hints. It accepts a different process only when the listener's PID matches that record and a fresh `/healthz` challenge proves possession of its attestation secret; an unauthenticated listener at a stale managed destination is not an owner, and a sole custom-home start still syncs. The mark closes `localClientSyncAllowed` in `src/codex/desired-state.ts` with its own skip reason
`sibling`, so startup sync, cache invalidation, Grok, the retained catalog writers and the native-main
lifecycle stand down (the sibling runs the no-op lifecycle, so it never contends for the owner lease;
its data-plane `auth.json` refresh still runs under the machine-wide exclusive claim). Owner-level
Expand Down
109 changes: 95 additions & 14 deletions tests/cli/sibling-home-client-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { join } from "node:path";
import { findCrossHomeOwner, markLiveHomeSibling } from "../../src/cli/cross-home-owner";
import { resetSiblingStartForTests, siblingOfLivePort } from "../../src/codex/sibling-start";
import { OCX_ROUTING_MARKER_LINE } from "../../src/codex/injected-marker";
import {
LOCAL_ATTESTATION_CHALLENGE_HEADER,
LOCAL_ATTESTATION_PROOF_HEADER,
createLocalAttestationProof,
} from "../../src/lib/local-management-attestation";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { repoPath } from "../helpers/repo-root";

Expand All @@ -13,6 +18,7 @@ const roots: string[] = [];
const servers: Array<ReturnType<typeof Bun.serve>> = [];
const children: Array<ReturnType<typeof Bun.spawn>> = [];
const detachedPids: number[] = [];
const TEST_ATTESTATION_SECRET = "A".repeat(43);

function fixture() {
const root = mkdtempSync(join(tmpdir(), "ocx-cross-home-"));
Expand All @@ -32,15 +38,31 @@ function fixture() {
return { root, home, ocx, codex, grok, claude };
}

function healthServer(pid: number | null, service = "opencodex") {
function healthServer(pid: number | null, service = "opencodex", listen: { hostname?: string; port?: number } = {}) {
let port = 0;
const server = Bun.serve({
hostname: "127.0.0.1", port: 0,
fetch: () => Response.json({ service, status: "ok", version: "0.0.0", uptime: 1, pid }),
hostname: listen.hostname ?? "127.0.0.1", port: listen.port ?? 0,
fetch: req => {
const headers = new Headers();
const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
const proof = challenge && pid !== null
? createLocalAttestationProof(TEST_ATTESTATION_SECRET, challenge, pid, port)
: null;
if (proof) headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
return Response.json({ service, status: "ok", version: "0.0.0", uptime: 1, pid }, { headers });
},
});
port = server.port;
servers.push(server);
return server.port;
}

function defaultRuntime(fx: ReturnType<typeof fixture>, pid: number, port: number) {
writeFileSync(join(fx.home, ".opencodex", "runtime-port.json"), JSON.stringify({
pid, port, attestationSecret: TEST_ATTESTATION_SECRET,
}));
}

function grokFence(port: number | string) {
return `# user content\n# >>> opencodex managed block — do not edit (removed by \`ocx stop\`) >>>\n[model_providers.opencodex]\nbase_url = "http://127.0.0.1:${port}/v1"\n# <<< opencodex managed block <<<\n`;
}
Expand Down Expand Up @@ -116,27 +138,32 @@ test("cross-home discovery marks only a live other-process owner", async () => {
};
expect(await probe()).toEqual({ marked: false, port: null });
const ownerPort = healthServer(process.pid);
defaultRuntime(fx, process.pid, ownerPort);
writeFileSync(path, grokFence(ownerPort));
expect(await probe()).toEqual({ marked: true, port: ownerPort });
});

test("large managed Grok and Codex configs still reveal their owner", async () => {
test("large managed configs do not hide an attested default-home owner", async () => {
const fx = fixture();
const port = healthServer(process.pid + 1);
const ownerPid = process.pid + 1;
const port = healthServer(ownerPid);
defaultRuntime(fx, ownerPid, port);
const grokPath = join(fx.grok, "config.toml");
const codexPath = join(fx.codex, "config.toml");
writeFileSync(grokPath, `${"# padding\n".repeat(30_000)}${grokFence(port)}`);
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
writeFileSync(grokPath, `${grokFence(port)}${"#".repeat(16 * 1024 * 1024)}`);
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull();
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
writeFileSync(grokPath, "# no managed fence\n");
writeFileSync(codexPath, `${"# padding\n".repeat(30_000)}${codexRouting(port)}`);
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
});

test("Design B marker-owned root routing reveals the owner port", async () => {
const fx = fixture();
const port = healthServer(process.pid + 1);
const ownerPid = process.pid + 1;
const port = healthServer(ownerPid);
defaultRuntime(fx, ownerPid, port);
writeFileSync(join(fx.codex, "config.toml"), [
OCX_ROUTING_MARKER_LINE,
`openai_base_url = "http://127.0.0.1:${port}/v1"`,
Expand Down Expand Up @@ -164,10 +191,50 @@ test("only a distinct live identity in the default-home record counts", async ()
const fx = fixture();
const port = healthServer(process.pid + 1);
const record = join(fx.home, ".opencodex", "runtime-port.json");
writeFileSync(record, JSON.stringify({ pid: process.pid + 1, port }));
writeFileSync(record, JSON.stringify({ pid: process.pid + 1, port, attestationSecret: TEST_ATTESTATION_SECRET }));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
writeFileSync(record, JSON.stringify({ pid: process.pid, port, attestationSecret: TEST_ATTESTATION_SECRET }));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull();
});

test("the recorded ::1 owner is found beside an IPv4 listener on the same port", async () => {
const fx = fixture();
const ownerPid = process.pid + 1;
let v6: ReturnType<typeof Bun.serve>;
try {
v6 = Bun.serve({
hostname: "::1", port: 0,
fetch: req => {
const headers = new Headers();
const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
const proof = challenge
? createLocalAttestationProof(TEST_ATTESTATION_SECRET, challenge, ownerPid, v6.port)
: null;
if (proof) headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
return Response.json({ service: "opencodex", status: "ok", version: "0.0.0", uptime: 1, pid: ownerPid }, { headers });
},
});
} catch {
return; // IPv6 loopback is unavailable on this host.
}
servers.push(v6);
const port = v6.port;
// A different opencodex-looking process holds only the IPv4 loopback of the same
// port. Its pid mismatch must not mask the recorded ::1 owner.
try {
healthServer(ownerPid + 1, "opencodex", { hostname: "127.0.0.1", port });
} catch { /* the IPv6 bind is dual-stack on this host; the owner still answers */ }
const record = join(fx.home, ".opencodex", "runtime-port.json");
const writeRecord = (hostname?: string) => writeFileSync(record, JSON.stringify({
pid: ownerPid, port, attestationSecret: TEST_ATTESTATION_SECRET,
...(hostname === undefined ? {} : { hostname }),
}));
writeRecord("::1");
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
// A record without a hostname keeps trying every loopback family instead of
// stopping at the first IPv4 answer.
writeRecord();
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
writeFileSync(record, JSON.stringify({ pid: process.pid, port }));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port); // the responder, not a stale record, owns the port
});

test.skipIf(process.platform === "win32")("a FIFO in place of a hint file cannot stall discovery", async () => {
Expand All @@ -180,9 +247,11 @@ test.skipIf(process.platform === "win32")("a FIFO in place of a hint file cannot
expect(performance.now() - started).toBeLessThan(2_000);
}, 5_000);

test("managed Grok and Codex hints accept only a different positive PID", async () => {
test("malformed managed hints do not override an attested default-home owner", async () => {
const fx = fixture();
const port = healthServer(process.pid + 1);
const ownerPid = process.pid + 1;
const port = healthServer(ownerPid);
defaultRuntime(fx, ownerPid, port);
const grokPath = join(fx.grok, "config.toml");
const codexPath = join(fx.codex, "config.toml");
writeFileSync(grokPath, grokFence(port));
Expand All @@ -191,7 +260,7 @@ test("managed Grok and Codex hints accept only a different positive PID", async
writeFileSync(codexPath, codexRouting(port));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
writeFileSync(codexPath, codexRouting("invalid"));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull();
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port);
});

test("same PID, null PID, foreign, stale and remote hints grant no sibling ownership", async () => {
Expand All @@ -218,14 +287,25 @@ test("same PID, null PID, foreign, stale and remote hints grant no sibling owner
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull();
});

test("a forged health identity without the default home's attestation grants no ownership", async () => {
const fx = fixture();
const forgedPid = 1_000_000_000;
const port = healthServer(forgedPid);
writeFileSync(join(fx.grok, "config.toml"), grokFence(port));
writeFileSync(join(fx.home, ".opencodex", "runtime-port.json"), JSON.stringify({
pid: forgedPid, port, attestationSecret: "B".repeat(43),
}));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull();
});

test("a secondary start preserves shared client bytes and records the sibling owner", async () => {
const fx = fixture();
const fakeOwnerPid = 1_000_000_000;
const ownerPort = healthServer(fakeOwnerPid);
const reservation = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("reserved") });
const secondaryPort = reservation.port;
reservation.stop(true);
writeFileSync(join(fx.home, ".opencodex", "runtime-port.json"), JSON.stringify({ pid: fakeOwnerPid, port: ownerPort }));
defaultRuntime(fx, fakeOwnerPid, ownerPort);
const grokPath = join(fx.grok, "config.toml");
const codexPath = join(fx.codex, "config.toml");
const claudePath = join(fx.claude, "agents", "ocx-existing.md");
Expand Down Expand Up @@ -254,6 +334,7 @@ test("a secondary start preserves shared client bytes and records the sibling ow
test("a secondary ensure parent preserves shared Grok, Codex and Claude agent bytes", async () => {
const fx = fixture();
const ownerPort = healthServer(process.pid);
defaultRuntime(fx, process.pid, ownerPort);
const reservation = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("reserved") });
const secondaryPort = reservation.port;
reservation.stop(true);
Expand Down
Loading
Loading