@bitkyc08/opencodex 2.7.42 → 2.7.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/update/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync, readdirSync } from "node:fs";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
|
+
import { npmInvocation } from "./npm-invocation.mjs";
|
|
6
7
|
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -50,19 +51,24 @@ export function updateTag(current: string): Channel {
|
|
|
50
51
|
return defaultUpdateTag(current);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
function npmSpawnTarget(args: readonly string[]): { bin: string; args: string[]; options: { windowsVerbatimArguments?: boolean } } | null {
|
|
55
|
+
const invocation = npmInvocation(args);
|
|
56
|
+
if (!invocation) return null;
|
|
57
|
+
return { bin: invocation.file, args: invocation.args, options: invocation.options };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function updateSpawnTarget(bin: string, args: readonly string[]): { bin: string; args: string[]; options: { windowsVerbatimArguments?: boolean } } | null {
|
|
61
|
+
if (bin === "npm") return npmSpawnTarget(args);
|
|
62
|
+
if (process.platform === "win32" && bin === "bun") {
|
|
63
|
+
return { bin: process.execPath, args: [...args], options: {} };
|
|
64
|
+
}
|
|
65
|
+
return { bin, args: [...args], options: {} };
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
69
|
* The GUI update worker sets OCX_SERVICE=1 and has stdio ignored — inheriting that for
|
|
64
|
-
*
|
|
65
|
-
* relay bounded output after the child exits. (Ported from PR #167.)
|
|
70
|
+
* Background package-manager children can open stacked visible consoles on Windows.
|
|
71
|
+
* Pipe instead and relay bounded output after the child exits. (Ported from PR #167.)
|
|
66
72
|
*/
|
|
67
73
|
function updateChildStdio(): "inherit" | "pipe" {
|
|
68
74
|
if (process.env.OCX_SERVICE === "1") return "pipe";
|
|
@@ -79,8 +85,14 @@ function logSpawnOutput(label: string, result: { stdout?: string | Buffer | null
|
|
|
79
85
|
|
|
80
86
|
/** Latest published version from the registry (best-effort; null if npm isn't available). */
|
|
81
87
|
export function latestVersion(tag: string): string | null {
|
|
82
|
-
const npm = npmSpawnTarget("
|
|
83
|
-
|
|
88
|
+
const npm = npmSpawnTarget(["view", `${PKG}@${tag}`, "version"]);
|
|
89
|
+
if (!npm) return null;
|
|
90
|
+
const r = spawnSync(npm.bin, npm.args, {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
timeout: 12000,
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
...npm.options,
|
|
95
|
+
});
|
|
84
96
|
return r.status === 0 ? (r.stdout.trim() || null) : null;
|
|
85
97
|
}
|
|
86
98
|
|
|
@@ -116,11 +128,12 @@ export function checkUpdatePackageIntegrity(
|
|
|
116
128
|
spawn: typeof spawnSync = spawnSync,
|
|
117
129
|
): { ok: true; integrity: string } | { ok: false; reason: string } | { ok: "skipped"; reason: string } {
|
|
118
130
|
if (!version) return { ok: "skipped", reason: "no resolved version (registry unavailable)" };
|
|
119
|
-
const npm = npmSpawnTarget("
|
|
131
|
+
const npm = npmSpawnTarget(["view", `${PKG}@${version}`, "dist.integrity"]);
|
|
132
|
+
if (!npm) return { ok: "skipped", reason: "npm executable was not found on a trusted PATH entry" };
|
|
120
133
|
const r = spawn(
|
|
121
134
|
npm.bin,
|
|
122
|
-
|
|
123
|
-
{ encoding: "utf8", timeout: 12000, windowsHide: true,
|
|
135
|
+
npm.args,
|
|
136
|
+
{ encoding: "utf8", timeout: 12000, windowsHide: true, ...npm.options },
|
|
124
137
|
);
|
|
125
138
|
// status !== 0 covers nonzero exits AND timeouts (status === null).
|
|
126
139
|
if (r.status !== 0) return { ok: "skipped", reason: `registry integrity query failed (status ${r.status ?? "timeout"})` };
|
|
@@ -164,6 +177,13 @@ export async function runUpdate(): Promise<void> {
|
|
|
164
177
|
console.log(`Verified ${PKG}@${latest} integrity metadata ${integrity.integrity.slice(0, 24)}…`);
|
|
165
178
|
}
|
|
166
179
|
|
|
180
|
+
const { bin, args: cmdArgs } = updateCommand(installer, tag, latest);
|
|
181
|
+
const target = updateSpawnTarget(bin, cmdArgs);
|
|
182
|
+
if (!target) {
|
|
183
|
+
console.error("⚠️ Could not resolve npm from a trusted absolute PATH entry; aborting before stopping the proxy.");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
167
187
|
// Remember whether a background service manages the proxy BEFORE stopping — `ocx stop`
|
|
168
188
|
// unloads it permanently, so a successful update must reinstall/restart it afterwards.
|
|
169
189
|
let serviceWasInstalled = false;
|
|
@@ -240,17 +260,15 @@ export async function runUpdate(): Promise<void> {
|
|
|
240
260
|
}
|
|
241
261
|
}
|
|
242
262
|
|
|
243
|
-
const { bin, args: cmdArgs } = updateCommand(installer, tag, latest);
|
|
244
263
|
console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
|
|
245
264
|
|
|
246
|
-
const target = npmSpawnTarget(bin);
|
|
247
265
|
const installStdio = updateChildStdio();
|
|
248
|
-
const r = spawnSync(target.bin,
|
|
266
|
+
const r = spawnSync(target.bin, target.args, {
|
|
249
267
|
stdio: installStdio,
|
|
250
268
|
encoding: installStdio === "pipe" ? "utf8" : undefined,
|
|
251
269
|
timeout: 180000,
|
|
252
270
|
windowsHide: true,
|
|
253
|
-
|
|
271
|
+
...target.options,
|
|
254
272
|
});
|
|
255
273
|
if (installStdio === "pipe") logSpawnOutput("", r);
|
|
256
274
|
if (r.status === 0) {
|
package/src/update/job.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { atomicWriteFile, getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
|
-
import { killProxy } from "../lib/process-control";
|
|
6
|
+
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
7
7
|
import { reclaimListenPort } from "../server/port-reclaim";
|
|
8
8
|
import { isOpencodexHealthz, probeHostname, proxyIdentityAt, type HealthzIdentity } from "../server/proxy-liveness";
|
|
9
9
|
import { isServiceInstalled } from "../service";
|
|
@@ -28,6 +28,8 @@ const UPDATE_TIMEOUT_MS = 180_000;
|
|
|
28
28
|
const RESTART_TIMEOUT_MS = 60_000;
|
|
29
29
|
const RESTART_HEALTH_TIMEOUT_MS = 15_000;
|
|
30
30
|
const RESTART_STABILITY_WINDOW_MS = 15_000;
|
|
31
|
+
/** Legacy active records did not persist a worker PID, so age is their only safe recovery signal. */
|
|
32
|
+
export const UPDATE_JOB_LEGACY_STALE_MS = 10 * 60_000;
|
|
31
33
|
/** How long update restart waits for the captured port to become bindable after stop. */
|
|
32
34
|
export const RESTART_PORT_RECLAIM_MS = 30_000;
|
|
33
35
|
|
|
@@ -77,6 +79,19 @@ export interface UpdateCheckDeps {
|
|
|
77
79
|
latestVersion: (tag: Channel) => string | null;
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
interface UpdateWorkerProcess {
|
|
83
|
+
pid?: number;
|
|
84
|
+
unref(): void;
|
|
85
|
+
once(event: "error", listener: (error: Error) => void): unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface StartUpdateJobDeps {
|
|
89
|
+
checkForUpdateFn: (channel: Channel) => UpdateCheckResult;
|
|
90
|
+
spawnWorkerFn: (jobId: string, channel: Channel, restart: boolean) => UpdateWorkerProcess;
|
|
91
|
+
isProcessAliveFn: (pid: number) => boolean;
|
|
92
|
+
nowMs: () => number;
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
const defaultCheckDeps: UpdateCheckDeps = {
|
|
81
96
|
currentVersion,
|
|
82
97
|
detectInstall,
|
|
@@ -155,7 +170,9 @@ export function updateExecutionCommand(
|
|
|
155
170
|
return { bin, args, display: formatCommand(bin, args) };
|
|
156
171
|
}
|
|
157
172
|
if (installer === "bun") {
|
|
158
|
-
const
|
|
173
|
+
const command = updateCommand(installer, channel, resolvedVersion);
|
|
174
|
+
const bin = process.platform === "win32" ? process.execPath : command.bin;
|
|
175
|
+
const { args } = command;
|
|
159
176
|
return { bin, args, display: updateCommandStr(installer, channel, resolvedVersion) };
|
|
160
177
|
}
|
|
161
178
|
return { bin: "sh", args: ["-lc", manualSourceCommand()], display: manualSourceCommand() };
|
|
@@ -225,19 +242,77 @@ function newJobId(): string {
|
|
|
225
242
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
226
243
|
}
|
|
227
244
|
|
|
228
|
-
|
|
245
|
+
/**
|
|
246
|
+
* [Decision Log]
|
|
247
|
+
* - Purpose: recover dashboard updates after a detached worker dies without unlocking concurrent live updates.
|
|
248
|
+
* - Existing constraints: legacy records have no PID, while a healthy update may legitimately run for minutes.
|
|
249
|
+
* - Alternatives considered: clear every active record by age, or require operators to delete the file manually.
|
|
250
|
+
* - Chosen approach: trust PID liveness first and use a conservative age limit only for legacy no-PID records.
|
|
251
|
+
* - Why: age-only recovery can start two installers, while never recovering leaves the dashboard permanently blocked.
|
|
252
|
+
* - Impact: live PID records remain locked regardless of age; dead PIDs recover immediately; legacy records recover after ten minutes.
|
|
253
|
+
*/
|
|
254
|
+
export function staleActiveUpdateJobReason(
|
|
255
|
+
job: Pick<UpdateJobState, "status" | "pid" | "updatedAt">,
|
|
256
|
+
now = Date.now(),
|
|
257
|
+
isAlive: (pid: number) => boolean = isProcessAlive,
|
|
258
|
+
): string | null {
|
|
259
|
+
if (job.status !== "running" && job.status !== "restarting") return null;
|
|
260
|
+
if (typeof job.pid === "number" && Number.isSafeInteger(job.pid) && job.pid > 0) {
|
|
261
|
+
return isAlive(job.pid) ? null : `update worker PID ${job.pid} is no longer running`;
|
|
262
|
+
}
|
|
263
|
+
const updatedAt = Date.parse(job.updatedAt);
|
|
264
|
+
if (Number.isFinite(updatedAt) && now - updatedAt >= UPDATE_JOB_LEGACY_STALE_MS) {
|
|
265
|
+
return "legacy active update record has no worker PID and exceeded the stale window";
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const defaultStartUpdateJobDeps: StartUpdateJobDeps = {
|
|
271
|
+
checkForUpdateFn: channel => checkForUpdate(channel),
|
|
272
|
+
spawnWorkerFn: (jobId, channel, restart) => spawn(
|
|
273
|
+
process.execPath,
|
|
274
|
+
[process.argv[1], "__gui-update-worker", jobId, channel, restart ? "restart" : "no-restart"],
|
|
275
|
+
{
|
|
276
|
+
detached: true,
|
|
277
|
+
stdio: "ignore",
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
280
|
+
},
|
|
281
|
+
),
|
|
282
|
+
isProcessAliveFn: isProcessAlive,
|
|
283
|
+
nowMs: Date.now,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export function startUpdateJob(
|
|
287
|
+
channel: Channel,
|
|
288
|
+
restart: boolean,
|
|
289
|
+
deps: Partial<StartUpdateJobDeps> = {},
|
|
290
|
+
): UpdateJobState {
|
|
291
|
+
const resolvedDeps = { ...defaultStartUpdateJobDeps, ...deps };
|
|
229
292
|
const running = readUpdateJob();
|
|
230
293
|
if (running?.status === "running" || running?.status === "restarting") {
|
|
231
|
-
|
|
294
|
+
const staleReason = staleActiveUpdateJobReason(
|
|
295
|
+
running,
|
|
296
|
+
resolvedDeps.nowMs(),
|
|
297
|
+
resolvedDeps.isProcessAliveFn,
|
|
298
|
+
);
|
|
299
|
+
if (!staleReason) {
|
|
300
|
+
throw new UpdateJobError("An update job is already running", 409, "update_already_running");
|
|
301
|
+
}
|
|
302
|
+
updateJob(
|
|
303
|
+
running,
|
|
304
|
+
{ status: "failed", error: `Recovered stale update job: ${staleReason}.`, exitCode: null },
|
|
305
|
+
`Recovered stale update job: ${staleReason}.`,
|
|
306
|
+
);
|
|
232
307
|
}
|
|
233
308
|
|
|
234
|
-
const check =
|
|
309
|
+
const check = resolvedDeps.checkForUpdateFn(channel);
|
|
235
310
|
if (!check.canUpdate) {
|
|
236
311
|
throw new UpdateJobError(check.reason ?? "No update is available", 409, check.reason ?? "update_unavailable");
|
|
237
312
|
}
|
|
238
313
|
|
|
239
314
|
const id = newJobId();
|
|
240
|
-
const now = new Date().toISOString();
|
|
315
|
+
const now = new Date(resolvedDeps.nowMs()).toISOString();
|
|
241
316
|
const job: UpdateJobState = {
|
|
242
317
|
id,
|
|
243
318
|
status: "running",
|
|
@@ -254,14 +329,30 @@ export function startUpdateJob(channel: Channel, restart: boolean): UpdateJobSta
|
|
|
254
329
|
};
|
|
255
330
|
writeJob(job);
|
|
256
331
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
332
|
+
let child: UpdateWorkerProcess;
|
|
333
|
+
try {
|
|
334
|
+
child = resolvedDeps.spawnWorkerFn(id, channel, restart);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
updateJob(job, { status: "failed", error: `Could not start update worker: ${message}` }, "Update worker failed to start.");
|
|
338
|
+
throw new UpdateJobError("Could not start update worker", 500, "update_worker_start_failed");
|
|
339
|
+
}
|
|
340
|
+
if (typeof child.pid !== "number" || !Number.isSafeInteger(child.pid) || child.pid <= 0) {
|
|
341
|
+
updateJob(job, { status: "failed", error: "Could not start update worker: no worker PID was returned." }, "Update worker failed to start.");
|
|
342
|
+
throw new UpdateJobError("Could not start update worker", 500, "update_worker_start_failed");
|
|
343
|
+
}
|
|
344
|
+
const startedJob = updateJob(job, { pid: child.pid }, `Update worker started as PID ${child.pid}.`);
|
|
345
|
+
child.once("error", error => {
|
|
346
|
+
const current = readUpdateJob(id);
|
|
347
|
+
if (!current || current.pid !== child.pid || (current.status !== "running" && current.status !== "restarting")) return;
|
|
348
|
+
updateJob(
|
|
349
|
+
current,
|
|
350
|
+
{ status: "failed", error: `Update worker failed to start: ${error.message}` },
|
|
351
|
+
"Update worker emitted a startup error.",
|
|
352
|
+
);
|
|
262
353
|
});
|
|
263
354
|
child.unref();
|
|
264
|
-
return
|
|
355
|
+
return startedJob;
|
|
265
356
|
}
|
|
266
357
|
|
|
267
358
|
function runLoggedCommand(job: UpdateJobState, bin: string, args: string[], timeout: number): { status: number | null; signal: NodeJS.Signals | null } {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface NpmInvocationDeps {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
exists?: (path: string) => boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface NpmInvocation {
|
|
7
|
+
file: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
options: { windowsVerbatimArguments?: boolean };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare function resolveNpmCommand(
|
|
13
|
+
platform?: NodeJS.Platform,
|
|
14
|
+
env?: Record<string, string | undefined>,
|
|
15
|
+
deps?: NpmInvocationDeps,
|
|
16
|
+
): string | null;
|
|
17
|
+
|
|
18
|
+
export declare function npmInvocation(
|
|
19
|
+
args: readonly string[],
|
|
20
|
+
platform?: NodeJS.Platform,
|
|
21
|
+
env?: Record<string, string | undefined>,
|
|
22
|
+
deps?: NpmInvocationDeps,
|
|
23
|
+
): NpmInvocation | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
|
|
4
|
+
const CMD_META = /([()%!^"`<>&|;, *?])/g;
|
|
5
|
+
|
|
6
|
+
function escapeCmdArg(arg) {
|
|
7
|
+
let out = String(arg).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
|
|
8
|
+
return `"${out}"`.replace(CMD_META, "^$1");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function escapeCmdCommand(command) {
|
|
12
|
+
return command.replace(CMD_META, "^$1");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether a PATH entry *is* the current directory. The hijack this guards against is
|
|
17
|
+
* cmd.exe resolving a bare `npm` out of the directory opencodex was launched from, so
|
|
18
|
+
* only that exact directory has to be skipped — every candidate we hand to spawn is an
|
|
19
|
+
* absolute path, which is what actually defeats the implicit cwd-first search.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately not a subtree test: npm's default Windows global prefix is
|
|
22
|
+
* `%AppData%\npm` (`C:\Users\x\AppData\Roaming\npm`), so excluding everything under the
|
|
23
|
+
* cwd would fail closed for anyone whose shell sits in their home directory — a normal
|
|
24
|
+
* setup, not the untrusted-project case this hardening is for.
|
|
25
|
+
*/
|
|
26
|
+
function isCurrentDirectory(cwd, entry) {
|
|
27
|
+
const left = win32.resolve(entry);
|
|
28
|
+
const right = win32.resolve(cwd);
|
|
29
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cleanPathEntry(entry) {
|
|
33
|
+
const trimmed = entry.trim();
|
|
34
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1);
|
|
35
|
+
return trimmed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveNpmCommand(
|
|
39
|
+
platform = process.platform,
|
|
40
|
+
env = process.env,
|
|
41
|
+
deps = {},
|
|
42
|
+
) {
|
|
43
|
+
if (platform !== "win32") return "npm";
|
|
44
|
+
const exists = deps.exists ?? existsSync;
|
|
45
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
46
|
+
const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
47
|
+
.split(";")
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const pathEntries = (env.PATH ?? env.Path ?? "")
|
|
50
|
+
.split(win32.delimiter)
|
|
51
|
+
.map(cleanPathEntry)
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
|
|
54
|
+
for (const entry of pathEntries) {
|
|
55
|
+
if (!win32.isAbsolute(entry)) continue;
|
|
56
|
+
if (isCurrentDirectory(cwd, entry)) continue;
|
|
57
|
+
for (const extension of extensions) {
|
|
58
|
+
const candidate = win32.join(entry, `npm${extension.toLowerCase()}`);
|
|
59
|
+
if (exists(candidate)) return win32.resolve(candidate);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function systemCommandProcessor(env) {
|
|
66
|
+
const systemRoot = env.SystemRoot ?? env.windir;
|
|
67
|
+
if (systemRoot && win32.isAbsolute(systemRoot)) {
|
|
68
|
+
return win32.join(systemRoot, "System32", "cmd.exe");
|
|
69
|
+
}
|
|
70
|
+
const comSpec = env.ComSpec;
|
|
71
|
+
return comSpec && win32.isAbsolute(comSpec) ? win32.resolve(comSpec) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function npmInvocation(
|
|
75
|
+
args,
|
|
76
|
+
platform = process.platform,
|
|
77
|
+
env = process.env,
|
|
78
|
+
deps = {},
|
|
79
|
+
) {
|
|
80
|
+
const npm = resolveNpmCommand(platform, env, deps);
|
|
81
|
+
if (!npm) return null;
|
|
82
|
+
if (platform !== "win32" || !/\.(cmd|bat)$/i.test(npm)) {
|
|
83
|
+
return { file: npm, args: [...args], options: {} };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const commandProcessor = systemCommandProcessor(env);
|
|
87
|
+
if (!commandProcessor) return null;
|
|
88
|
+
const line = [escapeCmdCommand(npm), ...args.map(escapeCmdArg)].join(" ");
|
|
89
|
+
return {
|
|
90
|
+
file: commandProcessor,
|
|
91
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
92
|
+
options: { windowsVerbatimArguments: true },
|
|
93
|
+
};
|
|
94
|
+
}
|
package/src/usage/debug.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFi
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { getConfigDir } from "../config";
|
|
6
6
|
import { DEBUG_ENV } from "../lib/debug-settings";
|
|
7
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
7
8
|
import type { DebugLogEntry } from "../lib/debug-log-buffer";
|
|
8
9
|
import { redactSecretString, redactSecrets } from "../lib/redact";
|
|
9
10
|
import type { OcxUsage } from "../types";
|
|
@@ -42,6 +43,7 @@ export function truncateForDebug(text: string, max = USAGE_DEBUG_BODY_SAMPLE_BYT
|
|
|
42
43
|
|
|
43
44
|
function ensureUsageDebugDir(): void {
|
|
44
45
|
const dir = getConfigDir();
|
|
46
|
+
recordOwnedConfigPath(dir, usageDebugPath());
|
|
45
47
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
48
|
try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
|
|
47
49
|
}
|
package/src/usage/log.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, appendFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getConfigDir } from "../config";
|
|
4
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
4
5
|
import { usageDisplayTotalTokens } from "./totals";
|
|
5
6
|
import type { OcxUsage } from "../types";
|
|
6
7
|
|
|
@@ -11,6 +12,7 @@ export type AttemptRecoveryKind =
|
|
|
11
12
|
| "connection-reset"
|
|
12
13
|
| "oauth-401"
|
|
13
14
|
| "key-429"
|
|
15
|
+
| "anthropic-oauth-429"
|
|
14
16
|
| "image-413";
|
|
15
17
|
|
|
16
18
|
export interface PersistedUsageAttempt {
|
|
@@ -140,6 +142,7 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
|
|
|
140
142
|
"connection-reset",
|
|
141
143
|
"oauth-401",
|
|
142
144
|
"key-429",
|
|
145
|
+
"anthropic-oauth-429",
|
|
143
146
|
"image-413",
|
|
144
147
|
]);
|
|
145
148
|
const USAGE_STATUSES = new Set<UsageStatus>([
|
|
@@ -315,6 +318,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
315
318
|
|
|
316
319
|
function ensureUsageLogDir(): void {
|
|
317
320
|
const dir = getConfigDir();
|
|
321
|
+
recordOwnedConfigPath(dir, usageLogPath());
|
|
318
322
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
319
323
|
try { chmodSync(dir, 0o700); } catch { /* best-effort on platforms that ignore chmod */ }
|
|
320
324
|
}
|