@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.
@@ -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)