agentic-workflow-manager 9.16.1 → 9.17.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/preflight/checks.js +63 -13
- package/dist/src/commands/sensors/bootstrap.js +2 -1
- package/dist/src/commands/sensors/compatibility/contract.js +30 -26
- package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
- package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
- package/dist/src/commands/sensors/compatibility/source.js +3 -2
- package/dist/src/commands/sensors/coverage/render.js +8 -2
- package/dist/src/commands/sensors/coverage/resolve.js +7 -0
- package/dist/src/commands/sensors/init.js +15 -2
- package/dist/src/commands/sensors/migrate.js +13 -10
- package/dist/src/commands/sensors/prepare.js +53 -6
- package/dist/src/commands/sensors/project.js +60 -6
- package/dist/src/commands/sensors/result.js +18 -3
- package/dist/src/commands/sensors/run.js +124 -16
- package/dist/src/commands/sensors/status.js +163 -18
- package/dist/src/core/init/steps.js +5 -1
- package/dist/tests/commands/preflight/preflight.test.js +121 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
- package/dist/tests/commands/sensors/init.test.js +107 -0
- package/dist/tests/commands/sensors/migrate.test.js +4 -3
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
- package/dist/tests/commands/sensors/prepare.test.js +62 -0
- package/dist/tests/commands/sensors/result.test.js +66 -0
- package/dist/tests/commands/sensors/run.test.js +331 -0
- package/dist/tests/commands/sensors/status.test.js +204 -0
- package/dist/tests/structural/native-release-artifacts.test.js +4 -2
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -100,13 +100,18 @@ function checkContextKernel(cwd) {
|
|
|
100
100
|
remedy: inspection.remedy,
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
+
function isDisabledCheckDetail(detail) {
|
|
104
|
+
return detail === 'disabled' || detail.startsWith('disabled ');
|
|
105
|
+
}
|
|
103
106
|
function manifestView(status) {
|
|
104
|
-
|
|
107
|
+
// Only-project READY has pack null but non-empty checks — still a valid view (RF-2.8).
|
|
108
|
+
// Do not invent a fake pack id; keep pack null and surface sensors from status.checks.
|
|
109
|
+
if (status.mode !== 'project-sensors' && status.mode !== 'legacy-v1')
|
|
105
110
|
return null;
|
|
106
111
|
const sensors = Object.fromEntries(Object.entries(status.checks).map(([name, check]) => [name, {
|
|
107
|
-
enabled: check.detail
|
|
112
|
+
enabled: !isDisabledCheckDetail(check.detail),
|
|
108
113
|
}]));
|
|
109
|
-
return { pack: status.pack, sensors };
|
|
114
|
+
return { pack: status.pack ?? null, sensors };
|
|
110
115
|
}
|
|
111
116
|
/**
|
|
112
117
|
* Total sensor entries and how many are enabled (`enabled !== false`, so a missing
|
|
@@ -139,12 +144,32 @@ function checkManifest(status) {
|
|
|
139
144
|
};
|
|
140
145
|
}
|
|
141
146
|
if (status.mode === 'invalid') {
|
|
147
|
+
if (status.reason === 'schema-unsupported') {
|
|
148
|
+
return {
|
|
149
|
+
id: 'manifest',
|
|
150
|
+
ok: false,
|
|
151
|
+
detail: '.awm/sensors.json uses an unsupported schema',
|
|
152
|
+
remedy: 'fix or regenerate it with `awm sensors init`',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// JSON.parse succeeded but schema/entry validation failed (RF-4.1/4.2).
|
|
156
|
+
// Never claim "not valid JSON"; name the sensor/field from status.reason.
|
|
157
|
+
if (typeof status.reason === 'string' && (status.reason.startsWith('schema-invalid') || status.reason.includes('Invalid sensor manifest'))) {
|
|
158
|
+
const detail = status.reason.startsWith('schema-invalid:')
|
|
159
|
+
? status.reason.slice('schema-invalid:'.length).trim()
|
|
160
|
+
: status.reason;
|
|
161
|
+
// RF-3.3: project schema invalidity must not lead with `awm sensors init`.
|
|
162
|
+
return {
|
|
163
|
+
id: 'manifest',
|
|
164
|
+
ok: false,
|
|
165
|
+
detail,
|
|
166
|
+
remedy: 'repair the invalid sensor entry in .awm/sensors.json',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
142
169
|
return {
|
|
143
170
|
id: 'manifest',
|
|
144
171
|
ok: false,
|
|
145
|
-
detail:
|
|
146
|
-
? '.awm/sensors.json uses an unsupported schema'
|
|
147
|
-
: '.awm/sensors.json is not valid JSON',
|
|
172
|
+
detail: '.awm/sensors.json is not valid JSON',
|
|
148
173
|
remedy: 'fix or regenerate it with `awm sensors init`',
|
|
149
174
|
};
|
|
150
175
|
}
|
|
@@ -165,6 +190,14 @@ function checkManifest(status) {
|
|
|
165
190
|
// for the detected stack, so `awm sensors init` built an honest, empty manifest
|
|
166
191
|
// rather than inventing defaults. That must not read as "all sensors disabled".
|
|
167
192
|
if (total === 0) {
|
|
193
|
+
if (manifest.pack === null) {
|
|
194
|
+
return {
|
|
195
|
+
id: 'manifest',
|
|
196
|
+
ok: false,
|
|
197
|
+
detail: 'only-project manifest has no sensors',
|
|
198
|
+
remedy: 'add source:"project" sensor entries to .awm/sensors.json, or declare a pack',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
168
201
|
return {
|
|
169
202
|
id: 'manifest',
|
|
170
203
|
ok: false,
|
|
@@ -179,12 +212,14 @@ function checkManifest(status) {
|
|
|
179
212
|
// gap is one missing tool could then configure sensors and still never pass.
|
|
180
213
|
const uninitialized = Object.keys(status.uninitialized ?? {});
|
|
181
214
|
const gap = uninitialized.length === 0 ? '' : `; ${uninitialized.length} applicable sensor(s) not initialized: ${uninitialized.join(', ')}`;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
215
|
+
const summary = manifest.pack === null
|
|
216
|
+
? (enabled === 0
|
|
217
|
+
? `only-project, all ${total} sensors disabled (deliberate opt-out)`
|
|
218
|
+
: `only-project, ${enabled}/${total} project-declared sensors enabled`)
|
|
219
|
+
: (enabled === 0
|
|
220
|
+
? `pack ${manifest.pack}, all ${total} sensors disabled (deliberate opt-out)`
|
|
221
|
+
: `pack ${manifest.pack}, ${enabled}/${total} sensors enabled`);
|
|
222
|
+
return { id: 'manifest', ok: true, detail: summary + gap };
|
|
188
223
|
}
|
|
189
224
|
/** Every enabled sensor's command must resolve. This is the check nothing was calling. */
|
|
190
225
|
async function checkTools(cwd, status) {
|
|
@@ -198,11 +233,14 @@ async function checkTools(cwd, status) {
|
|
|
198
233
|
// already guards against; this defends the invariant independently rather than
|
|
199
234
|
// relying solely on `checkManifest`'s gate to catch this exact manifest shape.
|
|
200
235
|
if (Object.keys(status.checks).length === 0) {
|
|
236
|
+
const packId = typeof status.pack === 'string' && status.pack.length > 0 ? status.pack : null;
|
|
201
237
|
return {
|
|
202
238
|
id: 'tools',
|
|
203
239
|
ok: false,
|
|
204
240
|
detail: 'no sensors configured to check (0 sensor entries in the manifest)',
|
|
205
|
-
remedy:
|
|
241
|
+
remedy: packId === null
|
|
242
|
+
? 'add source:"project" sensor entries to .awm/sensors.json, or declare a pack'
|
|
243
|
+
: `registry has no pack for '${packId}': run \`awm update\` or add a registry that has it`,
|
|
206
244
|
};
|
|
207
245
|
}
|
|
208
246
|
const broken = Object.entries(status.checks).filter(([, c]) => !c.ok);
|
|
@@ -227,6 +265,9 @@ function checkPack(cwd, status) {
|
|
|
227
265
|
const manifest = manifestView(status);
|
|
228
266
|
if (!manifest)
|
|
229
267
|
return { id: 'pack', ok: true, detail: 'skipped (no manifest)' };
|
|
268
|
+
if (manifest.pack === null) {
|
|
269
|
+
return { id: 'pack', ok: true, detail: 'no pack (project-declared sensors only)' };
|
|
270
|
+
}
|
|
230
271
|
const detection = (0, detection_1.detectStack)(cwd);
|
|
231
272
|
if (manifest.pack === 'generic' && detection.pack !== 'generic') {
|
|
232
273
|
return {
|
|
@@ -327,6 +368,15 @@ async function checkSensorExecution(cwd) {
|
|
|
327
368
|
try {
|
|
328
369
|
const output = await (0, run_1.runSensors)({ cwd, all: true });
|
|
329
370
|
if (output.overall === 'pass') {
|
|
371
|
+
const invalid = output.invalidEntries ?? [];
|
|
372
|
+
if (invalid.length > 0) {
|
|
373
|
+
return {
|
|
374
|
+
id: 'sensors-execution',
|
|
375
|
+
ok: false,
|
|
376
|
+
detail: `sensors passed but ${invalid.length} invalidEntries remain: ${invalid.map(entry => `${entry.name} (${entry.reason})`).join('; ')}`,
|
|
377
|
+
remedy: 'repair the invalid sensor entries in .awm/sensors.json, then rerun `awm preflight --verify-sensors`',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
330
380
|
return { id: 'sensors-execution', ok: true, detail: 'all selected sensors completed with pass' };
|
|
331
381
|
}
|
|
332
382
|
return {
|
|
@@ -209,7 +209,8 @@ function applySensorBootstrap(plan) {
|
|
|
209
209
|
if (plan.manifest.mode === 'project-sensors') {
|
|
210
210
|
if (!plan.source)
|
|
211
211
|
throw new Error('project-sensors bootstrap plan requires its resolved source');
|
|
212
|
-
(0,
|
|
212
|
+
const packBound = (0, manifest_1.asPackBoundProjectSensors)(plan.manifest);
|
|
213
|
+
(0, materialize_1.materializePortableSensors)({ projectRoot: plan.projectRoot, pack: packBound.pack, source: plan.source, ...(packBound.packageRoot ? { packageRoot: packBound.packageRoot } : {}), sensors: packBound.sensors, configure: plan.configure !== false });
|
|
213
214
|
return 'created';
|
|
214
215
|
}
|
|
215
216
|
const parsed = (0, manifest_1.parseSensorManifest)(plan.manifest, 'bootstrap declaration');
|
|
@@ -137,61 +137,65 @@ function resolveSemgrepPolicy(policyRef, source, location = 'policy') {
|
|
|
137
137
|
}
|
|
138
138
|
return parseSemgrepPolicy(parsed, candidate);
|
|
139
139
|
}
|
|
140
|
-
function parseStructuredCommand(input, source) {
|
|
141
|
-
const value = record(input, source,
|
|
142
|
-
fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source,
|
|
143
|
-
const executable = text(value.executable, source,
|
|
140
|
+
function parseStructuredCommand(input, source, location = 'command') {
|
|
141
|
+
const value = record(input, source, location);
|
|
142
|
+
fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source, location);
|
|
143
|
+
const executable = text(value.executable, source, `${location}.executable`);
|
|
144
144
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(executable) || SHELL_EXECUTABLES.has(executable.toLowerCase().replace(/\.exe$/, ''))) {
|
|
145
|
-
invalid(source,
|
|
145
|
+
invalid(source, `${location}.executable must not be a shell or path`);
|
|
146
146
|
}
|
|
147
147
|
if (value.resolution !== 'node-modules-bin' && value.resolution !== 'python-environment' && value.resolution !== 'path') {
|
|
148
|
-
invalid(source,
|
|
148
|
+
invalid(source, `${location}.resolution must be a supported resolution`);
|
|
149
149
|
}
|
|
150
|
-
const args = stringArray(value.args, source,
|
|
150
|
+
const args = stringArray(value.args, source, `${location}.args`);
|
|
151
151
|
for (const [index, arg] of args.entries()) {
|
|
152
152
|
if (arg.includes('{files}') && arg !== '{files}')
|
|
153
|
-
invalid(source,
|
|
153
|
+
invalid(source, `${location}.args[${index}] must not embed {files}`);
|
|
154
154
|
}
|
|
155
155
|
const command = { executable, resolution: value.resolution, args };
|
|
156
156
|
if ('pythonEnvironmentRoot' in value) {
|
|
157
|
-
if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv'))
|
|
158
|
-
invalid(source,
|
|
157
|
+
if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv')) {
|
|
158
|
+
invalid(source, `${location}.pythonEnvironmentRoot must name the selected .venv or venv Python environment`);
|
|
159
|
+
}
|
|
159
160
|
command.pythonEnvironmentRoot = value.pythonEnvironmentRoot;
|
|
160
161
|
}
|
|
161
162
|
const executablePackageManager = normalizedPackageManager(executable);
|
|
162
|
-
if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value))
|
|
163
|
-
invalid(source,
|
|
163
|
+
if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value)) {
|
|
164
|
+
invalid(source, `${location}.packageManager is required for a package-manager executable`);
|
|
165
|
+
}
|
|
164
166
|
if ('packageManager' in value) {
|
|
165
|
-
if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager)))
|
|
166
|
-
invalid(source,
|
|
167
|
+
if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager))) {
|
|
168
|
+
invalid(source, `${location}.packageManager must be npm, pnpm, yarn, or bun`);
|
|
169
|
+
}
|
|
167
170
|
const packageManager = normalizedPackageManager(value.packageManager);
|
|
168
171
|
if (packageManager !== executablePackageManager)
|
|
169
|
-
invalid(source,
|
|
172
|
+
invalid(source, `${location}.packageManager must match executable`);
|
|
170
173
|
command.packageManager = packageManager;
|
|
171
174
|
}
|
|
172
175
|
if ('environment' in value) {
|
|
173
|
-
const environment = record(value.environment, source,
|
|
174
|
-
fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source,
|
|
175
|
-
if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1)
|
|
176
|
-
invalid(source,
|
|
176
|
+
const environment = record(value.environment, source, `${location}.environment`);
|
|
177
|
+
fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source, `${location}.environment`);
|
|
178
|
+
if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1) {
|
|
179
|
+
invalid(source, `${location}.environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping`);
|
|
180
|
+
}
|
|
177
181
|
command.environment = { ESLINT_USE_FLAT_CONFIG: environment.ESLINT_USE_FLAT_CONFIG };
|
|
178
182
|
}
|
|
179
183
|
if ('fileInput' in value) {
|
|
180
|
-
const fileInput = record(value.fileInput, source,
|
|
181
|
-
fields(fileInput, ['placeholder', 'extensions'], source,
|
|
184
|
+
const fileInput = record(value.fileInput, source, `${location}.fileInput`);
|
|
185
|
+
fields(fileInput, ['placeholder', 'extensions'], source, `${location}.fileInput`);
|
|
182
186
|
if (fileInput.placeholder !== '{files}')
|
|
183
|
-
invalid(source,
|
|
184
|
-
const extensions = stringArray(fileInput.extensions, source,
|
|
187
|
+
invalid(source, `${location}.fileInput.placeholder must be {files}`);
|
|
188
|
+
const extensions = stringArray(fileInput.extensions, source, `${location}.fileInput.extensions`);
|
|
185
189
|
for (const [index, extension] of extensions.entries()) {
|
|
186
190
|
if (!/^\.[A-Za-z0-9]+$/.test(extension))
|
|
187
|
-
invalid(source,
|
|
191
|
+
invalid(source, `${location}.fileInput.extensions[${index}] must be an extension`);
|
|
188
192
|
}
|
|
189
193
|
if (args.filter(arg => arg === '{files}').length !== 1)
|
|
190
|
-
invalid(source,
|
|
194
|
+
invalid(source, `${location}.fileInput requires exactly one {files} argument`);
|
|
191
195
|
command.fileInput = { placeholder: '{files}', extensions };
|
|
192
196
|
}
|
|
193
197
|
else if (args.includes('{files}')) {
|
|
194
|
-
invalid(source,
|
|
198
|
+
invalid(source, `${location} {files} argument requires fileInput`);
|
|
195
199
|
}
|
|
196
200
|
return command;
|
|
197
201
|
}
|
|
@@ -3,6 +3,7 @@ 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.discoverPresentMarkers = discoverPresentMarkers;
|
|
6
7
|
exports.discoverProjectEvidence = discoverProjectEvidence;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -21,6 +22,26 @@ function safeFile(root, relative) {
|
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Present contained marker files under cwd — same `safeFile` rules pack discovery
|
|
27
|
+
* uses when evaluating applicability allFiles/anyFiles.
|
|
28
|
+
*/
|
|
29
|
+
function discoverPresentMarkers(cwd, markers) {
|
|
30
|
+
if (typeof cwd !== 'string' || cwd.trim() === '')
|
|
31
|
+
throw new Error('cwd must be a non-empty path');
|
|
32
|
+
if (!Array.isArray(markers))
|
|
33
|
+
throw new Error('markers must be an array');
|
|
34
|
+
let root;
|
|
35
|
+
try {
|
|
36
|
+
root = fs_1.default.realpathSync(cwd);
|
|
37
|
+
if (!fs_1.default.statSync(root).isDirectory())
|
|
38
|
+
throw new Error();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`cwd is not a readable project directory: ${cwd}`);
|
|
42
|
+
}
|
|
43
|
+
return [...new Set(markers.filter((marker) => typeof marker === 'string' && safeFile(root, marker)))].sort();
|
|
44
|
+
}
|
|
24
45
|
function safeParts(parts) {
|
|
25
46
|
return parts.length > 0 && parts.every(part => Boolean(part) && part !== '.' && part !== '..' && !part.includes('/') && !part.includes('\\'));
|
|
26
47
|
}
|
|
@@ -3,7 +3,11 @@ 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.isProjectDeclaredSensor = isProjectDeclaredSensor;
|
|
7
|
+
exports.isPackBoundManifestSensor = isPackBoundManifestSensor;
|
|
8
|
+
exports.asPackBoundProjectSensors = asPackBoundProjectSensors;
|
|
6
9
|
exports.legacyCompatibility = legacyCompatibility;
|
|
10
|
+
exports.parseSensorManifestWithIssues = parseSensorManifestWithIssues;
|
|
7
11
|
exports.parseSensorManifest = parseSensorManifest;
|
|
8
12
|
exports.serializeManifestV2 = serializeManifestV2;
|
|
9
13
|
exports.serializeManifestV3 = serializeManifestV3;
|
|
@@ -11,6 +15,28 @@ const contract_1 = require("./contract");
|
|
|
11
15
|
const timeout_1 = require("./timeout");
|
|
12
16
|
const semver_1 = __importDefault(require("semver"));
|
|
13
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
+
/** Registered CLI formatter ids plus exit-code (default for project-declared sensors). */
|
|
19
|
+
const PROJECT_FORMATTERS = new Set([
|
|
20
|
+
'tsc', 'eslint-llm', 'semgrep', 'test', 'mypy', 'ruff', 'shellcheck', 'generic', 'exit-code',
|
|
21
|
+
]);
|
|
22
|
+
function isProjectDeclaredSensor(sensor) {
|
|
23
|
+
return 'source' in sensor && sensor.source === 'project';
|
|
24
|
+
}
|
|
25
|
+
function isPackBoundManifestSensor(sensor) {
|
|
26
|
+
return !isProjectDeclaredSensor(sensor);
|
|
27
|
+
}
|
|
28
|
+
/** Narrow a parsed v3 manifest to the pack-bound shape expected by migrate/materialize. */
|
|
29
|
+
function asPackBoundProjectSensors(manifest) {
|
|
30
|
+
if (typeof manifest.pack !== 'string' || manifest.pack.length === 0 || !manifest.source) {
|
|
31
|
+
throw new Error('project-sensors manifest requires a nonempty pack and source.registry');
|
|
32
|
+
}
|
|
33
|
+
for (const [name, sensor] of Object.entries(manifest.sensors)) {
|
|
34
|
+
if (isProjectDeclaredSensor(sensor)) {
|
|
35
|
+
throw new Error(`sensors.${name} is project-declared; pack-bound operation required`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return manifest;
|
|
39
|
+
}
|
|
14
40
|
function isRecord(value) {
|
|
15
41
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
42
|
}
|
|
@@ -93,6 +119,9 @@ function parseLegacySensor(input, source, location) {
|
|
|
93
119
|
if (typeof input === 'string')
|
|
94
120
|
return { cmd: text(input, source, `${location}.cmd`) };
|
|
95
121
|
const value = record(input, source, location);
|
|
122
|
+
if (value.source === 'project') {
|
|
123
|
+
invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
|
|
124
|
+
}
|
|
96
125
|
fields(value, ['cmd', 'fast', 'enabled', 'timeout', 'changedCmd', 'changedExtensions', 'formatter'], source, location);
|
|
97
126
|
const sensor = {};
|
|
98
127
|
if ('cmd' in value)
|
|
@@ -138,14 +167,96 @@ function parseLegacyManifest(value, source) {
|
|
|
138
167
|
}
|
|
139
168
|
return manifest;
|
|
140
169
|
}
|
|
170
|
+
function parseApplicability(input, source, location) {
|
|
171
|
+
const applicabilityInput = record(input, source, location);
|
|
172
|
+
fields(applicabilityInput, ['allFiles', 'anyFiles', 'kind'], source, location);
|
|
173
|
+
const applicability = {};
|
|
174
|
+
if ('allFiles' in applicabilityInput) {
|
|
175
|
+
applicability.allFiles = stringArray(applicabilityInput.allFiles, source, `${location}.allFiles`, false)
|
|
176
|
+
.map((file, index) => asset(file, source, `${location}.allFiles[${index}]`));
|
|
177
|
+
}
|
|
178
|
+
if ('anyFiles' in applicabilityInput) {
|
|
179
|
+
applicability.anyFiles = stringArray(applicabilityInput.anyFiles, source, `${location}.anyFiles`, false)
|
|
180
|
+
.map((file, index) => asset(file, source, `${location}.anyFiles[${index}]`));
|
|
181
|
+
}
|
|
182
|
+
if ('kind' in applicabilityInput) {
|
|
183
|
+
const kind = text(applicabilityInput.kind, source, `${location}.kind`);
|
|
184
|
+
if (kind !== 'explicit-or-supported-language' && kind !== 'explicit-opt-in') {
|
|
185
|
+
invalid(source, `${location}.kind must be a supported applicability kind`);
|
|
186
|
+
}
|
|
187
|
+
applicability.kind = kind;
|
|
188
|
+
}
|
|
189
|
+
if (Object.keys(applicability).length === 0)
|
|
190
|
+
invalid(source, `${location} must declare a condition`);
|
|
191
|
+
return applicability;
|
|
192
|
+
}
|
|
193
|
+
function parseProjectSensor(input, source, location) {
|
|
194
|
+
const value = record(input, source, location);
|
|
195
|
+
if ('variantId' in value)
|
|
196
|
+
invalid(source, `${location}.variantId is forbidden on source:"project" entries`);
|
|
197
|
+
if ('initializedCompatibility' in value)
|
|
198
|
+
invalid(source, `${location}.initializedCompatibility is forbidden on source:"project" entries`);
|
|
199
|
+
fields(value, ['source', 'enabled', 'command', 'formatter', 'fast', 'timeout', 'assets', 'applicability', 'description'], source, location);
|
|
200
|
+
if (value.source !== 'project')
|
|
201
|
+
invalid(source, `${location}.source must be "project"`);
|
|
202
|
+
if (typeof value.enabled !== 'boolean')
|
|
203
|
+
invalid(source, `${location}.enabled must be a boolean`);
|
|
204
|
+
const sensor = {
|
|
205
|
+
source: 'project',
|
|
206
|
+
enabled: value.enabled,
|
|
207
|
+
command: (0, contract_1.parseStructuredCommand)(value.command, source, `${location}.command`),
|
|
208
|
+
formatter: 'exit-code',
|
|
209
|
+
};
|
|
210
|
+
if ('formatter' in value) {
|
|
211
|
+
const formatter = text(value.formatter, source, `${location}.formatter`);
|
|
212
|
+
if (!PROJECT_FORMATTERS.has(formatter))
|
|
213
|
+
invalid(source, `${location}.formatter is not a registered formatter id`);
|
|
214
|
+
sensor.formatter = formatter;
|
|
215
|
+
}
|
|
216
|
+
if ('assets' in value) {
|
|
217
|
+
sensor.assets = stringArray(value.assets, source, `${location}.assets`, true)
|
|
218
|
+
.map((entry, index) => asset(entry, source, `${location}.assets[${index}]`));
|
|
219
|
+
}
|
|
220
|
+
if ('applicability' in value)
|
|
221
|
+
sensor.applicability = parseApplicability(value.applicability, source, `${location}.applicability`);
|
|
222
|
+
if ('description' in value)
|
|
223
|
+
sensor.description = text(value.description, source, `${location}.description`);
|
|
224
|
+
if ('fast' in value) {
|
|
225
|
+
if (typeof value.fast !== 'boolean')
|
|
226
|
+
invalid(source, `${location}.fast must be a boolean`);
|
|
227
|
+
sensor.fast = value.fast;
|
|
228
|
+
}
|
|
229
|
+
if ('timeout' in value) {
|
|
230
|
+
try {
|
|
231
|
+
sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return sensor;
|
|
238
|
+
}
|
|
239
|
+
function parseV3SensorEntry(input, source, location) {
|
|
240
|
+
const value = record(input, source, location);
|
|
241
|
+
if (value.source === 'project')
|
|
242
|
+
return parseProjectSensor(value, source, location);
|
|
243
|
+
const unmarkedCustom = !('variantId' in value) && !('initializedCompatibility' in value) && 'command' in value;
|
|
244
|
+
if (unmarkedCustom) {
|
|
245
|
+
invalid(source, `${location}.source is required for project-declared sensors; add source:"project"`);
|
|
246
|
+
}
|
|
247
|
+
return parseV2Sensor(input, source, location);
|
|
248
|
+
}
|
|
141
249
|
function parseV2Sensor(input, source, location) {
|
|
142
250
|
const value = record(input, source, location);
|
|
251
|
+
if (value.source === 'project') {
|
|
252
|
+
invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
|
|
253
|
+
}
|
|
143
254
|
fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
|
|
144
255
|
if (typeof value.enabled !== 'boolean')
|
|
145
256
|
invalid(source, `${location}.enabled must be a boolean`);
|
|
146
257
|
const sensor = {
|
|
147
258
|
enabled: value.enabled, variantId: id(value.variantId, source, `${location}.variantId`),
|
|
148
|
-
command: (0, contract_1.parseStructuredCommand)(value.command, source),
|
|
259
|
+
command: (0, contract_1.parseStructuredCommand)(value.command, source, `${location}.command`),
|
|
149
260
|
initializedCompatibility: parseCompatibilityEvidence(value.initializedCompatibility, source, `${location}.initializedCompatibility`),
|
|
150
261
|
};
|
|
151
262
|
if (sensor.initializedCompatibility.variantId !== null && sensor.initializedCompatibility.variantId !== sensor.variantId)
|
|
@@ -226,24 +337,28 @@ function parseV2Manifest(value, source) {
|
|
|
226
337
|
}
|
|
227
338
|
return manifest;
|
|
228
339
|
}
|
|
229
|
-
function
|
|
340
|
+
function parseV3ProjectManifestRoot(value, source) {
|
|
230
341
|
fields(value, ['schemaVersion', 'mode', 'pack', 'packSelection', 'source', 'packageRoot', 'sensors', 'concurrency'], source, 'root');
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
234
|
-
const sensors = {};
|
|
235
|
-
for (const name of Object.keys(sensorsInput))
|
|
236
|
-
sensors[id(name, source, 'sensor id')] = parseV2Sensor(sensorsInput[name], source, `sensors.${name}`);
|
|
342
|
+
const hasPackKey = 'pack' in value;
|
|
343
|
+
const pack = !hasPackKey ? undefined : value.pack === null ? null : id(value.pack, source, 'pack');
|
|
237
344
|
const manifest = {
|
|
238
345
|
schemaVersion: 3,
|
|
239
346
|
mode: 'project-sensors',
|
|
240
|
-
|
|
241
|
-
source: { registry: id(sourceValue.registry, source, 'source.registry') },
|
|
242
|
-
sensors,
|
|
347
|
+
...(hasPackKey ? { pack } : {}),
|
|
243
348
|
};
|
|
349
|
+
if (typeof pack === 'string' && pack.length > 0) {
|
|
350
|
+
const sourceValue = record(value.source, source, 'source');
|
|
351
|
+
fields(sourceValue, ['registry'], source, 'source');
|
|
352
|
+
manifest.source = { registry: id(sourceValue.registry, source, 'source.registry') };
|
|
353
|
+
}
|
|
354
|
+
else if ('source' in value) {
|
|
355
|
+
invalid(source, 'source requires a nonempty pack id');
|
|
356
|
+
}
|
|
244
357
|
if ('packSelection' in value) {
|
|
245
358
|
if (value.packSelection !== 'explicit')
|
|
246
359
|
invalid(source, 'packSelection must be "explicit" when present');
|
|
360
|
+
if (typeof pack !== 'string' || pack.length === 0)
|
|
361
|
+
invalid(source, 'packSelection requires a nonempty pack id');
|
|
247
362
|
manifest.packSelection = 'explicit';
|
|
248
363
|
}
|
|
249
364
|
if ('packageRoot' in value)
|
|
@@ -255,26 +370,63 @@ function parseV3ProjectManifest(value, source) {
|
|
|
255
370
|
}
|
|
256
371
|
return manifest;
|
|
257
372
|
}
|
|
373
|
+
function parseV3ProjectSensorsMap(sensorsInput, source, soft) {
|
|
374
|
+
const sensors = {};
|
|
375
|
+
const invalidEntries = [];
|
|
376
|
+
for (const name of Object.keys(sensorsInput)) {
|
|
377
|
+
const sensorName = id(name, source, 'sensor id');
|
|
378
|
+
try {
|
|
379
|
+
sensors[sensorName] = parseV3SensorEntry(sensorsInput[name], source, `sensors.${sensorName}`);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
383
|
+
if (!soft)
|
|
384
|
+
throw error instanceof Error ? error : new Error(reason);
|
|
385
|
+
invalidEntries.push({ name: sensorName, reason });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { sensors, invalidEntries };
|
|
389
|
+
}
|
|
390
|
+
function parseV3ProjectManifest(value, source, soft) {
|
|
391
|
+
const root = parseV3ProjectManifestRoot(value, source);
|
|
392
|
+
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
393
|
+
const { sensors, invalidEntries } = parseV3ProjectSensorsMap(sensorsInput, source, soft);
|
|
394
|
+
return { pack: { ...root, sensors }, invalidEntries };
|
|
395
|
+
}
|
|
258
396
|
function parseV3Manifest(value, source) {
|
|
259
397
|
if (value.schemaVersion !== 3)
|
|
260
398
|
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
|
|
261
399
|
if (value.mode === 'project-sensors')
|
|
262
|
-
return parseV3ProjectManifest(value, source);
|
|
400
|
+
return parseV3ProjectManifest(value, source, false).pack;
|
|
263
401
|
if (value.mode !== 'native-gate' && value.mode !== 'opt-out')
|
|
264
402
|
invalid(source, 'schemaVersion 3 mode must be "project-sensors", "native-gate", or "opt-out"');
|
|
265
403
|
fields(value, ['schemaVersion', 'mode', 'reason'], source, 'root');
|
|
266
404
|
return { schemaVersion: 3, mode: value.mode, reason: text(value.reason, source, 'reason') };
|
|
267
405
|
}
|
|
268
|
-
|
|
406
|
+
/** Soft per-entry parse for v3 project-sensors; document-level errors still throw. */
|
|
407
|
+
function parseSensorManifestWithIssues(input, source) {
|
|
269
408
|
const value = record(input, source, 'root');
|
|
270
409
|
if (!('schemaVersion' in value))
|
|
271
|
-
return { kind: 'legacy', pack: parseLegacyManifest(value, source) };
|
|
410
|
+
return { kind: 'legacy', pack: parseLegacyManifest(value, source), invalidEntries: [] };
|
|
272
411
|
if (value.schemaVersion === 2)
|
|
273
|
-
return { kind: 'v2', pack: parseV2Manifest(value, source) };
|
|
274
|
-
if (value.schemaVersion === 3)
|
|
275
|
-
|
|
412
|
+
return { kind: 'v2', pack: parseV2Manifest(value, source), invalidEntries: [] };
|
|
413
|
+
if (value.schemaVersion === 3) {
|
|
414
|
+
if (value.mode === 'project-sensors') {
|
|
415
|
+
const parsed = parseV3ProjectManifest(value, source, true);
|
|
416
|
+
return { kind: 'v3', pack: parsed.pack, invalidEntries: parsed.invalidEntries };
|
|
417
|
+
}
|
|
418
|
+
return { kind: 'v3', pack: parseV3Manifest(value, source), invalidEntries: [] };
|
|
419
|
+
}
|
|
276
420
|
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
|
|
277
421
|
}
|
|
422
|
+
function parseSensorManifest(input, source) {
|
|
423
|
+
const withIssues = parseSensorManifestWithIssues(input, source);
|
|
424
|
+
if (withIssues.invalidEntries.length > 0) {
|
|
425
|
+
throw new Error(withIssues.invalidEntries[0].reason);
|
|
426
|
+
}
|
|
427
|
+
const { invalidEntries: _invalidEntries, ...parsed } = withIssues;
|
|
428
|
+
return parsed;
|
|
429
|
+
}
|
|
278
430
|
function serializeManifestV2(input) {
|
|
279
431
|
const parsed = parseSensorManifest(input, 'manifest serialization');
|
|
280
432
|
if (parsed.kind !== 'v2')
|
|
@@ -217,6 +217,23 @@ function priorAssets(projectRoot) {
|
|
|
217
217
|
return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
|
|
218
218
|
return [];
|
|
219
219
|
}
|
|
220
|
+
/** Existing `"source": "project"` entries to keep when regenerating pack sensors (RF-3.1). */
|
|
221
|
+
function priorProjectSensors(projectRoot) {
|
|
222
|
+
const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
223
|
+
try {
|
|
224
|
+
const stat = fs_1.default.lstatSync(manifestPath, { bigint: true });
|
|
225
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
226
|
+
return {};
|
|
227
|
+
// Soft-parse so one invalid sibling does not drop every valid project entry.
|
|
228
|
+
const parsed = (0, manifest_1.parseSensorManifestWithIssues)(JSON.parse((0, safe_file_1.readInspectedBoundedFile)(manifestPath, stat, MAX_ASSET_BYTES, () => new Error('prior manifest unreadable')).toString('utf8')), manifestPath);
|
|
229
|
+
if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
|
|
230
|
+
return {};
|
|
231
|
+
return Object.fromEntries(Object.entries(parsed.pack.sensors).filter(([, sensor]) => (0, manifest_1.isProjectDeclaredSensor)(sensor)));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
220
237
|
/** Materialize a previously selected logical registry source into a portable v3 manifest. */
|
|
221
238
|
function materializePortableSensors(input) {
|
|
222
239
|
if (!input || typeof input !== 'object')
|
|
@@ -237,18 +254,25 @@ function materializePortableSensors(input) {
|
|
|
237
254
|
const parsed = (0, manifest_1.parseSensorManifest)(candidate, 'portable materialized manifest');
|
|
238
255
|
if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
|
|
239
256
|
throw new Error('portable materialized manifest must be v3 project-sensors');
|
|
240
|
-
const
|
|
241
|
-
for (const [name, sensor] of Object.entries(
|
|
242
|
-
if (sensor.policyRef)
|
|
257
|
+
const packBound = (0, manifest_1.asPackBoundProjectSensors)(parsed.pack);
|
|
258
|
+
for (const [name, sensor] of Object.entries(packBound.sensors)) {
|
|
259
|
+
if ((0, manifest_1.isPackBoundManifestSensor)(sensor) && sensor.policyRef)
|
|
243
260
|
(0, contract_1.resolveSemgrepPolicy)(sensor.policyRef, path_1.default.join(packRoot, 'pack.json'), `sensors.${name}`);
|
|
244
261
|
}
|
|
245
|
-
const
|
|
262
|
+
const priorProject = priorProjectSensors(projectRoot);
|
|
263
|
+
// Project entries win on name collision so regen never drops them (RF-3.1).
|
|
264
|
+
const manifest = {
|
|
265
|
+
...packBound,
|
|
266
|
+
sensors: { ...packBound.sensors, ...priorProject },
|
|
267
|
+
};
|
|
268
|
+
const selected = [...new Set(Object.values(packBound.sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
|
|
246
269
|
// Establish every registry authority and read every selected byte before the
|
|
247
270
|
// first project mutation. A bad later asset must not strand an earlier one.
|
|
248
271
|
const publications = input.configure === false ? [] : selected.map(asset => {
|
|
249
272
|
const selectedSource = selectedRegistryAsset(packRoot, asset);
|
|
250
273
|
return { asset, content: readSelectedRegistryAsset(packRoot, selectedSource, asset) };
|
|
251
274
|
});
|
|
275
|
+
const manifestExists = fs_1.default.existsSync(path_1.default.join(projectRoot, '.awm', 'sensors.json'));
|
|
252
276
|
return (0, safe_file_1.withProjectLease)(projectRoot, () => {
|
|
253
277
|
const configRoot = manifest.packageRoot ? containedPackageRoot(projectRoot, manifest.packageRoot) : projectRoot;
|
|
254
278
|
const prior = priorAssets(projectRoot);
|
|
@@ -273,7 +297,8 @@ function materializePortableSensors(input) {
|
|
|
273
297
|
preserved.push(publication.asset);
|
|
274
298
|
}
|
|
275
299
|
}
|
|
276
|
-
|
|
300
|
+
// Replace when regenerating over an existing manifest so project entries can be re-merged.
|
|
301
|
+
atomicWrite(projectRoot, '.awm/sensors.json', (0, manifest_1.serializeManifestV3)(manifest), !manifestExists);
|
|
277
302
|
}
|
|
278
303
|
catch (error) {
|
|
279
304
|
for (const entry of created.reverse()) {
|
|
@@ -49,10 +49,11 @@ function resolveSensorSource(manifest, deps) {
|
|
|
49
49
|
|| !v3.source || typeof v3.source.registry !== 'string') {
|
|
50
50
|
throw new Error('sensor source resolution requires a valid project-sensors manifest');
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
const registryName = v3.source.registry;
|
|
53
|
+
if (!/^[a-z][a-z0-9-]*$/.test(registryName)) {
|
|
53
54
|
throw new Error('sensor source registry must be a stable lowercase id');
|
|
54
55
|
}
|
|
55
|
-
const matches = registries.filter(entry => entry?.name ===
|
|
56
|
+
const matches = registries.filter(entry => entry?.name === registryName);
|
|
56
57
|
if (matches.length !== 1)
|
|
57
58
|
return unavailable('registry-not-installed');
|
|
58
59
|
const source = (0, pack_source_1.listPackSources)(v3.pack, { registries: matches })[0];
|
|
@@ -225,8 +225,9 @@ function assertCoverageEnvelope(report, renderer) {
|
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
if (staticReport.reason === 'no_reference') {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
const packReferenced = isNonBlankString(report.pack) && isNonBlankString(report.registry);
|
|
229
|
+
const onlyProject = report.pack === null && report.registry === null;
|
|
230
|
+
if (report.overall !== 'inconclusive' || staticReport.classes.length !== 0 || (!packReferenced && !onlyProject)) {
|
|
230
231
|
invalidReport(renderer);
|
|
231
232
|
}
|
|
232
233
|
if (report.empirical !== undefined) {
|
|
@@ -303,6 +304,11 @@ function renderCoverageHuman(report) {
|
|
|
303
304
|
return ['Sensor coverage', 'Overall: inconclusive', 'Reason: sensors are not configured', 'Run: awm sensors init', ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
304
305
|
}
|
|
305
306
|
if (report.static.reason === 'no_reference') {
|
|
307
|
+
if (report.pack === null && report.registry === null) {
|
|
308
|
+
return ['Sensor coverage', 'Pack: none', 'Overall: inconclusive',
|
|
309
|
+
'No pack coverage reference (project-declared sensors only)',
|
|
310
|
+
...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
311
|
+
}
|
|
306
312
|
return ['Sensor coverage', `Pack: ${safeHumanText(report.pack ?? 'unknown')}`, `Registry: ${safeHumanText(report.registry ?? 'unknown')}`,
|
|
307
313
|
'Overall: inconclusive', `No coverage reference for pack '${safeHumanText(report.pack ?? 'unknown')}'`, ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
308
314
|
}
|