agentic-workflow-manager 6.5.2 → 8.1.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/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +382 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +92 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +134 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +99 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +244 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +95 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +193 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
- package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +140 -2
- package/dist/tests/commands/sensors/exec.test.js +115 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +204 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -0,0 +1,92 @@
|
|
|
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.resolveLiveCompatibility = resolveLiveCompatibility;
|
|
7
|
+
exports.resolveParsedPackCompatibility = resolveParsedPackCompatibility;
|
|
8
|
+
const pack_source_1 = require("./pack-source");
|
|
9
|
+
const contract_1 = require("./contract");
|
|
10
|
+
const discovery_1 = require("./discovery");
|
|
11
|
+
const resolve_1 = require("./resolve");
|
|
12
|
+
const probe_1 = require("./probe");
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
/**
|
|
15
|
+
* Python package metadata is evidence only for a contained virtual environment.
|
|
16
|
+
* A v2 command that names a Python-runtime tool must therefore resolve through
|
|
17
|
+
* that same environment — never a same-named executable inherited from PATH.
|
|
18
|
+
*
|
|
19
|
+
* Keep this at the live pack boundary so the command used by probing, init's
|
|
20
|
+
* materialized manifest, and `sensors run` is one identical structured command.
|
|
21
|
+
*/
|
|
22
|
+
function containedRuntimeCommand(variant, pythonEnvironmentRoot) {
|
|
23
|
+
if (variant.requirements.runtime !== 'python')
|
|
24
|
+
return variant;
|
|
25
|
+
return {
|
|
26
|
+
...variant,
|
|
27
|
+
command: {
|
|
28
|
+
...variant.command,
|
|
29
|
+
resolution: 'python-environment',
|
|
30
|
+
...(pythonEnvironmentRoot ? { pythonEnvironmentRoot } : {}),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function bindContainedRuntimeCommands(pack, pythonEnvironmentRoot) {
|
|
35
|
+
return {
|
|
36
|
+
...pack,
|
|
37
|
+
sensors: Object.fromEntries(Object.entries(pack.sensors).map(([name, sensor]) => [
|
|
38
|
+
name,
|
|
39
|
+
{ ...sensor, variants: sensor.variants.map(variant => containedRuntimeCommand(variant, pythonEnvironmentRoot)) },
|
|
40
|
+
])),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Re-resolve a v2 pack from the configured registry and current project evidence.
|
|
45
|
+
* Manifest evidence is intentionally not an input: it is an init-time trace, not a
|
|
46
|
+
* live certification source. Probes are bounded and structured through the shared
|
|
47
|
+
* compatibility probe runner.
|
|
48
|
+
*/
|
|
49
|
+
async function resolveLiveCompatibility(cwd, packName, registryRoot, options = {}) {
|
|
50
|
+
if (typeof cwd !== 'string' || cwd.trim() === '')
|
|
51
|
+
throw new Error('cwd must be a non-empty path');
|
|
52
|
+
if (typeof packName !== 'string' || !/^[a-z][a-z0-9-]*$/.test(packName))
|
|
53
|
+
throw new Error('pack name must be a stable lowercase id');
|
|
54
|
+
if (registryRoot !== undefined && (typeof registryRoot !== 'string' || !path_1.default.isAbsolute(registryRoot) || path_1.default.normalize(registryRoot) !== registryRoot || /[\0\r\n]/.test(registryRoot))) {
|
|
55
|
+
throw new Error('registry root provenance must be an absolute normalized path');
|
|
56
|
+
}
|
|
57
|
+
const source = registryRoot === undefined
|
|
58
|
+
? (0, pack_source_1.resolvePackSource)(packName)
|
|
59
|
+
: (0, pack_source_1.resolvePackSource)(packName, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: registryRoot }] });
|
|
60
|
+
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
61
|
+
if (parsed.kind !== 'v2')
|
|
62
|
+
throw new Error(`sensor pack "${packName}" does not provide a v2 compatibility contract`);
|
|
63
|
+
return resolveParsedPackCompatibility(cwd, parsed.pack, options);
|
|
64
|
+
}
|
|
65
|
+
/** Resolve a pre-parsed v2 pack. Init uses this when given an explicit registry root. */
|
|
66
|
+
async function resolveParsedPackCompatibility(cwd, pack, options = {}) {
|
|
67
|
+
if (typeof cwd !== 'string' || cwd.trim() === '')
|
|
68
|
+
throw new Error('cwd must be a non-empty path');
|
|
69
|
+
if (!pack || typeof pack !== 'object' || pack.schemaVersion !== 2)
|
|
70
|
+
throw new Error('pack must be a parsed v2 sensor pack');
|
|
71
|
+
const evidence = (0, discovery_1.discoverProjectEvidence)(cwd, pack);
|
|
72
|
+
const executionPack = bindContainedRuntimeCommands(pack, evidence.pythonEnvironmentRoot);
|
|
73
|
+
const resolutionEvidence = { ...evidence, ...(options.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}) };
|
|
74
|
+
const initial = (0, resolve_1.resolveProjectCompatibility)(executionPack, resolutionEvidence).sensors;
|
|
75
|
+
const sensors = {};
|
|
76
|
+
for (const [name, sensor] of Object.entries(executionPack.sensors)) {
|
|
77
|
+
const base = initial[name];
|
|
78
|
+
const variant = base.variantId === null ? null : sensor.variants.find(candidate => candidate.id === base.variantId) ?? null;
|
|
79
|
+
const probe = variant
|
|
80
|
+
? await (0, probe_1.runCompatibilityProbe)(variant.probe, {
|
|
81
|
+
cwd,
|
|
82
|
+
toolExecutable: variant.command.executable,
|
|
83
|
+
toolResolution: variant.command.resolution,
|
|
84
|
+
pythonEnvironmentRoot: variant.command.pythonEnvironmentRoot,
|
|
85
|
+
configFiles: evidence.configFiles,
|
|
86
|
+
scripts: evidence.scripts,
|
|
87
|
+
})
|
|
88
|
+
: null;
|
|
89
|
+
sensors[name] = (0, resolve_1.resolveProjectCompatibility)({ ...executionPack, sensors: { [name]: sensor } }, { ...resolutionEvidence, probe: probe ?? undefined }).sensors[name];
|
|
90
|
+
}
|
|
91
|
+
return { pack: executionPack, sensors };
|
|
92
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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.legacyCompatibility = legacyCompatibility;
|
|
7
|
+
exports.parseSensorManifest = parseSensorManifest;
|
|
8
|
+
exports.serializeManifestV2 = serializeManifestV2;
|
|
9
|
+
const contract_1 = require("./contract");
|
|
10
|
+
const semver_1 = __importDefault(require("semver"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function sourceSuffix(source) {
|
|
16
|
+
return typeof source === 'string' && source.length > 0 && !/[\0\r\n]/.test(source) ? ` in ${source}` : ' in <unknown source>';
|
|
17
|
+
}
|
|
18
|
+
function invalid(source, message) {
|
|
19
|
+
throw new Error(`Invalid sensor manifest${sourceSuffix(source)}: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
function record(value, source, location) {
|
|
22
|
+
if (!isRecord(value))
|
|
23
|
+
invalid(source, `${location} must be an object`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function fields(value, allowed, source, location) {
|
|
27
|
+
for (const key of Object.keys(value)) {
|
|
28
|
+
if (!allowed.includes(key))
|
|
29
|
+
invalid(source, `${location} has unknown field "${key}"`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function text(value, source, location) {
|
|
33
|
+
if (typeof value !== 'string' || value.trim().length === 0 || /[\0\r\n]/.test(value)) {
|
|
34
|
+
invalid(source, `${location} must be a nonempty single-line string without NUL`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function id(value, source, location) {
|
|
39
|
+
const parsed = text(value, source, location);
|
|
40
|
+
if (!/^[a-z][a-z0-9-]*$/.test(parsed))
|
|
41
|
+
invalid(source, `${location} must be a stable lowercase id`);
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
function stringArray(value, source, location, allowEmpty) {
|
|
45
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
|
|
46
|
+
invalid(source, `${location} must be ${allowEmpty ? 'an array' : 'a nonempty array'}`);
|
|
47
|
+
}
|
|
48
|
+
return value.map((item, index) => text(item, source, `${location}[${index}]`));
|
|
49
|
+
}
|
|
50
|
+
function asset(value, source, location) {
|
|
51
|
+
const parsed = text(value, source, location);
|
|
52
|
+
if (parsed.startsWith('/') || /^[A-Za-z]:[\\/]/.test(parsed) || parsed.startsWith('\\\\') || parsed.includes('\\') || parsed.split('/').some(part => part === '' || part === '.' || part === '..'))
|
|
53
|
+
invalid(source, `${location} must be a contained relative asset path`);
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
const STATES = new Set(['certified', 'compatible-unverified', 'incompatible', 'missing-tool', 'unverifiable', 'not-applicable']);
|
|
57
|
+
function nullableText(value, source, location) {
|
|
58
|
+
return value === null ? null : text(value, source, location);
|
|
59
|
+
}
|
|
60
|
+
function parseCompatibilityEvidence(input, source, location) {
|
|
61
|
+
const value = record(input, source, location);
|
|
62
|
+
fields(value, ['state', 'reason', 'variantId', 'toolVersion', 'runtimeVersion', 'certifiedRange', 'evidence'], source, location);
|
|
63
|
+
if (typeof value.state !== 'string' || !STATES.has(value.state))
|
|
64
|
+
invalid(source, `${location}.state must be a supported state`);
|
|
65
|
+
if (!Array.isArray(value.evidence))
|
|
66
|
+
invalid(source, `${location}.evidence must be an array`);
|
|
67
|
+
const evidence = { state: value.state, reason: text(value.reason, source, `${location}.reason`), variantId: nullableText(value.variantId, source, `${location}.variantId`), toolVersion: nullableText(value.toolVersion, source, `${location}.toolVersion`), runtimeVersion: nullableText(value.runtimeVersion, source, `${location}.runtimeVersion`), certifiedRange: nullableText(value.certifiedRange, source, `${location}.certifiedRange`), evidence: value.evidence.map((entry, index) => { const item = record(entry, source, `${location}.evidence[${index}]`); fields(item, ['kind', 'status', 'path'], source, `${location}.evidence[${index}]`); return { kind: text(item.kind, source, `${location}.evidence[${index}].kind`), status: text(item.status, source, `${location}.evidence[${index}].status`), ...('path' in item ? { path: asset(item.path, source, `${location}.evidence[${index}].path`) } : {}) }; }) };
|
|
68
|
+
if (evidence.toolVersion !== null && semver_1.default.valid(evidence.toolVersion) === null)
|
|
69
|
+
invalid(source, `${location}.toolVersion must be a valid semver version`);
|
|
70
|
+
if (evidence.runtimeVersion !== null && semver_1.default.valid(evidence.runtimeVersion) === null)
|
|
71
|
+
invalid(source, `${location}.runtimeVersion must be a valid semver version`);
|
|
72
|
+
if (evidence.certifiedRange !== null && semver_1.default.validRange(evidence.certifiedRange) === null)
|
|
73
|
+
invalid(source, `${location}.certifiedRange must be a valid semver range`);
|
|
74
|
+
return evidence;
|
|
75
|
+
}
|
|
76
|
+
function legacyCompatibility(reason = 'legacy manifest without schemaVersion') {
|
|
77
|
+
if (typeof reason !== 'string' || reason.trim().length === 0 || /[\0\r\n]/.test(reason)) {
|
|
78
|
+
throw new Error('legacy compatibility reason must be a nonempty single-line string without NUL');
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
state: 'compatible-unverified',
|
|
82
|
+
reason,
|
|
83
|
+
variantId: null,
|
|
84
|
+
toolVersion: null,
|
|
85
|
+
runtimeVersion: null,
|
|
86
|
+
certifiedRange: null,
|
|
87
|
+
evidence: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseLegacySensor(input, source, location) {
|
|
91
|
+
if (typeof input === 'string')
|
|
92
|
+
return { cmd: text(input, source, `${location}.cmd`) };
|
|
93
|
+
const value = record(input, source, location);
|
|
94
|
+
fields(value, ['cmd', 'fast', 'enabled', 'timeout', 'changedCmd', 'changedExtensions', 'formatter'], source, location);
|
|
95
|
+
const sensor = {};
|
|
96
|
+
if ('cmd' in value)
|
|
97
|
+
sensor.cmd = text(value.cmd, source, `${location}.cmd`);
|
|
98
|
+
if ('fast' in value) {
|
|
99
|
+
if (typeof value.fast !== 'boolean')
|
|
100
|
+
invalid(source, `${location}.fast must be a boolean`);
|
|
101
|
+
sensor.fast = value.fast;
|
|
102
|
+
}
|
|
103
|
+
if ('enabled' in value) {
|
|
104
|
+
if (typeof value.enabled !== 'boolean')
|
|
105
|
+
invalid(source, `${location}.enabled must be a boolean`);
|
|
106
|
+
sensor.enabled = value.enabled;
|
|
107
|
+
}
|
|
108
|
+
if ('timeout' in value) {
|
|
109
|
+
if (typeof value.timeout !== 'number' || !Number.isSafeInteger(value.timeout) || value.timeout <= 0)
|
|
110
|
+
invalid(source, `${location}.timeout must be a positive safe integer`);
|
|
111
|
+
sensor.timeout = value.timeout;
|
|
112
|
+
}
|
|
113
|
+
if ('changedCmd' in value)
|
|
114
|
+
sensor.changedCmd = text(value.changedCmd, source, `${location}.changedCmd`);
|
|
115
|
+
if ('changedExtensions' in value)
|
|
116
|
+
sensor.changedExtensions = stringArray(value.changedExtensions, source, `${location}.changedExtensions`, true);
|
|
117
|
+
if ('formatter' in value)
|
|
118
|
+
sensor.formatter = text(value.formatter, source, `${location}.formatter`);
|
|
119
|
+
return sensor;
|
|
120
|
+
}
|
|
121
|
+
function parseLegacyManifest(value, source) {
|
|
122
|
+
fields(value, ['pack', 'sensors', 'concurrency'], source, 'root');
|
|
123
|
+
const pack = id(value.pack, source, 'pack');
|
|
124
|
+
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
125
|
+
const sensors = {};
|
|
126
|
+
for (const name of Object.keys(sensorsInput))
|
|
127
|
+
sensors[id(name, source, 'sensor id')] = parseLegacySensor(sensorsInput[name], source, `sensors.${name}`);
|
|
128
|
+
const manifest = { pack, sensors, compatibility: legacyCompatibility() };
|
|
129
|
+
if ('concurrency' in value) {
|
|
130
|
+
if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
|
|
131
|
+
invalid(source, 'concurrency must be a positive safe integer');
|
|
132
|
+
manifest.concurrency = value.concurrency;
|
|
133
|
+
}
|
|
134
|
+
return manifest;
|
|
135
|
+
}
|
|
136
|
+
function parseV2Sensor(input, source, location) {
|
|
137
|
+
const value = record(input, source, location);
|
|
138
|
+
fields(value, ['enabled', 'fast', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
|
|
139
|
+
if (typeof value.enabled !== 'boolean')
|
|
140
|
+
invalid(source, `${location}.enabled must be a boolean`);
|
|
141
|
+
const sensor = {
|
|
142
|
+
enabled: value.enabled, variantId: id(value.variantId, source, `${location}.variantId`),
|
|
143
|
+
command: (0, contract_1.parseStructuredCommand)(value.command, source),
|
|
144
|
+
initializedCompatibility: parseCompatibilityEvidence(value.initializedCompatibility, source, `${location}.initializedCompatibility`),
|
|
145
|
+
};
|
|
146
|
+
if (sensor.initializedCompatibility.variantId !== null && sensor.initializedCompatibility.variantId !== sensor.variantId)
|
|
147
|
+
invalid(source, `${location}.initializedCompatibility.variantId must match variantId`);
|
|
148
|
+
if (['certified', 'compatible-unverified', 'incompatible'].includes(sensor.initializedCompatibility.state) && sensor.initializedCompatibility.variantId === null)
|
|
149
|
+
invalid(source, `${location}.initializedCompatibility.variantId is required for ${sensor.initializedCompatibility.state}`);
|
|
150
|
+
if (sensor.initializedCompatibility.state === 'certified' && (sensor.initializedCompatibility.toolVersion === null || sensor.initializedCompatibility.runtimeVersion === null || sensor.initializedCompatibility.certifiedRange === null))
|
|
151
|
+
invalid(source, `${location}.initializedCompatibility certified evidence is incomplete`);
|
|
152
|
+
if (sensor.initializedCompatibility.state === 'certified' && !semver_1.default.satisfies(sensor.initializedCompatibility.toolVersion, sensor.initializedCompatibility.certifiedRange))
|
|
153
|
+
invalid(source, `${location}.initializedCompatibility.toolVersion must satisfy certifiedRange`);
|
|
154
|
+
if ('assets' in value)
|
|
155
|
+
sensor.assets = stringArray(value.assets, source, `${location}.assets`, true).map((entry, index) => asset(entry, source, `${location}.assets[${index}]`));
|
|
156
|
+
if ('policyRef' in value) {
|
|
157
|
+
if (value.policyRef !== 'shared/semgrep-policy.json')
|
|
158
|
+
invalid(source, `${location}.policyRef must be the contained AWM-owned shared/semgrep-policy.json`);
|
|
159
|
+
sensor.policyRef = value.policyRef;
|
|
160
|
+
}
|
|
161
|
+
if ('fast' in value) {
|
|
162
|
+
if (typeof value.fast !== 'boolean')
|
|
163
|
+
invalid(source, `${location}.fast must be a boolean`);
|
|
164
|
+
sensor.fast = value.fast;
|
|
165
|
+
}
|
|
166
|
+
return sensor;
|
|
167
|
+
}
|
|
168
|
+
function provenanceRoot(value, source) {
|
|
169
|
+
const parsed = text(value, source, 'registryRoot');
|
|
170
|
+
if (!path_1.default.isAbsolute(parsed) || path_1.default.normalize(parsed) !== parsed)
|
|
171
|
+
invalid(source, 'registryRoot must be an absolute normalized path');
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
174
|
+
function parseV2Manifest(value, source) {
|
|
175
|
+
fields(value, ['schemaVersion', 'pack', 'packSelection', 'registryRoot', 'sensors', 'concurrency'], source, 'root');
|
|
176
|
+
if (value.schemaVersion !== 2)
|
|
177
|
+
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2; upgrade or migrate the manifest`);
|
|
178
|
+
const pack = id(value.pack, source, 'pack');
|
|
179
|
+
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
180
|
+
const sensors = {};
|
|
181
|
+
for (const name of Object.keys(sensorsInput))
|
|
182
|
+
sensors[id(name, source, 'sensor id')] = parseV2Sensor(sensorsInput[name], source, `sensors.${name}`);
|
|
183
|
+
const manifest = { schemaVersion: 2, pack, sensors };
|
|
184
|
+
if ('packSelection' in value) {
|
|
185
|
+
if (value.packSelection !== 'explicit')
|
|
186
|
+
invalid(source, 'packSelection must be "explicit" when present');
|
|
187
|
+
manifest.packSelection = 'explicit';
|
|
188
|
+
}
|
|
189
|
+
if ('registryRoot' in value)
|
|
190
|
+
manifest.registryRoot = provenanceRoot(value.registryRoot, source);
|
|
191
|
+
if ('concurrency' in value) {
|
|
192
|
+
if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
|
|
193
|
+
invalid(source, 'concurrency must be a positive safe integer');
|
|
194
|
+
manifest.concurrency = value.concurrency;
|
|
195
|
+
}
|
|
196
|
+
return manifest;
|
|
197
|
+
}
|
|
198
|
+
function parseSensorManifest(input, source) {
|
|
199
|
+
const value = record(input, source, 'root');
|
|
200
|
+
return 'schemaVersion' in value ? { kind: 'v2', pack: parseV2Manifest(value, source) } : { kind: 'legacy', pack: parseLegacyManifest(value, source) };
|
|
201
|
+
}
|
|
202
|
+
function serializeManifestV2(input) {
|
|
203
|
+
const parsed = parseSensorManifest(input, 'manifest serialization');
|
|
204
|
+
if (parsed.kind !== 'v2')
|
|
205
|
+
throw new Error('Cannot serialize a legacy sensor manifest as v2');
|
|
206
|
+
return JSON.stringify(parsed.pack, null, 2);
|
|
207
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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.materializeResolvedSensors = materializeResolvedSensors;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const manifest_1 = require("./manifest");
|
|
10
|
+
const contract_1 = require("./contract");
|
|
11
|
+
function stableId(value, label) {
|
|
12
|
+
if (typeof value !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value))
|
|
13
|
+
throw new Error(`${label} must be a stable lowercase id`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
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 === '..'))
|
|
18
|
+
throw new Error(`${label} must be a contained relative asset path`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function root(value, label) {
|
|
22
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
23
|
+
throw new Error(`${label} must be a non-empty path`);
|
|
24
|
+
const resolved = path_1.default.resolve(value);
|
|
25
|
+
try {
|
|
26
|
+
if (!fs_1.default.statSync(resolved).isDirectory())
|
|
27
|
+
throw new Error();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`${label} must be an existing directory`);
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
function atomicWrite(destination, content) {
|
|
35
|
+
const directory = path_1.default.dirname(destination);
|
|
36
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
37
|
+
const temporary = path_1.default.join(directory, `.${path_1.default.basename(destination)}.${process.pid}.${Date.now()}.tmp`);
|
|
38
|
+
try {
|
|
39
|
+
fs_1.default.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
40
|
+
fs_1.default.renameSync(temporary, destination);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
try {
|
|
44
|
+
if (fs_1.default.existsSync(temporary))
|
|
45
|
+
fs_1.default.unlinkSync(temporary);
|
|
46
|
+
}
|
|
47
|
+
catch { /* best effort cleanup */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function priorAssets(projectRoot) {
|
|
51
|
+
const manifest = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
52
|
+
if (!fs_1.default.existsSync(manifest))
|
|
53
|
+
return [];
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(fs_1.default.readFileSync(manifest, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error(`cannot materialize over invalid sensor manifest ${manifest}`);
|
|
60
|
+
}
|
|
61
|
+
const parsedManifest = (0, manifest_1.parseSensorManifest)(parsed, manifest);
|
|
62
|
+
if (parsedManifest.kind !== 'v2')
|
|
63
|
+
return [];
|
|
64
|
+
return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
|
|
65
|
+
}
|
|
66
|
+
/** Write a selected, v2-only sensor configuration without overwriting user files.
|
|
67
|
+
* The manifest is the commit point: created assets are removed best-effort if writing it fails. */
|
|
68
|
+
function materializeResolvedSensors(input) {
|
|
69
|
+
if (!input || typeof input !== 'object')
|
|
70
|
+
throw new Error('materialization input is required');
|
|
71
|
+
const projectRoot = root(input.projectRoot, 'projectRoot');
|
|
72
|
+
const packRoot = root(input.packRoot, 'packRoot');
|
|
73
|
+
const pack = stableId(input.pack, 'pack');
|
|
74
|
+
if (!input.sensors || typeof input.sensors !== 'object' || Array.isArray(input.sensors))
|
|
75
|
+
throw new Error('sensors must be an object');
|
|
76
|
+
const sensors = {};
|
|
77
|
+
for (const [name, sensor] of Object.entries(input.sensors)) {
|
|
78
|
+
stableId(name, 'sensor id');
|
|
79
|
+
// Reuse the durable parser as the single schema boundary before persisting.
|
|
80
|
+
const parsed = (0, manifest_1.parseSensorManifest)({ schemaVersion: 2, pack, sensors: { [name]: sensor } }, 'materialized sensor');
|
|
81
|
+
if (parsed.kind !== 'v2')
|
|
82
|
+
throw new Error('materialized sensor must be v2');
|
|
83
|
+
sensors[name] = parsed.pack.sensors[name];
|
|
84
|
+
}
|
|
85
|
+
const manifest = { schemaVersion: 2, pack, sensors, ...(input.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}), ...(input.registryRoot ? { registryRoot: input.registryRoot } : {}) };
|
|
86
|
+
// A policy reference is deliberately not a materialized asset. Resolve it here
|
|
87
|
+
// from the registry-owned sibling only, so a manifest cannot turn arbitrary
|
|
88
|
+
// registry content into a project write through a cosmetic policy field.
|
|
89
|
+
for (const [name, sensor] of Object.entries(sensors)) {
|
|
90
|
+
if (sensor.policyRef)
|
|
91
|
+
(0, contract_1.resolveSemgrepPolicy)(sensor.policyRef, path_1.default.join(packRoot, 'pack.json'), `sensors.${name}`);
|
|
92
|
+
}
|
|
93
|
+
const selected = [...new Set(Object.values(sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
|
|
94
|
+
const prior = priorAssets(projectRoot);
|
|
95
|
+
const configured = [];
|
|
96
|
+
const preserved = [];
|
|
97
|
+
const created = [];
|
|
98
|
+
try {
|
|
99
|
+
if (input.configure !== false) {
|
|
100
|
+
for (const asset of selected) {
|
|
101
|
+
const source = path_1.default.join(packRoot, ...asset.split('/'));
|
|
102
|
+
const destination = path_1.default.join(projectRoot, ...asset.split('/'));
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = fs_1.default.lstatSync(source);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error(`selected asset is missing from pack: ${asset}`);
|
|
109
|
+
}
|
|
110
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
111
|
+
throw new Error(`selected asset must be a regular file: ${asset}`);
|
|
112
|
+
if (fs_1.default.existsSync(destination)) {
|
|
113
|
+
preserved.push(asset);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
117
|
+
atomicWrite(destination, fs_1.default.readFileSync(source, 'utf8'));
|
|
118
|
+
created.push(destination);
|
|
119
|
+
configured.push(asset);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
atomicWrite(path_1.default.join(projectRoot, '.awm', 'sensors.json'), (0, manifest_1.serializeManifestV2)(manifest));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
for (const file of created.reverse()) {
|
|
126
|
+
try {
|
|
127
|
+
fs_1.default.unlinkSync(file);
|
|
128
|
+
}
|
|
129
|
+
catch { /* best effort transaction rollback */ }
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
return { manifest, configured, preserved, orphaned: prior.filter(asset => !selected.includes(asset)).sort() };
|
|
134
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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.resolvePackSource = resolvePackSource;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const registries_1 = require("../../../core/registries");
|
|
10
|
+
const MAX_PACK_BYTES = 1024 * 1024;
|
|
11
|
+
function validPackName(pack) {
|
|
12
|
+
return typeof pack === 'string' && /^[a-z][a-z0-9-]*$/.test(pack);
|
|
13
|
+
}
|
|
14
|
+
function contained(root, candidate) {
|
|
15
|
+
const prefix = root.endsWith(path_1.default.sep) ? root : root + path_1.default.sep;
|
|
16
|
+
return candidate.startsWith(prefix);
|
|
17
|
+
}
|
|
18
|
+
/** Walk each component under contentRoot with lstat: realpath alone would hide an
|
|
19
|
+
* internal symlink whose target happens to remain inside the registry. */
|
|
20
|
+
function inspectContainedPack(root, pack) {
|
|
21
|
+
const components = ['sensor-packs', pack, 'pack.json'];
|
|
22
|
+
let candidate = root;
|
|
23
|
+
for (let index = 0; index < components.length; index++) {
|
|
24
|
+
candidate = path_1.default.join(candidate, components[index]);
|
|
25
|
+
let stat;
|
|
26
|
+
try {
|
|
27
|
+
stat = fs_1.default.lstatSync(candidate);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT')
|
|
31
|
+
return undefined;
|
|
32
|
+
if (error.code === 'ENOTDIR')
|
|
33
|
+
throw new Error(`pack source ${candidate} has a non-regular or symbolic parent`);
|
|
34
|
+
throw new Error(`cannot inspect pack source ${candidate}`);
|
|
35
|
+
}
|
|
36
|
+
if (stat.isSymbolicLink())
|
|
37
|
+
throw new Error(`pack source ${candidate} must not contain symbolic links`);
|
|
38
|
+
if (index < components.length - 1 && !stat.isDirectory())
|
|
39
|
+
throw new Error(`pack source ${candidate} has a non-regular parent`);
|
|
40
|
+
if (index === components.length - 1)
|
|
41
|
+
return { candidate, stat };
|
|
42
|
+
}
|
|
43
|
+
throw new Error('pack source component walk did not reach pack.json');
|
|
44
|
+
}
|
|
45
|
+
/** Resolve only an exact, regular pack.json beneath the first configured registry.
|
|
46
|
+
* Registry order is authority; an unsafe claimed source is a hard failure, not a fallback. */
|
|
47
|
+
function resolvePackSource(pack, options = {}) {
|
|
48
|
+
if (!validPackName(pack))
|
|
49
|
+
throw new Error('pack name must be a stable lowercase id');
|
|
50
|
+
const registries = options.registries ?? (0, registries_1.listRegistries)();
|
|
51
|
+
if (!Array.isArray(registries))
|
|
52
|
+
throw new Error('registries must be an array');
|
|
53
|
+
for (const registry of registries) {
|
|
54
|
+
if (!registry || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '')
|
|
55
|
+
throw new Error('registry has an invalid content root');
|
|
56
|
+
const inspected = inspectContainedPack(registry.contentRoot, pack);
|
|
57
|
+
if (!inspected)
|
|
58
|
+
continue;
|
|
59
|
+
const { candidate, stat } = inspected;
|
|
60
|
+
if (!stat.isFile())
|
|
61
|
+
throw new Error(`pack source ${candidate} must be a contained regular file, never a symbolic link`);
|
|
62
|
+
if (stat.size > MAX_PACK_BYTES)
|
|
63
|
+
throw new Error(`pack source ${candidate} exceeds the 1 MiB limit`);
|
|
64
|
+
let root;
|
|
65
|
+
let real;
|
|
66
|
+
try {
|
|
67
|
+
root = fs_1.default.realpathSync(registry.contentRoot);
|
|
68
|
+
real = fs_1.default.realpathSync(candidate);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`cannot canonicalize pack source ${candidate}`);
|
|
72
|
+
}
|
|
73
|
+
if (!contained(root, real))
|
|
74
|
+
throw new Error(`pack source ${candidate} escapes its registry root`);
|
|
75
|
+
return { path: real, content: fs_1.default.readFileSync(real, 'utf8'), registry };
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`sensor pack "${pack}" was not found in configured registries`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCompatibilityProbe = runCompatibilityProbe;
|
|
4
|
+
const exec_1 = require("../exec");
|
|
5
|
+
const KINDS = new Set(['version', 'eslint-print-config', 'typescript-show-config', 'semgrep-validate', 'package-script-present', 'config-present']);
|
|
6
|
+
function commandFor(kind, evidence) {
|
|
7
|
+
const executable = evidence.toolExecutable ?? (kind.startsWith('typescript') ? 'tsc' : kind.startsWith('eslint') ? 'eslint' : kind.startsWith('semgrep') ? 'semgrep' : 'node');
|
|
8
|
+
// Probe an executable through the same bounded resolver as its variant's
|
|
9
|
+
// eventual execution. Falling back preserves legacy probe-only kinds.
|
|
10
|
+
const resolution = executable === 'node'
|
|
11
|
+
? 'path'
|
|
12
|
+
: evidence.toolResolution ?? (kind === 'semgrep-validate'
|
|
13
|
+
? 'python-environment'
|
|
14
|
+
: 'node-modules-bin');
|
|
15
|
+
if (kind === 'package-script-present')
|
|
16
|
+
return null;
|
|
17
|
+
if (kind === 'config-present')
|
|
18
|
+
return null;
|
|
19
|
+
const command = { executable, resolution, ...(resolution === 'python-environment' && evidence.pythonEnvironmentRoot ? { pythonEnvironmentRoot: evidence.pythonEnvironmentRoot } : {}) };
|
|
20
|
+
if (kind === 'version')
|
|
21
|
+
return { ...command, args: ['--version'] };
|
|
22
|
+
if (kind === 'eslint-print-config')
|
|
23
|
+
return { ...command, args: ['--print-config', evidence.configFiles?.[0] ?? 'package.json'] };
|
|
24
|
+
if (kind === 'typescript-show-config')
|
|
25
|
+
return { ...command, args: ['--showConfig'] };
|
|
26
|
+
return { ...command, args: ['--validate'] };
|
|
27
|
+
}
|
|
28
|
+
/** Executes only the closed probe enum. Raw output is intentionally discarded. */
|
|
29
|
+
async function runCompatibilityProbe(probe, evidence, executor = exec_1.runStructuredCommand) {
|
|
30
|
+
if (!probe || typeof probe !== 'object' || !KINDS.has(probe.kind))
|
|
31
|
+
throw new Error('compatibility probe kind must be allowed');
|
|
32
|
+
if (!evidence || typeof evidence.cwd !== 'string' || evidence.cwd.trim() === '')
|
|
33
|
+
throw new Error('probe evidence requires cwd');
|
|
34
|
+
if (probe.kind === 'package-script-present')
|
|
35
|
+
return { status: (evidence.scripts?.length ?? 0) > 0 ? 'matched' : 'not-matched', reason: 'package-script' };
|
|
36
|
+
if (probe.kind === 'config-present')
|
|
37
|
+
return { status: (evidence.configFiles?.length ?? 0) > 0 ? 'matched' : 'not-matched', reason: 'config-file' };
|
|
38
|
+
try {
|
|
39
|
+
const result = await executor(commandFor(probe.kind, evidence), { cwd: evidence.cwd, timeout: 5_000, maxBuffer: 64 * 1024 });
|
|
40
|
+
if (result.timedOut || result.overflowed || result.spawnError)
|
|
41
|
+
return { status: 'unverifiable', reason: result.timedOut ? 'probe-timeout' : result.overflowed ? 'probe-output-limit' : 'probe-spawn-error' };
|
|
42
|
+
return result.code === 0 ? { status: 'matched', reason: 'probe-exit-zero' } : { status: 'not-matched', reason: 'probe-exit-nonzero' };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { status: 'unverifiable', reason: 'probe-exception' };
|
|
46
|
+
}
|
|
47
|
+
}
|