@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.0
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.
- package/README.md +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -3,6 +3,10 @@ import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
|
|
|
3
3
|
import { decodeCursorArgsMap } from "./arg-codec";
|
|
4
4
|
import { normalizeArgKeys } from "./arg-normalize";
|
|
5
5
|
import {
|
|
6
|
+
cursorShellBridgeArgsValid,
|
|
7
|
+
cursorShellBridgeDropError,
|
|
8
|
+
defaultShellBridgeArgNormalizeSchema,
|
|
9
|
+
isCodexShellBridgeToolName,
|
|
6
10
|
normalizeCursorWireName,
|
|
7
11
|
OCX_RESPONSES_TOOL_PROVIDER,
|
|
8
12
|
resolveShellBridgeAliasKey,
|
|
@@ -324,10 +328,18 @@ export function mapSyntheticMcpExecToToolEvents(
|
|
|
324
328
|
out.push(...commitToolCall(options.state, callId, finalArgs));
|
|
325
329
|
return out;
|
|
326
330
|
}
|
|
331
|
+
const responsesName = responsesToolNameFromCursorWire(cursorWireName);
|
|
332
|
+
const normSchema = defaultShellBridgeArgNormalizeSchema(responsesName);
|
|
333
|
+
const normalizedArgs = JSON.stringify(normalizeArgKeys(decodeCursorArgsMap(args?.args), normSchema));
|
|
334
|
+
if (!cursorShellBridgeArgsValid(normalizedArgs, responsesName, normSchema)) {
|
|
335
|
+
if (isCodexShellBridgeToolName(responsesName)) {
|
|
336
|
+
return [{ type: "error", message: cursorShellBridgeDropError(responsesName) }];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
327
339
|
// Stateless fallback (no shared event state): emit a complete, self-contained tool call.
|
|
328
340
|
return [
|
|
329
|
-
{ type: "tool_call_start", id: callId, name:
|
|
330
|
-
{ type: "tool_call_delta", arguments:
|
|
341
|
+
{ type: "tool_call_start", id: callId, name: responsesName },
|
|
342
|
+
...(normalizedArgs.length > 2 ? [{ type: "tool_call_delta" as const, arguments: normalizedArgs }] : []),
|
|
331
343
|
{ type: "tool_call_end", id: callId },
|
|
332
344
|
];
|
|
333
345
|
}
|
|
@@ -361,9 +373,19 @@ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorW
|
|
|
361
373
|
* recorded in `openToolCalls`. Because each completion emits a whole non-interleaved unit, the bridge
|
|
362
374
|
* (which tracks a single current tool call) serializes parallel Cursor calls correctly.
|
|
363
375
|
*/
|
|
376
|
+
function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
|
|
377
|
+
state.openToolCalls.delete(callId);
|
|
378
|
+
state.completedToolCalls.add(callId);
|
|
379
|
+
return [{ type: "error", message: cursorShellBridgeDropError(toolName) }];
|
|
380
|
+
}
|
|
381
|
+
|
|
364
382
|
function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
|
|
365
383
|
const open = state.openToolCalls.get(callId);
|
|
366
384
|
if (!open) return [];
|
|
385
|
+
const schema = toolSchemaForWireName(state, open.name);
|
|
386
|
+
if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
|
|
387
|
+
if (isCodexShellBridgeToolName(open.name)) return dropShellBridgeCall(state, callId, open.name);
|
|
388
|
+
}
|
|
367
389
|
const out: CursorServerMessage[] = [{ type: "tool_call_start", id: callId, name: open.name }];
|
|
368
390
|
if (finalArgs.length > 0) out.push({ type: "tool_call_delta", arguments: finalArgs });
|
|
369
391
|
out.push(...endToolCall(state, callId));
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
} from "./gen/agent_pb";
|
|
39
39
|
import {
|
|
40
40
|
appendCursorGenericToolUseHint,
|
|
41
|
-
appendCursorShellAliasHint,
|
|
42
41
|
cursorToolsForActivePrompt,
|
|
43
42
|
buildCursorToolGuidanceSystemNote,
|
|
44
43
|
buildCursorToolDefinitions,
|
|
@@ -560,7 +559,7 @@ export function prepareCursorRunRequest(
|
|
|
560
559
|
const rawText = activePromptText(request);
|
|
561
560
|
const lastRole = request.messages.at(-1)?.role;
|
|
562
561
|
const text = lastRole === "user" || lastRole === "developer"
|
|
563
|
-
?
|
|
562
|
+
? appendCursorGenericToolUseHint(request.tools, rawText)
|
|
564
563
|
: rawText;
|
|
565
564
|
// Tool-result-only turns resume the remembered Cursor conversation with results in history.
|
|
566
565
|
const lastRawIsToolResult = request.rawMessages?.at(-1)?.role === "toolResult";
|
|
@@ -12,8 +12,6 @@ export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
|
|
|
12
12
|
export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const;
|
|
13
13
|
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
|
|
14
14
|
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell; never say native shell is blocked.';
|
|
15
|
-
export const CURSOR_SHELL_ALIAS_USER_HINT =
|
|
16
|
-
"Use the Codex shell bridge tool listed this turn (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` name). Prefer that bridge over Cursor-native Shell/Read. Never say phrases like \"Native shell access is blocked\" or \"Shell access is blocked\" — silently use the bridge unless that bridge tool itself fails.";
|
|
17
15
|
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
|
|
18
16
|
|
|
19
17
|
export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
@@ -213,17 +211,6 @@ function shellBridgeArgNormalizeSchema(tool: OcxTool): unknown {
|
|
|
213
211
|
};
|
|
214
212
|
}
|
|
215
213
|
|
|
216
|
-
function activeTextMentionsExecCommand(text: string): boolean {
|
|
217
|
-
return /\b(?:exec_command|shell_command)\b/i.test(text);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function looksLikeShellCommandRequest(text: string): boolean {
|
|
221
|
-
const hasKnownCommand = /(?:^|[\s`$])(?:echo|pwd|ls|cat|grep|rg|find|python3?|node|bun|npm|pnpm|yarn|git|curl|wget|chmod|mkdir|rm|cp|mv|touch|docker|kubectl|make|cargo|go|pytest)(?=\s|$|[`:;|&])/i.test(text);
|
|
222
|
-
const hasRunIntent = /\b(?:run|execute|exec)\b/i.test(text) || /\b(?:stdout|stderr|exit\s+code)\b/i.test(text);
|
|
223
|
-
const hasShellTarget = /\b(?:shell|terminal|command|cmd)\b/i.test(text);
|
|
224
|
-
return /\b(?:run|execute|exec)\s*:/i.test(text) || hasKnownCommand || (hasRunIntent && hasShellTarget);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
214
|
export function isGenericToolUseCountDemoPrompt(text: string): boolean {
|
|
228
215
|
const trimmed = text.trim();
|
|
229
216
|
if (trimmed.length === 0) return false;
|
|
@@ -316,23 +303,69 @@ export function cursorToolsForActivePrompt<T extends Pick<OcxTool, "namespace" |
|
|
|
316
303
|
return execTools && execTools.length > 0 ? execTools : tools;
|
|
317
304
|
}
|
|
318
305
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Required command payload keys for a shell bridge tool, derived from the advertised schema when present.
|
|
308
|
+
*/
|
|
309
|
+
export function shellBridgeRequiredCommandKeys(
|
|
310
|
+
toolName: string,
|
|
311
|
+
schema?: unknown,
|
|
312
|
+
): readonly ("cmd" | "command")[] {
|
|
313
|
+
if (schema && typeof schema === "object") {
|
|
314
|
+
const required = (schema as Record<string, unknown>).required;
|
|
315
|
+
if (Array.isArray(required)) {
|
|
316
|
+
const keys = required.filter((key): key is "cmd" | "command" => key === "cmd" || key === "command");
|
|
317
|
+
if (keys.length > 0) return keys;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return toolName === CODEX_SHELL_COMMAND_TOOL ? ["command"] : ["cmd"];
|
|
328
321
|
}
|
|
329
322
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
323
|
+
/** Normalize-schema defaults used when validating stateless synthetic shell-bridge calls. */
|
|
324
|
+
export function defaultShellBridgeArgNormalizeSchema(toolName: string): unknown {
|
|
325
|
+
return toolName === CODEX_SHELL_COMMAND_TOOL
|
|
326
|
+
? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA
|
|
327
|
+
: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: CURSOR_EXEC_COMMAND_INPUT_SCHEMA.properties,
|
|
330
|
+
required: ["cmd"],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function cursorShellBridgeDropError(toolName: string): string {
|
|
335
|
+
return `Cursor emitted ${toolName} without a non-empty command; the tool call was dropped.`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Extract a non-empty shell command from completed Cursor bridge args using the schema's required
|
|
340
|
+
* command key (`cmd` for bare exec_command, `command` for shell_command).
|
|
341
|
+
*/
|
|
342
|
+
export function nonEmptyShellBridgeCommandFromArgs(
|
|
343
|
+
finalArgs: string,
|
|
344
|
+
toolName: string,
|
|
345
|
+
schema?: unknown,
|
|
346
|
+
): string | undefined {
|
|
347
|
+
let parsed: unknown;
|
|
348
|
+
try {
|
|
349
|
+
parsed = finalArgs.length > 0 ? JSON.parse(finalArgs) : {};
|
|
350
|
+
} catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
354
|
+
const record = parsed as Record<string, unknown>;
|
|
355
|
+
for (const key of shellBridgeRequiredCommandKeys(toolName, schema)) {
|
|
356
|
+
const value = record[key];
|
|
357
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function cursorShellBridgeArgsValid(
|
|
363
|
+
finalArgs: string,
|
|
364
|
+
toolName: string,
|
|
365
|
+
schema?: unknown,
|
|
366
|
+
): boolean {
|
|
367
|
+
return !isCodexShellBridgeToolName(toolName)
|
|
368
|
+
|| nonEmptyShellBridgeCommandFromArgs(finalArgs, toolName, schema) !== undefined;
|
|
336
369
|
}
|
|
337
370
|
|
|
338
371
|
export function cursorToolAllowedByChoice(
|
|
@@ -384,6 +417,11 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
384
417
|
const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice);
|
|
385
418
|
const discoveryTools = discoveryToolLabel(wireNames);
|
|
386
419
|
const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
|
|
420
|
+
// Host-shell-neutral: the Codex client executes bridge commands, and may differ from
|
|
421
|
+
// the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls.
|
|
422
|
+
const hostShellNote = hasBareExec
|
|
423
|
+
? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`<<EOF`); `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`. POSIX: use portable commands. After a shell failure, make at most one corrected bridge attempt, then report the error and stop — do not repeat equivalent failing commands."
|
|
424
|
+
: undefined;
|
|
387
425
|
const notes = [
|
|
388
426
|
`Cursor tool calls: available tool names are exactly ${listedNames}.`,
|
|
389
427
|
"Use the current tool catalog as ground truth and call only those exact names with their listed argument keys.",
|
|
@@ -399,6 +437,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
399
437
|
hasBareExec
|
|
400
438
|
? "Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Prefer the bridge over Cursor-native Shell/Read; do not narrate phrases like \"Native shell access is blocked\" — silently call `shell_command` / `exec_command`."
|
|
401
439
|
: undefined,
|
|
440
|
+
hostShellNote,
|
|
402
441
|
"Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
|
|
403
442
|
hasBareExec
|
|
404
443
|
? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.`
|
|
@@ -418,7 +457,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
418
457
|
: undefined,
|
|
419
458
|
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
420
459
|
hasBareExec
|
|
421
|
-
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with
|
|
460
|
+
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use \`apply_patch\` when available.`
|
|
422
461
|
: undefined,
|
|
423
462
|
].filter((note): note is string => typeof note === "string");
|
|
424
463
|
return notes.join(" ");
|
|
@@ -137,6 +137,10 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
|
|
|
137
137
|
: (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
|
|
138
138
|
if (thinkingLevel) out.thinkingConfig = { thinkingLevel };
|
|
139
139
|
}
|
|
140
|
+
if (Array.isArray(value.responseModalities)) {
|
|
141
|
+
const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m));
|
|
142
|
+
if (valid.length > 0) out.responseModalities = valid;
|
|
143
|
+
}
|
|
140
144
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
141
145
|
}
|
|
142
146
|
|
package/src/adapters/google.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
2
2
|
import { debugDroppedFrame } from "../lib/debug";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
|
|
4
5
|
import type {
|
|
5
6
|
AdapterEvent,
|
|
6
7
|
OcxAssistantMessage,
|
|
@@ -233,6 +234,38 @@ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage |
|
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Cap on the buffered non-streaming response body (100 MiB), matching
|
|
239
|
+
* IMAGES_RESPONSE_MAX_BYTES in src/server/images.ts. Enforced by streaming the
|
|
240
|
+
* body with a hard byte cap before JSON.parse — Content-Length alone is not
|
|
241
|
+
* trusted (missing/lying headers must still reject oversized payloads).
|
|
242
|
+
* Streaming SSE responses also cap each data frame before JSON.parse.
|
|
243
|
+
*/
|
|
244
|
+
const MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
245
|
+
const MAX_SSE_FRAME_BYTES = MAX_RESPONSE_BYTES;
|
|
246
|
+
|
|
247
|
+
// Note: imagen-* models use a different API surface (prediction/image-generation
|
|
248
|
+
// schema) and must NOT be treated as responseModalities-capable Gemini models.
|
|
249
|
+
// Explicit allowlist only — never `/gemini/ && /image/` (resurrects media-gen IDs).
|
|
250
|
+
const IMAGE_CAPABLE_MODELS = new Set([
|
|
251
|
+
"gemini-3.1-flash-image",
|
|
252
|
+
"gemini-2.0-flash-preview-image-generation",
|
|
253
|
+
"gemini-3-pro-image-preview",
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
function isImageCapableModel(modelId: string): boolean {
|
|
257
|
+
return IMAGE_CAPABLE_MODELS.has(modelId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Model-visible markdown link for a materialized artifact. Uses the authenticated
|
|
262
|
+
* opaque HTTP route so remote/container clients can fetch the image without host
|
|
263
|
+
* filesystem paths leaking into the transcript.
|
|
264
|
+
*/
|
|
265
|
+
function artifactMarkdownUrl(filePath: string): string {
|
|
266
|
+
return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
|
|
267
|
+
}
|
|
268
|
+
|
|
236
269
|
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
237
270
|
// Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
|
|
238
271
|
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
@@ -272,6 +305,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
272
305
|
? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
|
|
273
306
|
: undefined;
|
|
274
307
|
if (directFlashThinking) generationConfig.thinkingConfig = { thinkingLevel: directFlashThinking };
|
|
308
|
+
if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
|
|
309
|
+
generationConfig.responseModalities = ["TEXT", "IMAGE"];
|
|
310
|
+
}
|
|
275
311
|
if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
|
|
276
312
|
|
|
277
313
|
const method = parsed.stream ? "streamGenerateContent" : "generateContent";
|
|
@@ -377,6 +413,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
377
413
|
yield { type: "error", message: "No response body" };
|
|
378
414
|
return;
|
|
379
415
|
}
|
|
416
|
+
// Streaming responses are processed incrementally (SSE chunks), so the full body
|
|
417
|
+
// is never buffered — no Content-Length pre-check is needed here. Per-image size
|
|
418
|
+
// protection is enforced on each chunk via MAX_ENCODED_BYTES_PER_IMAGE before
|
|
419
|
+
// materializeInlineImage is called (see the inline.data check below).
|
|
380
420
|
|
|
381
421
|
const reader = response.body.getReader();
|
|
382
422
|
const decoder = new TextDecoder();
|
|
@@ -387,9 +427,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
387
427
|
let sawAnyFrame = false;
|
|
388
428
|
let sawTerminalSignal = false;
|
|
389
429
|
|
|
390
|
-
const handleDataLine = function* (line: string):
|
|
430
|
+
const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
|
|
391
431
|
const payload = line.slice(5).trim();
|
|
392
432
|
if (!payload) return "continue";
|
|
433
|
+
if (payload.length > MAX_SSE_FRAME_BYTES) {
|
|
434
|
+
yield { type: "error", message: `upstream SSE data frame exceeds ${MAX_SSE_FRAME_BYTES} bytes` };
|
|
435
|
+
return "terminate";
|
|
436
|
+
}
|
|
393
437
|
let emittedContentEvent = false;
|
|
394
438
|
|
|
395
439
|
let chunk: Record<string, unknown>;
|
|
@@ -452,6 +496,21 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
452
496
|
emittedContentEvent = true;
|
|
453
497
|
yield { type: "text_delta", text: part.text };
|
|
454
498
|
}
|
|
499
|
+
const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
|
|
500
|
+
if (inline && typeof inline.data === "string") {
|
|
501
|
+
if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
|
|
502
|
+
yield { type: "error", message: "inline image exceeds per-image size cap" };
|
|
503
|
+
} else {
|
|
504
|
+
try {
|
|
505
|
+
const filePath = await materializeInlineImage(inline.data, imageBudget);
|
|
506
|
+
const escapedPath = artifactMarkdownUrl(filePath);
|
|
507
|
+
emittedContentEvent = true;
|
|
508
|
+
yield { type: "text_delta", text: `\n\n` };
|
|
509
|
+
} catch {
|
|
510
|
+
yield { type: "error", message: "failed to materialize inline image" };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
455
514
|
if (part.functionCall) {
|
|
456
515
|
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
457
516
|
toolCallsStarted++;
|
|
@@ -464,12 +523,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
464
523
|
}
|
|
465
524
|
return emittedContentEvent ? "content" : "continue";
|
|
466
525
|
};
|
|
526
|
+
const imageBudget = createImageBudget();
|
|
467
527
|
|
|
468
528
|
try {
|
|
469
529
|
while (true) {
|
|
470
530
|
const { done, value } = await reader.read();
|
|
471
531
|
if (done) break;
|
|
472
532
|
buffer += decoder.decode(value, { stream: true });
|
|
533
|
+
// Cap incomplete frames before waiting for a newline — otherwise a single
|
|
534
|
+
// unterminated data: payload can grow without bound.
|
|
535
|
+
if (buffer.length > MAX_SSE_FRAME_BYTES) {
|
|
536
|
+
yield { type: "error", message: `upstream SSE data frame exceeds ${MAX_SSE_FRAME_BYTES} bytes` };
|
|
537
|
+
try { await reader.cancel(); } catch { /* ignore */ }
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
473
540
|
|
|
474
541
|
const lines = buffer.split("\n");
|
|
475
542
|
buffer = lines.pop() ?? "";
|
|
@@ -526,7 +593,51 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
526
593
|
},
|
|
527
594
|
|
|
528
595
|
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
529
|
-
|
|
596
|
+
// Reject oversized responses before JSON parse. Prefer Content-Length when
|
|
597
|
+
// present and truthful; always stream-read with a hard byte cap so a missing
|
|
598
|
+
// or lying Content-Length cannot force a full in-memory buffer + parse.
|
|
599
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
600
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
|
|
601
|
+
try { await response.body?.cancel(); } catch { /* ignore */ }
|
|
602
|
+
return [{ type: "error", message: `google response too large (content-length ${contentLength} exceeds ${MAX_RESPONSE_BYTES} bytes)` }];
|
|
603
|
+
}
|
|
604
|
+
let rawText: string;
|
|
605
|
+
try {
|
|
606
|
+
const reader = response.body?.getReader();
|
|
607
|
+
if (!reader) return [{ type: "error", message: "google response had no body" }];
|
|
608
|
+
const chunks: Uint8Array[] = [];
|
|
609
|
+
let total = 0;
|
|
610
|
+
try {
|
|
611
|
+
for (;;) {
|
|
612
|
+
const { done, value } = await reader.read();
|
|
613
|
+
if (done) break;
|
|
614
|
+
total += value.byteLength;
|
|
615
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
616
|
+
await reader.cancel().catch(() => {});
|
|
617
|
+
return [{ type: "error", message: `google response too large (exceeded ${MAX_RESPONSE_BYTES} bytes)` }];
|
|
618
|
+
}
|
|
619
|
+
chunks.push(value);
|
|
620
|
+
}
|
|
621
|
+
} finally {
|
|
622
|
+
try { await reader.cancel(); } catch { /* ignore */ }
|
|
623
|
+
reader.releaseLock();
|
|
624
|
+
}
|
|
625
|
+
const bytes = new Uint8Array(total);
|
|
626
|
+
let offset = 0;
|
|
627
|
+
for (const chunk of chunks) {
|
|
628
|
+
bytes.set(chunk, offset);
|
|
629
|
+
offset += chunk.byteLength;
|
|
630
|
+
}
|
|
631
|
+
rawText = new TextDecoder().decode(bytes);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
return [{ type: "error", message: err instanceof Error ? err.message : "failed to read google response body" }];
|
|
634
|
+
}
|
|
635
|
+
let raw: Record<string, unknown>;
|
|
636
|
+
try {
|
|
637
|
+
raw = JSON.parse(rawText) as Record<string, unknown>;
|
|
638
|
+
} catch {
|
|
639
|
+
return [{ type: "error", message: "google response was not valid JSON" }];
|
|
640
|
+
}
|
|
530
641
|
if (raw.error) {
|
|
531
642
|
const err = raw.error as { message?: string };
|
|
532
643
|
return [{ type: "error", message: err.message ?? "upstream error" }];
|
|
@@ -547,6 +658,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
547
658
|
return [{ type: "error", message: "google response contained no candidates" }];
|
|
548
659
|
}
|
|
549
660
|
let toolCallsStarted = 0;
|
|
661
|
+
const imageBudget = createImageBudget();
|
|
550
662
|
if (candidates?.[0]?.content?.parts) {
|
|
551
663
|
// Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
|
|
552
664
|
if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession) {
|
|
@@ -554,6 +666,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
554
666
|
}
|
|
555
667
|
for (const part of candidates[0].content.parts) {
|
|
556
668
|
if (part.text) events.push({ type: "text_delta", text: part.text });
|
|
669
|
+
const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
|
|
670
|
+
if (inline && typeof inline.data === "string") {
|
|
671
|
+
if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
|
|
672
|
+
events.push({ type: "error", message: "inline image exceeds per-image size cap" });
|
|
673
|
+
} else {
|
|
674
|
+
try {
|
|
675
|
+
const filePath = await materializeInlineImage(inline.data, imageBudget);
|
|
676
|
+
const escapedPath = artifactMarkdownUrl(filePath);
|
|
677
|
+
events.push({ type: "text_delta", text: `\n\n` });
|
|
678
|
+
} catch {
|
|
679
|
+
events.push({ type: "error", message: "failed to materialize inline image" });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
557
683
|
if (part.functionCall) {
|
|
558
684
|
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
559
685
|
toolCallsStarted++;
|
package/src/adapters/identity.ts
CHANGED
|
@@ -14,9 +14,19 @@
|
|
|
14
14
|
* to be a specific first-party client.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/** Historical exact identity line Codex injected for every model. */
|
|
18
18
|
export const CODEX_GPT5_IDENTITY_LINE = "You are Codex, a coding agent based on GPT-5.";
|
|
19
19
|
|
|
20
|
+
/** Codex CLI 0.145.0+ wording (#622) — still GPT-5 identity, slightly different phrasing. */
|
|
21
|
+
export const CODEX_GPT5_IDENTITY_LINE_AGENT = "You are Codex, an agent based on GPT-5.";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Known Codex GPT-5 identity sentences. Narrow: only "coding agent" / "an agent" + GPT-5(.x)?
|
|
25
|
+
* Avoid a broad `You are Codex.*` rewrite that could touch unrelated content.
|
|
26
|
+
*/
|
|
27
|
+
const CODEX_GPT5_IDENTITY_RE =
|
|
28
|
+
/You are Codex, (?:a coding agent|an agent) based on GPT-5(?:\.[0-9]+)*\./g;
|
|
29
|
+
|
|
20
30
|
/** Proxy-neutral replacement: no "opencodex proxy" mention, just the GPT-5/OpenAI disclaimer. */
|
|
21
31
|
export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be GPT-5 or to be made by OpenAI.";
|
|
22
32
|
|
|
@@ -27,7 +37,7 @@ export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be
|
|
|
27
37
|
* the leak can't reappear in one adapter while being fixed in another.
|
|
28
38
|
*/
|
|
29
39
|
export function neutralizeIdentity(systemText: string): string {
|
|
30
|
-
return systemText.replace(
|
|
40
|
+
return systemText.replace(CODEX_GPT5_IDENTITY_RE, NEUTRAL_IDENTITY_LINE);
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
/** The catalog (static, on-disk) replacement for `base_instructions`. Same neutral wording. */
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -52,6 +52,24 @@ const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantRespons
|
|
|
52
52
|
const SDK_VERSION = "1.0.27";
|
|
53
53
|
const NODE_VERSION = "22.21.1";
|
|
54
54
|
const KIRO_IDE_VERSION = "1.0.0";
|
|
55
|
+
type KiroWireClient = "ide" | "cli";
|
|
56
|
+
|
|
57
|
+
function kiroCliPlatform(): "linux" | "macos" | "windows" {
|
|
58
|
+
return process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function kiroCliUserAgent(includeAppVersion: boolean): string {
|
|
62
|
+
return [
|
|
63
|
+
"aws-sdk-rust/1.3.15",
|
|
64
|
+
"ua/2.1",
|
|
65
|
+
"api/codewhispererstreaming/0.1.17975",
|
|
66
|
+
`os/${kiroCliPlatform()}`,
|
|
67
|
+
"lang/rust/1.92.0",
|
|
68
|
+
...(includeAppVersion ? ["md/appVersion-2.14.2"] : []),
|
|
69
|
+
"m/F",
|
|
70
|
+
"app/AmazonQ-For-CLI",
|
|
71
|
+
].join(" ");
|
|
72
|
+
}
|
|
55
73
|
|
|
56
74
|
// Payload construction (conversationState)
|
|
57
75
|
interface KiroToolUse {
|
|
@@ -68,7 +86,10 @@ interface KiroUserInputMessage {
|
|
|
68
86
|
content: string;
|
|
69
87
|
modelId?: string;
|
|
70
88
|
origin?: string;
|
|
71
|
-
userInputMessageContext?: {
|
|
89
|
+
userInputMessageContext?: {
|
|
90
|
+
tools?: unknown[];
|
|
91
|
+
toolResults?: KiroToolResult[];
|
|
92
|
+
};
|
|
72
93
|
images?: KiroImage[];
|
|
73
94
|
}
|
|
74
95
|
interface KiroHistoryEntry {
|
|
@@ -385,6 +406,7 @@ export function buildKiroPayload(
|
|
|
385
406
|
parsed: OcxParsedRequest,
|
|
386
407
|
profileArn: string | undefined,
|
|
387
408
|
forcedCompletionMode?: KiroCompletionMode,
|
|
409
|
+
wireClient: KiroWireClient = "ide",
|
|
388
410
|
): {
|
|
389
411
|
payload: Record<string, unknown>;
|
|
390
412
|
nameMap: Map<string, string>;
|
|
@@ -476,7 +498,11 @@ export function buildKiroPayload(
|
|
|
476
498
|
if (!priorCalls.has(toolUseId)) {
|
|
477
499
|
throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`);
|
|
478
500
|
}
|
|
479
|
-
|
|
501
|
+
// Carrier text is a placeholder for an OTHERWISE EMPTY tool-result turn, not a prefix.
|
|
502
|
+
// Passing it here would push proxy filler AHEAD of a human instruction that Claude Code
|
|
503
|
+
// sends in the same turn (mid-turn steering / queued_command, issue #543), burying the
|
|
504
|
+
// newest user intent behind boilerplate. Backfill below only when nothing else speaks.
|
|
505
|
+
pushUser("", images, [{
|
|
480
506
|
content: [{ text: resultText }],
|
|
481
507
|
status: tr.isError ? "error" : "success",
|
|
482
508
|
toolUseId,
|
|
@@ -496,6 +522,16 @@ export function buildKiroPayload(
|
|
|
496
522
|
});
|
|
497
523
|
}
|
|
498
524
|
|
|
525
|
+
// Give tool-result turns a carrier sentence ONLY when they carry no other text. This runs
|
|
526
|
+
// before the pop below so the current turn is covered too: skipping it there would ship an
|
|
527
|
+
// empty current content, which validateKiroConversationState accepts (tool results count as
|
|
528
|
+
// payload) and would therefore fail silently.
|
|
529
|
+
for (const turn of turns) {
|
|
530
|
+
if (turn.kind === "user" && !turn.content.trim() && turn.toolResults.length > 0) {
|
|
531
|
+
turn.content = KIRO_TOOL_RESULT_CARRIER_MESSAGE;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
499
535
|
const currentTurn = turns.pop();
|
|
500
536
|
if (!currentTurn || currentTurn.kind !== "user") throw new Error("Kiro request must end with a user turn");
|
|
501
537
|
const toEntry = (turn: KiroTurn): KiroHistoryEntry => turn.kind === "assistant"
|
|
@@ -509,7 +545,7 @@ export function buildKiroPayload(
|
|
|
509
545
|
userInputMessage: {
|
|
510
546
|
content: turn.content,
|
|
511
547
|
modelId,
|
|
512
|
-
origin: "AI_EDITOR",
|
|
548
|
+
origin: wireClient === "cli" ? "KIRO_CLI" : "AI_EDITOR",
|
|
513
549
|
...(turn.images.length > 0 ? { images: turn.images } : {}),
|
|
514
550
|
...(turn.toolResults.length > 0 ? { userInputMessageContext: { toolResults: turn.toolResults } } : {}),
|
|
515
551
|
},
|
|
@@ -539,6 +575,10 @@ export function buildKiroPayload(
|
|
|
539
575
|
const payload: Record<string, unknown> = {
|
|
540
576
|
conversationState: {
|
|
541
577
|
chatTriggerType: "MANUAL",
|
|
578
|
+
...(wireClient === "cli" ? {
|
|
579
|
+
agentContinuationId: crypto.randomUUID(),
|
|
580
|
+
agentTaskType: "vibe",
|
|
581
|
+
} : {}),
|
|
542
582
|
conversationId,
|
|
543
583
|
currentMessage: { userInputMessage: currentUim },
|
|
544
584
|
...(history.length > 0 ? { history } : {}),
|
|
@@ -1445,10 +1485,26 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1445
1485
|
if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
|
|
1446
1486
|
throw new Error("kiro token missing — run ocx login kiro");
|
|
1447
1487
|
}
|
|
1448
|
-
const region = resolveKiroApiRegion();
|
|
1449
|
-
const
|
|
1488
|
+
const region = resolveKiroApiRegion(parsed._kiroAuthContext);
|
|
1489
|
+
const resolvedProfileArn = resolveKiroProfileArn(parsed._kiroAuthContext);
|
|
1490
|
+
const isApiKey = provider.apiKey.trim().startsWith("ksk_");
|
|
1491
|
+
const profileArn = isApiKey ? undefined : resolvedProfileArn;
|
|
1492
|
+
// Builder ID and Kiro API keys have no profile ARN and are accepted only on Kiro's CLI
|
|
1493
|
+
// request path. Enterprise profiles retain the existing IDE-shaped request.
|
|
1494
|
+
const wireClient: KiroWireClient = isApiKey || !profileArn ? "cli" : "ide";
|
|
1450
1495
|
const fp = fingerprint().slice(0, 64);
|
|
1451
|
-
const headers: Record<string, string> = {
|
|
1496
|
+
const headers: Record<string, string> = wireClient === "cli" ? {
|
|
1497
|
+
authorization: `Bearer ${provider.apiKey}`,
|
|
1498
|
+
"content-type": "application/x-amz-json-1.0",
|
|
1499
|
+
accept: "*/*",
|
|
1500
|
+
"x-amz-target": AMZ_TARGET,
|
|
1501
|
+
"user-agent": kiroCliUserAgent(true),
|
|
1502
|
+
"x-amz-user-agent": kiroCliUserAgent(false),
|
|
1503
|
+
"x-amzn-codewhisperer-optout": "true",
|
|
1504
|
+
"amz-sdk-request": "attempt=1; max=3",
|
|
1505
|
+
"amz-sdk-invocation-id": invocationId(),
|
|
1506
|
+
...(isApiKey ? { tokentype: "API_KEY" } : {}),
|
|
1507
|
+
} : {
|
|
1452
1508
|
authorization: `Bearer ${provider.apiKey}`,
|
|
1453
1509
|
"content-type": "application/x-amz-json-1.0",
|
|
1454
1510
|
accept: "application/vnd.amazon.eventstream",
|
|
@@ -1460,7 +1516,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1460
1516
|
"amz-sdk-invocation-id": invocationId(),
|
|
1461
1517
|
};
|
|
1462
1518
|
if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
|
|
1463
|
-
const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode);
|
|
1519
|
+
const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient);
|
|
1464
1520
|
await normalizeKiroImages(built.payload);
|
|
1465
1521
|
const contextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
|
|
1466
1522
|
const body = JSON.stringify(built.payload);
|
|
@@ -1472,6 +1528,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1472
1528
|
messageCount: kiroPayloadMessages(parsed).length,
|
|
1473
1529
|
toolCount: parsed.context.tools?.length ?? 0,
|
|
1474
1530
|
hasProfileArn: Boolean(profileArn),
|
|
1531
|
+
wireClient,
|
|
1475
1532
|
hasPreviousResponseId: Boolean(parsed.previousResponseId),
|
|
1476
1533
|
});
|
|
1477
1534
|
return {
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
5
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
5
6
|
import type { OcxProviderConfig, OcxParsedRequest } from "../types";
|
|
6
7
|
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
7
8
|
import type { ProviderAdapter, AdapterRequest } from "./base";
|
|
@@ -59,6 +60,7 @@ export function getMimoClientId(): string {
|
|
|
59
60
|
} catch { /* fall through to regenerate */ }
|
|
60
61
|
const fresh = randomUUID();
|
|
61
62
|
try {
|
|
63
|
+
recordOwnedConfigPath(dir, file);
|
|
62
64
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
63
65
|
writeFileSync(file, `${fresh}\n`, "utf8");
|
|
64
66
|
} catch { /* persist best-effort; still usable for this process */ }
|