pumuki 6.3.26 → 6.3.28
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/README.md +3 -1
- package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
- package/bin/pumuki-mcp-evidence-stdio.js +5 -0
- package/core/gate/conditionMatches.ts +1 -21
- package/core/gate/evaluateGate.js +5 -0
- package/core/gate/evaluateRules.js +5 -0
- package/core/gate/evaluateRules.ts +1 -24
- package/core/gate/scopeMatcher.ts +84 -0
- package/docs/EXECUTION_BOARD.md +749 -376
- package/docs/MCP_SERVERS.md +41 -2
- package/docs/README.md +6 -2
- package/docs/REFRACTOR_PROGRESS.md +374 -6
- package/docs/validation/README.md +11 -1
- package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
- package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
- package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
- package/integrations/config/skillsCompliance.ts +212 -0
- package/integrations/evidence/integrity.ts +352 -0
- package/integrations/evidence/rulesCoverage.ts +94 -0
- package/integrations/evidence/schema.test.ts +16 -0
- package/integrations/evidence/schema.ts +41 -0
- package/integrations/evidence/writeEvidence.test.ts +68 -0
- package/integrations/evidence/writeEvidence.ts +23 -2
- package/integrations/gate/evaluateAiGate.ts +382 -15
- package/integrations/gate/stagePolicies.ts +70 -15
- package/integrations/gate/waivers.ts +209 -0
- package/integrations/git/findingTraceability.ts +3 -23
- package/integrations/git/index.js +5 -0
- package/integrations/git/runCliCommand.ts +16 -0
- package/integrations/git/runPlatformGate.ts +53 -1
- package/integrations/git/runPlatformGateEvaluation.ts +13 -0
- package/integrations/git/stageRunners.ts +168 -5
- package/integrations/lifecycle/adapter.templates.json +72 -5
- package/integrations/lifecycle/adapter.ts +78 -4
- package/integrations/lifecycle/cli.ts +384 -14
- package/integrations/lifecycle/doctor.ts +534 -0
- package/integrations/lifecycle/hookBlock.ts +2 -1
- package/integrations/lifecycle/index.js +5 -0
- package/integrations/lifecycle/install.ts +115 -3
- package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
- package/integrations/lifecycle/preWriteAutomation.ts +142 -0
- package/integrations/mcp/aiGateCheck.ts +6 -0
- package/integrations/mcp/aiGateReceipt.ts +188 -0
- package/integrations/mcp/enterpriseServer.ts +14 -1
- package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
- package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
- package/integrations/mcp/index.js +5 -0
- package/integrations/sdd/index.js +5 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/policy.ts +191 -2
- package/integrations/sdd/sessionStore.ts +139 -19
- package/integrations/sdd/syncDocs.ts +180 -0
- package/integrations/sdd/types.ts +4 -1
- package/integrations/telemetry/structuredTelemetry.ts +197 -0
- package/package.json +27 -8
- package/scripts/build-p9-validation-manifests.ts +53 -0
- package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
- package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
- package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
- package/scripts/check-p9-ruralgo-install-health.ts +288 -0
- package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
- package/scripts/check-package-manifest.ts +49 -0
- package/scripts/check-tracking-single-active.sh +40 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
- package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
- package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
- package/scripts/manage-library.sh +1 -1
- package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
- package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
- package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
- package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
- package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
- package/scripts/p9-validation-manifests-lib.ts +366 -0
- package/scripts/package-manifest-lib.ts +9 -0
- package/skills.lock.json +1 -1
|
@@ -3,8 +3,26 @@ import { readEvidenceResult } from '../evidence/readEvidence';
|
|
|
3
3
|
import { captureRepoState } from '../evidence/repoState';
|
|
4
4
|
import type { RepoState } from '../evidence/schema';
|
|
5
5
|
import { resolvePolicyForStage } from './stagePolicies';
|
|
6
|
-
import {
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
7
9
|
import type { SkillsStage } from '../config/skillsLock';
|
|
10
|
+
import type { ResolvedStagePolicy } from './stagePolicies';
|
|
11
|
+
import {
|
|
12
|
+
readMcpAiGateReceipt,
|
|
13
|
+
resolveMcpAiGateReceiptPath,
|
|
14
|
+
type McpAiGateReceiptReadResult,
|
|
15
|
+
} from '../mcp/aiGateReceipt';
|
|
16
|
+
import {
|
|
17
|
+
resolveEvidenceSigningConfig,
|
|
18
|
+
verifyEvidenceIntegrity,
|
|
19
|
+
type EvidenceIntegrityVerification,
|
|
20
|
+
} from '../evidence/integrity';
|
|
21
|
+
import {
|
|
22
|
+
applyAiGateWaivers,
|
|
23
|
+
type AppliedAiGateWaiver,
|
|
24
|
+
} from './waivers';
|
|
25
|
+
import { emitStructuredTelemetry } from '../telemetry/structuredTelemetry';
|
|
8
26
|
|
|
9
27
|
export type AiGateStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
10
28
|
|
|
@@ -23,16 +41,39 @@ export type AiGateCheckResult = {
|
|
|
23
41
|
resolved_stage: SkillsStage;
|
|
24
42
|
block_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
|
|
25
43
|
warn_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
|
|
26
|
-
trace:
|
|
27
|
-
source: 'default' | 'skills.policy' | 'hard-mode';
|
|
28
|
-
bundle: string;
|
|
29
|
-
hash: string;
|
|
30
|
-
};
|
|
44
|
+
trace: ResolvedStagePolicy['trace'];
|
|
31
45
|
};
|
|
32
46
|
evidence: {
|
|
33
47
|
kind: EvidenceReadResult['kind'];
|
|
34
48
|
max_age_seconds: number;
|
|
35
49
|
age_seconds: number | null;
|
|
50
|
+
source: 'local_file_ai_evidence';
|
|
51
|
+
path: string;
|
|
52
|
+
digest: string | null;
|
|
53
|
+
generated_at: string | null;
|
|
54
|
+
integrity: {
|
|
55
|
+
status: EvidenceIntegrityVerification['status'];
|
|
56
|
+
code: string | null;
|
|
57
|
+
payload_hash: string | null;
|
|
58
|
+
previous_chain_hash: string | null;
|
|
59
|
+
chain_hash: string | null;
|
|
60
|
+
signature_present: boolean;
|
|
61
|
+
signature_key_id: string | null;
|
|
62
|
+
signature_verified: boolean | null;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
mcp_receipt: {
|
|
66
|
+
required: boolean;
|
|
67
|
+
kind: 'disabled' | McpAiGateReceiptReadResult['kind'];
|
|
68
|
+
path: string;
|
|
69
|
+
max_age_seconds: number | null;
|
|
70
|
+
age_seconds: number | null;
|
|
71
|
+
};
|
|
72
|
+
waivers: {
|
|
73
|
+
path: string;
|
|
74
|
+
status: 'none' | 'applied' | 'invalid';
|
|
75
|
+
invalid_reason: string | null;
|
|
76
|
+
applied: AppliedAiGateWaiver[];
|
|
36
77
|
};
|
|
37
78
|
repo_state: RepoState;
|
|
38
79
|
violations: AiGateViolation[];
|
|
@@ -40,16 +81,22 @@ export type AiGateCheckResult = {
|
|
|
40
81
|
|
|
41
82
|
type AiGateDependencies = {
|
|
42
83
|
now: () => number;
|
|
84
|
+
env: NodeJS.ProcessEnv;
|
|
43
85
|
readEvidenceResult: (repoRoot: string) => EvidenceReadResult;
|
|
86
|
+
readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult;
|
|
44
87
|
captureRepoState: (repoRoot: string) => RepoState;
|
|
45
88
|
resolvePolicyForStage: (stage: SkillsStage, repoRoot: string) => ReturnType<typeof resolvePolicyForStage>;
|
|
89
|
+
emitStructuredTelemetry: typeof emitStructuredTelemetry;
|
|
46
90
|
};
|
|
47
91
|
|
|
48
92
|
const defaultDependencies: AiGateDependencies = {
|
|
49
93
|
now: () => Date.now(),
|
|
94
|
+
env: process.env,
|
|
50
95
|
readEvidenceResult,
|
|
96
|
+
readMcpAiGateReceipt,
|
|
51
97
|
captureRepoState,
|
|
52
98
|
resolvePolicyForStage,
|
|
99
|
+
emitStructuredTelemetry,
|
|
53
100
|
};
|
|
54
101
|
|
|
55
102
|
const DEFAULT_MAX_AGE_SECONDS: Readonly<Record<AiGateStage, number>> = {
|
|
@@ -203,20 +250,49 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
203
250
|
return violations;
|
|
204
251
|
};
|
|
205
252
|
|
|
253
|
+
const toIntegritySummary = (verification: EvidenceIntegrityVerification): {
|
|
254
|
+
status: EvidenceIntegrityVerification['status'];
|
|
255
|
+
code: string | null;
|
|
256
|
+
payload_hash: string | null;
|
|
257
|
+
previous_chain_hash: string | null;
|
|
258
|
+
chain_hash: string | null;
|
|
259
|
+
signature_present: boolean;
|
|
260
|
+
signature_key_id: string | null;
|
|
261
|
+
signature_verified: boolean | null;
|
|
262
|
+
} => ({
|
|
263
|
+
status: verification.status,
|
|
264
|
+
code: verification.code,
|
|
265
|
+
payload_hash: verification.payloadHash,
|
|
266
|
+
previous_chain_hash: verification.previousChainHash,
|
|
267
|
+
chain_hash: verification.chainHash,
|
|
268
|
+
signature_present: verification.signature.present,
|
|
269
|
+
signature_key_id: verification.signature.keyId,
|
|
270
|
+
signature_verified: verification.signature.verified,
|
|
271
|
+
});
|
|
272
|
+
|
|
206
273
|
const collectEvidenceViolations = (
|
|
207
274
|
result: EvidenceReadResult,
|
|
208
275
|
repoRoot: string,
|
|
209
276
|
repoState: RepoState,
|
|
210
277
|
stage: AiGateStage,
|
|
211
278
|
nowMs: number,
|
|
212
|
-
maxAgeSecondsByStage: Readonly<Record<AiGateStage, number
|
|
213
|
-
|
|
279
|
+
maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>,
|
|
280
|
+
verification: EvidenceIntegrityVerification
|
|
281
|
+
): {
|
|
282
|
+
violations: AiGateViolation[];
|
|
283
|
+
ageSeconds: number | null;
|
|
284
|
+
integrity: ReturnType<typeof toIntegritySummary>;
|
|
285
|
+
} => {
|
|
214
286
|
const violations: AiGateViolation[] = [];
|
|
215
287
|
const maxAgeSeconds = maxAgeSecondsByStage[stage];
|
|
216
288
|
|
|
217
289
|
if (result.kind === 'missing') {
|
|
218
290
|
violations.push(toErrorViolation('EVIDENCE_MISSING', '.ai_evidence.json is missing.'));
|
|
219
|
-
return {
|
|
291
|
+
return {
|
|
292
|
+
violations,
|
|
293
|
+
ageSeconds: null,
|
|
294
|
+
integrity: toIntegritySummary(verification),
|
|
295
|
+
};
|
|
220
296
|
}
|
|
221
297
|
|
|
222
298
|
if (result.kind === 'invalid') {
|
|
@@ -226,13 +302,30 @@ const collectEvidenceViolations = (
|
|
|
226
302
|
`.ai_evidence.json is invalid${result.version ? ` (version=${result.version})` : ''}.`
|
|
227
303
|
)
|
|
228
304
|
);
|
|
229
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
violations,
|
|
307
|
+
ageSeconds: null,
|
|
308
|
+
integrity: toIntegritySummary(verification),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!verification.ok) {
|
|
313
|
+
violations.push(
|
|
314
|
+
toErrorViolation(
|
|
315
|
+
verification.code ?? 'EVIDENCE_INTEGRITY_INVALID',
|
|
316
|
+
verification.message ?? 'Evidence integrity verification failed.'
|
|
317
|
+
)
|
|
318
|
+
);
|
|
230
319
|
}
|
|
231
320
|
|
|
232
321
|
const ageSeconds = toTimestampAgeSeconds(result.evidence.timestamp, nowMs);
|
|
233
322
|
if (ageSeconds === null) {
|
|
234
323
|
violations.push(toErrorViolation('EVIDENCE_TIMESTAMP_INVALID', 'Evidence timestamp is invalid.'));
|
|
235
|
-
return {
|
|
324
|
+
return {
|
|
325
|
+
violations,
|
|
326
|
+
ageSeconds: null,
|
|
327
|
+
integrity: toIntegritySummary(verification),
|
|
328
|
+
};
|
|
236
329
|
}
|
|
237
330
|
|
|
238
331
|
if (ageSeconds > maxAgeSeconds) {
|
|
@@ -259,7 +352,40 @@ const collectEvidenceViolations = (
|
|
|
259
352
|
);
|
|
260
353
|
}
|
|
261
354
|
|
|
262
|
-
return {
|
|
355
|
+
return {
|
|
356
|
+
violations,
|
|
357
|
+
ageSeconds,
|
|
358
|
+
integrity: toIntegritySummary(verification),
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const resolveEvidenceSourceDetails = (
|
|
363
|
+
repoRoot: string,
|
|
364
|
+
result: EvidenceReadResult
|
|
365
|
+
): {
|
|
366
|
+
source: 'local_file_ai_evidence';
|
|
367
|
+
path: string;
|
|
368
|
+
digest: string | null;
|
|
369
|
+
generated_at: string | null;
|
|
370
|
+
} => {
|
|
371
|
+
const evidencePath = resolve(repoRoot, '.ai_evidence.json');
|
|
372
|
+
let digest: string | null = null;
|
|
373
|
+
|
|
374
|
+
if (existsSync(evidencePath)) {
|
|
375
|
+
try {
|
|
376
|
+
const buffer = readFileSync(evidencePath);
|
|
377
|
+
digest = createHash('sha256').update(buffer).digest('hex');
|
|
378
|
+
} catch {
|
|
379
|
+
digest = null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
source: 'local_file_ai_evidence',
|
|
385
|
+
path: evidencePath,
|
|
386
|
+
digest,
|
|
387
|
+
generated_at: result.kind === 'valid' ? result.evidence.timestamp : null,
|
|
388
|
+
};
|
|
263
389
|
};
|
|
264
390
|
|
|
265
391
|
const collectGitflowViolations = (
|
|
@@ -288,12 +414,126 @@ const toPolicyStage = (stage: AiGateStage): SkillsStage => {
|
|
|
288
414
|
return stage;
|
|
289
415
|
};
|
|
290
416
|
|
|
417
|
+
const collectMcpReceiptViolations = (params: {
|
|
418
|
+
required: boolean;
|
|
419
|
+
stage: AiGateStage;
|
|
420
|
+
repoRoot: string;
|
|
421
|
+
nowMs: number;
|
|
422
|
+
maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>;
|
|
423
|
+
readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult;
|
|
424
|
+
}): {
|
|
425
|
+
required: boolean;
|
|
426
|
+
kind: 'disabled' | McpAiGateReceiptReadResult['kind'];
|
|
427
|
+
path: string;
|
|
428
|
+
maxAgeSeconds: number | null;
|
|
429
|
+
ageSeconds: number | null;
|
|
430
|
+
violations: AiGateViolation[];
|
|
431
|
+
} => {
|
|
432
|
+
const path = resolveMcpAiGateReceiptPath(params.repoRoot);
|
|
433
|
+
if (!params.required || params.stage !== 'PRE_WRITE') {
|
|
434
|
+
return {
|
|
435
|
+
required: params.required,
|
|
436
|
+
kind: 'disabled',
|
|
437
|
+
path,
|
|
438
|
+
maxAgeSeconds: null,
|
|
439
|
+
ageSeconds: null,
|
|
440
|
+
violations: [],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const maxAgeSeconds = params.maxAgeSecondsByStage[params.stage];
|
|
445
|
+
const receiptRead = params.readMcpAiGateReceipt(params.repoRoot);
|
|
446
|
+
if (receiptRead.kind === 'missing') {
|
|
447
|
+
return {
|
|
448
|
+
required: true,
|
|
449
|
+
kind: 'missing',
|
|
450
|
+
path: receiptRead.path,
|
|
451
|
+
maxAgeSeconds,
|
|
452
|
+
ageSeconds: null,
|
|
453
|
+
violations: [
|
|
454
|
+
toErrorViolation(
|
|
455
|
+
'MCP_ENTERPRISE_RECEIPT_MISSING',
|
|
456
|
+
'MCP receipt is missing. Call tool ai_gate_check in pumuki-enterprise MCP before PRE_WRITE.'
|
|
457
|
+
),
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (receiptRead.kind === 'invalid') {
|
|
463
|
+
return {
|
|
464
|
+
required: true,
|
|
465
|
+
kind: 'invalid',
|
|
466
|
+
path: receiptRead.path,
|
|
467
|
+
maxAgeSeconds,
|
|
468
|
+
ageSeconds: null,
|
|
469
|
+
violations: [
|
|
470
|
+
toErrorViolation(
|
|
471
|
+
'MCP_ENTERPRISE_RECEIPT_INVALID',
|
|
472
|
+
`MCP receipt is invalid: ${receiptRead.reason}`
|
|
473
|
+
),
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const violations: AiGateViolation[] = [];
|
|
479
|
+
if (toCanonicalPath(receiptRead.receipt.repo_root) !== toCanonicalPath(params.repoRoot)) {
|
|
480
|
+
violations.push(
|
|
481
|
+
toErrorViolation(
|
|
482
|
+
'MCP_ENTERPRISE_RECEIPT_REPO_ROOT_MISMATCH',
|
|
483
|
+
`MCP receipt repo root mismatch (${receiptRead.receipt.repo_root} != ${params.repoRoot}).`
|
|
484
|
+
)
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (receiptRead.receipt.stage !== params.stage) {
|
|
488
|
+
violations.push(
|
|
489
|
+
toErrorViolation(
|
|
490
|
+
'MCP_ENTERPRISE_RECEIPT_STAGE_MISMATCH',
|
|
491
|
+
`MCP receipt stage mismatch (${receiptRead.receipt.stage} != ${params.stage}).`
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const ageSeconds = toTimestampAgeSeconds(receiptRead.receipt.issued_at, params.nowMs);
|
|
496
|
+
if (ageSeconds === null) {
|
|
497
|
+
violations.push(
|
|
498
|
+
toErrorViolation(
|
|
499
|
+
'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_INVALID',
|
|
500
|
+
'MCP receipt issued_at timestamp is invalid.'
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
} else if (ageSeconds > maxAgeSeconds) {
|
|
504
|
+
violations.push(
|
|
505
|
+
toErrorViolation(
|
|
506
|
+
'MCP_ENTERPRISE_RECEIPT_STALE',
|
|
507
|
+
`MCP receipt is stale (${ageSeconds}s > ${maxAgeSeconds}s for ${params.stage}).`
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (isTimestampFuture(receiptRead.receipt.issued_at, params.nowMs)) {
|
|
512
|
+
violations.push(
|
|
513
|
+
toErrorViolation(
|
|
514
|
+
'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_FUTURE',
|
|
515
|
+
'MCP receipt timestamp is in the future.'
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
required: true,
|
|
522
|
+
kind: 'valid',
|
|
523
|
+
path: receiptRead.path,
|
|
524
|
+
maxAgeSeconds,
|
|
525
|
+
ageSeconds,
|
|
526
|
+
violations,
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
291
530
|
export const evaluateAiGate = (
|
|
292
531
|
params: {
|
|
293
532
|
repoRoot: string;
|
|
294
533
|
stage: AiGateStage;
|
|
295
534
|
maxAgeSecondsByStage?: Readonly<Record<AiGateStage, number>>;
|
|
296
535
|
protectedBranches?: ReadonlyArray<string>;
|
|
536
|
+
requireMcpReceipt?: boolean;
|
|
297
537
|
},
|
|
298
538
|
dependencies: Partial<AiGateDependencies> = {}
|
|
299
539
|
): AiGateCheckResult => {
|
|
@@ -305,6 +545,34 @@ export const evaluateAiGate = (
|
|
|
305
545
|
const protectedBranches = new Set(params.protectedBranches ?? Array.from(DEFAULT_PROTECTED_BRANCHES));
|
|
306
546
|
const nowMs = activeDependencies.now();
|
|
307
547
|
const evidenceResult = activeDependencies.readEvidenceResult(params.repoRoot);
|
|
548
|
+
const evidenceSourceDetails = resolveEvidenceSourceDetails(
|
|
549
|
+
params.repoRoot,
|
|
550
|
+
evidenceResult
|
|
551
|
+
);
|
|
552
|
+
const evidenceIntegrityVerification: EvidenceIntegrityVerification =
|
|
553
|
+
evidenceResult.kind === 'valid'
|
|
554
|
+
? verifyEvidenceIntegrity(evidenceResult.evidence, {
|
|
555
|
+
signatureConfig: resolveEvidenceSigningConfig(activeDependencies.env),
|
|
556
|
+
enforceSignature: false,
|
|
557
|
+
})
|
|
558
|
+
: {
|
|
559
|
+
ok: false,
|
|
560
|
+
status: evidenceResult.kind === 'missing' ? 'missing' : 'invalid',
|
|
561
|
+
code: evidenceResult.kind === 'missing'
|
|
562
|
+
? 'EVIDENCE_INTEGRITY_MISSING'
|
|
563
|
+
: 'EVIDENCE_INTEGRITY_UNAVAILABLE',
|
|
564
|
+
message: evidenceResult.kind === 'missing'
|
|
565
|
+
? 'Evidence integrity metadata is missing.'
|
|
566
|
+
: 'Evidence integrity cannot be verified from invalid payload.',
|
|
567
|
+
payloadHash: null,
|
|
568
|
+
previousChainHash: null,
|
|
569
|
+
chainHash: null,
|
|
570
|
+
signature: {
|
|
571
|
+
present: false,
|
|
572
|
+
keyId: null,
|
|
573
|
+
verified: null,
|
|
574
|
+
},
|
|
575
|
+
};
|
|
308
576
|
const repoState = activeDependencies.captureRepoState(params.repoRoot);
|
|
309
577
|
const policyStage = toPolicyStage(params.stage);
|
|
310
578
|
const resolvedPolicy = activeDependencies.resolvePolicyForStage(
|
|
@@ -317,13 +585,42 @@ export const evaluateAiGate = (
|
|
|
317
585
|
repoState,
|
|
318
586
|
params.stage,
|
|
319
587
|
nowMs,
|
|
320
|
-
maxAgeSecondsByStage
|
|
588
|
+
maxAgeSecondsByStage,
|
|
589
|
+
evidenceIntegrityVerification
|
|
321
590
|
);
|
|
591
|
+
const mcpReceiptAssessment = collectMcpReceiptViolations({
|
|
592
|
+
required: params.requireMcpReceipt ?? false,
|
|
593
|
+
stage: params.stage,
|
|
594
|
+
repoRoot: params.repoRoot,
|
|
595
|
+
nowMs,
|
|
596
|
+
maxAgeSecondsByStage,
|
|
597
|
+
readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt,
|
|
598
|
+
});
|
|
322
599
|
const gitflowViolations = collectGitflowViolations(repoState, protectedBranches);
|
|
323
|
-
const
|
|
600
|
+
const baseViolations = [
|
|
601
|
+
...evidenceAssessment.violations,
|
|
602
|
+
...gitflowViolations,
|
|
603
|
+
...mcpReceiptAssessment.violations,
|
|
604
|
+
];
|
|
605
|
+
const waiverAssessment = applyAiGateWaivers({
|
|
606
|
+
repoRoot: params.repoRoot,
|
|
607
|
+
stage: params.stage,
|
|
608
|
+
branch: repoState.git.branch,
|
|
609
|
+
violations: baseViolations,
|
|
610
|
+
now: new Date(nowMs),
|
|
611
|
+
});
|
|
612
|
+
const violations = [...waiverAssessment.violations];
|
|
613
|
+
if (waiverAssessment.status === 'invalid') {
|
|
614
|
+
violations.push(
|
|
615
|
+
toErrorViolation(
|
|
616
|
+
'WAIVER_POLICY_INVALID',
|
|
617
|
+
`Waiver file is invalid: ${waiverAssessment.invalid_reason ?? 'unknown_error'}.`
|
|
618
|
+
)
|
|
619
|
+
);
|
|
620
|
+
}
|
|
324
621
|
const blocked = violations.some((violation) => violation.severity === 'ERROR');
|
|
325
622
|
|
|
326
|
-
|
|
623
|
+
const result: AiGateCheckResult = {
|
|
327
624
|
stage: params.stage,
|
|
328
625
|
status: blocked ? 'BLOCKED' : 'ALLOWED',
|
|
329
626
|
allowed: !blocked,
|
|
@@ -338,8 +635,78 @@ export const evaluateAiGate = (
|
|
|
338
635
|
kind: evidenceResult.kind,
|
|
339
636
|
max_age_seconds: maxAgeSecondsByStage[params.stage],
|
|
340
637
|
age_seconds: evidenceAssessment.ageSeconds,
|
|
638
|
+
source: evidenceSourceDetails.source,
|
|
639
|
+
path: evidenceSourceDetails.path,
|
|
640
|
+
digest: evidenceSourceDetails.digest,
|
|
641
|
+
generated_at: evidenceSourceDetails.generated_at,
|
|
642
|
+
integrity: evidenceAssessment.integrity,
|
|
643
|
+
},
|
|
644
|
+
mcp_receipt: {
|
|
645
|
+
required: mcpReceiptAssessment.required,
|
|
646
|
+
kind: mcpReceiptAssessment.kind,
|
|
647
|
+
path: mcpReceiptAssessment.path,
|
|
648
|
+
max_age_seconds: mcpReceiptAssessment.maxAgeSeconds,
|
|
649
|
+
age_seconds: mcpReceiptAssessment.ageSeconds,
|
|
650
|
+
},
|
|
651
|
+
waivers: {
|
|
652
|
+
path: waiverAssessment.path,
|
|
653
|
+
status: waiverAssessment.status,
|
|
654
|
+
invalid_reason: waiverAssessment.invalid_reason,
|
|
655
|
+
applied: [...waiverAssessment.applied],
|
|
341
656
|
},
|
|
342
657
|
repo_state: repoState,
|
|
343
658
|
violations,
|
|
344
659
|
};
|
|
660
|
+
|
|
661
|
+
activeDependencies.emitStructuredTelemetry({
|
|
662
|
+
repoRoot: params.repoRoot,
|
|
663
|
+
env: activeDependencies.env,
|
|
664
|
+
event: {
|
|
665
|
+
schema_version: '1',
|
|
666
|
+
timestamp: new Date(nowMs).toISOString(),
|
|
667
|
+
source: 'pumuki',
|
|
668
|
+
channel: 'ai_gate',
|
|
669
|
+
event: 'ai_gate.evaluated',
|
|
670
|
+
stage: result.stage,
|
|
671
|
+
repo_root: params.repoRoot,
|
|
672
|
+
status: result.status,
|
|
673
|
+
decision: result.allowed ? 'ALLOW' : 'BLOCK',
|
|
674
|
+
policy: {
|
|
675
|
+
source: result.policy.trace.source,
|
|
676
|
+
bundle: result.policy.trace.bundle,
|
|
677
|
+
hash: result.policy.trace.hash,
|
|
678
|
+
version: result.policy.trace.version ?? 'n/a',
|
|
679
|
+
signature: result.policy.trace.signature ?? 'n/a',
|
|
680
|
+
},
|
|
681
|
+
evidence: {
|
|
682
|
+
kind: result.evidence.kind,
|
|
683
|
+
age_seconds: result.evidence.age_seconds,
|
|
684
|
+
max_age_seconds: result.evidence.max_age_seconds,
|
|
685
|
+
source: result.evidence.source,
|
|
686
|
+
path: result.evidence.path,
|
|
687
|
+
digest: result.evidence.digest,
|
|
688
|
+
generated_at: result.evidence.generated_at,
|
|
689
|
+
integrity_status: result.evidence.integrity.status,
|
|
690
|
+
chain_hash: result.evidence.integrity.chain_hash,
|
|
691
|
+
},
|
|
692
|
+
metrics: {
|
|
693
|
+
violations_total: result.violations.length,
|
|
694
|
+
violations_error: result.violations.filter((violation) => violation.severity === 'ERROR').length,
|
|
695
|
+
violations_warn: result.violations.filter((violation) => violation.severity === 'WARN').length,
|
|
696
|
+
},
|
|
697
|
+
mcp_receipt: {
|
|
698
|
+
required: result.mcp_receipt.required,
|
|
699
|
+
kind: result.mcp_receipt.kind,
|
|
700
|
+
age_seconds: result.mcp_receipt.age_seconds,
|
|
701
|
+
max_age_seconds: result.mcp_receipt.max_age_seconds,
|
|
702
|
+
},
|
|
703
|
+
waivers: {
|
|
704
|
+
status: result.waivers.status,
|
|
705
|
+
applied: result.waivers.applied.length,
|
|
706
|
+
path: result.waivers.path,
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
return result;
|
|
345
712
|
};
|
|
@@ -44,6 +44,8 @@ export type ResolvedStagePolicy = {
|
|
|
44
44
|
source: 'default' | 'skills.policy' | 'hard-mode';
|
|
45
45
|
bundle: string;
|
|
46
46
|
hash: string;
|
|
47
|
+
version?: string;
|
|
48
|
+
signature?: string;
|
|
47
49
|
};
|
|
48
50
|
};
|
|
49
51
|
|
|
@@ -181,6 +183,8 @@ const hardModePolicyProfileByStage: Record<
|
|
|
181
183
|
};
|
|
182
184
|
|
|
183
185
|
const HARD_MODE_CONFIG_PATH = '.pumuki/hard-mode.json';
|
|
186
|
+
const POLICY_TRACE_VERSION_DEFAULT = 'policy-as-code/default@1.0';
|
|
187
|
+
const POLICY_TRACE_VERSION_HARD_MODE = 'policy-as-code/hard-mode@1.0';
|
|
184
188
|
|
|
185
189
|
const toHardModeProfileName = (value: unknown): HardModeProfileName | null => {
|
|
186
190
|
if (typeof value !== 'string') {
|
|
@@ -287,6 +291,26 @@ const createPolicyTraceHash = (params: {
|
|
|
287
291
|
.digest('hex');
|
|
288
292
|
};
|
|
289
293
|
|
|
294
|
+
const createPolicyTraceSignature = (params: {
|
|
295
|
+
version: string;
|
|
296
|
+
source: 'default' | 'skills.policy' | 'hard-mode';
|
|
297
|
+
bundle: string;
|
|
298
|
+
hash: string;
|
|
299
|
+
sourcePolicyHash?: string;
|
|
300
|
+
}): string => {
|
|
301
|
+
return createHash('sha256')
|
|
302
|
+
.update(
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
version: params.version,
|
|
305
|
+
source: params.source,
|
|
306
|
+
bundle: params.bundle,
|
|
307
|
+
hash: params.hash,
|
|
308
|
+
sourcePolicyHash: params.sourcePolicyHash ?? null,
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
.digest('hex');
|
|
312
|
+
};
|
|
313
|
+
|
|
290
314
|
export const resolvePolicyForStage = (
|
|
291
315
|
stage: SkillsStage,
|
|
292
316
|
repoRoot: string = process.cwd()
|
|
@@ -301,16 +325,25 @@ export const resolvePolicyForStage = (
|
|
|
301
325
|
const bundle = profileName
|
|
302
326
|
? `gate-policy.hard-mode.${profileName}.${stage}`
|
|
303
327
|
: `gate-policy.hard-mode.${stage}`;
|
|
328
|
+
const hash = createPolicyTraceHash({
|
|
329
|
+
stage,
|
|
330
|
+
source: 'hard-mode',
|
|
331
|
+
blockOnOrAbove: hardModePolicy.blockOnOrAbove,
|
|
332
|
+
warnOnOrAbove: hardModePolicy.warnOnOrAbove,
|
|
333
|
+
sourcePolicyHash: profileName ?? undefined,
|
|
334
|
+
});
|
|
304
335
|
return {
|
|
305
336
|
policy: hardModePolicy,
|
|
306
337
|
trace: {
|
|
307
338
|
source: 'hard-mode',
|
|
308
339
|
bundle,
|
|
309
|
-
hash
|
|
310
|
-
|
|
340
|
+
hash,
|
|
341
|
+
version: POLICY_TRACE_VERSION_HARD_MODE,
|
|
342
|
+
signature: createPolicyTraceSignature({
|
|
343
|
+
version: POLICY_TRACE_VERSION_HARD_MODE,
|
|
311
344
|
source: 'hard-mode',
|
|
312
|
-
|
|
313
|
-
|
|
345
|
+
bundle,
|
|
346
|
+
hash,
|
|
314
347
|
sourcePolicyHash: profileName ?? undefined,
|
|
315
348
|
}),
|
|
316
349
|
},
|
|
@@ -322,16 +355,25 @@ export const resolvePolicyForStage = (
|
|
|
322
355
|
const stageOverride = loadedPolicy?.stages[stage];
|
|
323
356
|
|
|
324
357
|
if (!stageOverride) {
|
|
358
|
+
const hash = createPolicyTraceHash({
|
|
359
|
+
stage,
|
|
360
|
+
source: 'default',
|
|
361
|
+
blockOnOrAbove: defaults.blockOnOrAbove,
|
|
362
|
+
warnOnOrAbove: defaults.warnOnOrAbove,
|
|
363
|
+
});
|
|
364
|
+
const bundle = `gate-policy.default.${stage}`;
|
|
325
365
|
return {
|
|
326
366
|
policy: defaults,
|
|
327
367
|
trace: {
|
|
328
368
|
source: 'default',
|
|
329
|
-
bundle
|
|
330
|
-
hash
|
|
331
|
-
|
|
369
|
+
bundle,
|
|
370
|
+
hash,
|
|
371
|
+
version: POLICY_TRACE_VERSION_DEFAULT,
|
|
372
|
+
signature: createPolicyTraceSignature({
|
|
373
|
+
version: POLICY_TRACE_VERSION_DEFAULT,
|
|
332
374
|
source: 'default',
|
|
333
|
-
|
|
334
|
-
|
|
375
|
+
bundle,
|
|
376
|
+
hash,
|
|
335
377
|
}),
|
|
336
378
|
},
|
|
337
379
|
};
|
|
@@ -343,17 +385,30 @@ export const resolvePolicyForStage = (
|
|
|
343
385
|
warnOnOrAbove: stageOverride.warnOnOrAbove,
|
|
344
386
|
};
|
|
345
387
|
|
|
388
|
+
const sourcePolicyHash = createSkillsPolicyDeterministicHash(loadedPolicy);
|
|
389
|
+
const bundle = `gate-policy.skills.policy.${stage}`;
|
|
390
|
+
const version = `policy-as-code/skills.policy@${loadedPolicy.version}`;
|
|
391
|
+
const hash = createPolicyTraceHash({
|
|
392
|
+
stage,
|
|
393
|
+
source: 'skills.policy',
|
|
394
|
+
blockOnOrAbove: resolvedPolicy.blockOnOrAbove,
|
|
395
|
+
warnOnOrAbove: resolvedPolicy.warnOnOrAbove,
|
|
396
|
+
sourcePolicyHash,
|
|
397
|
+
});
|
|
398
|
+
|
|
346
399
|
return {
|
|
347
400
|
policy: resolvedPolicy,
|
|
348
401
|
trace: {
|
|
349
402
|
source: 'skills.policy',
|
|
350
|
-
bundle
|
|
351
|
-
hash
|
|
352
|
-
|
|
403
|
+
bundle,
|
|
404
|
+
hash,
|
|
405
|
+
version,
|
|
406
|
+
signature: createPolicyTraceSignature({
|
|
407
|
+
version,
|
|
353
408
|
source: 'skills.policy',
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
sourcePolicyHash
|
|
409
|
+
bundle,
|
|
410
|
+
hash,
|
|
411
|
+
sourcePolicyHash,
|
|
357
412
|
}),
|
|
358
413
|
},
|
|
359
414
|
};
|