@bitkyc08/opencodex 2.76.0 → 2.77.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/gui/dist/assets/App-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -191,12 +191,18 @@ export function stripRejectedSamplingParams(
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
* When `store
|
|
194
|
+
* When `store: false`, the upstream cannot retrieve stored turns by ID. An ID
|
|
195
195
|
* forwarded in `input` is then interpreted as a reference to a stored item that does not
|
|
196
|
-
* exist, producing a 404. Strip all item IDs in this case
|
|
196
|
+
* exist, producing a 404. Strip all item IDs in this case unless `preserveIds` is true
|
|
197
|
+
* — `call_id` pairing is unaffected. Also repairs missing or broken xAI custom_tool_call IDs
|
|
198
|
+
* when `requireCustomCallIds` is true, regardless of `preserveIds`.
|
|
197
199
|
* Matches codex-rs behavior (core/src/client.rs:918-925).
|
|
198
200
|
*/
|
|
199
|
-
export function stripItemIdsWhenUnstored(
|
|
201
|
+
export function stripItemIdsWhenUnstored(
|
|
202
|
+
body: unknown,
|
|
203
|
+
requireCustomCallIds = false,
|
|
204
|
+
preserveIds = false,
|
|
205
|
+
): unknown {
|
|
200
206
|
const repairCustomCallIds = requireCustomCallIds === true;
|
|
201
207
|
if (!isPlainObject(body) || (body.store !== false && !repairCustomCallIds)) return body;
|
|
202
208
|
if (!Array.isArray(body.input)) return body;
|
|
@@ -226,7 +232,7 @@ export function stripItemIdsWhenUnstored(body: unknown, requireCustomCallIds = f
|
|
|
226
232
|
return item;
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
|
-
if (body.store !== false || !("id" in item)) return item;
|
|
235
|
+
if (preserveIds || body.store !== false || !("id" in item)) return item;
|
|
230
236
|
changed = true;
|
|
231
237
|
const next = { ...item };
|
|
232
238
|
delete next.id;
|
package/src/adapters/zed.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { createGoogleAdapter } from "./google";
|
|
|
5
5
|
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
6
6
|
import { createResponsesPassthroughAdapter } from "./openai-responses";
|
|
7
7
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
8
|
-
import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator-budget";
|
|
8
|
+
import { createTranslatorBudget, releaseTranslatedEvent, retainTranslatedEvent, type TranslatorBudget } from "../lib/translator-budget";
|
|
9
|
+
import { releaseCompactionCiphertextLease } from "../responses/compaction";
|
|
9
10
|
import { redactSecretString } from "../lib/redact";
|
|
10
11
|
import {
|
|
11
12
|
normalizeZedProvider,
|
|
@@ -365,12 +366,33 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
|
365
366
|
status: response.status,
|
|
366
367
|
headers: { "Content-Type": "text/event-stream" },
|
|
367
368
|
});
|
|
368
|
-
|
|
369
|
+
try {
|
|
370
|
+
yield* delegate.adapter.parseStream(translated, budget);
|
|
371
|
+
} finally {
|
|
372
|
+
// Some delegates only unlock their reader when a collector stops early.
|
|
373
|
+
try { await translated.body?.cancel(); } catch { /* already closed/errored */ }
|
|
374
|
+
}
|
|
369
375
|
},
|
|
370
376
|
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
371
377
|
const events: AdapterEvent[] = [];
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
try {
|
|
379
|
+
for await (const event of adapter.parseStream(response, budget)) {
|
|
380
|
+
try {
|
|
381
|
+
retainTranslatedEvent(event, budget, events.at(-1));
|
|
382
|
+
} catch (error) {
|
|
383
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
events.push(event);
|
|
387
|
+
}
|
|
388
|
+
return events;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
for (const event of events) {
|
|
391
|
+
releaseTranslatedEvent(event, budget);
|
|
392
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
393
|
+
}
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
374
396
|
},
|
|
375
397
|
};
|
|
376
398
|
return adapter;
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type OcxErrorPayload,
|
|
18
18
|
} from "../lib/errors";
|
|
19
19
|
import { mayBecomePatchEnvelope, repairFreeformToolInput } from "../responses/apply-patch-envelope";
|
|
20
|
-
import { encodeCompactionSummary } from "../responses/compaction";
|
|
20
|
+
import { encodeCompactionSummary, releaseCompactionCiphertextLease } from "../responses/compaction";
|
|
21
21
|
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
|
|
22
22
|
import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
|
|
23
23
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "../responses/reasoning-envelope";
|
|
@@ -59,21 +59,21 @@ export function buildResponseJSON(
|
|
|
59
59
|
): Record<string, unknown> {
|
|
60
60
|
// Default-budget safety net: a caller that omits the budget gets a bounded
|
|
61
61
|
// default (disposed with the call), never the unbounded append path.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
if (options.recordBufferedDelivery !== false) {
|
|
68
|
-
attemptDeliveryRecorder(
|
|
62
|
+
const ownsBudget = !options?.translatorBudget;
|
|
63
|
+
const budget = options?.translatorBudget ?? createTranslatorBudget();
|
|
64
|
+
try {
|
|
65
|
+
const body = buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
|
|
66
|
+
// Buffered delivery has no per-frame recorder; retain its existing one-shot attribution.
|
|
67
|
+
if (options?.translatorBudget && options.recordBufferedDelivery !== false) {
|
|
68
|
+
attemptDeliveryRecorder(budget)?.noteBufferedDelivery(body);
|
|
69
69
|
}
|
|
70
70
|
return body;
|
|
71
|
-
}
|
|
72
|
-
const budget = createTranslatorBudget();
|
|
73
|
-
try {
|
|
74
|
-
return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
|
|
75
71
|
} finally {
|
|
76
|
-
|
|
72
|
+
for (const event of events) {
|
|
73
|
+
releaseTranslatedEvent(event, budget);
|
|
74
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
75
|
+
}
|
|
76
|
+
if (ownsBudget) budget.dispose();
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -618,7 +618,7 @@ function buildResponseJSONWithBudget(
|
|
|
618
618
|
type: "compaction", id: `cmp_${uuid()}`,
|
|
619
619
|
encrypted_content: compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(batchCompaction)),
|
|
620
620
|
};
|
|
621
|
-
pushOutput(item, compactionEncryptedContent ?
|
|
621
|
+
pushOutput(item, compactionEncryptedContent ? 0 : batchCompaction.bytes);
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
|
package/src/bridge/sse.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
repairFreeformToolInput,
|
|
23
23
|
} from "../responses/apply-patch-envelope";
|
|
24
24
|
import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
|
|
25
|
-
import { encodeCompactionSummary } from "../responses/compaction";
|
|
25
|
+
import { encodeCompactionSummary, releaseCompactionCiphertextLease } from "../responses/compaction";
|
|
26
26
|
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
|
|
27
27
|
import { mayBecomeCodeModeShellInput } from "../responses/code-mode-shell-input";
|
|
28
28
|
import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
|
|
@@ -714,6 +714,10 @@ export function bridgeToResponsesSSE(
|
|
|
714
714
|
firstOutputReported = true;
|
|
715
715
|
try { options?.onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
|
|
716
716
|
};
|
|
717
|
+
const releaseEvent = (event: AdapterEvent) => {
|
|
718
|
+
releaseTranslatedEvent(event, budget);
|
|
719
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
720
|
+
};
|
|
717
721
|
const it = events[Symbol.asyncIterator]();
|
|
718
722
|
let iteratorStarted = false;
|
|
719
723
|
let iteratorReturned = false;
|
|
@@ -734,8 +738,11 @@ export function bridgeToResponsesSSE(
|
|
|
734
738
|
if (!iteratorStarted) {
|
|
735
739
|
iteratorStarted = true;
|
|
736
740
|
try {
|
|
737
|
-
void it.next().then(
|
|
741
|
+
void it.next().then(next => {
|
|
742
|
+
try { if (!next.done) releaseEvent(next.value); } finally { finishReturn(); }
|
|
743
|
+
}, finishReturn).catch(() => {});
|
|
738
744
|
} catch {
|
|
745
|
+
finishReturn();
|
|
739
746
|
/* synchronous iterator start failure is also best-effort */
|
|
740
747
|
}
|
|
741
748
|
return;
|
|
@@ -809,6 +816,7 @@ export function bridgeToResponsesSSE(
|
|
|
809
816
|
while (!terminated && !closed && emittedFrames === emittedAtStart) {
|
|
810
817
|
iteratorStarted = true;
|
|
811
818
|
const next = await it.next();
|
|
819
|
+
try {
|
|
812
820
|
// A cancel during this await disposes the owned budget; a late event
|
|
813
821
|
// must never be processed or charged against it. Exit step() outright:
|
|
814
822
|
// falling into EOF synthesis would let closeCurrentMessage() charge
|
|
@@ -1228,9 +1236,8 @@ export function bridgeToResponsesSSE(
|
|
|
1228
1236
|
encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(compaction)),
|
|
1229
1237
|
};
|
|
1230
1238
|
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
1231
|
-
retainFinishedItem(item as OutputItem, event.compactionEncryptedContent
|
|
1232
|
-
|
|
1233
|
-
: compaction.bytes);
|
|
1239
|
+
retainFinishedItem(item as OutputItem, event.compactionEncryptedContent ? 0 : compaction.bytes);
|
|
1240
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
1234
1241
|
outputIndex++;
|
|
1235
1242
|
}
|
|
1236
1243
|
// Recognize every adapter's truncation vocabulary, not just the canonical pair.
|
|
@@ -1333,6 +1340,9 @@ export function bridgeToResponsesSSE(
|
|
|
1333
1340
|
terminated = true;
|
|
1334
1341
|
break;
|
|
1335
1342
|
}
|
|
1343
|
+
} finally {
|
|
1344
|
+
if (!next.done) releaseEvent(next.value);
|
|
1345
|
+
}
|
|
1336
1346
|
}
|
|
1337
1347
|
} catch (err) {
|
|
1338
1348
|
if (isTranslatorBudgetExceededError(err)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { lstatSync } from "node:fs";
|
|
3
|
-
import { dirname, relative, sep } from "node:path";
|
|
3
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Launch and restore execute code from a discovered bundle, so the bundle must be
|
|
7
|
+
* OpenAI-signed and pass ownership/permission checks along its filesystem path. Bundle
|
|
8
8
|
* identifier alone (what discovery checks) is plain text in Info.plist that anyone can copy.
|
|
9
9
|
*/
|
|
10
10
|
export const OPENAI_TEAM_ID = "2DC432GLL2";
|
|
@@ -14,17 +14,23 @@ const CODESIGN_TIMEOUT_MS = 30_000;
|
|
|
14
14
|
|
|
15
15
|
export interface BundleTrustDeps {
|
|
16
16
|
readonly uid: number;
|
|
17
|
-
|
|
17
|
+
readonly adminGroupId?: number;
|
|
18
|
+
stat(path: string): { uid: number; gid?: number; mode: number; isFile: boolean; isDirectory: boolean; isSymbolicLink: boolean } | null;
|
|
18
19
|
codesign(args: readonly string[]): { status: number | null; output: string };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function defaultBundleTrustDeps(): BundleTrustDeps {
|
|
23
|
+
const admin = spawnSync("/usr/bin/dscl", [".", "-read", "/Groups/admin", "PrimaryGroupID"], {
|
|
24
|
+
encoding: "utf8", timeout: 5000,
|
|
25
|
+
});
|
|
26
|
+
const adminId = admin.status === 0 ? /^PrimaryGroupID: ([0-9]+)\s*$/.exec(admin.stdout)?.[1] : undefined;
|
|
22
27
|
return {
|
|
28
|
+
adminGroupId: adminId === undefined ? undefined : Number(adminId),
|
|
23
29
|
uid: typeof process.getuid === "function" ? process.getuid() : -1,
|
|
24
30
|
stat(path) {
|
|
25
31
|
try {
|
|
26
32
|
const s = lstatSync(path);
|
|
27
|
-
return { uid: s.uid, mode: s.mode, isFile: s.isFile(), isDirectory: s.isDirectory(), isSymbolicLink: s.isSymbolicLink() };
|
|
33
|
+
return { uid: s.uid, gid: s.gid, mode: s.mode, isFile: s.isFile(), isDirectory: s.isDirectory(), isSymbolicLink: s.isSymbolicLink() };
|
|
28
34
|
} catch {
|
|
29
35
|
return null;
|
|
30
36
|
}
|
|
@@ -46,26 +52,50 @@ function ownedSafely(deps: BundleTrustDeps, path: string, kind: "file" | "direct
|
|
|
46
52
|
return null;
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
/** Protect the bundle's directory entry all the way to the filesystem root. */
|
|
56
|
+
function untrustedAncestorReason(bundleRoot: string, deps: BundleTrustDeps): string | null {
|
|
57
|
+
for (let dir = dirname(bundleRoot); ; dir = dirname(dir)) {
|
|
58
|
+
const s = deps.stat(dir);
|
|
59
|
+
if (!s || !s.isDirectory || s.isSymbolicLink) return `${dir} is not a regular ancestor directory`;
|
|
60
|
+
if (s.uid !== 0 && s.uid !== deps.uid) return `${dir} is owned by another user`;
|
|
61
|
+
// A trusted sticky parent protects the already-checked child from replacement.
|
|
62
|
+
// Root-owned admin-group install containers are privileged OS-controlled paths;
|
|
63
|
+
// resolve that group from the local directory service rather than assuming a GID.
|
|
64
|
+
const sticky = (s.mode & 0o1000) !== 0;
|
|
65
|
+
const privileged = s.uid === 0 && (s.mode & 0o002) === 0
|
|
66
|
+
&& deps.adminGroupId !== undefined && s.gid === deps.adminGroupId;
|
|
67
|
+
if ((s.mode & 0o022) !== 0 && !sticky && !privileged) return `${dir} is writable by group or others`;
|
|
68
|
+
if (dirname(dir) === dir) return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
/**
|
|
50
|
-
* Null when the bundle root and the
|
|
51
|
-
*
|
|
73
|
+
* Null when the bundle root and the selected executable may be run by this user.
|
|
74
|
+
* Restore omits the app-server binary and validates the app shell instead, so a missing
|
|
75
|
+
* app-server does not prevent returning to a normal launch. Otherwise returns a short reason.
|
|
76
|
+
* Every directory from the binary up to the bundle root, and the
|
|
52
77
|
* binary itself, must be owned by root or this user and not group/other-writable, and both the
|
|
53
78
|
* bundle and the binary must pass strict code-signature verification under OpenAI's team ID.
|
|
79
|
+
* Ancestors above the bundle are also checked for POSIX directory-entry replacement rights.
|
|
54
80
|
*/
|
|
55
81
|
export function untrustedChatgptBundleReason(
|
|
56
82
|
bundleRoot: string,
|
|
57
|
-
binary: string,
|
|
83
|
+
binary: string | undefined = undefined,
|
|
58
84
|
deps: BundleTrustDeps = defaultBundleTrustDeps(),
|
|
59
85
|
): string | null {
|
|
86
|
+
const label = binary === undefined ? "app executable" : "app-server binary";
|
|
87
|
+
binary ??= join(bundleRoot, "Contents", "MacOS", "ChatGPT");
|
|
60
88
|
const inside = relative(bundleRoot, binary);
|
|
61
|
-
if (!inside || inside.startsWith("..") || inside.startsWith(sep)) return
|
|
89
|
+
if (!inside || inside.startsWith("..") || inside.startsWith(sep)) return `the ${label} is outside the bundle`;
|
|
62
90
|
const fileProblem = ownedSafely(deps, binary, "file");
|
|
63
|
-
if (fileProblem) return `the
|
|
91
|
+
if (fileProblem) return `the ${label} ${fileProblem}`;
|
|
64
92
|
for (let dir = dirname(binary); ; dir = dirname(dir)) {
|
|
65
93
|
const dirProblem = ownedSafely(deps, dir, "directory");
|
|
66
94
|
if (dirProblem) return `${dir} ${dirProblem}`;
|
|
67
95
|
if (dir === bundleRoot || dirname(dir) === dir) break;
|
|
68
96
|
}
|
|
97
|
+
const ancestorProblem = untrustedAncestorReason(bundleRoot, deps);
|
|
98
|
+
if (ancestorProblem) return ancestorProblem;
|
|
69
99
|
for (const target of [bundleRoot, binary]) {
|
|
70
100
|
if (deps.codesign(["--verify", "--strict", target]).status !== 0) return `${target} failed code-signature verification`;
|
|
71
101
|
const details = deps.codesign(["-dv", "--verbose=2", target]);
|
|
@@ -38,7 +38,10 @@ export function unlockRateLimitGate(value: unknown): boolean {
|
|
|
38
38
|
return { cleared, exhausted, blocked };
|
|
39
39
|
}
|
|
40
40
|
if (!isRecord(node)) return { cleared, exhausted, blocked };
|
|
41
|
+
// A usage window at 100%, in either spelling the gate fields come in: `usedPercent` in the
|
|
42
|
+
// app-server's JSON-RPC, `used_percent` in the web usage snapshot.
|
|
41
43
|
if (typeof node.usedPercent === "number" && node.usedPercent >= 100) exhausted = true;
|
|
44
|
+
if (typeof node.used_percent === "number" && node.used_percent >= 100) exhausted = true;
|
|
42
45
|
if (node.spendControlReached !== undefined && node.spendControlReached !== null && node.spendControlReached !== false) blocked = true;
|
|
43
46
|
if (isRecord(node.spend_control) && node.spend_control.reached === true) blocked = true;
|
|
44
47
|
const reachedType = node.rate_limit_reached_type;
|
|
@@ -15,14 +15,12 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileS
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { renameAtomicFile } from "../lib/windows-atomic-replace";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { entryParts, SAFE_AGENT_MODEL_ID, withSubagentContextMarker } from "./subagent-model";
|
|
19
|
+
import { stripOneMillionMarker } from "./context-windows";
|
|
20
20
|
import { claudeConfigDir } from "./gateway-cache";
|
|
21
|
-
import { DEFAULT_SUBAGENT_MODELS
|
|
21
|
+
import { DEFAULT_SUBAGENT_MODELS } from "../config";
|
|
22
22
|
import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
|
|
23
23
|
import { AnthropicRequestError } from "./inbound-records";
|
|
24
|
-
import { knownModelIdsForProvider } from "../router";
|
|
25
|
-
import { decodeRoutedModelIdOrThrow } from "../providers/slug-codec";
|
|
26
24
|
import { siblingOfLivePort } from "../codex/sibling-start";
|
|
27
25
|
|
|
28
26
|
export interface ClaudeAgentDef {
|
|
@@ -37,7 +35,7 @@ export interface ClaudeAgentDef {
|
|
|
37
35
|
const OWNED_PREFIX = "ocx-";
|
|
38
36
|
/** Ownership proof (audit 071 #2): a file without this marker is NEVER touched. */
|
|
39
37
|
const GENERATED_MARKER = "generated-by: opencodex";
|
|
40
|
-
|
|
38
|
+
|
|
41
39
|
|
|
42
40
|
function sanitizeName(value: string): string {
|
|
43
41
|
const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -59,43 +57,6 @@ function pickerDefaultModel(configDir: string): string | null {
|
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
/** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id".
|
|
63
|
-
* Codex-facing encoded ids (`provider/vendor-model`) decode to the native slash id first
|
|
64
|
-
* so the alias joins the raw-native context-window map (context-windows.ts). */
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Generated subagent defs cannot rely on the parent's auto-context compaction
|
|
68
|
-
* pairing, so their [1m] marker follows the AUTHORITATIVE window only: mark when
|
|
69
|
-
* the effective window (exact selector, then the canonical [1m] form, then bare)
|
|
70
|
-
* is genuinely >= 1M; strip an inherited unsafe marker back to the bare selector;
|
|
71
|
-
* with no window information, keep the selector as it was. Genuine routed [1m]
|
|
72
|
-
* ids are preserved through the canonical-exact lookup. (#854)
|
|
73
|
-
*/
|
|
74
|
-
function withSubagentContextMarker(selector: string, windows: Record<string, number>): string {
|
|
75
|
-
const bare = stripOneMillionMarker(selector);
|
|
76
|
-
const wasMarked = selector !== bare;
|
|
77
|
-
const canonicalExact = wasMarked ? `${bare}[1m]` : selector;
|
|
78
|
-
const authoritativeWindow = windows[selector] ?? windows[canonicalExact] ?? windows[bare];
|
|
79
|
-
if (typeof authoritativeWindow === "number" && authoritativeWindow > 0) {
|
|
80
|
-
return shouldMarkOneMillion(authoritativeWindow, AUTO_CONTEXT_OFF)
|
|
81
|
-
? (withOneMillionMarker(selector, windows) ?? selector)
|
|
82
|
-
: bare;
|
|
83
|
-
}
|
|
84
|
-
return wasMarked ? selector : bare;
|
|
85
|
-
}
|
|
86
|
-
function entryParts(entry: string, config: OcxConfig): { alias: string; id: string; provider: string } {
|
|
87
|
-
const slash = entry.indexOf("/");
|
|
88
|
-
if (slash > 0) {
|
|
89
|
-
const provider = entry.slice(0, slash);
|
|
90
|
-
const prov = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
|
|
91
|
-
const id = prov
|
|
92
|
-
? decodeRoutedModelIdOrThrow(entry.slice(slash + 1), knownModelIdsForProvider(provider, prov, config))
|
|
93
|
-
: entry.slice(slash + 1);
|
|
94
|
-
return { alias: claudeCodeAlias(provider, id), id, provider };
|
|
95
|
-
}
|
|
96
|
-
return { alias: claudeCodeNativeAlias(entry), id: entry, provider: "native" };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
60
|
export function buildClaudeAgentDefs(
|
|
100
61
|
config: OcxConfig,
|
|
101
62
|
windows: Record<string, number>,
|
|
@@ -73,19 +73,13 @@ export function writeGatewayModelCache(baseUrl: string, models: readonly Gateway
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
): Promise<string | null>;
|
|
81
|
-
export async function refreshGatewayModelCacheFromProxy(
|
|
82
|
-
target: GatewayModelTarget,
|
|
83
|
-
options?: GatewayModelCacheRefreshOptions,
|
|
84
|
-
): Promise<string | null>;
|
|
85
|
-
export async function refreshGatewayModelCacheFromProxy(
|
|
76
|
+
export interface GatewayModelSnapshot { baseUrl: string; models: GatewayModelRow[] }
|
|
77
|
+
|
|
78
|
+
/** Acquire fresh exposure independently of whether the local cache is writable. */
|
|
79
|
+
export async function fetchGatewayModels(
|
|
86
80
|
portOrTarget: number | GatewayModelTarget,
|
|
87
81
|
options: GatewayModelCacheRefreshOptions = {},
|
|
88
|
-
): Promise<
|
|
82
|
+
): Promise<GatewayModelSnapshot | null> {
|
|
89
83
|
try {
|
|
90
84
|
const headers = new Headers({ "anthropic-version": "2023-06-01" });
|
|
91
85
|
// A wildcard/non-loopback listener requires data-plane admission even for a
|
|
@@ -114,14 +108,23 @@ export async function refreshGatewayModelCacheFromProxy(
|
|
|
114
108
|
const body = await res.json() as { data?: unknown };
|
|
115
109
|
if (!Array.isArray(body.data)) return null;
|
|
116
110
|
const models: GatewayModelRow[] = body.data
|
|
117
|
-
.filter(m => typeof m
|
|
111
|
+
.filter(m => m && typeof m === "object" && typeof m.id === "string" && m.id.length > 0)
|
|
118
112
|
.map(m => ({
|
|
119
113
|
id: m.id as string,
|
|
120
114
|
display_name: typeof m.display_name === "string" ? m.display_name : undefined,
|
|
121
115
|
description: typeof m.description === "string" ? m.description : undefined,
|
|
122
116
|
}));
|
|
123
|
-
return
|
|
117
|
+
return { baseUrl, models };
|
|
124
118
|
} catch {
|
|
125
119
|
return null;
|
|
126
120
|
}
|
|
127
121
|
}
|
|
122
|
+
|
|
123
|
+
/** Preserve the cache-refresh API used by launchers and profile application. */
|
|
124
|
+
export async function refreshGatewayModelCacheFromProxy(
|
|
125
|
+
portOrTarget: number | GatewayModelTarget,
|
|
126
|
+
options: GatewayModelCacheRefreshOptions = {},
|
|
127
|
+
): Promise<string | null> {
|
|
128
|
+
const snapshot = await fetchGatewayModels(portOrTarget, options);
|
|
129
|
+
return snapshot ? writeGatewayModelCache(snapshot.baseUrl, snapshot.models, options.configDir) : null;
|
|
130
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OcxClaudeCodeConfig } from "../types";
|
|
1
|
+
import type { OcxConfig, OcxClaudeCodeConfig } from "../types";
|
|
2
2
|
import { isAnthropicOutputSchema, satisfiesOpenAiStrictSchema } from "../adapters/anthropic-output-schema";
|
|
3
3
|
import { resolveAlias } from "./alias";
|
|
4
4
|
import { stripOneMillionMarker } from "./context-windows";
|
|
@@ -153,7 +153,14 @@ export function extractOcxRouteDirective(body: unknown): string | null {
|
|
|
153
153
|
const text = systemText(body);
|
|
154
154
|
if (!text) return null;
|
|
155
155
|
const match = OCX_ROUTE_RE.exec(text);
|
|
156
|
-
|
|
156
|
+
if (!match) return null;
|
|
157
|
+
// Explicit gateway selection belongs to this invocation, not the mutable saved
|
|
158
|
+
// force setting. Bare Claude ids still need the legacy frontmatter fallback.
|
|
159
|
+
if (isRec(body) && typeof body.model === "string") {
|
|
160
|
+
const selector = stripOneMillionMarker(body.model);
|
|
161
|
+
if (resolveAlias(selector) || resolveDesktop3pAlias(selector)) return body.model;
|
|
162
|
+
}
|
|
163
|
+
return match[1]!;
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
/**
|
|
@@ -17,6 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import type { ClaudeFirstPartyDesired } from "../first-party-settings";
|
|
18
18
|
import { classifyInterceptClient } from "./client-class";
|
|
19
19
|
import { BOOTSTRAP_MAX_DECODED_BYTES, injectPickerModels, type PickerModelEntry } from "./picker-bootstrap";
|
|
20
|
+
import { PickerRewriteBudget } from "./picker-budget";
|
|
20
21
|
|
|
21
22
|
export type CliCatalogKind = "model_selector" | "bootstrap";
|
|
22
23
|
|
|
@@ -60,26 +61,36 @@ export function injectCliBootstrapOptions(bootstrap: unknown, models: readonly P
|
|
|
60
61
|
if (current !== undefined && current !== null && !Array.isArray(current)) return 0;
|
|
61
62
|
const options = Array.isArray(current) ? current : [];
|
|
62
63
|
const existing = new Set(options.map(option => record(option)?.model));
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
const additions: Record<string, unknown>[] = [];
|
|
65
|
+
try {
|
|
66
|
+
const budget = new PickerRewriteBudget(body);
|
|
67
|
+
// A missing/null property needs a key and array, plus a possible separator (conservative).
|
|
68
|
+
if (!Array.isArray(current)) budget.reserveBytes(32);
|
|
69
|
+
for (const model of models) {
|
|
70
|
+
if (existing.has(model.id)) continue;
|
|
71
|
+
const row = { model: model.id, name: model.name, description: model.description ?? "" };
|
|
72
|
+
budget.reserveRow(row);
|
|
73
|
+
additions.push(row);
|
|
74
|
+
existing.add(model.id);
|
|
75
|
+
}
|
|
76
|
+
} catch { return 0; } // Fail open without mutating the original options.
|
|
77
|
+
for (const row of additions) options.push(row);
|
|
78
|
+
if (additions.length > 0) body.additional_model_options = options;
|
|
79
|
+
return additions.length;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/** Rewrite a decoded catalog body; `null` when nothing was added or the body is not the expected shape. */
|
|
75
83
|
export function rewriteCliCatalogBody(kind: CliCatalogKind, text: string, models: readonly PickerModelEntry[]): string | null {
|
|
76
|
-
if (models.length === 0) return null;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
if (models.length === 0 || Buffer.byteLength(text) > BOOTSTRAP_MAX_DECODED_BYTES) return null;
|
|
85
|
+
try {
|
|
86
|
+
const parsed: unknown = JSON.parse(text);
|
|
87
|
+
const added = kind === "bootstrap"
|
|
88
|
+
? injectCliBootstrapOptions(parsed, models)
|
|
89
|
+
: injectPickerModels(parsed, models, undefined, { surfaces: CLI_PICKER_SURFACE_IDS, extraStrippedKeys: CLI_EXTRA_STRIPPED_KEYS });
|
|
90
|
+
if (added === 0) return null;
|
|
91
|
+
const rewritten = JSON.stringify(parsed);
|
|
92
|
+
return Buffer.byteLength(rewritten) <= BOOTSTRAP_MAX_DECODED_BYTES ? rewritten : null;
|
|
93
|
+
} catch { return null; }
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
/** Read at most `cap` bytes; null (and the stream cancelled) once the body would exceed it. */
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* A failed or inapplicable transform leaves the upstream bytes untouched.
|
|
4
4
|
*/
|
|
5
5
|
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
6
|
+
import { PICKER_MAX_OUTPUT_BYTES, PickerRewriteBudget, pickerRowBytes } from "./picker-budget";
|
|
6
7
|
|
|
7
8
|
/** One opencodex model offered in Desktop's Code-tab picker. */
|
|
8
9
|
export interface PickerModelEntry {
|
|
@@ -16,7 +17,7 @@ export interface PickerModelEntry {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const BOOTSTRAP_MAX_ENCODED_BYTES = 4 * 1024 * 1024;
|
|
19
|
-
export const BOOTSTRAP_MAX_DECODED_BYTES =
|
|
20
|
+
export const BOOTSTRAP_MAX_DECODED_BYTES = PICKER_MAX_OUTPUT_BYTES;
|
|
20
21
|
const BOOTSTRAP_PATH = /^\/(?:edge-api|api)\/bootstrap(?:\/[A-Za-z0-9-]+\/app_start)?\/?$/;
|
|
21
22
|
const REWRITE_REMOVED_HEADERS = new Set([
|
|
22
23
|
"content-encoding", "content-length", "etag", "digest", "content-md5", "transfer-encoding",
|
|
@@ -56,11 +57,14 @@ export interface PickerInjectionOptions {
|
|
|
56
57
|
extraStrippedKeys?: readonly string[];
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
interface SurfaceInjection { entries: unknown[]; additions: Record<string, unknown>[] }
|
|
61
|
+
|
|
62
|
+
function planSurfaceInjection(
|
|
60
63
|
surface: Record<string, unknown>,
|
|
61
64
|
models: readonly PickerModelEntry[],
|
|
62
65
|
stripped: readonly string[],
|
|
63
|
-
|
|
66
|
+
budget: PickerRewriteBudget,
|
|
67
|
+
): SurfaceInjection | string {
|
|
64
68
|
if (!Array.isArray(surface.models)) return "no_models";
|
|
65
69
|
const entries = surface.models as unknown[];
|
|
66
70
|
const template = entries.map(record).find(entry =>
|
|
@@ -68,24 +72,28 @@ function injectIntoSurface(
|
|
|
68
72
|
&& !entry.disabled && !entry.disabled_reason && entry.section !== "deprecated");
|
|
69
73
|
if (!template) return `no_template(models=${entries.length})`;
|
|
70
74
|
const existing = new Set(entries.map(record).map(entry => entry?.id));
|
|
71
|
-
|
|
75
|
+
// Strip by shallow projection before measuring or cloning retained metadata. Native rows stay intact.
|
|
76
|
+
const retained = { ...template };
|
|
77
|
+
for (const key of Object.keys(retained)) {
|
|
78
|
+
if (stripped.includes(key) || /version/i.test(key)
|
|
79
|
+
|| ["id", "name", "section", "context_window"].includes(key)) delete retained[key];
|
|
80
|
+
}
|
|
81
|
+
pickerRowBytes(retained);
|
|
82
|
+
const additions: Record<string, unknown>[] = [];
|
|
72
83
|
for (const model of models) {
|
|
73
84
|
if (existing.has(model.id)) continue;
|
|
74
|
-
const copy =
|
|
85
|
+
const copy = { ...retained };
|
|
75
86
|
copy.id = model.id;
|
|
76
87
|
copy.name = model.name;
|
|
77
88
|
copy.section = "main";
|
|
78
89
|
if (model.contextWindow === undefined) delete copy.context_window;
|
|
79
90
|
else copy.context_window = model.contextWindow;
|
|
80
|
-
for (const key of Object.keys(copy)) {
|
|
81
|
-
if (stripped.includes(key) || /version/i.test(key)) delete copy[key];
|
|
82
|
-
}
|
|
83
91
|
if (model.description !== undefined) copy.description = model.description;
|
|
84
|
-
|
|
92
|
+
budget.reserveRow(copy);
|
|
93
|
+
additions.push(copy);
|
|
85
94
|
existing.add(model.id);
|
|
86
|
-
added++;
|
|
87
95
|
}
|
|
88
|
-
return
|
|
96
|
+
return { entries, additions };
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
export function injectPickerModels(
|
|
@@ -110,10 +118,21 @@ export function injectPickerModels(
|
|
|
110
118
|
}
|
|
111
119
|
let added = 0;
|
|
112
120
|
const skipped: string[] = [];
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
try {
|
|
122
|
+
const budget = new PickerRewriteBudget(bootstrap);
|
|
123
|
+
const plans: SurfaceInjection[] = [];
|
|
124
|
+
for (const surface of targets) {
|
|
125
|
+
const result = planSurfaceInjection(surface, models, stripped, budget);
|
|
126
|
+
if (typeof result === "string") skipped.push(`${String(surface.id)}:${result}`);
|
|
127
|
+
else { plans.push(result); added += result.additions.length; }
|
|
128
|
+
}
|
|
129
|
+
// All surfaces must fit before any deep clone or mutation; a refusal cannot publish a partial list.
|
|
130
|
+
const copies = plans.map(plan => plan.additions.map(row => structuredClone(row)));
|
|
131
|
+
for (let i = 0; i < plans.length; i++) {
|
|
132
|
+
for (const row of copies[i]!) plans[i]!.entries.push(row);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
return unchanged("rewrite_limit");
|
|
117
136
|
}
|
|
118
137
|
if (added === 0) return unchanged(skipped.length > 0 ? skipped.join(";") : models.length === 0 ? "no_routes" : "all_present");
|
|
119
138
|
explain?.({ kind: "rewritten", added });
|
|
@@ -142,7 +161,9 @@ export function rewriteBootstrapBody(
|
|
|
142
161
|
if (decoded.length > BOOTSTRAP_MAX_DECODED_BYTES) return unchanged("decoded_cap");
|
|
143
162
|
const parsed: unknown = JSON.parse(decoded.toString("utf8"));
|
|
144
163
|
if (injectPickerModels(parsed, models, explain) === 0) return null;
|
|
145
|
-
|
|
164
|
+
const text = JSON.stringify(parsed);
|
|
165
|
+
if (Buffer.byteLength(text) > BOOTSTRAP_MAX_DECODED_BYTES) return unchanged("rewrite_limit");
|
|
166
|
+
return Buffer.from(text, "utf8");
|
|
146
167
|
} catch {
|
|
147
168
|
return unchanged("decode_or_parse_failed");
|
|
148
169
|
}
|