Skip to content

Commit e335e84

Browse files
committed
fix(google,kiro): strip Codex's Responses-only encrypted marker from tool schemas (#85)
Upstream openai/codex 5f4d06ef stamps encrypted:true on v2 collaboration tool schemas (spawn_agent/send_message/followup_task message). Gemini/CCA rejects the whole request with 400 'Unknown name'; Kiro/Bedrock validators reject an undocumented narrow schema subset, so strip it there too. - google-tool-schema: add encrypted to DROPPED_SCHEMA_KEYS - kiro-tools: add encrypted to KIRO_REJECTED_SCHEMA_KEYS (property named 'encrypted' still survives via SCHEMA_MAP_KEYS) - regression tests: sanitizer-level, google adapter end-to-end, kiro wire
1 parent 4ccc6da commit e335e84

5 files changed

Lines changed: 84 additions & 0 deletions

File tree

src/adapters/google-tool-schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ type Schema = Record<string, unknown>;
44
// emits full JSON-Schema (draft 2020-12) tool definitions, so passing them through verbatim makes
55
// CCA reject the whole request with "Request contains an invalid argument" / "Unknown name ...".
66
// Every keyword below was confirmed live against the Antigravity backend to trigger a 400.
7+
// `encrypted` is Codex's Responses-only marker (openai/codex 5f4d06ef, PR #26210) stamped on v2
8+
// collaboration tool schemas (spawn_agent/send_message/followup_task `message`); CCA rejects it
9+
// with 400 "Unknown name \"encrypted\"" (issue #85). It is an annotation for the ChatGPT backend
10+
// only, so dropping it never changes tool behavior.
711
const DROPPED_SCHEMA_KEYS = new Set([
812
"$schema", "$id", "$comment", "$ref", "$defs", "definitions",
913
"examples", "patternProperties", "if", "then", "else",
1014
"uniqueItems", "additionalItems", "unevaluatedProperties", "unevaluatedItems",
1115
"dependentRequired", "dependentSchemas", "propertyNames", "contains",
16+
"encrypted",
1217
]);
1318

1419
const MAX_DEREF_DEPTH = 64;

src/adapters/kiro-tools.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ const KIRO_REJECTED_SCHEMA_KEYS = new Set([
4141
"contains",
4242
"unevaluatedProperties",
4343
"unevaluatedItems",
44+
// Codex's Responses-only `encrypted: true` marker (openai/codex 5f4d06ef) stamped on v2
45+
// collaboration tool schemas. Kiro/Bedrock validators reject a narrower, undocumented schema
46+
// subset (issue #85 class); the marker is a ChatGPT-backend annotation with no meaning here.
47+
"encrypted",
4448
]);
4549

4650
// Keys whose values are maps of *property/definition name -> schema* (not schema keywords). Their

tests/google-adapter.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,29 @@ describe("google adapter — tool result images", () => {
8383
});
8484

8585
describe("google adapter — tool-call ids on the wire", () => {
86+
test("v2 collaboration encrypted marker never reaches functionDeclarations (issue #85)", async () => {
87+
// Codex Desktop v2 stamps `encrypted: true` on collaboration message properties; CCA/Gemini
88+
// rejects the whole request with 400 "Unknown name". The sanitizer must strip it end-to-end.
89+
const collabParams = {
90+
type: "object",
91+
properties: {
92+
target: { type: "string", description: "Agent id." },
93+
message: { type: "string", description: "Message text.", encrypted: true },
94+
},
95+
required: ["target", "message"],
96+
additionalProperties: false,
97+
};
98+
const body = await geminiBody(parsedWith(
99+
[{ role: "user", content: "hi" }],
100+
[{ name: "followup_task", namespace: "collaboration", description: "Send follow-up", parameters: collabParams }],
101+
));
102+
const decls = (body.tools as { functionDeclarations: Record<string, unknown>[] }[])[0].functionDeclarations;
103+
expect(decls.length).toBe(1);
104+
expect(JSON.stringify(decls)).not.toContain("encrypted");
105+
const params = decls[0].parameters as { properties: Record<string, Record<string, unknown>> };
106+
expect(params.properties.message.type).toBe("string");
107+
});
108+
86109
test("systemInstruction includes the non-OpenAI tool catalog nudge when tools are present", async () => {
87110
const body = await geminiBody(parsedWith(
88111
[{ role: "user", content: "find files" }],

tests/google-tool-schema.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ describe("sanitizeGeminiToolParameters", () => {
2929
expect((props.b.items as Record<string, unknown>).type).toBe("string");
3030
});
3131

32+
test("drops Codex's Responses-only encrypted marker recursively (issue #85)", () => {
33+
// Upstream codex stamps `encrypted: true` on v2 collaboration tool schemas
34+
// (spawn_agent/send_message/followup_task `message`); CCA 400s on the unknown name.
35+
const input = {
36+
type: "object",
37+
properties: {
38+
message: { type: "string", description: "...", encrypted: true },
39+
nested: {
40+
type: "object",
41+
properties: { inner: { type: "string", encrypted: false } },
42+
},
43+
list: { type: "array", items: { type: "string", encrypted: true } },
44+
},
45+
required: ["message"],
46+
};
47+
const before = JSON.stringify(input);
48+
const out = sanitizeGeminiToolParameters(input);
49+
const props = out.properties as Record<string, Record<string, unknown>>;
50+
expect(props.message.encrypted).toBeUndefined();
51+
expect(props.message.type).toBe("string");
52+
const inner = (props.nested.properties as Record<string, Record<string, unknown>>).inner;
53+
expect(inner.encrypted).toBeUndefined(); // `encrypted: false` is equally unsupported
54+
expect((props.list.items as Record<string, unknown>).encrypted).toBeUndefined();
55+
expect(out.required).toEqual(["message"]);
56+
expect(JSON.stringify(input)).toBe(before); // input object is never mutated
57+
});
58+
3259
test("collapses type arrays to a single nullable type", () => {
3360
const out = sanitizeGeminiToolParameters({
3461
type: "object",

tests/kiro-adapter.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,31 @@ describe("kiro adapter — buildRequest", () => {
303303
expect(schema.required).toEqual(["filename", "note"]);
304304
});
305305

306+
test("Codex's Responses-only encrypted marker is stripped from v2 collaboration schemas", () => {
307+
// openai/codex 5f4d06ef stamps `encrypted: true` on spawn_agent/send_message/followup_task
308+
// `message` properties (issue #85 class). Kiro/Bedrock validators reject unknown keywords, and
309+
// the marker only means something to the ChatGPT Responses backend.
310+
const parameters = {
311+
type: "object",
312+
properties: {
313+
target: { type: "string" },
314+
message: { type: "string", description: "Message text.", encrypted: true },
315+
// A property literally named "encrypted" must survive as a property.
316+
encrypted: { type: "boolean" },
317+
},
318+
required: ["target", "message"],
319+
};
320+
const { body } = createKiroAdapter(provider).buildRequest(
321+
parsedWith([{ role: "user", content: "hi" }], [{ name: "followup_task", namespace: "collaboration", description: "Send follow-up", parameters }]),
322+
);
323+
const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json;
324+
325+
expect(schema.properties.message.encrypted).toBeUndefined();
326+
expect(schema.properties.message.type).toBe("string");
327+
expect(schema.properties.encrypted).toEqual({ type: "boolean" });
328+
expect(schema.required).toEqual(["target", "message"]);
329+
});
330+
306331
test("validation-only applicator keywords are dropped while $defs are preserved", () => {
307332
const parameters = {
308333
type: "object",

0 commit comments

Comments
 (0)