Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
14 changes: 13 additions & 1 deletion src/cli/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { HubStateOAuthEntry, HubStateProvider } from "../remote/hub-state";
import type { HubStateSource } from "../client/hub-state";
import { readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
import { tokenCollidesWithAdmin } from "../lib/admin-secrets";
import { readClientConnectionState } from "../client/state";
export { proxyHealthFailureReason, isConnectionRefused, isUncleanExitEvidence, probeUncleanExitState } from "./status-probes";
export type { ListenTarget } from "./status-probes";
import { checkProxyHealth, probeUncleanExitState, type ListenTarget } from "./status-probes";
Expand Down Expand Up @@ -340,14 +341,25 @@ export async function collectRemoteHubStatus(
return disconnectedRemoteHubStatus();
}
const { resolveHubState } = await import("../client/hub-state");
// Re-read the connection and token as one ownership observation. The connection passed above
// is an earlier status snapshot; combining its URL with an independently read token can send a
// newly rotated or reconnected credential to the old hub. A transition between these two reads
// is also safe: the new token's fingerprint cannot match the old connection generation.
const currentConnection = readClientConnectionState();
const token = readServiceApiTokenState();
const tokenBelongsToSnapshot = currentConnection.kind === "connected"
&& currentConnection.value.serverUrl === connection.serverUrl
&& currentConnection.value.apiKeyId === connection.apiKeyId
&& currentConnection.value.connectedAt === connection.connectedAt
&& token.kind === "present"
&& token.fingerprint === currentConnection.value.tokenFingerprint;
const resolved = await resolveHubState({
owner: {
serverUrl: connection.serverUrl,
apiKeyId: connection.apiKeyId,
connectedAt: connection.connectedAt,
},
token: token.kind === "present" ? token.token : null,
token: tokenBelongsToSnapshot ? token.token : null,
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
...(options.now === undefined ? {} : { now: options.now }),
Expand Down
31 changes: 31 additions & 0 deletions tests/cli/cli-status-hub-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ function jsonFetch(body: unknown): typeof fetch {
})) as unknown as typeof fetch;
}

function rejectingFetch(onCall: () => void): typeof fetch {
return (async () => {
onCall();
throw new Error("fetch must not be called");
}) as unknown as typeof fetch;
}

beforeEach(() => {
testHome = mkdtempSync(join(tmpdir(), "ocx-status-hub-"));
process.env.OPENCODEX_HOME = testHome;
Expand Down Expand Up @@ -126,6 +133,30 @@ describe("collectRemoteHubStatus", () => {
expect(remoteHub.reason).toContain("data-plane token");
});

test("a mismatched token is never sent to the snapshotted hub", async () => {
writeConnectedHome(testHome, "https://hub.example.test:8443");
writeFileSync(join(testHome, "service-api-token"), "token-from-another-connection", { mode: 0o600 });
let fetchCalls = 0;
const remoteHub = await collectRemoteHubStatus(
{ state: "connected", serverUrl: "https://hub.example.test:8443", apiKeyId: "status-hub-state", connectedAt: "2026-09-06T00:00:00.000Z" },
{ fetchImpl: rejectingFetch(() => fetchCalls++) },
);
expect(fetchCalls).toBe(0);
expect(remoteHub.stateSource).toBe("unavailable");
expect(remoteHub.reason).toContain("data-plane token");
});

test("a new connection's token is never sent to an earlier connection snapshot", async () => {
const snapshot = { state: "connected" as const, serverUrl: "https://hub-a.example.test", apiKeyId: "status-hub-state", connectedAt: "2026-09-06T00:00:00.000Z" };
writeConnectedHome(testHome, "https://hub-b.example.test");
let fetchCalls = 0;
const remoteHub = await collectRemoteHubStatus(snapshot, {
fetchImpl: rejectingFetch(() => fetchCalls++),
});
expect(fetchCalls).toBe(0);
expect(remoteHub.stateSource).toBe("unavailable");
});

test("a disconnected machine asks the hub nothing", async () => {
const remoteHub = await collectRemoteHubStatus({ state: "disconnected" });
expect(remoteHub).toEqual(disconnectedRemoteHubStatus());
Expand Down
Loading