@bitkyc08/opencodex 2.57.0 → 2.58.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/gui/dist/assets/{index-Cz7CLdif.js → index-BbrHOIY0.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-responses/passthrough.ts +30 -4
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/claude/outbound.ts +14 -4
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +44 -2
- package/src/cli/system-command.ts +70 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/codex/auth-context.ts +42 -8
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +3 -7
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/config/atomic-write.ts +83 -8
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +132 -68
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/providers/derive.ts +6 -0
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +50 -28
- package/src/providers/registry/model-seeds.ts +67 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +1 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +11 -0
- package/src/server/responses/passthrough-dispatch.ts +33 -1
- package/src/server/responses/request-prepare.ts +41 -0
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +13 -0
|
@@ -9,28 +9,80 @@ export interface BoundedSubprocessExit {
|
|
|
9
9
|
timedOut: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
export type SubprocessDeadlineScheduler = (
|
|
13
|
+
callback: () => void,
|
|
14
|
+
milliseconds: number,
|
|
15
|
+
) => () => void;
|
|
16
|
+
|
|
17
|
+
const scheduleDeadline: SubprocessDeadlineScheduler = (callback, milliseconds) => {
|
|
18
|
+
const timer = setTimeout(callback, milliseconds);
|
|
19
|
+
return () => clearTimeout(timer);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compatibility allowance used by the ACL runner's outer watchdog.
|
|
24
|
+
*
|
|
25
|
+
* `kill()` only REQUESTS termination. It returns before the kernel has torn the process down, and
|
|
26
|
+
* every handle that process holds stays held until it does. On Windows that is not a detail: file
|
|
27
|
+
* locking is mandatory, so a directory an abandoned `icacls.exe` still has open cannot be removed
|
|
28
|
+
* by anyone, and the removal fails with EPERM rather than waiting.
|
|
29
|
+
*/
|
|
30
|
+
export const SUBPROCESS_KILL_GRACE_MS = 2_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wait for a child until the deadline. At the deadline, kill it AND wait for it to actually die.
|
|
34
|
+
*
|
|
35
|
+
* This used to kill, `unref`, and resolve in the same tick, which made every caller's "I waited
|
|
36
|
+
* for my child" guarantee false precisely when it mattered. The ACL runner now waits here until
|
|
37
|
+
* actual exit; if its separate caller-facing belt fires first, that layer registers the target so
|
|
38
|
+
* removal can wait for the reap without making ordinary startup or shutdown unbounded.
|
|
39
|
+
*
|
|
40
|
+
* That cost three failed fixes. #4789 blamed the removal retry budget and asked for more than
|
|
41
|
+
* 2.5s; #4796 gave it a 15s exponential schedule; a later change awaited the hardening flight from
|
|
42
|
+
* the test hook. Windows shard 1/6 failed identically through all three, because none of them
|
|
43
|
+
* addressed a live process holding the handle -- run 35108652486 burned the full 15s budget and
|
|
44
|
+
* still threw `EPERM ... rm ocx-management-auth-fDchUb`, with two
|
|
45
|
+
* `ACL hardening timed out (ETIMEDOUT) - transient icacls stall` lines logged beside it.
|
|
46
|
+
*
|
|
47
|
+
* The old grace still abandoned a live child after two seconds. That recreated the same false
|
|
48
|
+
* ownership contract on a slower clock: the ACL flight settled, cleanup removed the directory,
|
|
49
|
+
* and Windows returned EPERM because the child still held it. A handle-bearing caller therefore
|
|
50
|
+
* has no second deadline after kill. The child's actual exit is the only release signal.
|
|
51
|
+
*
|
|
52
|
+
* Pass `0` to opt out for a child that holds no path anyone will remove. The numeric form is kept
|
|
53
|
+
* for compatibility with the existing callers; any positive value means that reaping is required.
|
|
54
|
+
* The injected scheduler is a test seam so deadline and exit ordering can be proved without sleep.
|
|
55
|
+
*/
|
|
13
56
|
export function waitForSubprocessExit(
|
|
14
57
|
proc: KillableSubprocess,
|
|
15
58
|
timeoutMs: number,
|
|
59
|
+
reapAfterKill: number = SUBPROCESS_KILL_GRACE_MS,
|
|
60
|
+
schedule: SubprocessDeadlineScheduler = scheduleDeadline,
|
|
16
61
|
): Promise<BoundedSubprocessExit> {
|
|
17
62
|
return new Promise(resolve => {
|
|
18
63
|
let settled = false;
|
|
19
|
-
let
|
|
64
|
+
let deadlineFired = false;
|
|
65
|
+
let cancelDeadline: (() => void) | undefined;
|
|
20
66
|
const finish = (result: BoundedSubprocessExit): void => {
|
|
21
67
|
if (settled) return;
|
|
22
68
|
settled = true;
|
|
23
|
-
|
|
69
|
+
cancelDeadline?.();
|
|
24
70
|
resolve(result);
|
|
25
71
|
};
|
|
26
|
-
|
|
72
|
+
const reaped = proc.exited.then(
|
|
73
|
+
exitCode => finish(deadlineFired
|
|
74
|
+
? { exitCode: null, timedOut: true }
|
|
75
|
+
: { exitCode, timedOut: false }),
|
|
76
|
+
() => finish({ exitCode: null, timedOut: deadlineFired }),
|
|
77
|
+
);
|
|
78
|
+
cancelDeadline = schedule(() => {
|
|
79
|
+
deadlineFired = true;
|
|
27
80
|
try { proc.kill(); } catch { /* already exited */ }
|
|
28
|
-
|
|
29
|
-
|
|
81
|
+
if (reapAfterKill <= 0) {
|
|
82
|
+
try { proc.unref?.(); } catch { /* abandonment is still authoritative */ }
|
|
83
|
+
finish({ exitCode: null, timedOut: true });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
30
86
|
}, Math.max(1, timeoutMs));
|
|
31
|
-
void proc.exited.then(
|
|
32
|
-
exitCode => finish({ exitCode, timedOut: false }),
|
|
33
|
-
() => finish({ exitCode: null, timedOut: false }),
|
|
34
|
-
);
|
|
35
87
|
});
|
|
36
88
|
}
|
|
@@ -30,8 +30,13 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { existsSync, statSync } from "node:fs";
|
|
33
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
33
34
|
import { env, platform } from "node:process";
|
|
34
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
SUBPROCESS_KILL_GRACE_MS,
|
|
37
|
+
waitForSubprocessExit,
|
|
38
|
+
type SubprocessDeadlineScheduler,
|
|
39
|
+
} from "./bounded-subprocess";
|
|
35
40
|
import { resolveTrustedWindowsIcaclsExe } from "./windows-elevation";
|
|
36
41
|
import {
|
|
37
42
|
cachedCurrentWindowsIdentity,
|
|
@@ -48,17 +53,32 @@ const hardenedPaths = new Map<string, HardenedIdentity>();
|
|
|
48
53
|
* that attempt was consumed. Ordinary callers never consume it.
|
|
49
54
|
*/
|
|
50
55
|
const timedOutPaths = new Map<string, boolean>();
|
|
56
|
+
/** Compatibility slack before the outer belt releases a caller whose killed child has not reaped. */
|
|
57
|
+
const ASYNC_ICACLS_BELT_MARGIN_MS = 250;
|
|
58
|
+
const pendingAsyncIcaclsReaps = new Map<string, Set<Promise<void>>>();
|
|
59
|
+
|
|
60
|
+
const scheduleAsyncIcaclsBelt: SubprocessDeadlineScheduler = (callback, milliseconds) => {
|
|
61
|
+
const timer = setTimeout(callback, milliseconds);
|
|
62
|
+
return () => clearTimeout(timer);
|
|
63
|
+
};
|
|
64
|
+
let asyncIcaclsBeltScheduler: SubprocessDeadlineScheduler = scheduleAsyncIcaclsBelt;
|
|
51
65
|
|
|
52
66
|
/**
|
|
53
|
-
* The memo value: `object
|
|
54
|
-
* to.
|
|
67
|
+
* The memo value: the `object` plus `freshness` of a file a harden was actually
|
|
68
|
+
* attributed to.
|
|
55
69
|
*
|
|
56
70
|
* There is deliberately no null member. An observation that cannot be read is
|
|
57
71
|
* not stored at all — the entry is deleted — because a "recorded as unverifiable"
|
|
58
72
|
* value was dead code the moment attribution became a before/after comparison,
|
|
59
73
|
* and a branch nothing can reach is a branch no test can defend.
|
|
74
|
+
*
|
|
75
|
+
* It is the observation itself rather than a joined string so that the two
|
|
76
|
+
* questions stay separately askable after storage. `reattributeHardenedSecretPath`
|
|
77
|
+
* has to compare the object while deliberately ignoring the freshness, and
|
|
78
|
+
* recovering one half out of `dev:ino:ctimeNs` by counting colons would make that
|
|
79
|
+
* comparison depend on a format nothing declares.
|
|
60
80
|
*/
|
|
61
|
-
type HardenedIdentity =
|
|
81
|
+
type HardenedIdentity = PathObservation;
|
|
62
82
|
|
|
63
83
|
/**
|
|
64
84
|
* What a stat can tell us about WHICH OBJECT is at a path.
|
|
@@ -128,8 +148,8 @@ function observe(targetPath: string): PathObservation | null {
|
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
|
|
131
|
-
function
|
|
132
|
-
return
|
|
151
|
+
function sameObservation(a: PathObservation, b: PathObservation): boolean {
|
|
152
|
+
return a.object === b.object && a.freshness === b.freshness;
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
/**
|
|
@@ -155,7 +175,7 @@ function memoSatisfied(cache: Map<string, HardenedIdentity>, targetPath: string)
|
|
|
155
175
|
// without any ACL work. That needs exact-identity ABA to bite — outside the
|
|
156
176
|
// proof bound this unit claims — but "the consequence is out of scope" is not a
|
|
157
177
|
// reason to keep an entry we have just proven does not describe what is there.
|
|
158
|
-
if (current === null ||
|
|
178
|
+
if (current === null || !sameObservation(current, remembered)) {
|
|
159
179
|
cache.delete(targetPath);
|
|
160
180
|
return false;
|
|
161
181
|
}
|
|
@@ -203,7 +223,7 @@ function recordHarden(
|
|
|
203
223
|
cache.delete(targetPath);
|
|
204
224
|
return false;
|
|
205
225
|
}
|
|
206
|
-
cache.set(targetPath,
|
|
226
|
+
cache.set(targetPath, after);
|
|
207
227
|
return true;
|
|
208
228
|
}
|
|
209
229
|
|
|
@@ -339,7 +359,7 @@ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
|
|
|
339
359
|
/**
|
|
340
360
|
* Async icacls runner (#612): yields the event loop while waiting for the child.
|
|
341
361
|
* Async Subprocess has no exitedDueToTimeout, so the shared settlement helper
|
|
342
|
-
* classifies the deadline and
|
|
362
|
+
* classifies the deadline and keeps waiting for a killed child to actually exit.
|
|
343
363
|
*/
|
|
344
364
|
async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
|
|
345
365
|
const proc = trySpawnIcacls(args);
|
|
@@ -359,21 +379,78 @@ async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Prom
|
|
|
359
379
|
function awaitAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
|
|
360
380
|
return new Promise(resolve => {
|
|
361
381
|
let settled = false;
|
|
362
|
-
let
|
|
382
|
+
let cancelBelt: (() => void) | undefined;
|
|
363
383
|
const finish = (result: IcaclsResult): void => {
|
|
364
384
|
if (settled) return;
|
|
365
385
|
settled = true;
|
|
366
|
-
|
|
386
|
+
cancelBelt?.();
|
|
367
387
|
resolve(result);
|
|
368
388
|
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
389
|
+
const runner = asyncIcaclsRunner(args, timeoutMs).then(
|
|
390
|
+
result => { finish(result); },
|
|
391
|
+
() => { finish(spawnFailedResult()); },
|
|
392
|
+
);
|
|
393
|
+
// The belt has to outlast the runner it is guarding, or it is not a belt -- it is the
|
|
394
|
+
// deadline. The runner may now legitimately outlive it while a killed child is reaped. The
|
|
395
|
+
// caller is still released, but the target is registered so removal can wait for the distinct
|
|
396
|
+
// handle-release question instead of treating flight settlement as proof that the child died.
|
|
397
|
+
cancelBelt = asyncIcaclsBeltScheduler(
|
|
398
|
+
() => {
|
|
399
|
+
const targetPath = args[0];
|
|
400
|
+
if (targetPath) registerPendingAsyncIcaclsReap(targetPath, runner);
|
|
401
|
+
finish({ success: false, exitCode: null, timedOut: true, stdout: "" });
|
|
402
|
+
},
|
|
403
|
+
Math.max(1, timeoutMs) + SUBPROCESS_KILL_GRACE_MS + ASYNC_ICACLS_BELT_MARGIN_MS,
|
|
372
404
|
);
|
|
373
|
-
void asyncIcaclsRunner(args, timeoutMs).then(finish, () => finish(spawnFailedResult()));
|
|
374
405
|
});
|
|
375
406
|
}
|
|
376
407
|
|
|
408
|
+
function registerPendingAsyncIcaclsReap(targetPath: string, reap: Promise<void>): void {
|
|
409
|
+
let pending = pendingAsyncIcaclsReaps.get(targetPath);
|
|
410
|
+
if (!pending) {
|
|
411
|
+
pending = new Set();
|
|
412
|
+
pendingAsyncIcaclsReaps.set(targetPath, pending);
|
|
413
|
+
}
|
|
414
|
+
pending.add(reap);
|
|
415
|
+
void reap.finally(() => {
|
|
416
|
+
pending!.delete(reap);
|
|
417
|
+
if (pending!.size === 0) pendingAsyncIcaclsReaps.delete(targetPath);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function pathIsAtOrBelow(targetPath: string, rootPath: string): boolean {
|
|
422
|
+
const relativePath = relative(resolve(rootPath), resolve(targetPath));
|
|
423
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** True while an async icacls runner still owns this exact path after its caller's belt fired. */
|
|
427
|
+
export function windowsSecretAclReapPendingForPath(targetPath: string): boolean {
|
|
428
|
+
return (pendingAsyncIcaclsReaps.get(targetPath)?.size ?? 0) > 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Non-blocking removal guard for callers that must refuse rather than wait for a stuck child. */
|
|
432
|
+
export function windowsSecretAclReapPendingAtOrBelow(rootPath: string): boolean {
|
|
433
|
+
return [...pendingAsyncIcaclsReaps.keys()]
|
|
434
|
+
.some(targetPath => pathIsAtOrBelow(targetPath, rootPath));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Removal barrier for a file or tree that may still be held by a timed-out icacls child.
|
|
439
|
+
*
|
|
440
|
+
* This wait is deliberately separate from ordinary startup and shutdown: a genuinely stuck child
|
|
441
|
+
* must not defeat the caller-facing belt. Code that chooses to remove the target has the stricter
|
|
442
|
+
* contract and must not proceed until every registered runner at or below it has actually reaped.
|
|
443
|
+
*/
|
|
444
|
+
export async function flushWindowsSecretAclReapsBeforeRemoval(rootPath: string): Promise<void> {
|
|
445
|
+
while (true) {
|
|
446
|
+
const pending = [...pendingAsyncIcaclsReaps]
|
|
447
|
+
.filter(([targetPath]) => pathIsAtOrBelow(targetPath, rootPath))
|
|
448
|
+
.flatMap(([, reaps]) => [...reaps]);
|
|
449
|
+
if (pending.length === 0) return;
|
|
450
|
+
await Promise.all(pending);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
377
454
|
let icaclsRunner: IcaclsRunner = defaultIcaclsRunner;
|
|
378
455
|
let asyncIcaclsRunner: AsyncIcaclsRunner = defaultAsyncIcaclsRunner;
|
|
379
456
|
let platformOverride: string | null = null;
|
|
@@ -389,6 +466,13 @@ export function setAsyncIcaclsRunnerForTests(runner: AsyncIcaclsRunner | null):
|
|
|
389
466
|
asyncIcaclsRunner = runner ?? defaultAsyncIcaclsRunner;
|
|
390
467
|
}
|
|
391
468
|
|
|
469
|
+
/** Test seam: fire the outer caller-facing belt without sleeping. */
|
|
470
|
+
export function setAsyncIcaclsBeltSchedulerForTests(
|
|
471
|
+
scheduler: SubprocessDeadlineScheduler | null,
|
|
472
|
+
): void {
|
|
473
|
+
asyncIcaclsBeltScheduler = scheduler ?? scheduleAsyncIcaclsBelt;
|
|
474
|
+
}
|
|
475
|
+
|
|
392
476
|
/**
|
|
393
477
|
* Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner.
|
|
394
478
|
*
|
|
@@ -423,6 +507,58 @@ export function forgetHardenedSecretPath(targetPath: string): void {
|
|
|
423
507
|
hardenedPaths.delete(targetPath);
|
|
424
508
|
}
|
|
425
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Re-attribute an existing file memo to the SAME object after the caller wrote
|
|
512
|
+
* content to it through a descriptor whose identity it verified.
|
|
513
|
+
*
|
|
514
|
+
* This exists because `freshness` is `ctimeNs`, and on Windows libuv reports
|
|
515
|
+
* `st_ctim` from the NTFS ChangeTime, which moves when file DATA is written. An
|
|
516
|
+
* atomic writer therefore invalidated its own memo between the harden that
|
|
517
|
+
* protects the empty temp and the harden before the rename, and paid a second
|
|
518
|
+
* full `/grant:r` + `/inheritance:r` + `/remove:g` sequence to reapply the ACL
|
|
519
|
+
* that was already on the file. Every secret write on Windows paid it twice.
|
|
520
|
+
*
|
|
521
|
+
* Only the freshness moves, and only for an unchanged object: a different object
|
|
522
|
+
* retires the entry instead. A caller must have proven, immediately beforehand,
|
|
523
|
+
* that `targetPath` resolves to the object its own descriptor refers to.
|
|
524
|
+
*
|
|
525
|
+
* The cost of this is worth stating exactly, because `PathObservation` documents
|
|
526
|
+
* that freshness also moves when PERMISSIONS change, and this call cannot tell
|
|
527
|
+
* the two apart. So a DACL change landing between the harden and this call is
|
|
528
|
+
* absorbed instead of forcing a re-harden. That window is the caller's own
|
|
529
|
+
* content write; every permission change after this call still moves ctime
|
|
530
|
+
* again and still misses the memo, so the detection this memo provides is
|
|
531
|
+
* relocated, not removed.
|
|
532
|
+
*
|
|
533
|
+
* What makes the absorbed window acceptable is who can be in it. Once the harden
|
|
534
|
+
* has run, the DACL is an explicit owner-only ACE with inheritance removed, so
|
|
535
|
+
* no other principal can open the file for `WRITE_DAC` at all. The one principal
|
|
536
|
+
* who can still rewrite that DACL is one holding a handle opened BEFORE the
|
|
537
|
+
* harden, and Windows keeps the access granted to an open handle: that principal
|
|
538
|
+
* can equally rewrite the DACL after any later harden, and after the rename, on
|
|
539
|
+
* the same object. A second mutation pass never bounded that capability — it
|
|
540
|
+
* stripped an ACE the holder could immediately re-add — so declining to repeat
|
|
541
|
+
* it removes no guarantee anyone had.
|
|
542
|
+
*
|
|
543
|
+
* Refusal is cheap and safe in either direction: an unmoved memo simply means the
|
|
544
|
+
* caller's next harden runs in full.
|
|
545
|
+
*
|
|
546
|
+
* Returns whether the memo now describes what is at the path.
|
|
547
|
+
*/
|
|
548
|
+
export function reattributeHardenedSecretPath(targetPath: string): boolean {
|
|
549
|
+
const remembered = hardenedPaths.get(targetPath);
|
|
550
|
+
if (remembered === undefined) return false;
|
|
551
|
+
const current = observe(targetPath);
|
|
552
|
+
// Unreadable, or a different object: this is exactly the case the memo must
|
|
553
|
+
// not cover. Retire it so the next harden is a real one.
|
|
554
|
+
if (current === null || current.object !== remembered.object) {
|
|
555
|
+
hardenedPaths.delete(targetPath);
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
hardenedPaths.set(targetPath, current);
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
|
|
426
562
|
/**
|
|
427
563
|
* Ephemeral-path lifecycle release: clears the success memo AND any timeout
|
|
428
564
|
* memo keyed by THIS TEMP path in both namespaces. Call only after the temp is
|
|
@@ -160,7 +160,11 @@ async function defaultAsyncWindowsPrincipalRunner(
|
|
|
160
160
|
stderr: "ignore",
|
|
161
161
|
windowsHide: true,
|
|
162
162
|
});
|
|
163
|
-
|
|
163
|
+
// No kill grace. The grace exists so a dying child releases a path someone is about to
|
|
164
|
+
// remove; this lookup holds no such path, and it runs during `ocx start`, where the composed
|
|
165
|
+
// acceptance cases already measure real startups at up to 38.8s against a bounded watchdog.
|
|
166
|
+
// Paying two extra seconds per timed-out resolution there buys nothing and costs margin.
|
|
167
|
+
const { exitCode, timedOut } = await waitForSubprocessExit(proc, timeoutMs, 0);
|
|
164
168
|
// `.bytes()` rather than `.text()`, for the same reason as the sync runner above.
|
|
165
169
|
const stdout: string | Uint8Array = !timedOut && proc.stdout
|
|
166
170
|
? await new Response(proc.stdout).bytes().catch(() => new Uint8Array())
|
package/src/providers/derive.ts
CHANGED
|
@@ -266,6 +266,9 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
266
266
|
...(entry.requiresAdjacentResponsesToolResults !== undefined
|
|
267
267
|
? { requiresAdjacentResponsesToolResults: entry.requiresAdjacentResponsesToolResults }
|
|
268
268
|
: {}),
|
|
269
|
+
...(entry.requiresPairedResponsesToolResults !== undefined
|
|
270
|
+
? { requiresPairedResponsesToolResults: entry.requiresPairedResponsesToolResults }
|
|
271
|
+
: {}),
|
|
269
272
|
...(entry.annotateEmptyToolOutputs !== undefined
|
|
270
273
|
? { annotateEmptyToolOutputs: entry.annotateEmptyToolOutputs }
|
|
271
274
|
: {}),
|
|
@@ -541,6 +544,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
541
544
|
if (prov.requiresAdjacentResponsesToolResults === undefined && seed.requiresAdjacentResponsesToolResults !== undefined) {
|
|
542
545
|
prov.requiresAdjacentResponsesToolResults = seed.requiresAdjacentResponsesToolResults;
|
|
543
546
|
}
|
|
547
|
+
if (prov.requiresPairedResponsesToolResults === undefined && seed.requiresPairedResponsesToolResults !== undefined) {
|
|
548
|
+
prov.requiresPairedResponsesToolResults = seed.requiresPairedResponsesToolResults;
|
|
549
|
+
}
|
|
544
550
|
if (prov.annotateEmptyToolOutputs === undefined && seed.annotateEmptyToolOutputs !== undefined) {
|
|
545
551
|
prov.annotateEmptyToolOutputs = seed.annotateEmptyToolOutputs;
|
|
546
552
|
}
|
|
@@ -129,6 +129,14 @@ export function providerModelDiscoverySpecError(spec: ProviderModelDiscoverySpec
|
|
|
129
129
|
if (queryEntries.some(([key, value]) => !key.trim() || key.length > 128 || typeof value !== "string" || value.length > 512)) {
|
|
130
130
|
return "discovery query keys/values exceed their bounds";
|
|
131
131
|
}
|
|
132
|
+
for (const [field, value] of [
|
|
133
|
+
["envelopeKey", spec.envelopeKey],
|
|
134
|
+
["idField", spec.idField],
|
|
135
|
+
] as const) {
|
|
136
|
+
if (value !== undefined && (
|
|
137
|
+
typeof value !== "string" || !value || value !== value.trim() || value.length > 128
|
|
138
|
+
)) return `${field} must be a nonblank field name up to 128 characters`;
|
|
139
|
+
}
|
|
132
140
|
for (const [field, value, hardLimit] of [
|
|
133
141
|
["maxResponseBytes", spec.maxResponseBytes, MODEL_DISCOVERY_MAX_RESPONSE_BYTES],
|
|
134
142
|
["maxModels", spec.maxModels, MODEL_DISCOVERY_MAX_MODELS],
|
|
@@ -422,7 +430,7 @@ export function extractModelEnvelopeRows(
|
|
|
422
430
|
return { ok: true, rows };
|
|
423
431
|
}
|
|
424
432
|
|
|
425
|
-
/** Validate, bound, deduplicate, and
|
|
433
|
+
/** Validate, bound, deduplicate, and filter the declared envelope or a top-level array (Together `#617`). */
|
|
426
434
|
/**
|
|
427
435
|
* Metadata a sibling `models[]` array may contribute to an ALREADY-ADMITTED
|
|
428
436
|
* `data[]` row (#1797).
|
|
@@ -501,24 +509,26 @@ export function extractProviderModelItems(
|
|
|
501
509
|
let data: unknown[];
|
|
502
510
|
let siblings: SiblingIndex | null = null;
|
|
503
511
|
if (Array.isArray(value)) {
|
|
504
|
-
// Together-style top-level /models arrays.
|
|
505
|
-
// `models` key on openai-chat responses as valid
|
|
512
|
+
// Together-style top-level /models arrays. The default contract must not treat a stray
|
|
513
|
+
// `models` key on openai-chat responses as valid; only a provider spec may opt into it.
|
|
506
514
|
if (value.length > limit) return { ok: false, reason: "too_many_models" };
|
|
507
515
|
data = value;
|
|
508
516
|
} else {
|
|
509
|
-
const
|
|
517
|
+
const envelopeKey = discovery.spec?.envelopeKey ?? "data";
|
|
518
|
+
const envelope = extractModelEnvelopeRows(value, discovery.maxModels, [envelopeKey]);
|
|
510
519
|
if (!envelope.ok) return envelope;
|
|
511
520
|
data = envelope.rows;
|
|
512
|
-
siblings = buildSiblingIndex(value, limit);
|
|
521
|
+
siblings = envelopeKey === "data" ? buildSiblingIndex(value, limit) : null;
|
|
513
522
|
}
|
|
514
523
|
|
|
515
524
|
const items: ProviderModelsApiItem[] = [];
|
|
516
525
|
const seen = new Set<string>();
|
|
526
|
+
const idField = discovery.spec?.idField ?? "id";
|
|
517
527
|
for (const raw of data) {
|
|
518
528
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
519
529
|
return { ok: false, reason: "invalid_shape" };
|
|
520
530
|
}
|
|
521
|
-
const id = (raw as
|
|
531
|
+
const id = (raw as Record<string, unknown>)[idField];
|
|
522
532
|
if (!isValidModelDiscoveryModelId(id)) return { ok: false, reason: "invalid_shape" };
|
|
523
533
|
const prefix = discovery.spec?.stripIdPrefix;
|
|
524
534
|
let finalId = id;
|
|
@@ -526,7 +536,9 @@ export function extractProviderModelItems(
|
|
|
526
536
|
finalId = finalId.slice(prefix.length);
|
|
527
537
|
if (!isValidModelDiscoveryModelId(finalId)) continue;
|
|
528
538
|
}
|
|
529
|
-
const item = finalId === id
|
|
539
|
+
const item = finalId === id && idField === "id"
|
|
540
|
+
? raw as ProviderModelsApiItem
|
|
541
|
+
: { ...(raw as Record<string, unknown>), id: finalId };
|
|
530
542
|
// Admission is decided on the ORIGINAL `data[]` row, before any sibling
|
|
531
543
|
// enrichment. Merging first let a `models[]` entry supply the very field a
|
|
532
544
|
// provider filter requires — reproduced against the real Chutes policy,
|
|
@@ -282,6 +282,17 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
282
282
|
forwardCallerServiceTier: false,
|
|
283
283
|
},
|
|
284
284
|
},
|
|
285
|
+
// Grok 4.6/4.5 OAuth Responses replays Codex tool history. After a mid-stream 502/reset,
|
|
286
|
+
// the client can resend a function_call without a matching output, or with hook-injected
|
|
287
|
+
// developer context between the pair. Google already synthesizes a missing tool_result
|
|
288
|
+
// (#2199). xAI's Responses parser does not, so the next turns 400 and the thread snowballs.
|
|
289
|
+
// Reuse the existing adjacency capability (Kimi #4726, DeepSeek #1292). Do not set
|
|
290
|
+
// statelessResponses: xAI stores responses for 30 days and documents previous_response_id.
|
|
291
|
+
// https://docs.x.ai/developers/model-capabilities/text/comparison
|
|
292
|
+
requiresAdjacentResponsesToolResults: true,
|
|
293
|
+
// The dangling half of the same failure: a call whose output never arrived. Kimi accepts that
|
|
294
|
+
// shape, so this is a second capability rather than a widening of the one above.
|
|
295
|
+
requiresPairedResponsesToolResults: true,
|
|
285
296
|
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
|
|
286
297
|
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
|
|
287
298
|
// inputModalities undefined, and deriveComboCatalogModel defaults an undefined member to
|
|
@@ -56,6 +56,11 @@ import {
|
|
|
56
56
|
ALIBABA_TOKEN_PLAN_MODELS,
|
|
57
57
|
ALIBABA_TOKEN_PLAN_QWEN_MODELS,
|
|
58
58
|
ALIBABA_TOKEN_PLAN_INPUT_MODALITIES,
|
|
59
|
+
ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
|
|
60
|
+
ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
|
|
61
|
+
ALIBABA_TOKEN_PLAN_NO_VISION,
|
|
62
|
+
ALIBABA_TOKEN_PLAN_PRESERVE_REASONING,
|
|
63
|
+
QWEN38_FAMILY,
|
|
59
64
|
ALIBABA_INTL_TOKEN_PLAN_MODELS,
|
|
60
65
|
ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS,
|
|
61
66
|
TENCENT_CODING_PLAN_MODELS,
|
|
@@ -427,6 +432,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
|
|
|
427
432
|
// model_access_denied, which is why the Chat path cannot simply hang off the new base.
|
|
428
433
|
responsesPath: "/api/v1/responses",
|
|
429
434
|
chatCompletionsPath: "/api/coding/paas/v4/chat/completions",
|
|
435
|
+
modelDiscovery: { path: "/api/v1/models", envelopeKey: "models", idField: "slug" },
|
|
430
436
|
// The address this row occupied before the move. A saved custom provider still pointing
|
|
431
437
|
// at the Chat endpoint keeps receiving this row's metadata (#1100).
|
|
432
438
|
destinationAliases: [{ baseUrl: "https://api.z.ai/api/coding/paas/v4", adapter: "openai-chat" }],
|
|
@@ -724,22 +730,35 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
|
|
|
724
730
|
liveModels: false,
|
|
725
731
|
note: "Token Plan Personal Edition · China (Beijing)",
|
|
726
732
|
modelInputModalities: ALIBABA_TOKEN_PLAN_INPUT_MODALITIES,
|
|
727
|
-
modelContextWindows:
|
|
728
|
-
|
|
729
|
-
"qwen3.6-flash": 1_000_000, "glm-5.3": 1_000_000, "glm-5.3-flash": 1_000_000, "glm-5.2": 1_000_000,
|
|
730
|
-
},
|
|
733
|
+
modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
|
|
734
|
+
modelMaxOutputTokens: ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
|
|
731
735
|
modelReasoningEfforts: {
|
|
732
736
|
...Object.fromEntries(ALIBABA_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
733
|
-
|
|
734
|
-
"glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
|
|
735
|
-
"glm-5.3-flash": ZAI_GLM_53_REASONING_EFFORTS,
|
|
737
|
+
...Object.fromEntries(QWEN38_FAMILY.map(id => [id, QWEN38_REASONING_EFFORTS])),
|
|
736
738
|
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
739
|
+
"deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek-v4-pro"),
|
|
740
|
+
"deepseek-v4-pro-0813": deepseekThinkingEffortsFor("deepseek-v4-pro-0813"),
|
|
741
|
+
"deepseek-v4-flash-0731": deepseekThinkingEffortsFor("deepseek-v4-flash-0731"),
|
|
742
|
+
"deepseek-v4.1-flash": deepseekThinkingEffortsFor("deepseek-v4.1-flash"),
|
|
743
|
+
},
|
|
744
|
+
modelReasoningEffortMap: {
|
|
745
|
+
"deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro"),
|
|
746
|
+
"deepseek-v4-pro-0813": deepseekReasoningMapFor("deepseek-v4-pro-0813"),
|
|
747
|
+
"deepseek-v4-flash-0731": deepseekReasoningMapFor("deepseek-v4-flash-0731"),
|
|
748
|
+
"deepseek-v4.1-flash": deepseekReasoningMapFor("deepseek-v4.1-flash"),
|
|
737
749
|
},
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
750
|
+
// Probed 260915 on the plan gateway: json_object returns valid JSON, strict
|
|
751
|
+
// json_schema is rejected 400 ("This response_format type is unavailable now")
|
|
752
|
+
// in both thinking modes, so requests downgrade to json_object rather than
|
|
753
|
+
// sending a schema the gateway refuses.
|
|
754
|
+
noJsonSchemaModels: ["deepseek-v4.1-flash"],
|
|
755
|
+
modelDefaultReasoningEfforts: Object.fromEntries(QWEN38_FAMILY.map(id => [id, "xhigh"])),
|
|
756
|
+
directReasoningEffortModels: QWEN38_FAMILY,
|
|
757
|
+
thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS.filter(id => !QWEN38_FAMILY.includes(id)),
|
|
758
|
+
preserveReasoningContentModels: ALIBABA_TOKEN_PLAN_PRESERVE_REASONING,
|
|
759
|
+
noVisionModels: ALIBABA_TOKEN_PLAN_NO_VISION,
|
|
760
|
+
// The gateway accepts prompt_cache_key on every Token Plan chat model (probed 260902).
|
|
761
|
+
promptCacheKey: true,
|
|
743
762
|
},
|
|
744
763
|
{
|
|
745
764
|
id: "alibaba-token-plan-intl",
|
|
@@ -756,31 +775,34 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
|
|
|
756
775
|
note: "Token Plan Team Edition · Singapore (ap-southeast-1)",
|
|
757
776
|
metadataModelIdNormalize: "case-insensitive",
|
|
758
777
|
modelInputModalities: ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES,
|
|
759
|
-
modelContextWindows:
|
|
760
|
-
|
|
761
|
-
"qwen3.7-max": 1_000_000, "qwen3.7-plus": 1_000_000, "qwen3.6-plus": 1_000_000, "qwen3.6-flash": 1_000_000,
|
|
762
|
-
"deepseek-v4-flash": 1_000_000, "deepseek-v3.2": 131_072,
|
|
763
|
-
"kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 262_144,
|
|
764
|
-
"glm-5.3": 1_000_000, "glm-5.3-flash": 1_000_000, "glm-5.2": 1_000_000, "glm-5.1": 1_000_000, "glm-5": 1_000_000,
|
|
765
|
-
"MiniMax-M2.5": 204_800,
|
|
766
|
-
},
|
|
778
|
+
modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
|
|
779
|
+
modelMaxOutputTokens: ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
|
|
767
780
|
modelReasoningEfforts: {
|
|
768
781
|
...Object.fromEntries(ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
769
|
-
|
|
770
|
-
"glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
|
|
771
|
-
"glm-5.3-flash": ZAI_GLM_53_REASONING_EFFORTS,
|
|
782
|
+
...Object.fromEntries(QWEN38_FAMILY.map(id => [id, QWEN38_REASONING_EFFORTS])),
|
|
772
783
|
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
784
|
+
"deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek-v4-pro"),
|
|
785
|
+
"deepseek-v4-pro-0813": deepseekThinkingEffortsFor("deepseek-v4-pro-0813"),
|
|
773
786
|
"deepseek-v4-flash": deepseekThinkingEffortsFor("deepseek-v4-flash"),
|
|
787
|
+
"deepseek-v4-flash-0731": deepseekThinkingEffortsFor("deepseek-v4-flash-0731"),
|
|
788
|
+
"deepseek-v4.1-flash": deepseekThinkingEffortsFor("deepseek-v4.1-flash"),
|
|
774
789
|
},
|
|
775
790
|
modelReasoningEffortMap: {
|
|
791
|
+
"deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro"),
|
|
792
|
+
"deepseek-v4-pro-0813": deepseekReasoningMapFor("deepseek-v4-pro-0813"),
|
|
776
793
|
"deepseek-v4-flash": deepseekReasoningMapFor("deepseek-v4-flash"),
|
|
794
|
+
"deepseek-v4-flash-0731": deepseekReasoningMapFor("deepseek-v4-flash-0731"),
|
|
795
|
+
"deepseek-v4.1-flash": deepseekReasoningMapFor("deepseek-v4.1-flash"),
|
|
777
796
|
},
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
797
|
+
// Same 260915 json_schema rejection probe as the Beijing entry.
|
|
798
|
+
noJsonSchemaModels: ["deepseek-v4.1-flash"],
|
|
799
|
+
directReasoningEffortModels: QWEN38_FAMILY,
|
|
800
|
+
thinkingBudgetModels: ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.filter(id => !QWEN38_FAMILY.includes(id)),
|
|
801
|
+
preserveReasoningContentModels: ALIBABA_TOKEN_PLAN_PRESERVE_REASONING,
|
|
802
|
+
noVisionModels: ALIBABA_TOKEN_PLAN_NO_VISION,
|
|
782
803
|
noReasoningModels: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "deepseek-v3.2", "glm-5.1", "glm-5", "MiniMax-M2.5"],
|
|
783
|
-
modelDefaultReasoningEfforts:
|
|
804
|
+
modelDefaultReasoningEfforts: Object.fromEntries(QWEN38_FAMILY.map(id => [id, "xhigh"])),
|
|
805
|
+
promptCacheKey: true,
|
|
784
806
|
},
|
|
785
807
|
// NEEDS_HUMAN 2026-07-10: kept for config compatibility, but this is a dashboard URL,
|
|
786
808
|
// no /models endpoint is documented, and tools are silently ignored upstream per docs.parallel.ai.
|