agentic-workflow-manager 9.7.1 → 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/README.md +5 -1
- package/dist/src/commands/job/exec-wrapper.js +53 -3
- 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 +182 -7
- package/dist/src/commands/sensors/bootstrap.js +36 -5
- package/dist/src/commands/sensors/compatibility/contract.js +19 -4
- package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
- package/dist/src/commands/sensors/compatibility/live.js +18 -1
- package/dist/src/commands/sensors/compatibility/probe.js +8 -2
- package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
- package/dist/src/commands/sensors/coverage/evidence.js +19 -10
- package/dist/src/commands/sensors/coverage/index.js +11 -2
- package/dist/src/commands/sensors/coverage/resolve.js +10 -0
- package/dist/src/commands/sensors/exec.js +16 -1
- package/dist/src/commands/sensors/migrate.js +62 -2
- package/dist/src/commands/sensors/prepare.js +2 -0
- package/dist/src/commands/sensors/result.js +2 -0
- package/dist/src/commands/sensors/run.js +64 -1
- package/dist/src/commands/sensors/status.js +6 -4
- package/dist/src/commands/watch/apply.js +32 -0
- package/dist/src/commands/watch/archive-unused.js +138 -0
- package/dist/src/commands/watch/generations.js +32 -9
- package/dist/src/commands/watch/index.js +108 -2
- package/dist/src/commands/watch/init.js +149 -18
- package/dist/src/commands/watch/supervisor.js +167 -13
- package/dist/src/commands/watch/teardown-driver.js +13 -13
- package/dist/src/commands/watch/track-state.js +59 -0
- package/dist/src/commands/watch/tracks.js +38 -102
- package/dist/src/core/admission/index.js +231 -0
- package/dist/src/core/admission/registry-contracts.js +290 -0
- package/dist/src/core/journal/fingerprint.js +35 -1
- package/dist/src/core/journal/paths.js +11 -0
- package/dist/src/core/journal/requests.js +1 -1
- package/dist/src/core/journal/store.js +146 -4
- package/dist/src/core/journal/types.js +57 -2
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- 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 +77 -0
- package/dist/src/core/plan/validate.js +191 -78
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
- 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/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +682 -14
- package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
- package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
- package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
- package/dist/tests/commands/sensors/exec.test.js +112 -0
- package/dist/tests/commands/sensors/run-changed.test.js +11 -0
- package/dist/tests/commands/sensors/run.test.js +15 -2
- package/dist/tests/commands/sensors/status.test.js +3 -3
- package/dist/tests/commands/watch/apply.test.js +12 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
- package/dist/tests/commands/watch/generations.test.js +113 -4
- package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
- package/dist/tests/commands/watch/track-finalize.test.js +20 -3
- package/dist/tests/commands/watch/track-freeze.test.js +43 -30
- package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
- package/dist/tests/commands/watch/watch-init.test.js +244 -0
- package/dist/tests/core/admission/index.test.js +140 -0
- package/dist/tests/core/journal/fingerprint.test.js +22 -0
- package/dist/tests/core/journal/store.test.js +53 -0
- package/dist/tests/core/journal/types.test.js +7 -0
- package/dist/tests/core/migration/facts.test.js +289 -0
- package/dist/tests/core/migration/historical-provenance.test.js +25 -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 +360 -8
- package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
- package/dist/tests/integration/pack-e2e.test.js +4 -1
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
- package/dist/tests/providers/injection-config.test.js +14 -6
- package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +30 -0
- package/package.json +4 -3
- 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,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`); }
|
|
@@ -0,0 +1,77 @@
|
|
|
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.EXECUTION_IDENTITY_SCHEMA = exports.MAX_PLAN_SNAPSHOT_BYTES = void 0;
|
|
7
|
+
exports.normalizedPlanSnapshot = normalizedPlanSnapshot;
|
|
8
|
+
exports.fullPlanDigest = fullPlanDigest;
|
|
9
|
+
exports.executionPlanDigest = executionPlanDigest;
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
exports.MAX_PLAN_SNAPSHOT_BYTES = 1024 * 1024;
|
|
12
|
+
exports.EXECUTION_IDENTITY_SCHEMA = 'awm-plan-execution/v1';
|
|
13
|
+
const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
|
|
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 -->';
|
|
17
|
+
/** Only lifecycle syntax actually emitted by the skills is non-executable.
|
|
18
|
+
* Unknown metadata, prose, inline comments and code remain identity-bearing. */
|
|
19
|
+
function lifecycleMarker(line) {
|
|
20
|
+
const match = /^ {0,3}<!-- (awm-(?:qa|docs|retro)-complete)(?:: (\d{4}-\d{2}-\d{2}|Release [A-Za-z0-9][A-Za-z0-9_-]*))? -->$/.exec(line);
|
|
21
|
+
if (!match)
|
|
22
|
+
return undefined;
|
|
23
|
+
const metadata = match[2];
|
|
24
|
+
if (metadata && !metadata.startsWith('Release ')) {
|
|
25
|
+
const parsed = new Date(`${metadata}T00:00:00.000Z`);
|
|
26
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== metadata)
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return `${match[1]}:${metadata?.startsWith('Release ') ? metadata : ''}`;
|
|
30
|
+
}
|
|
31
|
+
function normalizedPlanSnapshot(text) {
|
|
32
|
+
if (typeof text !== 'string' || text.length === 0 || Buffer.byteLength(text, 'utf8') > exports.MAX_PLAN_SNAPSHOT_BYTES || text.includes('\0')) {
|
|
33
|
+
throw new Error('plan snapshot must be nonempty bounded text without NUL');
|
|
34
|
+
}
|
|
35
|
+
return text.replace(/\r\n?/g, '\n');
|
|
36
|
+
}
|
|
37
|
+
function fullPlanDigest(text) {
|
|
38
|
+
return crypto_1.default.createHash('sha256').update(normalizedPlanSnapshot(text), 'utf8').digest('hex');
|
|
39
|
+
}
|
|
40
|
+
/** This versioned identity preserves ALL plan bytes except recognized progress
|
|
41
|
+
* status bytes and unique standalone lifecycle marker lines outside code/JSON.
|
|
42
|
+
* It is never a replacement for the full validated-plan digest. */
|
|
43
|
+
function executionPlanDigest(text) {
|
|
44
|
+
const lines = normalizedPlanSnapshot(text).split('\n');
|
|
45
|
+
let fence;
|
|
46
|
+
let manifest = false;
|
|
47
|
+
const candidates = lines.map(line => {
|
|
48
|
+
const matched = /^(?: {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
|
|
49
|
+
const marker = matched?.[1];
|
|
50
|
+
if (marker && (fence
|
|
51
|
+
? marker[0] === fence.character && marker.length >= fence.length && /^[ \t]*$/.test(matched[2])
|
|
52
|
+
: marker[0] !== '`' || !matched[2].includes('`'))) {
|
|
53
|
+
fence = fence ? undefined : { character: marker[0], length: marker.length };
|
|
54
|
+
return { line };
|
|
55
|
+
}
|
|
56
|
+
if (fence)
|
|
57
|
+
return { line };
|
|
58
|
+
if (line === START || line === START_V2) {
|
|
59
|
+
manifest = true;
|
|
60
|
+
return { line };
|
|
61
|
+
}
|
|
62
|
+
if (line === END || line === END_V2) {
|
|
63
|
+
manifest = false;
|
|
64
|
+
return { line };
|
|
65
|
+
}
|
|
66
|
+
if (manifest)
|
|
67
|
+
return { line };
|
|
68
|
+
const lifecycle = lifecycleMarker(line);
|
|
69
|
+
return { line: line.replace(/^( {0,3}[-*+] +)\[[ xX]\]( +\S.*)$/, '$1[ ]$2'), lifecycle };
|
|
70
|
+
});
|
|
71
|
+
const counts = new Map();
|
|
72
|
+
for (const { lifecycle } of candidates)
|
|
73
|
+
if (lifecycle)
|
|
74
|
+
counts.set(lifecycle, (counts.get(lifecycle) ?? 0) + 1);
|
|
75
|
+
const canonical = candidates.filter(({ lifecycle }) => !lifecycle || counts.get(lifecycle) !== 1).map(({ line }) => line).join('\n');
|
|
76
|
+
return crypto_1.default.createHash('sha256').update(`${exports.EXECUTION_IDENTITY_SCHEMA}\0`).update(canonical, 'utf8').digest('hex');
|
|
77
|
+
}
|
|
@@ -3,22 +3,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.assertVerifiedValidPlanReport = assertVerifiedValidPlanReport;
|
|
7
|
+
exports.verifiedPlanSnapshot = verifiedPlanSnapshot;
|
|
6
8
|
exports.validatePlanFile = validatePlanFile;
|
|
9
|
+
exports.validatePlanSnapshot = validatePlanSnapshot;
|
|
7
10
|
const fs_1 = __importDefault(require("fs"));
|
|
8
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
13
|
const json_1 = require("./json");
|
|
14
|
+
const identity_1 = require("./identity");
|
|
10
15
|
const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
|
|
11
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 -->';
|
|
12
19
|
const MAX_PLAN = 1024 * 1024;
|
|
13
20
|
const MAX_MANIFEST = 256 * 1024;
|
|
14
21
|
const MAX_SOURCE = 1024 * 1024;
|
|
15
22
|
const MAX_STRING = 4096;
|
|
16
|
-
const
|
|
23
|
+
const ENTITY_ID = /^[A-Z][A-Z0-9-]{0,63}$/;
|
|
24
|
+
const REQUIREMENT_ID = /^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*(?:\.[A-Z0-9]+(?:-[A-Z0-9]+)*)*$/;
|
|
17
25
|
const PLAN_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
18
26
|
const SHELL = new Set([
|
|
19
27
|
'sh', 'bash', 'zsh', 'dash', 'ksh', 'fish', 'cmd', 'powershell', 'pwsh',
|
|
20
28
|
'env', 'busybox', 'node', 'nodejs', 'deno', 'bun', 'python', 'python3', 'ruby', 'perl', 'php', 'lua',
|
|
21
29
|
]);
|
|
30
|
+
const verifiedValidReports = new WeakSet();
|
|
31
|
+
const verifiedSnapshots = new WeakMap();
|
|
32
|
+
function freezeJson(value) {
|
|
33
|
+
if (value === null || typeof value !== 'object')
|
|
34
|
+
return;
|
|
35
|
+
for (const child of Object.values(value))
|
|
36
|
+
freezeJson(child);
|
|
37
|
+
Object.freeze(value);
|
|
38
|
+
}
|
|
39
|
+
/** Require a process-local, immutable report issued by successful core validation. */
|
|
40
|
+
function assertVerifiedValidPlanReport(report) {
|
|
41
|
+
if (!report || typeof report !== 'object' || report.state !== 'valid' || !verifiedValidReports.has(report)) {
|
|
42
|
+
throw new Error('plan validator returned an invalid valid report');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The exact immutable validator snapshot, never a later reopen of the path. */
|
|
46
|
+
function verifiedPlanSnapshot(report) {
|
|
47
|
+
assertVerifiedValidPlanReport(report);
|
|
48
|
+
const snapshot = verifiedSnapshots.get(report);
|
|
49
|
+
if (snapshot === undefined)
|
|
50
|
+
throw new Error('verified plan snapshot unavailable');
|
|
51
|
+
return snapshot;
|
|
52
|
+
}
|
|
22
53
|
function isLauncher(program) {
|
|
23
54
|
const base = path_1.default.posix.basename(program).toLowerCase();
|
|
24
55
|
return SHELL.has(base.replace(/\.exe$/, '')) || /^(?:node(?:js)?|py(?:thon(?:w)?\d*(?:\.\d+)?)?|deno|bun|ruby(?:\d+(?:\.\d+)*)?|perl(?:\d+(?:\.\d+)*)?|(?:a|ba|c|tc|z|da|k)?sh|busybox|cmd|powershell|pwsh|wscript|cscript)(?:\.exe)?(?:[-.][a-z0-9][a-z0-9.-]*)?$/i.test(base);
|
|
@@ -37,39 +68,49 @@ function schemaClassification(candidate) {
|
|
|
37
68
|
return diagnostic('PLAN_MARKERS', 'compact markers must occur once in order');
|
|
38
69
|
}
|
|
39
70
|
function embeddedJsonObjects(text) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for (; end < text.length && end - start <= MAX_MANIFEST; end += 1) {
|
|
49
|
-
const character = text[end];
|
|
50
|
-
if (quoted) {
|
|
51
|
-
if (escaped)
|
|
52
|
-
escaped = false;
|
|
53
|
-
else if (character === '\\')
|
|
54
|
-
escaped = true;
|
|
55
|
-
else if (character === '"')
|
|
56
|
-
quoted = false;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (character === '"') {
|
|
60
|
-
quoted = true;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
71
|
+
const outer = [];
|
|
72
|
+
const nested = [];
|
|
73
|
+
const starts = [];
|
|
74
|
+
let quoted = false;
|
|
75
|
+
let escaped = false;
|
|
76
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
77
|
+
const character = text[index];
|
|
78
|
+
if (starts.length === 0) {
|
|
63
79
|
if (character === '{')
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
starts.push(index);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (quoted) {
|
|
84
|
+
if (character === '\n') {
|
|
85
|
+
quoted = false;
|
|
86
|
+
escaped = false;
|
|
69
87
|
}
|
|
88
|
+
else if (escaped)
|
|
89
|
+
escaped = false;
|
|
90
|
+
else if (character === '\\')
|
|
91
|
+
escaped = true;
|
|
92
|
+
else if (character === '"')
|
|
93
|
+
quoted = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (character === '"') {
|
|
97
|
+
quoted = true;
|
|
98
|
+
continue;
|
|
70
99
|
}
|
|
100
|
+
if (character === '{') {
|
|
101
|
+
starts.push(index);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (character !== '}')
|
|
105
|
+
continue;
|
|
106
|
+
const start = starts.pop();
|
|
107
|
+
if (start === undefined || index - start > MAX_MANIFEST)
|
|
108
|
+
continue;
|
|
109
|
+
const found = starts.length === 0 ? outer : nested;
|
|
110
|
+
if (found.length < 64)
|
|
111
|
+
found.push(text.slice(start, index + 1));
|
|
71
112
|
}
|
|
72
|
-
return
|
|
113
|
+
return [...outer, ...nested].slice(0, 64);
|
|
73
114
|
}
|
|
74
115
|
function markerlessSchemaClassification(text) {
|
|
75
116
|
const candidates = [text.trim(), ...embeddedJsonObjects(text)];
|
|
@@ -123,11 +164,18 @@ function compactSchemaSignal(text) {
|
|
|
123
164
|
}
|
|
124
165
|
return false;
|
|
125
166
|
}
|
|
167
|
+
function executionModeFromValidatedText(text) {
|
|
168
|
+
const raw = /^\s*\*\*Modo de ejecución:\*\*\s*([^\r\n]+)\s*$/mi.exec(text)?.[1]?.trim().replace(/^`|`$/g, '');
|
|
169
|
+
return raw === 'desatendido' ? 'desatendido' : 'interactivo';
|
|
170
|
+
}
|
|
126
171
|
function exact(value, fields) {
|
|
127
172
|
return object(value) && Object.keys(value).length === fields.length && fields.every((field) => Object.prototype.hasOwnProperty.call(value, field));
|
|
128
173
|
}
|
|
129
174
|
function allStrings(value, max = MAX_STRING) { return Array.isArray(value) && value.every((item) => typeof item === 'string' && Buffer.byteLength(item, 'utf8') <= max); }
|
|
130
|
-
function validId(value) { return typeof value === 'string' &&
|
|
175
|
+
function validId(value) { return typeof value === 'string' && ENTITY_ID.test(value); }
|
|
176
|
+
function validRequirementId(value) {
|
|
177
|
+
return typeof value === 'string' && Buffer.byteLength(value, 'utf8') <= 64 && REQUIREMENT_ID.test(value);
|
|
178
|
+
}
|
|
131
179
|
function inside(root, candidate) { const relative = path_1.default.relative(root, candidate); return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative)); }
|
|
132
180
|
function validRelative(value) {
|
|
133
181
|
return typeof value === 'string' && value.length > 0 && Buffer.byteLength(value, 'utf8') <= MAX_STRING && !path_1.default.isAbsolute(value) && !path_1.default.win32.isAbsolute(value) && !value.includes('\\') && value.split('/').every((part) => part !== '' && part !== '.' && part !== '..') && path_1.default.posix.normalize(value) === value;
|
|
@@ -140,30 +188,74 @@ function regularInside(root, relative) {
|
|
|
140
188
|
return undefined;
|
|
141
189
|
const parts = relative.split('/');
|
|
142
190
|
let current = root;
|
|
191
|
+
let stat;
|
|
143
192
|
try {
|
|
144
193
|
for (const part of parts) {
|
|
145
194
|
current = path_1.default.join(current, part);
|
|
146
|
-
|
|
195
|
+
stat = fs_1.default.lstatSync(current, { bigint: true });
|
|
196
|
+
if (stat.isSymbolicLink())
|
|
147
197
|
return undefined;
|
|
148
198
|
}
|
|
149
|
-
if (!
|
|
199
|
+
if (!stat?.isFile())
|
|
150
200
|
return undefined;
|
|
151
201
|
const real = fs_1.default.realpathSync(candidate);
|
|
152
|
-
return inside(root, real) ? real : undefined;
|
|
202
|
+
return inside(root, real) ? { path: real, stat } : undefined;
|
|
153
203
|
}
|
|
154
204
|
catch {
|
|
155
205
|
return undefined;
|
|
156
206
|
}
|
|
157
207
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
208
|
+
function sameIdentity(inspected, opened) {
|
|
209
|
+
return typeof inspected.dev === 'bigint' && typeof inspected.ino === 'bigint' && inspected.ino !== 0n
|
|
210
|
+
&& opened.isFile() && inspected.dev === opened.dev && inspected.ino === opened.ino;
|
|
211
|
+
}
|
|
212
|
+
function readInspected(file, max) {
|
|
213
|
+
if (file.stat.size > BigInt(max))
|
|
214
|
+
return { state: 'limit' };
|
|
215
|
+
let descriptor;
|
|
160
216
|
try {
|
|
161
|
-
|
|
217
|
+
descriptor = fs_1.default.openSync(file.path, fs_1.default.constants.O_RDONLY | (fs_1.default.constants.O_NOFOLLOW ?? 0));
|
|
218
|
+
const opened = fs_1.default.fstatSync(descriptor, { bigint: true });
|
|
219
|
+
if (!sameIdentity(file.stat, opened))
|
|
220
|
+
return { state: 'unsafe' };
|
|
221
|
+
if (opened.size > BigInt(max))
|
|
222
|
+
return { state: 'limit' };
|
|
223
|
+
if (opened.size !== file.stat.size)
|
|
224
|
+
return { state: 'unsafe' };
|
|
225
|
+
const bytes = Buffer.alloc(Number(opened.size));
|
|
226
|
+
let offset = 0;
|
|
227
|
+
while (offset < bytes.length) {
|
|
228
|
+
const read = fs_1.default.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
|
|
229
|
+
if (read <= 0)
|
|
230
|
+
return { state: 'read' };
|
|
231
|
+
offset += read;
|
|
232
|
+
}
|
|
233
|
+
const after = fs_1.default.fstatSync(descriptor, { bigint: true });
|
|
234
|
+
if (!sameIdentity(file.stat, after))
|
|
235
|
+
return { state: 'unsafe' };
|
|
236
|
+
if (after.size > BigInt(max) || bytes.length > max)
|
|
237
|
+
return { state: 'limit' };
|
|
238
|
+
if (after.size !== opened.size)
|
|
239
|
+
return { state: 'unsafe' };
|
|
240
|
+
return { state: 'ok', bytes };
|
|
162
241
|
}
|
|
163
|
-
catch {
|
|
164
|
-
|
|
242
|
+
catch (error) {
|
|
243
|
+
const code = error.code;
|
|
244
|
+
return { state: code === 'ELOOP' || code === 'ENOENT' || code === 'ENOTDIR' ? 'unsafe' : 'read' };
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
if (descriptor !== undefined) {
|
|
248
|
+
try {
|
|
249
|
+
fs_1.default.closeSync(descriptor);
|
|
250
|
+
}
|
|
251
|
+
catch { /* best-effort close after a failed inspected read */ }
|
|
252
|
+
}
|
|
165
253
|
}
|
|
166
254
|
}
|
|
255
|
+
function executableInside(root, relative) {
|
|
256
|
+
const file = regularInside(root, relative);
|
|
257
|
+
return file && (process.platform === 'win32' ? /\.(?:exe|cmd|bat)$/i.test(file.path) : (file.stat.mode & 73n) !== 0n) ? file.path : undefined;
|
|
258
|
+
}
|
|
167
259
|
function refs(value, known) { return allStrings(value) && value.every((id) => known.has(id)); }
|
|
168
260
|
function uniqueRefs(value, known) { return refs(value, known) && new Set(value).size === value.length; }
|
|
169
261
|
function unsafeCommand(value) { return /[\0\r\n]|[`$]|\$\(|\$\{|[;&|<>*?\[\]{}()]/.test(value); }
|
|
@@ -219,8 +311,8 @@ function checkMarkdown(text, slices) {
|
|
|
219
311
|
}
|
|
220
312
|
return undefined;
|
|
221
313
|
}
|
|
222
|
-
/**
|
|
223
|
-
function
|
|
314
|
+
/** Shared parser. A supplied snapshot is already descriptor-anchored by its caller. */
|
|
315
|
+
function validatePlan(planPath, cwd, snapshot) {
|
|
224
316
|
if (typeof planPath !== 'string' || planPath.length === 0)
|
|
225
317
|
throw new Error('planPath must be a non-empty path');
|
|
226
318
|
if (typeof cwd !== 'string' || cwd.length === 0)
|
|
@@ -238,43 +330,46 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
|
238
330
|
if (!inside(root, candidate))
|
|
239
331
|
return diagnostic('PLAN_PATH_UNSAFE', 'plan path must be inside cwd');
|
|
240
332
|
const relativePlan = path_1.default.relative(root, candidate).replace(/\\/g, '/');
|
|
241
|
-
const planFile = regularInside(root, relativePlan);
|
|
242
|
-
if (!planFile)
|
|
243
|
-
return diagnostic('PLAN_PATH_UNSAFE', 'plan path must be a contained regular non-symlink file');
|
|
244
333
|
let bytes;
|
|
245
|
-
|
|
246
|
-
if (
|
|
334
|
+
if (snapshot) {
|
|
335
|
+
if (snapshot.length > MAX_PLAN)
|
|
247
336
|
return diagnostic('PLAN_LIMIT', 'plan exceeds maximum size');
|
|
248
|
-
bytes =
|
|
337
|
+
bytes = Buffer.from(snapshot);
|
|
249
338
|
}
|
|
250
|
-
|
|
251
|
-
|
|
339
|
+
else {
|
|
340
|
+
const planFile = regularInside(root, relativePlan);
|
|
341
|
+
if (!planFile)
|
|
342
|
+
return diagnostic('PLAN_PATH_UNSAFE', 'plan path must be a contained regular non-symlink file');
|
|
343
|
+
const planRead = readInspected(planFile, MAX_PLAN);
|
|
344
|
+
if (planRead.state === 'unsafe')
|
|
345
|
+
return diagnostic('PLAN_PATH_UNSAFE', 'plan path changed after inspection');
|
|
346
|
+
if (planRead.state === 'limit')
|
|
347
|
+
return diagnostic('PLAN_LIMIT', 'plan exceeds maximum size');
|
|
348
|
+
if (planRead.state === 'read')
|
|
349
|
+
return diagnostic('PLAN_READ', 'plan cannot be read');
|
|
350
|
+
bytes = planRead.bytes;
|
|
252
351
|
}
|
|
253
352
|
let text;
|
|
254
353
|
try {
|
|
255
|
-
text = normalizeLineEndings(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
354
|
+
text = normalizeLineEndings(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes));
|
|
256
355
|
}
|
|
257
356
|
catch {
|
|
258
357
|
return diagnostic('PLAN_ENCODING', 'plan must be valid UTF-8');
|
|
259
358
|
}
|
|
260
359
|
const starts = count(text, START);
|
|
261
360
|
const ends = count(text, END);
|
|
262
|
-
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);
|
|
263
362
|
if (!signaled)
|
|
264
|
-
return markerlessSchemaClassification(text) ?? { state: '
|
|
265
|
-
|
|
363
|
+
return markerlessSchemaClassification(text) ?? { state: 'migration-required', reason: 'unmarked-plan' };
|
|
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)
|
|
266
369
|
return markerlessSchemaClassification(text) ?? diagnostic('PLAN_MARKERS', 'compact markers must occur once in order');
|
|
267
|
-
if (
|
|
268
|
-
const partial = starts === 1 ? text.slice(text.indexOf(START) + START.length, ends ? text.indexOf(END) : undefined).trim() : '';
|
|
269
|
-
try {
|
|
270
|
-
const candidate = (0, json_1.parseJsonNoDuplicate)(partial);
|
|
271
|
-
if (object(candidate) && typeof candidate.schema === 'string' && Buffer.byteLength(candidate.schema, 'utf8') <= MAX_STRING && candidate.schema.startsWith('compact-slices/') && candidate.schema !== 'compact-slices/v1')
|
|
272
|
-
return unsupported(candidate.schema);
|
|
273
|
-
}
|
|
274
|
-
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)))
|
|
275
371
|
return diagnostic('PLAN_MARKERS', 'compact markers must occur once in order');
|
|
276
|
-
|
|
277
|
-
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));
|
|
278
373
|
if (Buffer.byteLength(body, 'utf8') > MAX_MANIFEST)
|
|
279
374
|
return diagnostic('PLAN_LIMIT', 'manifest exceeds maximum size');
|
|
280
375
|
let raw;
|
|
@@ -288,7 +383,7 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
|
288
383
|
return diagnostic('PLAN_SHAPE', 'manifest must be an object');
|
|
289
384
|
if (typeof raw.schema === 'string' && Buffer.byteLength(raw.schema, 'utf8') > MAX_STRING)
|
|
290
385
|
return diagnostic('PLAN_LIMIT', 'schema exceeds maximum string size');
|
|
291
|
-
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))
|
|
292
387
|
return unsupported(raw.schema);
|
|
293
388
|
if (!exact(raw, ['schema', 'planId', 'requirements', 'sources', 'commands', 'slices', 'closureCommands']))
|
|
294
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');
|
|
@@ -298,9 +393,9 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
|
298
393
|
return diagnostic('PLAN_LIMIT', 'manifest exceeds collection limits');
|
|
299
394
|
if (typeof raw.planId === 'string' && Buffer.byteLength(raw.planId, 'utf8') > MAX_STRING)
|
|
300
395
|
return diagnostic('PLAN_LIMIT', 'plan id exceeds maximum string size');
|
|
301
|
-
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)
|
|
302
397
|
return diagnostic('PLAN_SHAPE', 'manifest scalar fields are invalid');
|
|
303
|
-
if (raw.requirements.some((id) => !
|
|
398
|
+
if (raw.requirements.some((id) => !validRequirementId(id)) || !allStrings(raw.closureCommands) || new Set(raw.closureCommands).size !== raw.closureCommands.length)
|
|
304
399
|
return diagnostic('PLAN_SHAPE', 'manifest arrays or identifiers are invalid');
|
|
305
400
|
const sourceIds = new Set();
|
|
306
401
|
for (const source of raw.sources) {
|
|
@@ -310,18 +405,16 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
|
310
405
|
const file = regularInside(root, source.path);
|
|
311
406
|
if (!file)
|
|
312
407
|
return diagnostic('PLAN_SOURCE_UNSAFE', 'source path must be a contained regular non-symlink file');
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
catch {
|
|
408
|
+
const sourceRead = readInspected(file, MAX_SOURCE);
|
|
409
|
+
if (sourceRead.state === 'unsafe')
|
|
410
|
+
return diagnostic('PLAN_SOURCE_UNSAFE', 'source path changed after inspection');
|
|
411
|
+
if (sourceRead.state === 'limit')
|
|
412
|
+
return diagnostic('PLAN_SOURCE_LIMIT', 'source exceeds maximum size');
|
|
413
|
+
if (sourceRead.state === 'read')
|
|
320
414
|
return diagnostic('PLAN_SOURCE_UNSAFE', 'source cannot be read');
|
|
321
|
-
}
|
|
322
415
|
let contents;
|
|
323
416
|
try {
|
|
324
|
-
contents = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
417
|
+
contents = new TextDecoder('utf-8', { fatal: true }).decode(sourceRead.bytes);
|
|
325
418
|
}
|
|
326
419
|
catch {
|
|
327
420
|
return diagnostic('PLAN_SOURCE_ENCODING', 'source must be valid UTF-8');
|
|
@@ -332,7 +425,7 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
|
332
425
|
const commandIds = new Set();
|
|
333
426
|
const closureIds = new Set(raw.closureCommands);
|
|
334
427
|
for (const command of raw.commands) {
|
|
335
|
-
if (!exact(command, ['id', 'program', 'args', 'covers']) || !validId(command.id) || commandIds.has(command.id) || typeof command.program !== 'string' || command.program.length === 0 || Buffer.byteLength(command.program, 'utf8') > MAX_STRING || !allStrings(command.args) || command.args.length > 128 || !allStrings(command.covers) || new Set(command.covers).size !== command.covers.length || (command.covers.length === 0 && !closureIds.has(command.id)) || command.covers.some((id) => !raw.requirements.includes(id)))
|
|
428
|
+
if (!exact(command, ['id', 'program', 'args', 'covers']) || !validId(command.id) || commandIds.has(command.id) || typeof command.program !== 'string' || command.program.length === 0 || Buffer.byteLength(command.program, 'utf8') > MAX_STRING || !allStrings(command.args) || command.args.length > 128 || !allStrings(command.covers) || new Set(command.covers).size !== command.covers.length || (command.covers.length === 0 && !closureIds.has(command.id)) || command.covers.some((id) => !validRequirementId(id) || !raw.requirements.includes(id)))
|
|
336
429
|
return diagnostic('PLAN_COMMAND_SHAPE', 'command fields are invalid');
|
|
337
430
|
commandIds.add(command.id);
|
|
338
431
|
const program = command.program;
|
|
@@ -343,8 +436,9 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
|
343
436
|
}
|
|
344
437
|
const sliceIds = new Set();
|
|
345
438
|
const owners = new Map();
|
|
439
|
+
const sliceFields = ['id', 'title', 'requirements', 'dependsOn', 'sectionAnchor', 'sources', 'redCommands', 'greenCommands', 'reviewEvidence', 'risk', 'fallback'];
|
|
346
440
|
for (const slice of raw.slices) {
|
|
347
|
-
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')
|
|
348
442
|
return diagnostic('PLAN_SLICE_SHAPE', 'slice fields are invalid');
|
|
349
443
|
sliceIds.add(slice.id);
|
|
350
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'))
|
|
@@ -389,5 +483,24 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
|
389
483
|
const markdown = checkMarkdown(text, slices);
|
|
390
484
|
if (markdown)
|
|
391
485
|
return markdown;
|
|
392
|
-
|
|
486
|
+
const report = {
|
|
487
|
+
state: 'valid', schema: raw.schema,
|
|
488
|
+
planDigest: crypto_1.default.createHash('sha256').update(text, 'utf8').digest('hex'),
|
|
489
|
+
executionDigest: (0, identity_1.executionPlanDigest)(text),
|
|
490
|
+
manifest: raw, executionMode: executionModeFromValidatedText(text),
|
|
491
|
+
};
|
|
492
|
+
freezeJson(report);
|
|
493
|
+
verifiedValidReports.add(report);
|
|
494
|
+
verifiedSnapshots.set(report, text);
|
|
495
|
+
return report;
|
|
496
|
+
}
|
|
497
|
+
/** Read-only validator for a bounded compact-slices/v1 Markdown plan. */
|
|
498
|
+
function validatePlanFile(planPath, cwd = process.cwd()) {
|
|
499
|
+
return validatePlan(planPath, cwd);
|
|
500
|
+
}
|
|
501
|
+
/** Validate an already-authenticated plan snapshot without reopening planPath. */
|
|
502
|
+
function validatePlanSnapshot(planPath, cwd, text) {
|
|
503
|
+
if (typeof text !== 'string')
|
|
504
|
+
throw new Error('plan snapshot must be text');
|
|
505
|
+
return validatePlan(planPath, cwd, Buffer.from(text, 'utf8'));
|
|
393
506
|
}
|
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;
|
|
@@ -17,6 +17,22 @@ describe('exec-wrapper', () => {
|
|
|
17
17
|
let dir;
|
|
18
18
|
beforeEach(() => { dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wrap-')); });
|
|
19
19
|
afterEach(() => { fs_1.default.rmSync(dir, { recursive: true, force: true }); });
|
|
20
|
+
test.each(['../escape', '..\\escape', '/escape', 'C:\\escape', '', '.', '..', 'sentinel\n', 'x'.repeat(129)])('rejects unsafe sidecar components before writes (%#)', async (value) => {
|
|
21
|
+
for (const helper of [exec_wrapper_1.claimPath, exec_wrapper_1.identityPath, exec_wrapper_1.resultPath, exec_wrapper_1.logPath, exec_wrapper_1.replayVerdict]) {
|
|
22
|
+
expect(() => helper(dir, value, 'nonce')).toThrow(/sidecar/);
|
|
23
|
+
expect(() => helper(dir, 'job', value)).toThrow(/sidecar/);
|
|
24
|
+
}
|
|
25
|
+
await expect((0, exec_wrapper_1.runExecWrapper)({ logsRoot: path_1.default.join(dir, 'unused'), jobId: value, nonce: 'nonce', argv: ['node', '-e', 'process.exit(0)'], cwd: '.' })).rejects.toThrow(/sidecar/);
|
|
26
|
+
expect(fs_1.default.readdirSync(dir)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
test.each([
|
|
29
|
+
{ argv: ['node'], cwd: '../escape' },
|
|
30
|
+
{ argv: [undefined], cwd: '.' },
|
|
31
|
+
{ argv: ['node', 'sentinel\u0000'], cwd: '.' },
|
|
32
|
+
])('rejects invalid execution inputs before sidecar writes (%#)', async (inputs) => {
|
|
33
|
+
await expect((0, exec_wrapper_1.runExecWrapper)({ logsRoot: path_1.default.join(dir, 'unused'), jobId: 'job', nonce: 'nonce', repoRoot: dir, ...inputs })).rejects.toThrow();
|
|
34
|
+
expect(fs_1.default.readdirSync(dir)).toEqual([]);
|
|
35
|
+
});
|
|
20
36
|
test('claim + identity sidecar + resultado terminal atomico (R1.8)', async () => {
|
|
21
37
|
const out = await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job1', nonce: 'nonceA', argv: ['node', '-e', 'setTimeout(()=>process.exit(0), 300)'], cwd: '.' });
|
|
22
38
|
expect(out.exitCode).toBe(0);
|