@bitkyc08/opencodex 2.35.0 → 2.36.0-preview.20260830
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/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DPl4nBMA.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DNdRKXK9.js +0 -112
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DPl4nBMA.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cy7Z_pl0.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.36.0-preview.20260830",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"dev:gui": "cd gui && bun run dev",
|
|
40
40
|
"start": "bun run src/cli/index.ts start",
|
|
41
41
|
"test": "bun scripts/test.ts",
|
|
42
|
+
"test:changed": "bun scripts/test.ts --changed=dev",
|
|
42
43
|
"typecheck": "bun x tsc --noEmit",
|
|
43
44
|
"audit:high": "bun audit --audit-level=high && cd gui && bun audit --audit-level=high",
|
|
44
45
|
"privacy:scan": "bun scripts/privacy-scan.ts",
|
package/src/AGENTS.md
CHANGED
|
@@ -23,6 +23,7 @@ This file applies to `src/` and inherits the repository-wide rules in `/AGENTS.m
|
|
|
23
23
|
|
|
24
24
|
- Place focused regression coverage near the existing tests for the affected subsystem.
|
|
25
25
|
- For focused behavior, run the relevant `bun test tests/<name>.test.ts` and `bun run typecheck`.
|
|
26
|
-
-
|
|
26
|
+
- If the change set is broader than one file, run `bun run test:changed` instead of the full suite.
|
|
27
|
+
- Run `bun run test` only before marking a PR review-ready, or when the user explicitly asks for the full suite.
|
|
27
28
|
- For logging, requests, credentials, account data, or fixtures, also run `bun run privacy:scan`.
|
|
28
29
|
- Update `docs-site/` when the change affects user-visible behavior or configuration.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const AGENTROUTER_LANGUAGE_PREAMBLE =
|
|
2
|
+
"[Instruction: Process the user request below and respond in the appropriate language.]";
|
|
3
|
+
|
|
4
|
+
/** Match only AgentRouter itself or one of its subdomains, never a lookalike hostname. */
|
|
5
|
+
export function isAgentRouterEndpoint(baseUrl: string): boolean {
|
|
6
|
+
try {
|
|
7
|
+
const { hostname } = new URL(baseUrl);
|
|
8
|
+
return hostname === "agentrouter.org" || hostname.endsWith(".agentrouter.org");
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Supply AgentRouter's stable Codex admission identity unless the operator set one. */
|
|
15
|
+
export function agentRouterDefaultHeaders(
|
|
16
|
+
baseUrl: string,
|
|
17
|
+
configuredHeaders?: Record<string, string>,
|
|
18
|
+
): Record<string, string> {
|
|
19
|
+
if (!isAgentRouterEndpoint(baseUrl)) return {};
|
|
20
|
+
const hasOriginator = Object.keys(configuredHeaders ?? {}).some(name => name.toLowerCase() === "originator");
|
|
21
|
+
return hasOriginator ? {} : { originator: "codex_cli_rs" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Prepend the compatibility marker as a distinct block on the first user turn. */
|
|
25
|
+
export function applyAgentRouterLanguageFraming(messages: unknown[]): void {
|
|
26
|
+
const firstUser = messages.find(
|
|
27
|
+
(message): message is { role: string; content: unknown } =>
|
|
28
|
+
typeof message === "object" && message !== null && (message as { role?: unknown }).role === "user",
|
|
29
|
+
);
|
|
30
|
+
if (!firstUser) return;
|
|
31
|
+
const preamble = { type: "text", text: AGENTROUTER_LANGUAGE_PREAMBLE };
|
|
32
|
+
if (typeof firstUser.content === "string") {
|
|
33
|
+
firstUser.content = firstUser.content === ""
|
|
34
|
+
? [preamble]
|
|
35
|
+
: [preamble, { type: "text", text: firstUser.content }];
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(firstUser.content)) return;
|
|
39
|
+
const [head] = firstUser.content as { type?: unknown; text?: unknown }[];
|
|
40
|
+
if (head?.type === "text" && head.text === AGENTROUTER_LANGUAGE_PREAMBLE) return;
|
|
41
|
+
(firstUser.content as unknown[]).unshift(preamble);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Frame only an owned copy so translated and passthrough callers remain unchanged. */
|
|
45
|
+
export function frameAgentRouterMessages(baseUrl: string, messages: unknown): unknown {
|
|
46
|
+
if (!isAgentRouterEndpoint(baseUrl) || !Array.isArray(messages)) return messages;
|
|
47
|
+
const copy = structuredClone(messages) as unknown[];
|
|
48
|
+
applyAgentRouterLanguageFraming(copy);
|
|
49
|
+
return copy;
|
|
50
|
+
}
|
|
@@ -28,6 +28,7 @@ import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
|
28
28
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
29
29
|
import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget";
|
|
30
30
|
import { isReasoningEffortOmitted, modelRecordValue } from "../reasoning-effort";
|
|
31
|
+
import { applyAgentRouterLanguageFraming, isAgentRouterEndpoint } from "./agentrouter";
|
|
31
32
|
|
|
32
33
|
/** Map a user content part to an Anthropic content block (text or image source). */
|
|
33
34
|
function toAnthropicContentPart(p: OcxContentPart): unknown {
|
|
@@ -647,57 +648,6 @@ function orphanToolResultText(msg: OcxToolResultMessage): string {
|
|
|
647
648
|
* user content, so an Anthropic `system` string cannot reach it — the framing has to sit in the
|
|
648
649
|
* first user turn.
|
|
649
650
|
*/
|
|
650
|
-
const AGENTROUTER_LANGUAGE_PREAMBLE =
|
|
651
|
-
"[Instruction: Process the user request below and respond in the appropriate language.]";
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Exact host match, not a substring.
|
|
655
|
-
*
|
|
656
|
-
* A `hostname.includes("agentrouter")` test also matches `notagentrouter.example` and
|
|
657
|
-
* `agentrouter.org.attacker.example`, which would let an unrelated destination silently
|
|
658
|
-
* receive an injected instruction block. A prompt mutation keyed on a provider's identity
|
|
659
|
-
* must be keyed on that identity exactly.
|
|
660
|
-
*/
|
|
661
|
-
function isAgentRouterEndpoint(baseUrl: string): boolean {
|
|
662
|
-
try {
|
|
663
|
-
const { hostname } = new URL(baseUrl);
|
|
664
|
-
return hostname === "agentrouter.org" || hostname.endsWith(".agentrouter.org");
|
|
665
|
-
} catch {
|
|
666
|
-
return false;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Prepend the framing as its OWN text block instead of splicing it into the user's string.
|
|
672
|
-
*
|
|
673
|
-
* The distinction matters: rewriting `content` to `${marker}\n\n${original}` edits what the
|
|
674
|
-
* user wrote, and every downstream consumer — logs, retries, an upstream that echoes the turn —
|
|
675
|
-
* then sees a sentence the user never typed as if they had. A separate leading block carries the
|
|
676
|
-
* same signal to the filter while the original text survives byte-for-byte.
|
|
677
|
-
*
|
|
678
|
-
* Only the first user turn is framed, because only the first is what the gateway rejects.
|
|
679
|
-
*/
|
|
680
|
-
function applyAgentRouterLanguageFraming(messages: unknown[]): void {
|
|
681
|
-
const firstUser = messages.find(
|
|
682
|
-
(m): m is { role: string; content: unknown } =>
|
|
683
|
-
typeof m === "object" && m !== null && (m as { role?: unknown }).role === "user",
|
|
684
|
-
);
|
|
685
|
-
if (!firstUser) return;
|
|
686
|
-
const preamble = { type: "text", text: AGENTROUTER_LANGUAGE_PREAMBLE };
|
|
687
|
-
if (typeof firstUser.content === "string") {
|
|
688
|
-
firstUser.content = firstUser.content === ""
|
|
689
|
-
? [preamble]
|
|
690
|
-
: [preamble, { type: "text", text: firstUser.content }];
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
if (!Array.isArray(firstUser.content)) return;
|
|
694
|
-
// Idempotence is keyed on the LEADING block being exactly the marker. A substring test would
|
|
695
|
-
// let a user who quotes the marker later in their own prompt suppress the framing entirely.
|
|
696
|
-
const [head] = firstUser.content as { type?: unknown; text?: unknown }[];
|
|
697
|
-
if (head?.type === "text" && head.text === AGENTROUTER_LANGUAGE_PREAMBLE) return;
|
|
698
|
-
(firstUser.content as unknown[]).unshift(preamble);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
651
|
function messagesToAnthropicFormat(
|
|
702
652
|
parsed: OcxParsedRequest,
|
|
703
653
|
toolNames: { toWire: (name: string) => string },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Reversible single-line codec for Cursor composite tool-call ids.
|
|
3
5
|
*
|
|
@@ -5,22 +7,73 @@
|
|
|
5
7
|
* literal newline ("call-<uuid>-<n>\nfc_<uuid>_<n>"). OpenCodex forwards ids
|
|
6
8
|
* verbatim, so that newline leaked into Responses-visible `call_id` values,
|
|
7
9
|
* where line-oriented clients (logging, splitting, validation) break. The codec
|
|
8
|
-
* encodes
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* encodes ids containing CR/LF into a versioned single-line form. It also
|
|
11
|
+
* escapes ids already in that form's reserved namespace so encoding remains
|
|
12
|
+
* injective. Both forms decode back to the exact upstream bytes before
|
|
13
|
+
* anything is serialized toward Cursor. Every encoded value carries a keyed tag
|
|
14
|
+
* generated by this process. Prefix and base64 shape alone are never provenance:
|
|
15
|
+
* an opaque upstream id may equal any stateless encoder output.
|
|
16
|
+
*
|
|
17
|
+
* The process key is intentionally ephemeral. After restart, the decoder leaves
|
|
18
|
+
* values from the prior process unchanged rather than guessing from attacker- or
|
|
19
|
+
* provider-controlled text. This keeps provenance state constant-size instead of
|
|
20
|
+
* retaining an unbounded id map.
|
|
11
21
|
*/
|
|
12
22
|
|
|
13
23
|
const CALL_ID_PREFIX = "ocxc1_";
|
|
24
|
+
/** Escape namespace for ids that already sit in a reserved namespace. */
|
|
25
|
+
const CALL_ID_ESCAPE_PREFIX = "ocxc1e_";
|
|
26
|
+
const CALL_ID_PROVENANCE_DOMAIN = "opencodex:cursor-call-id:v1\0";
|
|
27
|
+
const CALL_ID_PROVENANCE_SEPARATOR = ".";
|
|
28
|
+
const CALL_ID_PROVENANCE_TAG_BYTES = 16;
|
|
29
|
+
let callIdProvenanceKey: Uint8Array = randomBytes(32);
|
|
14
30
|
|
|
15
31
|
/** True when the id needs encoding to survive line-oriented consumers. */
|
|
16
32
|
function needsEncoding(id: string): boolean {
|
|
17
33
|
return id.includes("\n") || id.includes("\r");
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
/** True when the id sits in a namespace this codec owns and must be escaped. */
|
|
37
|
+
function isReserved(id: string): boolean {
|
|
38
|
+
return id.startsWith(CALL_ID_PREFIX) || id.startsWith(CALL_ID_ESCAPE_PREFIX);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function provenanceTag(prefix: string, payload: string): Buffer {
|
|
42
|
+
return createHmac("sha256", callIdProvenanceKey)
|
|
43
|
+
.update(CALL_ID_PROVENANCE_DOMAIN)
|
|
44
|
+
.update(prefix)
|
|
45
|
+
.update(payload)
|
|
46
|
+
.digest()
|
|
47
|
+
.subarray(0, CALL_ID_PROVENANCE_TAG_BYTES);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function encodeWithProvenance(prefix: string, id: string): string {
|
|
51
|
+
const payload = Buffer.from(id, "utf8").toString("base64url");
|
|
52
|
+
const tag = provenanceTag(prefix, payload).toString("base64url");
|
|
53
|
+
return `${prefix}${payload}${CALL_ID_PROVENANCE_SEPARATOR}${tag}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasValidProvenance(prefix: string, payload: string, tag: string): boolean {
|
|
57
|
+
let received: Buffer;
|
|
58
|
+
try {
|
|
59
|
+
received = Buffer.from(tag, "base64url");
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
received.byteLength !== CALL_ID_PROVENANCE_TAG_BYTES
|
|
65
|
+
|| received.toString("base64url") !== tag
|
|
66
|
+
) return false;
|
|
67
|
+
return timingSafeEqual(received, provenanceTag(prefix, payload));
|
|
68
|
+
}
|
|
69
|
+
|
|
20
70
|
/** Encode a Cursor wire call id into a single-line Responses-safe id. */
|
|
21
71
|
export function encodeCursorCallId(id: string): string {
|
|
22
|
-
|
|
23
|
-
|
|
72
|
+
// CR/LF content is the codec's actual job, so it wins the primary namespace.
|
|
73
|
+
if (needsEncoding(id)) return encodeWithProvenance(CALL_ID_PREFIX, id);
|
|
74
|
+
// A reserved id carries no newline; it only needs to stop looking like our output.
|
|
75
|
+
if (isReserved(id)) return encodeWithProvenance(CALL_ID_ESCAPE_PREFIX, id);
|
|
76
|
+
return id;
|
|
24
77
|
}
|
|
25
78
|
|
|
26
79
|
/**
|
|
@@ -30,15 +83,30 @@ export function encodeCursorCallId(id: string): string {
|
|
|
30
83
|
* through rather than corrupting pairing.
|
|
31
84
|
*/
|
|
32
85
|
export function decodeCursorCallId(id: string): string {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
const escaped = id.startsWith(CALL_ID_ESCAPE_PREFIX);
|
|
87
|
+
if (!escaped && !id.startsWith(CALL_ID_PREFIX)) return id;
|
|
88
|
+
const prefix = escaped ? CALL_ID_ESCAPE_PREFIX : CALL_ID_PREFIX;
|
|
89
|
+
const encoded = id.slice(prefix.length);
|
|
90
|
+
const separator = encoded.indexOf(CALL_ID_PROVENANCE_SEPARATOR);
|
|
91
|
+
if (separator <= 0 || separator !== encoded.lastIndexOf(CALL_ID_PROVENANCE_SEPARATOR)) return id;
|
|
92
|
+
const payload = encoded.slice(0, separator);
|
|
93
|
+
const tag = encoded.slice(separator + CALL_ID_PROVENANCE_SEPARATOR.length);
|
|
94
|
+
if (!hasValidProvenance(prefix, payload, tag)) return id;
|
|
36
95
|
try {
|
|
37
96
|
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
|
38
97
|
// Round-trip guard: only trust payloads our encoder could have produced.
|
|
39
98
|
if (Buffer.from(decoded, "utf8").toString("base64url") !== payload) return id;
|
|
99
|
+
// Each namespace admits exactly what its encoder puts there. An `ocxc1_` payload
|
|
100
|
+
// that decodes to newline-free text is NOT our output — it is an opaque upstream
|
|
101
|
+
// id that merely looks like ours, and unwrapping it would change the id.
|
|
102
|
+
if (escaped ? !isReserved(decoded) : !needsEncoding(decoded)) return id;
|
|
40
103
|
return decoded;
|
|
41
104
|
} catch {
|
|
42
105
|
return id;
|
|
43
106
|
}
|
|
44
107
|
}
|
|
108
|
+
|
|
109
|
+
/** Simulates process restart without reloading the module. */
|
|
110
|
+
export function resetCursorCallIdProvenanceForTests(): void {
|
|
111
|
+
callIdProvenanceKey = randomBytes(32);
|
|
112
|
+
}
|
|
@@ -23,7 +23,12 @@ export type CursorCheckpointInvalidationReason =
|
|
|
23
23
|
| "trailing_tool_result"
|
|
24
24
|
| "force_fresh"
|
|
25
25
|
| "upstream_invalid_argument"
|
|
26
|
-
| "lineage_mismatch"
|
|
26
|
+
| "lineage_mismatch"
|
|
27
|
+
/**
|
|
28
|
+
* The checkpoint's own roots leave no room for the uncovered suffix inside Cursor's root envelope.
|
|
29
|
+
* Resuming would send history the model cannot see; a full replay prunes coherently instead.
|
|
30
|
+
*/
|
|
31
|
+
| "envelope_exhausted";
|
|
27
32
|
|
|
28
33
|
export interface CursorCheckpointSnapshot {
|
|
29
34
|
ref: string;
|
|
@@ -71,6 +71,50 @@ export class CursorUnexpectedCancelError extends Error {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The assembled root-blob envelope exceeded what Cursor's external workers accept.
|
|
76
|
+
*
|
|
77
|
+
* Raised locally, BEFORE the request is sent. Cursor rejects an oversized replay set with a late
|
|
78
|
+
* `invalid_argument` only after hydrating every blob, so the upstream failure arrives with no
|
|
79
|
+
* usable measurement — that is why this carries the counts it measured. Never retryable: replaying
|
|
80
|
+
* the same over-envelope request reproduces it exactly.
|
|
81
|
+
*
|
|
82
|
+
* The measurement is taken on the FINAL root set, after checkpoint and suffix assembly. Bounding
|
|
83
|
+
* an intermediate set is what let 192 checkpoint roots plus a two-root suffix emit 194 (#1527).
|
|
84
|
+
*/
|
|
85
|
+
export class CursorRootEnvelopeLimitError extends Error {
|
|
86
|
+
public readonly code = "cursor_root_envelope_limit";
|
|
87
|
+
public readonly status = 400;
|
|
88
|
+
|
|
89
|
+
constructor(
|
|
90
|
+
public readonly rootCount: number,
|
|
91
|
+
public readonly rootBytes: number,
|
|
92
|
+
public readonly maxRootCount: number,
|
|
93
|
+
public readonly maxRootBytes: number,
|
|
94
|
+
) {
|
|
95
|
+
super(
|
|
96
|
+
// No "Cursor invalid request:" prefix here: `safeCursorErrorMessage` adds it for
|
|
97
|
+
// invalid-argument classes, and carrying it in the message produced it twice.
|
|
98
|
+
"the assembled conversation exceeds the replay envelope "
|
|
99
|
+
+ `(${rootCount} root blobs / ${rootBytes} bytes against a limit of ${maxRootCount} / ${maxRootBytes}). `
|
|
100
|
+
+ "Start a new conversation or reduce the pending tool output.",
|
|
101
|
+
);
|
|
102
|
+
this.name = "CursorRootEnvelopeLimitError";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The envelope failure is local and deterministic; a retry cannot change the outcome.
|
|
108
|
+
*
|
|
109
|
+
* A companion `CursorRootMeasurementError` was drafted for the case where a root blob's size
|
|
110
|
+
* cannot be read, then deleted: an unmeasurable root is legitimate (a resumed conversation
|
|
111
|
+
* references ids Cursor minted and this process never stored), so the guard counts it instead of
|
|
112
|
+
* failing. There is no reachable second case, and an unreachable error class cannot be tested.
|
|
113
|
+
*/
|
|
114
|
+
export function isCursorRootEnvelopeError(value: unknown): boolean {
|
|
115
|
+
return value instanceof CursorRootEnvelopeLimitError;
|
|
116
|
+
}
|
|
117
|
+
|
|
74
118
|
export function isCursorBenignCancelError(value: unknown): boolean {
|
|
75
119
|
// An unexpected cancel is never benign, however it is spelled. This class is raised only when
|
|
76
120
|
// the transport knows WE did not request the cancel, so its provenance outranks the code match
|
|
@@ -403,6 +403,19 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque
|
|
|
403
403
|
return blobId;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Stored byte length of one blob, or null when it is not in the store.
|
|
408
|
+
*
|
|
409
|
+
* Size only, never content: the envelope guard needs to measure the FINAL root set, which mixes
|
|
410
|
+
* roots minted this turn with roots carried inside a checkpoint. Reading them back through a
|
|
411
|
+
* hydration path would both defeat the request-scope sealing and log served bytes for a request
|
|
412
|
+
* that may never be sent.
|
|
413
|
+
*/
|
|
414
|
+
export function cursorBlobByteLength(blobId: Uint8Array): number | null {
|
|
415
|
+
const entry = blobs.get(key(blobId));
|
|
416
|
+
return entry ? entry.data.byteLength : null;
|
|
417
|
+
}
|
|
418
|
+
|
|
406
419
|
/**
|
|
407
420
|
* Serve-time integrity for content-addressed blobs (devlog 260826_cursor_responses_gap 080):
|
|
408
421
|
* a raw 32-byte blob id IS the SHA-256 of its bytes, so served data whose digest mismatches
|