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.
- package/dist/src/commands/preflight/checks.js +2 -2
- 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 +29 -1
- package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
- package/dist/src/commands/sensors/compatibility/safe-file.js +240 -5
- package/dist/src/commands/sensors/coverage/index.js +2 -2
- package/dist/src/commands/sensors/coverage/resolve.js +20 -10
- 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 -170
- package/dist/src/commands/sensors/migrate.js +209 -0
- package/dist/src/commands/sensors/run.js +2 -2
- package/dist/src/core/init/steps.js +24 -11
- package/dist/src/core/secure-fs/native-bridge.js +209 -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 +67 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +34 -1
- package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +10 -0
- 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/core/init/orchestrator.test.js +3 -1
- package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +10 -2
- 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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.planV2Migration = planV2Migration;
|
|
7
|
+
exports.replaceV2ManifestWithV3 = replaceV2ManifestWithV3;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const contract_1 = require("./compatibility/contract");
|
|
11
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
12
|
+
const safe_file_1 = require("./compatibility/safe-file");
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function sameSensorField(v2, v3, pick) {
|
|
17
|
+
const names = Object.keys(v2.sensors);
|
|
18
|
+
return names.length === Object.keys(v3.sensors).length
|
|
19
|
+
&& names.every(name => name in v3.sensors && JSON.stringify(pick(v2.sensors[name])) === JSON.stringify(pick(v3.sensors[name])));
|
|
20
|
+
}
|
|
21
|
+
function compareV2Semantics(v2, v3, registry) {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
pack: v2.pack === v3.pack && v2.packSelection === v3.packSelection,
|
|
24
|
+
enabledDisabled: sameSensorField(v2, v3, sensor => sensor.enabled),
|
|
25
|
+
structuredCommands: sameSensorField(v2, v3, sensor => sensor.command),
|
|
26
|
+
assets: sameSensorField(v2, v3, sensor => sensor.assets),
|
|
27
|
+
timeouts: sameSensorField(v2, v3, sensor => sensor.timeout),
|
|
28
|
+
fast: sameSensorField(v2, v3, sensor => sensor.fast),
|
|
29
|
+
variants: sameSensorField(v2, v3, sensor => sensor.variantId),
|
|
30
|
+
policies: sameSensorField(v2, v3, sensor => sensor.policyRef),
|
|
31
|
+
sensorIdentityAndOrder: JSON.stringify(Object.keys(v2.sensors)) === JSON.stringify(Object.keys(v3.sensors)),
|
|
32
|
+
concurrency: v2.concurrency === v3.concurrency,
|
|
33
|
+
compatibilityEvidence: sameSensorField(v2, v3, sensor => sensor.initializedCompatibility),
|
|
34
|
+
packageRoot: v2.packageRoot === v3.packageRoot,
|
|
35
|
+
logicalSourceBinding: v3.source.registry === registry,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function allEquivalent(report) {
|
|
39
|
+
return Object.values(report).every(value => value === true);
|
|
40
|
+
}
|
|
41
|
+
function exactLogicalSource(input, pack) {
|
|
42
|
+
if (!isRecord(input))
|
|
43
|
+
throw new Error('v2 migration source is required');
|
|
44
|
+
if (input.kind === 'source-unavailable')
|
|
45
|
+
throw new Error('v2 migration source is unavailable');
|
|
46
|
+
if (input.kind === 'source-ambiguous')
|
|
47
|
+
throw new Error('v2 migration source is ambiguous');
|
|
48
|
+
if ((input.kind !== 'logical' && input.kind !== 'legacy-rebound') || !isRecord(input.source))
|
|
49
|
+
throw new Error('v2 migration source must be a unique logical resolution');
|
|
50
|
+
const source = input.source;
|
|
51
|
+
if (Object.keys(source).some(key => !['path', 'content', 'registry'].includes(key))
|
|
52
|
+
|| typeof source.path !== 'string' || typeof source.content !== 'string' || !isRecord(source.registry)) {
|
|
53
|
+
throw new Error('v2 migration source must contain an exact resolved pack');
|
|
54
|
+
}
|
|
55
|
+
const registry = source.registry;
|
|
56
|
+
if (Object.keys(registry).some(key => !['name', 'remote', 'contentRoot'].includes(key))
|
|
57
|
+
|| typeof registry.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(registry.name)
|
|
58
|
+
|| typeof registry.remote !== 'string' || registry.remote.trim() === '' || /[\0\r\n]/.test(registry.remote)
|
|
59
|
+
|| typeof registry.contentRoot !== 'string' || !path_1.default.isAbsolute(registry.contentRoot) || path_1.default.normalize(registry.contentRoot) !== registry.contentRoot) {
|
|
60
|
+
throw new Error('v2 migration source registry must be a stable logical registry');
|
|
61
|
+
}
|
|
62
|
+
// `resolveSensorSource` already selected and read this exact source. The
|
|
63
|
+
// migration planner consumes that immutable snapshot; re-canonicalizing its
|
|
64
|
+
// host directory would make a no-write plan depend on later filesystem state.
|
|
65
|
+
const expectedPath = path_1.default.join(registry.contentRoot, 'sensor-packs', pack, 'pack.json');
|
|
66
|
+
if (source.path !== expectedPath)
|
|
67
|
+
throw new Error('v2 migration source must contain an exact resolved pack');
|
|
68
|
+
let parsedSource;
|
|
69
|
+
try {
|
|
70
|
+
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), 'v2 migration resolved source');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new Error('v2 migration source exact v2 pack is invalid');
|
|
74
|
+
}
|
|
75
|
+
if (parsedSource.kind !== 'v2' || parsedSource.pack.name !== pack)
|
|
76
|
+
throw new Error('v2 migration source must contain the exact v2 pack');
|
|
77
|
+
return source;
|
|
78
|
+
}
|
|
79
|
+
function assertSourceCompatibleWithManifest(source, manifest) {
|
|
80
|
+
let parsedSource;
|
|
81
|
+
try {
|
|
82
|
+
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), 'v2 migration resolved source');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error('v2 migration source exact v2 pack is invalid');
|
|
86
|
+
}
|
|
87
|
+
if (parsedSource.kind !== 'v2' || parsedSource.pack.name !== manifest.pack) {
|
|
88
|
+
throw new Error('v2 migration source must contain the exact v2 pack');
|
|
89
|
+
}
|
|
90
|
+
for (const [name, sensor] of Object.entries(manifest.sensors)) {
|
|
91
|
+
const selected = parsedSource.pack.sensors[name];
|
|
92
|
+
if (!selected || !selected.variants.some(variant => variant.id === sensor.variantId)) {
|
|
93
|
+
throw new Error('v2 migration source is not compatible with selected manifest sensors');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function hasPhysicalSensorPath(value, registryRoots) {
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
const normalized = path_1.default.posix.normalize(value.replace(/\\/g, '/')).toLowerCase();
|
|
100
|
+
const withoutWebUrls = value.replace(/https?:\/\/[^\s"']+/gi, '');
|
|
101
|
+
const roots = registryRoots
|
|
102
|
+
.filter((root) => typeof root === 'string' && root.length > 0)
|
|
103
|
+
.map(root => path_1.default.posix.normalize(root.replace(/\\/g, '/')).toLowerCase());
|
|
104
|
+
const hasAbsolutePathAtTokenBoundary = /(?:^|[^A-Za-z0-9_.~\/\\-])["']?(?:\/|\\|[A-Za-z]:|\/\/)/.test(withoutWebUrls);
|
|
105
|
+
return roots.some(root => normalized.includes(root))
|
|
106
|
+
|| hasAbsolutePathAtTokenBoundary;
|
|
107
|
+
}
|
|
108
|
+
return Array.isArray(value) ? value.some(item => hasPhysicalSensorPath(item, registryRoots))
|
|
109
|
+
: isRecord(value) && Object.values(value).some(item => hasPhysicalSensorPath(item, registryRoots));
|
|
110
|
+
}
|
|
111
|
+
function inspectManifest(manifestPath, failure) {
|
|
112
|
+
let stat;
|
|
113
|
+
try {
|
|
114
|
+
stat = fs_1.default.lstatSync(manifestPath, { bigint: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw failure('open');
|
|
118
|
+
}
|
|
119
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
120
|
+
throw failure('regular');
|
|
121
|
+
return { stat, ...(0, safe_file_1.readInspectedBoundedFileWithIdentity)(manifestPath, stat, 1024 * 1024, failure) };
|
|
122
|
+
}
|
|
123
|
+
function assertManifestUnchanged(manifestPath, original) {
|
|
124
|
+
const changed = () => new Error('v2 migration original manifest changed before commit');
|
|
125
|
+
const current = inspectManifest(manifestPath, () => changed());
|
|
126
|
+
if (current.stat.dev !== original.stat.dev || current.stat.ino !== original.stat.ino
|
|
127
|
+
|| current.stat.size !== original.stat.size || !current.content.equals(original.content)) {
|
|
128
|
+
throw changed();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Build, but never persist, a portable v3 replacement for a validated v2 manifest. */
|
|
132
|
+
function planV2Migration(input) {
|
|
133
|
+
if (!input || typeof input !== 'object')
|
|
134
|
+
throw new Error('v2 migration input is required');
|
|
135
|
+
const parsed = (0, manifest_1.parseSensorManifest)(input.manifest, 'v2 migration manifest');
|
|
136
|
+
if (parsed.kind !== 'v2')
|
|
137
|
+
throw new Error('v2 migration requires a v2 project-sensors manifest');
|
|
138
|
+
const source = exactLogicalSource(input.source, parsed.pack.pack);
|
|
139
|
+
assertSourceCompatibleWithManifest(source, parsed.pack);
|
|
140
|
+
if (hasPhysicalSensorPath(parsed.pack.sensors, [parsed.pack.registryRoot, source.registry.contentRoot])
|
|
141
|
+
|| hasPhysicalSensorPath(parsed.pack.packageRoot, []))
|
|
142
|
+
throw new Error('v2 migration sensor semantics contain a physical path');
|
|
143
|
+
const candidate = {
|
|
144
|
+
schemaVersion: 3,
|
|
145
|
+
mode: 'project-sensors',
|
|
146
|
+
pack: parsed.pack.pack,
|
|
147
|
+
...(parsed.pack.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}),
|
|
148
|
+
source: { registry: source.registry.name },
|
|
149
|
+
...(parsed.pack.packageRoot ? { packageRoot: parsed.pack.packageRoot } : {}),
|
|
150
|
+
sensors: parsed.pack.sensors,
|
|
151
|
+
...(parsed.pack.concurrency ? { concurrency: parsed.pack.concurrency } : {}),
|
|
152
|
+
};
|
|
153
|
+
const validated = (0, manifest_1.parseSensorManifest)(candidate, 'v2 migration candidate');
|
|
154
|
+
if (validated.kind !== 'v3' || validated.pack.mode !== 'project-sensors') {
|
|
155
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
156
|
+
}
|
|
157
|
+
const equivalence = compareV2Semantics(parsed.pack, validated.pack, source.registry.name);
|
|
158
|
+
if (!allEquivalent(equivalence))
|
|
159
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
160
|
+
return { candidate: validated.pack, equivalent: true, equivalence };
|
|
161
|
+
}
|
|
162
|
+
/** Validate the old and new durable contracts, then atomically replace only the manifest. */
|
|
163
|
+
function replaceV2ManifestWithV3(manifestPath, candidate, source) {
|
|
164
|
+
if (typeof manifestPath !== 'string' || !path_1.default.isAbsolute(manifestPath) || path_1.default.normalize(manifestPath) !== manifestPath
|
|
165
|
+
|| manifestPath.includes('\0') || path_1.default.basename(manifestPath) !== 'sensors.json'
|
|
166
|
+
|| path_1.default.basename(path_1.default.dirname(manifestPath)) !== '.awm') {
|
|
167
|
+
throw new Error('v2 migration manifest path must be the canonical project manifest <project>/.awm/sensors.json');
|
|
168
|
+
}
|
|
169
|
+
const manifestDirectory = path_1.default.dirname(manifestPath);
|
|
170
|
+
const projectRoot = path_1.default.dirname(manifestDirectory);
|
|
171
|
+
const destination = '.awm/sensors.json';
|
|
172
|
+
(0, safe_file_1.withProjectLease)(projectRoot, () => {
|
|
173
|
+
const originalFailure = (_reason) => new Error('v2 migration original manifest must be readable JSON');
|
|
174
|
+
let inspected;
|
|
175
|
+
let original;
|
|
176
|
+
try {
|
|
177
|
+
inspected = inspectManifest(manifestPath, originalFailure);
|
|
178
|
+
original = JSON.parse(inspected.content.toString('utf8'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new Error('v2 migration original manifest must be readable JSON');
|
|
182
|
+
}
|
|
183
|
+
const before = (0, manifest_1.parseSensorManifest)(original, manifestPath);
|
|
184
|
+
const after = (0, manifest_1.parseSensorManifest)(candidate, 'v2 migration candidate');
|
|
185
|
+
if (before.kind !== 'v2' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
|
|
186
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
187
|
+
}
|
|
188
|
+
const logicalSource = exactLogicalSource(source, before.pack.pack);
|
|
189
|
+
assertSourceCompatibleWithManifest(logicalSource, before.pack);
|
|
190
|
+
if (after.pack.source.registry !== logicalSource.registry.name) {
|
|
191
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
192
|
+
}
|
|
193
|
+
if (hasPhysicalSensorPath(before.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
194
|
+
|| hasPhysicalSensorPath(after.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
195
|
+
|| hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(after.pack.packageRoot, [])) {
|
|
196
|
+
throw new Error('v2 migration candidate contains a physical path');
|
|
197
|
+
}
|
|
198
|
+
if (!allEquivalent(compareV2Semantics(before.pack, after.pack, logicalSource.registry.name))) {
|
|
199
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
200
|
+
}
|
|
201
|
+
// The native implementation reopens every ancestor without following links,
|
|
202
|
+
// verifies these exact bytes at the publication fence, and preserves a
|
|
203
|
+
// competing target instead of replacing it.
|
|
204
|
+
assertManifestUnchanged(manifestPath, inspected);
|
|
205
|
+
(0, safe_file_1.writeProjectFile)(projectRoot, destination, Buffer.from((0, manifest_1.serializeManifestV3)(after.pack), 'utf8'), {
|
|
206
|
+
mode: 'replace', expected: inspected.content, expectedIdentity: inspected.identity, createParents: false,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -16,7 +16,7 @@ Object.defineProperty(exports, "applyBaseline", { enumerable: true, get: functio
|
|
|
16
16
|
const changed_1 = require("./changed");
|
|
17
17
|
// Solo `detectStack` (puro, lee el arbol) — NO `initSensors`, que escribe. `run` es un
|
|
18
18
|
// verbo de lectura: no debe tener a mano ninguna funcion capaz de mutar el proyecto.
|
|
19
|
-
const
|
|
19
|
+
const detection_1 = require("./detection");
|
|
20
20
|
const live_1 = require("./compatibility/live");
|
|
21
21
|
const contract_1 = require("./compatibility/contract");
|
|
22
22
|
const source_1 = require("./compatibility/source");
|
|
@@ -67,7 +67,7 @@ function findManifestDir(startCwd) {
|
|
|
67
67
|
* and failed on by preflight's `pack` check.
|
|
68
68
|
*/
|
|
69
69
|
function detectPackDrift(manifestDir, manifest) {
|
|
70
|
-
const detection = (0,
|
|
70
|
+
const detection = (0, detection_1.detectStack)(manifestDir);
|
|
71
71
|
if (manifest.pack !== 'generic' || detection.pack === 'generic')
|
|
72
72
|
return { detection };
|
|
73
73
|
return {
|
|
@@ -23,7 +23,8 @@ exports.stepContextInjection = stepContextInjection;
|
|
|
23
23
|
const registries_1 = require("../registries");
|
|
24
24
|
const install_1 = require("../../commands/hooks/install");
|
|
25
25
|
const bundle_install_1 = require("../bundle-install");
|
|
26
|
-
const
|
|
26
|
+
const bootstrap_1 = require("../../commands/sensors/bootstrap");
|
|
27
|
+
const detection_1 = require("../../commands/sensors/detection");
|
|
27
28
|
const profile_1 = require("../profile");
|
|
28
29
|
const context_1 = require("../diagnostics/context");
|
|
29
30
|
const detector_1 = require("./detector");
|
|
@@ -66,15 +67,15 @@ exports.defaultActions = {
|
|
|
66
67
|
contentDir: o.contentDir,
|
|
67
68
|
}),
|
|
68
69
|
initSensors: async (o) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
70
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(o.cwd, { mode: 'project-sensors', registryRoot: o.registryRoot, configure: o.configure });
|
|
71
|
+
if (plan.kind === 'blocked')
|
|
72
|
+
throw new Error(`sensor bootstrap blocked: ${plan.reason}; ${plan.remedy}`);
|
|
73
|
+
if (plan.kind === 'migrate')
|
|
74
|
+
throw new Error('sensor init does not migrate an existing v2 manifest; run awm sensors bootstrap');
|
|
75
|
+
if (plan.kind === 'create')
|
|
76
|
+
(0, bootstrap_1.applySensorBootstrap)(plan);
|
|
77
|
+
const pack = plan.kind === 'create' && plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : (0, detection_1.detectStack)(o.cwd).pack;
|
|
78
|
+
return { detection: { pack }, manifest: { pack } };
|
|
78
79
|
},
|
|
79
80
|
addExtension: (root, name) => { (0, profile_1.addExtension)(root, name); },
|
|
80
81
|
ensureProfile: (root) => { (0, profile_1.ensureProfile)(root); },
|
|
@@ -325,7 +326,19 @@ async function stepSensors(d) {
|
|
|
325
326
|
return ok('project.sensors', 'project', 'skipped', 'no project');
|
|
326
327
|
if (proj.sensors.present)
|
|
327
328
|
return ok('project.sensors', 'project', 'skipped');
|
|
328
|
-
|
|
329
|
+
if (!d.sensorPacksRoot)
|
|
330
|
+
return ok('project.sensors', 'project', 'skipped', 'no configured sensor-pack registry');
|
|
331
|
+
let res;
|
|
332
|
+
try {
|
|
333
|
+
res = await d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
if (/^(?:source-unavailable|source-unsupported|registry-root-not-configured|sensor-variant-unresolvable):/.test(message)) {
|
|
338
|
+
return ok('project.sensors', 'project', 'skipped', message);
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
329
342
|
// A registry without the detected pack yields a fallback (or empty) manifest. The
|
|
330
343
|
// step still succeeded — a manifest was written — but reporting a bare 'applied'
|
|
331
344
|
// would let `awm init` hand back a quality gate that measures less than the
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.secureFs = exports.PROJECT_DESTINATION_ALREADY_EXISTS_MESSAGE = void 0;
|
|
7
|
+
exports.nativeArtifactStatus = nativeArtifactStatus;
|
|
8
|
+
exports.loadNativeSecureFsBridge = loadNativeSecureFsBridge;
|
|
9
|
+
exports.createSecureFsBoundary = createSecureFsBoundary;
|
|
10
|
+
exports.resolvePackageRoot = resolvePackageRoot;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const supportedPlatforms = new Set(['linux', 'darwin', 'win32']);
|
|
14
|
+
const supportedArchitectures = new Set(['x64', 'arm64']);
|
|
15
|
+
function artifactPath(root, platform, arch) {
|
|
16
|
+
if (!supportedPlatforms.has(platform) || !supportedArchitectures.has(arch)) {
|
|
17
|
+
throw new Error(`secure-fs native bridge does not support ${platform}-${arch}`);
|
|
18
|
+
}
|
|
19
|
+
return path_1.default.join(root, 'prebuilds', `${platform}-${arch}`, 'secure_fs.node');
|
|
20
|
+
}
|
|
21
|
+
function nativeArtifactStatus(options) {
|
|
22
|
+
const artifact = artifactPath(options.root, options.platform, options.arch);
|
|
23
|
+
return { status: fs_1.default.existsSync(artifact) ? 'available' : 'missing', platform: options.platform, arch: options.arch, path: artifact };
|
|
24
|
+
}
|
|
25
|
+
function loadNativeSecureFsBridge(options) {
|
|
26
|
+
const status = nativeArtifactStatus(options);
|
|
27
|
+
if (status.status !== 'available')
|
|
28
|
+
throw new Error(`secure-fs native artifact is unavailable for ${status.platform}-${status.arch}`);
|
|
29
|
+
let loaded;
|
|
30
|
+
try {
|
|
31
|
+
loaded = options.load(status.path);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`secure-fs native artifact is incompatible for ${status.platform}-${status.arch}`);
|
|
35
|
+
}
|
|
36
|
+
if (!loaded || typeof loaded !== 'object'
|
|
37
|
+
|| typeof loaded.acquireProjectLease !== 'function'
|
|
38
|
+
|| typeof loaded.releaseProjectLease !== 'function'
|
|
39
|
+
|| typeof loaded.readRegularFile !== 'function'
|
|
40
|
+
|| typeof loaded.writeProjectTransaction !== 'function'
|
|
41
|
+
|| typeof loaded.removeObservedProjectFile !== 'function') {
|
|
42
|
+
throw new Error(`secure-fs native artifact is incompatible for ${status.platform}-${status.arch}`);
|
|
43
|
+
}
|
|
44
|
+
return loaded;
|
|
45
|
+
}
|
|
46
|
+
const IDENTITY_TOKEN_BYTES = 24;
|
|
47
|
+
const NATIVE_DESTINATION_EXISTS_CODE = 'AWM_SECURE_FS_DESTINATION_EXISTS';
|
|
48
|
+
exports.PROJECT_DESTINATION_ALREADY_EXISTS_MESSAGE = 'project destination already exists';
|
|
49
|
+
const identityObservations = new WeakMap();
|
|
50
|
+
function absoluteFile(value, label) {
|
|
51
|
+
if (typeof value === 'string' && value.includes('\0'))
|
|
52
|
+
throw new Error(`${label} must not contain a NUL byte`);
|
|
53
|
+
if (typeof value !== 'string' || !path_1.default.isAbsolute(value) || path_1.default.normalize(value) !== value)
|
|
54
|
+
throw new Error(`${label} must be an absolute normalized path`);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function isNativeDestinationExistsError(error) {
|
|
58
|
+
return !!error && typeof error === 'object'
|
|
59
|
+
&& error.code === NATIVE_DESTINATION_EXISTS_CODE;
|
|
60
|
+
}
|
|
61
|
+
function boundedBytes(value, label) {
|
|
62
|
+
if (!Buffer.isBuffer(value))
|
|
63
|
+
throw new Error(`${label} must be a Buffer`);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function projectDestination(value) {
|
|
67
|
+
if (typeof value !== 'string' || !value || value.includes('\\')
|
|
68
|
+
|| path_1.default.posix.isAbsolute(value) || path_1.default.win32.isAbsolute(value) || /^[A-Za-z]:/.test(value)) {
|
|
69
|
+
throw new Error('destination must be a canonical project-relative path');
|
|
70
|
+
}
|
|
71
|
+
const components = value.split('/');
|
|
72
|
+
if (components.some(component => !component || component === '.' || component === '..' || component.includes(':') || component.includes('\0'))) {
|
|
73
|
+
throw new Error('destination must contain only safe project-relative components');
|
|
74
|
+
}
|
|
75
|
+
return components.join(path_1.default.sep);
|
|
76
|
+
}
|
|
77
|
+
function positiveSafeInteger(value, label) {
|
|
78
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
79
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function nativeIdentity(value) {
|
|
83
|
+
if (!Buffer.isBuffer(value) || value.length !== IDENTITY_TOKEN_BYTES
|
|
84
|
+
|| value.toString('ascii', 0, 4) !== 'SFSI' || value[4] !== 1
|
|
85
|
+
|| (value[5] !== 1 && value[5] !== 2) || value[6] !== 0 || value[7] !== 0) {
|
|
86
|
+
throw new Error('secure-fs native bridge returned invalid read identity');
|
|
87
|
+
}
|
|
88
|
+
const expectedKind = process.platform === 'win32' ? 2 : 1;
|
|
89
|
+
if (value[5] !== expectedKind)
|
|
90
|
+
throw new Error('secure-fs native bridge returned identity for the wrong platform');
|
|
91
|
+
return Buffer.from(value);
|
|
92
|
+
}
|
|
93
|
+
function secureReadResult(file, value) {
|
|
94
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
95
|
+
throw new Error('secure-fs native bridge returned invalid read result');
|
|
96
|
+
const candidate = value;
|
|
97
|
+
if (!Buffer.isBuffer(candidate.bytes))
|
|
98
|
+
throw new Error('secure-fs native bridge returned invalid read bytes');
|
|
99
|
+
const bytes = Buffer.from(candidate.bytes);
|
|
100
|
+
const token = Object.freeze({});
|
|
101
|
+
identityObservations.set(token, { file, bytes: Buffer.from(bytes), identity: nativeIdentity(candidate.identity) });
|
|
102
|
+
return Object.freeze({ bytes, identity: token });
|
|
103
|
+
}
|
|
104
|
+
function writeOptions(value) {
|
|
105
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
106
|
+
throw new Error('transaction options must be an object');
|
|
107
|
+
const candidate = value;
|
|
108
|
+
if (candidate.mode !== 'create' && candidate.mode !== 'replace')
|
|
109
|
+
throw new Error('transaction mode must be create or replace');
|
|
110
|
+
if (typeof candidate.createParents !== 'boolean')
|
|
111
|
+
throw new Error('transaction createParents must be boolean');
|
|
112
|
+
if (candidate.mode === 'create') {
|
|
113
|
+
if (candidate.expected !== undefined || candidate.expectedIdentity !== undefined)
|
|
114
|
+
throw new Error('create transaction cannot have replacement expectations');
|
|
115
|
+
return { mode: 'create', createParents: candidate.createParents };
|
|
116
|
+
}
|
|
117
|
+
if (!Buffer.isBuffer(candidate.expected))
|
|
118
|
+
throw new Error('replace transaction requires expected Buffer bytes');
|
|
119
|
+
if (!candidate.expectedIdentity || typeof candidate.expectedIdentity !== 'object')
|
|
120
|
+
throw new Error('replace transaction requires expected identity token');
|
|
121
|
+
const observation = identityObservations.get(candidate.expectedIdentity);
|
|
122
|
+
if (!observation)
|
|
123
|
+
throw new Error('replace transaction requires an opaque identity token from readRegularFile');
|
|
124
|
+
if (!candidate.expected.equals(observation.bytes))
|
|
125
|
+
throw new Error('replace expected bytes do not match the identity observation');
|
|
126
|
+
return {
|
|
127
|
+
mode: 'replace',
|
|
128
|
+
expected: Buffer.from(observation.bytes),
|
|
129
|
+
expectedIdentity: Buffer.from(observation.identity),
|
|
130
|
+
createParents: candidate.createParents,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function createSecureFsBoundary(load) {
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
withProjectLease(projectRoot, operation) {
|
|
136
|
+
const validatedRoot = absoluteFile(projectRoot, 'projectRoot');
|
|
137
|
+
if (typeof operation !== 'function')
|
|
138
|
+
throw new Error('project lease operation must be a function');
|
|
139
|
+
const binding = load();
|
|
140
|
+
let token;
|
|
141
|
+
try {
|
|
142
|
+
token = binding.acquireProjectLease(validatedRoot);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const message = String(error);
|
|
146
|
+
if (message.includes('project lease is already held'))
|
|
147
|
+
throw new Error('project lease conflict');
|
|
148
|
+
if (message.includes('rejected path ancestor'))
|
|
149
|
+
throw new Error('project destination is unsafe for lease');
|
|
150
|
+
throw new Error('project lease acquisition failed');
|
|
151
|
+
}
|
|
152
|
+
if (!token || typeof token !== 'object' || Array.isArray(token)) {
|
|
153
|
+
throw new Error('secure-fs native bridge returned invalid project lease token');
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
return operation();
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
binding.releaseProjectLease(token);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
readRegularFile(file, maxBytes) {
|
|
163
|
+
const validatedFile = absoluteFile(file, 'file');
|
|
164
|
+
const validatedMaxBytes = positiveSafeInteger(maxBytes, 'maxBytes');
|
|
165
|
+
const result = load().readRegularFile(validatedFile, validatedMaxBytes);
|
|
166
|
+
return secureReadResult(validatedFile, result);
|
|
167
|
+
},
|
|
168
|
+
writeProjectTransaction(projectRoot, destination, content, options) {
|
|
169
|
+
const validatedRoot = absoluteFile(projectRoot, 'projectRoot');
|
|
170
|
+
const validatedDestination = projectDestination(destination);
|
|
171
|
+
const validatedContent = boundedBytes(content, 'content');
|
|
172
|
+
const validatedOptions = writeOptions(options);
|
|
173
|
+
try {
|
|
174
|
+
load().writeProjectTransaction(validatedRoot, validatedDestination, validatedContent, validatedOptions);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (isNativeDestinationExistsError(error))
|
|
178
|
+
throw new Error(exports.PROJECT_DESTINATION_ALREADY_EXISTS_MESSAGE);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
removeObservedProjectFile(projectRoot, destination, identity) {
|
|
183
|
+
const validatedRoot = absoluteFile(projectRoot, 'projectRoot');
|
|
184
|
+
const validatedDestination = projectDestination(destination);
|
|
185
|
+
if (!identity || typeof identity !== 'object')
|
|
186
|
+
throw new Error('remove requires an opaque identity token from readRegularFile');
|
|
187
|
+
const observation = identityObservations.get(identity);
|
|
188
|
+
if (!observation)
|
|
189
|
+
throw new Error('remove requires an opaque identity token from readRegularFile');
|
|
190
|
+
const observedDestination = path_1.default.join(validatedRoot, validatedDestination);
|
|
191
|
+
if (observation.file !== observedDestination)
|
|
192
|
+
throw new Error('remove requires the exact observed destination');
|
|
193
|
+
load().removeObservedProjectFile(validatedRoot, validatedDestination, Buffer.from(observation.identity));
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function resolvePackageRoot(moduleDirectory) {
|
|
198
|
+
const sourceLayout = path_1.default.resolve(moduleDirectory, '..', '..', '..');
|
|
199
|
+
return fs_1.default.existsSync(path_1.default.join(sourceLayout, 'package.json'))
|
|
200
|
+
? sourceLayout
|
|
201
|
+
: path_1.default.resolve(sourceLayout, '..');
|
|
202
|
+
}
|
|
203
|
+
const packageRoot = resolvePackageRoot(__dirname);
|
|
204
|
+
exports.secureFs = createSecureFsBoundary(() => loadNativeSecureFsBridge({
|
|
205
|
+
root: packageRoot,
|
|
206
|
+
platform: process.platform,
|
|
207
|
+
arch: process.arch,
|
|
208
|
+
load: artifact => require(artifact),
|
|
209
|
+
}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const bootstrap_1 = require("../../../src/commands/sensors/bootstrap");
|
|
10
|
+
describe('planSensorBootstrap observational boundary', () => {
|
|
11
|
+
it('does not create project files while reporting a missing explicit mode', async () => {
|
|
12
|
+
const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-observe-'));
|
|
13
|
+
try {
|
|
14
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), '{"name":"fixture"}\n');
|
|
15
|
+
const before = fs_1.default.readdirSync(project).sort();
|
|
16
|
+
await expect((0, bootstrap_1.planSensorBootstrap)(project)).resolves.toMatchObject({ kind: 'blocked', reason: 'mode-required', changes: [] });
|
|
17
|
+
expect(fs_1.default.readdirSync(project).sort()).toEqual(before);
|
|
18
|
+
expect(fs_1.default.existsSync(path_1.default.join(project, '.awm'))).toBe(false);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
fs_1.default.rmSync(project, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|