agentic-workflow-manager 9.14.0 → 9.14.1
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/src/core/admission/index.js +12 -6
- package/dist/src/core/admission/registry-contracts.js +14 -1
- package/dist/tests/commands/plan/index.test.js +26 -0
- package/dist/tests/commands/sensors/exec.test.js +18 -9
- package/dist/tests/core/admission/index.test.js +39 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -166,7 +166,7 @@ function sanitizeAdmissionReport(report) {
|
|
|
166
166
|
invalidAdmission();
|
|
167
167
|
if (value.cliCurrentness !== undefined && !CLI_CURRENTNESS.has(value.cliCurrentness))
|
|
168
168
|
invalidAdmission();
|
|
169
|
-
if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound')
|
|
169
|
+
if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound' && value.sensorEvidence !== 'r8-registry-opt-out')
|
|
170
170
|
invalidAdmission();
|
|
171
171
|
if (value.planDigest !== undefined && (typeof value.planDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.planDigest)))
|
|
172
172
|
invalidAdmission();
|
|
@@ -225,12 +225,18 @@ async function admitPlan(input) {
|
|
|
225
225
|
if (input.compatibilityDiagnostics?.length)
|
|
226
226
|
return blocked(input, input.compatibilityDiagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable', cliCurrentness });
|
|
227
227
|
}
|
|
228
|
+
let sensorsReported = 'not-required';
|
|
229
|
+
let sensorEvidence = {};
|
|
228
230
|
if (input.verifySensors) {
|
|
229
231
|
if (!input.sensors)
|
|
230
232
|
return blocked(input, [diagnostic('ADMISSION_SENSORS_REQUIRED', 'Sensor evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness });
|
|
231
233
|
const sensors = sensorVerdict(input.sensors);
|
|
232
|
-
|
|
234
|
+
const registryOptOut = sensors === 'not-certified' && input.sensorClosurePolicy === 'r8-registry-opt-out' && input.sensors.mode === 'opt-out';
|
|
235
|
+
if (sensors !== 'pass' && !registryOptOut)
|
|
233
236
|
return blocked(input, [diagnostic('ADMISSION_SENSORS_BLOCKED', `Sensor verdict is ${sensors}.`)], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors });
|
|
237
|
+
sensorsReported = sensors;
|
|
238
|
+
if (registryOptOut)
|
|
239
|
+
sensorEvidence = { sensorEvidence: 'r8-registry-opt-out' };
|
|
234
240
|
}
|
|
235
241
|
if (mode === 'desatendido') {
|
|
236
242
|
// The durable controller (journal + awm watch + jobs) is opt-in, as it
|
|
@@ -241,7 +247,7 @@ async function admitPlan(input) {
|
|
|
241
247
|
const observed = journalStatus(input, plan);
|
|
242
248
|
const journal = observed === 'missing' && plan.schema === 'compact-slices/v1' ? 'not-required' : observed;
|
|
243
249
|
if (journal !== 'current' && journal !== 'not-required')
|
|
244
|
-
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors:
|
|
250
|
+
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: sensorsReported, ...sensorEvidence });
|
|
245
251
|
const capabilities = unattendedCapabilities(provider, input.controllerAutonomy);
|
|
246
252
|
const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
247
253
|
if (resolution.outcome === 'blocked') {
|
|
@@ -252,17 +258,17 @@ async function admitPlan(input) {
|
|
|
252
258
|
const reason = exports.PROVIDER_EXECUTION_CAPABILITIES[provider].unattendedController !== 'supported'
|
|
253
259
|
? diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)
|
|
254
260
|
: diagnostic('ADMISSION_CONTROLLER_AUTONOMY_REQUIRED', `Unattended dispatch on ${provider} requires an explicit controller autonomy posture; without it the controller blocks on its first tool call.`);
|
|
255
|
-
return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors:
|
|
261
|
+
return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: sensorsReported, ...sensorEvidence, capabilityResolution: resolution });
|
|
256
262
|
}
|
|
257
263
|
const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
|
|
258
|
-
return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked',
|
|
264
|
+
return { ...completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', sensorsReported, resolution, cliCurrentness), ...sensorEvidence };
|
|
259
265
|
}
|
|
260
266
|
const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
|
|
261
267
|
const resolution = { outcome: capabilities.interactiveExecution === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
262
268
|
if (resolution.outcome === 'blocked')
|
|
263
269
|
return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified interactive execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, capabilityResolution: resolution });
|
|
264
270
|
const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
|
|
265
|
-
return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked',
|
|
271
|
+
return { ...completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', sensorsReported, resolution, cliCurrentness), ...sensorEvidence };
|
|
266
272
|
}
|
|
267
273
|
function completeAdmission(input, plan, provider, executionMode, journal, currentness, sensors, capabilityResolution, cliCurrentness = 'not-checked') {
|
|
268
274
|
if (plan.schema === 'compact-slices/v1')
|
|
@@ -271,6 +271,17 @@ function hasOwnedGenerationAdmission(input) {
|
|
|
271
271
|
|| (generation.wrapperRef !== undefined && generation.wrapperRef.spawnNonce === generation.spawnNonce
|
|
272
272
|
&& (0, process_1.refIsAlive)(generation.wrapperRef));
|
|
273
273
|
}
|
|
274
|
+
/** R8: only a content registry (its manifest at the sensor project root) may close on a deliberate opt-out. */
|
|
275
|
+
function registryOptOutPolicy(sensors) {
|
|
276
|
+
if (sensors?.mode !== 'opt-out' || !sensors.projectRoot)
|
|
277
|
+
return undefined;
|
|
278
|
+
try {
|
|
279
|
+
return fs_1.default.statSync(path_1.default.join(sensors.projectRoot, registries_1.REGISTRY_MANIFEST_NAME)).isFile() ? 'r8-registry-opt-out' : undefined;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
274
285
|
/** One admission authority for the public command AND actual watch dispatch.
|
|
275
286
|
* No empirical execution occurs before provenance/currentness/CLI-floor gates. */
|
|
276
287
|
async function admitRegistryPlan(input, dependencies = {}) {
|
|
@@ -308,8 +319,9 @@ async function admitRegistryPlan(input, dependencies = {}) {
|
|
|
308
319
|
return scope.provenance === 'proven';
|
|
309
320
|
};
|
|
310
321
|
let generationBound = false;
|
|
322
|
+
let sensorClosurePolicy;
|
|
311
323
|
const compose = async (fields) => {
|
|
312
|
-
const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...fields });
|
|
324
|
+
const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...(sensorClosurePolicy ? { sensorClosurePolicy } : {}), ...fields });
|
|
313
325
|
const withEvidence = generationBound && report.sensors === 'pass' ? { ...report, sensorEvidence: 'generation-bound' } : report;
|
|
314
326
|
if (scope.provenanceDiagnostic && report.diagnostics.some(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED')) {
|
|
315
327
|
return { ...withEvidence, diagnostics: report.diagnostics.map(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED' ? scope.provenanceDiagnostic : diagnostic) };
|
|
@@ -337,6 +349,7 @@ async function admitRegistryPlan(input, dependencies = {}) {
|
|
|
337
349
|
const sensors = input.verifySensors ? input.sensors ?? (generationBound
|
|
338
350
|
? { sensors: [], overall: 'pass', reason: 'generation-bound-admission' }
|
|
339
351
|
: await (dependencies.runSensors ?? run_1.runSensors)({ cwd: input.cwd, all: true, readOnly: true })) : undefined;
|
|
352
|
+
sensorClosurePolicy = registryOptOutPolicy(sensors);
|
|
340
353
|
if (input.requireCurrent && input.verifySensors) {
|
|
341
354
|
if (!refresh())
|
|
342
355
|
return compose({ currentness, sensors });
|
|
@@ -309,6 +309,32 @@ describe('plan admit Commander wiring', () => {
|
|
|
309
309
|
process.exitCode = undefined;
|
|
310
310
|
}
|
|
311
311
|
});
|
|
312
|
+
it.each([
|
|
313
|
+
['a content registry', true, 'admitted'],
|
|
314
|
+
['an ordinary project', false, 'blocked'],
|
|
315
|
+
])('applies the R8 sensor opt-out exception only to %s', async (_name, isRegistry, expected) => {
|
|
316
|
+
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-r8-')));
|
|
317
|
+
const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
318
|
+
if (isRegistry)
|
|
319
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
|
|
320
|
+
const sensors = jest.fn().mockResolvedValue({ overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: root, manifestPath: path_1.default.join(root, '.awm', 'sensors.json') });
|
|
321
|
+
const program = new commander_1.Command();
|
|
322
|
+
(0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, listRegistries: () => [], readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }), runSensors: sensors });
|
|
323
|
+
try {
|
|
324
|
+
await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--verify-sensors', '--json']);
|
|
325
|
+
const report = JSON.parse(String(output.mock.calls.at(-1)[0]));
|
|
326
|
+
expect(report).toMatchObject({ state: expected, sensors: 'not-certified' });
|
|
327
|
+
if (isRegistry)
|
|
328
|
+
expect(report.sensorEvidence).toBe('r8-registry-opt-out');
|
|
329
|
+
else
|
|
330
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_SENSORS_BLOCKED');
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
output.mockRestore();
|
|
334
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
335
|
+
process.exitCode = undefined;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
312
338
|
it('derives desatendido from the canonical plan header; a native v1 plan needs no journal (opt-in durable custody)', async () => {
|
|
313
339
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-header-'));
|
|
314
340
|
const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
@@ -161,21 +161,30 @@ describe('runCommand — exit codes and output', () => {
|
|
|
161
161
|
});
|
|
162
162
|
itLinux('never lets a structured capture file grow beyond maxBuffer', async () => {
|
|
163
163
|
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-cap-'));
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// The overflow watcher kills the child and deletes the capture as soon as
|
|
165
|
+
// the cap is reached, so polling for the file races that cleanup. Capture
|
|
166
|
+
// files only grow, so their size at deletion is the largest they ever were.
|
|
167
|
+
const captureSizes = [];
|
|
168
|
+
const rmSync = fs_1.default.rmSync.bind(fs_1.default);
|
|
169
|
+
const removal = jest.spyOn(fs_1.default, 'rmSync').mockImplementation((target, options) => {
|
|
170
|
+
if (typeof target === 'string' && path_1.default.basename(target).startsWith('awm-sensor-output-')) {
|
|
171
|
+
captureSizes.push(fs_1.default.statSync(path_1.default.join(target, 'stdout')).size);
|
|
172
|
+
}
|
|
173
|
+
return rmSync(target, options);
|
|
174
|
+
});
|
|
166
175
|
try {
|
|
167
|
-
const result = (0, exec_1.runStructuredCommand)({
|
|
176
|
+
const result = await (0, exec_1.runStructuredCommand)({
|
|
168
177
|
executable: 'node',
|
|
169
178
|
resolution: 'path',
|
|
170
|
-
args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {}
|
|
179
|
+
args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {} setTimeout(() => {}, 1000);`],
|
|
171
180
|
}, { timeout: 5_000, cwd: dir, maxBuffer: 1_024 });
|
|
172
|
-
expect(
|
|
173
|
-
|
|
174
|
-
expect(
|
|
175
|
-
expect(
|
|
176
|
-
await expect(result).resolves.toMatchObject({ overflowed: true });
|
|
181
|
+
expect(result).toMatchObject({ overflowed: true });
|
|
182
|
+
expect(captureSizes).toHaveLength(1);
|
|
183
|
+
expect(captureSizes[0]).toBeGreaterThan(0);
|
|
184
|
+
expect(captureSizes[0]).toBeLessThanOrEqual(1_024);
|
|
177
185
|
}
|
|
178
186
|
finally {
|
|
187
|
+
removal.mockRestore();
|
|
179
188
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
180
189
|
}
|
|
181
190
|
});
|
|
@@ -178,6 +178,45 @@ describe('admitPlan', () => {
|
|
|
178
178
|
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
// R8: a content registry whose sensors are ALL deliberately disabled may close
|
|
182
|
+
// work; its release proof lives in CI. The local verdict stays not-certified —
|
|
183
|
+
// never reported as pass — and the exception is named in the evidence.
|
|
184
|
+
describe('the R8 registry opt-out closure exception', () => {
|
|
185
|
+
const optOut = { overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json' };
|
|
186
|
+
const unattended = () => ({ ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } });
|
|
187
|
+
const base = () => ({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], verifySensors: true, sensors: optOut, sensorClosurePolicy: 'r8-registry-opt-out' });
|
|
188
|
+
it('admits interactive work without reporting the opt-out as a pass', async () => {
|
|
189
|
+
const report = await (0, admission_1.admitPlan)(base());
|
|
190
|
+
expect(report).toMatchObject({ state: 'admitted', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
|
|
191
|
+
});
|
|
192
|
+
it('admits journal-less unattended work under the same exception', async () => {
|
|
193
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
|
|
194
|
+
expect(report).toMatchObject({ state: 'admitted', executionMode: 'desatendido', journal: 'not-required', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
|
|
195
|
+
});
|
|
196
|
+
it('reports the preserved verdict when a later gate blocks', async () => {
|
|
197
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md' });
|
|
198
|
+
expect(report).toMatchObject({ state: 'blocked', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
|
|
199
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
200
|
+
});
|
|
201
|
+
it.each([
|
|
202
|
+
['no declared policy', { sensorClosurePolicy: undefined }],
|
|
203
|
+
['a failing sensor', { sensors: { ...optOut, overall: 'fail', mode: 'project-sensors' } }],
|
|
204
|
+
['an absent manifest', { sensors: { ...optOut, mode: 'missing', reason: 'manifest-absent' } }],
|
|
205
|
+
['a native gate', { sensors: { ...optOut, mode: 'native-gate', reason: 'native-gate-declared' } }],
|
|
206
|
+
['an unavailable sensor source', { sensors: { ...optOut, mode: 'source-unavailable' } }],
|
|
207
|
+
['all-disabled legacy sensors', { sensors: { ...optOut, overall: 'skipped', mode: 'project-sensors' } }],
|
|
208
|
+
])('keeps blocking with %s', async (_name, fields) => {
|
|
209
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), ...fields });
|
|
210
|
+
expect(report.state).toBe('blocked');
|
|
211
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_SENSORS_BLOCKED' });
|
|
212
|
+
expect(report.sensorEvidence).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
it('accepts only the known sensor evidence values at the public sanitizer', () => {
|
|
215
|
+
const report = { state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-certified', diagnostics: [] };
|
|
216
|
+
expect((0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'r8-registry-opt-out' }).sensorEvidence).toBe('r8-registry-opt-out');
|
|
217
|
+
expect(() => (0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'opt-out' })).toThrow('invalid report');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
181
220
|
it('admits unattended work only with an exact schema-2 plan binding', async () => {
|
|
182
221
|
const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
|
|
183
222
|
const journal = { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "9.14.
|
|
3
|
+
"version": "9.14.1",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"dependency-cruiser": "16.10.4",
|
|
66
66
|
"eslint": "10.8.1",
|
|
67
67
|
"jest": "^30.2.0",
|
|
68
|
-
"node-gyp": "^
|
|
68
|
+
"node-gyp": "^12.4.0",
|
|
69
69
|
"ts-jest": "^29.4.6",
|
|
70
70
|
"ts-node": "^10.9.2",
|
|
71
71
|
"typescript": "^5.9.3"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|