@bitkyc08/opencodex 2.6.7 → 2.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ type Schema = Record<string, unknown>;
2
+
3
+ // Gemini / Antigravity (CCA) accept only an OpenAPI-3.0 subset for function `parameters`. Codex
4
+ // emits full JSON-Schema (draft 2020-12) tool definitions, so passing them through verbatim makes
5
+ // CCA reject the whole request with "Request contains an invalid argument" / "Unknown name ...".
6
+ // Every keyword below was confirmed live against the Antigravity backend to trigger a 400.
7
+ const DROPPED_SCHEMA_KEYS = new Set([
8
+ "$schema", "$id", "$comment", "$ref", "$defs", "definitions",
9
+ "examples", "patternProperties", "if", "then", "else",
10
+ "uniqueItems", "additionalItems", "unevaluatedProperties", "unevaluatedItems",
11
+ "dependentRequired", "dependentSchemas", "propertyNames", "contains",
12
+ ]);
13
+
14
+ const MAX_DEREF_DEPTH = 64;
15
+
16
+ function resolveRef(ref: string, defs: Map<string, unknown>): unknown {
17
+ // Only local pointers into the schema's own $defs/definitions are supported (e.g.
18
+ // "#/$defs/Foo"). Anything else cannot be inlined, so it collapses to an unconstrained object.
19
+ const match = /^#\/(?:\$defs|definitions)\/(.+)$/.exec(ref);
20
+ if (!match) return undefined;
21
+ return defs.get(decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~")));
22
+ }
23
+
24
+ function collectDefs(root: unknown, defs: Map<string, unknown>): void {
25
+ if (!root || typeof root !== "object") return;
26
+ for (const bag of ["$defs", "definitions"] as const) {
27
+ const group = (root as Schema)[bag];
28
+ if (group && typeof group === "object" && !Array.isArray(group)) {
29
+ for (const [name, value] of Object.entries(group as Schema)) {
30
+ if (!defs.has(name)) defs.set(name, value);
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ function normalizeType(value: unknown, out: Schema): void {
37
+ // JSON-Schema allows `type` to be an array (e.g. ["string","null"]); OpenAPI 3.0 does not.
38
+ // Collapse to the first non-null type and mark the field nullable when "null" was present.
39
+ if (!Array.isArray(value)) {
40
+ out.type = value;
41
+ return;
42
+ }
43
+ const nonNull = value.filter(t => t !== "null");
44
+ if (value.includes("null")) out.nullable = true;
45
+ if (nonNull.length > 0) out.type = nonNull[0];
46
+ }
47
+
48
+ function sanitize(node: unknown, defs: Map<string, unknown>, depth: number): unknown {
49
+ if (Array.isArray(node)) return node.map(item => sanitize(item, defs, depth));
50
+ if (!node || typeof node !== "object") return node;
51
+ const input = node as Schema;
52
+
53
+ if (typeof input.$ref === "string" && depth < MAX_DEREF_DEPTH) {
54
+ const target = resolveRef(input.$ref, defs);
55
+ if (target && typeof target === "object") {
56
+ const merged: Schema = { ...(target as Schema) };
57
+ for (const [key, value] of Object.entries(input)) {
58
+ if (key !== "$ref") merged[key] = value;
59
+ }
60
+ return sanitize(merged, defs, depth + 1);
61
+ }
62
+ }
63
+
64
+ const out: Schema = {};
65
+ for (const [key, value] of Object.entries(input)) {
66
+ if (DROPPED_SCHEMA_KEYS.has(key)) continue;
67
+ if (key === "type") { normalizeType(value, out); continue; }
68
+ if (key === "const") { out.enum = [value]; continue; }
69
+ if (key === "exclusiveMinimum" && typeof value === "number") { out.minimum = value; continue; }
70
+ if (key === "exclusiveMaximum" && typeof value === "number") { out.maximum = value; continue; }
71
+ if (key === "additionalProperties") {
72
+ // A boolean additionalProperties is accepted, but a nested schema is only meaningful with
73
+ // its own sanitize pass.
74
+ out.additionalProperties = typeof value === "boolean" ? value : sanitize(value, defs, depth);
75
+ continue;
76
+ }
77
+ out[key] = sanitize(value, defs, depth);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ export function sanitizeGeminiToolParameters(parameters: unknown): Record<string, unknown> {
83
+ const defs = new Map<string, unknown>();
84
+ collectDefs(parameters, defs);
85
+ const result = sanitize(parameters, defs, 0);
86
+ return result && typeof result === "object" && !Array.isArray(result)
87
+ ? result as Record<string, unknown>
88
+ : { type: "object" };
89
+ }
@@ -0,0 +1,13 @@
1
+ import { redactSecretString } from "../redact";
2
+
3
+ /** Gemini/Vertex finishReason values that mean the turn was cut off, not cleanly stopped. */
4
+ const TRUNCATION_REASONS = new Set(["MAX_TOKENS", "MALFORMED_FUNCTION_CALL"]);
5
+
6
+ export function isVertexTruncationReason(finishReason: string | undefined): boolean {
7
+ return finishReason !== undefined && TRUNCATION_REASONS.has(finishReason);
8
+ }
9
+
10
+ export function vertexTruncationErrorMessage(reason?: string): string {
11
+ const suffix = reason ? ` (${redactSecretString(reason).slice(0, 160)})` : "";
12
+ return `Vertex AI response truncated upstream before the turn completed${suffix}`;
13
+ }
@@ -1,4 +1,4 @@
1
- import type { ProviderAdapter } from "./base";
1
+ import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
2
  import { debugDroppedFrame } from "../debug";
3
3
  import type {
4
4
  AdapterEvent,
@@ -12,6 +12,31 @@ import type {
12
12
  } from "../types";
13
13
  import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types";
14
14
  import { contentPartsToText, parseDataUrl } from "./image";
15
+ import { getVertexAccessToken } from "../lib/gcp-adc";
16
+ import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
17
+ import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google-truncation";
18
+ import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
19
+ import { sanitizeGeminiToolParameters } from "./google-tool-schema";
20
+ import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
21
+
22
+ // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
23
+ // tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
24
+ // while still driving tools to completion. The FINAL answer is explicitly exempt so task output is
25
+ // not truncated. Appended to systemInstruction for the `google` adapter only, so non-Google
26
+ // providers are unaffected.
27
+ const GOOGLE_BREVITY_INSTRUCTION = [
28
+ "Output style for this session:",
29
+ "- While you are still working (between tool calls), keep any text you emit to a single short line; do not narrate at length.",
30
+ "- Do detailed reasoning internally, not as visible intermediate output.",
31
+ "- Prefer taking the next tool action over explaining; keep calling tools until the task is complete.",
32
+ "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
33
+ ].join("\n");
34
+
35
+ /** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
36
+ function resolveVertexApiKey(optKey?: string): string | undefined {
37
+ const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
38
+ return realKey || process.env.GOOGLE_CLOUD_API_KEY;
39
+ }
15
40
 
16
41
  /**
17
42
  * Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
@@ -30,9 +55,8 @@ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
30
55
  }
31
56
 
32
57
  function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
33
- const systemInstruction = parsed.context.systemPrompt?.length
34
- ? { parts: [{ text: parsed.context.systemPrompt.join("\n\n") }] }
35
- : undefined;
58
+ const systemText = [...(parsed.context.systemPrompt ?? []), GOOGLE_BREVITY_INSTRUCTION].join("\n\n");
59
+ const systemInstruction = { parts: [{ text: systemText }] };
36
60
 
37
61
  const contents: unknown[] = [];
38
62
 
@@ -63,7 +87,14 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
63
87
  if (p.type === "text") parts.push({ text: (p as OcxTextContent).text });
64
88
  else if (p.type === "toolCall") {
65
89
  const tc = p as OcxToolCall;
66
- parts.push({ functionCall: { name: namespacedToolName(tc.namespace, tc.name), args: tc.arguments } });
90
+ // Preserve the thought signature on the function-call part so Antigravity/Gemini-3
91
+ // reasoning continuity survives history-driven (stateless) turns, not just same-process
92
+ // streaming covered by the replay cache. Only forward a REAL upstream signature — the
93
+ // Responses parser also stashes synthetic item ids (`fc_...`) on this field, and sending
94
+ // those as a thoughtSignature breaks continuity (the replay cache supplies the real one).
95
+ const part: Record<string, unknown> = { functionCall: { name: namespacedToolName(tc.namespace, tc.name), args: tc.arguments } };
96
+ if (isLikelyRealThoughtSignature(tc.thoughtSignature)) part.thoughtSignature = tc.thoughtSignature;
97
+ parts.push(part);
67
98
  }
68
99
  }
69
100
  contents.push({ role: "model", parts });
@@ -100,7 +131,7 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
100
131
  functionDeclarations: tools.map(t => ({
101
132
  name: namespacedToolName(t.namespace, t.name),
102
133
  description: t.description,
103
- parameters: t.parameters,
134
+ parameters: sanitizeGeminiToolParameters(t.parameters),
104
135
  })),
105
136
  }];
106
137
  }
@@ -116,10 +147,23 @@ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage |
116
147
  }
117
148
 
118
149
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
150
+ // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
151
+ // can stash the CCA model/session for parseStream's reasoning-replay observation.
152
+ let antigravityModel: string | undefined;
153
+ let antigravitySession: string | undefined;
119
154
  return {
120
155
  name: "google",
121
156
 
122
- buildRequest(parsed: OcxParsedRequest) {
157
+ // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors. AI-Studio
158
+ // Gemini keeps the default server fetch path (fetchResponse stays undefined so server.ts falls back).
159
+ ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
160
+ ? {
161
+ fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
162
+ (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
163
+ }
164
+ : {}),
165
+
166
+ async buildRequest(parsed: OcxParsedRequest) {
123
167
  const { systemInstruction, contents } = messagesToGeminiFormat(parsed);
124
168
  const tools = toolsToGeminiFormat(parsed);
125
169
 
@@ -136,12 +180,67 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
136
180
 
137
181
  const method = parsed.stream ? "streamGenerateContent" : "generateContent";
138
182
  const streamParam = parsed.stream ? "?alt=sse" : "";
139
- const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
140
-
141
183
  const headers: Record<string, string> = { "Content-Type": "application/json" };
142
- if (provider.apiKey) headers["x-goog-api-key"] = provider.apiKey;
143
184
  if (provider.headers) Object.assign(headers, provider.headers);
144
185
 
186
+ if (provider.googleMode === "cloud-code-assist") {
187
+ // Google Antigravity (Cloud Code Assist): wrap the flat Gemini body in the CCA envelope.
188
+ const base = provider.baseUrl || "https://daily-cloudcode-pa.googleapis.com";
189
+ const url = `${base}/v1internal:${method}${streamParam}`;
190
+ const project = provider.project;
191
+ if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
192
+ const sessionId = antigravitySessionId(parsed);
193
+ antigravityModel = parsed.modelId;
194
+ antigravitySession = sessionId;
195
+ // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
196
+ // sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures.
197
+ if (Array.isArray((body as { contents?: unknown[] }).contents)) {
198
+ const contents = (body as { contents: unknown[] }).contents;
199
+ if (antigravityUsesReplayCache(parsed.modelId)) {
200
+ applyAntigravityReplay(parsed.modelId, sessionId, contents);
201
+ } else {
202
+ sanitizeAntigravityClaudeSignatures(contents);
203
+ }
204
+ }
205
+ // The CCA client serializes `session_id` (snake_case); send both spellings so the
206
+ // deterministic session id is honored regardless of which the backend accepts.
207
+ const request: Record<string, unknown> = { ...body, sessionId, session_id: sessionId };
208
+ const envelope = {
209
+ model: parsed.modelId,
210
+ userAgent: ANTIGRAVITY_REQUEST_UA,
211
+ requestType: "agent",
212
+ project,
213
+ requestId: `agent-${crypto.randomUUID()}`,
214
+ request,
215
+ };
216
+ headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
217
+ if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
218
+ return { url, method: "POST", headers, body: JSON.stringify(envelope) };
219
+ }
220
+
221
+ if (provider.googleMode === "vertex") {
222
+ // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
223
+ const apiKey = resolveVertexApiKey(provider.apiKey);
224
+ if (apiKey) {
225
+ const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
226
+ headers["x-goog-api-key"] = apiKey;
227
+ return { url, method: "POST", headers, body: JSON.stringify(body) };
228
+ }
229
+ const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
230
+ if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
231
+ const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
232
+ if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
233
+ const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
234
+ const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
235
+ const token = await getVertexAccessToken();
236
+ headers["Authorization"] = `Bearer ${token}`;
237
+ return { url, method: "POST", headers, body: JSON.stringify(body) };
238
+ }
239
+
240
+ // ai-studio (default): Generative Language API + x-goog-api-key.
241
+ const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
242
+ if (provider.apiKey) headers["x-goog-api-key"] = provider.apiKey;
243
+
145
244
  return { url, method: "POST", headers, body: JSON.stringify(body) };
146
245
  },
147
246
 
@@ -155,6 +254,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
155
254
  const decoder = new TextDecoder();
156
255
  let buffer = "";
157
256
  let pendingUsage: OcxUsage | undefined;
257
+ let toolCallsStarted = 0;
258
+ let lastFinishReason: string | undefined;
158
259
 
159
260
  try {
160
261
  while (true) {
@@ -176,14 +277,38 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
176
277
  // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
177
278
  if (chunk.error) {
178
279
  const err = chunk.error as { message?: string } | undefined;
280
+ // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
281
+ // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
282
+ if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession
283
+ && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
284
+ clearAntigravityReplay(antigravityModel, antigravitySession);
285
+ }
179
286
  yield { type: "error", message: err?.message ?? "upstream error" };
180
287
  return;
181
288
  }
182
289
 
183
- const candidates = chunk.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
290
+ // Antigravity (CCA) nests the standard Gemini payload under `response`.
291
+ const root = (provider.googleMode === "cloud-code-assist"
292
+ ? (chunk.response as Record<string, unknown> | undefined) ?? chunk
293
+ : chunk);
294
+ // usageMetadata is a top-level field independent of candidates; read it BEFORE the
295
+ // candidates guard so a usage-only final chunk is not dropped.
296
+ const usageMeta = root.usageMetadata as Record<string, number> | undefined;
297
+ if (usageMeta) {
298
+ // Accumulate usage; emit a single terminal `done` post-loop so usage is never
299
+ // dropped on EOF and the stream never yields two `done` events.
300
+ pendingUsage = usageFromGemini(usageMeta);
301
+ }
302
+ const candidates = root.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
184
303
  if (!candidates?.length) continue;
185
304
 
305
+ lastFinishReason = candidates[0].finishReason ?? lastFinishReason;
306
+
186
307
  const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
308
+ // Antigravity reasoning-replay: record thoughtSignatures from the model parts for the next turn.
309
+ if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel && antigravitySession) {
310
+ observeAntigravityReplay(antigravityModel, antigravitySession, parts as unknown[]);
311
+ }
187
312
  if (parts) {
188
313
  for (const part of parts) {
189
314
  if (part.text) {
@@ -191,21 +316,22 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
191
316
  }
192
317
  if (part.functionCall) {
193
318
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
319
+ toolCallsStarted++;
194
320
  yield { type: "tool_call_start", id, name: part.functionCall.name };
195
321
  yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
196
322
  yield { type: "tool_call_end" };
197
323
  }
198
324
  }
199
325
  }
200
-
201
- const usageMeta = chunk.usageMetadata as Record<string, number> | undefined;
202
- if (usageMeta) {
203
- // Accumulate usage; emit a single terminal `done` post-loop so usage is never
204
- // dropped on EOF and the stream never yields two `done` events.
205
- pendingUsage = usageFromGemini(usageMeta);
206
- }
207
326
  }
208
327
  }
328
+ // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
329
+ // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
330
+ if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
331
+ && toolCallsStarted > 0 && isVertexTruncationReason(lastFinishReason)) {
332
+ yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
333
+ return;
334
+ }
209
335
  yield { type: "done", usage: pendingUsage };
210
336
  } finally {
211
337
  reader.releaseLock();
@@ -213,15 +339,25 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
213
339
  },
214
340
 
215
341
  async parseResponse(response: Response): Promise<AdapterEvent[]> {
216
- const json = await response.json() as Record<string, unknown>;
342
+ const raw = await response.json() as Record<string, unknown>;
343
+ // Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
344
+ const json = (provider.googleMode === "cloud-code-assist"
345
+ ? (raw.response as Record<string, unknown> | undefined) ?? raw
346
+ : raw);
217
347
  const events: AdapterEvent[] = [];
218
348
 
219
- const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] } }[] | undefined;
349
+ const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] }; finishReason?: string }[] | undefined;
350
+ let toolCallsStarted = 0;
220
351
  if (candidates?.[0]?.content?.parts) {
352
+ // Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
353
+ if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession) {
354
+ observeAntigravityReplay(antigravityModel, antigravitySession, candidates[0].content.parts as unknown[]);
355
+ }
221
356
  for (const part of candidates[0].content.parts) {
222
357
  if (part.text) events.push({ type: "text_delta", text: part.text });
223
358
  if (part.functionCall) {
224
359
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
360
+ toolCallsStarted++;
225
361
  events.push({ type: "tool_call_start", id, name: part.functionCall.name });
226
362
  events.push({ type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) });
227
363
  events.push({ type: "tool_call_end" });
@@ -229,6 +365,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
229
365
  }
230
366
  }
231
367
 
368
+ // Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
369
+ // (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
370
+ if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
371
+ && toolCallsStarted > 0 && isVertexTruncationReason(candidates?.[0]?.finishReason)) {
372
+ return [{ type: "error", message: vertexTruncationErrorMessage(candidates?.[0]?.finishReason) }];
373
+ }
374
+
232
375
  const usage = json.usageMetadata as Record<string, number> | undefined;
233
376
  events.push({
234
377
  type: "done",
@@ -3,14 +3,67 @@ import { namespacedToolName } from "../types";
3
3
 
4
4
  const MAX_KIRO_TOOL_DESCRIPTION = 1024;
5
5
 
6
+ // JSON Schema validation/annotation keywords that Kiro's runtimeservice tool-spec validator
7
+ // rejects ("ValidationException: Invalid tool use format."). Codex's built-in tools omit these,
8
+ // but the `memories__*` tools (add_ad_hoc_note/read/search/list) emit pattern/length/range
9
+ // constraints via schemars, which trip the validator. Strip them everywhere in the schema tree;
10
+ // the constraints are advisory for the model, so dropping them does not change tool behavior.
11
+ const KIRO_REJECTED_SCHEMA_KEYS = new Set([
12
+ "additionalProperties",
13
+ "pattern",
14
+ "format",
15
+ "minLength",
16
+ "maxLength",
17
+ "minimum",
18
+ "maximum",
19
+ "exclusiveMinimum",
20
+ "exclusiveMaximum",
21
+ "multipleOf",
22
+ "minItems",
23
+ "maxItems",
24
+ "uniqueItems",
25
+ "minProperties",
26
+ "maxProperties",
27
+ "contentEncoding",
28
+ "contentMediaType",
29
+ "$schema",
30
+ // Validation-only composition/applicator keywords that Bedrock/Kiro do not support. Unlike
31
+ // `properties`/`$defs`, these are not plain property->schema maps the model needs, so they are
32
+ // dropped outright rather than recursed into.
33
+ "patternProperties",
34
+ "propertyNames",
35
+ "dependentSchemas",
36
+ "dependentRequired",
37
+ "if",
38
+ "then",
39
+ "else",
40
+ "contains",
41
+ "unevaluatedProperties",
42
+ "unevaluatedItems",
43
+ ]);
44
+
45
+ // Keys whose values are maps of *property/definition name -> schema* (not schema keywords). Their
46
+ // child keys must never be treated as schema keywords, or a legitimate property named e.g.
47
+ // "format"/"pattern" would be deleted. We recurse into the value schemas but keep every name intact.
48
+ const SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
49
+
50
+ function sanitizeSchemaMap(value: unknown): unknown {
51
+ if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeKiroSchema(value);
52
+ const out: Record<string, unknown> = {};
53
+ for (const [name, child] of Object.entries(value as Record<string, unknown>)) {
54
+ out[name] = sanitizeKiroSchema(child);
55
+ }
56
+ return out;
57
+ }
58
+
6
59
  function sanitizeKiroSchema(value: unknown): unknown {
7
60
  if (Array.isArray(value)) return value.map(sanitizeKiroSchema);
8
61
  if (!value || typeof value !== "object") return value;
9
62
  const out: Record<string, unknown> = {};
10
63
  for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
11
- if (key === "additionalProperties") continue;
64
+ if (KIRO_REJECTED_SCHEMA_KEYS.has(key)) continue;
12
65
  if (key === "required" && Array.isArray(child) && child.length === 0) continue;
13
- out[key] = sanitizeKiroSchema(child);
66
+ out[key] = SCHEMA_MAP_KEYS.has(key) ? sanitizeSchemaMap(child) : sanitizeKiroSchema(child);
14
67
  }
15
68
  return out;
16
69
  }
package/src/cli-status.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { durableBunRuntime } from "./bun-runtime";
2
2
  import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics, readPid, readRuntimePort, type RuntimePortState } from "./config";
3
+ import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "./codex-plugins-doctor";
3
4
  import type { OcxConfig } from "./types";
4
5
  import { serviceStatusSummary } from "./service";
5
6
 
@@ -44,6 +45,7 @@ export type CliStatusJson = {
44
45
  };
45
46
  service: { summary: string };
46
47
  codexShim: { summary: string };
48
+ codexPlugins: CodexPluginsDiagnostic;
47
49
  };
48
50
 
49
51
  export type CliStatusView = {
@@ -114,6 +116,7 @@ export async function collectStatus(): Promise<CliStatusView> {
114
116
  const serviceSummary = serviceStatusSummary();
115
117
  const { codexShimStatus } = await import("./codex-shim");
116
118
  const codexShimSummary = codexShimStatus();
119
+ const codexPlugins = diagnoseCodexBundledPlugins();
117
120
  const proxyLabel = pid && health.ok
118
121
  ? `running (PID ${pid})`
119
122
  : pid
@@ -159,6 +162,7 @@ export async function collectStatus(): Promise<CliStatusView> {
159
162
  },
160
163
  service: { summary: serviceSummary },
161
164
  codexShim: { summary: codexShimSummary },
165
+ codexPlugins,
162
166
  },
163
167
  };
164
168
  }
package/src/cli.ts CHANGED
@@ -348,6 +348,13 @@ async function handleStatus() {
348
348
  console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
349
349
  console.log(` Service: ${status.json.service.summary}`);
350
350
  console.log(` ${status.json.codexShim.summary}`);
351
+ if (status.json.codexPlugins.applicable) {
352
+ const icon = status.json.codexPlugins.stale ? "⚠️ " : "✅";
353
+ console.log(` ${icon} Codex bundled plugins: ${status.json.codexPlugins.summary}`);
354
+ if (status.json.codexPlugins.suggestedRepair) {
355
+ console.log(` Suggested: ${status.json.codexPlugins.suggestedRepair}`);
356
+ }
357
+ }
351
358
  }
352
359
 
353
360
  function handleRecoverHistory() {