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.
- package/dist/scripts/sensor-support-matrix.js +110 -12
- package/dist/scripts/support-matrix.js +4 -1
- package/dist/src/commands/preflight/index.js +4 -1
- 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/integration/preflight-json-pipe.e2e.test.js +26 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +9 -7
- package/dist/tests/structural/support-matrix-is-current.test.js +78 -6
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 = [];
|