agentic-workflow-manager 9.8.0 → 9.9.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 +73 -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/journal.js +124 -0
- package/dist/src/core/model-policy/paths.js +12 -0
- package/dist/src/core/model-policy/resolve.js +116 -0
- package/dist/src/core/model-policy/store.js +182 -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 +57 -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/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 +111 -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,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,116 @@
|
|
|
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
|
+
const canDegrade = (capability, allowed, code) => {
|
|
83
|
+
const state = capabilities.capabilities[capability];
|
|
84
|
+
if (state === 'supported')
|
|
85
|
+
return undefined;
|
|
86
|
+
if (state === 'unverified')
|
|
87
|
+
return blocked(code, `${capability} is unverified.`);
|
|
88
|
+
if (!allowed || !capabilities.runtimeDefaultSelection || !equal(capabilities.runtimeDefaultSelection, mapping.fullCapability))
|
|
89
|
+
return blocked(code, `${capability} is unavailable and no matching approved full default is attested.`);
|
|
90
|
+
selection = mapping.fullCapability;
|
|
91
|
+
outcome = 'degraded';
|
|
92
|
+
unavailableEvidence.push(capability);
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
const model = canDegrade('modelOverride', mapping.degradation.allowMissingModelOverride, 'ROUTING_MODEL_OVERRIDE_UNAVAILABLE');
|
|
96
|
+
if (model)
|
|
97
|
+
return model;
|
|
98
|
+
if (!capabilities.availableSelections.some(candidate => equal(candidate, selection)))
|
|
99
|
+
return blocked('ROUTING_SELECTION_UNAVAILABLE', 'Approved selection is not currently available; no substitute is allowed.');
|
|
100
|
+
if (selection.effort.kind === 'runtime-default') {
|
|
101
|
+
if (!capabilities.runtimeDefaultSelection || !equal(capabilities.runtimeDefaultSelection, selection))
|
|
102
|
+
return blocked('ROUTING_SELECTION_UNAVAILABLE', 'Runtime-default selection is not explicitly attested.');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const effort = canDegrade('effortOverride', mapping.degradation.allowMissingEffortOverride, 'ROUTING_EFFORT_OVERRIDE_UNAVAILABLE');
|
|
106
|
+
if (effort)
|
|
107
|
+
return effort;
|
|
108
|
+
}
|
|
109
|
+
if (capabilities.capabilities.observedModelEvidence !== 'supported') {
|
|
110
|
+
if (!mapping.degradation.allowMissingObservedIdentity)
|
|
111
|
+
return blocked('ROUTING_OBSERVED_IDENTITY_UNAVAILABLE', 'Observed model identity is required and unavailable.');
|
|
112
|
+
outcome = 'degraded';
|
|
113
|
+
unavailableEvidence.push('observedModelEvidence');
|
|
114
|
+
}
|
|
115
|
+
return { state: 'resolved', selection, effectiveProfile, outcome, policyDigest: policy.contentDigest, capabilityDigest: (0, capabilities_1.capabilityReceiptDigest)(capabilities), unavailableEvidence };
|
|
116
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
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.readEffectivePolicy = readEffectivePolicy;
|
|
7
|
+
exports.approvePolicyWithBoundary = approvePolicyWithBoundary;
|
|
8
|
+
exports.approvePolicy = approvePolicy;
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const json_1 = require("../plan/json");
|
|
13
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
14
|
+
const canonical_1 = require("./canonical");
|
|
15
|
+
const paths_1 = require("./paths");
|
|
16
|
+
const validate_1 = require("./validate");
|
|
17
|
+
function assertPath(value, label) {
|
|
18
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 4096 || /[\u0000-\u001f\u007f-\u009f]/.test(value))
|
|
19
|
+
throw new Error(`${label} must be a bounded path without control characters`);
|
|
20
|
+
}
|
|
21
|
+
function ancestors(file) { const absolute = path_1.default.resolve(file); const result = []; for (let current = path_1.default.dirname(absolute);; current = path_1.default.dirname(current)) {
|
|
22
|
+
result.unshift(current);
|
|
23
|
+
if (current === path_1.default.dirname(current))
|
|
24
|
+
return result;
|
|
25
|
+
} }
|
|
26
|
+
function assertSafeParents(file) {
|
|
27
|
+
for (const parent of ancestors(file)) {
|
|
28
|
+
try {
|
|
29
|
+
const stat = fs_1.default.lstatSync(parent, { bigint: true });
|
|
30
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
31
|
+
throw new Error(`unsafe policy parent: ${parent}`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code !== 'ENOENT')
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function assertRegularOrAbsent(file) { try {
|
|
40
|
+
const stat = fs_1.default.lstatSync(file, { bigint: true });
|
|
41
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
42
|
+
throw new Error(`policy file rejects symlink or non-regular file: ${file}`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code !== 'ENOENT')
|
|
46
|
+
throw error;
|
|
47
|
+
} }
|
|
48
|
+
function readBounded(file) {
|
|
49
|
+
assertSafeParents(file);
|
|
50
|
+
let inspected;
|
|
51
|
+
try {
|
|
52
|
+
inspected = fs_1.default.lstatSync(file, { bigint: true });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === 'ENOENT')
|
|
56
|
+
return null;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (inspected.isSymbolicLink() || !inspected.isFile() || inspected.size < 0n || inspected.size > BigInt(validate_1.MAX_POLICY_BYTES))
|
|
60
|
+
throw new Error('policy file is unsafe or exceeds 256 KiB');
|
|
61
|
+
const fd = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | (fs_1.default.constants.O_NOFOLLOW ?? 0));
|
|
62
|
+
try {
|
|
63
|
+
const opened = fs_1.default.fstatSync(fd, { bigint: true });
|
|
64
|
+
if (!opened.isFile() || opened.dev !== inspected.dev || opened.ino !== inspected.ino || opened.size !== inspected.size)
|
|
65
|
+
throw new Error('policy file identity changed');
|
|
66
|
+
assertSafeParents(file);
|
|
67
|
+
return fs_1.default.readFileSync(fd, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
fs_1.default.closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function parseApproved(raw) { const policy = (0, validate_1.validateApprovedPolicy)((0, json_1.parseJsonNoDuplicate)(raw)); if ((0, canonical_1.canonicalPolicyDigest)(policy.content) !== policy.contentDigest)
|
|
74
|
+
throw new Error('approved policy content digest mismatch'); return policy; }
|
|
75
|
+
function policyTarget(scope, cwd) { if (scope !== 'user' && scope !== 'project')
|
|
76
|
+
throw new Error('scope must be user or project'); assertPath(cwd, 'cwd'); return scope === 'user' ? (0, paths_1.userPolicyPath)() : (0, paths_1.projectPolicyPath)(cwd); }
|
|
77
|
+
function createDirectories(file) {
|
|
78
|
+
const chain = ancestors(file);
|
|
79
|
+
for (const directory of chain) {
|
|
80
|
+
try {
|
|
81
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
82
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
83
|
+
throw new Error(`unsafe policy parent: ${directory}`);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== 'ENOENT')
|
|
87
|
+
throw error;
|
|
88
|
+
fs_1.default.mkdirSync(directory, { mode: 0o700 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function lock(file) { const lockPath = `${file}.lock`; assertRegularOrAbsent(lockPath); let fd; try {
|
|
93
|
+
fd = fs_1.default.openSync(lockPath, 'wx', 0o600);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code === 'EEXIST')
|
|
97
|
+
throw new Error('policy writer lock already exists; explicit recovery required');
|
|
98
|
+
throw error;
|
|
99
|
+
} return () => { try {
|
|
100
|
+
fs_1.default.closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
fs_1.default.rmSync(lockPath, { force: false });
|
|
104
|
+
} }; }
|
|
105
|
+
function readEffectivePolicy(cwd) {
|
|
106
|
+
assertPath(cwd, 'cwd');
|
|
107
|
+
const project = (0, paths_1.projectPolicyPath)(cwd);
|
|
108
|
+
try {
|
|
109
|
+
const raw = readBounded(project);
|
|
110
|
+
if (raw !== null)
|
|
111
|
+
return { state: 'approved', provenance: 'project', policy: parseApproved(raw) };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return { state: 'invalid', provenance: 'project', reason: error.message.slice(0, 4096) };
|
|
115
|
+
}
|
|
116
|
+
const user = (0, paths_1.userPolicyPath)();
|
|
117
|
+
try {
|
|
118
|
+
const raw = readBounded(user);
|
|
119
|
+
if (raw === null)
|
|
120
|
+
return { state: 'absent' };
|
|
121
|
+
return { state: 'approved', provenance: 'user', policy: parseApproved(raw) };
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return { state: 'invalid', provenance: 'user', reason: error.message.slice(0, 4096) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Testable transaction seam; production callers use approvePolicy, which binds secureFs. */
|
|
128
|
+
function approvePolicyWithBoundary(input, boundary) {
|
|
129
|
+
if (!input || typeof input !== 'object')
|
|
130
|
+
throw new Error('approval input is required');
|
|
131
|
+
assertPath(input.file, 'file');
|
|
132
|
+
assertPath(input.cwd, 'cwd');
|
|
133
|
+
(0, validate_1.assertDigest)(input.expectedDigest, 'expectedDigest');
|
|
134
|
+
if (input.replaceDigest !== undefined)
|
|
135
|
+
(0, validate_1.assertDigest)(input.replaceDigest, 'replaceDigest');
|
|
136
|
+
const cwd = path_1.default.resolve(input.cwd);
|
|
137
|
+
const candidateRaw = readBounded(path_1.default.resolve(cwd, input.file));
|
|
138
|
+
if (candidateRaw === null)
|
|
139
|
+
throw new Error('policy candidate is absent');
|
|
140
|
+
const content = (0, validate_1.validatePolicyContent)((0, json_1.parseJsonNoDuplicate)(candidateRaw));
|
|
141
|
+
const digest = (0, canonical_1.canonicalPolicyDigest)(content);
|
|
142
|
+
if (digest !== input.expectedDigest)
|
|
143
|
+
throw new Error('policy content digest does not match expectedDigest');
|
|
144
|
+
const target = policyTarget(input.scope, cwd);
|
|
145
|
+
createDirectories(target);
|
|
146
|
+
assertSafeParents(target);
|
|
147
|
+
assertRegularOrAbsent(target);
|
|
148
|
+
const unlock = lock(target);
|
|
149
|
+
try {
|
|
150
|
+
const existingRaw = readBounded(target);
|
|
151
|
+
const existing = existingRaw === null ? null : parseApproved(existingRaw);
|
|
152
|
+
if (existing === null && input.replaceDigest !== undefined)
|
|
153
|
+
throw new Error('replacement requires an existing predecessor');
|
|
154
|
+
if (existing !== null && input.replaceDigest === undefined)
|
|
155
|
+
throw new Error('existing policy requires replaceDigest');
|
|
156
|
+
if (existing !== null && existing.contentDigest !== input.replaceDigest)
|
|
157
|
+
throw new Error('replacement predecessor digest mismatch');
|
|
158
|
+
const approved = { schema: 'approved-model-policy/v1', content, contentDigest: digest, approval: { approvedAt: new Date().toISOString(), approvalId: crypto_1.default.randomUUID() }, lineage: { previousDigest: existing?.contentDigest ?? null } };
|
|
159
|
+
const serialized = `${JSON.stringify(approved, null, 2)}\n`;
|
|
160
|
+
const root = input.scope === 'project' ? cwd : path_1.default.dirname(target);
|
|
161
|
+
const destination = input.scope === 'project' ? '.awm/model-policy.json' : path_1.default.basename(target);
|
|
162
|
+
boundary.withProjectLease(root, () => {
|
|
163
|
+
if (existing === null) {
|
|
164
|
+
boundary.writeProjectTransaction(root, destination, Buffer.from(serialized, 'utf8'), { mode: 'create', createParents: true });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// A native identity-fenced transaction proves that neither bytes nor
|
|
168
|
+
// file identity changed after the bounded observation. A JS rename
|
|
169
|
+
// cannot make that CAS claim, so it is deliberately not used here.
|
|
170
|
+
const observed = boundary.readRegularFile(target, validate_1.MAX_POLICY_BYTES);
|
|
171
|
+
const current = parseApproved(new TextDecoder('utf-8', { fatal: true }).decode(observed.bytes));
|
|
172
|
+
if (current.contentDigest !== input.replaceDigest)
|
|
173
|
+
throw new Error('replacement predecessor digest changed under writer lock');
|
|
174
|
+
boundary.writeProjectTransaction(root, destination, Buffer.from(serialized, 'utf8'), { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity });
|
|
175
|
+
});
|
|
176
|
+
return approved;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
unlock();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function approvePolicy(input) { return approvePolicyWithBoundary(input, native_bridge_1.secureFs); }
|