agentic-workflow-manager 8.0.0 → 8.1.1

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 (28) hide show
  1. package/dist/scripts/sensor-support-matrix.js +110 -12
  2. package/dist/scripts/support-matrix.js +4 -1
  3. package/dist/src/commands/preflight/index.js +4 -1
  4. package/dist/src/commands/sensors/compatibility/contract.js +142 -21
  5. package/dist/src/commands/sensors/compatibility/discovery.js +1 -1
  6. package/dist/src/commands/sensors/compatibility/live.js +40 -7
  7. package/dist/src/commands/sensors/compatibility/manifest.js +13 -3
  8. package/dist/src/commands/sensors/compatibility/materialize.js +9 -1
  9. package/dist/src/commands/sensors/compatibility/probe.js +9 -9
  10. package/dist/src/commands/sensors/compatibility/resolve.js +3 -0
  11. package/dist/src/commands/sensors/exec.js +106 -12
  12. package/dist/src/commands/sensors/init.js +6 -3
  13. package/dist/src/commands/sensors/run.js +1 -1
  14. package/dist/src/commands/sensors/status.js +1 -1
  15. package/dist/tests/commands/sensors/compatibility/contract.test.js +95 -0
  16. package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
  17. package/dist/tests/commands/sensors/compatibility/manifest.test.js +9 -0
  18. package/dist/tests/commands/sensors/compatibility/materialize.test.js +19 -0
  19. package/dist/tests/commands/sensors/compatibility/probe.test.js +10 -0
  20. package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
  21. package/dist/tests/commands/sensors/compatibility/resolve.test.js +8 -0
  22. package/dist/tests/commands/sensors/exec-windows.test.js +58 -10
  23. package/dist/tests/commands/sensors/exec.test.js +51 -3
  24. package/dist/tests/commands/sensors/init.test.js +72 -0
  25. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +26 -0
  26. package/dist/tests/structural/r3-cli-major-version.test.js +9 -7
  27. package/dist/tests/structural/support-matrix-is-current.test.js +78 -6
  28. package/package.json +3 -2
@@ -3,7 +3,9 @@ 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.R3_PREPUBLICATION_FIXTURE_PURPOSE = exports.R3_PREPUBLICATION_FIXTURE_RELATIVE_PATH = exports.SENSOR_DOC_PATH = exports.SENSOR_PACKS = exports.SENSOR_END_MARKER = exports.SENSOR_BEGIN_MARKER = void 0;
6
+ exports.R3_PUBLISHED_REGISTRY_COMMIT = exports.R3_PUBLISHED_REGISTRY_TAG = exports.R3_PREPUBLICATION_FIXTURE_PURPOSE = exports.R3_PREPUBLICATION_FIXTURE_RELATIVE_PATH = exports.SENSOR_DOC_PATH = exports.SENSOR_PACKS = exports.SENSOR_END_MARKER = exports.SENSOR_BEGIN_MARKER = void 0;
7
+ exports.extractPublishedSupportMetadata = extractPublishedSupportMetadata;
8
+ exports.verifyPublishedRegistryIdentity = verifyPublishedRegistryIdentity;
7
9
  exports.parseRegistrySensorPacks = parseRegistrySensorPacks;
8
10
  exports.renderSensorSupportMatrix = renderSensorSupportMatrix;
9
11
  exports.spliceSensorSupportMatrix = spliceSensorSupportMatrix;
@@ -15,6 +17,7 @@ exports.registryRootFromArgs = registryRootFromArgs;
15
17
  */
16
18
  const fs_1 = __importDefault(require("fs"));
17
19
  const path_1 = __importDefault(require("path"));
20
+ const child_process_1 = require("child_process");
18
21
  const contract_1 = require("../src/commands/sensors/compatibility/contract");
19
22
  exports.SENSOR_BEGIN_MARKER = '<!-- BEGIN GENERATED: sensor-pack-support -->';
20
23
  exports.SENSOR_END_MARKER = '<!-- END GENERATED: sensor-pack-support -->';
@@ -27,6 +30,66 @@ exports.R3_PREPUBLICATION_FIXTURE_RELATIVE_PATH = 'tests/fixtures/sensor-support
27
30
  * never an invented description of an unpublished registry release.
28
31
  */
29
32
  exports.R3_PREPUBLICATION_FIXTURE_PURPOSE = 'R3 pre-publication contract fixture';
33
+ exports.R3_PUBLISHED_REGISTRY_TAG = 'v2.0.0';
34
+ exports.R3_PUBLISHED_REGISTRY_COMMIT = 'c35c087a0801c0b4e69e0a4ac3eafef9ecdf37cd';
35
+ /**
36
+ * Extract the registry-owned certification matrix without copying or rewording it.
37
+ * `SUPPORT.md` is generated by the registry from its manifests and frozen tool pins;
38
+ * this consumer must preserve that published evidence rather than infer certification
39
+ * from a compatibility range.
40
+ */
41
+ function extractPublishedSupportMetadata(source) {
42
+ if (typeof source !== 'string' || source.length === 0) {
43
+ throw new Error('published sensor support metadata must be non-empty text');
44
+ }
45
+ const begin = '<!-- BEGIN GENERATED: sensor-pack-support -->';
46
+ const end = '<!-- END GENERATED: sensor-pack-support -->';
47
+ const starts = source.split(begin).length - 1;
48
+ const ends = source.split(end).length - 1;
49
+ if (starts !== 1 || ends !== 1) {
50
+ throw new Error('published sensor support metadata requires exactly one generated marker pair');
51
+ }
52
+ const start = source.indexOf(begin) + begin.length;
53
+ const finish = source.indexOf(end);
54
+ if (finish <= start)
55
+ throw new Error('published sensor support metadata has invalid marker order');
56
+ const metadata = source.slice(start, finish).trim();
57
+ if (!metadata.includes('| Pack | Sensor | Variant | Tool | Certified range | Supported OS | OS certification evidence | Status | Evidence |')) {
58
+ throw new Error('published sensor support metadata lacks the certification table');
59
+ }
60
+ if (!metadata.includes('Status: `certified` has a matching frozen tool pin;')) {
61
+ throw new Error('published sensor support metadata lacks certification status semantics');
62
+ }
63
+ return metadata;
64
+ }
65
+ function exactArgument(argv, flag) {
66
+ const index = argv.indexOf(flag);
67
+ if (index === -1 || argv[index + 1] === undefined || argv[index + 1].startsWith('--') || index !== argv.lastIndexOf(flag)) {
68
+ throw new Error(`sensor support matrix requires exactly one ${flag} <value>`);
69
+ }
70
+ return argv[index + 1];
71
+ }
72
+ /** Refuse a mutable sibling checkout: published evidence is valid only for this immutable release. */
73
+ function verifyPublishedRegistryIdentity(registryRoot, tag, commit) {
74
+ const root = registryPath(registryRoot);
75
+ if (tag !== exports.R3_PUBLISHED_REGISTRY_TAG || commit !== exports.R3_PUBLISHED_REGISTRY_COMMIT) {
76
+ throw new Error(`sensor support matrix requires published registry ${exports.R3_PUBLISHED_REGISTRY_TAG}@${exports.R3_PUBLISHED_REGISTRY_COMMIT}`);
77
+ }
78
+ const resolve = (revision) => {
79
+ try {
80
+ return (0, child_process_1.execFileSync)('git', ['-C', root, 'rev-parse', revision], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
81
+ }
82
+ catch {
83
+ throw new Error(`published registry root must be a git checkout at ${exports.R3_PUBLISHED_REGISTRY_TAG}`);
84
+ }
85
+ };
86
+ const head = resolve('HEAD');
87
+ const taggedCommit = resolve(`${tag}^{commit}`);
88
+ if (head !== commit)
89
+ throw new Error(`published registry HEAD ${head} does not match expected commit ${commit}`);
90
+ if (taggedCommit !== commit)
91
+ throw new Error(`published registry tag ${tag} resolves to ${taggedCommit}, expected commit ${commit}`);
92
+ }
30
93
  function registryPath(value) {
31
94
  if (typeof value !== 'string' || value.trim().length === 0 || value.includes('\0')) {
32
95
  throw new Error('sensor support matrix requires a non-empty --registry-root');
@@ -58,24 +121,47 @@ function parseRegistrySensorPacks(registryRoot) {
58
121
  function variants(pack) {
59
122
  return Object.entries(pack.sensors).flatMap(([sensor, definition]) => definition.variants.map((variant) => `\`${sensor}/${variant.id}\`: ${variant.requirements.tool} ${variant.requirements.toolRange}; ${variant.requirements.runtime} ${variant.requirements.runtimeRange}; certified ${variant.certifiedRange}`)).join('<br>');
60
123
  }
124
+ function publishedSupportMetadata(registryRoot) {
125
+ const file = path_1.default.join(registryRoot, 'sensor-packs', 'SUPPORT.md');
126
+ if (!fs_1.default.existsSync(file))
127
+ return null;
128
+ const stat = fs_1.default.lstatSync(file);
129
+ if (!stat.isFile() || stat.isSymbolicLink())
130
+ throw new Error(`published sensor support metadata must be a regular file: ${file}`);
131
+ return extractPublishedSupportMetadata(fs_1.default.readFileSync(file, 'utf8'));
132
+ }
61
133
  /** Pure renderer used by freshness tests and the documentation command. */
62
134
  function renderSensorSupportMatrix(registryRoot) {
135
+ const root = registryPath(registryRoot);
136
+ const publishedMetadata = publishedSupportMetadata(root);
63
137
  const lines = [
64
- `### Sensor-pack compatibility contract (${exports.R3_PREPUBLICATION_FIXTURE_PURPOSE})`,
138
+ publishedMetadata === null
139
+ ? `### Sensor-pack compatibility contract (${exports.R3_PREPUBLICATION_FIXTURE_PURPOSE})`
140
+ : '### Sensor-pack compatibility contract (published registry evidence)',
65
141
  '',
66
142
  '| Pack | Contract | Version-aware variants and certified ranges | Evidence status |',
67
143
  '|---|---|---|---|',
68
144
  ];
69
- for (const { name, parsed } of parseRegistrySensorPacks(registryRoot)) {
145
+ for (const { name, parsed } of parseRegistrySensorPacks(root)) {
70
146
  if (parsed.kind === 'legacy') {
71
147
  lines.push(`| \`${name}\` | legacy pack | No v2 variants declared | compatible-unverified — migrate the pack before claiming version certification |`);
72
148
  }
73
149
  else {
74
- lines.push(`| \`${name}\` | pack schema v2 | ${variants(parsed.pack)} | Fixture-declared ranges only; real-tool and OS certification awaits published registry release evidence |`);
150
+ lines.push(`| \`${name}\` | pack schema v2 | ${variants(parsed.pack)} | ${publishedMetadata === null ? 'Fixture-declared ranges only; real-tool and OS certification awaits published registry release evidence' : 'Published compatibility contract; certification evidence is reproduced below from the registry'} |`);
75
151
  }
76
152
  }
77
153
  lines.push('');
78
- lines.push('> Generated from the pinned R3 pre-publication contract fixture, not the published `awm-baseline-registry` manifests. **Do not edit by hand** — `npm run docs:matrix` regenerates this block. T13 verifies the actual registry tag and release evidence.');
154
+ if (publishedMetadata === null) {
155
+ lines.push('> Generated from the pinned R3 pre-publication contract fixture, not the published `awm-baseline-registry` manifests. **Do not edit by hand** — `npm run docs:matrix:prepublication` regenerates this fixture-only block. T13 verifies the actual registry tag and release evidence.');
156
+ }
157
+ else {
158
+ lines.push('');
159
+ lines.push('#### Published certification evidence');
160
+ lines.push('');
161
+ lines.push(publishedMetadata);
162
+ lines.push('');
163
+ lines.push('> Generated from the supplied published `awm-baseline-registry` root. Certification states, frozen pins, and OS evidence are registry-owned metadata reproduced verbatim; **do not edit by hand**.');
164
+ }
79
165
  return lines.join('\n');
80
166
  }
81
167
  function spliceSensorSupportMatrix(markdown, generated) {
@@ -83,20 +169,32 @@ function spliceSensorSupportMatrix(markdown, generated) {
83
169
  const end = markdown.indexOf(exports.SENSOR_END_MARKER);
84
170
  if (begin === -1 || end === -1 || end < begin)
85
171
  throw new Error(`support-matrix.md lacks ${exports.SENSOR_BEGIN_MARKER} / ${exports.SENSOR_END_MARKER}`);
86
- const eol = markdown.includes('\r\n') ? '\r\n' : '\n';
172
+ // Preserve the line ending of the block this renderer owns. Another generated
173
+ // block may legitimately have a different EOL during a Windows checkout.
174
+ const eol = markdown.slice(begin, end).includes('\r\n') ? '\r\n' : '\n';
87
175
  const block = generated.split('\n').join(eol);
88
176
  return markdown.slice(0, begin + exports.SENSOR_BEGIN_MARKER.length) + eol + eol + block + eol + eol + markdown.slice(end);
89
177
  }
90
178
  function registryRootFromArgs(argv) {
91
- const index = argv.indexOf('--registry-root');
92
- if (index === -1 || argv[index + 1] === undefined || argv[index + 1].startsWith('--') || index !== argv.lastIndexOf('--registry-root')) {
93
- throw new Error('sensor support matrix requires exactly one --registry-root <path>');
94
- }
95
- return registryPath(argv[index + 1]);
179
+ return registryPath(exactArgument(argv, '--registry-root'));
96
180
  }
97
181
  /* istanbul ignore next: command shell is covered through exported functions. */
98
182
  if (require.main === module) {
99
- const root = registryRootFromArgs(process.argv.slice(2));
183
+ const argv = process.argv.slice(2);
184
+ const root = registryRootFromArgs(argv);
185
+ const stdout = argv.includes('--stdout');
186
+ if (argv.includes('--prepublication-fixture')) {
187
+ if (argv.filter((value) => value === '--prepublication-fixture').length !== 1 || argv.filter((value) => value === '--stdout').length > 1 || (argv.length !== 3 && argv.length !== 4)) {
188
+ throw new Error('sensor support matrix fixture mode accepts only --registry-root <path> --prepublication-fixture [--stdout]');
189
+ }
190
+ }
191
+ else {
192
+ verifyPublishedRegistryIdentity(root, exactArgument(argv, '--registry-tag'), exactArgument(argv, '--registry-commit'));
193
+ }
194
+ if (stdout) {
195
+ process.stdout.write(`${renderSensorSupportMatrix(root)}\n`);
196
+ process.exit(0);
197
+ }
100
198
  const current = fs_1.default.readFileSync(exports.SENSOR_DOC_PATH, 'utf8');
101
199
  fs_1.default.writeFileSync(exports.SENSOR_DOC_PATH, spliceSensorSupportMatrix(current, renderSensorSupportMatrix(root)), 'utf8');
102
200
  process.stdout.write('support-matrix.md sensor-pack evidence regenerated from registry manifests\n');
@@ -144,7 +144,10 @@ function spliceGenerated(markdown, generated) {
144
144
  if (begin === -1 || end === -1 || end < begin) {
145
145
  throw new Error(`support-matrix.md no tiene los marcadores ${exports.BEGIN_MARKER} / ${exports.END_MARKER}`);
146
146
  }
147
- const eol = markdown.includes('\r\n') ? '\r\n' : '\n';
147
+ // Each renderer owns only its marked block. A document can temporarily contain
148
+ // another generated block with different line endings, so using the first CRLF
149
+ // anywhere in the file would rewrite this block and create CI-only drift.
150
+ const eol = markdown.slice(begin, end).includes('\r\n') ? '\r\n' : '\n';
148
151
  const block = generated.split('\n').join(eol);
149
152
  return markdown.slice(0, begin + exports.BEGIN_MARKER.length)
150
153
  + eol + eol + block + eol + eol
@@ -49,7 +49,10 @@ function registerPreflightCommand(program) {
49
49
  const report = await (0, checks_1.preflight)(opts.cwd ?? process.cwd());
50
50
  process.stdout.write(opts.json ? JSON.stringify(report, null, 2) + '\n' : formatReport(report));
51
51
  const code = exitCodeFor(report);
52
+ // `process.exit()` may truncate the JSON written immediately above when
53
+ // stdout is a pipe (CI, an API consumer, or a shell capture). Preserve
54
+ // the semantic exit code while allowing Node to flush the report.
52
55
  if (code !== 0)
53
- process.exit(code);
56
+ process.exitCode = code;
54
57
  });
55
58
  }
@@ -3,13 +3,18 @@ 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.parseSemgrepPolicy = parseSemgrepPolicy;
7
+ exports.resolveSemgrepPolicy = resolveSemgrepPolicy;
6
8
  exports.parseStructuredCommand = parseStructuredCommand;
7
9
  exports.assertNoEqualPriorityOverlap = assertNoEqualPriorityOverlap;
8
10
  exports.parseSensorPack = parseSensorPack;
9
11
  const semver_1 = __importDefault(require("semver"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
10
14
  const contract_1 = require("../coverage/contract");
11
15
  const PACK_SCHEMA_VERSION = 2;
12
16
  const SHELL_EXECUTABLES = new Set(['sh', 'bash', 'cmd', 'powershell']);
17
+ const PACKAGE_MANAGERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
13
18
  const ALLOWED_PROBES = new Set([
14
19
  'version',
15
20
  'eslint-print-config',
@@ -18,6 +23,9 @@ const ALLOWED_PROBES = new Set([
18
23
  'package-script-present',
19
24
  'config-present',
20
25
  ]);
26
+ function normalizedPackageManager(value) {
27
+ return value.toLowerCase().replace(/\.exe$/, '');
28
+ }
21
29
  function isRecord(value) {
22
30
  return typeof value === 'object' && value !== null && !Array.isArray(value);
23
31
  }
@@ -62,9 +70,72 @@ function stringArray(value, source, location) {
62
70
  invalid(source, `${location} must be a nonempty array`);
63
71
  return value.map((item, index) => text(item, source, `${location}[${index}]`));
64
72
  }
73
+ function assetArray(value, source, location, allowEmpty = false) {
74
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0))
75
+ invalid(source, `${location} must be a ${allowEmpty ? '' : 'nonempty '}array`);
76
+ return value.map((item, index) => asset(item, source, `${location}[${index}]`));
77
+ }
78
+ const SEMGREP_POLICY_REF = 'shared/semgrep-policy.json';
79
+ const MAX_POLICY_BYTES = 64 * 1024;
80
+ function contained(root, candidate) {
81
+ const prefix = root.endsWith(path_1.default.sep) ? root : root + path_1.default.sep;
82
+ return candidate.startsWith(prefix);
83
+ }
84
+ function parseSemgrepPolicy(input, source) {
85
+ const value = record(input, source, 'Semgrep policy');
86
+ fields(value, ['tool', 'toolRange', 'runtime', 'runtimeRange', 'probe'], source, 'Semgrep policy');
87
+ if (value.tool !== 'semgrep' || value.runtime !== 'python' || value.probe !== 'semgrep-validate') {
88
+ invalid(source, 'Semgrep policy must declare semgrep, python, and semgrep-validate');
89
+ }
90
+ const toolRange = text(value.toolRange, source, 'Semgrep policy.toolRange');
91
+ const runtimeRange = text(value.runtimeRange, source, 'Semgrep policy.runtimeRange');
92
+ if (semver_1.default.validRange(toolRange) === null || semver_1.default.validRange(runtimeRange) === null)
93
+ invalid(source, 'Semgrep policy ranges must be valid semver ranges');
94
+ return { tool: 'semgrep', toolRange, runtime: 'python', runtimeRange, probe: 'semgrep-validate' };
95
+ }
96
+ function resolveSemgrepPolicy(policyRef, source, location = 'policy') {
97
+ if (policyRef !== SEMGREP_POLICY_REF)
98
+ invalid(source, `${location}.policyRef must be the contained AWM-owned ${SEMGREP_POLICY_REF}`);
99
+ if (typeof source !== 'string' || !path_1.default.isAbsolute(source) || path_1.default.normalize(source) !== source || path_1.default.basename(source) !== 'pack.json') {
100
+ invalid(source, `${location}.policyRef requires an absolute pack.json source`);
101
+ }
102
+ const packRoot = path_1.default.dirname(source);
103
+ const sensorPacksRoot = path_1.default.dirname(packRoot);
104
+ const candidate = path_1.default.join(sensorPacksRoot, ...SEMGREP_POLICY_REF.split('/'));
105
+ if (!contained(sensorPacksRoot, candidate))
106
+ invalid(source, `${location}.policyRef escapes sensor-packs root`);
107
+ let stat;
108
+ try {
109
+ stat = fs_1.default.lstatSync(candidate);
110
+ }
111
+ catch {
112
+ invalid(source, `${location}.policyRef does not resolve to a regular policy file`);
113
+ }
114
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_POLICY_BYTES)
115
+ invalid(source, `${location}.policyRef must resolve to a bounded regular policy file`);
116
+ let realRoot;
117
+ let realPolicy;
118
+ try {
119
+ realRoot = fs_1.default.realpathSync(sensorPacksRoot);
120
+ realPolicy = fs_1.default.realpathSync(candidate);
121
+ }
122
+ catch {
123
+ invalid(source, `${location}.policyRef cannot be canonicalized`);
124
+ }
125
+ if (!contained(realRoot, realPolicy))
126
+ invalid(source, `${location}.policyRef escapes sensor-packs root`);
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(fs_1.default.readFileSync(realPolicy, 'utf8'));
130
+ }
131
+ catch {
132
+ invalid(source, `${location}.policyRef must contain valid JSON`);
133
+ }
134
+ return parseSemgrepPolicy(parsed, realPolicy);
135
+ }
65
136
  function parseStructuredCommand(input, source) {
66
137
  const value = record(input, source, 'command');
67
- fields(value, ['executable', 'resolution', 'args', 'fileInput'], source, 'command');
138
+ fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source, 'command');
68
139
  const executable = text(value.executable, source, 'command.executable');
69
140
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(executable) || SHELL_EXECUTABLES.has(executable.toLowerCase().replace(/\.exe$/, ''))) {
70
141
  invalid(source, 'command.executable must not be a shell or path');
@@ -78,6 +149,29 @@ function parseStructuredCommand(input, source) {
78
149
  invalid(source, `command.args[${index}] must not embed {files}`);
79
150
  }
80
151
  const command = { executable, resolution: value.resolution, args };
152
+ if ('pythonEnvironmentRoot' in value) {
153
+ if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv'))
154
+ invalid(source, 'command.pythonEnvironmentRoot must name the selected .venv or venv Python environment');
155
+ command.pythonEnvironmentRoot = value.pythonEnvironmentRoot;
156
+ }
157
+ const executablePackageManager = normalizedPackageManager(executable);
158
+ if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value))
159
+ invalid(source, 'command.packageManager is required for a package-manager executable');
160
+ if ('packageManager' in value) {
161
+ if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager)))
162
+ invalid(source, 'command.packageManager must be npm, pnpm, yarn, or bun');
163
+ const packageManager = normalizedPackageManager(value.packageManager);
164
+ if (packageManager !== executablePackageManager)
165
+ invalid(source, 'command.packageManager must match executable');
166
+ command.packageManager = packageManager;
167
+ }
168
+ if ('environment' in value) {
169
+ const environment = record(value.environment, source, 'command.environment');
170
+ fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source, 'command.environment');
171
+ if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1)
172
+ invalid(source, 'command.environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
173
+ command.environment = { ESLINT_USE_FLAT_CONFIG: environment.ESLINT_USE_FLAT_CONFIG };
174
+ }
81
175
  if ('fileInput' in value) {
82
176
  const fileInput = record(value.fileInput, source, 'command.fileInput');
83
177
  fields(fileInput, ['placeholder', 'extensions'], source, 'command.fileInput');
@@ -99,40 +193,61 @@ function parseStructuredCommand(input, source) {
99
193
  }
100
194
  function parseVariant(input, source, location) {
101
195
  const value = record(input, source, location);
102
- fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'assets', 'formatter', 'probe'], source, location);
196
+ fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'assets', 'formatter', 'probe', 'policyRef'], source, location);
103
197
  const certifiedRange = text(value.certifiedRange, source, `${location}.certifiedRange`);
104
198
  if (semver_1.default.validRange(certifiedRange) === null)
105
199
  invalid(source, `${location}.certifiedRange must be a valid semver range`);
106
200
  if (typeof value.priority !== 'number' || !Number.isSafeInteger(value.priority))
107
201
  invalid(source, `${location}.priority must be a safe integer`);
108
- const requirements = record(value.requirements, source, `${location}.requirements`);
109
- fields(requirements, ['tool', 'toolRange', 'runtime', 'runtimeRange', 'configFiles'], source, `${location}.requirements`);
110
- const toolRange = text(requirements.toolRange, source, `${location}.requirements.toolRange`);
111
- const runtimeRange = text(requirements.runtimeRange, source, `${location}.requirements.runtimeRange`);
202
+ const policy = 'policyRef' in value ? resolveSemgrepPolicy(value.policyRef, source, location) : undefined;
203
+ if (policy && ('requirements' in value || 'probe' in value))
204
+ invalid(source, `${location}.policyRef is authoritative; requirements and probe must not duplicate it`);
205
+ if (!policy && (!('requirements' in value) || !('probe' in value)))
206
+ invalid(source, `${location} requires requirements and probe when policyRef is absent`);
207
+ const requirements = policy ? undefined : record(value.requirements, source, `${location}.requirements`);
208
+ if (requirements)
209
+ fields(requirements, ['tool', 'toolRange', 'runtime', 'runtimeRange', 'configFiles'], source, `${location}.requirements`);
210
+ const toolRange = policy?.toolRange ?? text(requirements.toolRange, source, `${location}.requirements.toolRange`);
211
+ const runtimeRange = policy?.runtimeRange ?? text(requirements.runtimeRange, source, `${location}.requirements.runtimeRange`);
112
212
  if (semver_1.default.validRange(toolRange) === null || semver_1.default.validRange(runtimeRange) === null)
113
213
  invalid(source, `${location}.requirements ranges must be valid semver ranges`);
114
- const probe = record(value.probe, source, `${location}.probe`);
115
- fields(probe, ['kind'], source, `${location}.probe`);
116
- if (typeof probe.kind !== 'string' || !ALLOWED_PROBES.has(probe.kind))
117
- invalid(source, `${location}.probe.kind must be an allowed probe`);
214
+ const probe = policy ? undefined : record(value.probe, source, `${location}.probe`);
215
+ if (probe) {
216
+ fields(probe, ['kind'], source, `${location}.probe`);
217
+ if (typeof probe.kind !== 'string' || !ALLOWED_PROBES.has(probe.kind))
218
+ invalid(source, `${location}.probe.kind must be an allowed probe`);
219
+ }
118
220
  return {
119
221
  id: id(value.id, source, `${location}.id`),
120
222
  priority: value.priority,
121
223
  certifiedRange,
122
- requirements: { tool: text(requirements.tool, source, `${location}.requirements.tool`), toolRange, runtime: text(requirements.runtime, source, `${location}.requirements.runtime`), runtimeRange, ...('configFiles' in requirements ? { configFiles: stringArray(requirements.configFiles, source, `${location}.requirements.configFiles`).map((file, index) => asset(file, source, `${location}.requirements.configFiles[${index}]`)) } : {}) },
123
- assets: stringArray(value.assets, source, `${location}.assets`).map((entry, index) => asset(entry, source, `${location}.assets[${index}]`)),
224
+ requirements: policy
225
+ ? { tool: policy.tool, toolRange, runtime: policy.runtime, runtimeRange }
226
+ : { tool: text(requirements.tool, source, `${location}.requirements.tool`), toolRange, runtime: text(requirements.runtime, source, `${location}.requirements.runtime`), runtimeRange, ...('configFiles' in requirements ? { configFiles: stringArray(requirements.configFiles, source, `${location}.requirements.configFiles`).map((file, index) => asset(file, source, `${location}.requirements.configFiles[${index}]`)) } : {}) },
227
+ assets: assetArray(value.assets, source, `${location}.assets`, true),
124
228
  formatter: text(value.formatter, source, `${location}.formatter`),
125
- probe: { kind: probe.kind },
229
+ probe: { kind: policy?.probe ?? probe.kind },
230
+ ...(policy ? { policyRef: SEMGREP_POLICY_REF } : {}),
126
231
  command: parseStructuredCommand(value.command, source),
127
232
  };
128
233
  }
129
- function assertNoEqualPriorityOverlap(variants) {
130
- if (!Array.isArray(variants) || variants.length === 0)
131
- throw new Error('variants must be a nonempty array');
132
- const parsed = variants.map((variant, index) => parseVariant(variant, 'public overlap validator', `variants[${index}]`));
234
+ function parseHardening(input, source) {
235
+ const value = record(input, source, 'hardening');
236
+ const names = Object.keys(value);
237
+ if (names.length === 0)
238
+ invalid(source, 'hardening must be nonempty when declared');
239
+ const hardening = {};
240
+ for (const name of names) {
241
+ const entry = record(value[name], source, `hardening.${name}`);
242
+ fields(entry, ['assets'], source, `hardening.${name}`);
243
+ hardening[id(name, source, 'hardening id')] = { assets: assetArray(entry.assets, source, `hardening.${name}.assets`) };
244
+ }
245
+ return hardening;
246
+ }
247
+ function assertNoEqualPriorityOverlapParsed(parsed) {
133
248
  for (let left = 0; left < parsed.length; left++) {
134
249
  const first = parsed[left];
135
- for (let right = left + 1; right < variants.length; right++) {
250
+ for (let right = left + 1; right < parsed.length; right++) {
136
251
  const second = parsed[right];
137
252
  const toolRangesIntersect = semver_1.default.intersects(first.requirements.toolRange, second.requirements.toolRange);
138
253
  const runtimeRangesIntersect = semver_1.default.intersects(first.requirements.runtimeRange, second.requirements.runtimeRange);
@@ -142,6 +257,11 @@ function assertNoEqualPriorityOverlap(variants) {
142
257
  }
143
258
  }
144
259
  }
260
+ function assertNoEqualPriorityOverlap(variants) {
261
+ if (!Array.isArray(variants) || variants.length === 0)
262
+ throw new Error('variants must be a nonempty array');
263
+ assertNoEqualPriorityOverlapParsed(variants.map((variant, index) => parseVariant(variant, 'public overlap validator', `variants[${index}]`)));
264
+ }
145
265
  function parseSensor(input, source, location, variantIds) {
146
266
  const value = record(input, source, location);
147
267
  fields(value, ['applicability', 'variants', 'fast'], source, location);
@@ -154,7 +274,7 @@ function parseSensor(input, source, location, variantIds) {
154
274
  variantIds.add(variant.id);
155
275
  }
156
276
  try {
157
- assertNoEqualPriorityOverlap(variants);
277
+ assertNoEqualPriorityOverlapParsed(variants);
158
278
  }
159
279
  catch (error) {
160
280
  invalid(source, `${location}.variants ${error instanceof Error ? error.message : 'overlap validation failed'}`);
@@ -234,7 +354,7 @@ function parseSensorPack(input, source) {
234
354
  const value = record(input, source, 'root');
235
355
  if (!('schemaVersion' in value))
236
356
  return { kind: 'legacy', pack: parseLegacyPack(value, source) };
237
- fields(value, ['schemaVersion', 'name', 'description', 'detects', 'sensors', 'coverage'], source, 'root');
357
+ fields(value, ['schemaVersion', 'name', 'description', 'detects', 'sensors', 'coverage', 'hardening'], source, 'root');
238
358
  if (value.schemaVersion !== PACK_SCHEMA_VERSION)
239
359
  invalid(source, `unsupported pack schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2; upgrade or migrate the pack`);
240
360
  const sensorsInput = record(value.sensors, source, 'sensors');
@@ -252,10 +372,11 @@ function parseSensorPack(input, source) {
252
372
  catch (error) {
253
373
  invalid(source, `coverage.${error instanceof Error ? error.message.replace(/^.*?: /, '') : 'is invalid'}`);
254
374
  }
375
+ const hardening = 'hardening' in value ? parseHardening(value.hardening, source) : undefined;
255
376
  return { kind: 'v2', pack: {
256
377
  schemaVersion: PACK_SCHEMA_VERSION,
257
378
  name: id(value.name, source, 'name'), description: text(value.description, source, 'description'), detects: stringArray(value.detects, source, 'detects'),
258
- sensors,
379
+ sensors, ...(hardening ? { hardening } : {}),
259
380
  coverage,
260
381
  } };
261
382
  }
@@ -200,7 +200,7 @@ function discoverProjectEvidence(cwd, pack, dependencies = {}) {
200
200
  const sitePackages = environment ? pythonSitePackages(root, environment.rootParts, targetPlatform) : [];
201
201
  const toolVersions = Object.fromEntries([...tools].sort().map(tool => [tool, pythonToolVersion(root, sitePackages, tool) ?? installedPackageVersion(root, tool)]));
202
202
  return {
203
- cwd: root, os: targetPlatform, runtimeVersions: { node: process.versions.node ?? null, ...(environment ? { python: environment.runtimeVersion } : {}) }, declaredToolRanges, toolVersions,
203
+ cwd: root, os: targetPlatform, runtimeVersions: { node: process.versions.node ?? null, ...(environment ? { python: environment.runtimeVersion } : {}) }, pythonEnvironmentRoot: environment?.rootParts[0] ?? null, declaredToolRanges, toolVersions,
204
204
  packageManager: declaredManager ?? (lockManagers.size === 1 ? [...lockManagers][0] : null), packageManagerConflict: lockManagers.size > 1,
205
205
  scripts, configFiles, paths: [...new Set([...(safeFile(root, 'package.json') ? ['package.json'] : []), ...locks, ...configFiles])].sort(),
206
206
  };
@@ -11,13 +11,42 @@ const discovery_1 = require("./discovery");
11
11
  const resolve_1 = require("./resolve");
12
12
  const probe_1 = require("./probe");
13
13
  const path_1 = __importDefault(require("path"));
14
+ /**
15
+ * Python package metadata is evidence only for a contained virtual environment.
16
+ * A v2 command that names a Python-runtime tool must therefore resolve through
17
+ * that same environment — never a same-named executable inherited from PATH.
18
+ *
19
+ * Keep this at the live pack boundary so the command used by probing, init's
20
+ * materialized manifest, and `sensors run` is one identical structured command.
21
+ */
22
+ function containedRuntimeCommand(variant, pythonEnvironmentRoot) {
23
+ if (variant.requirements.runtime !== 'python')
24
+ return variant;
25
+ return {
26
+ ...variant,
27
+ command: {
28
+ ...variant.command,
29
+ resolution: 'python-environment',
30
+ ...(pythonEnvironmentRoot ? { pythonEnvironmentRoot } : {}),
31
+ },
32
+ };
33
+ }
34
+ function bindContainedRuntimeCommands(pack, pythonEnvironmentRoot) {
35
+ return {
36
+ ...pack,
37
+ sensors: Object.fromEntries(Object.entries(pack.sensors).map(([name, sensor]) => [
38
+ name,
39
+ { ...sensor, variants: sensor.variants.map(variant => containedRuntimeCommand(variant, pythonEnvironmentRoot)) },
40
+ ])),
41
+ };
42
+ }
14
43
  /**
15
44
  * Re-resolve a v2 pack from the configured registry and current project evidence.
16
45
  * Manifest evidence is intentionally not an input: it is an init-time trace, not a
17
46
  * live certification source. Probes are bounded and structured through the shared
18
47
  * compatibility probe runner.
19
48
  */
20
- async function resolveLiveCompatibility(cwd, packName, registryRoot) {
49
+ async function resolveLiveCompatibility(cwd, packName, registryRoot, options = {}) {
21
50
  if (typeof cwd !== 'string' || cwd.trim() === '')
22
51
  throw new Error('cwd must be a non-empty path');
23
52
  if (typeof packName !== 'string' || !/^[a-z][a-z0-9-]*$/.test(packName))
@@ -31,29 +60,33 @@ async function resolveLiveCompatibility(cwd, packName, registryRoot) {
31
60
  const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
32
61
  if (parsed.kind !== 'v2')
33
62
  throw new Error(`sensor pack "${packName}" does not provide a v2 compatibility contract`);
34
- return resolveParsedPackCompatibility(cwd, parsed.pack);
63
+ return resolveParsedPackCompatibility(cwd, parsed.pack, options);
35
64
  }
36
65
  /** Resolve a pre-parsed v2 pack. Init uses this when given an explicit registry root. */
37
- async function resolveParsedPackCompatibility(cwd, pack) {
66
+ async function resolveParsedPackCompatibility(cwd, pack, options = {}) {
38
67
  if (typeof cwd !== 'string' || cwd.trim() === '')
39
68
  throw new Error('cwd must be a non-empty path');
40
69
  if (!pack || typeof pack !== 'object' || pack.schemaVersion !== 2)
41
70
  throw new Error('pack must be a parsed v2 sensor pack');
42
71
  const evidence = (0, discovery_1.discoverProjectEvidence)(cwd, pack);
43
- const initial = (0, resolve_1.resolveProjectCompatibility)(pack, evidence).sensors;
72
+ const executionPack = bindContainedRuntimeCommands(pack, evidence.pythonEnvironmentRoot);
73
+ const resolutionEvidence = { ...evidence, ...(options.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}) };
74
+ const initial = (0, resolve_1.resolveProjectCompatibility)(executionPack, resolutionEvidence).sensors;
44
75
  const sensors = {};
45
- for (const [name, sensor] of Object.entries(pack.sensors)) {
76
+ for (const [name, sensor] of Object.entries(executionPack.sensors)) {
46
77
  const base = initial[name];
47
78
  const variant = base.variantId === null ? null : sensor.variants.find(candidate => candidate.id === base.variantId) ?? null;
48
79
  const probe = variant
49
80
  ? await (0, probe_1.runCompatibilityProbe)(variant.probe, {
50
81
  cwd,
51
82
  toolExecutable: variant.command.executable,
83
+ toolResolution: variant.command.resolution,
84
+ pythonEnvironmentRoot: variant.command.pythonEnvironmentRoot,
52
85
  configFiles: evidence.configFiles,
53
86
  scripts: evidence.scripts,
54
87
  })
55
88
  : null;
56
- sensors[name] = (0, resolve_1.resolveProjectCompatibility)({ ...pack, sensors: { [name]: sensor } }, { ...evidence, probe: probe ?? undefined }).sensors[name];
89
+ sensors[name] = (0, resolve_1.resolveProjectCompatibility)({ ...executionPack, sensors: { [name]: sensor } }, { ...resolutionEvidence, probe: probe ?? undefined }).sensors[name];
57
90
  }
58
- return { pack, sensors };
91
+ return { pack: executionPack, sensors };
59
92
  }
@@ -135,7 +135,7 @@ function parseLegacyManifest(value, source) {
135
135
  }
136
136
  function parseV2Sensor(input, source, location) {
137
137
  const value = record(input, source, location);
138
- fields(value, ['enabled', 'fast', 'variantId', 'command', 'assets', 'initializedCompatibility'], source, location);
138
+ fields(value, ['enabled', 'fast', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
139
139
  if (typeof value.enabled !== 'boolean')
140
140
  invalid(source, `${location}.enabled must be a boolean`);
141
141
  const sensor = {
@@ -152,7 +152,12 @@ function parseV2Sensor(input, source, location) {
152
152
  if (sensor.initializedCompatibility.state === 'certified' && !semver_1.default.satisfies(sensor.initializedCompatibility.toolVersion, sensor.initializedCompatibility.certifiedRange))
153
153
  invalid(source, `${location}.initializedCompatibility.toolVersion must satisfy certifiedRange`);
154
154
  if ('assets' in value)
155
- sensor.assets = stringArray(value.assets, source, `${location}.assets`, false).map((entry, index) => asset(entry, source, `${location}.assets[${index}]`));
155
+ sensor.assets = stringArray(value.assets, source, `${location}.assets`, true).map((entry, index) => asset(entry, source, `${location}.assets[${index}]`));
156
+ if ('policyRef' in value) {
157
+ if (value.policyRef !== 'shared/semgrep-policy.json')
158
+ invalid(source, `${location}.policyRef must be the contained AWM-owned shared/semgrep-policy.json`);
159
+ sensor.policyRef = value.policyRef;
160
+ }
156
161
  if ('fast' in value) {
157
162
  if (typeof value.fast !== 'boolean')
158
163
  invalid(source, `${location}.fast must be a boolean`);
@@ -167,7 +172,7 @@ function provenanceRoot(value, source) {
167
172
  return parsed;
168
173
  }
169
174
  function parseV2Manifest(value, source) {
170
- fields(value, ['schemaVersion', 'pack', 'registryRoot', 'sensors', 'concurrency'], source, 'root');
175
+ fields(value, ['schemaVersion', 'pack', 'packSelection', 'registryRoot', 'sensors', 'concurrency'], source, 'root');
171
176
  if (value.schemaVersion !== 2)
172
177
  invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2; upgrade or migrate the manifest`);
173
178
  const pack = id(value.pack, source, 'pack');
@@ -176,6 +181,11 @@ function parseV2Manifest(value, source) {
176
181
  for (const name of Object.keys(sensorsInput))
177
182
  sensors[id(name, source, 'sensor id')] = parseV2Sensor(sensorsInput[name], source, `sensors.${name}`);
178
183
  const manifest = { schemaVersion: 2, pack, sensors };
184
+ if ('packSelection' in value) {
185
+ if (value.packSelection !== 'explicit')
186
+ invalid(source, 'packSelection must be "explicit" when present');
187
+ manifest.packSelection = 'explicit';
188
+ }
179
189
  if ('registryRoot' in value)
180
190
  manifest.registryRoot = provenanceRoot(value.registryRoot, source);
181
191
  if ('concurrency' in value) {
@@ -7,6 +7,7 @@ exports.materializeResolvedSensors = materializeResolvedSensors;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const manifest_1 = require("./manifest");
10
+ const contract_1 = require("./contract");
10
11
  function stableId(value, label) {
11
12
  if (typeof value !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value))
12
13
  throw new Error(`${label} must be a stable lowercase id`);
@@ -81,7 +82,14 @@ function materializeResolvedSensors(input) {
81
82
  throw new Error('materialized sensor must be v2');
82
83
  sensors[name] = parsed.pack.sensors[name];
83
84
  }
84
- const manifest = { schemaVersion: 2, pack, sensors, ...(input.registryRoot ? { registryRoot: input.registryRoot } : {}) };
85
+ const manifest = { schemaVersion: 2, pack, sensors, ...(input.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}), ...(input.registryRoot ? { registryRoot: input.registryRoot } : {}) };
86
+ // A policy reference is deliberately not a materialized asset. Resolve it here
87
+ // from the registry-owned sibling only, so a manifest cannot turn arbitrary
88
+ // registry content into a project write through a cosmetic policy field.
89
+ for (const [name, sensor] of Object.entries(sensors)) {
90
+ if (sensor.policyRef)
91
+ (0, contract_1.resolveSemgrepPolicy)(sensor.policyRef, path_1.default.join(packRoot, 'pack.json'), `sensors.${name}`);
92
+ }
85
93
  const selected = [...new Set(Object.values(sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
86
94
  const prior = priorAssets(projectRoot);
87
95
  const configured = [];