@bitkyc08/opencodex 2.81.0 → 2.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -801,6 +801,11 @@ export interface CoordinatedIntegrationOptions {
|
|
|
801
801
|
* non-null result refuses without writing anything.
|
|
802
802
|
*/
|
|
803
803
|
revalidate?: (frozen: IntegrationWriteInput) => Promise<WriteOutcome | null>;
|
|
804
|
+
/**
|
|
805
|
+
* Synchronous last word, evaluated with no await between it and the transaction (after
|
|
806
|
+
* `revalidate` and any lock acquisition). A non-null result refuses without writing.
|
|
807
|
+
*/
|
|
808
|
+
guard?: (frozen: IntegrationWriteInput) => WriteOutcome | null;
|
|
804
809
|
}
|
|
805
810
|
|
|
806
811
|
/** Freeze all mutable resolution seams before the first lock await. */
|
|
@@ -942,21 +947,22 @@ async function coordinatedWrite(
|
|
|
942
947
|
if (!prepared.ok) return prepared.refusal;
|
|
943
948
|
const frozen = prepared.value;
|
|
944
949
|
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
950
|
+
const guarded = (bound: IntegrationWriteInput) => options?.guard?.(bound) ?? operation(bound);
|
|
945
951
|
if (!spec.writerLock) {
|
|
946
952
|
const refused = await options?.revalidate?.(frozen);
|
|
947
|
-
return refused ??
|
|
953
|
+
return refused ?? guarded(frozen);
|
|
948
954
|
}
|
|
949
955
|
|
|
950
956
|
// An absent client home is not created merely to acquire a sibling lock.
|
|
951
957
|
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
952
958
|
const refused = await options?.revalidate?.(frozen);
|
|
953
959
|
if (refused) return refused;
|
|
954
|
-
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return
|
|
960
|
+
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return guarded(frozen);
|
|
955
961
|
}
|
|
956
962
|
return withClientLocks(
|
|
957
963
|
frozen,
|
|
958
964
|
spec.writerLock.suffix,
|
|
959
|
-
() =>
|
|
965
|
+
() => guarded(frozen),
|
|
960
966
|
options,
|
|
961
967
|
);
|
|
962
968
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface DesktopCliIo {
|
|
2
|
+
stat?: (path: string) => { isFile(): boolean };
|
|
3
|
+
access?: (path: string, mode: number) => void;
|
|
4
|
+
}
|
|
5
|
+
export interface PathBunIo extends DesktopCliIo {
|
|
6
|
+
now?: () => number;
|
|
7
|
+
realpath?: (path: string) => string;
|
|
8
|
+
stat?: (path: string) => { isFile(): boolean; mode: number };
|
|
9
|
+
isRealBunBinary?: (path: string) => boolean;
|
|
10
|
+
spawnSync?: (path: string, args: string[], options: {
|
|
11
|
+
env: NodeJS.ProcessEnv;
|
|
12
|
+
encoding: "utf8";
|
|
13
|
+
timeout: number;
|
|
14
|
+
maxBuffer: number;
|
|
15
|
+
killSignal: "SIGKILL";
|
|
16
|
+
shell: false;
|
|
17
|
+
windowsHide: true;
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"];
|
|
19
|
+
}) => { status: number | null; stdout?: string; signal?: string | null; error?: unknown };
|
|
20
|
+
}
|
|
21
|
+
export declare function findPathBun(options: {
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
platform?: NodeJS.Platform;
|
|
24
|
+
pinnedVersion: string;
|
|
25
|
+
deadlineMs?: number;
|
|
26
|
+
io?: PathBunIo;
|
|
27
|
+
}): { path: string; version: string } | null;
|
|
28
|
+
export declare function findDesktopCli(options?: {
|
|
29
|
+
platform?: NodeJS.Platform;
|
|
30
|
+
home?: string;
|
|
31
|
+
io?: DesktopCliIo;
|
|
32
|
+
}): string | null;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { isRealBunBinary } from "./bun-binary-validator.mjs";
|
|
7
|
+
|
|
8
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
9
|
+
const MAX_VERSION_BYTES = 4096;
|
|
10
|
+
const IDENTITY_PROBE = "process.stdout.write(String(typeof Bun==='object'&&Bun.version))";
|
|
11
|
+
|
|
12
|
+
function stableVersion(value) {
|
|
13
|
+
if (typeof value !== "string" || value.length > 64) return null;
|
|
14
|
+
const match = STABLE.exec(value);
|
|
15
|
+
if (!match) return null;
|
|
16
|
+
const numbers = match.slice(1, 4).map(Number);
|
|
17
|
+
return numbers.every(Number.isSafeInteger) ? numbers : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Discover the first absolute PATH Bun passing version-policy and identity checks. */
|
|
21
|
+
export function findPathBun({
|
|
22
|
+
env = process.env, platform = process.platform, pinnedVersion,
|
|
23
|
+
deadlineMs = 750, io = {},
|
|
24
|
+
}) {
|
|
25
|
+
const pin = stableVersion(pinnedVersion);
|
|
26
|
+
if (!pin || !Number.isFinite(deadlineMs) || deadlineMs <= 0) return null;
|
|
27
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
28
|
+
const absolute = value => paths.isAbsolute(value)
|
|
29
|
+
&& (platform !== "win32" || /^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+)/.test(value));
|
|
30
|
+
const now = io.now ?? (() => performance.now());
|
|
31
|
+
const end = now() + deadlineMs;
|
|
32
|
+
const realpath = io.realpath ?? realpathSync.native;
|
|
33
|
+
const stat = io.stat ?? statSync;
|
|
34
|
+
const access = io.access ?? accessSync;
|
|
35
|
+
const validate = io.isRealBunBinary ?? isRealBunBinary;
|
|
36
|
+
const spawn = io.spawnSync ?? spawnSync;
|
|
37
|
+
const key = platform === "win32"
|
|
38
|
+
? Object.keys(env).find(name => name.toLowerCase() === "path") : "PATH";
|
|
39
|
+
const directories = (env[key ?? "PATH"] ?? "").split(platform === "win32" ? ";" : ":");
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const probe = (path, args) => {
|
|
42
|
+
const remaining = Math.floor(end - now());
|
|
43
|
+
if (remaining <= 0) return null;
|
|
44
|
+
const result = spawn(path, args, {
|
|
45
|
+
env, encoding: "utf8", timeout: remaining, maxBuffer: MAX_VERSION_BYTES,
|
|
46
|
+
killSignal: "SIGKILL", shell: false, windowsHide: true,
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
});
|
|
49
|
+
if (now() >= end || result.error || result.signal || result.status !== 0
|
|
50
|
+
|| typeof result.stdout !== "string" || result.stdout.length > MAX_VERSION_BYTES) return null;
|
|
51
|
+
return result.stdout.trim();
|
|
52
|
+
};
|
|
53
|
+
for (const directory of directories) {
|
|
54
|
+
if (now() >= end) break;
|
|
55
|
+
if (!directory || !absolute(directory)) continue;
|
|
56
|
+
try {
|
|
57
|
+
const path = realpath(paths.join(directory, platform === "win32" ? "bun.exe" : "bun"));
|
|
58
|
+
if (!absolute(path)) continue;
|
|
59
|
+
const identity = platform === "win32" ? path.toLowerCase() : path;
|
|
60
|
+
if (seen.has(identity)) continue;
|
|
61
|
+
seen.add(identity);
|
|
62
|
+
const file = stat(path);
|
|
63
|
+
if (!file.isFile()) continue;
|
|
64
|
+
if (platform !== "win32" && ((file.mode & 0o022) || (stat(paths.dirname(path)).mode & 0o022))) continue;
|
|
65
|
+
access(path, constants.X_OK);
|
|
66
|
+
if (!validate(path)) continue;
|
|
67
|
+
const version = probe(path, ["--version"]);
|
|
68
|
+
const candidate = stableVersion(version);
|
|
69
|
+
if (!candidate || candidate[0] !== pin[0] || candidate[1] < pin[1]) continue;
|
|
70
|
+
if (probe(path, ["-e", IDENTITY_PROBE]) !== version) continue;
|
|
71
|
+
return { path, version };
|
|
72
|
+
} catch {
|
|
73
|
+
// Unreadable, unexecutable, vanished or invalid: try the next absolute candidate.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Failure-text pointer only. Never execute or delegate to this binary. */
|
|
80
|
+
export function findDesktopCli({ platform = process.platform, home = homedir(), io = {} } = {}) {
|
|
81
|
+
if (platform !== "darwin") return null;
|
|
82
|
+
const stat = io.stat ?? statSync;
|
|
83
|
+
const access = io.access ?? accessSync;
|
|
84
|
+
for (const path of [
|
|
85
|
+
"/Applications/OpenCodex.app/Contents/MacOS/ocx",
|
|
86
|
+
posix.join(home, "Applications/OpenCodex.app/Contents/MacOS/ocx"),
|
|
87
|
+
]) {
|
|
88
|
+
try {
|
|
89
|
+
if (!stat(path).isFile()) continue;
|
|
90
|
+
access(path, constants.X_OK);
|
|
91
|
+
return path;
|
|
92
|
+
} catch { /* Missing or not executable: no pointer for this candidate. */ }
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { lstatSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { DurableBunRuntime } from "./bun-runtime";
|
|
6
|
+
|
|
7
|
+
export type RuntimePreflightReason = "spawn" | "timeout" | "create" | "remove" | "protocol";
|
|
8
|
+
export const RUNTIME_PREFLIGHT_TIMEOUT_MS = 5_000;
|
|
9
|
+
export const RUNTIME_PREFLIGHT_GUIDANCE = "The selected Bun runtime could not create and remove a directory in the OpenCodex config directory. "
|
|
10
|
+
+ "Windows application policy may deny writes from this executable location. "
|
|
11
|
+
+ "Set OPENCODEX_BUN_PATH to a trusted Bun executable allowed by your policy before launching ocx, "
|
|
12
|
+
+ "or reinstall opencodex with npm install -g @bitkyc08/opencodex, then retry. No alternate runtime was selected.";
|
|
13
|
+
|
|
14
|
+
export class RuntimePreflightError extends Error {
|
|
15
|
+
readonly code = "OCX_RUNTIME_PREFLIGHT_FAILED";
|
|
16
|
+
constructor(readonly reason: RuntimePreflightReason) {
|
|
17
|
+
super(`${RUNTIME_PREFLIGHT_GUIDANCE} (preflight: ${reason})`);
|
|
18
|
+
this.name = "RuntimePreflightError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type DirectoryIdentity = { dev: number; ino: number; isDirectory(): boolean; isSymbolicLink(): boolean };
|
|
23
|
+
export interface RuntimePreflightFs {
|
|
24
|
+
lstatSync: (path: string) => DirectoryIdentity;
|
|
25
|
+
mkdirSync: (path: string, mode: number) => unknown;
|
|
26
|
+
rmdirSync: (path: string) => void;
|
|
27
|
+
readdirSync: (path: string) => readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export interface RuntimePreflightOptions {
|
|
30
|
+
platform?: NodeJS.Platform;
|
|
31
|
+
rootWasAbsent?: boolean;
|
|
32
|
+
spawnSync?: typeof spawnSync;
|
|
33
|
+
fs?: RuntimePreflightFs;
|
|
34
|
+
nonce?: () => string;
|
|
35
|
+
}
|
|
36
|
+
/** Caller seams keep one selection through admission, rendering and state publication. */
|
|
37
|
+
export interface RuntimePreflightDeps {
|
|
38
|
+
selectRuntime?: () => DurableBunRuntime;
|
|
39
|
+
assertRuntimeWritable?: typeof assertSelectedRuntimeWritable;
|
|
40
|
+
configDir?: () => string;
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const probeFs: RuntimePreflightFs = { lstatSync, mkdirSync, rmdirSync, readdirSync };
|
|
45
|
+
|
|
46
|
+
function sameDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): boolean {
|
|
47
|
+
const current = fs.lstatSync(path);
|
|
48
|
+
return current.isDirectory() && !current.isSymbolicLink()
|
|
49
|
+
&& current.dev === owned.dev && current.ino === owned.ino;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeOwnedEmptyDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): void {
|
|
53
|
+
try {
|
|
54
|
+
if (sameDirectory(fs, path, owned) && fs.readdirSync(path).length === 0
|
|
55
|
+
&& sameDirectory(fs, path, owned)) fs.rmdirSync(path);
|
|
56
|
+
} catch { /* Never delete contents or a replacement; cleanup does not mask refusal. */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function prepareRoot(fs: RuntimePreflightFs, root: string, rootWasAbsent?: boolean): void {
|
|
60
|
+
try {
|
|
61
|
+
if (rootWasAbsent === undefined) {
|
|
62
|
+
try { fs.lstatSync(root); rootWasAbsent = false; }
|
|
63
|
+
catch (error) {
|
|
64
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
65
|
+
rootWasAbsent = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (rootWasAbsent) {
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(root, 0o700); // Exclusive, non-recursive: a missing parent refuses.
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
73
|
+
// Use a concurrently created root, subject to the directory check below.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const current = fs.lstatSync(root);
|
|
77
|
+
if (!current.isDirectory() || current.isSymbolicLink()) throw new RuntimePreflightError("create");
|
|
78
|
+
} catch {
|
|
79
|
+
throw new RuntimePreflightError("create");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function probeInProcess(fs: RuntimePreflightFs, path: string): void {
|
|
84
|
+
let owned: DirectoryIdentity | undefined;
|
|
85
|
+
try {
|
|
86
|
+
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); }
|
|
87
|
+
catch { throw new RuntimePreflightError("create"); }
|
|
88
|
+
try { fs.rmdirSync(path); }
|
|
89
|
+
catch { throw new RuntimePreflightError("remove"); }
|
|
90
|
+
} finally {
|
|
91
|
+
if (owned) removeOwnedEmptyDirectory(fs, path, owned);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function probeChild(runtime: DurableBunRuntime, path: string, nonce: string, spawn: typeof spawnSync): void {
|
|
96
|
+
// The child emits only this invocation's nonce and a fixed result, never paths/errors.
|
|
97
|
+
const script = `const fs = require("node:fs"); const path = ${JSON.stringify(path)};
|
|
98
|
+
let result = "create", owned;
|
|
99
|
+
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); result = "remove";
|
|
100
|
+
fs.rmdirSync(path); result = "ok";
|
|
101
|
+
} catch { /* result already names the failed step; details never leave the child */ } finally {
|
|
102
|
+
if (owned) { try { const now = fs.lstatSync(path);
|
|
103
|
+
if (now.isDirectory() && !now.isSymbolicLink() && now.dev === owned.dev && now.ino === owned.ino
|
|
104
|
+
&& fs.readdirSync(path).length === 0) fs.rmdirSync(path);
|
|
105
|
+
} catch { /* leave any probe entry that cannot be proven ours */ } }
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write(${JSON.stringify(nonce)} + ":" + result);
|
|
108
|
+
process.exitCode = result === "ok" ? 0 : 1;`;
|
|
109
|
+
let child: ReturnType<typeof spawnSync>;
|
|
110
|
+
try {
|
|
111
|
+
child = spawn(runtime.path, ["-e", script], {
|
|
112
|
+
shell: false, windowsHide: true, timeout: RUNTIME_PREFLIGHT_TIMEOUT_MS,
|
|
113
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 1024,
|
|
114
|
+
});
|
|
115
|
+
} catch { throw new RuntimePreflightError("spawn"); }
|
|
116
|
+
if (child.error) {
|
|
117
|
+
throw new RuntimePreflightError((child.error as NodeJS.ErrnoException).code === "ETIMEDOUT" ? "timeout" : "spawn");
|
|
118
|
+
}
|
|
119
|
+
const ack = String(child.stdout ?? "");
|
|
120
|
+
if (ack === `${nonce}:create` && child.status === 1) throw new RuntimePreflightError("create");
|
|
121
|
+
if (ack === `${nonce}:remove` && child.status === 1) throw new RuntimePreflightError("remove");
|
|
122
|
+
if (ack !== `${nonce}:ok` || child.status !== 0) throw new RuntimePreflightError("protocol");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Windows admission of ONLY the already selected runtime. No discovery or memoization. */
|
|
126
|
+
export function assertSelectedRuntimeWritable(
|
|
127
|
+
runtime: DurableBunRuntime,
|
|
128
|
+
configDir: string,
|
|
129
|
+
deps: RuntimePreflightOptions = {},
|
|
130
|
+
): void {
|
|
131
|
+
if ((deps.platform ?? process.platform) !== "win32") return;
|
|
132
|
+
if (runtime.source === "standalone" && runtime.path !== process.execPath) throw new RuntimePreflightError("protocol");
|
|
133
|
+
const fs = deps.fs ?? probeFs;
|
|
134
|
+
// The config root is durable: never delete it by pathname, even on refusal.
|
|
135
|
+
prepareRoot(fs, configDir, deps.rootWasAbsent);
|
|
136
|
+
const nonce = (deps.nonce ?? randomUUID)();
|
|
137
|
+
const path = join(configDir, `.ocx-runtime-probe-${nonce}`);
|
|
138
|
+
if (runtime.source === "standalone") probeInProcess(fs, path);
|
|
139
|
+
else probeChild(runtime, path, nonce, deps.spawnSync ?? spawnSync);
|
|
140
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { spawn } from "node:child_process";
|
|
2
|
+
import type { accessSync, lstatSync, statSync } from "node:fs";
|
|
3
|
+
import type { DesktopCliRecordRead, DesktopCliRecordIssue } from "./desktop-cli-record.mjs";
|
|
4
|
+
export type DesktopHandoffIssue = DesktopCliRecordIssue | "target-invalid" | "target-self" | "target-unusable" | "spawn-failed";
|
|
5
|
+
export type DesktopCliHandoffPlan =
|
|
6
|
+
| { kind: "continue"; reason: "excluded" | "missing" | "disabled" | "target-missing" | "windows-path-only" }
|
|
7
|
+
| { kind: "error"; issue: DesktopHandoffIssue }
|
|
8
|
+
| { kind: "handoff"; target: string };
|
|
9
|
+
export type DesktopCliHandoffExit =
|
|
10
|
+
| { kind: "continue" }
|
|
11
|
+
| { kind: "error"; issue: "spawn-failed" }
|
|
12
|
+
| { kind: "exit"; code: number; signal: NodeJS.Signals | null };
|
|
13
|
+
export function desktopHandoffExcluded(argv: string[], env?: NodeJS.ProcessEnv): boolean;
|
|
14
|
+
export function planDesktopCliHandoff(input?: {
|
|
15
|
+
argv?: string[]; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform;
|
|
16
|
+
recordRead?: DesktopCliRecordRead; selfPaths?: (string | undefined)[];
|
|
17
|
+
}, deps?: { stat?: typeof statSync; access?: typeof accessSync; realpath?: (path: string) => string }): DesktopCliHandoffPlan;
|
|
18
|
+
export function runDesktopCliHandoff(plan: Extract<DesktopCliHandoffPlan, { kind: "handoff" }>, launch: {
|
|
19
|
+
argv: string[]; env: NodeJS.ProcessEnv; proof: string; context: string;
|
|
20
|
+
}, deps?: { spawn?: typeof spawn; lstat?: typeof lstatSync; parent?: Pick<NodeJS.Process, "on" | "removeListener">; platform?: NodeJS.Platform }): Promise<DesktopCliHandoffExit>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, lstatSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { readDesktopCliRecord } from "./desktop-cli-record.mjs";
|
|
5
|
+
import { isCodexCliUpdateInspectionArgv } from "../update/codex-cli-update-launch-policy.mjs";
|
|
6
|
+
|
|
7
|
+
const PROOF_PREFIX = "--ocx-internal-launch-proof=";
|
|
8
|
+
|
|
9
|
+
export function desktopHandoffExcluded(argv, env = process.env) {
|
|
10
|
+
const args = argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
|
|
11
|
+
return env.OCX_NO_DESKTOP_HANDOFF === "1"
|
|
12
|
+
|| ["update", "uninstall", "remove"].includes(args[0])
|
|
13
|
+
|| (args[0] ?? "").startsWith("__")
|
|
14
|
+
|| isCodexCliUpdateInspectionArgv(["node", "ocx.mjs", ...args]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function planDesktopCliHandoff(input = {}, deps = {}) {
|
|
18
|
+
const platform = input.platform ?? process.platform;
|
|
19
|
+
if (platform === "win32") return { kind: "continue", reason: "windows-path-only" };
|
|
20
|
+
const argv = input.argv ?? process.argv.slice(2);
|
|
21
|
+
const env = input.env ?? process.env;
|
|
22
|
+
if (desktopHandoffExcluded(argv, env)) return { kind: "continue", reason: "excluded" };
|
|
23
|
+
const read = input.recordRead ?? readDesktopCliRecord({ env, platform: input.platform });
|
|
24
|
+
if (read.state === "missing" || read.state === "disabled") {
|
|
25
|
+
return { kind: "continue", reason: read.state };
|
|
26
|
+
}
|
|
27
|
+
if (read.state !== "ready") return { kind: "error", issue: read.issue };
|
|
28
|
+
const api = platform === "win32" ? win32 : posix;
|
|
29
|
+
const target = read.record.cliExecutable;
|
|
30
|
+
if (!api.isAbsolute(target)) return { kind: "error", issue: "target-invalid" };
|
|
31
|
+
const stat = deps.stat ?? statSync;
|
|
32
|
+
const access = deps.access ?? accessSync;
|
|
33
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
34
|
+
try {
|
|
35
|
+
if (!stat(target).isFile()) return { kind: "error", issue: "target-invalid" };
|
|
36
|
+
access(target, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
37
|
+
const physical = realpath(target);
|
|
38
|
+
const key = path => platform === "win32" ? path.toLowerCase() : path;
|
|
39
|
+
for (const self of input.selfPaths ?? [process.argv[1], process.execPath]) {
|
|
40
|
+
if (!self) continue;
|
|
41
|
+
let own;
|
|
42
|
+
try { own = realpath(self); }
|
|
43
|
+
catch (error) { if (error?.code === "ENOENT") continue; throw error; }
|
|
44
|
+
if (key(own) === key(physical)) return { kind: "error", issue: "target-self" };
|
|
45
|
+
}
|
|
46
|
+
return { kind: "handoff", target: physical };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return error?.code === "ENOENT"
|
|
49
|
+
? { kind: "continue", reason: "target-missing" }
|
|
50
|
+
: { kind: "error", issue: "target-unusable" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function runDesktopCliHandoff(plan, launch, deps = {}) {
|
|
55
|
+
const spawnChild = deps.spawn ?? spawn;
|
|
56
|
+
const parent = deps.parent ?? process;
|
|
57
|
+
const platform = deps.platform ?? process.platform;
|
|
58
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
59
|
+
const spawnFailure = error => {
|
|
60
|
+
if (error?.code === "ENOENT") {
|
|
61
|
+
try { lstat(plan.target); }
|
|
62
|
+
catch (checkError) { if (checkError?.code === "ENOENT") return { kind: "continue" }; }
|
|
63
|
+
}
|
|
64
|
+
return { kind: "error", issue: "spawn-failed" };
|
|
65
|
+
};
|
|
66
|
+
const argv = launch.argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
|
|
67
|
+
const env = { ...launch.env, OCX_NO_DESKTOP_HANDOFF: "1", OCX_NODE_LAUNCH_CONTEXT: launch.context };
|
|
68
|
+
delete env.OCX_BUN_RUNTIME_SOURCE;
|
|
69
|
+
delete env.OCX_BUN_RUNTIME_PATH;
|
|
70
|
+
return new Promise(resolveExit => {
|
|
71
|
+
let child;
|
|
72
|
+
try {
|
|
73
|
+
child = spawnChild(plan.target, [`${PROOF_PREFIX}${launch.proof}`, ...argv], {
|
|
74
|
+
stdio: "inherit", shell: false, windowsHide: true, env,
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
resolveExit(spawnFailure(error));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const forwarded = platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
81
|
+
const handlers = forwarded.map(signal => {
|
|
82
|
+
const handler = () => { try { child.kill(signal); } catch { /* child already exited */ } };
|
|
83
|
+
parent.on(signal, handler);
|
|
84
|
+
return [signal, handler];
|
|
85
|
+
});
|
|
86
|
+
const clear = () => { for (const [signal, handler] of handlers) parent.removeListener(signal, handler); };
|
|
87
|
+
let done = false;
|
|
88
|
+
const finish = result => { if (done) return; done = true; clear(); resolveExit(result); };
|
|
89
|
+
child.once("error", error => finish(spawnFailure(error)));
|
|
90
|
+
child.once("exit", (code, signal) => finish({ kind: "exit", code: code ?? 1, signal }));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const DESKTOP_CLI_RECORD_MAX_BYTES: number;
|
|
4
|
+
export type DesktopCliRecord = Readonly<{
|
|
5
|
+
platform: "darwin" | "win32" | "linux";
|
|
6
|
+
kind: "macos-app" | "windows-install" | "linux-deb";
|
|
7
|
+
cliExecutable: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type DesktopCliRecordIssue = "record-invalid" | "record-too-large" | "record-pending" | "record-unreadable" | "record-unsafe";
|
|
10
|
+
export type DesktopCliRecordRead =
|
|
11
|
+
| { state: "missing"; path: string }
|
|
12
|
+
| { state: "disabled"; path: string; cleanupPending: boolean }
|
|
13
|
+
| { state: "ready"; path: string; record: DesktopCliRecord }
|
|
14
|
+
| { state: "invalid" | "unreadable"; path: string; issue: DesktopCliRecordIssue };
|
|
15
|
+
export type DesktopCliRecordOptions = {
|
|
16
|
+
platform?: NodeJS.Platform;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
home?: string;
|
|
19
|
+
recordPath?: string;
|
|
20
|
+
};
|
|
21
|
+
export function desktopCliRecordPath(options?: DesktopCliRecordOptions): string;
|
|
22
|
+
export type DesktopCliRecordDeps = {
|
|
23
|
+
lstat?: (path: string) => Stats;
|
|
24
|
+
fstat?: (fd: number) => Stats;
|
|
25
|
+
open?: (path: string, flags: number) => number;
|
|
26
|
+
euid?: number;
|
|
27
|
+
checkAcl?: (directory: string, path: string) => boolean;
|
|
28
|
+
};
|
|
29
|
+
export function readDesktopCliRecord(options?: DesktopCliRecordOptions, deps?: DesktopCliRecordDeps): DesktopCliRecordRead;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, posix, resolve, win32 } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const DESKTOP_CLI_RECORD_MAX_BYTES = 64 * 1024;
|
|
7
|
+
|
|
8
|
+
export function desktopCliRecordPath(options = {}) {
|
|
9
|
+
const platform = options.platform ?? process.platform;
|
|
10
|
+
const env = options.env ?? process.env;
|
|
11
|
+
const home = options.home ?? (platform === "win32" ? env.USERPROFILE || homedir() : homedir());
|
|
12
|
+
return (platform === "win32" ? win32 : posix).join(home, ".opencodex-desktop", "cli.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Keep these lexical rules aligned with Rust's absolute_for in cli_command_record.rs.
|
|
16
|
+
function validTargetPath(path, platform) {
|
|
17
|
+
if ([...path].length > 4096 || /[\x00\r\n]/.test(path)) return false;
|
|
18
|
+
const absolute = platform === "win32"
|
|
19
|
+
? /^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\") || path.startsWith("//")
|
|
20
|
+
: path.startsWith("/");
|
|
21
|
+
return absolute && !path.split(platform === "win32" ? /[/\\]/ : /\//)
|
|
22
|
+
.some(part => part === "." || part === "..");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function noMacAcl(directory, path) {
|
|
26
|
+
const result = spawnSync("/bin/ls", ["-lde", directory, path], {
|
|
27
|
+
encoding: "utf8", timeout: 1000, shell: false, windowsHide: true,
|
|
28
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
29
|
+
});
|
|
30
|
+
if (result.error || result.status !== 0 || result.stderr) return false;
|
|
31
|
+
// Each path has one metadata line; ACL entries add numbered lines and a "+" marker.
|
|
32
|
+
const lines = result.stdout.trimEnd().split("\n");
|
|
33
|
+
return lines.length === 2 && lines.every(line => /^[d-][rwxStTs-]{9}[.@]?\s/.test(line));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readDesktopCliRecord(options = {}, deps = {}) {
|
|
37
|
+
const platform = options.platform ?? process.platform;
|
|
38
|
+
const path = options.recordPath ?? desktopCliRecordPath(options);
|
|
39
|
+
const directory = dirname(resolve(path));
|
|
40
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
41
|
+
const fstat = deps.fstat ?? fstatSync;
|
|
42
|
+
const open = deps.open ?? openSync;
|
|
43
|
+
const unsafe = () => ({ state: "invalid", path, issue: "record-unsafe" });
|
|
44
|
+
const posixHost = platform !== "win32";
|
|
45
|
+
let fd;
|
|
46
|
+
let checkingSafety = true;
|
|
47
|
+
let absenceAllowed = true;
|
|
48
|
+
try {
|
|
49
|
+
const dirInfo = lstat(directory);
|
|
50
|
+
if (dirInfo.isSymbolicLink() || !dirInfo.isDirectory()) return unsafe();
|
|
51
|
+
const uid = posixHost ? deps.euid ?? process.geteuid?.() : undefined;
|
|
52
|
+
if (posixHost && (uid === undefined || dirInfo.uid !== uid || (dirInfo.mode & 0o077) !== 0)) return unsafe();
|
|
53
|
+
const fileInfo = lstat(path);
|
|
54
|
+
absenceAllowed = false;
|
|
55
|
+
if (fileInfo.isSymbolicLink() || !fileInfo.isFile()
|
|
56
|
+
|| (posixHost && (fileInfo.uid !== uid || (fileInfo.mode & 0o077) !== 0))) return unsafe();
|
|
57
|
+
const flags = constants.O_RDONLY | (posixHost ? constants.O_NOFOLLOW | constants.O_NONBLOCK : 0);
|
|
58
|
+
fd = open(path, flags);
|
|
59
|
+
const info = fstat(fd);
|
|
60
|
+
if (!info.isFile() || ["dev", "ino", "uid", "mode"].some(key => info[key] !== fileInfo[key])) return unsafe();
|
|
61
|
+
if (platform === "darwin" && !(deps.checkAcl ?? noMacAcl)(directory, resolve(path))) return unsafe();
|
|
62
|
+
checkingSafety = false;
|
|
63
|
+
if (info.size > DESKTOP_CLI_RECORD_MAX_BYTES) {
|
|
64
|
+
return { state: "invalid", path, issue: "record-too-large" };
|
|
65
|
+
}
|
|
66
|
+
const bytes = Buffer.alloc(DESKTOP_CLI_RECORD_MAX_BYTES + 1);
|
|
67
|
+
let used = 0;
|
|
68
|
+
while (used < bytes.length) {
|
|
69
|
+
const n = readSync(fd, bytes, used, bytes.length - used, null);
|
|
70
|
+
if (n === 0) break;
|
|
71
|
+
used += n;
|
|
72
|
+
}
|
|
73
|
+
if (used > DESKTOP_CLI_RECORD_MAX_BYTES) {
|
|
74
|
+
return { state: "invalid", path, issue: "record-too-large" };
|
|
75
|
+
}
|
|
76
|
+
let value;
|
|
77
|
+
try { value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, used))); }
|
|
78
|
+
catch { return { state: "invalid", path, issue: "record-invalid" }; }
|
|
79
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
80
|
+
|| value.version !== 1 || typeof value.enabled !== "boolean") {
|
|
81
|
+
return { state: "invalid", path, issue: "record-invalid" };
|
|
82
|
+
}
|
|
83
|
+
if (!value.enabled) return { state: "disabled", path, cleanupPending: value.pending != null };
|
|
84
|
+
if (value.pending != null) return { state: "invalid", path, issue: "record-pending" };
|
|
85
|
+
const bundle = value.bundle;
|
|
86
|
+
const kind = { darwin: "macos-app", win32: "windows-install", linux: "linux-deb" }[platform];
|
|
87
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)
|
|
88
|
+
|| !kind || bundle.platform !== platform || bundle.kind !== kind
|
|
89
|
+
|| typeof bundle.cliExecutable !== "string" || !validTargetPath(bundle.cliExecutable, platform)) {
|
|
90
|
+
return { state: "invalid", path, issue: "record-invalid" };
|
|
91
|
+
}
|
|
92
|
+
return { state: "ready", path, record: {
|
|
93
|
+
platform, kind, cliExecutable: bundle.cliExecutable,
|
|
94
|
+
} };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return error?.code === "ENOENT" && absenceAllowed
|
|
97
|
+
? { state: "missing", path }
|
|
98
|
+
: checkingSafety ? unsafe() : { state: "unreadable", path, issue: "record-unreadable" };
|
|
99
|
+
} finally {
|
|
100
|
+
if (fd !== undefined) closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -17,6 +17,7 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
|
|
|
17
17
|
* string for a client to be able to tell this apart from a provider rate limit.
|
|
18
18
|
*/
|
|
19
19
|
export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
|
|
20
|
+
export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
|
|
20
21
|
|
|
21
22
|
/** First nonblank string across the canonical upstream error paths, in priority order. */
|
|
22
23
|
export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
|
|
@@ -357,6 +358,9 @@ export function isUpstreamResetReplayRefusedMessage(text: string): boolean {
|
|
|
357
358
|
|
|
358
359
|
export function classifyError(status: number, type: string, message: string): OcxErrorPayload {
|
|
359
360
|
const text = message.toLowerCase();
|
|
361
|
+
if (type === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
|
|
362
|
+
return { message, type: "server_error", code: SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE };
|
|
363
|
+
}
|
|
360
364
|
if (type === "previous_response_not_found") {
|
|
361
365
|
return { message, type: "invalid_request_error", code: "previous_response_not_found" };
|
|
362
366
|
}
|
|
@@ -639,6 +643,24 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
|
|
|
639
643
|
return { httpStatus, error };
|
|
640
644
|
}
|
|
641
645
|
|
|
646
|
+
/** Narrow structured classes carried through a synthesized bare-error terminal. */
|
|
647
|
+
export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
|
|
648
|
+
{ type: string; code: string; httpStatus: 429 | 503 } | undefined {
|
|
649
|
+
const code = typeof error.code === "string" ? error.code : undefined;
|
|
650
|
+
// An explicit unknown code must not gain a recognized class from its type or copy.
|
|
651
|
+
if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
|
|
652
|
+
if (code === "rate_limit_exceeded" || code === "rate_limit_error"
|
|
653
|
+
|| (code === undefined && error.type === "rate_limit_error")) {
|
|
654
|
+
return { type: "rate_limit_error", code: code ?? "rate_limit_exceeded", httpStatus: 429 };
|
|
655
|
+
}
|
|
656
|
+
if (code === "server_is_overloaded" || code === "overloaded_error"
|
|
657
|
+
|| (code === undefined && error.type === "overloaded_error")) {
|
|
658
|
+
return { type: error.type === "overloaded_error" ? "overloaded_error" : "server_error",
|
|
659
|
+
code: code ?? "server_is_overloaded", httpStatus: 503 };
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
642
664
|
/** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
|
|
643
665
|
export function httpStatusFromTerminalError(error: {
|
|
644
666
|
type?: string;
|
|
@@ -646,10 +668,13 @@ export function httpStatusFromTerminalError(error: {
|
|
|
646
668
|
message?: string;
|
|
647
669
|
} | undefined): number {
|
|
648
670
|
if (!error) return 502;
|
|
671
|
+
if (error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) return 503;
|
|
649
672
|
if (error.code === "client_closed_request" || error.code === "client_cancelled") return 499;
|
|
650
673
|
if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
|
|
651
674
|
return 400;
|
|
652
675
|
}
|
|
676
|
+
const recognized = recognizedUpstreamError(error);
|
|
677
|
+
if (recognized) return recognized.httpStatus;
|
|
653
678
|
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
654
679
|
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
655
680
|
if (
|
package/src/lib/redact.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const REDACTED_SECRET = "[REDACTED]";
|
|
|
7
7
|
* credentials over an unsafe channel (e.g. plaintext non-loopback HTTP) rather than
|
|
8
8
|
* re-deriving a narrower local list.
|
|
9
9
|
*/
|
|
10
|
-
export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
|
|
10
|
+
export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Colon-labelled credential headers echoed back inside an error body
|
|
@@ -40,7 +40,7 @@ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cook
|
|
|
40
40
|
// Every letter position also accepts \u0001, the placeholder the fold emits for
|
|
41
41
|
// an unresolved HTML named reference: `authorⅈzation` is the label with one
|
|
42
42
|
// character we cannot name, and that is still the label.
|
|
43
|
-
const CREDENTIAL_HEADER_LABEL_RAW = "x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
43
|
+
const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
44
44
|
|
|
45
45
|
const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW
|
|
46
46
|
.replace(/(?<![\[\\])([A-Za-z])(?![\]\-])/g, "[$1\u0001]");
|
|
@@ -194,6 +194,8 @@ export interface RequestExecutionBudget extends TransientSendBudget {
|
|
|
194
194
|
readonly alternateTargetSends: number;
|
|
195
195
|
readonly targetTransitions: number;
|
|
196
196
|
readonly lastTargetKey: string | undefined;
|
|
197
|
+
/** Seed a fresh derived scope with the actual endpoint of its already-paid initial send. */
|
|
198
|
+
bindPrepaidTarget?(targetKey: string): void;
|
|
197
199
|
/**
|
|
198
200
|
* Spend one operator-granted replacement for an AMBIGUOUS failure of this logical request,
|
|
199
201
|
* up to `limit`. False once the request has none left.
|
|
@@ -390,6 +392,7 @@ function createAmbiguousResendGrant(): Pick<SharedSendLedger, "claimAmbiguousRes
|
|
|
390
392
|
};
|
|
391
393
|
}
|
|
392
394
|
|
|
395
|
+
/** Keep target-local transition state over the shared ledger and its single-use bookings. */
|
|
393
396
|
function createRequestExecutionBudgetWithLedger(
|
|
394
397
|
policy: RequestExecutionBudgetPolicy,
|
|
395
398
|
logicalRequestId: string | undefined,
|
|
@@ -441,6 +444,11 @@ function createRequestExecutionBudgetWithLedger(
|
|
|
441
444
|
get alternateTargetSends() { return alternateTargetSends; },
|
|
442
445
|
get targetTransitions() { return targetTransitions; },
|
|
443
446
|
get lastTargetKey() { return lastTargetKey; },
|
|
447
|
+
/** Seed only a fresh scope; never reset recovery history on an existing target. */
|
|
448
|
+
bindPrepaidTarget(targetKey: string): void {
|
|
449
|
+
// Only a fresh scope may bind: this is initial identity, never a recovery rebase.
|
|
450
|
+
if (lastTargetKey === undefined) lastTargetKey = targetKey;
|
|
451
|
+
},
|
|
444
452
|
remainingBaseSends(cap: number): number {
|
|
445
453
|
const capped = Number.isFinite(cap) ? Math.trunc(cap) : 0;
|
|
446
454
|
return Math.max(0, Math.min(capped, policy.baseSendAllowance - counter.spent));
|