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.
- package/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/config.json +14 -4
- package/CHANGELOG.md +137 -0
- package/MINDFORGE.md +5 -5
- package/RELEASENOTES.md +1 -1
- package/bin/autonomous/audit-writer.js +108 -86
- package/bin/autonomous/auto-runner.js +304 -19
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/mesh-self-healer.js +101 -28
- package/bin/autonomous/wave-executor.js +20 -1
- package/bin/browser/regression-writer.js +45 -3
- package/bin/browser/session-manager.js +21 -17
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/server.js +1 -1
- package/bin/dashboard/sse-bridge.js +9 -12
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/logic-drift-detector.js +14 -6
- package/bin/engine/logic-validator.js +155 -25
- package/bin/engine/orbital-guardian.js +56 -10
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/reason-source-aligner.js +19 -6
- package/bin/engine/remediation-engine.js +1 -1
- package/bin/engine/self-corrective-synthesizer.js +1 -1
- package/bin/engine/sre-manager.js +33 -6
- package/bin/engine/temporal-cli.js +4 -2
- package/bin/engine/verification-runner.js +131 -0
- package/bin/engine/verify-cli.js +34 -0
- package/bin/eval/eval-harness.js +82 -0
- package/bin/eval/golden-set-retrieval.json +46 -0
- package/bin/governance/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/policy-engine.js +17 -4
- package/bin/governance/quantum-crypto.js +63 -9
- package/bin/governance/ztai-archiver.js +74 -9
- package/bin/governance/ztai-manager.js +33 -5
- package/bin/hindsight-injector.js +5 -6
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/installer-core.js +31 -2
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/eis-client.js +45 -4
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-store.js +30 -6
- package/bin/memory/retrieval-fusion.js +58 -0
- package/bin/memory/semantic-hub.js +2 -2
- package/bin/memory/vector-hub.js +143 -6
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +13 -4
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +6 -2
- package/bin/models/model-router.js +31 -18
- package/bin/models/openai-provider.js +6 -3
- package/bin/models/pricing-registry.js +128 -0
- package/bin/review/ads-engine.js +1 -1
- package/bin/review/finding-synthesizer.js +35 -6
- package/bin/security/trust-boundaries.js +194 -0
- package/bin/security/trust-gate-hook.js +49 -0
- package/bin/skill-registry.js +34 -22
- package/bin/skills-builder/marketplace-cli.js +5 -3
- package/bin/skills-builder/skill-registrar.js +4 -6
- package/bin/sre/sentinel.js +7 -5
- package/bin/sre/shadow-mirror.js +90 -40
- package/bin/utils/append-queue.js +67 -0
- package/bin/utils/file-io.js +29 -80
- package/bin/utils/version-check.js +75 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/package.json +1 -1
- 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
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
98
|
-
//
|
|
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
|
}
|