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.
- package/dist/src/commands/sensors/compatibility/contract.js +142 -21
- package/dist/src/commands/sensors/compatibility/discovery.js +1 -1
- package/dist/src/commands/sensors/compatibility/live.js +40 -7
- package/dist/src/commands/sensors/compatibility/manifest.js +13 -3
- package/dist/src/commands/sensors/compatibility/materialize.js +9 -1
- package/dist/src/commands/sensors/compatibility/probe.js +9 -9
- package/dist/src/commands/sensors/compatibility/resolve.js +3 -0
- package/dist/src/commands/sensors/exec.js +106 -12
- package/dist/src/commands/sensors/init.js +6 -3
- package/dist/src/commands/sensors/run.js +1 -1
- package/dist/src/commands/sensors/status.js +1 -1
- package/dist/tests/commands/sensors/compatibility/contract.test.js +95 -0
- package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +9 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +8 -0
- package/dist/tests/commands/sensors/exec-windows.test.js +58 -10
- package/dist/tests/commands/sensors/exec.test.js +51 -3
- package/dist/tests/commands/sensors/init.test.js +72 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +3 -3
- package/package.json +1 -1
|
@@ -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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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:
|
|
123
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 <
|
|
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
|
-
|
|
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
|
|
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(
|
|
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)({ ...
|
|
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`,
|
|
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 = [];
|
|
@@ -5,25 +5,25 @@ const exec_1 = require("../exec");
|
|
|
5
5
|
const KINDS = new Set(['version', 'eslint-print-config', 'typescript-show-config', 'semgrep-validate', 'package-script-present', 'config-present']);
|
|
6
6
|
function commandFor(kind, evidence) {
|
|
7
7
|
const executable = evidence.toolExecutable ?? (kind.startsWith('typescript') ? 'tsc' : kind.startsWith('eslint') ? 'eslint' : kind.startsWith('semgrep') ? 'semgrep' : 'node');
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// resolved: it is runtime evidence, not a package-local tool assertion.
|
|
8
|
+
// Probe an executable through the same bounded resolver as its variant's
|
|
9
|
+
// eventual execution. Falling back preserves legacy probe-only kinds.
|
|
11
10
|
const resolution = executable === 'node'
|
|
12
11
|
? 'path'
|
|
13
|
-
: kind === 'semgrep-validate'
|
|
12
|
+
: evidence.toolResolution ?? (kind === 'semgrep-validate'
|
|
14
13
|
? 'python-environment'
|
|
15
|
-
: 'node-modules-bin';
|
|
14
|
+
: 'node-modules-bin');
|
|
16
15
|
if (kind === 'package-script-present')
|
|
17
16
|
return null;
|
|
18
17
|
if (kind === 'config-present')
|
|
19
18
|
return null;
|
|
19
|
+
const command = { executable, resolution, ...(resolution === 'python-environment' && evidence.pythonEnvironmentRoot ? { pythonEnvironmentRoot: evidence.pythonEnvironmentRoot } : {}) };
|
|
20
20
|
if (kind === 'version')
|
|
21
|
-
return {
|
|
21
|
+
return { ...command, args: ['--version'] };
|
|
22
22
|
if (kind === 'eslint-print-config')
|
|
23
|
-
return {
|
|
23
|
+
return { ...command, args: ['--print-config', evidence.configFiles?.[0] ?? 'package.json'] };
|
|
24
24
|
if (kind === 'typescript-show-config')
|
|
25
|
-
return {
|
|
26
|
-
return {
|
|
25
|
+
return { ...command, args: ['--showConfig'] };
|
|
26
|
+
return { ...command, args: ['--validate'] };
|
|
27
27
|
}
|
|
28
28
|
/** Executes only the closed probe enum. Raw output is intentionally discarded. */
|
|
29
29
|
async function runCompatibilityProbe(probe, evidence, executor = exec_1.runStructuredCommand) {
|
|
@@ -26,6 +26,9 @@ function result(state, reason, variant, evidence) {
|
|
|
26
26
|
return { state, reason, variantId: variant?.id ?? null, toolVersion, runtimeVersion, certifiedRange: variant?.certifiedRange ?? null, evidence: refs.slice(0, 32) };
|
|
27
27
|
}
|
|
28
28
|
function applies(sensor, evidence) {
|
|
29
|
+
if (sensor.applicability.kind === 'explicit-or-supported-language') {
|
|
30
|
+
return evidence.applicable === true || evidence.packSelection === 'explicit';
|
|
31
|
+
}
|
|
29
32
|
if (evidence.applicable === false)
|
|
30
33
|
return false;
|
|
31
34
|
const paths = new Set(evidence.paths ?? []);
|