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.
Files changed (107) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/job/exec-wrapper.js +53 -3
  3. package/dist/src/commands/job/gate.js +18 -0
  4. package/dist/src/commands/job/index.js +61 -0
  5. package/dist/src/commands/model-policy/index.js +73 -0
  6. package/dist/src/commands/plan/index.js +182 -7
  7. package/dist/src/commands/sensors/bootstrap.js +36 -5
  8. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  9. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  10. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  11. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  12. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  13. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  14. package/dist/src/commands/sensors/coverage/index.js +11 -2
  15. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  16. package/dist/src/commands/sensors/exec.js +16 -1
  17. package/dist/src/commands/sensors/migrate.js +62 -2
  18. package/dist/src/commands/sensors/prepare.js +2 -0
  19. package/dist/src/commands/sensors/result.js +2 -0
  20. package/dist/src/commands/sensors/run.js +64 -1
  21. package/dist/src/commands/sensors/status.js +6 -4
  22. package/dist/src/commands/watch/apply.js +32 -0
  23. package/dist/src/commands/watch/archive-unused.js +138 -0
  24. package/dist/src/commands/watch/generations.js +32 -9
  25. package/dist/src/commands/watch/index.js +108 -2
  26. package/dist/src/commands/watch/init.js +149 -18
  27. package/dist/src/commands/watch/supervisor.js +167 -13
  28. package/dist/src/commands/watch/teardown-driver.js +13 -13
  29. package/dist/src/commands/watch/track-state.js +59 -0
  30. package/dist/src/commands/watch/tracks.js +38 -102
  31. package/dist/src/core/admission/index.js +231 -0
  32. package/dist/src/core/admission/registry-contracts.js +290 -0
  33. package/dist/src/core/journal/fingerprint.js +35 -1
  34. package/dist/src/core/journal/paths.js +11 -0
  35. package/dist/src/core/journal/requests.js +1 -1
  36. package/dist/src/core/journal/store.js +146 -4
  37. package/dist/src/core/journal/types.js +57 -2
  38. package/dist/src/core/migration/historical-provenance.js +18 -0
  39. package/dist/src/core/migration/index.js +322 -0
  40. package/dist/src/core/model-policy/canonical.js +18 -0
  41. package/dist/src/core/model-policy/capabilities.js +197 -0
  42. package/dist/src/core/model-policy/capability-types.js +2 -0
  43. package/dist/src/core/model-policy/journal.js +124 -0
  44. package/dist/src/core/model-policy/paths.js +12 -0
  45. package/dist/src/core/model-policy/resolve.js +116 -0
  46. package/dist/src/core/model-policy/store.js +182 -0
  47. package/dist/src/core/model-policy/types.js +2 -0
  48. package/dist/src/core/model-policy/validate.js +112 -0
  49. package/dist/src/core/plan/identity.js +77 -0
  50. package/dist/src/core/plan/validate.js +191 -78
  51. package/dist/src/index.js +2 -0
  52. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  53. package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
  54. package/dist/tests/commands/job/gate.test.js +9 -0
  55. package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
  56. package/dist/tests/commands/job/routing.test.js +11 -0
  57. package/dist/tests/commands/model-policy/index.test.js +57 -0
  58. package/dist/tests/commands/pin.test.js +23 -23
  59. package/dist/tests/commands/plan/index.test.js +682 -14
  60. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  61. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  62. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  63. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  64. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  65. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  66. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  67. package/dist/tests/commands/sensors/exec.test.js +112 -0
  68. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  69. package/dist/tests/commands/sensors/run.test.js +15 -2
  70. package/dist/tests/commands/sensors/status.test.js +3 -3
  71. package/dist/tests/commands/watch/apply.test.js +12 -0
  72. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  73. package/dist/tests/commands/watch/generations.test.js +113 -4
  74. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  75. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  76. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  77. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  78. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  79. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  80. package/dist/tests/core/admission/index.test.js +140 -0
  81. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  82. package/dist/tests/core/journal/store.test.js +53 -0
  83. package/dist/tests/core/journal/types.test.js +7 -0
  84. package/dist/tests/core/migration/facts.test.js +289 -0
  85. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  86. package/dist/tests/core/model-policy/capabilities.test.js +365 -0
  87. package/dist/tests/core/model-policy/journal.test.js +46 -0
  88. package/dist/tests/core/model-policy/policy.test.js +47 -0
  89. package/dist/tests/core/model-policy/resolve.test.js +111 -0
  90. package/dist/tests/core/model-policy/store.test.js +177 -0
  91. package/dist/tests/core/plan/validate.test.js +360 -8
  92. package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
  93. package/dist/tests/integration/pack-e2e.test.js +4 -1
  94. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  95. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  96. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  97. package/dist/tests/providers/injection-config.test.js +14 -6
  98. package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
  99. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  100. package/dist/tests/structural/native-release-artifacts.test.js +30 -0
  101. package/package.json +4 -3
  102. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  103. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  104. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  105. package/prebuilds/linux-x64/secure_fs.node +0 -0
  106. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  107. 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 ID = /^[A-Z][A-Z0-9-]{0,63}$/;
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 objects = [];
41
- for (let start = 0; start < text.length && objects.length < 64; start += 1) {
42
- if (text[start] !== '{')
43
- continue;
44
- let depth = 0;
45
- let quoted = false;
46
- let escaped = false;
47
- let end = start;
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
- depth += 1;
65
- if (character === '}' && --depth === 0) {
66
- objects.push(text.slice(start, end + 1));
67
- start = end;
68
- break;
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 objects;
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' && ID.test(value); }
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
- if (fs_1.default.lstatSync(current).isSymbolicLink())
195
+ stat = fs_1.default.lstatSync(current, { bigint: true });
196
+ if (stat.isSymbolicLink())
147
197
  return undefined;
148
198
  }
149
- if (!fs_1.default.lstatSync(candidate).isFile())
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 executableInside(root, relative) {
159
- const file = regularInside(root, relative);
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
- return file && (process.platform === 'win32' ? /\.(?:exe|cmd|bat)$/i.test(file) : (fs_1.default.statSync(file).mode & 0o111) !== 0) ? file : undefined;
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
- return undefined;
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
- /** Read-only validator for a bounded compact-slices/v1 Markdown plan. */
223
- function validatePlanFile(planPath, cwd = process.cwd()) {
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
- try {
246
- if (fs_1.default.statSync(planFile).size > MAX_PLAN)
334
+ if (snapshot) {
335
+ if (snapshot.length > MAX_PLAN)
247
336
  return diagnostic('PLAN_LIMIT', 'plan exceeds maximum size');
248
- bytes = fs_1.default.readFileSync(planFile);
337
+ bytes = Buffer.from(snapshot);
249
338
  }
250
- catch {
251
- return diagnostic('PLAN_READ', 'plan cannot be read');
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: 'legacy' };
265
- if (starts === 0 && ends === 0)
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 (starts !== 1 || ends !== 1 || text.indexOf(START) > text.indexOf(END)) {
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) => !ID.test(id)) || !allStrings(raw.closureCommands) || new Set(raw.closureCommands).size !== raw.closureCommands.length)
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
- let bytes;
314
- try {
315
- if (fs_1.default.statSync(file).size > MAX_SOURCE)
316
- return diagnostic('PLAN_SOURCE_LIMIT', 'source exceeds maximum size');
317
- bytes = fs_1.default.readFileSync(file);
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, ['id', 'title', 'requirements', 'dependsOn', 'sectionAnchor', 'sources', 'redCommands', 'greenCommands', 'reviewEvidence', 'risk', 'fallback']) || !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) => !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')
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
- return { state: 'valid', schema: 'compact-slices/v1', manifest: raw };
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);