mindforge-cc 11.0.0 → 11.2.1

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.
Files changed (70) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/config.json +14 -4
  3. package/CHANGELOG.md +137 -0
  4. package/MINDFORGE.md +5 -5
  5. package/RELEASENOTES.md +1 -1
  6. package/bin/autonomous/audit-writer.js +108 -86
  7. package/bin/autonomous/auto-runner.js +304 -19
  8. package/bin/autonomous/dependency-dag.js +59 -0
  9. package/bin/autonomous/mesh-self-healer.js +101 -28
  10. package/bin/autonomous/wave-executor.js +20 -1
  11. package/bin/browser/regression-writer.js +45 -3
  12. package/bin/browser/session-manager.js +21 -17
  13. package/bin/council-cli.js +161 -0
  14. package/bin/dashboard/approval-handler.js +3 -1
  15. package/bin/dashboard/server.js +1 -1
  16. package/bin/dashboard/sse-bridge.js +9 -12
  17. package/bin/engine/council-runtime.js +124 -0
  18. package/bin/engine/logic-drift-detector.js +14 -6
  19. package/bin/engine/logic-validator.js +155 -25
  20. package/bin/engine/orbital-guardian.js +56 -10
  21. package/bin/engine/otel-exporter.js +123 -0
  22. package/bin/engine/reason-source-aligner.js +19 -6
  23. package/bin/engine/remediation-engine.js +1 -1
  24. package/bin/engine/self-corrective-synthesizer.js +1 -1
  25. package/bin/engine/sre-manager.js +33 -6
  26. package/bin/engine/temporal-cli.js +4 -2
  27. package/bin/engine/verification-runner.js +131 -0
  28. package/bin/engine/verify-cli.js +34 -0
  29. package/bin/eval/eval-harness.js +82 -0
  30. package/bin/eval/golden-set-retrieval.json +46 -0
  31. package/bin/governance/audit-hash.js +12 -0
  32. package/bin/governance/audit-verifier.js +60 -0
  33. package/bin/governance/policy-engine.js +17 -4
  34. package/bin/governance/quantum-crypto.js +63 -9
  35. package/bin/governance/ztai-archiver.js +74 -9
  36. package/bin/governance/ztai-manager.js +33 -5
  37. package/bin/hindsight-injector.js +5 -6
  38. package/bin/hooks/instinct-capture-hook.js +186 -0
  39. package/bin/installer-core.js +31 -2
  40. package/bin/memory/auto-shadow.js +32 -3
  41. package/bin/memory/eis-client.js +45 -4
  42. package/bin/memory/identity-synthesizer.js +2 -2
  43. package/bin/memory/knowledge-store.js +30 -6
  44. package/bin/memory/retrieval-fusion.js +58 -0
  45. package/bin/memory/semantic-hub.js +2 -2
  46. package/bin/memory/vector-hub.js +143 -6
  47. package/bin/mindforge-cli.js +4 -5
  48. package/bin/models/anthropic-provider.js +13 -4
  49. package/bin/models/cost-tracker.js +3 -1
  50. package/bin/models/difficulty-scorer.js +54 -0
  51. package/bin/models/gemini-provider.js +6 -2
  52. package/bin/models/model-router.js +31 -18
  53. package/bin/models/openai-provider.js +6 -3
  54. package/bin/models/pricing-registry.js +128 -0
  55. package/bin/review/ads-engine.js +1 -1
  56. package/bin/review/finding-synthesizer.js +35 -6
  57. package/bin/security/trust-boundaries.js +194 -0
  58. package/bin/security/trust-gate-hook.js +49 -0
  59. package/bin/skill-registry.js +34 -22
  60. package/bin/skills-builder/marketplace-cli.js +5 -3
  61. package/bin/skills-builder/skill-registrar.js +4 -6
  62. package/bin/sre/sentinel.js +7 -5
  63. package/bin/sre/shadow-mirror.js +90 -40
  64. package/bin/utils/append-queue.js +67 -0
  65. package/bin/utils/file-io.js +29 -80
  66. package/bin/utils/version-check.js +75 -0
  67. package/bin/verify-audit.js +12 -0
  68. package/bin/wizard/theme.js +1 -2
  69. package/package.json +1 -1
  70. package/bin/dashboard/team-tracker.js +0 -0
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const MAX_OUTPUT_LENGTH = 2000;
8
+
9
+ /**
10
+ * Stage definitions — each maps a stage name to its command and optional skip condition.
11
+ * The tests stage guards against recursion: if NODE_ENV=test (set by run-all.js) or
12
+ * MINDFORGE_VERIFICATION_ACTIVE=1 (set by this runner), we skip to prevent infinite nesting.
13
+ */
14
+ const STAGE_DEFS = {
15
+ tests: {
16
+ command: 'node tests/run-all.js',
17
+ skipIf: () =>
18
+ process.env.MINDFORGE_VERIFICATION_ACTIVE === '1' ||
19
+ process.env.NODE_ENV === 'test',
20
+ },
21
+ lint: {
22
+ command: 'npx eslint . --max-warnings=0',
23
+ skipIf: null,
24
+ },
25
+ audit: {
26
+ command: 'node bin/verify-audit.js',
27
+ skipIf: null,
28
+ },
29
+ typecheck: {
30
+ command: 'npx tsc --noEmit',
31
+ skipIf: (cwd) => !fs.existsSync(path.join(cwd, 'tsconfig.json')),
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Run a single stage, returning a structured result object.
37
+ */
38
+ function executeStage(name, cwd) {
39
+ const def = STAGE_DEFS[name];
40
+ if (!def) {
41
+ return { name, status: 'skip', durationMs: 0, output: `Unknown stage: ${name}` };
42
+ }
43
+
44
+ // Check skip condition
45
+ if (def.skipIf && def.skipIf(cwd)) {
46
+ return { name, status: 'skip', durationMs: 0, output: '' };
47
+ }
48
+
49
+ const start = Date.now();
50
+ let output = '';
51
+ let status = 'pass';
52
+
53
+ try {
54
+ const env = Object.assign({}, process.env, {
55
+ MINDFORGE_VERIFICATION_ACTIVE: '1',
56
+ });
57
+ const result = execSync(def.command, {
58
+ cwd,
59
+ encoding: 'utf8',
60
+ stdio: ['pipe', 'pipe', 'pipe'],
61
+ timeout: 120000,
62
+ env,
63
+ });
64
+ output = (result || '').slice(0, MAX_OUTPUT_LENGTH);
65
+ } catch (err) {
66
+ status = 'fail';
67
+ const stdout = err.stdout || '';
68
+ const stderr = err.stderr || '';
69
+ output = (stdout + '\n' + stderr).trim().slice(0, MAX_OUTPUT_LENGTH);
70
+ }
71
+
72
+ const durationMs = Date.now() - start;
73
+ return { name, status, durationMs, output };
74
+ }
75
+
76
+ /**
77
+ * Run verification across multiple stages.
78
+ * @param {{ cwd: string, stages: string[] }} opts
79
+ * @returns {Promise<object>} Structured verification result
80
+ */
81
+ async function runVerification({ cwd, stages }) {
82
+ const resolvedCwd = path.resolve(cwd);
83
+ const results = [];
84
+
85
+ for (const stageName of stages) {
86
+ const result = executeStage(stageName, resolvedCwd);
87
+ results.push(result);
88
+ }
89
+
90
+ const passed = results.filter(s => s.status === 'pass').length;
91
+ const failed = results.filter(s => s.status === 'fail').length;
92
+ const skipped = results.filter(s => s.status === 'skip').length;
93
+ const totalDurationMs = results.reduce((sum, s) => sum + s.durationMs, 0);
94
+
95
+ return {
96
+ stages: results,
97
+ summary: { passed, failed, skipped, totalDurationMs },
98
+ timestamp: new Date().toISOString(),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Format a verification result as a markdown report.
104
+ * @param {object} result — output from runVerification
105
+ * @returns {string} Markdown report
106
+ */
107
+ function formatReport(result) {
108
+ const statusEmoji = { pass: '✅', fail: '❌', skip: '⏭️' };
109
+ const lines = [];
110
+
111
+ lines.push('# Verification Report');
112
+ lines.push('');
113
+ lines.push(`**Timestamp:** ${result.timestamp}`);
114
+ lines.push('');
115
+ lines.push('| Stage | Status | Duration |');
116
+ lines.push('|-------|--------|----------|');
117
+
118
+ for (const stage of result.stages) {
119
+ const emoji = statusEmoji[stage.status] || '?';
120
+ const duration = stage.durationMs > 0 ? `${stage.durationMs}ms` : '-';
121
+ lines.push(`| ${stage.name} | ${emoji} ${stage.status} | ${duration} |`);
122
+ }
123
+
124
+ lines.push('');
125
+ lines.push(`**Summary:** ${result.summary.passed} passed, ${result.summary.failed} failed, ${result.summary.skipped} skipped (${result.summary.totalDurationMs}ms total)`);
126
+ lines.push('');
127
+
128
+ return lines.join('\n');
129
+ }
130
+
131
+ module.exports = { runVerification, formatReport };
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * verify-cli.js — Entrypoint for the `verify` CLI command.
6
+ * Calls the unified verification runner across all stages and writes
7
+ * the formatted report to .planning/VERIFICATION.md.
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const { runVerification, formatReport } = require('./verification-runner');
13
+
14
+ const STAGES = ['tests', 'lint', 'audit', 'typecheck'];
15
+ const CWD = process.env.MINDFORGE_ROOT || path.resolve(__dirname, '../..');
16
+
17
+ async function main() {
18
+ const planningDir = path.join(CWD, '.planning');
19
+ if (!fs.existsSync(planningDir)) {
20
+ fs.mkdirSync(planningDir, { recursive: true });
21
+ }
22
+
23
+ const result = await runVerification({ cwd: CWD, stages: STAGES });
24
+ const report = formatReport(result);
25
+
26
+ fs.writeFileSync(path.join(planningDir, 'VERIFICATION.md'), report);
27
+ process.stdout.write(report + '\n');
28
+ process.exit(result.summary.failed > 0 ? 1 : 0);
29
+ }
30
+
31
+ main().catch(err => {
32
+ console.error('Verification runner failed:', err.message);
33
+ process.exit(1);
34
+ });
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Recall@K — fraction of relevant items found in the top-k retrieved results.
5
+ * @param {string[]} retrieved - IDs in ranked order
6
+ * @param {string[]} relevant - ground-truth relevant IDs
7
+ * @param {number} k - cutoff
8
+ * @returns {number} recall in [0, 1]
9
+ */
10
+ function recallAtK(retrieved, relevant, k) {
11
+ if (relevant.length === 0) return 0;
12
+ const topK = retrieved.slice(0, k);
13
+ const relevantSet = new Set(relevant);
14
+ const found = topK.filter(id => relevantSet.has(id)).length;
15
+ return found / relevant.length;
16
+ }
17
+
18
+ /**
19
+ * nDCG (Normalized Discounted Cumulative Gain) with graded relevance.
20
+ * @param {string[]} retrieved - IDs in ranked order
21
+ * @param {Object.<string, number>} relevanceMap - {id: grade} where grade is 0-3
22
+ * @param {number} k - cutoff
23
+ * @returns {number} nDCG in [0, 1]
24
+ */
25
+ function ndcg(retrieved, relevanceMap, k) {
26
+ const topK = retrieved.slice(0, k);
27
+
28
+ // DCG = Σ (2^rel_i - 1) / log2(i + 2) for i = 0..k-1
29
+ const dcg = topK.reduce((sum, id, i) => {
30
+ const rel = relevanceMap[id] || 0;
31
+ return sum + (Math.pow(2, rel) - 1) / Math.log2(i + 2);
32
+ }, 0);
33
+
34
+ // IDCG — ideal ordering: sort all relevance grades descending, take top-k
35
+ const idealGrades = Object.values(relevanceMap)
36
+ .filter(g => g > 0)
37
+ .sort((a, b) => b - a)
38
+ .slice(0, k);
39
+
40
+ const idcg = idealGrades.reduce((sum, rel, i) => {
41
+ return sum + (Math.pow(2, rel) - 1) / Math.log2(i + 2);
42
+ }, 0);
43
+
44
+ if (idcg === 0) return 0;
45
+ return dcg / idcg;
46
+ }
47
+
48
+ /**
49
+ * Run a full evaluation over a golden set of queries.
50
+ * @param {Object} opts
51
+ * @param {Array<{query: string, relevant: string[]}>} opts.goldenSet
52
+ * @param {function(string): string[]} opts.retriever
53
+ * @param {number} opts.k
54
+ * @returns {Promise<{meanRecallAtK: number, meanNDCG: number, perQuery: Array}>}
55
+ */
56
+ async function runEval({ goldenSet, retriever, k }) {
57
+ const perQuery = [];
58
+
59
+ for (const { query, relevant } of goldenSet) {
60
+ const retrieved = await Promise.resolve(retriever(query));
61
+
62
+ // Binary relevance map: relevant items get grade 1, others 0
63
+ const relevanceMap = {};
64
+ for (const id of relevant) {
65
+ relevanceMap[id] = 1;
66
+ }
67
+
68
+ const recall = recallAtK(retrieved, relevant, k);
69
+ const ndcgScore = ndcg(retrieved, relevanceMap, k);
70
+
71
+ perQuery.push({ query, recall, ndcg: ndcgScore, retrieved });
72
+ }
73
+
74
+ if (perQuery.length === 0) return { meanRecallAtK: 0, meanNDCG: 0, perQuery: [] };
75
+
76
+ const meanRecallAtK = perQuery.reduce((s, q) => s + q.recall, 0) / perQuery.length;
77
+ const meanNDCG = perQuery.reduce((s, q) => s + q.ndcg, 0) / perQuery.length;
78
+
79
+ return { meanRecallAtK, meanNDCG, perQuery };
80
+ }
81
+
82
+ module.exports = { recallAtK, ndcg, runEval };
@@ -0,0 +1,46 @@
1
+ {
2
+ "description": "Golden set for retrieval quality evaluation. Each entry has a natural-language query and the IDs of documents that SHOULD be retrieved.",
3
+ "version": "1.0.0",
4
+ "queries": [
5
+ {
6
+ "query": "how does the audit hash chain work",
7
+ "relevant": ["audit-hash", "audit-verifier", "verify-audit"]
8
+ },
9
+ {
10
+ "query": "what model should I use for a security-sensitive task",
11
+ "relevant": ["difficulty-scorer", "model-router", "pricing-registry", "trust-boundaries"]
12
+ },
13
+ {
14
+ "query": "how does wave execution and parallel orchestration work",
15
+ "relevant": ["wave-executor", "swarm-controller", "auto-executor"]
16
+ },
17
+ {
18
+ "query": "how do I track token costs and budget enforcement",
19
+ "relevant": ["cost-tracker", "token-ledger", "budget-enforcer", "finops-hub"]
20
+ },
21
+ {
22
+ "query": "how does the knowledge store persist and retrieve entries",
23
+ "relevant": ["knowledge-store", "knowledge-graph-protocol", "shard-controller"]
24
+ },
25
+ {
26
+ "query": "what happens during council consensus and synthesis",
27
+ "relevant": ["council-protocol", "synthesis-engine", "council-templates"]
28
+ },
29
+ {
30
+ "query": "how do instincts get captured and promoted to skills",
31
+ "relevant": ["capture-engine", "promotion-engine", "instinct-schema", "skill-registry"]
32
+ },
33
+ {
34
+ "query": "what verification checks run before marking a task complete",
35
+ "relevant": ["verification-pipeline", "trust-verifier", "policy-engine"]
36
+ },
37
+ {
38
+ "query": "how does the autonomous stuck detector recover failed tasks",
39
+ "relevant": ["stuck-detector", "node-repair", "steering-manager", "progress-reporter"]
40
+ },
41
+ {
42
+ "query": "how are hooks triggered and what security gates exist pre-commit",
43
+ "relevant": ["trust-gate-hook", "instinct-capture-hook", "policy-gate-hardened", "impact-analyzer"]
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+ const crypto = require('crypto');
3
+ /**
4
+ * Canonical audit hash material. MUST be the single source of truth for both
5
+ * the writer (pre-_hash entry) and the verifier (entry with _hash stripped).
6
+ * Hashes {...entry, previous_hash} — entry must NOT contain _hash.
7
+ */
8
+ function hashAuditEntry(entryWithoutHash, previousHash) {
9
+ const material = JSON.stringify({ ...entryWithoutHash, previous_hash: previousHash });
10
+ return crypto.createHash('sha256').update(material).digest('hex');
11
+ }
12
+ module.exports = { hashAuditEntry };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ /**
3
+ * MindForge — Audit chain verifier (UC-04). Fail-closed: any break => valid:false.
4
+ *
5
+ * Walks the JSONL audit log line-by-line and re-derives each entry's hash from its
6
+ * content plus the prior entry's hash. The chain is valid only if EVERY link holds:
7
+ * 1. entry.previous_hash matches the running previousHash (null for the first entry)
8
+ * 2. entry._hash equals sha256({...entry-without-_hash, previous_hash})
9
+ *
10
+ * CRITICAL INVARIANT: hashEntry() here MUST produce byte-identical material to the
11
+ * writer (bin/autonomous/audit-writer.js). The writer hashes {...e, previous_hash}
12
+ * where `e` has no `_hash`. The stored entry is {...e, previous_hash, _hash}; stripping
13
+ * `_hash` via destructuring yields {...e, previous_hash} with the SAME key insertion
14
+ * order, so {...rest, previous_hash} reproduces the writer's material exactly.
15
+ */
16
+ const fs = require('fs');
17
+ const { hashAuditEntry } = require('./audit-hash');
18
+
19
+ /**
20
+ * Re-derives an entry's chained hash. Excludes `_hash` from the material by
21
+ * stripping it, then delegating to the canonical {@link hashAuditEntry} so the
22
+ * writer and verifier share ONE hasher (no material drift possible).
23
+ * @param {object} entry — stored entry (may include `_hash`, which is stripped)
24
+ * @param {string|null} previousHash — prior entry's `_hash` (null for the first link)
25
+ * @returns {string} hex-encoded SHA-256 digest
26
+ */
27
+ function hashEntry(entry, previousHash) {
28
+ const { _hash, ...rest } = entry; // exclude _hash from material
29
+ return hashAuditEntry(rest, previousHash);
30
+ }
31
+
32
+ /**
33
+ * Verifies the integrity of a hash-chained audit log. Fail-closed.
34
+ * @param {string} auditPath — path to the AUDIT.jsonl file
35
+ * @returns {{ valid: boolean, count: number, brokenAt?: number, reason?: string }}
36
+ */
37
+ function verifyAuditChain(auditPath) {
38
+ let lines;
39
+ try { lines = fs.readFileSync(auditPath, 'utf8').split('\n').filter(Boolean); }
40
+ catch (e) { return { valid: false, count: 0, brokenAt: 0, reason: `unreadable: ${e.message}` }; }
41
+
42
+ let previousHash = null;
43
+ for (let i = 0; i < lines.length; i++) {
44
+ let entry;
45
+ try { entry = JSON.parse(lines[i]); }
46
+ catch { return { valid: false, count: lines.length, brokenAt: i, reason: 'unparseable line' }; }
47
+
48
+ if (entry.previous_hash !== previousHash) {
49
+ return { valid: false, count: lines.length, brokenAt: i, reason: 'previous_hash mismatch' };
50
+ }
51
+ const expected = hashEntry(entry, previousHash);
52
+ if (entry._hash !== expected) {
53
+ return { valid: false, count: lines.length, brokenAt: i, reason: 'hash mismatch (entry mutated)' };
54
+ }
55
+ previousHash = entry._hash;
56
+ }
57
+ return { valid: true, count: lines.length };
58
+ }
59
+
60
+ module.exports = { verifyAuditChain };
@@ -92,10 +92,14 @@ class PolicyEngine {
92
92
  return verdict;
93
93
  }
94
94
 
95
- // [ENTERPRISE] Tier 3 Reasoning/PQ Proof Bypass
95
+ // [ENTERPRISE] Tier 3 Sovereign Proof Bypass (fail-closed).
96
+ // A blast-radius override demands a CRYPTOGRAPHIC proof. Only a
97
+ // pq_proof verified via verifyZKProof may authorize the bypass.
98
+ // intent.reasoning_proof is free-form text validated nowhere, so it
99
+ // MUST NOT, on its own, grant an override (UC-22 authz bypass fix).
96
100
  if (intent.tier >= 3 && (intent.reasoning_proof || intent.pq_proof)) {
97
101
  const quantumCrypto = require('./quantum-crypto');
98
- let isProofValid = true;
102
+ let isProofValid = false; // fail-closed: deny unless a real proof verifies
99
103
 
100
104
  if (intent.pq_proof) {
101
105
  const zkResult = quantumCrypto.verifyZKProof(intent.pq_proof, intent.id);
@@ -106,12 +110,21 @@ class PolicyEngine {
106
110
  }
107
111
 
108
112
  if (isProofValid) {
109
- console.log(`[APO-BYPASS] [${requestId}] Tier 3 'Sovereign Proof' verified (${intent.pq_proof ? 'ZK-PQ' : 'Standard'}). Overriding Blast Radius limit.`);
113
+ console.log(`[APO-BYPASS] [${requestId}] Tier 3 'Sovereign Proof' verified (ZK-PQ). Overriding Blast Radius limit.`);
110
114
  // Continue to permit check
111
- } else {
115
+ } else if (intent.pq_proof) {
112
116
  verdict = { verdict: 'DENY', reason: 'ZK proof verification failed. Configure a verifier module or provide a valid proof.', requestId };
113
117
  this.logAudit(intent, impactScore, verdict);
114
118
  return verdict;
119
+ } else {
120
+ // Only a reasoning_proof was supplied — not a cryptographic proof.
121
+ verdict = {
122
+ verdict: 'DENY',
123
+ reason: 'reasoning_proof is not a cryptographic proof; provide a valid pq_proof / Sovereign Proof for blast-radius override.',
124
+ requestId
125
+ };
126
+ this.logAudit(intent, impactScore, verdict);
127
+ return verdict;
115
128
  }
116
129
  } else {
117
130
  verdict = {
@@ -10,20 +10,59 @@
10
10
  const crypto = require('node:crypto');
11
11
  const configManager = require('./config-manager');
12
12
 
13
+ /**
14
+ * Honest-disclosure guard message. The signatures produced by this module are
15
+ * SIMULATED Dilithium-5 (base64(SHA3 + random) — NOT real ML-DSA/FIPS-204
16
+ * lattice crypto). They must NEVER sit on the live trust path silently. The
17
+ * simulated implementation is preserved for demonstration only and is gated
18
+ * behind an explicit, opt-in flag.
19
+ */
20
+ const PQC_DEMO_DISABLED_MSG =
21
+ 'PQC demo disabled — set experimental.pqc_demo=true to use simulated lattice crypto (NOT for production trust)';
22
+
13
23
  class QuantumCrypto {
14
24
  constructor() {
15
25
  this.providerId = configManager.get('security.provider', 'simulated-lattice');
16
- this.pqasEnabled = configManager.get('security.pqas_enabled', true);
26
+ // UC-24: simulated PQC is OFF the live trust path by default. Real PQC has
27
+ // not shipped. The simulated path is gated SOLELY behind the explicit
28
+ // experimental.pqc_demo opt-in (read fresh via isPqcDemoEnabled), so there
29
+ // is exactly one switch controlling the simulated minter. The legacy
30
+ // security.pqas_enabled flag is retained for provider metadata only and
31
+ // MUST NOT independently gate minting (see getProvider).
32
+ this.pqasEnabled = configManager.get('security.pqas_enabled', false);
33
+ }
34
+
35
+ /**
36
+ * Returns true only when the operator has explicitly opted into the SIMULATED
37
+ * post-quantum demo. Read fresh from config so a runtime toggle is honored.
38
+ * @returns {boolean}
39
+ */
40
+ isPqcDemoEnabled() {
41
+ return configManager.get('experimental.pqc_demo', false) === true;
42
+ }
43
+
44
+ /**
45
+ * Hard gate: throws unless the operator has explicitly enabled the simulated
46
+ * PQC demo. Prevents simulated (false-assurance) signatures from silently
47
+ * landing on the live trust path.
48
+ */
49
+ _assertPqcDemoEnabled() {
50
+ if (!this.isPqcDemoEnabled()) {
51
+ throw new Error(PQC_DEMO_DISABLED_MSG);
52
+ }
17
53
  }
18
54
 
19
55
  /**
20
56
  * Returns the current active crypto provider.
21
57
  */
22
58
  getProvider() {
23
- // In v7, this would resolve to a real provider like 'oqs-provider.js'
59
+ // In v7, this would resolve to a real provider like 'oqs-provider.js'.
60
+ // Read pqas_enabled FRESH from config (not the constructor cache) for
61
+ // symmetry with the demo gate. This is metadata only — it does NOT gate
62
+ // minting; experimental.pqc_demo is the single source of truth for that.
24
63
  return {
25
64
  id: this.providerId,
26
- pqas_enabled: this.pqasEnabled,
65
+ pqas_enabled: configManager.get('security.pqas_enabled', false),
27
66
  algorithm: 'Dilithium-5'
28
67
  };
29
68
  }
@@ -32,7 +71,10 @@ class QuantumCrypto {
32
71
  * Generates a key pair using the configured PQ provider.
33
72
  */
34
73
  async generateLatticeKeyPair() {
35
- if (!this.pqasEnabled) throw new Error('PQAS is disabled in configuration.');
74
+ // UC-24: simulated keys are demo-only. The SINGLE gate is the fresh-read
75
+ // experimental.pqc_demo flag — enabling it is sufficient to mint, and
76
+ // disabling it fails closed. security.pqas_enabled does NOT gate this.
77
+ this._assertPqcDemoEnabled();
36
78
 
37
79
  // Simulate high-entropy lattice seeds
38
80
  const seed = crypto.randomBytes(64).toString('hex');
@@ -53,7 +95,11 @@ class QuantumCrypto {
53
95
  * @returns {{ signature: string, simulated: true, algorithm: string }}
54
96
  */
55
97
  async signPQ(data, privateKey) {
56
- if (!this.pqasEnabled) throw new Error('PQAS is disabled.');
98
+ // UC-24: never produce a simulated (false-assurance) signature on the live
99
+ // trust path. The SINGLE gate is the fresh-read experimental.pqc_demo flag
100
+ // (same flag _selectProvider routes on) — security.pqas_enabled does NOT
101
+ // independently block signing.
102
+ this._assertPqcDemoEnabled();
57
103
  if (!privateKey.startsWith('mfq7_dilithium5_priv_')) {
58
104
  throw new Error('Invalid Post-Quantum private key format.');
59
105
  }
@@ -92,19 +138,27 @@ class QuantumCrypto {
92
138
  }
93
139
 
94
140
  /**
95
- * Generates a simulated ZK-Proof of policy adherence.
96
- * This mimics a SNARK where the agent proves it ran the PolicyEngine rules.
141
+ * Generates a SIMULATED ZK-Proof of policy adherence.
142
+ * This mimics a SNARK where the agent proves it ran the PolicyEngine rules,
143
+ * but it is NOT a real zero-knowledge proof. The returned token is explicitly
144
+ * stamped with a `sim` marker (zkp_v1_sim_...) so downstream code and audit
145
+ * logs can never mistake it for a real SNARK. It remains inert by default —
146
+ * verifyZKProof DENYs unless an external verifier module is configured.
147
+ * @returns {string} A simulated ZK-proof token prefixed `zkp_v1_sim_`.
97
148
  */
98
149
  generateZKProof(intent, result) {
99
150
  const proofPayload = JSON.stringify({
100
151
  intent: intent.id,
101
152
  verdict: result.verdict,
102
153
  timestamp: Date.now(),
103
- entropy: crypto.randomBytes(16).toString('hex')
154
+ entropy: crypto.randomBytes(16).toString('hex'),
155
+ simulated: true
104
156
  });
105
157
 
106
158
  const hash = crypto.createHash('sha256').update(proofPayload).digest('hex');
107
- return `zkp_v1_${hash}_${crypto.randomBytes(32).toString('base64')}`;
159
+ // `sim` marker is embedded AFTER the `zkp_v1_` prefix so the existing
160
+ // format check (startsWith 'zkp_v1_') and any external verifier still work.
161
+ return `zkp_v1_sim_${hash}_${crypto.randomBytes(32).toString('base64')}`;
108
162
  }
109
163
 
110
164
  verifyZKProof(proof, intentId) {
@@ -20,19 +20,35 @@ class ZTAIArchiver {
20
20
  * @param {string} archiverDid - The DID of the archiver (e.g., Release Manager)
21
21
  * @returns {Promise<Object>} - The signed manifest
22
22
  */
23
- async generateManifest(entries, archiverDid) {
24
- if (!entries || entries.length === 0) return null;
25
-
26
- // 1. Calculate the Merkle-like root hash of the block
27
- const blockHashes = entries.map(e =>
23
+ /**
24
+ * Computes the cumulative-hash-chain "Merkle root" for a block of entries.
25
+ *
26
+ * This is the SINGLE source of truth for the root algorithm. Both
27
+ * generateManifest() (write path) and verifyIntegrity() (read/verify path)
28
+ * MUST call this so the two can never drift — a drift would re-introduce the
29
+ * false-assurance defect (audit finding #15 / UC-22).
30
+ *
31
+ * @param {Array<Object>} entries - Ordered block of audit entries.
32
+ * @returns {string} - The cumulative SHA-256 hash chain (hex).
33
+ */
34
+ _computeMerkleRoot(entries) {
35
+ const blockHashes = entries.map(e =>
28
36
  crypto.createHash('sha256').update(JSON.stringify(e)).digest('hex')
29
37
  );
30
-
31
- // Simple cumulative hash chain as a Merkle Root equivalent
38
+
39
+ // Simple cumulative hash chain as a Merkle Root equivalent.
32
40
  let cumulativeHash = '';
33
41
  for (const h of blockHashes) {
34
42
  cumulativeHash = crypto.createHash('sha256').update(cumulativeHash + h).digest('hex');
35
43
  }
44
+ return cumulativeHash;
45
+ }
46
+
47
+ async generateManifest(entries, archiverDid) {
48
+ if (!entries || entries.length === 0) return null;
49
+
50
+ // 1. Calculate the Merkle-like root hash of the block.
51
+ const cumulativeHash = this._computeMerkleRoot(entries);
36
52
 
37
53
  const manifestMetadata = {
38
54
  blockStart: entries[0].timestamp,
@@ -94,8 +110,57 @@ class ZTAIArchiver {
94
110
  throw new Error(`CRITICAL: Manifest signature invalid for ${manifestPath}`);
95
111
  }
96
112
 
97
- // 2. Recalculate and Verify Merkle Root (Simulated)
98
- // In a real environment, this would compare against the actual AUDIT.jsonl data slices.
113
+ // 2. Recalculate and Verify Merkle Root against the LIVE AUDIT.jsonl.
114
+ //
115
+ // A valid signature only proves the manifest itself is authentic; it says
116
+ // NOTHING about whether the underlying audit log still matches. We must
117
+ // recompute the root from disk and compare — anything less is false
118
+ // assurance (audit finding #15 / UC-22). Fail-closed on every error path:
119
+ // a missing/unreadable log MUST NOT pass.
120
+ let auditData;
121
+ try {
122
+ auditData = await fs.readFile(this.auditPath, 'utf8');
123
+ } catch (err) {
124
+ throw new Error(`CRITICAL: Audit log unreadable at ${this.auditPath} — cannot verify integrity (${err.message})`);
125
+ }
126
+
127
+ let allEntries;
128
+ try {
129
+ allEntries = auditData
130
+ .split('\n')
131
+ .filter(l => l.trim() !== '')
132
+ .map(l => JSON.parse(l));
133
+ } catch (err) {
134
+ throw new Error(`CRITICAL: Audit log corrupted / unparseable for ${manifestPath} (${err.message})`);
135
+ }
136
+
137
+ // Select the block this manifest covers: entries whose timestamp falls
138
+ // within [blockStart, blockEnd] inclusive.
139
+ const start = Date.parse(manifest.blockStart);
140
+ const end = Date.parse(manifest.blockEnd);
141
+ const blockEntries = allEntries.filter(e => {
142
+ const ts = Date.parse(e.timestamp);
143
+ return ts >= start && ts <= end;
144
+ });
145
+
146
+ // Sanity-check the selected count against the manifest. A mismatch means
147
+ // entries were added or deleted within the covered window.
148
+ if (blockEntries.length !== manifest.entryCount) {
149
+ throw new Error(
150
+ 'CRITICAL: Audit log tampering detected — block entry count mismatch ' +
151
+ `(manifest=${manifest.entryCount}, found=${blockEntries.length}) for ${manifestPath}`
152
+ );
153
+ }
154
+
155
+ // Recompute the root with the SAME algorithm used at archive time.
156
+ const recomputedRoot = this._computeMerkleRoot(blockEntries);
157
+ if (recomputedRoot !== manifest.merkleRoot) {
158
+ throw new Error(
159
+ `CRITICAL: Audit log tampering detected — Merkle root mismatch for ${manifestPath} ` +
160
+ `(expected=${manifest.merkleRoot}, recomputed=${recomputedRoot}).`
161
+ );
162
+ }
163
+
99
164
  console.log(`[ZTAI-ARCHIVER] Integrity Verified for block ending ${manifest.blockEnd}`);
100
165
  return true;
101
166
  }