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
@@ -3,18 +3,22 @@ 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.materializePortableSensors = materializePortableSensors;
6
7
  exports.materializeResolvedSensors = materializeResolvedSensors;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const manifest_1 = require("./manifest");
10
11
  const contract_1 = require("./contract");
12
+ const safe_file_1 = require("./safe-file");
13
+ const native_bridge_1 = require("../../../core/secure-fs/native-bridge");
14
+ const MAX_ASSET_BYTES = 1024 * 1024;
11
15
  function stableId(value, label) {
12
16
  if (typeof value !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value))
13
17
  throw new Error(`${label} must be a stable lowercase id`);
14
18
  return value;
15
19
  }
16
20
  function containedAsset(value, label) {
17
- if (typeof value !== 'string' || value.length === 0 || value.includes('\\') || path_1.default.isAbsolute(value) || value.split('/').some(part => !part || part === '.' || part === '..'))
21
+ if (typeof value !== 'string' || value.length === 0 || value.includes('\\') || path_1.default.isAbsolute(value) || /^(?:[A-Za-z]:|[\\/])/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..'))
18
22
  throw new Error(`${label} must be a contained relative asset path`);
19
23
  return value;
20
24
  }
@@ -31,45 +35,266 @@ function root(value, label) {
31
35
  }
32
36
  return resolved;
33
37
  }
34
- function atomicWrite(destination, content) {
35
- const directory = path_1.default.dirname(destination);
36
- fs_1.default.mkdirSync(directory, { recursive: true });
37
- const temporary = path_1.default.join(directory, `.${path_1.default.basename(destination)}.${process.pid}.${Date.now()}.tmp`);
38
+ /** Resolve a monorepo package only when it is a real directory below the project.
39
+ * A lexical `foo/bar` is not enough: a symlink must never turn an asset write
40
+ * into an authority outside the project transaction. */
41
+ function containedPackageRoot(projectRoot, packageRoot) {
42
+ const candidate = path_1.default.join(projectRoot, packageRoot);
43
+ let stat;
38
44
  try {
39
- fs_1.default.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
40
- fs_1.default.renameSync(temporary, destination);
45
+ stat = fs_1.default.lstatSync(candidate);
41
46
  }
42
- finally {
47
+ catch {
48
+ throw new Error('packageRoot must be an existing contained directory');
49
+ }
50
+ if (!stat.isDirectory() || stat.isSymbolicLink())
51
+ throw new Error('packageRoot must be a non-symbolic contained directory');
52
+ const realProject = fs_1.default.realpathSync(projectRoot);
53
+ const realPackage = fs_1.default.realpathSync(candidate);
54
+ const relative = path_1.default.relative(realProject, realPackage);
55
+ if (relative === '' || relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
56
+ throw new Error('packageRoot must be a contained directory');
57
+ }
58
+ return realPackage;
59
+ }
60
+ function registryPackRoot(value) {
61
+ const resolved = root(value, 'packRoot');
62
+ const parsed = path_1.default.parse(resolved);
63
+ let current = parsed.root;
64
+ for (const component of path_1.default.relative(parsed.root, resolved).split(path_1.default.sep).filter(Boolean)) {
65
+ let stat;
43
66
  try {
44
- if (fs_1.default.existsSync(temporary))
45
- fs_1.default.unlinkSync(temporary);
67
+ stat = fs_1.default.lstatSync(current);
68
+ }
69
+ catch {
70
+ throw new Error('packRoot must be an existing directory');
46
71
  }
47
- catch { /* best effort cleanup */ }
72
+ if (stat.isSymbolicLink())
73
+ throw new Error('packRoot contains a symlink');
74
+ current = path_1.default.join(current, component);
75
+ }
76
+ let stat;
77
+ try {
78
+ stat = fs_1.default.lstatSync(current);
79
+ }
80
+ catch {
81
+ throw new Error('packRoot must be an existing directory');
82
+ }
83
+ if (stat.isSymbolicLink())
84
+ throw new Error('packRoot contains a symlink');
85
+ return resolved;
86
+ }
87
+ function atomicWrite(configRoot, asset, content, noClobber = false) {
88
+ const destination = path_1.default.join(configRoot, ...asset.split('/'));
89
+ if (noClobber) {
90
+ (0, safe_file_1.writeProjectFile)(configRoot, asset, Buffer.from(content, 'utf8'), { mode: 'create', createParents: true });
91
+ return;
92
+ }
93
+ let expected;
94
+ try {
95
+ expected = (0, safe_file_1.readInspectedBoundedFileWithIdentity)(destination, fs_1.default.lstatSync(destination, { bigint: true }), MAX_ASSET_BYTES, () => new Error(`project destination cannot be safely read: ${asset}`));
96
+ }
97
+ catch (error) {
98
+ if (error.code !== 'ENOENT')
99
+ throw error;
100
+ }
101
+ (0, safe_file_1.writeProjectFile)(configRoot, asset, Buffer.from(content, 'utf8'), expected
102
+ ? { mode: 'replace', expected: expected.content, expectedIdentity: expected.identity, createParents: false }
103
+ : { mode: 'create', createParents: true });
104
+ }
105
+ /** Publish a new asset without ever replacing an owner-created destination. */
106
+ function atomicCreateAsset(configRoot, asset, content) {
107
+ try {
108
+ (0, safe_file_1.writeProjectFile)(configRoot, asset, Buffer.from(content, 'utf8'), { mode: 'create', createParents: true });
109
+ }
110
+ catch (error) {
111
+ if (error.message === `project destination rejected by secure-fs: ${native_bridge_1.PROJECT_DESTINATION_ALREADY_EXISTS_MESSAGE}`)
112
+ return false;
113
+ throw error;
114
+ }
115
+ return true;
116
+ }
117
+ function selectedRegistryAsset(packRoot, asset) {
118
+ let current = packRoot;
119
+ for (const [index, component] of asset.split('/').entries()) {
120
+ let stat;
121
+ try {
122
+ stat = fs_1.default.lstatSync(current, { bigint: true });
123
+ }
124
+ catch {
125
+ throw new Error(`selected asset is missing from pack: ${asset}`);
126
+ }
127
+ if (stat.isSymbolicLink())
128
+ throw new Error(`selected asset contains a symlink: ${asset}`);
129
+ if (index < asset.split('/').length - 1 && !stat.isDirectory())
130
+ throw new Error(`selected asset is missing from pack: ${asset}`);
131
+ current = path_1.default.join(current, component);
132
+ }
133
+ let stat;
134
+ try {
135
+ stat = fs_1.default.lstatSync(current, { bigint: true });
136
+ }
137
+ catch {
138
+ throw new Error(`selected asset is missing from pack: ${asset}`);
139
+ }
140
+ if (!stat.isFile() || stat.isSymbolicLink())
141
+ throw new Error(`selected asset must be a regular file: ${asset}`);
142
+ if (stat.size > BigInt(MAX_ASSET_BYTES))
143
+ throw new Error(`selected asset exceeds the 1 MiB limit: ${asset}`);
144
+ return { file: current, stat };
145
+ }
146
+ function readSelectedRegistryAsset(packRoot, selected, asset) {
147
+ // Re-walk the bounded parent chain immediately before the leaf-safe-open.
148
+ // A replaced ancestor can otherwise redirect the same pathname outside the
149
+ // inspected registry between selection and the O_NOFOLLOW leaf open.
150
+ const current = selectedRegistryAsset(packRoot, asset);
151
+ if (current.stat.dev !== selected.stat.dev || current.stat.ino !== selected.stat.ino) {
152
+ throw new Error(`selected asset changed identity during safe open: ${asset}`);
153
+ }
154
+ const failure = (reason) => {
155
+ if (reason === 'open')
156
+ return new Error(`selected asset cannot be safely opened: ${asset}`);
157
+ if (reason === 'regular')
158
+ return new Error(`selected asset must be a regular file: ${asset}`);
159
+ if (reason === 'identity')
160
+ return new Error(`selected asset changed identity during safe open: ${asset}`);
161
+ if (reason === 'size')
162
+ return new Error(`selected asset changed size during safe open: ${asset}`);
163
+ return new Error(`selected asset exceeds the 1 MiB limit: ${asset}`);
164
+ };
165
+ return (0, safe_file_1.readInspectedBoundedFile)(selected.file, selected.stat, MAX_ASSET_BYTES, failure).toString('utf8');
166
+ }
167
+ function portableSource(source, pack) {
168
+ if (!source || typeof source !== 'object')
169
+ throw new Error('portable source is required');
170
+ const candidate = source;
171
+ if (!candidate.registry || typeof candidate.registry !== 'object' || typeof candidate.registry.name !== 'string' || typeof candidate.registry.contentRoot !== 'string' || typeof candidate.path !== 'string') {
172
+ throw new Error('portable source must be a validated pack source');
173
+ }
174
+ const registry = stableId(candidate.registry.name, 'source.registry.name');
175
+ const configuredRoot = root(candidate.registry.contentRoot, 'source.registry.contentRoot');
176
+ let contentRoot;
177
+ try {
178
+ contentRoot = fs_1.default.realpathSync(configuredRoot);
48
179
  }
180
+ catch {
181
+ throw new Error('source.registry.contentRoot must be canonicalizable');
182
+ }
183
+ const packRoot = registryPackRoot(path_1.default.join(contentRoot, 'sensor-packs', pack));
184
+ const expectedPack = path_1.default.join(packRoot, 'pack.json');
185
+ if (path_1.default.resolve(candidate.path) !== expectedPack)
186
+ throw new Error('portable source selected pack path does not match its registry');
187
+ const inspectedPack = selectedRegistryAsset(packRoot, 'pack.json');
188
+ if (readSelectedRegistryAsset(packRoot, inspectedPack, 'pack.json') !== candidate.content) {
189
+ throw new Error('portable source changed after bootstrap planning');
190
+ }
191
+ return { registry, packRoot };
49
192
  }
50
193
  function priorAssets(projectRoot) {
51
194
  const manifest = path_1.default.join(projectRoot, '.awm', 'sensors.json');
52
- if (!fs_1.default.existsSync(manifest))
53
- return [];
195
+ let stat;
196
+ try {
197
+ stat = fs_1.default.lstatSync(manifest, { bigint: true });
198
+ }
199
+ catch (error) {
200
+ if (error.code === 'ENOENT')
201
+ return [];
202
+ throw new Error(`cannot materialize over invalid sensor manifest ${manifest}`);
203
+ }
204
+ if (!stat.isFile() || stat.isSymbolicLink())
205
+ throw new Error(`cannot materialize over invalid sensor manifest ${manifest}`);
54
206
  let parsed;
55
207
  try {
56
- parsed = JSON.parse(fs_1.default.readFileSync(manifest, 'utf8'));
208
+ parsed = JSON.parse((0, safe_file_1.readInspectedBoundedFile)(manifest, stat, MAX_ASSET_BYTES, () => new Error(`cannot materialize over invalid sensor manifest ${manifest}`)).toString('utf8'));
57
209
  }
58
210
  catch {
59
211
  throw new Error(`cannot materialize over invalid sensor manifest ${manifest}`);
60
212
  }
61
213
  const parsedManifest = (0, manifest_1.parseSensorManifest)(parsed, manifest);
62
- if (parsedManifest.kind !== 'v2')
63
- return [];
64
- return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
214
+ if (parsedManifest.kind === 'v2')
215
+ return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
216
+ if (parsedManifest.kind === 'v3' && parsedManifest.pack.mode === 'project-sensors')
217
+ return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
218
+ return [];
219
+ }
220
+ /** Materialize a previously selected logical registry source into a portable v3 manifest. */
221
+ function materializePortableSensors(input) {
222
+ if (!input || typeof input !== 'object')
223
+ throw new Error('materialization input is required');
224
+ const projectRoot = root(input.projectRoot, 'projectRoot');
225
+ const pack = stableId(input.pack, 'pack');
226
+ const source = portableSource(input.source, pack);
227
+ const packRoot = source.packRoot;
228
+ if (!input.sensors || typeof input.sensors !== 'object' || Array.isArray(input.sensors))
229
+ throw new Error('sensors must be an object');
230
+ const candidate = {
231
+ schemaVersion: 3, mode: 'project-sensors', pack,
232
+ ...(input.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}),
233
+ source: { registry: source.registry },
234
+ ...(input.packageRoot ? { packageRoot: containedAsset(input.packageRoot, 'packageRoot') } : {}),
235
+ sensors: input.sensors,
236
+ };
237
+ const parsed = (0, manifest_1.parseSensorManifest)(candidate, 'portable materialized manifest');
238
+ if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
239
+ throw new Error('portable materialized manifest must be v3 project-sensors');
240
+ const manifest = parsed.pack;
241
+ for (const [name, sensor] of Object.entries(manifest.sensors)) {
242
+ if (sensor.policyRef)
243
+ (0, contract_1.resolveSemgrepPolicy)(sensor.policyRef, path_1.default.join(packRoot, 'pack.json'), `sensors.${name}`);
244
+ }
245
+ const selected = [...new Set(Object.values(manifest.sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
246
+ // Establish every registry authority and read every selected byte before the
247
+ // first project mutation. A bad later asset must not strand an earlier one.
248
+ const publications = input.configure === false ? [] : selected.map(asset => {
249
+ const selectedSource = selectedRegistryAsset(packRoot, asset);
250
+ return { asset, content: readSelectedRegistryAsset(packRoot, selectedSource, asset) };
251
+ });
252
+ return (0, safe_file_1.withProjectLease)(projectRoot, () => {
253
+ const configRoot = manifest.packageRoot ? containedPackageRoot(projectRoot, manifest.packageRoot) : projectRoot;
254
+ const prior = priorAssets(projectRoot);
255
+ const configured = [];
256
+ const preserved = [];
257
+ const created = [];
258
+ try {
259
+ if (input.configure !== false) {
260
+ for (const publication of publications) {
261
+ if (atomicCreateAsset(configRoot, publication.asset, publication.content)) {
262
+ const destination = manifest.packageRoot ? `${manifest.packageRoot}/${publication.asset}` : publication.asset;
263
+ const absolute = path_1.default.join(projectRoot, ...destination.split('/'));
264
+ const stat = fs_1.default.lstatSync(absolute, { bigint: true });
265
+ const observed = (0, safe_file_1.readInspectedBoundedFileWithIdentity)(absolute, stat, MAX_ASSET_BYTES, () => new Error(`created asset cannot be safely observed: ${publication.asset}`));
266
+ if (!observed.content.equals(Buffer.from(publication.content, 'utf8'))) {
267
+ throw new Error(`created asset changed before manifest publication: ${publication.asset}`);
268
+ }
269
+ created.push({ destination, identity: observed.identity });
270
+ configured.push(publication.asset);
271
+ }
272
+ else
273
+ preserved.push(publication.asset);
274
+ }
275
+ }
276
+ atomicWrite(projectRoot, '.awm/sensors.json', (0, manifest_1.serializeManifestV3)(manifest), true);
277
+ }
278
+ catch (error) {
279
+ for (const entry of created.reverse()) {
280
+ try {
281
+ (0, safe_file_1.removeObservedProjectFile)(projectRoot, entry.destination, entry.identity);
282
+ }
283
+ catch { /* Preserve a competing replacement; report the original publication failure. */ }
284
+ }
285
+ throw error;
286
+ }
287
+ return { manifest, configured, preserved, orphaned: prior.filter(asset => !selected.includes(asset)).sort() };
288
+ });
65
289
  }
66
290
  /** Write a selected, v2-only sensor configuration without overwriting user files.
67
- * The manifest is the commit point: created assets are removed best-effort if writing it fails. */
291
+ * Assets published before a manifest failure are deliberately left in place: a
292
+ * pathname rollback cannot be made safe after a concurrent project-parent swap. */
68
293
  function materializeResolvedSensors(input) {
69
294
  if (!input || typeof input !== 'object')
70
295
  throw new Error('materialization input is required');
71
296
  const projectRoot = root(input.projectRoot, 'projectRoot');
72
- const packRoot = root(input.packRoot, 'packRoot');
297
+ const packRoot = registryPackRoot(input.packRoot);
73
298
  const pack = stableId(input.pack, 'pack');
74
299
  if (!input.sensors || typeof input.sensors !== 'object' || Array.isArray(input.sensors))
75
300
  throw new Error('sensors must be an object');
@@ -86,7 +311,6 @@ function materializeResolvedSensors(input) {
86
311
  // Assets land where sensors will actually run — projectRoot by default, or
87
312
  // packageRoot underneath it for a monorepo. The manifest write below stays
88
313
  // pinned to projectRoot either way (see atomicWrite call at the end).
89
- const configRoot = manifest.packageRoot ? root(path_1.default.join(projectRoot, manifest.packageRoot), 'packageRoot') : projectRoot;
90
314
  // A policy reference is deliberately not a materialized asset. Resolve it here
91
315
  // from the registry-owned sibling only, so a manifest cannot turn arbitrary
92
316
  // registry content into a project write through a cosmetic policy field.
@@ -95,44 +319,22 @@ function materializeResolvedSensors(input) {
95
319
  (0, contract_1.resolveSemgrepPolicy)(sensor.policyRef, path_1.default.join(packRoot, 'pack.json'), `sensors.${name}`);
96
320
  }
97
321
  const selected = [...new Set(Object.values(sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
98
- const prior = priorAssets(projectRoot);
99
- const configured = [];
100
- const preserved = [];
101
- const created = [];
102
- try {
322
+ return (0, safe_file_1.withProjectLease)(projectRoot, () => {
323
+ const configRoot = manifest.packageRoot ? root(path_1.default.join(projectRoot, manifest.packageRoot), 'packageRoot') : projectRoot;
324
+ const prior = priorAssets(projectRoot);
325
+ const configured = [];
326
+ const preserved = [];
103
327
  if (input.configure !== false) {
104
328
  for (const asset of selected) {
105
- const source = path_1.default.join(packRoot, ...asset.split('/'));
106
- const destination = path_1.default.join(configRoot, ...asset.split('/'));
107
- let stat;
108
- try {
109
- stat = fs_1.default.lstatSync(source);
110
- }
111
- catch {
112
- throw new Error(`selected asset is missing from pack: ${asset}`);
329
+ const selectedSource = selectedRegistryAsset(packRoot, asset);
330
+ if (atomicCreateAsset(configRoot, asset, readSelectedRegistryAsset(packRoot, selectedSource, asset))) {
331
+ configured.push(asset);
113
332
  }
114
- if (!stat.isFile() || stat.isSymbolicLink())
115
- throw new Error(`selected asset must be a regular file: ${asset}`);
116
- if (fs_1.default.existsSync(destination)) {
333
+ else
117
334
  preserved.push(asset);
118
- continue;
119
- }
120
- fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
121
- atomicWrite(destination, fs_1.default.readFileSync(source, 'utf8'));
122
- created.push(destination);
123
- configured.push(asset);
124
335
  }
125
336
  }
126
- atomicWrite(path_1.default.join(projectRoot, '.awm', 'sensors.json'), (0, manifest_1.serializeManifestV2)(manifest));
127
- }
128
- catch (error) {
129
- for (const file of created.reverse()) {
130
- try {
131
- fs_1.default.unlinkSync(file);
132
- }
133
- catch { /* best effort transaction rollback */ }
134
- }
135
- throw error;
136
- }
137
- return { manifest, configured, preserved, orphaned: prior.filter(asset => !selected.includes(asset)).sort() };
337
+ atomicWrite(projectRoot, '.awm/sensors.json', (0, manifest_1.serializeManifestV2)(manifest));
338
+ return { manifest, configured, preserved, orphaned: prior.filter(asset => !selected.includes(asset)).sort() };
339
+ });
138
340
  }
@@ -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
  }