@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.
- package/README.md +113 -57
- package/atlas.js +0 -1
- package/bin/atlas +1 -1
- package/config/sentinel-keys.json +15 -0
- package/config/trust-boundaries.json +64 -0
- package/index.js +10 -0
- package/package.json +7 -3
- package/src/bridge/RingBridge.js +0 -2
- package/src/core/SecretRedactor.js +81 -1
- package/src/enforcement/RuntimeSandbox.js +155 -26
- package/src/interface/cli/commands/BuildCommand.js +68 -35
- package/src/kernel/automation/AutomationRegistry.js +159 -0
- package/src/kernel/automation/AutomationSchema.js +109 -0
- package/src/kernel/automation/AutomationVerifier.js +135 -0
- package/src/kernel/automation/LastRunLedger.js +220 -0
- package/src/kernel/boot/AgentProcess.js +0 -1
- package/src/kernel/boot/Bootstrap.js +105 -0
- package/src/kernel/boot/SystemInitializer.js +6 -1
- package/src/kernel/constitution/SentinelInterface.js +180 -3
- package/src/kernel/constitution/invariants/automation.invariants.js +126 -0
- package/src/kernel/constitution/invariants/frequency.invariants.js +77 -0
- package/src/kernel/constitution/invariants/registry.invariants.js +70 -0
- package/src/kernel/crypto/DirectoryHasher.js +101 -0
- package/src/kernel/io/AuditSink.js +95 -1
- package/src/kernel/io/DecisionTrace.js +19 -0
- package/src/kernel/io/FeedbackLearner.js +6 -1
- package/src/kernel/io/HashChainedLog.js +57 -3
- package/src/kernel/io/TelemetryService.js +6 -1
- package/src/kernel/modes/ActiveMode.js +158 -0
- package/src/kernel/modes/dev.js +33 -0
- package/src/kernel/modes/fortress.js +33 -0
- package/src/kernel/modes/prod.js +41 -0
- package/src/kernel/security/ConfigDecryptor.js +61 -7
- package/src/kernel/sentinel/KeyRegistry.js +399 -0
- 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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
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
|
|
316
|
-
if (!
|
|
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
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
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
|
-
//
|
|
431
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
66
|
+
const crypto = require('crypto');
|
|
67
|
+
const goalHash = crypto.createHash('sha256').update(goal).digest('hex');
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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 };
|