agentic-workflow-manager 9.5.0 → 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 (46) hide show
  1. package/dist/src/commands/preflight/checks.js +2 -2
  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 +29 -1
  5. package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
  6. package/dist/src/commands/sensors/compatibility/safe-file.js +240 -5
  7. package/dist/src/commands/sensors/coverage/index.js +2 -2
  8. package/dist/src/commands/sensors/coverage/resolve.js +20 -10
  9. package/dist/src/commands/sensors/detection.js +31 -0
  10. package/dist/src/commands/sensors/exec.js +116 -11
  11. package/dist/src/commands/sensors/index.js +51 -10
  12. package/dist/src/commands/sensors/init.js +17 -170
  13. package/dist/src/commands/sensors/migrate.js +209 -0
  14. package/dist/src/commands/sensors/run.js +2 -2
  15. package/dist/src/core/init/steps.js +24 -11
  16. package/dist/src/core/secure-fs/native-bridge.js +209 -0
  17. package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
  18. package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
  19. package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
  20. package/dist/tests/commands/sensors/compatibility/manifest.test.js +67 -0
  21. package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
  22. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +34 -1
  23. package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
  24. package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
  25. package/dist/tests/commands/sensors/coverage/resolve.test.js +10 -0
  26. package/dist/tests/commands/sensors/exec.test.js +86 -0
  27. package/dist/tests/commands/sensors/index.test.js +54 -0
  28. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
  29. package/dist/tests/commands/sensors/init.test.js +56 -490
  30. package/dist/tests/commands/sensors/migrate.test.js +513 -0
  31. package/dist/tests/core/init/orchestrator.test.js +3 -1
  32. package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
  33. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
  34. package/dist/tests/integration/sensor-compatibility.e2e.test.js +10 -2
  35. package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
  36. package/dist/tests/structural/native-release-artifacts.test.js +22 -0
  37. package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
  38. package/dist/tests/structural/release-registry-provenance.test.js +4 -0
  39. package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
  40. package/package.json +6 -2
  41. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  42. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  43. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  44. package/prebuilds/linux-x64/secure_fs.node +0 -0
  45. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  46. 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,16 +3,226 @@ 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.setSecureFsForTests = setSecureFsForTests;
7
+ exports.setPortableReadForTests = setPortableReadForTests;
8
+ exports.withProjectLease = withProjectLease;
9
+ exports.writeProjectFile = writeProjectFile;
10
+ exports.removeObservedProjectFile = removeObservedProjectFile;
11
+ exports.withNoFollowDirectory = withNoFollowDirectory;
12
+ exports.withDescriptorBoundWriteDirectory = withDescriptorBoundWriteDirectory;
13
+ exports.withNoFollowChildDirectory = withNoFollowChildDirectory;
6
14
  exports.readInspectedBoundedFile = readInspectedBoundedFile;
15
+ exports.readInspectedBoundedFileWithIdentity = readInspectedBoundedFileWithIdentity;
7
16
  const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const native_bridge_1 = require("../../../core/secure-fs/native-bridge");
19
+ let secureFsForTest;
20
+ let portableReadForTests = false;
21
+ /** Test-only seam for filesystem race fixtures. Production always uses the packaged bridge. */
22
+ function setSecureFsForTests(boundary) {
23
+ secureFsForTest = boundary;
24
+ }
25
+ /** Test-only opt-in for Node-level filesystem race fixtures. */
26
+ function setPortableReadForTests(enabled) {
27
+ portableReadForTests = enabled;
28
+ }
29
+ function activeSecureFs() {
30
+ return secureFsForTest ?? native_bridge_1.secureFs;
31
+ }
32
+ /** Hold the native project lease for one complete sensor mutation callback. */
33
+ function withProjectLease(projectRoot, operation) {
34
+ return activeSecureFs().withProjectLease(projectRoot, operation);
35
+ }
36
+ /** Native-only project publication. Production never falls back to pathname writes. */
37
+ function writeProjectFile(projectRoot, destination, content, options) {
38
+ try {
39
+ activeSecureFs().writeProjectTransaction(projectRoot, destination, content, options);
40
+ }
41
+ catch (error) {
42
+ throw new Error(`project destination rejected by secure-fs: ${error.message}`);
43
+ }
44
+ }
45
+ /** Compensate only a file that this transaction read and identity-fenced. */
46
+ function removeObservedProjectFile(projectRoot, destination, identity) {
47
+ try {
48
+ const remove = activeSecureFs().removeObservedProjectFile;
49
+ if (typeof remove !== 'function')
50
+ throw new Error('secure-fs native bridge does not support identity-fenced removal');
51
+ remove(projectRoot, destination, identity);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`project destination rejected by secure-fs: ${error.message}`);
55
+ }
56
+ }
57
+ function sameIdentity(left, right) {
58
+ return left.dev === right.dev && left.ino === right.ino;
59
+ }
60
+ function snapshotDirectoryChain(directory, failure) {
61
+ if (!path_1.default.isAbsolute(directory))
62
+ throw failure();
63
+ const resolved = path_1.default.resolve(directory);
64
+ const parsed = path_1.default.parse(resolved);
65
+ const snapshots = [];
66
+ let current = parsed.root;
67
+ for (const component of ['', ...path_1.default.relative(parsed.root, resolved).split(path_1.default.sep).filter(Boolean)]) {
68
+ if (component)
69
+ current = path_1.default.join(current, component);
70
+ let stat;
71
+ try {
72
+ stat = fs_1.default.lstatSync(current, { bigint: true });
73
+ }
74
+ catch {
75
+ throw failure();
76
+ }
77
+ if (!stat.isDirectory() || stat.isSymbolicLink())
78
+ throw failure();
79
+ snapshots.push({ name: current, stat });
80
+ }
81
+ return snapshots;
82
+ }
83
+ function assertDirectoryChainUnchanged(snapshots, failure) {
84
+ for (const snapshot of snapshots) {
85
+ let current;
86
+ try {
87
+ current = fs_1.default.lstatSync(snapshot.name, { bigint: true });
88
+ }
89
+ catch {
90
+ throw failure();
91
+ }
92
+ if (!current.isDirectory() || current.isSymbolicLink() || !sameIdentity(current, snapshot.stat))
93
+ throw failure();
94
+ }
95
+ }
96
+ function withPortableValidatedDirectory(directory, failure, operation) {
97
+ const snapshots = snapshotDirectoryChain(directory, failure);
98
+ let result;
99
+ let operationError;
100
+ try {
101
+ result = operation(directory);
102
+ }
103
+ catch (error) {
104
+ operationError = error;
105
+ }
106
+ assertDirectoryChainUnchanged(snapshots, failure);
107
+ if (operationError !== undefined)
108
+ throw operationError;
109
+ return result;
110
+ }
111
+ function procfsDescriptorBridgeAvailable() {
112
+ try {
113
+ return process.platform === 'linux' && fs_1.default.statSync('/proc/self/fd').isDirectory();
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ function withOpenedNoFollowDirectory(open, failure, operation) {
120
+ let descriptor;
121
+ try {
122
+ descriptor = open();
123
+ const opened = fs_1.default.fstatSync(descriptor, { bigint: true });
124
+ if (!opened.isDirectory())
125
+ throw failure();
126
+ return operation(`/proc/self/fd/${descriptor}`);
127
+ }
128
+ catch {
129
+ throw failure();
130
+ }
131
+ finally {
132
+ if (descriptor !== undefined) {
133
+ try {
134
+ fs_1.default.closeSync(descriptor);
135
+ }
136
+ catch { /* best effort descriptor cleanup */ }
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Bind a directory without following an observable symlink. Linux uses a
142
+ * descriptor path so ancestor renames cannot redirect work. Other platforms
143
+ * validate every component and its identity before and after the operation;
144
+ * Node exposes no portable openat equivalent, so any observable change fails
145
+ * closed rather than being accepted as a pathname race.
146
+ */
147
+ function withNoFollowDirectory(directory, failure, operation) {
148
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
149
+ const directoryFlag = fs_1.default.constants.O_DIRECTORY;
150
+ if (!path_1.default.isAbsolute(directory))
151
+ throw failure();
152
+ if (typeof noFollow !== 'number' || typeof directoryFlag !== 'number' || !procfsDescriptorBridgeAvailable()) {
153
+ return withPortableValidatedDirectory(directory, failure, operation);
154
+ }
155
+ let descriptor;
156
+ try {
157
+ const parsed = path_1.default.parse(directory);
158
+ descriptor = fs_1.default.openSync(parsed.root, fs_1.default.constants.O_RDONLY | noFollow | directoryFlag);
159
+ const relative = path_1.default.relative(parsed.root, path_1.default.resolve(directory));
160
+ for (const component of relative.split(path_1.default.sep).filter(Boolean)) {
161
+ const next = fs_1.default.openSync(`/proc/self/fd/${descriptor}/${component}`, fs_1.default.constants.O_RDONLY | noFollow | directoryFlag);
162
+ fs_1.default.closeSync(descriptor);
163
+ descriptor = next;
164
+ }
165
+ const finalDescriptor = descriptor;
166
+ descriptor = undefined;
167
+ return withOpenedNoFollowDirectory(() => finalDescriptor, failure, operation);
168
+ }
169
+ catch {
170
+ throw failure();
171
+ }
172
+ finally {
173
+ if (descriptor !== undefined) {
174
+ try {
175
+ fs_1.default.closeSync(descriptor);
176
+ }
177
+ catch { /* already closed during a failed handoff */ }
178
+ }
179
+ }
180
+ }
8
181
  /**
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.
182
+ * Run a filesystem mutation only through a descriptor-bound directory path.
183
+ * Identity snapshots are sufficient for bounded reads, but cannot make a
184
+ * pathname write safe: a parent can be replaced after the snapshot and before
185
+ * `mkdtemp`, staging, or publication. Platforms without the Linux descriptor
186
+ * bridge therefore reject before the mutation callback begins.
13
187
  */
188
+ function withDescriptorBoundWriteDirectory(directory, failure, operation) {
189
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
190
+ const directoryFlag = fs_1.default.constants.O_DIRECTORY;
191
+ if (typeof noFollow !== 'number' || typeof directoryFlag !== 'number' || !procfsDescriptorBridgeAvailable())
192
+ throw failure();
193
+ return withNoFollowDirectory(directory, failure, operation);
194
+ }
195
+ /** Open one direct child from a previously bound directory without path re-resolution. */
196
+ function withNoFollowChildDirectory(parentDirectory, component, failure, operation) {
197
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
198
+ const directoryFlag = fs_1.default.constants.O_DIRECTORY;
199
+ if (!component || component === '.' || component === '..' || /[\\/]/.test(component))
200
+ throw failure();
201
+ if (typeof noFollow !== 'number' || typeof directoryFlag !== 'number' || !procfsDescriptorBridgeAvailable()) {
202
+ return withPortableValidatedDirectory(path_1.default.join(parentDirectory, component), failure, operation);
203
+ }
204
+ return withOpenedNoFollowDirectory(() => fs_1.default.openSync(path_1.default.join(parentDirectory, component), fs_1.default.constants.O_RDONLY | noFollow | directoryFlag), failure, operation);
205
+ }
206
+ /** Read the exact regular file that was previously inspected. */
14
207
  function readInspectedBoundedFile(file, inspected, maxBytes, failure) {
208
+ // The JS descriptor bridge passes /proc/self/fd paths internally on Linux.
209
+ // They are already descriptor-bound; the native loader correctly rejects the
210
+ // procfs symlink representation, so retain this narrow internal path only.
211
+ const descriptorBoundInternalPath = process.platform === 'linux' && file.startsWith('/proc/self/fd/');
212
+ // The native boundary is mandatory in every shipped runtime. The
213
+ // filesystem race corpus opts into the portable path through a test-only
214
+ // seam; environment variables can never weaken production reads.
215
+ if (!secureFsForTest && !descriptorBoundInternalPath && !portableReadForTests) {
216
+ return readInspectedBoundedFileWithIdentity(file, inspected, maxBytes, failure).content;
217
+ }
218
+ // Test adapters observe the same boundary, then deliberately exercise the
219
+ // legacy race fixtures below. They are never selected by production code.
220
+ if (secureFsForTest)
221
+ secureFsForTest.readRegularFile(file, maxBytes);
15
222
  const noFollow = fs_1.default.constants.O_NOFOLLOW;
223
+ const directorySnapshots = typeof noFollow === 'number' && procfsDescriptorBridgeAvailable()
224
+ ? undefined
225
+ : snapshotDirectoryChain(path_1.default.dirname(file), () => failure('identity'));
16
226
  let descriptor;
17
227
  try {
18
228
  descriptor = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | (typeof noFollow === 'number' ? noFollow : 0));
@@ -30,7 +240,7 @@ function readInspectedBoundedFile(file, inspected, maxBytes, failure) {
30
240
  }
31
241
  if (!opened.isFile())
32
242
  throw failure('regular');
33
- if (opened.dev !== inspected.dev || opened.ino !== inspected.ino)
243
+ if (!sameIdentity(opened, inspected))
34
244
  throw failure('identity');
35
245
  if (opened.size !== inspected.size)
36
246
  throw failure('size');
@@ -43,9 +253,34 @@ function readInspectedBoundedFile(file, inspected, maxBytes, failure) {
43
253
  throw failure('limit');
44
254
  if (BigInt(count) !== opened.size)
45
255
  throw failure('size');
256
+ const after = fs_1.default.fstatSync(descriptor, { bigint: true });
257
+ if (!after.isFile() || !sameIdentity(after, opened))
258
+ throw failure('identity');
259
+ if (after.size !== opened.size)
260
+ throw failure('size');
261
+ if (directorySnapshots)
262
+ assertDirectoryChainUnchanged(directorySnapshots, () => failure('identity'));
46
263
  return buffer.subarray(0, count);
47
264
  }
48
265
  finally {
49
266
  fs_1.default.closeSync(descriptor);
50
267
  }
51
268
  }
269
+ /**
270
+ * Preserve the opaque identity minted by the same native handle that supplied
271
+ * the bytes. Replacement callers must pass this observation back unchanged.
272
+ */
273
+ function readInspectedBoundedFileWithIdentity(file, inspected, maxBytes, failure) {
274
+ if (process.platform === 'linux' && file.startsWith('/proc/self/fd/'))
275
+ throw failure('identity');
276
+ let observed;
277
+ try {
278
+ observed = activeSecureFs().readRegularFile(file, maxBytes);
279
+ }
280
+ catch {
281
+ throw failure('open');
282
+ }
283
+ if (BigInt(observed.bytes.length) !== inspected.size)
284
+ throw failure('size');
285
+ return Object.freeze({ content: observed.bytes, identity: observed.identity });
286
+ }
@@ -39,8 +39,8 @@ async function runCoverage(cwd, dependencies = {}, options = {}) {
39
39
  empirical: empirical(deps, input.projectRoot, {}, min, 'unavailable') };
40
40
  }
41
41
  let live;
42
- if (input.manifest.kind === 'v2') {
43
- live = (await deps.resolveLive(input.projectRoot, input.pack, input.manifest.pack.registryRoot)).sensors;
42
+ if (input.manifest.kind !== 'legacy') {
43
+ live = (await deps.resolveLive(input.projectRoot, input.pack, input.registryRoot)).sensors;
44
44
  }
45
45
  else {
46
46
  live = Object.fromEntries(Object.keys(input.manifest.pack.sensors).map((name) => [name, (0, manifest_1.legacyCompatibility)('legacy manifest without schemaVersion')]));