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
4 changes: 2 additions & 2 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ function resolveVertexApiKey(optKey?: string): string | undefined {
/** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients
* that omit it. The replay store hashes this value and never retains the raw session identifier. */
function vertexReplaySessionId(parsed: OcxParsedRequest): string {
const promptCacheKey = parsed.options.promptCacheKey?.trim();
return promptCacheKey || antigravitySessionId(parsed);
const threadId = parsed._clientThreadId?.trim();
return threadId || antigravitySessionId(parsed);
Comment on lines +57 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Hash the client thread ID before using it as the replay-session key.

Line 58 returns the raw x-codex-parent-thread-id value. vertexReplaySession receives it at Line 454 and passes it to applyAntigravityReplay at Lines 459-463, so the replay cache retains the raw client identifier.

Use the same one-way hash used by antigravitySessionId, and keep the existing fallback for an absent or blank ID.

Proposed fix
 function vertexReplaySessionId(parsed: OcxParsedRequest): string {
   const threadId = parsed._clientThreadId?.trim();
-  return threadId || antigravitySessionId(parsed);
+  if (!threadId) return antigravitySessionId(parsed);
+  const digest = createHash("sha256").update(threadId, "utf8").digest();
+  const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
+  return `-${masked.toString()}`;
 }

Based on the PR objective to avoid persisting raw thread identifiers and the supplied replay-session contract.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const threadId = parsed._clientThreadId?.trim();
return threadId || antigravitySessionId(parsed);
const threadId = parsed._clientThreadId?.trim();
if (!threadId) return antigravitySessionId(parsed);
const digest = createHash("sha256").update(threadId, "utf8").digest();
const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
return `-${masked.toString()}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/google.ts` around lines 57 - 58, Update the thread ID selection
near antigravitySessionId so a trimmed client thread ID is transformed with the
same one-way hashing used by antigravitySessionId before being returned as the
replay-session key. Preserve the existing fallback to
antigravitySessionId(parsed) when the client ID is absent or blank, and ensure
vertexReplaySession/applyAntigravityReplay receive only the hashed value.

}

/**
Expand Down
38 changes: 38 additions & 0 deletions tests/google-vertex-thought-signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ const continuation = () => request([
},
], false);

function scopedReplayRequest(
parsed: OcxParsedRequest,
threadId: string | undefined,
promptCacheKey: string | undefined,
): OcxParsedRequest {
if (threadId !== undefined) parsed._clientThreadId = threadId;
if (promptCacheKey !== undefined) parsed.options.promptCacheKey = promptCacheKey;
return parsed;
}

function vertexResponseBody(): Record<string, unknown> {
return {
candidates: [{
Expand Down Expand Up @@ -107,6 +117,34 @@ describe("Vertex thought-signature continuation (#1254)", () => {
expect(replayedFunctionCall(followup.body as string).thoughtSignature).toBe(SIGNATURE);
});

test("#1312: shared prompt cache keys cannot cross client-thread replay namespaces", async () => {
const first = scopedReplayRequest(firstTurn(false), "thread-a", "shared-cache-cohort");
const firstAdapter = createGoogleAdapter(provider);
await firstAdapter.buildRequest(first);
await firstAdapter.parseResponse!(new Response(JSON.stringify(vertexResponseBody())));

const otherThread = await createGoogleAdapter(provider).buildRequest(
scopedReplayRequest(continuation(), "thread-b", "shared-cache-cohort"),
);
expect(replayedFunctionCall(otherThread.body as string).thoughtSignature).toBeUndefined();

const originalThread = await createGoogleAdapter(provider).buildRequest(
scopedReplayRequest(continuation(), "thread-a", "different-cache-cohort"),
);
expect(replayedFunctionCall(originalThread.body as string).thoughtSignature).toBe(SIGNATURE);
});

test("#1312: threadless clients keep deterministic replay regardless of prompt cache key", async () => {
const firstAdapter = createGoogleAdapter(provider);
await firstAdapter.buildRequest(scopedReplayRequest(firstTurn(false), undefined, "cohort-a"));
await firstAdapter.parseResponse!(new Response(JSON.stringify(vertexResponseBody())));

const followup = await createGoogleAdapter(provider).buildRequest(
scopedReplayRequest(continuation(), undefined, "cohort-b"),
);
expect(replayedFunctionCall(followup.body as string).thoughtSignature).toBe(SIGNATURE);
});

test("Vertex signatures cannot enter the Antigravity replay namespace", async () => {
const first = firstTurn(false);
const adapter = createGoogleAdapter(provider);
Expand Down
Loading