agentic-workflow-manager 9.9.0 → 9.10.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/commands/model-policy/index.js +22 -1
- package/dist/src/commands/plan/index.js +33 -4
- package/dist/src/commands/watch/supervisor.js +34 -3
- 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/commands/plan/index.test.js +58 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +38 -0
- 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)
|
|
@@ -183,10 +183,39 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
|
|
|
183
183
|
if (!roles.includes(options.role))
|
|
184
184
|
throw new Error('--role is invalid');
|
|
185
185
|
const runtime = (0, capabilities_1.validateRuntimeKey)({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest });
|
|
186
|
-
const result = report.schema === 'compact-slices/v1' && !options.optInV1 ? (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, policy: undefined, capabilities: undefined, runtime, now: new Date(), optInV1: false }) : (() => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const result = report.schema === 'compact-slices/v1' && !options.optInV1 ? (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, policy: undefined, capabilities: undefined, runtime, now: new Date(), optInV1: false }) : (() => {
|
|
187
|
+
const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(cwd);
|
|
188
|
+
const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, new Date());
|
|
189
|
+
if (!options.lineage) {
|
|
190
|
+
const first = (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, sliceId: options.slice, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date(), optInV1: options.optInV1 === true });
|
|
191
|
+
// The consumer forwards THIS envelope to `job routing-reserve`, which
|
|
192
|
+
// is what creates the lineage the --lineage escalation path below
|
|
193
|
+
// then reads. Emitting it only there made the first attempt of any
|
|
194
|
+
// lineage unreservable: envelope <= lineage <= reserve <= envelope.
|
|
195
|
+
// Every field comes from values already in hand, so the CLI stays the
|
|
196
|
+
// sole author and the consumer never assembles a protocol document.
|
|
197
|
+
// No custodyHandoff here on purpose: the obligation and lineage ids
|
|
198
|
+
// are the caller's to choose on a first attempt, and inventing them
|
|
199
|
+
// would put identity the CLI cannot know into custody.
|
|
200
|
+
if (first.state !== 'resolved' || report.schema !== 'compact-slices/v2' || report.executionDigest === undefined)
|
|
201
|
+
return first;
|
|
202
|
+
const slice = report.manifest.slices.find((candidate) => candidate.id === options.slice);
|
|
203
|
+
const requestedProfile = options.role === 'implementer' ? slice?.implementerProfile : 'full';
|
|
204
|
+
if (requestedProfile === undefined)
|
|
205
|
+
return first;
|
|
206
|
+
return { ...first, envelope: { schema: 'routing-envelope/v1', runtime, role: options.role, planDigest: report.planDigest, executionDigest: report.executionDigest, ...(options.slice === undefined ? {} : { sliceId: options.slice }), requestedProfile, effectiveProfile: first.effectiveProfile, resolved: first.selection, policyDigest: first.policyDigest, capabilityDigest: first.capabilityDigest, outcome: first.outcome, unavailableEvidence: first.unavailableEvidence } };
|
|
207
|
+
}
|
|
208
|
+
const state = journalObservation(cwd).journalState;
|
|
209
|
+
const lineage = state.implementationLineages.find(candidate => candidate.id === options.lineage);
|
|
210
|
+
const next = (0, journal_1.resolveLineageEscalation)(state, options.lineage, lineage.initialProfile);
|
|
211
|
+
const resolved = (0, resolve_1.resolveSelection)({ role: 'implementer', requestedProfile: next.profile, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date() });
|
|
212
|
+
if (resolved.state === 'blocked')
|
|
213
|
+
return resolved;
|
|
214
|
+
const highRequiresFull = next.effort !== 'high' || (policy.state === 'approved' && (() => { const full = policy.policy.content.mappings.find(row => row.target === runtime.target && row.runtimeKind === runtime.kind)?.fullCapability; return full?.selector.kind === resolved.selection.selector.kind && full.selector.id === resolved.selection.selector.id && full.effort.kind === 'explicit' && full.effort.value === 'high'; })());
|
|
215
|
+
if (resolved.selection.effort.kind !== 'explicit' || resolved.selection.effort.value !== next.effort || !highRequiresFull)
|
|
216
|
+
return { state: 'blocked', diagnostics: [{ code: 'ROUTING_ESCALATION_EFFORT_UNAVAILABLE', message: 'The approved selected effort does not match the next lineage escalation.' }] };
|
|
217
|
+
return { ...resolved, envelope: { schema: 'routing-envelope/v1', runtime, role: 'implementer', planDigest: report.planDigest, executionDigest: report.executionDigest, sliceId: lineage.sliceId, requestedProfile: lineage.initialProfile, effectiveProfile: next.profile, resolved: resolved.selection, policyDigest: resolved.policyDigest, capabilityDigest: resolved.capabilityDigest, outcome: resolved.outcome, unavailableEvidence: resolved.unavailableEvidence }, custodyHandoff: { kind: 'routing-reserve', obligationId: lineage.obligationId, lineageId: lineage.id } };
|
|
218
|
+
})();
|
|
190
219
|
process.stdout.write(options.json ? `${JSON.stringify(result)}\n` : `Plan routing: ${result.state}\n`);
|
|
191
220
|
if (result.state === 'blocked')
|
|
192
221
|
process.exitCode = 2;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Supervisor = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
|
|
3
|
+
exports.Supervisor = exports.MAX_SENSOR_DEFERRALS = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
|
|
4
4
|
exports.runSupervisorLoop = runSupervisorLoop;
|
|
5
5
|
const store_1 = require("../../core/journal/store");
|
|
6
6
|
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
@@ -120,6 +120,15 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
|
|
|
120
120
|
jobStallObservationMs: 5 * 60000, // R3.5 default: mismo orden de magnitud que heartbeatTimeoutMs, concern independiente
|
|
121
121
|
maxParallelTracks: 1, // overridden en tiempo de ejecución con loadDefaultParallelism() (watch/index.ts)
|
|
122
122
|
};
|
|
123
|
+
// 'frozen' (R5.2/R6.3, Task 10): SOLO puede ocurrir en el journal de un
|
|
124
|
+
// TRACK individual (nunca en el del plan) — el track terminó, con las 6
|
|
125
|
+
// observaciones demostrables (cero jobs vivos, gate local verde, worktree
|
|
126
|
+
// limpio, generación propia terminada con identidad confirmada), de
|
|
127
|
+
// atender un `track-freeze-request` pedido por el supervisor del plan.
|
|
128
|
+
/** How many consecutive ticks an inconclusive sensor verdict may defer dispatch
|
|
129
|
+
* before the supervisor says so out loud. Small: the deferral covers a verdict
|
|
130
|
+
* still settling, not one that never will. */
|
|
131
|
+
exports.MAX_SENSOR_DEFERRALS = 3;
|
|
123
132
|
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
124
133
|
function recoveryWhitelistBlocker(state) {
|
|
125
134
|
if (state.cycle.status === 'BLOCKED')
|
|
@@ -149,6 +158,10 @@ class Supervisor {
|
|
|
149
158
|
dispatchAdmission;
|
|
150
159
|
backoff = new generations_1.Backoff();
|
|
151
160
|
relaunchNotBefore = 0;
|
|
161
|
+
/** Consecutive ticks deferred on an inconclusive sensor verdict. Bounded:
|
|
162
|
+
* the deferral exists for a verdict that is still settling, and a verdict
|
|
163
|
+
* that never settles must become visible rather than loop forever. */
|
|
164
|
+
sensorDeferrals = 0;
|
|
152
165
|
lastActivity = null;
|
|
153
166
|
lastGenerationToken = null;
|
|
154
167
|
trackRuntime;
|
|
@@ -226,12 +239,30 @@ class Supervisor {
|
|
|
226
239
|
// the already-started controller is still settling. It never
|
|
227
240
|
// authorizes dispatch; retry the read-only admission next tick
|
|
228
241
|
// instead of permanently custodying a healthy cycle.
|
|
242
|
+
//
|
|
243
|
+
// Bounded, because the verdict is not always transient: a
|
|
244
|
+
// registry whose sensors are all disabled reports
|
|
245
|
+
// `not-certified` forever. Unbounded, this returned before
|
|
246
|
+
// `consumePendingRequests` below, so the supervisor ticked
|
|
247
|
+
// indefinitely applying nothing and writing nothing — no event,
|
|
248
|
+
// no state change, no exit. Refusing to progress is correct
|
|
249
|
+
// here; refusing invisibly is not. Consuming requests cannot be
|
|
250
|
+
// hoisted above this gate to compensate: applying a job-request
|
|
251
|
+
// creates a Job that runnerTick can start, which is the dispatch
|
|
252
|
+
// this admission exists to authorize.
|
|
229
253
|
if (admission.planState === 'valid' && admission.currentness === 'current'
|
|
230
|
-
&& admission.sensors === 'not-certified' && admission.journal === 'not-required')
|
|
231
|
-
|
|
254
|
+
&& admission.sensors === 'not-certified' && admission.journal === 'not-required') {
|
|
255
|
+
this.sensorDeferrals += 1;
|
|
256
|
+
if (this.sensorDeferrals <= exports.MAX_SENSOR_DEFERRALS)
|
|
257
|
+
return 'continue';
|
|
258
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `veredicto de sensores no concluyente tras ${this.sensorDeferrals} ticks: ${admission.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join('; ')}`);
|
|
259
|
+
return 'custody';
|
|
260
|
+
}
|
|
232
261
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión compacta desatendida bloqueada antes de dispatch: ${admission.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join('; ')}`);
|
|
233
262
|
return 'custody';
|
|
234
263
|
}
|
|
264
|
+
// The verdict settled, which is exactly what the deferral waited for.
|
|
265
|
+
this.sensorDeferrals = 0;
|
|
235
266
|
}
|
|
236
267
|
let recoveryResumeAction;
|
|
237
268
|
// Schema-2 custody is reconciled before any controller launch. This is
|
|
@@ -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
|
});
|
|
@@ -13,12 +13,20 @@ const plan_1 = require("../../../src/commands/plan");
|
|
|
13
13
|
const init_1 = require("../../../src/commands/watch/init");
|
|
14
14
|
const providers_1 = require("../../../src/providers");
|
|
15
15
|
const registry_1 = require("../../../src/core/renderers/registry");
|
|
16
|
+
const canonical_1 = require("../../../src/core/model-policy/canonical");
|
|
17
|
+
const capabilities_1 = require("../../../src/core/model-policy/capabilities");
|
|
18
|
+
const journal_1 = require("../../../src/core/model-policy/journal");
|
|
19
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
16
20
|
const stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
17
21
|
const repositoryRoot = path_1.default.resolve(__dirname, '../../../..');
|
|
18
22
|
const approved = (0, validate_1.validatePlanFile)('docs/plans/2026-09-14-compact-only-bootstrap-plan.md', repositoryRoot);
|
|
19
23
|
if (approved.state !== 'valid')
|
|
20
24
|
throw new Error(`tracked bootstrap plan must validate before command tests: ${approved.state}`);
|
|
21
25
|
const valid = approved;
|
|
26
|
+
const approvedV2 = (0, validate_1.validatePlanFile)('valid.md', path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v2'));
|
|
27
|
+
if (approvedV2.state !== 'valid')
|
|
28
|
+
throw new Error(`v2 fixture must validate before command tests: ${approvedV2.state}`);
|
|
29
|
+
const v2Valid = approvedV2;
|
|
22
30
|
const invalid = {
|
|
23
31
|
state: 'invalid', diagnostics: [{ code: 'PLAN_MARKERS', message: 'compact markers must occur once in order' }],
|
|
24
32
|
};
|
|
@@ -60,6 +68,56 @@ describe('plan validate Commander wiring', () => {
|
|
|
60
68
|
expect(policy).not.toHaveBeenCalled();
|
|
61
69
|
expect(capabilities).not.toHaveBeenCalled();
|
|
62
70
|
});
|
|
71
|
+
// The consumer reference says: call `plan resolve` for the exact role and
|
|
72
|
+
// local slice, then send ITS frozen envelope to `job routing-reserve`. The
|
|
73
|
+
// envelope was only ever built on the --lineage branch, which resolves the
|
|
74
|
+
// lineage out of journal state — and lineages are created by
|
|
75
|
+
// reserveRoutingAttempt, i.e. by routing-reserve itself. envelope <= lineage
|
|
76
|
+
// <= reserve <= envelope, so the first attempt of any lineage could never be
|
|
77
|
+
// reserved. This test goes from resolve output to a reserved attempt with no
|
|
78
|
+
// hand-built envelope literal, which is the only shape that can catch it:
|
|
79
|
+
// tests/commands/job/routing.test.ts passes a literal and so starts one step
|
|
80
|
+
// past the gap.
|
|
81
|
+
it('emits a reservable envelope on a first v2 attempt, closing the resolve to reserve path', async () => {
|
|
82
|
+
// A genuinely validated report: the resolver rejects any object that did
|
|
83
|
+
// not come out of validatePlanFile, so a spread-and-relabel fixture
|
|
84
|
+
// cannot reach this path at all.
|
|
85
|
+
const sliceId = 'S1';
|
|
86
|
+
const sel = { selector: { kind: 'model', id: 'gpt-5.6-luna' }, effort: { kind: 'explicit', value: 'medium' } };
|
|
87
|
+
const full = { selector: { kind: 'model', id: 'gpt-5.6-sol' }, effort: { kind: 'explicit', value: 'high' } };
|
|
88
|
+
const sha = 'a'.repeat(64);
|
|
89
|
+
const content = { schema: 'model-policy/v1', mappings: [{ target: 'codex', runtimeKind: 'native', profiles: { mechanical: sel, integration: sel, judgment: sel }, fullCapability: full, degradation: { allowMissingModelOverride: false, allowMissingEffortOverride: false, allowMissingObservedIdentity: false } }], implementationBudget: { maxAttempts: 3, escalation: ['mechanical', 'integration', 'judgment'], judgmentEfforts: ['medium', 'high'] } };
|
|
90
|
+
const readEffectivePolicy = () => ({ state: 'approved', provenance: 'user', policy: { schema: 'approved-model-policy/v1', content, contentDigest: (0, canonical_1.canonicalPolicyDigest)(content), approval: { approvedAt: '2026-09-19T00:00:00.000Z', approvalId: 'a1' }, lineage: { previousDigest: null } } });
|
|
91
|
+
const receipt = {
|
|
92
|
+
schema: 'routing-capabilities/v1',
|
|
93
|
+
runtime: { target: 'codex', kind: 'native', version: '1.0.0', accountScopeDigest: sha },
|
|
94
|
+
recordedAt: new Date(Date.now() - 60_000).toISOString(), expiresAt: new Date(Date.now() + 6 * 3600_000).toISOString(),
|
|
95
|
+
capabilities: { artifactDelivery: 'supported', interactiveExecution: 'supported', unattendedController: 'supported', nativeSubagents: 'supported', modelOverride: 'supported', effortOverride: 'supported', observedModelEvidence: 'supported', durableResume: 'unverified' },
|
|
96
|
+
availableSelections: [sel, full], runtimeDefaultSelection: full,
|
|
97
|
+
evidence: ['modelOverride', 'effortOverride', 'observedModelEvidence', 'nativeSubagents', 'artifactDelivery', 'interactiveExecution', 'unattendedController'].map(capability => ({ capability, kind: 'native-control', receiptDigest: 'b'.repeat(64) })),
|
|
98
|
+
approval: { approvalId: 'r1', snapshotDigest: 'c'.repeat(64) },
|
|
99
|
+
};
|
|
100
|
+
const readCapabilities = () => ({ state: 'current', receipt, digest: (0, capabilities_1.capabilityReceiptDigest)(receipt) });
|
|
101
|
+
const program = new commander_1.Command();
|
|
102
|
+
program.exitOverride();
|
|
103
|
+
program.configureOutput({ writeErr: () => undefined });
|
|
104
|
+
(0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => v2Valid, readEffectivePolicy: readEffectivePolicy, readCapabilities: readCapabilities });
|
|
105
|
+
await program.parseAsync(['node', 'awm', 'plan', 'resolve', 'plans/r4.md', '--provider', 'codex', '--runtime-kind', 'native', '--runtime-version', '1.0.0', '--account-scope-digest', sha, '--role', 'implementer', '--slice', sliceId, '--cwd', repositoryRoot, '--json']);
|
|
106
|
+
const emitted = JSON.parse(String(stdoutWrite.mock.calls[stdoutWrite.mock.calls.length - 1][0]));
|
|
107
|
+
expect(emitted.state).toBe('resolved');
|
|
108
|
+
expect(emitted.envelope).toBeDefined();
|
|
109
|
+
// The CLI must be the sole author: the envelope has to satisfy the same
|
|
110
|
+
// predicate routing-reserve applies, with nothing filled in by the caller.
|
|
111
|
+
expect((0, types_1.isRoutingEnvelope)(emitted.envelope)).toBe(true);
|
|
112
|
+
expect(emitted.envelope).toMatchObject({ schema: 'routing-envelope/v1', role: 'implementer', sliceId, requestedProfile: 'mechanical', effectiveProfile: 'mechanical', resolved: sel });
|
|
113
|
+
expect(emitted.envelope.planDigest).toBe(v2Valid.planDigest);
|
|
114
|
+
expect(emitted.envelope.executionDigest).toBe(v2Valid.executionDigest);
|
|
115
|
+
// The point of the whole thing: that envelope reserves a first attempt,
|
|
116
|
+
// which is what creates the lineage the escalation path later needs.
|
|
117
|
+
const reserved = (0, journal_1.reserveRoutingAttempt)((0, types_1.emptyState)('main'), { obligationId: 'o1', lineageId: 'l1', envelope: emitted.envelope, fingerprint: 'e'.repeat(64) }, '2026-09-19T00:00:00.000Z');
|
|
118
|
+
expect(reserved.attemptId).toEqual(expect.any(String));
|
|
119
|
+
expect(reserved.state.implementationLineages).toEqual([expect.objectContaining({ id: 'l1', sliceId, initialProfile: 'mechanical' })]);
|
|
120
|
+
});
|
|
63
121
|
it('emits deterministic human output for a valid compact plan', async () => {
|
|
64
122
|
await commandFor(valid).parseAsync(['node', 'awm', 'plan', 'validate', 'plans/r4.md']);
|
|
65
123
|
expect(String(stdoutWrite.mock.calls[0][0])).toBe('Plan validation: valid "plans/r4.md" (compact-slices/v1; 1 slices; 5 requirements; complete ownership)\n');
|
|
@@ -390,6 +390,44 @@ describe('supervisor loop', () => {
|
|
|
390
390
|
await expect(new supervisor_1.Supervisor(repo, 'main', cfg, spy, undefined, admitted).tick()).rejects.toThrow(/rama|branch/i);
|
|
391
391
|
expect(calls).toBe(0);
|
|
392
392
|
});
|
|
393
|
+
// The sensor deferral at the top of tick() covers a first inconclusive read
|
|
394
|
+
// while a controller settles, and returns 'continue' so a transient does not
|
|
395
|
+
// custody a healthy cycle. It returns BEFORE consumePendingRequests, and
|
|
396
|
+
// `sensors: 'not-certified'` is not always transient: it is the steady state
|
|
397
|
+
// of any registry whose sensors are all disabled, which the baseline registry
|
|
398
|
+
// documents about itself. Unbounded, the supervisor ticked forever applying
|
|
399
|
+
// nothing and writing nothing — no event, no state change, no exit. Observed
|
|
400
|
+
// against published 9.10.0: a routing-reserve and an unrelated
|
|
401
|
+
// controller-heartbeat both sat unconsumed for ~36 ticks with appliedRequests
|
|
402
|
+
// empty and the cycle still IN_PROGRESS.
|
|
403
|
+
test('an inconclusive sensor verdict defers dispatch a bounded number of ticks, then says so', async () => {
|
|
404
|
+
const repo = setupRepo();
|
|
405
|
+
initUnattendedFixture(repo);
|
|
406
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
407
|
+
const token = (0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'main').state).token;
|
|
408
|
+
(0, request_1.requestJob)(repo, 'main', token, ['node', '-e', 'process.exit(0)'], [], '.');
|
|
409
|
+
// Exactly what `awm plan admit --require-current --verify-sensors` returns
|
|
410
|
+
// for that registry: valid plan, current contracts, sensors not certified.
|
|
411
|
+
// Every field matches the deferral condition.
|
|
412
|
+
const notCertified = async () => ({
|
|
413
|
+
state: 'blocked', planState: 'valid', executionMode: 'desatendido',
|
|
414
|
+
journal: 'not-required', currentness: 'current', sensors: 'not-certified',
|
|
415
|
+
diagnostics: [{ code: 'ADMISSION_SENSORS_BLOCKED', message: 'sensors not certified' }],
|
|
416
|
+
});
|
|
417
|
+
let spawned = 0;
|
|
418
|
+
const sup = new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { spawned += 1; }, undefined, notCertified);
|
|
419
|
+
for (let i = 0; i < supervisor_1.MAX_SENSOR_DEFERRALS; i += 1) {
|
|
420
|
+
expect(await sup.tick()).toBe('continue');
|
|
421
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('IN_PROGRESS');
|
|
422
|
+
}
|
|
423
|
+
// The verdict never settled, so the supervisor must stop pretending it might.
|
|
424
|
+
expect(await sup.tick()).toBe('custody');
|
|
425
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
426
|
+
expect(state.cycle.status).toBe('BLOCKED');
|
|
427
|
+
expect(state.cycle.blockedReason).toMatch(/sensor/i);
|
|
428
|
+
// Deferring dispatch was right throughout, and stays right.
|
|
429
|
+
expect(spawned).toBe(0);
|
|
430
|
+
});
|
|
393
431
|
test('una admisión compacta bloqueada no lanza controlador ni wrappers', async () => {
|
|
394
432
|
initUnattendedFixture(repo);
|
|
395
433
|
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
@@ -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
|