@bitkyc08/opencodex 2.78.0 → 2.80.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.
Files changed (91) hide show
  1. package/gui/dist/assets/App-CSJvmjNs.css +1 -0
  2. package/gui/dist/assets/App-MbDVfUeC.js +54 -0
  3. package/gui/dist/assets/{Tray-DiJpSub0.js → Tray-CzkxK82A.js} +1 -1
  4. package/gui/dist/assets/{index-DmQB8D_R.css → index-Brk6F3fn.css} +1 -1
  5. package/gui/dist/assets/index-UJt6npXk.js +94 -0
  6. package/gui/dist/assets/{quota-summary-B_a96nXj.js → quota-summary-DU3sD6Z-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +3 -2
  9. package/src/adapters/cursor/live-transport.ts +5 -3
  10. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  11. package/src/adapters/cursor/native-exec.ts +13 -5
  12. package/src/adapters/cursor/protobuf-request.ts +13 -7
  13. package/src/adapters/cursor/request-builder.ts +23 -10
  14. package/src/adapters/cursor/tool-definitions.ts +9 -6
  15. package/src/adapters/cursor/tool-guidance.ts +29 -8
  16. package/src/adapters/cursor/tool-naming.ts +40 -7
  17. package/src/adapters/cursor/tool-wording.ts +46 -0
  18. package/src/adapters/cursor.ts +3 -0
  19. package/src/adapters/devin.ts +37 -2
  20. package/src/adapters/openai-chat/messages.ts +3 -1
  21. package/src/claude/desktop-picker.ts +11 -9
  22. package/src/claude/inbound-content-options.ts +29 -2
  23. package/src/claude/inbound.ts +9 -0
  24. package/src/claude/intercept/local-ca-files.ts +265 -0
  25. package/src/claude/intercept/local-ca-windows.ts +73 -0
  26. package/src/claude/intercept/local-ca.ts +12 -24
  27. package/src/claude/intercept/picker-ca-persistence.ts +106 -0
  28. package/src/claude/intercept/picker-ca-startup.ts +16 -0
  29. package/src/claude/intercept/picker-ca-store.ts +70 -0
  30. package/src/claude/intercept/picker-ca.ts +43 -9
  31. package/src/claude/intercept/picker-runtime.ts +14 -4
  32. package/src/claude/intercept/runtime.ts +32 -23
  33. package/src/cli/ensure-readiness.ts +48 -0
  34. package/src/cli/gui-pair-client.ts +15 -7
  35. package/src/cli/gui.ts +4 -1
  36. package/src/cli/hub.ts +1 -0
  37. package/src/cli/index.ts +10 -11
  38. package/src/cli/status.ts +2 -2
  39. package/src/cli/update-restart-child.ts +4 -1
  40. package/src/codex/account-credit-use.ts +8 -2
  41. package/src/codex/auth-api/account-pause-group.ts +113 -0
  42. package/src/codex/auth-api/routes.ts +18 -12
  43. package/src/codex/auth-context.ts +45 -3
  44. package/src/codex/autostart-health.ts +13 -3
  45. package/src/codex/journal.ts +36 -5
  46. package/src/codex/pool-credit-policy.ts +67 -0
  47. package/src/codex/shim-overlay.ts +9 -1
  48. package/src/combos/jev.ts +3 -1
  49. package/src/generated/compatibility-version.json +135 -63
  50. package/src/lib/gui-pair-capability.ts +1 -1
  51. package/src/lib/gui-pair-intent.ts +170 -0
  52. package/src/lib/windows-owner-acl.ts +143 -0
  53. package/src/providers/openai-sidecar-credit.ts +26 -0
  54. package/src/providers/openai-sidecar.ts +35 -0
  55. package/src/server/claude-messages.ts +1 -0
  56. package/src/server/gui-pair-delivery.ts +23 -0
  57. package/src/server/index/serve-options.ts +5 -8
  58. package/src/server/index.ts +12 -14
  59. package/src/server/live-native-calls.ts +94 -0
  60. package/src/server/live.ts +28 -5
  61. package/src/server/local-management-read-client.ts +15 -11
  62. package/src/server/management/oauth-account-routes.ts +15 -0
  63. package/src/server/management/route-registry.ts +1 -0
  64. package/src/server/management/shared.ts +9 -3
  65. package/src/server/messages-native-scope.ts +60 -0
  66. package/src/server/messages-native.ts +9 -0
  67. package/src/server/responses/adapter-dispatch.ts +31 -2
  68. package/src/server/responses/combo-requested-effort.ts +14 -0
  69. package/src/server/responses/compact.ts +17 -2
  70. package/src/server/responses/core-auth.ts +18 -1
  71. package/src/server/responses/core-codex-account.ts +18 -8
  72. package/src/server/responses/core-combo.ts +5 -7
  73. package/src/server/responses/non-replayable-error.ts +48 -0
  74. package/src/server/responses/passthrough-delivery.ts +5 -1
  75. package/src/server/responses/passthrough-dispatch.ts +21 -19
  76. package/src/server/responses/request-prepare.ts +11 -1
  77. package/src/server/responses/sidecar-execution.ts +1 -0
  78. package/src/server/search.ts +4 -0
  79. package/src/server/startup-health-cache.ts +2 -6
  80. package/src/service/desktop-startup.ts +100 -3
  81. package/src/vision/describe.ts +17 -10
  82. package/src/vision/index.ts +1 -0
  83. package/src/web-search/executor.ts +17 -10
  84. package/src/web-search/format-result.ts +304 -20
  85. package/src/web-search/header-deadline.ts +67 -0
  86. package/src/web-search/loop.ts +7 -6
  87. package/src/web-search/passthrough-bridge.ts +1 -0
  88. package/src/web-search/run-turn-loop.ts +1 -1
  89. package/gui/dist/assets/App-BtfG5id-.js +0 -54
  90. package/gui/dist/assets/App-lCIslZ1L.css +0 -1
  91. package/gui/dist/assets/index-Btc3Ogw4.js +0 -94
@@ -0,0 +1,170 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, rmdirSync, unlinkSync, writeFileSync, type BigIntStats } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getConfigDir } from "../config/paths";
5
+ import { assertNotRealHomeUnderTest } from "./test-home-guard";
6
+ import { forgetEphemeralSecretPath, hardenSecretDir, hardenSecretPath, windowsSecretAclApplies } from "./windows-secret-acl";
7
+
8
+ import { verifyWindowsPrivateEntries, WINDOWS_OWNER_ACL_TIMEOUT_MS } from "./windows-owner-acl";
9
+
10
+ export const GUI_PAIR_INTENT_HEADER = "x-opencodex-gui-pair-intent";
11
+ const TOKEN = /^[A-Za-z0-9_-]{43}$/;
12
+ const INTENT_NAME = /^[0-9a-f]{64}$/;
13
+ // Records outlive their ~10s capability only when the publishing CLI died; a wide
14
+ // margin keeps an active command's fresh record from ever looking stale.
15
+ const STALE_INTENT_MS = 60_000;
16
+ const STALE_INTENT_SWEEP_LIMIT = 256;
17
+ const digest = (value: string) => createHash("sha256").update(value).digest("hex");
18
+
19
+ function owned(stat: BigIntStats): boolean {
20
+ return process.platform === "win32" || (stat.uid === BigInt(process.getuid!()) && (stat.mode & 0o022n) === 0n);
21
+ }
22
+ function same(a: BigIntStats, b: BigIntStats): boolean {
23
+ return a.dev === b.dev && a.ino === b.ino && a.ctimeNs === b.ctimeNs;
24
+ }
25
+ function directory(path: string): BigIntStats {
26
+ const stat = lstatSync(path, { bigint: true });
27
+ if (!stat.isDirectory() || stat.isSymbolicLink() || !owned(stat)) throw new Error("Unsafe GUI pairing intent directory");
28
+ return stat;
29
+ }
30
+ function location(capability: string, configDir: string): { dir: string; path: string } {
31
+ if (!TOKEN.test(capability)) throw new Error("Invalid GUI pairing capability");
32
+ const dir = join(configDir, "gui-pair-intents");
33
+ return { dir, path: join(dir, digest(`opencodex-gui-pair-intent-v1\n${capability}`)) };
34
+ }
35
+ function releaseConsumeLock(path: string, identity: BigIntStats): boolean {
36
+ try {
37
+ if (!same(identity, lstatSync(path, { bigint: true }))) return false;
38
+ rmdirSync(path);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function removeOwned(path: string, identity: BigIntStats): void {
46
+ try {
47
+ if (same(identity, lstatSync(path, { bigint: true }))) unlinkSync(path);
48
+ } catch { /* expired or already consumed; never remove a replacement */ }
49
+ forgetEphemeralSecretPath(path);
50
+ }
51
+
52
+ /** Best-effort stale cleanup; bound per-entry work before creating the next expiring intent. */
53
+ function sweepStaleIntents(dir: string): void {
54
+ let names: string[];
55
+ try { names = readdirSync(dir); } catch { return; }
56
+ const now = Date.now();
57
+ // Count every entry, including foreign and fresh ones, rather than only successful deletes.
58
+ // This caps metadata/deletion work, not readdirSync's array allocation or syscall duration.
59
+ // Bun 1.4.0 also snapshots names behind opendirSync, so an iterator alone is not a memory cap.
60
+ const limit = Math.min(names.length, STALE_INTENT_SWEEP_LIMIT);
61
+ for (let index = 0; index < limit; index++) {
62
+ const name = names[index]!;
63
+ if (!INTENT_NAME.test(name)) continue;
64
+ const path = join(dir, name);
65
+ try {
66
+ const stat = lstatSync(path, { bigint: true });
67
+ if (!stat.isFile() || stat.isSymbolicLink() || !owned(stat) || stat.nlink !== 1n) continue;
68
+ if (now - Number(stat.mtimeMs) < STALE_INTENT_MS) continue;
69
+ removeOwned(path, stat);
70
+ } catch { /* leave unreadable entries alone */ }
71
+ }
72
+ }
73
+
74
+ export interface GuiPairIntent {
75
+ proof: string;
76
+ dispose(): void;
77
+ }
78
+
79
+ /**
80
+ * Prove write access to the private configuration home, separately from runtime-state reads.
81
+ * Only a SHA-256 commitment goes to disk; the random verifier remains in the requesting CLI.
82
+ * This is an owner-write boundary, NOT proof of human presence or protection from a fully
83
+ * privileged same-user process. The existing HMAC binds PID, port, origin, nonce and expiry.
84
+ */
85
+ export function createGuiPairIntent(capability: string, configDir = getConfigDir()): GuiPairIntent {
86
+ assertNotRealHomeUnderTest(configDir);
87
+ directory(configDir);
88
+ const { dir, path } = location(capability, configDir);
89
+ try { mkdirSync(dir, { mode: 0o700 }); }
90
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
91
+ directory(dir);
92
+ if (!hardenSecretDir(dir, { required: true, deadlineMs: 2_000 }).ok) throw new Error("GUI pairing intent ACL refused");
93
+ sweepStaleIntents(dir);
94
+ const parent = directory(dir);
95
+ const proof = randomBytes(32).toString("base64url");
96
+ const fd = openSync(path, "wx", 0o600);
97
+ let identity = fstatSync(fd, { bigint: true });
98
+ try {
99
+ writeFileSync(fd, `${digest(proof)}\n`, "utf8");
100
+ if (!hardenSecretPath(path, { required: true, deadlineMs: 2_000 }).ok) throw new Error("GUI pairing intent ACL refused");
101
+ identity = fstatSync(fd, { bigint: true });
102
+ if (!same(identity, lstatSync(path, { bigint: true })) || !owned(identity)
103
+ || identity.nlink !== 1n || parent.dev !== directory(dir).dev || parent.ino !== directory(dir).ino) {
104
+ throw new Error("GUI pairing intent changed during publication");
105
+ }
106
+ } catch (error) {
107
+ identity = fstatSync(fd, { bigint: true });
108
+ closeSync(fd);
109
+ removeOwned(path, identity);
110
+ throw error;
111
+ }
112
+ closeSync(fd);
113
+ // Disposal consumes the record: the same directory, identity and content checks as
114
+ // redemption, so a corrupted or replaced commitment is retained on every platform.
115
+ const dispose = (): void => { consumeGuiPairIntent(capability, proof, configDir); };
116
+ return { proof, dispose };
117
+ }
118
+
119
+ /** Called only after the existing process-bound, expiring capability passed authorization. */
120
+ export function consumeGuiPairIntent(capability: string | null, proof: string | null, configDir = getConfigDir()): boolean {
121
+ if (!capability || !proof || !TOKEN.test(capability) || !TOKEN.test(proof)) return false;
122
+ let fd: number | undefined;
123
+ let consumeLock: string | undefined;
124
+ let consumeLockIdentity: BigIntStats | undefined;
125
+ try {
126
+ assertNotRealHomeUnderTest(configDir);
127
+ directory(configDir);
128
+ const { dir, path } = location(capability, configDir);
129
+ const parent = directory(dir);
130
+ consumeLock = `${path}.consuming`;
131
+ mkdirSync(consumeLock, { mode: 0o700 });
132
+ consumeLockIdentity = directory(consumeLock);
133
+ const lockedParent = directory(dir);
134
+ if (parent.dev !== lockedParent.dev || parent.ino !== lockedParent.ino) return false;
135
+ const before = lstatSync(path, { bigint: true });
136
+ if (!before.isFile() || before.isSymbolicLink() || !owned(before) || before.nlink !== 1n || before.size !== 65n) return false;
137
+ // Do not block on a substituted FIFO. Windows retains descriptor/path identity checks.
138
+ const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK);
139
+ fd = openSync(path, flags);
140
+ const opened = fstatSync(fd, { bigint: true });
141
+ if (!same(before, opened) || !opened.isFile()) return false;
142
+ const bytes = Buffer.alloc(66);
143
+ const size = readSync(fd, bytes, 0, bytes.length, 0);
144
+ if (size !== 65 || !timingSafeEqual(bytes.subarray(0, size), Buffer.from(`${digest(proof)}\n`))) return false;
145
+ const after = fstatSync(fd, { bigint: true });
146
+ if (!same(opened, after) || !same(after, lstatSync(path, { bigint: true }))
147
+ || parent.dev !== directory(dir).dev || parent.ino !== directory(dir).ino) return false;
148
+ closeSync(fd); fd = undefined;
149
+ // Verify only a matching commitment, with its descriptor closed and lock held.
150
+ // Redemption must never harden an untrusted record into acceptable authority.
151
+ if (windowsSecretAclApplies() && !verifyWindowsPrivateEntries([
152
+ { path: dir, directory: true }, { path, directory: false },
153
+ ], WINDOWS_OWNER_ACL_TIMEOUT_MS)) return false;
154
+ const verifiedParent = directory(dir);
155
+ if (parent.dev !== verifiedParent.dev || parent.ino !== verifiedParent.ino
156
+ || !same(after, lstatSync(path, { bigint: true }))) return false;
157
+ unlinkSync(path);
158
+ forgetEphemeralSecretPath(path);
159
+ return true;
160
+ } catch { return false; }
161
+ finally {
162
+ try {
163
+ if (fd !== undefined) closeSync(fd);
164
+ } finally {
165
+ if (consumeLock && consumeLockIdentity) {
166
+ releaseConsumeLock(consumeLock, consumeLockIdentity);
167
+ }
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,143 @@
1
+ import { resolveTrustedWindowsPowerShellExe } from "./windows-elevation";
2
+
3
+ export interface WindowsPrivateEntry {
4
+ path: string;
5
+ directory: boolean;
6
+ }
7
+ export interface WindowsOwnerAclResult {
8
+ success: boolean;
9
+ timedOut: boolean;
10
+ stdout: string | Uint8Array;
11
+ }
12
+ export type WindowsOwnerAclRunner = (entries: readonly WindowsPrivateEntry[], timeoutMs: number) => WindowsOwnerAclResult;
13
+
14
+ export const WINDOWS_OWNER_ACL_TIMEOUT_MS = 5_000;
15
+ const SID = /^S-1-\d+(?:-\d+)+$/i;
16
+ const ADMINISTRATORS = "S-1-5-32-544";
17
+ const PRIVILEGED_GRANTEES = new Set(["S-1-5-18", ADMINISTRATORS]); // LocalSystem, BUILTIN\Administrators
18
+ const PRIVATE_DACL_FLAGS = 0x1004; // DiscretionaryAclPresent | DiscretionaryAclProtected
19
+ const FULL_CONTROL = 2032127;
20
+
21
+ // This script is constant. Literal paths are environment data, never PowerShell source.
22
+ // Read the descriptor through .NET (Owner, Group and Access, as Get-Acl does). Get-Acl is a module-autoloaded
23
+ // cmdlet: PowerShell 5.1 resolves it through the per-profile module analysis cache, and with a fresh or
24
+ // redirected LOCALAPPDATA that rebuild scans every PSModulePath module and can outlast the inspection budget
25
+ // (hosted Windows runners: the pairing CLI timed out at 30 s). Every call below is a .NET member, so nothing
26
+ // autoloads and no module on PSModulePath can shadow the read. GetAccessRules projects away unsupported ACE details, so the
27
+ // ACEs are read from the serialized descriptor instead. .NET canonicalizes it first: compatible entries for the
28
+ // same principal merge and entries that grant nothing drop, but another principal's entry is never folded into
29
+ // the user's, and the owner is untouched. The policy is therefore the effective DACL, not the on-disk bytes.
30
+ const ACL_SCRIPT = String.raw`
31
+ $ErrorActionPreference='Stop'
32
+ try {
33
+ # ASCII has no preamble, so a UTF-8 console code page cannot prefix the protocol with a BOM.
34
+ try { [Console]::OutputEncoding=[System.Text.Encoding]::ASCII } catch { $encodingUnchanged=$true }
35
+ $countText=[Environment]::GetEnvironmentVariable('OCX_ACL_COUNT')
36
+ if ($countText -notmatch '^[1-9][0-9]*$') { throw 'count' }
37
+ $count=[int]$countText
38
+ $identity=[System.Security.Principal.WindowsIdentity]::GetCurrent()
39
+ $principal=[System.Security.Principal.WindowsPrincipal]::new($identity)
40
+ $administrators=[System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
41
+ $enabled=$principal.IsInRole($administrators)
42
+ [Console]::WriteLine(('U|{0}|{1}|{2}' -f $identity.User.Value,$identity.Owner.Value,$enabled))
43
+ $sections=[System.Security.AccessControl.AccessControlSections]'Owner,Group,Access'
44
+ for ($i=0; $i -lt $count; $i++) {
45
+ $path=[Environment]::GetEnvironmentVariable(('OCX_ACL_PATH_{0}' -f $i))
46
+ if ([string]::IsNullOrEmpty($path)) { throw 'path' }
47
+ if ([System.IO.Directory]::Exists($path)) { $acl=[System.Security.AccessControl.DirectorySecurity]::new($path, $sections) }
48
+ else { $acl=[System.Security.AccessControl.FileSecurity]::new($path, $sections) }
49
+ $acls=@($acl)
50
+ if ($acls.Count -ne 1) { throw 'acl' }
51
+ $bytes=$acls[0].GetSecurityDescriptorBinaryForm()
52
+ $raw=[System.Security.AccessControl.RawSecurityDescriptor]::new([byte[]]$bytes, 0)
53
+ $aceCount=-1
54
+ if ($null -ne $raw.DiscretionaryAcl) { $aceCount=$raw.DiscretionaryAcl.Count }
55
+ [Console]::WriteLine(('E|{0}|{1}|{2}|{3}' -f $i,$raw.Owner.Value,[int]$raw.ControlFlags,$aceCount))
56
+ if ($null -ne $raw.DiscretionaryAcl) {
57
+ foreach ($ace in $raw.DiscretionaryAcl) {
58
+ if ($ace -is [System.Security.AccessControl.CommonAce]) {
59
+ [Console]::WriteLine(('A|{0}|{1}|{2}|{3}|{4}|{5}' -f $i,[int]$ace.AceType,[int]$ace.AceFlags,[int]$ace.AccessMask,$ace.SecurityIdentifier.Value,$ace.IsCallback))
60
+ } else {
61
+ [Console]::WriteLine(('A|{0}|{1}|X' -f $i,[int]$ace.AceType))
62
+ }
63
+ }
64
+ }
65
+ }
66
+ [Console]::WriteLine('END')
67
+ exit 0
68
+ } catch { exit 3 }
69
+ `;
70
+ const ENCODED_ACL_SCRIPT = Buffer.from(ACL_SCRIPT, "utf16le").toString("base64");
71
+
72
+ function defaultWindowsOwnerAclRunner(entries: readonly WindowsPrivateEntry[], timeoutMs: number): WindowsOwnerAclResult {
73
+ const env: Record<string, string | undefined> = { ...process.env, OCX_ACL_COUNT: String(entries.length) };
74
+ for (let i = 0; i < entries.length; i++) env[`OCX_ACL_PATH_${i}`] = entries[i]!.path;
75
+ const result = Bun.spawnSync([
76
+ resolveTrustedWindowsPowerShellExe(), "-NoLogo", "-NoProfile", "-NonInteractive",
77
+ "-ExecutionPolicy", "Bypass", "-EncodedCommand", ENCODED_ACL_SCRIPT,
78
+ ], {
79
+ env, stdin: "ignore", stdout: "pipe", stderr: "ignore",
80
+ timeout: timeoutMs, windowsHide: true,
81
+ });
82
+ return { success: result.success, timedOut: result.exitedDueToTimeout ?? false, stdout: result.stdout ?? new Uint8Array() };
83
+ }
84
+ let ownerAclRunner: WindowsOwnerAclRunner = defaultWindowsOwnerAclRunner;
85
+ /** Test-only access to the real subprocess runner, for native diagnostics. */
86
+ export const windowsOwnerAclDefaultRunnerForTests: WindowsOwnerAclRunner = defaultWindowsOwnerAclRunner;
87
+
88
+ export function setWindowsOwnerAclRunnerForTests(runner: WindowsOwnerAclRunner | null): void {
89
+ ownerAclRunner = runner ?? defaultWindowsOwnerAclRunner;
90
+ }
91
+
92
+ /** Pure, strict line-protocol parser and owner/effective-DACL policy. No account names or SDDL. */
93
+ export function windowsPrivateEntriesAclMatches(stdout: string | Uint8Array, entries: readonly WindowsPrivateEntry[]): boolean {
94
+ if (entries.length === 0) return false;
95
+ // The script emits ASCII only. Reject BOMs, NULs, non-ASCII bytes and replacement decoding.
96
+ if (typeof stdout !== "string" && stdout.some(byte => byte > 127)) return false;
97
+ const text = typeof stdout === "string" ? stdout : Buffer.from(stdout).toString("ascii");
98
+ if (/[^\x20-\x7e\r\n]/.test(text)) return false;
99
+ const normalized = text.replace(/\r\n/g, "\n");
100
+ if (normalized.includes("\r")) return false;
101
+ const lines = (normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized).split("\n");
102
+ if (lines.at(-1) !== "END") return false;
103
+ const token = lines[0]!.split("|");
104
+ if (token.length !== 4 || token[0] !== "U" || !SID.test(token[1]!) || !SID.test(token[2]!)
105
+ || (token[3] !== "True" && token[3] !== "False")) return false;
106
+ const user = token[1]!.toUpperCase();
107
+ const acceptsAdministrators = token[2]!.toUpperCase() === ADMINISTRATORS && token[3] === "True";
108
+ let at = 1;
109
+ for (let i = 0; i < entries.length; i++) {
110
+ const entry = (lines[at++] ?? "").split("|");
111
+ if (entry.length !== 5 || entry[0] !== "E" || entry[1] !== String(i) || !SID.test(entry[2]!)
112
+ || !/^(0|[1-9]\d{0,4})$/.test(entry[3]!) || !/^[1-3]$/.test(entry[4]!)) return false;
113
+ const flags = Number(entry[3]);
114
+ if (flags > 0xffff || (flags & PRIVATE_DACL_FLAGS) !== PRIVATE_DACL_FLAGS) return false;
115
+ const owner = entry[2]!.toUpperCase();
116
+ if (owner !== user && !(owner === ADMINISTRATORS && acceptsAdministrators)) return false;
117
+ // The serving account needs its own Full Control entry. SYSTEM and Administrators may also hold plain
118
+ // allow entries: LocalSystem and an elevated administrator can already take ownership of any object, and a
119
+ // filtered administrator token holds Administrators only as deny-only, so the allow entry grants it nothing.
120
+ const grantees = new Set<string>();
121
+ for (let k = Number(entry[4]); k > 0; k--) {
122
+ const ace = (lines[at++] ?? "").split("|");
123
+ if (ace.length !== 7 || ace[0] !== "A" || ace[1] !== String(i) || ace[2] !== "0"
124
+ || ace[3] !== (entries[i]!.directory ? "3" : "0") || !/^[1-9]\d{0,9}$/.test(ace[4]!)
125
+ || !SID.test(ace[5]!) || ace[6] !== "False") return false;
126
+ const grantee = ace[5]!.toUpperCase();
127
+ if (grantees.has(grantee)) return false;
128
+ grantees.add(grantee);
129
+ if (grantee === user ? ace[4] !== String(FULL_CONTROL) : !PRIVILEGED_GRANTEES.has(grantee)) return false;
130
+ }
131
+ if (!grantees.has(user)) return false;
132
+ }
133
+ return at === lines.length - 1;
134
+ }
135
+
136
+ /** Fresh, read-only verification; runner failures never disclose paths, SIDs or ACL output. */
137
+ export function verifyWindowsPrivateEntries(entries: readonly WindowsPrivateEntry[], timeoutMs: number): boolean {
138
+ if (entries.length === 0 || !Number.isFinite(timeoutMs) || timeoutMs <= 0) return false;
139
+ try {
140
+ const result = ownerAclRunner(entries, Math.max(1, Math.min(WINDOWS_OWNER_ACL_TIMEOUT_MS, Math.floor(timeoutMs))));
141
+ return result.success && !result.timedOut && windowsPrivateEntriesAclMatches(result.stdout, entries);
142
+ } catch { return false; }
143
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ CodexPoolAccountCreditsOffError,
3
+ releaseCodexAuthContextProbeLease,
4
+ unwrapUpstreamRetryEvidenceError,
5
+ type CodexAuthContext,
6
+ type CodexAuthPolicyConfig,
7
+ } from "../codex/auth-context";
8
+ import { poolContextCreditHoldResetAt } from "../codex/pool-credit-policy";
9
+
10
+ export function createOpenAiSidecarCreditGuard(
11
+ context: CodexAuthContext,
12
+ policy: CodexAuthPolicyConfig,
13
+ ): (() => void) | undefined {
14
+ if (context.kind !== "pool") return undefined;
15
+ return () => {
16
+ const resetAt = poolContextCreditHoldResetAt(context, policy);
17
+ if (resetAt === undefined) return;
18
+ releaseCodexAuthContextProbeLease(context);
19
+ throw new CodexPoolAccountCreditsOffError(context.accountId, resetAt);
20
+ };
21
+ }
22
+
23
+ export function openAiSidecarCreditRefusal(error: unknown): CodexPoolAccountCreditsOffError | undefined {
24
+ const cause = unwrapUpstreamRetryEvidenceError(error);
25
+ return cause instanceof CodexPoolAccountCreditsOffError ? cause : undefined;
26
+ }
@@ -1,4 +1,5 @@
1
1
  import { resolveProviderApiKey } from "./key-store";
2
+ import { createOpenAiSidecarCreditGuard } from "./openai-sidecar-credit";
2
3
  import {
3
4
  CodexPoolAuthenticationError,
4
5
  headersForCodexAuthContext,
@@ -31,6 +32,7 @@ export interface OpenAiForwardSidecarCandidate {
31
32
  export interface ResolvedOpenAiForwardSidecar extends OpenAiForwardSidecarCandidate {
32
33
  authContext: CodexAuthContext;
33
34
  headers: Headers;
35
+ beforeDispatch?: () => void;
34
36
  recordOutcome?: (outcome: CodexUpstreamOutcome) => void;
35
37
  /** Hand back an acquired recovery probe when no sidecar request reached upstream. */
36
38
  releaseProbeLease?: () => void;
@@ -123,6 +125,37 @@ function directSidecarHeaders(
123
125
  return selected;
124
126
  }
125
127
 
128
+ /**
129
+ * The caller's own ChatGPT credential for a call the caller created itself.
130
+ *
131
+ * A V3 `existingCall` sideband join attaches to a WebRTC call that ChatGPT voice or Codex Desktop
132
+ * created with the caller's login, so only that login can join it; a Pool-selected account is a
133
+ * different ChatGPT account and the upstream refuses the handshake. This hands the join the same
134
+ * caller-owned headers Codex Direct mode forwards: an explicit bearer whose token claim matches
135
+ * its `chatgpt-account-id`, never the proxy admission secret, materialized through the same main
136
+ * policy (hard lock, credits), whose refusals surface as `CodexAccountCooldownError`.
137
+ * Returns undefined when the caller presents no such credential.
138
+ */
139
+ export function resolveCallerOwnedOpenAiSidecar(
140
+ candidates: readonly OpenAiForwardSidecarCandidate[],
141
+ incomingHeaders: Headers,
142
+ config: OcxConfig,
143
+ options: { admission?: Pick<DataPlaneAdmission, "source">; codexAuthPolicy?: CodexAuthPolicyConfig } = {},
144
+ ): ResolvedOpenAiForwardSidecar | undefined {
145
+ const candidate = candidates[0];
146
+ if (!candidate) return undefined;
147
+ try {
148
+ validateForwardAdmissionCredential(incomingHeaders, config);
149
+ } catch (error) {
150
+ if (error instanceof ForwardAdmissionCredentialError) return undefined;
151
+ throw error;
152
+ }
153
+ if (!hasCallerCodexBearer(incomingHeaders)) return undefined;
154
+ const headers = directSidecarHeaders(incomingHeaders, options.codexAuthPolicy ?? config, options.admission);
155
+ if (!headers) return undefined;
156
+ return { ...candidate, authContext: { kind: "main", accountId: null }, headers };
157
+ }
158
+
126
159
  export async function resolveFirstUsableOpenAiSidecar(
127
160
  candidates: readonly OpenAiForwardSidecarCandidate[],
128
161
  incomingHeaders: Headers,
@@ -176,6 +209,7 @@ export async function resolveFirstUsableOpenAiSidecar(
176
209
  ...candidate,
177
210
  authContext,
178
211
  headers: selectedHeaders,
212
+ beforeDispatch: createOpenAiSidecarCreditGuard(authContext, policy),
179
213
  recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
180
214
  config,
181
215
  authContext.accountId,
@@ -226,6 +260,7 @@ export async function resolveFirstUsableOpenAiSidecar(
226
260
  ...candidate,
227
261
  authContext,
228
262
  headers: selectedHeaders,
263
+ beforeDispatch: createOpenAiSidecarCreditGuard(authContext, policy),
229
264
  ...(authContext.kind === "pool" || authContext.kind === "main-pool"
230
265
  ? {
231
266
  recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
@@ -1205,6 +1205,7 @@ async function handleClaudeMessagesWithBudget(
1205
1205
  return await handleNativeMessages({
1206
1206
  req, config, logCtx, ...(logIds ? { logIds } : {}),
1207
1207
  route: nativeMessagesRoute, body: nativeBody, requestedModel, translatorBudget, selector: nativeSelector,
1208
+ modelScope: resolveAdmissionModelScope(config, logIds?.admission),
1208
1209
  // Compatibility identity is an opaque request-local handle, separate from credentials.
1209
1210
  clientIdentity: captureAnthropicClientIdentity(req.headers),
1210
1211
  callerAnthropicBeta: req.headers.get("anthropic-beta"),
@@ -0,0 +1,23 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { GUI_PAIR_BROWSER_ORIGIN_HEADER, GUI_PAIR_CAPABILITY_HEADER, standaloneGuiPairingOrigin } from "../lib/gui-pair-capability";
3
+ import { consumeGuiPairIntent, GUI_PAIR_INTENT_HEADER } from "../lib/gui-pair-intent";
4
+ import { createGuiPairingGrant, type GuiSessionState } from "./gui-session";
5
+
6
+ export class GuiPairingIntentRequiredError extends Error {
7
+ constructor() {
8
+ super("Standalone GUI pairing requires a fresh local CLI intent");
9
+ this.name = "GuiPairingIntentRequiredError";
10
+ }
11
+ }
12
+
13
+ /** The serving route must authenticate the existing process-bound capability first. */
14
+ export function deliverGuiPairingGrant(req: Request, config: OcxConfig, state: GuiSessionState) {
15
+ const origin = req.headers.get(GUI_PAIR_BROWSER_ORIGIN_HEADER) ?? "";
16
+ if (config.runtimeRole !== "hub") {
17
+ if (standaloneGuiPairingOrigin(config) !== origin
18
+ || !consumeGuiPairIntent(req.headers.get(GUI_PAIR_CAPABILITY_HEADER), req.headers.get(GUI_PAIR_INTENT_HEADER))) {
19
+ throw new GuiPairingIntentRequiredError();
20
+ }
21
+ }
22
+ return createGuiPairingGrant(origin, config, state);
23
+ }
@@ -174,15 +174,14 @@ import { LOCAL_MANAGEMENT_NONCE_HEADER } from "../../lib/local-management-capabi
174
174
  import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../../lib/local-provider-reload-contract";
175
175
  import { LOCAL_ASIDE_SYNC_CAPABILITY_VERSION } from "../../lib/local-aside-sync-contract";
176
176
  import {
177
- GUI_PAIR_BROWSER_ORIGIN_HEADER,
178
177
  GUI_PAIR_CAPABILITY_VERSION,
179
178
  GUI_PAIR_PATH,
180
179
  } from "../../lib/gui-pair-capability";
181
180
  import {
182
181
  GuiPairingGrantRateLimitError,
183
182
  consumeGuiPairingGrant,
184
- createGuiPairingGrant,
185
183
  } from "../gui-session";
184
+ import { deliverGuiPairingGrant, GuiPairingIntentRequiredError } from "../gui-pair-delivery";
186
185
  import { recordCursorSeen } from "../../integrations/cursor-seen";
187
186
  import { detectCursorInstalls } from "../../integrations/cursor-detect";
188
187
  import { loadCursorEffortTable } from "../../integrations/cursor-effort-table";
@@ -681,18 +680,16 @@ export function createServeOptions(ctx: ServeOptionsContext) {
681
680
  return withManagementCors(Response.json({ error: "GUI pairing capability required" }, { status: 403 }), req, config);
682
681
  }
683
682
  try {
684
- const grant = createGuiPairingGrant(
685
- req.headers.get(GUI_PAIR_BROWSER_ORIGIN_HEADER) ?? "",
686
- config,
687
- managementAuth,
688
- );
683
+ const grant = deliverGuiPairingGrant(req, config, managementAuth);
689
684
  return withManagementCors(Response.json(grant, {
690
685
  status: 201,
691
686
  headers: { "Cache-Control": "no-store" },
692
687
  }), req, config);
693
688
  } catch (error) {
694
689
  const status = error instanceof GuiPairingGrantRateLimitError ? 429 : 403;
695
- return withManagementCors(Response.json({ error: "GUI pairing grant refused" }, {
690
+ return withManagementCors(Response.json({ error: "GUI pairing grant refused",
691
+ ...(error instanceof GuiPairingIntentRequiredError ? { code: "local_pairing_intent_required" } : {}),
692
+ }, {
696
693
  status,
697
694
  ...(status === 429 ? { headers: { "Retry-After": "60" } } : {}),
698
695
  }), req, config);
@@ -463,21 +463,19 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
463
463
  return serveGuiFile(rawPath) !== null;
464
464
  }
465
465
 
466
- // Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
467
- // the server_is_overloaded code so clients can back off, but always return a JSON envelope.
468
- // These two run BEFORE the auth/origin checks, so they need the receiving listener's policy
469
- // explicitly (#1102). Reaching for the shared `config` here would attach public-policy CORS
470
- // headers to a 503 on the loopback listener — no model runs and no credential is spent, but
471
- // it is the one error path that would answer a rebinding origin with its own origin echoed
472
- // back.
466
+ // Codex maps 503 + server_is_overloaded to ServerOverloaded ("model at capacity"), retried
467
+ // only with retry advice that older clients ignore (#6642); server_restarting is a
468
+ // retryable UnexpectedStatus in every version and never reads as model capacity.
469
+ // Keep JSON (empty / non-JSON 503 means "Unknown error", #452) and Retry-After.
470
+ // Drain and busy run BEFORE auth/origin checks: use the receiving listener's policy
471
+ // (#1102), because shared public `config` could echo a rebinding origin on loopback.
473
472
  function drainingResponse(req: Request, policy: RequestPolicyView): Response {
474
- const response = formatErrorResponse(503, "server_error", "Service shutting down");
475
- const headers = new Headers(response.headers);
476
- for (const [name, value] of Object.entries(corsHeaders(req, policy))) {
477
- headers.set(name, value);
478
- }
479
- headers.set("Retry-After", "5");
480
- return new Response(response.body, { status: 503, headers });
473
+ return withCors(new Response(JSON.stringify({
474
+ error: { type: "server_error", code: "server_restarting", message: "OpenCodex is restarting; retry this request." },
475
+ }), {
476
+ status: 503,
477
+ headers: { "Content-Type": "application/json", "Retry-After": "5" },
478
+ }), req, policy);
481
479
  }
482
480
 
483
481
  function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Upstream call ids created through the native voice call-create path (`handleLive`).
3
+ *
4
+ * A sideband join names only a call id, and two kinds of call reach the same join routes. A call
5
+ * this process created was negotiated under the account the Pool selected, and its join must stay
6
+ * on that account (openai/codex #35830; thread affinity carries it). A call the client created
7
+ * itself — ChatGPT voice handing a call to a Codex thread, or Codex Desktop when its renderer owns
8
+ * the call — belongs to the caller's own ChatGPT login and arrives as a V3 `existingCall` join with
9
+ * no create on this proxy. Membership here is what tells the two apart.
10
+ *
11
+ * Ids only: no account, credential, model, or caller is stored. Process-local and bounded, so a
12
+ * restart, the TTL, or eviction past capacity turns a created call into an unknown one; a later
13
+ * rejoin is then authenticated as the caller (see `resolveLiveRelay`).
14
+ */
15
+ export const NATIVE_LIVE_CALL_TTL_MS = 6 * 60 * 60_000;
16
+ export const MAX_NATIVE_LIVE_CALLS = 1024;
17
+ const MAX_LOCATION_LENGTH = 4096;
18
+ const MAX_CALL_ID_LENGTH = 128;
19
+ const RTC_CALL_ID_RE = /^rtc_[A-Za-z0-9_-]+$/;
20
+ const UUID_CALL_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
21
+
22
+ function isRealtimeCallIdSegment(segment: string): boolean {
23
+ if (segment.length > MAX_CALL_ID_LENGTH) return false;
24
+ return RTC_CALL_ID_RE.test(segment) || UUID_CALL_ID_RE.test(segment);
25
+ }
26
+
27
+ /**
28
+ * The call id a client reads from a call-create Location, extracted the way openai/codex does
29
+ * (`decode_call_id_from_location`, codex-api realtime_call.rs): drop the query, scan the path
30
+ * segments from the end, and take the first `rtc_…` or hyphenated-UUID segment. The backend
31
+ * answers `/v1/realtime/calls/calls/rtc_…`, the API `/v1/live/rtc_…`; both yield the id the client
32
+ * then joins with.
33
+ */
34
+ export function liveCallIdFromLocation(location: string | null | undefined): string | null {
35
+ if (!location || location.length > MAX_LOCATION_LENGTH) return null;
36
+ const path = location.split("?")[0] ?? "";
37
+ for (const segment of path.split("/").reverse()) {
38
+ if (isRealtimeCallIdSegment(segment)) return segment;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export class NativeLiveCalls {
44
+ /** Insertion order is creation order, so the first key is always the oldest entry. */
45
+ private readonly entries = new Map<string, number>();
46
+
47
+ constructor(
48
+ private readonly now: () => number = Date.now,
49
+ private readonly ttlMs: number = NATIVE_LIVE_CALL_TTL_MS,
50
+ private readonly capacity: number = MAX_NATIVE_LIVE_CALLS,
51
+ ) {}
52
+
53
+ /** Record the call a successful create's Location names; returns the id, or null when none. */
54
+ record(location: string | null | undefined): string | null {
55
+ const id = liveCallIdFromLocation(location);
56
+ if (!id) return null;
57
+ this.prune();
58
+ this.entries.delete(id);
59
+ while (this.entries.size >= this.capacity) {
60
+ const oldest = this.entries.keys().next().value;
61
+ if (oldest === undefined) break;
62
+ this.entries.delete(oldest);
63
+ }
64
+ this.entries.set(id, this.now() + this.ttlMs);
65
+ return id;
66
+ }
67
+
68
+ has(id: string): boolean {
69
+ const expiresAt = this.entries.get(id);
70
+ if (expiresAt === undefined) return false;
71
+ if (expiresAt <= this.now()) {
72
+ this.entries.delete(id);
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ get size(): number {
79
+ this.prune();
80
+ return this.entries.size;
81
+ }
82
+
83
+ clear(): void {
84
+ this.entries.clear();
85
+ }
86
+
87
+ private prune(): void {
88
+ const now = this.now();
89
+ for (const [id, expiresAt] of this.entries) if (expiresAt <= now) this.entries.delete(id);
90
+ }
91
+ }
92
+
93
+ /** The process-wide registry `handleLive` writes and native sideband joins read. */
94
+ export const nativeLiveCalls = new NativeLiveCalls();