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.
@@ -83,13 +83,13 @@ function checkContextKernel(cwd) {
83
83
  remedy: inspection.remedy,
84
84
  };
85
85
  }
86
- function readManifest(cwd) {
87
- try {
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(cwd, manifest) {
115
- if (!fs_1.default.existsSync(path_1.default.join(cwd, MANIFEST))) {
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 (!manifest) {
124
+ if (status.mode === 'invalid') {
125
125
  return {
126
126
  id: 'manifest',
127
127
  ok: false,
128
- detail: '.awm/sensors.json is not valid JSON',
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, manifest) {
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, manifest) {
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 manifest = readManifest(cwd);
436
- const manifestExists = fs_1.default.existsSync(path_1.default.join(cwd, MANIFEST));
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(cwd, manifest),
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 ? [await checkTools(cwd), checkPack(cwd, manifest), checkSensorsBaseline(cwd, manifest)] : []),
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
- : !manifestExists ? 'not_configured'
459
- : checks.some(blockingFailure) ? 'degraded'
460
- : 'ready';
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
- : !manifestExists ? 'not_configured'
468
- : strictFailure ? 'degraded'
469
- : 'ready';
470
- return currentness ? { status: strictStatus, checks, currentness } : { status, checks };
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
- return 'schemaVersion' in value ? { kind: 'v2', pack: parseV2Manifest(value, source) } : { kind: 'legacy', pack: parseLegacyManifest(value, source) };
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(`pack source ${candidate} has a non-regular or symbolic parent`);
34
- throw new Error(`cannot inspect pack source ${candidate}`);
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(`pack source ${candidate} must not contain symbolic links`);
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(`pack source ${candidate} has a non-regular parent`);
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 resolvePackSource(pack, options = {}) {
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 inspected = inspectContainedPack(registry.contentRoot, pack);
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(`pack source ${candidate} must be a contained regular file, never a symbolic link`);
62
- if (stat.size > MAX_PACK_BYTES)
63
- throw new Error(`pack source ${candidate} exceeds the 1 MiB limit`);
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(`cannot canonicalize pack source ${candidate}`);
108
+ throw new Error(`registry "${registryName}" pack source cannot be canonicalized`);
72
109
  }
73
110
  if (!contained(root, real))
74
- throw new Error(`pack source ${candidate} escapes its registry root`);
75
- return { path: real, content: fs_1.default.readFileSync(real, 'utf8'), registry };
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
- throw new Error(`sensor pack "${pack}" was not found in configured registries`);
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 noFollow = fs_1.default.constants.O_NOFOLLOW;
33
- if (typeof noFollow !== 'number')
34
- throw new Error(`Cannot read ${file}: platform cannot guarantee no symlink dereference`);
35
- let descriptor;
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
- descriptor = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | noFollow);
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.