agentic-workflow-manager 8.0.0 → 8.1.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.
@@ -90,6 +90,7 @@ function collectSpawn(input, opts) {
90
90
  shell: false,
91
91
  cwd: opts.cwd,
92
92
  detached: !(0, paths_1.isWindowsNative)(),
93
+ ...(input.environment ? { env: { ...process.env, ...input.environment } } : {}),
93
94
  // stdin closed: a sensor must never block waiting for input, and the
94
95
  // EOF also tells watch-mode-capable tools (vitest, jest) to run once.
95
96
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -144,14 +145,72 @@ function collectSpawn(input, opts) {
144
145
  });
145
146
  }
146
147
  function validateStructuredCommand(command) {
147
- if (!command || typeof command !== 'object' || typeof command.executable !== 'string' || (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(command.executable) && !path_1.default.isAbsolute(command.executable))) {
148
+ if (!command || typeof command !== 'object' || typeof command.executable !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(command.executable)) {
148
149
  throw new Error('structured command executable must be a safe executable name');
149
150
  }
150
- if (!Array.isArray(command.args) || command.args.some(arg => typeof arg !== 'string' || /[\0\r\n]/.test(arg))) {
151
- throw new Error('structured command args must be an array of single-line strings without NUL');
151
+ const normalizedExecutable = command.executable.toLowerCase().replace(/\.exe$/, '');
152
+ if (new Set(['sh', 'bash', 'cmd', 'powershell']).has(normalizedExecutable)) {
153
+ throw new Error('structured command executable must not be a shell');
154
+ }
155
+ if (!Array.isArray(command.args) || command.args.length === 0) {
156
+ throw new Error('structured command args must be a nonempty array');
157
+ }
158
+ for (const [index, arg] of command.args.entries()) {
159
+ if (typeof arg !== 'string' || arg.trim() === '' || /[\0\r\n]/.test(arg)) {
160
+ throw new Error(`structured command args[${index}] must be a nonempty single-line string without NUL`);
161
+ }
162
+ if (arg.includes('{files}') && arg !== '{files}') {
163
+ throw new Error(`structured command args[${index}] must not embed {files}`);
164
+ }
152
165
  }
153
166
  if (!['node-modules-bin', 'python-environment', 'path'].includes(command.resolution))
154
167
  throw new Error('structured command resolution is unsupported');
168
+ if (command.pythonEnvironmentRoot !== undefined && (command.resolution !== 'python-environment' || (command.pythonEnvironmentRoot !== '.venv' && command.pythonEnvironmentRoot !== 'venv'))) {
169
+ throw new Error('structured command pythonEnvironmentRoot must name the selected .venv or venv Python environment');
170
+ }
171
+ if (command.resolution === 'python-environment' && command.pythonEnvironmentRoot === undefined)
172
+ throw new Error('python environment command requires a discovery-bound contained local environment root');
173
+ const packageManagers = new Set(['npm', 'pnpm', 'yarn', 'bun']);
174
+ const normalizedPackageManager = typeof command.packageManager === 'string' ? command.packageManager.toLowerCase().replace(/\.exe$/, '') : undefined;
175
+ if (packageManagers.has(normalizedExecutable) && normalizedPackageManager !== normalizedExecutable)
176
+ throw new Error('structured command packageManager must explicitly match its executable');
177
+ if (normalizedPackageManager !== undefined && !packageManagers.has(normalizedPackageManager))
178
+ throw new Error('structured command packageManager is unsupported');
179
+ if (normalizedPackageManager !== undefined && normalizedPackageManager !== normalizedExecutable)
180
+ throw new Error('structured command packageManager must explicitly match its executable');
181
+ if (command.environment !== undefined) {
182
+ const environment = command.environment;
183
+ if (!environment || typeof environment !== 'object' || Array.isArray(environment) || Object.keys(environment).length !== 1 || !Object.prototype.hasOwnProperty.call(environment, 'ESLINT_USE_FLAT_CONFIG')) {
184
+ throw new Error('structured command environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
185
+ }
186
+ const flatConfig = environment.ESLINT_USE_FLAT_CONFIG;
187
+ if (flatConfig !== 'true' && flatConfig !== 'false')
188
+ throw new Error('structured command environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
189
+ }
190
+ const fileArguments = command.args.filter(arg => arg === '{files}').length;
191
+ if (command.fileInput === undefined) {
192
+ if (fileArguments !== 0)
193
+ throw new Error('structured command {files} argument requires fileInput');
194
+ return;
195
+ }
196
+ const fileInput = command.fileInput;
197
+ if (!fileInput || typeof fileInput !== 'object' || Array.isArray(fileInput) || Object.keys(fileInput).length !== 2 || !Object.prototype.hasOwnProperty.call(fileInput, 'placeholder') || !Object.prototype.hasOwnProperty.call(fileInput, 'extensions')) {
198
+ throw new Error('structured command fileInput must contain only placeholder and extensions');
199
+ }
200
+ const { placeholder, extensions } = fileInput;
201
+ if (placeholder !== '{files}')
202
+ throw new Error('structured command fileInput.placeholder must be {files}');
203
+ if (!Array.isArray(extensions) || extensions.length === 0 || extensions.some(extension => typeof extension !== 'string' || extension.trim() === '' || /[\0\r\n]/.test(extension) || !/^\.[A-Za-z0-9]+$/.test(extension))) {
204
+ throw new Error('structured command fileInput.extensions must be a nonempty array of extensions');
205
+ }
206
+ if (fileArguments !== 1)
207
+ throw new Error('structured command fileInput requires exactly one {files} argument');
208
+ }
209
+ function safeWindowsExecutableExtensions() {
210
+ return (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
211
+ .split(';')
212
+ .map(ext => ext.toLowerCase())
213
+ .filter(ext => ext === '.exe' || ext === '.com');
155
214
  }
156
215
  function regularFile(candidate) {
157
216
  try {
@@ -162,6 +221,25 @@ function regularFile(candidate) {
162
221
  return false;
163
222
  }
164
223
  }
224
+ /** Resolve a Python environment executable only when every selected path
225
+ * component is a non-symbolic-link local entry. A leaf-only lstat follows a
226
+ * linked .venv/venv ancestor and can otherwise execute outside the project. */
227
+ function localPythonEnvironmentExecutable(cwd, environmentRoot, executable) {
228
+ const parts = [environmentRoot, (0, paths_1.isWindowsNative)() ? 'Scripts' : 'bin', executable];
229
+ let candidate = cwd;
230
+ try {
231
+ for (const part of parts) {
232
+ candidate = path_1.default.join(candidate, part);
233
+ const stat = fs_1.default.lstatSync(candidate);
234
+ if (stat.isSymbolicLink())
235
+ return null;
236
+ }
237
+ return fs_1.default.lstatSync(candidate).isFile() ? candidate : null;
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
165
243
  function containedPath(root, candidate) {
166
244
  const relative = path_1.default.relative(root, candidate);
167
245
  return relative !== '' && !relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative);
@@ -202,7 +280,9 @@ function resolveStructuredExecutable(command, cwd) {
202
280
  return local;
203
281
  throw new Error('node_modules executable is not a contained local file');
204
282
  }
205
- const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map(ext => ext.toLowerCase()).filter(ext => ext === '.exe' || ext === '.com');
283
+ const extensions = safeWindowsExecutableExtensions();
284
+ if (extensions.length === 0)
285
+ throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
206
286
  const candidates = [path_1.default.join(bin, command.executable), ...extensions.map(extension => path_1.default.join(bin, command.executable + extension))];
207
287
  for (const candidate of candidates) {
208
288
  const lower = candidate.toLowerCase();
@@ -218,8 +298,24 @@ function resolveStructuredExecutable(command, cwd) {
218
298
  if (command.resolution === 'python-environment') {
219
299
  if (path_1.default.isAbsolute(command.executable))
220
300
  throw new Error('python environment executable must be a contained local name');
221
- candidates.push(path_1.default.join(cwd, '.venv', (0, paths_1.isWindowsNative)() ? 'Scripts' : 'bin', command.executable));
222
- candidates.push(path_1.default.join(cwd, 'venv', (0, paths_1.isWindowsNative)() ? 'Scripts' : 'bin', command.executable));
301
+ if (!(0, paths_1.isWindowsNative)()) {
302
+ const local = localPythonEnvironmentExecutable(cwd, command.pythonEnvironmentRoot, command.executable);
303
+ if (local)
304
+ return local;
305
+ throw new Error('python environment executable is not a contained local regular file');
306
+ }
307
+ const extensions = safeWindowsExecutableExtensions();
308
+ if (extensions.length === 0)
309
+ throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
310
+ for (const executable of [command.executable, ...extensions.map(extension => command.executable + extension)]) {
311
+ const lower = executable.toLowerCase();
312
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
313
+ throw new Error('structured commands cannot execute Windows command wrappers');
314
+ const local = localPythonEnvironmentExecutable(cwd, command.pythonEnvironmentRoot, executable);
315
+ if (local && extensions.some(extension => lower.endsWith(extension)))
316
+ return local;
317
+ }
318
+ throw new Error('python environment executable is not a contained local regular file');
223
319
  }
224
320
  else if (path_1.default.isAbsolute(command.executable))
225
321
  candidates.push(command.executable);
@@ -231,11 +327,11 @@ function resolveStructuredExecutable(command, cwd) {
231
327
  const found = candidates.find(regularFile);
232
328
  if (found)
233
329
  return found;
234
- if (command.resolution === 'python-environment')
235
- throw new Error('python environment executable is not a contained local regular file');
236
330
  return command.executable;
237
331
  }
238
- const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map(ext => ext.toLowerCase()).filter(ext => ext === '.exe' || ext === '.com');
332
+ const extensions = safeWindowsExecutableExtensions();
333
+ if (extensions.length === 0)
334
+ throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
239
335
  for (const candidate of candidates) {
240
336
  const lower = candidate.toLowerCase();
241
337
  if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
@@ -246,14 +342,12 @@ function resolveStructuredExecutable(command, cwd) {
246
342
  if (regularFile(candidate + extension))
247
343
  return candidate + extension;
248
344
  }
249
- if (command.resolution === 'python-environment')
250
- throw new Error('python environment executable is not a contained local regular file');
251
345
  return command.executable;
252
346
  }
253
347
  /** Execute a v2 command as an executable plus literal argv; it never starts a shell. */
254
348
  function runStructuredCommand(command, opts) {
255
349
  validateStructuredCommand(command);
256
- return collectSpawn({ executable: resolveStructuredExecutable(command, opts.cwd), args: command.args, shell: false }, opts);
350
+ return collectSpawn({ executable: resolveStructuredExecutable(command, opts.cwd), args: command.args, shell: false, environment: command.environment }, opts);
257
351
  }
258
352
  /** Legacy sensor strings intentionally retain their documented shell semantics. */
259
353
  function runCommand(cmd, opts) {
@@ -253,9 +253,11 @@ async function initSensors(opts = {}) {
253
253
  if (opts.registryRoot) {
254
254
  const resolvedV2 = readV2Pack(resolvedPack, opts.registryRoot);
255
255
  if (resolvedV2) {
256
- const compatibility = (await (0, live_1.resolveParsedPackCompatibility)(cwd, resolvedV2.pack)).sensors;
256
+ const packSelection = opts.pack ? 'explicit' : undefined;
257
+ const live = await (0, live_1.resolveParsedPackCompatibility)(cwd, resolvedV2.pack, { packSelection });
258
+ const compatibility = live.sensors;
257
259
  const sensors = {};
258
- for (const [name, sensor] of Object.entries(resolvedV2.pack.sensors)) {
260
+ for (const [name, sensor] of Object.entries(live.pack.sensors)) {
259
261
  const resolved = compatibility[name];
260
262
  const variant = resolved.variantId === null ? null : sensor.variants.find(candidate => candidate.id === resolved.variantId) ?? null;
261
263
  // Unresolved states do not receive an arbitrary command. They remain
@@ -273,6 +275,7 @@ async function initSensors(opts = {}) {
273
275
  variantId: variant.id,
274
276
  command: variant.command,
275
277
  assets: variant.assets,
278
+ ...(variant.policyRef ? { policyRef: variant.policyRef } : {}),
276
279
  initializedCompatibility: migratedOverride
277
280
  ? { ...resolved, state: 'compatible-unverified', reason: 'legacy custom command requires explicit v2 migration' }
278
281
  : resolved,
@@ -281,7 +284,7 @@ async function initSensors(opts = {}) {
281
284
  }
282
285
  const materialized = (0, materialize_1.materializeResolvedSensors)({
283
286
  projectRoot: cwd, packRoot: resolvedV2.packRoot,
284
- pack: resolvedPack, registryRoot: opts.registryRoot, sensors, configure,
287
+ pack: resolvedPack, ...(packSelection ? { packSelection } : {}), registryRoot: opts.registryRoot, sensors, configure,
285
288
  });
286
289
  return { detection, ...materialized,
287
290
  compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
@@ -73,7 +73,7 @@ async function resolveLiveV2(cwd, manifest) {
73
73
  if (manifest.kind !== 'v2')
74
74
  return null;
75
75
  try {
76
- return await (0, live_1.resolveLiveCompatibility)(cwd, manifest.pack.pack, manifest.pack.registryRoot);
76
+ return await (0, live_1.resolveLiveCompatibility)(cwd, manifest.pack.pack, manifest.pack.registryRoot, { packSelection: manifest.pack.packSelection });
77
77
  }
78
78
  catch {
79
79
  return null;
@@ -68,7 +68,7 @@ async function computeSensorStatus(cwd = process.cwd()) {
68
68
  const checks = {};
69
69
  let live;
70
70
  try {
71
- live = await (0, live_1.resolveLiveCompatibility)(cwd, parsed.pack.pack, parsed.pack.registryRoot);
71
+ live = await (0, live_1.resolveLiveCompatibility)(cwd, parsed.pack.pack, parsed.pack.registryRoot, { packSelection: parsed.pack.packSelection });
72
72
  }
73
73
  catch (error) {
74
74
  const detail = `compatibility revalidation failed: ${error instanceof Error ? error.message : String(error)}`;
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const contract_1 = require("../../../../src/commands/sensors/compatibility/contract");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
4
10
  const coverage = {
5
11
  schemaVersion: 1,
6
12
  classes: {
@@ -37,9 +43,92 @@ function validPack() {
37
43
  };
38
44
  }
39
45
  describe('sensor pack v2 contract', () => {
46
+ it('derives Semgrep compatibility from a contained shared policy reference', () => {
47
+ const sensorPacks = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-policy-'));
48
+ const packDir = path_1.default.join(sensorPacks, 'python');
49
+ try {
50
+ fs_1.default.mkdirSync(path_1.default.join(sensorPacks, 'shared'), { recursive: true });
51
+ fs_1.default.mkdirSync(packDir);
52
+ fs_1.default.writeFileSync(path_1.default.join(sensorPacks, 'shared', 'semgrep-policy.json'), JSON.stringify({
53
+ tool: 'semgrep', toolRange: '>=1.0.0', runtime: 'python', runtimeRange: '>=3.9.0', probe: 'semgrep-validate',
54
+ }));
55
+ const pack = validPack();
56
+ pack.sensors.lint.variants[0] = {
57
+ id: 'semgrep-python', priority: 10, certifiedRange: '>=1.0.0', policyRef: 'shared/semgrep-policy.json',
58
+ command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml', '--json', '.'] },
59
+ assets: ['.semgrep.awm.yml'], formatter: 'semgrep',
60
+ };
61
+ const parsed = (0, contract_1.parseSensorPack)(pack, path_1.default.join(packDir, 'pack.json'));
62
+ expect(parsed).toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{
63
+ policyRef: 'shared/semgrep-policy.json', requirements: { tool: 'semgrep', runtime: 'python' }, probe: { kind: 'semgrep-validate' },
64
+ }] } } } });
65
+ }
66
+ finally {
67
+ fs_1.default.rmSync(sensorPacks, { recursive: true, force: true });
68
+ }
69
+ });
70
+ it('fails closed when a policy reference is not the AWM-owned shared Semgrep policy', () => {
71
+ const pack = validPack();
72
+ pack.sensors.lint.variants[0] = {
73
+ id: 'semgrep-python', priority: 10, certifiedRange: '>=1.0.0', policyRef: '../secret.json',
74
+ command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml', '--json', '.'] },
75
+ assets: ['.semgrep.awm.yml'], formatter: 'semgrep',
76
+ };
77
+ expect(() => (0, contract_1.parseSensorPack)(pack, '/tmp/sensor-packs/python/pack.json')).toThrow('policyRef');
78
+ });
40
79
  it('parses a valid versioned pack', () => {
41
80
  expect((0, contract_1.parseSensorPack)(validPack(), 'pack.json')).toMatchObject({ kind: 'v2', pack: validPack() });
42
81
  });
82
+ it('accepts an opt-in hardening asset while variants may require no assets', () => {
83
+ const pack = {
84
+ ...validPack(),
85
+ hardening: { 'typescript-strict': { assets: ['tsconfig.awm.json'] } },
86
+ sensors: {
87
+ lint: {
88
+ ...validPack().sensors.lint,
89
+ variants: [{ ...validPack().sensors.lint.variants[0], assets: [] }],
90
+ },
91
+ },
92
+ };
93
+ expect((0, contract_1.parseSensorPack)(pack, 'pack.json')).toMatchObject({
94
+ kind: 'v2',
95
+ pack: { hardening: { 'typescript-strict': { assets: ['tsconfig.awm.json'] } }, sensors: { lint: { variants: [{ assets: [] }] } } },
96
+ });
97
+ });
98
+ it.each(['true', 'false'])('accepts the exact ESLINT_USE_FLAT_CONFIG=%s environment mapping', flatConfig => {
99
+ const command = {
100
+ executable: 'npm',
101
+ resolution: 'path',
102
+ args: ['run', 'lint'],
103
+ packageManager: 'npm',
104
+ environment: { ESLINT_USE_FLAT_CONFIG: flatConfig },
105
+ };
106
+ expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command }] } } }, 'pack.json'))
107
+ .toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ command }] } } } });
108
+ });
109
+ it.each([
110
+ { ESLINT_USE_FLAT_CONFIG: 'yes' },
111
+ { ESLINT_USE_FLAT_CONFIG: 'true', OTHER: 'value' },
112
+ ])('rejects an unknown or expanded ESLint environment mapping', environment => {
113
+ const command = { executable: 'npm', resolution: 'path', args: ['run', 'lint'], packageManager: 'npm', environment };
114
+ expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command }] } } }, 'pack.json'))
115
+ .toThrow('command.environment');
116
+ });
117
+ it('normalizes package-manager executable spelling before enforcing its explicit selection', () => {
118
+ const variant = validPack().sensors.lint.variants[0];
119
+ const withExecutable = (executable, packageManager) => ({
120
+ ...validPack(),
121
+ sensors: { lint: { ...validPack().sensors.lint, variants: [{
122
+ ...variant,
123
+ command: { executable, resolution: 'path', args: ['run', 'lint'], ...(packageManager === undefined ? {} : { packageManager }) },
124
+ }] } },
125
+ });
126
+ expect(() => (0, contract_1.parseSensorPack)(withExecutable('NPM'), 'pack.json')).toThrow('packageManager');
127
+ expect((0, contract_1.parseSensorPack)(withExecutable('npm.exe', 'NPM'), 'pack.json')).toMatchObject({
128
+ kind: 'v2', pack: { sensors: { lint: { variants: [{ command: { executable: 'npm.exe', packageManager: 'npm' } }] } } },
129
+ });
130
+ expect(() => (0, contract_1.parseSensorPack)(withExecutable('NPM', 'pnpm'), 'pack.json')).toThrow('match executable');
131
+ });
43
132
  it('keeps an unversioned pack on the legacy compatibility path', () => {
44
133
  const legacy = { name: 'legacy', sensors: {} };
45
134
  expect((0, contract_1.parseSensorPack)(legacy, 'pack.json')).toMatchObject({ kind: 'legacy', pack: { ...legacy, compatibility: { state: 'compatible-unverified' } } });
@@ -90,8 +179,14 @@ describe('sensor pack v2 contract', () => {
90
179
  [{ ...validPack(), sensors: { lint: { variants: [validPack().sensors.lint.variants[0], { ...validPack().sensors.lint.variants[0], id: 'eslint-9-next', certifiedRange: '>=9.1.0 <10.0.0' }] } } }, 'overlap'],
91
180
  [{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], assets: ['C:/secret'] }] } } }, 'asset'],
92
181
  [{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'cmd.exe', resolution: 'path', args: ['x'] } }] } } }, 'executable'],
182
+ [{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'npm', resolution: 'path', args: ['run', 'lint'] } }] } } }, 'packageManager'],
183
+ [{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'npm', resolution: 'path', args: ['run', 'lint'], packageManager: 'pnpm' } }] } } }, 'match executable'],
184
+ [{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { ...validPack().sensors.lint.variants[0].command, environment: { NODE_OPTIONS: '--require unsafe' } } }] } } }, 'environment'],
93
185
  [{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { ...validPack().sensors.lint.variants[0].command, args: ['prefix{files}'] } }] } } }, 'embed'],
94
186
  [{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], formatter: 'bad\nformat' }] } } }, 'formatter'],
187
+ [{ ...validPack(), hardening: { 'typescript-strict': { assets: ['../tsconfig.awm.json'] } } }, 'asset'],
188
+ [{ ...validPack(), hardening: { 'typescript-strict': { assets: [] } } }, 'hardening'],
189
+ [{ ...validPack(), hardening: { 'typescript-strict': { assets: ['tsconfig.awm.json'], command: {} } } }, 'unknown field'],
95
190
  ])('rejects malformed pack %j', (input, message) => {
96
191
  expect(() => (0, contract_1.parseSensorPack)(input, 'pack.json')).toThrow(message);
97
192
  });
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const live_1 = require("../../../../src/commands/sensors/compatibility/live");
10
+ const exec_1 = require("../../../../src/commands/sensors/exec");
11
+ const python_venv_fixture_1 = require("./python-venv-fixture");
12
+ const itPosix = process.platform !== 'win32' ? it : it.skip;
13
+ describe('resolveParsedPackCompatibility — contained Python commands', () => {
14
+ test.each([
15
+ ['linux', ['.venv', 'bin', 'semgrep']],
16
+ ['win32', ['.venv', 'Scripts', 'semgrep.exe']],
17
+ ])('creates a local Semgrep executable in the %s virtual-environment layout', (targetPlatform, executableParts) => {
18
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-venv-layout-'));
19
+ try {
20
+ (0, python_venv_fixture_1.createSemgrepVenvFixture)(root, targetPlatform);
21
+ expect(fs_1.default.lstatSync(path_1.default.join(root, ...executableParts)).isFile()).toBe(true);
22
+ }
23
+ finally {
24
+ fs_1.default.rmSync(root, { recursive: true, force: true });
25
+ }
26
+ });
27
+ it('resolves the Windows virtual-environment executable from Scripts instead of PATH', async () => {
28
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-windows-resolution-'));
29
+ const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-windows-global-'));
30
+ const originalPlatform = process.platform;
31
+ const savedPath = process.env.PATH;
32
+ const savedPathExt = process.env.PATHEXT;
33
+ try {
34
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
35
+ process.env.PATH = global;
36
+ process.env.PATHEXT = '.EXE';
37
+ (0, python_venv_fixture_1.createSemgrepVenvFixture)(root, 'win32');
38
+ fs_1.default.writeFileSync(path_1.default.join(global, 'semgrep.exe'), 'must-not-run');
39
+ await expect((0, exec_1.runStructuredCommand)({
40
+ executable: 'semgrep', resolution: 'python-environment', pythonEnvironmentRoot: '.venv', args: ['--version'],
41
+ }, { cwd: root, timeout: 5_000 })).resolves.toMatchObject({
42
+ code: 0,
43
+ stdout: expect.stringContaining(process.version),
44
+ });
45
+ }
46
+ finally {
47
+ Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
48
+ if (savedPath === undefined)
49
+ delete process.env.PATH;
50
+ else
51
+ process.env.PATH = savedPath;
52
+ if (savedPathExt === undefined)
53
+ delete process.env.PATHEXT;
54
+ else
55
+ process.env.PATHEXT = savedPathExt;
56
+ fs_1.default.rmSync(root, { recursive: true, force: true });
57
+ fs_1.default.rmSync(global, { recursive: true, force: true });
58
+ }
59
+ });
60
+ itPosix('binds Semgrep discovery, validation probe, and execution to the same local virtual-environment executable', async () => {
61
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-contained-'));
62
+ const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-global-'));
63
+ const savedPath = process.env.PATH;
64
+ try {
65
+ (0, python_venv_fixture_1.createSemgrepVenvFixture)(root);
66
+ fs_1.default.writeFileSync(path_1.default.join(root, '.venv', 'bin', 'semgrep'), '#!/bin/sh\nif [ "$1" = "--validate" ]; then echo local-semgrep; exit 0; fi\nexit 1\n', { mode: 0o755 });
67
+ fs_1.default.writeFileSync(path_1.default.join(global, 'semgrep'), '#!/bin/sh\necho global-semgrep\n', { mode: 0o755 });
68
+ process.env.PATH = global;
69
+ const pack = {
70
+ schemaVersion: 2,
71
+ name: 'python',
72
+ description: 'test',
73
+ detects: ['pyproject.toml'],
74
+ coverage: {},
75
+ sensors: {
76
+ security: {
77
+ applicability: { allFiles: ['pyproject.toml'] },
78
+ variants: [{
79
+ id: 'semgrep-1', priority: 1, certifiedRange: '>=1 <2',
80
+ requirements: { tool: 'semgrep', toolRange: '>=1 <2', runtime: 'python', runtimeRange: '>=3.12 <4' },
81
+ assets: [], formatter: 'semgrep', probe: { kind: 'semgrep-validate' },
82
+ command: { executable: 'semgrep', resolution: 'path', args: ['--validate'] },
83
+ }],
84
+ },
85
+ },
86
+ };
87
+ fs_1.default.writeFileSync(path_1.default.join(root, 'pyproject.toml'), '[project]\nname = "sample"\n');
88
+ const live = await (0, live_1.resolveParsedPackCompatibility)(root, pack);
89
+ const command = live.pack.sensors.security.variants[0].command;
90
+ expect(live.sensors.security).toMatchObject({ state: 'certified', toolVersion: '1.91.0' });
91
+ expect(command).toMatchObject({ executable: 'semgrep', resolution: 'python-environment' });
92
+ await expect((0, exec_1.runStructuredCommand)(command, { cwd: root, timeout: 5_000 })).resolves.toMatchObject({ code: 0, stdout: 'local-semgrep\n' });
93
+ }
94
+ finally {
95
+ process.env.PATH = savedPath;
96
+ fs_1.default.rmSync(root, { recursive: true, force: true });
97
+ fs_1.default.rmSync(global, { recursive: true, force: true });
98
+ }
99
+ });
100
+ it('never falls back from the discovered .venv to a sibling venv executable', async () => {
101
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-environment-identity-'));
102
+ try {
103
+ (0, python_venv_fixture_1.createSemgrepVenvFixture)(root, process.platform, '.venv', { executable: false });
104
+ (0, python_venv_fixture_1.createSemgrepVenvFixture)(root, process.platform, 'venv');
105
+ fs_1.default.writeFileSync(path_1.default.join(root, 'pyproject.toml'), '[project]\nname = "sample"\n');
106
+ const pack = {
107
+ schemaVersion: 2, name: 'python', description: 'test', detects: ['pyproject.toml'], coverage: {},
108
+ sensors: { security: { applicability: { allFiles: ['pyproject.toml'] }, variants: [{
109
+ id: 'semgrep-1', priority: 1, certifiedRange: '>=1 <2',
110
+ requirements: { tool: 'semgrep', toolRange: '>=1 <2', runtime: 'python', runtimeRange: '>=3.12 <4' },
111
+ assets: [], formatter: 'semgrep', probe: { kind: 'semgrep-validate' },
112
+ command: { executable: 'semgrep', resolution: 'path', args: ['--validate'] },
113
+ }] } },
114
+ };
115
+ const live = await (0, live_1.resolveParsedPackCompatibility)(root, pack);
116
+ expect(live.sensors.security).toMatchObject({ state: 'unverifiable', reason: 'probe-inconclusive', toolVersion: '1.91.0' });
117
+ expect(() => (0, exec_1.runStructuredCommand)(live.pack.sensors.security.variants[0].command, { cwd: root, timeout: 5_000 }))
118
+ .toThrow('python environment executable is not a contained local regular file');
119
+ }
120
+ finally {
121
+ fs_1.default.rmSync(root, { recursive: true, force: true });
122
+ }
123
+ });
124
+ });
@@ -35,6 +35,15 @@ describe('sensor manifest contract', () => {
35
35
  expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({ kind: 'v2', pack: manifest });
36
36
  expect(JSON.parse((0, manifest_1.serializeManifestV2)(manifest))).toEqual(manifest);
37
37
  });
38
+ it('persists only an explicit v2 pack selection as applicability provenance', () => {
39
+ const manifest = {
40
+ schemaVersion: 2, pack: 'generic', packSelection: 'explicit',
41
+ sensors: {},
42
+ };
43
+ expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({ kind: 'v2', pack: { packSelection: 'explicit' } });
44
+ expect(JSON.parse((0, manifest_1.serializeManifestV2)(manifest))).toEqual(manifest);
45
+ expect(() => (0, manifest_1.parseSensorManifest)({ ...manifest, packSelection: 'detected' }, 'sensors.json')).toThrow('packSelection');
46
+ });
38
47
  it('accepts optional contained assets and rejects traversal', () => {
39
48
  const sensor = { enabled: true, variantId: 'eslint-9', assets: ['eslint.config.awm.mjs'], command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, initializedCompatibility: { state: 'certified', reason: 'ok', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] } };
40
49
  expect((0, manifest_1.parseSensorManifest)({ schemaVersion: 2, pack: 'js-ts', sensors: { lint: sensor } }, 'sensors.json')).toMatchObject({ kind: 'v2' });
@@ -38,6 +38,25 @@ describe('materializeResolvedSensors', () => {
38
38
  expect(result.preserved).toEqual(['eslint.config.awm.mjs']);
39
39
  expect(fs_1.default.readFileSync(path_1.default.join(projectRoot, 'eslint.config.awm.mjs'), 'utf8')).toBe('owner content');
40
40
  });
41
+ it('revalidates a shared Semgrep policy reference without materializing the policy itself', () => {
42
+ const sensorPacks = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-materialize-policy-'));
43
+ const semgrepPack = path_1.default.join(sensorPacks, 'python');
44
+ try {
45
+ fs_1.default.mkdirSync(path_1.default.join(sensorPacks, 'shared'), { recursive: true });
46
+ fs_1.default.mkdirSync(semgrepPack);
47
+ fs_1.default.writeFileSync(path_1.default.join(semgrepPack, 'pack.json'), '{}');
48
+ fs_1.default.writeFileSync(path_1.default.join(semgrepPack, '.semgrep.awm.yml'), 'rules: []\n');
49
+ fs_1.default.writeFileSync(path_1.default.join(sensorPacks, 'shared', 'semgrep-policy.json'), JSON.stringify({ tool: 'semgrep', toolRange: '>=1.0.0', runtime: 'python', runtimeRange: '>=3.9.0', probe: 'semgrep-validate' }));
50
+ const result = (0, materialize_1.materializeResolvedSensors)({ projectRoot, packRoot: semgrepPack, pack: 'python', sensors: {
51
+ security: { enabled: true, variantId: 'semgrep-python', command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml', '--json', '.'] }, assets: ['.semgrep.awm.yml'], policyRef: 'shared/semgrep-policy.json', initializedCompatibility: { ...evidence, variantId: 'semgrep-python' } },
52
+ } });
53
+ expect(result.configured).toEqual(['.semgrep.awm.yml']);
54
+ expect(fs_1.default.existsSync(path_1.default.join(projectRoot, 'shared', 'semgrep-policy.json'))).toBe(false);
55
+ }
56
+ finally {
57
+ fs_1.default.rmSync(sensorPacks, { recursive: true, force: true });
58
+ }
59
+ });
41
60
  it('reports previous AWM assets as orphaned and never deletes them', () => {
42
61
  fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'));
43
62
  fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'sensors.json'), JSON.stringify({ schemaVersion: 2, pack: 'js-ts', sensors: {
@@ -24,4 +24,14 @@ describe('runCompatibilityProbe', () => {
24
24
  await (0, probe_1.runCompatibilityProbe)({ kind: 'semgrep-validate' }, evidence, fakeExecutor);
25
25
  expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: 'semgrep', resolution: 'python-environment' }), expect.any(Object));
26
26
  });
27
+ it('keeps Windows Semgrep validation on the discovered .venv executable', async () => {
28
+ await (0, probe_1.runCompatibilityProbe)({ kind: 'semgrep-validate' }, {
29
+ cwd: process.cwd(), toolExecutable: 'semgrep', toolResolution: 'python-environment', pythonEnvironmentRoot: '.venv',
30
+ }, fakeExecutor);
31
+ expect(fakeExecutor).toHaveBeenCalledWith({ executable: 'semgrep', resolution: 'python-environment', pythonEnvironmentRoot: '.venv', args: ['--validate'] }, expect.objectContaining({ cwd: process.cwd() }));
32
+ });
33
+ it.each(['mypy', 'ruff', 'pytest'])('probes a %s variant through the contained Python environment', async (toolExecutable) => {
34
+ await (0, probe_1.runCompatibilityProbe)({ kind: 'version' }, { ...evidence, toolExecutable, toolResolution: 'python-environment' }, fakeExecutor);
35
+ expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: toolExecutable, resolution: 'python-environment', args: ['--version'] }), expect.any(Object));
36
+ });
27
37
  });
@@ -0,0 +1,29 @@
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.createSemgrepVenvFixture = createSemgrepVenvFixture;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ /** Creates contained Semgrep metadata and the host-appropriate virtualenv executable. */
10
+ function createSemgrepVenvFixture(root, targetPlatform = process.platform, environmentRoot = '.venv', options = {}) {
11
+ if (typeof root !== 'string' || root.trim() === '')
12
+ throw new Error('fixture root must be a non-empty path');
13
+ const windows = targetPlatform === 'win32';
14
+ const sitePackages = windows
15
+ ? [environmentRoot, 'Lib', 'site-packages']
16
+ : [environmentRoot, 'lib', 'python3.12', 'site-packages'];
17
+ const metadata = path_1.default.join(root, ...sitePackages, 'semgrep-1.91.0.dist-info');
18
+ fs_1.default.mkdirSync(metadata, { recursive: true });
19
+ fs_1.default.writeFileSync(path_1.default.join(root, environmentRoot, 'pyvenv.cfg'), 'version = 3.12.4\n');
20
+ fs_1.default.writeFileSync(path_1.default.join(metadata, 'METADATA'), 'Name: semgrep\nVersion: 1.91.0\n');
21
+ if (options.executable === false)
22
+ return;
23
+ const executable = path_1.default.join(root, environmentRoot, windows ? 'Scripts' : 'bin', windows ? 'semgrep.exe' : 'semgrep');
24
+ fs_1.default.mkdirSync(path_1.default.dirname(executable), { recursive: true });
25
+ if (windows)
26
+ fs_1.default.copyFileSync(process.execPath, executable);
27
+ else
28
+ fs_1.default.writeFileSync(executable, '#!/bin/sh\necho local-semgrep\n', { mode: 0o755 });
29
+ }
@@ -12,6 +12,14 @@ const sensor = { applicability: { allFiles: ['package.json'] }, variants: [varia
12
12
  const evidence = (input = {}) => ({ paths: ['package.json'], applicable: true, packageManagerConflict: false, toolVersion: '10.4.1', runtimeVersion: '22.0.0', probe: { status: 'matched' }, ...input });
13
13
  const context = { pack: 'js-ts', sensor: 'lint' };
14
14
  describe('resolveSensorCompatibility', () => {
15
+ it('keeps an automatically selected generic pack not-applicable despite language markers', () => {
16
+ const resolved = (0, resolve_1.resolveSensorCompatibility)({ applicability: { kind: 'explicit-or-supported-language' }, variants: [variant('semgrep')] }, evidence({ applicable: undefined, paths: ['pyproject.toml'], toolVersion: '1.0.0', runtimeVersion: '3.12.0', probe: { status: 'matched' } }), { pack: 'generic', sensor: 'security' });
17
+ expect(resolved).toMatchObject({ state: 'not-applicable', reason: 'applicability-not-met' });
18
+ });
19
+ it('treats the persisted explicit generic pack selection as positive capability in project resolution', () => {
20
+ const resolved = (0, resolve_1.resolveProjectCompatibility)({ schemaVersion: 2, name: 'generic', sensors: { security: { applicability: { kind: 'explicit-or-supported-language' }, variants: [variant('semgrep')] } } }, evidence({ applicable: undefined, paths: [], packSelection: 'explicit' }));
21
+ expect(resolved.sensors.security.state).not.toBe('not-applicable');
22
+ });
15
23
  test.each([
16
24
  ['certified', evidence(), 'eslint-main'],
17
25
  ['compatible-unverified', evidence({ toolVersion: '11.0.0' }), 'eslint-main'],