@yansigit/opencodex 2.31.3 → 2.33.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 +2 -2
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-DKLr4LTE.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -5
- package/src/adapters/anthropic.ts +25 -13
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +5 -1
- package/src/adapters/command-code.ts +42 -10
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/protobuf-events.ts +158 -7
- package/src/adapters/cursor/protobuf-request.ts +33 -15
- package/src/adapters/cursor/request-builder.ts +4 -3
- package/src/adapters/cursor/tool-definitions.ts +27 -1
- package/src/adapters/cursor/types.ts +4 -3
- package/src/adapters/cursor.ts +9 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +154 -33
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +4 -1
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +133 -24
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +26 -13
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +20 -6
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +8 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +25 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -0
- package/src/codex/catalog/effort.ts +42 -12
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +593 -0
- package/src/codex/catalog/parsing.ts +71 -27
- package/src/codex/catalog/provider-fetch.ts +189 -33
- package/src/codex/catalog/sync.ts +6 -5
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-app-restart.ts +342 -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.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/shim.ts +56 -3
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +214 -0
- package/src/config.ts +153 -814
- package/src/generated/compatibility-version.json +232 -148
- package/src/images/loop.ts +37 -6
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +15 -3
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +59 -13
- package/src/providers/registry.ts +4 -2
- package/src/providers/request-pacing.ts +33 -6
- 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/custom-tool-compat.ts +132 -38
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +14 -2
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +8 -2
- package/src/server/auth-cors.ts +37 -7
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +102 -21
- 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 +206 -16
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +35 -6
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -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 +25 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/request-log.ts +21 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +36 -22
- package/src/server/responses/core.ts +584 -247
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +20 -102
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/service.ts +9 -5
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +34 -1
- package/src/types/tools.ts +114 -11
- package/src/types.ts +7 -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/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
package/src/images/loop.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuatio
|
|
|
18
18
|
import { namespacedToolName, toolChoiceToolPredicate } from "../types";
|
|
19
19
|
import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
|
|
20
20
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
21
|
-
import { bridgeToResponsesSSE } from "../bridge";
|
|
21
|
+
import { bridgeToResponsesSSE, diagnoseAdapterEvent, type BridgeDiagnosticContext } from "../bridge";
|
|
22
22
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
23
23
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
24
24
|
import { fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
|
|
@@ -35,6 +35,7 @@ import { submitVideoJob } from "./xai-video-client";
|
|
|
35
35
|
import { downloadVideoToArtifact, createImageBudget, pruneArtifacts } from "./artifacts";
|
|
36
36
|
import { IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "./synthetic-tool";
|
|
37
37
|
import type { ImageBridgePlan, VideoBridgePlan } from "./types";
|
|
38
|
+
import { OcxRequestValidationError } from "../lib/errors";
|
|
38
39
|
|
|
39
40
|
const SSE_HEADERS = {
|
|
40
41
|
"Content-Type": "text/event-stream",
|
|
@@ -209,8 +210,14 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
209
210
|
return parts;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
function jsonError(status: number, message: string): Response {
|
|
213
|
-
return new Response(JSON.stringify({
|
|
213
|
+
function jsonError(status: number, message: string, errorType = "upstream_error", code: string | null = null): Response {
|
|
214
|
+
return new Response(JSON.stringify({
|
|
215
|
+
error: {
|
|
216
|
+
message,
|
|
217
|
+
type: errorType,
|
|
218
|
+
code,
|
|
219
|
+
},
|
|
220
|
+
}), {
|
|
214
221
|
status,
|
|
215
222
|
headers: { "Content-Type": "application/json" },
|
|
216
223
|
});
|
|
@@ -219,7 +226,12 @@ function jsonError(status: number, message: string): Response {
|
|
|
219
226
|
/** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
|
|
220
227
|
* non-2xx jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
|
|
221
228
|
class LoopError extends Error {
|
|
222
|
-
constructor(
|
|
229
|
+
constructor(
|
|
230
|
+
readonly status: number,
|
|
231
|
+
message: string,
|
|
232
|
+
readonly errorType?: string,
|
|
233
|
+
readonly code?: string,
|
|
234
|
+
) {
|
|
223
235
|
super(message);
|
|
224
236
|
this.name = "LoopError";
|
|
225
237
|
}
|
|
@@ -245,6 +257,8 @@ export interface ImageBridgeDeps {
|
|
|
245
257
|
onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
|
|
246
258
|
/** Called after each upstream request is built (parity with web-search / normal path). */
|
|
247
259
|
onRequestBuilt?: (request: AdapterRequest) => void;
|
|
260
|
+
/** Validate the final adapter before every cached replay or request build. */
|
|
261
|
+
validateAdapter?: (parsed: OcxParsedRequest, adapter: ProviderAdapter) => void;
|
|
248
262
|
abortSignal?: AbortSignal;
|
|
249
263
|
onFirstOutput?: () => void;
|
|
250
264
|
/** Max image-generation rounds before forcing a final answer. Defaults to 3; clamped to [0, 10]. */
|
|
@@ -270,6 +284,8 @@ export interface ImageBridgeDeps {
|
|
|
270
284
|
onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
|
|
271
285
|
/** WebSocket Responses path only — leave response id empty for protocol compatibility. */
|
|
272
286
|
forceEmptyResponseId?: boolean;
|
|
287
|
+
/** Internal, opt-in structural stream diagnostics shared with the final bridge. */
|
|
288
|
+
diagnostic?: BridgeDiagnosticContext;
|
|
273
289
|
}
|
|
274
290
|
|
|
275
291
|
/**
|
|
@@ -487,6 +503,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
487
503
|
* header deadline. The caller owns same-target 429 replays and key rotation around it.
|
|
488
504
|
*/
|
|
489
505
|
const fetchOnce = async (requestAdapter: ProviderAdapter, recovery?: AttemptRecoveryKind): Promise<IterationResponse> => {
|
|
506
|
+
deps.validateAdapter?.(iterParsed, requestAdapter);
|
|
490
507
|
let request: AdapterRequest;
|
|
491
508
|
if (cachedRequest !== undefined && cachedAdapter === requestAdapter) {
|
|
492
509
|
request = cachedRequest;
|
|
@@ -609,6 +626,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
609
626
|
return prepared;
|
|
610
627
|
} catch (error) {
|
|
611
628
|
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
629
|
+
if (error instanceof OcxRequestValidationError) {
|
|
630
|
+
throw new LoopError(error.status, error.message, "invalid_request_error", "invalid_request_error");
|
|
631
|
+
}
|
|
612
632
|
if (headerDeadline.didExpire()) {
|
|
613
633
|
throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
|
|
614
634
|
}
|
|
@@ -638,6 +658,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
638
658
|
inactivityTimeoutMs: stallTimeoutMs,
|
|
639
659
|
translatorBudget,
|
|
640
660
|
})) {
|
|
661
|
+
if (deps.diagnostic) {
|
|
662
|
+
deps.diagnostic.adapterName = prepared.responseAdapter.name;
|
|
663
|
+
diagnoseAdapterEvent(deps.diagnostic, event);
|
|
664
|
+
}
|
|
641
665
|
if (event.type === "heartbeat") yield event;
|
|
642
666
|
else events.push(event);
|
|
643
667
|
}
|
|
@@ -674,7 +698,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
674
698
|
firstPrepared = await prepareIterationDrained(maxRounds <= 0);
|
|
675
699
|
} catch (e) {
|
|
676
700
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
677
|
-
if (e instanceof LoopError) return jsonError(e.status, e.message);
|
|
701
|
+
if (e instanceof LoopError) return jsonError(e.status, e.message, e.errorType, e.code ?? null);
|
|
678
702
|
throw e;
|
|
679
703
|
}
|
|
680
704
|
}
|
|
@@ -920,7 +944,13 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
920
944
|
yield {
|
|
921
945
|
type: "error",
|
|
922
946
|
message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
|
|
923
|
-
...(e instanceof LoopError ? {
|
|
947
|
+
...(e instanceof LoopError ? {
|
|
948
|
+
status: e.status,
|
|
949
|
+
...(e.errorType !== undefined || e.status === 400
|
|
950
|
+
? { errorType: e.errorType ?? "upstream_error" }
|
|
951
|
+
: {}),
|
|
952
|
+
...(e.code !== undefined ? { code: e.code } : {}),
|
|
953
|
+
} : {}),
|
|
924
954
|
...(hiddenUsage ? { usage: hiddenUsage } : {}),
|
|
925
955
|
};
|
|
926
956
|
}
|
|
@@ -949,6 +979,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
949
979
|
onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(usage),
|
|
950
980
|
} : {}),
|
|
951
981
|
...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
|
|
982
|
+
...(deps.diagnostic ? { diagnostic: deps.diagnostic } : {}),
|
|
952
983
|
},
|
|
953
984
|
);
|
|
954
985
|
return new Response(sse, { headers: SSE_HEADERS });
|
package/src/images/plan.ts
CHANGED
|
@@ -48,11 +48,12 @@ export async function planImageBridge(
|
|
|
48
48
|
if (config.images?.bridgeEnabled !== true) return undefined;
|
|
49
49
|
if (!parsed._imageGeneration) return undefined;
|
|
50
50
|
const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
|
|
51
|
-
const toolNames = new Set(
|
|
52
|
-
|
|
53
|
-
.filter(name => toolAllowed({ name })),
|
|
54
|
-
);
|
|
51
|
+
const toolNames = new Set([...parsed._imageGeneration.toolNames].filter(name => toolAllowed({ name })));
|
|
52
|
+
if (toolAllowed({ name: IMAGE_GEN_TOOL_NAME })) toolNames.add(IMAGE_GEN_TOOL_NAME);
|
|
55
53
|
if (toolNames.size === 0) return undefined;
|
|
54
|
+
// Responses advertises and rewrites authorized aliases to this synthetic name, so the loop
|
|
55
|
+
// must always intercept it once any image-generation name has armed the bridge.
|
|
56
|
+
toolNames.add(IMAGE_GEN_TOOL_NAME);
|
|
56
57
|
// Don't intercept for OpenAI native passthrough
|
|
57
58
|
const host = (() => { try { return new URL(routedProvider.baseUrl).hostname; } catch { return ""; } })();
|
|
58
59
|
if (host === "api.openai.com") return undefined;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-scoped exceptions to strict managed-fragment ownership.
|
|
3
|
+
*
|
|
4
|
+
* Most clients must preserve every byte represented by their managed
|
|
5
|
+
* contribution. A client that persists runtime-derived fields back into an
|
|
6
|
+
* OpenCodex-owned fragment needs a narrower contract: name the exact paths it
|
|
7
|
+
* may rewrite, then fingerprint everything else. Keeping that policy here
|
|
8
|
+
* prevents a client quirk from weakening the shared classifier.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
OPENCODE_PROVIDER_ID,
|
|
12
|
+
type ManagedContribution,
|
|
13
|
+
type ManagedFragment,
|
|
14
|
+
} from "../clients/config-export";
|
|
15
|
+
import { canonicalContribution, fingerprint } from "./ownership";
|
|
16
|
+
|
|
17
|
+
type JsonObject = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
function isObject(value: unknown): value is JsonObject {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pathStartsWith(path: readonly string[], prefix: readonly string[]): boolean {
|
|
24
|
+
return prefix.length <= path.length && prefix.every((part, index) => path[index] === part);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Delete one nested object member and prune only the empty ancestors created by that deletion. */
|
|
28
|
+
function deletePath(root: unknown, path: readonly string[]): void {
|
|
29
|
+
if (!isObject(root) || path.length === 0) return;
|
|
30
|
+
const parents: Array<{ parent: JsonObject; key: string }> = [];
|
|
31
|
+
let cursor: JsonObject = root;
|
|
32
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
33
|
+
const key = path[index]!;
|
|
34
|
+
const next = cursor[key];
|
|
35
|
+
if (!isObject(next)) return;
|
|
36
|
+
parents.push({ parent: cursor, key });
|
|
37
|
+
cursor = next;
|
|
38
|
+
}
|
|
39
|
+
delete cursor[path[path.length - 1]!];
|
|
40
|
+
for (let index = parents.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
if (Object.keys(cursor).length > 0) break;
|
|
42
|
+
const { parent, key } = parents[index]!;
|
|
43
|
+
delete parent[key];
|
|
44
|
+
cursor = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cloneFragment(fragment: ManagedFragment): ManagedFragment {
|
|
49
|
+
return {
|
|
50
|
+
path: [...fragment.path],
|
|
51
|
+
value: structuredClone(fragment.value),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Paths a client is documented to derive after OpenCodex writes its block.
|
|
57
|
+
*
|
|
58
|
+
* ZCode 3.8.1 persists reasoning and output defaults for every generated
|
|
59
|
+
* model. It may also fill a context default when OpenCodex intentionally
|
|
60
|
+
* omitted one; an authoritative context emitted by OpenCodex remains
|
|
61
|
+
* protected and is never listed here.
|
|
62
|
+
*/
|
|
63
|
+
export function refreshablePathsOf(
|
|
64
|
+
contribution: ManagedContribution,
|
|
65
|
+
): readonly (readonly string[])[] {
|
|
66
|
+
if (contribution.clientId !== "zcode") return [];
|
|
67
|
+
const fragment = contribution.fragments.find(candidate => (
|
|
68
|
+
candidate.path.length === 2
|
|
69
|
+
&& candidate.path[0] === "provider"
|
|
70
|
+
&& candidate.path[1] === OPENCODE_PROVIDER_ID
|
|
71
|
+
));
|
|
72
|
+
if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return [];
|
|
73
|
+
|
|
74
|
+
const paths: string[][] = [];
|
|
75
|
+
for (const modelId of Object.keys(fragment.value.models).sort()) {
|
|
76
|
+
const entry = fragment.value.models[modelId];
|
|
77
|
+
if (!isObject(entry)) continue;
|
|
78
|
+
const base = [...fragment.path, "models", modelId];
|
|
79
|
+
paths.push([...base, "reasoning"]);
|
|
80
|
+
paths.push([...base, "limit", "output"]);
|
|
81
|
+
const limit = entry.limit;
|
|
82
|
+
if (!isObject(limit) || typeof limit.context !== "number") {
|
|
83
|
+
paths.push([...base, "limit", "context"]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return paths;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validRefreshablePaths(
|
|
90
|
+
contribution: ManagedContribution,
|
|
91
|
+
value: unknown,
|
|
92
|
+
): value is readonly (readonly string[])[] {
|
|
93
|
+
if (contribution.clientId !== "zcode" || !Array.isArray(value) || value.length === 0) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const fragment = contribution.fragments.find(candidate => (
|
|
97
|
+
candidate.path.length === 2
|
|
98
|
+
&& candidate.path[0] === "provider"
|
|
99
|
+
&& candidate.path[1] === OPENCODE_PROVIDER_ID
|
|
100
|
+
));
|
|
101
|
+
if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return false;
|
|
102
|
+
|
|
103
|
+
const modelIds = new Set(Object.keys(fragment.value.models));
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
return value.every(path => {
|
|
106
|
+
if (!Array.isArray(path) || !path.every(part => typeof part === "string")) return false;
|
|
107
|
+
const modelId = path[3];
|
|
108
|
+
const isReasoning = path.length === 5 && path[4] === "reasoning";
|
|
109
|
+
const isLimitDefault = path.length === 6
|
|
110
|
+
&& path[4] === "limit"
|
|
111
|
+
&& (path[5] === "output" || path[5] === "context");
|
|
112
|
+
const key = path.join("\u0000");
|
|
113
|
+
if (
|
|
114
|
+
path[0] !== "provider"
|
|
115
|
+
|| path[1] !== OPENCODE_PROVIDER_ID
|
|
116
|
+
|| path[2] !== "models"
|
|
117
|
+
|| typeof modelId !== "string"
|
|
118
|
+
|| !modelIds.has(modelId)
|
|
119
|
+
|| (!isReasoning && !isLimitDefault)
|
|
120
|
+
|| seen.has(key)
|
|
121
|
+
) return false;
|
|
122
|
+
seen.add(key);
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Fingerprint a contribution after removing only its explicitly refreshable paths. */
|
|
128
|
+
export function protectedContributionFingerprint(
|
|
129
|
+
contribution: ManagedContribution,
|
|
130
|
+
refreshablePaths: readonly (readonly string[])[],
|
|
131
|
+
): string {
|
|
132
|
+
const fragments = contribution.fragments.map(cloneFragment);
|
|
133
|
+
for (const refreshablePath of refreshablePaths) {
|
|
134
|
+
for (const fragment of fragments) {
|
|
135
|
+
if (!pathStartsWith(refreshablePath, fragment.path)) continue;
|
|
136
|
+
deletePath(fragment.value, refreshablePath.slice(fragment.path.length));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return fingerprint(canonicalContribution({ ...contribution, fragments }));
|
|
141
|
+
}
|
|
@@ -41,6 +41,16 @@ export interface OwnershipRecord {
|
|
|
41
41
|
fileFingerprint: string;
|
|
42
42
|
/** Hash of our contribution — detects catalog/port drift. */
|
|
43
43
|
blockFingerprint: string;
|
|
44
|
+
/**
|
|
45
|
+
* Hash of the fields the client must not rewrite. Present only when a
|
|
46
|
+
* client has explicitly declared runtime-derived paths below.
|
|
47
|
+
*/
|
|
48
|
+
protectedBlockFingerprint?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Exact document paths a client may derive after apply. These are recorded
|
|
51
|
+
* per operation so later catalog changes cannot widen an older grant.
|
|
52
|
+
*/
|
|
53
|
+
refreshablePaths?: readonly (readonly string[])[];
|
|
44
54
|
/** The exact paths we own. Removal touches these and nothing else. */
|
|
45
55
|
fragmentPaths: readonly (readonly string[])[];
|
|
46
56
|
/**
|
|
@@ -13,6 +13,11 @@ import type { OcxConfig } from "../types";
|
|
|
13
13
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
14
14
|
import { SNAPSHOT_RETENTION } from "./journal";
|
|
15
15
|
import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
|
|
16
|
+
import {
|
|
17
|
+
protectedContributionFingerprint,
|
|
18
|
+
refreshablePathsOf,
|
|
19
|
+
validRefreshablePaths,
|
|
20
|
+
} from "./ownership-policy";
|
|
16
21
|
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
17
22
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
18
23
|
|
|
@@ -106,10 +111,10 @@ export function blockedContainerPath(
|
|
|
106
111
|
* values lets another integration or a user add a sibling without blocking a
|
|
107
112
|
* later refresh, while a change inside our block still fails closed.
|
|
108
113
|
*/
|
|
109
|
-
function
|
|
114
|
+
function recordedContribution(
|
|
110
115
|
doc: unknown,
|
|
111
116
|
record: OwnershipRecord,
|
|
112
|
-
):
|
|
117
|
+
): ManagedContribution | null {
|
|
113
118
|
if (
|
|
114
119
|
!Array.isArray(record.fragmentPaths)
|
|
115
120
|
|| record.fragmentPaths.length === 0
|
|
@@ -125,10 +130,44 @@ function recordedFragmentFingerprint(
|
|
|
125
130
|
if (value === undefined) return null;
|
|
126
131
|
fragments.push({ path, value });
|
|
127
132
|
}
|
|
128
|
-
return
|
|
133
|
+
return {
|
|
129
134
|
clientId: record.clientId,
|
|
130
135
|
fragments,
|
|
131
|
-
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Prove that every protected field still matches what OpenCodex wrote.
|
|
141
|
+
*
|
|
142
|
+
* New records carry an operation-scoped protected fingerprint and the exact
|
|
143
|
+
* paths excluded from it. Legacy records can recover only when the desired
|
|
144
|
+
* contribution has not moved since apply; otherwise catalog drift and a
|
|
145
|
+
* foreign edit are indistinguishable, so the classifier keeps failing closed.
|
|
146
|
+
*/
|
|
147
|
+
function recordedBlockIsOwned(
|
|
148
|
+
doc: unknown,
|
|
149
|
+
record: OwnershipRecord,
|
|
150
|
+
desired: ManagedContribution,
|
|
151
|
+
): boolean {
|
|
152
|
+
const observed = recordedContribution(doc, record);
|
|
153
|
+
if (!observed) return false;
|
|
154
|
+
if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
typeof record.protectedBlockFingerprint === "string"
|
|
158
|
+
&& validRefreshablePaths(observed, record.refreshablePaths)
|
|
159
|
+
&& record.refreshablePaths.length > 0
|
|
160
|
+
) {
|
|
161
|
+
return protectedContributionFingerprint(observed, record.refreshablePaths)
|
|
162
|
+
=== record.protectedBlockFingerprint;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
166
|
+
if (desiredFingerprint !== record.blockFingerprint) return false;
|
|
167
|
+
const legacyPaths = refreshablePathsOf(desired);
|
|
168
|
+
return legacyPaths.length > 0
|
|
169
|
+
&& protectedContributionFingerprint(observed, legacyPaths)
|
|
170
|
+
=== protectedContributionFingerprint(desired, legacyPaths);
|
|
132
171
|
}
|
|
133
172
|
|
|
134
173
|
/**
|
|
@@ -191,7 +230,7 @@ export function classifyIntegration(input: {
|
|
|
191
230
|
* conflict no matter what the rest of the file looks like, so the sibling-
|
|
192
231
|
* edit exemption below can never mask it.
|
|
193
232
|
*/
|
|
194
|
-
if (
|
|
233
|
+
if (!recordedBlockIsOwned(input.parsed, input.record, input.contribution)) {
|
|
195
234
|
return { state: "conflict", reason: "foreign-edit" };
|
|
196
235
|
}
|
|
197
236
|
if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
|
|
@@ -16,6 +16,7 @@ import { isLoopbackHostname } from "../codex/inject";
|
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
18
18
|
import { fingerprint, canonicalContribution, fragmentPathsOf, type OwnershipRecord } from "./ownership";
|
|
19
|
+
import { protectedContributionFingerprint, refreshablePathsOf } from "./ownership-policy";
|
|
19
20
|
import { createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
20
21
|
import { INTEGRATION_CLIENTS, isLoopbackOnly, type IntegrationClientId } from "./registry";
|
|
21
22
|
import { classifyIntegration, exportContextOf } from "./state";
|
|
@@ -353,12 +354,17 @@ function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: bo
|
|
|
353
354
|
opId, clientId, kind: classified.state === "stale" ? "refresh" : "apply", at, configPath,
|
|
354
355
|
snapshot, resultFingerprint: fingerprint(text), resultAbsent: false, priorRecord: record,
|
|
355
356
|
};
|
|
357
|
+
const refreshablePaths = refreshablePathsOf(contribution);
|
|
356
358
|
return commit({
|
|
357
359
|
io, store, clientId, configPath, before, nextText: text, state: "current",
|
|
358
360
|
priorRecord: record,
|
|
359
361
|
record: {
|
|
360
362
|
clientId, configPath, fileFingerprint: fingerprint(text),
|
|
361
363
|
blockFingerprint: fingerprint(canonicalContribution(contribution)),
|
|
364
|
+
...(refreshablePaths.length > 0 ? {
|
|
365
|
+
protectedBlockFingerprint: protectedContributionFingerprint(contribution, refreshablePaths),
|
|
366
|
+
refreshablePaths,
|
|
367
|
+
} : {}),
|
|
362
368
|
fragmentPaths: fragmentPathsOf(contribution), createdContainers: created,
|
|
363
369
|
appliedAt: at, opId,
|
|
364
370
|
},
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const AZURE_IDENTITY_SCOPE = "https://cognitiveservices.azure.com/.default";
|
|
4
|
+
export const AZURE_IDENTITY_UNAVAILABLE_ERROR = "Azure identity credential unavailable";
|
|
5
|
+
|
|
6
|
+
export interface AzureTokenCredential {
|
|
7
|
+
getToken(scope: string): Promise<{ token?: string } | null>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type AzureCredentialFactory = (options?: { managedIdentityClientId?: string }) =>
|
|
11
|
+
AzureTokenCredential | Promise<AzureTokenCredential>;
|
|
12
|
+
export type AzureIdentityModuleLoader = () => Promise<{
|
|
13
|
+
DefaultAzureCredential: new (options?: { managedIdentityClientId?: string }) => AzureTokenCredential;
|
|
14
|
+
}>;
|
|
15
|
+
export type AzureLoggerModuleLoader = () => Promise<{
|
|
16
|
+
AzureLogger: { log: (...args: unknown[]) => void };
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
const credentials = new Map<string, AzureTokenCredential>();
|
|
20
|
+
const inflight = new Map<string, Promise<AzureTokenCredential>>();
|
|
21
|
+
const invalidatedInflight = new Set<Promise<AzureTokenCredential>>();
|
|
22
|
+
let testFactory: AzureCredentialFactory | undefined;
|
|
23
|
+
let testModuleLoader: AzureIdentityModuleLoader | undefined;
|
|
24
|
+
let testLoggerModuleLoader: AzureLoggerModuleLoader | undefined;
|
|
25
|
+
|
|
26
|
+
async function loadAzureLoggerModule(): Promise<{
|
|
27
|
+
AzureLogger: { log: (...args: unknown[]) => void };
|
|
28
|
+
}> {
|
|
29
|
+
const packageName: string = "@azure/logger";
|
|
30
|
+
return await import(packageName) as unknown as {
|
|
31
|
+
AzureLogger: { log: (...args: unknown[]) => void };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function suppressAzureSdkLogging(): Promise<void> {
|
|
36
|
+
const module = await (testLoggerModuleLoader ?? loadAzureLoggerModule)();
|
|
37
|
+
module.AzureLogger.log = () => {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function loadAzureIdentityModule(): Promise<{
|
|
41
|
+
DefaultAzureCredential: new (options?: { managedIdentityClientId?: string }) => AzureTokenCredential;
|
|
42
|
+
}> {
|
|
43
|
+
// Keep the package out of the static module graph until Task 3 installs it.
|
|
44
|
+
const packageName: string = "@azure/identity";
|
|
45
|
+
return await import(packageName) as unknown as {
|
|
46
|
+
DefaultAzureCredential: new (options?: { managedIdentityClientId?: string }) => AzureTokenCredential;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadDefaultAzureCredential(options?: { managedIdentityClientId?: string }): Promise<AzureTokenCredential> {
|
|
51
|
+
await suppressAzureSdkLogging();
|
|
52
|
+
const module = await (testModuleLoader ?? loadAzureIdentityModule)();
|
|
53
|
+
return options === undefined
|
|
54
|
+
? new module.DefaultAzureCredential()
|
|
55
|
+
: new module.DefaultAzureCredential(options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clientIdFor(provider: OcxProviderConfig): string {
|
|
59
|
+
return provider.azureCredential?.managedIdentityClientId?.trim() ?? "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function credentialFor(clientId: string): Promise<AzureTokenCredential> {
|
|
63
|
+
const cached = credentials.get(clientId);
|
|
64
|
+
if (cached) return cached;
|
|
65
|
+
const running = inflight.get(clientId);
|
|
66
|
+
if (running) return running;
|
|
67
|
+
const generation = cacheGeneration;
|
|
68
|
+
let promise!: Promise<AzureTokenCredential>;
|
|
69
|
+
promise = Promise.resolve((testFactory ?? loadDefaultAzureCredential)(clientId ? { managedIdentityClientId: clientId } : undefined))
|
|
70
|
+
.then(credential => {
|
|
71
|
+
if (cacheGeneration === generation && !invalidatedInflight.has(promise)) {
|
|
72
|
+
credentials.set(clientId, credential);
|
|
73
|
+
}
|
|
74
|
+
return credential;
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
invalidatedInflight.delete(promise);
|
|
78
|
+
if (inflight.get(clientId) === promise) inflight.delete(clientId);
|
|
79
|
+
});
|
|
80
|
+
inflight.set(clientId, promise);
|
|
81
|
+
return promise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function azureIdentityClientId(provider: Pick<OcxProviderConfig, "azureCredential">): string | undefined {
|
|
85
|
+
if (provider.azureCredential?.type !== "default-azure-credential") return undefined;
|
|
86
|
+
return provider.azureCredential.managedIdentityClientId?.trim() ?? "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function invalidateAzureCredentialCache(clientId: string): void {
|
|
90
|
+
const key = clientId.trim();
|
|
91
|
+
const running = inflight.get(key);
|
|
92
|
+
if (running) invalidatedInflight.add(running);
|
|
93
|
+
credentials.delete(key);
|
|
94
|
+
inflight.delete(key);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function __azureCredentialCacheStateForTests(): {
|
|
98
|
+
credentials: number;
|
|
99
|
+
inflight: number;
|
|
100
|
+
invalidatedInflight: number;
|
|
101
|
+
} {
|
|
102
|
+
return {
|
|
103
|
+
credentials: credentials.size,
|
|
104
|
+
inflight: inflight.size,
|
|
105
|
+
invalidatedInflight: invalidatedInflight.size,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getAzureAccessToken(provider: OcxProviderConfig): Promise<string> {
|
|
110
|
+
try {
|
|
111
|
+
const credential = await credentialFor(clientIdFor(provider));
|
|
112
|
+
const result = await credential.getToken(AZURE_IDENTITY_SCOPE);
|
|
113
|
+
const token = result?.token?.trim();
|
|
114
|
+
if (!token) throw new Error(AZURE_IDENTITY_UNAVAILABLE_ERROR);
|
|
115
|
+
return token;
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error(AZURE_IDENTITY_UNAVAILABLE_ERROR);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function setAzureCredentialFactoryForTests(factory: AzureCredentialFactory): void {
|
|
122
|
+
cacheGeneration++;
|
|
123
|
+
testFactory = factory;
|
|
124
|
+
testModuleLoader = undefined;
|
|
125
|
+
testLoggerModuleLoader = undefined;
|
|
126
|
+
credentials.clear();
|
|
127
|
+
inflight.clear();
|
|
128
|
+
invalidatedInflight.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function __resetAzureCredentialCache(): void {
|
|
132
|
+
cacheGeneration++;
|
|
133
|
+
testFactory = undefined;
|
|
134
|
+
testModuleLoader = undefined;
|
|
135
|
+
testLoggerModuleLoader = undefined;
|
|
136
|
+
credentials.clear();
|
|
137
|
+
inflight.clear();
|
|
138
|
+
invalidatedInflight.clear();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let cacheGeneration = 0;
|
|
142
|
+
|
|
143
|
+
export function setAzureIdentityModuleLoaderForTests(loader: AzureIdentityModuleLoader): void {
|
|
144
|
+
cacheGeneration++;
|
|
145
|
+
testFactory = undefined;
|
|
146
|
+
testModuleLoader = loader;
|
|
147
|
+
credentials.clear();
|
|
148
|
+
inflight.clear();
|
|
149
|
+
invalidatedInflight.clear();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function setAzureLoggerModuleLoaderForTests(loader: AzureLoggerModuleLoader): void {
|
|
153
|
+
testLoggerModuleLoader = loader;
|
|
154
|
+
}
|
package/src/lib/bounded-body.ts
CHANGED
|
@@ -103,6 +103,16 @@ function cancelWithoutWaiting(reader: ReadableStreamDefaultReader<Uint8Array>, r
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function cancelBodyWithoutWaiting(body: ReadableStream<Uint8Array>, reason?: unknown): void {
|
|
107
|
+
// A signal can already be aborted before a reader is attached. Still settle the
|
|
108
|
+
// original body so fetch-backed streams cannot retain a rejected read in that gap.
|
|
109
|
+
try {
|
|
110
|
+
void body.cancel(reason).catch(() => undefined);
|
|
111
|
+
} catch {
|
|
112
|
+
// A locked or non-conforming stream may throw synchronously from cancel().
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
/**
|
|
107
117
|
* Consume the original response body as raw bytes under a strict memory ceiling.
|
|
108
118
|
*
|
|
@@ -208,9 +218,11 @@ export async function readBoundedResponseBody(
|
|
|
208
218
|
options: BoundedBodyOptions = {},
|
|
209
219
|
): Promise<BoundedBodyResult> {
|
|
210
220
|
const signal = options.signal;
|
|
211
|
-
if (signal?.aborted) throw signal.reason;
|
|
212
|
-
|
|
213
221
|
const body = response.body;
|
|
222
|
+
if (signal?.aborted) {
|
|
223
|
+
if (body) cancelBodyWithoutWaiting(body, signal.reason);
|
|
224
|
+
throw signal.reason;
|
|
225
|
+
}
|
|
214
226
|
if (!body) {
|
|
215
227
|
return {
|
|
216
228
|
text: "",
|
package/src/lib/debug.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
1
2
|
import { appendDebugLogLine } from "./debug-log-buffer";
|
|
2
3
|
import { isDebugEnabled } from "./debug-settings";
|
|
3
4
|
import { redactSecrets } from "./redact";
|
|
4
5
|
|
|
6
|
+
let debugFingerprintKey: Uint8Array | undefined;
|
|
7
|
+
|
|
5
8
|
function emitDebugLine(line: string): void {
|
|
6
9
|
if (!isDebugEnabled()) return;
|
|
7
10
|
try {
|
|
@@ -29,3 +32,42 @@ export function debugProviderDiagnostic(adapter: string, event: string, details:
|
|
|
29
32
|
/* diagnostics must never affect request handling */
|
|
30
33
|
}
|
|
31
34
|
}
|
|
35
|
+
|
|
36
|
+
/** Process-local, content-free correlation aid for opt-in provider diagnostics. */
|
|
37
|
+
export function debugFingerprint(value: string | Uint8Array): string | undefined {
|
|
38
|
+
if (!isDebugEnabled()) return undefined;
|
|
39
|
+
try {
|
|
40
|
+
debugFingerprintKey ??= randomBytes(32);
|
|
41
|
+
return createHmac("sha256", debugFingerprintKey).update(value).digest("hex");
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DebugStreamDiagnosticContext {
|
|
48
|
+
requestId: string;
|
|
49
|
+
adapterName: string;
|
|
50
|
+
attempt?: number;
|
|
51
|
+
recovery?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type DebugStreamDiagnosticStage = "adapter" | "bridge";
|
|
55
|
+
|
|
56
|
+
/** Emit one structural line for an adapter/bridge event without retaining its content. */
|
|
57
|
+
export function debugStreamDiagnostic(
|
|
58
|
+
context: DebugStreamDiagnosticContext,
|
|
59
|
+
stage: DebugStreamDiagnosticStage,
|
|
60
|
+
sequence: number,
|
|
61
|
+
eventType: string,
|
|
62
|
+
details?: Record<string, unknown>,
|
|
63
|
+
): void {
|
|
64
|
+
debugProviderDiagnostic(context.adapterName, "stream", {
|
|
65
|
+
stage,
|
|
66
|
+
sequence,
|
|
67
|
+
eventType,
|
|
68
|
+
...(context.requestId !== undefined ? { requestId: context.requestId } : {}),
|
|
69
|
+
...(context.attempt !== undefined ? { attempt: context.attempt } : {}),
|
|
70
|
+
...(context.recovery !== undefined ? { recovery: context.recovery } : {}),
|
|
71
|
+
...details,
|
|
72
|
+
});
|
|
73
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -4,6 +4,16 @@ export interface OcxErrorPayload {
|
|
|
4
4
|
code: string | null;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/** A local request validation failure that must remain a client-visible HTTP 400. */
|
|
8
|
+
export class OcxRequestValidationError extends Error {
|
|
9
|
+
readonly status = 400;
|
|
10
|
+
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "OcxRequestValidationError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
/** OpenAI / Codex hard block for high-risk cybersecurity activity (HTTP 400 or mid-stream). */
|
|
8
18
|
export const CYBER_POLICY_ERROR_CODE = "cyber_policy";
|
|
9
19
|
|
|
@@ -180,6 +190,10 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
180
190
|
}
|
|
181
191
|
if (
|
|
182
192
|
text.includes("insufficient_quota") ||
|
|
193
|
+
text.includes("insufficient credits") ||
|
|
194
|
+
text.includes("purchase more credits") ||
|
|
195
|
+
text.includes("run out of credits") ||
|
|
196
|
+
text.includes("out of credits") ||
|
|
183
197
|
text.includes("exceeded your current quota") ||
|
|
184
198
|
text.includes("quota exhausted") ||
|
|
185
199
|
text.includes("account quota exceeded") ||
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { loadConfig
|
|
2
|
+
import { loadConfig } from "../config";
|
|
3
|
+
import { readRuntimePort } from "../config/process-state";
|
|
3
4
|
import { configuredAdminToken } from "./admin-secrets";
|
|
4
5
|
|
|
5
6
|
export function isProcessAlive(pid: number): boolean {
|