@cassiomc1/forgeloop 1.2.1 → 1.2.2
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/.cursor/rules/project-loop.mdc +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/DOCS_INDEX.md +3 -0
- package/ENG/design-code-eng.md +59 -0
- package/ENG/premium-sites-studio-eng.md +28 -0
- package/LOOP_ENGINEERING.md +23 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -5
- package/ORCHESTRATOR_INTEGRATION.md +37 -4
- package/PROTOCOL_INTEGRATION.md +13 -0
- package/README.md +34 -2
- package/TERMINOLOGY.md +10 -0
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/THREAT_MODEL.md +12 -1
- package/docs/ARTIFACT_REFERENCE.md +150 -0
- package/docs/CLI_REFERENCE.md +263 -30
- package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
- package/docs/DOCUMENTATION_GUIDE.md +41 -4
- package/docs/GETTING_STARTED.md +9 -4
- package/docs/RECIPES.md +31 -1
- package/docs/TROUBLESHOOTING.md +191 -0
- package/package.json +1 -1
- package/schemas/policy-baseline.schema.json +26 -0
- package/schemas/policy-discovery.schema.json +45 -0
- package/schemas/policy-lock.schema.json +16 -0
- package/schemas/policy-rules.schema.json +48 -0
- package/schemas/policy-snapshot.schema.json +16 -0
- package/src/cli.js +69 -1
- package/src/commands/baseline.js +120 -0
- package/src/commands/init.js +304 -6
- package/src/commands/policy-diff.js +51 -0
- package/src/commands/policy-discover.js +42 -0
- package/src/commands/policy-status.js +33 -0
- package/src/commands/profile-interview.js +50 -0
- package/src/commands/reconcile-closure.js +49 -0
- package/src/commands/rule-verify.js +36 -0
- package/src/commands/validate-receipt.js +38 -3
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +24 -0
- package/src/core/cli-command-definitions.js +114 -7
- package/src/core/cli-metadata.js +1 -1
- package/src/core/completion-artifacts.js +29 -3
- package/src/core/completion.js +101 -10
- package/src/core/error-codes.js +227 -0
- package/src/core/events.js +22 -0
- package/src/core/execution-prerequisites.js +38 -20
- package/src/core/execution.js +20 -3
- package/src/core/native-adapters.js +14 -4
- package/src/core/next-action-model.js +9 -0
- package/src/core/next-action.js +128 -82
- package/src/core/policy-adapters.js +276 -0
- package/src/core/policy-baseline.js +144 -0
- package/src/core/policy-diff.js +133 -0
- package/src/core/policy-discovery.js +225 -0
- package/src/core/policy-engine.js +533 -0
- package/src/core/policy-mutation.js +139 -0
- package/src/core/preflight-consistency.js +23 -15
- package/src/core/preflight.js +65 -1
- package/src/core/reconcile-closure.js +173 -0
- package/src/core/schema-validation.js +6 -0
- package/src/core/task-context.js +11 -0
- package/src/core/task-discovery.js +67 -1
- package/src/core/task-paths.js +9 -0
- package/src/core/templates.js +5 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileExists } from "./filesystem.js";
|
|
4
|
+
import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
|
|
5
|
+
import { assertJsonLimits } from "./json-safety.js";
|
|
6
|
+
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
7
|
+
import { canonicalFingerprint, writeJsonArtifact } from "./artifacts.js";
|
|
8
|
+
import { sha256 } from "./manifest.js";
|
|
9
|
+
import { BUILTIN_POLICY_RULES, discoverPolicy } from "./policy-discovery.js";
|
|
10
|
+
import { getPolicyAdapter } from "./policy-adapters.js";
|
|
11
|
+
import { evaluateBaselineViolations, readBaseline, writeBaseline } from "./policy-baseline.js";
|
|
12
|
+
import { verifyRuleMutation } from "./policy-mutation.js";
|
|
13
|
+
import { diffPolicies } from "./policy-diff.js";
|
|
14
|
+
|
|
15
|
+
export { readBaseline, writeBaseline, evaluateBaselineViolations };
|
|
16
|
+
|
|
17
|
+
export async function readProjectRules(target, packageRoot) {
|
|
18
|
+
const relPath = PROJECT_ARTIFACT_PATHS.policyRules;
|
|
19
|
+
const fullPath = path.join(target, relPath);
|
|
20
|
+
if (!(await fileExists(fullPath))) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const raw = await readFile(fullPath, "utf8");
|
|
24
|
+
assertJsonLimits(raw, relPath);
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
const schema = await readSchema("policy-rules", packageRoot);
|
|
27
|
+
assertSchema(parsed, schema, "policy-rules");
|
|
28
|
+
return parsed.rules ?? [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function writeProjectRules(target, rules, packageRoot) {
|
|
32
|
+
const relPath = PROJECT_ARTIFACT_PATHS.policyRules;
|
|
33
|
+
const payload = { schemaVersion: 1, rules };
|
|
34
|
+
const schema = await readSchema("policy-rules", packageRoot);
|
|
35
|
+
assertSchema(payload, schema, "policy-rules");
|
|
36
|
+
await writeJsonArtifact(target, relPath, payload, "policy-rules", packageRoot);
|
|
37
|
+
return payload;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function readDiscoveryReport(target, packageRoot) {
|
|
41
|
+
const relPath = PROJECT_ARTIFACT_PATHS.policyDiscovery;
|
|
42
|
+
const fullPath = path.join(target, relPath);
|
|
43
|
+
if (!(await fileExists(fullPath))) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const raw = await readFile(fullPath, "utf8");
|
|
47
|
+
assertJsonLimits(raw, relPath);
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
const schema = await readSchema("policy-discovery", packageRoot);
|
|
50
|
+
assertSchema(parsed, schema, "policy-discovery");
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function writeDiscoveryReport(target, discovery, packageRoot) {
|
|
55
|
+
const relPath = PROJECT_ARTIFACT_PATHS.policyDiscovery;
|
|
56
|
+
const schema = await readSchema("policy-discovery", packageRoot);
|
|
57
|
+
assertSchema(discovery, schema, "policy-discovery");
|
|
58
|
+
await writeJsonArtifact(target, relPath, discovery, "policy-discovery", packageRoot);
|
|
59
|
+
return discovery;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function readPolicyLock(target, packageRoot) {
|
|
63
|
+
const relPath = PROJECT_ARTIFACT_PATHS.policyLock;
|
|
64
|
+
const fullPath = path.join(target, relPath);
|
|
65
|
+
if (!(await fileExists(fullPath))) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const raw = await readFile(fullPath, "utf8");
|
|
69
|
+
assertJsonLimits(raw, relPath);
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
const schema = await readSchema("policy-lock", packageRoot);
|
|
72
|
+
assertSchema(parsed, schema, "policy-lock");
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function writePolicyLock(target, lock, packageRoot) {
|
|
77
|
+
const relPath = PROJECT_ARTIFACT_PATHS.policyLock;
|
|
78
|
+
const schema = await readSchema("policy-lock", packageRoot);
|
|
79
|
+
assertSchema(lock, schema, "policy-lock");
|
|
80
|
+
await writeJsonArtifact(target, relPath, lock, "policy-lock", packageRoot);
|
|
81
|
+
return lock;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readTaskPolicySnapshot(target, taskId, packageRoot) {
|
|
85
|
+
const relPath = taskArtifactPath(taskId, "policySnapshot");
|
|
86
|
+
const fullPath = path.join(target, relPath);
|
|
87
|
+
if (!(await fileExists(fullPath))) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const raw = await readFile(fullPath, "utf8");
|
|
91
|
+
assertJsonLimits(raw, relPath);
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
const schema = await readSchema("policy-snapshot", packageRoot);
|
|
94
|
+
assertSchema(parsed, schema, "policy-snapshot");
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function writeTaskPolicySnapshot(target, taskId, snapshot, packageRoot) {
|
|
99
|
+
const relPath = taskArtifactPath(taskId, "policySnapshot");
|
|
100
|
+
const schema = await readSchema("policy-snapshot", packageRoot);
|
|
101
|
+
assertSchema(snapshot, schema, "policy-snapshot");
|
|
102
|
+
await writeJsonArtifact(target, relPath, snapshot, "policy-snapshot", packageRoot);
|
|
103
|
+
return snapshot;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function loadEffectiveRules(target, packageRoot) {
|
|
107
|
+
const ruleMap = new Map();
|
|
108
|
+
|
|
109
|
+
// 1. Built-in rules
|
|
110
|
+
for (const rule of BUILTIN_POLICY_RULES) {
|
|
111
|
+
ruleMap.set(rule.id, { ...rule });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Discovered rules
|
|
115
|
+
let discovery = await readDiscoveryReport(target, packageRoot);
|
|
116
|
+
if (!discovery) {
|
|
117
|
+
discovery = await discoverPolicy({ target });
|
|
118
|
+
}
|
|
119
|
+
for (const rule of discovery.discoveredRules ?? []) {
|
|
120
|
+
ruleMap.set(rule.id, { ...rule });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3. Project rules (highest precedence)
|
|
124
|
+
const projectRules = await readProjectRules(target, packageRoot);
|
|
125
|
+
if (projectRules) {
|
|
126
|
+
for (const rule of projectRules) {
|
|
127
|
+
ruleMap.set(rule.id, { ...rule, source: "project" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [...ruleMap.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function detectPolicyCapability(target, packageRoot) {
|
|
135
|
+
const rulesRel = PROJECT_ARTIFACT_PATHS.policyRules;
|
|
136
|
+
const baselineRel = PROJECT_ARTIFACT_PATHS.policyBaseline;
|
|
137
|
+
const discoveryRel = PROJECT_ARTIFACT_PATHS.policyDiscovery;
|
|
138
|
+
const lockRel = PROJECT_ARTIFACT_PATHS.policyLock;
|
|
139
|
+
|
|
140
|
+
const hasRules = await fileExists(path.join(target, rulesRel));
|
|
141
|
+
const hasBaseline = await fileExists(path.join(target, baselineRel));
|
|
142
|
+
const hasDiscovery = await fileExists(path.join(target, discoveryRel));
|
|
143
|
+
const hasLock = await fileExists(path.join(target, lockRel));
|
|
144
|
+
|
|
145
|
+
if (!hasRules && !hasBaseline && !hasDiscovery && !hasLock) {
|
|
146
|
+
return "NOT_PRESENT";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
if (hasRules) await readProjectRules(target, packageRoot);
|
|
151
|
+
if (hasBaseline) await readBaseline(target, packageRoot);
|
|
152
|
+
if (hasDiscovery) await readDiscoveryReport(target, packageRoot);
|
|
153
|
+
if (hasLock) await readPolicyLock(target, packageRoot);
|
|
154
|
+
return "AVAILABLE";
|
|
155
|
+
} catch {
|
|
156
|
+
return "INVALID";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function canonicalizeRules(rules) {
|
|
161
|
+
const seenIds = new Set();
|
|
162
|
+
const sorted = [...(rules ?? [])].sort((a, b) => a.id.localeCompare(b.id));
|
|
163
|
+
for (const r of sorted) {
|
|
164
|
+
if (seenIds.has(r.id)) {
|
|
165
|
+
throw new Error(`Duplicate rule ID detected: ${r.id}`);
|
|
166
|
+
}
|
|
167
|
+
seenIds.add(r.id);
|
|
168
|
+
}
|
|
169
|
+
return sorted;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function canonicalizeBaseline(baseline) {
|
|
173
|
+
if (!baseline || !Array.isArray(baseline.entries)) {
|
|
174
|
+
return { schemaVersion: 1, entries: [] };
|
|
175
|
+
}
|
|
176
|
+
const entries = baseline.entries.map((entry) => {
|
|
177
|
+
const fps = entry.fingerprints ?? [];
|
|
178
|
+
if (new Set(fps).size !== fps.length) {
|
|
179
|
+
throw new Error(`Duplicate fingerprint detected for rule ${entry.ruleId}`);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
ruleId: entry.ruleId,
|
|
183
|
+
fingerprints: [...fps].sort(),
|
|
184
|
+
...(entry.reviewBy ? { reviewBy: entry.reviewBy } : {}),
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
schemaVersion: baseline.schemaVersion ?? 1,
|
|
189
|
+
entries: entries.sort((a, b) => a.ruleId.localeCompare(b.ruleId)),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function computePolicyLockData(rules, baseline) {
|
|
194
|
+
const canonicalRules = canonicalizeRules(rules);
|
|
195
|
+
const canonicalBase = canonicalizeBaseline(baseline);
|
|
196
|
+
const rulesDigest = sha256(canonicalFingerprint(canonicalRules));
|
|
197
|
+
const baselineDigest = sha256(canonicalFingerprint(canonicalBase));
|
|
198
|
+
const fullDigest = sha256(`${rulesDigest}:${baselineDigest}`);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
algorithm: "sha256",
|
|
203
|
+
digest: `sha256:${fullDigest}`,
|
|
204
|
+
rulesDigest: `sha256:${rulesDigest}`,
|
|
205
|
+
baselineDigest: `sha256:${baselineDigest}`,
|
|
206
|
+
capturedAt: new Date().toISOString(),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function verifyPolicyLock(target, packageRoot) {
|
|
211
|
+
const capability = await detectPolicyCapability(target, packageRoot);
|
|
212
|
+
if (capability === "NOT_PRESENT") {
|
|
213
|
+
return { status: "NOT_APPLICABLE" };
|
|
214
|
+
}
|
|
215
|
+
if (capability === "INVALID") {
|
|
216
|
+
return { status: "INVALID", error: "Policy artifacts are malformed" };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const persistedLock = await readPolicyLock(target, packageRoot);
|
|
220
|
+
if (!persistedLock) {
|
|
221
|
+
return { status: "MISMATCH", error: "Policy lockfile is missing" };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const rules = await loadEffectiveRules(target, packageRoot);
|
|
225
|
+
const baseline = await readBaseline(target, packageRoot);
|
|
226
|
+
const expectedLock = computePolicyLockData(rules, baseline);
|
|
227
|
+
|
|
228
|
+
const mismatches = [];
|
|
229
|
+
if (persistedLock.algorithm !== expectedLock.algorithm) {
|
|
230
|
+
mismatches.push("algorithm");
|
|
231
|
+
}
|
|
232
|
+
if (persistedLock.digest !== expectedLock.digest) {
|
|
233
|
+
mismatches.push("digest");
|
|
234
|
+
}
|
|
235
|
+
if (persistedLock.rulesDigest !== expectedLock.rulesDigest) {
|
|
236
|
+
mismatches.push("rulesDigest");
|
|
237
|
+
}
|
|
238
|
+
if (persistedLock.baselineDigest !== expectedLock.baselineDigest) {
|
|
239
|
+
mismatches.push("baselineDigest");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (mismatches.length > 0) {
|
|
243
|
+
return {
|
|
244
|
+
status: "MISMATCH",
|
|
245
|
+
mismatches,
|
|
246
|
+
expected: {
|
|
247
|
+
algorithm: expectedLock.algorithm,
|
|
248
|
+
digest: expectedLock.digest,
|
|
249
|
+
rulesDigest: expectedLock.rulesDigest,
|
|
250
|
+
baselineDigest: expectedLock.baselineDigest,
|
|
251
|
+
},
|
|
252
|
+
observed: {
|
|
253
|
+
algorithm: persistedLock.algorithm ?? null,
|
|
254
|
+
digest: persistedLock.digest ?? null,
|
|
255
|
+
rulesDigest: persistedLock.rulesDigest ?? null,
|
|
256
|
+
baselineDigest: persistedLock.baselineDigest ?? null,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
status: "VALID",
|
|
263
|
+
digest: expectedLock.digest,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function evaluateTargetPolicy({
|
|
268
|
+
target = process.cwd(),
|
|
269
|
+
packageRoot,
|
|
270
|
+
taskId = null,
|
|
271
|
+
files = null,
|
|
272
|
+
overrideAdapters = null,
|
|
273
|
+
now = new Date().toISOString(),
|
|
274
|
+
} = {}) {
|
|
275
|
+
const capability = await detectPolicyCapability(target, packageRoot);
|
|
276
|
+
if (capability === "NOT_PRESENT") {
|
|
277
|
+
return {
|
|
278
|
+
status: "VALID",
|
|
279
|
+
capability: "NOT_PRESENT",
|
|
280
|
+
rules: [],
|
|
281
|
+
provenRules: 0,
|
|
282
|
+
inertRules: 0,
|
|
283
|
+
unsupportedRules: 0,
|
|
284
|
+
baselineViolations: 0,
|
|
285
|
+
newViolations: [],
|
|
286
|
+
resolvedViolations: [],
|
|
287
|
+
ratchetedBaseline: null,
|
|
288
|
+
lock: { status: "NOT_APPLICABLE" },
|
|
289
|
+
drift: { detected: false },
|
|
290
|
+
errors: [],
|
|
291
|
+
warnings: [],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (capability === "INVALID") {
|
|
296
|
+
return {
|
|
297
|
+
status: "INVALID",
|
|
298
|
+
capability: "INVALID",
|
|
299
|
+
rules: [],
|
|
300
|
+
provenRules: 0,
|
|
301
|
+
inertRules: 0,
|
|
302
|
+
unsupportedRules: 0,
|
|
303
|
+
baselineViolations: 0,
|
|
304
|
+
newViolations: [],
|
|
305
|
+
resolvedViolations: [],
|
|
306
|
+
ratchetedBaseline: null,
|
|
307
|
+
lock: { status: "INVALID" },
|
|
308
|
+
drift: { detected: false },
|
|
309
|
+
errors: [
|
|
310
|
+
{
|
|
311
|
+
code: "E_POLICY_INVALID",
|
|
312
|
+
why: "Policy configuration or baseline artifacts are malformed or fail schema validation.",
|
|
313
|
+
fix: "Validate and repair rules.json, baseline.json, or discovery.json against schemas.",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
warnings: [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const rules = await loadEffectiveRules(target, packageRoot);
|
|
321
|
+
const baseline = await readBaseline(target, packageRoot);
|
|
322
|
+
const errors = [];
|
|
323
|
+
const warnings = [];
|
|
324
|
+
const evaluatedRules = [];
|
|
325
|
+
const rawViolations = [];
|
|
326
|
+
|
|
327
|
+
// Active lock verification
|
|
328
|
+
const lockVerification = await verifyPolicyLock(target, packageRoot);
|
|
329
|
+
if (lockVerification.status === "MISMATCH") {
|
|
330
|
+
const expStr = typeof lockVerification.expected === "object" ? lockVerification.expected.digest : lockVerification.expected;
|
|
331
|
+
const obsStr = typeof lockVerification.observed === "object" ? lockVerification.observed.digest : lockVerification.observed;
|
|
332
|
+
errors.push({
|
|
333
|
+
code: "POLICY_LOCK_MISMATCH",
|
|
334
|
+
why: `Persisted policy lock does not match current effective policy: expected ${expStr}, observed ${obsStr}`,
|
|
335
|
+
fix: "Re-evaluate effective rules and update policy.lock or restore modified rules.",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const rule of rules) {
|
|
340
|
+
const adapterId = rule.check?.adapter ?? (typeof rule.check === "string" ? rule.check : null);
|
|
341
|
+
const adapter = overrideAdapters?.[adapterId] ?? getPolicyAdapter(adapterId);
|
|
342
|
+
|
|
343
|
+
if (!adapter) {
|
|
344
|
+
evaluatedRules.push({
|
|
345
|
+
ruleId: rule.id,
|
|
346
|
+
rule,
|
|
347
|
+
status: "UNSUPPORTED",
|
|
348
|
+
why: `No policy adapter found for ${adapterId}`,
|
|
349
|
+
fix: "Configure an existing adapter or remove the rule.",
|
|
350
|
+
});
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let checkResult;
|
|
355
|
+
try {
|
|
356
|
+
checkResult = await adapter.check({ target, rule, files });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
errors.push({
|
|
359
|
+
code: "POLICY_EVALUATION_FAILED",
|
|
360
|
+
ruleId: rule.id,
|
|
361
|
+
why: `Policy evaluation threw an unexpected error for rule ${rule.id}: ${error.message}`,
|
|
362
|
+
fix: "Inspect adapter check logic and target files for unhandled exceptions.",
|
|
363
|
+
});
|
|
364
|
+
checkResult = { passed: false, isInert: false, violations: [], error: error.message };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Handle inert checks
|
|
368
|
+
if (checkResult.isInert) {
|
|
369
|
+
if (rule.source === "discovered") {
|
|
370
|
+
// Gracefully downgrade discovered rules
|
|
371
|
+
evaluatedRules.push({
|
|
372
|
+
ruleId: rule.id,
|
|
373
|
+
rule,
|
|
374
|
+
status: "UNSUPPORTED",
|
|
375
|
+
isInert: true,
|
|
376
|
+
why: "Discovered rule is currently inert (no applicable files in repository).",
|
|
377
|
+
fix: "Rule will automatically activate when matching files are present.",
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
} else if (rule.source === "project" && rule.blocking) {
|
|
381
|
+
errors.push({
|
|
382
|
+
code: "CHECK_INERT",
|
|
383
|
+
ruleId: rule.id,
|
|
384
|
+
why: `Configured blocking check ${rule.id} has no effective target scope.`,
|
|
385
|
+
fix: "Provide an applicable target scope or mark rule non-blocking.",
|
|
386
|
+
});
|
|
387
|
+
evaluatedRules.push({
|
|
388
|
+
ruleId: rule.id,
|
|
389
|
+
rule,
|
|
390
|
+
status: "INERT",
|
|
391
|
+
isInert: true,
|
|
392
|
+
errorCode: "CHECK_INERT",
|
|
393
|
+
why: `Configured blocking check ${rule.id} has no effective target scope.`,
|
|
394
|
+
fix: "Provide an applicable target scope or mark rule non-blocking.",
|
|
395
|
+
});
|
|
396
|
+
continue;
|
|
397
|
+
} else {
|
|
398
|
+
evaluatedRules.push({
|
|
399
|
+
ruleId: rule.id,
|
|
400
|
+
rule,
|
|
401
|
+
status: "INERT",
|
|
402
|
+
isInert: true,
|
|
403
|
+
why: "Check is enabled but has no effective target.",
|
|
404
|
+
fix: "Configure an applicable scope or mark the rule as unsupported.",
|
|
405
|
+
});
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Verify mutation for blocking rules
|
|
411
|
+
let mutationProof = null;
|
|
412
|
+
if (rule.blocking) {
|
|
413
|
+
mutationProof = await verifyRuleMutation({ target, rule, adapter });
|
|
414
|
+
if (mutationProof.status !== "PROVEN") {
|
|
415
|
+
if (mutationProof.errorCode === "CHECK_MUTATION_EXECUTION_ERROR") {
|
|
416
|
+
errors.push({
|
|
417
|
+
code: "CHECK_MUTATION_EXECUTION_ERROR",
|
|
418
|
+
ruleId: rule.id,
|
|
419
|
+
why: mutationProof.why,
|
|
420
|
+
fix: mutationProof.fix,
|
|
421
|
+
});
|
|
422
|
+
} else {
|
|
423
|
+
errors.push({
|
|
424
|
+
code: "CHECK_MUTATION_NOT_DETECTED",
|
|
425
|
+
ruleId: rule.id,
|
|
426
|
+
why: mutationProof.why,
|
|
427
|
+
fix: mutationProof.fix,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (checkResult.violations && checkResult.violations.length > 0) {
|
|
434
|
+
rawViolations.push(...checkResult.violations);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
evaluatedRules.push({
|
|
438
|
+
ruleId: rule.id,
|
|
439
|
+
rule,
|
|
440
|
+
status: rule.blocking
|
|
441
|
+
? mutationProof?.status === "PROVEN" ? "PROVEN" : "UNPROVEN"
|
|
442
|
+
: "ACTIVE",
|
|
443
|
+
mutationProof,
|
|
444
|
+
violations: checkResult.violations ?? [],
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Baseline evaluation
|
|
449
|
+
const baselineEval = evaluateBaselineViolations(baseline, rawViolations, { now });
|
|
450
|
+
warnings.push(...baselineEval.warnings);
|
|
451
|
+
|
|
452
|
+
for (const newViolation of baselineEval.newViolations) {
|
|
453
|
+
const matchingRule = rules.find((r) => r.id === newViolation.ruleId);
|
|
454
|
+
if (matchingRule?.blocking) {
|
|
455
|
+
errors.push({
|
|
456
|
+
code: "NEW_VIOLATION",
|
|
457
|
+
ruleId: newViolation.ruleId,
|
|
458
|
+
file: newViolation.file,
|
|
459
|
+
line: newViolation.line,
|
|
460
|
+
fingerprint: newViolation.fingerprint,
|
|
461
|
+
why: `New policy violation detected not present in baseline: ${newViolation.message}`,
|
|
462
|
+
fix: matchingRule.fix || "Resolve the violation before completing the task.",
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
warnings.push({
|
|
466
|
+
code: "NEW_ADVISORY_VIOLATION",
|
|
467
|
+
ruleId: newViolation.ruleId,
|
|
468
|
+
file: newViolation.file,
|
|
469
|
+
message: newViolation.message,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Drift evaluation against task policy snapshot
|
|
475
|
+
let drift = null;
|
|
476
|
+
if (taskId) {
|
|
477
|
+
const taskSnapshot = await readTaskPolicySnapshot(target, taskId, packageRoot);
|
|
478
|
+
if (taskSnapshot) {
|
|
479
|
+
const currentLock = computePolicyLockData(rules, baseline);
|
|
480
|
+
if (taskSnapshot.policyDigest !== currentLock.digest) {
|
|
481
|
+
const policyDiff = diffPolicies(
|
|
482
|
+
{ rules: taskSnapshot.rules, baseline: taskSnapshot.baseline, baselineDigest: taskSnapshot.baselineDigest },
|
|
483
|
+
{ rules, baseline, baselineDigest: currentLock.baselineDigest },
|
|
484
|
+
);
|
|
485
|
+
drift = {
|
|
486
|
+
detected: true,
|
|
487
|
+
classification: policyDiff.classification,
|
|
488
|
+
snapshotDigest: taskSnapshot.policyDigest,
|
|
489
|
+
currentDigest: currentLock.digest,
|
|
490
|
+
changes: policyDiff.changes,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
if (policyDiff.classification === "WEAKEN") {
|
|
494
|
+
errors.push({
|
|
495
|
+
code: "POLICY_WEAKENING",
|
|
496
|
+
why: "Policy weakening detected relative to task activation snapshot.",
|
|
497
|
+
fix: "Restore the original policy rules or obtain explicit project authority.",
|
|
498
|
+
});
|
|
499
|
+
} else if (policyDiff.classification === "UNKNOWN") {
|
|
500
|
+
errors.push({
|
|
501
|
+
code: "POLICY_DRIFT_UNKNOWN",
|
|
502
|
+
why: "Policy drift detected against task snapshot but baseline state cannot be semantically compared.",
|
|
503
|
+
fix: "Re-verify the task under the current policy state.",
|
|
504
|
+
});
|
|
505
|
+
} else if (policyDiff.classification === "TIGHTEN") {
|
|
506
|
+
warnings.push({
|
|
507
|
+
code: "POLICY_TIGHTENING",
|
|
508
|
+
why: "Policy tightened after task activation. Re-verification required.",
|
|
509
|
+
fix: "Re-run verification checks under the tightened policy.",
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const currentLock = computePolicyLockData(rules, baseline);
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
status: errors.length === 0 ? "VALID" : "INVALID",
|
|
520
|
+
rules: evaluatedRules,
|
|
521
|
+
provenRules: evaluatedRules.filter((r) => r.status === "PROVEN").length,
|
|
522
|
+
inertRules: evaluatedRules.filter((r) => r.status === "INERT").length,
|
|
523
|
+
unsupportedRules: evaluatedRules.filter((r) => r.status === "UNSUPPORTED").length,
|
|
524
|
+
baselineViolations: baselineEval.baselinedViolations.length,
|
|
525
|
+
newViolations: baselineEval.newViolations,
|
|
526
|
+
resolvedViolations: baselineEval.resolvedViolations,
|
|
527
|
+
ratchetedBaseline: baselineEval.ratchetedBaseline,
|
|
528
|
+
lock: currentLock,
|
|
529
|
+
drift: drift ?? { detected: false },
|
|
530
|
+
errors,
|
|
531
|
+
warnings,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { sha256 } from "./manifest.js";
|
|
2
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
3
|
+
import { getPolicyAdapter } from "./policy-adapters.js";
|
|
4
|
+
|
|
5
|
+
// Synthetic secret-shaped values are assembled at runtime so committed source
|
|
6
|
+
// contains no paste-ready secret literals; mutation fixtures still receive the
|
|
7
|
+
// full value through content overrides.
|
|
8
|
+
const FAKE_AWS_ACCESS_KEY = ["AKIA", "1234567890", "ABCDEF"].join("");
|
|
9
|
+
|
|
10
|
+
const MUTATION_FIXTURES = Object.freeze({
|
|
11
|
+
["secret-detection"]: {
|
|
12
|
+
mutationName: "hardcoded-secret",
|
|
13
|
+
files: ["src/config/credentials.js"],
|
|
14
|
+
contentOverrides: {
|
|
15
|
+
"src/config/credentials.js": `const awsKey = '${FAKE_AWS_ACCESS_KEY}';\nmodule.exports = { awsKey };\n`,
|
|
16
|
+
},
|
|
17
|
+
expected: "FAIL",
|
|
18
|
+
},
|
|
19
|
+
"grain-complexity": {
|
|
20
|
+
mutationName: "deep-nesting",
|
|
21
|
+
files: ["src/utils/complex.js"],
|
|
22
|
+
contentOverrides: {
|
|
23
|
+
"src/utils/complex.js": "function deep() {\n" + " if (true) {\n".repeat(20) + " console.log('deep');\n" + " }\n".repeat(20) + "}\n",
|
|
24
|
+
},
|
|
25
|
+
expected: "FAIL",
|
|
26
|
+
},
|
|
27
|
+
"architecture-layers": {
|
|
28
|
+
mutationName: "layer-inversion",
|
|
29
|
+
files: ["src/domain/user.js"],
|
|
30
|
+
contentOverrides: {
|
|
31
|
+
"src/domain/user.js": "import { db } from '../infrastructure/db.js';\nexport function getUser() { return db.query(); }\n",
|
|
32
|
+
},
|
|
33
|
+
expected: "FAIL",
|
|
34
|
+
},
|
|
35
|
+
"repo-structure": {
|
|
36
|
+
mutationName: "missing-manifest",
|
|
37
|
+
files: [],
|
|
38
|
+
contentOverrides: {},
|
|
39
|
+
expected: "FAIL",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export function getMutationFixture(adapterId) {
|
|
44
|
+
return MUTATION_FIXTURES[adapterId] ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function verifyRuleMutation({
|
|
48
|
+
target = process.cwd(),
|
|
49
|
+
rule,
|
|
50
|
+
adapter = null,
|
|
51
|
+
fixture = null,
|
|
52
|
+
overrideChecker = null,
|
|
53
|
+
} = {}) {
|
|
54
|
+
const adapterId = rule?.check?.adapter ?? (typeof rule?.check === "string" ? rule.check : null);
|
|
55
|
+
const activeAdapter = overrideChecker ?? adapter ?? getPolicyAdapter(adapterId);
|
|
56
|
+
|
|
57
|
+
if (!activeAdapter) {
|
|
58
|
+
return {
|
|
59
|
+
ruleId: rule?.id ?? "UNKNOWN",
|
|
60
|
+
status: "UNSUPPORTED",
|
|
61
|
+
why: `No policy adapter found for ${adapterId}`,
|
|
62
|
+
fix: "Configure an existing adapter or provide a custom check handler.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const activeFixture = fixture ?? getMutationFixture(adapterId);
|
|
67
|
+
if (!activeFixture) {
|
|
68
|
+
return {
|
|
69
|
+
ruleId: rule?.id ?? "UNKNOWN",
|
|
70
|
+
status: "UNSUPPORTED",
|
|
71
|
+
observed: "UNKNOWN",
|
|
72
|
+
proofDigest: null,
|
|
73
|
+
why: `No mutation fixture available for adapter ${adapterId}`,
|
|
74
|
+
fix: "Provide a mutation fixture to verify this rule.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Execute check on mutation fixture (in-memory content overrides, non-mutating)
|
|
79
|
+
let checkResult;
|
|
80
|
+
let executionError = null;
|
|
81
|
+
try {
|
|
82
|
+
checkResult = await activeAdapter.check({
|
|
83
|
+
target,
|
|
84
|
+
rule,
|
|
85
|
+
files: activeFixture.files,
|
|
86
|
+
contentOverrides: activeFixture.contentOverrides,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
executionError = error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (executionError) {
|
|
93
|
+
return {
|
|
94
|
+
ruleId: rule?.id ?? "UNKNOWN",
|
|
95
|
+
mutation: activeFixture.mutationName,
|
|
96
|
+
expected: activeFixture.expected,
|
|
97
|
+
observed: "ERROR",
|
|
98
|
+
status: "UNPROVEN",
|
|
99
|
+
errorCode: "CHECK_MUTATION_EXECUTION_ERROR",
|
|
100
|
+
proofDigest: null,
|
|
101
|
+
why: `The policy checker failed while evaluating its mutation fixture: ${executionError.message}`,
|
|
102
|
+
fix: "Repair checker execution path and rerun rule verification.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const observed = checkResult?.passed ? "PASS" : "FAIL";
|
|
107
|
+
const expected = activeFixture.expected;
|
|
108
|
+
const detected = observed === expected;
|
|
109
|
+
|
|
110
|
+
const proofDigest = sha256(
|
|
111
|
+
`${canonicalFingerprint(rule)}:${canonicalFingerprint(activeFixture)}:${expected}:${observed}`,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (detected) {
|
|
115
|
+
return {
|
|
116
|
+
ruleId: rule.id,
|
|
117
|
+
mutation: activeFixture.mutationName,
|
|
118
|
+
expected,
|
|
119
|
+
observed,
|
|
120
|
+
status: "PROVEN",
|
|
121
|
+
proofDigest: `sha256:${proofDigest}`,
|
|
122
|
+
scannedFiles: checkResult?.scannedFiles ?? 1,
|
|
123
|
+
why: "Mutation fixture was successfully detected by checker.",
|
|
124
|
+
fix: "Rule is proven and active.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
ruleId: rule.id,
|
|
130
|
+
mutation: activeFixture.mutationName,
|
|
131
|
+
expected,
|
|
132
|
+
observed,
|
|
133
|
+
status: "UNPROVEN",
|
|
134
|
+
errorCode: "CHECK_MUTATION_NOT_DETECTED",
|
|
135
|
+
proofDigest: null,
|
|
136
|
+
why: `Checker failed to detect mutation fixture: expected ${expected} but observed ${observed}.`,
|
|
137
|
+
fix: "Inspect checker logic to ensure it catches invalid states.",
|
|
138
|
+
};
|
|
139
|
+
}
|