agentic-workflow-manager 9.8.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/job/gate.js +18 -0
- package/dist/src/commands/job/index.js +61 -0
- package/dist/src/commands/model-policy/index.js +94 -0
- package/dist/src/commands/plan/index.js +59 -2
- package/dist/src/commands/watch/apply.js +32 -0
- package/dist/src/core/admission/index.js +52 -2
- package/dist/src/core/admission/registry-contracts.js +1 -1
- package/dist/src/core/journal/requests.js +1 -1
- package/dist/src/core/journal/types.js +22 -1
- package/dist/src/core/model-policy/canonical.js +18 -0
- package/dist/src/core/model-policy/capabilities.js +197 -0
- package/dist/src/core/model-policy/capability-types.js +2 -0
- package/dist/src/core/model-policy/digest.js +35 -0
- package/dist/src/core/model-policy/journal.js +124 -0
- package/dist/src/core/model-policy/paths.js +12 -0
- package/dist/src/core/model-policy/resolve.js +155 -0
- package/dist/src/core/model-policy/store.js +189 -0
- package/dist/src/core/model-policy/types.js +2 -0
- package/dist/src/core/model-policy/validate.js +112 -0
- package/dist/src/core/plan/identity.js +4 -2
- package/dist/src/core/plan/validate.js +15 -16
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +16 -0
- package/dist/tests/commands/job/gate.test.js +9 -0
- package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
- package/dist/tests/commands/job/routing.test.js +11 -0
- package/dist/tests/commands/model-policy/index.test.js +117 -0
- package/dist/tests/commands/plan/index.test.js +52 -0
- package/dist/tests/commands/watch/apply.test.js +12 -0
- package/dist/tests/core/admission/index.test.js +53 -0
- package/dist/tests/core/model-policy/capabilities.test.js +365 -0
- package/dist/tests/core/model-policy/digest.test.js +117 -0
- package/dist/tests/core/model-policy/journal.test.js +46 -0
- package/dist/tests/core/model-policy/policy.test.js +47 -0
- package/dist/tests/core/model-policy/resolve.test.js +185 -0
- package/dist/tests/core/model-policy/store.test.js +177 -0
- package/dist/tests/core/plan/validate.test.js +40 -2
- package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
- package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
- package/dist/tests/structural/native-release-artifacts.test.js +14 -0
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
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.validateRuntimeKey = validateRuntimeKey;
|
|
7
|
+
exports.capabilityReceiptDigest = capabilityReceiptDigest;
|
|
8
|
+
exports.validateCapabilityReceipt = validateCapabilityReceipt;
|
|
9
|
+
exports.receiptIsCurrent = receiptIsCurrent;
|
|
10
|
+
exports.routingDiagnostic = routingDiagnostic;
|
|
11
|
+
exports.capabilityReceiptPath = capabilityReceiptPath;
|
|
12
|
+
exports.readCapabilities = readCapabilities;
|
|
13
|
+
exports.approveCapabilities = approveCapabilities;
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const providers_1 = require("../../providers");
|
|
18
|
+
const validate_1 = require("./validate");
|
|
19
|
+
const paths_1 = require("../paths");
|
|
20
|
+
const json_1 = require("../plan/json");
|
|
21
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
22
|
+
const RUNTIME_KIND = /^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
23
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
24
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
25
|
+
const CAPABILITY_KEYS = ['artifactDelivery', 'interactiveExecution', 'unattendedController', 'nativeSubagents', 'modelOverride', 'effortOverride', 'observedModelEvidence', 'durableResume'];
|
|
26
|
+
const CAPABILITY_VALUES = new Set(['supported', 'unsupported', 'unverified']);
|
|
27
|
+
function object(value, name) { if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
28
|
+
throw new Error(`${name} must be an object`); return value; }
|
|
29
|
+
function exact(value, keys, name) { const actual = Object.keys(value); if (actual.length !== keys.length || actual.some(key => !keys.includes(key)))
|
|
30
|
+
throw new Error(`${name} has unknown or missing fields`); }
|
|
31
|
+
function text(value, name, pattern) { if (typeof value !== 'string' || value.length === 0 || value.length > validate_1.MAX_IDENTIFIER || CONTROL.test(value) || (pattern && !pattern.test(value)))
|
|
32
|
+
throw new Error(`${name} is invalid`); return value; }
|
|
33
|
+
function timestamp(value, name) { return (0, validate_1.canonicalUtcTimestamp)(value, name); }
|
|
34
|
+
function selection(value, name) {
|
|
35
|
+
const item = object(value, name);
|
|
36
|
+
exact(item, ['selector', 'effort'], name);
|
|
37
|
+
const selector = object(item.selector, `${name}.selector`);
|
|
38
|
+
exact(selector, ['kind', 'id'], `${name}.selector`);
|
|
39
|
+
if (selector.kind !== 'model' && selector.kind !== 'tier')
|
|
40
|
+
throw new Error(`${name}.selector.kind is invalid`);
|
|
41
|
+
const effort = object(item.effort, `${name}.effort`);
|
|
42
|
+
if (effort.kind === 'explicit') {
|
|
43
|
+
exact(effort, ['kind', 'value'], `${name}.effort`);
|
|
44
|
+
return { selector: { kind: selector.kind, id: text(selector.id, `${name}.selector.id`) }, effort: { kind: 'explicit', value: text(effort.value, `${name}.effort.value`) } };
|
|
45
|
+
}
|
|
46
|
+
if (effort.kind === 'runtime-default') {
|
|
47
|
+
exact(effort, ['kind'], `${name}.effort`);
|
|
48
|
+
return { selector: { kind: selector.kind, id: text(selector.id, `${name}.selector.id`) }, effort: { kind: 'runtime-default' } };
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`${name}.effort.kind is invalid`);
|
|
51
|
+
}
|
|
52
|
+
function sameSelection(left, right) { return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value); }
|
|
53
|
+
function validateRuntimeKey(value) {
|
|
54
|
+
const item = object(value, 'runtime');
|
|
55
|
+
exact(item, ['target', 'kind', 'version', 'accountScopeDigest'], 'runtime');
|
|
56
|
+
if (typeof item.target !== 'string' || !providers_1.AGENT_TARGETS.includes(item.target))
|
|
57
|
+
throw new Error('runtime target is invalid');
|
|
58
|
+
const accountScopeDigest = text(item.accountScopeDigest, 'runtime.accountScopeDigest');
|
|
59
|
+
(0, validate_1.assertDigest)(accountScopeDigest, 'runtime.accountScopeDigest');
|
|
60
|
+
return { target: item.target, kind: text(item.kind, 'runtime.kind', RUNTIME_KIND), version: text(item.version, 'runtime.version', SEMVER), accountScopeDigest };
|
|
61
|
+
}
|
|
62
|
+
function capabilities(value) {
|
|
63
|
+
const item = object(value, 'capabilities');
|
|
64
|
+
exact(item, CAPABILITY_KEYS, 'capabilities');
|
|
65
|
+
for (const key of CAPABILITY_KEYS)
|
|
66
|
+
if (!CAPABILITY_VALUES.has(item[key]))
|
|
67
|
+
throw new Error(`capabilities.${key} is invalid`);
|
|
68
|
+
return item;
|
|
69
|
+
}
|
|
70
|
+
function canonical(value) { if (value === null || typeof value !== 'object')
|
|
71
|
+
return JSON.stringify(value); if (Array.isArray(value))
|
|
72
|
+
return `[${value.map(canonical).join(',')}]`; const item = value; return `{${Object.keys(item).sort().map(key => `${JSON.stringify(key)}:${canonical(item[key])}`).join(',')}}`; }
|
|
73
|
+
function capabilityReceiptDigest(receipt) { return (0, crypto_1.createHash)('sha256').update(canonical(validateCapabilityReceipt(receipt)), 'utf8').digest('hex'); }
|
|
74
|
+
function validateCapabilityReceipt(value) {
|
|
75
|
+
const item = object(value, 'capability receipt');
|
|
76
|
+
exact(item, ['schema', 'runtime', 'recordedAt', 'expiresAt', 'capabilities', 'availableSelections', 'runtimeDefaultSelection', 'evidence', 'approval'].filter(key => item[key] !== undefined), 'capability receipt');
|
|
77
|
+
if (item.schema !== 'routing-capabilities/v1')
|
|
78
|
+
throw new Error('capability receipt schema is invalid');
|
|
79
|
+
const runtime = validateRuntimeKey(item.runtime);
|
|
80
|
+
const recordedAt = timestamp(item.recordedAt, 'recordedAt');
|
|
81
|
+
const expiresAt = timestamp(item.expiresAt, 'expiresAt');
|
|
82
|
+
const elapsed = Date.parse(expiresAt) - Date.parse(recordedAt);
|
|
83
|
+
if (elapsed < 0 || elapsed > 24 * 60 * 60 * 1000)
|
|
84
|
+
throw new Error('capability receipt expiry must be within 24 hours');
|
|
85
|
+
const caps = capabilities(item.capabilities);
|
|
86
|
+
if (!Array.isArray(item.availableSelections) || item.availableSelections.length === 0 || item.availableSelections.length > 64)
|
|
87
|
+
throw new Error('availableSelections is invalid');
|
|
88
|
+
const availableSelections = item.availableSelections.map((entry, index) => selection(entry, `availableSelections[${index}]`));
|
|
89
|
+
if (new Set(availableSelections.map(canonical)).size !== availableSelections.length)
|
|
90
|
+
throw new Error('availableSelections contains duplicates');
|
|
91
|
+
const runtimeDefaultSelection = item.runtimeDefaultSelection === undefined ? undefined : selection(item.runtimeDefaultSelection, 'runtimeDefaultSelection');
|
|
92
|
+
if (runtimeDefaultSelection && !availableSelections.some(entry => sameSelection(entry, runtimeDefaultSelection)))
|
|
93
|
+
throw new Error('runtimeDefaultSelection must be available');
|
|
94
|
+
if (!Array.isArray(item.evidence) || item.evidence.length > 64)
|
|
95
|
+
throw new Error('evidence is invalid');
|
|
96
|
+
const evidence = item.evidence.map((entry, index) => { const proof = object(entry, `evidence[${index}]`); exact(proof, ['capability', 'kind', 'receiptDigest'], `evidence[${index}]`); if (!CAPABILITY_KEYS.includes(proof.capability) || !['native-control', 'native-dispatch', 'unsupported'].includes(proof.kind))
|
|
97
|
+
throw new Error(`evidence[${index}] is invalid`); const receiptDigest = text(proof.receiptDigest, `evidence[${index}].receiptDigest`); (0, validate_1.assertDigest)(receiptDigest, `evidence[${index}].receiptDigest`); return { capability: proof.capability, kind: proof.kind, receiptDigest }; });
|
|
98
|
+
for (const key of CAPABILITY_KEYS)
|
|
99
|
+
if (caps[key] === 'supported' && !evidence.some(proof => proof.capability === key && (proof.kind === 'native-control' || proof.kind === 'native-dispatch')))
|
|
100
|
+
throw new Error(`supported capability ${key} lacks native evidence`);
|
|
101
|
+
const approval = object(item.approval, 'approval');
|
|
102
|
+
exact(approval, ['approvalId', 'snapshotDigest'], 'approval');
|
|
103
|
+
const approvalId = text(approval.approvalId, 'approval.approvalId');
|
|
104
|
+
const snapshotDigest = text(approval.snapshotDigest, 'approval.snapshotDigest');
|
|
105
|
+
(0, validate_1.assertDigest)(snapshotDigest, 'approval.snapshotDigest');
|
|
106
|
+
return { schema: 'routing-capabilities/v1', runtime, recordedAt, expiresAt, capabilities: caps, availableSelections, ...(runtimeDefaultSelection ? { runtimeDefaultSelection } : {}), evidence, approval: { approvalId, snapshotDigest } };
|
|
107
|
+
}
|
|
108
|
+
function receiptIsCurrent(receipt, runtime, now) { return receipt.runtime.target === runtime.target && receipt.runtime.kind === runtime.kind && receipt.runtime.version === runtime.version && receipt.runtime.accountScopeDigest === runtime.accountScopeDigest && Number.isFinite(now.getTime()) && Date.parse(receipt.recordedAt) <= now.getTime() && now.getTime() <= Date.parse(receipt.expiresAt); }
|
|
109
|
+
function routingDiagnostic(code, message) { return { code, message }; }
|
|
110
|
+
function capabilityReceiptPath(runtime) { const valid = validateRuntimeKey(runtime); return path_1.default.join((0, paths_1.awmHome)(), 'routing-capabilities', valid.target, `${valid.kind}.json`); }
|
|
111
|
+
function safeParents(file) { for (let dir = path_1.default.dirname(path_1.default.resolve(file));; dir = path_1.default.dirname(dir)) {
|
|
112
|
+
try {
|
|
113
|
+
const stat = fs_1.default.lstatSync(dir);
|
|
114
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
115
|
+
throw new Error(`unsafe capability parent: ${dir}`);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code !== 'ENOENT')
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
if (dir === path_1.default.dirname(dir))
|
|
122
|
+
return;
|
|
123
|
+
} }
|
|
124
|
+
function readBytes(file) { safeParents(file); try {
|
|
125
|
+
const stat = fs_1.default.lstatSync(file, { bigint: true });
|
|
126
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > BigInt(256 * 1024))
|
|
127
|
+
throw new Error('capability receipt is unsafe or exceeds 256 KiB');
|
|
128
|
+
return fs_1.default.readFileSync(file);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error.code === 'ENOENT')
|
|
132
|
+
return null;
|
|
133
|
+
throw error;
|
|
134
|
+
} }
|
|
135
|
+
function parseReceipt(bytes) { return validateCapabilityReceipt((0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(bytes))); }
|
|
136
|
+
function readCapabilities(runtime, now) {
|
|
137
|
+
try {
|
|
138
|
+
const valid = validateRuntimeKey(runtime);
|
|
139
|
+
const bytes = readBytes(capabilityReceiptPath(valid));
|
|
140
|
+
if (!bytes)
|
|
141
|
+
return { state: 'absent' };
|
|
142
|
+
const receipt = parseReceipt(bytes);
|
|
143
|
+
if (!receiptIsCurrent(receipt, valid, now))
|
|
144
|
+
return { state: 'stale', reason: 'receipt runtime does not match or attestation expired' };
|
|
145
|
+
return { state: 'current', receipt, digest: capabilityReceiptDigest(receipt) };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return { state: 'invalid', reason: error.message.slice(0, 4096) };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function ensureParents(file) { const chain = []; for (let dir = path_1.default.dirname(path_1.default.resolve(file));; dir = path_1.default.dirname(dir)) {
|
|
152
|
+
chain.unshift(dir);
|
|
153
|
+
if (dir === path_1.default.dirname(dir))
|
|
154
|
+
break;
|
|
155
|
+
} for (const dir of chain) {
|
|
156
|
+
try {
|
|
157
|
+
const stat = fs_1.default.lstatSync(dir);
|
|
158
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
159
|
+
throw new Error(`unsafe capability parent: ${dir}`);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error.code !== 'ENOENT')
|
|
163
|
+
throw error;
|
|
164
|
+
fs_1.default.mkdirSync(dir, { mode: 0o700 });
|
|
165
|
+
}
|
|
166
|
+
} }
|
|
167
|
+
function approveCapabilities(input) {
|
|
168
|
+
if (!input || typeof input !== 'object' || typeof input.file !== 'string' || typeof input.cwd !== 'string')
|
|
169
|
+
throw new Error('capability approval input is required');
|
|
170
|
+
(0, validate_1.assertDigest)(input.expectedDigest, 'expectedDigest');
|
|
171
|
+
if (input.replaceDigest !== undefined)
|
|
172
|
+
(0, validate_1.assertDigest)(input.replaceDigest, 'replaceDigest');
|
|
173
|
+
const now = input.now ?? new Date();
|
|
174
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
175
|
+
throw new Error('capability approval requires a finite clock');
|
|
176
|
+
const candidate = readBytes(path_1.default.resolve(input.cwd, input.file));
|
|
177
|
+
if (!candidate)
|
|
178
|
+
throw new Error('capability candidate is absent');
|
|
179
|
+
const receipt = parseReceipt(candidate);
|
|
180
|
+
if (!receiptIsCurrent(receipt, receipt.runtime, now))
|
|
181
|
+
throw new Error('capability receipt is future or stale');
|
|
182
|
+
const digest = capabilityReceiptDigest(receipt);
|
|
183
|
+
if (digest !== input.expectedDigest)
|
|
184
|
+
throw new Error('capability receipt digest does not match expectedDigest');
|
|
185
|
+
const target = capabilityReceiptPath(receipt.runtime);
|
|
186
|
+
ensureParents(target);
|
|
187
|
+
const root = (0, paths_1.awmHome)();
|
|
188
|
+
const destination = path_1.default.relative(root, target).split(path_1.default.sep).join('/');
|
|
189
|
+
native_bridge_1.secureFs.withProjectLease(root, () => { const current = readBytes(target); if (!current) {
|
|
190
|
+
if (input.replaceDigest)
|
|
191
|
+
throw new Error('replacement requires existing capability receipt');
|
|
192
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`), { mode: 'create', createParents: true });
|
|
193
|
+
return;
|
|
194
|
+
} const observed = native_bridge_1.secureFs.readRegularFile(target, 256 * 1024); const prior = parseReceipt(observed.bytes); if (!input.replaceDigest || capabilityReceiptDigest(prior) !== input.replaceDigest)
|
|
195
|
+
throw new Error('capability receipt replacement predecessor mismatch'); native_bridge_1.secureFs.writeProjectTransaction(root, destination, Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`), { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity }); });
|
|
196
|
+
return receipt;
|
|
197
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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.reserveRoutingAttempt = reserveRoutingAttempt;
|
|
7
|
+
exports.observeRoutingAttempt = observeRoutingAttempt;
|
|
8
|
+
exports.resolveLineageEscalation = resolveLineageEscalation;
|
|
9
|
+
exports.routingReport = routingReport;
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const types_1 = require("../journal/types");
|
|
12
|
+
function assertTimestamp(value) { if (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))
|
|
13
|
+
throw new Error('routing timestamp is invalid'); }
|
|
14
|
+
function canonical(value) { if (Array.isArray(value))
|
|
15
|
+
return `[${value.map(canonical).join(',')}]`; if (value !== null && typeof value === 'object') {
|
|
16
|
+
const record = value;
|
|
17
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
|
|
18
|
+
} return JSON.stringify(value); }
|
|
19
|
+
function digest(value) { return crypto_1.default.createHash('sha256').update(canonical(value)).digest('hex'); }
|
|
20
|
+
function clone(state) { const next = structuredClone(state); if (!Array.isArray(next.routingAttempts))
|
|
21
|
+
next.routingAttempts = []; return next; }
|
|
22
|
+
function reserveRoutingAttempt(state, input, now) {
|
|
23
|
+
assertTimestamp(now);
|
|
24
|
+
if (!(0, types_1.isWellFormedState)(state) || !input || typeof input.obligationId !== 'string' || input.obligationId.length === 0 || input.obligationId.length > 128 || typeof input.lineageId !== 'string' || input.lineageId.length === 0 || input.lineageId.length > 128 || !(0, types_1.isRoutingEnvelope)(input.envelope) || !/^[a-f0-9]{64}$/.test(input.fingerprint))
|
|
25
|
+
throw new Error('routing reservation is invalid');
|
|
26
|
+
const next = clone(state);
|
|
27
|
+
const envelopeDigest = digest(input.envelope);
|
|
28
|
+
const existing = next.routingAttempts.find(item => item.lineageId === input.lineageId && item.obligationId === input.obligationId && item.envelopeDigest === envelopeDigest && item.fingerprint === input.fingerprint && ['reserved', 'active'].includes(item.state));
|
|
29
|
+
if (existing)
|
|
30
|
+
return { state: next, attemptId: existing.id };
|
|
31
|
+
if (input.envelope.role === 'implementer' && input.envelope.sliceId === undefined)
|
|
32
|
+
throw new Error('routing implementer requires a slice');
|
|
33
|
+
next.implementationLineages ??= [];
|
|
34
|
+
const priorLineage = next.implementationLineages.find(lineage => lineage.id === input.lineageId);
|
|
35
|
+
if (input.envelope.role === 'implementer' && priorLineage === undefined)
|
|
36
|
+
next.implementationLineages.push({ id: input.lineageId, obligationId: input.obligationId, sliceId: input.envelope.sliceId, planDigest: input.envelope.planDigest, executionDigest: input.envelope.executionDigest, initialProfile: input.envelope.requestedProfile, initialEffort: input.envelope.resolved.effort.kind === 'explicit' && input.envelope.resolved.effort.value === 'high' ? 'high' : 'medium', attempts: 0 });
|
|
37
|
+
if (priorLineage !== undefined && (priorLineage.obligationId !== input.obligationId || priorLineage.planDigest !== input.envelope.planDigest || priorLineage.executionDigest !== input.envelope.executionDigest || priorLineage.sliceId !== input.envelope.sliceId || priorLineage.initialProfile !== input.envelope.requestedProfile))
|
|
38
|
+
throw new Error('routing lineage binding mismatch');
|
|
39
|
+
const attempts = next.routingAttempts.filter(item => item.lineageId === input.lineageId && item.envelope.role === 'implementer');
|
|
40
|
+
if (input.envelope.role === 'implementer' && attempts.length >= 3)
|
|
41
|
+
throw new Error('routing implementation budget exhausted');
|
|
42
|
+
if (input.envelope.role === 'implementer') {
|
|
43
|
+
const initial = input.envelope.requestedProfile;
|
|
44
|
+
if (initial === 'full')
|
|
45
|
+
throw new Error('routing implementer profile is invalid');
|
|
46
|
+
const expected = resolveLineageEscalation(next, input.lineageId, initial);
|
|
47
|
+
const actualEffort = input.envelope.resolved.effort.kind === 'explicit' ? input.envelope.resolved.effort.value : 'runtime-default';
|
|
48
|
+
if (input.envelope.effectiveProfile !== expected.profile || actualEffort !== expected.effort)
|
|
49
|
+
throw new Error('routing effective escalation mismatch');
|
|
50
|
+
}
|
|
51
|
+
const attempt = attempts.length + 1;
|
|
52
|
+
const id = `route-${crypto_1.default.createHash('sha256').update(`${input.lineageId}\0${attempt}\0${envelopeDigest}\0${input.fingerprint}`).digest('hex').slice(0, 24)}`;
|
|
53
|
+
next.routingAttempts.push({ schema: 'routing-attempt/v1', id, obligationId: input.obligationId, lineageId: input.lineageId, attempt, envelope: structuredClone(input.envelope), envelopeDigest, fingerprint: input.fingerprint, state: 'reserved' });
|
|
54
|
+
const lineage = next.implementationLineages.find(candidate => candidate.id === input.lineageId);
|
|
55
|
+
if (lineage !== undefined)
|
|
56
|
+
lineage.attempts = attempt;
|
|
57
|
+
return { state: next, attemptId: id };
|
|
58
|
+
}
|
|
59
|
+
function observeRoutingAttempt(state, input, now) {
|
|
60
|
+
assertTimestamp(now);
|
|
61
|
+
if (!(0, types_1.isWellFormedState)(state) || !input || typeof input.attemptId !== 'string' || input.attemptId.length === 0 || typeof input.nativeAgentId !== 'string' || input.nativeAgentId.length === 0 || input.nativeAgentId.length > 128 || (input.observed !== undefined && !(0, types_1.isRoutingSelection)(input.observed)) || (input.unavailableReason !== undefined && (typeof input.unavailableReason !== 'string' || input.unavailableReason.length === 0 || input.unavailableReason.length > 128)))
|
|
62
|
+
throw new Error('routing observation is invalid');
|
|
63
|
+
const next = clone(state);
|
|
64
|
+
const attempt = next.routingAttempts.find(item => item.id === input.attemptId);
|
|
65
|
+
if (!attempt)
|
|
66
|
+
throw new Error('routing attempt is unknown');
|
|
67
|
+
if (attempt.state === 'active') {
|
|
68
|
+
if (attempt.nativeAgentId === input.nativeAgentId && canonical(attempt.observed) === canonical(input.observed))
|
|
69
|
+
return next;
|
|
70
|
+
throw new Error('routing observation is immutable');
|
|
71
|
+
}
|
|
72
|
+
if (attempt.state !== 'reserved')
|
|
73
|
+
throw new Error('routing attempt cannot be observed');
|
|
74
|
+
attempt.state = 'active';
|
|
75
|
+
attempt.nativeAgentId = input.nativeAgentId;
|
|
76
|
+
if (input.observed)
|
|
77
|
+
attempt.observed = structuredClone(input.observed);
|
|
78
|
+
if (input.unavailableReason)
|
|
79
|
+
attempt.reasonCode = input.unavailableReason;
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
/** Computes, but never persists, the only permitted implementation retry.
|
|
83
|
+
* A live/unknown attempt is custody, not an escalation opportunity. Judgment
|
|
84
|
+
* high is an effort escalation of the same approved judgment capability. */
|
|
85
|
+
function resolveLineageEscalation(state, lineageId, initial) {
|
|
86
|
+
if (!(0, types_1.isWellFormedState)(state) || typeof lineageId !== 'string' || lineageId.length === 0)
|
|
87
|
+
throw new Error('routing lineage is invalid');
|
|
88
|
+
const attempts = (state.routingAttempts ?? []).filter((attempt) => attempt.lineageId === lineageId && attempt.envelope.role === 'implementer').sort((left, right) => left.attempt - right.attempt);
|
|
89
|
+
if (attempts.some((attempt) => ['reserved', 'active', 'unknown'].includes(attempt.state)))
|
|
90
|
+
throw new Error('routing lineage has a live or unknown attempt');
|
|
91
|
+
if (attempts.length === 0)
|
|
92
|
+
return { profile: initial, effort: 'medium' };
|
|
93
|
+
const last = attempts[attempts.length - 1];
|
|
94
|
+
if (last.verdict !== 'fail')
|
|
95
|
+
throw new Error('routing lineage advances only after a terminal failed verdict');
|
|
96
|
+
if (last.envelope.effectiveProfile === 'mechanical')
|
|
97
|
+
return { profile: 'integration', effort: 'medium' };
|
|
98
|
+
if (last.envelope.effectiveProfile === 'integration')
|
|
99
|
+
return { profile: 'judgment', effort: 'medium' };
|
|
100
|
+
if (last.envelope.effectiveProfile === 'judgment' && last.envelope.resolved.effort.kind === 'explicit' && last.envelope.resolved.effort.value === 'medium')
|
|
101
|
+
return { profile: 'judgment', effort: 'high' };
|
|
102
|
+
throw new Error('routing lineage escalation exhausted');
|
|
103
|
+
}
|
|
104
|
+
/** Read-only operational summary. Deliberately excludes envelopes, prompts and
|
|
105
|
+
* native identities: this is safe to expose in a status command. */
|
|
106
|
+
function routingReport(state) {
|
|
107
|
+
const attempts = state.routingAttempts ?? [];
|
|
108
|
+
const plannedByRole = {};
|
|
109
|
+
const actualByRole = {};
|
|
110
|
+
const byState = {};
|
|
111
|
+
const unavailable = {};
|
|
112
|
+
const verdicts = {};
|
|
113
|
+
for (const attempt of attempts) {
|
|
114
|
+
plannedByRole[attempt.envelope.role] = (plannedByRole[attempt.envelope.role] ?? 0) + 1;
|
|
115
|
+
if (attempt.nativeAgentId !== undefined)
|
|
116
|
+
actualByRole[attempt.envelope.role] = (actualByRole[attempt.envelope.role] ?? 0) + 1;
|
|
117
|
+
byState[attempt.state] = (byState[attempt.state] ?? 0) + 1;
|
|
118
|
+
for (const reason of attempt.envelope.unavailableEvidence)
|
|
119
|
+
unavailable[reason] = (unavailable[reason] ?? 0) + 1;
|
|
120
|
+
if (attempt.verdict !== undefined)
|
|
121
|
+
verdicts[attempt.verdict] = (verdicts[attempt.verdict] ?? 0) + 1;
|
|
122
|
+
}
|
|
123
|
+
return { schema: 'routing-report/v1', attempts: attempts.length, plannedByRole, actualByRole, byState, retries: attempts.filter((attempt) => attempt.attempt > 1).length, fallbacks: attempts.filter((attempt) => attempt.envelope.outcome === 'degraded').length, unavailable, verdicts, administrativeRepairs: attempts.filter((attempt) => attempt.administrativeRepair === true).length };
|
|
124
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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.userPolicyPath = userPolicyPath;
|
|
7
|
+
exports.projectPolicyPath = projectPolicyPath;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const paths_1 = require("../paths");
|
|
10
|
+
function userPolicyPath() { return path_1.default.join((0, paths_1.awmHome)(), 'model-policy.json'); }
|
|
11
|
+
function projectPolicyPath(cwd) { if (typeof cwd !== 'string' || cwd.length === 0)
|
|
12
|
+
throw new Error('cwd must be a non-empty path'); return path_1.default.join(path_1.default.resolve(cwd), '.awm', 'model-policy.json'); }
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveDispatch = resolveDispatch;
|
|
4
|
+
exports.resolveV1Dispatch = resolveV1Dispatch;
|
|
5
|
+
exports.resolveSelection = resolveSelection;
|
|
6
|
+
const capabilities_1 = require("./capabilities");
|
|
7
|
+
const validate_1 = require("./validate");
|
|
8
|
+
const validate_2 = require("../plan/validate");
|
|
9
|
+
/** Resolve only from the immutable validator report; a reopened plan may never
|
|
10
|
+
* provide a routing profile after validation. */
|
|
11
|
+
function resolveDispatch(input) {
|
|
12
|
+
if (!input || typeof input !== 'object' || !input.plan)
|
|
13
|
+
throw new Error('resolveDispatch requires a plan');
|
|
14
|
+
(0, validate_2.assertVerifiedValidPlanReport)(input.plan);
|
|
15
|
+
const local = ['implementer', 'specification-reviewer', 'code-quality-reviewer'].includes(input.role);
|
|
16
|
+
if (input.plan.schema === 'compact-slices/v1') {
|
|
17
|
+
if (input.sliceId !== undefined && !input.plan.manifest.slices.some(slice => slice.id === input.sliceId))
|
|
18
|
+
throw new Error('routing slice does not exist in the validated plan');
|
|
19
|
+
if (local && typeof input.sliceId !== 'string')
|
|
20
|
+
throw new Error('local routing requires a slice');
|
|
21
|
+
return resolveV1Dispatch(input);
|
|
22
|
+
}
|
|
23
|
+
if (input.role !== 'implementer') {
|
|
24
|
+
if (local && (typeof input.sliceId !== 'string' || !input.plan.manifest.slices.some(slice => slice.id === input.sliceId)))
|
|
25
|
+
throw new Error('local routing requires a valid slice');
|
|
26
|
+
if (!local && input.sliceId !== undefined)
|
|
27
|
+
throw new Error('slice is only valid for local routing');
|
|
28
|
+
return resolveSelection({ ...input, requestedProfile: 'full' });
|
|
29
|
+
}
|
|
30
|
+
if (typeof input.sliceId !== 'string' || input.sliceId.length === 0)
|
|
31
|
+
throw new Error('implementer routing requires a slice');
|
|
32
|
+
const slice = input.plan.manifest.slices.find(candidate => candidate.id === input.sliceId);
|
|
33
|
+
if (!slice)
|
|
34
|
+
throw new Error('routing slice does not exist in the validated plan');
|
|
35
|
+
return resolveSelection({ ...input, requestedProfile: slice.implementerProfile });
|
|
36
|
+
}
|
|
37
|
+
function resolveV1Dispatch(input) {
|
|
38
|
+
if (!input.plan || input.plan.schema !== 'compact-slices/v1')
|
|
39
|
+
throw new Error('resolveV1Dispatch requires an authenticated v1 plan report');
|
|
40
|
+
(0, validate_2.assertVerifiedValidPlanReport)(input.plan);
|
|
41
|
+
if (!input.optInV1)
|
|
42
|
+
return { state: 'not-required', reason: 'v1-without-opt-in' };
|
|
43
|
+
return resolveSelection({ ...input, requestedProfile: 'full' });
|
|
44
|
+
}
|
|
45
|
+
function equal(left, right) { return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value); }
|
|
46
|
+
function blocked(code, message) { return { state: 'blocked', diagnostics: [(0, capabilities_1.routingDiagnostic)(code, message)] }; }
|
|
47
|
+
function resolveSelection(input) {
|
|
48
|
+
if (!input || typeof input !== 'object' || !input.runtime || !(input.now instanceof Date) || !Number.isFinite(input.now.getTime()))
|
|
49
|
+
throw new Error('resolveSelection requires a runtime and finite clock');
|
|
50
|
+
if (!['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'].includes(input.role))
|
|
51
|
+
throw new Error('resolveSelection role is invalid');
|
|
52
|
+
if (!['mechanical', 'integration', 'judgment', 'full'].includes(input.requestedProfile))
|
|
53
|
+
throw new Error('resolveSelection requestedProfile is invalid');
|
|
54
|
+
if (!input.policy)
|
|
55
|
+
return blocked('ROUTING_POLICY_ABSENT', 'A digest-bound approved routing policy is required.');
|
|
56
|
+
if (!input.capabilities)
|
|
57
|
+
return blocked('ROUTING_CAPABILITY_ABSENT', 'A current capability receipt is required.');
|
|
58
|
+
let policy;
|
|
59
|
+
let capabilities;
|
|
60
|
+
try {
|
|
61
|
+
policy = (0, validate_1.validateApprovedPolicy)(input.policy);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return blocked('ROUTING_POLICY_INVALID', 'Approved routing policy is corrupt.');
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
capabilities = (0, capabilities_1.validateCapabilityReceipt)(input.capabilities);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return blocked('ROUTING_CAPABILITY_INVALID', 'Capability receipt is corrupt.');
|
|
71
|
+
}
|
|
72
|
+
if (!(0, capabilities_1.receiptIsCurrent)(capabilities, input.runtime, input.now))
|
|
73
|
+
return blocked('ROUTING_CAPABILITY_RUNTIME_MISMATCH', 'Capability receipt does not match the current runtime or is stale.');
|
|
74
|
+
const mapping = policy.content.mappings.find(row => row.target === input.runtime.target && row.runtimeKind === input.runtime.kind);
|
|
75
|
+
if (!mapping)
|
|
76
|
+
return blocked('ROUTING_POLICY_MAPPING_ABSENT', 'Approved policy has no exact target/runtime mapping.');
|
|
77
|
+
const effectiveProfile = input.role === 'implementer' ? input.requestedProfile : 'full';
|
|
78
|
+
const wanted = effectiveProfile === 'full' ? mapping.fullCapability : mapping.profiles[effectiveProfile];
|
|
79
|
+
let selection = wanted;
|
|
80
|
+
const unavailableEvidence = [];
|
|
81
|
+
let outcome = 'native';
|
|
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;
|
|
89
|
+
if (state === 'supported')
|
|
90
|
+
return undefined;
|
|
91
|
+
if (state === 'unverified')
|
|
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.');
|
|
95
|
+
selection = mapping.fullCapability;
|
|
96
|
+
outcome = 'degraded';
|
|
97
|
+
unavailableEvidence.push('modelOverride');
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
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();
|
|
124
|
+
if (model)
|
|
125
|
+
return model;
|
|
126
|
+
if (selection.effort.kind === 'explicit') {
|
|
127
|
+
const effort = degradeEffort();
|
|
128
|
+
if (effort)
|
|
129
|
+
return effort;
|
|
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.');
|
|
148
|
+
if (capabilities.capabilities.observedModelEvidence !== 'supported') {
|
|
149
|
+
if (!mapping.degradation.allowMissingObservedIdentity)
|
|
150
|
+
return blocked('ROUTING_OBSERVED_IDENTITY_UNAVAILABLE', 'Observed model identity is required and unavailable.');
|
|
151
|
+
outcome = 'degraded';
|
|
152
|
+
unavailableEvidence.push('observedModelEvidence');
|
|
153
|
+
}
|
|
154
|
+
return { state: 'resolved', selection, effectiveProfile, outcome, policyDigest: policy.contentDigest, capabilityDigest: (0, capabilities_1.capabilityReceiptDigest)(capabilities), unavailableEvidence };
|
|
155
|
+
}
|