@newrelic/preflight 1.18.2 → 1.18.4
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/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/dashboard-server.d.ts +8 -0
- package/dist/dashboard/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard/dashboard-server.js +33 -0
- package/dist/dashboard/dashboard-server.js.map +1 -1
- package/dist/dashboard/live-event-bus.d.ts +8 -0
- package/dist/dashboard/live-event-bus.d.ts.map +1 -1
- package/dist/dashboard/live-event-bus.js +6 -5
- package/dist/dashboard/live-event-bus.js.map +1 -1
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +24 -1
- package/dist/dashboard/routes/api-handler.js.map +1 -1
- package/dist/dashboard/routes/sse-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/sse-handler.js +6 -2
- package/dist/dashboard/routes/sse-handler.js.map +1 -1
- package/dist/homelab/homelab-accumulator.d.ts +21 -0
- package/dist/homelab/homelab-accumulator.d.ts.map +1 -0
- package/dist/homelab/homelab-accumulator.js +139 -0
- package/dist/homelab/homelab-accumulator.js.map +1 -0
- package/dist/homelab/homelab-forwarder.d.ts +24 -0
- package/dist/homelab/homelab-forwarder.d.ts.map +1 -0
- package/dist/homelab/homelab-forwarder.js +96 -0
- package/dist/homelab/homelab-forwarder.js.map +1 -0
- package/dist/homelab/index.d.ts +5 -0
- package/dist/homelab/index.d.ts.map +1 -0
- package/dist/homelab/index.js +3 -0
- package/dist/homelab/index.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +117 -3
- package/dist/index.js.map +1 -1
- package/dist/metrics/retry-detector.d.ts +14 -0
- package/dist/metrics/retry-detector.d.ts.map +1 -1
- package/dist/metrics/retry-detector.js +57 -3
- package/dist/metrics/retry-detector.js.map +1 -1
- package/dist/security/ssrf.d.ts +15 -2
- package/dist/security/ssrf.d.ts.map +1 -1
- package/dist/security/ssrf.js +25 -7
- package/dist/security/ssrf.js.map +1 -1
- package/dist/transport/nr-ingest.d.ts +24 -0
- package/dist/transport/nr-ingest.d.ts.map +1 -1
- package/dist/transport/nr-ingest.js +46 -0
- package/dist/transport/nr-ingest.js.map +1 -1
- package/dist/web/assets/index-C_HZ_0it.js +64 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-BBZyx02c.js +0 -64
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createLogger } from '../shared/index.js';
|
|
2
|
+
import { ZERO_QUALITY_PROXY_COUNTS } from '../metrics/quality-proxy-tracker.js';
|
|
3
|
+
import { SessionStore } from '../storage/session-store.js';
|
|
4
|
+
const logger = createLogger('homelab-accumulator');
|
|
5
|
+
const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
6
|
+
const STALE_SESSION_MS = 3 * 60 * 1000;
|
|
7
|
+
const FLUSH_INTERVAL_MS = 60_000;
|
|
8
|
+
function isIngestPayload(v) {
|
|
9
|
+
if (typeof v !== 'object' || v === null)
|
|
10
|
+
return false;
|
|
11
|
+
const obj = v;
|
|
12
|
+
return (typeof obj.developer === 'string' &&
|
|
13
|
+
typeof obj.sessionId === 'string' &&
|
|
14
|
+
Array.isArray(obj.records));
|
|
15
|
+
}
|
|
16
|
+
export class HomelabAccumulator {
|
|
17
|
+
sessionStore;
|
|
18
|
+
sessions = new Map();
|
|
19
|
+
flushTimer = null;
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.sessionStore = new SessionStore({ storagePath: opts.storagePath });
|
|
22
|
+
}
|
|
23
|
+
handleIngest(payload) {
|
|
24
|
+
if (!isIngestPayload(payload))
|
|
25
|
+
return 'invalid';
|
|
26
|
+
const { developer, sessionId, records } = payload;
|
|
27
|
+
if (!developer || !SESSION_ID_RE.test(sessionId))
|
|
28
|
+
return 'invalid';
|
|
29
|
+
const key = `${developer}:${sessionId}`;
|
|
30
|
+
const existing = this.sessions.get(key);
|
|
31
|
+
if (existing) {
|
|
32
|
+
existing.records.push(...records);
|
|
33
|
+
existing.lastSeen = Date.now();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.sessions.set(key, {
|
|
37
|
+
developer,
|
|
38
|
+
sessionId,
|
|
39
|
+
records: [...records],
|
|
40
|
+
startTime: records[0]?.timestamp ?? Date.now(),
|
|
41
|
+
lastSeen: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return 'ok';
|
|
45
|
+
}
|
|
46
|
+
start() {
|
|
47
|
+
this.flushTimer = setInterval(() => this.flushStaleSessions(), FLUSH_INTERVAL_MS);
|
|
48
|
+
this.flushTimer.unref?.();
|
|
49
|
+
}
|
|
50
|
+
stop() {
|
|
51
|
+
if (this.flushTimer !== null) {
|
|
52
|
+
clearInterval(this.flushTimer);
|
|
53
|
+
this.flushTimer = null;
|
|
54
|
+
}
|
|
55
|
+
this.flushAllSessions();
|
|
56
|
+
}
|
|
57
|
+
flushStaleSessions() {
|
|
58
|
+
const cutoff = Date.now() - STALE_SESSION_MS;
|
|
59
|
+
for (const [key, session] of this.sessions) {
|
|
60
|
+
if (session.lastSeen < cutoff) {
|
|
61
|
+
this.saveSession(session);
|
|
62
|
+
this.sessions.delete(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
flushAllSessions() {
|
|
67
|
+
for (const session of this.sessions.values()) {
|
|
68
|
+
this.saveSession(session);
|
|
69
|
+
}
|
|
70
|
+
this.sessions.clear();
|
|
71
|
+
}
|
|
72
|
+
saveSession(session) {
|
|
73
|
+
const summary = buildMinimalSummary(session);
|
|
74
|
+
try {
|
|
75
|
+
this.sessionStore.saveSession(summary);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
logger.warn('Failed to save homelab session', {
|
|
79
|
+
error: String(err),
|
|
80
|
+
sessionId: session.sessionId,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function buildMinimalSummary(session) {
|
|
86
|
+
const toolBreakdown = {};
|
|
87
|
+
for (const r of session.records) {
|
|
88
|
+
toolBreakdown[r.toolName] = (toolBreakdown[r.toolName] ?? 0) + 1;
|
|
89
|
+
}
|
|
90
|
+
const endTime = session.records.at(-1)?.timestamp ?? session.lastSeen;
|
|
91
|
+
return {
|
|
92
|
+
sessionId: session.sessionId,
|
|
93
|
+
startTime: session.startTime,
|
|
94
|
+
endTime,
|
|
95
|
+
durationMs: endTime - session.startTime,
|
|
96
|
+
toolCallCount: session.records.length,
|
|
97
|
+
developer: session.developer,
|
|
98
|
+
sessionName: null,
|
|
99
|
+
sessionNameSource: null,
|
|
100
|
+
sessionIntent: null,
|
|
101
|
+
repoName: null,
|
|
102
|
+
model: null,
|
|
103
|
+
toolBreakdown,
|
|
104
|
+
filesRead: [],
|
|
105
|
+
filesModified: [],
|
|
106
|
+
linesAdded: 0,
|
|
107
|
+
linesRemoved: 0,
|
|
108
|
+
bashCommandCount: 0,
|
|
109
|
+
testRunCount: 0,
|
|
110
|
+
testPassCount: 0,
|
|
111
|
+
buildRunCount: 0,
|
|
112
|
+
buildPassCount: 0,
|
|
113
|
+
estimatedCostUsd: null,
|
|
114
|
+
subagentCostUsd: 0,
|
|
115
|
+
tokensInput: 0,
|
|
116
|
+
tokensOutput: 0,
|
|
117
|
+
tokensThinking: 0,
|
|
118
|
+
tokensCacheRead: 0,
|
|
119
|
+
tokensCacheCreation: 0,
|
|
120
|
+
cacheSavingsUsd: 0,
|
|
121
|
+
efficiencyScore: null,
|
|
122
|
+
toolSelectionMetrics: null,
|
|
123
|
+
modelBreakdown: {},
|
|
124
|
+
costByWorkflowRunId: {},
|
|
125
|
+
qualityProxy: { ...ZERO_QUALITY_PROXY_COUNTS },
|
|
126
|
+
antiPatterns: [],
|
|
127
|
+
taskCount: 0,
|
|
128
|
+
taskSuccessRate: null,
|
|
129
|
+
toolSuccessRate: null,
|
|
130
|
+
contextCompressions: 0,
|
|
131
|
+
agentSpawns: 0,
|
|
132
|
+
userMessages: 0,
|
|
133
|
+
assistantMessages: 0,
|
|
134
|
+
userCorrections: 0,
|
|
135
|
+
outcome: 'unknown',
|
|
136
|
+
platform: 'homelab',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=homelab-accumulator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"homelab-accumulator.js","sourceRoot":"","sources":["../../src/homelab/homelab-accumulator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ToolCallRecord } from '../storage/types.js';
|
|
2
|
+
export interface HomelabForwarderOptions {
|
|
3
|
+
readonly serverUrl: string;
|
|
4
|
+
readonly token: string;
|
|
5
|
+
readonly developer: string;
|
|
6
|
+
readonly sessionId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class HomelabForwarder {
|
|
9
|
+
private readonly serverUrl;
|
|
10
|
+
private readonly token;
|
|
11
|
+
private readonly developer;
|
|
12
|
+
private readonly sessionId;
|
|
13
|
+
private buffer;
|
|
14
|
+
private timer;
|
|
15
|
+
private stopped;
|
|
16
|
+
private readonly dispatcher;
|
|
17
|
+
constructor(opts: HomelabForwarderOptions);
|
|
18
|
+
start(): void;
|
|
19
|
+
enqueue(record: ToolCallRecord): void;
|
|
20
|
+
flush(): Promise<void>;
|
|
21
|
+
stop(): Promise<void>;
|
|
22
|
+
private post;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=homelab-forwarder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"homelab-forwarder.d.ts","sourceRoot":"","sources":["../../src/homelab/homelab-forwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAO1D,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,OAAO,CAAS;IAOxB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;gBAEvB,IAAI,EAAE,uBAAuB;IAazC,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAQ/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAYb,IAAI;CA2BnB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Agent } from 'undici';
|
|
2
|
+
import { createLogger } from '../shared/index.js';
|
|
3
|
+
import { validateSsrfUrl, createSsrfSafeLookup } from '../security/ssrf.js';
|
|
4
|
+
const logger = createLogger('homelab-forwarder');
|
|
5
|
+
const MAX_BUFFER_SIZE = 1000;
|
|
6
|
+
const FLUSH_INTERVAL_MS = 5_000;
|
|
7
|
+
export class HomelabForwarder {
|
|
8
|
+
serverUrl;
|
|
9
|
+
token;
|
|
10
|
+
developer;
|
|
11
|
+
sessionId;
|
|
12
|
+
buffer = [];
|
|
13
|
+
timer = null;
|
|
14
|
+
stopped = false;
|
|
15
|
+
// Built once and reused for every post() call. Its connect.lookup resolves
|
|
16
|
+
// and validates the address actually connected to (see
|
|
17
|
+
// createSsrfSafeLookup()'s doc comment) — allowPrivateNetworks: true
|
|
18
|
+
// because a homelab server is, by design, almost always at a private LAN
|
|
19
|
+
// address; only cloud metadata endpoints and non-http(s) schemes are
|
|
20
|
+
// blocked here, not RFC-1918/loopback ranges.
|
|
21
|
+
dispatcher;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
validateSsrfUrl('HomelabForwarder', new URL(opts.serverUrl), { allowPrivateNetworks: true });
|
|
24
|
+
this.serverUrl = opts.serverUrl;
|
|
25
|
+
this.token = opts.token;
|
|
26
|
+
this.developer = opts.developer;
|
|
27
|
+
this.sessionId = opts.sessionId;
|
|
28
|
+
this.dispatcher = new Agent({
|
|
29
|
+
connect: {
|
|
30
|
+
lookup: createSsrfSafeLookup('HomelabForwarder (resolved)', { allowPrivateNetworks: true }),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
start() {
|
|
35
|
+
if (this.stopped)
|
|
36
|
+
return;
|
|
37
|
+
this.timer = setInterval(() => {
|
|
38
|
+
void this.flush();
|
|
39
|
+
}, FLUSH_INTERVAL_MS);
|
|
40
|
+
this.timer.unref?.();
|
|
41
|
+
}
|
|
42
|
+
enqueue(record) {
|
|
43
|
+
if (this.stopped)
|
|
44
|
+
return;
|
|
45
|
+
if (this.buffer.length >= MAX_BUFFER_SIZE) {
|
|
46
|
+
this.buffer.shift();
|
|
47
|
+
}
|
|
48
|
+
this.buffer.push(record);
|
|
49
|
+
}
|
|
50
|
+
async flush() {
|
|
51
|
+
if (this.buffer.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const batch = this.buffer.splice(0);
|
|
54
|
+
await this.post(batch);
|
|
55
|
+
}
|
|
56
|
+
async stop() {
|
|
57
|
+
this.stopped = true;
|
|
58
|
+
if (this.timer !== null) {
|
|
59
|
+
clearInterval(this.timer);
|
|
60
|
+
this.timer = null;
|
|
61
|
+
}
|
|
62
|
+
await this.flush();
|
|
63
|
+
await this.dispatcher.close().catch((err) => {
|
|
64
|
+
logger.warn('Failed to close homelab forwarder dispatcher', { error: String(err) });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async post(records) {
|
|
68
|
+
const payload = JSON.stringify({
|
|
69
|
+
developer: this.developer,
|
|
70
|
+
sessionId: this.sessionId,
|
|
71
|
+
records,
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(`${this.serverUrl}/ingest`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
Authorization: `Bearer ${this.token}`,
|
|
79
|
+
},
|
|
80
|
+
body: payload,
|
|
81
|
+
signal: AbortSignal.timeout(5_000),
|
|
82
|
+
dispatcher: this.dispatcher,
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`HTTP ${response.status}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
logger.warn('Failed to forward events to homelab server', {
|
|
90
|
+
error: String(err),
|
|
91
|
+
count: records.length,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=homelab-forwarder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"homelab-forwarder.js","sourceRoot":"","sources":["../../src/homelab/homelab-forwarder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { HomelabForwarder } from './homelab-forwarder.js';
|
|
2
|
+
export type { HomelabForwarderOptions } from './homelab-forwarder.js';
|
|
3
|
+
export { HomelabAccumulator } from './homelab-accumulator.js';
|
|
4
|
+
export type { HomelabIngestPayload } from './homelab-accumulator.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/homelab/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/homelab/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { Command, Option } from 'commander';
|
|
4
4
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
6
7
|
import { AlertLog } from './alerts/alert-log.js';
|
|
7
8
|
import { AlertSnapshotCollector } from './alerts/alert-snapshot-collector.js';
|
|
@@ -75,6 +76,7 @@ import { SessionSpan } from './tracing/session-span.js';
|
|
|
75
76
|
import { TaskSpanTracker } from './tracing/task-span-tracker.js';
|
|
76
77
|
import { emitToolCallSpan } from './tracing/tool-call-span.js';
|
|
77
78
|
import { NrIngestManager } from './transport/nr-ingest.js';
|
|
79
|
+
import { HomelabAccumulator, HomelabForwarder } from './homelab/index.js';
|
|
78
80
|
import { VERSION } from './version.js';
|
|
79
81
|
export { loadMcpConfig, redactSensitive } from './config.js';
|
|
80
82
|
export { AmazonQAdapter, ClaudeCodeAdapter, ContinueAdapter, CopilotAdapter, createDefaultRegistry, CursorAdapter, GenericMcpAdapter, parseCopilotUsageResponse, PlatformRegistry, REPORT_SESSION_END_TOOL, REPORT_SESSION_START_TOOL, REPORT_TOOL_CALL_TOOL, validateReportToolCallInput, WindsurfAdapter, ZedAdapter, } from './platforms/index.js';
|
|
@@ -324,6 +326,7 @@ export function startDashboardRepoll(opts) {
|
|
|
324
326
|
const SUBCOMMAND_NAMES = [
|
|
325
327
|
'deploy-dashboards',
|
|
326
328
|
'deploy-alerts',
|
|
329
|
+
'server',
|
|
327
330
|
'install',
|
|
328
331
|
'uninstall',
|
|
329
332
|
'setup',
|
|
@@ -335,8 +338,8 @@ const SUBCOMMAND_NAMES = [
|
|
|
335
338
|
];
|
|
336
339
|
// Install-CLI subcommands — a subset of SUBCOMMAND_NAMES routed to runInstallCli.
|
|
337
340
|
// Derived from SUBCOMMAND_NAMES to ensure a single source of truth: deploy-*
|
|
338
|
-
// commands are excluded and handled by the commander path in dispatchSubcommand.
|
|
339
|
-
const INSTALL_CLI_SUBCOMMANDS = SUBCOMMAND_NAMES.filter((s) => s !== 'deploy-dashboards' && s !== 'deploy-alerts');
|
|
341
|
+
// and server commands are excluded and handled by the commander path in dispatchSubcommand.
|
|
342
|
+
const INSTALL_CLI_SUBCOMMANDS = SUBCOMMAND_NAMES.filter((s) => s !== 'deploy-dashboards' && s !== 'deploy-alerts' && s !== 'server');
|
|
340
343
|
function isSubcommand(value) {
|
|
341
344
|
return typeof value === 'string' && SUBCOMMAND_NAMES.includes(value);
|
|
342
345
|
}
|
|
@@ -392,6 +395,76 @@ export async function dispatchSubcommand(argv) {
|
|
|
392
395
|
process.exitCode = code;
|
|
393
396
|
});
|
|
394
397
|
}
|
|
398
|
+
else if (sub === 'server') {
|
|
399
|
+
program
|
|
400
|
+
.command('server')
|
|
401
|
+
.description('Start Preflight in homelab server mode — receives events forwarded from remote clients and accumulates them to disk (no dashboard yet — see docs/homelab.md)')
|
|
402
|
+
.option('--port <port>', 'Port to bind to (default: 7777 or NEW_RELIC_AI_HOMELAB_SERVER_PORT)')
|
|
403
|
+
.option('--token <token>', 'Bearer token for /ingest auth (default: NEW_RELIC_AI_HOMELAB_TOKEN)')
|
|
404
|
+
.option('--bind <address>', 'Bind address (default: 0.0.0.0 or NEW_RELIC_AI_HOMELAB_BIND_ADDRESS)')
|
|
405
|
+
.option('--config <path>', 'Path to config file')
|
|
406
|
+
.action(async (opts) => {
|
|
407
|
+
if (opts.port)
|
|
408
|
+
process.env.NEW_RELIC_AI_HOMELAB_SERVER_PORT = opts.port;
|
|
409
|
+
if (opts.token)
|
|
410
|
+
process.env.NEW_RELIC_AI_HOMELAB_TOKEN = opts.token;
|
|
411
|
+
if (opts.bind)
|
|
412
|
+
process.env.NEW_RELIC_AI_HOMELAB_BIND_ADDRESS = opts.bind;
|
|
413
|
+
// server mode never needs cloud credentials — force local so loadMcpConfig
|
|
414
|
+
// doesn't require licenseKey/accountId.
|
|
415
|
+
process.env.NR_AI_MODE = 'local';
|
|
416
|
+
const config = loadConfigOrDie({ config: opts.config });
|
|
417
|
+
const token = config.homelabToken;
|
|
418
|
+
if (!token) {
|
|
419
|
+
process.stderr.write('[preflight] error: homelab server requires a token. Set --token or NEW_RELIC_AI_HOMELAB_TOKEN.\n');
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
const accumulator = new HomelabAccumulator({ storagePath: config.storagePath });
|
|
423
|
+
accumulator.start();
|
|
424
|
+
const bus = new LiveEventBus();
|
|
425
|
+
const dashboardServer = new DashboardServer({
|
|
426
|
+
port: config.homelabServer.port,
|
|
427
|
+
host: config.homelabServer.bindAddress,
|
|
428
|
+
bus,
|
|
429
|
+
serverMode: true,
|
|
430
|
+
ingestHandler: (authHeader, body) => {
|
|
431
|
+
const expected = `Bearer ${token}`;
|
|
432
|
+
const isAuthed = authHeader !== undefined &&
|
|
433
|
+
authHeader.length === expected.length &&
|
|
434
|
+
timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected));
|
|
435
|
+
if (!isAuthed)
|
|
436
|
+
return 401;
|
|
437
|
+
const result = accumulator.handleIngest(body);
|
|
438
|
+
return result === 'ok' ? 204 : 400;
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
await dashboardServer.start();
|
|
442
|
+
logger.info('Homelab server ready', {
|
|
443
|
+
port: config.homelabServer.port,
|
|
444
|
+
bind: config.homelabServer.bindAddress,
|
|
445
|
+
});
|
|
446
|
+
// Keep the action (and therefore program.parseAsync) alive until a
|
|
447
|
+
// signal fires — without this, the action returns immediately and
|
|
448
|
+
// dispatchSubcommand exits the process.
|
|
449
|
+
await new Promise((resolve) => {
|
|
450
|
+
let shutting = false;
|
|
451
|
+
const shutdown = async () => {
|
|
452
|
+
if (shutting)
|
|
453
|
+
return;
|
|
454
|
+
shutting = true;
|
|
455
|
+
accumulator.stop();
|
|
456
|
+
await dashboardServer.stop();
|
|
457
|
+
resolve();
|
|
458
|
+
};
|
|
459
|
+
process.on('SIGTERM', () => {
|
|
460
|
+
void shutdown();
|
|
461
|
+
});
|
|
462
|
+
process.on('SIGINT', () => {
|
|
463
|
+
void shutdown();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
}
|
|
395
468
|
else {
|
|
396
469
|
program
|
|
397
470
|
.command('deploy-alerts')
|
|
@@ -582,6 +655,7 @@ async function main() {
|
|
|
582
655
|
}, getPendingConfirmationCapMs());
|
|
583
656
|
pendingConfirmationCapTimer.unref?.();
|
|
584
657
|
};
|
|
658
|
+
let homelabForwarder = null;
|
|
585
659
|
let shuttingDown = false;
|
|
586
660
|
const shutdown = async () => {
|
|
587
661
|
if (shuttingDown)
|
|
@@ -652,6 +726,7 @@ async function main() {
|
|
|
652
726
|
nrIngest ? nrIngest.stop() : Promise.resolve(),
|
|
653
727
|
mcpServer ? mcpServer.close() : Promise.resolve(),
|
|
654
728
|
proxyManager ? proxyManager.stop() : Promise.resolve(),
|
|
729
|
+
homelabForwarder ? homelabForwarder.stop() : Promise.resolve(),
|
|
655
730
|
]);
|
|
656
731
|
for (const r of stopResults) {
|
|
657
732
|
if (r.status === 'rejected') {
|
|
@@ -1549,6 +1624,28 @@ async function main() {
|
|
|
1549
1624
|
});
|
|
1550
1625
|
capturedNrIngest = nrIngest;
|
|
1551
1626
|
}
|
|
1627
|
+
// Start homelab forwarder if configured. Construction validates
|
|
1628
|
+
// homelabServerUrl (scheme + cloud-metadata-endpoint check) and can
|
|
1629
|
+
// throw — caught here so a bad/malicious URL only disables forwarding,
|
|
1630
|
+
// matching the "forwarding failures are non-fatal" contract in
|
|
1631
|
+
// docs/homelab.md, rather than crashing MCP startup.
|
|
1632
|
+
if (config.homelabServerUrl && config.homelabToken) {
|
|
1633
|
+
try {
|
|
1634
|
+
homelabForwarder = new HomelabForwarder({
|
|
1635
|
+
serverUrl: config.homelabServerUrl,
|
|
1636
|
+
token: config.homelabToken,
|
|
1637
|
+
developer: config.developer,
|
|
1638
|
+
sessionId: sessionTraceId ?? 'unknown',
|
|
1639
|
+
});
|
|
1640
|
+
homelabForwarder.start();
|
|
1641
|
+
}
|
|
1642
|
+
catch (err) {
|
|
1643
|
+
homelabForwarder = null;
|
|
1644
|
+
logger.error('Homelab forwarder failed to start — continuing without it', {
|
|
1645
|
+
error: String(err),
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1552
1649
|
const capturedAlertEngine = alertEngine;
|
|
1553
1650
|
const capturedAlertSnapshotCollector = alertSnapshotCollector;
|
|
1554
1651
|
budgetTracker.setOnThreshold((event) => {
|
|
@@ -1614,7 +1711,23 @@ async function main() {
|
|
|
1614
1711
|
contextWindowTracker.recordToolCall(rawRecord);
|
|
1615
1712
|
contextTracker.recordToolCall(rawRecord);
|
|
1616
1713
|
latencyTracker.recordToolCall(rawRecord);
|
|
1617
|
-
retryDetector.recordToolCall(rawRecord);
|
|
1714
|
+
const thrashingAlert = retryDetector.recordToolCall(rawRecord);
|
|
1715
|
+
if (thrashingAlert) {
|
|
1716
|
+
capturedNrIngest?.ingestRetryAlert(thrashingAlert, {
|
|
1717
|
+
platform: typeof rawRecord.platform === 'string' ? rawRecord.platform : undefined,
|
|
1718
|
+
});
|
|
1719
|
+
liveBus.emit('retry-alert', {
|
|
1720
|
+
// alert.sessionId (not sessionTraceId) — see RetryDetector's
|
|
1721
|
+
// session-grouping comment: in --local mode this process's own
|
|
1722
|
+
// RetryDetector drains every session's buffer, so only the
|
|
1723
|
+
// alert's own sessionId can be trusted to name the offending one.
|
|
1724
|
+
sessionId: thrashingAlert.sessionId ?? undefined,
|
|
1725
|
+
toolName: thrashingAlert.toolName,
|
|
1726
|
+
occurrences: thrashingAlert.occurrences,
|
|
1727
|
+
tokensWasted: thrashingAlert.tokensWastedEstimate,
|
|
1728
|
+
ts: thrashingAlert.timestamp,
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1618
1731
|
qualityProxyTracker.recordToolCall(rawRecord);
|
|
1619
1732
|
const turnId = turnTracker.recordToolCall(rawRecord);
|
|
1620
1733
|
const turnNumber = turnTracker.getCurrentTurnNumber();
|
|
@@ -1777,6 +1890,7 @@ async function main() {
|
|
|
1777
1890
|
});
|
|
1778
1891
|
}
|
|
1779
1892
|
}
|
|
1893
|
+
homelabForwarder?.enqueue(record);
|
|
1780
1894
|
},
|
|
1781
1895
|
onTokenEvent: (tokenEvent) => {
|
|
1782
1896
|
if (!costTracker || !config)
|