@atlasnomos/atlas 1.1.14 → 10.0.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 (35) hide show
  1. package/README.md +113 -57
  2. package/atlas.js +0 -1
  3. package/bin/atlas +1 -1
  4. package/config/sentinel-keys.json +15 -0
  5. package/config/trust-boundaries.json +64 -0
  6. package/index.js +10 -0
  7. package/package.json +7 -3
  8. package/src/bridge/RingBridge.js +0 -2
  9. package/src/core/SecretRedactor.js +81 -1
  10. package/src/enforcement/RuntimeSandbox.js +155 -26
  11. package/src/interface/cli/commands/BuildCommand.js +68 -35
  12. package/src/kernel/automation/AutomationRegistry.js +159 -0
  13. package/src/kernel/automation/AutomationSchema.js +109 -0
  14. package/src/kernel/automation/AutomationVerifier.js +135 -0
  15. package/src/kernel/automation/LastRunLedger.js +220 -0
  16. package/src/kernel/boot/AgentProcess.js +0 -1
  17. package/src/kernel/boot/Bootstrap.js +105 -0
  18. package/src/kernel/boot/SystemInitializer.js +6 -1
  19. package/src/kernel/constitution/SentinelInterface.js +180 -3
  20. package/src/kernel/constitution/invariants/automation.invariants.js +126 -0
  21. package/src/kernel/constitution/invariants/frequency.invariants.js +77 -0
  22. package/src/kernel/constitution/invariants/registry.invariants.js +70 -0
  23. package/src/kernel/crypto/DirectoryHasher.js +101 -0
  24. package/src/kernel/io/AuditSink.js +95 -1
  25. package/src/kernel/io/DecisionTrace.js +19 -0
  26. package/src/kernel/io/FeedbackLearner.js +6 -1
  27. package/src/kernel/io/HashChainedLog.js +57 -3
  28. package/src/kernel/io/TelemetryService.js +6 -1
  29. package/src/kernel/modes/ActiveMode.js +158 -0
  30. package/src/kernel/modes/dev.js +33 -0
  31. package/src/kernel/modes/fortress.js +33 -0
  32. package/src/kernel/modes/prod.js +41 -0
  33. package/src/kernel/security/ConfigDecryptor.js +61 -7
  34. package/src/kernel/sentinel/KeyRegistry.js +399 -0
  35. package/src/monitoring/CostTracker.js +70 -0
@@ -21,6 +21,97 @@ const { execSync, spawn } = require('child_process');
21
21
  const fs = require('fs');
22
22
  const path = require('path');
23
23
  const { BlockError } = require('./PipelinePrimitives');
24
+ const SentinelInterface = require('../kernel/constitution/SentinelInterface');
25
+
26
+ // Hash-chained audit log for SANDBOX_VIOLATION events
27
+ let HashChainedLog = null;
28
+ let _sandboxEventLog = null;
29
+ const SANDBOX_EVENT_LOG_PATH = path.join(process.cwd(), '.atlas', 'audit', 'sandbox_events.chainlog');
30
+
31
+ function getSandboxEventLog() {
32
+ if (!HashChainedLog) {
33
+ try {
34
+ HashChainedLog = require('../kernel/io/HashChainedLog').HashChainedLog;
35
+ } catch (e) {
36
+ return null;
37
+ }
38
+ }
39
+ if (!_sandboxEventLog) {
40
+ _sandboxEventLog = new HashChainedLog(SANDBOX_EVENT_LOG_PATH, {
41
+ name: 'SandboxEventLog',
42
+ panicOnFailure: false
43
+ });
44
+ try {
45
+ _sandboxEventLog.initialize();
46
+ } catch (e) {
47
+ return null;
48
+ }
49
+ }
50
+ return _sandboxEventLog;
51
+ }
52
+
53
+ /**
54
+ * Emit SANDBOX_VIOLATION event to hash-chained audit log
55
+ * @param {string} violationType - Type of violation (NETWORK, FILESYSTEM, TIMEOUT, etc.)
56
+ * @param {Object} details - Violation details
57
+ */
58
+ function emitSandboxViolation(violationType, details) {
59
+ const log = getSandboxEventLog();
60
+ if (log) {
61
+ try {
62
+ const entry = log.append({
63
+ event: 'SANDBOX_VIOLATION',
64
+ timestamp: new Date().toISOString(),
65
+ violation_type: violationType,
66
+ tier: process.env.SENTINEL_TIER || 'DEV',
67
+ action: 'BLOCK',
68
+ ...details
69
+ });
70
+
71
+ // PROD HARDENING: Anchor to main audit chain
72
+ try {
73
+ const Sentinel = SentinelInterface.SentinelInterface || SentinelInterface;
74
+ if (Sentinel.getInstance) {
75
+ Sentinel.getInstance().logAnchor('SandboxEventLog', entry.hash, entry.seq);
76
+ }
77
+ } catch (anchorErr) {
78
+ // Non-fatal
79
+ }
80
+ } catch (e) {
81
+ console.error('[RuntimeSandbox] Failed to emit SANDBOX_VIOLATION:', e.message);
82
+ }
83
+ }
84
+ }
85
+
86
+
87
+ /**
88
+ * H-03 FIX: Emit positive execution evidence ("Dark Matter" coverage).
89
+ * @param {Object} details - Execution details
90
+ */
91
+ function emitSandboxExecutionComplete(details) {
92
+ const log = getSandboxEventLog();
93
+ if (log) {
94
+ try {
95
+ const entry = log.append({
96
+ event: 'SANDBOX_EXECUTION_COMPLETE',
97
+ timestamp: new Date().toISOString(),
98
+ ...details
99
+ });
100
+
101
+ // Anchor to main chain
102
+ try {
103
+ const Sentinel = SentinelInterface.SentinelInterface || SentinelInterface;
104
+ if (Sentinel.getInstance) {
105
+ Sentinel.getInstance().logAnchor('SandboxEventLog', entry.hash, entry.seq);
106
+ }
107
+ } catch (anchorErr) {
108
+ // Non-fatal
109
+ }
110
+ } catch (e) {
111
+ console.error('[RuntimeSandbox] Failed to emit SANDBOX_EXECUTION_COMPLETE:', e.message);
112
+ }
113
+ }
114
+ }
24
115
 
25
116
  // ═══════════════════════════════════════════════════════════════
26
117
  // CONFIGURATION
@@ -46,11 +137,11 @@ const DOCKER_IMAGES = {
46
137
  rust: 'rust:1.75-alpine'
47
138
  };
48
139
 
49
- // Mocked API templates per language (v8.1 Enhanced)
140
+ // Mocked API templates per language (v8.1 Enhanced + H-01 Deterministic Seeding)
50
141
  const MOCKED_APIS = {
51
- javascript: `
142
+ javascript: (seed) => `
52
143
  // ATLAS Sandbox: Mocked APIs for Deterministic Execution
53
- const ATLAS_SEED = ${Date.now()};
144
+ const ATLAS_SEED = ${seed};
54
145
  let randomCounter = 0;
55
146
 
56
147
  // Mock Date to fixed timestamp
@@ -95,14 +186,14 @@ require('fs').writeFileSync = function(filepath, ...args) {
95
186
  return originalWriteFileSync(filepath, ...args);
96
187
  };
97
188
  `,
98
- python: `
189
+ python: (seed) => `
99
190
  # ATLAS Sandbox: Mocked APIs for Deterministic Execution
100
191
  import time
101
192
  import random
102
193
  import sys
103
194
  from datetime import datetime
104
195
 
105
- ATLAS_SEED = ${Date.now()}
196
+ ATLAS_SEED = ${seed}
106
197
  random_counter = 0
107
198
 
108
199
  # Mock time.time() to fixed timestamp
@@ -131,7 +222,7 @@ def _mocked_open(filepath, mode='r', *args, **kwargs):
131
222
  return _original_open(filepath, mode, *args, **kwargs)
132
223
  builtins.open = _mocked_open
133
224
  `,
134
- java: `
225
+ java: (seed) => `
135
226
  // ATLAS Sandbox: Mocked APIs for Deterministic Execution
136
227
  import java.io.*;
137
228
  import java.util.*;
@@ -139,7 +230,7 @@ import java.net.*;
139
230
  import java.nio.file.*;
140
231
 
141
232
  public class AtlasSandbox {
142
- public static final long ATLAS_SEED = ${Date.now()}L;
233
+ public static final long ATLAS_SEED = ${seed}L;
143
234
  private static int randomCounter = 0;
144
235
 
145
236
  static {
@@ -179,7 +270,7 @@ public class AtlasSandbox {
179
270
  }
180
271
  }
181
272
  `,
182
- go: `
273
+ go: (seed) => `
183
274
  // ATLAS Sandbox: Mocked APIs for Deterministic Execution
184
275
  package main
185
276
 
@@ -190,7 +281,7 @@ import (
190
281
  "os"
191
282
  )
192
283
 
193
- const ATLAS_SEED int64 = ${Date.now()}
284
+ const ATLAS_SEED int64 = ${seed}
194
285
  var randomCounter int = 0
195
286
 
196
287
  func init() {
@@ -217,12 +308,12 @@ func atlasTime() time.Time {
217
308
  return time.Unix(ATLAS_SEED/1000, 0)
218
309
  }
219
310
  `,
220
- rust: `
311
+ rust: (seed) => `
221
312
  // ATLAS Sandbox: Mocked APIs for Deterministic Execution
222
313
  use std::time::{SystemTime, UNIX_EPOCH, Duration};
223
314
  use std::f64;
224
315
 
225
- const ATLAS_SEED: i64 = ${Date.now()};
316
+ const ATLAS_SEED: i64 = ${seed};
226
317
  static mut RANDOM_COUNTER: i32 = 0;
227
318
 
228
319
  // Mock SystemTime::now()
@@ -310,13 +401,20 @@ class RuntimeSandbox {
310
401
  /**
311
402
  * Prepare code for sandboxed execution.
312
403
  * Injects mocked APIs based on language.
404
+ * @param {string} code - User code
405
+ * @param {string} language - Language
406
+ * @param {string} entrypoint - Entry point code
407
+ * @param {number} seed - Deterministic seed (H-01 FIX)
313
408
  */
314
- prepareCode(code, language, entrypoint) {
315
- const mockedApi = MOCKED_APIS[language];
316
- if (!mockedApi) {
409
+ prepareCode(code, language, entrypoint, seed) {
410
+ const mockedApiFn = MOCKED_APIS[language];
411
+ if (!mockedApiFn) {
317
412
  throw new Error(`Unsupported language for sandbox: ${language}`);
318
413
  }
319
414
 
415
+ // Generate mocked API with deterministic seed
416
+ const mockedApi = mockedApiFn(seed);
417
+
320
418
  // Inject mocked APIs at the beginning
321
419
  const preparedCode = mockedApi + '\n\n' + code;
322
420
 
@@ -395,16 +493,29 @@ fn main() {
395
493
  /**
396
494
  * Execute code in Docker sandbox.
397
495
  *
398
- * Returns: {
399
- * success,
400
- * stdout,
401
- * stderr,
402
- * exitCode,
403
- * execution_hash,
404
- * execution_time_ms
405
- * }
496
+ * @param {string} code - Code to execute
497
+ * @param {string} language - Language
498
+ * @param {string} entrypoint - Entry point
499
+ * @param {string} input - Input
500
+ * @param {Object} provenance - R-02 PROVENANCE (Required in PROD)
501
+ * @param {string} provenance.approval_hash
502
+ * @param {string} provenance.policy_hash
503
+ *
504
+ * Returns: { ... }
406
505
  */
407
- async execute(code, language, entrypoint = '', input = '') {
506
+ async execute(code, language, entrypoint = '', input = '', provenance = null) {
507
+ // R-02 FIX: Enforce Provenance Binding in PROD
508
+ // "EXECUTION must reference exactly one approval + policy"
509
+ if (process.env.SENTINEL_TIER !== 'DEV' && process.env.SENTINEL_TIER !== 'TEST') {
510
+ if (!provenance || !provenance.approval_hash || !provenance.policy_hash) {
511
+ const { panic } = require('../kernel/boot/HardKill'); // Lazy load
512
+ panic('EXECUTION_WITHOUT_PROVENANCE', {
513
+ reason: 'PROD execution requires explicit approval + policy binding',
514
+ language,
515
+ input_summary: input.substring(0, 32)
516
+ });
517
+ }
518
+ }
408
519
  // Check Docker availability
409
520
  const dockerCheck = this.checkDocker();
410
521
  if (!dockerCheck.available) {
@@ -427,8 +538,15 @@ fn main() {
427
538
  };
428
539
  }
429
540
 
430
- // Prepare code with mocked APIs
431
- const preparedCode = this.prepareCode(code, language, entrypoint);
541
+ // H-01 FIX: Deterministic Seed Derivation
542
+ // Seed = SHA256(Input + Code)[0..48] -> integer
543
+ // Eliminates "Seed-Gap" by binding entropy to input content.
544
+ const seedPayload = input + code;
545
+ const seedHash = crypto.createHash('sha256').update(seedPayload).digest('hex');
546
+ const seed = parseInt(seedHash.substring(0, 13), 16); // Safe integer range
547
+
548
+ // Prepare code with mocked APIs and deterministic seed
549
+ const preparedCode = this.prepareCode(code, language, entrypoint, seed);
432
550
 
433
551
  // Create temporary directory for execution
434
552
  const tempDir = path.join(this.outputDir, `exec-${Date.now()}`);
@@ -581,10 +699,21 @@ fn main() {
581
699
  execution_hash: executionHash,
582
700
  execution_time_ms: executionTimeMs,
583
701
  resource_profile: resourceProfile, // v8.1
584
- blocked: !success,
585
702
  reason: success ? null : 'SANDBOX_EXECUTION_FAILED'
586
703
  };
587
704
 
705
+ // H-03 FIX: Positive Execution Evidence
706
+ if (success) {
707
+ emitSandboxExecutionComplete({
708
+ execution_hash: executionHash,
709
+ execution_time_ms: executionTimeMs,
710
+ resource_summary: resourceProfile || { estimated_ms: executionTimeMs },
711
+ tier: process.env.SENTINEL_TIER || 'DEV',
712
+ // R-02: Bind Provenance to Evidence
713
+ provenance: provenance || { warning: 'NO_PROVENANCE_PROVIDED' }
714
+ });
715
+ }
716
+
588
717
  // v8.1: Store in execution history for replay
589
718
  this.executionHistory.push({
590
719
  timestamp: new Date().toISOString(),
@@ -29,6 +29,16 @@ class BuildCommand {
29
29
  return { success: false };
30
30
  }
31
31
 
32
+ // INSTRUMENTATION: Audit Sink (Platform Bridge)
33
+ const { getAuditSink } = require('../../../kernel/io/AuditSink');
34
+ const auditSink = getAuditSink();
35
+ // Reserve execution ID (Emits RUN_STARTED)
36
+ const reservationId = auditSink.reserve({
37
+ tool: 'atlas-build',
38
+ tenantId: process.env.SENTINEL_TENANT_ID || 'local-dev',
39
+ executionId: trace?.id || `build_${Date.now()}`
40
+ });
41
+
32
42
  // AUTO-HEALING: Capture pre-build snapshot for recovery
33
43
  await SelfHealEngine.captureSnapshot();
34
44
  const { getInstance: getSession } = require('../../../core/SessionContext');
@@ -38,48 +48,58 @@ class BuildCommand {
38
48
  console.log(` ATLAS BUILD — Full Pipeline (Session: ${session.getId().substring(0, 8)})`);
39
49
  console.log('═══════════════════════════════════════════════════════════\n');
40
50
 
41
- // 0. PREFLIGHT: Environment & Credential Check (UX Optimization)
42
- this._validateEnvironment();
43
-
44
- // NOTE: Removed ATLAS_HARDWARE_TOKEN check - local env vars are not security controls
45
- // Only Sentinel authorization provides security guarantees
46
-
47
- const { getInstance: getSentinel } = require('../../../kernel/constitution/SentinelInterface');
48
- const sentinel = getSentinel();
49
- if (!await sentinel.isHealthy()) {
50
- console.error('❌ Sentinel Unreachable. Build Aborted (Fail-Closed).');
51
- return { success: false, reason: 'Sentinel Unreachable' };
52
- }
51
+ try {
52
+ // 0. PREFLIGHT: Environment & Credential Check (UX Optimization)
53
+ this._validateEnvironment();
54
+
55
+ // NOTE: Removed ATLAS_HARDWARE_TOKEN check - local env vars are not security controls
56
+ // Only Sentinel authorization provides security guarantees
57
+
58
+ const { getInstance: getSentinel } = require('../../../kernel/constitution/SentinelInterface');
59
+ const sentinel = getSentinel();
60
+ if (!await sentinel.isHealthy()) {
61
+ console.error('❌ Sentinel Unreachable. Build Aborted (Fail-Closed).');
62
+ auditSink.abandon(reservationId, 'Sentinel Unreachable');
63
+ return { success: false, reason: 'Sentinel Unreachable' };
64
+ }
53
65
 
54
- const crypto = require('crypto');
55
- const goalHash = crypto.createHash('sha256').update(goal).digest('hex');
66
+ const crypto = require('crypto');
67
+ const goalHash = crypto.createHash('sha256').update(goal).digest('hex');
56
68
 
57
- const traceId = trace?.id || `build_${Date.now()}`;
58
- const approved = await sentinel.requestApproval(traceId, 'BUILD_PROJECT', {
59
- goal,
60
- goal_hash: goalHash,
61
- user: process.env.USERNAME || 'operator',
62
- session_id: session.getId()
63
- });
69
+ const traceId = reservationId; // Use reservation ID as trace ID for consistency
70
+ const approved = await sentinel.requestApproval(traceId, 'BUILD_PROJECT', {
71
+ goal,
72
+ goal_hash: goalHash,
73
+ user: process.env.USERNAME || 'operator',
74
+ session_id: session.getId()
75
+ });
64
76
 
65
- if (!approved.approved && !approved.locally_permitted) {
66
- console.error('⛔ Build DENIED by Sentinel Policy.');
67
- return { success: false, reason: 'Sentinel Denied' };
68
- }
77
+ // INSTRUMENTATION: Record Decision (Emits DECISION_MADE)
78
+ auditSink.recordDecision(
79
+ reservationId,
80
+ approved.approved || approved.locally_permitted ? 'AUTHORIZED' : 'BLOCKED',
81
+ 'POLICY-BUILD-001',
82
+ approved.attested ? (approved.token?.key_id || 'sentinel-key') : 'local-dev-key'
83
+ );
84
+
85
+ if (!approved.approved && !approved.locally_permitted) {
86
+ console.error('⛔ Build DENIED by Sentinel Policy.');
87
+ auditSink.abandon(reservationId, 'Sentinel Denied');
88
+ return { success: false, reason: 'Sentinel Denied' };
89
+ }
69
90
 
70
91
 
71
- // V2: Display attestation status
72
- if (approved.attested) {
73
- console.log('✅ Sentinel Approved (ATTESTED). Proceeding with Build Pipeline.\n');
74
- } else if (approved.locally_permitted) {
75
- console.log('⚠️ Local Governance Active (UNATTESTED). Proceeding locally.\n');
76
- } else {
77
- console.log('✅ Sentinel Approved (NON-ATTESTED). Proceeding in ' + this.tierManager.getTier() + ' mode.\n');
78
- }
92
+ // V2: Display attestation status
93
+ if (approved.attested) {
94
+ console.log('✅ Sentinel Approved (ATTESTED). Proceeding with Build Pipeline.\n');
95
+ } else if (approved.locally_permitted) {
96
+ console.log('⚠️ Local Governance Active (UNATTESTED). Proceeding locally.\n');
97
+ } else {
98
+ console.log('✅ Sentinel Approved (NON-ATTESTED). Proceeding in ' + this.tierManager.getTier() + ' mode.\n');
99
+ }
79
100
 
80
- console.log('🧠 Engaging Hive Mind (Ring-2 Process B)...');
101
+ console.log('🧠 Engaging Hive Mind (Ring-2 Process B)...');
81
102
 
82
- try {
83
103
  // 1. PLANNING
84
104
  console.log('📋 PlannerAgent Active. Decomposing Goal...');
85
105
 
@@ -93,6 +113,7 @@ class BuildCommand {
93
113
 
94
114
  if (!planResult.success || !planResult.plan) {
95
115
  console.error('❌ Planning failed:', planResult.error);
116
+ auditSink.abandon(reservationId, 'Planning Failed');
96
117
  return { success: false, reason: 'Planning Failed' };
97
118
  }
98
119
 
@@ -106,6 +127,7 @@ class BuildCommand {
106
127
 
107
128
  if (!genResult.success) {
108
129
  console.error('❌ Generation failed:', genResult.error);
130
+ auditSink.abandon(reservationId, 'Generation Failed');
109
131
  return { success: false, reason: 'Generation Failed' };
110
132
  }
111
133
 
@@ -195,12 +217,14 @@ class BuildCommand {
195
217
 
196
218
  if (!commitApproval.approved && !commitApproval.locally_permitted) {
197
219
  console.error('⛔ COMMIT DENIED by Sentinel. Integrity check failed or unauthorized.');
220
+ auditSink.abandon(reservationId, 'Commit Denied');
198
221
  return { success: false, reason: 'Commit Denied' };
199
222
  }
200
223
 
201
224
  // Verify the signature on the commit matches the hash [Only in attested mode]
202
225
  if (commitApproval.attested && commitApproval.payload.metadata?.artifact_hash !== artifactHash) {
203
226
  console.error('❌ CRITICAL: Sentinel signed a DIFFERENT hash than our candidate! Possible interception.');
227
+ auditSink.abandon(reservationId, 'Integrity Mismatch');
204
228
  return { success: false, reason: 'Integrity Mismatch' };
205
229
  }
206
230
 
@@ -288,6 +312,14 @@ class BuildCommand {
288
312
  // V2: Display attestation summary
289
313
  this.attestationManager.displayResult(commitApproval);
290
314
 
315
+ // INSTRUMENTATION: Complete Audit
316
+ auditSink.complete(reservationId, {
317
+ result: 'SUCCESS',
318
+ cost: 0,
319
+ model: 'mcp-router',
320
+ role: 'builder'
321
+ });
322
+
291
323
  return { success: true, attested: commitApproval.attested };
292
324
 
293
325
  } catch (persistError) {
@@ -298,6 +330,7 @@ class BuildCommand {
298
330
 
299
331
  } catch (e) {
300
332
  console.error('❌ Agent Execution Protocol Failed:', e.message);
333
+ auditSink.abandon(reservationId, `Agent Crash: ${e.message}`);
301
334
  return { success: false, reason: 'Agent Crash' };
302
335
  }
303
336
  }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * ATLAS v10 — Automation Registry (AURORA)
3
+ *
4
+ * PURPOSE: Load and maintain active Automation Grants.
5
+ *
6
+ * SECURITY INVARIANTS:
7
+ * - PROD-ONLY: Construction throws in DEV.
8
+ * - Immutable: Grants cannot be added at runtime (except via Sentinel Push - future).
9
+ * - Verified: All loaded grants must pass Sentinel Signature check.
10
+ *
11
+ * @module kernel/automation/AutomationRegistry
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+ const { getActiveMode, isProdOrHigher } = require('../modes/ActiveMode');
18
+ const { panic } = require('../boot/HardKill');
19
+ const SentinelInterface = require('../constitution/SentinelInterface');
20
+ const { AUTOMATION_EVENTS } = require('./AutomationSchema');
21
+
22
+ const GRANTS_PATH = path.join(process.cwd(), '.atlas', 'automation', 'grants.json');
23
+
24
+ class AutomationRegistry {
25
+ constructor() {
26
+ // INVARIANT: Registry is PROD-only
27
+ if (!isProdOrHigher()) {
28
+ throw new Error('DEV_MODE_VIOLATION: AutomationRegistry cannot be instantiated in DEV.');
29
+ }
30
+
31
+ this._grants = new Map(); // grant_id -> Grant
32
+ this._initialized = false;
33
+ }
34
+
35
+ /**
36
+ * Initialize registry by loading grants from trusted storage.
37
+ * @returns {Promise<void>}
38
+ */
39
+ async initialize() {
40
+ if (this._initialized) return;
41
+
42
+ if (!fs.existsSync(GRANTS_PATH)) {
43
+ // No grants file implies no automation. That's fine.
44
+ this._initialized = true;
45
+ return;
46
+ }
47
+
48
+ let rawGrants = [];
49
+ try {
50
+ rawGrants = JSON.parse(fs.readFileSync(GRANTS_PATH, 'utf8'));
51
+ } catch (e) {
52
+ panic('AUTOMATION_CONFIG_CORRUPT', { error: e.message });
53
+ }
54
+
55
+ if (!Array.isArray(rawGrants)) {
56
+ panic('AUTOMATION_CONFIG_INVALID', { reason: 'Root must be array' });
57
+ }
58
+
59
+ const sentinel = SentinelInterface.getInstance();
60
+
61
+ for (const grant of rawGrants) {
62
+ // 1. Verify Trust
63
+ const valid = await sentinel.verifyAutomationGrant(grant);
64
+ if (!valid) {
65
+ panic('AUTOMATION_GRANT_INVALID', { grantId: grant.grant_id });
66
+ }
67
+
68
+ // 2. Check Expiry
69
+ const now = new Date();
70
+ const expiresAt = new Date(grant.expires_at);
71
+ if (now > expiresAt) {
72
+ console.warn(`[Automation] Skipping expired grant: ${grant.grant_id}`);
73
+ continue;
74
+ }
75
+
76
+ // 3. Register
77
+ this._grants.set(grant.grant_id, Object.freeze(grant));
78
+
79
+ // 4. Log Event
80
+ sentinel.logAudit({
81
+ event: AUTOMATION_EVENTS.REGISTERED,
82
+ grantId: grant.grant_id,
83
+ scope: grant.scope
84
+ });
85
+ }
86
+
87
+ // 5. Compute Merkle Root (Registry Sealing)
88
+ // Sort grant IDs to ensure deterministic root
89
+ const sortedIds = Array.from(this._grants.keys()).sort();
90
+ const grantHashes = sortedIds.map(id => {
91
+ const grant = this._grants.get(id);
92
+ // Hash GrantID + Signature for binding
93
+ return crypto.createHash('sha256')
94
+ .update(`${grant.grant_id}:${grant.sentinel_signature}`)
95
+ .digest('hex');
96
+ });
97
+
98
+ const merkleRoot = grantHashes.reduce((root, hash) => {
99
+ return crypto.createHash('sha256').update(root + hash).digest('hex');
100
+ }, '0000000000000000000000000000000000000000000000000000000000000000'); // Genesis Zero
101
+
102
+ // 6. Seal Registry
103
+ sentinel.logAudit({
104
+ event: 'AUTOMATION_REGISTRY_SEALED',
105
+ active_grants: this._grants.size,
106
+ merkle_root: merkleRoot,
107
+ timestamp: new Date().toISOString()
108
+ });
109
+
110
+ console.log(`[AutomationRegistry] Initialized with ${this._grants.size} active grants. Registry Sealed.`);
111
+
112
+ // v10.2: Emit AUTHORITY_SNAPSHOT (Snapshot Authority Model)
113
+ // Grants are valid for session lifetime. Revocation applies at next boot.
114
+ sentinel.logAudit({
115
+ event: 'AUTOMATION_AUTHORITY_SNAPSHOT',
116
+ active_grants: this._grants.size,
117
+ grant_ids: Array.from(this._grants.keys()),
118
+ merkle_root: merkleRoot,
119
+ session_id: `BOOT_${Date.now()}`,
120
+ model: 'SNAPSHOT', // Revocation applies next boot
121
+ timestamp: new Date().toISOString()
122
+ });
123
+
124
+ this._initialized = true;
125
+ }
126
+
127
+ /**
128
+ * Get a grant by ID.
129
+ * @param {string} grantId
130
+ * @returns {Object|undefined}
131
+ */
132
+ getGrant(grantId) {
133
+ return this._grants.get(grantId);
134
+ }
135
+
136
+ /**
137
+ * list active grants (for Verifier)
138
+ */
139
+ getAllGrants() {
140
+ return Array.from(this._grants.values());
141
+ }
142
+ }
143
+
144
+ // Singleton (Lazy)
145
+ let _instance = null;
146
+
147
+ function getInstance() {
148
+ if (!_instance) {
149
+ if (isProdOrHigher()) {
150
+ _instance = new AutomationRegistry();
151
+ } else {
152
+ // In DEV, accessing the instance is a violation
153
+ throw new Error('DEV_MODE_VIOLATION: Accessing AutomationRegistry in DEV');
154
+ }
155
+ }
156
+ return _instance;
157
+ }
158
+
159
+ module.exports = { AutomationRegistry, getInstance };