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
|
@@ -7,11 +7,12 @@ exports.computeSensorStatus = computeSensorStatus;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const paths_1 = require("../../core/paths");
|
|
10
|
-
const manifest_1 = require("./compatibility/manifest");
|
|
11
10
|
const contract_1 = require("./compatibility/contract");
|
|
12
11
|
const discovery_1 = require("./compatibility/discovery");
|
|
13
|
-
const pack_source_1 = require("./compatibility/pack-source");
|
|
14
12
|
const resolve_1 = require("./compatibility/resolve");
|
|
13
|
+
const project_1 = require("./project");
|
|
14
|
+
const source_1 = require("./compatibility/source");
|
|
15
|
+
const registries_1 = require("../../core/registries");
|
|
15
16
|
/** First non-flag token after `npx` — the tool the command actually runs. */
|
|
16
17
|
function npxTool(parts) {
|
|
17
18
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -94,10 +95,7 @@ function checkStructuredCommand(command, cwd, assets = []) {
|
|
|
94
95
|
* into a health check. A selected variant still proves that its current tool
|
|
95
96
|
* and runtime ranges are compatible; its probe state remains unverifiable.
|
|
96
97
|
*/
|
|
97
|
-
function resolveStaticV2Compatibility(cwd, manifest) {
|
|
98
|
-
const source = manifest.registryRoot === undefined
|
|
99
|
-
? (0, pack_source_1.resolvePackSource)(manifest.pack)
|
|
100
|
-
: (0, pack_source_1.resolvePackSource)(manifest.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: manifest.registryRoot }] });
|
|
98
|
+
function resolveStaticV2Compatibility(cwd, manifest, source) {
|
|
101
99
|
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
102
100
|
if (parsed.kind !== 'v2')
|
|
103
101
|
throw new Error(`sensor pack "${manifest.pack}" does not provide a v2 compatibility contract`);
|
|
@@ -135,57 +133,104 @@ function staticCompatibilityCheck(sensor, live) {
|
|
|
135
133
|
}
|
|
136
134
|
return null;
|
|
137
135
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
function resolvedSourceMetadata(source) {
|
|
137
|
+
return {
|
|
138
|
+
reason: source.kind === 'logical' ? 'configured-v3' : source.kind,
|
|
139
|
+
source: { kind: source.kind, registry: source.source.registry.name },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function projectMetadata(project, mode, reason, extra = {}) {
|
|
143
|
+
return {
|
|
144
|
+
mode, reason, projectRoot: project.projectRoot, manifestPath: project.manifestPath,
|
|
145
|
+
...(project.state === 'configured' ? { packageRoot: project.packageRoot } : {}),
|
|
146
|
+
...extra,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function sourceFailure(project, pack, source) {
|
|
150
|
+
if (source.kind === 'source-unavailable') {
|
|
151
|
+
return {
|
|
152
|
+
overall: 'DEGRADED', pack, checks: {},
|
|
153
|
+
...projectMetadata(project, 'source-unavailable', source.reason, { remedy: source.remedy }),
|
|
154
|
+
};
|
|
142
155
|
}
|
|
143
|
-
|
|
156
|
+
return {
|
|
157
|
+
overall: 'DEGRADED', pack, checks: {},
|
|
158
|
+
...projectMetadata(project, 'source-ambiguous', source.reason, { remedy: source.remedy, candidates: source.candidates }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function invalidStatus(project, pack, reason = 'manifest-malformed', remedy = 'repair-sensor-manifest') {
|
|
162
|
+
return {
|
|
163
|
+
overall: 'DEGRADED', pack, checks: {},
|
|
164
|
+
...projectMetadata(project, 'invalid', reason, { remedy }),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function computeSensorStatus(cwd = process.cwd()) {
|
|
168
|
+
let project;
|
|
144
169
|
try {
|
|
145
|
-
|
|
146
|
-
const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
|
|
147
|
-
if (parsed.kind === 'v2') {
|
|
148
|
-
// Monorepo support (mirrors run.ts/init.ts): detection and structured-command
|
|
149
|
-
// asset resolution both need to see the real package, not the (possibly
|
|
150
|
-
// unrelated) directory the manifest lives in.
|
|
151
|
-
const projectCwd = parsed.pack.packageRoot ? path_1.default.resolve(cwd, parsed.pack.packageRoot) : cwd;
|
|
152
|
-
const checks = {};
|
|
153
|
-
let compatibility;
|
|
154
|
-
try {
|
|
155
|
-
compatibility = resolveStaticV2Compatibility(projectCwd, parsed.pack);
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
const detail = error instanceof Error ? error.message : 'live compatibility unavailable';
|
|
159
|
-
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
160
|
-
checks[name] = sensor.enabled === false ? { ok: true, detail: 'disabled' } : { ok: false, detail };
|
|
161
|
-
}
|
|
162
|
-
return { overall: 'DEGRADED', pack: parsed.pack.pack, checks };
|
|
163
|
-
}
|
|
164
|
-
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
165
|
-
if (sensor.enabled === false) {
|
|
166
|
-
checks[name] = { ok: true, detail: 'disabled' };
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
checks[name] = staticCompatibilityCheck(sensor, compatibility[name])
|
|
170
|
-
?? checkStructuredCommand(sensor.command, projectCwd, sensor.assets);
|
|
171
|
-
}
|
|
172
|
-
return { overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED', pack: parsed.pack.pack, checks };
|
|
173
|
-
}
|
|
174
|
-
manifest = parsed.pack;
|
|
170
|
+
project = (0, project_1.resolveSensorProject)(cwd);
|
|
175
171
|
}
|
|
176
172
|
catch {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
const root = typeof cwd === 'string' && cwd.trim() !== '' ? path_1.default.resolve(cwd) : process.cwd();
|
|
174
|
+
return { overall: 'DEGRADED', pack: null, checks: {}, mode: 'invalid', reason: 'invalid-start-cwd', projectRoot: root, manifestPath: path_1.default.join(root, '.awm', 'sensors.json'), remedy: 'provide-an-existing-project-directory' };
|
|
175
|
+
}
|
|
176
|
+
if (project.state === 'missing') {
|
|
177
|
+
return { overall: 'NOT_CONFIGURED', pack: null, checks: {}, ...projectMetadata(project, 'missing', 'manifest-absent', { remedy: 'run-awm-sensors-bootstrap' }) };
|
|
178
|
+
}
|
|
179
|
+
if (project.state === 'invalid') {
|
|
180
|
+
let pack = null;
|
|
180
181
|
try {
|
|
181
|
-
const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
182
|
+
const raw = JSON.parse(fs_1.default.readFileSync(project.manifestPath, 'utf-8'));
|
|
182
183
|
if (raw && typeof raw === 'object' && !Array.isArray(raw) && !('schemaVersion' in raw) && typeof raw.pack === 'string') {
|
|
183
|
-
|
|
184
|
+
pack = raw.pack;
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
catch { /* malformed JSON is not configured */ }
|
|
187
|
-
|
|
188
|
+
const reason = project.reason.includes('unsupported manifest schemaVersion') ? 'schema-unsupported' : 'manifest-malformed';
|
|
189
|
+
return invalidStatus(project, pack, reason);
|
|
188
190
|
}
|
|
191
|
+
const packageRoot = project.packageRoot;
|
|
192
|
+
const parsed = project.manifest;
|
|
193
|
+
if (parsed.kind === 'v3' && parsed.pack.mode !== 'project-sensors') {
|
|
194
|
+
return {
|
|
195
|
+
overall: 'NOT_CONFIGURED', pack: null, checks: {},
|
|
196
|
+
...projectMetadata(project, parsed.pack.mode, `${parsed.pack.mode}-declared`, { declarationReason: parsed.pack.reason }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (parsed.kind === 'v2' || parsed.kind === 'v3') {
|
|
200
|
+
const parsedPack = parsed.pack;
|
|
201
|
+
let resolution;
|
|
202
|
+
try {
|
|
203
|
+
resolution = (0, source_1.resolveSensorSource)(parsed, { registries: (0, registries_1.listRegistries)() });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return invalidStatus(project, parsedPack.pack, 'sensor-source-invalid', 'repair-or-run-awm-update');
|
|
207
|
+
}
|
|
208
|
+
if (!('source' in resolution))
|
|
209
|
+
return sourceFailure(project, parsedPack.pack, resolution);
|
|
210
|
+
const sourceMeta = resolvedSourceMetadata(resolution);
|
|
211
|
+
const manifest = parsedPack;
|
|
212
|
+
const checks = {};
|
|
213
|
+
let compatibility;
|
|
214
|
+
try {
|
|
215
|
+
compatibility = resolveStaticV2Compatibility(packageRoot, manifest, resolution.source);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const detail = error instanceof Error ? error.message : 'live compatibility unavailable';
|
|
219
|
+
for (const [name, sensor] of Object.entries(manifest.sensors))
|
|
220
|
+
checks[name] = sensor.enabled === false ? { ok: true, detail: 'disabled' } : { ok: false, detail };
|
|
221
|
+
return { overall: 'DEGRADED', pack: manifest.pack, checks, ...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }) };
|
|
222
|
+
}
|
|
223
|
+
for (const [name, sensor] of Object.entries(manifest.sensors)) {
|
|
224
|
+
checks[name] = sensor.enabled === false ? { ok: true, detail: 'disabled' }
|
|
225
|
+
: staticCompatibilityCheck(sensor, compatibility[name]) ?? checkStructuredCommand(sensor.command, packageRoot, sensor.assets);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
229
|
+
pack: manifest.pack, checks,
|
|
230
|
+
...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const manifest = parsed.pack;
|
|
189
234
|
const checks = {};
|
|
190
235
|
for (const [name, config] of Object.entries(manifest.sensors ?? {})) {
|
|
191
236
|
if (config.enabled === false) {
|
|
@@ -196,14 +241,10 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
196
241
|
checks[name] = { ok: false, detail: 'no cmd configured' };
|
|
197
242
|
continue;
|
|
198
243
|
}
|
|
199
|
-
checks[name] = checkCmd(config.cmd,
|
|
200
|
-
}
|
|
201
|
-
// `Object.values({}).every(...)` is vacuously true — a manifest with zero sensor
|
|
202
|
-
// entries (the registry had no pack.json for this stack; see init.ts) must not read
|
|
203
|
-
// as READY just because there was nothing to fail. Same false-green `checkManifest`
|
|
204
|
-
// guards against in preflight.
|
|
205
|
-
if (Object.keys(manifest.sensors ?? {}).length === 0) {
|
|
206
|
-
return { overall: 'DEGRADED', pack: manifest.pack, checks };
|
|
244
|
+
checks[name] = checkCmd(config.cmd, packageRoot);
|
|
207
245
|
}
|
|
208
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
overall: Object.keys(manifest.sensors ?? {}).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
248
|
+
pack: manifest.pack, checks, ...projectMetadata(project, 'project-sensors', 'legacy-v1'),
|
|
249
|
+
};
|
|
209
250
|
}
|
|
@@ -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
|
+
}));
|
|
@@ -718,4 +718,35 @@ describe('preflight', () => {
|
|
|
718
718
|
// not the old hardcoded 9 — the shorter id's row must carry visible padding.
|
|
719
719
|
expect(lines[0]).toMatch(/host {12,}@@marker/);
|
|
720
720
|
});
|
|
721
|
+
it.each([
|
|
722
|
+
['native-gate', 'CI is the authority.', 'native_gate', 'native-gate-declared'],
|
|
723
|
+
['opt-out', 'No local gate is required.', 'ungated', 'opt-out-declared'],
|
|
724
|
+
])('exposes stable authority metadata for %s', async (mode, declarationReason, status, reason) => {
|
|
725
|
+
const dir = make({ manifest: { schemaVersion: 3, mode, reason: declarationReason } });
|
|
726
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
727
|
+
expect(report).toMatchObject({
|
|
728
|
+
status,
|
|
729
|
+
mode,
|
|
730
|
+
reason,
|
|
731
|
+
projectRoot: dir,
|
|
732
|
+
manifestPath: path_1.default.join(dir, '.awm', 'sensors.json'),
|
|
733
|
+
remedy: expect.any(String),
|
|
734
|
+
});
|
|
735
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
736
|
+
expect(report.status).not.toBe('ready');
|
|
737
|
+
});
|
|
738
|
+
it('derives manifest and tools checks from the same shared authority result', async () => {
|
|
739
|
+
const dir = make({ manifest: '{not valid json' });
|
|
740
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
741
|
+
expect(report).toMatchObject({
|
|
742
|
+
status: 'degraded',
|
|
743
|
+
mode: 'invalid',
|
|
744
|
+
reason: 'manifest-malformed',
|
|
745
|
+
projectRoot: dir,
|
|
746
|
+
manifestPath: path_1.default.join(dir, '.awm', 'sensors.json'),
|
|
747
|
+
remedy: expect.any(String),
|
|
748
|
+
});
|
|
749
|
+
expect(check(report, 'manifest').detail).not.toContain('no .awm/sensors.json');
|
|
750
|
+
expect(check(report, 'tools')).toBeUndefined();
|
|
751
|
+
});
|
|
721
752
|
});
|
|
@@ -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
|
+
});
|