agentic-workflow-manager 9.16.1 → 9.17.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 +63 -13
- package/dist/src/commands/sensors/bootstrap.js +2 -1
- package/dist/src/commands/sensors/compatibility/contract.js +30 -26
- package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
- package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
- package/dist/src/commands/sensors/compatibility/source.js +3 -2
- package/dist/src/commands/sensors/coverage/render.js +8 -2
- package/dist/src/commands/sensors/coverage/resolve.js +7 -0
- package/dist/src/commands/sensors/init.js +15 -2
- package/dist/src/commands/sensors/migrate.js +13 -10
- package/dist/src/commands/sensors/prepare.js +53 -6
- package/dist/src/commands/sensors/project.js +60 -6
- package/dist/src/commands/sensors/result.js +18 -3
- package/dist/src/commands/sensors/run.js +124 -16
- package/dist/src/commands/sensors/status.js +163 -18
- package/dist/src/core/init/steps.js +5 -1
- package/dist/tests/commands/preflight/preflight.test.js +121 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
- package/dist/tests/commands/sensors/init.test.js +107 -0
- package/dist/tests/commands/sensors/migrate.test.js +4 -3
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
- package/dist/tests/commands/sensors/prepare.test.js +62 -0
- package/dist/tests/commands/sensors/result.test.js +66 -0
- package/dist/tests/commands/sensors/run.test.js +331 -0
- package/dist/tests/commands/sensors/status.test.js +204 -0
- package/dist/tests/structural/native-release-artifacts.test.js +4 -2
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -97,6 +97,13 @@ function resolveCoverageInputs(cwd) {
|
|
|
97
97
|
if (manifest.kind === 'v3' && manifest.pack.mode !== 'project-sensors')
|
|
98
98
|
return { kind: 'not_configured' };
|
|
99
99
|
const coverageManifest = manifest;
|
|
100
|
+
// Only-project manifests have no pack coverage contract to evaluate (RF-2.9).
|
|
101
|
+
if (coverageManifest.kind === 'v3') {
|
|
102
|
+
const packId = coverageManifest.pack.pack;
|
|
103
|
+
if (typeof packId !== 'string' || packId.length === 0) {
|
|
104
|
+
return { kind: 'no_reference', projectRoot, pack: null, registry: null, manifest: coverageManifest };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
100
107
|
const source = coverageManifest.kind === 'v3'
|
|
101
108
|
? (() => {
|
|
102
109
|
const resolution = (0, source_1.resolveSensorSource)(coverageManifest, { registries: (0, registries_1.listRegistries)() });
|
|
@@ -117,13 +117,15 @@ function readPackDefaults(pack, registryRoot, cwd) {
|
|
|
117
117
|
return sensors;
|
|
118
118
|
}
|
|
119
119
|
function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
|
|
120
|
-
const fromPack =
|
|
120
|
+
const fromPack = typeof pack === 'string' && pack.length > 0 && registryRoot
|
|
121
|
+
? readPackDefaults(pack, registryRoot, cwd)
|
|
122
|
+
: null;
|
|
121
123
|
// No registry root, or the pack has no pack.json there → `{}` is the honest floor,
|
|
122
124
|
// not a bug to paper over with CLI-hardcoded defaults. `checkManifest` (preflight)
|
|
123
125
|
// and `computeSensorStatus` both surface a zero-sensor manifest as degraded, with a
|
|
124
126
|
// remedy pointing at the registry — never silently inventing sensors here instead.
|
|
125
127
|
const defaults = fromPack ?? {};
|
|
126
|
-
const existingSensors = existing?.sensors ?? {};
|
|
128
|
+
const existingSensors = (existing?.sensors ?? {});
|
|
127
129
|
// Per-FIELD merge, not whole-sensor-object replacement: if `existingSensors.foo`
|
|
128
130
|
// exists at all, a naive `{ ...defaults, ...existingSensors }` would replace
|
|
129
131
|
// `defaults.foo` wholesale, permanently dropping any field that only lives in the
|
|
@@ -132,11 +134,22 @@ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
|
|
|
132
134
|
// field-by-field within each sensor entry lets a user's hand-edited field (e.g. a
|
|
133
135
|
// custom `cmd`) win, while still inheriting any field the existing manifest doesn't
|
|
134
136
|
// specify.
|
|
137
|
+
//
|
|
138
|
+
// Exception — `source: "project"` entries are project-owned: never overlay pack
|
|
139
|
+
// defaults onto them (RF-3.1). Keep the entry byte-stable in meaning.
|
|
135
140
|
const sensorNames = new Set([...Object.keys(defaults), ...Object.keys(existingSensors)]);
|
|
136
141
|
const sensors = {};
|
|
137
142
|
for (const name of sensorNames) {
|
|
143
|
+
const existingEntry = existingSensors[name];
|
|
144
|
+
if (existingEntry && existingEntry.source === 'project') {
|
|
145
|
+
sensors[name] = { ...existingEntry };
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
138
148
|
sensors[name] = { ...defaults[name], ...existingSensors[name] };
|
|
139
149
|
}
|
|
150
|
+
// Only-project write path: omit pack entirely — never invent `generic` (RF-3.2).
|
|
151
|
+
if (typeof pack !== 'string' || pack.length === 0)
|
|
152
|
+
return { sensors };
|
|
140
153
|
return { pack, sensors };
|
|
141
154
|
}
|
|
142
155
|
async function initSensors(opts = {}) {
|
|
@@ -172,10 +172,11 @@ function planV2Migration(input) {
|
|
|
172
172
|
if (validated.kind !== 'v3' || validated.pack.mode !== 'project-sensors') {
|
|
173
173
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
174
174
|
}
|
|
175
|
-
const
|
|
175
|
+
const packBound = (0, manifest_1.asPackBoundProjectSensors)(validated.pack);
|
|
176
|
+
const equivalence = compareV2Semantics(parsed.pack, packBound, source.registry.name);
|
|
176
177
|
if (!allEquivalent(equivalence))
|
|
177
178
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
178
|
-
return { candidate:
|
|
179
|
+
return { candidate: packBound, equivalent: true, equivalence };
|
|
179
180
|
}
|
|
180
181
|
/** Validate the old and new durable contracts, then atomically replace only the manifest. */
|
|
181
182
|
function replaceV2ManifestWithV3(manifestPath, candidate, source) {
|
|
@@ -203,17 +204,18 @@ function replaceV2ManifestWithV3(manifestPath, candidate, source) {
|
|
|
203
204
|
if (before.kind !== 'v2' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
|
|
204
205
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
205
206
|
}
|
|
207
|
+
const afterPack = (0, manifest_1.asPackBoundProjectSensors)(after.pack);
|
|
206
208
|
const logicalSource = exactLogicalSource(source, before.pack.pack);
|
|
207
209
|
assertSourceCompatibleWithManifest(logicalSource, before.pack);
|
|
208
|
-
if (
|
|
210
|
+
if (afterPack.source.registry !== logicalSource.registry.name) {
|
|
209
211
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
210
212
|
}
|
|
211
213
|
if (hasPhysicalSensorPath(before.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
212
|
-
|| hasPhysicalSensorPath(
|
|
213
|
-
|| hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(
|
|
214
|
+
|| hasPhysicalSensorPath(afterPack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
215
|
+
|| hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(afterPack.packageRoot, [])) {
|
|
214
216
|
throw new Error('v2 migration candidate contains a physical path');
|
|
215
217
|
}
|
|
216
|
-
if (!allEquivalent(compareV2Semantics(before.pack,
|
|
218
|
+
if (!allEquivalent(compareV2Semantics(before.pack, afterPack, logicalSource.registry.name))) {
|
|
217
219
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
218
220
|
}
|
|
219
221
|
// The native implementation reopens every ancestor without following links,
|
|
@@ -255,10 +257,11 @@ function replaceLegacyManifestWithV3(manifestPath, candidate, source) {
|
|
|
255
257
|
if (before.kind !== 'legacy' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
|
|
256
258
|
throw new Error('legacy migration candidate semantic mismatch');
|
|
257
259
|
}
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
const afterPack = (0, manifest_1.asPackBoundProjectSensors)(after.pack);
|
|
261
|
+
const logicalSource = exactLogicalSource({ kind: 'logical', source }, afterPack.pack);
|
|
262
|
+
assertSourceCompatibleWithCandidate(logicalSource, afterPack);
|
|
263
|
+
if (afterPack.source.registry !== logicalSource.registry.name || hasPhysicalSensorPath(afterPack.sensors, [logicalSource.registry.contentRoot])
|
|
264
|
+
|| hasPhysicalSensorPath(afterPack.packageRoot, [])) {
|
|
262
265
|
throw new Error('legacy migration candidate contains an unsafe source or physical path');
|
|
263
266
|
}
|
|
264
267
|
assertManifestUnchanged(manifestPath, inspected);
|
|
@@ -6,6 +6,7 @@ exports.prepareLegacySensor = prepareLegacySensor;
|
|
|
6
6
|
exports.prepareV2Sensor = prepareV2Sensor;
|
|
7
7
|
const changed_1 = require("./changed");
|
|
8
8
|
const timeout_1 = require("./compatibility/timeout");
|
|
9
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
9
10
|
function resolveRequestedScope(value) {
|
|
10
11
|
if (value === undefined)
|
|
11
12
|
return 'full';
|
|
@@ -108,13 +109,59 @@ function v2Synthetic(input, reason) {
|
|
|
108
109
|
syntheticReason: reason,
|
|
109
110
|
};
|
|
110
111
|
}
|
|
112
|
+
function projectApplicabilityMet(sensor, paths) {
|
|
113
|
+
const rule = sensor.applicability;
|
|
114
|
+
if (!rule)
|
|
115
|
+
return true;
|
|
116
|
+
if (rule.kind === 'explicit-or-supported-language' || rule.kind === 'explicit-opt-in') {
|
|
117
|
+
// Project sensors do not receive pack-selection signals; treat these kinds
|
|
118
|
+
// as met when no path markers were declared.
|
|
119
|
+
if (!rule.allFiles?.length && !rule.anyFiles?.length)
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const present = new Set(paths ?? []);
|
|
123
|
+
return (!rule.allFiles || rule.allFiles.every(file => present.has(file)))
|
|
124
|
+
&& (!rule.anyFiles || rule.anyFiles.some(file => present.has(file)));
|
|
125
|
+
}
|
|
111
126
|
/**
|
|
112
|
-
* Prepare a
|
|
113
|
-
*
|
|
127
|
+
* Prepare a project-declared sensor from its manifest entry. No live pack variant
|
|
128
|
+
* re-resolution: the structured command on the entry is authoritative.
|
|
129
|
+
*/
|
|
130
|
+
function prepareProjectSensor(input) {
|
|
131
|
+
const sensor = input.sensor;
|
|
132
|
+
const requestedScope = resolveRequestedScope(input.requestedScope);
|
|
133
|
+
if (!projectApplicabilityMet(sensor, input.applicabilityPaths)) {
|
|
134
|
+
return {
|
|
135
|
+
name: input.name,
|
|
136
|
+
...timeout(input.projectTimeout ?? sensor.timeout, input.packTimeout, sensor.fast ?? false),
|
|
137
|
+
requestedScope,
|
|
138
|
+
effectiveScope: 'full',
|
|
139
|
+
syntheticStatus: 'inconclusive',
|
|
140
|
+
syntheticReason: 'not-applicable: applicability-not-met',
|
|
141
|
+
certification: 'project-declared',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
name: input.name,
|
|
146
|
+
command: { kind: 'structured', value: sensor.command },
|
|
147
|
+
formatter: sensor.formatter || 'exit-code',
|
|
148
|
+
...timeout(input.projectTimeout ?? sensor.timeout, input.packTimeout, sensor.fast ?? false),
|
|
149
|
+
requestedScope,
|
|
150
|
+
effectiveScope: 'full',
|
|
151
|
+
certification: 'project-declared',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Prepare a v2/v3 command. Pack-bound sensors re-resolve from the live pack
|
|
156
|
+
* (manifest command is never authority). Project-declared sensors use the
|
|
157
|
+
* entry command and carry `project-declared` provenance.
|
|
114
158
|
*/
|
|
115
159
|
function prepareV2Sensor(input) {
|
|
116
160
|
if (!input || typeof input !== 'object' || !input.sensor || typeof input.name !== 'string' || input.name === '')
|
|
117
161
|
throw new Error('v2 preparation input is invalid');
|
|
162
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(input.sensor))
|
|
163
|
+
return prepareProjectSensor(input);
|
|
164
|
+
const packSensor = input.sensor;
|
|
118
165
|
const requestedScope = resolveRequestedScope(input.requestedScope);
|
|
119
166
|
// Un sensor se ejecuta cuando la herramienta ESTA y funciona aca: `certified` es
|
|
120
167
|
// la version exacta que el registry congelo, y `compatible-unverified` es una que
|
|
@@ -133,16 +180,16 @@ function prepareV2Sensor(input) {
|
|
|
133
180
|
if (!input.liveState || (input.liveState.state !== 'certified' && input.liveState.state !== 'compatible-unverified')) {
|
|
134
181
|
return v2Synthetic(input, input.liveState ? `${input.liveState.state}: ${input.liveState.reason}` : 'compatibility could not be revalidated');
|
|
135
182
|
}
|
|
136
|
-
if (input.liveState.variantId !==
|
|
137
|
-
return v2Synthetic(input, `variant-drift: manifest ${
|
|
138
|
-
const variant = input.liveSensor?.variants.find(candidate => candidate.id ===
|
|
183
|
+
if (input.liveState.variantId !== packSensor.variantId)
|
|
184
|
+
return v2Synthetic(input, `variant-drift: manifest ${packSensor.variantId}, live ${input.liveState.variantId ?? 'none'}; run \`awm sensors init\``);
|
|
185
|
+
const variant = input.liveSensor?.variants.find(candidate => candidate.id === packSensor.variantId);
|
|
139
186
|
if (!variant)
|
|
140
187
|
return v2Synthetic(input, 'variant-drift: selected live variant has no command; run `awm sensors init`');
|
|
141
188
|
const common = {
|
|
142
189
|
name: input.name,
|
|
143
190
|
certification: input.liveState.state === 'certified' ? 'certified' : 'operational-unverified',
|
|
144
191
|
...(variant.formatter ? { formatter: variant.formatter } : {}),
|
|
145
|
-
...timeout(input.projectTimeout ??
|
|
192
|
+
...timeout(input.projectTimeout ?? packSensor.timeout, input.packTimeout ?? input.liveSensor?.timeout, packSensor.fast ?? input.liveSensor?.fast ?? false),
|
|
146
193
|
requestedScope,
|
|
147
194
|
};
|
|
148
195
|
if (requestedScope !== 'changed' || !input.changed)
|
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveContainedPackageRoot = resolveContainedPackageRoot;
|
|
7
|
+
exports.softRecoverInvalidProject = softRecoverInvalidProject;
|
|
6
8
|
exports.resolveSensorProject = resolveSensorProject;
|
|
7
9
|
const fs_1 = __importDefault(require("fs"));
|
|
8
10
|
const path_1 = __importDefault(require("path"));
|
|
@@ -17,6 +19,60 @@ function regularManifest(manifestPath) {
|
|
|
17
19
|
return false;
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve packageRoot under projectRoot with the same realpath containment
|
|
24
|
+
* check used by hard authority selection. Returns null when the path is
|
|
25
|
+
* missing, not a directory, or escapes the project root (including via symlink).
|
|
26
|
+
*/
|
|
27
|
+
function resolveContainedPackageRoot(projectRoot, configuredPackageRoot) {
|
|
28
|
+
const packageRoot = configuredPackageRoot ? path_1.default.resolve(projectRoot, configuredPackageRoot) : projectRoot;
|
|
29
|
+
if (!fs_1.default.existsSync(packageRoot) || !fs_1.default.statSync(packageRoot).isDirectory())
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const realProjectRoot = fs_1.default.realpathSync(projectRoot);
|
|
33
|
+
const realPackageRoot = fs_1.default.realpathSync(packageRoot);
|
|
34
|
+
const relative = path_1.default.relative(realProjectRoot, realPackageRoot);
|
|
35
|
+
if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative))
|
|
36
|
+
return null;
|
|
37
|
+
return packageRoot;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Soft-recover schema-entry failures after a successful JSON.parse of a
|
|
45
|
+
* *regular* sensors.json. Refuses symlink/non-regular manifests and any
|
|
46
|
+
* packageRoot that escapes the project root — same trust boundary as hard parse.
|
|
47
|
+
*/
|
|
48
|
+
function softRecoverInvalidProject(project) {
|
|
49
|
+
if (!regularManifest(project.manifestPath))
|
|
50
|
+
return null;
|
|
51
|
+
if (/regular file/i.test(project.reason))
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
const text = new util_1.TextDecoder('utf-8', { fatal: true }).decode(fs_1.default.readFileSync(project.manifestPath));
|
|
55
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(JSON.parse(text), project.manifestPath);
|
|
56
|
+
if (soft.kind !== 'v3' || soft.pack.mode !== 'project-sensors')
|
|
57
|
+
return null;
|
|
58
|
+
if (Object.keys(soft.pack.sensors).length === 0 && soft.invalidEntries.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
const packageRoot = resolveContainedPackageRoot(project.projectRoot, soft.pack.packageRoot);
|
|
61
|
+
if (packageRoot === null)
|
|
62
|
+
return null;
|
|
63
|
+
const { invalidEntries, ...manifest } = soft;
|
|
64
|
+
return {
|
|
65
|
+
projectRoot: project.projectRoot,
|
|
66
|
+
manifestPath: project.manifestPath,
|
|
67
|
+
packageRoot,
|
|
68
|
+
manifest,
|
|
69
|
+
invalidEntries,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
20
76
|
function gitMarker(directory) {
|
|
21
77
|
try {
|
|
22
78
|
const stat = fs_1.default.lstatSync(path_1.default.join(directory, '.git'));
|
|
@@ -52,14 +108,12 @@ function manifestResolution(projectRoot) {
|
|
|
52
108
|
configuredPackageRoot = manifest.pack.packageRoot;
|
|
53
109
|
if (manifest.kind === 'v3' && manifest.pack.mode === 'project-sensors')
|
|
54
110
|
configuredPackageRoot = manifest.pack.packageRoot;
|
|
55
|
-
const
|
|
56
|
-
if (!fs_1.default.existsSync(
|
|
111
|
+
const candidate = configuredPackageRoot ? path_1.default.resolve(projectRoot, configuredPackageRoot) : projectRoot;
|
|
112
|
+
if (!fs_1.default.existsSync(candidate) || !fs_1.default.statSync(candidate).isDirectory()) {
|
|
57
113
|
return { state: 'invalid', projectRoot, manifestPath, reason: 'packageRoot must name an existing directory' };
|
|
58
114
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const relative = path_1.default.relative(realProjectRoot, realPackageRoot);
|
|
62
|
-
if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
|
|
115
|
+
const packageRoot = resolveContainedPackageRoot(projectRoot, configuredPackageRoot);
|
|
116
|
+
if (packageRoot === null) {
|
|
63
117
|
return { state: 'invalid', projectRoot, manifestPath, reason: 'packageRoot escapes the manifest directory' };
|
|
64
118
|
}
|
|
65
119
|
return { state: 'configured', projectRoot, manifestPath, packageRoot, manifest };
|
|
@@ -14,6 +14,13 @@ const mypy_1 = require("./formatters/mypy");
|
|
|
14
14
|
const ruff_1 = require("./formatters/ruff");
|
|
15
15
|
const shellcheck_1 = require("./formatters/shellcheck");
|
|
16
16
|
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
17
|
+
/** Exit-code sensors signal solely via process status — never invent per-file findings. */
|
|
18
|
+
function parseExitCodeOutput(_raw) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
function truncateEvidence(raw, max = 200) {
|
|
22
|
+
return raw.length <= max ? raw : raw.slice(0, max);
|
|
23
|
+
}
|
|
17
24
|
function formatterFor(name, formatter) {
|
|
18
25
|
if (formatter !== undefined) {
|
|
19
26
|
switch (formatter) {
|
|
@@ -25,6 +32,7 @@ function formatterFor(name, formatter) {
|
|
|
25
32
|
case 'ruff': return ruff_1.parseRuffOutput;
|
|
26
33
|
case 'shellcheck': return shellcheck_1.parseShellcheckOutput;
|
|
27
34
|
case 'generic': return generic_1.parseGenericOutput;
|
|
35
|
+
case 'exit-code': return parseExitCodeOutput;
|
|
28
36
|
default: return generic_1.parseGenericOutput;
|
|
29
37
|
}
|
|
30
38
|
}
|
|
@@ -92,12 +100,18 @@ function interpretResult(prepared, raw) {
|
|
|
92
100
|
return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: reason });
|
|
93
101
|
}
|
|
94
102
|
if (raw.code === 0) {
|
|
95
|
-
if (format === generic_1.parseGenericOutput)
|
|
103
|
+
if (format === generic_1.parseGenericOutput || format === parseExitCodeOutput) {
|
|
96
104
|
return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
|
|
105
|
+
}
|
|
97
106
|
const errors = format(raw.stdout);
|
|
98
107
|
return withEvidence({ name: prepared.name, status: errors.length ? 'fail' : 'pass', errors });
|
|
99
108
|
}
|
|
100
109
|
const output = raw.stdout + raw.stderr;
|
|
110
|
+
if (prepared.formatter === 'exit-code' || format === parseExitCodeOutput) {
|
|
111
|
+
const combined = output.trim();
|
|
112
|
+
const evidence = truncateEvidence(combined.length > 0 ? combined : `exit ${raw.code}`);
|
|
113
|
+
return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: evidence }] });
|
|
114
|
+
}
|
|
101
115
|
const errors = format(output);
|
|
102
116
|
if (errors.length > 0)
|
|
103
117
|
return withEvidence({ name: prepared.name, status: 'fail', errors });
|
|
@@ -108,10 +122,10 @@ function interpretResult(prepared, raw) {
|
|
|
108
122
|
|| lower.includes('enoent')
|
|
109
123
|
|| lower.includes('could not determine executable');
|
|
110
124
|
if (toolMissing)
|
|
111
|
-
return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${output
|
|
125
|
+
return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${truncateEvidence(output)}` }] });
|
|
112
126
|
if (prepared.name === 'test')
|
|
113
127
|
return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `SENSOR[${prepared.name}] failed (exit ${raw.code})` }] });
|
|
114
|
-
return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: `exit ${raw.code}: ${output
|
|
128
|
+
return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: `exit ${raw.code}: ${truncateEvidence(output)}` });
|
|
115
129
|
}
|
|
116
130
|
/** Execute a validated prepared command, or render its deliberate synthetic result. */
|
|
117
131
|
async function executePrepared(prepared, cwd = process.cwd()) {
|
|
@@ -123,6 +137,7 @@ async function executePrepared(prepared, cwd = process.cwd()) {
|
|
|
123
137
|
status,
|
|
124
138
|
errors: [],
|
|
125
139
|
...(prepared.syntheticReason ? { skipReason: prepared.syntheticReason } : {}),
|
|
140
|
+
...(prepared.certification ? { certification: prepared.certification } : {}),
|
|
126
141
|
execution: executionEvidence(prepared, 0),
|
|
127
142
|
}, prepared);
|
|
128
143
|
}
|
|
@@ -22,6 +22,8 @@ const detection_1 = require("./detection");
|
|
|
22
22
|
const live_1 = require("./compatibility/live");
|
|
23
23
|
const contract_1 = require("./compatibility/contract");
|
|
24
24
|
const source_1 = require("./compatibility/source");
|
|
25
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
26
|
+
const discovery_1 = require("./compatibility/discovery");
|
|
25
27
|
const registries_1 = require("../../core/registries");
|
|
26
28
|
const prepare_1 = require("./prepare");
|
|
27
29
|
const verdict_1 = require("./verdict");
|
|
@@ -172,6 +174,12 @@ async function pooled(tasks, limit) {
|
|
|
172
174
|
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
|
|
173
175
|
return results;
|
|
174
176
|
}
|
|
177
|
+
function v3ProjectSensors(parsed) {
|
|
178
|
+
return parsed.kind === 'v3' && parsed.pack.mode === 'project-sensors' ? parsed.pack : null;
|
|
179
|
+
}
|
|
180
|
+
function hasSelectedPack(manifest) {
|
|
181
|
+
return typeof manifest.pack === 'string' && manifest.pack.length > 0;
|
|
182
|
+
}
|
|
175
183
|
/**
|
|
176
184
|
* One orchestration path for both manifest contracts. Preparation selects the
|
|
177
185
|
* authorized command; execution, baseline handling, and verdict reduction are
|
|
@@ -190,22 +198,34 @@ async function runSensors(opts = {}) {
|
|
|
190
198
|
if (project.state === 'missing') {
|
|
191
199
|
return { sensors: [], overall: 'not_certified', mode: 'missing', reason: 'manifest-absent', projectRoot: project.projectRoot, manifestPath: project.manifestPath, remedy: 'run-awm-sensors-bootstrap' };
|
|
192
200
|
}
|
|
201
|
+
let invalidEntries = [];
|
|
202
|
+
let configured;
|
|
193
203
|
if (project.state === 'invalid') {
|
|
194
|
-
const
|
|
195
|
-
|
|
204
|
+
const recovered = (0, project_1.softRecoverInvalidProject)(project);
|
|
205
|
+
if (!recovered) {
|
|
206
|
+
const reason = project.reason.includes('unsupported manifest schemaVersion') ? 'schema-unsupported' : 'manifest-malformed';
|
|
207
|
+
return { sensors: [], overall: 'not_certified', mode: 'invalid', reason, projectRoot: project.projectRoot, manifestPath: project.manifestPath, remedy: 'repair-sensor-manifest' };
|
|
208
|
+
}
|
|
209
|
+
invalidEntries = recovered.invalidEntries;
|
|
210
|
+
configured = recovered;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
configured = project;
|
|
196
214
|
}
|
|
197
|
-
const manifestDir =
|
|
215
|
+
const manifestDir = configured.projectRoot;
|
|
198
216
|
const beforeReadOnly = opts.readOnly ? filesystemSnapshot(manifestDir) : undefined;
|
|
199
217
|
if (opts.readOnly && beforeReadOnly === null) {
|
|
200
|
-
return { sensors: [], overall: 'not_certified', projectRoot:
|
|
218
|
+
return { sensors: [], overall: 'not_certified', projectRoot: configured.projectRoot, manifestPath: configured.manifestPath, mode: 'invalid', reason: 'read-only-observation-unavailable', remedy: 'run admission from a readable git worktree' };
|
|
201
219
|
}
|
|
202
|
-
const parsed =
|
|
203
|
-
const authority = { projectRoot:
|
|
220
|
+
const parsed = configured.manifest;
|
|
221
|
+
const authority = { projectRoot: configured.projectRoot, manifestPath: configured.manifestPath };
|
|
204
222
|
if (parsed.kind === 'v3' && (parsed.pack.mode === 'native-gate' || parsed.pack.mode === 'opt-out')) {
|
|
205
223
|
return { sensors: [], overall: 'not_certified', ...authority, mode: parsed.pack.mode, reason: `${parsed.pack.mode}-declared`, declarationReason: parsed.pack.reason };
|
|
206
224
|
}
|
|
225
|
+
const projectSensorsManifest = v3ProjectSensors(parsed);
|
|
226
|
+
const onlyProject = projectSensorsManifest !== null && !hasSelectedPack(projectSensorsManifest);
|
|
207
227
|
let resolvedSource;
|
|
208
|
-
if (parsed.kind === 'v2' || parsed.kind === 'v3') {
|
|
228
|
+
if (parsed.kind === 'v2' || (parsed.kind === 'v3' && projectSensorsManifest && hasSelectedPack(projectSensorsManifest))) {
|
|
209
229
|
try {
|
|
210
230
|
resolvedSource = (0, source_1.resolveSensorSource)(parsed, { registries: (0, registries_1.listRegistries)() });
|
|
211
231
|
}
|
|
@@ -230,25 +250,108 @@ async function runSensors(opts = {}) {
|
|
|
230
250
|
// manifest itself stays discoverable at the repo root via findManifestDir.
|
|
231
251
|
// Legacy manifests never carry this field — manifestDir is always correct
|
|
232
252
|
// for them, unchanged.
|
|
233
|
-
const projectCwd =
|
|
253
|
+
const projectCwd = configured.packageRoot;
|
|
234
254
|
const live = parsed.kind !== 'legacy' && resolvedSource && 'source' in resolvedSource
|
|
235
|
-
? await resolveLiveFromSource(projectCwd, resolvedSource.source, 'packSelection' in
|
|
255
|
+
? await resolveLiveFromSource(projectCwd, resolvedSource.source, projectSensorsManifest && 'packSelection' in projectSensorsManifest ? projectSensorsManifest.packSelection : undefined)
|
|
236
256
|
: null;
|
|
237
257
|
const drift = parsed.kind === 'legacy' ? detectPackDrift(manifestDir, parsed.pack) : undefined;
|
|
238
258
|
const requestedScope = opts.changed ? 'changed' : 'full';
|
|
239
259
|
const prepared = [];
|
|
240
260
|
const activeManifest = parsed.pack;
|
|
241
|
-
|
|
261
|
+
const sensorsMap = activeManifest.sensors ?? {};
|
|
262
|
+
const packSelected = projectSensorsManifest !== null && hasSelectedPack(projectSensorsManifest);
|
|
263
|
+
for (const [name, sensor] of Object.entries(sensorsMap)) {
|
|
264
|
+
const entry = sensor;
|
|
265
|
+
const projectEntry = parsed.kind !== 'legacy' && (0, manifest_1.isProjectDeclaredSensor)(entry)
|
|
266
|
+
? entry
|
|
267
|
+
: null;
|
|
268
|
+
// RF-1.7: collision when the live pack defines this id. When pack is selected
|
|
269
|
+
// but live cannot be resolved, fail closed — never run a project override that
|
|
270
|
+
// might be colliding with an unresolved pack sensor name.
|
|
271
|
+
const collidedWithPack = projectEntry !== null && (packSelected && live === null
|
|
272
|
+
? true
|
|
273
|
+
: live?.pack.sensors[name] !== undefined);
|
|
274
|
+
if (collidedWithPack) {
|
|
275
|
+
invalidEntries.push({ name, reason: `project-sensor-name-collision: ${name}` });
|
|
276
|
+
// RF-1.7: keep the pack sensor when live is available; otherwise skip.
|
|
277
|
+
if (live === null || live.pack.sensors[name] === undefined)
|
|
278
|
+
continue;
|
|
279
|
+
// Fall through to pack-bound prepare using live.
|
|
280
|
+
}
|
|
281
|
+
else if (projectEntry !== null) {
|
|
282
|
+
const fast = projectEntry.fast ?? false;
|
|
283
|
+
if (!shouldRun(fast, opts))
|
|
284
|
+
continue;
|
|
285
|
+
const markers = [
|
|
286
|
+
...(projectEntry.applicability?.allFiles ?? []),
|
|
287
|
+
...(projectEntry.applicability?.anyFiles ?? []),
|
|
288
|
+
];
|
|
289
|
+
const applicabilityPaths = markers.length > 0
|
|
290
|
+
? (0, discovery_1.discoverPresentMarkers)(projectCwd, markers)
|
|
291
|
+
: undefined;
|
|
292
|
+
const execution = (0, prepare_1.prepareV2Sensor)({
|
|
293
|
+
name,
|
|
294
|
+
sensor: projectEntry,
|
|
295
|
+
requestedScope,
|
|
296
|
+
changed: changed ?? undefined,
|
|
297
|
+
projectTimeout: projectEntry.timeout,
|
|
298
|
+
applicabilityPaths,
|
|
299
|
+
});
|
|
300
|
+
prepared.push(projectEntry.enabled === false
|
|
301
|
+
? { ...execution, command: undefined, syntheticStatus: 'skipped', syntheticReason: 'disabled' }
|
|
302
|
+
: execution);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
242
305
|
const liveSensor = parsed.kind !== 'legacy' ? live?.pack.sensors[name] : undefined;
|
|
306
|
+
const liveState = parsed.kind !== 'legacy' ? live?.sensors[name] : undefined;
|
|
307
|
+
let packBoundSensor = entry;
|
|
308
|
+
if (collidedWithPack && liveSensor) {
|
|
309
|
+
// Always reconstruct from live — never leave packBoundSensor as the project entry.
|
|
310
|
+
const fallbackVariant = liveSensor.variants[0];
|
|
311
|
+
if (!fallbackVariant)
|
|
312
|
+
continue;
|
|
313
|
+
const variantId = liveState?.variantId ?? fallbackVariant.id;
|
|
314
|
+
const variant = liveSensor.variants.find(candidate => candidate.id === variantId) ?? fallbackVariant;
|
|
315
|
+
const packSyntheticCompatibility = {
|
|
316
|
+
state: 'compatible-unverified',
|
|
317
|
+
reason: 'collision-pack-fallback',
|
|
318
|
+
variantId,
|
|
319
|
+
toolVersion: null,
|
|
320
|
+
runtimeVersion: null,
|
|
321
|
+
certifiedRange: null,
|
|
322
|
+
evidence: [],
|
|
323
|
+
};
|
|
324
|
+
const reconstructed = {
|
|
325
|
+
enabled: true,
|
|
326
|
+
...(liveSensor.fast !== undefined ? { fast: liveSensor.fast } : {}),
|
|
327
|
+
...(liveSensor.timeout !== undefined ? { timeout: liveSensor.timeout } : {}),
|
|
328
|
+
variantId,
|
|
329
|
+
command: variant.command,
|
|
330
|
+
initializedCompatibility: liveState ?? packSyntheticCompatibility,
|
|
331
|
+
};
|
|
332
|
+
packBoundSensor = reconstructed;
|
|
333
|
+
}
|
|
243
334
|
const fast = parsed.kind !== 'legacy'
|
|
244
|
-
?
|
|
245
|
-
:
|
|
335
|
+
? packBoundSensor.fast ?? liveSensor?.fast ?? false
|
|
336
|
+
: entry.fast ?? false;
|
|
246
337
|
if (!shouldRun(fast, opts))
|
|
247
338
|
continue;
|
|
248
339
|
const execution = parsed.kind !== 'legacy'
|
|
249
|
-
? (0, prepare_1.prepareV2Sensor)({
|
|
250
|
-
|
|
251
|
-
|
|
340
|
+
? (0, prepare_1.prepareV2Sensor)({
|
|
341
|
+
name,
|
|
342
|
+
sensor: packBoundSensor,
|
|
343
|
+
liveSensor,
|
|
344
|
+
liveState,
|
|
345
|
+
requestedScope,
|
|
346
|
+
changed: changed ?? undefined,
|
|
347
|
+
})
|
|
348
|
+
: (0, prepare_1.prepareLegacySensor)({
|
|
349
|
+
name,
|
|
350
|
+
config: entry,
|
|
351
|
+
requestedScope,
|
|
352
|
+
changed: changed ?? undefined,
|
|
353
|
+
});
|
|
354
|
+
prepared.push(packBoundSensor.enabled === false
|
|
252
355
|
? { ...execution, command: undefined, syntheticStatus: 'skipped', syntheticReason: 'disabled' }
|
|
253
356
|
: execution);
|
|
254
357
|
}
|
|
@@ -268,9 +371,14 @@ async function runSensors(opts = {}) {
|
|
|
268
371
|
overall,
|
|
269
372
|
...authority,
|
|
270
373
|
mode: 'project-sensors',
|
|
271
|
-
reason: parsed.kind === 'legacy'
|
|
374
|
+
reason: parsed.kind === 'legacy'
|
|
375
|
+
? 'legacy-v1'
|
|
376
|
+
: onlyProject
|
|
377
|
+
? 'configured-v3'
|
|
378
|
+
: resolvedSource?.kind === 'logical' ? 'configured-v3' : resolvedSource?.kind,
|
|
272
379
|
...(parsed.kind !== 'legacy' && resolvedSource && 'source' in resolvedSource
|
|
273
380
|
? { source: { kind: resolvedSource.kind, registry: resolvedSource.source.registry.name } } : {}),
|
|
381
|
+
...(invalidEntries.length > 0 ? { invalidEntries } : {}),
|
|
274
382
|
...(drift?.drift ? { packDrift: drift.drift } : {}),
|
|
275
383
|
...(changed ? { changedScope: { files: changed.files.length, ...(changed.error ? { error: changed.error } : {}) } } : {}),
|
|
276
384
|
};
|