@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.3
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/dist/algo/candidateAssembly.js +9 -6
- package/dist/algo/cycleEngine.d.ts +11 -0
- package/dist/algo/cycleEngine.js +12 -6
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- package/dist/algo/geneSelection.d.ts +12 -1
- package/dist/algo/geneSelection.js +24 -8
- package/dist/algo/index.d.ts +1 -0
- package/dist/algo/index.js +1 -0
- package/dist/algo/memoryGraph.d.ts +62 -0
- package/dist/algo/memoryGraph.js +86 -0
- package/dist/algo/orchestrator.d.ts +3 -0
- package/dist/algo/orchestrator.js +14 -2
- package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
- package/dist/assetstore/assetSidecarRecords.js +142 -0
- package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
- package/dist/assetstore/assetSidecarRecovery.js +288 -0
- package/dist/assetstore/assetStoreHealth.d.ts +75 -0
- package/dist/assetstore/assetStoreHealth.js +277 -0
- package/dist/assetstore/assetStoreLayout.d.ts +2 -0
- package/dist/assetstore/assetStoreLayout.js +6 -0
- package/dist/assetstore/assetStoreStorage.d.ts +42 -0
- package/dist/assetstore/assetStoreStorage.js +318 -0
- package/dist/assetstore/assetSyncLedger.d.ts +5 -1
- package/dist/assetstore/assetSyncLedger.js +44 -64
- package/dist/assetstore/index.d.ts +2 -0
- package/dist/assetstore/index.js +2 -0
- package/dist/assetstore/localJsonl.d.ts +1 -0
- package/dist/assetstore/localJsonl.js +36 -32
- package/dist/assetstore/provenance.d.ts +13 -0
- package/dist/assetstore/provenance.js +60 -84
- package/dist/assetstore/provider.d.ts +2 -0
- package/dist/assetstore/reviewFilter.js +3 -1
- package/dist/assetstore/reviewLedger.d.ts +8 -2
- package/dist/assetstore/reviewLedger.js +71 -45
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/triggerShift.d.ts +62 -0
- package/dist/benchmark/triggerShift.js +106 -0
- package/dist/events/ingest.d.ts +1 -1
- package/dist/events/ingest.js +2 -0
- package/dist/events/paths.d.ts +1 -1
- package/dist/events/paths.js +2 -2
- package/dist/exec/autoExec.d.ts +6 -1
- package/dist/exec/autoExec.js +31 -0
- package/dist/exec/autonomousCycle.d.ts +2 -0
- package/dist/exec/autonomousCycle.js +5 -0
- package/dist/exec/claudeBridge.d.ts +7 -2
- package/dist/exec/claudeBridge.js +92 -14
- package/dist/exec/prompt.js +9 -0
- package/dist/exec/runnerRegistry.d.ts +56 -12
- package/dist/exec/runnerRegistry.js +272 -22
- package/dist/hub/bindings.js +12 -2
- package/dist/ops/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +10 -1
- package/dist/ops/selfUpdate.js +64 -15
- package/dist/util/fileLock.d.ts +19 -2
- package/dist/util/fileLock.js +166 -31
- package/package.json +5 -1
|
@@ -21,6 +21,13 @@ export class UnsupportedCursorSkipPermissionsError extends Error {
|
|
|
21
21
|
this.name = 'UnsupportedCursorSkipPermissionsError';
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/** Thrown when Gemini permission options cannot be mapped to a verified bounded CLI contract. */
|
|
25
|
+
export class UnsupportedGeminiPermissionOptionsError extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super('gemini runner does not support skipPermissions or allowedTools: --yolo is unbounded and --allowed-tools is deprecated; the verified runner uses --approval-mode auto_edit only');
|
|
28
|
+
this.name = 'UnsupportedGeminiPermissionOptionsError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
24
31
|
/** Thrown when Cursor's Windows installation cannot be reduced to a shell-free node.exe + index.js launch. */
|
|
25
32
|
export class UnsupportedCursorWindowsRunnerError extends Error {
|
|
26
33
|
constructor() {
|
|
@@ -114,41 +121,190 @@ function cursorVersionKey(version) {
|
|
|
114
121
|
const second = match[6] ?? '0';
|
|
115
122
|
return [year, month, day, hour, minute, second].map((part, index) => index === 0 ? part : part.padStart(2, '0')).join('');
|
|
116
123
|
}
|
|
124
|
+
const WINDOWS_TREE_KILL_TIMEOUT_MS = 5_000;
|
|
125
|
+
/** Build the shell-free taskkill invocation used for Windows process-tree termination. */
|
|
126
|
+
export function windowsTreeKillCommand(pid) {
|
|
127
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
128
|
+
throw new RangeError(`invalid process id: ${pid}`);
|
|
129
|
+
return { command: 'taskkill.exe', args: ['/PID', String(pid), '/T', '/F'] };
|
|
130
|
+
}
|
|
131
|
+
/** Run taskkill and report whether Windows accepted the process-tree termination request. */
|
|
132
|
+
export function killWindowsProcessTree(pid, spawnCommand = spawn, timeoutMs = WINDOWS_TREE_KILL_TIMEOUT_MS) {
|
|
133
|
+
const { command, args } = windowsTreeKillCommand(pid);
|
|
134
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
|
|
135
|
+
throw new RangeError(`invalid taskkill timeout: ${timeoutMs}`);
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
let settled = false;
|
|
138
|
+
let killer;
|
|
139
|
+
const finish = (ok, terminateKiller = false) => {
|
|
140
|
+
if (settled)
|
|
141
|
+
return;
|
|
142
|
+
settled = true;
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
if (terminateKiller) {
|
|
145
|
+
try {
|
|
146
|
+
killer?.kill?.('SIGKILL');
|
|
147
|
+
}
|
|
148
|
+
catch { /* best-effort watchdog cleanup */ }
|
|
149
|
+
}
|
|
150
|
+
resolve(ok);
|
|
151
|
+
};
|
|
152
|
+
const timer = setTimeout(() => finish(false, true), timeoutMs);
|
|
153
|
+
timer.unref?.();
|
|
154
|
+
try {
|
|
155
|
+
killer = spawnCommand(command, args, { shell: false, windowsHide: true, stdio: 'ignore' });
|
|
156
|
+
killer.once('error', () => finish(false));
|
|
157
|
+
killer.once('close', (code) => finish(code === 0));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
finish(false);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
117
164
|
/**
|
|
118
165
|
* Promise wrapper over spawn (shell:false). Optionally writes `input` to stdin; resolves with stdout/exit.
|
|
119
166
|
* On timeout the WHOLE process group is killed, not just the direct child (finding #39.5): an agent spawns
|
|
120
167
|
* tool subprocesses (grandchildren) that would otherwise orphan and leak. On POSIX we spawn detached (the
|
|
121
|
-
* child becomes its own group leader) and SIGKILL the group via the negative pid
|
|
122
|
-
*
|
|
168
|
+
* child becomes its own group leader) and SIGKILL the group via the negative pid. Windows runs
|
|
169
|
+
* `taskkill.exe /PID <pid> /T /F` without a shell and waits for that command before resolving.
|
|
123
170
|
*/
|
|
124
171
|
export function spawnCapture(cmd, args, opts) {
|
|
125
172
|
return new Promise((resolve, reject) => {
|
|
126
|
-
|
|
173
|
+
if (opts.signal?.aborted) {
|
|
174
|
+
resolve({ code: null, stdout: '', stderr: '', termination: 'cancelled' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const platform = opts.processPlatform ?? process.platform;
|
|
178
|
+
const detached = platform !== 'win32';
|
|
127
179
|
const r = resolveSpawnCommand(cmd, args, opts.env, opts.resolvePlatform ?? process.platform);
|
|
128
180
|
const child = spawn(r.cmd, r.args, { cwd: opts.cwd, shell: false, detached, ...(opts.env ? { env: opts.env } : {}) });
|
|
129
181
|
let stdout = '';
|
|
130
182
|
let stderr = '';
|
|
183
|
+
let termination = 'exit';
|
|
184
|
+
let killPromise;
|
|
185
|
+
let settled = false;
|
|
131
186
|
const killTree = () => {
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
187
|
+
if (killPromise)
|
|
188
|
+
return killPromise;
|
|
189
|
+
killPromise = (async () => {
|
|
190
|
+
if (platform === 'win32' && typeof child.pid === 'number') {
|
|
191
|
+
let killed = false;
|
|
192
|
+
try {
|
|
193
|
+
killed = await (opts.windowsProcessTreeKiller ?? killWindowsProcessTree)(child.pid);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Treat an injected/custom killer rejection like taskkill failure and fall back to the direct child.
|
|
197
|
+
}
|
|
198
|
+
if (killed)
|
|
199
|
+
return;
|
|
136
200
|
}
|
|
137
|
-
|
|
201
|
+
if (detached && typeof child.pid === 'number') {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
catch { /* group gone; fall back */ }
|
|
207
|
+
}
|
|
208
|
+
child.kill('SIGKILL');
|
|
209
|
+
})();
|
|
210
|
+
return killPromise;
|
|
211
|
+
};
|
|
212
|
+
const cancel = () => {
|
|
213
|
+
if (termination !== 'exit')
|
|
214
|
+
return;
|
|
215
|
+
termination = 'cancelled';
|
|
216
|
+
void killTree();
|
|
217
|
+
};
|
|
218
|
+
const timeout = () => {
|
|
219
|
+
if (termination !== 'exit')
|
|
220
|
+
return;
|
|
221
|
+
termination = 'timeout';
|
|
222
|
+
void killTree();
|
|
223
|
+
};
|
|
224
|
+
const ignoreProcessSignal = () => { };
|
|
225
|
+
const cleanup = () => {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
opts.signal?.removeEventListener('abort', cancel);
|
|
228
|
+
if (opts.processSignalMode === 'ignore') {
|
|
229
|
+
process.removeListener('SIGINT', ignoreProcessSignal);
|
|
230
|
+
process.removeListener('SIGTERM', ignoreProcessSignal);
|
|
138
231
|
}
|
|
139
|
-
|
|
232
|
+
else {
|
|
233
|
+
process.removeListener('SIGINT', cancel);
|
|
234
|
+
process.removeListener('SIGTERM', cancel);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const settle = async (finish) => {
|
|
238
|
+
if (settled)
|
|
239
|
+
return;
|
|
240
|
+
settled = true;
|
|
241
|
+
if (killPromise)
|
|
242
|
+
await killPromise;
|
|
243
|
+
cleanup();
|
|
244
|
+
finish();
|
|
140
245
|
};
|
|
141
|
-
const timer = setTimeout(
|
|
246
|
+
const timer = setTimeout(timeout, opts.timeoutMs);
|
|
247
|
+
opts.signal?.addEventListener('abort', cancel, { once: true });
|
|
248
|
+
// A detached POSIX child would otherwise survive Ctrl-C/SIGTERM. Cancel first so the bridge can clean its
|
|
249
|
+
// worktree and return a failure instead of leaking an agent or tool subprocess.
|
|
250
|
+
if (opts.processSignalMode === 'ignore') {
|
|
251
|
+
process.on('SIGINT', ignoreProcessSignal);
|
|
252
|
+
process.on('SIGTERM', ignoreProcessSignal);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
process.once('SIGINT', cancel);
|
|
256
|
+
process.once('SIGTERM', cancel);
|
|
257
|
+
}
|
|
142
258
|
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
143
259
|
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
144
|
-
child.on('error', (e) => {
|
|
145
|
-
child.on('close', (code) => {
|
|
260
|
+
child.on('error', (e) => { void settle(() => reject(e)); });
|
|
261
|
+
child.on('close', (code) => { void settle(() => resolve({ code, stdout, stderr, termination })); });
|
|
146
262
|
if (opts.input !== undefined) {
|
|
147
263
|
child.stdin?.write(opts.input);
|
|
148
264
|
child.stdin?.end();
|
|
149
265
|
}
|
|
150
266
|
});
|
|
151
267
|
}
|
|
268
|
+
/** Map the shared process result into the failure taxonomy used by plain-text runners. */
|
|
269
|
+
export function classifyBasicRunnerResult(runner, result, timeoutMs) {
|
|
270
|
+
if (result.termination === 'timeout') {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
output: result.stdout,
|
|
274
|
+
error: `${runner} timed out after ${timeoutMs}ms`,
|
|
275
|
+
failureKind: 'timeout',
|
|
276
|
+
exitCode: result.code,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (result.termination === 'cancelled') {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
output: result.stdout,
|
|
283
|
+
error: `${runner} execution cancelled`,
|
|
284
|
+
failureKind: 'cancelled',
|
|
285
|
+
exitCode: result.code,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (result.code !== 0) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
output: result.stdout,
|
|
292
|
+
error: result.stderr || `${runner} exited with code ${String(result.code)}`,
|
|
293
|
+
failureKind: 'non_zero_exit',
|
|
294
|
+
exitCode: result.code,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return { ok: true, output: result.stdout };
|
|
298
|
+
}
|
|
299
|
+
function spawnFailureResult(error) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
output: '',
|
|
303
|
+
error: error instanceof Error ? error.message : String(error),
|
|
304
|
+
failureKind: 'spawn_failed',
|
|
305
|
+
exitCode: null,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
152
308
|
/**
|
|
153
309
|
* Build the `claude -p` argv for the given options (pure — testable without spawning).
|
|
154
310
|
* Safety invariant: skipPermissions (bypassing prompts) is only allowed together with a non-empty
|
|
@@ -175,12 +331,13 @@ export function claudeRunnerArgs(opts = {}) {
|
|
|
175
331
|
export function makeClaudeHeadlessRunner(opts = {}) {
|
|
176
332
|
const args = claudeRunnerArgs(opts);
|
|
177
333
|
return async (prompt, ctx) => {
|
|
334
|
+
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
178
335
|
try {
|
|
179
|
-
const
|
|
180
|
-
return
|
|
336
|
+
const result = await spawnCapture('claude', args, { cwd: ctx.cwd, timeoutMs, input: prompt, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
|
|
337
|
+
return classifyBasicRunnerResult('claude', result, timeoutMs);
|
|
181
338
|
}
|
|
182
339
|
catch (e) {
|
|
183
|
-
return
|
|
340
|
+
return spawnFailureResult(e);
|
|
184
341
|
}
|
|
185
342
|
};
|
|
186
343
|
}
|
|
@@ -213,12 +370,95 @@ export function codexRunnerArgs(opts = {}) {
|
|
|
213
370
|
export function makeCodexHeadlessRunner(opts = {}) {
|
|
214
371
|
const args = codexRunnerArgs(opts);
|
|
215
372
|
return async (prompt, ctx) => {
|
|
373
|
+
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
216
374
|
try {
|
|
217
|
-
const
|
|
218
|
-
return
|
|
375
|
+
const result = await spawnCapture('codex', [...args, '--cd', ctx.cwd, prompt], { cwd: ctx.cwd, timeoutMs, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
|
|
376
|
+
return classifyBasicRunnerResult('codex', result, timeoutMs);
|
|
219
377
|
}
|
|
220
378
|
catch (e) {
|
|
221
|
-
return
|
|
379
|
+
return spawnFailureResult(e);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const GEMINI_PERMISSION_DENIAL_RE = /agent execution blocked|permission denied|approval required|not approved|denied by (?:policy|user|admin)|tool (?:call )?(?:was )?denied/i;
|
|
384
|
+
const GEMINI_TERMINATION_WARNINGS = [
|
|
385
|
+
{ pattern: /^(?:[^:\r\n]{1,80}:\s*)?Agent execution stopped\b/i, error: 'gemini agent execution stopped' },
|
|
386
|
+
{ pattern: /^(?:[^:\r\n]{1,80}:\s*)?Loop detected\b/i, error: 'gemini loop detected' },
|
|
387
|
+
{ pattern: /^(?:[^:\r\n]{1,80}:\s*)?Maximum session turns exceeded\b/i, error: 'gemini maximum session turns exceeded' },
|
|
388
|
+
];
|
|
389
|
+
function geminiMessage(value) {
|
|
390
|
+
if (typeof value === 'string')
|
|
391
|
+
return value;
|
|
392
|
+
if (value && typeof value === 'object') {
|
|
393
|
+
const record = value;
|
|
394
|
+
const type = typeof record['type'] === 'string' ? record['type'] : '';
|
|
395
|
+
const message = typeof record['message'] === 'string' ? record['message'] : '';
|
|
396
|
+
return [type, message].filter(Boolean).join(': ');
|
|
397
|
+
}
|
|
398
|
+
return value === undefined ? '' : String(value);
|
|
399
|
+
}
|
|
400
|
+
function geminiWarnings(value) {
|
|
401
|
+
return Array.isArray(value) ? value.map(geminiMessage).filter(Boolean) : [];
|
|
402
|
+
}
|
|
403
|
+
/** Build verified Gemini CLI argv. The prompt is appended separately as one argv element with shell:false. */
|
|
404
|
+
export function geminiRunnerArgs(opts = {}) {
|
|
405
|
+
if (opts.skipPermissions || (opts.allowedTools?.length ?? 0) > 0)
|
|
406
|
+
throw new UnsupportedGeminiPermissionOptionsError();
|
|
407
|
+
const args = ['--output-format', 'json', '--approval-mode', 'auto_edit', '--skip-trust'];
|
|
408
|
+
if (opts.model)
|
|
409
|
+
args.push('--model', opts.model);
|
|
410
|
+
return args;
|
|
411
|
+
}
|
|
412
|
+
/** Headless Gemini runner with structured failure classification; stdout text alone never proves execution success. */
|
|
413
|
+
export function makeGeminiHeadlessRunner(opts = {}) {
|
|
414
|
+
const args = geminiRunnerArgs(opts);
|
|
415
|
+
return async (prompt, ctx) => {
|
|
416
|
+
try {
|
|
417
|
+
const result = await spawnCapture('gemini', [...args, '--prompt', prompt], {
|
|
418
|
+
cwd: ctx.cwd,
|
|
419
|
+
timeoutMs: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
420
|
+
...(ctx.env ? { env: ctx.env } : {}),
|
|
421
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
422
|
+
});
|
|
423
|
+
if (result.termination === 'timeout') {
|
|
424
|
+
return { ok: false, output: result.stdout, error: `gemini timed out after ${ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, failureKind: 'timeout', exitCode: result.code };
|
|
425
|
+
}
|
|
426
|
+
if (result.termination === 'cancelled') {
|
|
427
|
+
return { ok: false, output: result.stdout, error: 'gemini execution cancelled', failureKind: 'cancelled', exitCode: result.code };
|
|
428
|
+
}
|
|
429
|
+
let envelope;
|
|
430
|
+
try {
|
|
431
|
+
const parsed = JSON.parse(result.stdout);
|
|
432
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
433
|
+
throw new Error('JSON object required');
|
|
434
|
+
envelope = parsed;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
const error = result.stderr || (result.code === 0 ? 'gemini returned invalid JSON output' : `gemini exited with code ${String(result.code)}`);
|
|
438
|
+
return { ok: false, output: result.stdout, error, failureKind: result.code === 0 ? 'invalid_output' : 'non_zero_exit', exitCode: result.code };
|
|
439
|
+
}
|
|
440
|
+
const structuredError = geminiMessage(envelope.error);
|
|
441
|
+
const warnings = geminiWarnings(envelope.warnings);
|
|
442
|
+
const terminationError = result.code === 0
|
|
443
|
+
? GEMINI_TERMINATION_WARNINGS.find(({ pattern }) => warnings.some((warning) => pattern.test(warning)))?.error
|
|
444
|
+
: undefined;
|
|
445
|
+
if (terminationError) {
|
|
446
|
+
return { ok: false, output: geminiMessage(envelope.response), error: terminationError, failureKind: 'runtime_error', exitCode: result.code };
|
|
447
|
+
}
|
|
448
|
+
const denial = [structuredError, ...warnings, result.stderr].find((message) => GEMINI_PERMISSION_DENIAL_RE.test(message));
|
|
449
|
+
if (denial) {
|
|
450
|
+
return { ok: false, output: geminiMessage(envelope.response), error: denial, failureKind: 'permission_denied', exitCode: result.code };
|
|
451
|
+
}
|
|
452
|
+
if (result.code !== 0) {
|
|
453
|
+
return { ok: false, output: geminiMessage(envelope.response), error: structuredError || result.stderr || `gemini exited with code ${String(result.code)}`, failureKind: 'non_zero_exit', exitCode: result.code };
|
|
454
|
+
}
|
|
455
|
+
if (structuredError) {
|
|
456
|
+
return { ok: false, output: geminiMessage(envelope.response), error: structuredError, failureKind: 'runtime_error', exitCode: result.code };
|
|
457
|
+
}
|
|
458
|
+
return { ok: true, output: geminiMessage(envelope.response), exitCode: result.code };
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
return { ok: false, output: '', error: error instanceof Error ? error.message : String(error), failureKind: 'spawn_failed', exitCode: null };
|
|
222
462
|
}
|
|
223
463
|
};
|
|
224
464
|
}
|
|
@@ -264,17 +504,19 @@ export function makeCursorHeadlessRunner(opts = {}, platform = process.platform)
|
|
|
264
504
|
const args = cursorRunnerArgs(opts);
|
|
265
505
|
return async (prompt, ctx) => {
|
|
266
506
|
assertCursorRunnerPlatformSupported(platform, ctx.env ?? process.env);
|
|
507
|
+
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
267
508
|
try {
|
|
268
|
-
const
|
|
509
|
+
const result = await spawnCapture('cursor-agent', [...args, prompt], {
|
|
269
510
|
cwd: ctx.cwd,
|
|
270
|
-
timeoutMs
|
|
511
|
+
timeoutMs,
|
|
271
512
|
resolvePlatform: platform,
|
|
272
513
|
...(ctx.env ? { env: ctx.env } : {}),
|
|
514
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
273
515
|
});
|
|
274
|
-
return
|
|
516
|
+
return classifyBasicRunnerResult('cursor', result, timeoutMs);
|
|
275
517
|
}
|
|
276
518
|
catch (e) {
|
|
277
|
-
return
|
|
519
|
+
return spawnFailureResult(e);
|
|
278
520
|
}
|
|
279
521
|
};
|
|
280
522
|
}
|
|
@@ -283,6 +525,14 @@ const RUNNER_SPECS = {
|
|
|
283
525
|
codex: { name: 'codex', makeRunner: makeCodexHeadlessRunner, envAllow: { prefixes: ['OPENAI_', 'CODEX_'] } },
|
|
284
526
|
// cursor keeps only its OWN auth env (CURSOR_); like every runner it never inherits another's vendor key.
|
|
285
527
|
cursor: { name: 'cursor', makeRunner: makeCursorHeadlessRunner, envAllow: { prefixes: ['CURSOR_'] } },
|
|
528
|
+
gemini: {
|
|
529
|
+
name: 'gemini',
|
|
530
|
+
makeRunner: makeGeminiHeadlessRunner,
|
|
531
|
+
envAllow: {
|
|
532
|
+
prefixes: ['GEMINI_'],
|
|
533
|
+
keys: ['GOOGLE_API_KEY', 'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION', 'GOOGLE_GENAI_USE_VERTEXAI'],
|
|
534
|
+
},
|
|
535
|
+
},
|
|
286
536
|
};
|
|
287
537
|
/** Resolve a runner spec by name (default 'claude' — byte-identical to the pre-registry behavior). */
|
|
288
538
|
export function getRunnerSpec(name = 'claude') {
|
package/dist/hub/bindings.js
CHANGED
|
@@ -67,7 +67,10 @@ export function makeHubBindings(cap, options = {}) {
|
|
|
67
67
|
const p = e.payload;
|
|
68
68
|
const taskId = firstString(p.taskId, p.task_id);
|
|
69
69
|
const assetId = firstString(p.assetId, p.asset_id);
|
|
70
|
-
const claimId = firstString(p.claimId, p.claim_id)
|
|
70
|
+
const claimId = firstString(p.claimId, p.claim_id);
|
|
71
|
+
if (!claimId) {
|
|
72
|
+
throw new PublishRejectedError('invalid_task_complete', true, 'claim_id is required because task claim handles are opaque');
|
|
73
|
+
}
|
|
71
74
|
const context = taskId && assetId ? { taskId, assetId } : undefined;
|
|
72
75
|
return cap.task.complete(claimId, p.result, context);
|
|
73
76
|
}
|
|
@@ -96,5 +99,12 @@ export function makeHubBindings(cap, options = {}) {
|
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
function firstString(...values) {
|
|
99
|
-
|
|
102
|
+
for (const value of values) {
|
|
103
|
+
if (typeof value !== 'string')
|
|
104
|
+
continue;
|
|
105
|
+
const trimmed = value.trim();
|
|
106
|
+
if (trimmed)
|
|
107
|
+
return trimmed;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
100
110
|
}
|
package/dist/ops/savingsCore.js
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
// gate (it reads the vendored files at runtime, so this module stays free of cross-rootDir JSON
|
|
7
7
|
// imports), and savings-core's daily drift-check locks the vendored copies to upstream. To change a
|
|
8
8
|
// coefficient: change savings-core first, regenerate vectors there, bump spec_version, then
|
|
9
|
-
// re-vendor
|
|
10
|
-
// evolver-private-dev / evolver-v2-enterprise-dev.
|
|
9
|
+
// re-vendor it into each consuming distribution.
|
|
11
10
|
//
|
|
12
11
|
// Core purity contract holds: no I/O, no Date.now/Math.random — pure functions over numbers.
|
|
13
12
|
export const SAVINGS_SPEC_VERSION = '0.3.0';
|
package/dist/ops/selfUpdate.d.ts
CHANGED
|
@@ -103,4 +103,13 @@ export interface DownloadedArtifact {
|
|
|
103
103
|
*
|
|
104
104
|
* Fail-closed everywhere: any doubt (bad structure, hash gap, signature failure, thrown crypto error) → reject.
|
|
105
105
|
*/
|
|
106
|
-
export declare function verifyManifest(manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey?: string | KeyObject): VerifyResult;
|
|
106
|
+
export declare function verifyManifest(manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey?: string | KeyObject): VerifyResult;
|
|
107
|
+
/**
|
|
108
|
+
* Verify exactly one explicitly selected download against a complete release manifest.
|
|
109
|
+
*
|
|
110
|
+
* Unlike verifyManifest, this verifier does not require downloading every platform artifact. When a public key is
|
|
111
|
+
* configured it first verifies the Ed25519 signature over the complete canonical manifest, then requires exactly
|
|
112
|
+
* one downloaded artifact and exactly one manifest entry with the same path, and finally checks that artifact's
|
|
113
|
+
* sha256. The complete manifest is never filtered or reconstructed before signature verification.
|
|
114
|
+
*/
|
|
115
|
+
export declare function verifySelectedManifestArtifact(manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey?: string | KeyObject): VerifyResult;
|
package/dist/ops/selfUpdate.js
CHANGED
|
@@ -238,6 +238,25 @@ function toPublicKey(publicKey) {
|
|
|
238
238
|
const der = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), raw]);
|
|
239
239
|
return createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
240
240
|
}
|
|
241
|
+
/** Verify the signature over the complete canonical manifest when a public key is configured. */
|
|
242
|
+
function verifyConfiguredManifestSignature(manifest, publicKey) {
|
|
243
|
+
if (!publicKey)
|
|
244
|
+
return { ok: true, reason: 'signature_not_required' };
|
|
245
|
+
if (!manifest.signature)
|
|
246
|
+
return { ok: false, reason: 'signature_required_but_missing' };
|
|
247
|
+
if (manifest.signatureAlg && manifest.signatureAlg !== 'ed25519') {
|
|
248
|
+
return { ok: false, reason: 'signature_alg_unsupported' };
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const key = toPublicKey(publicKey);
|
|
252
|
+
const ok = cryptoVerify(null, // Ed25519 ignores the digest-name arg
|
|
253
|
+
canonicalManifestBytes(manifest), key, Buffer.from(manifest.signature, 'base64'));
|
|
254
|
+
return ok ? { ok: true, reason: 'signature_verified' } : { ok: false, reason: 'signature_invalid' };
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return { ok: false, reason: `signature_verify_error: ${err instanceof Error ? err.message : String(err)}` };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
241
260
|
/**
|
|
242
261
|
* Verify downloaded artifacts against the manifest. PURE + deterministic (takes already-read bytes/hashes; does
|
|
243
262
|
* NO I/O). Two layers:
|
|
@@ -274,21 +293,51 @@ export function verifyManifest(manifest, downloaded, publicKey) {
|
|
|
274
293
|
return { ok: false, reason: `sha256_mismatch: ${art.path}` };
|
|
275
294
|
}
|
|
276
295
|
// Layer 2: Ed25519 signature — enforced ONLY when a key is configured (then fail-closed).
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
const signature = verifyConfiguredManifestSignature(m, publicKey);
|
|
297
|
+
if (!signature.ok)
|
|
298
|
+
return signature;
|
|
299
|
+
return { ok: true, reason: 'verified' };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Verify exactly one explicitly selected download against a complete release manifest.
|
|
303
|
+
*
|
|
304
|
+
* Unlike verifyManifest, this verifier does not require downloading every platform artifact. When a public key is
|
|
305
|
+
* configured it first verifies the Ed25519 signature over the complete canonical manifest, then requires exactly
|
|
306
|
+
* one downloaded artifact and exactly one manifest entry with the same path, and finally checks that artifact's
|
|
307
|
+
* sha256. The complete manifest is never filtered or reconstructed before signature verification.
|
|
308
|
+
*/
|
|
309
|
+
export function verifySelectedManifestArtifact(manifest, downloaded, publicKey) {
|
|
310
|
+
const structErr = manifestStructuralError(manifest);
|
|
311
|
+
if (structErr)
|
|
312
|
+
return { ok: false, reason: structErr };
|
|
313
|
+
const m = manifest;
|
|
314
|
+
const signature = verifyConfiguredManifestSignature(m, publicKey);
|
|
315
|
+
if (!signature.ok)
|
|
316
|
+
return signature;
|
|
317
|
+
if (downloaded.length !== 1) {
|
|
318
|
+
return { ok: false, reason: `downloaded_artifact_count_invalid: ${downloaded.length}` };
|
|
319
|
+
}
|
|
320
|
+
const selected = downloaded[0];
|
|
321
|
+
if (!selected || typeof selected.path !== 'string' || !selected.path) {
|
|
322
|
+
return { ok: false, reason: 'downloaded_artifact_path_invalid' };
|
|
323
|
+
}
|
|
324
|
+
const matches = m.artifacts.filter((artifact) => artifact.path === selected.path);
|
|
325
|
+
if (matches.length === 0)
|
|
326
|
+
return { ok: false, reason: `artifact_missing: ${selected.path}` };
|
|
327
|
+
if (matches.length !== 1)
|
|
328
|
+
return { ok: false, reason: `artifact_duplicate: ${selected.path}` };
|
|
329
|
+
const expected = matches[0];
|
|
330
|
+
if (!expected)
|
|
331
|
+
return { ok: false, reason: `artifact_missing: ${selected.path}` };
|
|
332
|
+
let digest;
|
|
333
|
+
if (selected.sha256)
|
|
334
|
+
digest = selected.sha256.toLowerCase();
|
|
335
|
+
else if (selected.bytes)
|
|
336
|
+
digest = sha256Hex(selected.bytes);
|
|
337
|
+
else
|
|
338
|
+
return { ok: false, reason: `artifact_no_content: ${selected.path}` };
|
|
339
|
+
if (digest !== expected.sha256.toLowerCase()) {
|
|
340
|
+
return { ok: false, reason: `sha256_mismatch: ${selected.path}` };
|
|
292
341
|
}
|
|
293
342
|
return { ok: true, reason: 'verified' };
|
|
294
343
|
}
|
package/dist/util/fileLock.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export declare function syncSleep(ms: number): void;
|
|
2
2
|
export declare class LockTimeoutError extends Error {
|
|
3
3
|
readonly code = "LOCK_TIMEOUT";
|
|
4
|
-
constructor(
|
|
4
|
+
constructor(_lockPath?: string);
|
|
5
5
|
}
|
|
6
|
+
export type UnsafeLockPathReason = 'symlink' | 'not_regular_file' | 'owner_too_large' | 'path_changed' | 'permission_denied' | 'invalid_owner';
|
|
7
|
+
export declare class UnsafeLockPathError extends Error {
|
|
8
|
+
readonly reason: UnsafeLockPathReason;
|
|
9
|
+
readonly code = "UNSAFE_LOCK_PATH";
|
|
10
|
+
constructor(reason: UnsafeLockPathReason);
|
|
11
|
+
}
|
|
12
|
+
export declare const MAX_LOCK_OWNER_BYTES = 4096;
|
|
6
13
|
export interface AcquireLockOptions {
|
|
7
14
|
maxTries?: number;
|
|
8
15
|
waitMs?: number;
|
|
@@ -21,4 +28,14 @@ export interface AcquireLockOptions {
|
|
|
21
28
|
* synchronous critical sections (append-only writes).
|
|
22
29
|
*/
|
|
23
30
|
export declare function acquireLock(lockPath: string, opts?: AcquireLockOptions): void;
|
|
24
|
-
export
|
|
31
|
+
export type ReleaseLockReason = 'released' | 'missing' | 'ownership_changed' | 'not_owned' | 'released_with_cleanup_error' | UnsafeLockPathReason | 'release_failed';
|
|
32
|
+
export interface ReleaseLockResult {
|
|
33
|
+
released: boolean;
|
|
34
|
+
reason: ReleaseLockReason;
|
|
35
|
+
}
|
|
36
|
+
export declare function releaseLock(lockPath: string): ReleaseLockResult;
|
|
37
|
+
export declare class LockReleaseError extends Error {
|
|
38
|
+
readonly reason: ReleaseLockReason;
|
|
39
|
+
readonly code = "LOCK_RELEASE_FAILED";
|
|
40
|
+
constructor(reason: ReleaseLockReason);
|
|
41
|
+
}
|