pumuki-ast-hooks 5.3.17 → 5.3.18
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/docs/VIOLATIONS_RESOLUTION_PLAN.md +9 -9
- package/docs/alerting-system.md +51 -0
- package/docs/observability.md +36 -0
- package/docs/type-safety.md +8 -0
- package/package.json +1 -1
- package/scripts/hooks-system/.AI_TOKEN_STATUS.txt +1 -1
- package/scripts/hooks-system/.audit-reports/notifications.log +11 -0
- package/scripts/hooks-system/.audit-reports/token-monitor.log +72 -0
- package/scripts/hooks-system/application/CompositionRoot.js +73 -24
- package/scripts/hooks-system/application/services/DynamicRulesLoader.js +2 -1
- package/scripts/hooks-system/application/services/RealtimeGuardService.js +85 -15
- package/scripts/hooks-system/application/services/guard/GuardAutoManagerService.js +31 -2
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +17 -9
- package/scripts/hooks-system/application/services/guard/GuardHeartbeatMonitor.js +6 -9
- package/scripts/hooks-system/application/services/guard/GuardProcessManager.js +29 -0
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +3 -0
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +2 -1
- package/scripts/hooks-system/application/services/logging/AuditLogger.js +88 -0
- package/scripts/hooks-system/application/services/logging/UnifiedLogger.js +13 -4
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitorService.js +7 -3
- package/scripts/hooks-system/application/services/token/TokenMetricsService.js +14 -1
- package/scripts/hooks-system/config/env.js +33 -0
- package/scripts/hooks-system/domain/events/__tests__/EventBus.spec.js +33 -0
- package/scripts/hooks-system/domain/events/index.js +16 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidAnalysisOrchestrator.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -20
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +8 -18
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/BackendPatternDetector.js +2 -1
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -8
- package/scripts/hooks-system/infrastructure/ast/frontend/ast-frontend.js +196 -196
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +3 -2
- package/scripts/hooks-system/infrastructure/hooks/skill-activation-prompt.js +3 -2
- package/scripts/hooks-system/infrastructure/logging/UnifiedLoggerFactory.js +35 -5
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +86 -16
- package/scripts/hooks-system/infrastructure/telemetry/metrics-server.js +51 -2
- package/scripts/hooks-system/infrastructure/validators/enforce-english-literals.js +6 -8
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { getGitTreeState, isTreeBeyondLimit } = require('./GitTreeState');
|
|
4
|
+
const AuditLogger = require('./logging/AuditLogger');
|
|
5
|
+
const { recordMetric } = require('../../infrastructure/telemetry/metrics-logger');
|
|
6
|
+
const env = require('../../config/env');
|
|
4
7
|
|
|
5
8
|
class RealtimeGuardService {
|
|
6
9
|
/**
|
|
@@ -12,6 +15,12 @@ class RealtimeGuardService {
|
|
|
12
15
|
* @param {Object} dependencies.config
|
|
13
16
|
*/
|
|
14
17
|
constructor(dependencies = {}) {
|
|
18
|
+
this._initializeDependencies(dependencies);
|
|
19
|
+
this._initializeConfiguration();
|
|
20
|
+
this._initializeMonitors();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_initializeDependencies(dependencies = {}) {
|
|
15
24
|
const {
|
|
16
25
|
logger,
|
|
17
26
|
notificationService,
|
|
@@ -30,39 +39,49 @@ class RealtimeGuardService {
|
|
|
30
39
|
this.monitors = monitors || {};
|
|
31
40
|
this.orchestration = orchestration;
|
|
32
41
|
this.config = config || {};
|
|
42
|
+
this.auditLogger = dependencies.auditLogger || new AuditLogger({ repoRoot: process.cwd(), logger: this.logger });
|
|
43
|
+
}
|
|
33
44
|
|
|
45
|
+
_initializeConfiguration() {
|
|
34
46
|
if (!this.config.debugLogPath) {
|
|
35
47
|
this.config.debugLogPath = path.join(process.cwd(), '.audit-reports', 'guard-debug.log');
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
this.evidencePath = this.config.evidencePath || path.join(process.cwd(), '.AI_EVIDENCE.json');
|
|
39
|
-
this.staleThresholdMs =
|
|
40
|
-
this.reminderIntervalMs =
|
|
41
|
-
this.inactivityGraceMs =
|
|
42
|
-
this.pollIntervalMs =
|
|
50
|
+
this.evidencePath = this.config.evidencePath || path.join(env.get('REPO_ROOT', process.cwd()), '.AI_EVIDENCE.json');
|
|
51
|
+
this.staleThresholdMs = env.getNumber('HOOK_GUARD_EVIDENCE_STALE_THRESHOLD', 60000);
|
|
52
|
+
this.reminderIntervalMs = env.getNumber('HOOK_GUARD_EVIDENCE_REMINDER_INTERVAL', 60000);
|
|
53
|
+
this.inactivityGraceMs = env.getNumber('HOOK_GUARD_INACTIVITY_GRACE_MS', 120000);
|
|
54
|
+
this.pollIntervalMs = env.getNumber('HOOK_GUARD_EVIDENCE_POLL_INTERVAL', 30000);
|
|
43
55
|
this.pollTimer = null;
|
|
44
56
|
this.lastStaleNotification = 0;
|
|
45
57
|
this.lastUserActivityAt = 0;
|
|
46
58
|
|
|
47
|
-
this.gitTreeStagedThreshold =
|
|
48
|
-
this.gitTreeUnstagedThreshold =
|
|
49
|
-
this.gitTreeTotalThreshold =
|
|
50
|
-
this.gitTreeCheckIntervalMs =
|
|
51
|
-
this.gitTreeReminderMs =
|
|
59
|
+
this.gitTreeStagedThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_STAGED_LIMIT', 10);
|
|
60
|
+
this.gitTreeUnstagedThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_UNSTAGED_LIMIT', 15);
|
|
61
|
+
this.gitTreeTotalThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_TOTAL_LIMIT', 20);
|
|
62
|
+
this.gitTreeCheckIntervalMs = env.getNumber('HOOK_GUARD_DIRTY_TREE_INTERVAL', 60000);
|
|
63
|
+
this.gitTreeReminderMs = env.getNumber('HOOK_GUARD_DIRTY_TREE_REMINDER', 300000);
|
|
52
64
|
this.gitTreeTimer = null;
|
|
53
65
|
this.lastDirtyTreeNotification = 0;
|
|
54
66
|
this.dirtyTreeActive = false;
|
|
55
67
|
|
|
56
|
-
this.autoRefreshCooldownMs =
|
|
68
|
+
this.autoRefreshCooldownMs = env.getNumber('HOOK_GUARD_EVIDENCE_AUTO_REFRESH_COOLDOWN', 180000);
|
|
57
69
|
this.lastAutoRefresh = 0;
|
|
58
70
|
this.autoRefreshInFlight = false;
|
|
59
71
|
|
|
60
72
|
this.watchers = [];
|
|
61
|
-
this.embedTokenMonitor =
|
|
73
|
+
this.embedTokenMonitor = env.getBool('HOOK_GUARD_EMBEDDED_TOKEN_MONITOR', false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_initializeMonitors() {
|
|
77
|
+
// Monitors are configured but not started here
|
|
78
|
+
// They are started in the start() method to allow for proper initialization order
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
start() {
|
|
65
82
|
this.logger.info('Starting RealtimeGuardService...');
|
|
83
|
+
this.auditLogger.record({ action: 'guard.realtime.start', resource: 'realtime_guard', status: 'success' });
|
|
84
|
+
recordMetric({ hook: 'realtime_guard', status: 'start' });
|
|
66
85
|
|
|
67
86
|
// Start all monitors
|
|
68
87
|
this._startEvidenceMonitoring();
|
|
@@ -82,6 +101,8 @@ class RealtimeGuardService {
|
|
|
82
101
|
|
|
83
102
|
stop() {
|
|
84
103
|
this.logger.info('Stopping RealtimeGuardService...');
|
|
104
|
+
this.auditLogger.record({ action: 'guard.realtime.stop', resource: 'realtime_guard', status: 'success' });
|
|
105
|
+
recordMetric({ hook: 'realtime_guard', status: 'stop' });
|
|
85
106
|
|
|
86
107
|
this.watchers.forEach(w => w.close());
|
|
87
108
|
this.watchers = [];
|
|
@@ -103,14 +124,27 @@ class RealtimeGuardService {
|
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
_startGitTreeMonitoring() {
|
|
106
|
-
if (
|
|
127
|
+
if (env.getBool('HOOK_GUARD_DIRTY_TREE_DISABLED', false)) return;
|
|
107
128
|
|
|
108
129
|
this.monitors.gitTree.startMonitoring((state) => {
|
|
109
130
|
if (state.isBeyondLimit) {
|
|
110
131
|
const message = `Git tree has too many files: ${state.total} total (${state.staged} staged, ${state.unstaged} unstaged)`;
|
|
111
132
|
this.notify(message, 'error', { forceDialog: true });
|
|
133
|
+
this.auditLogger.record({
|
|
134
|
+
action: 'guard.git_tree.dirty',
|
|
135
|
+
resource: 'git_tree',
|
|
136
|
+
status: 'warning',
|
|
137
|
+
meta: { total: state.total, staged: state.staged, unstaged: state.unstaged }
|
|
138
|
+
});
|
|
139
|
+
recordMetric({ hook: 'git_tree', status: 'dirty', total: state.total, staged: state.staged, unstaged: state.unstaged });
|
|
112
140
|
} else {
|
|
113
141
|
this.notify('✅ Git tree is clean', 'success');
|
|
142
|
+
this.auditLogger.record({
|
|
143
|
+
action: 'guard.git_tree.clean',
|
|
144
|
+
resource: 'git_tree',
|
|
145
|
+
status: 'success'
|
|
146
|
+
});
|
|
147
|
+
recordMetric({ hook: 'git_tree', status: 'clean' });
|
|
114
148
|
}
|
|
115
149
|
});
|
|
116
150
|
}
|
|
@@ -124,22 +158,44 @@ class RealtimeGuardService {
|
|
|
124
158
|
try {
|
|
125
159
|
this.monitors.token.start();
|
|
126
160
|
this.notify('🔋 Token monitor started', 'info');
|
|
161
|
+
this.auditLogger.record({
|
|
162
|
+
action: 'guard.token_monitor.start',
|
|
163
|
+
resource: 'token_monitor',
|
|
164
|
+
status: 'success'
|
|
165
|
+
});
|
|
166
|
+
recordMetric({ hook: 'token_monitor', status: 'start' });
|
|
127
167
|
} catch (error) {
|
|
128
168
|
this.notify(`Failed to start token monitor: ${error.message}`, 'error');
|
|
169
|
+
this.auditLogger.record({
|
|
170
|
+
action: 'guard.token_monitor.start',
|
|
171
|
+
resource: 'token_monitor',
|
|
172
|
+
status: 'fail',
|
|
173
|
+
meta: { message: error.message }
|
|
174
|
+
});
|
|
175
|
+
recordMetric({ hook: 'token_monitor', status: 'fail' });
|
|
129
176
|
}
|
|
130
177
|
}
|
|
131
178
|
|
|
132
179
|
_startGitFlowSync() {
|
|
133
180
|
if (!this.monitors.gitFlow.autoSyncEnabled) return;
|
|
134
181
|
|
|
182
|
+
this.auditLogger.record({
|
|
183
|
+
action: 'guard.gitflow.autosync.enabled',
|
|
184
|
+
resource: 'gitflow',
|
|
185
|
+
status: 'success',
|
|
186
|
+
meta: { intervalMs: env.getNumber('HOOK_GUARD_GITFLOW_AUTOSYNC_INTERVAL', 300000) }
|
|
187
|
+
});
|
|
188
|
+
recordMetric({ hook: 'gitflow_autosync', status: 'enabled' });
|
|
189
|
+
|
|
135
190
|
const syncInterval = setInterval(() => {
|
|
136
191
|
if (this.monitors.gitFlow.isClean()) {
|
|
137
192
|
const result = this.monitors.gitFlow.syncBranches();
|
|
138
193
|
if (result.success) {
|
|
139
194
|
this.notify('🔄 Branches synchronized', 'info');
|
|
195
|
+
recordMetric({ hook: 'gitflow_autosync', status: 'sync_success' });
|
|
140
196
|
}
|
|
141
197
|
}
|
|
142
|
-
},
|
|
198
|
+
}, env.getNumber('HOOK_GUARD_GITFLOW_AUTOSYNC_INTERVAL', 300000));
|
|
143
199
|
|
|
144
200
|
syncInterval.unref();
|
|
145
201
|
}
|
|
@@ -247,11 +303,18 @@ class RealtimeGuardService {
|
|
|
247
303
|
this.lastStaleNotification = now;
|
|
248
304
|
const ageSec = Math.floor(ageMs / 1000);
|
|
249
305
|
this.notify(`Evidence has been stale for ${ageSec}s (source: ${source}).`, 'warn', { forceDialog: true });
|
|
306
|
+
this.auditLogger.record({
|
|
307
|
+
action: 'guard.evidence.stale',
|
|
308
|
+
resource: 'evidence',
|
|
309
|
+
status: 'warning',
|
|
310
|
+
meta: { ageSec, source }
|
|
311
|
+
});
|
|
312
|
+
recordMetric({ hook: 'evidence', status: 'stale', ageSec, source });
|
|
250
313
|
void this.attemptAutoRefresh('stale');
|
|
251
314
|
}
|
|
252
315
|
|
|
253
316
|
async attemptAutoRefresh(reason = 'manual') {
|
|
254
|
-
if (
|
|
317
|
+
if (!env.getBool('HOOK_GUARD_AUTO_REFRESH', false)) {
|
|
255
318
|
return;
|
|
256
319
|
}
|
|
257
320
|
|
|
@@ -284,6 +347,13 @@ class RealtimeGuardService {
|
|
|
284
347
|
try {
|
|
285
348
|
await this.runDirectEvidenceRefresh(reason);
|
|
286
349
|
this.lastAutoRefresh = now;
|
|
350
|
+
this.auditLogger.record({
|
|
351
|
+
action: 'guard.evidence.auto_refresh',
|
|
352
|
+
resource: 'evidence',
|
|
353
|
+
status: 'success',
|
|
354
|
+
meta: { reason }
|
|
355
|
+
});
|
|
356
|
+
recordMetric({ hook: 'evidence', status: 'auto_refresh_success', reason });
|
|
287
357
|
} finally {
|
|
288
358
|
this.autoRefreshInFlight = false;
|
|
289
359
|
}
|
|
@@ -10,6 +10,9 @@ const GuardConfig = require('./GuardConfig');
|
|
|
10
10
|
const GuardNotificationHandler = require('./GuardNotificationHandler');
|
|
11
11
|
const GuardMonitorLoop = require('./GuardMonitorLoop');
|
|
12
12
|
const GuardHealthReminder = require('./GuardHealthReminder');
|
|
13
|
+
const AuditLogger = require('../logging/AuditLogger');
|
|
14
|
+
const envHelper = require('../../../config/env');
|
|
15
|
+
const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
|
|
13
16
|
|
|
14
17
|
class GuardAutoManagerService {
|
|
15
18
|
constructor({
|
|
@@ -19,9 +22,10 @@ class GuardAutoManagerService {
|
|
|
19
22
|
fsModule = fs,
|
|
20
23
|
childProcess = { spawnSync },
|
|
21
24
|
timers = { setInterval, clearInterval },
|
|
22
|
-
env =
|
|
25
|
+
env = envHelper,
|
|
23
26
|
processRef = process,
|
|
24
|
-
heartbeatMonitor = null
|
|
27
|
+
heartbeatMonitor = null,
|
|
28
|
+
auditLogger = null
|
|
25
29
|
} = {}) {
|
|
26
30
|
this.process = processRef;
|
|
27
31
|
|
|
@@ -30,6 +34,7 @@ class GuardAutoManagerService {
|
|
|
30
34
|
this.eventLogger = new GuardEventLogger({ repoRoot, logger, fsModule });
|
|
31
35
|
this.lockManager = new GuardLockManager({ repoRoot, logger, fsModule });
|
|
32
36
|
this.processManager = new GuardProcessManager({ repoRoot, logger, fsModule, childProcess });
|
|
37
|
+
this.auditLogger = auditLogger || new AuditLogger({ repoRoot, logger });
|
|
33
38
|
|
|
34
39
|
// Monitors & Handlers
|
|
35
40
|
this.heartbeatMonitor = heartbeatMonitor || new GuardHeartbeatMonitor({
|
|
@@ -59,10 +64,14 @@ class GuardAutoManagerService {
|
|
|
59
64
|
start() {
|
|
60
65
|
if (!this.lockManager.acquireLock()) {
|
|
61
66
|
this.eventLogger.log('Another guard auto manager instance detected. Exiting.');
|
|
67
|
+
this.auditLogger.record({ action: 'guard.lock.acquire', resource: 'guard_auto_manager', status: 'fail', meta: { reason: 'lock_exists' } });
|
|
68
|
+
recordMetric({ hook: 'guard_auto_manager', status: 'lock_fail' });
|
|
62
69
|
return false;
|
|
63
70
|
}
|
|
64
71
|
this.lockManager.writePidFile();
|
|
65
72
|
this.eventLogger.log('Guard auto manager started');
|
|
73
|
+
this.auditLogger.record({ action: 'guard.manager.start', resource: 'guard_auto_manager', status: 'success' });
|
|
74
|
+
recordMetric({ hook: 'guard_auto_manager', status: 'start' });
|
|
66
75
|
|
|
67
76
|
this.ensureSupervisor('initial-start');
|
|
68
77
|
this._startReminder();
|
|
@@ -94,6 +103,12 @@ class GuardAutoManagerService {
|
|
|
94
103
|
handleMissingSupervisor() {
|
|
95
104
|
this.lastHeartbeatState = { healthy: false, reason: 'missing-supervisor' };
|
|
96
105
|
this.eventLogger.recordEvent('Guard supervisor no se encuentra en ejecución; reinicio automático.');
|
|
106
|
+
this.auditLogger.record({
|
|
107
|
+
action: 'guard.supervisor.missing',
|
|
108
|
+
resource: 'guard_supervisor',
|
|
109
|
+
status: 'fail',
|
|
110
|
+
meta: { reason: 'missing-supervisor' }
|
|
111
|
+
});
|
|
97
112
|
this.ensureSupervisor('missing-supervisor');
|
|
98
113
|
}
|
|
99
114
|
|
|
@@ -106,9 +121,21 @@ class GuardAutoManagerService {
|
|
|
106
121
|
this.eventLogger.log(`Heartbeat degraded (${heartbeat.reason}); attempting supervisor ensure.`);
|
|
107
122
|
this.lastHeartbeatRestart = now;
|
|
108
123
|
this.ensureSupervisor(`heartbeat-${heartbeat.reason}`);
|
|
124
|
+
this.auditLogger.record({
|
|
125
|
+
action: 'guard.supervisor.ensure',
|
|
126
|
+
resource: 'guard_supervisor',
|
|
127
|
+
status: 'success',
|
|
128
|
+
meta: { reason: heartbeat.reason }
|
|
129
|
+
});
|
|
109
130
|
} else {
|
|
110
131
|
this.eventLogger.log(`Heartbeat degraded (${heartbeat.reason}); restart suppressed (cooldown).`);
|
|
111
132
|
this.eventLogger.recordEvent(`Heartbeat degradado (${heartbeat.reason}); reinicio omitido por cooldown.`);
|
|
133
|
+
this.auditLogger.record({
|
|
134
|
+
action: 'guard.supervisor.ensure',
|
|
135
|
+
resource: 'guard_supervisor',
|
|
136
|
+
status: 'fail',
|
|
137
|
+
meta: { reason: heartbeat.reason, suppressed: true }
|
|
138
|
+
});
|
|
112
139
|
}
|
|
113
140
|
} else {
|
|
114
141
|
this.eventLogger.recordEvent(`Heartbeat en estado ${heartbeat.reason}; reinicio no requerido.`);
|
|
@@ -148,6 +175,8 @@ class GuardAutoManagerService {
|
|
|
148
175
|
this.lockManager.removePidFile();
|
|
149
176
|
this.lockManager.releaseLock();
|
|
150
177
|
this.healthReminder.stop();
|
|
178
|
+
this.auditLogger.record({ action: 'guard.manager.stop', resource: 'guard_auto_manager', status: 'success' });
|
|
179
|
+
recordMetric({ hook: 'guard_auto_manager', status: 'stop' });
|
|
151
180
|
}
|
|
152
181
|
|
|
153
182
|
ensureSupervisor(reason) {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
const envHelper = require('../../../config/env');
|
|
2
|
+
|
|
1
3
|
class GuardConfig {
|
|
2
|
-
constructor(env =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
env.
|
|
4
|
+
constructor(env = envHelper) {
|
|
5
|
+
const getNumber = (name, def) =>
|
|
6
|
+
typeof env.getNumber === 'function' ? env.getNumber(name, def) : Number(env[name] || def);
|
|
7
|
+
const getBool = (name, def) =>
|
|
8
|
+
typeof env.getBool === 'function' ? env.getBool(name, def) : (env[name] !== 'false');
|
|
9
|
+
|
|
10
|
+
this.healthyReminderIntervalMs = getNumber('GUARD_AUTOSTART_HEALTHY_INTERVAL', 0);
|
|
11
|
+
this.heartbeatNotifyCooldownMs = getNumber('GUARD_AUTOSTART_NOTIFY_COOLDOWN', 60000);
|
|
12
|
+
this.healthyReminderCooldownMs = getNumber(
|
|
13
|
+
'GUARD_AUTOSTART_HEALTHY_COOLDOWN',
|
|
14
|
+
this.healthyReminderIntervalMs > 0 ? this.healthyReminderIntervalMs : 0
|
|
7
15
|
);
|
|
8
|
-
this.heartbeatRestartCooldownMs =
|
|
9
|
-
this.monitorIntervalMs =
|
|
10
|
-
this.restartCooldownMs =
|
|
11
|
-
this.stopSupervisorOnExit =
|
|
16
|
+
this.heartbeatRestartCooldownMs = getNumber('GUARD_AUTOSTART_HEARTBEAT_COOLDOWN', 60000);
|
|
17
|
+
this.monitorIntervalMs = getNumber('GUARD_AUTOSTART_MONITOR_INTERVAL', 5000);
|
|
18
|
+
this.restartCooldownMs = getNumber('GUARD_AUTOSTART_RESTART_COOLDOWN', 2000);
|
|
19
|
+
this.stopSupervisorOnExit = getBool('GUARD_AUTOSTART_STOP_SUPERVISOR_ON_EXIT', true);
|
|
12
20
|
}
|
|
13
21
|
}
|
|
14
22
|
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const env = require('../../../config/env');
|
|
3
4
|
|
|
4
5
|
class GuardHeartbeatMonitor {
|
|
5
6
|
constructor({
|
|
6
7
|
repoRoot = process.cwd(),
|
|
7
8
|
logger = console,
|
|
8
|
-
fsModule = fs
|
|
9
|
-
env = process.env
|
|
9
|
+
fsModule = fs
|
|
10
10
|
} = {}) {
|
|
11
11
|
this.repoRoot = repoRoot;
|
|
12
12
|
this.logger = logger;
|
|
13
13
|
this.fs = fsModule;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const heartbeatRelative = env.HOOK_GUARD_HEARTBEAT_PATH || path.join('.audit_tmp', 'guard-heartbeat.json');
|
|
14
|
+
const heartbeatRelative = env.get('HOOK_GUARD_HEARTBEAT_PATH', path.join('.audit_tmp', 'guard-heartbeat.json'));
|
|
17
15
|
this.heartbeatPath = path.isAbsolute(heartbeatRelative)
|
|
18
16
|
? heartbeatRelative
|
|
19
17
|
: path.join(this.repoRoot, heartbeatRelative);
|
|
20
18
|
|
|
21
|
-
this.heartbeatMaxAgeMs =
|
|
22
|
-
env.
|
|
23
|
-
);
|
|
19
|
+
this.heartbeatMaxAgeMs = env.getNumber('GUARD_AUTOSTART_HEARTBEAT_MAX_AGE',
|
|
20
|
+
env.getNumber('HOOK_GUARD_HEARTBEAT_MAX_AGE', 60000));
|
|
24
21
|
|
|
25
22
|
this.heartbeatRestartReasons = new Set(
|
|
26
|
-
(env.GUARD_AUTOSTART_HEARTBEAT_RESTART
|
|
23
|
+
(env.get('GUARD_AUTOSTART_HEARTBEAT_RESTART', 'missing,stale,invalid,degraded'))
|
|
27
24
|
.split(',')
|
|
28
25
|
.map(entry => entry.trim().toLowerCase())
|
|
29
26
|
.filter(Boolean)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { spawnSync } = require('child_process');
|
|
3
3
|
|
|
4
|
+
// Import AuditLogger for logging critical operations
|
|
5
|
+
const AuditLogger = require('../logging/AuditLogger');
|
|
6
|
+
|
|
4
7
|
class GuardProcessManager {
|
|
5
8
|
constructor({
|
|
6
9
|
repoRoot = process.cwd(),
|
|
@@ -15,6 +18,10 @@ class GuardProcessManager {
|
|
|
15
18
|
|
|
16
19
|
this.supervisorPidFile = path.join(this.repoRoot, '.guard-supervisor.pid');
|
|
17
20
|
this.startScript = path.join(this.repoRoot, 'bin', 'start-guards.sh');
|
|
21
|
+
this.busy = false;
|
|
22
|
+
|
|
23
|
+
// Initialize audit logger
|
|
24
|
+
this.auditLogger = new AuditLogger({ repoRoot, logger: this.logger });
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
isSupervisorRunning() {
|
|
@@ -58,6 +65,15 @@ class GuardProcessManager {
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
startSupervisor() {
|
|
68
|
+
if (this.busy) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: new Error('bulkhead_busy'),
|
|
72
|
+
stdout: '',
|
|
73
|
+
stderr: 'bulkhead_busy'
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
this.busy = true;
|
|
61
77
|
try {
|
|
62
78
|
const result = this.childProcess.spawnSync(this.startScript, ['start'], {
|
|
63
79
|
cwd: this.repoRoot,
|
|
@@ -80,10 +96,21 @@ class GuardProcessManager {
|
|
|
80
96
|
stdout: '',
|
|
81
97
|
stderr: error.message
|
|
82
98
|
};
|
|
99
|
+
} finally {
|
|
100
|
+
this.busy = false;
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
stopSupervisor() {
|
|
105
|
+
if (this.busy) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: new Error('bulkhead_busy'),
|
|
109
|
+
stdout: '',
|
|
110
|
+
stderr: 'bulkhead_busy'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
this.busy = true;
|
|
87
114
|
try {
|
|
88
115
|
const result = this.childProcess.spawnSync(this.startScript, ['stop'], {
|
|
89
116
|
cwd: this.repoRoot,
|
|
@@ -106,6 +133,8 @@ class GuardProcessManager {
|
|
|
106
133
|
stdout: '',
|
|
107
134
|
stderr: error.message
|
|
108
135
|
};
|
|
136
|
+
} finally {
|
|
137
|
+
this.busy = false;
|
|
109
138
|
}
|
|
110
139
|
}
|
|
111
140
|
}
|
|
@@ -2,6 +2,9 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync, spawnSync } = require('child_process');
|
|
4
4
|
|
|
5
|
+
// Import recordMetric for prometheus metrics
|
|
6
|
+
const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
|
|
7
|
+
|
|
5
8
|
const COLORS = {
|
|
6
9
|
reset: '\x1b[0m',
|
|
7
10
|
blue: '\x1b[34m',
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const env = require('../../config/env');
|
|
6
7
|
|
|
7
8
|
const COLORS = {
|
|
8
9
|
reset: '\x1b[0m',
|
|
@@ -31,7 +32,7 @@ function computeRepoFingerprint(repoRoot) {
|
|
|
31
32
|
|
|
32
33
|
function computeServerIdForRepo(repoRoot) {
|
|
33
34
|
const legacyServerId = 'ast-intelligence-automation';
|
|
34
|
-
const forced = (
|
|
35
|
+
const forced = (env.get('MCP_SERVER_ID', '') || '').trim();
|
|
35
36
|
if (forced.length > 0) return forced;
|
|
36
37
|
|
|
37
38
|
const repoName = path.basename(repoRoot || process.cwd());
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class AuditLogger {
|
|
5
|
+
/**
|
|
6
|
+
* @param {Object} options
|
|
7
|
+
* @param {string} [options.repoRoot=process.cwd()]
|
|
8
|
+
* @param {string} [options.filename='.audit_tmp/audit.log']
|
|
9
|
+
* @param {Object} [options.logger=console] - fallback logger for warnings
|
|
10
|
+
*/
|
|
11
|
+
constructor({ repoRoot = process.cwd(), filename, logger = console } = {}) {
|
|
12
|
+
this.repoRoot = repoRoot;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
this.logPath = filename
|
|
15
|
+
? (path.isAbsolute(filename) ? filename : path.join(repoRoot, filename))
|
|
16
|
+
: path.join(repoRoot, '.audit_tmp', 'audit.log');
|
|
17
|
+
|
|
18
|
+
this.ensureDir();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ensureDir() {
|
|
22
|
+
try {
|
|
23
|
+
const dir = path.dirname(this.logPath);
|
|
24
|
+
if (!fs.existsSync(dir)) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
if (!fs.existsSync(this.logPath)) {
|
|
28
|
+
fs.writeFileSync(this.logPath, '', { encoding: 'utf8' });
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.warn('AUDIT_LOGGER_INIT_ERROR', error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
warn(message, error) {
|
|
36
|
+
if (this.logger?.warn) {
|
|
37
|
+
this.logger.warn(message, { error: error?.message });
|
|
38
|
+
} else {
|
|
39
|
+
console.warn(message, error?.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Object} entry
|
|
45
|
+
* @param {string} entry.action
|
|
46
|
+
* @param {string} [entry.resource]
|
|
47
|
+
* @param {string} [entry.status='success']
|
|
48
|
+
* @param {string|null} [entry.actor=null]
|
|
49
|
+
* @param {Object} [entry.meta={}]
|
|
50
|
+
* @param {string|null} [entry.correlationId=null]
|
|
51
|
+
*/
|
|
52
|
+
record(entry = {}) {
|
|
53
|
+
if (!entry.action) return;
|
|
54
|
+
const safeMeta = this.sanitizeMeta(entry.meta || {});
|
|
55
|
+
|
|
56
|
+
const payload = {
|
|
57
|
+
ts: new Date().toISOString(),
|
|
58
|
+
action: entry.action,
|
|
59
|
+
resource: entry.resource || null,
|
|
60
|
+
status: entry.status || 'success',
|
|
61
|
+
actor: entry.actor || null,
|
|
62
|
+
correlationId: entry.correlationId || null,
|
|
63
|
+
meta: safeMeta
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
fs.appendFileSync(this.logPath, `${JSON.stringify(payload)}\n`, { encoding: 'utf8' });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.warn('AUDIT_LOGGER_WRITE_ERROR', error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sanitizeMeta(meta) {
|
|
74
|
+
const forbidden = ['token', 'password', 'secret', 'authorization', 'auth', 'apiKey'];
|
|
75
|
+
const clone = {};
|
|
76
|
+
Object.entries(meta).forEach(([k, v]) => {
|
|
77
|
+
const lowered = k.toLowerCase();
|
|
78
|
+
if (forbidden.some(f => lowered.includes(f))) {
|
|
79
|
+
clone[k] = '[REDACTED]';
|
|
80
|
+
} else {
|
|
81
|
+
clone[k] = v;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return clone;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = AuditLogger;
|
|
@@ -97,10 +97,19 @@ class UnifiedLogger {
|
|
|
97
97
|
this.rotateFileIfNeeded();
|
|
98
98
|
fs.appendFileSync(this.fileConfig.path, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
99
99
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
error
|
|
100
|
+
try {
|
|
101
|
+
const env = require('../../../config/env');
|
|
102
|
+
if (env.getBool('DEBUG', false)) {
|
|
103
|
+
console.error('[UnifiedLogger] Failed to write log file', {
|
|
104
|
+
path: this.fileConfig.path,
|
|
105
|
+
error: error.message
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
console.warn('[UnifiedLogger] File logging skipped due to error');
|
|
109
|
+
}
|
|
110
|
+
} catch (secondaryError) {
|
|
111
|
+
console.error('[UnifiedLogger] Secondary logging failure', {
|
|
112
|
+
error: secondaryError.message
|
|
104
113
|
});
|
|
105
114
|
}
|
|
106
115
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
|
+
const env = require('../../config/env');
|
|
5
|
+
|
|
6
|
+
// Import recordMetric for prometheus metrics
|
|
7
|
+
const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
|
|
4
8
|
|
|
5
9
|
function resolveUpdateEvidenceScript(repoRoot) {
|
|
6
10
|
const candidates = [
|
|
@@ -25,9 +29,9 @@ class EvidenceMonitorService {
|
|
|
25
29
|
updateScriptPath = resolveUpdateEvidenceScript(repoRoot) || path.join(process.cwd(), 'scripts', 'hooks-system', 'bin', 'update-evidence.sh'),
|
|
26
30
|
notifier = () => { },
|
|
27
31
|
logger = console,
|
|
28
|
-
autoRefreshEnabled =
|
|
29
|
-
autoRefreshCooldownMs =
|
|
30
|
-
staleThresholdMs =
|
|
32
|
+
autoRefreshEnabled = env.getBool('HOOK_GUARD_AUTO_REFRESH', true),
|
|
33
|
+
autoRefreshCooldownMs = env.getNumber('HOOK_GUARD_AUTO_REFRESH_COOLDOWN', 180000),
|
|
34
|
+
staleThresholdMs = env.getNumber('HOOK_GUARD_EVIDENCE_STALE_THRESHOLD', 10 * 60 * 1000),
|
|
31
35
|
fsModule = fs,
|
|
32
36
|
execFn = execSync
|
|
33
37
|
} = {}) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
|
|
3
|
+
// Import recordMetric for prometheus metrics
|
|
4
|
+
const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
|
|
5
|
+
|
|
3
6
|
class TokenMetricsService {
|
|
4
7
|
constructor(cursorTokenService, thresholds, logger) {
|
|
5
8
|
this.cursorTokenService = cursorTokenService;
|
|
@@ -54,7 +57,8 @@ class TokenMetricsService {
|
|
|
54
57
|
if (untrusted) {
|
|
55
58
|
level = 'ok';
|
|
56
59
|
}
|
|
57
|
-
const
|
|
60
|
+
const env = require('../../config/env');
|
|
61
|
+
const forceLevel = (env.get('TOKEN_MONITOR_FORCE_LEVEL', '') || '').toLowerCase();
|
|
58
62
|
if (forceLevel === 'warning' || forceLevel === 'critical' || forceLevel === 'ok') {
|
|
59
63
|
level = forceLevel;
|
|
60
64
|
}
|
|
@@ -75,6 +79,15 @@ class TokenMetricsService {
|
|
|
75
79
|
this.logger.debug('TOKEN_MONITOR_METRICS', metrics);
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
// Record prometheus metrics
|
|
83
|
+
recordMetric({
|
|
84
|
+
hook: 'token_monitor',
|
|
85
|
+
status: 'collect',
|
|
86
|
+
tokensUsed,
|
|
87
|
+
percentUsed: Number(percentUsed.toFixed(0)),
|
|
88
|
+
level
|
|
89
|
+
});
|
|
90
|
+
|
|
78
91
|
return metrics;
|
|
79
92
|
}
|
|
80
93
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const ENV = (process.env.NODE_ENV || 'development').toLowerCase();
|
|
2
|
+
|
|
3
|
+
function normalizeBool(val, defaultValue = false) {
|
|
4
|
+
if (val === undefined) return defaultValue;
|
|
5
|
+
if (typeof val === 'boolean') return val;
|
|
6
|
+
const str = String(val).trim().toLowerCase();
|
|
7
|
+
if (str === '') return defaultValue;
|
|
8
|
+
return !(['false', '0', 'no', 'off'].includes(str));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function get(name, defaultValue = undefined) {
|
|
12
|
+
return process.env[name] !== undefined ? process.env[name] : defaultValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getNumber(name, defaultValue = 0) {
|
|
16
|
+
const raw = process.env[name];
|
|
17
|
+
const parsed = Number(raw);
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : defaultValue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getBool(name, defaultValue = false) {
|
|
22
|
+
return normalizeBool(process.env[name], defaultValue);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
env: ENV,
|
|
27
|
+
isProd: ENV === 'production',
|
|
28
|
+
isStg: ENV === 'staging' || ENV === 'stage' || ENV === 'stg',
|
|
29
|
+
isDev: ENV === 'development' || ENV === 'dev',
|
|
30
|
+
get,
|
|
31
|
+
getNumber,
|
|
32
|
+
getBool,
|
|
33
|
+
};
|