@bitkyc08/opencodex 2.68.0-preview.20260927 → 2.69.0-preview.20260928
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 +25 -17
- package/gui/dist/assets/App-cDPyCb_V.js +51 -0
- package/gui/dist/assets/{Tray-DFnMiyD0.js → Tray-H1IAB8mN.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CI6YTp8z.js +86 -0
- package/gui/dist/assets/{quota-summary-i84eOU3o.js → quota-summary-CutOlPu2.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-B5-cAEd2.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-BcXblvem.js +0 -86
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createHash, X509Certificate } from "node:crypto";
|
|
2
|
-
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { createHash, randomUUID, X509Certificate } from "node:crypto";
|
|
2
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { withClientLifecycleSync } from "../../client/lifecycle-lock";
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
ensurePersistedAuthority,
|
|
6
|
+
createCertificateAuthority,
|
|
7
7
|
issueServerLeaf,
|
|
8
8
|
type LocalInterceptCa,
|
|
9
9
|
type PemKeyPair,
|
|
@@ -15,10 +15,18 @@ export const PICKER_CA_COMMON_NAME = "opencodex Claude Desktop Picker CA";
|
|
|
15
15
|
export const PICKER_STATE_DIR = "claude-picker";
|
|
16
16
|
|
|
17
17
|
export interface PickerCa extends LocalInterceptCa { fingerprint: string }
|
|
18
|
+
export interface PendingPickerCaUntrust { certPem: string; sha1: string; sha256: string }
|
|
19
|
+
|
|
20
|
+
const processAuthorities = new Map<string, PickerCa>();
|
|
21
|
+
const MAX_PENDING_CA_BYTES = 64 * 1024;
|
|
22
|
+
const LEGACY_OWNER_MTIME_TOLERANCE_MS = 2_000;
|
|
18
23
|
|
|
19
24
|
export function pickerStateDir(configDir: string): string { return join(configDir, PICKER_STATE_DIR); }
|
|
20
25
|
export function pickerCaCertPath(configDir: string): string { return join(pickerStateDir(configDir), "ca.pem"); }
|
|
21
26
|
export function pickerLeafCertPath(configDir: string): string { return join(pickerStateDir(configDir), "leaf.pem"); }
|
|
27
|
+
export function pickerCaOwnerPath(configDir: string): string { return join(pickerStateDir(configDir), "ca-owner.json"); }
|
|
28
|
+
export function pickerCaPendingUntrustPath(configDir: string): string { return join(pickerStateDir(configDir), "pending-untrust.json"); }
|
|
29
|
+
function pickerCaLockPath(configDir: string): string { return join(pickerStateDir(configDir), "ca.lock.sqlite"); }
|
|
22
30
|
|
|
23
31
|
export function pickerCaFingerprints(certPem: string): { sha1: string; sha256: string } {
|
|
24
32
|
const der = new X509Certificate(certPem).raw;
|
|
@@ -28,99 +36,291 @@ export function pickerCaFingerprints(certPem: string): { sha1: string; sha256: s
|
|
|
28
36
|
};
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
/** Atomically publish a public certificate; these files carry no key material. */
|
|
40
|
+
function publishPem(path: string, pem: string): void {
|
|
41
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
42
|
+
try {
|
|
43
|
+
writeFileSync(tmp, pem, { flag: "wx", mode: 0o644 });
|
|
44
|
+
try { chmodSync(tmp, 0o644); } catch { /* best-effort on platforms without POSIX modes */ }
|
|
45
|
+
renameSync(tmp, path);
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(tmp, { force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Publish this process's authority and record which pid owns it, so another process that shares
|
|
53
|
+
* the config directory can distinguish "the file went missing or stale" from "a live peer rotated
|
|
54
|
+
* the authority" — only the former may be overwritten.
|
|
55
|
+
*/
|
|
56
|
+
function publishAuthority(configDir: string, ca: PickerCa): void {
|
|
57
|
+
publishPem(pickerCaCertPath(configDir), ca.certPem);
|
|
58
|
+
publishOwner(configDir, ca);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function publishOwner(configDir: string, ca: PickerCa): void {
|
|
62
|
+
publishPem(pickerCaOwnerPath(configDir), JSON.stringify({
|
|
63
|
+
pid: process.pid,
|
|
64
|
+
startTime: processStartIdentity(process.pid),
|
|
65
|
+
sha256: ca.fingerprint,
|
|
66
|
+
}) + "\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function currentProcessOwnsPublishedCa(configDir: string, ca: PickerCa): boolean {
|
|
70
|
+
try {
|
|
71
|
+
const owner = JSON.parse(readFileSync(pickerCaOwnerPath(configDir), "utf8")) as Record<string, unknown>;
|
|
72
|
+
return owner.pid === process.pid && owner.sha256 === ca.fingerprint
|
|
73
|
+
&& owner.startTime === processStartIdentity(process.pid);
|
|
74
|
+
} catch { return false; }
|
|
75
|
+
}
|
|
32
76
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for (let i = 0; i < width; i++) length = length * 256 + bytes[cursor++]!;
|
|
77
|
+
/** The OS process start identity prevents a recycled PID from impersonating the recorded owner. */
|
|
78
|
+
function processStartIdentity(pid: number, utc = false): string | null {
|
|
79
|
+
if (process.platform === "linux") {
|
|
80
|
+
try {
|
|
81
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
82
|
+
const afterCommand = stat.lastIndexOf(") ");
|
|
83
|
+
const ticks = afterCommand < 0 ? undefined : stat.slice(afterCommand + 2).trim().split(/\s+/)[19];
|
|
84
|
+
return ticks && /^\d+$/.test(ticks) ? ticks : null;
|
|
85
|
+
} catch { return null; }
|
|
43
86
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
87
|
+
if (process.platform === "darwin") {
|
|
88
|
+
try {
|
|
89
|
+
const result = Bun.spawnSync(["/bin/ps", "-o", "lstart=", "-p", String(pid)], {
|
|
90
|
+
stdin: "ignore", stdout: "pipe", stderr: "ignore",
|
|
91
|
+
...(utc ? { env: { ...process.env, TZ: "UTC" } } : {}),
|
|
92
|
+
});
|
|
93
|
+
const value = result.stdout.toString().trim();
|
|
94
|
+
return result.exitCode === 0 && value.length > 0 && value.length <= 128 ? value : null;
|
|
95
|
+
} catch { return null; }
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Darwin's lstart is local wall-clock time; Linux's /proc start ticks are not comparable to mtime. */
|
|
101
|
+
function processStartEpochMs(pid: number): number | null {
|
|
102
|
+
if (process.platform !== "darwin") return null;
|
|
103
|
+
const identity = processStartIdentity(pid, true);
|
|
104
|
+
if (identity === null) return null;
|
|
105
|
+
const epoch = Date.parse(`${identity} UTC`);
|
|
106
|
+
return Number.isFinite(epoch) ? epoch : null;
|
|
46
107
|
}
|
|
47
108
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
109
|
+
function processAlive(pid: number): boolean {
|
|
110
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
111
|
+
if (pid === process.pid) return true;
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 0);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// EPERM means the process exists but is not signallable by us; ESRCH means it is gone.
|
|
117
|
+
return error !== null && typeof error === "object" && (error as { code?: unknown }).code === "EPERM";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* True when the published certificate belongs to a live process's authority. The owner
|
|
123
|
+
* record is only trusted while it describes the certificate actually on disk: a stale or
|
|
124
|
+
* third-party `ca-owner.json` cannot shield a file that was tampered with after the owner wrote it.
|
|
125
|
+
*/
|
|
126
|
+
function livePublishedOwner(configDir: string, published: string): boolean {
|
|
127
|
+
try {
|
|
128
|
+
const owner = JSON.parse(readFileSync(pickerCaOwnerPath(configDir), "utf8")) as unknown;
|
|
129
|
+
if (owner === null || typeof owner !== "object") return false;
|
|
130
|
+
const { pid, sha256, startTime } = owner as { pid?: unknown; sha256?: unknown; startTime?: unknown };
|
|
131
|
+
if (typeof pid !== "number" || typeof sha256 !== "string") return false;
|
|
132
|
+
if (sha256 !== pickerCaFingerprints(published).sha256) return false;
|
|
133
|
+
if (!processAlive(pid)) return false;
|
|
134
|
+
// Older records have no start identity. On Darwin, wall-clock start after the owner
|
|
135
|
+
// file proves PID reuse; otherwise preserve the conservative live-owner decision.
|
|
136
|
+
if (startTime === undefined || startTime === null) {
|
|
137
|
+
const startedAt = processStartEpochMs(pid);
|
|
138
|
+
if (startedAt === null) return true;
|
|
139
|
+
let mtime: number;
|
|
140
|
+
try { mtime = statSync(pickerCaOwnerPath(configDir)).mtimeMs; }
|
|
141
|
+
catch { return true; }
|
|
142
|
+
return !Number.isFinite(mtime) || startedAt <= mtime + LEGACY_OWNER_MTIME_TOLERANCE_MS;
|
|
143
|
+
}
|
|
144
|
+
if (typeof startTime !== "string" || startTime.length === 0 || startTime.length > 128) return false;
|
|
145
|
+
const actual = processStartIdentity(pid);
|
|
146
|
+
return actual === null || actual === startTime;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function publicCertificate(pem: string): PendingPickerCaUntrust | null {
|
|
153
|
+
try {
|
|
154
|
+
const certPem = new X509Certificate(pem).toString();
|
|
155
|
+
return { certPem, ...pickerCaFingerprints(certPem) };
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The pending record is public-only, but malformed or unsafe state must never be discarded. */
|
|
162
|
+
export function readPendingPickerCaUntrust(configDir: string): PendingPickerCaUntrust | null {
|
|
163
|
+
const path = pickerCaPendingUntrustPath(configDir);
|
|
164
|
+
let stat;
|
|
165
|
+
try { stat = lstatSync(path); }
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error !== null && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") return null;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_PENDING_CA_BYTES) {
|
|
171
|
+
throw new Error("picker_ca_pending_untrust_unsafe");
|
|
172
|
+
}
|
|
173
|
+
const raw = readFileSync(path, "utf8");
|
|
174
|
+
if (Buffer.byteLength(raw) > MAX_PENDING_CA_BYTES) throw new Error("picker_ca_pending_untrust_unsafe");
|
|
175
|
+
let value: unknown;
|
|
176
|
+
try { value = JSON.parse(raw); }
|
|
177
|
+
catch { throw new Error("picker_ca_pending_untrust_invalid"); }
|
|
178
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
179
|
+
throw new Error("picker_ca_pending_untrust_invalid");
|
|
55
180
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
181
|
+
const pending = value as Partial<PendingPickerCaUntrust>;
|
|
182
|
+
if (Object.keys(pending).sort().join(",") !== "certPem,sha1,sha256"
|
|
183
|
+
|| typeof pending.certPem !== "string" || typeof pending.sha1 !== "string" || typeof pending.sha256 !== "string") {
|
|
184
|
+
throw new Error("picker_ca_pending_untrust_invalid");
|
|
185
|
+
}
|
|
186
|
+
const publicOnly = publicCertificate(pending.certPem);
|
|
187
|
+
if (!publicOnly || pending.certPem !== publicOnly.certPem
|
|
188
|
+
|| pending.sha1 !== publicOnly.sha1 || pending.sha256 !== publicOnly.sha256) {
|
|
189
|
+
throw new Error("picker_ca_pending_untrust_invalid");
|
|
190
|
+
}
|
|
191
|
+
return publicOnly;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function writePendingPickerCaUntrust(configDir: string, pending: PendingPickerCaUntrust): void {
|
|
195
|
+
const path = pickerCaPendingUntrustPath(configDir);
|
|
196
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
197
|
+
try {
|
|
198
|
+
writeFileSync(temporary, JSON.stringify(pending) + "\n", { flag: "wx", mode: 0o600 });
|
|
199
|
+
try { chmodSync(temporary, 0o600); } catch { /* best-effort on platforms without POSIX modes */ }
|
|
200
|
+
renameSync(temporary, path);
|
|
201
|
+
} finally {
|
|
202
|
+
rmSync(temporary, { force: true });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A lock failure is separate from a successful void callback. Once the callback entered, an
|
|
208
|
+
* exception (including lock release failure) propagates: its effects may be partial.
|
|
209
|
+
*/
|
|
210
|
+
function underPickerCaLock<T>(configDir: string, check: () => T):
|
|
211
|
+
| { kind: "acquired"; value: T }
|
|
212
|
+
| { kind: "unavailable"; error: unknown } {
|
|
213
|
+
let entered = false;
|
|
214
|
+
try {
|
|
215
|
+
const value = withClientLifecycleSync(() => {
|
|
216
|
+
entered = true;
|
|
217
|
+
return check();
|
|
218
|
+
}, { lockPath: pickerCaLockPath(configDir) });
|
|
219
|
+
return { kind: "acquired", value };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (entered) throw error;
|
|
222
|
+
return { kind: "unavailable", error };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function lockedPickerCa<T>(configDir: string, check: () => T): T {
|
|
227
|
+
const result = underPickerCaLock(configDir, check);
|
|
228
|
+
if (result.kind === "unavailable") throw result.error;
|
|
229
|
+
return result.value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function publishedPickerCa(configDir: string): string | null {
|
|
233
|
+
try { return readFileSync(pickerCaCertPath(configDir), "utf8"); }
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error !== null && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") return null;
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A pending CA may still be serving when journal publication preceded a failed PEM replacement. */
|
|
241
|
+
export function pendingPickerCaHasLivePublishedOwner(configDir: string, pending: PendingPickerCaUntrust): boolean {
|
|
242
|
+
return lockedPickerCa(configDir, () => {
|
|
243
|
+
const current = readPendingPickerCaUntrust(configDir);
|
|
244
|
+
if (!current || current.sha1 !== pending.sha1
|
|
245
|
+
|| current.sha256 !== pending.sha256 || current.certPem !== pending.certPem) {
|
|
246
|
+
throw new Error("picker_ca_pending_untrust_changed");
|
|
247
|
+
}
|
|
248
|
+
const published = publishedPickerCa(configDir);
|
|
249
|
+
return published !== null && publicCertificate(published)?.sha256 === pending.sha256
|
|
250
|
+
&& livePublishedOwner(configDir, published);
|
|
72
251
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Acknowledgement never clears an entry another process created or replaced. */
|
|
255
|
+
export function acknowledgePendingPickerCaUntrust(
|
|
256
|
+
configDir: string,
|
|
257
|
+
pending: PendingPickerCaUntrust,
|
|
258
|
+
confirmedUntrust: { ok: boolean },
|
|
259
|
+
): boolean {
|
|
260
|
+
if (confirmedUntrust?.ok !== true) return false;
|
|
261
|
+
return lockedPickerCa(configDir, () => {
|
|
262
|
+
const current = readPendingPickerCaUntrust(configDir);
|
|
263
|
+
if (!current || current.certPem !== pending.certPem
|
|
264
|
+
|| current.sha1 !== pending.sha1 || current.sha256 !== pending.sha256) return false;
|
|
265
|
+
rmSync(pickerCaPendingUntrustPath(configDir));
|
|
266
|
+
return true;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Drop the legacy exportable signing key, if one exists in the picker state dir. Releases before
|
|
272
|
+
* the process-scoped authority persisted `ca.key` next to `ca.pem`; the removal is deliberately
|
|
273
|
+
* unconditional so a later failed rotation can never leave that key behind.
|
|
274
|
+
*/
|
|
275
|
+
export function discardPickerCaKey(configDir: string): void {
|
|
276
|
+
rmSync(join(pickerStateDir(configDir), "ca.key"), { force: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function ensurePickerCa(configDir: string, options: { rotation?: "startup" } = {}): PickerCa {
|
|
280
|
+
const dir = pickerStateDir(configDir);
|
|
281
|
+
// The signing key must never survive this process: another process under the same user could
|
|
282
|
+
// otherwise steal it and later take over the predictable loopback proxy. Drop a key left (or
|
|
283
|
+
// restored) by an older release on every call, including cache hits.
|
|
284
|
+
discardPickerCaKey(configDir);
|
|
285
|
+
const cached = processAuthorities.get(dir);
|
|
286
|
+
const pickerCa = cached ?? (() => {
|
|
287
|
+
const ca = createCertificateAuthority({ commonName: PICKER_CA_COMMON_NAME, permittedDnsNames: [PICKER_HOST] });
|
|
288
|
+
return { ...ca, fingerprint: pickerCaFingerprints(ca.certPem).sha256 };
|
|
289
|
+
})();
|
|
290
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
291
|
+
lockedPickerCa(configDir, () => {
|
|
292
|
+
if (readPendingPickerCaUntrust(configDir)) throw new Error("picker_ca_pending_untrust");
|
|
293
|
+
const published = publishedPickerCa(configDir);
|
|
294
|
+
if (published === pickerCa.certPem) {
|
|
295
|
+
// The signing key is still in this process. A missing/stale owner file must not let a
|
|
296
|
+
// second process rotate this live authority after an otherwise harmless cached ensure.
|
|
297
|
+
if (!currentProcessOwnsPublishedCa(configDir, pickerCa)) publishOwner(configDir, pickerCa);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (published !== null && livePublishedOwner(configDir, published)) {
|
|
301
|
+
throw new Error("picker_ca_live_owner");
|
|
302
|
+
}
|
|
303
|
+
const outgoing = published === null ? null : publicCertificate(published);
|
|
304
|
+
if (outgoing && outgoing.sha256 !== pickerCa.fingerprint) {
|
|
305
|
+
if (options.rotation !== "startup") throw new Error("picker_ca_rotation_requires_startup");
|
|
306
|
+
writePendingPickerCaUntrust(configDir, outgoing);
|
|
307
|
+
}
|
|
308
|
+
publishAuthority(configDir, pickerCa);
|
|
98
309
|
});
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
return !!fields && fields.length === 3
|
|
102
|
-
&& fields[1]!.tag === 0x01 && fields[1]!.body.equals(Buffer.from([0xff]))
|
|
103
|
-
&& fields[2]!.tag === 0x04 && constrainedToPickerHost(fields[2]!.body);
|
|
310
|
+
if (!cached) processAuthorities.set(dir, pickerCa);
|
|
311
|
+
return pickerCa;
|
|
104
312
|
}
|
|
105
313
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"ca-publication.sqlite",
|
|
111
|
-
acceptsPickerAuthority,
|
|
112
|
-
);
|
|
113
|
-
return { ...ca, fingerprint: pickerCaFingerprints(ca.certPem).sha256 };
|
|
314
|
+
/** Fingerprint of this process's authority only — null until ensurePickerCa has run. */
|
|
315
|
+
export function publishedPickerCaSha256(configDir: string): string | null {
|
|
316
|
+
const cached = processAuthorities.get(pickerStateDir(configDir));
|
|
317
|
+
return cached ? pickerCaFingerprints(cached.certPem).sha256 : null;
|
|
114
318
|
}
|
|
115
319
|
|
|
116
320
|
/** Persist only the public leaf, so trust inspection verifies this exact local issuer. */
|
|
117
321
|
export function issuePickerLeaf(ca: PickerCa, configDir: string): PemKeyPair {
|
|
118
322
|
const leaf = issueServerLeaf(ca, PICKER_CA_COMMON_NAME, [PICKER_HOST]);
|
|
119
|
-
const path = pickerLeafCertPath(configDir);
|
|
120
323
|
mkdirSync(pickerStateDir(configDir), { recursive: true, mode: 0o700 });
|
|
121
|
-
|
|
122
|
-
writeFileSync(tmp, leaf.certPem, { mode: 0o644 });
|
|
123
|
-
try { chmodSync(tmp, 0o644); } catch { /* best-effort on platforms without POSIX modes */ }
|
|
124
|
-
renameSync(tmp, path);
|
|
324
|
+
publishPem(pickerLeafCertPath(configDir), leaf.certPem);
|
|
125
325
|
return leaf;
|
|
126
326
|
}
|
|
@@ -7,6 +7,7 @@ import { dirname } from "node:path";
|
|
|
7
7
|
import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../../codex/catalog";
|
|
8
8
|
import type { OcxClaudeDesktopProfile } from "../../types";
|
|
9
9
|
import { aliasForRoute, claudeCodeNativeAlias } from "../alias";
|
|
10
|
+
import { AUTO_CONTEXT_OFF, withOneMillionMarker } from "../context-windows";
|
|
10
11
|
import { displayModelId, type Desktop3pRoutedModel } from "../desktop-3p";
|
|
11
12
|
import { reconcileDesktopProfile, renderDesktopProfile, type DesktopProfileModel } from "../desktop-profile";
|
|
12
13
|
import type { PickerModelEntry } from "./picker-bootstrap";
|
|
@@ -47,7 +48,9 @@ export function buildPickerModels(input: PickerRouteInput): PickerModelEntry[] {
|
|
|
47
48
|
const alias = provider === "native" ? claudeCodeNativeAlias(id) : aliasForRoute(provider, id);
|
|
48
49
|
if (!alias || seen.has(alias)) continue;
|
|
49
50
|
seen.add(alias);
|
|
50
|
-
|
|
51
|
+
// Desktop runners do not inherit the proxy's compaction env: mark only real >=1M windows.
|
|
52
|
+
const selector = withOneMillionMarker(alias, model.contextWindow === undefined ? {} : { [alias]: model.contextWindow }, AUTO_CONTEXT_OFF)!;
|
|
53
|
+
out.push({ id: selector, name: model.label,
|
|
51
54
|
...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }) });
|
|
52
55
|
}
|
|
53
56
|
return out;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { PICKER_CA_COMMON_NAME, PICKER_HOST } from "./picker-ca";
|
|
@@ -84,17 +84,30 @@ export async function trustPickerCa(
|
|
|
84
84
|
caPath: string,
|
|
85
85
|
run: SecurityRunner = defaultSecurityRunner,
|
|
86
86
|
platform: NodeJS.Platform = process.platform,
|
|
87
|
+
cert?: { pem?: string },
|
|
87
88
|
): Promise<{ ok: boolean; reason?: "unsupported" | "declined_or_failed" }> {
|
|
88
89
|
if (platform !== "darwin") return { ok: false, reason: "unsupported" };
|
|
90
|
+
// `cert.pem` is the caller-verified certificate bytes: the shared on-disk ca.pem is writable by
|
|
91
|
+
// same-user processes and could be swapped between fingerprint inspection and this install, so
|
|
92
|
+
// the keychain command reads a private copy under a fresh 0700 directory instead.
|
|
93
|
+
let installPath = caPath;
|
|
94
|
+
let privateDir: string | undefined;
|
|
95
|
+
if (cert?.pem !== undefined) {
|
|
96
|
+
privateDir = mkdtempSync(join(tmpdir(), "ocx-picker-ca-"));
|
|
97
|
+
installPath = join(privateDir, "ca.pem");
|
|
98
|
+
writeFileSync(installPath, cert.pem, { mode: 0o600 });
|
|
99
|
+
}
|
|
89
100
|
try {
|
|
90
101
|
// No `-s <host>` policy string: Chromium (Claude Desktop) skips trust settings that carry one,
|
|
91
102
|
// so a host-scoped setting leaves Desktop rejecting the picker leaf. The CA's critical name
|
|
92
103
|
// constraints already limit it to claude.ai; macOS verify-cert rejects any other name.
|
|
93
104
|
const result = await run(["add-trusted-cert", "-r", "trustRoot", "-p", "ssl",
|
|
94
|
-
"-k", loginKeychainPath(),
|
|
105
|
+
"-k", loginKeychainPath(), installPath]);
|
|
95
106
|
return result.code === 0 ? { ok: true } : { ok: false, reason: "declined_or_failed" };
|
|
96
107
|
} catch { // no-excuse-ok: catch -- user decline and command failure share a safe result.
|
|
97
108
|
return { ok: false, reason: "declined_or_failed" };
|
|
109
|
+
} finally {
|
|
110
|
+
if (privateDir) rmSync(privateDir, { recursive: true, force: true });
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
113
|
|