@aiwg/cli 2026.10.9 → 2026.10.11
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/agentic/code/providers/model-capabilities.v1.json +275 -17
- package/bin/aiwg.mjs +8 -0
- package/dist/src/cli/handlers/mc-admission.js +39 -0
- package/dist/src/cli/handlers/mc.js +2 -31
- package/dist/src/cli/handlers/models-help.js +52 -0
- package/dist/src/cli/handlers/models.js +11 -81
- package/dist/src/cli/handlers/sessions.js +198 -15
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +203 -234
- package/dist/src/cli/ui.js +1 -0
- package/dist/src/config/aiwg-config.js +38 -184
- package/dist/src/config/cli-help.js +54 -0
- package/dist/src/config/cli.js +5 -69
- package/dist/src/config/config-normalization.js +10 -0
- package/dist/src/config/worker-activation-gate.js +26 -0
- package/dist/src/config/worker-adoption-cli.js +102 -0
- package/dist/src/config/worker-adoption.js +368 -0
- package/dist/src/config/worker-config-digest.js +14 -0
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/config/worker-group-templates.js +150 -0
- package/dist/src/config/worker-guidance-warning.js +28 -0
- package/dist/src/config/worker-owner-lock.js +152 -0
- package/dist/src/config/worker-parallelism.js +154 -0
- package/dist/src/config/worker-write.js +75 -0
- package/dist/src/decision/telemetry/exporter.js +2 -1
- package/dist/src/decision/telemetry/index.js +1 -0
- package/dist/src/decision/telemetry/mapping.js +43 -0
- package/dist/src/decision/telemetry/otlp-http.js +5 -2
- package/dist/src/decision/telemetry/redaction.js +6 -1
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/telemetry/worker.js +69 -0
- package/dist/src/lint/rule-activation.js +15 -7
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/server.mjs +2 -11
- package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
- package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +114 -16
- package/dist/src/mission-protocol/codecs.js +8 -38
- package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
- package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
- package/dist/src/models/conductor-data-policy.d.mts +8 -0
- package/dist/src/models/conductor-data-policy.mjs +190 -0
- package/dist/src/models/config-loader.js +19 -3
- package/dist/src/models/config-writer.js +27 -0
- package/dist/src/models/model-capabilities.v1.json +275 -17
- package/dist/src/models/provider-policy.js +14 -155
- package/dist/src/models/resolver.js +2 -293
- package/dist/src/models/worker-artifact-binding.mjs +12 -0
- package/dist/src/models/worker-direct-readiness-policy.js +155 -0
- package/dist/src/models/worker-direct-readiness.js +9 -218
- package/dist/src/models/worker-health-readiness.js +105 -26
- package/dist/src/models/worker-installation.mjs +12 -14
- package/dist/src/models/worker-member-references.js +64 -0
- package/dist/src/models/worker-plan.js +317 -0
- package/dist/src/models/worker-selection-policy.js +217 -0
- package/dist/src/models/wrapper-deployment.js +1 -1
- package/dist/src/policy/authorization.js +29 -1
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/serve/shared-host-admission-checks.js +116 -0
- package/dist/src/serve/shared-host-scheduler.js +25 -107
- package/dist/src/serve/shared-host-starvation.js +73 -0
- package/dist/src/serve/worker-artifacts.d.mts +4 -0
- package/dist/src/serve/worker-artifacts.mjs +56 -0
- package/dist/src/serve/worker-command-request.js +68 -0
- package/dist/src/serve/worker-invocation-paths.js +144 -0
- package/dist/src/serve/worker-invocation-preparation.js +169 -0
- package/dist/src/serve/worker-invocation.js +24 -291
- package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
- package/dist/src/serve/worker-stdin-frame.mjs +6 -6
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/batch-import.js +34 -6
- package/dist/src/sessions/contracts.js +19 -0
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +23 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/workspace-discovery.js +5 -2
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/package.json +15 -1
- package/schemas/mission-v1.schema.json +20 -19
- package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
- package/tools/agents/providers/claude-post-deploy.mjs +35 -0
- package/tools/agents/providers/claude.mjs +3 -30
- package/tools/agents/providers/codex-post-deploy.mjs +13 -0
- package/tools/agents/providers/codex.mjs +2 -8
- package/tools/agents/providers/conductor-data-guard.mjs +61 -0
- package/tools/agents/providers/rule-delivery.mjs +3 -16
- package/tools/agents/providers/rule-presentation.mjs +38 -0
- package/tools/plugin/package-plugins.mjs +1 -1
- package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
- package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
- package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
- package/tools/ralph-external/lib/session-attempt.mjs +85 -0
- package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
- package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-final.mjs +143 -0
- package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
- package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +124 -313
- package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
|
@@ -46,7 +46,15 @@ import { sampleWorkerIdentity, sameWorkerIdentity, createWorkerIdentityCapture,
|
|
|
46
46
|
qualifyWorkerCpuTickRate, sampleWorkerCpuTime, isSafeWorkerGroupTarget } from './process-monitor.mjs';
|
|
47
47
|
import { createWorkerHealthObserver, sampleOwnedWorktreeContent, classifyWorkerHealthFailure } from './lib/worker-health.mjs';
|
|
48
48
|
import { ScopedFailureBreaker, attributeFailureScope } from './lib/consecutive-failure-circuit-breaker.mjs';
|
|
49
|
+
import { assertManagedWorkerLaunch, validateManagedAttempt } from './lib/managed-worker-attempt.mjs';
|
|
50
|
+
export { assertManagedWorkerLaunch } from './lib/managed-worker-attempt.mjs';
|
|
49
51
|
import { buildSessionEnvironment } from './lib/session-environment.mjs';
|
|
52
|
+
import { withWorkerPath, boundedPrivateRead } from './lib/worker-inputs.mjs';
|
|
53
|
+
import { collectWorkerFinal } from './lib/worker-final.mjs';
|
|
54
|
+
import { verifyRetryPredecessor } from './lib/worker-retry-evidence.mjs';
|
|
55
|
+
import { observeWorkerTermination, terminationRemedy } from './lib/worker-termination.mjs';
|
|
56
|
+
import { buildWorkerInvocationEnvironment, WORKER_INVOCATION_MARKER } from './lib/worker-invocation-processes.mjs';
|
|
57
|
+
import { resolveRetryReference } from './lib/worker-launch-cli.mjs';
|
|
50
58
|
import { launcherRecoveryEvidence, observeLauncherRecoveryOutcome, recordLauncherRecoveryHint } from './lib/launcher-recovery-evidence.mjs';
|
|
51
59
|
export { observeLauncherRecoveryOutcome } from './lib/launcher-recovery-evidence.mjs';
|
|
52
60
|
import { workerNeedsNativeNamespaces, probeWorkerNativeSandbox, nativeSandboxFailure, nativeSandboxProbeMessage, NATIVE_SANDBOX_UNAVAILABLE, checkWorkerDispatch, detectWorkerIsolation, launchIsolatedWorker, getWorkerIsolationStatus, stopWorkerUnit, readUnitControlGroup } from './lib/worker-isolation.mjs';
|
|
@@ -94,75 +102,9 @@ const bytesDigest = bytes => `sha256:${createHash('sha256').update(bytes).digest
|
|
|
94
102
|
export const beneath = (root, filename) => { const r = relative(root, filename); return r === '' || !r.startsWith('..') && !isAbsolute(r); };
|
|
95
103
|
const signalNumberToName = Object.fromEntries(Object.entries(osConstants.signals ?? {}).map(([name, number]) => [number, name]));
|
|
96
104
|
|
|
97
|
-
/** Resolve the deepest existing ancestor's realpath, then reattach any
|
|
98
|
-
* not-yet-created remainder. A lexical prefix match is not sufficient: a
|
|
99
|
-
* symlinked ancestor of either path can make `beneath()` wrong in either
|
|
100
|
-
* direction, so both sides are resolved before the lexical comparison. */
|
|
101
|
-
async function resolveRealish(target) {
|
|
102
|
-
const parts = target.split('/').filter(Boolean);
|
|
103
|
-
for (let i = parts.length; i >= 0; i--) {
|
|
104
|
-
const candidate = i === 0 ? '/' : `/${parts.slice(0, i).join('/')}`;
|
|
105
|
-
try {
|
|
106
|
-
const real = await realpath(candidate);
|
|
107
|
-
const remainder = parts.slice(i).join('/');
|
|
108
|
-
return remainder ? join(real, remainder) : real;
|
|
109
|
-
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
110
|
-
}
|
|
111
|
-
return target;
|
|
112
|
-
}
|
|
113
105
|
export async function realBeneath(root, filename) {
|
|
114
|
-
const
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Linux directory handles pin each ancestor. O_NOFOLLOW on a pathname alone
|
|
119
|
-
// cannot protect an ancestor that the worker replaces after inspection.
|
|
120
|
-
async function withWorkerPath(filename, operation) {
|
|
121
|
-
if (!isAbsolute(filename) || filename.includes('\0') || filename.split('/').includes('..')) throw new Error('Invalid worker path');
|
|
122
|
-
const components = filename.split('/').filter(Boolean);
|
|
123
|
-
const leaf = components.pop();
|
|
124
|
-
let directory = await open('/', constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
125
|
-
try {
|
|
126
|
-
for (const component of components) {
|
|
127
|
-
const next = await open(`/proc/self/fd/${directory.fd}/${component}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
128
|
-
await directory.close(); directory = next;
|
|
129
|
-
}
|
|
130
|
-
return await operation(`/proc/self/fd/${directory.fd}/${leaf ?? '.'}`);
|
|
131
|
-
} finally { await directory.close(); }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// #2915 review 4 (finding 4): `privateOutput: true` REPAIRS a loose mode
|
|
135
|
-
// (chmod 0600) rather than rejecting it — correct for the worker's own
|
|
136
|
-
// final-output file, which the launcher itself created and is free to
|
|
137
|
-
// re-tighten. `rejectInsecureMode: true` additionally REFUSES a mode other
|
|
138
|
-
// than exactly 0600 instead of repairing it, for evidence this launcher
|
|
139
|
-
// did NOT create moments ago (retry predecessor metadata) — a 0666 file
|
|
140
|
-
// there is itself a signal worth refusing on, not silently normalizing
|
|
141
|
-
// away.
|
|
142
|
-
async function boundedPrivateRead(filename, limit, inspect, privateOutput = false, rejectInsecureMode = false) {
|
|
143
|
-
if (!await inspect(filename, 'file')) throw new Error('Required worker input is missing');
|
|
144
|
-
const handle = await withWorkerPath(filename, target => open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK));
|
|
145
|
-
try {
|
|
146
|
-
const stat = await handle.stat();
|
|
147
|
-
if (!stat.isFile() || stat.nlink !== 1 || stat.size > limit) throw new Error('Worker input must be a bounded unaliased regular file');
|
|
148
|
-
if (privateOutput) {
|
|
149
|
-
if (stat.uid !== process.getuid()) throw new Error('Worker final must be conductor-owned');
|
|
150
|
-
if (rejectInsecureMode) {
|
|
151
|
-
if ((stat.mode & 0o777) !== 0o600) throw new Error('Worker input mode must already be exactly 0600; insecure modes are refused, not repaired');
|
|
152
|
-
} else {
|
|
153
|
-
await handle.chmod(0o600);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const buffer = Buffer.alloc(limit + 1);
|
|
157
|
-
let length = 0;
|
|
158
|
-
while (length < buffer.length) {
|
|
159
|
-
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
160
|
-
if (!bytesRead) break;
|
|
161
|
-
length += bytesRead;
|
|
162
|
-
}
|
|
163
|
-
if (length > limit) throw new Error('Worker input exceeds bound');
|
|
164
|
-
return buffer.subarray(0, length);
|
|
165
|
-
} finally { await handle.close(); }
|
|
106
|
+
const paths = await importImpl(import.meta.url, 'serve/worker-invocation-paths.js');
|
|
107
|
+
return paths.realBeneath(root, filename);
|
|
166
108
|
}
|
|
167
109
|
|
|
168
110
|
export async function publishWorkerRecord(runDir, name, value, inspect) {
|
|
@@ -330,55 +272,8 @@ function checkDispatchGuard(dispatchGuard) {
|
|
|
330
272
|
}
|
|
331
273
|
}
|
|
332
274
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
* future declaration kind and are refused here. */
|
|
336
|
-
export async function materializeWorkerArtifacts(invocation, runDir) {
|
|
337
|
-
const artifacts = invocation.artifacts ?? [];
|
|
338
|
-
if (!Array.isArray(artifacts) || artifacts.length > 16) throw new Error('Worker artifact count exceeds bound');
|
|
339
|
-
const names = new Set();
|
|
340
|
-
let total = 0;
|
|
341
|
-
for (const artifact of artifacts) {
|
|
342
|
-
if (!artifact || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(artifact.name)
|
|
343
|
-
|| /[^A-Za-z0-9_-]/.test(artifact.name) || names.has(artifact.name) || typeof artifact.content !== 'string'
|
|
344
|
-
|| artifact.location !== undefined && artifact.location !== 'run-directory') throw new Error('Invalid worker artifact declaration');
|
|
345
|
-
names.add(artifact.name);
|
|
346
|
-
const size = Buffer.byteLength(artifact.content);
|
|
347
|
-
total += size;
|
|
348
|
-
if (size > 1048576 || total > 4194304) throw new Error('Worker artifact bytes exceed bound');
|
|
349
|
-
}
|
|
350
|
-
const paths = new Map(artifacts.map(artifact => [artifact.name, join(runDir, `artifact-${artifact.name}`)]));
|
|
351
|
-
const argv = invocation.argv.map(token => {
|
|
352
|
-
if (!token.includes('${artifact:')) return token;
|
|
353
|
-
const match = /^\$\{artifact:([A-Za-z0-9][A-Za-z0-9_-]{0,63})\}$/.exec(token);
|
|
354
|
-
if (!match || !paths.has(match[1])) throw new Error('Undeclared worker artifact placeholder');
|
|
355
|
-
return paths.get(match[1]);
|
|
356
|
-
});
|
|
357
|
-
const { inspectWorkerPath } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
|
|
358
|
-
await inspectWorkerPath(runDir, 'directory');
|
|
359
|
-
const stat = await lstat(runDir);
|
|
360
|
-
if (stat.uid !== process.getuid() || (stat.mode & 0o077)) throw new Error('Worker artifact directory must be private and owned');
|
|
361
|
-
const created = [];
|
|
362
|
-
const cleanup = async () => {
|
|
363
|
-
const results = await Promise.allSettled(created.map(filename =>
|
|
364
|
-
withWorkerPath(filename, target => unlink(target)).catch(error => {
|
|
365
|
-
if (error.code !== 'ENOENT') throw error;
|
|
366
|
-
})));
|
|
367
|
-
const failed = results.find(result => result.status === 'rejected');
|
|
368
|
-
if (failed) throw failed.reason;
|
|
369
|
-
};
|
|
370
|
-
try {
|
|
371
|
-
for (const artifact of artifacts) {
|
|
372
|
-
const filename = paths.get(artifact.name);
|
|
373
|
-
const handle = await withWorkerPath(filename, target => open(target,
|
|
374
|
-
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600));
|
|
375
|
-
created.push(filename);
|
|
376
|
-
try { await handle.writeFile(artifact.content); await handle.sync(); }
|
|
377
|
-
finally { await handle.close(); }
|
|
378
|
-
}
|
|
379
|
-
return { argv, cleanup };
|
|
380
|
-
} catch (error) { await cleanup(); throw error; }
|
|
381
|
-
}
|
|
275
|
+
import { materializeWorkerArtifacts, WorkerArtifactLifecycle } from './lib/worker-artifacts.mjs';
|
|
276
|
+
export { materializeWorkerArtifacts } from './lib/worker-artifacts.mjs';
|
|
382
277
|
|
|
383
278
|
const _workerRecoverySubscriptions = new WeakMap();
|
|
384
279
|
const _workerRecoveryReporters = new WeakMap();
|
|
@@ -413,45 +308,6 @@ function subscribeWorkerRecovery(owner, breaker) {
|
|
|
413
308
|
_workerRecoverySubscriptions.set(owner, () => reporter.owners.delete(owner));
|
|
414
309
|
}
|
|
415
310
|
|
|
416
|
-
/** Reuse the MC authority and its registered one-shot identity at every spawn boundary. */
|
|
417
|
-
export async function assertManagedWorkerLaunch(options, invocation, inputs, projectRoot = process.cwd()) {
|
|
418
|
-
if (!options.managedAdmission || invocation.mode !== 'managed'
|
|
419
|
-
|| inputs.snapshot.config.parallelism?.worker_mode !== 'managed') throw new Error('Managed worker admission is required');
|
|
420
|
-
const { createMcAdmissionScheduler } = await importImpl(import.meta.url, 'cli/handlers/mc.js');
|
|
421
|
-
const { loadLauncherRegistry } = await importImpl(import.meta.url, 'cli/handlers/ralph-launcher.js');
|
|
422
|
-
const { workerModelKey } = await importImpl(import.meta.url, 'models/provider-policy.js');
|
|
423
|
-
const scheduler = await createMcAdmissionScheduler(projectRoot, inputs.snapshot.config);
|
|
424
|
-
const { requestId, generation } = options.managedAdmission;
|
|
425
|
-
const snapshot = scheduler.snapshot();
|
|
426
|
-
const record = snapshot.records[requestId];
|
|
427
|
-
const deadlineAtMs = Math.min(...[record?.accounting?.runKey, record?.accounting?.taskKey]
|
|
428
|
-
.map(key => snapshot.allowances?.[key]?.deadlineAt).filter(Boolean).map(Date.parse));
|
|
429
|
-
const registered = loadLauncherRegistry(projectRoot).loops[process.env.RALPH_LOOP_ID];
|
|
430
|
-
const worker = record?.managed;
|
|
431
|
-
const reservation = record?.accounting?.reservation;
|
|
432
|
-
if (!record || record.state !== 'admitted' || record.leaseGeneration !== generation
|
|
433
|
-
|| !record.launchClaimedAt || !record.metadata?.invocationId || Date.parse(record.leaseExpiresAt ?? '') <= Date.now()
|
|
434
|
-
|| deadlineAtMs <= Date.now()
|
|
435
|
-
|| worker?.kind !== 'external' || worker.group !== invocation.selected.group
|
|
436
|
-
|| worker.member !== invocation.selected.member || worker.cli !== invocation.selected.cli
|
|
437
|
-
|| worker.model !== workerModelKey(invocation.selected.inferenceProvider, invocation.selected.model)
|
|
438
|
-
|| reservation?.tokens != null && !(invocation.limits.tokens <= reservation.tokens)
|
|
439
|
-
|| reservation?.costUsd != null && !(invocation.limits.costUsd !== null && invocation.limits.costUsd <= reservation.costUsd)
|
|
440
|
-
|| registered?.managedAdmission?.requestId !== requestId || registered.managedAdmission.generation !== generation
|
|
441
|
-
|| registered.managedWorker?.invocationPath !== options.invocationPath
|
|
442
|
-
|| registered.managedWorker?.runDir !== options.runDir) {
|
|
443
|
-
throw new Error('Managed worker launch differs from the current claimed admission');
|
|
444
|
-
}
|
|
445
|
-
if (record.execution) {
|
|
446
|
-
const identity = sampleWorkerIdentity(process.pid);
|
|
447
|
-
if (record.execution.pid !== process.pid || !identity?.bootId || !identity.startTicks
|
|
448
|
-
|| record.execution.startedAt !== `${identity.bootId}:${identity.startTicks}:${identity.uid}`) {
|
|
449
|
-
throw new Error('Managed worker supervisor identity differs');
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return { ...record, deadlineAtMs };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
311
|
async function workerDispatchPath(filename) {
|
|
456
312
|
let candidate = filename;
|
|
457
313
|
while (dirname(candidate) !== candidate) {
|
|
@@ -491,8 +347,25 @@ function workerLaunchRefusal(outcome) {
|
|
|
491
347
|
// Neither worker output nor a caller-supplied result can populate this context.
|
|
492
348
|
const directStartupCorrectionContext = new WeakMap();
|
|
493
349
|
|
|
350
|
+
// Each attempt directory is created exclusively; its parent is the conductor's job.
|
|
351
|
+
async function createAttemptRunDirectory(runDir) {
|
|
352
|
+
try { await mkdir(runDir, { mode: 0o700 }); } catch (error) {
|
|
353
|
+
if (error?.code === 'ENOENT') throw new Error(`Worker run directory parent does not exist: ${dirname(runDir)}; create it (mode 0700) before launch`);
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
494
358
|
async function launchOneWorker(owner, options) {
|
|
495
|
-
const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan:
|
|
359
|
+
const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validatePlan } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
|
|
360
|
+
// Forward the saved-plan path: invocation identity is verified against it (#2905).
|
|
361
|
+
const validate = async (plan, snapshot, drivers, now, planPath = options.invocationPath) => {
|
|
362
|
+
const validateAt = (p, s, d, n) => validatePlan(p, s, d, n, planPath);
|
|
363
|
+
if (plan.mode !== 'managed' || !options.managedAdmission) return validateAt(plan, snapshot, drivers, now);
|
|
364
|
+
const checked = await validateManagedAttempt(options, plan, { snapshot, drivers }, now, validateAt,
|
|
365
|
+
attempt => publishWorkerRecord(dirname(options.invocationPath), `${basename(options.invocationPath)}.attempt.json`, attempt, inspect));
|
|
366
|
+
options.attempt = checked.attempt;
|
|
367
|
+
return checked.invocation;
|
|
368
|
+
};
|
|
496
369
|
const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
|
|
497
370
|
const planBytes = await boundedPrivateRead(options.invocationPath, 1048576, inspect);
|
|
498
371
|
const saved = JSON.parse(planBytes.toString('utf8'));
|
|
@@ -502,9 +375,28 @@ async function launchOneWorker(owner, options) {
|
|
|
502
375
|
let inputs = await loadInputs();
|
|
503
376
|
const clock = () => typeof options.now === 'function' ? options.now() : options.now ?? new Date().toISOString();
|
|
504
377
|
const now = clock();
|
|
505
|
-
const
|
|
378
|
+
const { assertConductorWorkerPlan } = await importImpl(import.meta.url, 'serve/worker-invocation-paths.js');
|
|
379
|
+
await assertConductorWorkerPlan(saved, options.invocationPath, inputs.drivers);
|
|
380
|
+
const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now, options.invocationPath);
|
|
381
|
+
// #3685 round 2: the worker CLI's AIWG bootstrap must be present in the
|
|
382
|
+
// worker workdir before any spawn — a context-free worker would run with
|
|
383
|
+
// no project rules/skills/hooks. Opt-in per entry point (the
|
|
384
|
+
// `--launch-worker` CLI enables it via `options.ensureWorkerContext`);
|
|
385
|
+
// absent means "no ensure", never a default deploy, so pinned baselines
|
|
386
|
+
// and synthetic launches are unaffected. A failed ensure throws the
|
|
387
|
+
// typed `OG_WORKER_CONTEXT_UNAVAILABLE` diagnostic (with remedy) and the
|
|
388
|
+
// worker never spawns.
|
|
389
|
+
if (options.ensureWorkerContext) {
|
|
390
|
+
const { ensureWorkerContextForLaunch } = await import('./lib/worker-context.mjs');
|
|
391
|
+
await ensureWorkerContextForLaunch({
|
|
392
|
+
workdir: invocation.cwd,
|
|
393
|
+
cli: invocation.selected?.cli,
|
|
394
|
+
config: inputs.snapshot?.config ?? null,
|
|
395
|
+
...(typeof options.ensureWorkerContext === 'object' ? options.ensureWorkerContext : {}),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
506
398
|
if (invocation.mode === 'managed' || options.managedAdmission) {
|
|
507
|
-
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
|
|
399
|
+
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs, options.projectRoot ?? process.cwd());
|
|
508
400
|
options = { ...options, scopedBreaker: null, managedInvocationId: admitted.metadata.invocationId,
|
|
509
401
|
managedDeadlineAtMs: admitted.deadlineAtMs };
|
|
510
402
|
}
|
|
@@ -536,12 +428,12 @@ async function launchOneWorker(owner, options) {
|
|
|
536
428
|
// check too.
|
|
537
429
|
let verifiedPreviousAttemptId = null;
|
|
538
430
|
if (options?.retryOf !== undefined) {
|
|
539
|
-
const exitRecord = await verifyRetryPredecessor(options.retryOf,
|
|
540
|
-
{ newInvocationPath: options?.invocationPath, inspect, workerWritableRoots: writableRoots });
|
|
431
|
+
const exitRecord = await verifyRetryPredecessor(await resolveRetryReference(options.retryOf, { inspect }),
|
|
432
|
+
{ newInvocationPath: options?.invocationPath, inspect, workerWritableRoots: writableRoots, checkDescendants: options.checkDescendants });
|
|
541
433
|
verifiedPreviousAttemptId = exitRecord.invocationId;
|
|
542
434
|
}
|
|
543
|
-
const paths =
|
|
544
|
-
if (new Set(Object.values(paths)).size !==
|
|
435
|
+
const paths = (await importImpl(import.meta.url, 'serve/worker-invocation-paths.js')).workerOutputPaths(invocation);
|
|
436
|
+
if (new Set(Object.values(paths)).size !== Object.keys(paths).length || Object.values(paths).some(filename =>
|
|
545
437
|
filename === options.invocationPath || filename === invocation.briefPath
|
|
546
438
|
|| dirname(filename) === runDir && basename(filename).startsWith('artifact-')
|
|
547
439
|
|| ['invocation.json', 'process.json', 'exit.json', 'health.json', 'final-evidence.json', 'worker-start.json', 'worker-identity.json', 'worker-exit.json',
|
|
@@ -571,7 +463,7 @@ async function launchOneWorker(owner, options) {
|
|
|
571
463
|
const isolation = invocation.isolation === 'native-sandbox' ? { mode: 'native-sandbox' }
|
|
572
464
|
: options.isolation ?? detectWorkerIsolation();
|
|
573
465
|
if (isolation.mode === 'systemd-unit' && workerNeedsNativeNamespaces(adapter, permissions)) {
|
|
574
|
-
const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ cwd: invocation.cwd,
|
|
466
|
+
const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ adapter, cwd: invocation.cwd,
|
|
575
467
|
envNames: invocation.envNames, env: Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
|
|
576
468
|
.map(name => [name, process.env[name]])) });
|
|
577
469
|
if (!probe.ok) throw new WorkerPolicyError(NATIVE_SANDBOX_UNAVAILABLE, nativeSandboxProbeMessage(probe));
|
|
@@ -608,7 +500,7 @@ async function launchOneWorker(owner, options) {
|
|
|
608
500
|
// themselves; pre-spawn refusals above never reach this point. Global
|
|
609
501
|
// dispatch AND per-CLI enablement are checked inside `admitDirectWorker`
|
|
610
502
|
// before any breaker construction (#2915 final review, finding 1).
|
|
611
|
-
const invocationId = randomUUID();
|
|
503
|
+
const invocationId = invocation.invocationId ?? randomUUID();
|
|
612
504
|
// #2915 review 4 (finding 8): computed ONCE, before admission, so the
|
|
613
505
|
// SAME digest both gates the allowance reservation here and is published
|
|
614
506
|
// into invocation.json below — never recomputed from a possibly-stale
|
|
@@ -718,13 +610,15 @@ async function launchOneWorker(owner, options) {
|
|
|
718
610
|
throw error;
|
|
719
611
|
}
|
|
720
612
|
}
|
|
613
|
+
// Created before run-directory preparation so its failure path can report it.
|
|
614
|
+
const artifactLifecycle = new WorkerArtifactLifecycle(invocation, invocationId);
|
|
721
615
|
// #2915 review 4 (finding 2): this run-directory preparation is
|
|
722
616
|
// pre-spawn work happening AFTER a successful admission above — any
|
|
723
617
|
// failure here (mkdir, ownership, the `.launch-owner` write) must settle
|
|
724
618
|
// the reservation exactly like every other pre-spawn exit, never leak it
|
|
725
619
|
// by throwing before the big try/catch below is reached.
|
|
726
620
|
try {
|
|
727
|
-
if (!runExists) await
|
|
621
|
+
if (!runExists) await createAttemptRunDirectory(runDir); // exclusive attempt: no recursive creation or reuse
|
|
728
622
|
const runStat = await lstat(runDir);
|
|
729
623
|
if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
|
|
730
624
|
await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
@@ -736,6 +630,7 @@ async function launchOneWorker(owner, options) {
|
|
|
736
630
|
// release without leaving a marker even past the canary deadline.
|
|
737
631
|
await settleLedger({ kind: 'uncertain', confirmedStop: true });
|
|
738
632
|
if (ledgerDiagnostics.length) error.ledgerDiagnostics = ledgerDiagnostics;
|
|
633
|
+
artifactLifecycle.failure(error);
|
|
739
634
|
throw error;
|
|
740
635
|
}
|
|
741
636
|
const supervisor = sampleWorkerIdentity(process.pid);
|
|
@@ -754,6 +649,7 @@ async function launchOneWorker(owner, options) {
|
|
|
754
649
|
// own outcome.
|
|
755
650
|
let latestProcessRecord = null;
|
|
756
651
|
const publish = (name, value) => {
|
|
652
|
+
artifactLifecycle.observe(name, value);
|
|
757
653
|
if (name === 'process.json') {
|
|
758
654
|
value = { ...value, recovery: { ...admission.breaker.recoveryStatus(), observedAt: new Date().toISOString() } };
|
|
759
655
|
if (options.startupCorrectionRecord) value.startupCorrection = options.startupCorrectionRecord;
|
|
@@ -790,6 +686,7 @@ async function launchOneWorker(owner, options) {
|
|
|
790
686
|
// task — never from an unverified caller claim.
|
|
791
687
|
previousAttemptId: verifiedPreviousAttemptId,
|
|
792
688
|
requestId: invocationId, taskDigest, role: saved.request.role,
|
|
689
|
+
...(options.managedAdmission ? { attempt: options.attempt } : {}),
|
|
793
690
|
selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
|
|
794
691
|
policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
|
|
795
692
|
expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
|
|
@@ -798,13 +695,13 @@ async function launchOneWorker(owner, options) {
|
|
|
798
695
|
executedArgvDigest: qualificationDigest(artifacts.argv),
|
|
799
696
|
artifacts: (invocation.artifacts ?? []).map(artifact => ({ name: artifact.name,
|
|
800
697
|
byteLength: Buffer.byteLength(artifact.content), sha256: bytesDigest(artifact.content) })),
|
|
801
|
-
envNames: invocation.envNames, stdin: invocation.stdin,
|
|
698
|
+
envNames: [...new Set([...invocation.envNames, WORKER_INVOCATION_MARKER])], stdin: invocation.stdin,
|
|
802
699
|
prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
|
|
803
700
|
worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
|
|
804
701
|
health: invocation.health ?? null, transport: invocation.transport,
|
|
805
702
|
isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
|
|
806
703
|
limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
|
|
807
|
-
await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
|
|
704
|
+
await publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor, worker: null,
|
|
808
705
|
launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
|
|
809
706
|
// All captures (including the reserved final) open successfully BEFORE spawn.
|
|
810
707
|
for (const filename of [...Object.values(paths), ...Object.values(captures)]) {
|
|
@@ -813,13 +710,12 @@ async function launchOneWorker(owner, options) {
|
|
|
813
710
|
}
|
|
814
711
|
// Recheck policy, prompt, installation and exact bindings after file preparation.
|
|
815
712
|
inputs = await loadInputs();
|
|
816
|
-
await validate(saved, inputs.snapshot, inputs.drivers, clock());
|
|
713
|
+
await validate(saved, inputs.snapshot, inputs.drivers, clock(), options.invocationPath);
|
|
817
714
|
gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
818
715
|
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
819
716
|
if (!gate.ok) throw workerDispatchRefusal(gate);
|
|
820
717
|
if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
|
|
821
|
-
const environment =
|
|
822
|
-
.map(name => [name, process.env[name]]));
|
|
718
|
+
const environment = buildWorkerInvocationEnvironment(invocation.envNames, invocationId);
|
|
823
719
|
const baselineContent = await sampleOwnedWorktreeContent(invocation.cwd, { files: options.healthArtifacts });
|
|
824
720
|
// #2915 round 8 (D2, replaces the former "debit immediately before this
|
|
825
721
|
// call" comment): the ledger debit now happens INSIDE the dispatch-
|
|
@@ -875,11 +771,12 @@ async function launchOneWorker(owner, options) {
|
|
|
875
771
|
// real child's own queued events (exit/close) race ahead of
|
|
876
772
|
// `captureOneWorker`'s own listener attachment in several tests (and
|
|
877
773
|
// would in production).
|
|
774
|
+
artifactLifecycle.spawning();
|
|
878
775
|
const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
|
|
879
776
|
command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
|
|
880
777
|
memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation, workerGroup: invocation.selected.group,
|
|
881
778
|
allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
|
|
882
|
-
|
|
779
|
+
...environment, stdin: invocation.stdin, runDir,
|
|
883
780
|
...(onDispatchCommit ? { onDispatchCommit } : {}) });
|
|
884
781
|
outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
|
|
885
782
|
if (!outcome?.child) throw workerLaunchRefusal(outcome);
|
|
@@ -988,6 +885,7 @@ async function launchOneWorker(owner, options) {
|
|
|
988
885
|
const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
|
|
989
886
|
const { confirmedStop } = await resolveStopEvidence({ isolationMode: outcome.mode,
|
|
990
887
|
unitName: outcome.unit ?? null, child: outcome.child, graceMs });
|
|
888
|
+
artifactLifecycle.confirmedStop = confirmedStop;
|
|
991
889
|
await settleLedger({ kind: 'uncertain', confirmedStop });
|
|
992
890
|
// #2915 review 6 (finding 3d): when this bounded stop escalation DOES
|
|
993
891
|
// confirm termination, also call the explicit reconciliation -- a
|
|
@@ -1005,7 +903,7 @@ async function launchOneWorker(owner, options) {
|
|
|
1005
903
|
} finally {
|
|
1006
904
|
admission.breaker.removeListener('transition', onScopeTransition);
|
|
1007
905
|
await publishing;
|
|
1008
|
-
try { await artifacts
|
|
906
|
+
try { await artifactLifecycle.finish(artifacts); } finally {
|
|
1009
907
|
// Never chmod a worker-replaced pathname; only the inodes we opened.
|
|
1010
908
|
for (const handle of handles) { await handle.chmod(0o600).catch(() => {}); await handle.close().catch(() => {}); }
|
|
1011
909
|
await inspect(runDir, 'directory');
|
|
@@ -1075,7 +973,7 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1075
973
|
// For systemd-unit mode the unit's MainPID is not known synchronously; it
|
|
1076
974
|
// is still resolved once inside observeIdentity.
|
|
1077
975
|
let nativeSupervisor = hasTransport && outcome.mode !== 'systemd-unit' ? sampleNativeSupervisor(child.pid) : null;
|
|
1078
|
-
const streams = [handles[0], handles[1], handles[
|
|
976
|
+
const streams = [handles[0], handles[1], handles[Object.keys(paths).length], handles[Object.keys(paths).length + 1]].map(handle => createWriteStream('', { fd: handle.fd, autoClose: false }));
|
|
1079
977
|
let identityObservation = 'pending';
|
|
1080
978
|
let reportedWorker = null;
|
|
1081
979
|
let output = Buffer.alloc(0);
|
|
@@ -1209,7 +1107,7 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1209
1107
|
for (const stream of streams) stream.on('error', () => { errorCode = 'CAPTURE_WRITE'; stop(); });
|
|
1210
1108
|
child.stdin?.on('error', error => { if (error.code !== 'EPIPE') { errorCode = 'STDIN_WRITE'; stop(); } });
|
|
1211
1109
|
child.stdin?.end(ctx.stdinBytes ?? undefined);
|
|
1212
|
-
const recordProcess = () => publish('process.json', { invocationId, origin: 'launcher', supervisor,
|
|
1110
|
+
const recordProcess = () => publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor,
|
|
1213
1111
|
transportProcess: hasTransport ? sampleWorkerIdentity(child.pid) : null,
|
|
1214
1112
|
nativeSupervisor,
|
|
1215
1113
|
worker: identity ?? reportedWorker, identityObservation, launchTime, monotonicStart, ownership: identity ? 'owned-group' : 'unknown',
|
|
@@ -1234,12 +1132,14 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1234
1132
|
await drainChildStdio(child, graceMs);
|
|
1235
1133
|
child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
|
|
1236
1134
|
for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
|
|
1237
|
-
|
|
1135
|
+
// Prefer the unit's retained state whenever a unit name is recorded
|
|
1136
|
+
// (#3674 round 4): launch-mode selection is unchanged, but unit (cgroup)
|
|
1137
|
+
// evidence is used wherever it is available.
|
|
1138
|
+
const native = outcome.unit ? readNative() : null;
|
|
1238
1139
|
const members = identity ? sampleWorkerGroup(identity.pgid) : null;
|
|
1239
|
-
//
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
: child.pid === undefined;
|
|
1140
|
+
// Require positive kernel evidence of owned containment being empty.
|
|
1141
|
+
const confirmed = (outcome.mode !== 'systemd-unit' && child.pid === undefined) || observeWorkerTermination({ mode: outcome.mode, native,
|
|
1142
|
+
workerExited: sawExit, identity, supervisor, invocationId }).confirmed;
|
|
1243
1143
|
// `worker-exit.json`/`worker-identity.json` are written into a directory
|
|
1244
1144
|
// the worker itself can reach (same UID, no filesystem confinement), so a
|
|
1245
1145
|
// worker can plant either file before the trusted transport ever writes
|
|
@@ -1275,7 +1175,7 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1275
1175
|
: outcome.mode === 'systemd-unit' && native?.execMainCode === 1 ? native.execMainStatus : waitCode;
|
|
1276
1176
|
const result = { invocationId, exitCode, signal: resultSignal,
|
|
1277
1177
|
endedAt: new Date().toISOString(), termination: confirmed ? 'confirmed' : 'uncertain',
|
|
1278
|
-
descendants: confirmed ? '
|
|
1178
|
+
descendants: confirmed ? 'none' : 'unknown', timedOut: resultTimedOut,
|
|
1279
1179
|
cancelled: stopReason === 'CANCELLED', stopReason, errorCode: errorCode ?? null, capturedBytes: Math.min(total, output.length),
|
|
1280
1180
|
// Unauthenticated: reported by the worker-writable transport record, not
|
|
1281
1181
|
// kernel-observed. Never used above for exitCode/signal/termination.
|
|
@@ -1294,9 +1194,8 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1294
1194
|
// ExecMainCode/Status when applicable, launcher-detected errors,
|
|
1295
1195
|
// timeout and explicit cancellation) — never worker stderr/stdout text
|
|
1296
1196
|
// or worker-writable records. A degraded-isolation attempt can reach
|
|
1297
|
-
// 'succeeded' here even
|
|
1298
|
-
//
|
|
1299
|
-
// systemd unit) — this is exactly the #2915 final-review fix: "give
|
|
1197
|
+
// 'succeeded' here even when descendant-stop evidence is unavailable.
|
|
1198
|
+
// This preserves the #2915 final-review fix: "give
|
|
1300
1199
|
// degraded successful execution a valid recovery-success path without
|
|
1301
1200
|
// falsely declaring descendant termination confirmed."
|
|
1302
1201
|
const directChildCleanExit = sawExit && waitCode === 0 && waitSignal == null;
|
|
@@ -1309,26 +1208,10 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1309
1208
|
let healthFinalValid = false;
|
|
1310
1209
|
let healthAttemptFinalValid = false;
|
|
1311
1210
|
try {
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (finalStat.size === 0 && driver.extractFinal) {
|
|
1317
|
-
const final = driver.extractFinal(output.toString('utf8'));
|
|
1318
|
-
if (final === null || final === undefined || final.success === false) throw new Error('Missing or unsuccessful final extraction');
|
|
1319
|
-
const bytes = JSON.stringify({ invocationId, source: 'adapter-extraction', candidate: true, verified: false, result: final });
|
|
1320
|
-
if (Buffer.byteLength(bytes) > 262144) throw new Error('Final exceeds bound');
|
|
1321
|
-
await handles[2].writeFile(bytes); extracted = true;
|
|
1322
|
-
}
|
|
1323
|
-
const finalBytes = await boundedPrivateRead(paths.final, 262144, ctx.inspect, true);
|
|
1324
|
-
if (!finalBytes.length) throw new Error('Missing final output');
|
|
1325
|
-
if (!extracted && driver.extractFinal) {
|
|
1326
|
-
const parsed = driver.extractFinal(finalBytes.toString('utf8'));
|
|
1327
|
-
if (parsed === null || parsed === undefined || parsed.success === false) throw new Error('Invalid final output');
|
|
1328
|
-
}
|
|
1329
|
-
await publish('final-evidence.json', { invocationId, path: paths.final, digest: bytesDigest(finalBytes),
|
|
1330
|
-
source: extracted ? 'adapter-extraction' : 'worker-output', candidate: true, verified: false,
|
|
1331
|
-
terminalSuccess: result.exitCode === 0 && !result.timedOut && !result.errorCode });
|
|
1211
|
+
const { finalBytes, extracted, parseFinal } = await collectWorkerFinal({
|
|
1212
|
+
path: paths.final, workdir: invocation.cwd, driver, output, invocationId, result,
|
|
1213
|
+
inspect: ctx.inspect, handle: handles[2], publish, outputFraming: ctx.profile.health?.outputFraming,
|
|
1214
|
+
});
|
|
1332
1215
|
// Consume #2890's strict final contract when supplied by the installed
|
|
1333
1216
|
// driver. Legacy extraction remains a candidate, never health authority.
|
|
1334
1217
|
if (driver.parseWorkerFinal) {
|
|
@@ -1336,7 +1219,7 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1336
1219
|
// Strict validation consumes original bytes. A legacy extracted
|
|
1337
1220
|
// object may already have lost duplicate keys and cannot authorize
|
|
1338
1221
|
// completion through reserialization.
|
|
1339
|
-
const claims = await
|
|
1222
|
+
const claims = await parseFinal(extracted ? output : finalBytes, {
|
|
1340
1223
|
format: extracted && ctx.profile.health?.outputFraming === 'jsonl' ? 'jsonl' : 'json', observation: { transport: 'shell',
|
|
1341
1224
|
status: result.exitCode === 0 && !result.errorCode && !result.timedOut ? 'completed' : 'failed',
|
|
1342
1225
|
termination: result.termination, exitCode: result.exitCode } });
|
|
@@ -1601,7 +1484,8 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1601
1484
|
//
|
|
1602
1485
|
// `launchOneWorkerBaseline` and `captureOneWorkerBaseline` reproduce
|
|
1603
1486
|
// `launchOneWorker`/`captureOneWorker`, apart from the lifecycle fixes above,
|
|
1604
|
-
//
|
|
1487
|
+
// with shared workerClaims/digest validation (#3675) applied to both paths,
|
|
1488
|
+
// otherwise as they existed at base commit `a4085b86a` (`git show
|
|
1605
1489
|
// a4085b86a:tools/ralph-external/session-launcher.mjs`) — before any
|
|
1606
1490
|
// #2915 direct-recovery code existed in this file. `launchOneWorker`
|
|
1607
1491
|
// above calls `launchOneWorkerBaseline` for every attempt
|
|
@@ -1633,6 +1517,7 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1633
1517
|
// `a4085b86a` called them — with NO `onDispatchCommit` — which that
|
|
1634
1518
|
// module's own dual-mode contract (`_runDispatchCommit`) defines as
|
|
1635
1519
|
// "behaves exactly as before the hook existed";
|
|
1520
|
+
// - shared final validation (`lib/worker-final.mjs`, #3675);
|
|
1636
1521
|
// - unchanged imports (`process-monitor.mjs`, `lib/worker-health.mjs`,
|
|
1637
1522
|
// `serve/worker-invocation.js`, `models/qualification-digest.mjs`).
|
|
1638
1523
|
// Neither function imports, reads, or references the breaker, the
|
|
@@ -1647,12 +1532,13 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1647
1532
|
loadInputs, validate, inspect, clock, qualificationDigest } = ctx;
|
|
1648
1533
|
let inputs = ctx.inputs;
|
|
1649
1534
|
let localGate = gate;
|
|
1650
|
-
if (!runExists) await
|
|
1535
|
+
if (!runExists) await createAttemptRunDirectory(runDir); // exclusive attempt: no recursive creation or reuse
|
|
1651
1536
|
const runStat = await lstat(runDir);
|
|
1652
1537
|
if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
|
|
1653
1538
|
await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
1654
1539
|
try { await handle.chmod(0o700); } finally { await handle.close(); } });
|
|
1655
|
-
const invocationId = options.managedAdmission ? options.managedInvocationId : randomUUID();
|
|
1540
|
+
const invocationId = invocation.invocationId ?? (options.managedAdmission ? options.managedInvocationId : randomUUID());
|
|
1541
|
+
if (options.managedAdmission && invocationId !== options.managedInvocationId) throw new Error('Worker artifact invocation id differs from managed admission');
|
|
1656
1542
|
const reservation = await withWorkerPath(join(runDir, '.launch-owner'), target => open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600));
|
|
1657
1543
|
try { await reservation.writeFile(invocationId); await reservation.sync(); } finally { await reservation.close(); }
|
|
1658
1544
|
const supervisor = sampleWorkerIdentity(process.pid);
|
|
@@ -1664,7 +1550,9 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1664
1550
|
const writeRecord = options.publishRecord ?? publishWorkerRecord;
|
|
1665
1551
|
const publishFailures = [];
|
|
1666
1552
|
let publishing = Promise.resolve();
|
|
1553
|
+
const artifactLifecycle = new WorkerArtifactLifecycle(invocation, invocationId);
|
|
1667
1554
|
const publish = (name, value) => {
|
|
1555
|
+
artifactLifecycle.observe(name, value);
|
|
1668
1556
|
const write = publishing.catch(() => {}).then(() => writeRecord(runDir, name, value, inspect));
|
|
1669
1557
|
publishing = write.catch(error => { publishFailures.push({ name, error: error?.message ?? String(error) }); });
|
|
1670
1558
|
return write;
|
|
@@ -1678,6 +1566,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1678
1566
|
const displayArgv = artifacts.argv.map((token, i) => i === 0 ? token : `<argument:${bytesDigest(token)}>`);
|
|
1679
1567
|
await publish('invocation.json', { version: 1, origin: 'launcher', invocationId, attemptId: invocationId,
|
|
1680
1568
|
previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: options.managedAdmission?.requestId ?? invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
|
|
1569
|
+
...(options.managedAdmission ? { attempt: options.attempt } : {}),
|
|
1681
1570
|
selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
|
|
1682
1571
|
policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
|
|
1683
1572
|
expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
|
|
@@ -1686,13 +1575,13 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1686
1575
|
executedArgvDigest: qualificationDigest(artifacts.argv),
|
|
1687
1576
|
artifacts: (invocation.artifacts ?? []).map(artifact => ({ name: artifact.name,
|
|
1688
1577
|
byteLength: Buffer.byteLength(artifact.content), sha256: bytesDigest(artifact.content) })),
|
|
1689
|
-
envNames: invocation.envNames, stdin: invocation.stdin,
|
|
1578
|
+
envNames: [...new Set([...invocation.envNames, WORKER_INVOCATION_MARKER])], stdin: invocation.stdin,
|
|
1690
1579
|
prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
|
|
1691
1580
|
worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
|
|
1692
1581
|
health: invocation.health ?? null, transport: invocation.transport,
|
|
1693
1582
|
isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
|
|
1694
1583
|
limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
|
|
1695
|
-
await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
|
|
1584
|
+
await publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor, worker: null,
|
|
1696
1585
|
launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
|
|
1697
1586
|
// All captures (including the reserved final) open successfully BEFORE spawn.
|
|
1698
1587
|
for (const filename of [...Object.values(paths), ...Object.values(captures)]) {
|
|
@@ -1701,13 +1590,12 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1701
1590
|
}
|
|
1702
1591
|
// Recheck policy, prompt, installation and exact bindings after file preparation.
|
|
1703
1592
|
inputs = await loadInputs();
|
|
1704
|
-
await validate(saved, inputs.snapshot, inputs.drivers, clock());
|
|
1593
|
+
await validate(saved, inputs.snapshot, inputs.drivers, clock(), options.invocationPath);
|
|
1705
1594
|
localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
1706
1595
|
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
1707
1596
|
if (!localGate.ok) throw workerDispatchRefusal(localGate);
|
|
1708
1597
|
if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
|
|
1709
|
-
const environment =
|
|
1710
|
-
.map(name => [name, process.env[name]]));
|
|
1598
|
+
const environment = buildWorkerInvocationEnvironment(invocation.envNames, invocationId);
|
|
1711
1599
|
const baselineContent = await sampleOwnedWorktreeContent(invocation.cwd, { files: options.healthArtifacts });
|
|
1712
1600
|
// The real `launchIsolatedWorker`, called with no `onDispatchCommit`
|
|
1713
1601
|
// (never passed anywhere in this frozen function), is synchronous —
|
|
@@ -1718,17 +1606,18 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1718
1606
|
// gated path's own test doubles, not a behavior this branch adds):
|
|
1719
1607
|
// `await`ing a value that was never a Promise resolves to that exact
|
|
1720
1608
|
// value with no observable difference.
|
|
1609
|
+
artifactLifecycle.spawning();
|
|
1721
1610
|
const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
|
|
1722
1611
|
command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
|
|
1723
1612
|
memoryMaxGb: options.memoryMaxGb ?? 4, gate: localGate, config: inputs.snapshot.config, isolation,
|
|
1724
1613
|
workerGroup: invocation.selected.group,
|
|
1725
1614
|
allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
|
|
1726
1615
|
...(options.managedAdmission ? { onDispatchCommit: async () => {
|
|
1727
|
-
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
|
|
1616
|
+
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs, options.projectRoot ?? process.cwd());
|
|
1728
1617
|
options.managedDeadlineAtMs = Math.min(options.managedDeadlineAtMs, admitted.deadlineAtMs);
|
|
1729
1618
|
} } : {}),
|
|
1730
1619
|
...(options.managedAdmission ? { onSpawnAttempt: () => { managedSpawnAttempted = true; } } : {}),
|
|
1731
|
-
|
|
1620
|
+
...environment, stdin: invocation.stdin, runDir });
|
|
1732
1621
|
outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
|
|
1733
1622
|
if (!outcome?.child) throw workerLaunchRefusal(outcome);
|
|
1734
1623
|
return await captureOneWorkerBaseline(owner, { child: outcome.child, outcome, invocation, invocationId, supervisor,
|
|
@@ -1739,9 +1628,10 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1739
1628
|
...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
|
|
1740
1629
|
endedAt: new Date().toISOString(), isolation: isolation.mode, termination: managedSpawnAttempted ? 'uncertain' : 'confirmed',
|
|
1741
1630
|
descendants: managedSpawnAttempted ? 'unknown' : 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
|
|
1631
|
+
artifactLifecycle.failure(error);
|
|
1742
1632
|
throw error;
|
|
1743
1633
|
} finally {
|
|
1744
|
-
try { await artifacts
|
|
1634
|
+
try { await artifactLifecycle.finish(artifacts); } finally {
|
|
1745
1635
|
// Never chmod a worker-replaced pathname; only the inodes we opened.
|
|
1746
1636
|
for (const handle of handles) { await handle.chmod(0o600).catch(() => {}); await handle.close().catch(() => {}); }
|
|
1747
1637
|
await inspect(runDir, 'directory');
|
|
@@ -1761,7 +1651,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1761
1651
|
let identity = immediate?.pid === child.pid && immediate?.pid === immediate?.pgid
|
|
1762
1652
|
&& immediate?.uid === process.getuid?.() && immediate?.parentPid === process.pid ? immediate : null;
|
|
1763
1653
|
let nativeSupervisor = hasTransport && outcome.mode !== 'systemd-unit' ? sampleNativeSupervisor(child.pid) : null;
|
|
1764
|
-
const streams = [handles[0], handles[1], handles[
|
|
1654
|
+
const streams = [handles[0], handles[1], handles[Object.keys(paths).length], handles[Object.keys(paths).length + 1]].map(handle => createWriteStream('', { fd: handle.fd, autoClose: false }));
|
|
1765
1655
|
let identityObservation = 'pending';
|
|
1766
1656
|
let reportedWorker = null;
|
|
1767
1657
|
let output = Buffer.alloc(0);
|
|
@@ -1886,7 +1776,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1886
1776
|
for (const stream of streams) stream.on('error', () => { errorCode = 'CAPTURE_WRITE'; stop(); });
|
|
1887
1777
|
child.stdin?.on('error', error => { if (error.code !== 'EPIPE') { errorCode = 'STDIN_WRITE'; stop(); } });
|
|
1888
1778
|
child.stdin?.end(ctx.stdinBytes ?? undefined);
|
|
1889
|
-
const recordProcess = () => publish('process.json', { invocationId, origin: 'launcher', supervisor,
|
|
1779
|
+
const recordProcess = () => publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor,
|
|
1890
1780
|
transportProcess: hasTransport ? sampleWorkerIdentity(child.pid) : null,
|
|
1891
1781
|
nativeSupervisor,
|
|
1892
1782
|
worker: identity ?? reportedWorker, identityObservation, launchTime, monotonicStart, ownership: identity ? 'owned-group' : 'unknown',
|
|
@@ -1912,10 +1802,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1912
1802
|
await drainChildStdio(child, graceMs);
|
|
1913
1803
|
child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
|
|
1914
1804
|
for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
|
|
1915
|
-
|
|
1805
|
+
// Prefer the unit's retained state whenever a unit name is recorded
|
|
1806
|
+
// (#3674 round 4): launch-mode selection is unchanged, but unit (cgroup)
|
|
1807
|
+
// evidence is used wherever it is available.
|
|
1808
|
+
const native = outcome.unit ? readNative() : null;
|
|
1916
1809
|
const members = identity ? sampleWorkerGroup(identity.pgid) : null;
|
|
1917
|
-
const confirmed = outcome.mode
|
|
1918
|
-
:
|
|
1810
|
+
const confirmed = (outcome.mode !== 'systemd-unit' && child.pid === undefined) || observeWorkerTermination({ mode: outcome.mode, native,
|
|
1811
|
+
workerExited: sawExit, identity, supervisor, invocationId }).confirmed;
|
|
1919
1812
|
const workerExit = hasTransport ? await transportRecord('worker-exit.json') : null;
|
|
1920
1813
|
if (!identity && hasTransport && nativeSupervisor) {
|
|
1921
1814
|
const workerStart = await transportRecord('worker-identity.json');
|
|
@@ -1934,7 +1827,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1934
1827
|
const result = { invocationId, exitCode, signal: resultSignal,
|
|
1935
1828
|
...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
|
|
1936
1829
|
endedAt: new Date().toISOString(), termination: confirmed ? 'confirmed' : 'uncertain',
|
|
1937
|
-
descendants: confirmed ? '
|
|
1830
|
+
descendants: confirmed ? 'none' : 'unknown', timedOut: resultTimedOut,
|
|
1938
1831
|
cancelled: stopReason === 'CANCELLED', stopReason, errorCode: errorCode ?? null, capturedBytes: Math.min(total, output.length),
|
|
1939
1832
|
workerTermination: hasTransport ? workerExit && !workerExit.errorCode ? 'confirmed' : 'uncertain' : sawExit ? 'confirmed' : 'uncertain',
|
|
1940
1833
|
workerReportedExit: workerExit ? { exitCode: workerExit.exitCode, signal: workerExit.signal, errorCode: workerExit.errorCode ?? null } : null,
|
|
@@ -1944,29 +1837,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1944
1837
|
publishFailures: ctx.publishFailures?.length ? ctx.publishFailures.slice() : null };
|
|
1945
1838
|
let healthFinalValid = false;
|
|
1946
1839
|
try {
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (finalStat.size === 0 && driver.extractFinal) {
|
|
1952
|
-
const final = driver.extractFinal(output.toString('utf8'));
|
|
1953
|
-
if (final === null || final === undefined || final.success === false) throw new Error('Missing or unsuccessful final extraction');
|
|
1954
|
-
const bytes = JSON.stringify({ invocationId, source: 'adapter-extraction', candidate: true, verified: false, result: final });
|
|
1955
|
-
if (Buffer.byteLength(bytes) > 262144) throw new Error('Final exceeds bound');
|
|
1956
|
-
await handles[2].writeFile(bytes); extracted = true;
|
|
1957
|
-
}
|
|
1958
|
-
const finalBytes = await boundedPrivateRead(paths.final, 262144, ctx.inspect, true);
|
|
1959
|
-
if (!finalBytes.length) throw new Error('Missing final output');
|
|
1960
|
-
if (!extracted && driver.extractFinal) {
|
|
1961
|
-
const parsed = driver.extractFinal(finalBytes.toString('utf8'));
|
|
1962
|
-
if (parsed === null || parsed === undefined || parsed.success === false) throw new Error('Invalid final output');
|
|
1963
|
-
}
|
|
1964
|
-
await publish('final-evidence.json', { invocationId, path: paths.final, digest: bytesDigest(finalBytes),
|
|
1965
|
-
source: extracted ? 'adapter-extraction' : 'worker-output', candidate: true, verified: false,
|
|
1966
|
-
terminalSuccess: result.exitCode === 0 && !result.timedOut && !result.errorCode });
|
|
1840
|
+
const { finalBytes, extracted, parseFinal } = await collectWorkerFinal({
|
|
1841
|
+
path: paths.final, workdir: invocation.cwd, driver, output, invocationId, result,
|
|
1842
|
+
inspect: ctx.inspect, handle: handles[2], publish, outputFraming: ctx.profile.health?.outputFraming,
|
|
1843
|
+
});
|
|
1967
1844
|
if (driver.parseWorkerFinal) {
|
|
1968
1845
|
try {
|
|
1969
|
-
await
|
|
1846
|
+
await parseFinal(extracted ? output : finalBytes, {
|
|
1970
1847
|
format: extracted && ctx.profile.health?.outputFraming === 'jsonl' ? 'jsonl' : 'json', observation: { transport: 'shell',
|
|
1971
1848
|
status: result.exitCode === 0 && !result.errorCode && !result.timedOut ? 'completed' : 'failed',
|
|
1972
1849
|
termination: result.termination, exitCode: result.exitCode } });
|
|
@@ -2160,13 +2037,13 @@ export const UNBOUND_RETRY_EVIDENCE_CODE = 'UNBOUND_RETRY_EVIDENCE';
|
|
|
2160
2037
|
* retry and is surfaced with a code the conductor can act on — it must never
|
|
2161
2038
|
* silently spawn a duplicate writer.
|
|
2162
2039
|
*/
|
|
2163
|
-
export function stopBeforeRetryGate(previousResult, { expectedInvocationId = null } = {}) {
|
|
2040
|
+
export function stopBeforeRetryGate(previousResult, { expectedInvocationId = null, runDir, missing } = {}) {
|
|
2164
2041
|
if (previousResult?.termination !== 'confirmed') {
|
|
2165
2042
|
return {
|
|
2166
2043
|
ok: false,
|
|
2167
2044
|
code: STOP_BEFORE_RETRY_CODE,
|
|
2168
2045
|
reason: `Previous attempt termination is '${previousResult?.termination ?? 'unknown'}', not confirmed: `
|
|
2169
|
-
+ 'retry is blocked until owned stop is proven (no duplicate writer while the old tree may continue).',
|
|
2046
|
+
+ 'retry is blocked until owned stop is proven (no duplicate writer while the old tree may continue). ' + (missing ? `Missing: ${missing}. ` : '') + terminationRemedy(previousResult, runDir),
|
|
2170
2047
|
previousResult: previousResult === undefined ? undefined : (previousResult ?? null),
|
|
2171
2048
|
};
|
|
2172
2049
|
}
|
|
@@ -3283,74 +3160,7 @@ export async function releaseLedgerPolicy({ captureRoot, taskChainDigest } = {})
|
|
|
3283
3160
|
* Returns the predecessor's verified `exit.json` record on success; throws
|
|
3284
3161
|
* on any refusal.
|
|
3285
3162
|
*/
|
|
3286
|
-
export
|
|
3287
|
-
const fail = (reason) => {
|
|
3288
|
-
const error = new Error(`Retry predecessor evidence refused: ${reason}`);
|
|
3289
|
-
error.code = UNBOUND_RETRY_EVIDENCE_CODE;
|
|
3290
|
-
throw error;
|
|
3291
|
-
};
|
|
3292
|
-
if (!retryOf || typeof retryOf.invocationId !== 'string' || !retryOf.invocationId
|
|
3293
|
-
|| typeof retryOf.runDir !== 'string' || !retryOf.runDir) {
|
|
3294
|
-
fail('retryOf requires both a non-empty invocationId and runDir');
|
|
3295
|
-
}
|
|
3296
|
-
const { invocationId: prevId, runDir: prevRunDir } = retryOf;
|
|
3297
|
-
// #2915 review 4 (finding 4): the minimal directory-authority repair —
|
|
3298
|
-
// reject a predecessor location inside any worker-writable root, exactly
|
|
3299
|
-
// like `launchOneWorker` already refuses for a NEW run directory. This
|
|
3300
|
-
// closes the easiest forgery (planting a complete, well-formed fake
|
|
3301
|
-
// predecessor inside the worktree the worker itself can write to) but is
|
|
3302
|
-
// explicitly NOT a defense against an unconfined malicious same-UID
|
|
3303
|
-
// worker in general: such a worker can still write anywhere this
|
|
3304
|
-
// conductor process can (e.g. directly under a configured capture root,
|
|
3305
|
-
// if that root is not itself outside every worker-writable path, or by
|
|
3306
|
-
// racing a legitimate predecessor's own directory before this check
|
|
3307
|
-
// runs). A real guarantee needs an enforced filesystem/process boundary
|
|
3308
|
-
// protecting conductor-owned metadata, or a different execution
|
|
3309
|
-
// identity — same-UID alone never provides it.
|
|
3310
|
-
if (Array.isArray(workerWritableRoots)) {
|
|
3311
|
-
for (const root of workerWritableRoots) {
|
|
3312
|
-
if (await realBeneath(root, prevRunDir)) fail('predecessor location is inside a worker-writable root');
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
if (!await inspect(prevRunDir, 'directory')) fail('predecessor run directory is missing');
|
|
3316
|
-
const dirStat = await lstat(prevRunDir);
|
|
3317
|
-
if (dirStat.uid !== process.getuid() || (dirStat.mode & 0o077)) fail('predecessor run directory is not privately owned');
|
|
3318
|
-
let ownerBytes, invocationBytes, exitBytes;
|
|
3319
|
-
try {
|
|
3320
|
-
// #2915 review 4 (finding 4): `rejectInsecureMode: true` — an insecure
|
|
3321
|
-
// mode on predecessor metadata REFUSES the retry instead of silently
|
|
3322
|
-
// chmod-repairing it and proceeding (the former behaviour the review
|
|
3323
|
-
// found: "the probe's 0666 files were accepted and changed to 0600").
|
|
3324
|
-
ownerBytes = await boundedPrivateRead(join(prevRunDir, '.launch-owner'), 256, inspect, true, true);
|
|
3325
|
-
invocationBytes = await boundedPrivateRead(join(prevRunDir, 'invocation.json'), 1048576, inspect, true, true);
|
|
3326
|
-
exitBytes = await boundedPrivateRead(join(prevRunDir, 'exit.json'), 1048576, inspect, true, true);
|
|
3327
|
-
} catch (error) { fail(`predecessor metadata is missing, insecurely-moded, or not a bounded, privately-owned, unaliased regular file (${error.message})`); }
|
|
3328
|
-
if (ownerBytes.toString('utf8') !== prevId) fail('.launch-owner does not match retryOf.invocationId');
|
|
3329
|
-
let invocationRecord, exitRecord;
|
|
3330
|
-
try {
|
|
3331
|
-
invocationRecord = JSON.parse(invocationBytes.toString('utf8'));
|
|
3332
|
-
exitRecord = JSON.parse(exitBytes.toString('utf8'));
|
|
3333
|
-
} catch { fail('predecessor metadata is not valid JSON'); }
|
|
3334
|
-
if (invocationRecord?.invocationId !== prevId || invocationRecord?.origin !== 'launcher') {
|
|
3335
|
-
fail('invocation.json identity does not match retryOf (or is not launcher-written)');
|
|
3336
|
-
}
|
|
3337
|
-
if (exitRecord?.invocationId !== prevId) fail('exit.json identity does not match retryOf');
|
|
3338
|
-
if (newInvocationPath !== undefined) {
|
|
3339
|
-
// #2915 review 4 (finding 4): the SAME stable task-chain identity
|
|
3340
|
-
// `launchOneWorker` publishes — role/task/group plus the brief CONTENT
|
|
3341
|
-
// digest, never `final_output`/`brief`/`workdir` PATHS, which legitimately
|
|
3342
|
-
// differ between a confirmed predecessor and this fresh attempt.
|
|
3343
|
-
let newTaskDigest;
|
|
3344
|
-
try {
|
|
3345
|
-
const newPlanBytes = await boundedPrivateRead(newInvocationPath, 1048576, inspect);
|
|
3346
|
-
const newPlan = JSON.parse(newPlanBytes.toString('utf8'));
|
|
3347
|
-
newTaskDigest = await stableTaskChainDigest(newPlan.request, newPlan.invocation?.briefDigest ?? null);
|
|
3348
|
-
} catch { fail('new attempt plan is missing or invalid; cannot bind the retry to a task'); }
|
|
3349
|
-
if (invocationRecord.taskDigest !== newTaskDigest) fail('predecessor task does not match the new attempt (task-chain binding)');
|
|
3350
|
-
}
|
|
3351
|
-
assertStopBeforeRetry(exitRecord, { expectedInvocationId: prevId });
|
|
3352
|
-
return exitRecord;
|
|
3353
|
-
}
|
|
3163
|
+
export { verifyRetryPredecessor } from './lib/worker-retry-evidence.mjs';
|
|
3354
3164
|
|
|
3355
3165
|
/**
|
|
3356
3166
|
* One corrected startup retry (#2915, 03 §11.5 step 1). A failed start may
|
|
@@ -4272,6 +4082,7 @@ export class SessionLauncher extends EventEmitter {
|
|
|
4272
4082
|
prompt: options.prompt,
|
|
4273
4083
|
// AIWG's tracking UUID is not an OMP native session to resume.
|
|
4274
4084
|
sessionId: this.providerAdapter.getName() === 'omp' ? options.resumeSession : options.sessionId,
|
|
4085
|
+
resumeSession: options.resumeSession,
|
|
4275
4086
|
model: options.model,
|
|
4276
4087
|
budget: options.budget,
|
|
4277
4088
|
maxTurns: options.maxTurns,
|