@bitkyc08/opencodex 2.10.1 → 2.10.2
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 +18 -9
- package/gui/dist/assets/index-BKVqyYqT.js +70 -0
- package/gui/dist/assets/{index-Cd6_PBKn.css → index-Ca_3269W.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/package.json +1 -1
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/google.ts +3 -0
- package/src/cli/claude.ts +37 -22
- package/src/cli/index.ts +16 -3
- package/src/cli/launcher-context.ts +77 -0
- package/src/codex/admission.ts +1 -1
- package/src/codex/app-server-processes.ts +44 -1
- package/src/codex/catalog/sync.ts +22 -3
- package/src/codex/catalog-write-serialization.ts +1 -1
- package/src/codex/codex-write-lock.ts +1 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/desired-state.ts +27 -7
- package/src/codex/history-job.ts +1 -1
- package/src/codex/history-lock.ts +1 -1
- package/src/codex/history-worker.ts +1 -1
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/transition-state.ts +1 -1
- package/src/codex/user-identity.ts +1 -1
- package/src/config.ts +6 -1
- package/src/integrations/config-io.ts +1 -1
- package/src/integrations/journal.ts +1 -1
- package/src/integrations/merge.ts +1 -1
- package/src/integrations/ownership.ts +1 -1
- package/src/integrations/registry.ts +1 -1
- package/src/integrations/serialize.ts +1 -1
- package/src/integrations/state.ts +1 -1
- package/src/integrations/store.ts +1 -1
- package/src/integrations/writer.ts +1 -1
- package/src/lib/bounded-body.ts +3 -1
- package/src/lib/bun-runtime.ts +21 -17
- package/src/lib/bun-stream-caps.ts +1 -1
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/shadow-call.ts +4 -4
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/health.ts +46 -2
- package/src/oauth/index.ts +38 -3
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/registry.ts +138 -26
- package/src/routing/capability.ts +1 -0
- package/src/server/adapter-resolve.ts +3 -0
- package/src/server/auth-cors.ts +6 -1
- package/src/server/index.ts +44 -2
- package/src/server/management/integration-routes.ts +1 -1
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/responses/core.ts +13 -9
- package/src/storage/scanner.ts +1 -1
- package/src/types.ts +6 -0
- package/src/usage/log.ts +1 -1
- package/gui/dist/assets/index-ChZQsmBY.js +0 -70
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-BKVqyYqT.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Ca_3269W.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Command Code</title><g fill="#8C4EDD" transform="translate(1.5,1.5) scale(0.237639) translate(-27.6163,-27.6163)"><path fill-rule="evenodd" clip-rule="evenodd" d="M98.8049 27.6163C89.3295 27.6163 81.6214 35.3243 81.6214 44.7997V52.1641H61.9832V44.7997C61.9832 35.3243 54.2752 27.6163 44.7997 27.6163C35.3243 27.6163 27.6163 35.3243 27.6163 44.7997C27.6163 54.2752 35.3243 61.9832 44.7997 61.9832H52.1641V81.6214H44.7997C35.3243 81.6214 27.6163 89.3295 27.6163 98.8049C27.6163 108.28 35.3243 115.988 44.7997 115.988C54.2752 115.988 61.9832 108.28 61.9832 98.8049V91.4406H81.6214V98.8049C81.6214 108.28 89.3295 115.988 98.8049 115.988C108.28 115.988 115.988 108.28 115.988 98.8049C115.988 89.3295 108.28 81.6214 98.8049 81.6214H91.4406V61.9832H98.8049C108.28 61.9832 115.988 54.2752 115.988 44.7997C115.988 35.3243 108.28 27.6163 98.8049 27.6163ZM91.4406 52.1641V44.7997C91.4406 40.7248 94.73 37.4354 98.8049 37.4354C102.88 37.4354 106.169 40.7248 106.169 44.7997C106.169 48.8747 102.88 52.1641 98.8049 52.1641H91.4406ZM44.7997 52.1641C40.7248 52.1641 37.4354 48.8747 37.4354 44.7997C37.4354 40.7248 40.7248 37.4354 44.7997 37.4354C48.8747 37.4354 52.1641 40.7248 52.1641 44.7997V52.1641H44.7997ZM61.9832 81.6214V61.9832H81.6214V81.6214H61.9832ZM98.8049 106.169C94.73 106.169 91.4406 102.88 91.4406 98.8049V91.4406H98.8049C102.88 91.4406 106.169 94.73 106.169 98.8049C106.169 102.88 102.88 106.169 98.8049 106.169ZM44.7997 106.169C40.7248 106.169 37.4354 102.88 37.4354 98.8049C37.4354 94.73 40.7248 91.4406 44.7997 91.4406H52.1641V98.8049C52.1641 102.88 48.8747 106.169 44.7997 106.169Z"/></g></svg>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg fill="#
|
|
1
|
+
<svg fill="#10A37F" fill-rule="evenodd" style="flex:none;line-height:1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"></path></svg>
|
package/package.json
CHANGED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { opendir } from "node:fs/promises";
|
|
5
|
+
import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types";
|
|
6
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice, toolChoiceAliases } from "../types";
|
|
7
|
+
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
8
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
9
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
10
|
+
import { configuredReasoningEfforts } from "../reasoning-effort";
|
|
11
|
+
import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
|
|
12
|
+
import { identifyRoutedModel } from "./identity";
|
|
13
|
+
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
14
|
+
import { parseDataUrl } from "./image";
|
|
15
|
+
|
|
16
|
+
// Retain the short ids emitted by the first local integration. New requests use the live catalog's
|
|
17
|
+
// provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
|
|
18
|
+
const COMMAND_CODE_MODEL_ALIASES: Readonly<Record<string, string>> = {
|
|
19
|
+
"deepseek-v4-flash": "deepseek/deepseek-v4-flash",
|
|
20
|
+
"kimi-k3": "moonshotai/Kimi-K3",
|
|
21
|
+
"glm-5.2": "zai-org/GLM-5.2",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Flatten tool-result content for the text-only wire output, keeping an `[image]` marker per image part in content order. */
|
|
25
|
+
function toolResultText(content: string | OcxContentPart[]): string {
|
|
26
|
+
if (typeof content === "string") return content;
|
|
27
|
+
return content.map(part => (part.type === "text" ? part.text : "[image]")).join("");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Best-effort media type from a remote https URL extension, e.g. image/png. */
|
|
31
|
+
function mediaTypeFromUrl(url: string): string | undefined {
|
|
32
|
+
const ext = url.split(/[?#]/)[0]!.match(/\.([a-zA-Z0-9]+)$/)?.[1]?.toLowerCase();
|
|
33
|
+
if (!ext) return undefined;
|
|
34
|
+
const known: Record<string, string> = {
|
|
35
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", bmp: "image/bmp",
|
|
36
|
+
};
|
|
37
|
+
return known[ext];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wire image part: `{ type, image, mediaType? }` per the /alpha/generate ModelMessage schema. */
|
|
41
|
+
function wireImagePart(imageUrl: string): Record<string, unknown> {
|
|
42
|
+
const parsed = parseDataUrl(imageUrl);
|
|
43
|
+
const mediaType = parsed?.mediaType ?? mediaTypeFromUrl(imageUrl);
|
|
44
|
+
return { type: "image", image: imageUrl, ...(mediaType ? { mediaType } : {}) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
|
|
48
|
+
const out: Array<Record<string, unknown>> = [];
|
|
49
|
+
for (const message of messages) {
|
|
50
|
+
if (message.role === "assistant") {
|
|
51
|
+
const content: Array<Record<string, unknown>> = [];
|
|
52
|
+
for (const part of message.content) {
|
|
53
|
+
if (part.type === "text") content.push({ type: "text", text: part.text });
|
|
54
|
+
else if (part.type === "thinking") content.push({ type: "reasoning", text: part.thinking });
|
|
55
|
+
else content.push({ type: "tool-call", toolCallId: part.id, toolName: namespacedToolName(part.namespace, part.name), input: part.arguments });
|
|
56
|
+
}
|
|
57
|
+
out.push({ role: "assistant", content });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (message.role === "toolResult") {
|
|
61
|
+
out.push({ role: "tool", content: [{
|
|
62
|
+
type: "tool-result",
|
|
63
|
+
toolCallId: message.toolCallId,
|
|
64
|
+
toolName: namespacedToolName(message.toolNamespace, message.toolName),
|
|
65
|
+
output: { type: message.isError ? "error-text" : "text", value: toolResultText(message.content) },
|
|
66
|
+
}] });
|
|
67
|
+
// The proprietary wire's tool-result output is text-only; image parts returned by a
|
|
68
|
+
// tool (e.g. Codex view_image) cannot live inside it. Carry them in a follow-up user
|
|
69
|
+
// message using the same image encoding as the user branch so the bytes reach the model.
|
|
70
|
+
const images = typeof message.content === "string" ? [] : message.content.filter(part => part.type === "image");
|
|
71
|
+
if (images.length > 0) {
|
|
72
|
+
out.push({ role: "user", content: images.map(part => wireImagePart((part as { imageUrl: string }).imageUrl)) });
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const content: Array<Record<string, unknown>> = [];
|
|
77
|
+
if (typeof message.content === "string") content.push({ type: "text", text: message.content });
|
|
78
|
+
else for (const part of message.content) {
|
|
79
|
+
if (part.type === "text") content.push({ type: "text", text: part.text });
|
|
80
|
+
else content.push(wireImagePart(part.imageUrl));
|
|
81
|
+
}
|
|
82
|
+
out.push({ role: "user", content });
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
|
|
88
|
+
const choice = parsed.options.toolChoice;
|
|
89
|
+
if (choice === "none") return [];
|
|
90
|
+
const tools = parsed.context.tools ?? [];
|
|
91
|
+
if (isAllowedToolChoice(choice)) {
|
|
92
|
+
const allowed = new Set(choice.allowedTools);
|
|
93
|
+
return tools.filter(tool => toolAllowedByChoice(tool, allowed));
|
|
94
|
+
}
|
|
95
|
+
if (choice && typeof choice !== "string") {
|
|
96
|
+
return tools.filter(tool => toolChoiceAliases(tool).includes(choice.name));
|
|
97
|
+
}
|
|
98
|
+
return tools;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toolChoiceInstruction(parsed: OcxParsedRequest): string | undefined {
|
|
102
|
+
const choice = parsed.options.toolChoice;
|
|
103
|
+
if (choice === "required" || (isAllowedToolChoice(choice) && choice.mode === "required")) {
|
|
104
|
+
return "Tool choice is required for this turn. Make at least one call from the advertised tool catalog before answering.";
|
|
105
|
+
}
|
|
106
|
+
if (choice && typeof choice !== "string" && !isAllowedToolChoice(choice)) {
|
|
107
|
+
// Resolve the forced name (bare, namespace__name, or namespace.name) to the advertised
|
|
108
|
+
// wire name so the instruction names a tool that is actually in the catalog.
|
|
109
|
+
const wireName = resolveToolChoiceWireName(parsed.context.tools, choice.name);
|
|
110
|
+
return `Tool choice is required for this turn. Call the advertised tool named ${wireName} before answering.`;
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function wireTools(tools: OcxTool[]): Array<Record<string, unknown>> {
|
|
116
|
+
return tools.map(tool => ({
|
|
117
|
+
name: namespacedToolName(tool.namespace, tool.name),
|
|
118
|
+
description: tool.description,
|
|
119
|
+
input_schema: tool.parameters,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function currentWorkingDirectory(): string | undefined {
|
|
124
|
+
try { return process.cwd(); } catch { return undefined; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Cap the workspace listing so a large directory does not ship every entry name upstream. */
|
|
128
|
+
const MAX_WORKSPACE_STRUCTURE_ENTRIES = 64;
|
|
129
|
+
/** Cap how many recent commit subjects the config carries. */
|
|
130
|
+
const MAX_RECENT_COMMITS = 8;
|
|
131
|
+
/** Cap each recent commit entry to keep the request bounded even for long subjects. */
|
|
132
|
+
const MAX_RECENT_COMMIT_LENGTH = 512;
|
|
133
|
+
/** Cap the git status text sent upstream. */
|
|
134
|
+
const MAX_GIT_STATUS_LENGTH = 2048;
|
|
135
|
+
/** Keep collected workspace/git metadata fresh for this long (ms) so repeated requests reuse it. */
|
|
136
|
+
const WORKSPACE_METADATA_TTL_MS = 30_000;
|
|
137
|
+
|
|
138
|
+
/** Derive a bounded project slug from the working directory for the `x-project-slug` header. */
|
|
139
|
+
function projectSlug(cwd: string): string {
|
|
140
|
+
return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase().slice(0, 64) || "workspace";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface GitWorkspaceInfo {
|
|
144
|
+
isGitRepo: boolean;
|
|
145
|
+
currentBranch: string;
|
|
146
|
+
mainBranch: string;
|
|
147
|
+
gitStatus: string;
|
|
148
|
+
recentCommits: string[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const workspaceMetadataCache = new Map<string, { collectedAt: number; value: GitWorkspaceInfo }>();
|
|
152
|
+
|
|
153
|
+
const execFile = promisify(execFileCallback);
|
|
154
|
+
|
|
155
|
+
/** Best-effort git metadata for the upstream config contract; every read fails safe and stays off the event loop. */
|
|
156
|
+
async function gitWorkspaceInfo(cwd: string | undefined): Promise<GitWorkspaceInfo> {
|
|
157
|
+
const fallback: GitWorkspaceInfo = { isGitRepo: false, currentBranch: "", mainBranch: "", gitStatus: "", recentCommits: [] };
|
|
158
|
+
if (!cwd) return fallback;
|
|
159
|
+
const cached = workspaceMetadataCache.get(cwd);
|
|
160
|
+
if (cached && Date.now() - cached.collectedAt < WORKSPACE_METADATA_TTL_MS) return cached.value;
|
|
161
|
+
const run = async (args: string[]): Promise<string> => {
|
|
162
|
+
try {
|
|
163
|
+
const { stdout } = await execFile("git", args, { cwd, encoding: "utf8", timeout: 2000, windowsHide: true });
|
|
164
|
+
return stdout.trim();
|
|
165
|
+
} catch {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const root = await run(["rev-parse", "--show-toplevel"]);
|
|
170
|
+
const value = root
|
|
171
|
+
? {
|
|
172
|
+
isGitRepo: true,
|
|
173
|
+
currentBranch: (await run(["rev-parse", "--abbrev-ref", "HEAD"])) || "HEAD",
|
|
174
|
+
mainBranch: (await run(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, "") || (await run(["rev-parse", "--abbrev-ref", "HEAD"])) || "",
|
|
175
|
+
gitStatus: (await run(["status", "--porcelain"])).slice(0, MAX_GIT_STATUS_LENGTH),
|
|
176
|
+
recentCommits: (await run(["log", "--oneline", `-${MAX_RECENT_COMMITS}`]))
|
|
177
|
+
.split("\n")
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.slice(0, MAX_RECENT_COMMITS)
|
|
180
|
+
.map(commit => commit.slice(0, MAX_RECENT_COMMIT_LENGTH)),
|
|
181
|
+
}
|
|
182
|
+
: fallback;
|
|
183
|
+
workspaceMetadataCache.set(cwd, { collectedAt: Date.now(), value });
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function commandCodeConfig(cwd: string | undefined): Promise<Record<string, unknown>> {
|
|
188
|
+
let structure: string[] = [];
|
|
189
|
+
if (cwd) {
|
|
190
|
+
try {
|
|
191
|
+
// Iterate and stop after the cap instead of materializing every entry: a directory with a
|
|
192
|
+
// huge number of names must not stall the request path for 64 metadata rows.
|
|
193
|
+
const dir = await opendir(cwd);
|
|
194
|
+
try {
|
|
195
|
+
for await (const entry of dir) {
|
|
196
|
+
if (entry.name.startsWith(".")) continue;
|
|
197
|
+
structure.push(entry.name);
|
|
198
|
+
if (structure.length >= MAX_WORKSPACE_STRUCTURE_ENTRIES) break;
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
await dir.close().catch(() => undefined);
|
|
202
|
+
}
|
|
203
|
+
} catch { /* workspace metadata is optional */ }
|
|
204
|
+
}
|
|
205
|
+
const git = await gitWorkspaceInfo(cwd);
|
|
206
|
+
return {
|
|
207
|
+
...(cwd ? { workingDir: cwd } : {}),
|
|
208
|
+
date: new Date().toISOString().slice(0, 10),
|
|
209
|
+
environment: process.platform,
|
|
210
|
+
structure,
|
|
211
|
+
...git,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function usage(value: unknown): OcxUsage | undefined {
|
|
216
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
217
|
+
const row = value as Record<string, unknown>;
|
|
218
|
+
const inputTokens = typeof row.inputTokens === "number" ? row.inputTokens : 0;
|
|
219
|
+
const outputTokens = typeof row.outputTokens === "number" ? row.outputTokens : 0;
|
|
220
|
+
const details = row.inputTokenDetails && typeof row.inputTokenDetails === "object" && !Array.isArray(row.inputTokenDetails)
|
|
221
|
+
? row.inputTokenDetails as Record<string, unknown> : {};
|
|
222
|
+
const cachedInputTokens = typeof details.cacheReadTokens === "number" ? details.cacheReadTokens : undefined;
|
|
223
|
+
const cacheCreationInputTokens = typeof details.cacheWriteTokens === "number" ? details.cacheWriteTokens : undefined;
|
|
224
|
+
return {
|
|
225
|
+
inputTokens, outputTokens, totalTokens: inputTokens + outputTokens,
|
|
226
|
+
...(cachedInputTokens !== undefined ? { cachedInputTokens, cacheReadInputTokens: cachedInputTokens } : {}),
|
|
227
|
+
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function eventError(value: unknown): string {
|
|
232
|
+
if (typeof value === "string" && value) return value;
|
|
233
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
234
|
+
const message = (value as Record<string, unknown>).message;
|
|
235
|
+
if (typeof message === "string" && message) return message;
|
|
236
|
+
}
|
|
237
|
+
return "Command Code stream error";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenerator<Record<string, unknown>> {
|
|
241
|
+
if (!response.body) throw new Error("Command Code response body missing");
|
|
242
|
+
const reader = response.body.getReader();
|
|
243
|
+
const decoder = new TextDecoder();
|
|
244
|
+
const encoder = new TextEncoder();
|
|
245
|
+
let buffer = "";
|
|
246
|
+
let bufferBytes = 0;
|
|
247
|
+
try {
|
|
248
|
+
for (;;) {
|
|
249
|
+
const { value, done } = await reader.read();
|
|
250
|
+
const next = buffer + decoder.decode(value, { stream: !done });
|
|
251
|
+
const nextBytes = encoder.encode(next).byteLength;
|
|
252
|
+
const reservation = budget.reserveTransient(nextBytes, { kind: "live_transient" });
|
|
253
|
+
buffer = next;
|
|
254
|
+
reservation.commitRetained();
|
|
255
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
256
|
+
bufferBytes = nextBytes;
|
|
257
|
+
let newline = buffer.indexOf("\n");
|
|
258
|
+
while (newline >= 0) {
|
|
259
|
+
const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1);
|
|
260
|
+
if (line) { try { yield JSON.parse(stripEventFrame(line)) as Record<string, unknown>; } catch { /* ignore non-events */ } }
|
|
261
|
+
newline = buffer.indexOf("\n");
|
|
262
|
+
}
|
|
263
|
+
const residualBytes = encoder.encode(buffer).byteLength;
|
|
264
|
+
const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" });
|
|
265
|
+
residualReservation.commitRetained();
|
|
266
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
267
|
+
bufferBytes = residualBytes;
|
|
268
|
+
if (done) break;
|
|
269
|
+
}
|
|
270
|
+
const final = buffer.trim();
|
|
271
|
+
if (final) { try { yield JSON.parse(stripEventFrame(final)) as Record<string, unknown>; } catch { /* ignore */ } }
|
|
272
|
+
} finally {
|
|
273
|
+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
274
|
+
try { await reader.cancel(); } catch { /* already closed */ }
|
|
275
|
+
reader.releaseLock();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The endpoint is newline-delimited JSON; defensively strip an SSE `data:` frame if the gateway ever switches shapes. */
|
|
280
|
+
function stripEventFrame(line: string): string {
|
|
281
|
+
return line.startsWith("data:") ? line.slice("data:".length).trim() : line;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isReasoningEffortRejection(status: number, payload: string): boolean {
|
|
285
|
+
return (status === 400 || status === 422) && /reasoning[_ -]?effort|unsupported effort|invalid effort/i.test(payload);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function requestWithoutReasoningEffort(request: AdapterRequest): AdapterRequest | undefined {
|
|
289
|
+
try {
|
|
290
|
+
const body = JSON.parse(request.body) as { params?: Record<string, unknown> };
|
|
291
|
+
if (!body.params?.reasoning_effort) return undefined;
|
|
292
|
+
delete body.params.reasoning_effort;
|
|
293
|
+
return { ...request, body: JSON.stringify(body), reasoningLog: undefined };
|
|
294
|
+
} catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContext | undefined, executor: typeof globalThis.fetch): Promise<Response> {
|
|
300
|
+
const timeout = new AbortController();
|
|
301
|
+
const timer = setTimeout(() => timeout.abort(new DOMException("Timeout elapsed", "TimeoutError")), ctx?.timeoutMs ?? 200_000);
|
|
302
|
+
const callerSignal = ctx?.abortSignal ?? new AbortController().signal;
|
|
303
|
+
try {
|
|
304
|
+
return await executor(request.url, {
|
|
305
|
+
method: request.method,
|
|
306
|
+
headers: request.headers,
|
|
307
|
+
body: request.body,
|
|
308
|
+
redirect: "manual",
|
|
309
|
+
signal: AbortSignal.any([callerSignal, timeout.signal]),
|
|
310
|
+
});
|
|
311
|
+
} finally {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined {
|
|
317
|
+
if (!requested || requested === "none") return undefined;
|
|
318
|
+
// Compatibility ids (deepseek-v4-flash / glm-5.2) must resolve to their canonical
|
|
319
|
+
// Command Code id before the effort lookup, or legacy requests silently lose the
|
|
320
|
+
// reasoning effort because the official table is keyed by the canonical ids.
|
|
321
|
+
const canonicalId = COMMAND_CODE_MODEL_ALIASES[modelId] ?? modelId;
|
|
322
|
+
const supported = commandCodeReasoningEfforts(canonicalId) ?? configuredReasoningEfforts(provider, canonicalId);
|
|
323
|
+
if (!supported) return undefined;
|
|
324
|
+
// Command Code's official profiles describe xhigh and ultra as the CLI labels that map to
|
|
325
|
+
// the wire value `max`; preserve that mapping without advertising a synthetic tier.
|
|
326
|
+
const wire = (requested === "xhigh" || requested === "ultra") && supported.includes("max") ? "max" : requested;
|
|
327
|
+
return supported.includes(wire) ? wire : undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
331
|
+
const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
|
|
332
|
+
return {
|
|
333
|
+
name: "command-code",
|
|
334
|
+
async buildRequest(parsed: OcxParsedRequest): Promise<AdapterRequest> {
|
|
335
|
+
if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code");
|
|
336
|
+
const cwd = currentWorkingDirectory();
|
|
337
|
+
const tools = visibleTools(parsed);
|
|
338
|
+
const toolNudge = buildNonOpenAIToolCatalogNudgeForTools(tools, parsed.options.toolChoice);
|
|
339
|
+
const choiceInstruction = toolChoiceInstruction(parsed);
|
|
340
|
+
const system = identifyRoutedModel([
|
|
341
|
+
...(parsed.context.systemPrompt ?? []),
|
|
342
|
+
...(toolNudge ? [toolNudge] : []),
|
|
343
|
+
...(choiceInstruction ? [choiceInstruction] : []),
|
|
344
|
+
].join("\n\n"), parsed.modelId);
|
|
345
|
+
const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
346
|
+
const body = {
|
|
347
|
+
config: await commandCodeConfig(cwd), memory: "", taste: null, skills: null,
|
|
348
|
+
permissionMode: "standard", mode: "agent",
|
|
349
|
+
params: {
|
|
350
|
+
model: COMMAND_CODE_MODEL_ALIASES[parsed.modelId] ?? parsed.modelId,
|
|
351
|
+
messages: wireMessages(parsed.context.messages),
|
|
352
|
+
tools: wireTools(tools),
|
|
353
|
+
system,
|
|
354
|
+
max_tokens: parsed.options.maxOutputTokens ?? provider.defaultMaxOutputTokens ?? 64_000,
|
|
355
|
+
// The proprietary /alpha/generate endpoint is NDJSON-stream-only; the proxy converts
|
|
356
|
+
// the buffered/streamed events to the client's requested shape (parsed.stream).
|
|
357
|
+
stream: true,
|
|
358
|
+
...(parsed.options.temperature !== undefined ? { temperature: parsed.options.temperature } : {}),
|
|
359
|
+
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
const headers: Record<string, string> = {
|
|
363
|
+
Authorization: `Bearer ${provider.apiKey}`,
|
|
364
|
+
"Content-Type": "application/json",
|
|
365
|
+
"User-Agent": "cli",
|
|
366
|
+
"x-command-code-version": provider.commandCodeVersion ?? "0.52.1",
|
|
367
|
+
"x-cli-environment": "production",
|
|
368
|
+
"x-taste-learning": "false",
|
|
369
|
+
"x-co-flag": "false",
|
|
370
|
+
"x-session-id": randomUUID(),
|
|
371
|
+
};
|
|
372
|
+
if (cwd) headers["x-project-slug"] = projectSlug(cwd);
|
|
373
|
+
return {
|
|
374
|
+
url: `${provider.baseUrl.replace(/\/$/, "")}/alpha/generate`, method: "POST",
|
|
375
|
+
headers,
|
|
376
|
+
body: JSON.stringify(body),
|
|
377
|
+
...(reasoningEffort ? { reasoningLog: { effectiveEffort: reasoningEffort, wireField: "reasoning_effort" as const, wireValue: reasoningEffort } } : {}),
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
async fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
|
|
381
|
+
const response = await fetchCommandCode(request, ctx, executor);
|
|
382
|
+
if (response.ok) return response;
|
|
383
|
+
const currentEffort = (() => {
|
|
384
|
+
try { return (JSON.parse(request.body) as { params?: { reasoning_effort?: unknown } }).params?.reasoning_effort; } catch { return undefined; }
|
|
385
|
+
})();
|
|
386
|
+
if (typeof currentEffort !== "string") return response;
|
|
387
|
+
let body = "";
|
|
388
|
+
try {
|
|
389
|
+
const observed = await readBoundedResponseBody(response.clone(), { signal: ctx?.abortSignal, maxBytes: 8 * 1024 });
|
|
390
|
+
if (!observed.displaySafe) return response;
|
|
391
|
+
body = observed.text;
|
|
392
|
+
} catch { return response; }
|
|
393
|
+
if (!isReasoningEffortRejection(response.status, body)) return response;
|
|
394
|
+
const modelId = (() => {
|
|
395
|
+
try { return (JSON.parse(request.body) as { params?: { model?: unknown } }).params?.model; } catch { return undefined; }
|
|
396
|
+
})();
|
|
397
|
+
if (typeof modelId !== "string") return response;
|
|
398
|
+
const refreshed = await refreshCommandCodeReasoningEfforts(modelId, executor);
|
|
399
|
+
if (!refreshed || refreshed.includes(currentEffort)) return response;
|
|
400
|
+
const retry = requestWithoutReasoningEffort(request);
|
|
401
|
+
if (!retry) return response;
|
|
402
|
+
try { void response.body?.cancel(); } catch { /* already closed */ }
|
|
403
|
+
return fetchCommandCode(retry, ctx, executor);
|
|
404
|
+
},
|
|
405
|
+
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
|
|
406
|
+
let sawFinish = false;
|
|
407
|
+
for await (const event of ndjson(response, budget)) {
|
|
408
|
+
switch (event.type) {
|
|
409
|
+
case "text-delta": if (typeof event.text === "string") yield { type: "text_delta", text: event.text }; break;
|
|
410
|
+
case "reasoning-delta": if (typeof event.text === "string") yield { type: "thinking_delta", thinking: event.text }; break;
|
|
411
|
+
case "tool-call": {
|
|
412
|
+
const id = typeof event.toolCallId === "string" ? event.toolCallId : randomUUID();
|
|
413
|
+
const name = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
414
|
+
const input = event.input ?? event.args ?? {};
|
|
415
|
+
const argumentsText = typeof input === "string" ? input : JSON.stringify(input);
|
|
416
|
+
yield { type: "tool_call_start", id, name };
|
|
417
|
+
budget.openCall(id);
|
|
418
|
+
try {
|
|
419
|
+
const reservation = budget.reserveTransient(new TextEncoder().encode(argumentsText).byteLength, { kind: "tool_args", callId: id });
|
|
420
|
+
reservation.commitRetained();
|
|
421
|
+
yield { type: "tool_call_delta", arguments: argumentsText };
|
|
422
|
+
yield { type: "tool_call_end" };
|
|
423
|
+
} finally {
|
|
424
|
+
budget.closeCall(id);
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case "finish-step":
|
|
429
|
+
case "finish": {
|
|
430
|
+
// Both events are terminal on the current wire; streams commonly carry both, so
|
|
431
|
+
// only the first one emits the done. finish-step carries `usage`; finish may carry
|
|
432
|
+
// `totalUsage` (current flows) or `usage`.
|
|
433
|
+
if (sawFinish) break;
|
|
434
|
+
sawFinish = true;
|
|
435
|
+
const usageValue = event.totalUsage ?? event.usage;
|
|
436
|
+
const stopReason = typeof event.rawFinishReason === "string" ? event.rawFinishReason : typeof event.finishReason === "string" ? event.finishReason : undefined;
|
|
437
|
+
yield { type: "done", usage: usage(usageValue), stopReason };
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case "error": yield { type: "error", message: eventError(event.error), status: 502 }; break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// A stream that ends without a finish event still needs a terminal done so the
|
|
444
|
+
// server does not wait on an adapter that silently stopped emitting.
|
|
445
|
+
if (!sawFinish) yield { type: "done", usage: undefined, stopReason: undefined };
|
|
446
|
+
},
|
|
447
|
+
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
448
|
+
const events: AdapterEvent[] = [];
|
|
449
|
+
for await (const event of this.parseStream(response, budget)) events.push(event);
|
|
450
|
+
return events;
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
|
23
23
|
import { identifyRoutedModel } from "./identity";
|
|
24
24
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
25
25
|
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
26
|
+
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
26
27
|
import {
|
|
27
28
|
isTranslatorBudgetExceededError,
|
|
28
29
|
retainTranslatedEventBatch,
|
|
@@ -436,6 +437,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
436
437
|
if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
|
|
437
438
|
const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
|
|
438
439
|
if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
|
|
440
|
+
const locationError = googleVertexLocationConfigError(location);
|
|
441
|
+
if (locationError) throw new Error(locationError);
|
|
439
442
|
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
440
443
|
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
441
444
|
const token = await getVertexAccessToken();
|
package/src/cli/claude.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { configuredAdminToken } from "../lib/admin-secrets";
|
|
|
18
18
|
import { PROXY_MARKER, ownAdmissionTokens, defaultAuthDetectDeps, detectClaudeAuth, type AuthDetectDeps } from "../claude/auth-detect";
|
|
19
19
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
20
20
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
21
|
+
import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "./launcher-context";
|
|
21
22
|
|
|
22
23
|
export interface ClaudeLaunchEnv {
|
|
23
24
|
[key: string]: string | undefined;
|
|
@@ -27,13 +28,18 @@ export interface ClaudeLaunchEnv {
|
|
|
27
28
|
* Injectable IO for tests. `env` is deliberately NOT injectable: it is bound to the
|
|
28
29
|
* launch base so detection and the spawned process can never disagree (audit R3-3).
|
|
29
30
|
*/
|
|
30
|
-
export type ClaudeEnvDeps = {
|
|
31
|
+
export type ClaudeEnvDeps = {
|
|
32
|
+
authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
|
|
33
|
+
/** Test seam; production uses the authenticated Node-launcher context. */
|
|
34
|
+
preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
|
|
35
|
+
};
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
38
|
* Pure env assembly (unit-tested): never sets ANTHROPIC_API_KEY (setting both
|
|
34
39
|
* token vars triggers Claude Code's auth-conflict warning, 003 E1), and never
|
|
35
|
-
*
|
|
36
|
-
* ANTHROPIC_BASE_URL values owned by a previous
|
|
40
|
+
* preserves Anthropic variables proven to exist in the parent Node launcher,
|
|
41
|
+
* apart from stale loopback ANTHROPIC_BASE_URL values owned by a previous
|
|
42
|
+
* opencodex launch. Unproven ambient values fail closed as project dotenv.
|
|
37
43
|
*/
|
|
38
44
|
export function buildClaudeEnv(
|
|
39
45
|
config: OcxConfig,
|
|
@@ -49,27 +55,36 @@ export function buildClaudeEnv(
|
|
|
49
55
|
// leaving the child with no token at all (audit R2-1). It is opencodex state, never
|
|
50
56
|
// user auth, so dropping it unconditionally is safe.
|
|
51
57
|
if (env.ANTHROPIC_AUTH_TOKEN === PROXY_MARKER) delete env.ANTHROPIC_AUTH_TOKEN;
|
|
52
|
-
// Step 1b — drop Anthropic credentials
|
|
53
|
-
// project `.env`/`.env.local
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
// Step 1b — drop Anthropic credentials AND destinations that Bun may have synthesized
|
|
59
|
+
// from a project `.env`/`.env.local`. The plain-Node launcher records genuine parent
|
|
60
|
+
// exports before Bun starts and pairs that context with an argv proof, so with a
|
|
61
|
+
// trusted context we know exactly which slots the user really exported.
|
|
62
|
+
//
|
|
63
|
+
// Without a trusted context all three slots are treated as project-controlled. An
|
|
64
|
+
// earlier revision of this branch preserved credentials here, reasoning that the
|
|
65
|
+
// destination is pinned below so a dotenv key would only ever reach the local proxy.
|
|
66
|
+
// That reasoning is wrong, and review caught it: `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`
|
|
67
|
+
// is only set when we own an auth token (see below, and #253 for why asserting it
|
|
68
|
+
// otherwise logs a subscriber out), so on a subscription launch Claude Code's
|
|
69
|
+
// settings.env merge can still replace `ANTHROPIC_BASE_URL` after we return. A
|
|
70
|
+
// preserved key then travels to that host. The repository documents that residual for
|
|
71
|
+
// subscription mode; it must not be widened into a credential leak.
|
|
72
|
+
//
|
|
73
|
+
// Direct `bun src/cli/index.ts` therefore loses ambient Anthropic values. That is a
|
|
74
|
+
// real cost to a documented entry point, and the escape hatch is the launcher: run
|
|
75
|
+
// through `ocx` (the published bin) and genuine shell exports are preserved by proof.
|
|
76
|
+
const explicitSlots = deps.preBunAnthropicSlots;
|
|
77
|
+
const trustedSlots = explicitSlots === undefined
|
|
78
|
+
? trustedNodeLauncherContext()?.anthropicEnvSlots ?? []
|
|
79
|
+
: explicitSlots ?? [];
|
|
80
|
+
const exported = new Set<AnthropicParentEnvSlot>(trustedSlots);
|
|
81
|
+
for (const name of ANTHROPIC_PARENT_ENV_SLOTS) {
|
|
82
|
+
const value = env[name];
|
|
83
|
+
if (value !== undefined && value !== "" && !exported.has(name)) delete env[name];
|
|
70
84
|
}
|
|
71
|
-
// Never forward
|
|
85
|
+
// Never forward old or current provenance seams to Claude Code.
|
|
72
86
|
delete env.OCX_PRE_BUN_ANTHROPIC_ENV;
|
|
87
|
+
delete env.OCX_NODE_LAUNCH_CONTEXT;
|
|
73
88
|
const setDefault = (name: string, value: string | undefined) => {
|
|
74
89
|
if (value === undefined || value.length === 0) return;
|
|
75
90
|
if (env[name] !== undefined && env[name] !== "") return; // user wins
|
package/src/cli/index.ts
CHANGED
|
@@ -46,7 +46,10 @@ import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
|
46
46
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
47
47
|
import { removeOwnedConfigState } from "../lib/config-ownership";
|
|
48
48
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
49
|
+
import { initializeNodeLauncherContext } from "./launcher-context";
|
|
50
|
+
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
49
51
|
|
|
52
|
+
initializeNodeLauncherContext();
|
|
50
53
|
const args = process.argv.slice(2);
|
|
51
54
|
const command = args[0];
|
|
52
55
|
|
|
@@ -195,9 +198,10 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
195
198
|
// the same port only (never hop — that was the remaining PR #152 gap).
|
|
196
199
|
let port = await chooseListenPort(requestedPort);
|
|
197
200
|
let server: ReturnType<typeof startServer>;
|
|
201
|
+
const localAttestationSecret = createLocalAttestationSecret();
|
|
198
202
|
for (let attempt = 0; ; attempt++) {
|
|
199
203
|
try {
|
|
200
|
-
server = startServer(port);
|
|
204
|
+
server = startServer(port, { localAttestationSecret });
|
|
201
205
|
// Prewarm the live provider model cache as soon as the port is bound so the
|
|
202
206
|
// first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
|
|
203
207
|
// instead of racing duplicate upstream /models fetches.
|
|
@@ -225,7 +229,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
225
229
|
writePid(process.pid);
|
|
226
230
|
|
|
227
231
|
const config = loadConfig();
|
|
228
|
-
writeRuntimePort({ pid: process.pid, port, hostname: config.hostname });
|
|
232
|
+
writeRuntimePort({ pid: process.pid, port, hostname: config.hostname, attestationSecret: localAttestationSecret });
|
|
229
233
|
// No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
|
|
230
234
|
// is about to transform; snapshotting earlier only captured a baseline that could
|
|
231
235
|
// already be stale by the time injection ran (#477).
|
|
@@ -317,7 +321,16 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
317
321
|
installShellHook();
|
|
318
322
|
|
|
319
323
|
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
320
|
-
await syncCodexOnStartIfEnabled(port, config);
|
|
324
|
+
const startupSync = await syncCodexOnStartIfEnabled(port, config);
|
|
325
|
+
// #1046: one warning per startup, after BOTH writes. The server's cache
|
|
326
|
+
// invalidation happens first and the catalog sync second, so the mtime is only
|
|
327
|
+
// final here — and neither write site warns on its own, or a boot that hits
|
|
328
|
+
// both would warn twice.
|
|
329
|
+
const { consumeStartupCacheInvalidationWrite } = await import("../server");
|
|
330
|
+
if (consumeStartupCacheInvalidationWrite() || startupSync.catalogWritten || startupSync.cacheSynced) {
|
|
331
|
+
const { warnIfStaleCodexAppServersAfterStartupWrite } = await import("../codex/app-server-processes");
|
|
332
|
+
warnIfStaleCodexAppServersAfterStartupWrite({ log: console });
|
|
333
|
+
}
|
|
321
334
|
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
322
335
|
historyGuardian = startHistoryMigrationGuardian();
|
|
323
336
|
}
|