mindforge-cc 11.2.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.
@@ -35,26 +35,43 @@ class OrbitalGuardian {
35
35
  throw new Error(`[ORBITAL-GUARDIAN] DID ${did} has insufficient Trust Tier for Orbital Attestation.`);
36
36
  }
37
37
 
38
- // 1. Sign the attestation payload using the Hardware Enclave provider
39
- const attestationPayload = await ztaiManager.signData(did, JSON.stringify({
38
+ // 1. Build the EXACT canonical message and sign it with the agent's key.
39
+ // UC-22: this canonical string is persisted verbatim alongside the DID so
40
+ // verify() can re-verify the signature later. We must store the precise
41
+ // bytes that were signed — recomputing them (e.g. with a fresh timestamp)
42
+ // would never verify — so capture the message once, here, and reuse it.
43
+ const signedMessage = JSON.stringify({
40
44
  requestId,
41
45
  payload,
42
46
  timestamp: new Date().toISOString()
43
- }));
47
+ });
48
+ const signature = await ztaiManager.signData(did, signedMessage);
44
49
 
45
50
  const attestation = {
46
51
  id: `att_${crypto.randomBytes(4).toString('hex')}`,
47
52
  request_id: requestId,
48
53
  status: 'APPROVED',
49
- attestation_payload: attestationPayload,
54
+ did,
55
+ signed_message: signedMessage,
56
+ attestation_payload: signature,
50
57
  timestamp: new Date().toISOString()
51
58
  };
52
59
 
53
- // 2. Persist to SQLite (Source of truth for v8 Governance Dashboard)
60
+ // 2. Persist to SQLite (Source of truth for v8 Governance Dashboard).
61
+ // did + signed_message + signature together let verify() re-check the
62
+ // cryptographic signature; status='APPROVED' alone is NOT trusted.
54
63
  vectorHub.run(
55
- `INSERT INTO attestations (id, request_id, status, attestation_payload, timestamp)
56
- VALUES (?, ?, ?, ?, ?)`,
57
- [attestation.id, attestation.request_id, attestation.status, attestation.attestation_payload, attestation.timestamp]
64
+ `INSERT INTO attestations (id, request_id, status, did, signed_message, attestation_payload, timestamp)
65
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
66
+ [
67
+ attestation.id,
68
+ attestation.request_id,
69
+ attestation.status,
70
+ attestation.did,
71
+ attestation.signed_message,
72
+ attestation.attestation_payload,
73
+ attestation.timestamp
74
+ ]
58
75
  );
59
76
 
60
77
  console.log(`[ORBITAL-GUARDIAN] Attestation SUCCESS: ${attestation.id}`);
@@ -63,9 +80,16 @@ class OrbitalGuardian {
63
80
 
64
81
  /**
65
82
  * Verifies if a request has a valid hardware bypass.
83
+ *
84
+ * UC-22 (audit finding #2): an APPROVED row is NOT trusted on its own. The
85
+ * stored signature is re-verified against the signer's registered public key
86
+ * over the EXACT canonical message that attest() signed. Anyone who forges an
87
+ * APPROVED row but cannot produce a valid signature is rejected. The check is
88
+ * fail-closed: a missing field, an unregistered/revoked DID, or any thrown
89
+ * error all resolve to { verified:false }.
66
90
  */
67
91
  async verify(requestId) {
68
- if (!requestId) return { verified: false };
92
+ if (!requestId) return { verified: false, reason: 'missing requestId' };
69
93
  await this.ensureInit();
70
94
 
71
95
  const results = vectorHub.query(
@@ -74,7 +98,29 @@ class OrbitalGuardian {
74
98
  );
75
99
 
76
100
  const record = results[0];
77
- if (!record) return { verified: false };
101
+ if (!record) return { verified: false, reason: 'no APPROVED attestation found' };
102
+
103
+ // Re-verify the cryptographic signature. Without a DID, the canonical signed
104
+ // message, AND a signature we cannot prove the row was produced by attest().
105
+ if (!record.did || !record.signed_message || !record.attestation_payload) {
106
+ return { verified: false, reason: 'attestation missing signature material' };
107
+ }
108
+
109
+ let signatureValid = false;
110
+ try {
111
+ signatureValid = ztaiManager.verifySignature(
112
+ record.did,
113
+ record.signed_message,
114
+ record.attestation_payload
115
+ );
116
+ } catch (err) {
117
+ // Unregistered/revoked DID or malformed signature → fail closed.
118
+ return { verified: false, reason: `signature verification error: ${err.message}` };
119
+ }
120
+
121
+ if (!signatureValid) {
122
+ return { verified: false, reason: 'signature verification failed' };
123
+ }
78
124
 
79
125
  return {
80
126
  verified: true,
@@ -45,7 +45,17 @@ class ReasonSourceAligner {
45
45
  * @returns {Object} - Alignment results.
46
46
  */
47
47
  checkAlignment(thought) {
48
- if (!this.initialized) return { score: 1.0, reason: 'uninitialized' }; // Fail-safe stable
48
+ // Fail-safe stable: when no requirements are loaded we CANNOT assess
49
+ // alignment, so we honestly decline rather than assert perfect alignment.
50
+ // Returning the SAME shape as the normal branch means the sole caller
51
+ // (auto-runner.checkMissionFidelity) reads a real boolean instead of
52
+ // `undefined`, so the mission-fidelity gate is no longer silently disabled.
53
+ // is_aligned:false is the safe direction — the caller only injects a
54
+ // correction when is_aligned is truthy, so an honest "can't assess" simply
55
+ // does nothing (no false correction, no silent shape mismatch).
56
+ if (!this.initialized) {
57
+ return { is_aligned: false, best_match_id: null, confidence: 0, status: 'uninitialized' };
58
+ }
49
59
 
50
60
  const alignmentScores = this.registry.map(req => {
51
61
  const score = this._calculateSimilarity(thought, req.summary + ' ' + req.description);
@@ -58,6 +68,7 @@ class ReasonSourceAligner {
58
68
  is_aligned: bestMatch ? bestMatch.score > 0.25 : false, // Sparse mapping allowed
59
69
  best_match_id: bestMatch ? bestMatch.id : null,
60
70
  confidence: bestMatch ? parseFloat(bestMatch.score.toFixed(4)) : 0,
71
+ status: 'assessed',
61
72
  };
62
73
  }
63
74
 
@@ -82,19 +93,21 @@ class ReasonSourceAligner {
82
93
  }
83
94
 
84
95
  /**
85
- * Similarity Heuristic (Keyword-based overlap)
96
+ * Keyword-based overlap heuristic (Jaccard similarity).
97
+ * NOTE: This is a token-overlap heuristic, NOT semantic embeddings.
98
+ * Returns |A ∩ B| / |A ∪ B| in [0, 1].
86
99
  */
87
100
  _calculateSimilarity(a, b) {
88
101
  const getTokens = (str) => new Set(str.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(t => t.length > 3));
89
102
  const tokensA = getTokens(a);
90
103
  const tokensB = getTokens(b);
91
-
104
+
92
105
  if (tokensA.size === 0 || tokensB.size === 0) return 0;
93
-
106
+
94
107
  const intersection = new Set([...tokensA].filter(x => tokensB.has(x)));
95
108
  const union = new Set([...tokensA, ...tokensB]);
96
-
97
- return intersection.size / tokensA.size; // Weighted by thought coverage
109
+
110
+ return intersection.size / union.size; // Jaccard: overlap over combined vocabulary
98
111
  }
99
112
 
100
113
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MindForge v11.1.0 — Neural Drift Remediation (NDR)
2
+ * MindForge v11.2.0 — Neural Drift Remediation (NDR)
3
3
  * Component: Remediation Engine (Pillar X)
4
4
  *
5
5
  * Triggers corrective actions when logic drift or reasoning
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MindForge v11.1.0 — Self-Corrective Synthesis (SCS)
2
+ * MindForge v11.2.0 — Self-Corrective Synthesis (SCS)
3
3
  * Component: Self-Corrective Synthesizer (Pillar XII)
4
4
  *
5
5
  * Analyzes mission drift and logic stagnation to synthesize
@@ -47,12 +47,22 @@ class SREManager {
47
47
  }
48
48
 
49
49
  /**
50
- * Sanitizes a thought chain and generates a ZK-Proof Compliance Certificate.
51
- * Ensures that sensitive IP or "zero-visibility" thoughts are isolated while proving audit-eligibility.
50
+ * Sanitizes a thought chain and generates an HMAC integrity certificate.
51
+ *
52
+ * IMPORTANT — HONEST LABELING: This is NOT a zero-knowledge proof. The
53
+ * artifact is an HMAC-SHA256 tag computed with a process-local shared secret
54
+ * (EPHEMERAL_ENCLAVE_KEY). It provides tamper-evidence/integrity over the
55
+ * proof payload, but:
56
+ * - any party holding the key can forge it (symmetric MAC, not asymmetric),
57
+ * - the payload carries the plaintext sha256(thoughtChain) digest, so it is
58
+ * not "zero-visibility".
59
+ * The enclave is simulated (no hardware TEE). Consumers must treat the
60
+ * returned object as an integrity tag, not a cryptographic ZK proof.
61
+ *
52
62
  * @param {string} thoughtChain - The raw agentic thought chain.
53
63
  * @param {string} enclaveId - The active enclave ID.
54
64
  * @param {Object} policyResult - Whether the content passed internal policy checks.
55
- * @returns {Object} - ZK-Proof compliance certificate.
65
+ * @returns {Object} - HMAC integrity certificate (simulated enclave).
56
66
  */
57
67
  sanitizeThoughtChain(thoughtChain, enclaveId, policyResult = { passed: true }) {
58
68
  if (!this.activeEnclaves.has(enclaveId)) {
@@ -64,7 +74,7 @@ class SREManager {
64
74
  const prevHash = enclaveData.cumulativeHash;
65
75
  const digest = crypto.createHash('sha256').update(thoughtChain).digest('hex');
66
76
 
67
- // Generate a simulated ZK-Proof Compliance Certificate
77
+ // Generate a simulated-enclave HMAC integrity certificate (NOT a ZK proof)
68
78
  const proofPayload = {
69
79
  enclaveId: enclaveId,
70
80
  digest: digest,
@@ -85,19 +95,36 @@ class SREManager {
85
95
 
86
96
  const certificate = {
87
97
  status: 'SRE-ISOLATED',
98
+ // Honest labeling: this is a symmetric HMAC integrity tag, not a ZK proof.
99
+ type: 'hmac-integrity-certificate',
100
+ method: 'hmac-sha256-integrity',
101
+ simulated: true,
102
+ zeroKnowledge: false,
103
+ disclosure: 'HMAC integrity tag (simulated enclave; NOT a zero-knowledge proof). '
104
+ + 'Forgeable by any holder of the shared enclave key; payload carries the plaintext sha256(thought) digest.',
88
105
  proof: proofPayload,
89
106
  signature: signature,
90
107
  proofHash: proofHash,
91
108
  verificationDid: SYSTEM_DID,
92
- message: `[SRE-ZK-PROOF] Confidential reasoning (sha256:${digest.substring(0, 8)}...) verified by Enclave Auditor.`
109
+ message: `[SRE-HMAC] HMAC integrity tag for confidential reasoning (sha256:${digest.substring(0, 8)}...) `
110
+ + 'in simulated enclave — NOT a zero-knowledge proof.'
93
111
  };
94
112
 
95
113
  return certificate;
96
114
  }
97
115
 
98
116
  /**
99
- * Verifies an SRE Compliance Certificate without seeing the original content.
117
+ * Verifies an SRE integrity certificate's HMAC tag and policy flag.
118
+ *
119
+ * NOTE — HONEST LABELING: this recomputes the HMAC-SHA256 tag using the
120
+ * shared enclave key and compares it. It is symmetric MAC verification, NOT
121
+ * zero-knowledge verification. The method name is retained for API
122
+ * compatibility with existing callers; see verifyIntegrityCertificate alias.
100
123
  */
124
+ verifyIntegrityCertificate(certificate) {
125
+ return this.verifyZKProof(certificate);
126
+ }
127
+
101
128
  verifyZKProof(certificate) {
102
129
  if (certificate.status !== 'SRE-ISOLATED') return false;
103
130
 
@@ -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 = {
@@ -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
  }
@@ -65,7 +65,7 @@ class SecureEnclaveProvider extends KeyProvider {
65
65
  }
66
66
 
67
67
  async generate(did) {
68
- console.log(`[ZTAI-HSM] Provisioning protected identity enclave for ${did}...`);
68
+ console.log(`[ZTAI-HSM-SIM] Provisioning simulated (in-process) identity enclave for ${did}...`);
69
69
  const { publicKey, privateKey } = await generateKeyPair('ed25519');
70
70
  const pubPEM = publicKey.export({ type: 'spki', format: 'pem' });
71
71
 
@@ -82,7 +82,7 @@ class SecureEnclaveProvider extends KeyProvider {
82
82
  const record = this.enclaveStore.get(did);
83
83
  if (!record) throw new Error(`Enclave record not found for ${did}`);
84
84
 
85
- console.log(`[ZTAI-HSM] Delegating signature to hardware enclave [DID: ${did}]`);
85
+ console.log(`[ZTAI-HSM-SIM] Signing via simulated in-process enclave (NOT a hardware HSM/TPM) [DID: ${did}]`);
86
86
 
87
87
  // Simulate enclave "wrapping" or "sealing" logic
88
88
  const signature = crypto.sign(null, Buffer.from(data), record.privateKey);
@@ -93,7 +93,7 @@ class SecureEnclaveProvider extends KeyProvider {
93
93
  }
94
94
 
95
95
  async rotate(did) {
96
- console.log(`[ZTAI-HSM] Rotating enclave keys for ${did}...`);
96
+ console.log(`[ZTAI-HSM-SIM] Rotating simulated enclave keys for ${did}...`);
97
97
  return this.generate(did);
98
98
  }
99
99
 
@@ -95,6 +95,25 @@ const RUNTIMES = {
95
95
  },
96
96
  };
97
97
 
98
+ /**
99
+ * Reads the target project's experimental.pqc_demo flag — the SINGLE gate that
100
+ * the engine (bin/governance/quantum-crypto.js) uses to enable the simulated
101
+ * PQAS minter. Defaults to false (engine default) when the config is absent or
102
+ * unreadable, so the installer never over-claims that PQAS is enabled.
103
+ * @param {string} cwd - Target project root being installed into.
104
+ * @returns {boolean} - true only when experimental.pqc_demo === true.
105
+ */
106
+ function isPqcDemoEnabled(cwd) {
107
+ try {
108
+ const cfgPath = path.join(cwd, '.mindforge', 'config.json');
109
+ if (!fs.existsSync(cfgPath)) return false;
110
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
111
+ return cfg && cfg.experimental && cfg.experimental.pqc_demo === true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
98
117
  /**
99
118
  * Generates runtime-specific entry file content.
100
119
  * e.g. replacing "Claude" with "Gemini" in GEMINI.md
@@ -650,9 +669,19 @@ async function install(runtime, scope, options = {}) {
650
669
  }
651
670
  });
652
671
 
653
- // ✨ SOVEREIGN INITIALIZATION: Mark project as PQAS & Proactive enabled
672
+ // ✨ SOVEREIGN INITIALIZATION: report actual security posture honestly.
673
+ // The PQAS minter is gated SOLELY behind experimental.pqc_demo (see
674
+ // bin/governance/quantum-crypto.js: getProvider/_assertPqcDemoEnabled). When
675
+ // that flag is off (the default) PQAS is inert/simulated — claiming it is
676
+ // "enabled" would contradict the engine and mislead operators (UC-22).
654
677
  Theme.printStatus(c.magenta('Sovereign Intelligence v8.2.0 activated'), 'done');
655
- Theme.printStatus(c.dim(' - Post-Quantum Agentic Security (PQAS) enabled'), 'info');
678
+ if (isPqcDemoEnabled(process.cwd())) {
679
+ Theme.printStatus(c.dim(' - Post-Quantum Agentic Security (PQAS): SIMULATED demo ENABLED '
680
+ + '(experimental.pqc_demo=true — simulated lattice crypto, NOT production trust)'), 'info');
681
+ } else {
682
+ Theme.printStatus(c.dim(' - Post-Quantum Agentic Security (PQAS): available in simulated/experimental '
683
+ + 'mode (inactive by default — set experimental.pqc_demo=true to enable the simulated demo)'), 'info');
684
+ }
656
685
  Theme.printStatus(c.dim(' - Proactive Semantic Intent Harvesting active'), 'info');
657
686
 
658
687
  // bin/ utilities (remaining non-engine scripts)
@@ -100,10 +100,48 @@ class EISClient {
100
100
  return [];
101
101
  }
102
102
 
103
- // TODO: implement when remote nodes are available
103
+ /**
104
+ * Verifies the provenance of a remote knowledge entry by cryptographically
105
+ * checking its signature against the signer DID's registered public key.
106
+ *
107
+ * HONEST / FAIL-CLOSED CONTRACT (UC-22, finding #22): this method NEVER
108
+ * returns true for a signature it has not actually verified. It returns true
109
+ * ONLY when ZTAI.verifySignature confirms the signature against a public key
110
+ * that is resolvable in the local trust registry. Every other case fails
111
+ * closed → false:
112
+ * - no/empty signature,
113
+ * - no signer DID on the entry,
114
+ * - the DID is not resolvable here (e.g. a genuinely remote peer whose key
115
+ * is not in the local registry — there is no remote DID-resolution infra
116
+ * yet, see resolveRemoteNode),
117
+ * - tampered payload or signature (crypto.verify returns false / throws).
118
+ *
119
+ * @param {{did?: string, signedData?: string}} entry - Provenance-bearing entry.
120
+ * `did` is the signer's DID; `signedData` is the exact canonical bytes that
121
+ * were signed (defaults to a deterministic JSON of the entry if absent).
122
+ * @param {string} signature - Base64 signature to verify.
123
+ * @returns {boolean} true only if cryptographically verified; false otherwise.
124
+ */
104
125
  verifyRemoteProvenance(entry, signature) {
105
- if (!signature) return false;
106
- return true;
126
+ if (!signature || typeof signature !== 'string') return false;
127
+ if (!entry || typeof entry !== 'object') return false;
128
+
129
+ const did = entry.did;
130
+ if (!did || typeof did !== 'string') return false;
131
+
132
+ // Canonical signed bytes: prefer an explicit signedData field, else a
133
+ // deterministic JSON of the entry (excluding the signature envelope).
134
+ const signedData = typeof entry.signedData === 'string'
135
+ ? entry.signedData
136
+ : JSON.stringify(entry);
137
+
138
+ try {
139
+ // ZTAI.verifySignature throws for an unresolvable (unregistered) DID —
140
+ // treat that as fail-closed rather than asserting verified provenance.
141
+ return ZTAI.verifySignature(did, signedData, signature) === true;
142
+ } catch {
143
+ return false;
144
+ }
107
145
  }
108
146
 
109
147
  // TODO: implement when remote nodes are available
@@ -113,7 +151,10 @@ class EISClient {
113
151
 
114
152
  /**
115
153
  * [HARDEN] Generates a cryptographically signed auth header using the agent's DID.
116
- * This ensures verifiable provenance of knowledge within the mesh.
154
+ * This attaches OUTBOUND provenance to locally-originated requests (it signs
155
+ * what this node sends). It does NOT verify the provenance of inbound remote
156
+ * entries — that is verifyRemoteProvenance's job, which fails closed unless a
157
+ * signature is cryptographically verified against a resolvable public key.
117
158
  */
118
159
  async getAuthHeader(action, resource) {
119
160
  const manager = new ZTAI();
@@ -56,6 +56,26 @@ class VectorHub {
56
56
  }
57
57
  }
58
58
 
59
+ /**
60
+ * Idempotently add a column to an existing table (lightweight migration).
61
+ * SQLite has no "ADD COLUMN IF NOT EXISTS", so we run the ALTER and swallow
62
+ * only the "duplicate column name" error — which simply means the column is
63
+ * already present (the table was created with it, or a prior run added it).
64
+ * Any other error is re-thrown so genuine schema problems surface loudly.
65
+ * @param {string} table
66
+ * @param {string} column
67
+ * @param {string} typeDecl - e.g. 'TEXT', 'INTEGER DEFAULT 0'
68
+ */
69
+ _addColumnIfMissing(table, column, typeDecl) {
70
+ try {
71
+ this._db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${typeDecl}`);
72
+ } catch (err) {
73
+ if (!/duplicate column name/i.test(err && err.message)) {
74
+ throw err;
75
+ }
76
+ }
77
+ }
78
+
59
79
  /**
60
80
  * Initialize the WASM SQLite database and create tables + indexes.
61
81
  */
@@ -124,11 +144,23 @@ class VectorHub {
124
144
  id TEXT PRIMARY KEY,
125
145
  request_id TEXT NOT NULL,
126
146
  status TEXT NOT NULL,
147
+ did TEXT,
148
+ signed_message TEXT,
127
149
  attestation_payload TEXT,
128
150
  timestamp TEXT NOT NULL
129
151
  )
130
152
  `);
131
153
 
154
+ // UC-22 (audit finding #2): orbital attestations must carry the signer DID
155
+ // and the EXACT canonical message that was signed so verify() can re-check
156
+ // the cryptographic signature instead of trusting status='APPROVED' alone.
157
+ // CREATE TABLE IF NOT EXISTS won't add columns to a database created before
158
+ // this fix, so back-fill them with guarded ALTER TABLE statements. SQLite
159
+ // throws "duplicate column name" when the column already exists — that case
160
+ // is the success path (already migrated), so it is swallowed.
161
+ this._addColumnIfMissing('attestations', 'did', 'TEXT');
162
+ this._addColumnIfMissing('attestations', 'signed_message', 'TEXT');
163
+
132
164
  this._db.run(`
133
165
  CREATE TABLE IF NOT EXISTS mesh_config (
134
166
  key TEXT PRIMARY KEY,
@@ -6,6 +6,10 @@
6
6
 
7
7
  const SEVERITY_ORDER = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
8
8
 
9
+ // A severity spread of this many levels (or more) within a single location-group
10
+ // is treated as a contradiction (e.g. CRITICAL=3 vs LOW=0 → gap 3).
11
+ const CONTRADICTION_GAP_THRESHOLD = 2;
12
+
9
13
  function synthesizeFindings(reviews) {
10
14
  const allFindings = [];
11
15
  const modelSpecific = {};
@@ -18,8 +22,9 @@ function synthesizeFindings(reviews) {
18
22
  }
19
23
  }
20
24
 
21
- // Detect consensus
25
+ // Detect consensus and contradictions from the same location-groups.
22
26
  const consensus = [];
27
+ const contradictions = [];
23
28
  const processed = new Set();
24
29
 
25
30
  for (let i = 0; i < allFindings.length; i++) {
@@ -31,7 +36,7 @@ function synthesizeFindings(reviews) {
31
36
  for (let j = i + 1; j < allFindings.length; j++) {
32
37
  if (processed.has(j)) continue;
33
38
  const f2 = allFindings[j];
34
-
39
+
35
40
  if (isSameFinding(f1, f2)) {
36
41
  group.push(f2);
37
42
  processed.add(j);
@@ -45,13 +50,12 @@ function synthesizeFindings(reviews) {
45
50
  severity: getHighestSeverity(group.map(f => f.severity)),
46
51
  models: group.map(f => f.model),
47
52
  });
53
+
54
+ const contradiction = detectContradiction(f1.location, group);
55
+ if (contradiction) contradictions.push(contradiction);
48
56
  }
49
57
  }
50
58
 
51
- // Detect contradictions (large severity gap on same finding)
52
- const contradictions = [];
53
- // (In a real implementation, we'd more deeply analyze conflicting logic)
54
-
55
59
  return {
56
60
  consensus,
57
61
  model_specific: modelSpecific,
@@ -92,6 +96,31 @@ function normalizeLocation(loc) {
92
96
  });
93
97
  }
94
98
 
99
+ function severityRank(severity) {
100
+ const idx = SEVERITY_ORDER.indexOf(severity);
101
+ return idx < 0 ? 0 : idx;
102
+ }
103
+
104
+ // A location-group is contradictory when its reviews disagree on severity by
105
+ // CONTRADICTION_GAP_THRESHOLD levels or more (e.g. CRITICAL vs LOW). Reuses the
106
+ // already-computed location-group rather than re-deriving it.
107
+ function detectContradiction(location, group) {
108
+ const ranks = group.map(f => severityRank(f.severity));
109
+ const maxRank = Math.max(...ranks);
110
+ const minRank = Math.min(...ranks);
111
+
112
+ if (maxRank - minRank < CONTRADICTION_GAP_THRESHOLD) return null;
113
+
114
+ return {
115
+ location,
116
+ severities: group.map(f => f.severity),
117
+ models: group.map(f => f.model),
118
+ description: `Severity disagreement at ${location}: ` +
119
+ `${SEVERITY_ORDER[minRank]} vs ${SEVERITY_ORDER[maxRank]} ` +
120
+ `(${maxRank - minRank}-level gap across ${group.length} reviews)`,
121
+ };
122
+ }
123
+
95
124
  function getHighestSeverity(severities) {
96
125
  let highest = 0;
97
126
  for (const s of severities) {