@bitkyc08/opencodex 2.80.0 → 2.81.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 +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +1 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +3 -2
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +320 -184
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +4 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +16 -11
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +27 -5
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { namespacedToolName, requiresToolCall, toolChoiceToolPredicate, type OcxParsedRequest } from "../../types";
|
|
2
|
+
import { stripResponsesOnlyEncryptedMarker } from "../responses-tool-schema";
|
|
3
|
+
|
|
4
|
+
// The selected catalog also supplies one comma-joined --allowed-tools argv value.
|
|
5
|
+
// Bound that full argument separately from individual names for Windows process creation.
|
|
6
|
+
export const CODING_AGENT_TOOL_LIMITS = Object.freeze({
|
|
7
|
+
maxTools: 128,
|
|
8
|
+
maxNameBytes: 512,
|
|
9
|
+
maxAllowedToolsArgBytes: 8 * 1024,
|
|
10
|
+
maxToolBytes: 256 * 1024,
|
|
11
|
+
maxCatalogBytes: 2 * 1024 * 1024,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
const INVALID_NAME_PART = /[,\s\u0000-\u001f\u007f-\u009f]/u;
|
|
16
|
+
|
|
17
|
+
export class CodingAgentToolCatalogError extends Error {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "CodingAgentToolCatalogError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isValidCodingAgentToolNamePart(value: unknown): value is string {
|
|
25
|
+
if (typeof value !== "string" || !value || INVALID_NAME_PART.test(value)) return false;
|
|
26
|
+
// TextEncoder replaces lone surrogates, which would change tool identity on the MCP wire.
|
|
27
|
+
for (let i = 0; i < value.length; i++) {
|
|
28
|
+
const unit = value.charCodeAt(i);
|
|
29
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
30
|
+
const next = value.charCodeAt(++i);
|
|
31
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
32
|
+
} else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One client-owned catalog; the CLI spelling is only a projection of its wire names. */
|
|
40
|
+
export function buildCodingAgentToolCatalog(parsed: OcxParsedRequest, serverName: string) {
|
|
41
|
+
const declared = parsed.context.tools ?? [];
|
|
42
|
+
const selected = declared.filter(toolChoiceToolPredicate(parsed.options.toolChoice, declared));
|
|
43
|
+
if (selected.length > CODING_AGENT_TOOL_LIMITS.maxTools) throw new CodingAgentToolCatalogError("tool catalog exceeds tool count limit");
|
|
44
|
+
const requireToolCall = requiresToolCall(parsed.options.toolChoice);
|
|
45
|
+
const emittedNameMap = new Map<string, string>();
|
|
46
|
+
// JSON array brackets and commas count too; avoid serializing the whole catalog twice.
|
|
47
|
+
let catalogBytes = 2;
|
|
48
|
+
let allowedToolsArgBytes = 0;
|
|
49
|
+
const tools = selected.map((tool, index) => {
|
|
50
|
+
if (!tool || !isValidCodingAgentToolNamePart(tool.name) || (tool.namespace !== undefined && !isValidCodingAgentToolNamePart(tool.namespace))) {
|
|
51
|
+
throw new CodingAgentToolCatalogError("invalid tool name or namespace");
|
|
52
|
+
}
|
|
53
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
54
|
+
if (encoder.encode(wireName).byteLength > CODING_AGENT_TOOL_LIMITS.maxNameBytes) {
|
|
55
|
+
throw new CodingAgentToolCatalogError("tool name exceeds byte limit");
|
|
56
|
+
}
|
|
57
|
+
const definition = {
|
|
58
|
+
name: wireName,
|
|
59
|
+
description: tool.description || `Tool: ${wireName}`,
|
|
60
|
+
inputSchema: stripResponsesOnlyEncryptedMarker(tool.parameters) as Record<string, unknown>,
|
|
61
|
+
};
|
|
62
|
+
const bytes = encoder.encode(JSON.stringify(definition)).byteLength;
|
|
63
|
+
if (bytes > CODING_AGENT_TOOL_LIMITS.maxToolBytes) throw new CodingAgentToolCatalogError("tool definition exceeds byte limit");
|
|
64
|
+
catalogBytes += bytes + (index ? 1 : 0);
|
|
65
|
+
if (catalogBytes > CODING_AGENT_TOOL_LIMITS.maxCatalogBytes) throw new CodingAgentToolCatalogError("tool catalog exceeds byte limit");
|
|
66
|
+
const emittedName = `mcp__${serverName}__${wireName}`;
|
|
67
|
+
allowedToolsArgBytes += encoder.encode(emittedName).byteLength + (index ? 1 : 0);
|
|
68
|
+
if (allowedToolsArgBytes > CODING_AGENT_TOOL_LIMITS.maxAllowedToolsArgBytes) {
|
|
69
|
+
throw new CodingAgentToolCatalogError("allowed-tools argument exceeds byte limit");
|
|
70
|
+
}
|
|
71
|
+
emittedNameMap.set(emittedName, wireName);
|
|
72
|
+
return definition;
|
|
73
|
+
});
|
|
74
|
+
return { tools, emittedNameMap, requireToolCall };
|
|
75
|
+
}
|
|
@@ -4,6 +4,8 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
6
6
|
import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
7
|
+
import { SendBudgetExhaustedError } from "../../lib/upstream-retry";
|
|
8
|
+
import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
|
|
7
9
|
import { commandInvocation } from "../../lib/win-exec";
|
|
8
10
|
import { isStandaloneBinary } from "../../lib/standalone";
|
|
9
11
|
import { modelRecordValue } from "../../reasoning-effort";
|
|
@@ -43,6 +45,9 @@ export interface CodingAgentDeps {
|
|
|
43
45
|
killWindowsProcessTree?: KillWindowsProcessTreeFn;
|
|
44
46
|
/** Test seam for a catalog/config write failure after private bridge-directory creation. */
|
|
45
47
|
writeToolBridgeFile?: typeof writeFile;
|
|
48
|
+
/** Injectable timer primitives for deterministic tests without sleeping. */
|
|
49
|
+
setTimeout?: typeof setTimeout;
|
|
50
|
+
clearTimeout?: typeof clearTimeout;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
@@ -113,28 +118,35 @@ export interface CodingAgentTurnInput {
|
|
|
113
118
|
/** Family-specific scoped env builder (credential + region switch on top of baseScopedEnv). */
|
|
114
119
|
buildEnv: (profile: CodingAgentProviderProfile, apiKey: string) => Record<string, string>;
|
|
115
120
|
/**
|
|
116
|
-
* Opt-in
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* names back to request wire names, and terminates the process tree at `message_stop` because
|
|
120
|
-
* the capture-only MCP handler intentionally never answers. Execution stays with the client.
|
|
121
|
+
* Opt-in isolated MCP catalog. The turn writes a catalog and MCP config to a private temp dir,
|
|
122
|
+
* advertises only the selected names, and returns structured CLI tool intent to the client.
|
|
123
|
+
* The MCP handler never executes the client tool.
|
|
121
124
|
*/
|
|
122
125
|
toolBridge?: CodingAgentToolBridgeInput;
|
|
123
126
|
deps: CodingAgentDeps;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
/** Opt-in
|
|
129
|
+
/** Opt-in isolated tool catalog for one coding-agent CLI turn. */
|
|
127
130
|
export interface CodingAgentToolBridgeInput {
|
|
128
131
|
/** MCP server name advertised to the CLI; tool_use blocks render it as `mcp__<name>__<tool>`. */
|
|
129
132
|
serverName: string;
|
|
130
|
-
/** Absolute path of the
|
|
133
|
+
/** Absolute path of the MCP server module, run with the serving runtime. */
|
|
131
134
|
serverModulePath: string;
|
|
132
135
|
/** Validated tool catalog advertised over ListTools; the server never executes a call. */
|
|
133
136
|
tools: ReadonlyArray<{ name: string; description: string; inputSchema: Record<string, unknown> }>;
|
|
134
137
|
/** CLI-emitted tool name (`mcp__<server>__<tool>`) to the request's wire tool name. */
|
|
135
138
|
emittedNameMap: Map<string, string>;
|
|
136
|
-
/**
|
|
139
|
+
/** Tool_use blocks accepted in one assistant message. */
|
|
137
140
|
maxTurnToolCalls: number;
|
|
141
|
+
/** Render prior tool calls with the CLI-emitted names. Off by default so CodeBuddy history is unchanged. */
|
|
142
|
+
projectHistoryToolNames?: boolean;
|
|
143
|
+
/** Finish the turn when a complete assistant frame supplies structured tool_use blocks. */
|
|
144
|
+
completeAssistantToolUse?: boolean;
|
|
145
|
+
/** Authoritative tool-turn stop signal; existing coding-agent bridges default to message_stop. */
|
|
146
|
+
toolTurnCompletionSignal?: "message_stop" | "assistant_tool_use_stop";
|
|
147
|
+
/** Family-specific CLI flag spelling and compiled MCP helper entrypoint. */
|
|
148
|
+
allowedToolsFlag?: string;
|
|
149
|
+
standaloneEntrypoint?: string;
|
|
138
150
|
/**
|
|
139
151
|
* The request's `tool_choice` requires a tool call (`required`, or a named selection).
|
|
140
152
|
* The nested CLI has no documented force-tool flag, so this is enforced locally: a
|
|
@@ -164,6 +176,8 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
164
176
|
const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
165
177
|
const reapTimeoutMs = deps.reapTimeoutMs ?? (killGraceMs * 2 + 250);
|
|
166
178
|
const platform = deps.platform ?? process.platform;
|
|
179
|
+
const setTimeoutFn = deps.setTimeout ?? setTimeout;
|
|
180
|
+
const clearTimeoutFn = deps.clearTimeout ?? clearTimeout;
|
|
167
181
|
|
|
168
182
|
if (incoming.abortSignal?.aborted) {
|
|
169
183
|
emit({ type: "error", message: "Coding-agent turn was aborted before start." });
|
|
@@ -240,7 +254,11 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
240
254
|
[toolBridge.serverName]: {
|
|
241
255
|
type: "stdio",
|
|
242
256
|
command: process.execPath,
|
|
243
|
-
args:
|
|
257
|
+
args: toolBridge.standaloneEntrypoint
|
|
258
|
+
? (isStandaloneBinary()
|
|
259
|
+
? [toolBridge.standaloneEntrypoint, catalogPath]
|
|
260
|
+
: [toolBridge.serverModulePath, catalogPath])
|
|
261
|
+
: codeBuddyMcpInvocation(toolBridge.serverModulePath, catalogPath),
|
|
244
262
|
defer_loading: false,
|
|
245
263
|
alwaysLoad: true,
|
|
246
264
|
},
|
|
@@ -265,12 +283,25 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
265
283
|
const args = buildArgs(profile, parsed, provider);
|
|
266
284
|
if (toolBridge && toolBridgeMcpConfigPath) {
|
|
267
285
|
// Exact names close the wildcard domain; --strict-mcp-config (family args) keeps user
|
|
268
|
-
// servers out, so
|
|
269
|
-
args.push("--allowedTools", [...toolBridge.emittedNameMap.keys()].join(","), "--mcp-config", toolBridgeMcpConfigPath);
|
|
286
|
+
// servers out, so this isolated server is the only capability this turn can reach.
|
|
287
|
+
args.push(toolBridge.allowedToolsFlag ?? "--allowedTools", [...toolBridge.emittedNameMap.keys()].join(","), "--mcp-config", toolBridgeMcpConfigPath);
|
|
270
288
|
}
|
|
271
289
|
const env = buildEnv(profile, apiKey);
|
|
272
290
|
const invocation = commandInvocation(binary, args, platform, { env });
|
|
273
291
|
|
|
292
|
+
// An opaque CLI invocation is one send. Its internal retries are not observable here;
|
|
293
|
+
// terminal usage still settles the complete amount, including usage above the estimate.
|
|
294
|
+
let producer: { close(): void } | undefined;
|
|
295
|
+
let permit: SingleUseDispatchPermit | undefined;
|
|
296
|
+
try {
|
|
297
|
+
producer = incoming.sendBudget?.beginSpendProducer?.();
|
|
298
|
+
const decision = incoming.sendBudget?.reserveDispatch({
|
|
299
|
+
sendClass: "initial", targetKey: `${provider.adapter}|${profile.canonicalBaseUrl}|${parsed.modelId}`,
|
|
300
|
+
});
|
|
301
|
+
if (decision && !decision.allowed) throw new SendBudgetExhaustedError();
|
|
302
|
+
permit = decision?.allowed ? decision.permit : undefined;
|
|
303
|
+
if (permit && !permit.use()) throw new SendBudgetExhaustedError();
|
|
304
|
+
incoming.onPhysicalSend?.({ ordinal: 1 });
|
|
274
305
|
let child: ChildProcess;
|
|
275
306
|
try {
|
|
276
307
|
child = spawnFn(invocation.file, invocation.args, {
|
|
@@ -288,9 +319,6 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
288
319
|
code: "cli_spawn_failed",
|
|
289
320
|
retryable: false,
|
|
290
321
|
});
|
|
291
|
-
// A synchronous spawn() throw skips the event-loop `finally` below, so the private
|
|
292
|
-
// bridge dir would leak unless it is removed here as well.
|
|
293
|
-
if (toolBridgeDir) await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
294
322
|
return;
|
|
295
323
|
}
|
|
296
324
|
|
|
@@ -339,7 +367,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
339
367
|
// mcp-server.ts exits on stdin EOF, so this ladder reaps the whole tree without knowing
|
|
340
368
|
// the grandchild pid.
|
|
341
369
|
try { child.kill("SIGTERM"); } catch { /* already gone */ }
|
|
342
|
-
killTimer =
|
|
370
|
+
killTimer = setTimeoutFn(() => {
|
|
343
371
|
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
344
372
|
}, killGraceMs);
|
|
345
373
|
};
|
|
@@ -352,7 +380,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
352
380
|
stopStream();
|
|
353
381
|
};
|
|
354
382
|
incoming.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
355
|
-
const timeoutTimer =
|
|
383
|
+
const timeoutTimer = setTimeoutFn(() => {
|
|
356
384
|
kill();
|
|
357
385
|
stopStream();
|
|
358
386
|
emitOnce({ type: "error", message: `${profile.label} turn timed out.`, status: 504, errorType: "upstream_error", code: "timeout", retryable: true });
|
|
@@ -364,7 +392,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
364
392
|
});
|
|
365
393
|
|
|
366
394
|
const cleanup = (): void => {
|
|
367
|
-
|
|
395
|
+
clearTimeoutFn(timeoutTimer);
|
|
368
396
|
incoming.abortSignal?.removeEventListener("abort", onAbort);
|
|
369
397
|
try { child.stdin?.destroy(); } catch { /* ignore */ }
|
|
370
398
|
// Termination is owned by the reap step below, not here: killing in cleanup would set
|
|
@@ -374,9 +402,10 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
374
402
|
let streamProtocolError: string | undefined;
|
|
375
403
|
let streamProtocolCode: string | undefined;
|
|
376
404
|
let turnError: string | undefined;
|
|
405
|
+
let failClosed = false;
|
|
377
406
|
// A successful result frame that arrived after every captured tool call completed but
|
|
378
|
-
// before
|
|
379
|
-
// done(tool_use) at
|
|
407
|
+
// before an authoritative tool-turn stop. The bridge contract still ends the leg with
|
|
408
|
+
// synthesized done(tool_use) at that stop, so result-derived done(stop) is deferred and its
|
|
380
409
|
// usage (authoritative vendor accounting) is folded into the synthesis. Set inside the
|
|
381
410
|
// stream loop; read by the synthesis and the stream-end error selection below.
|
|
382
411
|
let deferredResultDone: Extract<AdapterEvent, { type: "done" }> | undefined;
|
|
@@ -389,7 +418,13 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
389
418
|
maxToolBlockStarts: toolBridge?.maxTurnToolCalls,
|
|
390
419
|
strictToolBlockCapture: Boolean(toolBridge),
|
|
391
420
|
partialToolCallIds: toolBridge ? new Set<string>() : undefined,
|
|
421
|
+
completeAssistantToolUse: toolBridge?.completeAssistantToolUse,
|
|
422
|
+
toolBridgeNames: toolBridge?.completeAssistantToolUse ? toolBridge.emittedNameMap : undefined,
|
|
392
423
|
};
|
|
424
|
+
const toolTurnCompletionSignal = toolBridge?.toolTurnCompletionSignal ?? "message_stop";
|
|
425
|
+
const sawSelectedToolTurnStop = (): boolean => toolTurnCompletionSignal === "assistant_tool_use_stop"
|
|
426
|
+
? state.sawAssistantToolUseStop === true
|
|
427
|
+
: state.sawMessageStop === true;
|
|
393
428
|
|
|
394
429
|
try {
|
|
395
430
|
// Write the replayed conversation, then close stdin so a single-shot turn can complete.
|
|
@@ -403,14 +438,16 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
403
438
|
const historyCharLimit = projectedHistoryCharLimit(
|
|
404
439
|
modelRecordValue(provider.modelContextWindows, parsed.modelId) ?? provider.contextWindow,
|
|
405
440
|
);
|
|
406
|
-
|
|
441
|
+
const vendorNameByWire = toolBridge?.projectHistoryToolNames
|
|
442
|
+
? new Map([...toolBridge.emittedNameMap].map(([emitted, wire]) => [wire, emitted]))
|
|
443
|
+
: undefined;
|
|
444
|
+
for (const line of buildConversationInput(parsed, { maxHistoryChars: historyCharLimit, vendorNameByWire })) stdin.write(`${line}\n`);
|
|
407
445
|
stdin.end();
|
|
408
446
|
}
|
|
409
447
|
const stdout = child.stdout;
|
|
410
448
|
if (!stdout) throw new CodingAgentProtocolError(`${profile.label} CLI produced no stdout stream`);
|
|
411
449
|
try {
|
|
412
450
|
let initValidated = false;
|
|
413
|
-
let failClosed = false;
|
|
414
451
|
for await (const message of readJsonLines(stdout)) {
|
|
415
452
|
if (incoming.abortSignal?.aborted) break;
|
|
416
453
|
if (toolBridge) {
|
|
@@ -432,13 +469,19 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
432
469
|
? message.event as Record<string, unknown>
|
|
433
470
|
: undefined;
|
|
434
471
|
const rawBlock = rawEvent?.content_block;
|
|
472
|
+
const assistantMessage = toolBridge.completeAssistantToolUse && message.type === "assistant"
|
|
473
|
+
&& message.message !== null && typeof message.message === "object"
|
|
474
|
+
? message.message as Record<string, unknown> : undefined;
|
|
475
|
+
const completeToolUse = Array.isArray(assistantMessage?.content) && assistantMessage.content.some(block =>
|
|
476
|
+
block !== null && typeof block === "object" && (block as Record<string, unknown>).type === "tool_use",
|
|
477
|
+
);
|
|
435
478
|
if (
|
|
436
479
|
!initValidated
|
|
437
|
-
&& rawEvent?.type === "content_block_start"
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
480
|
+
&& (completeToolUse || (rawEvent?.type === "content_block_start"
|
|
481
|
+
&& rawBlock !== null
|
|
482
|
+
&& typeof rawBlock === "object"
|
|
483
|
+
&& !Array.isArray(rawBlock)
|
|
484
|
+
&& (rawBlock as Record<string, unknown>).type === "tool_use"))
|
|
442
485
|
) {
|
|
443
486
|
emitOnce({
|
|
444
487
|
type: "error",
|
|
@@ -522,7 +565,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
522
565
|
// other bridge contract violations use.
|
|
523
566
|
emitOnce({
|
|
524
567
|
type: "error",
|
|
525
|
-
message: "
|
|
568
|
+
message: "Coding-agent CLI finished without calling the required tool.",
|
|
526
569
|
status: 502,
|
|
527
570
|
errorType: "upstream_error",
|
|
528
571
|
code: "tool_call_required",
|
|
@@ -567,9 +610,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
567
610
|
// Every captured call completed and the CLI settled with a successful result before
|
|
568
611
|
// message_stop (instead of parking on the never-answering capture server). Emitting
|
|
569
612
|
// this done(stop) now would end the turn as a text completion and skip the
|
|
570
|
-
// synthesized done(tool_use) the client contract expects. Defer it:
|
|
571
|
-
//
|
|
572
|
-
// ends without
|
|
613
|
+
// synthesized done(tool_use) the client contract expects. Defer it: an authoritative
|
|
614
|
+
// tool-turn stop synthesizes the terminal event with this frame's usage, and a stream
|
|
615
|
+
// that ends without a stop fails closed with protocol_error below.
|
|
573
616
|
deferredResultDone = event;
|
|
574
617
|
continue;
|
|
575
618
|
}
|
|
@@ -578,10 +621,11 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
578
621
|
: event);
|
|
579
622
|
}
|
|
580
623
|
if (failClosed) break;
|
|
624
|
+
const sawToolTurnStop = sawSelectedToolTurnStop();
|
|
581
625
|
if (
|
|
582
626
|
toolBridge
|
|
583
627
|
&& !terminalEmitted
|
|
584
|
-
&&
|
|
628
|
+
&& sawToolTurnStop
|
|
585
629
|
&& (state.toolBlockStarts ?? 0) > 0
|
|
586
630
|
&& (state.completedToolCalls ?? 0) !== (state.toolBlockStarts ?? 0)
|
|
587
631
|
) {
|
|
@@ -596,9 +640,16 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
596
640
|
kill();
|
|
597
641
|
break;
|
|
598
642
|
}
|
|
599
|
-
if (toolBridge && !terminalEmitted &&
|
|
600
|
-
|
|
601
|
-
|
|
643
|
+
if (toolBridge && !terminalEmitted && sawToolTurnStop && (state.toolBlockStarts ?? 0) > 0
|
|
644
|
+
&& (state.completedToolCalls ?? 0) === (state.toolBlockStarts ?? 0)) {
|
|
645
|
+
if (!initValidated) {
|
|
646
|
+
emitOnce({ type: "error", message: "Coding-agent CLI ended before the tool bridge init handshake completed.",
|
|
647
|
+
status: 502, errorType: "upstream_error", code: "tool_bridge_init_missing", retryable: false });
|
|
648
|
+
kill();
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
// Both partial and complete tool calls require the validated handshake.
|
|
652
|
+
// The capture-only MCP handler never answers, so the CLI parks after the tool-turn stop.
|
|
602
653
|
// The completed tool_use blocks are this turn's structured output: end the leg here
|
|
603
654
|
// and terminate the tree; the client executes, and the next request continues.
|
|
604
655
|
// Pre-result usage snapshots keep this terminated leg accountable: no result frame
|
|
@@ -626,24 +677,21 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
626
677
|
} finally {
|
|
627
678
|
releaseOpenToolBlocks(state);
|
|
628
679
|
cleanup();
|
|
629
|
-
if (toolBridgeDir) {
|
|
630
|
-
await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
631
|
-
}
|
|
632
680
|
}
|
|
633
681
|
|
|
634
682
|
// Reap the process so no zombie is left behind (§三十): wait for the real `close`, and
|
|
635
683
|
// force-terminate only if it lingers past the grace window after the stream ended.
|
|
636
|
-
const graceTimer =
|
|
684
|
+
const graceTimer = setTimeoutFn(() => { kill(); }, killGraceMs);
|
|
637
685
|
let reapTimer: ReturnType<typeof setTimeout> | undefined;
|
|
638
686
|
await Promise.race([
|
|
639
687
|
processLifecycle,
|
|
640
688
|
new Promise<void>(resolve => {
|
|
641
|
-
reapTimer =
|
|
689
|
+
reapTimer = setTimeoutFn(resolve, reapTimeoutMs);
|
|
642
690
|
}),
|
|
643
691
|
]);
|
|
644
|
-
|
|
645
|
-
if (reapTimer)
|
|
646
|
-
if (killTimer)
|
|
692
|
+
clearTimeoutFn(graceTimer);
|
|
693
|
+
if (reapTimer) clearTimeoutFn(reapTimer);
|
|
694
|
+
if (killTimer) clearTimeoutFn(killTimer);
|
|
647
695
|
|
|
648
696
|
if (!terminalEmitted) {
|
|
649
697
|
const stderr = redactSecrets(boundedStderr(stderrChunks), profile.tokenEnv, apiKey);
|
|
@@ -686,10 +734,10 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
686
734
|
code: "process_exit_error",
|
|
687
735
|
retryable: false,
|
|
688
736
|
});
|
|
689
|
-
} else if (toolBridge && deferredResultDone !== undefined && !
|
|
737
|
+
} else if (toolBridge && deferredResultDone !== undefined && !sawSelectedToolTurnStop()) {
|
|
690
738
|
emitOnce({
|
|
691
739
|
type: "error",
|
|
692
|
-
message: `${profile.label} CLI delivered a terminal result before
|
|
740
|
+
message: `${profile.label} CLI delivered a terminal result before the authoritative tool-turn stop on a tool-bridge turn.`,
|
|
693
741
|
status: 502,
|
|
694
742
|
errorType: "upstream_error",
|
|
695
743
|
code: "protocol_error",
|
|
@@ -709,6 +757,15 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
709
757
|
});
|
|
710
758
|
}
|
|
711
759
|
}
|
|
760
|
+
} finally {
|
|
761
|
+
try { permit?.release(); }
|
|
762
|
+
finally {
|
|
763
|
+
try { producer?.close(); }
|
|
764
|
+
finally {
|
|
765
|
+
if (toolBridgeDir) await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
712
769
|
}
|
|
713
770
|
|
|
714
771
|
function boundedStderr(chunks: string[]): string {
|
|
@@ -140,6 +140,14 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
|
|
|
140
140
|
thinking: { levels: FULL, order: T },
|
|
141
141
|
},
|
|
142
142
|
},
|
|
143
|
+
// 261008 Haiku 5.5: cursor.com/docs/models/claude-haiku-5-5, 1M and low..max.
|
|
144
|
+
// Preemptive regular-only shape until GetUsableModels confirms a thinking variant.
|
|
145
|
+
"claude-haiku-5-5": {
|
|
146
|
+
displayName: "Claude Haiku 5.5",
|
|
147
|
+
window: CONTEXT_1M,
|
|
148
|
+
defaultVariant: "regular",
|
|
149
|
+
variants: { regular: { levels: FULL } },
|
|
150
|
+
},
|
|
143
151
|
// 260929 Claude Sonnet 5.5: cursor.com/docs/models/claude-sonnet-5-5 lists it (1M max context),
|
|
144
152
|
// but the live GetUsableModels roster does not yet. Shaped like Opus 5.5 (flat effort ids, no
|
|
145
153
|
// thinking variant); re-shape from the roster once it appears.
|
|
@@ -41,6 +41,8 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
41
41
|
"claude-opus-5-5-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
42
42
|
// 260929 preemptive Sonnet 5.5: Anthropic documents low..max; flat ids like Opus 5.5 until the
|
|
43
43
|
// live GetUsableModels roster lists the model.
|
|
44
|
+
// 261008 preemptive Haiku 5.5: flat regular low..max ids, per Cursor docs.
|
|
45
|
+
"claude-haiku-5-5": ["low", "medium", "high", "xhigh", "max"],
|
|
44
46
|
"claude-sonnet-5-5": ["low", "medium", "high", "xhigh", "max"],
|
|
45
47
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
46
48
|
"glm-5.2": ["high", "max"],
|
|
@@ -73,6 +73,8 @@ export const DEVIN_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
73
73
|
"claude-fable-5-1": 1_000_000,
|
|
74
74
|
// 260929 preemptive: Claude Sonnet 5.5 (1M per Anthropic) seeded before Devin's live catalog lists it.
|
|
75
75
|
"claude-sonnet-5-5": 1_000_000,
|
|
76
|
+
// 261008 preemptive: Haiku 5.5 (1M per Anthropic), ahead of Devin live discovery.
|
|
77
|
+
"claude-haiku-5-5": 1_000_000,
|
|
76
78
|
"claude-sonnet-5": 1_000_000,
|
|
77
79
|
"glm-5-2": 200_000,
|
|
78
80
|
"glm-5-3": 1_048_576,
|
|
@@ -183,12 +183,12 @@ async function fetchWithResetRecovery(
|
|
|
183
183
|
sendClass: "transient", targetKey: destination, rebasedTarget: destinationRebased,
|
|
184
184
|
});
|
|
185
185
|
destinationRebased = false;
|
|
186
|
-
if (decision &&
|
|
186
|
+
if (decision && !decision.allowed) {
|
|
187
187
|
throw new SendBudgetExhaustedError(destination);
|
|
188
188
|
}
|
|
189
189
|
// Reported after admission and before dispatch, so a refused send is never counted and an
|
|
190
190
|
// admitted one is counted exactly once whichever way the fetch below settles.
|
|
191
|
-
|
|
191
|
+
const permit = decision?.allowed ? decision.permit : undefined;
|
|
192
192
|
const requestUrl = request.url;
|
|
193
193
|
try {
|
|
194
194
|
const headers = new Headers(request.headers);
|
|
@@ -201,7 +201,12 @@ async function fetchWithResetRecovery(
|
|
|
201
201
|
...(recovered ? { keepalive: false } : {}),
|
|
202
202
|
}, timeoutMs, ctx.abortSignal, ctx.stream, (async (input, init) => {
|
|
203
203
|
try {
|
|
204
|
-
|
|
204
|
+
const run = async (): Promise<Response> => {
|
|
205
|
+
if (permit && !permit.use()) throw new SendBudgetExhaustedError(destination);
|
|
206
|
+
notePhysicalSend(attempt > 0);
|
|
207
|
+
return (executor.unpacedFetch ?? executor)(input, init);
|
|
208
|
+
};
|
|
209
|
+
return await (permit?.execute ? permit.execute(run) : run());
|
|
205
210
|
} catch (error) {
|
|
206
211
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
207
212
|
const signal = init?.signal;
|
|
@@ -224,7 +229,7 @@ async function fetchWithResetRecovery(
|
|
|
224
229
|
baseDelayMs: RESET_RETRY_BASE_MS,
|
|
225
230
|
maxDelayMs: RESET_RETRY_MAX_MS,
|
|
226
231
|
}), ctx.abortSignal);
|
|
227
|
-
}
|
|
232
|
+
} finally { permit?.release(); }
|
|
228
233
|
} finally {
|
|
229
234
|
releaseProviderRequestSlot(slot);
|
|
230
235
|
}
|
|
@@ -142,11 +142,12 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
// Gateways serve
|
|
148
|
-
// of leading path segments is accepted; the final segment
|
|
149
|
-
|
|
145
|
+
// Two pinned chat templates reject non-leading system and every developer role while a
|
|
146
|
+
// later user renders in place: Qwen3.8-27B and the OrcaSAQ-2-Cyber-27B GGUF family.
|
|
147
|
+
// Gateways serve them under namespaced ids (LiteLLM's openai/Qwen3.8-27B, #6674), so any
|
|
148
|
+
// number of leading path segments is accepted; the final segment names the checkpoint.
|
|
149
|
+
// Orca's GGUF marker must precede any colon quant tag; other Orca variants are unchanged.
|
|
150
|
+
const leadingSystemTemplate = !nativeOpenAI && /^(?:[^/]+\/)*(?:Qwen3\.8-27B|qwen3-8-27b(?:-(?:fp8|lora))?|OrcaSAQ-2-Cyber-27B(?=[^:/\s]*GGUF)[^:/\s]*(?::[\w-]+)?)$/i.test(parsed.modelId);
|
|
150
151
|
// Which role a developer message carries, and why the unrecorded state folds, is stated once
|
|
151
152
|
// in ./developer-role.ts and read from there by the native passthrough as well. Either way the
|
|
152
153
|
// message keeps the slot it arrived in — only the role changes, never the position.
|
|
@@ -196,7 +197,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
196
197
|
// message instead is rejected by some upstreams. Native OpenAI keeps its existing
|
|
197
198
|
// empty-developer wire, which is a separate question from placement.
|
|
198
199
|
if (!nativeOpenAI && text.length === 0) break;
|
|
199
|
-
chatMsg = { role:
|
|
200
|
+
chatMsg = { role: leadingSystemTemplate ? "user" : developerWireRole, content: text };
|
|
200
201
|
} else if (typeof msg.content === "string") {
|
|
201
202
|
chatMsg = { role: "user", content: msg.content };
|
|
202
203
|
} else if (!hasStructured) {
|
|
@@ -36,11 +36,14 @@ export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallbac
|
|
|
36
36
|
const physicalExecutor = (async (input, init) => {
|
|
37
37
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
38
38
|
if (init?.signal?.aborted) throw abortError(init.signal);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const run = async (): Promise<Response> => {
|
|
40
|
+
if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
|
|
41
|
+
dispatched = true;
|
|
42
|
+
ordinal += 1;
|
|
43
|
+
ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
|
|
44
|
+
return (executor.unpacedFetch ?? executor)(input, init);
|
|
45
|
+
};
|
|
46
|
+
return permit?.execute ? permit.execute(run) : run();
|
|
44
47
|
}) as typeof globalThis.fetch;
|
|
45
48
|
let slot: ProviderRequestSlot | undefined;
|
|
46
49
|
try {
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
2
3
|
import type { AdapterRequest, ProviderAdapter } from "../base";
|
|
3
4
|
import { mapReasoningEffort } from "../../reasoning-effort";
|
|
4
|
-
import { buildSystemPrompt } from "../coding-agent/protocol";
|
|
5
|
-
import {
|
|
5
|
+
import { buildSystemPrompt, MAX_TOOL_BLOCK_STARTS } from "../coding-agent/protocol";
|
|
6
|
+
import { buildCodingAgentToolCatalog, CodingAgentToolCatalogError } from "../coding-agent/tool-catalog";
|
|
7
|
+
import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type CodingAgentToolBridgeInput } from "../coding-agent/turn";
|
|
6
8
|
import { QODER_PROFILES, type QoderProfile } from "./profiles";
|
|
7
9
|
import { QoderScaffoldFilter, QODER_SCAFFOLD_ERROR_CODE, qoderScaffoldErrorMessage } from "./scaffold-guard";
|
|
8
10
|
|
|
9
11
|
export type QoderAdapterDeps = CodingAgentDeps;
|
|
10
12
|
|
|
13
|
+
const QODER_MCP_SERVER_NAME = "opencodex";
|
|
14
|
+
const QODER_MCP_SERVER_PATH = fileURLToPath(new URL("../coding-agent/mcp-server.ts", import.meta.url));
|
|
15
|
+
|
|
11
16
|
export function buildQoderChildEnv(profile: QoderProfile, apiKey: string, systemPrompt?: string): Record<string, string> {
|
|
12
17
|
const promptEnv = profile.region === "cn" ? "QODERCN_APPEND_SYSTEM_PROMPT" : "QODER_APPEND_SYSTEM_PROMPT";
|
|
13
18
|
return { ...baseScopedEnv(), NO_COLOR: "1", [profile.tokenEnv]: apiKey,
|
|
14
19
|
...(systemPrompt ? { [promptEnv]: systemPrompt } : {}) };
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
/**
|
|
22
|
+
/** Disable Qoder's built-in tools; only a request-scoped MCP catalog can be exposed. */
|
|
18
23
|
export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderConfig): string[] {
|
|
19
24
|
const args = [
|
|
20
25
|
"-p",
|
|
@@ -35,8 +40,8 @@ export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderCo
|
|
|
35
40
|
/**
|
|
36
41
|
* Wrap the turn's outbound channel with the scaffolding guard (#4190).
|
|
37
42
|
*
|
|
38
|
-
* The vendor CLI can put its own agent layer into the text channel despite
|
|
39
|
-
*
|
|
43
|
+
* The vendor CLI can put its own agent layer into the text channel despite built-in tools
|
|
44
|
+
* being disabled, and the shared stream-json parser forwards a text delta
|
|
40
45
|
* without inspecting it. This is the last point that is still qoder-specific, so the guard
|
|
41
46
|
* sits here rather than in the parser every coding-agent CLI shares.
|
|
42
47
|
*
|
|
@@ -123,7 +128,36 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
|
|
|
123
128
|
});
|
|
124
129
|
return;
|
|
125
130
|
}
|
|
126
|
-
|
|
131
|
+
let catalog: ReturnType<typeof buildCodingAgentToolCatalog>;
|
|
132
|
+
try {
|
|
133
|
+
catalog = buildCodingAgentToolCatalog(parsed, QODER_MCP_SERVER_NAME);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (!(error instanceof CodingAgentToolCatalogError)) throw error;
|
|
136
|
+
emit({
|
|
137
|
+
type: "error",
|
|
138
|
+
message: "Invalid Qoder tool catalog.",
|
|
139
|
+
status: 400,
|
|
140
|
+
errorType: "invalid_request_error",
|
|
141
|
+
code: "tool_catalog_invalid",
|
|
142
|
+
retryable: false,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const toolBridge: CodingAgentToolBridgeInput | undefined = catalog.tools.length > 0 || catalog.requireToolCall
|
|
147
|
+
? {
|
|
148
|
+
serverName: QODER_MCP_SERVER_NAME,
|
|
149
|
+
serverModulePath: QODER_MCP_SERVER_PATH,
|
|
150
|
+
tools: catalog.tools,
|
|
151
|
+
emittedNameMap: catalog.emittedNameMap,
|
|
152
|
+
maxTurnToolCalls: MAX_TOOL_BLOCK_STARTS,
|
|
153
|
+
projectHistoryToolNames: true,
|
|
154
|
+
completeAssistantToolUse: true,
|
|
155
|
+
toolTurnCompletionSignal: "assistant_tool_use_stop",
|
|
156
|
+
allowedToolsFlag: "--allowed-tools",
|
|
157
|
+
standaloneEntrypoint: "__qoder-mcp",
|
|
158
|
+
requireToolCall: catalog.requireToolCall,
|
|
159
|
+
}
|
|
160
|
+
: undefined;
|
|
127
161
|
// Keep the folded prompt out of argv and never inherit an ambient vendor prompt.
|
|
128
162
|
const system = buildSystemPrompt(parsed);
|
|
129
163
|
await runCodingAgentTurn({
|
|
@@ -135,6 +169,7 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
|
|
|
135
169
|
buildArgs: (_profile, req, prov) => buildQoderArgs(req, prov),
|
|
136
170
|
buildEnv: (profile, apiKey) => buildQoderChildEnv(profile as QoderProfile, apiKey, system),
|
|
137
171
|
deps,
|
|
172
|
+
...(toolBridge ? { toolBridge } : {}),
|
|
138
173
|
});
|
|
139
174
|
},
|
|
140
175
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vendor-scaffolding guard for the qoder route (#4190).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The vendor CLI does not always honour that. It has been observed emitting its own agent
|
|
4
|
+
* Even with built-in tools disabled and strict MCP isolation, Codex keeps tool execution
|
|
5
|
+
* ownership. The vendor CLI does not always honour that. It has been observed emitting its own agent
|
|
7
6
|
* layer into the assistant text channel — an MCP lazy-loading `<system-reminder>` block
|
|
8
7
|
* listing the local machine's configured MCP servers, and framework tool-call markup with a
|
|
9
8
|
* mismatched closer. Both reached the client verbatim, because the shared stream-json parser
|