agentic-workflow-manager 9.4.1 → 9.6.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.
Files changed (57) hide show
  1. package/dist/src/commands/preflight/checks.js +58 -27
  2. package/dist/src/commands/sensors/bootstrap.js +171 -0
  3. package/dist/src/commands/sensors/compatibility/contract.js +18 -15
  4. package/dist/src/commands/sensors/compatibility/manifest.js +75 -2
  5. package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
  6. package/dist/src/commands/sensors/compatibility/pack-source.js +61 -17
  7. package/dist/src/commands/sensors/compatibility/safe-file.js +286 -0
  8. package/dist/src/commands/sensors/compatibility/source.js +75 -0
  9. package/dist/src/commands/sensors/coverage/index.js +2 -2
  10. package/dist/src/commands/sensors/coverage/resolve.js +35 -29
  11. package/dist/src/commands/sensors/detection.js +31 -0
  12. package/dist/src/commands/sensors/exec.js +116 -11
  13. package/dist/src/commands/sensors/index.js +51 -10
  14. package/dist/src/commands/sensors/init.js +17 -168
  15. package/dist/src/commands/sensors/migrate.js +209 -0
  16. package/dist/src/commands/sensors/project.js +107 -0
  17. package/dist/src/commands/sensors/run.js +66 -47
  18. package/dist/src/commands/sensors/status.js +97 -56
  19. package/dist/src/core/init/steps.js +24 -11
  20. package/dist/src/core/secure-fs/native-bridge.js +209 -0
  21. package/dist/tests/commands/preflight/preflight.test.js +31 -0
  22. package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
  23. package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
  24. package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
  25. package/dist/tests/commands/sensors/compatibility/manifest.test.js +90 -0
  26. package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
  27. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +249 -0
  28. package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
  29. package/dist/tests/commands/sensors/compatibility/source.test.js +121 -0
  30. package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
  31. package/dist/tests/commands/sensors/coverage/resolve.test.js +36 -34
  32. package/dist/tests/commands/sensors/exec.test.js +86 -0
  33. package/dist/tests/commands/sensors/index.test.js +54 -0
  34. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
  35. package/dist/tests/commands/sensors/init.test.js +56 -490
  36. package/dist/tests/commands/sensors/migrate.test.js +513 -0
  37. package/dist/tests/commands/sensors/project.test.js +102 -0
  38. package/dist/tests/commands/sensors/run.test.js +107 -4
  39. package/dist/tests/commands/sensors/status.test.js +106 -2
  40. package/dist/tests/core/init/orchestrator.test.js +3 -1
  41. package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
  42. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +5 -0
  43. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
  44. package/dist/tests/integration/sensor-compatibility.e2e.test.js +14 -23
  45. package/dist/tests/integration/sensor-coverage.e2e.test.js +15 -24
  46. package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
  47. package/dist/tests/structural/native-release-artifacts.test.js +22 -0
  48. package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
  49. package/dist/tests/structural/release-registry-provenance.test.js +4 -0
  50. package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
  51. package/package.json +6 -2
  52. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  53. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  54. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  55. package/prebuilds/linux-x64/secure_fs.node +0 -0
  56. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  57. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -9,7 +9,7 @@ const child_process_1 = require("child_process");
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const status_1 = require("../sensors/status");
12
- const init_1 = require("../sensors/init");
12
+ const detection_1 = require("../sensors/detection");
13
13
  const baseline_1 = require("../sensors/baseline");
14
14
  const run_1 = require("../sensors/run");
15
15
  const paths_1 = require("../../core/paths");
@@ -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,10 +199,11 @@ 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
- const detection = (0, init_1.detectStack)(cwd);
206
+ const detection = (0, detection_1.detectStack)(cwd);
195
207
  if (manifest.pack === 'generic' && detection.pack !== 'generic') {
196
208
  return {
197
209
  id: 'pack',
@@ -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
  }
@@ -0,0 +1,171 @@
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.planSensorBootstrap = planSensorBootstrap;
7
+ exports.applySensorBootstrap = applySensorBootstrap;
8
+ const registries_1 = require("../../core/registries");
9
+ const contract_1 = require("./compatibility/contract");
10
+ const manifest_1 = require("./compatibility/manifest");
11
+ const pack_source_1 = require("./compatibility/pack-source");
12
+ const live_1 = require("./compatibility/live");
13
+ const source_1 = require("./compatibility/source");
14
+ const detection_1 = require("./detection");
15
+ const migrate_1 = require("./migrate");
16
+ const project_1 = require("./project");
17
+ const materialize_1 = require("./compatibility/materialize");
18
+ const migrate_2 = require("./migrate");
19
+ const safe_file_1 = require("./compatibility/safe-file");
20
+ const fs_1 = __importDefault(require("fs"));
21
+ const crypto_1 = require("crypto");
22
+ const path_1 = __importDefault(require("path"));
23
+ function blocked(projectRoot, manifestPath, dryRun, reason, remedy, candidates) {
24
+ return { kind: 'blocked', projectRoot, manifestPath, changes: [], dryRun, reason, remedy, ...(candidates ? { candidates } : {}) };
25
+ }
26
+ function options(input) {
27
+ if (input === undefined)
28
+ return { dryRun: false };
29
+ if (!input || typeof input !== 'object' || Array.isArray(input))
30
+ throw new Error('bootstrap options must be an object');
31
+ const value = input;
32
+ if (Object.keys(value).some(key => !['mode', 'reason', 'dryRun', 'registryRoot', 'configure', 'pack', 'packageRoot'].includes(key)))
33
+ throw new Error('bootstrap options contain an unknown field');
34
+ if (value.mode !== undefined && value.mode !== 'project-sensors' && value.mode !== 'native-gate' && value.mode !== 'opt-out')
35
+ throw new Error('bootstrap mode must be project-sensors, native-gate, or opt-out');
36
+ if (value.reason !== undefined && (typeof value.reason !== 'string' || value.reason.trim() === '' || value.reason.length > 512 || /[\0\r\n]/.test(value.reason)))
37
+ throw new Error('bootstrap reason must be a nonempty bounded single-line string');
38
+ if (value.dryRun !== undefined && typeof value.dryRun !== 'boolean')
39
+ throw new Error('bootstrap dryRun must be a boolean');
40
+ if (value.configure !== undefined && typeof value.configure !== 'boolean')
41
+ throw new Error('bootstrap configure must be a boolean');
42
+ if (value.registryRoot !== undefined && (typeof value.registryRoot !== 'string' || (!path_1.default.isAbsolute(value.registryRoot) && !path_1.default.win32.isAbsolute(value.registryRoot)) || /[\0\r\n]/.test(value.registryRoot)))
43
+ throw new Error('bootstrap registryRoot must be an absolute single-line path');
44
+ if (value.pack !== undefined && (typeof value.pack !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.pack)))
45
+ throw new Error('bootstrap pack must be a stable lowercase id');
46
+ if (value.packageRoot !== undefined && (typeof value.packageRoot !== 'string' || value.packageRoot.length === 0 || value.packageRoot.includes('\\') || value.packageRoot.split('/').some(part => part === '' || part === '.' || part === '..')))
47
+ throw new Error('bootstrap packageRoot must be a contained relative path');
48
+ return { mode: value.mode, reason: value.reason, dryRun: value.dryRun === true, registryRoot: value.registryRoot, configure: value.configure !== false, pack: value.pack, packageRoot: value.packageRoot };
49
+ }
50
+ function declaration(mode, reason) {
51
+ return { schemaVersion: 3, mode, reason };
52
+ }
53
+ function digest(bytes) { return (0, crypto_1.createHash)('sha256').update(bytes).digest('hex'); }
54
+ async function projectSensors(projectRoot, registryRoot, requestedPack, packageRoot) {
55
+ const detectionRoot = packageRoot ? path_1.default.resolve(projectRoot, packageRoot) : projectRoot;
56
+ const detected = (0, detection_1.detectStack)(detectionRoot);
57
+ const pack = requestedPack ?? detected.pack;
58
+ const registries = (0, registries_1.listRegistries)();
59
+ const selected = registryRoot === undefined ? registries : registries.filter(registry => registry.contentRoot === registryRoot);
60
+ if (registryRoot !== undefined && selected.length !== 1)
61
+ return { reason: 'registry-root-not-configured', remedy: 'configure-the-registry-before-bootstrap' };
62
+ const sources = (0, pack_source_1.listPackSources)(pack, { registries: selected });
63
+ if (sources.length === 0)
64
+ return { reason: 'source-unavailable', remedy: 'install-registry-or-run-awm-update' };
65
+ if (sources.length !== 1)
66
+ return { reason: 'source-ambiguous', remedy: 'configure-one-logical-registry', candidates: sources.map(item => item.registry.name).sort().slice(0, 32) };
67
+ const source = sources[0];
68
+ let parsed;
69
+ try {
70
+ parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
71
+ }
72
+ catch {
73
+ return { reason: 'source-invalid', remedy: 'repair-or-run-awm-update' };
74
+ }
75
+ if (parsed.kind !== 'v2')
76
+ return { reason: 'source-unsupported', remedy: 'install-a-v2-sensor-pack' };
77
+ let live;
78
+ try {
79
+ live = await (0, live_1.resolveParsedPackCompatibility)(detectionRoot, parsed.pack);
80
+ }
81
+ catch {
82
+ return { reason: 'compatibility-unresolvable', remedy: 'repair-project-tools-or-select-another-mode' };
83
+ }
84
+ const sensors = {};
85
+ for (const [name, packSensor] of Object.entries(live.pack.sensors)) {
86
+ const evidence = live.sensors[name];
87
+ const variant = evidence.variantId === null ? undefined : packSensor.variants.find(item => item.id === evidence.variantId);
88
+ if (!variant)
89
+ return { reason: 'sensor-variant-unresolvable', remedy: 'repair-project-tools-or-select-another-mode' };
90
+ sensors[name] = { enabled: true, ...(packSensor.fast === undefined ? {} : { fast: packSensor.fast }), ...(packSensor.timeout === undefined ? {} : { timeout: packSensor.timeout }), variantId: variant.id, command: variant.command, ...(variant.assets.length === 0 ? {} : { assets: variant.assets }), ...(variant.policyRef ? { policyRef: variant.policyRef } : {}), initializedCompatibility: evidence };
91
+ }
92
+ const manifest = (0, manifest_1.parseSensorManifest)({ schemaVersion: 3, mode: 'project-sensors', pack, source: { registry: source.registry.name }, ...(packageRoot ? { packageRoot } : {}), sensors }, 'bootstrap candidate');
93
+ if (manifest.kind !== 'v3' || manifest.pack.mode !== 'project-sensors')
94
+ throw new Error('bootstrap project-sensors candidate is invalid');
95
+ (0, manifest_1.serializeManifestV3)(manifest.pack);
96
+ const assets = [...new Set(Object.values(sensors).flatMap(sensor => sensor.assets ?? []))].sort();
97
+ return { manifest: manifest.pack, source, changes: [{ path: '.awm/sensors.json', action: 'create' }, ...assets.map(asset => ({ path: packageRoot ? `${packageRoot}/${asset}` : asset, action: 'create' }))] };
98
+ }
99
+ /** Plan a one-time portable bootstrap. This function never writes project or machine state. */
100
+ async function planSensorBootstrap(cwd = process.cwd(), input = {}) {
101
+ const opts = options(input);
102
+ const project = (0, project_1.resolveSensorProject)(cwd);
103
+ if (project.state === 'invalid')
104
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'manifest-invalid', 'repair-sensor-manifest');
105
+ if (project.state === 'configured') {
106
+ if (project.manifest.kind === 'v3') {
107
+ if (opts.mode !== undefined && opts.mode !== project.manifest.pack.mode) {
108
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'mode-conflicts-with-existing-declaration', 'run-bootstrap-without-mode');
109
+ }
110
+ return { kind: 'noop', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: [], dryRun: opts.dryRun };
111
+ }
112
+ if (project.manifest.kind === 'legacy')
113
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'legacy-v1-preserved', 'migrate-the-manifest-explicitly');
114
+ let source;
115
+ try {
116
+ source = (0, source_1.resolveSensorSource)(project.manifest, { registries: (0, registries_1.listRegistries)() });
117
+ }
118
+ catch {
119
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'source-invalid', 'repair-or-run-awm-update');
120
+ }
121
+ if (!('source' in source))
122
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, source.reason, source.remedy);
123
+ try {
124
+ const original = fs_1.default.readFileSync(project.manifestPath);
125
+ return { kind: 'migrate', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: [{ path: '.awm/sensors.json', action: 'replace' }], dryRun: opts.dryRun, migration: (0, migrate_1.planV2Migration)({ manifest: project.manifest.pack, source }), source, originalDigest: digest(original) };
126
+ }
127
+ catch {
128
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'migration-not-equivalent', 'repair-or-select-one-compatible-registry');
129
+ }
130
+ }
131
+ if (!opts.mode)
132
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'mode-required', 'choose-project-sensors-native-gate-or-opt-out');
133
+ if (opts.mode !== 'project-sensors') {
134
+ if (!opts.reason)
135
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'reason-required', 'provide-a-versioned-reason');
136
+ return { kind: 'create', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: [{ path: '.awm/sensors.json', action: 'create' }], dryRun: opts.dryRun, manifest: declaration(opts.mode, opts.reason), configure: opts.configure };
137
+ }
138
+ const planned = await projectSensors(project.projectRoot, opts.registryRoot, opts.pack, opts.packageRoot);
139
+ if ('reason' in planned)
140
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, planned.reason, planned.remedy, 'candidates' in planned ? planned.candidates : undefined);
141
+ return { kind: 'create', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: planned.changes, dryRun: opts.dryRun, manifest: planned.manifest, source: planned.source, configure: opts.configure };
142
+ }
143
+ /** Apply only a validated, non-dry-run create or migration plan. */
144
+ function applySensorBootstrap(plan) {
145
+ if (!plan || typeof plan !== 'object')
146
+ throw new Error('bootstrap plan is required');
147
+ if (plan.kind !== 'create' && plan.kind !== 'migrate')
148
+ throw new Error('only a create or migrate bootstrap plan can be applied');
149
+ if (plan.dryRun)
150
+ throw new Error('a dry-run bootstrap plan cannot be applied');
151
+ if (plan.kind === 'migrate') {
152
+ const current = fs_1.default.readFileSync(plan.manifestPath);
153
+ if (digest(current) !== plan.originalDigest)
154
+ throw new Error('bootstrap migration plan is stale: manifest changed after planning');
155
+ (0, migrate_2.replaceV2ManifestWithV3)(plan.manifestPath, plan.migration.candidate, plan.source);
156
+ return 'migrated';
157
+ }
158
+ if (plan.manifest.mode === 'project-sensors') {
159
+ if (!plan.source)
160
+ throw new Error('project-sensors bootstrap plan requires its resolved source');
161
+ (0, materialize_1.materializePortableSensors)({ projectRoot: plan.projectRoot, pack: plan.manifest.pack, source: plan.source, ...(plan.manifest.packageRoot ? { packageRoot: plan.manifest.packageRoot } : {}), sensors: plan.manifest.sensors, configure: plan.configure !== false });
162
+ return 'created';
163
+ }
164
+ const parsed = (0, manifest_1.parseSensorManifest)(plan.manifest, 'bootstrap declaration');
165
+ if (parsed.kind !== 'v3' || parsed.pack.mode === 'project-sensors')
166
+ throw new Error('bootstrap declaration plan is invalid');
167
+ (0, safe_file_1.withProjectLease)(plan.projectRoot, () => {
168
+ (0, safe_file_1.writeProjectFile)(plan.projectRoot, '.awm/sensors.json', Buffer.from((0, manifest_1.serializeManifestV3)(parsed.pack), 'utf8'), { mode: 'create', createParents: true });
169
+ });
170
+ return 'created';
171
+ }
@@ -12,6 +12,7 @@ const semver_1 = __importDefault(require("semver"));
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const contract_1 = require("../coverage/contract");
15
+ const safe_file_1 = require("./safe-file");
15
16
  const timeout_1 = require("./timeout");
16
17
  const PACK_SCHEMA_VERSION = 2;
17
18
  const SHELL_EXECUTABLES = new Set(['sh', 'bash', 'cmd', 'powershell']);
@@ -107,32 +108,34 @@ function resolveSemgrepPolicy(policyRef, source, location = 'policy') {
107
108
  invalid(source, `${location}.policyRef escapes sensor-packs root`);
108
109
  let stat;
109
110
  try {
110
- stat = fs_1.default.lstatSync(candidate);
111
+ stat = fs_1.default.lstatSync(candidate, { bigint: true });
111
112
  }
112
113
  catch {
113
114
  invalid(source, `${location}.policyRef does not resolve to a regular policy file`);
114
115
  }
115
116
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_POLICY_BYTES)
116
117
  invalid(source, `${location}.policyRef must resolve to a bounded regular policy file`);
117
- let realRoot;
118
- let realPolicy;
119
- try {
120
- realRoot = fs_1.default.realpathSync(sensorPacksRoot);
121
- realPolicy = fs_1.default.realpathSync(candidate);
122
- }
123
- catch {
124
- invalid(source, `${location}.policyRef cannot be canonicalized`);
125
- }
126
- if (!contained(realRoot, realPolicy))
127
- invalid(source, `${location}.policyRef escapes sensor-packs root`);
118
+ const failure = (reason) => {
119
+ if (reason === 'open')
120
+ return new Error(`Invalid sensor pack${sourceSuffix(source)}: ${location}.policyRef cannot be safely opened`);
121
+ if (reason === 'regular')
122
+ return new Error(`Invalid sensor pack${sourceSuffix(source)}: ${location}.policyRef must resolve to a bounded regular policy file`);
123
+ if (reason === 'identity')
124
+ return new Error(`Invalid sensor pack${sourceSuffix(source)}: ${location}.policyRef changed identity during safe open`);
125
+ if (reason === 'size')
126
+ return new Error(`Invalid sensor pack${sourceSuffix(source)}: ${location}.policyRef changed size during safe open`);
127
+ return new Error(`Invalid sensor pack${sourceSuffix(source)}: ${location}.policyRef must resolve to a bounded regular policy file`);
128
+ };
128
129
  let parsed;
129
130
  try {
130
- parsed = JSON.parse(fs_1.default.readFileSync(realPolicy, 'utf8'));
131
+ parsed = JSON.parse((0, safe_file_1.readInspectedBoundedFile)(candidate, stat, MAX_POLICY_BYTES, failure).toString('utf8'));
131
132
  }
132
- catch {
133
+ catch (error) {
134
+ if (error instanceof Error && error.message.startsWith('Invalid sensor pack'))
135
+ throw error;
133
136
  invalid(source, `${location}.policyRef must contain valid JSON`);
134
137
  }
135
- return parseSemgrepPolicy(parsed, realPolicy);
138
+ return parseSemgrepPolicy(parsed, candidate);
136
139
  }
137
140
  function parseStructuredCommand(input, source) {
138
141
  const value = record(input, source, 'command');
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.legacyCompatibility = legacyCompatibility;
7
7
  exports.parseSensorManifest = parseSensorManifest;
8
8
  exports.serializeManifestV2 = serializeManifestV2;
9
+ exports.serializeManifestV3 = serializeManifestV3;
9
10
  const contract_1 = require("./contract");
10
11
  const timeout_1 = require("./timeout");
11
12
  const semver_1 = __importDefault(require("semver"));
@@ -183,6 +184,22 @@ function provenanceRoot(value, source) {
183
184
  invalid(source, 'registryRoot must be an absolute normalized path');
184
185
  return parsed;
185
186
  }
187
+ /**
188
+ * A v3 declaration is copied between machines, so no nested string may retain
189
+ * a host path. Structured command arguments are intentionally free-form and
190
+ * are therefore the only remaining place an otherwise validated declaration
191
+ * could hide one.
192
+ */
193
+ function containsPhysicalPath(value) {
194
+ if (typeof value === 'string') {
195
+ // Web URLs are portable command values, not host filesystem paths.
196
+ const withoutWebUrls = value.replace(/https?:\/\/[^\s"']+/gi, '');
197
+ return /(?:^|[^A-Za-z0-9_.~\/\\-])["']?(?:\/|\\|[A-Za-z]:|\/\/)/.test(withoutWebUrls);
198
+ }
199
+ return Array.isArray(value)
200
+ ? value.some(containsPhysicalPath)
201
+ : isRecord(value) && Object.values(value).some(containsPhysicalPath);
202
+ }
186
203
  function parseV2Manifest(value, source) {
187
204
  fields(value, ['schemaVersion', 'pack', 'packSelection', 'registryRoot', 'packageRoot', 'sensors', 'concurrency'], source, 'root');
188
205
  if (value.schemaVersion !== 2)
@@ -209,13 +226,69 @@ function parseV2Manifest(value, source) {
209
226
  }
210
227
  return manifest;
211
228
  }
229
+ function parseV3ProjectManifest(value, source) {
230
+ fields(value, ['schemaVersion', 'mode', 'pack', 'packSelection', 'source', 'packageRoot', 'sensors', 'concurrency'], source, 'root');
231
+ const sourceValue = record(value.source, source, 'source');
232
+ fields(sourceValue, ['registry'], source, 'source');
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}`);
237
+ const manifest = {
238
+ schemaVersion: 3,
239
+ mode: 'project-sensors',
240
+ pack: id(value.pack, source, 'pack'),
241
+ source: { registry: id(sourceValue.registry, source, 'source.registry') },
242
+ sensors,
243
+ };
244
+ if ('packSelection' in value) {
245
+ if (value.packSelection !== 'explicit')
246
+ invalid(source, 'packSelection must be "explicit" when present');
247
+ manifest.packSelection = 'explicit';
248
+ }
249
+ if ('packageRoot' in value)
250
+ manifest.packageRoot = asset(value.packageRoot, source, 'packageRoot');
251
+ if ('concurrency' in value) {
252
+ if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
253
+ invalid(source, 'concurrency must be a positive safe integer');
254
+ manifest.concurrency = value.concurrency;
255
+ }
256
+ return manifest;
257
+ }
258
+ function parseV3Manifest(value, source) {
259
+ if (value.schemaVersion !== 3)
260
+ invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
261
+ if (value.mode === 'project-sensors')
262
+ return parseV3ProjectManifest(value, source);
263
+ if (value.mode !== 'native-gate' && value.mode !== 'opt-out')
264
+ invalid(source, 'schemaVersion 3 mode must be "project-sensors", "native-gate", or "opt-out"');
265
+ fields(value, ['schemaVersion', 'mode', 'reason'], source, 'root');
266
+ return { schemaVersion: 3, mode: value.mode, reason: text(value.reason, source, 'reason') };
267
+ }
212
268
  function parseSensorManifest(input, source) {
213
269
  const value = record(input, source, 'root');
214
- return 'schemaVersion' in value ? { kind: 'v2', pack: parseV2Manifest(value, source) } : { kind: 'legacy', pack: parseLegacyManifest(value, source) };
270
+ if (!('schemaVersion' in value))
271
+ return { kind: 'legacy', pack: parseLegacyManifest(value, source) };
272
+ if (value.schemaVersion === 2)
273
+ return { kind: 'v2', pack: parseV2Manifest(value, source) };
274
+ if (value.schemaVersion === 3)
275
+ return { kind: 'v3', pack: parseV3Manifest(value, source) };
276
+ invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
215
277
  }
216
278
  function serializeManifestV2(input) {
217
279
  const parsed = parseSensorManifest(input, 'manifest serialization');
218
280
  if (parsed.kind !== 'v2')
219
281
  throw new Error('Cannot serialize a legacy sensor manifest as v2');
220
- return JSON.stringify(parsed.pack, null, 2);
282
+ return JSON.stringify(parsed.pack, null, 2) + '\n';
283
+ }
284
+ /** Serialize a validated portable v3 declaration deterministically. */
285
+ function serializeManifestV3(input) {
286
+ const parsed = parseSensorManifest(input, 'manifest serialization');
287
+ if (parsed.kind !== 'v3') {
288
+ throw new Error('Cannot serialize a non-v3 sensor manifest as v3');
289
+ }
290
+ if (parsed.pack.mode === 'project-sensors' && containsPhysicalPath(parsed.pack)) {
291
+ throw new Error('Cannot serialize a v3 declaration containing a physical path');
292
+ }
293
+ return JSON.stringify(parsed.pack, null, 2) + '\n';
221
294
  }