agentic-workflow-manager 9.4.1 → 9.5.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 +56 -25
- package/dist/src/commands/sensors/compatibility/manifest.js +46 -1
- package/dist/src/commands/sensors/compatibility/pack-source.js +61 -17
- package/dist/src/commands/sensors/compatibility/safe-file.js +51 -0
- package/dist/src/commands/sensors/compatibility/source.js +75 -0
- package/dist/src/commands/sensors/coverage/resolve.js +17 -21
- package/dist/src/commands/sensors/init.js +3 -1
- package/dist/src/commands/sensors/project.js +107 -0
- package/dist/src/commands/sensors/run.js +64 -45
- package/dist/src/commands/sensors/status.js +97 -56
- package/dist/tests/commands/preflight/preflight.test.js +31 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +23 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +216 -0
- package/dist/tests/commands/sensors/compatibility/source.test.js +121 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +26 -34
- package/dist/tests/commands/sensors/project.test.js +102 -0
- package/dist/tests/commands/sensors/run.test.js +107 -4
- package/dist/tests/commands/sensors/status.test.js +106 -2
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +5 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +4 -21
- package/dist/tests/integration/sensor-coverage.e2e.test.js +15 -24
- package/package.json +1 -1
|
@@ -83,13 +83,13 @@ function checkContextKernel(cwd) {
|
|
|
83
83
|
remedy: inspection.remedy,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(cwd, MANIFEST), 'utf-8'));
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
86
|
+
function manifestView(status) {
|
|
87
|
+
if (!status.pack)
|
|
91
88
|
return null;
|
|
92
|
-
|
|
89
|
+
const sensors = Object.fromEntries(Object.entries(status.checks).map(([name, check]) => [name, {
|
|
90
|
+
enabled: check.detail !== 'disabled',
|
|
91
|
+
}]));
|
|
92
|
+
return { pack: status.pack, sensors };
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
95
|
* Total sensor entries and how many are enabled (`enabled !== false`, so a missing
|
|
@@ -111,8 +111,8 @@ function countEnabledSensors(manifest) {
|
|
|
111
111
|
* `awm sensors init`" look identical from the outside, and on a team the second one is
|
|
112
112
|
* the common case. Requiring the manifest turns the decision into a reviewable diff.
|
|
113
113
|
*/
|
|
114
|
-
function checkManifest(
|
|
115
|
-
if (
|
|
114
|
+
function checkManifest(status) {
|
|
115
|
+
if (status.mode === 'missing') {
|
|
116
116
|
return {
|
|
117
117
|
id: 'manifest',
|
|
118
118
|
ok: false,
|
|
@@ -121,14 +121,26 @@ function checkManifest(cwd, manifest) {
|
|
|
121
121
|
+ '`"enabled": false` — an unconfigured repo and a deliberate opt-out must not look alike)',
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
|
-
if (
|
|
124
|
+
if (status.mode === 'invalid') {
|
|
125
125
|
return {
|
|
126
126
|
id: 'manifest',
|
|
127
127
|
ok: false,
|
|
128
|
-
detail:
|
|
128
|
+
detail: status.reason === 'schema-unsupported'
|
|
129
|
+
? '.awm/sensors.json uses an unsupported schema'
|
|
130
|
+
: '.awm/sensors.json is not valid JSON',
|
|
129
131
|
remedy: 'fix or regenerate it with `awm sensors init`',
|
|
130
132
|
};
|
|
131
133
|
}
|
|
134
|
+
if (status.mode === 'native-gate')
|
|
135
|
+
return { id: 'manifest', ok: true, detail: 'native CI quality authority declared', remedy: 'verify the native CI gate before accepting this run' };
|
|
136
|
+
if (status.mode === 'opt-out')
|
|
137
|
+
return { id: 'manifest', ok: true, detail: 'local quality gate deliberately disabled', remedy: 'remove the opt-out declaration or provide a quality gate' };
|
|
138
|
+
if (status.mode === 'source-unavailable' || status.mode === 'source-ambiguous') {
|
|
139
|
+
return { id: 'manifest', ok: false, detail: `sensor source ${status.mode}: ${status.reason}`, remedy: status.remedy };
|
|
140
|
+
}
|
|
141
|
+
const manifest = manifestView(status);
|
|
142
|
+
if (!manifest)
|
|
143
|
+
return { id: 'manifest', ok: false, detail: 'sensor authority is unavailable', remedy: 'repair or regenerate the sensor manifest' };
|
|
132
144
|
const { total, enabled } = countEnabledSensors(manifest);
|
|
133
145
|
// total === 0 is NOT an opt-out: a deliberate opt-out lists every known sensor NAME
|
|
134
146
|
// explicitly with `enabled: false` (total > 0, enabled === 0). Zero entries means
|
|
@@ -151,8 +163,7 @@ function checkManifest(cwd, manifest) {
|
|
|
151
163
|
};
|
|
152
164
|
}
|
|
153
165
|
/** Every enabled sensor's command must resolve. This is the check nothing was calling. */
|
|
154
|
-
async function checkTools(cwd) {
|
|
155
|
-
const status = await (0, status_1.computeSensorStatus)(cwd);
|
|
166
|
+
async function checkTools(cwd, status) {
|
|
156
167
|
if (status.overall === 'NOT_CONFIGURED') {
|
|
157
168
|
return { id: 'tools', ok: false, detail: 'no manifest to check', remedy: 'run `awm sensors init`' };
|
|
158
169
|
}
|
|
@@ -188,7 +199,8 @@ async function checkTools(cwd) {
|
|
|
188
199
|
* same drift (`packDrift`) but never rewrites the manifest, so this is the blocking
|
|
189
200
|
* surface, and `awm sensors init` is the only thing that adopts the real pack.
|
|
190
201
|
*/
|
|
191
|
-
function checkPack(cwd,
|
|
202
|
+
function checkPack(cwd, status) {
|
|
203
|
+
const manifest = manifestView(status);
|
|
192
204
|
if (!manifest)
|
|
193
205
|
return { id: 'pack', ok: true, detail: 'skipped (no manifest)' };
|
|
194
206
|
const detection = (0, init_1.detectStack)(cwd);
|
|
@@ -231,7 +243,8 @@ function checkPack(cwd, manifest) {
|
|
|
231
243
|
* "baseline present" for that same case, reassuring the operator that debt is being
|
|
232
244
|
* suppressed when it silently is not.
|
|
233
245
|
*/
|
|
234
|
-
function checkSensorsBaseline(cwd,
|
|
246
|
+
function checkSensorsBaseline(cwd, status) {
|
|
247
|
+
const manifest = manifestView(status);
|
|
235
248
|
const enabled = manifest ? countEnabledSensors(manifest).enabled : 0;
|
|
236
249
|
if (enabled === 0) {
|
|
237
250
|
return { id: 'sensors-baseline', ok: true, detail: 'no enabled sensors — nothing to baseline' };
|
|
@@ -432,17 +445,21 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
432
445
|
|| (opts.requireCurrent !== undefined && typeof opts.requireCurrent !== 'boolean')) {
|
|
433
446
|
throw new Error('preflight options must contain optional boolean verifySensors and requireCurrent');
|
|
434
447
|
}
|
|
435
|
-
const
|
|
436
|
-
const manifestExists =
|
|
448
|
+
const sensorStatus = await (0, status_1.computeSensorStatus)(cwd);
|
|
449
|
+
const manifestExists = sensorStatus.mode !== 'missing';
|
|
437
450
|
const contextKernel = checkContextKernel(cwd);
|
|
438
451
|
const checks = [
|
|
439
452
|
checkContext(cwd),
|
|
440
453
|
...(contextKernel ? [contextKernel] : []),
|
|
441
|
-
checkManifest(
|
|
454
|
+
checkManifest(sensorStatus),
|
|
442
455
|
// Skipped when there is no manifest: reporting "tools broken" (or nudging toward
|
|
443
456
|
// a baseline that has nothing to snapshot) on a repo that was never set up
|
|
444
457
|
// buries the one thing the operator needs to read.
|
|
445
|
-
...(manifestExists
|
|
458
|
+
...(manifestExists && !['native-gate', 'opt-out', 'source-unavailable', 'source-ambiguous'].includes(sensorStatus.mode ?? '')
|
|
459
|
+
? [
|
|
460
|
+
...(sensorStatus.mode !== 'invalid' ? [await checkTools(cwd, sensorStatus), checkPack(cwd, sensorStatus)] : []),
|
|
461
|
+
checkSensorsBaseline(cwd, sensorStatus),
|
|
462
|
+
] : []),
|
|
446
463
|
// The empirical mode is intentionally opt-in: ordinary preflight remains a
|
|
447
464
|
// quick static inspection and must not dispatch project software.
|
|
448
465
|
...(opts.verifySensors === true ? [await checkSensorExecution(cwd)] : []),
|
|
@@ -455,17 +472,31 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
455
472
|
const blockingFailure = (check) => !check.ok && check.advisory !== true;
|
|
456
473
|
const invalidContextKernel = contextKernel !== null && blockingFailure(contextKernel);
|
|
457
474
|
const status = invalidContextKernel ? 'degraded'
|
|
458
|
-
:
|
|
459
|
-
:
|
|
460
|
-
: '
|
|
475
|
+
: sensorStatus.mode === 'native-gate' ? 'native_gate'
|
|
476
|
+
: sensorStatus.mode === 'opt-out' ? 'ungated'
|
|
477
|
+
: !manifestExists ? 'not_configured'
|
|
478
|
+
: checks.some(blockingFailure) ? 'degraded'
|
|
479
|
+
: 'ready';
|
|
461
480
|
const currentness = opts.requireCurrent === true ? await (0, check_1.checkCurrentness)(cwd) : undefined;
|
|
462
481
|
if (currentness) {
|
|
463
482
|
checks.splice(checks.length - 1, 0, checkAuthoritativeCurrentness(currentness));
|
|
464
483
|
}
|
|
465
484
|
const strictFailure = checks.some(blockingFailure);
|
|
466
485
|
const strictStatus = invalidContextKernel ? 'degraded'
|
|
467
|
-
:
|
|
468
|
-
:
|
|
469
|
-
: '
|
|
470
|
-
|
|
486
|
+
: sensorStatus.mode === 'native-gate' ? 'native_gate'
|
|
487
|
+
: sensorStatus.mode === 'opt-out' ? 'ungated'
|
|
488
|
+
: !manifestExists ? 'not_configured'
|
|
489
|
+
: strictFailure ? 'degraded'
|
|
490
|
+
: 'ready';
|
|
491
|
+
const authorityRemedy = sensorStatus.remedy
|
|
492
|
+
?? (sensorStatus.mode === 'native-gate' ? 'verify the declared native CI quality gate before accepting this run'
|
|
493
|
+
: sensorStatus.mode === 'opt-out' ? 'remove the opt-out declaration or provide a quality gate' : undefined);
|
|
494
|
+
const authority = {
|
|
495
|
+
mode: sensorStatus.mode ?? 'invalid',
|
|
496
|
+
reason: sensorStatus.reason ?? 'sensor-authority-unavailable',
|
|
497
|
+
projectRoot: sensorStatus.projectRoot ?? path_1.default.resolve(cwd),
|
|
498
|
+
manifestPath: sensorStatus.manifestPath ?? path_1.default.join(path_1.default.resolve(cwd), MANIFEST),
|
|
499
|
+
...(authorityRemedy ? { remedy: authorityRemedy } : {}),
|
|
500
|
+
};
|
|
501
|
+
return currentness ? { status: strictStatus, checks, currentness, ...authority } : { status, checks, ...authority };
|
|
471
502
|
}
|
|
@@ -209,9 +209,54 @@ function parseV2Manifest(value, source) {
|
|
|
209
209
|
}
|
|
210
210
|
return manifest;
|
|
211
211
|
}
|
|
212
|
+
function parseV3ProjectManifest(value, source) {
|
|
213
|
+
fields(value, ['schemaVersion', 'mode', 'pack', 'packSelection', 'source', 'packageRoot', 'sensors', 'concurrency'], source, 'root');
|
|
214
|
+
const sourceValue = record(value.source, source, 'source');
|
|
215
|
+
fields(sourceValue, ['registry'], source, 'source');
|
|
216
|
+
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
217
|
+
const sensors = {};
|
|
218
|
+
for (const name of Object.keys(sensorsInput))
|
|
219
|
+
sensors[id(name, source, 'sensor id')] = parseV2Sensor(sensorsInput[name], source, `sensors.${name}`);
|
|
220
|
+
const manifest = {
|
|
221
|
+
schemaVersion: 3,
|
|
222
|
+
mode: 'project-sensors',
|
|
223
|
+
pack: id(value.pack, source, 'pack'),
|
|
224
|
+
source: { registry: id(sourceValue.registry, source, 'source.registry') },
|
|
225
|
+
sensors,
|
|
226
|
+
};
|
|
227
|
+
if ('packSelection' in value) {
|
|
228
|
+
if (value.packSelection !== 'explicit')
|
|
229
|
+
invalid(source, 'packSelection must be "explicit" when present');
|
|
230
|
+
manifest.packSelection = 'explicit';
|
|
231
|
+
}
|
|
232
|
+
if ('packageRoot' in value)
|
|
233
|
+
manifest.packageRoot = asset(value.packageRoot, source, 'packageRoot');
|
|
234
|
+
if ('concurrency' in value) {
|
|
235
|
+
if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
|
|
236
|
+
invalid(source, 'concurrency must be a positive safe integer');
|
|
237
|
+
manifest.concurrency = value.concurrency;
|
|
238
|
+
}
|
|
239
|
+
return manifest;
|
|
240
|
+
}
|
|
241
|
+
function parseV3Manifest(value, source) {
|
|
242
|
+
if (value.schemaVersion !== 3)
|
|
243
|
+
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
|
|
244
|
+
if (value.mode === 'project-sensors')
|
|
245
|
+
return parseV3ProjectManifest(value, source);
|
|
246
|
+
if (value.mode !== 'native-gate' && value.mode !== 'opt-out')
|
|
247
|
+
invalid(source, 'schemaVersion 3 mode must be "project-sensors", "native-gate", or "opt-out"');
|
|
248
|
+
fields(value, ['schemaVersion', 'mode', 'reason'], source, 'root');
|
|
249
|
+
return { schemaVersion: 3, mode: value.mode, reason: text(value.reason, source, 'reason') };
|
|
250
|
+
}
|
|
212
251
|
function parseSensorManifest(input, source) {
|
|
213
252
|
const value = record(input, source, 'root');
|
|
214
|
-
|
|
253
|
+
if (!('schemaVersion' in value))
|
|
254
|
+
return { kind: 'legacy', pack: parseLegacyManifest(value, source) };
|
|
255
|
+
if (value.schemaVersion === 2)
|
|
256
|
+
return { kind: 'v2', pack: parseV2Manifest(value, source) };
|
|
257
|
+
if (value.schemaVersion === 3)
|
|
258
|
+
return { kind: 'v3', pack: parseV3Manifest(value, source) };
|
|
259
|
+
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
|
|
215
260
|
}
|
|
216
261
|
function serializeManifestV2(input) {
|
|
217
262
|
const parsed = parseSensorManifest(input, 'manifest serialization');
|
|
@@ -3,11 +3,19 @@ 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.listPackSources = listPackSources;
|
|
6
7
|
exports.resolvePackSource = resolvePackSource;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const registries_1 = require("../../../core/registries");
|
|
11
|
+
const safe_file_1 = require("./safe-file");
|
|
10
12
|
const MAX_PACK_BYTES = 1024 * 1024;
|
|
13
|
+
const MAX_DIAGNOSTIC_IDENTITY_LENGTH = 128;
|
|
14
|
+
function diagnosticRegistryName(name) {
|
|
15
|
+
return name.length <= MAX_DIAGNOSTIC_IDENTITY_LENGTH
|
|
16
|
+
? name
|
|
17
|
+
: `${name.slice(0, MAX_DIAGNOSTIC_IDENTITY_LENGTH - 3)}...`;
|
|
18
|
+
}
|
|
11
19
|
function validPackName(pack) {
|
|
12
20
|
return typeof pack === 'string' && /^[a-z][a-z0-9-]*$/.test(pack);
|
|
13
21
|
}
|
|
@@ -17,50 +25,79 @@ function contained(root, candidate) {
|
|
|
17
25
|
}
|
|
18
26
|
/** Walk each component under contentRoot with lstat: realpath alone would hide an
|
|
19
27
|
* internal symlink whose target happens to remain inside the registry. */
|
|
20
|
-
function inspectContainedPack(root, pack) {
|
|
28
|
+
function inspectContainedPack(root, pack, registryName) {
|
|
29
|
+
let rootStat;
|
|
30
|
+
try {
|
|
31
|
+
rootStat = fs_1.default.lstatSync(root, { bigint: true });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === 'ENOENT')
|
|
35
|
+
return undefined;
|
|
36
|
+
throw new Error(`registry "${registryName}" pack source root cannot be inspected`);
|
|
37
|
+
}
|
|
38
|
+
if (rootStat.isSymbolicLink())
|
|
39
|
+
throw new Error(`registry "${registryName}" pack source root must not be a symbolic link`);
|
|
40
|
+
if (!rootStat.isDirectory())
|
|
41
|
+
throw new Error(`registry "${registryName}" pack source root must be a regular directory`);
|
|
21
42
|
const components = ['sensor-packs', pack, 'pack.json'];
|
|
22
43
|
let candidate = root;
|
|
23
44
|
for (let index = 0; index < components.length; index++) {
|
|
24
45
|
candidate = path_1.default.join(candidate, components[index]);
|
|
25
46
|
let stat;
|
|
26
47
|
try {
|
|
27
|
-
stat = fs_1.default.lstatSync(candidate);
|
|
48
|
+
stat = fs_1.default.lstatSync(candidate, { bigint: true });
|
|
28
49
|
}
|
|
29
50
|
catch (error) {
|
|
30
51
|
if (error.code === 'ENOENT')
|
|
31
52
|
return undefined;
|
|
32
53
|
if (error.code === 'ENOTDIR')
|
|
33
|
-
throw new Error(`
|
|
34
|
-
throw new Error(`
|
|
54
|
+
throw new Error(`registry "${registryName}" pack source has a non-regular or symbolic parent`);
|
|
55
|
+
throw new Error(`registry "${registryName}" pack source cannot be inspected`);
|
|
35
56
|
}
|
|
36
57
|
if (stat.isSymbolicLink())
|
|
37
|
-
throw new Error(`
|
|
58
|
+
throw new Error(`registry "${registryName}" pack source must not contain symbolic links`);
|
|
38
59
|
if (index < components.length - 1 && !stat.isDirectory())
|
|
39
|
-
throw new Error(`
|
|
60
|
+
throw new Error(`registry "${registryName}" pack source has a non-regular parent`);
|
|
40
61
|
if (index === components.length - 1)
|
|
41
62
|
return { candidate, stat };
|
|
42
63
|
}
|
|
43
64
|
throw new Error('pack source component walk did not reach pack.json');
|
|
44
65
|
}
|
|
66
|
+
function readContainedPack(real, inspected, registryName) {
|
|
67
|
+
const failure = (reason) => {
|
|
68
|
+
if (reason === 'open')
|
|
69
|
+
return new Error(`registry "${registryName}" pack source cannot be safely opened`);
|
|
70
|
+
if (reason === 'regular')
|
|
71
|
+
return new Error(`registry "${registryName}" pack source must be a contained regular file, never a symbolic link`);
|
|
72
|
+
if (reason === 'identity')
|
|
73
|
+
return new Error(`registry "${registryName}" pack source changed identity during safe open`);
|
|
74
|
+
if (reason === 'size')
|
|
75
|
+
return new Error(`registry "${registryName}" pack source changed size during safe open`);
|
|
76
|
+
return new Error(`registry "${registryName}" pack source exceeds the 1 MiB limit`);
|
|
77
|
+
};
|
|
78
|
+
return (0, safe_file_1.readInspectedBoundedFile)(real, inspected, MAX_PACK_BYTES, failure).toString('utf8');
|
|
79
|
+
}
|
|
45
80
|
/** Resolve only an exact, regular pack.json beneath the first configured registry.
|
|
46
81
|
* Registry order is authority; an unsafe claimed source is a hard failure, not a fallback. */
|
|
47
|
-
function
|
|
82
|
+
function listPackSources(pack, options = {}) {
|
|
48
83
|
if (!validPackName(pack))
|
|
49
84
|
throw new Error('pack name must be a stable lowercase id');
|
|
50
85
|
const registries = options.registries ?? (0, registries_1.listRegistries)();
|
|
51
86
|
if (!Array.isArray(registries))
|
|
52
87
|
throw new Error('registries must be an array');
|
|
88
|
+
const sources = [];
|
|
53
89
|
for (const registry of registries) {
|
|
54
|
-
if (!registry || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '')
|
|
55
|
-
throw new Error('registry has an invalid content root');
|
|
56
|
-
const
|
|
90
|
+
if (!registry || typeof registry.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(registry.name) || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '')
|
|
91
|
+
throw new Error('registry has an invalid identity or content root');
|
|
92
|
+
const registryName = diagnosticRegistryName(registry.name);
|
|
93
|
+
const inspected = inspectContainedPack(registry.contentRoot, pack, registryName);
|
|
57
94
|
if (!inspected)
|
|
58
95
|
continue;
|
|
59
96
|
const { candidate, stat } = inspected;
|
|
60
97
|
if (!stat.isFile())
|
|
61
|
-
throw new Error(`
|
|
62
|
-
if (stat.size > MAX_PACK_BYTES)
|
|
63
|
-
throw new Error(`
|
|
98
|
+
throw new Error(`registry "${registryName}" pack source must be a contained regular file, never a symbolic link`);
|
|
99
|
+
if (stat.size > BigInt(MAX_PACK_BYTES))
|
|
100
|
+
throw new Error(`registry "${registryName}" pack source exceeds the 1 MiB limit`);
|
|
64
101
|
let root;
|
|
65
102
|
let real;
|
|
66
103
|
try {
|
|
@@ -68,11 +105,18 @@ function resolvePackSource(pack, options = {}) {
|
|
|
68
105
|
real = fs_1.default.realpathSync(candidate);
|
|
69
106
|
}
|
|
70
107
|
catch {
|
|
71
|
-
throw new Error(`
|
|
108
|
+
throw new Error(`registry "${registryName}" pack source cannot be canonicalized`);
|
|
72
109
|
}
|
|
73
110
|
if (!contained(root, real))
|
|
74
|
-
throw new Error(`
|
|
75
|
-
|
|
111
|
+
throw new Error(`registry "${registryName}" pack source escapes its registry root`);
|
|
112
|
+
const content = readContainedPack(real, stat, registryName);
|
|
113
|
+
sources.push({ path: real, content, registry });
|
|
76
114
|
}
|
|
77
|
-
|
|
115
|
+
return sources;
|
|
116
|
+
}
|
|
117
|
+
function resolvePackSource(pack, options = {}) {
|
|
118
|
+
const source = listPackSources(pack, options)[0];
|
|
119
|
+
if (source)
|
|
120
|
+
return source;
|
|
121
|
+
throw new Error('sensor pack was not found in configured registries');
|
|
78
122
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readInspectedBoundedFile = readInspectedBoundedFile;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
/**
|
|
9
|
+
* Read the exact regular file that was previously inspected. POSIX gets
|
|
10
|
+
* O_NOFOLLOW; platforms that do not expose it (notably Windows) compare the
|
|
11
|
+
* bigint device/inode identity and size across open. Bigints avoid losing the
|
|
12
|
+
* high bits of Windows file IDs.
|
|
13
|
+
*/
|
|
14
|
+
function readInspectedBoundedFile(file, inspected, maxBytes, failure) {
|
|
15
|
+
const noFollow = fs_1.default.constants.O_NOFOLLOW;
|
|
16
|
+
let descriptor;
|
|
17
|
+
try {
|
|
18
|
+
descriptor = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | (typeof noFollow === 'number' ? noFollow : 0));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw failure('open');
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
let opened;
|
|
25
|
+
try {
|
|
26
|
+
opened = fs_1.default.fstatSync(descriptor, { bigint: true });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw failure('identity');
|
|
30
|
+
}
|
|
31
|
+
if (!opened.isFile())
|
|
32
|
+
throw failure('regular');
|
|
33
|
+
if (opened.dev !== inspected.dev || opened.ino !== inspected.ino)
|
|
34
|
+
throw failure('identity');
|
|
35
|
+
if (opened.size !== inspected.size)
|
|
36
|
+
throw failure('size');
|
|
37
|
+
const limit = BigInt(maxBytes);
|
|
38
|
+
if (opened.size > limit)
|
|
39
|
+
throw failure('limit');
|
|
40
|
+
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
41
|
+
const count = fs_1.default.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
42
|
+
if (!Number.isSafeInteger(count) || count < 0 || count > maxBytes)
|
|
43
|
+
throw failure('limit');
|
|
44
|
+
if (BigInt(count) !== opened.size)
|
|
45
|
+
throw failure('size');
|
|
46
|
+
return buffer.subarray(0, count);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
fs_1.default.closeSync(descriptor);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveSensorSource = resolveSensorSource;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const contract_1 = require("./contract");
|
|
9
|
+
const pack_source_1 = require("./pack-source");
|
|
10
|
+
const MAX_CANDIDATE_IDENTITIES = 32;
|
|
11
|
+
const MAX_DIAGNOSTIC_IDENTITY_LENGTH = 128;
|
|
12
|
+
function diagnosticRegistryName(name) {
|
|
13
|
+
return name.length <= MAX_DIAGNOSTIC_IDENTITY_LENGTH
|
|
14
|
+
? name
|
|
15
|
+
: `${name.slice(0, MAX_DIAGNOSTIC_IDENTITY_LENGTH - 3)}...`;
|
|
16
|
+
}
|
|
17
|
+
function unavailable(reason) {
|
|
18
|
+
return { kind: 'source-unavailable', reason, remedy: 'install-registry-or-run-awm-update' };
|
|
19
|
+
}
|
|
20
|
+
function requireRegistries(deps) {
|
|
21
|
+
if (!deps || !Array.isArray(deps.registries))
|
|
22
|
+
throw new Error('resolveSensorSource requires a registry inventory');
|
|
23
|
+
for (const registry of deps.registries) {
|
|
24
|
+
if (!registry || typeof registry.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(registry.name)
|
|
25
|
+
|| typeof registry.remote !== 'string' || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '') {
|
|
26
|
+
throw new Error('resolveSensorSource received an invalid registry inventory entry');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return deps.registries;
|
|
30
|
+
}
|
|
31
|
+
function compatible(source, manifest) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error(`registry "${diagnosticRegistryName(source.registry.name)}" has an invalid sensor pack`);
|
|
38
|
+
}
|
|
39
|
+
if (parsed.kind !== 'v2' || parsed.pack.name !== manifest.pack)
|
|
40
|
+
return false;
|
|
41
|
+
return Object.entries(manifest.sensors).every(([name, sensor]) => parsed.pack.sensors[name]?.variants.some(variant => variant.id === sensor.variantId) === true);
|
|
42
|
+
}
|
|
43
|
+
/** Resolves only local registry inventory; it never fetches, probes tools, or writes. */
|
|
44
|
+
function resolveSensorSource(manifest, deps) {
|
|
45
|
+
const registries = requireRegistries(deps);
|
|
46
|
+
if (manifest.kind === 'v3') {
|
|
47
|
+
const v3 = manifest.pack;
|
|
48
|
+
if (!v3 || v3.mode !== 'project-sensors' || typeof v3.pack !== 'string'
|
|
49
|
+
|| !v3.source || typeof v3.source.registry !== 'string') {
|
|
50
|
+
throw new Error('sensor source resolution requires a valid project-sensors manifest');
|
|
51
|
+
}
|
|
52
|
+
if (!/^[a-z][a-z0-9-]*$/.test(v3.source.registry)) {
|
|
53
|
+
throw new Error('sensor source registry must be a stable lowercase id');
|
|
54
|
+
}
|
|
55
|
+
const matches = registries.filter(entry => entry?.name === v3.source.registry);
|
|
56
|
+
if (matches.length !== 1)
|
|
57
|
+
return unavailable('registry-not-installed');
|
|
58
|
+
const source = (0, pack_source_1.listPackSources)(v3.pack, { registries: matches })[0];
|
|
59
|
+
return source ? { kind: 'logical', source } : unavailable('no-compatible-registry');
|
|
60
|
+
}
|
|
61
|
+
if (manifest.kind !== 'v2')
|
|
62
|
+
throw new Error('sensor source resolution requires a v2 or v3 project-sensors manifest');
|
|
63
|
+
if (manifest.pack.registryRoot !== undefined && fs_1.default.existsSync(manifest.pack.registryRoot)) {
|
|
64
|
+
const source = (0, pack_source_1.listPackSources)(manifest.pack.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: manifest.pack.registryRoot }] })[0];
|
|
65
|
+
if (!source)
|
|
66
|
+
throw new Error('existing v2 registry provenance does not contain its selected sensor pack');
|
|
67
|
+
return { kind: 'legacy-bound', source };
|
|
68
|
+
}
|
|
69
|
+
const candidates = (0, pack_source_1.listPackSources)(manifest.pack.pack, { registries }).filter(source => compatible(source, manifest.pack));
|
|
70
|
+
if (candidates.length === 0)
|
|
71
|
+
return unavailable('no-compatible-registry');
|
|
72
|
+
if (candidates.length === 1)
|
|
73
|
+
return { kind: 'legacy-rebound', source: candidates[0] };
|
|
74
|
+
return { kind: 'source-ambiguous', candidates: candidates.map(candidate => diagnosticRegistryName(candidate.registry.name)).sort().slice(0, MAX_CANDIDATE_IDENTITIES), reason: 'multiple-compatible-registries', remedy: 'configure-one-logical-registry' };
|
|
75
|
+
}
|
|
@@ -11,6 +11,7 @@ const contract_1 = require("./contract");
|
|
|
11
11
|
const contract_2 = require("../compatibility/contract");
|
|
12
12
|
const manifest_1 = require("../compatibility/manifest");
|
|
13
13
|
const pack_source_1 = require("../compatibility/pack-source");
|
|
14
|
+
const safe_file_1 = require("../compatibility/safe-file");
|
|
14
15
|
function readFailure(file, error) {
|
|
15
16
|
return new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
16
17
|
}
|
|
@@ -20,40 +21,33 @@ function readBoundedJson(file) {
|
|
|
20
21
|
throw new Error('readBoundedJson: file must be a non-empty string');
|
|
21
22
|
let listed;
|
|
22
23
|
try {
|
|
23
|
-
listed = fs_1.default.lstatSync(file);
|
|
24
|
+
listed = fs_1.default.lstatSync(file, { bigint: true });
|
|
24
25
|
}
|
|
25
26
|
catch (error) {
|
|
26
27
|
throw readFailure(file, error);
|
|
27
28
|
}
|
|
28
29
|
if (!listed.isFile() || listed.isSymbolicLink())
|
|
29
30
|
throw new Error(`Cannot read ${file}: expected a regular file`);
|
|
30
|
-
if (listed.size > contract_1.MAX_COVERAGE_FILE_BYTES)
|
|
31
|
+
if (listed.size > BigInt(contract_1.MAX_COVERAGE_FILE_BYTES))
|
|
31
32
|
throw new Error(`Cannot read ${file}: exceeds 1 MiB limit`);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const failure = (reason) => {
|
|
34
|
+
if (reason === 'open')
|
|
35
|
+
return new Error('cannot be safely opened');
|
|
36
|
+
if (reason === 'regular')
|
|
37
|
+
return new Error('expected a regular file');
|
|
38
|
+
if (reason === 'identity')
|
|
39
|
+
return new Error('file changed identity during safe open');
|
|
40
|
+
if (reason === 'size')
|
|
41
|
+
return new Error('file changed size during safe open');
|
|
42
|
+
return new Error('exceeds 1 MiB limit');
|
|
43
|
+
};
|
|
36
44
|
let content;
|
|
37
45
|
try {
|
|
38
|
-
|
|
39
|
-
const opened = fs_1.default.fstatSync(descriptor);
|
|
40
|
-
if (!opened.isFile() || opened.size > contract_1.MAX_COVERAGE_FILE_BYTES) {
|
|
41
|
-
throw new Error('expected a regular file within the 1 MiB limit');
|
|
42
|
-
}
|
|
43
|
-
const buffer = Buffer.allocUnsafe(contract_1.MAX_COVERAGE_FILE_BYTES + 1);
|
|
44
|
-
const count = fs_1.default.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
45
|
-
if (!Number.isSafeInteger(count) || count < 0 || count > contract_1.MAX_COVERAGE_FILE_BYTES) {
|
|
46
|
-
throw new Error('exceeds 1 MiB limit');
|
|
47
|
-
}
|
|
48
|
-
content = buffer.subarray(0, count).toString('utf8');
|
|
46
|
+
content = (0, safe_file_1.readInspectedBoundedFile)(file, listed, contract_1.MAX_COVERAGE_FILE_BYTES, failure).toString('utf8');
|
|
49
47
|
}
|
|
50
48
|
catch (error) {
|
|
51
49
|
throw readFailure(file, error);
|
|
52
50
|
}
|
|
53
|
-
finally {
|
|
54
|
-
if (descriptor !== undefined)
|
|
55
|
-
fs_1.default.closeSync(descriptor);
|
|
56
|
-
}
|
|
57
51
|
try {
|
|
58
52
|
return JSON.parse(content);
|
|
59
53
|
}
|
|
@@ -88,6 +82,8 @@ function resolveCoverageInputs(cwd) {
|
|
|
88
82
|
return { kind: 'not_configured' };
|
|
89
83
|
const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
90
84
|
const manifest = (0, manifest_1.parseSensorManifest)(readBoundedJson(manifestPath), manifestPath);
|
|
85
|
+
if (manifest.kind === 'v3')
|
|
86
|
+
throw new Error('schemaVersion 3 sensor manifests require the project resolver');
|
|
91
87
|
const source = manifest.kind === 'v2' && manifest.pack.registryRoot !== undefined
|
|
92
88
|
? (0, pack_source_1.resolvePackSource)(manifest.pack.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: manifest.pack.registryRoot }] })
|
|
93
89
|
: (0, pack_source_1.resolvePackSource)(manifest.pack.pack);
|
|
@@ -246,8 +246,10 @@ async function initSensors(opts = {}) {
|
|
|
246
246
|
const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
|
|
247
247
|
if (parsed.kind === 'legacy')
|
|
248
248
|
existing = parsed.pack;
|
|
249
|
-
else
|
|
249
|
+
else if (parsed.kind === 'v2')
|
|
250
250
|
existingV2 = parsed.pack;
|
|
251
|
+
else
|
|
252
|
+
throw new Error('schemaVersion 3 sensor manifests require the project resolver');
|
|
251
253
|
}
|
|
252
254
|
// `detection` keeps saying what the tree IS; `pack` is what the registry can serve
|
|
253
255
|
// for it. They diverge only when the registry lacks the detected pack.
|