@iislee/opencodex 2.31.0 → 2.35.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -101,3 +101,55 @@ export function slugEquivalenceKey(slug: string): string {
|
|
|
101
101
|
export function slugsEquivalent(a: string, b: string): boolean {
|
|
102
102
|
return a === b || slugEquivalenceKey(a) === slugEquivalenceKey(b);
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve one config selection against a provider's known native ids (#2491).
|
|
107
|
+
*
|
|
108
|
+
* `slugEquivalenceKey` is deliberately lossy — the Codex one-slash rule forces `a/b` and
|
|
109
|
+
* `a-b` onto the same encoded form — so a selection written in either spelling matches BOTH
|
|
110
|
+
* when a provider publishes both. Filtering and persisted sync share that key, which keeps
|
|
111
|
+
* them consistent with each other but silently over-grants.
|
|
112
|
+
*
|
|
113
|
+
* This resolver keeps the tolerant behaviour (a selection still matches through either
|
|
114
|
+
* spelling, and an id absent from an incomplete live roster still resolves) while reporting
|
|
115
|
+
* whether the match was EXACT or merely equivalent. A caller that can afford to be strict —
|
|
116
|
+
* one holding a complete known-id set — can then prefer the exact row instead of granting the
|
|
117
|
+
* whole collision class.
|
|
118
|
+
*
|
|
119
|
+
* Returning the ambiguity rather than resolving it is deliberate: the roster is an incomplete
|
|
120
|
+
* dictionary, so silently narrowing to the exact spelling would hide a published id whenever
|
|
121
|
+
* discovery omitted it. The caller owns that tradeoff because only the caller knows whether
|
|
122
|
+
* its id set is complete.
|
|
123
|
+
*/
|
|
124
|
+
export interface SlugSelectionMatch {
|
|
125
|
+
/** Native ids this selection admits. */
|
|
126
|
+
readonly matched: readonly string[];
|
|
127
|
+
/** The id whose raw form the selection names exactly, when one exists. */
|
|
128
|
+
readonly exact: string | undefined;
|
|
129
|
+
/** True when more than one known id shares the selection's encoded form. */
|
|
130
|
+
readonly ambiguous: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function resolveSlugSelection(
|
|
134
|
+
provider: string,
|
|
135
|
+
selection: string,
|
|
136
|
+
knownIds: Iterable<string>,
|
|
137
|
+
): SlugSelectionMatch {
|
|
138
|
+
// A slash in the selection is ambiguous on its own: `p/a-b` is provider-qualified, while
|
|
139
|
+
// `a/b` is a bare NATIVE id that happens to contain a slash. Treating every slash-bearing
|
|
140
|
+
// selection as provider-qualified made `a/b` resolve against provider "a", so the same
|
|
141
|
+
// collision reported ambiguous through the dash spelling and unambiguous through the slash
|
|
142
|
+
// spelling — the exact asymmetry this resolver exists to remove.
|
|
143
|
+
const qualified = selection.startsWith(`${provider}/`)
|
|
144
|
+
? selection
|
|
145
|
+
: routedSlug(provider, selection);
|
|
146
|
+
const selectionKey = slugEquivalenceKey(qualified);
|
|
147
|
+
const matched: string[] = [];
|
|
148
|
+
let exact: string | undefined;
|
|
149
|
+
for (const id of knownIds) {
|
|
150
|
+
if (slugEquivalenceKey(routedSlug(provider, id)) !== selectionKey) continue;
|
|
151
|
+
matched.push(id);
|
|
152
|
+
if (id === selection || `${provider}/${id}` === selection) exact = id;
|
|
153
|
+
}
|
|
154
|
+
return { matched, exact, ambiguous: matched.length > 1 };
|
|
155
|
+
}
|
|
@@ -4,6 +4,27 @@ import { resolveGithubCopilotTransport } from "./github-copilot-transport";
|
|
|
4
4
|
|
|
5
5
|
export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
6
6
|
|
|
7
|
+
/** The two hosts that serve xAI's Responses API: the public API and the Grok CLI proxy. */
|
|
8
|
+
const XAI_RESPONSES_HOSTS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* True when this provider's Responses traffic terminates at xAI itself.
|
|
12
|
+
*
|
|
13
|
+
* Probed 2026-08-22 one field per request: the two hosts accept and refuse exactly the same
|
|
14
|
+
* web_search fields, so they are one dialect rather than two. Matching is exact-host over
|
|
15
|
+
* https, which keeps lookalikes (`api.x.ai.evil.test`) and nonstandard ports out.
|
|
16
|
+
*/
|
|
17
|
+
export function isXaiResponsesDestination(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(provider.baseUrl);
|
|
20
|
+
return url.protocol === "https:"
|
|
21
|
+
&& XAI_RESPONSES_HOSTS.has(url.hostname.toLowerCase())
|
|
22
|
+
&& (url.port === "" || url.port === "443");
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
export const XAI_GROK_COMPATIBILITY = {
|
|
8
29
|
version: "0.2.93",
|
|
9
30
|
userAgent: "opencodex-grok/0.2.93",
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -14,6 +14,16 @@ export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] =
|
|
|
14
14
|
const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
|
|
15
15
|
const CODEX_REASONING_SET = new Set(CODEX_REASONING_ORDER);
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sentinel wire value in reasoningEffortMap / modelReasoningEffortMap to explicitly
|
|
19
|
+
* omit the reasoning_effort field from the upstream wire request (issue #2356).
|
|
20
|
+
*/
|
|
21
|
+
export const REASONING_EFFORT_OMIT_SENTINEL = "__omit__";
|
|
22
|
+
|
|
23
|
+
export function isReasoningEffortOmitted(wireEffort: string | undefined): boolean {
|
|
24
|
+
return wireEffort === REASONING_EFFORT_OMIT_SENTINEL;
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
/** True when `effort` is a member of the Codex reasoning ladder (low..ultra). */
|
|
18
28
|
export function isCodexReasoningEffort(effort: string): boolean {
|
|
19
29
|
return CODEX_REASONING_SET.has(effort);
|
|
@@ -170,7 +180,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
|
|
|
170
180
|
const boundary = requested === "ultra" ? "max" : requested;
|
|
171
181
|
|
|
172
182
|
const wireMap = reasoningEffortMapFor(provider, modelId);
|
|
173
|
-
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary))
|
|
183
|
+
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) {
|
|
184
|
+
const mapped = wireMap[boundary];
|
|
185
|
+
return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
|
|
186
|
+
}
|
|
174
187
|
|
|
175
188
|
const supported = configuredReasoningEfforts(provider, modelId);
|
|
176
189
|
const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
|
|
@@ -178,6 +191,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
|
|
|
178
191
|
|
|
179
192
|
// Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it.
|
|
180
193
|
const wire = codexEffort === "ultra" ? "max" : codexEffort;
|
|
181
|
-
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire))
|
|
194
|
+
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) {
|
|
195
|
+
const mapped = wireMap[wire];
|
|
196
|
+
return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
|
|
197
|
+
}
|
|
198
|
+
if (wire === REASONING_EFFORT_OMIT_SENTINEL) return undefined;
|
|
182
199
|
return wire;
|
|
183
200
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Representation repair for top-level Codex apply_patch custom-tool payloads.
|
|
2
|
+
//
|
|
3
|
+
// Some routed models decorate the first and last lines as
|
|
4
|
+
// `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
|
|
5
|
+
// valid custom-tool payloads. Repair is deliberately limited to a complete,
|
|
6
|
+
// structurally recognizable top-level patch: arbitrary `exec` JavaScript is
|
|
7
|
+
// caller-authored executable input and must remain byte-identical.
|
|
8
|
+
//
|
|
9
|
+
// This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
|
|
10
|
+
// repair the one faithful reading, leave genuine patch content alone.
|
|
11
|
+
|
|
12
|
+
const PATCH_BEGIN = "*** Begin Patch";
|
|
13
|
+
const PATCH_END = "*** End Patch";
|
|
14
|
+
const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
|
|
15
|
+
const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
|
|
16
|
+
|
|
17
|
+
/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
|
|
18
|
+
export function unwrapFreeformToolInput(argumentsText: unknown): string {
|
|
19
|
+
if (typeof argumentsText !== "string") return "";
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(argumentsText);
|
|
22
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
23
|
+
const input = (parsed as { input?: unknown }).input;
|
|
24
|
+
if (typeof input === "string") return input;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// The string is the freeform body, not nested JSON.
|
|
28
|
+
}
|
|
29
|
+
return argumentsText;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strip trailing `***` only from the outer lines of one complete patch.
|
|
34
|
+
* Internal patch content, incomplete envelopes, and non-patch text are exact
|
|
35
|
+
* pass-throughs.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeApplyPatchDelimiters(text: string): string {
|
|
38
|
+
const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
|
|
39
|
+
if (!match) return text;
|
|
40
|
+
const [, begin, beginBreak, body, endBreak, end, trailingBreak = ""] = match;
|
|
41
|
+
if (!PATCH_OPERATION_LINE.test(body)) return text;
|
|
42
|
+
if (begin === PATCH_BEGIN && end === PATCH_END) return text;
|
|
43
|
+
return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Repair freeform input before Codex sees it.
|
|
48
|
+
*
|
|
49
|
+
* Only a bare or reserved-`functions` `apply_patch` payload may receive delimiter
|
|
50
|
+
* repair. Remote namespaces own their grammar; those bodies and every other
|
|
51
|
+
* freeform input are unwrapped and left byte-exact.
|
|
52
|
+
*/
|
|
53
|
+
export function repairFreeformToolInput(
|
|
54
|
+
argumentsText: unknown,
|
|
55
|
+
toolName = "",
|
|
56
|
+
namespace?: string,
|
|
57
|
+
): string {
|
|
58
|
+
const unwrapped = unwrapFreeformToolInput(argumentsText);
|
|
59
|
+
const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
|
|
60
|
+
return ownsApplyPatchGrammar && toolName === "apply_patch"
|
|
61
|
+
? normalizeApplyPatchDelimiters(unwrapped)
|
|
62
|
+
: unwrapped;
|
|
63
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { normalizeApplyPatchDelimiters } from "./apply-patch-envelope";
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function unwrapPatchInput(value: string): string {
|
|
8
|
+
try {
|
|
9
|
+
const parsed: unknown = JSON.parse(value);
|
|
10
|
+
if (isPlainObject(parsed)) {
|
|
11
|
+
if (typeof parsed.input === "string") return parsed.input;
|
|
12
|
+
if (typeof parsed.patch === "string") return parsed.patch;
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
// Native custom calls carry the patch body directly.
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert a nested Code Mode helper call into unified-exec JavaScript.
|
|
22
|
+
*
|
|
23
|
+
* Parsed values are serialized as data, never interpolated as source, so command and patch text
|
|
24
|
+
* cannot escape the generated call. Invalid structured shell payloads are also passed as data so
|
|
25
|
+
* nested-tool validation can reject them without evaluating provider text as JavaScript.
|
|
26
|
+
*/
|
|
27
|
+
export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string {
|
|
28
|
+
if (typeof argumentsText !== "string") return "";
|
|
29
|
+
if (toolName === "apply_patch") {
|
|
30
|
+
const patch = normalizeApplyPatchDelimiters(unwrapPatchInput(argumentsText));
|
|
31
|
+
return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
|
|
32
|
+
}
|
|
33
|
+
let parsed: unknown = argumentsText;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(argumentsText);
|
|
36
|
+
} catch {
|
|
37
|
+
// Keep malformed provider text as data rather than executable source.
|
|
38
|
+
}
|
|
39
|
+
const args: unknown = isPlainObject(parsed) ? { ...parsed } : parsed;
|
|
40
|
+
if (
|
|
41
|
+
toolName === "shell_command"
|
|
42
|
+
&& isPlainObject(args)
|
|
43
|
+
&& typeof args.command === "string"
|
|
44
|
+
&& args.cmd === undefined
|
|
45
|
+
) {
|
|
46
|
+
args.cmd = args.command;
|
|
47
|
+
delete args.command;
|
|
48
|
+
}
|
|
49
|
+
return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
|
|
50
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { namespacedToolName } from "../types";
|
|
1
|
+
import { namespacedToolName, normalizeDeclaredToolName } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
normalizeApplyPatchDelimiters,
|
|
4
|
+
repairFreeformToolInput,
|
|
5
|
+
unwrapFreeformToolInput,
|
|
6
|
+
} from "./apply-patch-envelope";
|
|
7
|
+
import { compileCodeModeHelperInput } from "./code-mode-helper-compat";
|
|
2
8
|
import { collectResponsesToolGroups } from "./tool-groups";
|
|
3
9
|
|
|
4
10
|
const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
|
|
@@ -15,19 +21,42 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
15
21
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
function customToolInput(argumentsText: unknown): string {
|
|
19
|
-
if (typeof argumentsText !== "string") return "";
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(argumentsText) as unknown;
|
|
22
|
-
if (isPlainObject(parsed) && typeof parsed.input === "string") return parsed.input;
|
|
23
|
-
} catch { /* malformed arguments stay visible to the client */ }
|
|
24
|
-
return argumentsText;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
24
|
function customToolWireName(namespace: string | undefined, name: string): string {
|
|
28
25
|
return namespace === BUILTIN_FUNCTIONS_NAMESPACE ? name : namespacedToolName(namespace, name);
|
|
29
26
|
}
|
|
30
27
|
|
|
28
|
+
function toolChoiceAllowsRoutedCustomTool(
|
|
29
|
+
body: unknown,
|
|
30
|
+
wireName: string,
|
|
31
|
+
candidateNames: ReadonlySet<string>,
|
|
32
|
+
): boolean {
|
|
33
|
+
if (!isPlainObject(body)) return true;
|
|
34
|
+
const choice = body.tool_choice;
|
|
35
|
+
if (choice === undefined || choice === null || choice === "auto" || choice === "required") {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (choice === "none") return false;
|
|
39
|
+
if (!isPlainObject(choice)) return true;
|
|
40
|
+
|
|
41
|
+
const selectorAllows = (selector: unknown): boolean => {
|
|
42
|
+
if (!isPlainObject(selector) || typeof selector.name !== "string") return false;
|
|
43
|
+
if (selector.type !== "custom") return false;
|
|
44
|
+
if (typeof selector.namespace === "string") {
|
|
45
|
+
return customToolWireName(selector.namespace, selector.name) === wireName;
|
|
46
|
+
}
|
|
47
|
+
if (selector.name === wireName) return true;
|
|
48
|
+
const suffix = `__${selector.name}`;
|
|
49
|
+
const candidates = [...candidateNames].filter(name => name.endsWith(suffix));
|
|
50
|
+
return candidates.length === 1 && candidates[0] === wireName;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (choice.type === "function" || choice.type === "custom") return selectorAllows(choice);
|
|
54
|
+
if (choice.type === "allowed_tools" && Array.isArray(choice.tools)) {
|
|
55
|
+
return choice.tools.some(selectorAllows);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
/** Final upstream identity of a call, including a namespace restored by an earlier rewrite. */
|
|
32
61
|
export function routedCustomToolWireName(value: unknown): string | undefined {
|
|
33
62
|
if (!isPlainObject(value) || typeof value.name !== "string") return undefined;
|
|
@@ -37,13 +66,28 @@ export function routedCustomToolWireName(value: unknown): string | undefined {
|
|
|
37
66
|
);
|
|
38
67
|
}
|
|
39
68
|
|
|
69
|
+
/** Resolve a provider-emitted wire name to the routed custom tool the client declared. */
|
|
70
|
+
export function routedCustomToolTargetName(
|
|
71
|
+
value: unknown,
|
|
72
|
+
names: ReadonlySet<string>,
|
|
73
|
+
declaredNames?: ReadonlySet<string>,
|
|
74
|
+
): string | undefined {
|
|
75
|
+
const wireName = routedCustomToolWireName(value);
|
|
76
|
+
if (wireName === undefined) return undefined;
|
|
77
|
+
if (names.has(wireName)) return wireName;
|
|
78
|
+
if (!isPlainObject(value) || typeof value.namespace === "string") return undefined;
|
|
79
|
+
const normalized = normalizeDeclaredToolName(wireName, declaredNames);
|
|
80
|
+
return normalized !== wireName && names.has(normalized) ? normalized : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
40
83
|
/**
|
|
41
|
-
* Names of
|
|
42
|
-
*
|
|
84
|
+
* Names of custom declarations after namespace lowering. The selection flag separates converted
|
|
85
|
+
* names from native passthrough names while keeping same-named function and custom children distinct.
|
|
43
86
|
*/
|
|
44
87
|
function collectRoutedCustomToolWireNames(
|
|
45
88
|
body: unknown,
|
|
46
89
|
supportsResponsesCustomTools?: boolean,
|
|
90
|
+
passthrough = false,
|
|
47
91
|
): Set<string> {
|
|
48
92
|
const names = new Set<string>();
|
|
49
93
|
const groups = collectResponsesToolGroups(body);
|
|
@@ -64,7 +108,7 @@ function collectRoutedCustomToolWireNames(
|
|
|
64
108
|
if (
|
|
65
109
|
tool.type === "custom"
|
|
66
110
|
&& typeof tool.name === "string"
|
|
67
|
-
&&
|
|
111
|
+
&& routedCustomToolPassesThrough(tool.name, supportsResponsesCustomTools) === passthrough
|
|
68
112
|
) {
|
|
69
113
|
names.add(tool.name);
|
|
70
114
|
continue;
|
|
@@ -77,7 +121,8 @@ function collectRoutedCustomToolWireNames(
|
|
|
77
121
|
isPlainObject(child)
|
|
78
122
|
&& child.type === "custom"
|
|
79
123
|
&& typeof child.name === "string"
|
|
80
|
-
&&
|
|
124
|
+
&& routedCustomToolPassesThrough(child.name, supportsResponsesCustomTools) === passthrough
|
|
125
|
+
&& (!passthrough || tool.name === BUILTIN_FUNCTIONS_NAMESPACE)
|
|
81
126
|
&& !(tool.name === BUILTIN_FUNCTIONS_NAMESPACE && bareWireNames.has(child.name))
|
|
82
127
|
) names.add(customToolWireName(tool.name, child.name));
|
|
83
128
|
}
|
|
@@ -85,7 +130,6 @@ function collectRoutedCustomToolWireNames(
|
|
|
85
130
|
}
|
|
86
131
|
return names;
|
|
87
132
|
}
|
|
88
|
-
|
|
89
133
|
export function customToolItemId(id: unknown): unknown {
|
|
90
134
|
if (typeof id !== "string") return id;
|
|
91
135
|
return id.startsWith("fc_") ? `ctc_${id.slice(3)}` : id;
|
|
@@ -203,64 +247,138 @@ export function rewriteRoutedCustomToolsForUpstream(
|
|
|
203
247
|
): {
|
|
204
248
|
body: unknown;
|
|
205
249
|
names: Set<string>;
|
|
250
|
+
repairNames: Set<string>;
|
|
206
251
|
} {
|
|
207
252
|
const conversionNames = collectRoutedCustomToolNames(body, supportsResponsesCustomTools);
|
|
208
253
|
const names = collectRoutedCustomToolWireNames(body, supportsResponsesCustomTools);
|
|
209
|
-
|
|
254
|
+
const repairNames = collectRoutedCustomToolWireNames(body, supportsResponsesCustomTools, true);
|
|
255
|
+
for (const name of repairNames) {
|
|
256
|
+
if (!toolChoiceAllowsRoutedCustomTool(body, name, repairNames)) repairNames.delete(name);
|
|
257
|
+
}
|
|
258
|
+
if (conversionNames.size === 0) return { body, names, repairNames };
|
|
210
259
|
const callIds = new Set<string>();
|
|
211
260
|
collectConvertedCallIds(body, conversionNames, callIds);
|
|
212
|
-
return { body: rewriteForUpstream(body, conversionNames, callIds), names };
|
|
261
|
+
return { body: rewriteForUpstream(body, conversionNames, callIds), names, repairNames };
|
|
213
262
|
}
|
|
214
263
|
|
|
215
264
|
export function restoreRoutedCustomCalls(
|
|
216
265
|
value: unknown,
|
|
217
266
|
names: ReadonlySet<string>,
|
|
267
|
+
repairNames: ReadonlySet<string> = new Set(),
|
|
268
|
+
declaredNames?: ReadonlySet<string>,
|
|
218
269
|
): { value: unknown; changed: boolean } {
|
|
219
|
-
if (
|
|
270
|
+
if (!isPlainObject(value)) return { value, changed: false };
|
|
271
|
+
|
|
272
|
+
const restoreItem = (item: unknown): { value: unknown; changed: boolean } => {
|
|
273
|
+
if (!isPlainObject(item)) return { value: item, changed: false };
|
|
274
|
+
const wireName = routedCustomToolWireName(item);
|
|
275
|
+
const targetName = routedCustomToolTargetName(item, names, declaredNames);
|
|
276
|
+
if (
|
|
277
|
+
(item.type === "function_call" || item.type === "custom_tool_call")
|
|
278
|
+
&& typeof item.name === "string"
|
|
279
|
+
&& wireName !== undefined
|
|
280
|
+
&& targetName !== undefined
|
|
281
|
+
) {
|
|
282
|
+
const sourceInput = item.type === "function_call" ? item.arguments : item.input;
|
|
283
|
+
const aliased = targetName !== wireName;
|
|
284
|
+
const restored: Record<string, unknown> = {
|
|
285
|
+
...item,
|
|
286
|
+
type: "custom_tool_call",
|
|
287
|
+
id: customToolItemId(item.id),
|
|
288
|
+
name: aliased ? targetName : item.name,
|
|
289
|
+
input: aliased && sourceInput !== ""
|
|
290
|
+
? compileCodeModeHelperInput(sourceInput, item.name)
|
|
291
|
+
: repairFreeformToolInput(
|
|
292
|
+
sourceInput,
|
|
293
|
+
targetName,
|
|
294
|
+
typeof item.namespace === "string" ? item.namespace : undefined,
|
|
295
|
+
),
|
|
296
|
+
};
|
|
297
|
+
delete restored.arguments;
|
|
298
|
+
if (aliased) delete restored.namespace;
|
|
299
|
+
return { value: restored, changed: true };
|
|
300
|
+
}
|
|
301
|
+
if (
|
|
302
|
+
item.type === "custom_tool_call"
|
|
303
|
+
&& typeof item.name === "string"
|
|
304
|
+
&& wireName !== undefined
|
|
305
|
+
&& repairNames.has(wireName)
|
|
306
|
+
&& typeof item.input === "string"
|
|
307
|
+
) {
|
|
308
|
+
const input = normalizeApplyPatchDelimiters(item.input);
|
|
309
|
+
if (input !== item.input) return { value: { ...item, input }, changed: true };
|
|
310
|
+
}
|
|
311
|
+
return { value: item, changed: false };
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const restoreOutput = (output: unknown): { value: unknown; changed: boolean } => {
|
|
315
|
+
if (!Array.isArray(output)) return { value: output, changed: false };
|
|
220
316
|
let changed = false;
|
|
221
|
-
const restored =
|
|
222
|
-
const result =
|
|
317
|
+
const restored = output.map(item => {
|
|
318
|
+
const result = restoreItem(item);
|
|
223
319
|
changed ||= result.changed;
|
|
224
320
|
return result.value;
|
|
225
321
|
});
|
|
226
|
-
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
227
|
-
}
|
|
228
|
-
if (!isPlainObject(value)) return { value, changed: false };
|
|
322
|
+
return changed ? { value: restored, changed: true } : { value: output, changed: false };
|
|
323
|
+
};
|
|
229
324
|
|
|
230
325
|
let changed = false;
|
|
231
|
-
const restored: Record<string, unknown> = {};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
restored
|
|
235
|
-
changed
|
|
326
|
+
const restored: Record<string, unknown> = { ...value };
|
|
327
|
+
const output = restoreOutput(value.output);
|
|
328
|
+
if (output.changed) {
|
|
329
|
+
restored.output = output.value;
|
|
330
|
+
changed = true;
|
|
236
331
|
}
|
|
237
332
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
333
|
+
if (
|
|
334
|
+
(value.type === "response.output_item.added" || value.type === "response.output_item.done")
|
|
335
|
+
&& isPlainObject(value.item)
|
|
336
|
+
) {
|
|
337
|
+
const item = restoreItem(value.item);
|
|
338
|
+
if (item.changed) {
|
|
339
|
+
restored.item = item.value;
|
|
340
|
+
changed = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
typeof value.type === "string"
|
|
346
|
+
&& value.type.startsWith("response.")
|
|
347
|
+
&& isPlainObject(value.response)
|
|
348
|
+
) {
|
|
349
|
+
const response = restoreRoutedCustomCalls(value.response, names, repairNames, declaredNames);
|
|
350
|
+
if (response.changed) {
|
|
351
|
+
restored.response = response.value;
|
|
352
|
+
changed = true;
|
|
353
|
+
}
|
|
245
354
|
}
|
|
355
|
+
|
|
246
356
|
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
247
357
|
}
|
|
248
358
|
|
|
249
359
|
export function restoreRoutedCustomCallsInJson(
|
|
250
360
|
text: string,
|
|
251
361
|
names: ReadonlySet<string>,
|
|
362
|
+
repairNames: ReadonlySet<string> = new Set(),
|
|
363
|
+
declaredNames?: ReadonlySet<string>,
|
|
252
364
|
): string {
|
|
253
|
-
if (names.size === 0) return text;
|
|
365
|
+
if (names.size === 0 && repairNames.size === 0) return text;
|
|
254
366
|
let payload: unknown;
|
|
255
367
|
try {
|
|
256
368
|
payload = JSON.parse(text);
|
|
257
369
|
} catch {
|
|
258
370
|
return text;
|
|
259
371
|
}
|
|
260
|
-
const restored = restoreRoutedCustomCalls(payload, names);
|
|
372
|
+
const restored = restoreRoutedCustomCalls(payload, names, repairNames, declaredNames);
|
|
261
373
|
return restored.changed ? JSON.stringify(restored.value) : text;
|
|
262
374
|
}
|
|
263
375
|
|
|
264
|
-
export function unwrapRoutedCustomToolArguments(
|
|
265
|
-
|
|
376
|
+
export function unwrapRoutedCustomToolArguments(
|
|
377
|
+
argumentsText: unknown,
|
|
378
|
+
toolName = "",
|
|
379
|
+
namespace?: string,
|
|
380
|
+
): string {
|
|
381
|
+
return toolName
|
|
382
|
+
? repairFreeformToolInput(argumentsText, toolName, namespace)
|
|
383
|
+
: unwrapFreeformToolInput(argumentsText);
|
|
266
384
|
}
|
|
@@ -4,6 +4,14 @@ import { collectResponsesToolGroups } from "./tool-groups";
|
|
|
4
4
|
export interface RoutedNamespaceToolIdentity {
|
|
5
5
|
namespace: string;
|
|
6
6
|
name: string;
|
|
7
|
+
/**
|
|
8
|
+
* The kind the tool was DECLARED as. Restoration and `tool_choice` matching both
|
|
9
|
+
* need it: a wire name identifies which tool, not which kind of call may carry it,
|
|
10
|
+
* so without this a tool declared `function` could be selected by a `custom`
|
|
11
|
+
* selector and come back as a `custom_tool_call` — the same name/kind mismatch
|
|
12
|
+
* that motivated narrowing the alias map in the first place.
|
|
13
|
+
*/
|
|
14
|
+
kind: "function" | "custom";
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
export type RoutedNamespaceToolAliases = ReadonlyMap<string, RoutedNamespaceToolIdentity>;
|
|
@@ -138,7 +146,10 @@ function buildRewritePlan(groups: readonly unknown[][]): NamespaceRewritePlan {
|
|
|
138
146
|
addSelector(selectors, `${parsed.namespace}.${childName}`, wireName);
|
|
139
147
|
addSelector(selectors, childName, wireName);
|
|
140
148
|
if (parsed.namespace !== BUILTIN_FUNCTIONS_NAMESPACE) {
|
|
141
|
-
|
|
149
|
+
// A child declared `custom` stays custom; everything else lowers to a
|
|
150
|
+
// function, which is how `buildTools` flattens it upstream.
|
|
151
|
+
const kind = child.type === "custom" ? "custom" : "function";
|
|
152
|
+
aliases.set(wireName, { namespace: parsed.namespace, name: childName, kind });
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
}
|
|
@@ -206,18 +217,34 @@ function rewriteToolList(
|
|
|
206
217
|
* the failure this layer exists to prevent, and this layer's own response restoration is what put
|
|
207
218
|
* the key on the item.
|
|
208
219
|
*/
|
|
220
|
+
/**
|
|
221
|
+
* A selector's `namespace` is either absent — meaning "unqualified, resolve the bare
|
|
222
|
+
* name" — or a string naming the group. A present-but-non-string value is neither, and
|
|
223
|
+
* a malformed selector must not authorize anything: not through the unqualified
|
|
224
|
+
* fallback, and not by happening to carry an already-flattened wire name, which would
|
|
225
|
+
* otherwise match the alias map exactly and arm it anyway.
|
|
226
|
+
*/
|
|
227
|
+
function hasMalformedNamespace(value: Record<string, unknown>): boolean {
|
|
228
|
+
return "namespace" in value && typeof value.namespace !== "string";
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
function rewriteNamedSelector(
|
|
210
232
|
value: unknown,
|
|
211
233
|
plan: NamespaceRewritePlan,
|
|
212
234
|
bareFallback: boolean,
|
|
213
235
|
): unknown {
|
|
214
236
|
if (!isPlainObject(value) || typeof value.name !== "string") return value;
|
|
215
|
-
|
|
237
|
+
// Malformed: hand it back untouched so no rewrite occurs. Authorization rejects it
|
|
238
|
+
// separately — returning it unchanged is not by itself enough, because the name it
|
|
239
|
+
// carries may already BE a wire name.
|
|
240
|
+
if (hasMalformedNamespace(value)) return value;
|
|
241
|
+
const namespace = value.namespace;
|
|
242
|
+
if (typeof namespace !== "string") {
|
|
216
243
|
if (!bareFallback) return value;
|
|
217
244
|
const wireName = plan.selectors.get(value.name) ?? undefined;
|
|
218
245
|
return wireName === undefined || wireName === value.name ? value : { ...value, name: wireName };
|
|
219
246
|
}
|
|
220
|
-
const { namespace, ...rest } = value;
|
|
247
|
+
const { namespace: _dropped, ...rest } = value;
|
|
221
248
|
const wireName = plan.identities.get(loweredIdentity(namespace, value.name))
|
|
222
249
|
?? loweredWireName(namespace, value.name);
|
|
223
250
|
return { ...rest, name: wireName };
|
|
@@ -239,6 +266,59 @@ function rewriteToolChoice(value: unknown, plan: NamespaceRewritePlan): unknown
|
|
|
239
266
|
return changed ? { ...value, tools } : value;
|
|
240
267
|
}
|
|
241
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Keep response restoration inside the caller's per-turn tool authorization boundary. The
|
|
271
|
+
* upstream sees every flattened declaration even when `tool_choice` narrows the tools it may call,
|
|
272
|
+
* so its output cannot be trusted merely because a wire name appeared in that catalog.
|
|
273
|
+
*/
|
|
274
|
+
function authorizedAliases(
|
|
275
|
+
aliases: Map<string, RoutedNamespaceToolIdentity>,
|
|
276
|
+
toolChoice: unknown,
|
|
277
|
+
): Map<string, RoutedNamespaceToolIdentity> {
|
|
278
|
+
if (toolChoice === undefined || toolChoice === "auto" || toolChoice === "required") return aliases;
|
|
279
|
+
if (toolChoice === "none" || !isPlainObject(toolChoice)) return new Map();
|
|
280
|
+
|
|
281
|
+
// name -> the kind the selector claimed. A selector authorizes a tool only when it
|
|
282
|
+
// names it AND agrees about what kind of tool it is.
|
|
283
|
+
let authorized: Map<string, "function" | "custom">;
|
|
284
|
+
if (
|
|
285
|
+
(toolChoice.type === "function" || toolChoice.type === "custom")
|
|
286
|
+
&& typeof toolChoice.name === "string"
|
|
287
|
+
) {
|
|
288
|
+
// A malformed namespace makes the whole selector untrustworthy, even when its
|
|
289
|
+
// name is already a flattened wire name that would match the alias map exactly.
|
|
290
|
+
if (hasMalformedNamespace(toolChoice)) return new Map();
|
|
291
|
+
authorized = new Map([[toolChoice.name, toolChoice.type]]);
|
|
292
|
+
} else if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
293
|
+
authorized = new Map();
|
|
294
|
+
for (const tool of toolChoice.tools) {
|
|
295
|
+
// Match the top-level branch above: only a function/custom selector can
|
|
296
|
+
// authorize a client namespace call. `allowed_tools` entries are typed
|
|
297
|
+
// `{type: string}` by the schema, so the accepted set is open-ended and
|
|
298
|
+
// an allowlist is the only closure that also covers kinds added later.
|
|
299
|
+
// Without this, `{type: "file_search", name: "<wire-name>"}` keeps the
|
|
300
|
+
// alias, and an upstream `function_call` carrying that name is restored
|
|
301
|
+
// into a namespace call the caller never permitted.
|
|
302
|
+
if (!isPlainObject(tool)) continue;
|
|
303
|
+
if (tool.type !== "function" && tool.type !== "custom") continue;
|
|
304
|
+
if (typeof tool.name !== "string") continue;
|
|
305
|
+
if (hasMalformedNamespace(tool)) continue;
|
|
306
|
+
authorized.set(tool.name, tool.type);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// An explicit selector for another tool kind does not authorize a client namespace call.
|
|
310
|
+
return new Map();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// The kind must agree too. A wire name says WHICH tool, not what kind of call may
|
|
314
|
+
// carry it, so a `custom` selector naming a tool declared `function` is the same
|
|
315
|
+
// name/kind mismatch as a `file_search` selector naming it — narrower, but the
|
|
316
|
+
// same class, and `allowed_tools[].type` accepts any string so both are reachable.
|
|
317
|
+
return new Map(
|
|
318
|
+
[...aliases].filter(([wireName, identity]) => authorized.get(wireName) === identity.kind),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
242
322
|
function rewriteInputItem(item: unknown, plan: NamespaceRewritePlan, emitted: Set<string>): unknown {
|
|
243
323
|
if (!isPlainObject(item)) return item;
|
|
244
324
|
if (item.type === "additional_tools" && Array.isArray(item.tools)) {
|
|
@@ -292,7 +372,7 @@ export function rewriteRoutedNamespaceToolsForUpstream(body: unknown): {
|
|
|
292
372
|
...(input !== body.input ? { input } : {}),
|
|
293
373
|
...(toolChoice !== body.tool_choice ? { tool_choice: toolChoice } : {}),
|
|
294
374
|
},
|
|
295
|
-
aliases: plan.aliases,
|
|
375
|
+
aliases: authorizedAliases(plan.aliases, toolChoice),
|
|
296
376
|
};
|
|
297
377
|
}
|
|
298
378
|
|