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.
- package/dist/src/commands/model-policy/index.js +22 -1
- package/dist/src/core/model-policy/digest.js +35 -0
- package/dist/src/core/model-policy/resolve.js +54 -15
- package/dist/src/core/model-policy/store.js +12 -5
- package/dist/tests/commands/model-policy/index.test.js +61 -1
- package/dist/tests/core/model-policy/digest.test.js +117 -0
- package/dist/tests/core/model-policy/resolve.test.js +74 -0
- package/package.json +1 -1
- 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
|
@@ -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
|
|
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
|
-
|
|
83
|
-
|
|
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(
|
|
88
|
-
if (!
|
|
89
|
-
return blocked(
|
|
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(
|
|
97
|
+
unavailableEvidence.push('modelOverride');
|
|
93
98
|
return undefined;
|
|
94
99
|
};
|
|
95
|
-
|
|
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 (
|
|
99
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|