@atlasnomos/atlas 1.1.15 → 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/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/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/Bootstrap.js +105 -0
- 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/HashChainedLog.js +57 -3
- 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/sentinel/KeyRegistry.js +399 -0
- package/src/monitoring/CostTracker.js +70 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAS v10 — Automation Schema (AURORA)
|
|
3
|
+
*
|
|
4
|
+
* PURPOSE: Define the immutable schema for Standing Automation Grants.
|
|
5
|
+
*
|
|
6
|
+
* SECURITY INVARIANTS:
|
|
7
|
+
* - Grants are strictly typed and Sentinel-signed.
|
|
8
|
+
* - Time-bounded (issued_at, expires_at).
|
|
9
|
+
* - Scope-limited (frequency, cost, runtime).
|
|
10
|
+
* - PROD-ONLY: Validating this schema in DEV is prohibited.
|
|
11
|
+
*
|
|
12
|
+
* @module kernel/automation/AutomationSchema
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { getActiveMode, isProdOrHigher } = require('../modes/ActiveMode');
|
|
16
|
+
const { panic } = require('../boot/HardKill');
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════
|
|
19
|
+
// EVENTS
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
const AUTOMATION_EVENTS = Object.freeze({
|
|
23
|
+
REGISTERED: 'AUTOMATION_REGISTERED',
|
|
24
|
+
TRIGGERED: 'AUTOMATION_TRIGGERED',
|
|
25
|
+
COMPLETE: 'AUTOMATION_EXECUTION_COMPLETE',
|
|
26
|
+
REVOKED: 'AUTOMATION_REVOKED',
|
|
27
|
+
EXPIRED: 'AUTOMATION_EXPIRED'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════
|
|
31
|
+
// SCHEMA VALIDATION
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
class AutomationSchema {
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate that a plain object matches the AutomationGrant schema.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} grant - The grant object to validate
|
|
40
|
+
* @throws {Error} If validation fails or called in DEV
|
|
41
|
+
*/
|
|
42
|
+
static validateGrant(grant) {
|
|
43
|
+
// INVARIANT: Automation logic is PROD-only via Schema
|
|
44
|
+
if (!isProdOrHigher()) {
|
|
45
|
+
// In DEV, this class functions exist but usage logic usually blocks higher up.
|
|
46
|
+
// However, if we reach here in DEV, we must reject strict validation
|
|
47
|
+
// or simple return false/throw to prevent "accidental" usage.
|
|
48
|
+
// Per prompt: "DEV MODE MUST NOT ... VERIFY ... AUTOMATIONS"
|
|
49
|
+
// So we throw.
|
|
50
|
+
throw new Error('DEV_MODE_VIOLATION: Automation Schema validation is prohibited in DEV.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!grant || typeof grant !== 'object') {
|
|
54
|
+
throw new Error('Invalid Grant: Must be an object');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const requiredFields = [
|
|
58
|
+
'grant_id',
|
|
59
|
+
'scope',
|
|
60
|
+
'trigger',
|
|
61
|
+
'policy_hash',
|
|
62
|
+
'issued_at',
|
|
63
|
+
'expires_at',
|
|
64
|
+
'sentinel_key_id',
|
|
65
|
+
'sentinel_signature'
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const field of requiredFields) {
|
|
69
|
+
if (!grant[field]) {
|
|
70
|
+
throw new Error(`Invalid Grant: Missing field '${field}'`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate Scope
|
|
75
|
+
this._validateScope(grant.scope);
|
|
76
|
+
|
|
77
|
+
// Validate Trigger
|
|
78
|
+
this._validateTrigger(grant.trigger);
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static _validateScope(scope) {
|
|
84
|
+
if (!scope || typeof scope !== 'object') throw new Error('Invalid Scope');
|
|
85
|
+
|
|
86
|
+
if (!Array.isArray(scope.allowed_actions)) throw new Error('Invalid Scope: allowed_actions must be array');
|
|
87
|
+
if (typeof scope.max_cost !== 'number') throw new Error('Invalid Scope: max_cost must be number');
|
|
88
|
+
if (typeof scope.max_runtime_ms !== 'number') throw new Error('Invalid Scope: max_runtime_ms must be number');
|
|
89
|
+
|
|
90
|
+
// Frequency is ISO duration string, e.g. "PT1H"
|
|
91
|
+
if (typeof scope.max_frequency !== 'string') throw new Error('Invalid Scope: max_frequency must be ISO string');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static _validateTrigger(trigger) {
|
|
95
|
+
if (!trigger || typeof trigger !== 'object') throw new Error('Invalid Trigger');
|
|
96
|
+
|
|
97
|
+
const validTypes = ['TIME', 'EVENT', 'STATE', 'SENTINEL'];
|
|
98
|
+
if (!validTypes.includes(trigger.type)) {
|
|
99
|
+
throw new Error(`Invalid Trigger: type must be one of ${validTypes.join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof trigger.condition !== 'string') throw new Error('Invalid Trigger: condition must be string');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
AutomationSchema,
|
|
108
|
+
AUTOMATION_EVENTS
|
|
109
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAS v10.2 — Automation Verifier (AURORA+)
|
|
3
|
+
*
|
|
4
|
+
* PURPOSE: Enforce scope and invariants for Automation Execution.
|
|
5
|
+
*
|
|
6
|
+
* SECURITY INVARIANTS:
|
|
7
|
+
* - Policy Hash Match: Grant must match current active policy.
|
|
8
|
+
* - Frequency Limit: Kernel-enforced via LastRunLedger (v10.2).
|
|
9
|
+
* - Cost Limit: Check predicted cost usage.
|
|
10
|
+
*
|
|
11
|
+
* @module kernel/automation/AutomationVerifier
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { isProdOrHigher } = require('../modes/ActiveMode');
|
|
15
|
+
const { panic } = require('../boot/HardKill');
|
|
16
|
+
const SentinelInterface = require('../constitution/SentinelInterface');
|
|
17
|
+
const { AUTOMATION_EVENTS } = require('./AutomationSchema');
|
|
18
|
+
const { checkFrequencyLimit } = require('../constitution/invariants/frequency.invariants');
|
|
19
|
+
const { getInstance: getLedger } = require('./LastRunLedger');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
|
|
22
|
+
class AutomationVerifier {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Verify if a specific grant can execute NOW.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} grant - The grant object
|
|
28
|
+
* @param {Object} context - Execution context (current policy hash, etc)
|
|
29
|
+
* @returns {Object} { allowed: boolean, reason: string }
|
|
30
|
+
*/
|
|
31
|
+
static checkExecutionEligibility(grant, context = {}) {
|
|
32
|
+
if (!isProdOrHigher()) {
|
|
33
|
+
throw new Error('DEV_MODE_VIOLATION: AutomationVerifier running in DEV');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sentinel = SentinelInterface.getInstance();
|
|
37
|
+
const grantId = grant.grant_id;
|
|
38
|
+
|
|
39
|
+
// Emit Execution Attempt
|
|
40
|
+
sentinel.logAudit({
|
|
41
|
+
event: 'AUTOMATION_EXECUTION_ATTEMPT',
|
|
42
|
+
grant_id: grantId,
|
|
43
|
+
timestamp: new Date().toISOString()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 1. Policy Integrity
|
|
47
|
+
if (context.currentPolicyHash && grant.policy_hash !== context.currentPolicyHash) {
|
|
48
|
+
return { allowed: false, reason: 'POLICY_HASH_MISMATCH' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Expiry Check
|
|
52
|
+
if (new Date() > new Date(grant.expires_at)) {
|
|
53
|
+
return { allowed: false, reason: 'GRANT_EXPIRED' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Frequency Check (v10.2: Kernel-Enforced)
|
|
57
|
+
const frequencyCheck = checkFrequencyLimit(grant);
|
|
58
|
+
if (!frequencyCheck.allowed) {
|
|
59
|
+
return {
|
|
60
|
+
allowed: false,
|
|
61
|
+
reason: frequencyCheck.reason,
|
|
62
|
+
violation: frequencyCheck.violation
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Cost Check
|
|
67
|
+
if (context.estimatedCost && context.estimatedCost > grant.scope.max_cost) {
|
|
68
|
+
return { allowed: false, reason: 'COST_LIMIT_EXCEEDED' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// All checks passed
|
|
72
|
+
sentinel.logAudit({
|
|
73
|
+
event: 'AUTOMATION_EXECUTION_ACCEPTED',
|
|
74
|
+
grant_id: grantId,
|
|
75
|
+
timestamp: new Date().toISOString()
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return { allowed: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Execute automation with full lifecycle tracking.
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} grant - The grant object
|
|
85
|
+
* @param {Function} executeFn - The execution function
|
|
86
|
+
* @param {Object} context - Execution context
|
|
87
|
+
* @returns {Promise<Object>} Execution result
|
|
88
|
+
*/
|
|
89
|
+
static async executeWithTracking(grant, executeFn, context = {}) {
|
|
90
|
+
if (!isProdOrHigher()) {
|
|
91
|
+
throw new Error('DEV_MODE_VIOLATION: executeWithTracking running in DEV');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const grantId = grant.grant_id;
|
|
95
|
+
const ledger = getLedger();
|
|
96
|
+
|
|
97
|
+
// Generate approval hash for this execution
|
|
98
|
+
const approvalHash = crypto.createHash('sha256')
|
|
99
|
+
.update(`${grantId}:${Date.now()}:${JSON.stringify(context)}`)
|
|
100
|
+
.digest('hex');
|
|
101
|
+
|
|
102
|
+
// Write-Ahead: Record start BEFORE execution
|
|
103
|
+
ledger.recordExecutionStart(grantId, approvalHash);
|
|
104
|
+
|
|
105
|
+
let result;
|
|
106
|
+
let eventHash;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Execute the automation
|
|
110
|
+
result = await executeFn();
|
|
111
|
+
|
|
112
|
+
// Hash the result for audit binding
|
|
113
|
+
eventHash = crypto.createHash('sha256')
|
|
114
|
+
.update(JSON.stringify(result || {}))
|
|
115
|
+
.digest('hex');
|
|
116
|
+
|
|
117
|
+
// Mark completion
|
|
118
|
+
ledger.recordExecutionComplete(grantId, eventHash);
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// Execution failed - still record completion to unblock future runs
|
|
122
|
+
eventHash = crypto.createHash('sha256')
|
|
123
|
+
.update(`ERROR:${error.message}`)
|
|
124
|
+
.digest('hex');
|
|
125
|
+
|
|
126
|
+
ledger.recordExecutionComplete(grantId, eventHash);
|
|
127
|
+
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { result, eventHash, approvalHash };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { AutomationVerifier };
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAS v10.2 — Last-Run State Ledger (AURORA+)
|
|
3
|
+
*
|
|
4
|
+
* PURPOSE: Persistent frequency state tracking for Automation Grants.
|
|
5
|
+
*
|
|
6
|
+
* SECURITY INVARIANTS:
|
|
7
|
+
* - PROD-ONLY: Like all automation, this is blocked in DEV.
|
|
8
|
+
* - Write-Ahead: State is written BEFORE execution begins.
|
|
9
|
+
* - Atomic: Files are written via temp+rename for crash safety.
|
|
10
|
+
* - Hash-Chained: Events are logged to Sentinel audit spine.
|
|
11
|
+
*
|
|
12
|
+
* @module kernel/automation/LastRunLedger
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { isProdOrHigher } = require('../modes/ActiveMode');
|
|
19
|
+
const { panic } = require('../boot/HardKill');
|
|
20
|
+
const SentinelInterface = require('../constitution/SentinelInterface');
|
|
21
|
+
|
|
22
|
+
const STATE_DIR = path.join(process.cwd(), '.atlas', 'automation', 'state');
|
|
23
|
+
|
|
24
|
+
class LastRunLedger {
|
|
25
|
+
constructor() {
|
|
26
|
+
if (!isProdOrHigher()) {
|
|
27
|
+
throw new Error('DEV_MODE_VIOLATION: LastRunLedger cannot be instantiated in DEV.');
|
|
28
|
+
}
|
|
29
|
+
this._ensureStateDir();
|
|
30
|
+
this._sessionId = `BOOT_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_ensureStateDir() {
|
|
34
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
35
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_getStatePath(grantId) {
|
|
40
|
+
// Sanitize grantId to prevent path traversal
|
|
41
|
+
const safeId = grantId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
42
|
+
return path.join(STATE_DIR, `${safeId}.state.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse ISO-8601 Duration to milliseconds.
|
|
47
|
+
* Supports: PT1H, PT30M, PT60S, P1D
|
|
48
|
+
*
|
|
49
|
+
* @param {string} duration - ISO-8601 duration string
|
|
50
|
+
* @returns {number} milliseconds
|
|
51
|
+
*/
|
|
52
|
+
_parseDuration(duration) {
|
|
53
|
+
if (!duration || typeof duration !== 'string') return 0;
|
|
54
|
+
|
|
55
|
+
const match = duration.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/);
|
|
56
|
+
if (!match) return 0;
|
|
57
|
+
|
|
58
|
+
const days = parseInt(match[1] || '0', 10);
|
|
59
|
+
const hours = parseInt(match[2] || '0', 10);
|
|
60
|
+
const mins = parseInt(match[3] || '0', 10);
|
|
61
|
+
const secs = parseInt(match[4] || '0', 10);
|
|
62
|
+
|
|
63
|
+
return ((days * 24 * 60 * 60) + (hours * 60 * 60) + (mins * 60) + secs) * 1000;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load state for a grant.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} grantId
|
|
70
|
+
* @returns {Object|null}
|
|
71
|
+
*/
|
|
72
|
+
getState(grantId) {
|
|
73
|
+
const statePath = this._getStatePath(grantId);
|
|
74
|
+
if (!fs.existsSync(statePath)) return null;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Corrupt state file = fail-close
|
|
80
|
+
panic('AUTOMATION_STATE_CORRUPT', { grantId, error: e.message });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if execution is allowed based on frequency.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} grantId
|
|
88
|
+
* @param {string} maxFrequency - ISO-8601 duration (e.g., "PT1H")
|
|
89
|
+
* @returns {{ allowed: boolean, reason?: string, lastExecutionAt?: string }}
|
|
90
|
+
*/
|
|
91
|
+
canExecute(grantId, maxFrequency) {
|
|
92
|
+
const state = this.getState(grantId);
|
|
93
|
+
const frequencyMs = this._parseDuration(maxFrequency);
|
|
94
|
+
|
|
95
|
+
if (!state) {
|
|
96
|
+
// Never executed before, allowed
|
|
97
|
+
return { allowed: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check for incomplete execution (crash recovery)
|
|
101
|
+
if (state.status === 'PENDING') {
|
|
102
|
+
// Previous execution did not complete. Fail-close.
|
|
103
|
+
return {
|
|
104
|
+
allowed: false,
|
|
105
|
+
reason: 'INCOMPLETE_EXECUTION_DETECTED',
|
|
106
|
+
lastExecutionAt: state.last_execution_at
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lastRun = new Date(state.last_execution_at).getTime();
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const elapsed = now - lastRun;
|
|
113
|
+
|
|
114
|
+
// Clock rollback detection
|
|
115
|
+
if (elapsed < 0) {
|
|
116
|
+
return {
|
|
117
|
+
allowed: false,
|
|
118
|
+
reason: 'CLOCK_ROLLBACK_DETECTED',
|
|
119
|
+
lastExecutionAt: state.last_execution_at
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (elapsed < frequencyMs) {
|
|
124
|
+
const nextAllowedAt = new Date(lastRun + frequencyMs).toISOString();
|
|
125
|
+
return {
|
|
126
|
+
allowed: false,
|
|
127
|
+
reason: 'FREQUENCY_LIMIT',
|
|
128
|
+
lastExecutionAt: state.last_execution_at,
|
|
129
|
+
nextAllowedAt
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { allowed: true, lastExecutionAt: state.last_execution_at };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Record execution start (Write-Ahead).
|
|
138
|
+
*
|
|
139
|
+
* @param {string} grantId
|
|
140
|
+
* @param {string} approvalHash - Hash of the approval context
|
|
141
|
+
*/
|
|
142
|
+
recordExecutionStart(grantId, approvalHash) {
|
|
143
|
+
const statePath = this._getStatePath(grantId);
|
|
144
|
+
const existingState = this.getState(grantId) || { execution_count: 0 };
|
|
145
|
+
|
|
146
|
+
const newState = {
|
|
147
|
+
grant_id: grantId,
|
|
148
|
+
last_execution_at: new Date().toISOString(),
|
|
149
|
+
execution_count: existingState.execution_count + 1,
|
|
150
|
+
session_id: this._sessionId,
|
|
151
|
+
approval_hash: approvalHash,
|
|
152
|
+
status: 'PENDING', // Mark as pending until complete
|
|
153
|
+
event_hash: null // Will be filled on completion
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Atomic write via temp file
|
|
157
|
+
const tempPath = `${statePath}.tmp`;
|
|
158
|
+
fs.writeFileSync(tempPath, JSON.stringify(newState, null, 2), 'utf8');
|
|
159
|
+
fs.renameSync(tempPath, statePath);
|
|
160
|
+
|
|
161
|
+
// Emit to audit chain
|
|
162
|
+
SentinelInterface.getInstance().logAudit({
|
|
163
|
+
event: 'AUTOMATION_EXECUTION_START',
|
|
164
|
+
grant_id: grantId,
|
|
165
|
+
execution_count: newState.execution_count,
|
|
166
|
+
session_id: this._sessionId,
|
|
167
|
+
approval_hash: approvalHash,
|
|
168
|
+
timestamp: newState.last_execution_at
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Record execution completion.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} grantId
|
|
176
|
+
* @param {string} eventHash - Hash of the execution result
|
|
177
|
+
*/
|
|
178
|
+
recordExecutionComplete(grantId, eventHash) {
|
|
179
|
+
const statePath = this._getStatePath(grantId);
|
|
180
|
+
const state = this.getState(grantId);
|
|
181
|
+
|
|
182
|
+
if (!state) {
|
|
183
|
+
panic('AUTOMATION_STATE_MISSING', { grantId, context: 'recordExecutionComplete' });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
state.status = 'COMPLETE';
|
|
187
|
+
state.event_hash = eventHash;
|
|
188
|
+
|
|
189
|
+
// Atomic write
|
|
190
|
+
const tempPath = `${statePath}.tmp`;
|
|
191
|
+
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), 'utf8');
|
|
192
|
+
fs.renameSync(tempPath, statePath);
|
|
193
|
+
|
|
194
|
+
// Emit to audit chain
|
|
195
|
+
SentinelInterface.getInstance().logAudit({
|
|
196
|
+
event: 'AUTOMATION_EXECUTION_COMPLETE',
|
|
197
|
+
grant_id: grantId,
|
|
198
|
+
execution_count: state.execution_count,
|
|
199
|
+
session_id: state.session_id,
|
|
200
|
+
event_hash: eventHash,
|
|
201
|
+
timestamp: new Date().toISOString()
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Singleton
|
|
207
|
+
let _instance = null;
|
|
208
|
+
|
|
209
|
+
function getInstance() {
|
|
210
|
+
if (!_instance) {
|
|
211
|
+
if (isProdOrHigher()) {
|
|
212
|
+
_instance = new LastRunLedger();
|
|
213
|
+
} else {
|
|
214
|
+
throw new Error('DEV_MODE_VIOLATION: Accessing LastRunLedger in DEV');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return _instance;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { LastRunLedger, getInstance };
|
|
@@ -102,7 +102,14 @@ const InvariantRegistry = require('../constitution/InvariantRegistry');
|
|
|
102
102
|
const InvariantChecker = require('../constitution/InvariantChecker');
|
|
103
103
|
const { verifyAtBoot: verifyInvariantHash } = require('../constitution/InvariantHashLock');
|
|
104
104
|
const { verifyDormancy } = require('../constitution/RingDormancyGate');
|
|
105
|
+
|
|
106
|
+
// Ring-0 Mode System (PROD enforcement)
|
|
107
|
+
const { lockMode, getActiveMode, isProdOrHigher, getLockMetadata } = require('../modes/ActiveMode');
|
|
105
108
|
const { getInstance: getTierManager } = require('../constitution/TierManager');
|
|
109
|
+
const SentinelInterface = require('../constitution/SentinelInterface');
|
|
110
|
+
const { DirectoryHasher } = require('../crypto/DirectoryHasher'); // R-03 Hardening
|
|
111
|
+
const { getInstance: getAutomationRegistry } = require('../automation/AutomationRegistry'); // AURORA
|
|
112
|
+
|
|
106
113
|
|
|
107
114
|
// Ring-0 Operator
|
|
108
115
|
const { getInstance: getIdentityContext } = require('../operator/IdentityContext');
|
|
@@ -156,6 +163,63 @@ class KernelBootstrap {
|
|
|
156
163
|
const tier = this.tierManager.getTier();
|
|
157
164
|
this.tierManager.displayBanner();
|
|
158
165
|
|
|
166
|
+
// V3: Lock mode at boot (IMMUTABLE after this point)
|
|
167
|
+
const activeMode = lockMode(tier);
|
|
168
|
+
console.log(` ✓ Mode Locked: ${tier} (immutable)`);
|
|
169
|
+
if (isProdOrHigher()) {
|
|
170
|
+
console.log(' ✓ PROD Enforcement: ACTIVE (fail-close)');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// R-03 FIX: Boot Sequence Trust Sealing
|
|
174
|
+
// Emit BOOT_TRUST_SEALED (Genesis Context for Replay)
|
|
175
|
+
try {
|
|
176
|
+
const Sentinel = SentinelInterface.SentinelInterface || SentinelInterface;
|
|
177
|
+
if (Sentinel.getInstance) {
|
|
178
|
+
const pkg = require('../../../package.json');
|
|
179
|
+
const lockMeta = getLockMetadata();
|
|
180
|
+
|
|
181
|
+
// Collect Trust Seal Input
|
|
182
|
+
const sealMetrics = {
|
|
183
|
+
kernel_version: pkg.version,
|
|
184
|
+
tier,
|
|
185
|
+
mode: activeMode,
|
|
186
|
+
build_hash: configHash || 'unknown',
|
|
187
|
+
lock_metadata: lockMeta,
|
|
188
|
+
// R-03B FIX: Real Deterministic Config Hashing
|
|
189
|
+
config_hash: (function () {
|
|
190
|
+
try {
|
|
191
|
+
// Hash <root>/config recursively
|
|
192
|
+
const configDir = path.resolve(process.cwd(), 'config');
|
|
193
|
+
return DirectoryHasher.hashDir(configDir);
|
|
194
|
+
} catch (hashErr) {
|
|
195
|
+
// PROD FAIL-CLOSE: If we cannot seal the config, we cannot boot.
|
|
196
|
+
// This catches symlinks (SECURITY VIOLATION) or FS errors.
|
|
197
|
+
if (tier === 'PROD' || tier === 'FORTRESS') {
|
|
198
|
+
panic('CONFIG_SEAL_FAILURE', {
|
|
199
|
+
reason: 'Failed to compute deterministic config hash',
|
|
200
|
+
error: hashErr.message
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// DEV: Warn but proceed (with tainted seal)
|
|
204
|
+
console.warn(` ! Config Hash Failed: ${hashErr.message}`);
|
|
205
|
+
return `HASH_FAILED:${hashErr.message}`;
|
|
206
|
+
}
|
|
207
|
+
})(),
|
|
208
|
+
registry_status: this.tierManager.requiresSentinel() ? 'REQUIRED' : 'OPTIONAL'
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
Sentinel.getInstance().logAudit({
|
|
212
|
+
event: 'BOOT_TRUST_SEALED',
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
seal_metrics: sealMetrics
|
|
215
|
+
});
|
|
216
|
+
console.log(' ✓ Boot Trust Seal: EMITTED');
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.warn(' ! Failed to emit BOOT_TRUST_SEALED:', e.message);
|
|
220
|
+
// In PROD this technically should be fatal, but usually covered by specific invariants
|
|
221
|
+
}
|
|
222
|
+
|
|
159
223
|
// TASK 4: Enforce Tier Restriction in LGM
|
|
160
224
|
if (IS_LGM && (tier === 'PROD' || tier === 'FORTRESS')) {
|
|
161
225
|
panic('LGM_TIER_VIOLATION', {
|
|
@@ -188,6 +252,24 @@ class KernelBootstrap {
|
|
|
188
252
|
this.identityContext = getIdentityContext();
|
|
189
253
|
this.identityContext.establish({ traceId, source: 'kernel_boot' });
|
|
190
254
|
|
|
255
|
+
// V2 SECURITY: Initialize Sentinel Key Registry (MUST be before any verification)
|
|
256
|
+
const { getInstance: getKeyRegistry } = require('../sentinel/KeyRegistry');
|
|
257
|
+
const keyRegistry = getKeyRegistry();
|
|
258
|
+
const keyRegStatus = keyRegistry.initialize();
|
|
259
|
+
if (keyRegStatus.success) {
|
|
260
|
+
console.log(` ✓ Sentinel Key Registry: Initialized (${keyRegStatus.keyCount || 0} keys)`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// AURORA: Initialize Automation Registry (PROD Only)
|
|
264
|
+
const currentMode = getActiveMode();
|
|
265
|
+
if (currentMode.id === 'PROD' || currentMode.id === 'FORTRESS') {
|
|
266
|
+
console.log(' ► Initializing Automation Registry (PROD Enforced)...');
|
|
267
|
+
const registry = getAutomationRegistry();
|
|
268
|
+
await registry.initialize();
|
|
269
|
+
} else {
|
|
270
|
+
console.log(` ► Automation Registry SKIPPED (Mode: ${currentMode.id})`);
|
|
271
|
+
}
|
|
272
|
+
|
|
191
273
|
// ═══════════════════════════════════════════════════════════════
|
|
192
274
|
// 3.5 Initialize Governance Trackers (ALL TIERS)
|
|
193
275
|
// DEV Mode Honesty: Trackers must be wired regardless of tier
|
|
@@ -299,6 +381,29 @@ class KernelBootstrap {
|
|
|
299
381
|
});
|
|
300
382
|
}
|
|
301
383
|
|
|
384
|
+
// PROD HARDENING: Emit MODE_LOCKED event (Root of Trust)
|
|
385
|
+
// Must happen after successful invariant checks
|
|
386
|
+
try {
|
|
387
|
+
const pkg = require('../../../package.json');
|
|
388
|
+
const lockMeta = getLockMetadata(); // Capture locking timestamp/stack
|
|
389
|
+
|
|
390
|
+
const Sentinel = SentinelInterface.SentinelInterface || SentinelInterface;
|
|
391
|
+
if (Sentinel.getInstance) {
|
|
392
|
+
Sentinel.getInstance().logAudit({
|
|
393
|
+
event: 'MODE_LOCKED',
|
|
394
|
+
tier,
|
|
395
|
+
mode: activeMode,
|
|
396
|
+
kernel_version: pkg.version,
|
|
397
|
+
build_hash: configHash || 'unknown',
|
|
398
|
+
lock_metadata: lockMeta,
|
|
399
|
+
timestamp: new Date().toISOString()
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {
|
|
403
|
+
console.warn(' ! Failed to emit MODE_LOCKED event:', e.message);
|
|
404
|
+
// Non-fatal, but noisy
|
|
405
|
+
}
|
|
406
|
+
|
|
302
407
|
// V2: tier now comes from TierManager
|
|
303
408
|
|
|
304
409
|
// Hardware Verification (MANDATORY IN PROD/FORTRESS)
|