@bitkyc08/opencodex 2.56.0 → 2.58.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Retry guard for upstream fetches that die on stale pooled keep-alive sockets.
|
|
3
3
|
*
|
|
4
4
|
* chatgpt.com (Cloudflare) closes idle keep-alive connections server-side; Bun's fetch pool
|
|
5
|
-
* reuses the half-closed socket and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* a
|
|
5
|
+
* reuses the half-closed socket and a request can fail before response headers arrive.
|
|
6
|
+
* A pre-header rejection does not prove that the origin did not process the request.
|
|
7
|
+
* Mechanically reusable bytes do not make a model POST idempotent: an ambiguous reset
|
|
8
|
+
* becomes a terminal, non-replayable response unless the operation is explicitly safe.
|
|
9
9
|
*
|
|
10
10
|
* Deliberately narrow: timeouts, aborts, ECONNREFUSED/DNS/TLS failures, and HTTP error
|
|
11
11
|
* statuses (returned as Response, never thrown) are NOT retried. Mid-stream SSE resets are
|
|
@@ -36,19 +36,66 @@ export function isNonReplayableResponse(response: Response): boolean {
|
|
|
36
36
|
return nonReplayableResponses.has(response);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The narrower marker: responses this proxy synthesized as a replay refusal.
|
|
41
|
+
*
|
|
42
|
+
* {@link isNonReplayableResponse} answers "must not be sent again", which the WebSocket
|
|
43
|
+
* post-send verdicts share. This one answers "the upstream never said this", and that is the
|
|
44
|
+
* question a quota recorder or a `Retry-After` synthesizer has to ask. Both were written for
|
|
45
|
+
* a status that only ever arrived from a provider, so a synthetic 429 reads to them as a
|
|
46
|
+
* credential that rate-limited us and as a wait worth honouring -- one writes a cooldown
|
|
47
|
+
* against a credential that refused nothing, the other instructs the client to send the turn
|
|
48
|
+
* again. A marker rather than a body check, because it has to be answerable before the body
|
|
49
|
+
* is read and cannot be spoofed by an upstream that happens to echo the code.
|
|
50
|
+
*/
|
|
51
|
+
const replayRefusalResponses = new WeakSet<Response>();
|
|
52
|
+
|
|
53
|
+
export function markReplayRefusalResponse(response: Response): void {
|
|
54
|
+
replayRefusalResponses.add(response);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isReplayRefusalResponse(response: Response): boolean {
|
|
58
|
+
return replayRefusalResponses.has(response);
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
/** Origin never produced a response event; the turn may still be executing. */
|
|
40
62
|
export const UPSTREAM_NO_RESPONSE_CODE = "upstream_no_response";
|
|
41
63
|
/** Transport closed after the send, before any response event. */
|
|
42
64
|
export const UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE = "upstream_closed_before_response";
|
|
65
|
+
/**
|
|
66
|
+
* This proxy refused to replay a pre-header fetch rejection.
|
|
67
|
+
*
|
|
68
|
+
* Distinct from {@link UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE}, which the Codex WebSocket
|
|
69
|
+
* transport settles as a 502 after the create frame was already sent. Both are ambiguous,
|
|
70
|
+
* but only this one is a refusal this process made before any response existed, so it
|
|
71
|
+
* follows the send-budget precedent and answers 429: the Codex client is configured with
|
|
72
|
+
* `retry_429: false` and `retry_5xx: true` over four attempts, so a 5xx here multiplies
|
|
73
|
+
* the duplicate send the refusal exists to prevent. See
|
|
74
|
+
* structure/transports/responses.md#ambiguous-connection-reset-replay-boundary.
|
|
75
|
+
*/
|
|
76
|
+
export const UPSTREAM_RESET_REPLAY_REFUSED_CODE = "upstream_reset_replay_refused";
|
|
43
77
|
const NON_REPLAYABLE_UPSTREAM_CODES: ReadonlySet<string> = new Set([
|
|
44
78
|
UPSTREAM_NO_RESPONSE_CODE,
|
|
45
79
|
UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
|
|
80
|
+
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
46
81
|
]);
|
|
47
82
|
|
|
48
83
|
export function isNonReplayableUpstreamCode(code: unknown): boolean {
|
|
49
84
|
return typeof code === "string" && NON_REPLAYABLE_UPSTREAM_CODES.has(code);
|
|
50
85
|
}
|
|
51
86
|
|
|
87
|
+
/**
|
|
88
|
+
* True for the one non-replayable code this proxy owns end to end. The status it carries is
|
|
89
|
+
* a local decision, so a re-wrapping formatter must restate it rather than inherit the
|
|
90
|
+
* caller's upstream-shaped status.
|
|
91
|
+
*/
|
|
92
|
+
export function isReplayRefusalCode(code: unknown): boolean {
|
|
93
|
+
return code === UPSTREAM_RESET_REPLAY_REFUSED_CODE;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Client-facing status for {@link UPSTREAM_RESET_REPLAY_REFUSED_CODE}. */
|
|
97
|
+
export const REPLAY_REFUSED_STATUS = 429;
|
|
98
|
+
|
|
52
99
|
// 1 initial + 2 retries: the pool may hold more than one stale socket.
|
|
53
100
|
const RESET_RETRY_MAX_ATTEMPTS = 3;
|
|
54
101
|
const RESET_RETRY_BASE_DELAY_MS = 150;
|
|
@@ -352,6 +399,12 @@ export async function fetchWithAttemptDeadline(
|
|
|
352
399
|
}
|
|
353
400
|
|
|
354
401
|
export interface ResetRetryOptions {
|
|
402
|
+
/**
|
|
403
|
+
* Opt in only when repeating this operation cannot duplicate upstream effects.
|
|
404
|
+
* This permits reset retries, not extra sends: attempts and onSendsConsumed still
|
|
405
|
+
* bound and count every physical send. A string body is not replay-safety proof.
|
|
406
|
+
*/
|
|
407
|
+
replaySafe?: boolean;
|
|
355
408
|
abortSignal?: AbortSignal;
|
|
356
409
|
/** Short host/path label for the retry warn log (no secrets/query strings). */
|
|
357
410
|
label?: string;
|
|
@@ -442,9 +495,9 @@ export function applyUpstreamRecoveryInit<T extends RequestInit>(
|
|
|
442
495
|
}
|
|
443
496
|
|
|
444
497
|
/**
|
|
445
|
-
* Run `doFetch
|
|
446
|
-
*
|
|
447
|
-
*
|
|
498
|
+
* Run `doFetch` within one send budget. Connection-reset-shaped rejections are
|
|
499
|
+
* terminal by default; only an explicitly replay-safe operation receives reset retries
|
|
500
|
+
* with jittered backoff. HTTP responses retain the caller's existing retry policy.
|
|
448
501
|
*/
|
|
449
502
|
export async function fetchWithResetRetry(
|
|
450
503
|
doFetch: ReplayableFetch,
|
|
@@ -475,6 +528,20 @@ export async function fetchWithResetRetry(
|
|
|
475
528
|
if (sawReset) throw new UpstreamRetryEvidenceError([], err, true);
|
|
476
529
|
throw err;
|
|
477
530
|
}
|
|
531
|
+
if (opts.replaySafe !== true) {
|
|
532
|
+
// Return evidence instead of throwing a generic transport error: outer catches
|
|
533
|
+
// otherwise turn it into a replayable 502 and a combo/account recovery resends it.
|
|
534
|
+
// The WeakSet protects in-process recovery; the code survives JSON re-wrapping.
|
|
535
|
+
// Never expose the raw exception, which can contain credentials or request data.
|
|
536
|
+
const response = new Response(JSON.stringify({ error: {
|
|
537
|
+
type: "upstream_error",
|
|
538
|
+
code: UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
539
|
+
message: "The upstream connection closed before a response was received. The request may already have been processed; automatic replay was stopped.",
|
|
540
|
+
} }), { status: REPLAY_REFUSED_STATUS, headers: { "content-type": "application/json" } });
|
|
541
|
+
markResponseNonReplayable(response);
|
|
542
|
+
markReplayRefusalResponse(response);
|
|
543
|
+
return response;
|
|
544
|
+
}
|
|
478
545
|
if (attempt === attempts - 1) throw err;
|
|
479
546
|
sawReset = true;
|
|
480
547
|
lastError = err;
|
|
@@ -491,9 +558,9 @@ export async function fetchWithResetRetry(
|
|
|
491
558
|
}
|
|
492
559
|
|
|
493
560
|
/**
|
|
494
|
-
* fetchWithResetRetry plus
|
|
495
|
-
*
|
|
496
|
-
*
|
|
561
|
+
* fetchWithResetRetry plus the caller-selected transient-5xx policy, PRE-STREAM only.
|
|
562
|
+
* A received HTTP error follows that policy; an ambiguous reset's non-replayable
|
|
563
|
+
* verdict always stops it. The failed attempt's body is cancelled before the
|
|
497
564
|
* retry; every returned response (ok, non-transient, aborted, slow, exhausted) keeps
|
|
498
565
|
* its body intact. Honors Retry-After via retryBackoffDelayMs.
|
|
499
566
|
*
|
|
@@ -250,6 +250,21 @@ export const OCX_ELEVATED_PROTOCOL_FAILED = 13;
|
|
|
250
250
|
/** Windows ERROR_CANCELLED — reserved for UAC denial; never emitted by the elevated script. */
|
|
251
251
|
export const OCX_ELEVATED_UAC_CANCELLED = 1223;
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* The elevated process could not read a staged payload (#4692).
|
|
255
|
+
*
|
|
256
|
+
* `hardenSecretPath` grants the staging account and strips inheritance, so a split-token
|
|
257
|
+
* elevation of the same user reads the file and an elevation answered with a DIFFERENT
|
|
258
|
+
* administrator's credentials does not. The elevated side cannot explain that itself: it
|
|
259
|
+
* runs hidden, so its stderr goes nowhere and only the exit code survives the boundary.
|
|
260
|
+
* Without a code of its own the operator would be told "exit code 1" for a cause that
|
|
261
|
+
* names its own remedy — the same undiagnosable failure this change set exists to remove.
|
|
262
|
+
*
|
|
263
|
+
* Deliberately outside OCX_ELEVATED_PROTOCOL_CODES: that list is the create-and-run
|
|
264
|
+
* transaction's alphabet, and this code belongs to the registration path.
|
|
265
|
+
*/
|
|
266
|
+
export const OCX_ELEVATED_STAGING_UNREADABLE = 14;
|
|
267
|
+
|
|
253
268
|
export const OCX_ELEVATED_PROTOCOL_CODES = [
|
|
254
269
|
OCX_ELEVATED_SUCCESS,
|
|
255
270
|
OCX_ELEVATED_CREATE_FAILED,
|
|
@@ -645,36 +660,83 @@ export function runWindowsElevated(file: string, args: string[]): Promise<number
|
|
|
645
660
|
}
|
|
646
661
|
|
|
647
662
|
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
663
|
+
* A task definition staged for the elevated process.
|
|
664
|
+
*
|
|
665
|
+
* The bytes live in a freshly created, ACL-hardened private directory, and the digest is
|
|
666
|
+
* taken over exactly those bytes by the caller that validated them. The elevated script
|
|
667
|
+
* reads the file once, hashes what it read, and refuses unless the digest matches, so a
|
|
668
|
+
* pathname is no longer a promise about content — it is a claim the receiver checks.
|
|
669
|
+
*/
|
|
670
|
+
export interface StagedWindowsTaskXml {
|
|
671
|
+
/** Path inside the caller's hardened staging directory. */
|
|
672
|
+
readonly path: string;
|
|
673
|
+
/** Lowercase hex SHA-256 of the staged bytes (UTF-16LE, no BOM). */
|
|
674
|
+
readonly sha256: string;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Read a staged payload, prove it is the one that was validated, and decode it.
|
|
679
|
+
*
|
|
680
|
+
* One read: the bytes that are hashed are the same array that is decoded and registered.
|
|
681
|
+
* Hashing a path and then reopening it would reintroduce the swap window this check
|
|
682
|
+
* exists to close.
|
|
683
|
+
*/
|
|
684
|
+
const READ_STAGED_TASK_XML = "function Read-OcxStagedTaskXml([string]$path, [string]$expectedHash) {"
|
|
685
|
+
// An unreadable payload is a diagnosable condition, not a generic throw: a hidden
|
|
686
|
+
// elevated process has nowhere to print, so the cause has to ride the exit code.
|
|
687
|
+
+ " try { $bytes = [IO.File]::ReadAllBytes($path) }"
|
|
688
|
+
+ " catch [System.UnauthorizedAccessException] { exit " + OCX_ELEVATED_STAGING_UNREADABLE + " }"
|
|
689
|
+
+ " catch [System.Security.SecurityException] { exit " + OCX_ELEVATED_STAGING_UNREADABLE + " };"
|
|
690
|
+
+ " $sha = [Security.Cryptography.SHA256]::Create();"
|
|
691
|
+
+ " try { $actual = [BitConverter]::ToString($sha.ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() };"
|
|
692
|
+
+ " if ($actual -cne $expectedHash) { throw 'Task Scheduler staged payload failed its integrity check.' };"
|
|
693
|
+
+ " return [Text.Encoding]::Unicode.GetString($bytes) }";
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Register one scheduled-task definition from staged, digest-verified bytes.
|
|
697
|
+
*
|
|
698
|
+
* The payloads used to be embedded as base64(utf16le) inside an inner PowerShell script
|
|
699
|
+
* that was itself base64(utf16le)-encoded into `-EncodedCommand`. Two layers of base64
|
|
700
|
+
* over UTF-16 cost about 14.2 command-line characters per XML character, and a
|
|
701
|
+
* replacement carries two payloads, so a ~2 KB task definition pushed the outer command
|
|
702
|
+
* past the Windows limit and the spawn failed with ENAMETOOLONG before UAC ever
|
|
703
|
+
* appeared (#4692). On a host where the trigger scope exports as an account name the
|
|
704
|
+
* re-register path runs on every repair, so repair could never succeed.
|
|
705
|
+
*
|
|
706
|
+
* The command now carries two paths and two 64-character digests, so its length no
|
|
707
|
+
* longer depends on the size of the XML at all.
|
|
708
|
+
*
|
|
709
|
+
* The original design goal was "immutable bytes, never a caller-writable pathname".
|
|
710
|
+
* That goal is kept by different means rather than abandoned: the staging directory is
|
|
711
|
+
* private and ACL-hardened, the files are created exclusively so nothing can be waiting
|
|
712
|
+
* at the path, and the digest makes a same-account swap during the UAC prompt fail
|
|
713
|
+
* closed instead of registering something else. An ACL alone could not do that last
|
|
714
|
+
* part, because a process running as the same user has the same SID.
|
|
715
|
+
*
|
|
716
|
+
* The replacement precondition is unchanged: the elevated process still re-queries the
|
|
717
|
+
* live registration and compares it to the captured predecessor before passing -Force.
|
|
651
718
|
*/
|
|
652
719
|
export function runWindowsElevatedScheduledTaskRegistration(
|
|
653
720
|
taskName: string,
|
|
654
|
-
xml:
|
|
721
|
+
xml: StagedWindowsTaskXml,
|
|
655
722
|
replace = false,
|
|
656
|
-
|
|
723
|
+
expectedExisting?: StagedWindowsTaskXml,
|
|
657
724
|
): Promise<number> {
|
|
658
|
-
if (replace && !
|
|
725
|
+
if (replace && !expectedExisting) {
|
|
659
726
|
throw new Error("Elevated Task Scheduler replacement requires a captured existing definition.");
|
|
660
727
|
}
|
|
661
|
-
const xmlBase64 = Buffer.from(xml, "utf16le").toString("base64");
|
|
662
|
-
const expectedExistingBase64 = expectedExistingXml === undefined
|
|
663
|
-
? null
|
|
664
|
-
: Buffer.from(expectedExistingXml, "utf16le").toString("base64");
|
|
665
728
|
const powerShellPath = windowsPowerShell();
|
|
666
729
|
const powerShellDirectory = powerShellPath.replace(/[\\/][^\\/]+$/, "");
|
|
667
730
|
const scheduledTasksModule = `${powerShellDirectory}\\Modules\\ScheduledTasks\\ScheduledTasks.psd1`;
|
|
668
731
|
const inner = [
|
|
669
732
|
`$taskName = ${psSingleQuote(taskName)}`,
|
|
670
|
-
|
|
671
|
-
|
|
733
|
+
READ_STAGED_TASK_XML,
|
|
734
|
+
`$xml = Read-OcxStagedTaskXml ${psSingleQuote(xml.path)} ${psSingleQuote(xml.sha256)}`,
|
|
672
735
|
`$module = Microsoft.PowerShell.Core\\Import-Module -Name ${psSingleQuote(scheduledTasksModule)} -PassThru -Force -ErrorAction Stop`,
|
|
673
736
|
"$registerTask = $module.ExportedCommands['Register-ScheduledTask']",
|
|
674
737
|
"if ($null -eq $registerTask) { throw 'Trusted ScheduledTasks module does not export Register-ScheduledTask.' }",
|
|
675
738
|
...(replace ? [
|
|
676
|
-
`$
|
|
677
|
-
"$expectedXml = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($expectedBase64))",
|
|
739
|
+
`$expectedXml = Read-OcxStagedTaskXml ${psSingleQuote(expectedExisting!.path)} ${psSingleQuote(expectedExisting!.sha256)}`,
|
|
678
740
|
`$schtasks = ${psSingleQuote(resolveTrustedWindowsSchtasksExe())}`,
|
|
679
741
|
"$currentXml = & $schtasks /query /tn $taskName /xml 2>$null | Out-String",
|
|
680
742
|
"if ($LASTEXITCODE -ne 0) { throw 'Task Scheduler replacement precondition could not be read.' }",
|
|
@@ -30,8 +30,13 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { existsSync, statSync } from "node:fs";
|
|
33
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
33
34
|
import { env, platform } from "node:process";
|
|
34
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
SUBPROCESS_KILL_GRACE_MS,
|
|
37
|
+
waitForSubprocessExit,
|
|
38
|
+
type SubprocessDeadlineScheduler,
|
|
39
|
+
} from "./bounded-subprocess";
|
|
35
40
|
import { resolveTrustedWindowsIcaclsExe } from "./windows-elevation";
|
|
36
41
|
import {
|
|
37
42
|
cachedCurrentWindowsIdentity,
|
|
@@ -48,17 +53,32 @@ const hardenedPaths = new Map<string, HardenedIdentity>();
|
|
|
48
53
|
* that attempt was consumed. Ordinary callers never consume it.
|
|
49
54
|
*/
|
|
50
55
|
const timedOutPaths = new Map<string, boolean>();
|
|
56
|
+
/** Compatibility slack before the outer belt releases a caller whose killed child has not reaped. */
|
|
57
|
+
const ASYNC_ICACLS_BELT_MARGIN_MS = 250;
|
|
58
|
+
const pendingAsyncIcaclsReaps = new Map<string, Set<Promise<void>>>();
|
|
59
|
+
|
|
60
|
+
const scheduleAsyncIcaclsBelt: SubprocessDeadlineScheduler = (callback, milliseconds) => {
|
|
61
|
+
const timer = setTimeout(callback, milliseconds);
|
|
62
|
+
return () => clearTimeout(timer);
|
|
63
|
+
};
|
|
64
|
+
let asyncIcaclsBeltScheduler: SubprocessDeadlineScheduler = scheduleAsyncIcaclsBelt;
|
|
51
65
|
|
|
52
66
|
/**
|
|
53
|
-
* The memo value: `object
|
|
54
|
-
* to.
|
|
67
|
+
* The memo value: the `object` plus `freshness` of a file a harden was actually
|
|
68
|
+
* attributed to.
|
|
55
69
|
*
|
|
56
70
|
* There is deliberately no null member. An observation that cannot be read is
|
|
57
71
|
* not stored at all — the entry is deleted — because a "recorded as unverifiable"
|
|
58
72
|
* value was dead code the moment attribution became a before/after comparison,
|
|
59
73
|
* and a branch nothing can reach is a branch no test can defend.
|
|
74
|
+
*
|
|
75
|
+
* It is the observation itself rather than a joined string so that the two
|
|
76
|
+
* questions stay separately askable after storage. `reattributeHardenedSecretPath`
|
|
77
|
+
* has to compare the object while deliberately ignoring the freshness, and
|
|
78
|
+
* recovering one half out of `dev:ino:ctimeNs` by counting colons would make that
|
|
79
|
+
* comparison depend on a format nothing declares.
|
|
60
80
|
*/
|
|
61
|
-
type HardenedIdentity =
|
|
81
|
+
type HardenedIdentity = PathObservation;
|
|
62
82
|
|
|
63
83
|
/**
|
|
64
84
|
* What a stat can tell us about WHICH OBJECT is at a path.
|
|
@@ -128,8 +148,8 @@ function observe(targetPath: string): PathObservation | null {
|
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
|
|
131
|
-
function
|
|
132
|
-
return
|
|
151
|
+
function sameObservation(a: PathObservation, b: PathObservation): boolean {
|
|
152
|
+
return a.object === b.object && a.freshness === b.freshness;
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
/**
|
|
@@ -155,7 +175,7 @@ function memoSatisfied(cache: Map<string, HardenedIdentity>, targetPath: string)
|
|
|
155
175
|
// without any ACL work. That needs exact-identity ABA to bite — outside the
|
|
156
176
|
// proof bound this unit claims — but "the consequence is out of scope" is not a
|
|
157
177
|
// reason to keep an entry we have just proven does not describe what is there.
|
|
158
|
-
if (current === null ||
|
|
178
|
+
if (current === null || !sameObservation(current, remembered)) {
|
|
159
179
|
cache.delete(targetPath);
|
|
160
180
|
return false;
|
|
161
181
|
}
|
|
@@ -203,7 +223,7 @@ function recordHarden(
|
|
|
203
223
|
cache.delete(targetPath);
|
|
204
224
|
return false;
|
|
205
225
|
}
|
|
206
|
-
cache.set(targetPath,
|
|
226
|
+
cache.set(targetPath, after);
|
|
207
227
|
return true;
|
|
208
228
|
}
|
|
209
229
|
|
|
@@ -339,7 +359,7 @@ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
|
|
|
339
359
|
/**
|
|
340
360
|
* Async icacls runner (#612): yields the event loop while waiting for the child.
|
|
341
361
|
* Async Subprocess has no exitedDueToTimeout, so the shared settlement helper
|
|
342
|
-
* classifies the deadline and
|
|
362
|
+
* classifies the deadline and keeps waiting for a killed child to actually exit.
|
|
343
363
|
*/
|
|
344
364
|
async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
|
|
345
365
|
const proc = trySpawnIcacls(args);
|
|
@@ -359,21 +379,78 @@ async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Prom
|
|
|
359
379
|
function awaitAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
|
|
360
380
|
return new Promise(resolve => {
|
|
361
381
|
let settled = false;
|
|
362
|
-
let
|
|
382
|
+
let cancelBelt: (() => void) | undefined;
|
|
363
383
|
const finish = (result: IcaclsResult): void => {
|
|
364
384
|
if (settled) return;
|
|
365
385
|
settled = true;
|
|
366
|
-
|
|
386
|
+
cancelBelt?.();
|
|
367
387
|
resolve(result);
|
|
368
388
|
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
389
|
+
const runner = asyncIcaclsRunner(args, timeoutMs).then(
|
|
390
|
+
result => { finish(result); },
|
|
391
|
+
() => { finish(spawnFailedResult()); },
|
|
392
|
+
);
|
|
393
|
+
// The belt has to outlast the runner it is guarding, or it is not a belt -- it is the
|
|
394
|
+
// deadline. The runner may now legitimately outlive it while a killed child is reaped. The
|
|
395
|
+
// caller is still released, but the target is registered so removal can wait for the distinct
|
|
396
|
+
// handle-release question instead of treating flight settlement as proof that the child died.
|
|
397
|
+
cancelBelt = asyncIcaclsBeltScheduler(
|
|
398
|
+
() => {
|
|
399
|
+
const targetPath = args[0];
|
|
400
|
+
if (targetPath) registerPendingAsyncIcaclsReap(targetPath, runner);
|
|
401
|
+
finish({ success: false, exitCode: null, timedOut: true, stdout: "" });
|
|
402
|
+
},
|
|
403
|
+
Math.max(1, timeoutMs) + SUBPROCESS_KILL_GRACE_MS + ASYNC_ICACLS_BELT_MARGIN_MS,
|
|
372
404
|
);
|
|
373
|
-
void asyncIcaclsRunner(args, timeoutMs).then(finish, () => finish(spawnFailedResult()));
|
|
374
405
|
});
|
|
375
406
|
}
|
|
376
407
|
|
|
408
|
+
function registerPendingAsyncIcaclsReap(targetPath: string, reap: Promise<void>): void {
|
|
409
|
+
let pending = pendingAsyncIcaclsReaps.get(targetPath);
|
|
410
|
+
if (!pending) {
|
|
411
|
+
pending = new Set();
|
|
412
|
+
pendingAsyncIcaclsReaps.set(targetPath, pending);
|
|
413
|
+
}
|
|
414
|
+
pending.add(reap);
|
|
415
|
+
void reap.finally(() => {
|
|
416
|
+
pending!.delete(reap);
|
|
417
|
+
if (pending!.size === 0) pendingAsyncIcaclsReaps.delete(targetPath);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function pathIsAtOrBelow(targetPath: string, rootPath: string): boolean {
|
|
422
|
+
const relativePath = relative(resolve(rootPath), resolve(targetPath));
|
|
423
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** True while an async icacls runner still owns this exact path after its caller's belt fired. */
|
|
427
|
+
export function windowsSecretAclReapPendingForPath(targetPath: string): boolean {
|
|
428
|
+
return (pendingAsyncIcaclsReaps.get(targetPath)?.size ?? 0) > 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Non-blocking removal guard for callers that must refuse rather than wait for a stuck child. */
|
|
432
|
+
export function windowsSecretAclReapPendingAtOrBelow(rootPath: string): boolean {
|
|
433
|
+
return [...pendingAsyncIcaclsReaps.keys()]
|
|
434
|
+
.some(targetPath => pathIsAtOrBelow(targetPath, rootPath));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Removal barrier for a file or tree that may still be held by a timed-out icacls child.
|
|
439
|
+
*
|
|
440
|
+
* This wait is deliberately separate from ordinary startup and shutdown: a genuinely stuck child
|
|
441
|
+
* must not defeat the caller-facing belt. Code that chooses to remove the target has the stricter
|
|
442
|
+
* contract and must not proceed until every registered runner at or below it has actually reaped.
|
|
443
|
+
*/
|
|
444
|
+
export async function flushWindowsSecretAclReapsBeforeRemoval(rootPath: string): Promise<void> {
|
|
445
|
+
while (true) {
|
|
446
|
+
const pending = [...pendingAsyncIcaclsReaps]
|
|
447
|
+
.filter(([targetPath]) => pathIsAtOrBelow(targetPath, rootPath))
|
|
448
|
+
.flatMap(([, reaps]) => [...reaps]);
|
|
449
|
+
if (pending.length === 0) return;
|
|
450
|
+
await Promise.all(pending);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
377
454
|
let icaclsRunner: IcaclsRunner = defaultIcaclsRunner;
|
|
378
455
|
let asyncIcaclsRunner: AsyncIcaclsRunner = defaultAsyncIcaclsRunner;
|
|
379
456
|
let platformOverride: string | null = null;
|
|
@@ -389,6 +466,13 @@ export function setAsyncIcaclsRunnerForTests(runner: AsyncIcaclsRunner | null):
|
|
|
389
466
|
asyncIcaclsRunner = runner ?? defaultAsyncIcaclsRunner;
|
|
390
467
|
}
|
|
391
468
|
|
|
469
|
+
/** Test seam: fire the outer caller-facing belt without sleeping. */
|
|
470
|
+
export function setAsyncIcaclsBeltSchedulerForTests(
|
|
471
|
+
scheduler: SubprocessDeadlineScheduler | null,
|
|
472
|
+
): void {
|
|
473
|
+
asyncIcaclsBeltScheduler = scheduler ?? scheduleAsyncIcaclsBelt;
|
|
474
|
+
}
|
|
475
|
+
|
|
392
476
|
/**
|
|
393
477
|
* Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner.
|
|
394
478
|
*
|
|
@@ -423,6 +507,58 @@ export function forgetHardenedSecretPath(targetPath: string): void {
|
|
|
423
507
|
hardenedPaths.delete(targetPath);
|
|
424
508
|
}
|
|
425
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Re-attribute an existing file memo to the SAME object after the caller wrote
|
|
512
|
+
* content to it through a descriptor whose identity it verified.
|
|
513
|
+
*
|
|
514
|
+
* This exists because `freshness` is `ctimeNs`, and on Windows libuv reports
|
|
515
|
+
* `st_ctim` from the NTFS ChangeTime, which moves when file DATA is written. An
|
|
516
|
+
* atomic writer therefore invalidated its own memo between the harden that
|
|
517
|
+
* protects the empty temp and the harden before the rename, and paid a second
|
|
518
|
+
* full `/grant:r` + `/inheritance:r` + `/remove:g` sequence to reapply the ACL
|
|
519
|
+
* that was already on the file. Every secret write on Windows paid it twice.
|
|
520
|
+
*
|
|
521
|
+
* Only the freshness moves, and only for an unchanged object: a different object
|
|
522
|
+
* retires the entry instead. A caller must have proven, immediately beforehand,
|
|
523
|
+
* that `targetPath` resolves to the object its own descriptor refers to.
|
|
524
|
+
*
|
|
525
|
+
* The cost of this is worth stating exactly, because `PathObservation` documents
|
|
526
|
+
* that freshness also moves when PERMISSIONS change, and this call cannot tell
|
|
527
|
+
* the two apart. So a DACL change landing between the harden and this call is
|
|
528
|
+
* absorbed instead of forcing a re-harden. That window is the caller's own
|
|
529
|
+
* content write; every permission change after this call still moves ctime
|
|
530
|
+
* again and still misses the memo, so the detection this memo provides is
|
|
531
|
+
* relocated, not removed.
|
|
532
|
+
*
|
|
533
|
+
* What makes the absorbed window acceptable is who can be in it. Once the harden
|
|
534
|
+
* has run, the DACL is an explicit owner-only ACE with inheritance removed, so
|
|
535
|
+
* no other principal can open the file for `WRITE_DAC` at all. The one principal
|
|
536
|
+
* who can still rewrite that DACL is one holding a handle opened BEFORE the
|
|
537
|
+
* harden, and Windows keeps the access granted to an open handle: that principal
|
|
538
|
+
* can equally rewrite the DACL after any later harden, and after the rename, on
|
|
539
|
+
* the same object. A second mutation pass never bounded that capability — it
|
|
540
|
+
* stripped an ACE the holder could immediately re-add — so declining to repeat
|
|
541
|
+
* it removes no guarantee anyone had.
|
|
542
|
+
*
|
|
543
|
+
* Refusal is cheap and safe in either direction: an unmoved memo simply means the
|
|
544
|
+
* caller's next harden runs in full.
|
|
545
|
+
*
|
|
546
|
+
* Returns whether the memo now describes what is at the path.
|
|
547
|
+
*/
|
|
548
|
+
export function reattributeHardenedSecretPath(targetPath: string): boolean {
|
|
549
|
+
const remembered = hardenedPaths.get(targetPath);
|
|
550
|
+
if (remembered === undefined) return false;
|
|
551
|
+
const current = observe(targetPath);
|
|
552
|
+
// Unreadable, or a different object: this is exactly the case the memo must
|
|
553
|
+
// not cover. Retire it so the next harden is a real one.
|
|
554
|
+
if (current === null || current.object !== remembered.object) {
|
|
555
|
+
hardenedPaths.delete(targetPath);
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
hardenedPaths.set(targetPath, current);
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
|
|
426
562
|
/**
|
|
427
563
|
* Ephemeral-path lifecycle release: clears the success memo AND any timeout
|
|
428
564
|
* memo keyed by THIS TEMP path in both namespaces. Call only after the temp is
|
|
@@ -160,7 +160,11 @@ async function defaultAsyncWindowsPrincipalRunner(
|
|
|
160
160
|
stderr: "ignore",
|
|
161
161
|
windowsHide: true,
|
|
162
162
|
});
|
|
163
|
-
|
|
163
|
+
// No kill grace. The grace exists so a dying child releases a path someone is about to
|
|
164
|
+
// remove; this lookup holds no such path, and it runs during `ocx start`, where the composed
|
|
165
|
+
// acceptance cases already measure real startups at up to 38.8s against a bounded watchdog.
|
|
166
|
+
// Paying two extra seconds per timed-out resolution there buys nothing and costs margin.
|
|
167
|
+
const { exitCode, timedOut } = await waitForSubprocessExit(proc, timeoutMs, 0);
|
|
164
168
|
// `.bytes()` rather than `.text()`, for the same reason as the sync runner above.
|
|
165
169
|
const stdout: string | Uint8Array = !timedOut && proc.stdout
|
|
166
170
|
? await new Response(proc.stdout).bytes().catch(() => new Uint8Array())
|
package/src/oauth/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
|
47
47
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
48
48
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
49
49
|
import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
|
|
50
|
-
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
50
|
+
import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
51
51
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
52
52
|
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
53
53
|
import { logOAuthEvent } from "./log";
|
|
@@ -1228,7 +1228,7 @@ export function buildModelsRequest(
|
|
|
1228
1228
|
return { url: discoveryUrl(`${base}/v1/models?limit=1000`), headers };
|
|
1229
1229
|
}
|
|
1230
1230
|
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1231
|
-
return { url: discoveryUrl(
|
|
1231
|
+
return { url: discoveryUrl(providerModelsUrl(effectiveProvider.baseUrl)), headers };
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
1234
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { deriveKeyLoginMap, enrichProviderFromRegistry, type DerivedKeyLoginProvider } from "../providers/derive";
|
|
3
|
-
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
3
|
+
import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
|
|
@@ -117,7 +117,7 @@ export async function validateApiKey(
|
|
|
117
117
|
providerName,
|
|
118
118
|
configuredProvider,
|
|
119
119
|
provider.baseUrl,
|
|
120
|
-
|
|
120
|
+
providerModelsUrl(provider.baseUrl),
|
|
121
121
|
);
|
|
122
122
|
const res = await fetch(modelsUrl, {
|
|
123
123
|
headers: { Authorization: `Bearer ${key}` },
|
package/src/providers/derive.ts
CHANGED
|
@@ -266,6 +266,9 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
266
266
|
...(entry.requiresAdjacentResponsesToolResults !== undefined
|
|
267
267
|
? { requiresAdjacentResponsesToolResults: entry.requiresAdjacentResponsesToolResults }
|
|
268
268
|
: {}),
|
|
269
|
+
...(entry.requiresPairedResponsesToolResults !== undefined
|
|
270
|
+
? { requiresPairedResponsesToolResults: entry.requiresPairedResponsesToolResults }
|
|
271
|
+
: {}),
|
|
269
272
|
...(entry.annotateEmptyToolOutputs !== undefined
|
|
270
273
|
? { annotateEmptyToolOutputs: entry.annotateEmptyToolOutputs }
|
|
271
274
|
: {}),
|
|
@@ -541,6 +544,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
541
544
|
if (prov.requiresAdjacentResponsesToolResults === undefined && seed.requiresAdjacentResponsesToolResults !== undefined) {
|
|
542
545
|
prov.requiresAdjacentResponsesToolResults = seed.requiresAdjacentResponsesToolResults;
|
|
543
546
|
}
|
|
547
|
+
if (prov.requiresPairedResponsesToolResults === undefined && seed.requiresPairedResponsesToolResults !== undefined) {
|
|
548
|
+
prov.requiresPairedResponsesToolResults = seed.requiresPairedResponsesToolResults;
|
|
549
|
+
}
|
|
544
550
|
if (prov.annotateEmptyToolOutputs === undefined && seed.annotateEmptyToolOutputs !== undefined) {
|
|
545
551
|
prov.annotateEmptyToolOutputs = seed.annotateEmptyToolOutputs;
|
|
546
552
|
}
|
|
@@ -47,9 +47,10 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
47
47
|
|
|
48
48
|
const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
// (`reasoning.effort`
|
|
52
|
-
// thinking instructions until their native
|
|
50
|
+
// The GPT-5.6 family (sol/terra/luna) and claude-opus-5 send these values through Kiro's verified
|
|
51
|
+
// native effort fields (`reasoning.effort` for the GPT-5.6 models, `output_config.effort` for
|
|
52
|
+
// claude-opus-5). Other models map them to bounded thinking instructions until their native
|
|
53
|
+
// effort support is verified.
|
|
53
54
|
export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
|
|
54
55
|
KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
|
|
55
56
|
);
|
package/src/providers/label.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import { CODEX_ACCOUNT_LOG_LABEL_RE, oauthAccountLogLabel } from "../codex/account-label";
|
|
1
|
+
import { CODEX_ACCOUNT_LOG_LABEL_RE, KEY_ACCOUNT_LOG_LABEL_RE, apiKeyAccountLogLabel, oauthAccountLogLabel } from "../codex/account-label";
|
|
2
2
|
import type { OcxProviderConfig } from "../types";
|
|
3
3
|
|
|
4
4
|
export function canonicalUsageProviderLabel(provider: string): string {
|
|
5
5
|
return provider === "chatgpt" || provider === "openai-multi" ? "openai" : provider;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export function usesApiKeyAccount(provider: Pick<OcxProviderConfig, "authMode" | "_apiKeyAttempt">): boolean {
|
|
9
|
+
return provider.authMode === "key"
|
|
10
|
+
|| (provider.authMode === undefined && !!provider._apiKeyAttempt?.reference);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Key identity comes from the captured selection, before env/keychain resolution. */
|
|
14
|
+
export function stampApiKeyAccountLabel(
|
|
15
|
+
logCtx: { accountLogLabel?: string },
|
|
16
|
+
providerName: string,
|
|
17
|
+
provider: Pick<OcxProviderConfig, "authMode" | "_apiKeyAttempt">,
|
|
18
|
+
): void {
|
|
19
|
+
if (usesApiKeyAccount(provider)) {
|
|
20
|
+
logCtx.accountLogLabel = apiKeyAccountLogLabel(providerName, provider._apiKeyAttempt);
|
|
21
|
+
} else if (KEY_ACCOUNT_LOG_LABEL_RE.test(logCtx.accountLogLabel ?? "")) {
|
|
22
|
+
delete logCtx.accountLogLabel;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
export function baseProviderLabel(provider: string): string {
|
|
9
27
|
const canonical = canonicalUsageProviderLabel(provider);
|
|
10
28
|
if (canonical !== provider) return canonical;
|