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,189 @@
|
|
|
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.readBoundedCandidate = readBoundedCandidate;
|
|
7
|
+
exports.readEffectivePolicy = readEffectivePolicy;
|
|
8
|
+
exports.approvePolicyWithBoundary = approvePolicyWithBoundary;
|
|
9
|
+
exports.approvePolicy = approvePolicy;
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const json_1 = require("../plan/json");
|
|
14
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
15
|
+
const canonical_1 = require("./canonical");
|
|
16
|
+
const paths_1 = require("./paths");
|
|
17
|
+
const validate_1 = require("./validate");
|
|
18
|
+
function assertPath(value, label) {
|
|
19
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 4096 || /[\u0000-\u001f\u007f-\u009f]/.test(value))
|
|
20
|
+
throw new Error(`${label} must be a bounded path without control characters`);
|
|
21
|
+
}
|
|
22
|
+
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)) {
|
|
23
|
+
result.unshift(current);
|
|
24
|
+
if (current === path_1.default.dirname(current))
|
|
25
|
+
return result;
|
|
26
|
+
} }
|
|
27
|
+
function assertSafeParents(file) {
|
|
28
|
+
for (const parent of ancestors(file)) {
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs_1.default.lstatSync(parent, { bigint: true });
|
|
31
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
32
|
+
throw new Error(`unsafe policy parent: ${parent}`);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== 'ENOENT')
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function assertRegularOrAbsent(file) { try {
|
|
41
|
+
const stat = fs_1.default.lstatSync(file, { bigint: true });
|
|
42
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
43
|
+
throw new Error(`policy file rejects symlink or non-regular file: ${file}`);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code !== 'ENOENT')
|
|
47
|
+
throw error;
|
|
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) {
|
|
56
|
+
assertSafeParents(file);
|
|
57
|
+
let inspected;
|
|
58
|
+
try {
|
|
59
|
+
inspected = fs_1.default.lstatSync(file, { bigint: true });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code === 'ENOENT')
|
|
63
|
+
return null;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
if (inspected.isSymbolicLink() || !inspected.isFile() || inspected.size < 0n || inspected.size > BigInt(validate_1.MAX_POLICY_BYTES))
|
|
67
|
+
throw new Error('policy file is unsafe or exceeds 256 KiB');
|
|
68
|
+
const fd = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | (fs_1.default.constants.O_NOFOLLOW ?? 0));
|
|
69
|
+
try {
|
|
70
|
+
const opened = fs_1.default.fstatSync(fd, { bigint: true });
|
|
71
|
+
if (!opened.isFile() || opened.dev !== inspected.dev || opened.ino !== inspected.ino || opened.size !== inspected.size)
|
|
72
|
+
throw new Error('policy file identity changed');
|
|
73
|
+
assertSafeParents(file);
|
|
74
|
+
return fs_1.default.readFileSync(fd, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
fs_1.default.closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parseApproved(raw) { const policy = (0, validate_1.validateApprovedPolicy)((0, json_1.parseJsonNoDuplicate)(raw)); if ((0, canonical_1.canonicalPolicyDigest)(policy.content) !== policy.contentDigest)
|
|
81
|
+
throw new Error('approved policy content digest mismatch'); return policy; }
|
|
82
|
+
function policyTarget(scope, cwd) { if (scope !== 'user' && scope !== 'project')
|
|
83
|
+
throw new Error('scope must be user or project'); assertPath(cwd, 'cwd'); return scope === 'user' ? (0, paths_1.userPolicyPath)() : (0, paths_1.projectPolicyPath)(cwd); }
|
|
84
|
+
function createDirectories(file) {
|
|
85
|
+
const chain = ancestors(file);
|
|
86
|
+
for (const directory of chain) {
|
|
87
|
+
try {
|
|
88
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
89
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
90
|
+
throw new Error(`unsafe policy parent: ${directory}`);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code !== 'ENOENT')
|
|
94
|
+
throw error;
|
|
95
|
+
fs_1.default.mkdirSync(directory, { mode: 0o700 });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function lock(file) { const lockPath = `${file}.lock`; assertRegularOrAbsent(lockPath); let fd; try {
|
|
100
|
+
fd = fs_1.default.openSync(lockPath, 'wx', 0o600);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code === 'EEXIST')
|
|
104
|
+
throw new Error('policy writer lock already exists; explicit recovery required');
|
|
105
|
+
throw error;
|
|
106
|
+
} return () => { try {
|
|
107
|
+
fs_1.default.closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
fs_1.default.rmSync(lockPath, { force: false });
|
|
111
|
+
} }; }
|
|
112
|
+
function readEffectivePolicy(cwd) {
|
|
113
|
+
assertPath(cwd, 'cwd');
|
|
114
|
+
const project = (0, paths_1.projectPolicyPath)(cwd);
|
|
115
|
+
try {
|
|
116
|
+
const raw = readBoundedCandidate(project);
|
|
117
|
+
if (raw !== null)
|
|
118
|
+
return { state: 'approved', provenance: 'project', policy: parseApproved(raw) };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { state: 'invalid', provenance: 'project', reason: error.message.slice(0, 4096) };
|
|
122
|
+
}
|
|
123
|
+
const user = (0, paths_1.userPolicyPath)();
|
|
124
|
+
try {
|
|
125
|
+
const raw = readBoundedCandidate(user);
|
|
126
|
+
if (raw === null)
|
|
127
|
+
return { state: 'absent' };
|
|
128
|
+
return { state: 'approved', provenance: 'user', policy: parseApproved(raw) };
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return { state: 'invalid', provenance: 'user', reason: error.message.slice(0, 4096) };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Testable transaction seam; production callers use approvePolicy, which binds secureFs. */
|
|
135
|
+
function approvePolicyWithBoundary(input, boundary) {
|
|
136
|
+
if (!input || typeof input !== 'object')
|
|
137
|
+
throw new Error('approval input is required');
|
|
138
|
+
assertPath(input.file, 'file');
|
|
139
|
+
assertPath(input.cwd, 'cwd');
|
|
140
|
+
(0, validate_1.assertDigest)(input.expectedDigest, 'expectedDigest');
|
|
141
|
+
if (input.replaceDigest !== undefined)
|
|
142
|
+
(0, validate_1.assertDigest)(input.replaceDigest, 'replaceDigest');
|
|
143
|
+
const cwd = path_1.default.resolve(input.cwd);
|
|
144
|
+
const candidateRaw = readBoundedCandidate(path_1.default.resolve(cwd, input.file));
|
|
145
|
+
if (candidateRaw === null)
|
|
146
|
+
throw new Error('policy candidate is absent');
|
|
147
|
+
const content = (0, validate_1.validatePolicyContent)((0, json_1.parseJsonNoDuplicate)(candidateRaw));
|
|
148
|
+
const digest = (0, canonical_1.canonicalPolicyDigest)(content);
|
|
149
|
+
if (digest !== input.expectedDigest)
|
|
150
|
+
throw new Error('policy content digest does not match expectedDigest');
|
|
151
|
+
const target = policyTarget(input.scope, cwd);
|
|
152
|
+
createDirectories(target);
|
|
153
|
+
assertSafeParents(target);
|
|
154
|
+
assertRegularOrAbsent(target);
|
|
155
|
+
const unlock = lock(target);
|
|
156
|
+
try {
|
|
157
|
+
const existingRaw = readBoundedCandidate(target);
|
|
158
|
+
const existing = existingRaw === null ? null : parseApproved(existingRaw);
|
|
159
|
+
if (existing === null && input.replaceDigest !== undefined)
|
|
160
|
+
throw new Error('replacement requires an existing predecessor');
|
|
161
|
+
if (existing !== null && input.replaceDigest === undefined)
|
|
162
|
+
throw new Error('existing policy requires replaceDigest');
|
|
163
|
+
if (existing !== null && existing.contentDigest !== input.replaceDigest)
|
|
164
|
+
throw new Error('replacement predecessor digest mismatch');
|
|
165
|
+
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 } };
|
|
166
|
+
const serialized = `${JSON.stringify(approved, null, 2)}\n`;
|
|
167
|
+
const root = input.scope === 'project' ? cwd : path_1.default.dirname(target);
|
|
168
|
+
const destination = input.scope === 'project' ? '.awm/model-policy.json' : path_1.default.basename(target);
|
|
169
|
+
boundary.withProjectLease(root, () => {
|
|
170
|
+
if (existing === null) {
|
|
171
|
+
boundary.writeProjectTransaction(root, destination, Buffer.from(serialized, 'utf8'), { mode: 'create', createParents: true });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// A native identity-fenced transaction proves that neither bytes nor
|
|
175
|
+
// file identity changed after the bounded observation. A JS rename
|
|
176
|
+
// cannot make that CAS claim, so it is deliberately not used here.
|
|
177
|
+
const observed = boundary.readRegularFile(target, validate_1.MAX_POLICY_BYTES);
|
|
178
|
+
const current = parseApproved(new TextDecoder('utf-8', { fatal: true }).decode(observed.bytes));
|
|
179
|
+
if (current.contentDigest !== input.replaceDigest)
|
|
180
|
+
throw new Error('replacement predecessor digest changed under writer lock');
|
|
181
|
+
boundary.writeProjectTransaction(root, destination, Buffer.from(serialized, 'utf8'), { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity });
|
|
182
|
+
});
|
|
183
|
+
return approved;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
unlock();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function approvePolicy(input) { return approvePolicyWithBoundary(input, native_bridge_1.secureFs); }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_IDENTIFIER = exports.MAX_MAPPINGS = exports.MAX_POLICY_BYTES = void 0;
|
|
4
|
+
exports.canonicalUtcTimestamp = canonicalUtcTimestamp;
|
|
5
|
+
exports.validatePolicyContent = validatePolicyContent;
|
|
6
|
+
exports.validateApprovedPolicy = validateApprovedPolicy;
|
|
7
|
+
exports.assertDigest = assertDigest;
|
|
8
|
+
const providers_1 = require("../../providers");
|
|
9
|
+
exports.MAX_POLICY_BYTES = 256 * 1024;
|
|
10
|
+
exports.MAX_MAPPINGS = 64;
|
|
11
|
+
exports.MAX_IDENTIFIER = 128;
|
|
12
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
13
|
+
const RUNTIME_KIND = /^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
14
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
15
|
+
const CANONICAL_UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
16
|
+
const profiles = ['mechanical', 'integration', 'judgment'];
|
|
17
|
+
function record(value, label) {
|
|
18
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
19
|
+
throw new Error(`${label} must be an object`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function exact(object, keys, label) {
|
|
23
|
+
const actual = Object.keys(object);
|
|
24
|
+
if (actual.length !== keys.length || actual.some(key => !keys.includes(key)))
|
|
25
|
+
throw new Error(`${label} has unknown or missing fields`);
|
|
26
|
+
}
|
|
27
|
+
function text(value, label, pattern) {
|
|
28
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > exports.MAX_IDENTIFIER || CONTROL.test(value) || (pattern && !pattern.test(value)))
|
|
29
|
+
throw new Error(`${label} is invalid`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function canonicalUtcTimestamp(value, label) {
|
|
33
|
+
if (typeof value !== 'string' || !CANONICAL_UTC_TIMESTAMP.test(value))
|
|
34
|
+
throw new Error(`${label} must be a canonical UTC ISO-8601 timestamp`);
|
|
35
|
+
const parsed = new Date(value);
|
|
36
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value)
|
|
37
|
+
throw new Error(`${label} must be a canonical UTC ISO-8601 timestamp`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function bool(value, label) { if (typeof value !== 'boolean')
|
|
41
|
+
throw new Error(`${label} must be boolean`); return value; }
|
|
42
|
+
function agent(value) { if (typeof value !== 'string' || !providers_1.AGENT_TARGETS.includes(value))
|
|
43
|
+
throw new Error('mapping target is invalid'); return value; }
|
|
44
|
+
function selection(value, label) {
|
|
45
|
+
const item = record(value, label);
|
|
46
|
+
exact(item, ['selector', 'effort'], label);
|
|
47
|
+
const selector = record(item.selector, `${label}.selector`);
|
|
48
|
+
exact(selector, ['kind', 'id'], `${label}.selector`);
|
|
49
|
+
if (selector.kind !== 'model' && selector.kind !== 'tier')
|
|
50
|
+
throw new Error(`${label}.selector.kind is invalid`);
|
|
51
|
+
const effort = record(item.effort, `${label}.effort`);
|
|
52
|
+
if (effort.kind === 'explicit') {
|
|
53
|
+
exact(effort, ['kind', 'value'], `${label}.effort`);
|
|
54
|
+
return { selector: { kind: selector.kind, id: text(selector.id, `${label}.selector.id`) }, effort: { kind: 'explicit', value: text(effort.value, `${label}.effort.value`) } };
|
|
55
|
+
}
|
|
56
|
+
if (effort.kind === 'runtime-default') {
|
|
57
|
+
exact(effort, ['kind'], `${label}.effort`);
|
|
58
|
+
return { selector: { kind: selector.kind, id: text(selector.id, `${label}.selector.id`) }, effort: { kind: 'runtime-default' } };
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`${label}.effort.kind is invalid`);
|
|
61
|
+
}
|
|
62
|
+
function mapping(value) {
|
|
63
|
+
const item = record(value, 'mapping');
|
|
64
|
+
exact(item, ['target', 'runtimeKind', 'profiles', 'fullCapability', 'degradation'], 'mapping');
|
|
65
|
+
const rawProfiles = record(item.profiles, 'mapping.profiles');
|
|
66
|
+
exact(rawProfiles, profiles, 'mapping.profiles');
|
|
67
|
+
const degradation = record(item.degradation, 'mapping.degradation');
|
|
68
|
+
exact(degradation, ['allowMissingModelOverride', 'allowMissingEffortOverride', 'allowMissingObservedIdentity'], 'mapping.degradation');
|
|
69
|
+
return { target: agent(item.target), runtimeKind: text(item.runtimeKind, 'mapping.runtimeKind', RUNTIME_KIND), profiles: {
|
|
70
|
+
mechanical: selection(rawProfiles.mechanical, 'mapping.profiles.mechanical'), integration: selection(rawProfiles.integration, 'mapping.profiles.integration'), judgment: selection(rawProfiles.judgment, 'mapping.profiles.judgment'),
|
|
71
|
+
}, fullCapability: selection(item.fullCapability, 'mapping.fullCapability'), degradation: {
|
|
72
|
+
allowMissingModelOverride: bool(degradation.allowMissingModelOverride, 'mapping.degradation.allowMissingModelOverride'), allowMissingEffortOverride: bool(degradation.allowMissingEffortOverride, 'mapping.degradation.allowMissingEffortOverride'), allowMissingObservedIdentity: bool(degradation.allowMissingObservedIdentity, 'mapping.degradation.allowMissingObservedIdentity'),
|
|
73
|
+
} };
|
|
74
|
+
}
|
|
75
|
+
function validatePolicyContent(value) {
|
|
76
|
+
const item = record(value, 'policy content');
|
|
77
|
+
exact(item, ['schema', 'mappings', 'implementationBudget'], 'policy content');
|
|
78
|
+
if (item.schema !== 'model-policy/v1')
|
|
79
|
+
throw new Error('policy schema is invalid');
|
|
80
|
+
if (!Array.isArray(item.mappings) || item.mappings.length > exports.MAX_MAPPINGS)
|
|
81
|
+
throw new Error('policy mappings are invalid');
|
|
82
|
+
const mappings = item.mappings.map(mapping);
|
|
83
|
+
const unique = new Set();
|
|
84
|
+
for (const row of mappings) {
|
|
85
|
+
const key = `${row.target}\0${row.runtimeKind}`;
|
|
86
|
+
if (unique.has(key))
|
|
87
|
+
throw new Error('duplicate policy target/runtime mapping');
|
|
88
|
+
unique.add(key);
|
|
89
|
+
}
|
|
90
|
+
const budget = record(item.implementationBudget, 'implementationBudget');
|
|
91
|
+
exact(budget, ['maxAttempts', 'escalation', 'judgmentEfforts'], 'implementationBudget');
|
|
92
|
+
if (budget.maxAttempts !== 3 || !Array.isArray(budget.escalation) || budget.escalation.length !== 3 || budget.escalation.join(',') !== 'mechanical,integration,judgment' || !Array.isArray(budget.judgmentEfforts) || budget.judgmentEfforts.join(',') !== 'medium,high')
|
|
93
|
+
throw new Error('implementation budget is invalid');
|
|
94
|
+
return { schema: 'model-policy/v1', mappings, implementationBudget: { maxAttempts: 3, escalation: ['mechanical', 'integration', 'judgment'], judgmentEfforts: ['medium', 'high'] } };
|
|
95
|
+
}
|
|
96
|
+
function validateApprovedPolicy(value) {
|
|
97
|
+
const item = record(value, 'approved policy');
|
|
98
|
+
exact(item, ['schema', 'content', 'contentDigest', 'approval', 'lineage'], 'approved policy');
|
|
99
|
+
if (item.schema !== 'approved-model-policy/v1')
|
|
100
|
+
throw new Error('approved policy schema is invalid');
|
|
101
|
+
const approval = record(item.approval, 'approval');
|
|
102
|
+
exact(approval, ['approvedAt', 'approvalId'], 'approval');
|
|
103
|
+
const lineage = record(item.lineage, 'lineage');
|
|
104
|
+
exact(lineage, ['previousDigest'], 'lineage');
|
|
105
|
+
const contentDigest = text(item.contentDigest, 'contentDigest', SHA256);
|
|
106
|
+
if (typeof approval.approvalId !== 'string' || approval.approvalId.length === 0 || approval.approvalId.length > exports.MAX_IDENTIFIER || CONTROL.test(approval.approvalId) || (lineage.previousDigest !== null && (typeof lineage.previousDigest !== 'string' || !SHA256.test(lineage.previousDigest))))
|
|
107
|
+
throw new Error('approved policy metadata is invalid');
|
|
108
|
+
const approvedAt = canonicalUtcTimestamp(approval.approvedAt, 'approval.approvedAt');
|
|
109
|
+
return { schema: 'approved-model-policy/v1', content: validatePolicyContent(item.content), contentDigest, approval: { approvedAt, approvalId: approval.approvalId }, lineage: { previousDigest: lineage.previousDigest } };
|
|
110
|
+
}
|
|
111
|
+
function assertDigest(value, label) { if (typeof value !== 'string' || !SHA256.test(value))
|
|
112
|
+
throw new Error(`${label} must be a 64-character lowercase SHA-256 digest`); }
|
|
@@ -12,6 +12,8 @@ exports.MAX_PLAN_SNAPSHOT_BYTES = 1024 * 1024;
|
|
|
12
12
|
exports.EXECUTION_IDENTITY_SCHEMA = 'awm-plan-execution/v1';
|
|
13
13
|
const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
|
|
14
14
|
const END = '<!-- AWM:COMPACT-SLICES:END v1 -->';
|
|
15
|
+
const START_V2 = '<!-- AWM:COMPACT-SLICES:START v2 -->';
|
|
16
|
+
const END_V2 = '<!-- AWM:COMPACT-SLICES:END v2 -->';
|
|
15
17
|
/** Only lifecycle syntax actually emitted by the skills is non-executable.
|
|
16
18
|
* Unknown metadata, prose, inline comments and code remain identity-bearing. */
|
|
17
19
|
function lifecycleMarker(line) {
|
|
@@ -53,11 +55,11 @@ function executionPlanDigest(text) {
|
|
|
53
55
|
}
|
|
54
56
|
if (fence)
|
|
55
57
|
return { line };
|
|
56
|
-
if (line === START) {
|
|
58
|
+
if (line === START || line === START_V2) {
|
|
57
59
|
manifest = true;
|
|
58
60
|
return { line };
|
|
59
61
|
}
|
|
60
|
-
if (line === END) {
|
|
62
|
+
if (line === END || line === END_V2) {
|
|
61
63
|
manifest = false;
|
|
62
64
|
return { line };
|
|
63
65
|
}
|
|
@@ -14,6 +14,8 @@ const json_1 = require("./json");
|
|
|
14
14
|
const identity_1 = require("./identity");
|
|
15
15
|
const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
|
|
16
16
|
const END = '<!-- AWM:COMPACT-SLICES:END v1 -->';
|
|
17
|
+
const START_V2 = '<!-- AWM:COMPACT-SLICES:START v2 -->';
|
|
18
|
+
const END_V2 = '<!-- AWM:COMPACT-SLICES:END v2 -->';
|
|
17
19
|
const MAX_PLAN = 1024 * 1024;
|
|
18
20
|
const MAX_MANIFEST = 256 * 1024;
|
|
19
21
|
const MAX_SOURCE = 1024 * 1024;
|
|
@@ -356,22 +358,18 @@ function validatePlan(planPath, cwd, snapshot) {
|
|
|
356
358
|
}
|
|
357
359
|
const starts = count(text, START);
|
|
358
360
|
const ends = count(text, END);
|
|
359
|
-
const signaled = starts > 0 || ends > 0 || compactSchemaSignal(text);
|
|
361
|
+
const signaled = starts > 0 || ends > 0 || count(text, START_V2) > 0 || count(text, END_V2) > 0 || compactSchemaSignal(text);
|
|
360
362
|
if (!signaled)
|
|
361
363
|
return markerlessSchemaClassification(text) ?? { state: 'migration-required', reason: 'unmarked-plan' };
|
|
362
|
-
|
|
364
|
+
const start = text.includes(START_V2) ? START_V2 : START;
|
|
365
|
+
const end = start === START_V2 ? END_V2 : END;
|
|
366
|
+
const startsForSchema = count(text, start);
|
|
367
|
+
const endsForSchema = count(text, end);
|
|
368
|
+
if (starts === 0 && ends === 0 && startsForSchema === 0 && endsForSchema === 0)
|
|
363
369
|
return markerlessSchemaClassification(text) ?? diagnostic('PLAN_MARKERS', 'compact markers must occur once in order');
|
|
364
|
-
if (
|
|
365
|
-
const partial = starts === 1 ? text.slice(text.indexOf(START) + START.length, ends ? text.indexOf(END) : undefined).trim() : '';
|
|
366
|
-
try {
|
|
367
|
-
const candidate = (0, json_1.parseJsonNoDuplicate)(partial);
|
|
368
|
-
if (object(candidate) && typeof candidate.schema === 'string' && Buffer.byteLength(candidate.schema, 'utf8') <= MAX_STRING && candidate.schema.startsWith('compact-slices/') && candidate.schema !== 'compact-slices/v1')
|
|
369
|
-
return unsupported(candidate.schema);
|
|
370
|
-
}
|
|
371
|
-
catch { /* invalid below */ }
|
|
370
|
+
if (startsForSchema !== 1 || endsForSchema !== 1 || text.indexOf(start) > text.indexOf(end) || (start === START && (count(text, START_V2) !== 0 || count(text, END_V2) !== 0)) || (start === START_V2 && (starts !== 0 || ends !== 0)))
|
|
372
371
|
return diagnostic('PLAN_MARKERS', 'compact markers must occur once in order');
|
|
373
|
-
|
|
374
|
-
const body = text.slice(text.indexOf(START) + START.length, text.indexOf(END));
|
|
372
|
+
const body = text.slice(text.indexOf(start) + start.length, text.indexOf(end));
|
|
375
373
|
if (Buffer.byteLength(body, 'utf8') > MAX_MANIFEST)
|
|
376
374
|
return diagnostic('PLAN_LIMIT', 'manifest exceeds maximum size');
|
|
377
375
|
let raw;
|
|
@@ -385,7 +383,7 @@ function validatePlan(planPath, cwd, snapshot) {
|
|
|
385
383
|
return diagnostic('PLAN_SHAPE', 'manifest must be an object');
|
|
386
384
|
if (typeof raw.schema === 'string' && Buffer.byteLength(raw.schema, 'utf8') > MAX_STRING)
|
|
387
385
|
return diagnostic('PLAN_LIMIT', 'schema exceeds maximum string size');
|
|
388
|
-
if (typeof raw.schema === 'string' && raw.schema.startsWith('compact-slices/') && raw.schema !== 'compact-slices/v1')
|
|
386
|
+
if (typeof raw.schema === 'string' && raw.schema.startsWith('compact-slices/') && raw.schema !== 'compact-slices/v1' && !(raw.schema === 'compact-slices/v2' && start === START_V2))
|
|
389
387
|
return unsupported(raw.schema);
|
|
390
388
|
if (!exact(raw, ['schema', 'planId', 'requirements', 'sources', 'commands', 'slices', 'closureCommands']))
|
|
391
389
|
return diagnostic(Object.keys(raw).some((key) => !['schema', 'planId', 'requirements', 'sources', 'commands', 'slices', 'closureCommands'].includes(key)) ? 'PLAN_UNKNOWN_FIELD' : 'PLAN_MISSING_FIELD', 'manifest fields must exactly match the contract');
|
|
@@ -395,7 +393,7 @@ function validatePlan(planPath, cwd, snapshot) {
|
|
|
395
393
|
return diagnostic('PLAN_LIMIT', 'manifest exceeds collection limits');
|
|
396
394
|
if (typeof raw.planId === 'string' && Buffer.byteLength(raw.planId, 'utf8') > MAX_STRING)
|
|
397
395
|
return diagnostic('PLAN_LIMIT', 'plan id exceeds maximum string size');
|
|
398
|
-
if (raw.schema !== 'compact-slices/v1' || typeof raw.planId !== 'string' || !PLAN_ID.test(raw.planId) || !allStrings(raw.requirements) || new Set(raw.requirements).size !== raw.requirements.length)
|
|
396
|
+
if ((raw.schema !== 'compact-slices/v1' && raw.schema !== 'compact-slices/v2') || (raw.schema === 'compact-slices/v1' && start !== START) || (raw.schema === 'compact-slices/v2' && start !== START_V2) || typeof raw.planId !== 'string' || !PLAN_ID.test(raw.planId) || !allStrings(raw.requirements) || new Set(raw.requirements).size !== raw.requirements.length)
|
|
399
397
|
return diagnostic('PLAN_SHAPE', 'manifest scalar fields are invalid');
|
|
400
398
|
if (raw.requirements.some((id) => !validRequirementId(id)) || !allStrings(raw.closureCommands) || new Set(raw.closureCommands).size !== raw.closureCommands.length)
|
|
401
399
|
return diagnostic('PLAN_SHAPE', 'manifest arrays or identifiers are invalid');
|
|
@@ -438,8 +436,9 @@ function validatePlan(planPath, cwd, snapshot) {
|
|
|
438
436
|
}
|
|
439
437
|
const sliceIds = new Set();
|
|
440
438
|
const owners = new Map();
|
|
439
|
+
const sliceFields = ['id', 'title', 'requirements', 'dependsOn', 'sectionAnchor', 'sources', 'redCommands', 'greenCommands', 'reviewEvidence', 'risk', 'fallback'];
|
|
441
440
|
for (const slice of raw.slices) {
|
|
442
|
-
if (!exact(slice,
|
|
441
|
+
if (!exact(slice, raw.schema === 'compact-slices/v2' ? [...sliceFields, 'implementerProfile'] : sliceFields) || (raw.schema === 'compact-slices/v2' && !['mechanical', 'integration', 'judgment'].includes(slice.implementerProfile)) || !validId(slice.id) || sliceIds.has(slice.id) || typeof slice.title !== 'string' || slice.title.length === 0 || Buffer.byteLength(slice.title, 'utf8') > MAX_STRING || !allStrings(slice.requirements) || slice.requirements.length === 0 || slice.requirements.some((id) => !validRequirementId(id) || !raw.requirements.includes(id)) || !allStrings(slice.dependsOn) || typeof slice.sectionAnchor !== 'string' || slice.sectionAnchor.length === 0 || Buffer.byteLength(slice.sectionAnchor, 'utf8') > MAX_STRING || !uniqueRefs(slice.sources, sourceIds) || !uniqueRefs(slice.redCommands, commandIds) || slice.redCommands.length === 0 || !uniqueRefs(slice.greenCommands, commandIds) || slice.greenCommands.length === 0 || !allStrings(slice.fallback) || slice.fallback.length === 0 || slice.fallback.some((item) => item.trim().length === 0) || slice.risk !== 'bounded' && slice.risk !== 'full-context')
|
|
443
442
|
return diagnostic('PLAN_SLICE_SHAPE', 'slice fields are invalid');
|
|
444
443
|
sliceIds.add(slice.id);
|
|
445
444
|
if (!Array.isArray(slice.reviewEvidence) || slice.reviewEvidence.length !== 2 || new Set(slice.reviewEvidence).size !== 2 || !slice.reviewEvidence.includes('specification') || !slice.reviewEvidence.includes('code-quality'))
|
|
@@ -485,7 +484,7 @@ function validatePlan(planPath, cwd, snapshot) {
|
|
|
485
484
|
if (markdown)
|
|
486
485
|
return markdown;
|
|
487
486
|
const report = {
|
|
488
|
-
state: 'valid', schema:
|
|
487
|
+
state: 'valid', schema: raw.schema,
|
|
489
488
|
planDigest: crypto_1.default.createHash('sha256').update(text, 'utf8').digest('hex'),
|
|
490
489
|
executionDigest: (0, identity_1.executionPlanDigest)(text),
|
|
491
490
|
manifest: raw, executionMode: executionModeFromValidatedText(text),
|
package/dist/src/index.js
CHANGED
|
@@ -46,6 +46,7 @@ const track_1 = require("./commands/track");
|
|
|
46
46
|
const evidence_1 = require("./commands/evidence");
|
|
47
47
|
const process_1 = require("./commands/process");
|
|
48
48
|
const context_1 = require("./commands/context");
|
|
49
|
+
const model_policy_1 = require("./commands/model-policy");
|
|
49
50
|
const add_1 = require("./commands/add");
|
|
50
51
|
const sync_1 = require("./commands/sync");
|
|
51
52
|
const update_1 = require("./commands/update");
|
|
@@ -54,6 +55,7 @@ const update_check_1 = require("./core/update-check");
|
|
|
54
55
|
const cli_version_1 = require("./core/cli-version");
|
|
55
56
|
exports.program = new commander_1.Command();
|
|
56
57
|
exports.program.name('awm').description('Agentic Workflow Manager').version((0, cli_version_1.cliVersion)());
|
|
58
|
+
(0, model_policy_1.registerModelPolicyCommand)(exports.program);
|
|
57
59
|
exports.program.hook('postAction', (_command, actionCommand) => {
|
|
58
60
|
if (actionCommand.name() === 'preflight')
|
|
59
61
|
return;
|
|
@@ -55,6 +55,22 @@ function passingState() {
|
|
|
55
55
|
return s;
|
|
56
56
|
}
|
|
57
57
|
describe('gate', () => {
|
|
58
|
+
test('un verdict routed exige intento observado, identidad y fingerprint coincidentes', () => {
|
|
59
|
+
const s = passingState();
|
|
60
|
+
s.verdicts[0] = { ...s.verdicts[0], routingAttemptId: 'route-1' };
|
|
61
|
+
let gate = (0, gate_1.computeGate)(s, false, fpCurrent);
|
|
62
|
+
expect(gate.reasons.some((reason) => reason.category === 'routing-evidence')).toBe(true);
|
|
63
|
+
s.routingAttempts = [{
|
|
64
|
+
schema: 'routing-attempt/v1', id: 'route-1', obligationId: 'o-spec', lineageId: 'lineage-1', attempt: 1,
|
|
65
|
+
envelope: { schema: 'routing-envelope/v1', runtime: { target: 'codex', kind: 'native', version: '1', accountScopeDigest: '0'.repeat(64) }, role: 'specification-reviewer', requestedProfile: 'full', effectiveProfile: 'full', resolved: { selector: { kind: 'model', id: 'm' }, effort: { kind: 'explicit', value: 'high' } }, outcome: 'native', unavailableEvidence: [], policyDigest: 'a'.repeat(64), capabilityDigest: 'b'.repeat(64), planDigest: 'c'.repeat(64), executionDigest: 'd'.repeat(64) },
|
|
66
|
+
envelopeDigest: 'a'.repeat(64), fingerprint: 'fp', state: 'active', nativeAgentId: 'native-1', observed: { selector: { kind: 'model', id: 'm' }, effort: { kind: 'explicit', value: 'high' } },
|
|
67
|
+
}];
|
|
68
|
+
s.planBinding = { path: 'plan.md', digest: 'c'.repeat(64), executionDigest: 'd'.repeat(64), executionIdentitySchema: 'awm-plan-execution/v1', schema: 'compact-slices/v2', executionMode: 'desatendido', boundAt: 'now' };
|
|
69
|
+
expect((0, gate_1.computeGate)(s, false, fpCurrent).pass).toBe(true);
|
|
70
|
+
s.routingAttempts[0].fingerprint = 'different';
|
|
71
|
+
gate = (0, gate_1.computeGate)(s, false, fpCurrent);
|
|
72
|
+
expect(gate.reasons.some((reason) => reason.category === 'routing-evidence')).toBe(true);
|
|
73
|
+
});
|
|
58
74
|
test('el estado de referencia pasa; la corrupcion bloquea (R3.2)', () => {
|
|
59
75
|
expect((0, gate_1.computeGate)(passingState(), false, fpCurrent).pass).toBe(true);
|
|
60
76
|
const g = (0, gate_1.computeGate)(null, true, fpCurrent);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const gate_1 = require("../../../src/commands/job/gate");
|
|
4
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
5
|
+
test('routed evidence cannot certify against a legacy journal without its attempt', () => {
|
|
6
|
+
const state = (0, types_1.emptyState)('main');
|
|
7
|
+
state.verdicts.push({ id: 'v1', obligationId: 'o1', result: 'pass', detail: '', receivedAt: 'now', fingerprint: 'fp', argv: [], paths: [], cwd: '.', routingAttemptId: 'missing' });
|
|
8
|
+
expect((0, gate_1.computeGate)(state, false, () => 'fp').reasons).toContainEqual(expect.objectContaining({ category: 'routing-evidence' }));
|
|
9
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
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 child_process_1 = require("child_process");
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const job_1 = require("../../../src/commands/job");
|
|
12
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
13
|
+
const journal_1 = require("../../../src/core/model-policy/journal");
|
|
14
|
+
const envelope = { schema: 'routing-envelope/v1', runtime: { target: 'codex', kind: 'native', version: '1', accountScopeDigest: '0'.repeat(64) }, role: 'implementer', sliceId: 'S1', requestedProfile: 'mechanical', effectiveProfile: 'mechanical', resolved: { selector: { kind: 'model', id: 'model-private' }, effort: { kind: 'explicit', value: 'medium' } }, outcome: 'native', unavailableEvidence: [], policyDigest: 'a'.repeat(64), capabilityDigest: 'b'.repeat(64), planDigest: 'c'.repeat(64), executionDigest: 'd'.repeat(64) };
|
|
15
|
+
function gitInit(repo) {
|
|
16
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'init', '-q', '-b', 'main'], { cwd: repo });
|
|
17
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'fixture'), 'x');
|
|
18
|
+
(0, child_process_1.execFileSync)('git', ['add', '.'], { cwd: repo });
|
|
19
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'commit', '-qm', 'fixture'], { cwd: repo });
|
|
20
|
+
}
|
|
21
|
+
test('`job routing-report --json` emits only aggregate routing evidence', async () => {
|
|
22
|
+
const repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-routing-report-'));
|
|
23
|
+
try {
|
|
24
|
+
gitInit(repo);
|
|
25
|
+
(0, store_1.initJournal)(repo, 'main');
|
|
26
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
27
|
+
const reserved = (0, journal_1.reserveRoutingAttempt)(state, { obligationId: 'obligation', lineageId: 'lineage', envelope, fingerprint: 'e'.repeat(64) }, '2026-09-17T00:00:00.000Z');
|
|
28
|
+
(0, store_1.writeJournal)(repo, 'main', (0, journal_1.observeRoutingAttempt)(reserved.state, { attemptId: reserved.attemptId, nativeAgentId: 'native-private' }, '2026-09-17T00:01:00.000Z'));
|
|
29
|
+
const program = new commander_1.Command();
|
|
30
|
+
program.exitOverride();
|
|
31
|
+
(0, job_1.registerJobCommand)(program);
|
|
32
|
+
const out = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
33
|
+
const cwd = jest.spyOn(process, 'cwd').mockReturnValue(repo);
|
|
34
|
+
await program.parseAsync(['node', 'awm', 'job', 'routing-report', '--json']);
|
|
35
|
+
const rendered = String(out.mock.calls[0][0]);
|
|
36
|
+
const report = JSON.parse(rendered);
|
|
37
|
+
expect(report).toMatchObject({ schema: 'routing-report/v1', attempts: 1, plannedByRole: { implementer: 1 }, actualByRole: { implementer: 1 }, byState: { active: 1 } });
|
|
38
|
+
expect(rendered).not.toContain('native-private');
|
|
39
|
+
expect(rendered).not.toContain('model-private');
|
|
40
|
+
cwd.mockRestore();
|
|
41
|
+
out.mockRestore();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
4
|
+
const journal_1 = require("../../../src/core/model-policy/journal");
|
|
5
|
+
const envelope = { schema: 'routing-envelope/v1', runtime: { target: 'codex', kind: 'native', version: '1', accountScopeDigest: '0'.repeat(64) }, role: 'implementer', sliceId: 'S1', requestedProfile: 'mechanical', effectiveProfile: 'mechanical', resolved: { selector: { kind: 'model', id: 'm' }, effort: { kind: 'explicit', value: 'medium' } }, outcome: 'native', unavailableEvidence: [], policyDigest: 'a'.repeat(64), capabilityDigest: 'b'.repeat(64), planDigest: 'c'.repeat(64), executionDigest: 'd'.repeat(64) };
|
|
6
|
+
test('routing report remains read-only across an observed attempt', () => {
|
|
7
|
+
const reserved = (0, journal_1.reserveRoutingAttempt)((0, types_1.emptyState)('main'), { obligationId: 'o1', lineageId: 'l1', envelope, fingerprint: 'e'.repeat(64) }, '2026-09-17T00:00:00.000Z');
|
|
8
|
+
const observed = (0, journal_1.observeRoutingAttempt)(reserved.state, { attemptId: reserved.attemptId, nativeAgentId: 'native-id' }, '2026-09-17T00:01:00.000Z');
|
|
9
|
+
expect((0, journal_1.routingReport)(observed)).toMatchObject({ attempts: 1, byState: { active: 1 } });
|
|
10
|
+
expect(observed.routingAttempts[0].nativeAgentId).toBe('native-id');
|
|
11
|
+
});
|