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.
- package/dist/src/commands/preflight/checks.js +58 -27
- package/dist/src/commands/sensors/bootstrap.js +171 -0
- package/dist/src/commands/sensors/compatibility/contract.js +18 -15
- package/dist/src/commands/sensors/compatibility/manifest.js +75 -2
- package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
- package/dist/src/commands/sensors/compatibility/pack-source.js +61 -17
- package/dist/src/commands/sensors/compatibility/safe-file.js +286 -0
- package/dist/src/commands/sensors/compatibility/source.js +75 -0
- package/dist/src/commands/sensors/coverage/index.js +2 -2
- package/dist/src/commands/sensors/coverage/resolve.js +35 -29
- package/dist/src/commands/sensors/detection.js +31 -0
- package/dist/src/commands/sensors/exec.js +116 -11
- package/dist/src/commands/sensors/index.js +51 -10
- package/dist/src/commands/sensors/init.js +17 -168
- package/dist/src/commands/sensors/migrate.js +209 -0
- package/dist/src/commands/sensors/project.js +107 -0
- package/dist/src/commands/sensors/run.js +66 -47
- package/dist/src/commands/sensors/status.js +97 -56
- package/dist/src/core/init/steps.js +24 -11
- package/dist/src/core/secure-fs/native-bridge.js +209 -0
- package/dist/tests/commands/preflight/preflight.test.js +31 -0
- package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +90 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +249 -0
- package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
- package/dist/tests/commands/sensors/compatibility/source.test.js +121 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +36 -34
- package/dist/tests/commands/sensors/exec.test.js +86 -0
- package/dist/tests/commands/sensors/index.test.js +54 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
- package/dist/tests/commands/sensors/init.test.js +56 -490
- package/dist/tests/commands/sensors/migrate.test.js +513 -0
- package/dist/tests/commands/sensors/project.test.js +102 -0
- package/dist/tests/commands/sensors/run.test.js +107 -4
- package/dist/tests/commands/sensors/status.test.js +106 -2
- package/dist/tests/core/init/orchestrator.test.js +3 -1
- package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +5 -0
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +14 -23
- package/dist/tests/integration/sensor-coverage.e2e.test.js +15 -24
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
- package/dist/tests/structural/native-release-artifacts.test.js +22 -0
- package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
- package/dist/tests/structural/release-registry-provenance.test.js +4 -0
- package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
- package/package.json +6 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
40
|
-
fs_1.default.renameSync(temporary, destination);
|
|
45
|
+
stat = fs_1.default.lstatSync(candidate);
|
|
41
46
|
}
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
67
|
+
stat = fs_1.default.lstatSync(current);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error('packRoot must be an existing directory');
|
|
46
71
|
}
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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(
|
|
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
|
|
63
|
-
return [];
|
|
64
|
-
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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(
|
|
127
|
-
|
|
128
|
-
|
|
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(`
|
|
34
|
-
throw new Error(`
|
|
54
|
+
throw new Error(`registry "${registryName}" pack source has a non-regular or symbolic parent`);
|
|
55
|
+
throw new Error(`registry "${registryName}" pack source cannot be inspected`);
|
|
35
56
|
}
|
|
36
57
|
if (stat.isSymbolicLink())
|
|
37
|
-
throw new Error(`
|
|
58
|
+
throw new Error(`registry "${registryName}" pack source must not contain symbolic links`);
|
|
38
59
|
if (index < components.length - 1 && !stat.isDirectory())
|
|
39
|
-
throw new Error(`
|
|
60
|
+
throw new Error(`registry "${registryName}" pack source has a non-regular parent`);
|
|
40
61
|
if (index === components.length - 1)
|
|
41
62
|
return { candidate, stat };
|
|
42
63
|
}
|
|
43
64
|
throw new Error('pack source component walk did not reach pack.json');
|
|
44
65
|
}
|
|
66
|
+
function readContainedPack(real, inspected, registryName) {
|
|
67
|
+
const failure = (reason) => {
|
|
68
|
+
if (reason === 'open')
|
|
69
|
+
return new Error(`registry "${registryName}" pack source cannot be safely opened`);
|
|
70
|
+
if (reason === 'regular')
|
|
71
|
+
return new Error(`registry "${registryName}" pack source must be a contained regular file, never a symbolic link`);
|
|
72
|
+
if (reason === 'identity')
|
|
73
|
+
return new Error(`registry "${registryName}" pack source changed identity during safe open`);
|
|
74
|
+
if (reason === 'size')
|
|
75
|
+
return new Error(`registry "${registryName}" pack source changed size during safe open`);
|
|
76
|
+
return new Error(`registry "${registryName}" pack source exceeds the 1 MiB limit`);
|
|
77
|
+
};
|
|
78
|
+
return (0, safe_file_1.readInspectedBoundedFile)(real, inspected, MAX_PACK_BYTES, failure).toString('utf8');
|
|
79
|
+
}
|
|
45
80
|
/** Resolve only an exact, regular pack.json beneath the first configured registry.
|
|
46
81
|
* Registry order is authority; an unsafe claimed source is a hard failure, not a fallback. */
|
|
47
|
-
function
|
|
82
|
+
function listPackSources(pack, options = {}) {
|
|
48
83
|
if (!validPackName(pack))
|
|
49
84
|
throw new Error('pack name must be a stable lowercase id');
|
|
50
85
|
const registries = options.registries ?? (0, registries_1.listRegistries)();
|
|
51
86
|
if (!Array.isArray(registries))
|
|
52
87
|
throw new Error('registries must be an array');
|
|
88
|
+
const sources = [];
|
|
53
89
|
for (const registry of registries) {
|
|
54
|
-
if (!registry || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '')
|
|
55
|
-
throw new Error('registry has an invalid content root');
|
|
56
|
-
const
|
|
90
|
+
if (!registry || typeof registry.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(registry.name) || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '')
|
|
91
|
+
throw new Error('registry has an invalid identity or content root');
|
|
92
|
+
const registryName = diagnosticRegistryName(registry.name);
|
|
93
|
+
const inspected = inspectContainedPack(registry.contentRoot, pack, registryName);
|
|
57
94
|
if (!inspected)
|
|
58
95
|
continue;
|
|
59
96
|
const { candidate, stat } = inspected;
|
|
60
97
|
if (!stat.isFile())
|
|
61
|
-
throw new Error(`
|
|
62
|
-
if (stat.size > MAX_PACK_BYTES)
|
|
63
|
-
throw new Error(`
|
|
98
|
+
throw new Error(`registry "${registryName}" pack source must be a contained regular file, never a symbolic link`);
|
|
99
|
+
if (stat.size > BigInt(MAX_PACK_BYTES))
|
|
100
|
+
throw new Error(`registry "${registryName}" pack source exceeds the 1 MiB limit`);
|
|
64
101
|
let root;
|
|
65
102
|
let real;
|
|
66
103
|
try {
|
|
@@ -68,11 +105,18 @@ function resolvePackSource(pack, options = {}) {
|
|
|
68
105
|
real = fs_1.default.realpathSync(candidate);
|
|
69
106
|
}
|
|
70
107
|
catch {
|
|
71
|
-
throw new Error(`
|
|
108
|
+
throw new Error(`registry "${registryName}" pack source cannot be canonicalized`);
|
|
72
109
|
}
|
|
73
110
|
if (!contained(root, real))
|
|
74
|
-
throw new Error(`
|
|
75
|
-
|
|
111
|
+
throw new Error(`registry "${registryName}" pack source escapes its registry root`);
|
|
112
|
+
const content = readContainedPack(real, stat, registryName);
|
|
113
|
+
sources.push({ path: real, content, registry });
|
|
76
114
|
}
|
|
77
|
-
|
|
115
|
+
return sources;
|
|
116
|
+
}
|
|
117
|
+
function resolvePackSource(pack, options = {}) {
|
|
118
|
+
const source = listPackSources(pack, options)[0];
|
|
119
|
+
if (source)
|
|
120
|
+
return source;
|
|
121
|
+
throw new Error('sensor pack was not found in configured registries');
|
|
78
122
|
}
|