agentic-workflow-manager 9.9.0 → 9.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ const providers_1 = require("../../providers");
6
6
  const store_1 = require("../../core/model-policy/store");
7
7
  const validate_1 = require("../../core/model-policy/validate");
8
8
  const capabilities_1 = require("../../core/model-policy/capabilities");
9
+ const digest_1 = require("../../core/model-policy/digest");
9
10
  const roles = ['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'];
10
11
  const implementerProfiles = ['mechanical', 'integration', 'judgment'];
11
12
  const contract = { schema: 'routing-protocol/v1', supportedPlanSchemas: ['compact-slices/v1', 'compact-slices/v2'], roles, implementerProfiles, limits: { maxFileBytes: validate_1.MAX_POLICY_BYTES, maxMappings: validate_1.MAX_MAPPINGS, maxIdentifierChars: 128, maxDiagnostics: 20, maxDiagnosticChars: 4096 } };
@@ -20,9 +21,21 @@ function registerModelPolicyCommand(program, deps = {}) {
20
21
  const read = deps.readEffectivePolicy ?? store_1.readEffectivePolicy;
21
22
  const approveReceipt = deps.approveCapabilities ?? capabilities_1.approveCapabilities;
22
23
  const readReceipt = deps.readCapabilities ?? capabilities_1.readCapabilities;
24
+ const digest = deps.candidateDigest ?? digest_1.candidateDigest;
23
25
  const command = program.command('model-policy').description('inspect and explicitly approve portable routing policy');
24
26
  command.command('contract').option('--cwd <path>').option('--json').action((options) => { if (options.cwd !== undefined)
25
27
  text(options.cwd, '--cwd'); output({ ...contract, protocolDigest }, options.json === true); });
28
+ // Approval demands --expected-digest so that it stays an explicit operator
29
+ // act. Without a way to observe the digest first, that safeguard blocked the
30
+ // legitimate path instead of gating it, and the only way through was to
31
+ // reimplement canonicalisation outside the CLI — the second parser the
32
+ // admission contract forbids. This discloses the digest and approves nothing.
33
+ command.command('digest').description('compute the canonical digest of a candidate policy or capability receipt without approving it').requiredOption('--file <path>').option('--cwd <path>').option('--json').action((options) => {
34
+ text(options.file, '--file');
35
+ if (options.cwd !== undefined)
36
+ text(options.cwd, '--cwd');
37
+ output(digest({ file: options.file, cwd: options.cwd ?? process.cwd() }), options.json === true);
38
+ });
26
39
  command.command('approve').requiredOption('--file <path>').requiredOption('--scope <scope>').requiredOption('--expected-digest <sha>').option('--replace-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
27
40
  text(options.file, '--file');
28
41
  text(options.scope, '--scope');
@@ -37,7 +50,15 @@ function registerModelPolicyCommand(program, deps = {}) {
37
50
  const policy = approve({ file: options.file, scope: options.scope, cwd: options.cwd ?? process.cwd(), expectedDigest: options.expectedDigest, replaceDigest: options.replaceDigest });
38
51
  output({ state: 'approved', policy }, options.json === true);
39
52
  });
40
- command.command('capabilities approve').requiredOption('--file <path>').requiredOption('--expected-digest <sha>').option('--replace-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
53
+ // `command('capabilities approve')` registered ONE command named
54
+ // `capabilities` with a required positional `<approve>`, so Commander called
55
+ // the action as (positional, options, command) and the handler's first
56
+ // parameter — read as `options` — was the string 'approve'. Every invocation
57
+ // therefore died on `--file requires a non-empty value`, and the path could
58
+ // never approve anything. A real subcommand group keeps the invocation
59
+ // spelling identical and gives the action the options object it expects.
60
+ const capabilities = command.command('capabilities').description('inspect and explicitly approve runtime capability attestations');
61
+ capabilities.command('approve').requiredOption('--file <path>').requiredOption('--expected-digest <sha>').option('--replace-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
41
62
  text(options.file, '--file');
42
63
  text(options.expectedDigest, '--expected-digest');
43
64
  if (options.replaceDigest !== undefined)
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.candidateDigest = candidateDigest;
7
+ const path_1 = __importDefault(require("path"));
8
+ const json_1 = require("../plan/json");
9
+ const capabilities_1 = require("./capabilities");
10
+ const canonical_1 = require("./canonical");
11
+ const store_1 = require("./store");
12
+ const validate_1 = require("./validate");
13
+ const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
14
+ function assertPath(value, label) {
15
+ if (typeof value !== 'string' || value.length === 0 || value.length > 4096 || CONTROL.test(value))
16
+ throw new Error(`${label} must be a bounded path without control characters`);
17
+ }
18
+ function candidateDigest(input) {
19
+ if (!input || typeof input !== 'object')
20
+ throw new Error('digest input is required');
21
+ assertPath(input.file, 'file');
22
+ assertPath(input.cwd, 'cwd');
23
+ const raw = (0, store_1.readBoundedCandidate)(path_1.default.resolve(path_1.default.resolve(input.cwd), input.file));
24
+ if (raw === null)
25
+ throw new Error('digest candidate is absent');
26
+ const parsed = (0, json_1.parseJsonNoDuplicate)(raw);
27
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
28
+ throw new Error('digest candidate must be a JSON object');
29
+ const schema = parsed.schema;
30
+ if (schema === 'model-policy/v1')
31
+ return { schema, digest: (0, canonical_1.canonicalPolicyDigest)((0, validate_1.validatePolicyContent)(parsed)) };
32
+ if (schema === 'routing-capabilities/v1')
33
+ return { schema, digest: (0, capabilities_1.capabilityReceiptDigest)((0, capabilities_1.validateCapabilityReceipt)(parsed)) };
34
+ throw new Error('digest candidate must declare schema model-policy/v1 or routing-capabilities/v1');
35
+ }
@@ -79,33 +79,72 @@ function resolveSelection(input) {
79
79
  let selection = wanted;
80
80
  const unavailableEvidence = [];
81
81
  let outcome = 'native';
82
- const canDegrade = (capability, allowed, code) => {
83
- const state = capabilities.capabilities[capability];
82
+ // Surrendering the model means the runtime picks, so the only safe landing
83
+ // place is the attested runtime default, and it must be the approved full
84
+ // capability. Surrendering the EFFORT is a narrower loss and is handled
85
+ // separately below: collapsing to fullCapability there would throw away a
86
+ // model override the runtime demonstrably honours.
87
+ const degradeModel = () => {
88
+ const state = capabilities.capabilities.modelOverride;
84
89
  if (state === 'supported')
85
90
  return undefined;
86
91
  if (state === 'unverified')
87
- return blocked(code, `${capability} is unverified.`);
88
- if (!allowed || !capabilities.runtimeDefaultSelection || !equal(capabilities.runtimeDefaultSelection, mapping.fullCapability))
89
- return blocked(code, `${capability} is unavailable and no matching approved full default is attested.`);
92
+ return blocked('ROUTING_MODEL_OVERRIDE_UNAVAILABLE', 'modelOverride is unverified.');
93
+ if (!mapping.degradation.allowMissingModelOverride || !capabilities.runtimeDefaultSelection || !equal(capabilities.runtimeDefaultSelection, mapping.fullCapability))
94
+ return blocked('ROUTING_MODEL_OVERRIDE_UNAVAILABLE', 'modelOverride is unavailable and no matching approved full default is attested.');
90
95
  selection = mapping.fullCapability;
91
96
  outcome = 'degraded';
92
- unavailableEvidence.push(capability);
97
+ unavailableEvidence.push('modelOverride');
93
98
  return undefined;
94
99
  };
95
- const model = canDegrade('modelOverride', mapping.degradation.allowMissingModelOverride, 'ROUTING_MODEL_OVERRIDE_UNAVAILABLE');
100
+ // Degrading effort surrenders the effort component ONLY, keeping the routed
101
+ // model. On a runtime where the model can be overridden but reasoning effort
102
+ // cannot — effort belongs to the agent definition, not the invocation — the
103
+ // alternative was to route every profile to the full-capability model, so a
104
+ // mechanical slice would quietly run the judgment model while the diagnostic
105
+ // spoke only of effort. That is the silent defaulting the consumer reference
106
+ // forbids. The model at runtime-default effort must itself be attested.
107
+ const degradeEffort = () => {
108
+ const state = capabilities.capabilities.effortOverride;
109
+ if (state === 'supported')
110
+ return undefined;
111
+ if (state === 'unverified')
112
+ return blocked('ROUTING_EFFORT_OVERRIDE_UNAVAILABLE', 'effortOverride is unverified.');
113
+ if (!mapping.degradation.allowMissingEffortOverride)
114
+ return blocked('ROUTING_EFFORT_OVERRIDE_UNAVAILABLE', 'effortOverride is unavailable and the mapping does not allow degrading it.');
115
+ const degraded = { selector: selection.selector, effort: { kind: 'runtime-default' } };
116
+ if (!capabilities.runtimeDefaultSelection || !capabilities.availableSelections.some(candidate => equal(candidate, degraded)))
117
+ return blocked('ROUTING_EFFORT_OVERRIDE_UNAVAILABLE', 'effortOverride is unavailable and the routed model at runtime-default effort is not attested.');
118
+ selection = degraded;
119
+ outcome = 'degraded';
120
+ unavailableEvidence.push('effortOverride');
121
+ return undefined;
122
+ };
123
+ const model = degradeModel();
96
124
  if (model)
97
125
  return model;
98
- if (!capabilities.availableSelections.some(candidate => equal(candidate, selection)))
99
- return blocked('ROUTING_SELECTION_UNAVAILABLE', 'Approved selection is not currently available; no substitute is allowed.');
100
- if (selection.effort.kind === 'runtime-default') {
101
- if (!capabilities.runtimeDefaultSelection || !equal(capabilities.runtimeDefaultSelection, selection))
102
- return blocked('ROUTING_SELECTION_UNAVAILABLE', 'Runtime-default selection is not explicitly attested.');
103
- }
104
- else {
105
- const effort = canDegrade('effortOverride', mapping.degradation.allowMissingEffortOverride, 'ROUTING_EFFORT_OVERRIDE_UNAVAILABLE');
126
+ if (selection.effort.kind === 'explicit') {
127
+ const effort = degradeEffort();
106
128
  if (effort)
107
129
  return effort;
108
130
  }
131
+ // availableSelections is the receipt's attestation of what can actually be
132
+ // chosen, so it is checked on the FINAL selection, after any degradation
133
+ // rewrote it. Checking it first forced a runtime that cannot honour explicit
134
+ // effort to attest selections it could never run, purely to reach the
135
+ // degradation that would then discard them — the honest receipt was the one
136
+ // that failed.
137
+ if (!capabilities.availableSelections.some(candidate => equal(candidate, selection)))
138
+ return blocked('ROUTING_SELECTION_UNAVAILABLE', 'Approved selection is not currently available; no substitute is allowed.');
139
+ // A runtime-default effort is only meaningful if the receipt declares what the
140
+ // runtime default is. It need NOT equal this selection: requiring that made
141
+ // every per-profile selection unreachable on a runtime whose default model
142
+ // differs, even with modelOverride attested supported and the selection
143
+ // itself attested available. Attesting `haiku at runtime-default effort` is
144
+ // precisely the statement that the model may be overridden while effort is
145
+ // left alone.
146
+ if (selection.effort.kind === 'runtime-default' && !capabilities.runtimeDefaultSelection)
147
+ return blocked('ROUTING_SELECTION_UNAVAILABLE', 'Runtime-default selection is not explicitly attested.');
109
148
  if (capabilities.capabilities.observedModelEvidence !== 'supported') {
110
149
  if (!mapping.degradation.allowMissingObservedIdentity)
111
150
  return blocked('ROUTING_OBSERVED_IDENTITY_UNAVAILABLE', 'Observed model identity is required and unavailable.');
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readBoundedCandidate = readBoundedCandidate;
6
7
  exports.readEffectivePolicy = readEffectivePolicy;
7
8
  exports.approvePolicyWithBoundary = approvePolicyWithBoundary;
8
9
  exports.approvePolicy = approvePolicy;
@@ -45,7 +46,13 @@ catch (error) {
45
46
  if (error.code !== 'ENOENT')
46
47
  throw error;
47
48
  } }
48
- function readBounded(file) {
49
+ /**
50
+ * Bounded, symlink-refusing, identity-fenced read of a candidate or stored file.
51
+ * Exported so the read-only digest path observes a file under exactly the same
52
+ * constraints the approval path does, rather than growing a second reader that
53
+ * could drift from it.
54
+ */
55
+ function readBoundedCandidate(file) {
49
56
  assertSafeParents(file);
50
57
  let inspected;
51
58
  try {
@@ -106,7 +113,7 @@ function readEffectivePolicy(cwd) {
106
113
  assertPath(cwd, 'cwd');
107
114
  const project = (0, paths_1.projectPolicyPath)(cwd);
108
115
  try {
109
- const raw = readBounded(project);
116
+ const raw = readBoundedCandidate(project);
110
117
  if (raw !== null)
111
118
  return { state: 'approved', provenance: 'project', policy: parseApproved(raw) };
112
119
  }
@@ -115,7 +122,7 @@ function readEffectivePolicy(cwd) {
115
122
  }
116
123
  const user = (0, paths_1.userPolicyPath)();
117
124
  try {
118
- const raw = readBounded(user);
125
+ const raw = readBoundedCandidate(user);
119
126
  if (raw === null)
120
127
  return { state: 'absent' };
121
128
  return { state: 'approved', provenance: 'user', policy: parseApproved(raw) };
@@ -134,7 +141,7 @@ function approvePolicyWithBoundary(input, boundary) {
134
141
  if (input.replaceDigest !== undefined)
135
142
  (0, validate_1.assertDigest)(input.replaceDigest, 'replaceDigest');
136
143
  const cwd = path_1.default.resolve(input.cwd);
137
- const candidateRaw = readBounded(path_1.default.resolve(cwd, input.file));
144
+ const candidateRaw = readBoundedCandidate(path_1.default.resolve(cwd, input.file));
138
145
  if (candidateRaw === null)
139
146
  throw new Error('policy candidate is absent');
140
147
  const content = (0, validate_1.validatePolicyContent)((0, json_1.parseJsonNoDuplicate)(candidateRaw));
@@ -147,7 +154,7 @@ function approvePolicyWithBoundary(input, boundary) {
147
154
  assertRegularOrAbsent(target);
148
155
  const unlock = lock(target);
149
156
  try {
150
- const existingRaw = readBounded(target);
157
+ const existingRaw = readBoundedCandidate(target);
151
158
  const existing = existingRaw === null ? null : parseApproved(existingRaw);
152
159
  if (existing === null && input.replaceDigest !== undefined)
153
160
  throw new Error('replacement requires an existing predecessor');
@@ -47,11 +47,71 @@ describe('model-policy command', () => {
47
47
  expect(JSON.parse(String(out.mock.calls[0][0]))).toMatchObject({ policy: { state: 'absent' }, capability: { state: 'stale' } });
48
48
  expect(process.exitCode).toBe(2);
49
49
  });
50
+ it('discloses the canonical digest through a read-only subcommand', async () => {
51
+ const candidateDigest = jest.fn(() => ({ schema: 'model-policy/v1', digest: 'b'.repeat(64) }));
52
+ const program = new commander_1.Command();
53
+ program.exitOverride();
54
+ program.configureOutput({ writeErr: () => undefined });
55
+ (0, model_policy_1.registerModelPolicyCommand)(program, { candidateDigest });
56
+ await program.parseAsync(['node', 'awm', 'model-policy', 'digest', '--file', 'policy.json', '--cwd', 'fixture-root', '--json']);
57
+ expect(candidateDigest).toHaveBeenCalledWith({ file: 'policy.json', cwd: 'fixture-root' });
58
+ expect(JSON.parse(String(out.mock.calls[0][0]))).toEqual({ schema: 'model-policy/v1', digest: 'b'.repeat(64) });
59
+ });
60
+ it('takes no approval arguments on digest, so it cannot approve by accident', async () => {
61
+ const candidateDigest = jest.fn(() => ({ schema: 'model-policy/v1', digest: 'b'.repeat(64) }));
62
+ const approvePolicy = jest.fn();
63
+ const approveCapabilities = jest.fn();
64
+ const program = new commander_1.Command();
65
+ program.exitOverride();
66
+ program.configureOutput({ writeErr: () => undefined });
67
+ (0, model_policy_1.registerModelPolicyCommand)(program, { candidateDigest, approvePolicy: approvePolicy, approveCapabilities: approveCapabilities });
68
+ await expect(program.parseAsync(['node', 'awm', 'model-policy', 'digest', '--file', 'policy.json', '--expected-digest', 'a'.repeat(64)])).rejects.toThrow();
69
+ expect(approvePolicy).not.toHaveBeenCalled();
70
+ expect(approveCapabilities).not.toHaveBeenCalled();
71
+ });
72
+ it('rejects a missing or malformed --file on digest instead of digesting the flag', async () => {
73
+ const candidateDigest = jest.fn(() => ({ schema: 'model-policy/v1', digest: 'b'.repeat(64) }));
74
+ const program = new commander_1.Command();
75
+ program.exitOverride();
76
+ program.configureOutput({ writeErr: () => undefined });
77
+ (0, model_policy_1.registerModelPolicyCommand)(program, { candidateDigest });
78
+ await expect(program.parseAsync(['node', 'awm', 'model-policy', 'digest'])).rejects.toThrow();
79
+ await expect(program.parseAsync(['node', 'awm', 'model-policy', 'digest', '--file', '--cwd'])).rejects.toThrow(/requires a non-empty value/);
80
+ expect(candidateDigest).not.toHaveBeenCalled();
81
+ });
82
+ // This positive case is what the negative guard below could never establish.
83
+ // While `capabilities approve` was registered as one command with a
84
+ // positional `<approve>`, Commander handed the action (positional, options),
85
+ // so `--file` read as undefined and EVERY invocation — well-formed ones
86
+ // included — died on 'requires a non-empty value'. A suite that only
87
+ // asserted that rejection was green against a command that could not
88
+ // approve at all.
89
+ it('reaches the capability approval with parsed options, not a positional', async () => {
90
+ const approveCapabilities = jest.fn(() => ({ schema: 'routing-capabilities/v1' }));
91
+ const program = new commander_1.Command();
92
+ program.exitOverride();
93
+ program.configureOutput({ writeErr: () => undefined });
94
+ (0, model_policy_1.registerModelPolicyCommand)(program, { approveCapabilities: approveCapabilities });
95
+ await program.parseAsync(['node', 'awm', 'model-policy', 'capabilities', 'approve', '--file', 'receipt.json', '--expected-digest', 'a'.repeat(64), '--cwd', 'fixture-root', '--json']);
96
+ expect(approveCapabilities).toHaveBeenCalledWith({ file: 'receipt.json', cwd: 'fixture-root', expectedDigest: 'a'.repeat(64), replaceDigest: undefined });
97
+ expect(JSON.parse(String(out.mock.calls[0][0]))).toMatchObject({ state: 'approved' });
98
+ });
99
+ it('rejects an unknown capabilities subcommand instead of treating it as an approval', async () => {
100
+ const approveCapabilities = jest.fn();
101
+ const program = new commander_1.Command();
102
+ program.exitOverride();
103
+ program.configureOutput({ writeErr: () => undefined });
104
+ (0, model_policy_1.registerModelPolicyCommand)(program, { approveCapabilities: approveCapabilities });
105
+ await expect(program.parseAsync(['node', 'awm', 'model-policy', 'capabilities', 'revoke', '--file', 'receipt.json', '--expected-digest', 'a'.repeat(64)])).rejects.toThrow();
106
+ expect(approveCapabilities).not.toHaveBeenCalled();
107
+ });
50
108
  it('rejects an empty replacement digest for capability approval at the command boundary', async () => {
109
+ const approveCapabilities = jest.fn();
51
110
  const program = new commander_1.Command();
52
111
  program.exitOverride();
53
112
  program.configureOutput({ writeErr: () => undefined });
54
- (0, model_policy_1.registerModelPolicyCommand)(program, { approveCapabilities: jest.fn() });
113
+ (0, model_policy_1.registerModelPolicyCommand)(program, { approveCapabilities: approveCapabilities });
55
114
  await expect(program.parseAsync(['node', 'awm', 'model-policy', 'capabilities', 'approve', '--file', 'receipt.json', '--expected-digest', 'a'.repeat(64), '--replace-digest', ''])).rejects.toThrow(/requires a non-empty value/);
115
+ expect(approveCapabilities).not.toHaveBeenCalled();
56
116
  });
57
117
  });
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const digest_1 = require("../../../src/core/model-policy/digest");
10
+ const store_1 = require("../../../src/core/model-policy/store");
11
+ const capabilities_1 = require("../../../src/core/model-policy/capabilities");
12
+ const canonical_1 = require("../../../src/core/model-policy/canonical");
13
+ const capabilities_2 = require("../../../src/core/model-policy/capabilities");
14
+ const sha = 'a'.repeat(64);
15
+ const policy = () => ({ schema: 'model-policy/v1', mappings: [{ target: 'codex', runtimeKind: 'native', profiles: {
16
+ mechanical: { selector: { kind: 'model', id: 'gpt-5.6-luna' }, effort: { kind: 'explicit', value: 'medium' } },
17
+ integration: { selector: { kind: 'model', id: 'gpt-5.6-terra' }, effort: { kind: 'explicit', value: 'medium' } },
18
+ judgment: { selector: { kind: 'model', id: 'gpt-5.6-sol' }, effort: { kind: 'explicit', value: 'medium' } },
19
+ }, fullCapability: { selector: { kind: 'model', id: 'gpt-5.6-sol' }, effort: { kind: 'explicit', value: 'high' } }, degradation: { allowMissingModelOverride: false, allowMissingEffortOverride: false, allowMissingObservedIdentity: false } }], implementationBudget: { maxAttempts: 3, escalation: ['mechanical', 'integration', 'judgment'], judgmentEfforts: ['medium', 'high'] } });
20
+ const receipt = () => ({
21
+ schema: 'routing-capabilities/v1',
22
+ runtime: { target: 'codex', kind: 'native', version: '1.0.0', accountScopeDigest: sha },
23
+ recordedAt: '2026-09-17T00:00:00.000Z', expiresAt: '2026-09-18T00:00:00.000Z',
24
+ capabilities: { artifactDelivery: 'unverified', interactiveExecution: 'supported', unattendedController: 'supported', nativeSubagents: 'unverified', modelOverride: 'supported', effortOverride: 'supported', observedModelEvidence: 'supported', durableResume: 'unverified' },
25
+ availableSelections: [{ selector: { kind: 'model', id: 'full' }, effort: { kind: 'explicit', value: 'high' } }],
26
+ runtimeDefaultSelection: { selector: { kind: 'model', id: 'full' }, effort: { kind: 'explicit', value: 'high' } },
27
+ evidence: ['interactiveExecution', 'unattendedController', 'modelOverride', 'effortOverride', 'observedModelEvidence'].map(capability => ({ capability, kind: 'native-control', receiptDigest: sha })),
28
+ approval: { approvalId: 'approval-1', snapshotDigest: sha },
29
+ });
30
+ describe('candidateDigest', () => {
31
+ let cwd;
32
+ const home = process.env.HOME;
33
+ const awmHome = process.env.AWM_HOME;
34
+ beforeEach(() => { cwd = fs_1.default.mkdtempSync(path_1.default.join(fs_1.default.realpathSync(os_1.default.tmpdir()), 'awm-digest-')); process.env.HOME = cwd; process.env.AWM_HOME = path_1.default.join(cwd, '.awm'); });
35
+ afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); if (home === undefined)
36
+ delete process.env.HOME;
37
+ else
38
+ process.env.HOME = home; if (awmHome === undefined)
39
+ delete process.env.AWM_HOME;
40
+ else
41
+ process.env.AWM_HOME = awmHome; });
42
+ const write = (name, value) => { const file = path_1.default.join(cwd, name); fs_1.default.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); return name; };
43
+ it('emits the digest the policy approval path will demand, not a second opinion', () => {
44
+ const file = write('policy.json', policy());
45
+ expect((0, digest_1.candidateDigest)({ file, cwd })).toEqual({ schema: 'model-policy/v1', digest: (0, canonical_1.canonicalPolicyDigest)(policy()) });
46
+ });
47
+ it('emits the digest the capability approval path will demand', () => {
48
+ const file = write('receipt.json', receipt());
49
+ expect((0, digest_1.candidateDigest)({ file, cwd })).toEqual({ schema: 'routing-capabilities/v1', digest: (0, capabilities_2.capabilityReceiptDigest)(receipt()) });
50
+ });
51
+ it('is insensitive to key order and whitespace, because the digest is over the canonical form', () => {
52
+ const content = policy();
53
+ const reordered = { implementationBudget: content.implementationBudget, mappings: content.mappings, schema: content.schema };
54
+ fs_1.default.writeFileSync(path_1.default.join(cwd, 'a.json'), JSON.stringify(content));
55
+ fs_1.default.writeFileSync(path_1.default.join(cwd, 'b.json'), JSON.stringify(reordered, null, 4));
56
+ expect((0, digest_1.candidateDigest)({ file: 'a.json', cwd }).digest).toBe((0, digest_1.candidateDigest)({ file: 'b.json', cwd }).digest);
57
+ });
58
+ it('refuses a document whose declared schema it does not own, instead of guessing', () => {
59
+ const file = write('other.json', { schema: 'approved-model-policy/v1' });
60
+ expect(() => (0, digest_1.candidateDigest)({ file, cwd })).toThrow(/schema model-policy\/v1 or routing-capabilities\/v1/);
61
+ });
62
+ it('refuses an invalid document rather than emitting a digest approval would reject', () => {
63
+ const broken = policy();
64
+ delete broken.implementationBudget;
65
+ const file = write('broken.json', broken);
66
+ expect(() => (0, digest_1.candidateDigest)({ file, cwd })).toThrow();
67
+ });
68
+ it('reports an absent candidate loudly', () => {
69
+ expect(() => (0, digest_1.candidateDigest)({ file: 'missing.json', cwd })).toThrow(/absent/);
70
+ });
71
+ it('refuses a symlinked candidate, matching the approval path', () => {
72
+ write('policy.json', policy());
73
+ fs_1.default.symlinkSync(path_1.default.join(cwd, 'policy.json'), path_1.default.join(cwd, 'link.json'));
74
+ expect(() => (0, digest_1.candidateDigest)({ file: 'link.json', cwd })).toThrow(/unsafe/);
75
+ });
76
+ it('rejects malformed input instead of returning undefined', () => {
77
+ expect(() => (0, digest_1.candidateDigest)(undefined)).toThrow(/digest input is required/);
78
+ expect(() => (0, digest_1.candidateDigest)({ file: '', cwd })).toThrow(/bounded path/);
79
+ expect(() => (0, digest_1.candidateDigest)({ file: 'policy.json', cwd: '' })).toThrow(/bounded path/);
80
+ fs_1.default.writeFileSync(path_1.default.join(cwd, 'array.json'), '[]');
81
+ expect(() => (0, digest_1.candidateDigest)({ file: 'array.json', cwd })).toThrow(/must be a JSON object/);
82
+ });
83
+ // The point of the command: the digest it discloses is the one approval
84
+ // demands. A digest that merely looked plausible would be worthless, so the
85
+ // proof is that approval ACCEPTS it, not that two functions agree.
86
+ it('produces a digest that the policy approval path accepts end to end', () => {
87
+ const file = write('policy.json', policy());
88
+ const observed = [];
89
+ const boundary = {
90
+ withProjectLease: (_root, operation) => operation(),
91
+ readRegularFile: (target) => { const entry = { bytes: fs_1.default.readFileSync(target), identity: Object.freeze({}) }; observed.push(entry); return entry; },
92
+ writeProjectTransaction: (root, destination, content) => { const target = path_1.default.join(root, destination); fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true }); fs_1.default.writeFileSync(target, content); },
93
+ };
94
+ const approved = (0, store_1.approvePolicyWithBoundary)({ file, scope: 'project', cwd, expectedDigest: (0, digest_1.candidateDigest)({ file, cwd }).digest }, boundary);
95
+ expect(approved.contentDigest).toBe((0, digest_1.candidateDigest)({ file, cwd }).digest);
96
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(cwd, '.awm', 'model-policy.json'), 'utf8')).contentDigest).toBe(approved.contentDigest);
97
+ });
98
+ it('produces a digest that the capability approval path accepts end to end', () => {
99
+ const value = receipt();
100
+ const file = write('receipt.json', value);
101
+ const approved = (0, capabilities_1.approveCapabilities)({ file, cwd, expectedDigest: (0, digest_1.candidateDigest)({ file, cwd }).digest, now: new Date('2026-09-17T12:00:00.000Z') });
102
+ expect(approved).toEqual(value);
103
+ });
104
+ // Falsifiability: the same approval must REJECT any digest but the disclosed
105
+ // one, or the assertion above would pass against a command that emitted noise.
106
+ it('is the only digest approval accepts', () => {
107
+ const value = receipt();
108
+ const file = write('receipt.json', value);
109
+ expect(() => (0, capabilities_1.approveCapabilities)({ file, cwd, expectedDigest: 'f'.repeat(64), now: new Date('2026-09-17T12:00:00.000Z') })).toThrow(/does not match expectedDigest/);
110
+ });
111
+ it('writes nothing: digesting is not approving', () => {
112
+ const file = write('policy.json', policy());
113
+ (0, digest_1.candidateDigest)({ file, cwd });
114
+ expect(fs_1.default.existsSync(path_1.default.join(cwd, '.awm'))).toBe(false);
115
+ expect(fs_1.default.readdirSync(cwd).sort()).toEqual(['policy.json']);
116
+ });
117
+ });
@@ -98,6 +98,80 @@ describe('resolveSelection', () => {
98
98
  value.capabilities.capabilities.effortOverride = 'unsupported';
99
99
  expect((0, resolve_2.resolveV1Dispatch)({ ...value, plan: verifiedV1Plan, optInV1: true })).toMatchObject({ state: 'resolved', outcome: 'native', selection: runtimeDefaultSelection('full') });
100
100
  });
101
+ // Regression for the collapse that made routed model and surrendered effort
102
+ // indistinguishable: degrading effort used to rewrite the selection to
103
+ // fullCapability, so a mechanical slice silently ran the judgment model while
104
+ // the only evidence emitted named effortOverride.
105
+ it('keeps the routed per-profile model when effort override degrades', () => {
106
+ const value = input();
107
+ value.policy.content.mappings[0].degradation.allowMissingEffortOverride = true;
108
+ value.capabilities.capabilities.effortOverride = 'unsupported';
109
+ value.capabilities.runtimeDefaultSelection = runtimeDefaultSelection('full');
110
+ value.capabilities.availableSelections = [...value.capabilities.availableSelections, runtimeDefaultSelection('mechanical'), runtimeDefaultSelection('integration'), runtimeDefaultSelection('full')];
111
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'resolved', outcome: 'degraded', selection: runtimeDefaultSelection('mechanical'), effectiveProfile: 'mechanical', unavailableEvidence: ['effortOverride'] });
112
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'integration' })).toMatchObject({ state: 'resolved', outcome: 'degraded', selection: runtimeDefaultSelection('integration'), effectiveProfile: 'integration', unavailableEvidence: ['effortOverride'] });
113
+ });
114
+ // An honest receipt for a runtime with no effort control attests ONLY
115
+ // runtime-default selections — it cannot claim `mechanical at low effort` is
116
+ // selectable. Checking availability before degradation rejected exactly that
117
+ // receipt, so the contract only worked for a receipt that overclaimed.
118
+ it('degrades effort against a receipt that attests only runtime-default selections', () => {
119
+ const value = input();
120
+ value.policy.content.mappings[0].degradation.allowMissingEffortOverride = true;
121
+ value.capabilities.capabilities.effortOverride = 'unsupported';
122
+ value.capabilities.availableSelections = [runtimeDefaultSelection('mechanical'), runtimeDefaultSelection('integration'), runtimeDefaultSelection('judgment'), runtimeDefaultSelection('full')];
123
+ value.capabilities.runtimeDefaultSelection = runtimeDefaultSelection('full');
124
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'resolved', outcome: 'degraded', selection: runtimeDefaultSelection('mechanical'), unavailableEvidence: ['effortOverride'] });
125
+ });
126
+ // The runtime default is one selection; a per-profile selection at
127
+ // runtime-default effort is a different one. Requiring them equal made every
128
+ // profile but the default model unroutable, with modelOverride attested
129
+ // supported and the selection itself attested available.
130
+ it('routes a per-profile runtime-default selection whose model differs from the runtime default', () => {
131
+ const value = input();
132
+ value.policy.content.mappings[0].profiles.mechanical = runtimeDefaultSelection('mechanical');
133
+ value.capabilities.availableSelections = [runtimeDefaultSelection('mechanical'), runtimeDefaultSelection('full')];
134
+ value.capabilities.runtimeDefaultSelection = runtimeDefaultSelection('full');
135
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'resolved', outcome: 'native', selection: runtimeDefaultSelection('mechanical'), effectiveProfile: 'mechanical' });
136
+ });
137
+ it('still blocks a runtime-default selection the receipt does not attest at all', () => {
138
+ const value = input();
139
+ value.policy.content.mappings[0].profiles.mechanical = runtimeDefaultSelection('mechanical');
140
+ value.capabilities.availableSelections = [runtimeDefaultSelection('full')];
141
+ value.capabilities.runtimeDefaultSelection = runtimeDefaultSelection('full');
142
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_SELECTION_UNAVAILABLE' }] });
143
+ });
144
+ it('still blocks a runtime-default selection when the receipt declares no runtime default', () => {
145
+ const value = input();
146
+ value.policy.content.mappings[0].profiles.mechanical = runtimeDefaultSelection('mechanical');
147
+ value.capabilities.availableSelections = [runtimeDefaultSelection('mechanical')];
148
+ delete value.capabilities.runtimeDefaultSelection;
149
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_SELECTION_UNAVAILABLE' }] });
150
+ });
151
+ it('blocks effort degradation when the routed model at runtime-default effort is not attested', () => {
152
+ const value = input();
153
+ value.policy.content.mappings[0].degradation.allowMissingEffortOverride = true;
154
+ value.capabilities.capabilities.effortOverride = 'unsupported';
155
+ value.capabilities.runtimeDefaultSelection = runtimeDefaultSelection('full');
156
+ value.capabilities.availableSelections = [...value.capabilities.availableSelections, runtimeDefaultSelection('full')];
157
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_EFFORT_OVERRIDE_UNAVAILABLE' }] });
158
+ });
159
+ it('keeps effort strict when the mapping does not allow degrading it', () => {
160
+ const value = input();
161
+ value.capabilities.capabilities.effortOverride = 'unsupported';
162
+ value.capabilities.runtimeDefaultSelection = runtimeDefaultSelection('full');
163
+ value.capabilities.availableSelections = [...value.capabilities.availableSelections, runtimeDefaultSelection('mechanical'), runtimeDefaultSelection('full')];
164
+ expect(value.policy.content.mappings[0].degradation.allowMissingEffortOverride).toBe(false);
165
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_EFFORT_OVERRIDE_UNAVAILABLE' }] });
166
+ });
167
+ it('never degrades effort on an unverified capability, only on an attested unsupported one', () => {
168
+ const value = input();
169
+ value.policy.content.mappings[0].degradation.allowMissingEffortOverride = true;
170
+ value.capabilities.capabilities.effortOverride = 'unverified';
171
+ value.capabilities.runtimeDefaultSelection = runtimeDefaultSelection('full');
172
+ value.capabilities.availableSelections = [...value.capabilities.availableSelections, runtimeDefaultSelection('mechanical'), runtimeDefaultSelection('full')];
173
+ expect((0, resolve_1.resolveSelection)({ ...value, requestedProfile: 'mechanical' })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_EFFORT_OVERRIDE_UNAVAILABLE', message: expect.stringMatching(/unverified/) }] });
174
+ });
101
175
  it('does not require effort override after model fallback selects an attested runtime-default full selection', () => {
102
176
  const value = input();
103
177
  value.policy.content.mappings[0].fullCapability = runtimeDefaultSelection('full');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.9.0",
3
+ "version": "9.10.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file