Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 20 additions & 4 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 { probePortOwner, proveLiveProxyOwnedByHome, 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,15 @@ export async function findCrossHomeOwner(options: { homeDir?: string } = {}): Pr
} catch { /* an absent or invalid client home is not owner evidence */ }

for (const port of candidates) {
// 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;
const owner = await probePortOwner(port, {}, START_OWNERSHIP_LIVENESS);
if (owner && Number.isSafeInteger(owner.pid) && owner.pid! > 0 && owner.pid !== process.pid) return port;
if (owner?.pid !== defaultRuntime.pid) continue;
if (await proveLiveProxyOwnedByHome(
{ ...owner, port, source: "runtime" },
{ readRuntimeFn: () => defaultRuntime },
)) return port;
}
return null;
}
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
65 changes: 53 additions & 12 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 @@ -33,14 +39,30 @@ function fixture() {
}

function healthServer(pid: number | null, service = "opencodex") {
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 }),
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,10 @@ 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 }));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBe(port); // the responder, not a stale record, owns the port
writeFileSync(record, JSON.stringify({ pid: process.pid, port, attestationSecret: TEST_ATTESTATION_SECRET }));
expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull();
});

test.skipIf(process.platform === "win32")("a FIFO in place of a hint file cannot stall discovery", async () => {
Expand All @@ -180,9 +207,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 +220,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 +247,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 +294,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