mindforge-cc 10.7.0 → 11.2.0

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 (85) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
  3. package/.mindforge/config.json +18 -4
  4. package/CHANGELOG.md +165 -0
  5. package/MINDFORGE.md +3 -3
  6. package/README.md +49 -4
  7. package/RELEASENOTES.md +81 -1
  8. package/SECURITY.md +20 -8
  9. package/bin/autonomous/audit-writer.js +105 -70
  10. package/bin/autonomous/auto-runner.js +377 -34
  11. package/bin/autonomous/context-refactorer.js +26 -11
  12. package/bin/autonomous/dependency-dag.js +59 -0
  13. package/bin/autonomous/state-manager.js +62 -6
  14. package/bin/autonomous/stuck-monitor.js +46 -7
  15. package/bin/autonomous/wave-executor.js +86 -26
  16. package/bin/council-cli.js +161 -0
  17. package/bin/dashboard/api-router.js +43 -0
  18. package/bin/dashboard/approval-handler.js +3 -1
  19. package/bin/dashboard/metrics-aggregator.js +28 -1
  20. package/bin/dashboard/server.js +68 -5
  21. package/bin/dashboard/sse-bridge.js +10 -13
  22. package/bin/engine/council-runtime.js +124 -0
  23. package/bin/engine/feedback-loop.js +8 -0
  24. package/bin/engine/intelligence-interlock.js +32 -15
  25. package/bin/engine/logic-drift-detector.js +2 -1
  26. package/bin/engine/nexus-tracer.js +3 -2
  27. package/bin/engine/otel-exporter.js +123 -0
  28. package/bin/engine/remediation-engine.js +155 -32
  29. package/bin/engine/self-corrective-synthesizer.js +84 -10
  30. package/bin/engine/sre-manager.js +12 -4
  31. package/bin/engine/temporal-cli.js +4 -2
  32. package/bin/engine/temporal-hub.js +131 -34
  33. package/bin/engine/verification-runner.js +131 -0
  34. package/bin/engine/verify-cli.js +34 -0
  35. package/bin/eval/eval-harness.js +82 -0
  36. package/bin/eval/golden-set-retrieval.json +46 -0
  37. package/bin/governance/approve.js +41 -5
  38. package/bin/governance/audit-hash.js +12 -0
  39. package/bin/governance/audit-verifier.js +60 -0
  40. package/bin/governance/impact-analyzer.js +28 -0
  41. package/bin/governance/policy-engine.js +10 -3
  42. package/bin/governance/quantum-crypto.js +95 -28
  43. package/bin/governance/rbac-manager.js +74 -2
  44. package/bin/governance/ztai-manager.js +79 -9
  45. package/bin/hindsight-injector.js +8 -9
  46. package/bin/hooks/instinct-capture-hook.js +186 -0
  47. package/bin/memory/auto-shadow.js +32 -3
  48. package/bin/memory/eis-client.js +71 -34
  49. package/bin/memory/embedding-engine.js +61 -0
  50. package/bin/memory/identity-synthesizer.js +2 -2
  51. package/bin/memory/knowledge-graph.js +58 -5
  52. package/bin/memory/knowledge-indexer.js +53 -6
  53. package/bin/memory/knowledge-store.js +52 -6
  54. package/bin/memory/retrieval-fusion.js +58 -0
  55. package/bin/memory/semantic-hub.js +2 -2
  56. package/bin/memory/vector-hub.js +111 -6
  57. package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
  58. package/bin/migrations/schema-versions.js +13 -0
  59. package/bin/mindforge-cli.js +4 -5
  60. package/bin/models/anthropic-provider.js +58 -4
  61. package/bin/models/cloud-broker.js +68 -20
  62. package/bin/models/cost-tracker.js +3 -1
  63. package/bin/models/difficulty-scorer.js +54 -0
  64. package/bin/models/gemini-provider.js +57 -2
  65. package/bin/models/model-client.js +20 -0
  66. package/bin/models/model-router.js +59 -26
  67. package/bin/models/openai-provider.js +50 -3
  68. package/bin/models/pricing-registry.js +128 -0
  69. package/bin/review/ads-engine.js +1 -1
  70. package/bin/security/trust-boundaries.js +102 -0
  71. package/bin/security/trust-gate-hook.js +39 -0
  72. package/bin/skill-registry.js +3 -2
  73. package/bin/skills-builder/marketplace-cli.js +5 -3
  74. package/bin/skills-builder/skill-registrar.js +4 -6
  75. package/bin/sre/sentinel.js +7 -5
  76. package/bin/utils/append-queue.js +55 -0
  77. package/bin/utils/file-io.js +90 -38
  78. package/bin/utils/index.js +58 -0
  79. package/bin/utils/version-check.js +59 -0
  80. package/bin/verify-audit.js +12 -0
  81. package/bin/wizard/theme.js +1 -2
  82. package/docs/getting-started.md +1 -1
  83. package/docs/user-guide.md +2 -2
  84. package/package.json +2 -2
  85. package/bin/dashboard/team-tracker.js +0 -0
@@ -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 };
@@ -137,6 +137,34 @@ class ImpactAnalyzer {
137
137
  static resetSession(sessionId) {
138
138
  this.sessionState.delete(sessionId);
139
139
  }
140
+
141
+ /**
142
+ * Returns the current entropy count for a session without incrementing.
143
+ * Useful for diagnostics and monitoring.
144
+ */
145
+ static getSessionEntropy(sessionId) {
146
+ return this.sessionState.get(sessionId) || 0;
147
+ }
148
+
149
+ /**
150
+ * Clears all session state entries. Use during process cleanup or testing.
151
+ */
152
+ static clearAllSessions() {
153
+ this.sessionState.clear();
154
+ }
155
+
156
+ /**
157
+ * Clears sessions that have exceeded a given entropy threshold.
158
+ * Prevents unbounded memory growth from abandoned sessions.
159
+ * @param {number} maxEntropy - Sessions above this count are purged.
160
+ */
161
+ static clearStaleSessions(maxEntropy = 50) {
162
+ for (const [sessionId, count] of this.sessionState.entries()) {
163
+ if (count > maxEntropy) {
164
+ this.sessionState.delete(sessionId);
165
+ }
166
+ }
167
+ }
140
168
  }
141
169
 
142
170
  module.exports = ImpactAnalyzer;
@@ -95,14 +95,21 @@ class PolicyEngine {
95
95
  // [ENTERPRISE] Tier 3 Reasoning/PQ Proof Bypass
96
96
  if (intent.tier >= 3 && (intent.reasoning_proof || intent.pq_proof)) {
97
97
  const quantumCrypto = require('./quantum-crypto');
98
- const isProofValid = intent.pq_proof ?
99
- quantumCrypto.verifyZKProof(intent.pq_proof, intent.id) : true;
98
+ let isProofValid = true;
99
+
100
+ if (intent.pq_proof) {
101
+ const zkResult = quantumCrypto.verifyZKProof(intent.pq_proof, intent.id);
102
+ isProofValid = zkResult.verified === true;
103
+ if (!isProofValid) {
104
+ console.log(`[APO-ZK] [${requestId}] ZK proof denied: ${zkResult.reason}${zkResult.simulated ? ' (simulated)' : ''}`);
105
+ }
106
+ }
100
107
 
101
108
  if (isProofValid) {
102
109
  console.log(`[APO-BYPASS] [${requestId}] Tier 3 'Sovereign Proof' verified (${intent.pq_proof ? 'ZK-PQ' : 'Standard'}). Overriding Blast Radius limit.`);
103
110
  // Continue to permit check
104
111
  } else {
105
- verdict = { verdict: 'DENY', reason: 'Invalid or Malformed ZK-Proof detected.', requestId };
112
+ verdict = { verdict: 'DENY', reason: 'ZK proof verification failed. Configure a verifier module or provide a valid proof.', requestId };
106
113
  this.logAudit(intent, impactScore, verdict);
107
114
  return verdict;
108
115
  }
@@ -1,26 +1,68 @@
1
1
  /**
2
2
  * MindForge v7 — Post-Quantum Agentic Security (PQAS)
3
3
  * Simulated Lattice-Based Cryptography (Dilithium-5 / Kyber-1024)
4
+ *
5
+ * @typedef {Object} ZKVerifierProvider
6
+ * @property {(proof: string, intentId: string) => {verified: boolean, reason?: string}} verify
4
7
  */
5
8
  'use strict';
6
9
 
7
10
  const crypto = require('node:crypto');
8
11
  const configManager = require('./config-manager');
9
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
+
10
23
  class QuantumCrypto {
11
24
  constructor() {
12
25
  this.providerId = configManager.get('security.provider', 'simulated-lattice');
13
- 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
+ }
14
53
  }
15
54
 
16
55
  /**
17
56
  * Returns the current active crypto provider.
18
57
  */
19
58
  getProvider() {
20
- // 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.
21
63
  return {
22
64
  id: this.providerId,
23
- pqas_enabled: this.pqasEnabled,
65
+ pqas_enabled: configManager.get('security.pqas_enabled', false),
24
66
  algorithm: 'Dilithium-5'
25
67
  };
26
68
  }
@@ -29,7 +71,10 @@ class QuantumCrypto {
29
71
  * Generates a key pair using the configured PQ provider.
30
72
  */
31
73
  async generateLatticeKeyPair() {
32
- 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();
33
78
 
34
79
  // Simulate high-entropy lattice seeds
35
80
  const seed = crypto.randomBytes(64).toString('hex');
@@ -47,21 +92,27 @@ class QuantumCrypto {
47
92
 
48
93
  /**
49
94
  * Signs data using simulated Dilithium-5.
95
+ * @returns {{ signature: string, simulated: true, algorithm: string }}
50
96
  */
51
97
  async signPQ(data, privateKey) {
52
- 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();
53
103
  if (!privateKey.startsWith('mfq7_dilithium5_priv_')) {
54
104
  throw new Error('Invalid Post-Quantum private key format.');
55
105
  }
56
106
 
57
- // Simulate the lattice-based signature overhead
58
107
  const hash = crypto.createHash('sha3-512').update(data).digest('hex');
59
108
  const salt = crypto.randomBytes(16).toString('hex');
60
-
61
- // Dilithium signatures are significantly larger than Ed25519
62
- const simulatedSignature = `pqas_sig_d5_${Buffer.from(hash + salt).toString('base64')}_${crypto.randomBytes(128).toString('base64')}`;
63
-
64
- return simulatedSignature;
109
+ const signature = `pqas_sig_d5_${Buffer.from(hash + salt).toString('base64')}_${crypto.randomBytes(128).toString('base64')}`;
110
+
111
+ return {
112
+ signature,
113
+ simulated: true,
114
+ algorithm: 'Dilithium-5'
115
+ };
65
116
  }
66
117
 
67
118
  /**
@@ -69,10 +120,11 @@ class QuantumCrypto {
69
120
  */
70
121
  verifyPQ(data, signature, publicKey) {
71
122
  if (!publicKey.startsWith('mfq7_dilithium5_pub_')) return false;
72
- if (!signature.startsWith('pqas_sig_d5_')) return false;
123
+ const sig = typeof signature === 'object' && signature.signature ? signature.signature : signature;
124
+ if (!sig.startsWith('pqas_sig_d5_')) return false;
73
125
 
74
126
  try {
75
- const parts = signature.split('_');
127
+ const parts = sig.split('_');
76
128
  const blob = Buffer.from(parts[3], 'base64').toString('utf8');
77
129
  const hashInSig = blob.slice(0, 128);
78
130
 
@@ -86,33 +138,48 @@ class QuantumCrypto {
86
138
  }
87
139
 
88
140
  /**
89
- * Generates a simulated ZK-Proof of policy adherence.
90
- * 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_`.
91
148
  */
92
149
  generateZKProof(intent, result) {
93
150
  const proofPayload = JSON.stringify({
94
151
  intent: intent.id,
95
152
  verdict: result.verdict,
96
153
  timestamp: Date.now(),
97
- entropy: crypto.randomBytes(16).toString('hex')
154
+ entropy: crypto.randomBytes(16).toString('hex'),
155
+ simulated: true
98
156
  });
99
157
 
100
158
  const hash = crypto.createHash('sha256').update(proofPayload).digest('hex');
101
- 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')}`;
102
162
  }
103
163
 
104
164
  verifyZKProof(proof, intentId) {
105
- if (!proof.startsWith('zkp_v1_')) return false;
106
- // SECURITY: Real ZK verification is not yet implemented.
107
- // Governance gate MUST block by default — fail-closed.
108
- console.warn(
109
- `[SECURITY][quantum-crypto] verifyZKProof is a STUB — real ZK verification not yet implemented. ` +
110
- `Blocking proof for intent="${intentId}". All governance checks will fail until a real verifier is integrated.`
111
- );
112
- throw new Error(
113
- 'ZK proof verification is not implemented. Governance gate denies by default. ' +
114
- 'Integrate a real ZK verifier (e.g., snarkjs/circom) before enabling this path.'
115
- );
165
+ if (!proof || !proof.startsWith('zkp_v1_')) {
166
+ return { verified: false, reason: 'invalid_proof_format' };
167
+ }
168
+
169
+ try {
170
+ const verifierModule = configManager.get('security.zk_verifier_module');
171
+ if (verifierModule) {
172
+ const verifier = require(verifierModule);
173
+ return verifier.verify(proof, intentId);
174
+ }
175
+ } catch (e) { /* no external verifier configured */ }
176
+
177
+ return {
178
+ verified: false,
179
+ reason: 'no_verifier_configured',
180
+ simulated: true,
181
+ message: 'ZK proof verification requires an external verifier module (e.g., snarkjs/circom). Configure via security.zk_verifier_module in config.json.'
182
+ };
116
183
  }
117
184
  }
118
185
 
@@ -18,6 +18,7 @@ class RBACManager {
18
18
  'did:mindforge:researcher': ['knowledge-detective'],
19
19
  'did:mindforge:tool': ['system-operator']
20
20
  };
21
+ this.temporaryElevations = new Map(); // key: `${did}:${role}`, value: { timer, expiresAt }
21
22
  }
22
23
 
23
24
  /**
@@ -84,10 +85,72 @@ class RBACManager {
84
85
  fs.writeFileSync(this.rolesPath, JSON.stringify(current, null, 2));
85
86
  }
86
87
 
88
+ /**
89
+ * Temporarily elevates an agent to a role for a limited duration.
90
+ * The elevation auto-expires after ttlMs milliseconds.
91
+ * @param {string} did - Agent DID
92
+ * @param {string} role - Role to temporarily grant
93
+ * @param {number} ttlMs - Time-to-live in milliseconds (default: 1 hour)
94
+ */
95
+ elevateRole(did, role, ttlMs = 3600000) {
96
+ const key = `${did}:${role}`;
97
+
98
+ // Clear existing elevation if any
99
+ if (this.temporaryElevations.has(key)) {
100
+ clearTimeout(this.temporaryElevations.get(key).timer);
101
+ }
102
+
103
+ const timer = setTimeout(() => {
104
+ this.temporaryElevations.delete(key);
105
+ }, ttlMs);
106
+
107
+ // Prevent timer from keeping process alive
108
+ if (timer.unref) timer.unref();
109
+
110
+ this.temporaryElevations.set(key, {
111
+ timer,
112
+ expiresAt: Date.now() + ttlMs
113
+ });
114
+
115
+ return { did, role, expiresAt: Date.now() + ttlMs };
116
+ }
117
+
118
+ /**
119
+ * Checks if an agent currently has a temporary role elevation.
120
+ * @param {string} did - Agent DID
121
+ * @param {string} role - Role to check
122
+ */
123
+ hasTemporaryElevation(did, role) {
124
+ const key = `${did}:${role}`;
125
+ const elevation = this.temporaryElevations.get(key);
126
+ if (!elevation) return false;
127
+ if (Date.now() > elevation.expiresAt) {
128
+ clearTimeout(elevation.timer);
129
+ this.temporaryElevations.delete(key);
130
+ return false;
131
+ }
132
+ return true;
133
+ }
134
+
135
+ /**
136
+ * Revokes a temporary elevation before its TTL expires.
137
+ * @param {string} did - Agent DID
138
+ * @param {string} role - Role to revoke
139
+ */
140
+ revokeElevation(did, role) {
141
+ const key = `${did}:${role}`;
142
+ const elevation = this.temporaryElevations.get(key);
143
+ if (elevation) {
144
+ clearTimeout(elevation.timer);
145
+ this.temporaryElevations.delete(key);
146
+ }
147
+ }
148
+
87
149
  /**
88
150
  * Checks if an agent has a specific permission based on their roles.
89
- * @param {string} did
90
- * @param {string} permission
151
+ * Also checks temporary elevations.
152
+ * @param {string} did
153
+ * @param {string} permission
91
154
  */
92
155
  hasPermission(did, permission) {
93
156
  const roles = this.getRoles(did);
@@ -99,9 +162,18 @@ class RBACManager {
99
162
  'guest-agent': ['read_src']
100
163
  };
101
164
 
165
+ // Check static roles first
102
166
  for (const role of roles) {
103
167
  if (PERMISSION_MAP[role]?.includes(permission)) return true;
104
168
  }
169
+
170
+ // Check temporary elevations
171
+ for (const [role, permissions] of Object.entries(PERMISSION_MAP)) {
172
+ if (permissions.includes(permission) && this.hasTemporaryElevation(did, role)) {
173
+ return true;
174
+ }
175
+ }
176
+
105
177
  return false;
106
178
  }
107
179
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  const crypto = require('node:crypto');
7
7
  const { promisify } = require('node:util');
8
+ const configManager = require('./config-manager');
8
9
 
9
10
  const generateKeyPair = promisify(crypto.generateKeyPair);
10
11
 
@@ -122,9 +123,10 @@ class QuantumSafeKeyProvider extends KeyProvider {
122
123
  async sign(did, data) {
123
124
  const record = this.keyStore.get(did);
124
125
  if (!record) throw new Error(`PQ record not found for ${did}`);
125
-
126
+
126
127
  console.log(`[PQAS-DILITHIUM] Delegating signature to lattice enclave [DID: ${did}]`);
127
- return await this.quantumCrypto.signPQ(data, record.privateKey);
128
+ const result = await this.quantumCrypto.signPQ(data, record.privateKey);
129
+ return result;
128
130
  }
129
131
 
130
132
  async rotate(did) {
@@ -146,26 +148,63 @@ class ZTAIManager {
146
148
  };
147
149
  }
148
150
 
151
+ /**
152
+ * Selects a key provider for a given trust tier.
153
+ *
154
+ * UC-24: All tiers route to REAL crypto by default — Tier 1-2 use the
155
+ * in-memory Ed25519 provider, Tier 3+ use the (simulated-HSM) enclave
156
+ * provider which also signs with REAL Ed25519. The SIMULATED post-quantum
157
+ * lattice provider ('quantum') is NEVER selected on the live trust path
158
+ * unless the operator has explicitly opted into the demo via
159
+ * experimental.pqc_demo. This keeps false-assurance signatures off the
160
+ * default trust path while preserving the demo for explicit exploration.
161
+ *
162
+ * @param {number} tier - Trust tier (1-4)
163
+ * @returns {'local'|'enclave'|'quantum'}
164
+ */
165
+ _selectProvider(tier) {
166
+ const pqcDemo = configManager.get('experimental.pqc_demo', false) === true;
167
+
168
+ // Tier 4+ MAY use the simulated lattice provider, but only when the
169
+ // operator has explicitly enabled the PQC demo. Otherwise it falls back to
170
+ // the real-Ed25519 enclave provider so the trust path stays verifiable.
171
+ if (tier >= 4) {
172
+ return pqcDemo ? 'quantum' : 'enclave';
173
+ }
174
+
175
+ // Tier 3 agents use the SecureEnclaveProvider (real Ed25519).
176
+ return tier >= 3 ? 'enclave' : 'local';
177
+ }
178
+
149
179
  /**
150
180
  * Registers a new agent and assigns a provider based on Trust Tier.
181
+ * @param {string} persona - Agent persona identifier
182
+ * @param {number} tier - Trust tier (1-4)
183
+ * @param {string|null} sessionId - Optional session scope for isolation
151
184
  */
152
- async registerAgent(persona, tier = 1) {
185
+ async registerAgent(persona, tier = 1, sessionId = null) {
153
186
  const uuid = crypto.randomUUID();
154
187
  const did = `did:mindforge:${uuid}`;
155
-
156
- // Tier 3 agents use the SecureEnclaveProvider
157
- const providerType = tier >= 3 ? 'enclave' : 'local';
188
+
189
+ const providerType = this._selectProvider(tier);
158
190
  const provider = this.providers[providerType];
159
-
191
+
160
192
  const publicKeyPEM = await provider.generate(did);
161
193
 
162
- this.agentRegistry.set(did, {
194
+ const agentData = {
163
195
  publicKey: publicKeyPEM,
164
196
  persona,
165
197
  tier,
166
198
  providerType,
167
199
  createdAt: new Date().toISOString()
168
- });
200
+ };
201
+
202
+ // Store sessionId if provided for session-scoped isolation
203
+ if (sessionId) {
204
+ agentData.sessionId = sessionId;
205
+ }
206
+
207
+ this.agentRegistry.set(did, agentData);
169
208
 
170
209
  return did;
171
210
  }
@@ -219,6 +258,37 @@ class ZTAIManager {
219
258
  return this.agentRegistry.get(did);
220
259
  }
221
260
 
261
+ /**
262
+ * Returns all agents registered under a specific session.
263
+ * @param {string} sessionId - Session identifier to filter by
264
+ */
265
+ getSessionAgents(sessionId) {
266
+ const results = [];
267
+ for (const [did, agent] of this.agentRegistry.entries()) {
268
+ if (agent.sessionId === sessionId) {
269
+ results.push({ did, ...agent });
270
+ }
271
+ }
272
+ return results;
273
+ }
274
+
275
+ /**
276
+ * Revokes all agents belonging to a session. Used for session cleanup.
277
+ * @param {string} sessionId - Session identifier
278
+ */
279
+ revokeSessionAgents(sessionId) {
280
+ const dids = [];
281
+ for (const [did, agent] of this.agentRegistry.entries()) {
282
+ if (agent.sessionId === sessionId) {
283
+ dids.push(did);
284
+ }
285
+ }
286
+ for (const did of dids) {
287
+ this.revokeAgent(did);
288
+ }
289
+ return dids;
290
+ }
291
+
222
292
  /**
223
293
  * Specialized signing for FinOps budget decisions (Pillar V).
224
294
  */
@@ -21,17 +21,16 @@ class HindsightInjector {
21
21
  // 1. Rollback .planning directory
22
22
  TemporalHub.rollbackTo(auditId);
23
23
 
24
- // 2. Append the "Hindsight" event to AUDIT.jsonl
24
+ // 2. Append the "Hindsight" event to AUDIT.jsonl via the unified, hash-chained,
25
+ // durable append (UC-04b) so this entry links into the single verifiable chain.
26
+ const { appendAuditEntrySync } = require('./autonomous/audit-writer');
25
27
  const auditPath = path.join(process.cwd(), '.planning', 'AUDIT.jsonl');
26
- const hindsightEvent = {
27
- id: require('crypto').randomBytes(8).toString('hex'),
28
- timestamp: new Date().toISOString(),
28
+ const hindsightEvent = appendAuditEntrySync(auditPath, {
29
29
  event: 'hindsight_injected',
30
30
  target_id: auditId,
31
31
  description: fixDescription,
32
32
  agent: 'temporal-hub'
33
- };
34
- fs.appendFileSync(auditPath, JSON.stringify(hindsightEvent) + '\n');
33
+ });
35
34
 
36
35
  // 3. Mark the state as "ready_for_regeneration"
37
36
  const statePath = path.join(process.cwd(), '.planning', 'auto-state.json');
@@ -43,9 +42,9 @@ class HindsightInjector {
43
42
  }
44
43
 
45
44
  // 4. Capture the new state immediately
46
- TemporalHub.captureState(hindsightEvent.id, {
47
- event: 'hindsight_injected',
48
- target_id: auditId
45
+ await TemporalHub.captureState(hindsightEvent.id, {
46
+ event: 'hindsight_injected',
47
+ target_id: auditId
49
48
  });
50
49
 
51
50
  return { success: true, event: hindsightEvent };