@highflame/overwatch-v2 2.0.0-internal.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/LICENSE +21 -0
- package/README.md +36 -0
- package/bin/overwatch +12 -0
- package/dist/bin/overwatch +12 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +6295 -0
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.js +3578 -0
- package/dist/hooks/claudecode/hooks.json.template +20 -0
- package/dist/hooks/cursor/hooks.json.template +62 -0
- package/dist/hooks/universal-hook.ps1 +118 -0
- package/dist/hooks/universal-hook.sh +60 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3429 -0
- package/dist/installer.d.ts +30 -0
- package/dist/module.d.ts +55 -0
- package/dist/scanner.d.ts +79 -0
- package/dist/version.d.ts +4 -0
- package/lib/platform-loader.js +234 -0
- package/package.json +75 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,3578 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
13
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
14
|
+
};
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
28
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
29
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
30
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
31
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
32
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
33
|
+
mod
|
|
34
|
+
));
|
|
35
|
+
|
|
36
|
+
// src/utils/logger.ts
|
|
37
|
+
var fs, path, os, LOG_FILE, Logger, logger;
|
|
38
|
+
var init_logger = __esm({
|
|
39
|
+
"src/utils/logger.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
fs = __toESM(require("fs"));
|
|
42
|
+
path = __toESM(require("path"));
|
|
43
|
+
os = __toESM(require("os"));
|
|
44
|
+
LOG_FILE = path.join(os.homedir(), ".overwatch", "guardian.log");
|
|
45
|
+
Logger = class {
|
|
46
|
+
constructor(level = 1 /* INFO */) {
|
|
47
|
+
this.fileEnabled = true;
|
|
48
|
+
this.level = level;
|
|
49
|
+
this.ensureLogDir();
|
|
50
|
+
}
|
|
51
|
+
ensureLogDir() {
|
|
52
|
+
try {
|
|
53
|
+
const dir = path.dirname(LOG_FILE);
|
|
54
|
+
if (!fs.existsSync(dir)) {
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
this.fileEnabled = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
setLevel(level) {
|
|
62
|
+
this.level = level;
|
|
63
|
+
}
|
|
64
|
+
getCallerFile(offset) {
|
|
65
|
+
const err = new Error();
|
|
66
|
+
const stack = err.stack?.split("\n");
|
|
67
|
+
if (stack && stack.length > offset) {
|
|
68
|
+
const callerLine = stack[offset] || "";
|
|
69
|
+
const match = callerLine.match(/at\s+(?:.*\s+\()?(.+?):\d+:\d+\)?$/);
|
|
70
|
+
if (match && match[1]) {
|
|
71
|
+
return path.basename(match[1]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return "unknown";
|
|
75
|
+
}
|
|
76
|
+
formatArgs(args) {
|
|
77
|
+
if (args.length === 0)
|
|
78
|
+
return "";
|
|
79
|
+
return args.map((arg) => {
|
|
80
|
+
if (typeof arg === "object" && arg !== null) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.stringify(arg).replace(/^{|}$/g, "").replace(/"([^"]+)":/g, "$1=");
|
|
83
|
+
} catch {
|
|
84
|
+
return String(arg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return String(arg);
|
|
88
|
+
}).join(" ");
|
|
89
|
+
}
|
|
90
|
+
writeToFile(level, message, args) {
|
|
91
|
+
if (!this.fileEnabled)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
95
|
+
const callerFile = this.getCallerFile(4);
|
|
96
|
+
const formattedArgs = this.formatArgs(args);
|
|
97
|
+
const argsStr = formattedArgs ? " " + formattedArgs : "";
|
|
98
|
+
const line = `${timestamp} [${level.padEnd(5)}] [${callerFile}] ${message}${argsStr}
|
|
99
|
+
`;
|
|
100
|
+
fs.appendFileSync(LOG_FILE, line);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
debug(message, ...args) {
|
|
105
|
+
if (this.level <= 0 /* DEBUG */) {
|
|
106
|
+
const callerFile = this.getCallerFile(3);
|
|
107
|
+
console.debug(`[DEBUG] [${callerFile}] ${message}`, ...args);
|
|
108
|
+
this.writeToFile("DEBUG", message, args);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
info(message, ...args) {
|
|
112
|
+
if (this.level <= 1 /* INFO */) {
|
|
113
|
+
const callerFile = this.getCallerFile(3);
|
|
114
|
+
console.info(`[INFO ] [${callerFile}] ${message}`, ...args);
|
|
115
|
+
this.writeToFile("INFO", message, args);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
warn(message, ...args) {
|
|
119
|
+
if (this.level <= 2 /* WARN */) {
|
|
120
|
+
const callerFile = this.getCallerFile(3);
|
|
121
|
+
console.warn(`[WARN ] [${callerFile}] ${message}`, ...args);
|
|
122
|
+
this.writeToFile("WARN", message, args);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
error(message, ...args) {
|
|
126
|
+
if (this.level <= 3 /* ERROR */) {
|
|
127
|
+
const callerFile = this.getCallerFile(3);
|
|
128
|
+
console.error(`[ERROR] [${callerFile}] ${message}`, ...args);
|
|
129
|
+
this.writeToFile("ERROR", message, args);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
logger = new Logger();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// src/utils/performance.ts
|
|
138
|
+
var init_performance = __esm({
|
|
139
|
+
"src/utils/performance.ts"() {
|
|
140
|
+
"use strict";
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// src/utils/index.ts
|
|
145
|
+
var init_utils = __esm({
|
|
146
|
+
"src/utils/index.ts"() {
|
|
147
|
+
"use strict";
|
|
148
|
+
init_logger();
|
|
149
|
+
init_performance();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// src/auth/pkce.ts
|
|
154
|
+
function base64URLEncode(buffer) {
|
|
155
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
156
|
+
}
|
|
157
|
+
function generateCodeVerifier() {
|
|
158
|
+
const buffer = crypto.randomBytes(32);
|
|
159
|
+
return base64URLEncode(buffer);
|
|
160
|
+
}
|
|
161
|
+
function generateCodeChallenge(verifier) {
|
|
162
|
+
const hash = crypto.createHash("sha256").update(verifier).digest();
|
|
163
|
+
return base64URLEncode(hash);
|
|
164
|
+
}
|
|
165
|
+
function generateState() {
|
|
166
|
+
return crypto.randomBytes(16).toString("hex");
|
|
167
|
+
}
|
|
168
|
+
var crypto;
|
|
169
|
+
var init_pkce = __esm({
|
|
170
|
+
"src/auth/pkce.ts"() {
|
|
171
|
+
"use strict";
|
|
172
|
+
crypto = __toESM(require("crypto"));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// src/version.ts
|
|
177
|
+
var VERSION, HIGHFLAME_API_URL, HIGHFLAME_OAUTH_URL, HIGHFLAME_CERBERUS_URL;
|
|
178
|
+
var init_version = __esm({
|
|
179
|
+
"src/version.ts"() {
|
|
180
|
+
"use strict";
|
|
181
|
+
VERSION = true ? "2.0.0-internal.1" : "0.0.0-dev";
|
|
182
|
+
HIGHFLAME_API_URL = "https://api.highflame.ai";
|
|
183
|
+
HIGHFLAME_OAUTH_URL = "https://studio.highflame.ai";
|
|
184
|
+
HIGHFLAME_CERBERUS_URL = "https://cerberus.highflame.ai";
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// src/types/config.ts
|
|
189
|
+
var DEFAULT_OVERWATCH_CONFIG;
|
|
190
|
+
var init_config = __esm({
|
|
191
|
+
"src/types/config.ts"() {
|
|
192
|
+
"use strict";
|
|
193
|
+
init_version();
|
|
194
|
+
DEFAULT_OVERWATCH_CONFIG = {
|
|
195
|
+
version: VERSION,
|
|
196
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
197
|
+
highflame: {
|
|
198
|
+
baseUrl: HIGHFLAME_API_URL,
|
|
199
|
+
oauthUrl: HIGHFLAME_OAUTH_URL,
|
|
200
|
+
cerberusUrl: HIGHFLAME_CERBERUS_URL
|
|
201
|
+
},
|
|
202
|
+
engines: {
|
|
203
|
+
javelin: {
|
|
204
|
+
enabled: true
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
daemon: {
|
|
208
|
+
autoStart: true,
|
|
209
|
+
logLevel: "info"
|
|
210
|
+
},
|
|
211
|
+
enabled: true
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// src/types/events.ts
|
|
217
|
+
var init_events = __esm({
|
|
218
|
+
"src/types/events.ts"() {
|
|
219
|
+
"use strict";
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// src/types/remote-policy.ts
|
|
224
|
+
var init_remote_policy = __esm({
|
|
225
|
+
"src/types/remote-policy.ts"() {
|
|
226
|
+
"use strict";
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// src/types/index.ts
|
|
231
|
+
var init_types = __esm({
|
|
232
|
+
"src/types/index.ts"() {
|
|
233
|
+
"use strict";
|
|
234
|
+
init_config();
|
|
235
|
+
init_events();
|
|
236
|
+
init_remote_policy();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// src/auth/token-store.ts
|
|
241
|
+
function loadSession() {
|
|
242
|
+
try {
|
|
243
|
+
if (fs2.existsSync(SESSION_PATH)) {
|
|
244
|
+
const data = fs2.readFileSync(SESSION_PATH, "utf-8");
|
|
245
|
+
return JSON.parse(data);
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
logger.debug("Failed to load session file", {
|
|
249
|
+
error: e instanceof Error ? e.message : String(e),
|
|
250
|
+
path: SESSION_PATH
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
function writeSession(session) {
|
|
256
|
+
if (!fs2.existsSync(CONFIG_DIR)) {
|
|
257
|
+
fs2.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
const data = JSON.stringify(session, null, 2);
|
|
260
|
+
fs2.writeFileSync(SESSION_PATH, data, { mode: SESSION_FILE_MODE });
|
|
261
|
+
try {
|
|
262
|
+
fs2.chmodSync(SESSION_PATH, SESSION_FILE_MODE);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
logger.debug("Failed to set session file permissions", {
|
|
265
|
+
error: e instanceof Error ? e.message : String(e)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function saveTokens(tokens, userInfo2) {
|
|
270
|
+
const expiresAt = tokens.expires_at ?? Date.now() + TOKEN_EXPIRY_SECONDS * 1e3;
|
|
271
|
+
const auth = {
|
|
272
|
+
access_token: tokens.access_token,
|
|
273
|
+
refresh_token: tokens.refresh_token || "",
|
|
274
|
+
expires_at: expiresAt,
|
|
275
|
+
token_type: tokens.token_type,
|
|
276
|
+
user: userInfo2 ? {
|
|
277
|
+
email: userInfo2.email
|
|
278
|
+
} : void 0,
|
|
279
|
+
authenticated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
280
|
+
};
|
|
281
|
+
writeSession(auth);
|
|
282
|
+
}
|
|
283
|
+
function loadTokens() {
|
|
284
|
+
return loadSession();
|
|
285
|
+
}
|
|
286
|
+
async function getValidAccessToken() {
|
|
287
|
+
const auth = loadTokens();
|
|
288
|
+
if (!auth)
|
|
289
|
+
return null;
|
|
290
|
+
if (isTokenExpired(auth.expires_at)) {
|
|
291
|
+
if (auth.refresh_token) {
|
|
292
|
+
const { refreshAccessToken: refreshAccessToken2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
|
|
293
|
+
const refreshed = await refreshAccessToken2(auth.refresh_token);
|
|
294
|
+
if (refreshed) {
|
|
295
|
+
saveTokens(refreshed);
|
|
296
|
+
return refreshed.access_token;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return auth.access_token;
|
|
302
|
+
}
|
|
303
|
+
function getAuthStatus() {
|
|
304
|
+
const auth = loadTokens();
|
|
305
|
+
if (!auth) {
|
|
306
|
+
return { authenticated: false };
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
authenticated: true,
|
|
310
|
+
email: auth.user?.email,
|
|
311
|
+
name: auth.user?.name,
|
|
312
|
+
orgName: auth.user?.org_name,
|
|
313
|
+
expiresAt: auth.expires_at,
|
|
314
|
+
expired: isTokenExpired(auth.expires_at),
|
|
315
|
+
expiringSoon: isTokenExpiringSoon(auth.expires_at)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
var fs2, path2, os2, CONFIG_DIR, SESSION_PATH, SESSION_FILE_MODE;
|
|
319
|
+
var init_token_store = __esm({
|
|
320
|
+
"src/auth/token-store.ts"() {
|
|
321
|
+
"use strict";
|
|
322
|
+
fs2 = __toESM(require("fs"));
|
|
323
|
+
path2 = __toESM(require("path"));
|
|
324
|
+
os2 = __toESM(require("os"));
|
|
325
|
+
init_oauth();
|
|
326
|
+
init_utils();
|
|
327
|
+
CONFIG_DIR = path2.join(os2.homedir(), ".overwatch");
|
|
328
|
+
SESSION_PATH = path2.join(CONFIG_DIR, "session.json");
|
|
329
|
+
SESSION_FILE_MODE = 384;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// src/config/manager.ts
|
|
334
|
+
function loadConfig() {
|
|
335
|
+
try {
|
|
336
|
+
if (fs3.existsSync(CONFIG_PATH)) {
|
|
337
|
+
const data = fs3.readFileSync(CONFIG_PATH, "utf-8");
|
|
338
|
+
return JSON.parse(data);
|
|
339
|
+
}
|
|
340
|
+
} catch (e) {
|
|
341
|
+
console.error("Failed to load config:", e);
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
function getBaseUrl() {
|
|
346
|
+
const config = loadConfig();
|
|
347
|
+
return config?.highflame?.baseUrl || process.env.HIGHFLAME_BASE_URL || HIGHFLAME_API_URL;
|
|
348
|
+
}
|
|
349
|
+
function getOAuthUrl() {
|
|
350
|
+
const config = loadConfig();
|
|
351
|
+
return config?.highflame?.oauthUrl || HIGHFLAME_OAUTH_URL;
|
|
352
|
+
}
|
|
353
|
+
function getCerberusUrl() {
|
|
354
|
+
const config = loadConfig();
|
|
355
|
+
return config?.highflame?.cerberusUrl || process.env.CERBERUS_BASE_URL || HIGHFLAME_CERBERUS_URL;
|
|
356
|
+
}
|
|
357
|
+
function getLLMConfig() {
|
|
358
|
+
const config = loadConfig();
|
|
359
|
+
const envKey = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY;
|
|
360
|
+
if (envKey) {
|
|
361
|
+
return {
|
|
362
|
+
apiKey: envKey,
|
|
363
|
+
provider: config?.llm?.provider || "openai"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return config?.llm || null;
|
|
367
|
+
}
|
|
368
|
+
var fs3, path3, os3, CONFIG_DIR2, CONFIG_PATH;
|
|
369
|
+
var init_manager = __esm({
|
|
370
|
+
"src/config/manager.ts"() {
|
|
371
|
+
"use strict";
|
|
372
|
+
fs3 = __toESM(require("fs"));
|
|
373
|
+
path3 = __toESM(require("path"));
|
|
374
|
+
os3 = __toESM(require("os"));
|
|
375
|
+
init_types();
|
|
376
|
+
init_token_store();
|
|
377
|
+
init_version();
|
|
378
|
+
CONFIG_DIR2 = path3.join(os3.homedir(), ".overwatch");
|
|
379
|
+
CONFIG_PATH = path3.join(CONFIG_DIR2, "config.json");
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// src/config/index.ts
|
|
384
|
+
var init_config2 = __esm({
|
|
385
|
+
"src/config/index.ts"() {
|
|
386
|
+
"use strict";
|
|
387
|
+
init_manager();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// src/auth/oauth.ts
|
|
392
|
+
var oauth_exports = {};
|
|
393
|
+
__export(oauth_exports, {
|
|
394
|
+
DEFAULT_EXPIRY_BUFFER_SECONDS: () => DEFAULT_EXPIRY_BUFFER_SECONDS,
|
|
395
|
+
EXPIRY_WARN_THRESHOLD_SECONDS: () => EXPIRY_WARN_THRESHOLD_SECONDS,
|
|
396
|
+
OAUTH_CONFIG: () => OAUTH_CONFIG,
|
|
397
|
+
TOKEN_EXPIRY_SECONDS: () => TOKEN_EXPIRY_SECONDS,
|
|
398
|
+
buildAuthorizationUrl: () => buildAuthorizationUrl,
|
|
399
|
+
createOAuthState: () => createOAuthState,
|
|
400
|
+
exchangeCodeForTokens: () => exchangeCodeForTokens,
|
|
401
|
+
isTokenExpired: () => isTokenExpired,
|
|
402
|
+
isTokenExpiringSoon: () => isTokenExpiringSoon,
|
|
403
|
+
refreshAccessToken: () => refreshAccessToken
|
|
404
|
+
});
|
|
405
|
+
function buildAuthorizationUrl(state, codeChallenge, redirectUri) {
|
|
406
|
+
const params = new URLSearchParams({
|
|
407
|
+
response_type: "code",
|
|
408
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
409
|
+
redirect_uri: redirectUri,
|
|
410
|
+
code_challenge: codeChallenge,
|
|
411
|
+
code_challenge_method: "S256",
|
|
412
|
+
state,
|
|
413
|
+
scope: OAUTH_CONFIG.scopes.join(" ")
|
|
414
|
+
});
|
|
415
|
+
return `${OAUTH_CONFIG.authUrl}?${params.toString()}`;
|
|
416
|
+
}
|
|
417
|
+
async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
|
|
418
|
+
logger.debug("[OAuth] Exchanging code for tokens", {
|
|
419
|
+
tokenUrl: OAUTH_CONFIG.tokenUrl
|
|
420
|
+
});
|
|
421
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: {
|
|
424
|
+
"Content-Type": "application/json",
|
|
425
|
+
Accept: "application/json"
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
grant_type: "authorization_code",
|
|
429
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
430
|
+
code,
|
|
431
|
+
redirect_uri: redirectUri,
|
|
432
|
+
code_verifier: codeVerifier
|
|
433
|
+
}),
|
|
434
|
+
signal: AbortSignal.timeout(3e4)
|
|
435
|
+
});
|
|
436
|
+
if (!response.ok) {
|
|
437
|
+
const errorText = await response.text();
|
|
438
|
+
logger.error("[OAuth] Token exchange failed", {
|
|
439
|
+
status: response.status,
|
|
440
|
+
body: errorText.substring(0, 500)
|
|
441
|
+
});
|
|
442
|
+
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
443
|
+
}
|
|
444
|
+
const data = await response.json();
|
|
445
|
+
logger.debug("[OAuth] Token exchange successful");
|
|
446
|
+
const expiresInSeconds = data.expires_in || TOKEN_EXPIRY_SECONDS;
|
|
447
|
+
data.expires_at = Date.now() + expiresInSeconds * 1e3;
|
|
448
|
+
const userInfo2 = {
|
|
449
|
+
id: data.user?.id || "",
|
|
450
|
+
email: data.user?.email || ""
|
|
451
|
+
};
|
|
452
|
+
return { tokens: data, userInfo: userInfo2 };
|
|
453
|
+
}
|
|
454
|
+
async function refreshAccessToken(_refreshToken) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function isTokenExpired(expiresAt, bufferSeconds = DEFAULT_EXPIRY_BUFFER_SECONDS) {
|
|
458
|
+
return Date.now() >= expiresAt - bufferSeconds * 1e3;
|
|
459
|
+
}
|
|
460
|
+
function isTokenExpiringSoon(expiresAt, thresholdSeconds = EXPIRY_WARN_THRESHOLD_SECONDS) {
|
|
461
|
+
if (isTokenExpired(expiresAt))
|
|
462
|
+
return false;
|
|
463
|
+
return Date.now() >= expiresAt - thresholdSeconds * 1e3;
|
|
464
|
+
}
|
|
465
|
+
function createOAuthState(guardianPort) {
|
|
466
|
+
const state = generateState();
|
|
467
|
+
const codeVerifier = generateCodeVerifier();
|
|
468
|
+
const redirectUri = `http://127.0.0.1:${guardianPort}/oauth/callback`;
|
|
469
|
+
return {
|
|
470
|
+
state,
|
|
471
|
+
codeVerifier,
|
|
472
|
+
redirectUri,
|
|
473
|
+
createdAt: Date.now(),
|
|
474
|
+
status: "pending"
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
var OAUTH_CONFIG, TOKEN_EXPIRY_SECONDS, DEFAULT_EXPIRY_BUFFER_SECONDS, EXPIRY_WARN_THRESHOLD_SECONDS;
|
|
478
|
+
var init_oauth = __esm({
|
|
479
|
+
"src/auth/oauth.ts"() {
|
|
480
|
+
"use strict";
|
|
481
|
+
init_pkce();
|
|
482
|
+
init_config2();
|
|
483
|
+
init_utils();
|
|
484
|
+
OAUTH_CONFIG = {
|
|
485
|
+
get authUrl() {
|
|
486
|
+
return `${getOAuthUrl()}/cli-auth`;
|
|
487
|
+
},
|
|
488
|
+
get tokenUrl() {
|
|
489
|
+
return `${getOAuthUrl()}/api/cli-auth/token`;
|
|
490
|
+
},
|
|
491
|
+
clientId: "overwatch-cli",
|
|
492
|
+
scopes: ["account:read", "gateway:read"]
|
|
493
|
+
};
|
|
494
|
+
TOKEN_EXPIRY_SECONDS = 90 * 24 * 60 * 60;
|
|
495
|
+
DEFAULT_EXPIRY_BUFFER_SECONDS = 5 * 60;
|
|
496
|
+
EXPIRY_WARN_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// lib/platform-loader.js
|
|
501
|
+
var require_platform_loader = __commonJS({
|
|
502
|
+
"lib/platform-loader.js"(exports2, module2) {
|
|
503
|
+
"use strict";
|
|
504
|
+
var path14 = require("path");
|
|
505
|
+
var fs15 = require("fs");
|
|
506
|
+
var os14 = require("os");
|
|
507
|
+
var PLATFORM_MAP = {
|
|
508
|
+
"darwin-arm64": {
|
|
509
|
+
package: "@highflame/overwatch-v2-darwin-arm64",
|
|
510
|
+
rustTarget: "aarch64-apple-darwin",
|
|
511
|
+
binaryName: "ramparts-aarch64-apple-darwin"
|
|
512
|
+
},
|
|
513
|
+
"darwin-x64": {
|
|
514
|
+
package: "@highflame/overwatch-v2-darwin-x64",
|
|
515
|
+
rustTarget: "x86_64-apple-darwin",
|
|
516
|
+
binaryName: "ramparts-x86_64-apple-darwin"
|
|
517
|
+
},
|
|
518
|
+
"linux-x64": {
|
|
519
|
+
package: "@highflame/overwatch-v2-linux-x64",
|
|
520
|
+
rustTarget: "x86_64-unknown-linux-gnu",
|
|
521
|
+
binaryName: "ramparts-x86_64-unknown-linux-gnu"
|
|
522
|
+
},
|
|
523
|
+
"linux-arm64": {
|
|
524
|
+
package: "@highflame/overwatch-v2-linux-arm64",
|
|
525
|
+
rustTarget: "aarch64-unknown-linux-gnu",
|
|
526
|
+
binaryName: "ramparts-aarch64-unknown-linux-gnu"
|
|
527
|
+
},
|
|
528
|
+
"win32-x64": {
|
|
529
|
+
package: "@highflame/overwatch-v2-win32-x64",
|
|
530
|
+
rustTarget: "x86_64-pc-windows-msvc",
|
|
531
|
+
binaryName: "ramparts-x86_64-pc-windows-msvc.exe"
|
|
532
|
+
},
|
|
533
|
+
"win32-arm64": {
|
|
534
|
+
package: "@highflame/overwatch-v2-win32-arm64",
|
|
535
|
+
rustTarget: "aarch64-pc-windows-msvc",
|
|
536
|
+
binaryName: "ramparts-aarch64-pc-windows-msvc.exe"
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
function getPlatformKey() {
|
|
540
|
+
return `${process.platform}-${process.arch}`;
|
|
541
|
+
}
|
|
542
|
+
function getPlatformConfig() {
|
|
543
|
+
const key = getPlatformKey();
|
|
544
|
+
return PLATFORM_MAP[key] || null;
|
|
545
|
+
}
|
|
546
|
+
function loadPlatformPackage() {
|
|
547
|
+
const config = getPlatformConfig();
|
|
548
|
+
if (!config) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const dynamicRequire = typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require;
|
|
553
|
+
return dynamicRequire(config.package);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function getPackageRoot() {
|
|
559
|
+
let currentDir = __dirname;
|
|
560
|
+
const baseName = path14.basename(currentDir);
|
|
561
|
+
if (baseName === "lib" || baseName === "dist") {
|
|
562
|
+
return path14.resolve(currentDir, "..");
|
|
563
|
+
}
|
|
564
|
+
return path14.resolve(currentDir, "..");
|
|
565
|
+
}
|
|
566
|
+
function getRampartsBinaryPath() {
|
|
567
|
+
const config = getPlatformConfig();
|
|
568
|
+
const platformPkg = loadPlatformPackage();
|
|
569
|
+
if (platformPkg && typeof platformPkg.getRampartsBinaryPath === "function") {
|
|
570
|
+
const binaryPath = platformPkg.getRampartsBinaryPath();
|
|
571
|
+
if (binaryPath && fs15.existsSync(binaryPath)) {
|
|
572
|
+
return binaryPath;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const packageRoot = getPackageRoot();
|
|
576
|
+
const localPaths = [];
|
|
577
|
+
const isWindows = process.platform === "win32";
|
|
578
|
+
const genericBinaryName = isWindows ? "ramparts.exe" : "ramparts";
|
|
579
|
+
if (config) {
|
|
580
|
+
localPaths.push(path14.join(packageRoot, "bin", config.binaryName));
|
|
581
|
+
}
|
|
582
|
+
localPaths.push(
|
|
583
|
+
path14.join(packageRoot, "bin", genericBinaryName),
|
|
584
|
+
path14.join(packageRoot, "dist", "bin", genericBinaryName)
|
|
585
|
+
);
|
|
586
|
+
for (const p of localPaths) {
|
|
587
|
+
if (fs15.existsSync(p)) {
|
|
588
|
+
return p;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const overwatchPaths = [];
|
|
592
|
+
if (config) {
|
|
593
|
+
overwatchPaths.push(
|
|
594
|
+
path14.join(os14.homedir(), ".overwatch", "bin", config.binaryName)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
overwatchPaths.push(
|
|
598
|
+
path14.join(os14.homedir(), ".overwatch", "bin", genericBinaryName)
|
|
599
|
+
);
|
|
600
|
+
for (const p of overwatchPaths) {
|
|
601
|
+
if (fs15.existsSync(p)) {
|
|
602
|
+
return p;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const { execSync } = require("child_process");
|
|
607
|
+
const findCmd = isWindows ? `where ${genericBinaryName}` : `which ${genericBinaryName}`;
|
|
608
|
+
const result = execSync(findCmd, { encoding: "utf-8" }).trim();
|
|
609
|
+
const foundPath = result.split(/\r?\n/)[0];
|
|
610
|
+
if (foundPath && fs15.existsSync(foundPath)) {
|
|
611
|
+
return foundPath;
|
|
612
|
+
}
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
function isPlatformSupported() {
|
|
618
|
+
return getPlatformConfig() !== null;
|
|
619
|
+
}
|
|
620
|
+
function getPlatformNotFoundMessage() {
|
|
621
|
+
const key = getPlatformKey();
|
|
622
|
+
const config = getPlatformConfig();
|
|
623
|
+
if (!config) {
|
|
624
|
+
return `Unsupported platform: ${key}. Supported platforms: ${Object.keys(PLATFORM_MAP).join(", ")}`;
|
|
625
|
+
}
|
|
626
|
+
return `Platform binaries not found for ${key}.
|
|
627
|
+
|
|
628
|
+
To fix this, either:
|
|
629
|
+
1. Install the platform package: npm install ${config.package}
|
|
630
|
+
2. Run the native deps build: ./scripts/build-native-deps.sh
|
|
631
|
+
3. Place binaries in ~/.overwatch/bin/
|
|
632
|
+
`;
|
|
633
|
+
}
|
|
634
|
+
module2.exports = {
|
|
635
|
+
getPlatformKey,
|
|
636
|
+
getPlatformConfig,
|
|
637
|
+
loadPlatformPackage,
|
|
638
|
+
getRampartsBinaryPath,
|
|
639
|
+
isPlatformSupported,
|
|
640
|
+
getPlatformNotFoundMessage,
|
|
641
|
+
PLATFORM_MAP
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// src/daemon.ts
|
|
647
|
+
var fs14 = __toESM(require("fs"));
|
|
648
|
+
var path13 = __toESM(require("path"));
|
|
649
|
+
var os13 = __toESM(require("os"));
|
|
650
|
+
|
|
651
|
+
// src/http/server.ts
|
|
652
|
+
var http = __toESM(require("http"));
|
|
653
|
+
init_utils();
|
|
654
|
+
var GuardianHttpServer = class {
|
|
655
|
+
constructor(port) {
|
|
656
|
+
this.requestListeners = /* @__PURE__ */ new Map();
|
|
657
|
+
this.requestCount = 0;
|
|
658
|
+
this.port = port;
|
|
659
|
+
logger.debug("Creating server instance", {
|
|
660
|
+
requestedPort: port,
|
|
661
|
+
isEphemeral: port === 0
|
|
662
|
+
});
|
|
663
|
+
this.server = http.createServer(this.handleRequest.bind(this));
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Start the server
|
|
667
|
+
*/
|
|
668
|
+
start() {
|
|
669
|
+
logger.info("Starting server...");
|
|
670
|
+
return new Promise((resolve, reject) => {
|
|
671
|
+
this.server.listen(this.port, "127.0.0.1", () => {
|
|
672
|
+
const address = this.server.address();
|
|
673
|
+
const originalPort = this.port;
|
|
674
|
+
if (address && typeof address === "object") {
|
|
675
|
+
this.port = address.port;
|
|
676
|
+
}
|
|
677
|
+
logger.info("Server started successfully", {
|
|
678
|
+
requestedPort: originalPort,
|
|
679
|
+
assignedPort: this.port,
|
|
680
|
+
host: "127.0.0.1",
|
|
681
|
+
registeredPaths: Array.from(this.requestListeners.keys())
|
|
682
|
+
});
|
|
683
|
+
resolve();
|
|
684
|
+
});
|
|
685
|
+
this.server.on("error", (error) => {
|
|
686
|
+
logger.error("Server startup error:", {
|
|
687
|
+
code: error.code,
|
|
688
|
+
message: error.message,
|
|
689
|
+
port: this.port
|
|
690
|
+
});
|
|
691
|
+
reject(error);
|
|
692
|
+
});
|
|
693
|
+
this.server.on("close", () => {
|
|
694
|
+
logger.debug("Server closed");
|
|
695
|
+
});
|
|
696
|
+
this.server.on("connection", (socket) => {
|
|
697
|
+
logger.debug("New connection", {
|
|
698
|
+
remoteAddress: socket.remoteAddress,
|
|
699
|
+
remotePort: socket.remotePort
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Stop the server
|
|
706
|
+
*/
|
|
707
|
+
stop() {
|
|
708
|
+
logger.info("Stopping server...");
|
|
709
|
+
return new Promise((resolve) => {
|
|
710
|
+
if (this.server.listening) {
|
|
711
|
+
this.server.close(() => {
|
|
712
|
+
logger.info("Server stopped", {
|
|
713
|
+
totalRequestsServed: this.requestCount
|
|
714
|
+
});
|
|
715
|
+
resolve();
|
|
716
|
+
});
|
|
717
|
+
} else {
|
|
718
|
+
logger.debug("Server was not listening");
|
|
719
|
+
resolve();
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Register a request handler for a specific path
|
|
725
|
+
*/
|
|
726
|
+
on(path14, listener) {
|
|
727
|
+
this.requestListeners.set(path14, listener);
|
|
728
|
+
logger.debug("Registered handler", {
|
|
729
|
+
path: path14,
|
|
730
|
+
totalHandlers: this.requestListeners.size
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Handle incoming requests
|
|
735
|
+
*/
|
|
736
|
+
handleRequest(req, res) {
|
|
737
|
+
const startTime = Date.now();
|
|
738
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
739
|
+
logger.debug("Incoming request", {
|
|
740
|
+
method: req.method,
|
|
741
|
+
path: url.pathname,
|
|
742
|
+
query: url.search || "(none)",
|
|
743
|
+
userAgent: req.headers["user-agent"],
|
|
744
|
+
contentLength: req.headers["content-length"]
|
|
745
|
+
});
|
|
746
|
+
let listener = this.requestListeners.get(url.pathname);
|
|
747
|
+
let matchedPath = url.pathname;
|
|
748
|
+
if (!listener) {
|
|
749
|
+
for (const [path14, handler] of this.requestListeners.entries()) {
|
|
750
|
+
if (url.pathname.startsWith(path14 + "/") || url.pathname === path14) {
|
|
751
|
+
listener = handler;
|
|
752
|
+
matchedPath = path14;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (listener) {
|
|
758
|
+
logger.debug(`Handler found for ${matchedPath}`);
|
|
759
|
+
const originalEnd = res.end.bind(res);
|
|
760
|
+
res.end = (...args) => {
|
|
761
|
+
const duration = Date.now() - startTime;
|
|
762
|
+
logger.debug("Response sent", {
|
|
763
|
+
statusCode: res.statusCode,
|
|
764
|
+
duration
|
|
765
|
+
});
|
|
766
|
+
return originalEnd(...args);
|
|
767
|
+
};
|
|
768
|
+
listener(req, res);
|
|
769
|
+
} else {
|
|
770
|
+
const duration = Date.now() - startTime;
|
|
771
|
+
logger.warn(`No handler for path: ${url.pathname}`, {
|
|
772
|
+
availablePaths: Array.from(this.requestListeners.keys()),
|
|
773
|
+
duration
|
|
774
|
+
});
|
|
775
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
776
|
+
res.end(JSON.stringify({ error: "Not Found", path: url.pathname }));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Get the port the server is listening on
|
|
781
|
+
*/
|
|
782
|
+
getPort() {
|
|
783
|
+
return this.port;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Check if server is currently listening
|
|
787
|
+
*/
|
|
788
|
+
isListening() {
|
|
789
|
+
return this.server.listening;
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/javelin/config-reader.ts
|
|
794
|
+
var fs4 = __toESM(require("fs"));
|
|
795
|
+
var path4 = __toESM(require("path"));
|
|
796
|
+
var os4 = __toESM(require("os"));
|
|
797
|
+
init_utils();
|
|
798
|
+
|
|
799
|
+
// src/auth/index.ts
|
|
800
|
+
init_pkce();
|
|
801
|
+
init_oauth();
|
|
802
|
+
|
|
803
|
+
// src/auth/cli-oauth.ts
|
|
804
|
+
init_pkce();
|
|
805
|
+
init_oauth();
|
|
806
|
+
init_token_store();
|
|
807
|
+
|
|
808
|
+
// src/auth/html-utils.ts
|
|
809
|
+
function escapeHtml(unsafe) {
|
|
810
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
811
|
+
}
|
|
812
|
+
var COMMON_STYLES = `
|
|
813
|
+
* {
|
|
814
|
+
margin: 0;
|
|
815
|
+
padding: 0;
|
|
816
|
+
box-sizing: border-box;
|
|
817
|
+
}
|
|
818
|
+
body {
|
|
819
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
820
|
+
-webkit-font-smoothing: antialiased;
|
|
821
|
+
-moz-osx-font-smoothing: grayscale;
|
|
822
|
+
background: #ffffff;
|
|
823
|
+
min-height: 100vh;
|
|
824
|
+
display: flex;
|
|
825
|
+
align-items: center;
|
|
826
|
+
justify-content: center;
|
|
827
|
+
padding: 16px;
|
|
828
|
+
}
|
|
829
|
+
.container {
|
|
830
|
+
background: white;
|
|
831
|
+
border-radius: 12px;
|
|
832
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
833
|
+
width: 100%;
|
|
834
|
+
max-width: 448px;
|
|
835
|
+
padding: 0;
|
|
836
|
+
overflow: hidden;
|
|
837
|
+
}
|
|
838
|
+
.card-header {
|
|
839
|
+
text-align: center;
|
|
840
|
+
padding: 32px 24px 24px;
|
|
841
|
+
}
|
|
842
|
+
.icon-container {
|
|
843
|
+
margin: 0 auto 16px;
|
|
844
|
+
width: 64px;
|
|
845
|
+
height: 64px;
|
|
846
|
+
display: flex;
|
|
847
|
+
align-items: center;
|
|
848
|
+
justify-content: center;
|
|
849
|
+
border-radius: 50%;
|
|
850
|
+
}
|
|
851
|
+
.success .icon-container {
|
|
852
|
+
background: #dcfce7;
|
|
853
|
+
}
|
|
854
|
+
.error .icon-container {
|
|
855
|
+
background: #fee2e2;
|
|
856
|
+
}
|
|
857
|
+
.icon {
|
|
858
|
+
width: 32px;
|
|
859
|
+
height: 32px;
|
|
860
|
+
}
|
|
861
|
+
.success .icon {
|
|
862
|
+
color: #16a34a;
|
|
863
|
+
}
|
|
864
|
+
.error .icon {
|
|
865
|
+
color: #dc2626;
|
|
866
|
+
}
|
|
867
|
+
h1 {
|
|
868
|
+
font-size: 24px;
|
|
869
|
+
font-weight: 600;
|
|
870
|
+
line-height: 1.2;
|
|
871
|
+
margin-bottom: 8px;
|
|
872
|
+
}
|
|
873
|
+
.success h1 {
|
|
874
|
+
color: #16a34a;
|
|
875
|
+
}
|
|
876
|
+
.error h1 {
|
|
877
|
+
color: #dc2626;
|
|
878
|
+
}
|
|
879
|
+
.description {
|
|
880
|
+
color: #6b7280;
|
|
881
|
+
font-size: 14px;
|
|
882
|
+
line-height: 1.5;
|
|
883
|
+
margin-top: 8px;
|
|
884
|
+
}
|
|
885
|
+
.email {
|
|
886
|
+
font-weight: 500;
|
|
887
|
+
color: #111827;
|
|
888
|
+
}
|
|
889
|
+
.error-message {
|
|
890
|
+
color: #dc2626;
|
|
891
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
892
|
+
font-size: 13px;
|
|
893
|
+
background: #fef2f2;
|
|
894
|
+
padding: 12px;
|
|
895
|
+
border-radius: 6px;
|
|
896
|
+
margin: 16px 24px;
|
|
897
|
+
border: 1px solid #fecaca;
|
|
898
|
+
word-break: break-word;
|
|
899
|
+
}
|
|
900
|
+
`;
|
|
901
|
+
function generateSuccessHtml(message, email) {
|
|
902
|
+
const safeMessage = escapeHtml(message);
|
|
903
|
+
const safeEmail = email ? escapeHtml(email) : "user";
|
|
904
|
+
return `<!DOCTYPE html>
|
|
905
|
+
<html lang="en">
|
|
906
|
+
<head>
|
|
907
|
+
<meta charset="UTF-8">
|
|
908
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
909
|
+
<title>Authorization Successful</title>
|
|
910
|
+
<style>${COMMON_STYLES}</style>
|
|
911
|
+
</head>
|
|
912
|
+
<body>
|
|
913
|
+
<div class="container success">
|
|
914
|
+
<div class="card-header">
|
|
915
|
+
<div class="icon-container">
|
|
916
|
+
<svg class="icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
917
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
918
|
+
</svg>
|
|
919
|
+
</div>
|
|
920
|
+
<h1>Authorization Successful!</h1>
|
|
921
|
+
<p class="description">
|
|
922
|
+
${email ? `Welcome, <span class="email">${safeEmail}</span>!` : ""}
|
|
923
|
+
${safeMessage}
|
|
924
|
+
</p>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
</body>
|
|
928
|
+
</html>`;
|
|
929
|
+
}
|
|
930
|
+
function generateErrorHtml(error) {
|
|
931
|
+
const safeError = escapeHtml(error);
|
|
932
|
+
return `<!DOCTYPE html>
|
|
933
|
+
<html lang="en">
|
|
934
|
+
<head>
|
|
935
|
+
<meta charset="UTF-8">
|
|
936
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
937
|
+
<title>Authorization Failed</title>
|
|
938
|
+
<style>${COMMON_STYLES}</style>
|
|
939
|
+
</head>
|
|
940
|
+
<body>
|
|
941
|
+
<div class="container error">
|
|
942
|
+
<div class="card-header">
|
|
943
|
+
<div class="icon-container">
|
|
944
|
+
<svg class="icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
945
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
946
|
+
</svg>
|
|
947
|
+
</div>
|
|
948
|
+
<h1>Authorization Failed</h1>
|
|
949
|
+
<p class="description">Please try again from the terminal.</p>
|
|
950
|
+
</div>
|
|
951
|
+
<div class="error-message">${safeError}</div>
|
|
952
|
+
</div>
|
|
953
|
+
</body>
|
|
954
|
+
</html>`;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/auth/cli-oauth.ts
|
|
958
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
959
|
+
|
|
960
|
+
// src/auth/index.ts
|
|
961
|
+
init_token_store();
|
|
962
|
+
|
|
963
|
+
// src/javelin/config-reader.ts
|
|
964
|
+
init_config2();
|
|
965
|
+
var ConfigReader = class {
|
|
966
|
+
constructor(configPath) {
|
|
967
|
+
this.rawConfig = null;
|
|
968
|
+
this.configPath = configPath || path4.join(os4.homedir(), ".overwatch", "config.json");
|
|
969
|
+
logger.debug("Initialized", { configPath: this.configPath });
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Get Highflame API configuration
|
|
973
|
+
*/
|
|
974
|
+
getHighflameConfig() {
|
|
975
|
+
const envBaseUrl = process.env.HIGHFLAME_BASE_URL;
|
|
976
|
+
return {
|
|
977
|
+
baseUrl: envBaseUrl || getBaseUrl(),
|
|
978
|
+
enabled: this.rawConfig?.engines?.javelin?.enabled !== false
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Get a fresh, validated JWT token for API authentication.
|
|
983
|
+
* Re-reads session.json each time, checks expiry, and refreshes if needed.
|
|
984
|
+
*/
|
|
985
|
+
async getJwtToken() {
|
|
986
|
+
const envToken = process.env.HIGHFLAME_TOKEN;
|
|
987
|
+
if (envToken) {
|
|
988
|
+
return envToken;
|
|
989
|
+
}
|
|
990
|
+
const token = await getValidAccessToken();
|
|
991
|
+
if (!token) {
|
|
992
|
+
logger.warn(
|
|
993
|
+
"No valid authentication token. Admin API calls will be skipped. Run `overwatch login` to authenticate."
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
return token;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Get the full overwatch config
|
|
1000
|
+
*/
|
|
1001
|
+
getOverwatchConfig() {
|
|
1002
|
+
return this.rawConfig;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Load raw config from file
|
|
1006
|
+
*/
|
|
1007
|
+
async loadRawConfig() {
|
|
1008
|
+
if (!fs4.existsSync(this.configPath)) {
|
|
1009
|
+
logger.warn(`Config file not found: ${this.configPath}`);
|
|
1010
|
+
this.rawConfig = null;
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const configData = await fs4.promises.readFile(this.configPath, "utf-8");
|
|
1015
|
+
this.rawConfig = JSON.parse(configData);
|
|
1016
|
+
logger.info("Config loaded:", {
|
|
1017
|
+
version: this.rawConfig.version,
|
|
1018
|
+
hasHighflame: !!this.rawConfig.highflame
|
|
1019
|
+
});
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
logger.error("Failed to load config:", {
|
|
1022
|
+
error: String(error)
|
|
1023
|
+
});
|
|
1024
|
+
this.rawConfig = null;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Get admin API configuration (derived from Highflame config)
|
|
1029
|
+
*/
|
|
1030
|
+
getAdminConfig() {
|
|
1031
|
+
const highflameConfig = this.getHighflameConfig();
|
|
1032
|
+
return {
|
|
1033
|
+
enabled: highflameConfig.enabled,
|
|
1034
|
+
baseUrl: highflameConfig.baseUrl || null
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get admin token for API authentication
|
|
1039
|
+
*/
|
|
1040
|
+
async getAdminToken() {
|
|
1041
|
+
return await this.getJwtToken();
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
// src/javelin/admin-client.ts
|
|
1046
|
+
init_utils();
|
|
1047
|
+
var DEFAULT_TIMEOUT = 5e3;
|
|
1048
|
+
var AdminClient = class {
|
|
1049
|
+
constructor(baseUrl, tokenGetter, timeout = DEFAULT_TIMEOUT) {
|
|
1050
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
1051
|
+
this.tokenGetter = typeof tokenGetter === "string" ? () => Promise.resolve(tokenGetter) : tokenGetter;
|
|
1052
|
+
this.timeout = timeout;
|
|
1053
|
+
logger.debug("Initialized", {
|
|
1054
|
+
baseUrl: this.baseUrl,
|
|
1055
|
+
timeout: this.timeout
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
async getToken() {
|
|
1059
|
+
return this.tokenGetter();
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Record installation event
|
|
1063
|
+
* Called on first event from a new user/IDE combination
|
|
1064
|
+
*/
|
|
1065
|
+
async recordInstallation(event) {
|
|
1066
|
+
const url = `${this.baseUrl}/v1/admin/guardian/installations`;
|
|
1067
|
+
const token = await this.getToken();
|
|
1068
|
+
if (!token) {
|
|
1069
|
+
logger.warn("No valid auth token, skipping installation recording");
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
logger.info("Recording installation", {
|
|
1073
|
+
ide_type: event.ide_type,
|
|
1074
|
+
user_email: event.user_email
|
|
1075
|
+
});
|
|
1076
|
+
try {
|
|
1077
|
+
const response = await fetch(url, {
|
|
1078
|
+
method: "POST",
|
|
1079
|
+
headers: {
|
|
1080
|
+
"Content-Type": "application/json",
|
|
1081
|
+
Authorization: `Bearer ${token}`
|
|
1082
|
+
},
|
|
1083
|
+
body: JSON.stringify(event),
|
|
1084
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1085
|
+
});
|
|
1086
|
+
if (!response.ok) {
|
|
1087
|
+
const errorText = await response.text();
|
|
1088
|
+
logger.error("Failed to record installation", {
|
|
1089
|
+
status: response.status,
|
|
1090
|
+
error: errorText
|
|
1091
|
+
});
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
logger.info("Installation recorded successfully");
|
|
1095
|
+
return true;
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
logger.error("Error recording installation", {
|
|
1098
|
+
error: String(error)
|
|
1099
|
+
});
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Report coverage metrics to admin API.
|
|
1105
|
+
* Called periodically by GuardianModule (default cadence: every 10
|
|
1106
|
+
* minutes; see COVERAGE_INTERVAL_MS in module.ts) to report agent
|
|
1107
|
+
* detection coverage.
|
|
1108
|
+
*/
|
|
1109
|
+
async reportCoverageMetrics(report) {
|
|
1110
|
+
const url = `${this.baseUrl}/v1/admin/guardian/coverage`;
|
|
1111
|
+
const token = await this.getToken();
|
|
1112
|
+
if (!token) {
|
|
1113
|
+
logger.warn("No valid auth token, skipping coverage reporting");
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
logger.info("Reporting coverage metrics", {
|
|
1117
|
+
installed_count: report.installed_count,
|
|
1118
|
+
monitored_count: report.monitored_count,
|
|
1119
|
+
coverage_percentage: report.coverage_percentage,
|
|
1120
|
+
platform: report.platform
|
|
1121
|
+
});
|
|
1122
|
+
try {
|
|
1123
|
+
const response = await fetch(url, {
|
|
1124
|
+
method: "POST",
|
|
1125
|
+
headers: {
|
|
1126
|
+
"Content-Type": "application/json",
|
|
1127
|
+
Authorization: `Bearer ${token}`
|
|
1128
|
+
},
|
|
1129
|
+
body: JSON.stringify(report),
|
|
1130
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1131
|
+
});
|
|
1132
|
+
if (!response.ok) {
|
|
1133
|
+
const errorText = await response.text();
|
|
1134
|
+
logger.error("Failed to report coverage metrics", {
|
|
1135
|
+
status: response.status,
|
|
1136
|
+
error: errorText
|
|
1137
|
+
});
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
logger.info("Coverage metrics reported successfully");
|
|
1141
|
+
return true;
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
logger.error("Error reporting coverage metrics", {
|
|
1144
|
+
error: String(error)
|
|
1145
|
+
});
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Health check - verify connection to admin API
|
|
1151
|
+
*/
|
|
1152
|
+
async healthCheck() {
|
|
1153
|
+
const token = await this.getToken();
|
|
1154
|
+
if (!token)
|
|
1155
|
+
return false;
|
|
1156
|
+
try {
|
|
1157
|
+
const response = await fetch(
|
|
1158
|
+
`${this.baseUrl}/v1/admin/applications?type=code_agent&limit=1`,
|
|
1159
|
+
{
|
|
1160
|
+
method: "GET",
|
|
1161
|
+
headers: {
|
|
1162
|
+
"Content-Type": "application/json",
|
|
1163
|
+
Authorization: `Bearer ${token}`
|
|
1164
|
+
},
|
|
1165
|
+
signal: AbortSignal.timeout(5e3)
|
|
1166
|
+
}
|
|
1167
|
+
);
|
|
1168
|
+
const healthy = response.ok;
|
|
1169
|
+
logger.debug(`Health check: ${healthy ? "OK" : "FAILED"}`);
|
|
1170
|
+
return healthy;
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
logger.debug("Health check failed", {
|
|
1173
|
+
error: String(error)
|
|
1174
|
+
});
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// src/module.ts
|
|
1181
|
+
init_utils();
|
|
1182
|
+
|
|
1183
|
+
// src/scanner.ts
|
|
1184
|
+
var import_child_process = require("child_process");
|
|
1185
|
+
var fs5 = __toESM(require("fs"));
|
|
1186
|
+
init_utils();
|
|
1187
|
+
init_config2();
|
|
1188
|
+
var platformLoader = require_platform_loader();
|
|
1189
|
+
var MCPScanner = class {
|
|
1190
|
+
constructor() {
|
|
1191
|
+
this.rampartsPath = null;
|
|
1192
|
+
this.rampartsPath = this.findRampartsBinary();
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Find the ramparts binary based on platform
|
|
1196
|
+
* Uses platform-loader for cross-platform discovery.
|
|
1197
|
+
*
|
|
1198
|
+
* Search order:
|
|
1199
|
+
* 1. Platform package (npm optionalDependency)
|
|
1200
|
+
* 2. Local bin/ directory (dev mode)
|
|
1201
|
+
* 3. ~/.overwatch/bin/
|
|
1202
|
+
* 4. System PATH
|
|
1203
|
+
*/
|
|
1204
|
+
findRampartsBinary() {
|
|
1205
|
+
if (!platformLoader.isPlatformSupported()) {
|
|
1206
|
+
const key = platformLoader.getPlatformKey();
|
|
1207
|
+
logger.warn(`Unsupported platform: ${key}`);
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const binaryPath = platformLoader.getRampartsBinaryPath();
|
|
1211
|
+
if (binaryPath) {
|
|
1212
|
+
logger.info(`Found ramparts at: ${binaryPath}`);
|
|
1213
|
+
return binaryPath;
|
|
1214
|
+
}
|
|
1215
|
+
logger.warn("ramparts binary not found");
|
|
1216
|
+
logger.warn(platformLoader.getPlatformNotFoundMessage());
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Check if scanner is available
|
|
1221
|
+
*/
|
|
1222
|
+
isAvailable() {
|
|
1223
|
+
return this.rampartsPath !== null;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Get the path to ramparts binary
|
|
1227
|
+
*/
|
|
1228
|
+
getBinaryPath() {
|
|
1229
|
+
return this.rampartsPath;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Run MCP config scan
|
|
1233
|
+
* @param rulesDir Optional custom YARA rules directory
|
|
1234
|
+
*/
|
|
1235
|
+
async runScan(rulesDir) {
|
|
1236
|
+
if (!this.rampartsPath) {
|
|
1237
|
+
throw new Error("ramparts binary not found");
|
|
1238
|
+
}
|
|
1239
|
+
const args = ["scan-config", "--format", "json"];
|
|
1240
|
+
const env = {
|
|
1241
|
+
...process.env,
|
|
1242
|
+
JAVELIN_BYPASS: "true"
|
|
1243
|
+
// Skip Javelin checks during scan
|
|
1244
|
+
};
|
|
1245
|
+
if (rulesDir && fs5.existsSync(rulesDir)) {
|
|
1246
|
+
env.RAMPARTS_RULES_DIR = rulesDir;
|
|
1247
|
+
logger.debug(`Using rules from: ${rulesDir}`);
|
|
1248
|
+
}
|
|
1249
|
+
const llmConfig = getLLMConfig();
|
|
1250
|
+
if (llmConfig?.apiKey) {
|
|
1251
|
+
env.LLM_API_KEY = llmConfig.apiKey;
|
|
1252
|
+
if (llmConfig.provider === "openai") {
|
|
1253
|
+
env.OPENAI_API_KEY = llmConfig.apiKey;
|
|
1254
|
+
} else if (llmConfig.provider === "anthropic") {
|
|
1255
|
+
env.ANTHROPIC_API_KEY = llmConfig.apiKey;
|
|
1256
|
+
}
|
|
1257
|
+
logger.debug(
|
|
1258
|
+
`LLM API key configured for ramparts (${llmConfig.provider || "openai"})`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
logger.info(`Running: ${this.rampartsPath} ${args.join(" ")}`);
|
|
1262
|
+
return new Promise((resolve, reject) => {
|
|
1263
|
+
const proc = (0, import_child_process.spawn)(this.rampartsPath, args, {
|
|
1264
|
+
env,
|
|
1265
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1266
|
+
});
|
|
1267
|
+
let stdout = "";
|
|
1268
|
+
let stderr = "";
|
|
1269
|
+
proc.stdout?.on("data", (data) => {
|
|
1270
|
+
stdout += data.toString();
|
|
1271
|
+
});
|
|
1272
|
+
proc.stderr?.on("data", (data) => {
|
|
1273
|
+
stderr += data.toString();
|
|
1274
|
+
});
|
|
1275
|
+
proc.on("close", (code) => {
|
|
1276
|
+
if (code !== 0) {
|
|
1277
|
+
logger.warn(`ramparts exited with code ${code}`);
|
|
1278
|
+
if (stderr) {
|
|
1279
|
+
logger.debug(`stderr: ${stderr}`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
const result = this.parseOutput(stdout);
|
|
1284
|
+
resolve(result);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1287
|
+
logger.error(`Failed to parse output: ${errorMsg}`);
|
|
1288
|
+
reject(new Error(`Failed to parse scan output: ${errorMsg}`));
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
proc.on("error", (err) => {
|
|
1292
|
+
logger.error(`Process error: ${err.message}`);
|
|
1293
|
+
reject(err);
|
|
1294
|
+
});
|
|
1295
|
+
setTimeout(() => {
|
|
1296
|
+
proc.kill();
|
|
1297
|
+
reject(new Error("Scan timed out after 600 seconds"));
|
|
1298
|
+
}, 6e5);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Parse ramparts CLI output
|
|
1303
|
+
* Handles cases where CLI prints banner/logs before JSON
|
|
1304
|
+
*/
|
|
1305
|
+
parseOutput(output) {
|
|
1306
|
+
try {
|
|
1307
|
+
return JSON.parse(output);
|
|
1308
|
+
} catch {
|
|
1309
|
+
}
|
|
1310
|
+
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1311
|
+
let searchIndex = cleanOutput.indexOf('"scan_type"');
|
|
1312
|
+
if (searchIndex === -1) {
|
|
1313
|
+
searchIndex = cleanOutput.indexOf('"results"');
|
|
1314
|
+
}
|
|
1315
|
+
if (searchIndex === -1) {
|
|
1316
|
+
logger.warn("Could not find JSON content in output");
|
|
1317
|
+
return this.getEmptyResult();
|
|
1318
|
+
}
|
|
1319
|
+
let startIndex = -1;
|
|
1320
|
+
for (let i = searchIndex; i >= 0; i--) {
|
|
1321
|
+
if (cleanOutput[i] === "{") {
|
|
1322
|
+
startIndex = i;
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (startIndex !== -1) {
|
|
1327
|
+
let braceCount = 0;
|
|
1328
|
+
let endIndex = -1;
|
|
1329
|
+
for (let i = startIndex; i < cleanOutput.length; i++) {
|
|
1330
|
+
if (cleanOutput[i] === "{")
|
|
1331
|
+
braceCount++;
|
|
1332
|
+
else if (cleanOutput[i] === "}")
|
|
1333
|
+
braceCount--;
|
|
1334
|
+
if (braceCount === 0) {
|
|
1335
|
+
endIndex = i + 1;
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (endIndex !== -1) {
|
|
1340
|
+
const jsonStr = cleanOutput.substring(startIndex, endIndex);
|
|
1341
|
+
try {
|
|
1342
|
+
return JSON.parse(jsonStr);
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1345
|
+
logger.warn(`Failed to parse extracted JSON: ${errorMsg}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
logger.warn("Could not parse JSON, returning empty result");
|
|
1350
|
+
return this.getEmptyResult();
|
|
1351
|
+
}
|
|
1352
|
+
getEmptyResult() {
|
|
1353
|
+
return {
|
|
1354
|
+
scan_type: "mcp_config",
|
|
1355
|
+
total_servers: 0,
|
|
1356
|
+
results: []
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
// src/module.ts
|
|
1362
|
+
init_version();
|
|
1363
|
+
|
|
1364
|
+
// src/cerberus/client.ts
|
|
1365
|
+
init_utils();
|
|
1366
|
+
init_version();
|
|
1367
|
+
|
|
1368
|
+
// src/handlers/utils.ts
|
|
1369
|
+
async function parseBody(req) {
|
|
1370
|
+
return new Promise((resolve) => {
|
|
1371
|
+
let body = "";
|
|
1372
|
+
req.on("data", (chunk) => {
|
|
1373
|
+
body += chunk.toString();
|
|
1374
|
+
});
|
|
1375
|
+
req.on("end", () => {
|
|
1376
|
+
try {
|
|
1377
|
+
resolve(JSON.parse(body || "{}"));
|
|
1378
|
+
} catch {
|
|
1379
|
+
resolve({});
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
req.on("error", () => resolve({}));
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
function getOfflineFailOpen(_source) {
|
|
1386
|
+
return {};
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/cerberus/client.ts
|
|
1390
|
+
var DEFAULT_TIMEOUT2 = 1e4;
|
|
1391
|
+
var CerberusClient = class {
|
|
1392
|
+
constructor(baseUrl, tokenGetter, machineId, timeout = DEFAULT_TIMEOUT2) {
|
|
1393
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
1394
|
+
this.tokenGetter = tokenGetter;
|
|
1395
|
+
this.machineId = machineId;
|
|
1396
|
+
this.timeout = timeout;
|
|
1397
|
+
logger.debug("CerberusClient initialized", {
|
|
1398
|
+
baseUrl: this.baseUrl,
|
|
1399
|
+
timeout: this.timeout
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Forward a hook event to Cerberus for evaluation.
|
|
1404
|
+
* Returns the Cerberus response, or a fail-open allow on error.
|
|
1405
|
+
*/
|
|
1406
|
+
async evaluate(req) {
|
|
1407
|
+
const url = `${this.baseUrl}/v1/hooks/evaluate`;
|
|
1408
|
+
const startTime = Date.now();
|
|
1409
|
+
const body = {
|
|
1410
|
+
source: req.source,
|
|
1411
|
+
event: req.event,
|
|
1412
|
+
payload: req.payload,
|
|
1413
|
+
client: {
|
|
1414
|
+
version: VERSION,
|
|
1415
|
+
machine_id: this.machineId,
|
|
1416
|
+
platform: process.platform,
|
|
1417
|
+
...req.client
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
const token = await this.tokenGetter();
|
|
1421
|
+
if (!token) {
|
|
1422
|
+
logger.warn("No auth token \u2014 allowing (fail-open)");
|
|
1423
|
+
return this.failOpen(req.source);
|
|
1424
|
+
}
|
|
1425
|
+
const headers = {
|
|
1426
|
+
"Content-Type": "application/json",
|
|
1427
|
+
Authorization: `Bearer ${token}`,
|
|
1428
|
+
"User-Agent": `overwatch/${VERSION}`
|
|
1429
|
+
};
|
|
1430
|
+
const requestBody = JSON.stringify(body);
|
|
1431
|
+
logger.info("Cerberus request", {
|
|
1432
|
+
url,
|
|
1433
|
+
source: req.source,
|
|
1434
|
+
event: req.event,
|
|
1435
|
+
bodySize: requestBody.length
|
|
1436
|
+
});
|
|
1437
|
+
logger.debug("Cerberus request body (shape)", {
|
|
1438
|
+
payloadKeys: Object.keys(body.payload || {}),
|
|
1439
|
+
bodySize: requestBody.length
|
|
1440
|
+
});
|
|
1441
|
+
try {
|
|
1442
|
+
const response = await fetch(url, {
|
|
1443
|
+
method: "POST",
|
|
1444
|
+
headers,
|
|
1445
|
+
body: requestBody,
|
|
1446
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1447
|
+
});
|
|
1448
|
+
const duration = Date.now() - startTime;
|
|
1449
|
+
const responseText = await response.text();
|
|
1450
|
+
logger.debug("Cerberus raw response", {
|
|
1451
|
+
status: response.status,
|
|
1452
|
+
duration: `${duration}ms`,
|
|
1453
|
+
body: responseText.substring(0, 1e3)
|
|
1454
|
+
});
|
|
1455
|
+
if (!response.ok) {
|
|
1456
|
+
logger.error("Cerberus returned error \u2014 allowing (fail-open)", {
|
|
1457
|
+
status: response.status,
|
|
1458
|
+
error: responseText.substring(0, 500),
|
|
1459
|
+
duration: `${duration}ms`
|
|
1460
|
+
});
|
|
1461
|
+
return this.failOpen(req.source);
|
|
1462
|
+
}
|
|
1463
|
+
const result = JSON.parse(responseText);
|
|
1464
|
+
logger.info(`Cerberus response in ${duration}ms`, {
|
|
1465
|
+
decision: result.decision,
|
|
1466
|
+
allowed: result.allowed,
|
|
1467
|
+
reason: result.reason,
|
|
1468
|
+
event_id: result.event_id,
|
|
1469
|
+
ide_response: result.ide_response
|
|
1470
|
+
});
|
|
1471
|
+
return result;
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
const duration = Date.now() - startTime;
|
|
1474
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1475
|
+
logger.error("Cerberus call failed \u2014 allowing (fail-open)", {
|
|
1476
|
+
error: errorMessage,
|
|
1477
|
+
duration: `${duration}ms`,
|
|
1478
|
+
url
|
|
1479
|
+
});
|
|
1480
|
+
return this.failOpen(req.source);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
failOpen(source) {
|
|
1484
|
+
return {
|
|
1485
|
+
allowed: true,
|
|
1486
|
+
decision: "allow",
|
|
1487
|
+
reason: "Cerberus unavailable \u2014 fail-open",
|
|
1488
|
+
ide_response: getOfflineFailOpen(source)
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
// src/module.ts
|
|
1494
|
+
init_config2();
|
|
1495
|
+
|
|
1496
|
+
// src/data/ingestor.ts
|
|
1497
|
+
var fs6 = __toESM(require("fs"));
|
|
1498
|
+
var path5 = __toESM(require("path"));
|
|
1499
|
+
var os5 = __toESM(require("os"));
|
|
1500
|
+
init_utils();
|
|
1501
|
+
var MAX_RETRIES = 5;
|
|
1502
|
+
var RETRY_INTERVAL_MS = 3e4;
|
|
1503
|
+
var AdminIngestor = class {
|
|
1504
|
+
constructor(baseUrl, tokenGetter, endpoint, applicationIdResolver) {
|
|
1505
|
+
this.retryTimer = null;
|
|
1506
|
+
// Mid-flight guard so boot + interval calls don't overlap on the pending file.
|
|
1507
|
+
this.processing = false;
|
|
1508
|
+
this.applicationIdResolver = null;
|
|
1509
|
+
this.baseUrl = baseUrl;
|
|
1510
|
+
this.tokenGetter = tokenGetter;
|
|
1511
|
+
this.endpoint = endpoint;
|
|
1512
|
+
this.applicationIdResolver = applicationIdResolver || null;
|
|
1513
|
+
this.pendingFile = path5.join(
|
|
1514
|
+
os5.homedir(),
|
|
1515
|
+
".overwatch",
|
|
1516
|
+
`pending-${endpoint}.jsonl`
|
|
1517
|
+
);
|
|
1518
|
+
this.deadLetterFile = path5.join(
|
|
1519
|
+
os5.homedir(),
|
|
1520
|
+
".overwatch",
|
|
1521
|
+
`dead-letter-${endpoint}.jsonl`
|
|
1522
|
+
);
|
|
1523
|
+
logger.info(`Initialized`, {
|
|
1524
|
+
baseUrl: this.baseUrl,
|
|
1525
|
+
endpoint: this.endpoint,
|
|
1526
|
+
pendingFile: this.pendingFile,
|
|
1527
|
+
deadLetterFile: this.deadLetterFile
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
start() {
|
|
1531
|
+
if (this.retryTimer) {
|
|
1532
|
+
logger.debug("Already started");
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
logger.info("Starting", {
|
|
1536
|
+
retryIntervalMs: RETRY_INTERVAL_MS
|
|
1537
|
+
});
|
|
1538
|
+
this.processQueue();
|
|
1539
|
+
this.retryTimer = setInterval(() => this.processQueue(), RETRY_INTERVAL_MS);
|
|
1540
|
+
}
|
|
1541
|
+
stop() {
|
|
1542
|
+
if (this.retryTimer) {
|
|
1543
|
+
logger.info("Stopping");
|
|
1544
|
+
clearInterval(this.retryTimer);
|
|
1545
|
+
this.retryTimer = null;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
/** Fire-and-forget record ingestion */
|
|
1549
|
+
ingest(record) {
|
|
1550
|
+
const recordObj = record;
|
|
1551
|
+
logger.debug("Ingesting record", {
|
|
1552
|
+
recordSize: JSON.stringify(record).length,
|
|
1553
|
+
recordKeys: Object.keys(recordObj),
|
|
1554
|
+
hasResults: "results" in recordObj,
|
|
1555
|
+
resultsType: recordObj.results ? Array.isArray(recordObj.results) ? "array" : typeof recordObj.results : "missing",
|
|
1556
|
+
resultsLength: Array.isArray(recordObj.results) ? recordObj.results.length : "N/A"
|
|
1557
|
+
});
|
|
1558
|
+
this.send(record).catch((error) => {
|
|
1559
|
+
logger.warn(
|
|
1560
|
+
`[AdminIngestor:${this.endpoint}] Initial send failed, saving for retry`,
|
|
1561
|
+
{
|
|
1562
|
+
error: String(error),
|
|
1563
|
+
errorMessage: error?.message
|
|
1564
|
+
}
|
|
1565
|
+
);
|
|
1566
|
+
this.saveForRetry(record, 0);
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
async send(record) {
|
|
1570
|
+
const url = `${this.baseUrl}/v1/admin/guardian/${this.endpoint}`;
|
|
1571
|
+
const body = JSON.stringify(record);
|
|
1572
|
+
const token = await this.tokenGetter();
|
|
1573
|
+
if (!token) {
|
|
1574
|
+
logger.warn(`[AdminIngestor:${this.endpoint}] No valid auth token, skipping send`);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const applicationId = this.applicationIdResolver ? this.applicationIdResolver(record) : null;
|
|
1578
|
+
logger.debug("Sending record", {
|
|
1579
|
+
url,
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
bodySize: body.length,
|
|
1582
|
+
hasApplicationId: !!applicationId
|
|
1583
|
+
});
|
|
1584
|
+
const headers = {
|
|
1585
|
+
"Content-Type": "application/json",
|
|
1586
|
+
Authorization: `Bearer ${token}`
|
|
1587
|
+
};
|
|
1588
|
+
try {
|
|
1589
|
+
const res = await fetch(url, {
|
|
1590
|
+
method: "POST",
|
|
1591
|
+
headers,
|
|
1592
|
+
body,
|
|
1593
|
+
signal: AbortSignal.timeout(3e4)
|
|
1594
|
+
});
|
|
1595
|
+
if (!res.ok) {
|
|
1596
|
+
const errorText = await res.text().catch(() => "");
|
|
1597
|
+
logger.error("Send failed", {
|
|
1598
|
+
status: res.status,
|
|
1599
|
+
statusText: res.statusText,
|
|
1600
|
+
errorBody: errorText.substring(0, 200),
|
|
1601
|
+
url
|
|
1602
|
+
});
|
|
1603
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
1604
|
+
}
|
|
1605
|
+
logger.info("Successfully sent record", {
|
|
1606
|
+
status: res.status,
|
|
1607
|
+
url
|
|
1608
|
+
});
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
const errorObj = error;
|
|
1611
|
+
logger.error("Send error", {
|
|
1612
|
+
error: String(error),
|
|
1613
|
+
errorMessage: errorObj?.message,
|
|
1614
|
+
errorName: errorObj?.name,
|
|
1615
|
+
url
|
|
1616
|
+
});
|
|
1617
|
+
throw error;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
saveForRetry(record, retryCount) {
|
|
1621
|
+
try {
|
|
1622
|
+
this.ensureDir(this.pendingFile);
|
|
1623
|
+
fs6.appendFileSync(
|
|
1624
|
+
this.pendingFile,
|
|
1625
|
+
JSON.stringify({ record, retryCount }) + "\n"
|
|
1626
|
+
);
|
|
1627
|
+
logger.info("Saved record for retry", {
|
|
1628
|
+
retryCount,
|
|
1629
|
+
pendingFile: this.pendingFile
|
|
1630
|
+
});
|
|
1631
|
+
} catch (e) {
|
|
1632
|
+
logger.error(
|
|
1633
|
+
`[AdminIngestor:${this.endpoint}] Failed to save for retry`,
|
|
1634
|
+
{
|
|
1635
|
+
error: String(e),
|
|
1636
|
+
retryCount,
|
|
1637
|
+
pendingFile: this.pendingFile
|
|
1638
|
+
}
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
async processQueue() {
|
|
1643
|
+
if (this.processing) {
|
|
1644
|
+
logger.debug(
|
|
1645
|
+
`[AdminIngestor:${this.endpoint}] processQueue already running, skipping this tick`
|
|
1646
|
+
);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
this.processing = true;
|
|
1650
|
+
try {
|
|
1651
|
+
await this.processQueueInner();
|
|
1652
|
+
} finally {
|
|
1653
|
+
this.processing = false;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
async processQueueInner() {
|
|
1657
|
+
if (!fs6.existsSync(this.pendingFile)) {
|
|
1658
|
+
logger.debug(
|
|
1659
|
+
`[AdminIngestor:${this.endpoint}] No pending file, skipping queue processing`
|
|
1660
|
+
);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const pending = this.loadPending();
|
|
1664
|
+
if (pending.length === 0) {
|
|
1665
|
+
logger.debug(
|
|
1666
|
+
`[AdminIngestor:${this.endpoint}] No pending records to process`
|
|
1667
|
+
);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
logger.info("Processing queue", {
|
|
1671
|
+
pendingCount: pending.length
|
|
1672
|
+
});
|
|
1673
|
+
const remaining = [];
|
|
1674
|
+
for (const item of pending) {
|
|
1675
|
+
try {
|
|
1676
|
+
logger.debug("Retrying record", {
|
|
1677
|
+
retryCount: item.retryCount
|
|
1678
|
+
});
|
|
1679
|
+
await this.send(item.record);
|
|
1680
|
+
logger.info(
|
|
1681
|
+
`[AdminIngestor:${this.endpoint}] Successfully retried record`,
|
|
1682
|
+
{
|
|
1683
|
+
retryCount: item.retryCount
|
|
1684
|
+
}
|
|
1685
|
+
);
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
const newRetryCount = item.retryCount + 1;
|
|
1688
|
+
if (newRetryCount >= MAX_RETRIES) {
|
|
1689
|
+
logger.error(
|
|
1690
|
+
`[AdminIngestor:${this.endpoint}] Max retries reached, moving to dead letter`,
|
|
1691
|
+
{
|
|
1692
|
+
retryCount: item.retryCount,
|
|
1693
|
+
maxRetries: MAX_RETRIES,
|
|
1694
|
+
error: String(e)
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
this.moveToDeadLetter(item, String(e));
|
|
1698
|
+
} else {
|
|
1699
|
+
logger.warn(
|
|
1700
|
+
`[AdminIngestor:${this.endpoint}] Retry failed, will retry again`,
|
|
1701
|
+
{
|
|
1702
|
+
retryCount: item.retryCount,
|
|
1703
|
+
newRetryCount,
|
|
1704
|
+
maxRetries: MAX_RETRIES,
|
|
1705
|
+
error: String(e)
|
|
1706
|
+
}
|
|
1707
|
+
);
|
|
1708
|
+
remaining.push({ record: item.record, retryCount: newRetryCount });
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (remaining.length > 0) {
|
|
1713
|
+
logger.info(
|
|
1714
|
+
`[AdminIngestor:${this.endpoint}] Queue processing complete, ${remaining.length} records remaining`,
|
|
1715
|
+
{
|
|
1716
|
+
remainingCount: remaining.length,
|
|
1717
|
+
processedCount: pending.length - remaining.length
|
|
1718
|
+
}
|
|
1719
|
+
);
|
|
1720
|
+
} else {
|
|
1721
|
+
logger.info(
|
|
1722
|
+
`[AdminIngestor:${this.endpoint}] Queue processing complete, all records processed`,
|
|
1723
|
+
{
|
|
1724
|
+
processedCount: pending.length
|
|
1725
|
+
}
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
this.savePending(remaining);
|
|
1729
|
+
}
|
|
1730
|
+
loadPending() {
|
|
1731
|
+
try {
|
|
1732
|
+
const content = fs6.readFileSync(this.pendingFile, "utf-8");
|
|
1733
|
+
return content.split("\n").filter(Boolean).map((line) => {
|
|
1734
|
+
try {
|
|
1735
|
+
return JSON.parse(line);
|
|
1736
|
+
} catch {
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
}).filter(Boolean);
|
|
1740
|
+
} catch {
|
|
1741
|
+
return [];
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
savePending(items) {
|
|
1745
|
+
try {
|
|
1746
|
+
if (items.length === 0) {
|
|
1747
|
+
if (fs6.existsSync(this.pendingFile))
|
|
1748
|
+
fs6.unlinkSync(this.pendingFile);
|
|
1749
|
+
} else {
|
|
1750
|
+
fs6.writeFileSync(
|
|
1751
|
+
this.pendingFile,
|
|
1752
|
+
items.map((i) => JSON.stringify(i)).join("\n") + "\n"
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
} catch (e) {
|
|
1756
|
+
logger.error("Failed to save pending", {
|
|
1757
|
+
error: String(e)
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
moveToDeadLetter(item, reason) {
|
|
1762
|
+
try {
|
|
1763
|
+
this.ensureDir(this.deadLetterFile);
|
|
1764
|
+
fs6.appendFileSync(
|
|
1765
|
+
this.deadLetterFile,
|
|
1766
|
+
JSON.stringify({
|
|
1767
|
+
...item,
|
|
1768
|
+
reason,
|
|
1769
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1770
|
+
}) + "\n"
|
|
1771
|
+
);
|
|
1772
|
+
logger.error(
|
|
1773
|
+
`[AdminIngestor:${this.endpoint}] Moved record to dead letter`,
|
|
1774
|
+
{
|
|
1775
|
+
retryCount: item.retryCount,
|
|
1776
|
+
reason,
|
|
1777
|
+
deadLetterFile: this.deadLetterFile
|
|
1778
|
+
}
|
|
1779
|
+
);
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
logger.error(
|
|
1782
|
+
`[AdminIngestor:${this.endpoint}] Failed to write dead letter`,
|
|
1783
|
+
{
|
|
1784
|
+
error: String(e),
|
|
1785
|
+
deadLetterFile: this.deadLetterFile
|
|
1786
|
+
}
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
ensureDir(filePath) {
|
|
1791
|
+
const dir = path5.dirname(filePath);
|
|
1792
|
+
if (!fs6.existsSync(dir))
|
|
1793
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// src/data/installation-recorder.ts
|
|
1798
|
+
var InstallationRecorder = class {
|
|
1799
|
+
constructor(send) {
|
|
1800
|
+
this.send = send;
|
|
1801
|
+
this.seen = /* @__PURE__ */ new Set();
|
|
1802
|
+
this.inflight = /* @__PURE__ */ new Map();
|
|
1803
|
+
}
|
|
1804
|
+
async tryRecord(source, userEmail) {
|
|
1805
|
+
const key = `${userEmail}:${source}`;
|
|
1806
|
+
if (this.seen.has(key))
|
|
1807
|
+
return;
|
|
1808
|
+
let p = this.inflight.get(key);
|
|
1809
|
+
if (!p) {
|
|
1810
|
+
p = this.send(source, userEmail);
|
|
1811
|
+
this.inflight.set(key, p);
|
|
1812
|
+
void p.finally(() => {
|
|
1813
|
+
this.inflight.delete(key);
|
|
1814
|
+
}).catch(() => {
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
try {
|
|
1818
|
+
const ok = await p;
|
|
1819
|
+
if (ok)
|
|
1820
|
+
this.seen.add(key);
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
// Test-only accessors keyed as `${userEmail}:${source}`.
|
|
1825
|
+
has(key) {
|
|
1826
|
+
return this.seen.has(key);
|
|
1827
|
+
}
|
|
1828
|
+
inflightCount() {
|
|
1829
|
+
return this.inflight.size;
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
|
|
1833
|
+
// src/pipeline/hook-pipeline.ts
|
|
1834
|
+
init_utils();
|
|
1835
|
+
var HookPipeline = class {
|
|
1836
|
+
constructor(cerberusClient, onFirstInstall) {
|
|
1837
|
+
this.cerberusClient = cerberusClient;
|
|
1838
|
+
this.onFirstInstall = onFirstInstall;
|
|
1839
|
+
}
|
|
1840
|
+
async process(source, event, input) {
|
|
1841
|
+
logger.debug("Pipeline processing", { source, event });
|
|
1842
|
+
try {
|
|
1843
|
+
this.onFirstInstall(source);
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
logger.error("onFirstInstall threw, continuing", {
|
|
1846
|
+
source,
|
|
1847
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
const cerberusResponse = await this.cerberusClient.evaluate({
|
|
1852
|
+
source,
|
|
1853
|
+
event,
|
|
1854
|
+
payload: input
|
|
1855
|
+
});
|
|
1856
|
+
return {
|
|
1857
|
+
response: cerberusResponse.ide_response,
|
|
1858
|
+
allowed: cerberusResponse.allowed,
|
|
1859
|
+
decision: cerberusResponse.decision,
|
|
1860
|
+
reason: cerberusResponse.reason,
|
|
1861
|
+
eventId: cerberusResponse.event_id
|
|
1862
|
+
};
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1865
|
+
logger.error("Pipeline error \u2014 allowing (fail-open)", {
|
|
1866
|
+
error: errorMessage,
|
|
1867
|
+
source,
|
|
1868
|
+
event
|
|
1869
|
+
});
|
|
1870
|
+
return {
|
|
1871
|
+
response: getOfflineFailOpen(source),
|
|
1872
|
+
allowed: true,
|
|
1873
|
+
decision: "allow",
|
|
1874
|
+
reason: `Pipeline error: ${errorMessage}`
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
// src/module.ts
|
|
1881
|
+
init_token_store();
|
|
1882
|
+
|
|
1883
|
+
// src/handlers/hook-handler.ts
|
|
1884
|
+
init_utils();
|
|
1885
|
+
|
|
1886
|
+
// src/data/recorder.ts
|
|
1887
|
+
var path6 = __toESM(require("path"));
|
|
1888
|
+
var os6 = __toESM(require("os"));
|
|
1889
|
+
var fs7 = __toESM(require("fs"));
|
|
1890
|
+
var import_crypto = require("crypto");
|
|
1891
|
+
init_utils();
|
|
1892
|
+
var EVENTS_FILE = path6.join(os6.homedir(), ".overwatch", "events.jsonl");
|
|
1893
|
+
function getHookCategory(event) {
|
|
1894
|
+
if (event === "stop" || event === "SessionEnd")
|
|
1895
|
+
return "stop";
|
|
1896
|
+
if (event === "SessionStart")
|
|
1897
|
+
return "before";
|
|
1898
|
+
if (event.startsWith("after"))
|
|
1899
|
+
return "after";
|
|
1900
|
+
if (event === "PostToolUse" || event === "AfterTool")
|
|
1901
|
+
return "after";
|
|
1902
|
+
if (event === "UserPromptSubmit" || event === "PreToolUse" || event === "BeforeAgent" || event === "BeforeTool") {
|
|
1903
|
+
return "before";
|
|
1904
|
+
}
|
|
1905
|
+
return "before";
|
|
1906
|
+
}
|
|
1907
|
+
function recordEvent(source, event, input, cerberusResponse, totalDuration) {
|
|
1908
|
+
const record = {
|
|
1909
|
+
id: cerberusResponse.event_id || (0, import_crypto.randomUUID)(),
|
|
1910
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1911
|
+
source,
|
|
1912
|
+
event,
|
|
1913
|
+
hook_category: getHookCategory(event),
|
|
1914
|
+
input,
|
|
1915
|
+
allowed: cerberusResponse.allowed,
|
|
1916
|
+
decision: cerberusResponse.decision,
|
|
1917
|
+
reason: cerberusResponse.reason,
|
|
1918
|
+
response: cerberusResponse.ide_response,
|
|
1919
|
+
total_duration_ms: totalDuration
|
|
1920
|
+
};
|
|
1921
|
+
try {
|
|
1922
|
+
const dir = path6.dirname(EVENTS_FILE);
|
|
1923
|
+
if (!fs7.existsSync(dir)) {
|
|
1924
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1925
|
+
}
|
|
1926
|
+
fs7.appendFile(EVENTS_FILE, JSON.stringify(record) + "\n", (err) => {
|
|
1927
|
+
if (err) {
|
|
1928
|
+
logger.error("Failed to write event:", { error: String(err) });
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
logger.debug("Recorded:", {
|
|
1932
|
+
id: record.id,
|
|
1933
|
+
event,
|
|
1934
|
+
allowed: record.allowed,
|
|
1935
|
+
decision: record.decision
|
|
1936
|
+
});
|
|
1937
|
+
});
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
logger.error("Failed to write event:", { error: String(error) });
|
|
1940
|
+
}
|
|
1941
|
+
return record;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/handlers/hook-handler.ts
|
|
1945
|
+
var HOOK_SOURCE_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1946
|
+
"cursor",
|
|
1947
|
+
"claudecode",
|
|
1948
|
+
"github_copilot",
|
|
1949
|
+
"gemini_cli",
|
|
1950
|
+
"codex",
|
|
1951
|
+
"langgraph",
|
|
1952
|
+
"pydantic_ai"
|
|
1953
|
+
]);
|
|
1954
|
+
var HookHandler = class {
|
|
1955
|
+
constructor(pipeline) {
|
|
1956
|
+
this.pipeline = pipeline;
|
|
1957
|
+
this.eventCount = 0;
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Set callback for project skills scanning
|
|
1961
|
+
*/
|
|
1962
|
+
setProjectSkillsScanCallback(callback) {
|
|
1963
|
+
this.onProjectSkillsScan = callback;
|
|
1964
|
+
}
|
|
1965
|
+
getEventCount() {
|
|
1966
|
+
return this.eventCount;
|
|
1967
|
+
}
|
|
1968
|
+
async handle(req, res) {
|
|
1969
|
+
const startTime = Date.now();
|
|
1970
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
1971
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
1972
|
+
const sourceRaw = pathParts[1];
|
|
1973
|
+
const eventRaw = pathParts[2];
|
|
1974
|
+
if (!sourceRaw || !eventRaw) {
|
|
1975
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1976
|
+
res.end(
|
|
1977
|
+
JSON.stringify({
|
|
1978
|
+
error: "Missing source or event in path",
|
|
1979
|
+
expected: "/hook/<source>/<event>"
|
|
1980
|
+
})
|
|
1981
|
+
);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (!HOOK_SOURCE_ALLOWLIST.has(sourceRaw)) {
|
|
1985
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1986
|
+
res.end(
|
|
1987
|
+
JSON.stringify({
|
|
1988
|
+
error: `Unsupported source: ${sourceRaw}`,
|
|
1989
|
+
supported: Array.from(HOOK_SOURCE_ALLOWLIST)
|
|
1990
|
+
})
|
|
1991
|
+
);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const source = sourceRaw;
|
|
1995
|
+
const event = eventRaw;
|
|
1996
|
+
logger.info(`${source}/${event} received`);
|
|
1997
|
+
const body = await parseBody(req);
|
|
1998
|
+
const result = await this.pipeline.process(source, event, body);
|
|
1999
|
+
this.eventCount++;
|
|
2000
|
+
const duration = Date.now() - startTime;
|
|
2001
|
+
const cerberusResponse = {
|
|
2002
|
+
allowed: result.allowed,
|
|
2003
|
+
decision: result.decision,
|
|
2004
|
+
reason: result.reason,
|
|
2005
|
+
ide_response: result.response,
|
|
2006
|
+
event_id: result.eventId
|
|
2007
|
+
};
|
|
2008
|
+
logger.info(
|
|
2009
|
+
`${source}/${event} completed in ${duration}ms`,
|
|
2010
|
+
result.response
|
|
2011
|
+
);
|
|
2012
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2013
|
+
res.end(JSON.stringify(result.response));
|
|
2014
|
+
recordEvent(
|
|
2015
|
+
source,
|
|
2016
|
+
event,
|
|
2017
|
+
body,
|
|
2018
|
+
cerberusResponse,
|
|
2019
|
+
duration
|
|
2020
|
+
);
|
|
2021
|
+
const workspace = this.extractWorkspace(body);
|
|
2022
|
+
if (workspace && this.onProjectSkillsScan) {
|
|
2023
|
+
try {
|
|
2024
|
+
this.onProjectSkillsScan(workspace);
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
logger.error("Error in project skills scan callback", {
|
|
2027
|
+
workspace,
|
|
2028
|
+
error: String(error)
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Extract workspace path from event body
|
|
2035
|
+
*/
|
|
2036
|
+
extractWorkspace(body) {
|
|
2037
|
+
if (body && typeof body === "object" && body !== null) {
|
|
2038
|
+
const input = body;
|
|
2039
|
+
if (input.workspace_roots && Array.isArray(input.workspace_roots)) {
|
|
2040
|
+
const roots = input.workspace_roots;
|
|
2041
|
+
if (roots.length > 0 && typeof roots[0] === "string" && roots[0].length > 0) {
|
|
2042
|
+
return roots[0];
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const workspaceFields = ["cwd", "workingDirectory", "workspace", "directory", "path"];
|
|
2047
|
+
for (const field of workspaceFields) {
|
|
2048
|
+
const value = body[field];
|
|
2049
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2050
|
+
return value;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
if (body.context && typeof body.context === "object" && body.context !== null) {
|
|
2054
|
+
const context = body.context;
|
|
2055
|
+
for (const field of workspaceFields) {
|
|
2056
|
+
const value = context[field];
|
|
2057
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2058
|
+
return value;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/handlers/scan-handler.ts
|
|
2067
|
+
var fs8 = __toESM(require("fs"));
|
|
2068
|
+
var path7 = __toESM(require("path"));
|
|
2069
|
+
var os7 = __toESM(require("os"));
|
|
2070
|
+
var crypto2 = __toESM(require("crypto"));
|
|
2071
|
+
init_utils();
|
|
2072
|
+
var SCANS_FILE = path7.join(os7.homedir(), ".overwatch", "scans.jsonl");
|
|
2073
|
+
var ScanHandler = class {
|
|
2074
|
+
constructor(scanner, ingestor) {
|
|
2075
|
+
this.scanner = scanner;
|
|
2076
|
+
this.ingestor = ingestor;
|
|
2077
|
+
}
|
|
2078
|
+
async handle(req, res) {
|
|
2079
|
+
const body = await parseBody(req);
|
|
2080
|
+
if (body.action === "run") {
|
|
2081
|
+
logger.info("Running internal scan...");
|
|
2082
|
+
if (!this.scanner.isAvailable()) {
|
|
2083
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
2084
|
+
res.end(
|
|
2085
|
+
JSON.stringify({ status: "error", error: "Scanner not available" })
|
|
2086
|
+
);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
try {
|
|
2090
|
+
const rulesDir = body.rulesDir;
|
|
2091
|
+
const results = await this.scanner.runScan(rulesDir);
|
|
2092
|
+
this.recordScan(results);
|
|
2093
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2094
|
+
res.end(JSON.stringify({ status: "ok", ...results }));
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2097
|
+
logger.error("Internal scan failed:", { error: errorMsg });
|
|
2098
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2099
|
+
res.end(JSON.stringify({ status: "error", error: errorMsg }));
|
|
2100
|
+
}
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
logger.info("Received external scan results");
|
|
2104
|
+
this.recordScan(body);
|
|
2105
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2106
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
2107
|
+
}
|
|
2108
|
+
// Public so module.ts can use it for initial scan
|
|
2109
|
+
async runInitialScan() {
|
|
2110
|
+
if (!this.scanner.isAvailable()) {
|
|
2111
|
+
logger.warn("Scanner not available, skipping initial MCP scan");
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
logger.info("Running initial MCP scan...");
|
|
2116
|
+
const results = await this.scanner.runScan();
|
|
2117
|
+
this.recordScan(results);
|
|
2118
|
+
logger.info("Initial MCP scan completed");
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
logger.error("Initial MCP scan failed:", { error: String(error) });
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
recordScan(input) {
|
|
2124
|
+
const results = input.results || [];
|
|
2125
|
+
const totalServers = input.total_servers || results.length;
|
|
2126
|
+
let totalIssues = 0;
|
|
2127
|
+
let maxSeverity;
|
|
2128
|
+
const severityOrder = {
|
|
2129
|
+
low: 1,
|
|
2130
|
+
medium: 2,
|
|
2131
|
+
high: 3,
|
|
2132
|
+
critical: 4
|
|
2133
|
+
};
|
|
2134
|
+
for (const result of results) {
|
|
2135
|
+
const yaraResults = result.yara_results || [];
|
|
2136
|
+
for (const yr of yaraResults) {
|
|
2137
|
+
if (yr.status === "warning") {
|
|
2138
|
+
totalIssues++;
|
|
2139
|
+
const metadata = yr.rule_metadata;
|
|
2140
|
+
const sev = (metadata?.severity || "low").toLowerCase();
|
|
2141
|
+
if (!maxSeverity || (severityOrder[sev] || 0) > (severityOrder[maxSeverity] || 0)) {
|
|
2142
|
+
maxSeverity = sev;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
const source = results[0]?.ide_source || "cursor";
|
|
2148
|
+
const record = {
|
|
2149
|
+
id: crypto2.randomUUID(),
|
|
2150
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2151
|
+
source,
|
|
2152
|
+
total_servers: totalServers,
|
|
2153
|
+
total_issues: totalIssues,
|
|
2154
|
+
max_severity: maxSeverity,
|
|
2155
|
+
raw: input
|
|
2156
|
+
};
|
|
2157
|
+
try {
|
|
2158
|
+
const dir = path7.dirname(SCANS_FILE);
|
|
2159
|
+
if (!fs8.existsSync(dir))
|
|
2160
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
2161
|
+
fs8.appendFileSync(SCANS_FILE, JSON.stringify(record) + "\n");
|
|
2162
|
+
logger.info("Recorded:", {
|
|
2163
|
+
id: record.id,
|
|
2164
|
+
servers: totalServers,
|
|
2165
|
+
issues: totalIssues
|
|
2166
|
+
});
|
|
2167
|
+
const serverPayload = {
|
|
2168
|
+
id: record.id,
|
|
2169
|
+
timestamp: record.timestamp,
|
|
2170
|
+
source: record.source,
|
|
2171
|
+
total_servers: record.total_servers,
|
|
2172
|
+
total_issues: record.total_issues,
|
|
2173
|
+
max_severity: record.max_severity,
|
|
2174
|
+
results: input.results,
|
|
2175
|
+
scan_type: input.scan_type,
|
|
2176
|
+
...input.total_servers !== void 0 && {
|
|
2177
|
+
total_servers_raw: input.total_servers
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
logger.debug("Ingesting scan with results at top level", {
|
|
2181
|
+
id: record.id,
|
|
2182
|
+
resultsCount: Array.isArray(input.results) ? input.results.length : 0
|
|
2183
|
+
});
|
|
2184
|
+
if (results.length === 0) {
|
|
2185
|
+
logger.info("Skipping ingestion: no scan results to send");
|
|
2186
|
+
} else {
|
|
2187
|
+
this.ingestor?.ingest(serverPayload);
|
|
2188
|
+
}
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
logger.error("Failed to write scan:", { error: String(error) });
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
// src/handlers/oauth-handler.ts
|
|
2196
|
+
init_utils();
|
|
2197
|
+
var OAuthHandler = class {
|
|
2198
|
+
constructor(port, pendingOAuthStates, oauthStateTimeout) {
|
|
2199
|
+
this.port = port;
|
|
2200
|
+
this.pendingOAuthStates = pendingOAuthStates;
|
|
2201
|
+
this.oauthStateTimeout = oauthStateTimeout;
|
|
2202
|
+
}
|
|
2203
|
+
async handleStart(req, res) {
|
|
2204
|
+
if (req.method !== "POST") {
|
|
2205
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
2206
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
const oauthState = createOAuthState(this.port);
|
|
2210
|
+
const codeChallenge = generateCodeChallenge(oauthState.codeVerifier);
|
|
2211
|
+
const authUrl = buildAuthorizationUrl(
|
|
2212
|
+
oauthState.state,
|
|
2213
|
+
codeChallenge,
|
|
2214
|
+
oauthState.redirectUri
|
|
2215
|
+
);
|
|
2216
|
+
this.pendingOAuthStates.set(oauthState.state, oauthState);
|
|
2217
|
+
setTimeout(() => {
|
|
2218
|
+
const state = this.pendingOAuthStates.get(oauthState.state);
|
|
2219
|
+
if (state && state.status === "pending") {
|
|
2220
|
+
state.status = "expired";
|
|
2221
|
+
}
|
|
2222
|
+
setTimeout(() => {
|
|
2223
|
+
this.pendingOAuthStates.delete(oauthState.state);
|
|
2224
|
+
}, 3e4);
|
|
2225
|
+
}, this.oauthStateTimeout);
|
|
2226
|
+
logger.info("Started new auth flow", { state: oauthState.state });
|
|
2227
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2228
|
+
res.end(
|
|
2229
|
+
JSON.stringify({
|
|
2230
|
+
state: oauthState.state,
|
|
2231
|
+
auth_url: authUrl
|
|
2232
|
+
})
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
async handleCallback(req, res) {
|
|
2236
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
2237
|
+
const code = url.searchParams.get("code");
|
|
2238
|
+
const state = url.searchParams.get("state");
|
|
2239
|
+
const error = url.searchParams.get("error");
|
|
2240
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
2241
|
+
if (error) {
|
|
2242
|
+
logger.error("Auth error:", { error, errorDescription });
|
|
2243
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2244
|
+
res.end(generateErrorHtml(errorDescription || error));
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (!state || !code) {
|
|
2248
|
+
logger.error("Missing state or code parameter");
|
|
2249
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2250
|
+
res.end(generateErrorHtml("Missing required parameters"));
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const oauthState = this.pendingOAuthStates.get(state);
|
|
2254
|
+
if (!oauthState) {
|
|
2255
|
+
logger.error("Unknown or expired state:", { state });
|
|
2256
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2257
|
+
res.end(generateErrorHtml("Invalid or expired login session"));
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
if (oauthState.status !== "pending") {
|
|
2261
|
+
logger.error("State not pending:", { state, status: oauthState.status });
|
|
2262
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2263
|
+
res.end(generateErrorHtml("Login session already processed"));
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
try {
|
|
2267
|
+
const { tokens, userInfo: userInfo2 } = await exchangeCodeForTokens(
|
|
2268
|
+
code,
|
|
2269
|
+
oauthState.codeVerifier,
|
|
2270
|
+
oauthState.redirectUri
|
|
2271
|
+
);
|
|
2272
|
+
saveTokens(tokens, userInfo2);
|
|
2273
|
+
oauthState.status = "completed";
|
|
2274
|
+
oauthState.tokens = tokens;
|
|
2275
|
+
oauthState.userInfo = userInfo2;
|
|
2276
|
+
logger.info("Login successful", { email: userInfo2?.email });
|
|
2277
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2278
|
+
res.end(
|
|
2279
|
+
generateSuccessHtml(
|
|
2280
|
+
"You can close this window and return to the terminal.",
|
|
2281
|
+
userInfo2?.email
|
|
2282
|
+
)
|
|
2283
|
+
);
|
|
2284
|
+
} catch (err) {
|
|
2285
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2286
|
+
logger.error("Token exchange failed:", { error: errorMsg });
|
|
2287
|
+
oauthState.status = "failed";
|
|
2288
|
+
oauthState.error = errorMsg;
|
|
2289
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2290
|
+
res.end(generateErrorHtml(errorMsg));
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
async handleStatus(req, res) {
|
|
2294
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
2295
|
+
const state = url.searchParams.get("state");
|
|
2296
|
+
if (!state) {
|
|
2297
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2298
|
+
res.end(JSON.stringify({ error: "Missing state parameter" }));
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
const oauthState = this.pendingOAuthStates.get(state);
|
|
2302
|
+
if (!oauthState) {
|
|
2303
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2304
|
+
res.end(JSON.stringify({ error: "Unknown state", status: "unknown" }));
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2308
|
+
res.end(
|
|
2309
|
+
JSON.stringify({
|
|
2310
|
+
status: oauthState.status,
|
|
2311
|
+
user: oauthState.userInfo,
|
|
2312
|
+
error: oauthState.error
|
|
2313
|
+
})
|
|
2314
|
+
);
|
|
2315
|
+
if (oauthState.status !== "pending") {
|
|
2316
|
+
this.pendingOAuthStates.delete(state);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
// src/skills/scanner.ts
|
|
2322
|
+
var fs9 = __toESM(require("fs/promises"));
|
|
2323
|
+
var path8 = __toESM(require("path"));
|
|
2324
|
+
var os8 = __toESM(require("os"));
|
|
2325
|
+
var crypto3 = __toESM(require("crypto"));
|
|
2326
|
+
init_utils();
|
|
2327
|
+
var CLAUDE_SKILLS_DIR = path8.join(os8.homedir(), ".claude", "skills");
|
|
2328
|
+
var CURSOR_SKILLS_DIR = path8.join(os8.homedir(), ".cursor", "skills");
|
|
2329
|
+
var SkillsScanner = class {
|
|
2330
|
+
/**
|
|
2331
|
+
* Scan personal skills from both directories:
|
|
2332
|
+
* - ~/.claude/skills/
|
|
2333
|
+
* - ~/.cursor/skills/
|
|
2334
|
+
* Returns current state
|
|
2335
|
+
*/
|
|
2336
|
+
async scan() {
|
|
2337
|
+
const startTime = Date.now();
|
|
2338
|
+
logger.info("Starting skills scan", {
|
|
2339
|
+
directories: [CLAUDE_SKILLS_DIR, CURSOR_SKILLS_DIR]
|
|
2340
|
+
});
|
|
2341
|
+
const [claudeSkills, cursorSkills] = await Promise.all([
|
|
2342
|
+
this.scanDirectory(CLAUDE_SKILLS_DIR),
|
|
2343
|
+
this.scanDirectory(CURSOR_SKILLS_DIR)
|
|
2344
|
+
]);
|
|
2345
|
+
const skills = [...claudeSkills, ...cursorSkills];
|
|
2346
|
+
const scanDurationMs = Date.now() - startTime;
|
|
2347
|
+
const result = {
|
|
2348
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2349
|
+
totalSkills: skills.length,
|
|
2350
|
+
skills,
|
|
2351
|
+
scanDurationMs
|
|
2352
|
+
};
|
|
2353
|
+
logger.info("Skills scan completed", {
|
|
2354
|
+
totalSkills: skills.length,
|
|
2355
|
+
durationMs: scanDurationMs
|
|
2356
|
+
});
|
|
2357
|
+
return result;
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Scan skills directory
|
|
2361
|
+
*/
|
|
2362
|
+
async scanDirectory(dir) {
|
|
2363
|
+
const skills = [];
|
|
2364
|
+
if (!await this.exists(dir)) {
|
|
2365
|
+
logger.debug("Skills directory does not exist", { dir });
|
|
2366
|
+
return skills;
|
|
2367
|
+
}
|
|
2368
|
+
try {
|
|
2369
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
2370
|
+
for (const entry of entries) {
|
|
2371
|
+
if (!entry.isDirectory())
|
|
2372
|
+
continue;
|
|
2373
|
+
const skillDir = path8.join(dir, entry.name);
|
|
2374
|
+
const content = await this.readSkillContent(skillDir);
|
|
2375
|
+
if (content === null) {
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
const files = await this.listSkillFiles(skillDir);
|
|
2379
|
+
const contentHash = this.hashContent(content);
|
|
2380
|
+
skills.push({
|
|
2381
|
+
name: entry.name,
|
|
2382
|
+
path: skillDir,
|
|
2383
|
+
content,
|
|
2384
|
+
contentHash,
|
|
2385
|
+
files
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
} catch (error) {
|
|
2389
|
+
logger.error("Failed to scan skills directory", {
|
|
2390
|
+
dir,
|
|
2391
|
+
error: String(error)
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
return skills;
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Check if a path exists
|
|
2398
|
+
*/
|
|
2399
|
+
async exists(filePath) {
|
|
2400
|
+
try {
|
|
2401
|
+
await fs9.access(filePath);
|
|
2402
|
+
return true;
|
|
2403
|
+
} catch {
|
|
2404
|
+
return false;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Read full SKILL.md content
|
|
2409
|
+
*/
|
|
2410
|
+
async readSkillContent(skillDir) {
|
|
2411
|
+
const skillFile = path8.join(skillDir, "SKILL.md");
|
|
2412
|
+
if (!await this.exists(skillFile)) {
|
|
2413
|
+
return null;
|
|
2414
|
+
}
|
|
2415
|
+
try {
|
|
2416
|
+
return await fs9.readFile(skillFile, "utf-8");
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
logger.error("Failed to read skill file", {
|
|
2419
|
+
skillFile,
|
|
2420
|
+
error: String(error)
|
|
2421
|
+
});
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* List files in skill directory (recursive)
|
|
2427
|
+
*/
|
|
2428
|
+
async listSkillFiles(skillDir) {
|
|
2429
|
+
const files = [];
|
|
2430
|
+
try {
|
|
2431
|
+
const listFiles = async (dir, prefix = "") => {
|
|
2432
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
2433
|
+
for (const entry of entries) {
|
|
2434
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
2435
|
+
if (entry.isDirectory()) {
|
|
2436
|
+
await listFiles(path8.join(dir, entry.name), relativePath);
|
|
2437
|
+
} else {
|
|
2438
|
+
files.push(relativePath);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
await listFiles(skillDir);
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
logger.error("Failed to list skill files", {
|
|
2445
|
+
skillDir,
|
|
2446
|
+
error: String(error)
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
return files;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Hash content for backend deduplication
|
|
2453
|
+
*/
|
|
2454
|
+
hashContent(content) {
|
|
2455
|
+
const hash = crypto3.createHash("sha256");
|
|
2456
|
+
hash.update(content);
|
|
2457
|
+
return `sha256:${hash.digest("hex")}`;
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Scan project-level skills from:
|
|
2461
|
+
* - {workspace}/.claude/skills/
|
|
2462
|
+
* - {workspace}/.cursor/skills/
|
|
2463
|
+
*/
|
|
2464
|
+
async scanProjectSkills(workspace) {
|
|
2465
|
+
const startTime = Date.now();
|
|
2466
|
+
const claudeSkillsDir = path8.join(workspace, ".claude", "skills");
|
|
2467
|
+
const cursorSkillsDir = path8.join(workspace, ".cursor", "skills");
|
|
2468
|
+
logger.info("Starting project skills scan", {
|
|
2469
|
+
workspace,
|
|
2470
|
+
directories: [claudeSkillsDir, cursorSkillsDir]
|
|
2471
|
+
});
|
|
2472
|
+
const [claudeSkills, cursorSkills] = await Promise.all([
|
|
2473
|
+
this.scanDirectory(claudeSkillsDir),
|
|
2474
|
+
this.scanDirectory(cursorSkillsDir)
|
|
2475
|
+
]);
|
|
2476
|
+
const skills = [...claudeSkills, ...cursorSkills];
|
|
2477
|
+
const scanDurationMs = Date.now() - startTime;
|
|
2478
|
+
const result = {
|
|
2479
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2480
|
+
totalSkills: skills.length,
|
|
2481
|
+
skills,
|
|
2482
|
+
scanDurationMs,
|
|
2483
|
+
workspace
|
|
2484
|
+
};
|
|
2485
|
+
logger.info("Project skills scan completed", {
|
|
2486
|
+
workspace,
|
|
2487
|
+
totalSkills: skills.length,
|
|
2488
|
+
durationMs: scanDurationMs
|
|
2489
|
+
});
|
|
2490
|
+
return result;
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
// src/coverage/detector.ts
|
|
2495
|
+
var fs12 = __toESM(require("fs"));
|
|
2496
|
+
var path12 = __toESM(require("path"));
|
|
2497
|
+
|
|
2498
|
+
// src/installer.ts
|
|
2499
|
+
var fs11 = __toESM(require("fs"));
|
|
2500
|
+
var path10 = __toESM(require("path"));
|
|
2501
|
+
var os10 = __toESM(require("os"));
|
|
2502
|
+
init_version();
|
|
2503
|
+
|
|
2504
|
+
// src/utils/port-manager.ts
|
|
2505
|
+
var net = __toESM(require("net"));
|
|
2506
|
+
var fs10 = __toESM(require("fs"));
|
|
2507
|
+
var os9 = __toESM(require("os"));
|
|
2508
|
+
var path9 = __toESM(require("path"));
|
|
2509
|
+
var GUARDIAN_PORT = 17580;
|
|
2510
|
+
var PID_FILE_PATH = path9.join(
|
|
2511
|
+
os9.homedir(),
|
|
2512
|
+
".overwatch",
|
|
2513
|
+
"guardian.pid"
|
|
2514
|
+
);
|
|
2515
|
+
var PortManager = class _PortManager {
|
|
2516
|
+
static async isPortAvailable(port) {
|
|
2517
|
+
return new Promise((resolve) => {
|
|
2518
|
+
const server = net.createServer();
|
|
2519
|
+
server.once("error", () => resolve(false));
|
|
2520
|
+
server.once("listening", () => {
|
|
2521
|
+
server.close(() => resolve(true));
|
|
2522
|
+
});
|
|
2523
|
+
server.listen(port, "127.0.0.1");
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
static writePidFile(pid = process.pid) {
|
|
2527
|
+
const dir = path9.dirname(PID_FILE_PATH);
|
|
2528
|
+
if (!fs10.existsSync(dir)) {
|
|
2529
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
2530
|
+
}
|
|
2531
|
+
fs10.writeFileSync(PID_FILE_PATH, String(pid));
|
|
2532
|
+
}
|
|
2533
|
+
static readPidFile() {
|
|
2534
|
+
try {
|
|
2535
|
+
const raw = fs10.readFileSync(PID_FILE_PATH, "utf-8").trim();
|
|
2536
|
+
const pid = parseInt(raw, 10);
|
|
2537
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2538
|
+
return null;
|
|
2539
|
+
}
|
|
2540
|
+
return pid;
|
|
2541
|
+
} catch {
|
|
2542
|
+
return null;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
static removePidFile() {
|
|
2546
|
+
try {
|
|
2547
|
+
fs10.unlinkSync(PID_FILE_PATH);
|
|
2548
|
+
} catch {
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
static isProcessAlive(pid) {
|
|
2552
|
+
try {
|
|
2553
|
+
process.kill(pid, 0);
|
|
2554
|
+
return true;
|
|
2555
|
+
} catch (err) {
|
|
2556
|
+
return err?.code === "EPERM";
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* If GUARDIAN_PORT is held by a process whose PID matches our pidfile, send
|
|
2561
|
+
* SIGTERM (then SIGKILL) and wait for the port to free. Returns:
|
|
2562
|
+
* - "free": port was already available; nothing to do
|
|
2563
|
+
* - "killed": port was held by our pidfile owner; we killed it and the port is now free
|
|
2564
|
+
* - "foreign": port is held by something we don't own (or pidfile is stale and the holder is unknown)
|
|
2565
|
+
*/
|
|
2566
|
+
static async reconcileStaleDaemon() {
|
|
2567
|
+
if (await _PortManager.isPortAvailable(GUARDIAN_PORT)) {
|
|
2568
|
+
const stalePid = _PortManager.readPidFile();
|
|
2569
|
+
if (stalePid !== null && !_PortManager.isProcessAlive(stalePid)) {
|
|
2570
|
+
_PortManager.removePidFile();
|
|
2571
|
+
}
|
|
2572
|
+
return { kind: "free" };
|
|
2573
|
+
}
|
|
2574
|
+
const pid = _PortManager.readPidFile();
|
|
2575
|
+
if (pid === null || !_PortManager.isProcessAlive(pid)) {
|
|
2576
|
+
return { kind: "foreign" };
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
process.kill(pid, "SIGTERM");
|
|
2580
|
+
} catch {
|
|
2581
|
+
}
|
|
2582
|
+
const sigtermDeadline = Date.now() + 5e3;
|
|
2583
|
+
while (Date.now() < sigtermDeadline) {
|
|
2584
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2585
|
+
if (await _PortManager.isPortAvailable(GUARDIAN_PORT)) {
|
|
2586
|
+
_PortManager.removePidFile();
|
|
2587
|
+
return { kind: "killed", pid };
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
try {
|
|
2591
|
+
process.kill(pid, "SIGKILL");
|
|
2592
|
+
} catch {
|
|
2593
|
+
}
|
|
2594
|
+
const sigkillDeadline = Date.now() + 2e3;
|
|
2595
|
+
while (Date.now() < sigkillDeadline) {
|
|
2596
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2597
|
+
if (await _PortManager.isPortAvailable(GUARDIAN_PORT)) {
|
|
2598
|
+
_PortManager.removePidFile();
|
|
2599
|
+
return { kind: "killed", pid };
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return { kind: "foreign" };
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// src/installer.ts
|
|
2607
|
+
var IDE_REGISTRY = {
|
|
2608
|
+
cursor: {
|
|
2609
|
+
hooksLocation: path10.join(os10.homedir(), ".cursor", "hooks.json"),
|
|
2610
|
+
events: [
|
|
2611
|
+
"beforeSubmitPrompt",
|
|
2612
|
+
"beforeShellExecution",
|
|
2613
|
+
"beforeMCPExecution",
|
|
2614
|
+
"beforeReadFile",
|
|
2615
|
+
"afterShellExecution",
|
|
2616
|
+
"afterMCPExecution",
|
|
2617
|
+
"afterFileEdit",
|
|
2618
|
+
"afterAgentResponse",
|
|
2619
|
+
"afterAgentThought",
|
|
2620
|
+
"stop"
|
|
2621
|
+
],
|
|
2622
|
+
installMethod: "native"
|
|
2623
|
+
},
|
|
2624
|
+
claudecode: {
|
|
2625
|
+
hooksLocation: path10.join(os10.homedir(), ".claude", "settings.json"),
|
|
2626
|
+
events: ["UserPromptSubmit", "PreToolUse", "PostToolUse"],
|
|
2627
|
+
installMethod: "native"
|
|
2628
|
+
// Writes hooks to ~/.claude/settings.json
|
|
2629
|
+
},
|
|
2630
|
+
github_copilot: {
|
|
2631
|
+
hooksLocation: path10.join(os10.homedir(), ".overwatch", "github_copilot", "hooks.json"),
|
|
2632
|
+
events: ["sessionStart", "sessionEnd", "userPromptSubmitted", "preToolUse", "postToolUse", "errorOccurred"],
|
|
2633
|
+
installMethod: "native"
|
|
2634
|
+
},
|
|
2635
|
+
gemini_cli: {
|
|
2636
|
+
hooksLocation: path10.join(os10.homedir(), ".gemini", "settings.json"),
|
|
2637
|
+
events: [
|
|
2638
|
+
"SessionStart",
|
|
2639
|
+
"SessionEnd",
|
|
2640
|
+
"BeforeAgent",
|
|
2641
|
+
"AfterAgent",
|
|
2642
|
+
"BeforeModel",
|
|
2643
|
+
"AfterModel",
|
|
2644
|
+
"BeforeToolSelection",
|
|
2645
|
+
"BeforeTool",
|
|
2646
|
+
"AfterTool",
|
|
2647
|
+
"PreCompress",
|
|
2648
|
+
"Notification"
|
|
2649
|
+
],
|
|
2650
|
+
installMethod: "native"
|
|
2651
|
+
},
|
|
2652
|
+
codex: {
|
|
2653
|
+
hooksLocation: path10.join(os10.homedir(), ".codex", "hooks.json"),
|
|
2654
|
+
events: [
|
|
2655
|
+
"SessionStart",
|
|
2656
|
+
"PreToolUse",
|
|
2657
|
+
"PermissionRequest",
|
|
2658
|
+
"PostToolUse",
|
|
2659
|
+
"UserPromptSubmit",
|
|
2660
|
+
"Stop"
|
|
2661
|
+
],
|
|
2662
|
+
installMethod: "native"
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
var SUPPORTED_IDES = [
|
|
2666
|
+
"cursor",
|
|
2667
|
+
"claudecode",
|
|
2668
|
+
"github_copilot",
|
|
2669
|
+
"gemini_cli",
|
|
2670
|
+
"codex"
|
|
2671
|
+
];
|
|
2672
|
+
var OVERWATCH_META_KEY = "_overwatch";
|
|
2673
|
+
async function listInstalledHooks() {
|
|
2674
|
+
const results = [];
|
|
2675
|
+
for (const ide of SUPPORTED_IDES) {
|
|
2676
|
+
const ideConfig = IDE_REGISTRY[ide];
|
|
2677
|
+
const hooksLocation = ideConfig.hooksLocation;
|
|
2678
|
+
if (fs11.existsSync(hooksLocation)) {
|
|
2679
|
+
try {
|
|
2680
|
+
const content = fs11.readFileSync(hooksLocation, "utf-8");
|
|
2681
|
+
const hasOverwatchHooks = content.includes(`"${OVERWATCH_META_KEY}"`) || content.includes("universal-hook.sh") || content.includes("universal-hook.ps1");
|
|
2682
|
+
results.push({
|
|
2683
|
+
ide,
|
|
2684
|
+
hooksLocation,
|
|
2685
|
+
hooksInstalled: hasOverwatchHooks
|
|
2686
|
+
});
|
|
2687
|
+
} catch {
|
|
2688
|
+
results.push({
|
|
2689
|
+
ide,
|
|
2690
|
+
hooksLocation,
|
|
2691
|
+
hooksInstalled: false
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return results;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
// src/coverage/detector.ts
|
|
2700
|
+
init_utils();
|
|
2701
|
+
|
|
2702
|
+
// src/coverage/definitions.ts
|
|
2703
|
+
var path11 = __toESM(require("path"));
|
|
2704
|
+
var os11 = __toESM(require("os"));
|
|
2705
|
+
var HOME = os11.homedir();
|
|
2706
|
+
var VSCODE_EXTENSIONS = {
|
|
2707
|
+
darwin: path11.join(HOME, ".vscode", "extensions"),
|
|
2708
|
+
linux: path11.join(HOME, ".vscode", "extensions"),
|
|
2709
|
+
win32: path11.join(HOME, ".vscode", "extensions")
|
|
2710
|
+
};
|
|
2711
|
+
var JETBRAINS_CONFIG = {
|
|
2712
|
+
darwin: path11.join(HOME, "Library", "Application Support", "JetBrains"),
|
|
2713
|
+
linux: path11.join(HOME, ".config", "JetBrains"),
|
|
2714
|
+
win32: path11.join(HOME, "AppData", "Roaming", "JetBrains")
|
|
2715
|
+
};
|
|
2716
|
+
var AGENT_DEFINITIONS = [
|
|
2717
|
+
// ============================================
|
|
2718
|
+
// AI-Native IDEs
|
|
2719
|
+
// ============================================
|
|
2720
|
+
{
|
|
2721
|
+
id: "cursor",
|
|
2722
|
+
name: "Cursor",
|
|
2723
|
+
category: "ide",
|
|
2724
|
+
paths: {
|
|
2725
|
+
darwin: [
|
|
2726
|
+
path11.join(HOME, ".cursor"),
|
|
2727
|
+
"/Applications/Cursor.app"
|
|
2728
|
+
],
|
|
2729
|
+
linux: [
|
|
2730
|
+
path11.join(HOME, ".cursor"),
|
|
2731
|
+
"/usr/share/applications/cursor.desktop"
|
|
2732
|
+
],
|
|
2733
|
+
win32: [
|
|
2734
|
+
path11.join(HOME, ".cursor"),
|
|
2735
|
+
path11.join(process.env.LOCALAPPDATA || "", "Programs", "cursor")
|
|
2736
|
+
]
|
|
2737
|
+
}
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
id: "zed",
|
|
2741
|
+
name: "Zed",
|
|
2742
|
+
category: "ide",
|
|
2743
|
+
paths: {
|
|
2744
|
+
darwin: [
|
|
2745
|
+
path11.join(HOME, ".zed"),
|
|
2746
|
+
"/Applications/Zed.app"
|
|
2747
|
+
],
|
|
2748
|
+
linux: [path11.join(HOME, ".config", "zed")]
|
|
2749
|
+
// Zed not available on Windows
|
|
2750
|
+
}
|
|
2751
|
+
},
|
|
2752
|
+
{
|
|
2753
|
+
id: "void",
|
|
2754
|
+
name: "Void",
|
|
2755
|
+
category: "ide",
|
|
2756
|
+
paths: {
|
|
2757
|
+
darwin: [
|
|
2758
|
+
path11.join(HOME, ".void"),
|
|
2759
|
+
"/Applications/Void.app"
|
|
2760
|
+
],
|
|
2761
|
+
linux: [path11.join(HOME, ".void")],
|
|
2762
|
+
win32: [path11.join(HOME, ".void")]
|
|
2763
|
+
}
|
|
2764
|
+
},
|
|
2765
|
+
{
|
|
2766
|
+
id: "pearai",
|
|
2767
|
+
name: "PearAI",
|
|
2768
|
+
category: "ide",
|
|
2769
|
+
paths: {
|
|
2770
|
+
darwin: [
|
|
2771
|
+
path11.join(HOME, ".pearai"),
|
|
2772
|
+
"/Applications/PearAI.app"
|
|
2773
|
+
],
|
|
2774
|
+
linux: [path11.join(HOME, ".pearai")],
|
|
2775
|
+
win32: [path11.join(HOME, ".pearai")]
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
// ============================================
|
|
2779
|
+
// CLI Coding Tools
|
|
2780
|
+
// ============================================
|
|
2781
|
+
{
|
|
2782
|
+
id: "claudecode",
|
|
2783
|
+
name: "Claude Code",
|
|
2784
|
+
category: "cli",
|
|
2785
|
+
paths: {
|
|
2786
|
+
darwin: [path11.join(HOME, ".claude")],
|
|
2787
|
+
linux: [path11.join(HOME, ".claude")],
|
|
2788
|
+
win32: [path11.join(HOME, ".claude")]
|
|
2789
|
+
}
|
|
2790
|
+
},
|
|
2791
|
+
{
|
|
2792
|
+
id: "aider",
|
|
2793
|
+
name: "Aider",
|
|
2794
|
+
category: "cli",
|
|
2795
|
+
paths: {
|
|
2796
|
+
darwin: [path11.join(HOME, ".aider")],
|
|
2797
|
+
linux: [path11.join(HOME, ".aider")],
|
|
2798
|
+
win32: [path11.join(HOME, ".aider")]
|
|
2799
|
+
}
|
|
2800
|
+
},
|
|
2801
|
+
{
|
|
2802
|
+
id: "mentat",
|
|
2803
|
+
name: "Mentat",
|
|
2804
|
+
category: "cli",
|
|
2805
|
+
paths: {
|
|
2806
|
+
darwin: [path11.join(HOME, ".mentat")],
|
|
2807
|
+
linux: [path11.join(HOME, ".mentat")],
|
|
2808
|
+
win32: [path11.join(HOME, ".mentat")]
|
|
2809
|
+
}
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
id: "gpt_pilot",
|
|
2813
|
+
name: "GPT-Pilot",
|
|
2814
|
+
category: "cli",
|
|
2815
|
+
paths: {
|
|
2816
|
+
darwin: [path11.join(HOME, ".gpt-pilot")],
|
|
2817
|
+
linux: [path11.join(HOME, ".gpt-pilot")],
|
|
2818
|
+
win32: [path11.join(HOME, ".gpt-pilot")]
|
|
2819
|
+
}
|
|
2820
|
+
},
|
|
2821
|
+
{
|
|
2822
|
+
id: "codex",
|
|
2823
|
+
name: "OpenAI Codex",
|
|
2824
|
+
category: "cli",
|
|
2825
|
+
paths: {
|
|
2826
|
+
darwin: [path11.join(HOME, ".codex")],
|
|
2827
|
+
linux: [path11.join(HOME, ".codex")],
|
|
2828
|
+
win32: [path11.join(HOME, ".codex")]
|
|
2829
|
+
}
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
id: "gemini_cli",
|
|
2833
|
+
name: "Gemini CLI",
|
|
2834
|
+
category: "cli",
|
|
2835
|
+
paths: {
|
|
2836
|
+
darwin: [path11.join(HOME, ".gemini")],
|
|
2837
|
+
linux: [path11.join(HOME, ".gemini")],
|
|
2838
|
+
win32: [path11.join(HOME, ".gemini")]
|
|
2839
|
+
}
|
|
2840
|
+
},
|
|
2841
|
+
// ============================================
|
|
2842
|
+
// Traditional IDEs with AI
|
|
2843
|
+
// ============================================
|
|
2844
|
+
{
|
|
2845
|
+
id: "vscode",
|
|
2846
|
+
name: "VS Code",
|
|
2847
|
+
category: "ide",
|
|
2848
|
+
paths: {
|
|
2849
|
+
darwin: [path11.join(HOME, ".vscode")],
|
|
2850
|
+
linux: [path11.join(HOME, ".vscode")],
|
|
2851
|
+
win32: [path11.join(HOME, ".vscode")]
|
|
2852
|
+
}
|
|
2853
|
+
},
|
|
2854
|
+
{
|
|
2855
|
+
id: "jetbrains",
|
|
2856
|
+
name: "JetBrains IDEs",
|
|
2857
|
+
category: "ide",
|
|
2858
|
+
paths: {
|
|
2859
|
+
darwin: [path11.join(HOME, "Library", "Application Support", "JetBrains")],
|
|
2860
|
+
linux: [path11.join(HOME, ".config", "JetBrains")],
|
|
2861
|
+
win32: [path11.join(HOME, "AppData", "Roaming", "JetBrains")]
|
|
2862
|
+
}
|
|
2863
|
+
},
|
|
2864
|
+
// ============================================
|
|
2865
|
+
// VS Code Plugins
|
|
2866
|
+
// ============================================
|
|
2867
|
+
{
|
|
2868
|
+
id: "github_copilot",
|
|
2869
|
+
name: "GitHub Copilot",
|
|
2870
|
+
category: "plugin",
|
|
2871
|
+
hostIde: "vscode",
|
|
2872
|
+
paths: {},
|
|
2873
|
+
vscodeExtension: "github.copilot"
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
id: "continue",
|
|
2877
|
+
name: "Continue",
|
|
2878
|
+
category: "plugin",
|
|
2879
|
+
hostIde: "vscode",
|
|
2880
|
+
paths: {
|
|
2881
|
+
darwin: [path11.join(HOME, ".continue")],
|
|
2882
|
+
linux: [path11.join(HOME, ".continue")],
|
|
2883
|
+
win32: [path11.join(HOME, ".continue")]
|
|
2884
|
+
},
|
|
2885
|
+
vscodeExtension: "continue.continue"
|
|
2886
|
+
},
|
|
2887
|
+
{
|
|
2888
|
+
id: "tabnine",
|
|
2889
|
+
name: "Tabnine",
|
|
2890
|
+
category: "plugin",
|
|
2891
|
+
hostIde: "vscode",
|
|
2892
|
+
paths: {
|
|
2893
|
+
darwin: [path11.join(HOME, ".tabnine")],
|
|
2894
|
+
linux: [path11.join(HOME, ".tabnine")],
|
|
2895
|
+
win32: [path11.join(HOME, ".tabnine")]
|
|
2896
|
+
},
|
|
2897
|
+
vscodeExtension: "tabnine.tabnine"
|
|
2898
|
+
},
|
|
2899
|
+
{
|
|
2900
|
+
id: "codeium",
|
|
2901
|
+
name: "Codeium",
|
|
2902
|
+
category: "plugin",
|
|
2903
|
+
hostIde: "vscode",
|
|
2904
|
+
paths: {},
|
|
2905
|
+
vscodeExtension: "codeium.codeium"
|
|
2906
|
+
},
|
|
2907
|
+
// ============================================
|
|
2908
|
+
// JetBrains Plugins
|
|
2909
|
+
// ============================================
|
|
2910
|
+
{
|
|
2911
|
+
id: "jetbrains_ai",
|
|
2912
|
+
name: "JetBrains AI Assistant",
|
|
2913
|
+
category: "plugin",
|
|
2914
|
+
hostIde: "jetbrains",
|
|
2915
|
+
paths: {},
|
|
2916
|
+
jetbrainsPlugin: "com.intellij.ai"
|
|
2917
|
+
}
|
|
2918
|
+
];
|
|
2919
|
+
var MONITORABLE_AGENTS = /* @__PURE__ */ new Set([
|
|
2920
|
+
"cursor",
|
|
2921
|
+
"claudecode",
|
|
2922
|
+
"github_copilot",
|
|
2923
|
+
"gemini_cli",
|
|
2924
|
+
"codex"
|
|
2925
|
+
]);
|
|
2926
|
+
|
|
2927
|
+
// src/coverage/detector.ts
|
|
2928
|
+
var PLATFORM = process.platform;
|
|
2929
|
+
var AgentDetector = class {
|
|
2930
|
+
constructor() {
|
|
2931
|
+
this.cachedMetrics = null;
|
|
2932
|
+
this.cacheTimestamp = 0;
|
|
2933
|
+
this.CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
2934
|
+
}
|
|
2935
|
+
// 10 minute cache
|
|
2936
|
+
/**
|
|
2937
|
+
* Detect all agents and return coverage metrics
|
|
2938
|
+
*/
|
|
2939
|
+
async detectAll() {
|
|
2940
|
+
if (this.cachedMetrics && Date.now() - this.cacheTimestamp < this.CACHE_TTL_MS) {
|
|
2941
|
+
return this.cachedMetrics;
|
|
2942
|
+
}
|
|
2943
|
+
const detected = await this.detectInstalledAgents();
|
|
2944
|
+
const monitoredAgents = await this.getMonitoredAgents();
|
|
2945
|
+
const monitoredIds = new Set(monitoredAgents.map((a) => a.id));
|
|
2946
|
+
const agents = detected.map((a) => ({
|
|
2947
|
+
...a,
|
|
2948
|
+
monitored: monitoredIds.has(a.id)
|
|
2949
|
+
}));
|
|
2950
|
+
const installedAgents = agents.filter((a) => a.installed);
|
|
2951
|
+
const installedCount = installedAgents.length;
|
|
2952
|
+
const monitoredCount = installedAgents.filter((a) => a.monitored).length;
|
|
2953
|
+
const metrics = {
|
|
2954
|
+
installedCount,
|
|
2955
|
+
monitoredCount,
|
|
2956
|
+
percentage: installedCount > 0 ? Math.round(monitoredCount / installedCount * 100) : 0,
|
|
2957
|
+
agents,
|
|
2958
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2959
|
+
platform: PLATFORM
|
|
2960
|
+
};
|
|
2961
|
+
this.cachedMetrics = metrics;
|
|
2962
|
+
this.cacheTimestamp = Date.now();
|
|
2963
|
+
logger.debug("Agent detection completed", {
|
|
2964
|
+
installed: installedCount,
|
|
2965
|
+
monitored: monitoredCount,
|
|
2966
|
+
percentage: metrics.percentage
|
|
2967
|
+
});
|
|
2968
|
+
return metrics;
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Detect all installed agents on the system
|
|
2972
|
+
*/
|
|
2973
|
+
async detectInstalledAgents() {
|
|
2974
|
+
const agents = [];
|
|
2975
|
+
for (const def of AGENT_DEFINITIONS) {
|
|
2976
|
+
const detected = this.detectAgent(def);
|
|
2977
|
+
agents.push(detected);
|
|
2978
|
+
}
|
|
2979
|
+
return agents;
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Get agents that are currently monitored by Overwatch
|
|
2983
|
+
*/
|
|
2984
|
+
async getMonitoredAgents() {
|
|
2985
|
+
const installedHooks = await listInstalledHooks();
|
|
2986
|
+
const monitoredAgents = [];
|
|
2987
|
+
for (const hook of installedHooks) {
|
|
2988
|
+
if (hook.hooksInstalled) {
|
|
2989
|
+
const def = AGENT_DEFINITIONS.find((d) => d.id === hook.ide);
|
|
2990
|
+
if (def) {
|
|
2991
|
+
monitoredAgents.push({
|
|
2992
|
+
id: def.id,
|
|
2993
|
+
name: def.name,
|
|
2994
|
+
category: def.category,
|
|
2995
|
+
installed: true,
|
|
2996
|
+
monitored: true,
|
|
2997
|
+
configPath: hook.hooksLocation,
|
|
2998
|
+
hostIde: def.hostIde
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
return monitoredAgents;
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Check if a specific agent can be monitored by Overwatch
|
|
3007
|
+
*/
|
|
3008
|
+
isMonitorable(agentId) {
|
|
3009
|
+
return MONITORABLE_AGENTS.has(agentId);
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Clear cached detection results
|
|
3013
|
+
*/
|
|
3014
|
+
clearCache() {
|
|
3015
|
+
this.cachedMetrics = null;
|
|
3016
|
+
this.cacheTimestamp = 0;
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Generate a coverage report ready for the Admin API
|
|
3020
|
+
* Handles the transformation from internal CoverageMetrics to API CoverageReport
|
|
3021
|
+
* @param machineId - Machine identifier (generated once at module init)
|
|
3022
|
+
*/
|
|
3023
|
+
async generateCoverageReport(machineId) {
|
|
3024
|
+
const metrics = await this.detectAll();
|
|
3025
|
+
const installedAgents = metrics.agents.filter((a) => a.installed);
|
|
3026
|
+
return {
|
|
3027
|
+
machine_id: machineId,
|
|
3028
|
+
platform: metrics.platform,
|
|
3029
|
+
agents: installedAgents.map((agent) => ({
|
|
3030
|
+
id: agent.id,
|
|
3031
|
+
name: agent.name,
|
|
3032
|
+
category: agent.category,
|
|
3033
|
+
installed: agent.installed,
|
|
3034
|
+
monitored: agent.monitored,
|
|
3035
|
+
config_path: agent.configPath,
|
|
3036
|
+
host_ide: agent.hostIde
|
|
3037
|
+
})),
|
|
3038
|
+
installed_count: metrics.installedCount,
|
|
3039
|
+
monitored_count: metrics.monitoredCount,
|
|
3040
|
+
coverage_percentage: metrics.percentage,
|
|
3041
|
+
timestamp: metrics.timestamp
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Detect a single agent based on its definition
|
|
3046
|
+
*/
|
|
3047
|
+
detectAgent(def) {
|
|
3048
|
+
let installed = false;
|
|
3049
|
+
let configPath;
|
|
3050
|
+
const platformPaths = def.paths[PLATFORM] || [];
|
|
3051
|
+
for (const p of platformPaths) {
|
|
3052
|
+
if (this.checkPathExists(p)) {
|
|
3053
|
+
installed = true;
|
|
3054
|
+
configPath = p;
|
|
3055
|
+
break;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
if (!installed && def.vscodeExtension) {
|
|
3059
|
+
const extensionPath = this.findVSCodeExtension(def.vscodeExtension);
|
|
3060
|
+
if (extensionPath) {
|
|
3061
|
+
installed = true;
|
|
3062
|
+
configPath = extensionPath;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
if (!installed && def.jetbrainsPlugin) {
|
|
3066
|
+
const pluginPath = this.findJetBrainsPlugin(def.jetbrainsPlugin);
|
|
3067
|
+
if (pluginPath) {
|
|
3068
|
+
installed = true;
|
|
3069
|
+
configPath = pluginPath;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return {
|
|
3073
|
+
id: def.id,
|
|
3074
|
+
name: def.name,
|
|
3075
|
+
category: def.category,
|
|
3076
|
+
installed,
|
|
3077
|
+
monitored: false,
|
|
3078
|
+
// Will be updated by detectAll()
|
|
3079
|
+
configPath,
|
|
3080
|
+
hostIde: def.hostIde
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Check if a path exists (file or directory)
|
|
3085
|
+
*/
|
|
3086
|
+
checkPathExists(p) {
|
|
3087
|
+
try {
|
|
3088
|
+
return fs12.existsSync(p);
|
|
3089
|
+
} catch {
|
|
3090
|
+
return false;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* Find a VS Code extension by ID pattern
|
|
3095
|
+
*/
|
|
3096
|
+
findVSCodeExtension(extensionId) {
|
|
3097
|
+
const extensionsDir = VSCODE_EXTENSIONS[PLATFORM];
|
|
3098
|
+
if (!extensionsDir || !this.checkPathExists(extensionsDir)) {
|
|
3099
|
+
return void 0;
|
|
3100
|
+
}
|
|
3101
|
+
try {
|
|
3102
|
+
const entries = fs12.readdirSync(extensionsDir);
|
|
3103
|
+
const target = extensionId.toLowerCase();
|
|
3104
|
+
for (const entry of entries) {
|
|
3105
|
+
const lower = entry.toLowerCase();
|
|
3106
|
+
const versionTail = lower.startsWith(target + "-") ? lower.slice(target.length + 1) : null;
|
|
3107
|
+
if (lower === target || versionTail !== null && /^\d/.test(versionTail)) {
|
|
3108
|
+
return path12.join(extensionsDir, entry);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
return void 0;
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Find a JetBrains plugin by ID
|
|
3117
|
+
*/
|
|
3118
|
+
findJetBrainsPlugin(pluginId) {
|
|
3119
|
+
const jetbrainsDir = JETBRAINS_CONFIG[PLATFORM];
|
|
3120
|
+
if (!jetbrainsDir || !this.checkPathExists(jetbrainsDir)) {
|
|
3121
|
+
return void 0;
|
|
3122
|
+
}
|
|
3123
|
+
try {
|
|
3124
|
+
const entries = fs12.readdirSync(jetbrainsDir);
|
|
3125
|
+
for (const entry of entries) {
|
|
3126
|
+
const pluginsDir = path12.join(jetbrainsDir, entry, "plugins");
|
|
3127
|
+
if (this.checkPathExists(pluginsDir)) {
|
|
3128
|
+
const plugins = fs12.readdirSync(pluginsDir);
|
|
3129
|
+
for (const plugin of plugins) {
|
|
3130
|
+
if (plugin.toLowerCase().includes(pluginId.toLowerCase())) {
|
|
3131
|
+
return path12.join(pluginsDir, plugin);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
} catch {
|
|
3137
|
+
}
|
|
3138
|
+
return void 0;
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
var agentDetector = new AgentDetector();
|
|
3142
|
+
|
|
3143
|
+
// src/coverage/machine-id.ts
|
|
3144
|
+
var fs13 = __toESM(require("fs"));
|
|
3145
|
+
var os12 = __toESM(require("os"));
|
|
3146
|
+
var crypto4 = __toESM(require("crypto"));
|
|
3147
|
+
function generateMachineId() {
|
|
3148
|
+
const platformId = getPlatformMachineId();
|
|
3149
|
+
if (platformId) {
|
|
3150
|
+
return crypto4.createHash("sha256").update(platformId).digest("hex").substring(0, 32);
|
|
3151
|
+
}
|
|
3152
|
+
const components = [
|
|
3153
|
+
os12.hostname(),
|
|
3154
|
+
os12.platform(),
|
|
3155
|
+
os12.arch(),
|
|
3156
|
+
os12.userInfo().username || "unknown",
|
|
3157
|
+
os12.homedir()
|
|
3158
|
+
];
|
|
3159
|
+
const combined = components.join("|");
|
|
3160
|
+
return crypto4.createHash("sha256").update(combined).digest("hex").substring(0, 32);
|
|
3161
|
+
}
|
|
3162
|
+
function getPlatformMachineId() {
|
|
3163
|
+
try {
|
|
3164
|
+
switch (os12.platform()) {
|
|
3165
|
+
case "darwin":
|
|
3166
|
+
return getMacOSMachineId();
|
|
3167
|
+
case "linux":
|
|
3168
|
+
return getLinuxMachineId();
|
|
3169
|
+
case "win32":
|
|
3170
|
+
return getWindowsMachineId();
|
|
3171
|
+
default:
|
|
3172
|
+
return null;
|
|
3173
|
+
}
|
|
3174
|
+
} catch {
|
|
3175
|
+
return null;
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
function getMacOSMachineId() {
|
|
3179
|
+
try {
|
|
3180
|
+
const { execSync } = require("child_process");
|
|
3181
|
+
const output = execSync(
|
|
3182
|
+
"ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID",
|
|
3183
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
3184
|
+
);
|
|
3185
|
+
const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
3186
|
+
return match ? match[1] : null;
|
|
3187
|
+
} catch {
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
function getLinuxMachineId() {
|
|
3192
|
+
try {
|
|
3193
|
+
const machineIdPath = "/etc/machine-id";
|
|
3194
|
+
if (fs13.existsSync(machineIdPath)) {
|
|
3195
|
+
return fs13.readFileSync(machineIdPath, "utf-8").trim();
|
|
3196
|
+
}
|
|
3197
|
+
const dbusPath = "/var/lib/dbus/machine-id";
|
|
3198
|
+
if (fs13.existsSync(dbusPath)) {
|
|
3199
|
+
return fs13.readFileSync(dbusPath, "utf-8").trim();
|
|
3200
|
+
}
|
|
3201
|
+
return null;
|
|
3202
|
+
} catch {
|
|
3203
|
+
return null;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
function getWindowsMachineId() {
|
|
3207
|
+
try {
|
|
3208
|
+
const { execSync } = require("child_process");
|
|
3209
|
+
const output = execSync(
|
|
3210
|
+
'reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
|
|
3211
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
3212
|
+
);
|
|
3213
|
+
const match = output.match(/MachineGuid\s+REG_SZ\s+([^\s\r\n]+)/);
|
|
3214
|
+
return match ? match[1] : null;
|
|
3215
|
+
} catch {
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
// src/module.ts
|
|
3221
|
+
var GuardianModule = class _GuardianModule {
|
|
3222
|
+
constructor(config = {}) {
|
|
3223
|
+
this.projectSkillsCache = /* @__PURE__ */ new Map();
|
|
3224
|
+
this.machineId = "";
|
|
3225
|
+
this.coverageInterval = null;
|
|
3226
|
+
// 10 minutes
|
|
3227
|
+
// Ingestors (scan results + skill scan results to admin API)
|
|
3228
|
+
this.scanIngestor = null;
|
|
3229
|
+
this.skillsIngestor = null;
|
|
3230
|
+
// Admin client (for installation tracking + coverage reporting)
|
|
3231
|
+
this.adminClient = null;
|
|
3232
|
+
// Tracks (user, IDE) installation records that have already been
|
|
3233
|
+
// sent to the admin API, with success-only caching and in-flight
|
|
3234
|
+
// dedup. See InstallationRecorder for the rationale.
|
|
3235
|
+
this.installationRecorder = new InstallationRecorder(
|
|
3236
|
+
(source, email) => this.recordInstallation(source, email)
|
|
3237
|
+
);
|
|
3238
|
+
this.pendingOAuthStates = /* @__PURE__ */ new Map();
|
|
3239
|
+
this.oauthStateTimeout = 5 * 60 * 1e3;
|
|
3240
|
+
this.initialized = false;
|
|
3241
|
+
this.debug = config.debug || false;
|
|
3242
|
+
this.configReader = new ConfigReader();
|
|
3243
|
+
this.scanner = new MCPScanner();
|
|
3244
|
+
this.skillsScanner = new SkillsScanner();
|
|
3245
|
+
this.agentDetector = new AgentDetector();
|
|
3246
|
+
if (this.debug)
|
|
3247
|
+
logger.setLevel(0 /* DEBUG */);
|
|
3248
|
+
this.httpServer = new GuardianHttpServer(config.httpPort || 0);
|
|
3249
|
+
}
|
|
3250
|
+
static {
|
|
3251
|
+
this.PROJECT_SKILLS_CACHE_TTL = 5 * 60 * 1e3;
|
|
3252
|
+
}
|
|
3253
|
+
static {
|
|
3254
|
+
// Cadence at which the daemon reports coverage metrics to the admin
|
|
3255
|
+
// API. 10 minutes = 144 calls/day/machine; if backend cost becomes a
|
|
3256
|
+
// concern, raise this to 1h or 24h. Keep the comments at module.ts
|
|
3257
|
+
// line 140 and javelin/admin-client.ts (reportCoverageMetrics) in
|
|
3258
|
+
// sync with this value.
|
|
3259
|
+
this.COVERAGE_INTERVAL_MS = 10 * 60 * 1e3;
|
|
3260
|
+
}
|
|
3261
|
+
async init() {
|
|
3262
|
+
if (this.initialized)
|
|
3263
|
+
return;
|
|
3264
|
+
logger.info("Initializing...");
|
|
3265
|
+
this.machineId = generateMachineId();
|
|
3266
|
+
logger.debug("Generated machine ID", { machineId: this.machineId });
|
|
3267
|
+
await this.initAdminClient();
|
|
3268
|
+
const highflameConfig = this.configReader.getHighflameConfig();
|
|
3269
|
+
if (highflameConfig.enabled) {
|
|
3270
|
+
const ingestorTokenGetter = () => this.configReader.getJwtToken();
|
|
3271
|
+
this.scanIngestor = new AdminIngestor(
|
|
3272
|
+
highflameConfig.baseUrl,
|
|
3273
|
+
ingestorTokenGetter,
|
|
3274
|
+
"mcp-scans"
|
|
3275
|
+
);
|
|
3276
|
+
this.scanIngestor.start();
|
|
3277
|
+
this.skillsIngestor = new AdminIngestor(
|
|
3278
|
+
highflameConfig.baseUrl,
|
|
3279
|
+
ingestorTokenGetter,
|
|
3280
|
+
"skills"
|
|
3281
|
+
);
|
|
3282
|
+
this.skillsIngestor.start();
|
|
3283
|
+
}
|
|
3284
|
+
const tokenGetter = () => this.configReader.getJwtToken();
|
|
3285
|
+
this.cerberusClient = new CerberusClient(
|
|
3286
|
+
getCerberusUrl(),
|
|
3287
|
+
tokenGetter,
|
|
3288
|
+
this.machineId
|
|
3289
|
+
);
|
|
3290
|
+
this.hookPipeline = new HookPipeline(
|
|
3291
|
+
this.cerberusClient,
|
|
3292
|
+
(source) => this.checkInstallation(source)
|
|
3293
|
+
);
|
|
3294
|
+
this.hookHandler = new HookHandler(this.hookPipeline);
|
|
3295
|
+
this.hookHandler.setProjectSkillsScanCallback((workspace) => {
|
|
3296
|
+
this.scanProjectSkillsIfNeeded(workspace).catch(() => {
|
|
3297
|
+
});
|
|
3298
|
+
});
|
|
3299
|
+
this.scanHandler = new ScanHandler(this.scanner, this.scanIngestor);
|
|
3300
|
+
this.oauthHandler = new OAuthHandler(
|
|
3301
|
+
this.httpServer.getPort(),
|
|
3302
|
+
this.pendingOAuthStates,
|
|
3303
|
+
this.oauthStateTimeout
|
|
3304
|
+
);
|
|
3305
|
+
this.initialized = true;
|
|
3306
|
+
logger.info("Initialized");
|
|
3307
|
+
this.startCoverageReporting();
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Start coverage reporting - runs on startup and every 10 minutes
|
|
3311
|
+
*/
|
|
3312
|
+
startCoverageReporting() {
|
|
3313
|
+
this.reportCoverage().catch((error) => {
|
|
3314
|
+
logger.error("Failed to report initial coverage", { error: String(error) });
|
|
3315
|
+
});
|
|
3316
|
+
this.coverageInterval = setInterval(
|
|
3317
|
+
() => this.reportCoverage().catch((error) => {
|
|
3318
|
+
logger.error("Failed to report periodic coverage", { error: String(error) });
|
|
3319
|
+
}),
|
|
3320
|
+
_GuardianModule.COVERAGE_INTERVAL_MS
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Report coverage metrics to admin API
|
|
3325
|
+
*/
|
|
3326
|
+
async reportCoverage() {
|
|
3327
|
+
if (!this.adminClient) {
|
|
3328
|
+
logger.debug("Coverage reporting skipped - no admin client");
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const report = await this.agentDetector.generateCoverageReport(this.machineId);
|
|
3332
|
+
const success = await this.adminClient.reportCoverageMetrics(report);
|
|
3333
|
+
if (success) {
|
|
3334
|
+
logger.info("Coverage report sent successfully", {
|
|
3335
|
+
installed: report.installed_count,
|
|
3336
|
+
monitored: report.monitored_count,
|
|
3337
|
+
percentage: report.coverage_percentage
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Initialize admin client for installation tracking and coverage reporting.
|
|
3343
|
+
* Extracted from the removed initPolicyManager().
|
|
3344
|
+
*/
|
|
3345
|
+
async initAdminClient() {
|
|
3346
|
+
const adminConfig = this.configReader.getAdminConfig();
|
|
3347
|
+
const tokenGetter = () => this.configReader.getAdminToken();
|
|
3348
|
+
if (adminConfig.enabled && adminConfig.baseUrl) {
|
|
3349
|
+
this.adminClient = new AdminClient(adminConfig.baseUrl, tokenGetter);
|
|
3350
|
+
logger.info("Admin client initialized", { baseUrl: adminConfig.baseUrl });
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
checkInstallation(source) {
|
|
3354
|
+
const authStatus = getAuthStatus();
|
|
3355
|
+
if (!authStatus.authenticated || !authStatus.email) {
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
this.installationRecorder.tryRecord(source, authStatus.email).catch(() => {
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
3361
|
+
async recordInstallation(source, userEmail) {
|
|
3362
|
+
if (!this.adminClient)
|
|
3363
|
+
return false;
|
|
3364
|
+
try {
|
|
3365
|
+
return await this.adminClient.recordInstallation({
|
|
3366
|
+
user_id: userEmail,
|
|
3367
|
+
user_email: userEmail,
|
|
3368
|
+
ide_type: source,
|
|
3369
|
+
version: VERSION,
|
|
3370
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3371
|
+
});
|
|
3372
|
+
} catch (error) {
|
|
3373
|
+
logger.error("Failed to record installation", { error: String(error) });
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
async startServer() {
|
|
3378
|
+
if (!this.initialized)
|
|
3379
|
+
await this.init();
|
|
3380
|
+
this.httpServer.on(
|
|
3381
|
+
"/hook",
|
|
3382
|
+
(req, res) => this.hookHandler?.handle(req, res)
|
|
3383
|
+
);
|
|
3384
|
+
this.httpServer.on("/health", async (_req, res) => {
|
|
3385
|
+
const authStatus = getAuthStatus();
|
|
3386
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3387
|
+
res.end(
|
|
3388
|
+
JSON.stringify({
|
|
3389
|
+
status: "ok",
|
|
3390
|
+
version: VERSION,
|
|
3391
|
+
initialized: this.initialized,
|
|
3392
|
+
eventCount: this.hookHandler?.getEventCount() || 0,
|
|
3393
|
+
auth: {
|
|
3394
|
+
authenticated: authStatus.authenticated,
|
|
3395
|
+
expired: authStatus.expired || false,
|
|
3396
|
+
expiringSoon: authStatus.expiringSoon || false,
|
|
3397
|
+
email: authStatus.email
|
|
3398
|
+
}
|
|
3399
|
+
})
|
|
3400
|
+
);
|
|
3401
|
+
});
|
|
3402
|
+
this.httpServer.on("/coverage", async (_req, res) => {
|
|
3403
|
+
try {
|
|
3404
|
+
const metrics = await this.agentDetector.detectAll();
|
|
3405
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3406
|
+
res.end(JSON.stringify(metrics));
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3409
|
+
res.end(JSON.stringify({ error: "Failed to detect agents" }));
|
|
3410
|
+
}
|
|
3411
|
+
});
|
|
3412
|
+
this.httpServer.on(
|
|
3413
|
+
"/scan",
|
|
3414
|
+
(req, res) => this.scanHandler?.handle(req, res)
|
|
3415
|
+
);
|
|
3416
|
+
this.httpServer.on(
|
|
3417
|
+
"/oauth/start",
|
|
3418
|
+
(req, res) => this.oauthHandler?.handleStart(req, res)
|
|
3419
|
+
);
|
|
3420
|
+
this.httpServer.on(
|
|
3421
|
+
"/oauth/callback",
|
|
3422
|
+
(req, res) => this.oauthHandler?.handleCallback(req, res)
|
|
3423
|
+
);
|
|
3424
|
+
this.httpServer.on(
|
|
3425
|
+
"/oauth/status",
|
|
3426
|
+
(req, res) => this.oauthHandler?.handleStatus(req, res)
|
|
3427
|
+
);
|
|
3428
|
+
this.httpServer.on(
|
|
3429
|
+
"/shutdown",
|
|
3430
|
+
(req, res) => this.handleShutdown(req, res)
|
|
3431
|
+
);
|
|
3432
|
+
await this.httpServer.start();
|
|
3433
|
+
logger.info(`Server started on port ${this.httpServer.getPort()}`);
|
|
3434
|
+
if (this.scanHandler)
|
|
3435
|
+
this.scanHandler.runInitialScan();
|
|
3436
|
+
this.runStartupSkillsScan();
|
|
3437
|
+
}
|
|
3438
|
+
async runStartupSkillsScan() {
|
|
3439
|
+
if (!this.skillsIngestor) {
|
|
3440
|
+
logger.debug("Skipping startup skills scan: no ingestor configured");
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
try {
|
|
3444
|
+
logger.info("Running startup skills scan...");
|
|
3445
|
+
const result = await this.skillsScanner.scan();
|
|
3446
|
+
if (this.skillsIngestor) {
|
|
3447
|
+
this.skillsIngestor.ingest(result);
|
|
3448
|
+
}
|
|
3449
|
+
logger.info("Startup skills scan completed", {
|
|
3450
|
+
totalSkills: result.totalSkills
|
|
3451
|
+
});
|
|
3452
|
+
} catch (error) {
|
|
3453
|
+
logger.error("Startup skills scan failed", { error: String(error) });
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
handleShutdown(req, res) {
|
|
3457
|
+
if (req.method !== "POST") {
|
|
3458
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
3459
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3463
|
+
res.end(JSON.stringify({ status: "shutting_down" }), () => {
|
|
3464
|
+
logger.info("Shutdown requested via API, sending SIGTERM...");
|
|
3465
|
+
process.kill(process.pid, "SIGTERM");
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
async stopServer() {
|
|
3469
|
+
if (this.coverageInterval) {
|
|
3470
|
+
clearInterval(this.coverageInterval);
|
|
3471
|
+
this.coverageInterval = null;
|
|
3472
|
+
}
|
|
3473
|
+
this.scanIngestor?.stop();
|
|
3474
|
+
this.skillsIngestor?.stop();
|
|
3475
|
+
await this.httpServer.stop();
|
|
3476
|
+
}
|
|
3477
|
+
getPort() {
|
|
3478
|
+
return this.httpServer.getPort();
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* Scan project-level skills if not recently cached
|
|
3482
|
+
* Called from hook handler when workspace is present in event
|
|
3483
|
+
*/
|
|
3484
|
+
async scanProjectSkillsIfNeeded(workspace) {
|
|
3485
|
+
const cached = this.projectSkillsCache.get(workspace);
|
|
3486
|
+
const now = Date.now();
|
|
3487
|
+
if (cached && now - cached.timestamp < _GuardianModule.PROJECT_SKILLS_CACHE_TTL) {
|
|
3488
|
+
logger.debug("Project skills cache hit", { workspace });
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
try {
|
|
3492
|
+
const result = await this.skillsScanner.scanProjectSkills(workspace);
|
|
3493
|
+
this.projectSkillsCache.set(workspace, { timestamp: now });
|
|
3494
|
+
if (result.totalSkills > 0 && this.skillsIngestor) {
|
|
3495
|
+
this.skillsIngestor.ingest(result);
|
|
3496
|
+
}
|
|
3497
|
+
} catch (error) {
|
|
3498
|
+
logger.error("Failed to scan project skills", {
|
|
3499
|
+
workspace,
|
|
3500
|
+
error: String(error)
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
|
|
3506
|
+
// src/daemon.ts
|
|
3507
|
+
function parseArgs() {
|
|
3508
|
+
const args = process.argv.slice(2);
|
|
3509
|
+
let debug = false;
|
|
3510
|
+
for (const arg of args) {
|
|
3511
|
+
if (arg === "--debug") {
|
|
3512
|
+
debug = true;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
return { debug };
|
|
3516
|
+
}
|
|
3517
|
+
async function main() {
|
|
3518
|
+
const { debug } = parseArgs();
|
|
3519
|
+
const portFile = path13.join(os13.homedir(), ".overwatch", "guardian_port");
|
|
3520
|
+
console.log("[Guardian] Starting daemon...");
|
|
3521
|
+
console.log(`[Guardian] Port: ${GUARDIAN_PORT}`);
|
|
3522
|
+
console.log(`[Guardian] Debug: ${debug}`);
|
|
3523
|
+
const portDir = path13.dirname(portFile);
|
|
3524
|
+
if (!fs14.existsSync(portDir)) {
|
|
3525
|
+
fs14.mkdirSync(portDir, { recursive: true });
|
|
3526
|
+
}
|
|
3527
|
+
const port = GUARDIAN_PORT;
|
|
3528
|
+
const guardian = new GuardianModule({
|
|
3529
|
+
httpPort: port,
|
|
3530
|
+
debug
|
|
3531
|
+
});
|
|
3532
|
+
try {
|
|
3533
|
+
await guardian.startServer();
|
|
3534
|
+
} catch (err) {
|
|
3535
|
+
const code = err?.code;
|
|
3536
|
+
if (code === "EADDRINUSE") {
|
|
3537
|
+
console.error(
|
|
3538
|
+
`[Guardian] Port ${port} is already in use. Another instance is likely running; exiting cleanly.`
|
|
3539
|
+
);
|
|
3540
|
+
process.exit(0);
|
|
3541
|
+
}
|
|
3542
|
+
throw err;
|
|
3543
|
+
}
|
|
3544
|
+
fs14.writeFileSync(portFile, String(port));
|
|
3545
|
+
PortManager.writePidFile();
|
|
3546
|
+
console.log(`[Guardian] Server started on port ${port}`);
|
|
3547
|
+
console.log(`[Guardian] Port written to ${portFile}`);
|
|
3548
|
+
const shutdown = async (signal) => {
|
|
3549
|
+
console.log(`[Guardian] Received ${signal}, shutting down...`);
|
|
3550
|
+
try {
|
|
3551
|
+
await guardian.stopServer();
|
|
3552
|
+
if (fs14.existsSync(portFile)) {
|
|
3553
|
+
fs14.unlinkSync(portFile);
|
|
3554
|
+
}
|
|
3555
|
+
PortManager.removePidFile();
|
|
3556
|
+
console.log("[Guardian] Shutdown complete");
|
|
3557
|
+
process.exit(0);
|
|
3558
|
+
} catch (error) {
|
|
3559
|
+
console.error("[Guardian] Error during shutdown:", error);
|
|
3560
|
+
process.exit(1);
|
|
3561
|
+
}
|
|
3562
|
+
};
|
|
3563
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
3564
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
3565
|
+
process.on("uncaughtException", (error) => {
|
|
3566
|
+
console.error("[Guardian] Uncaught exception:", error);
|
|
3567
|
+
process.exit(1);
|
|
3568
|
+
});
|
|
3569
|
+
process.on("unhandledRejection", (reason) => {
|
|
3570
|
+
console.error("[Guardian] Unhandled rejection:", reason);
|
|
3571
|
+
process.exit(1);
|
|
3572
|
+
});
|
|
3573
|
+
console.log("[Guardian] Daemon running. Press Ctrl+C to stop.");
|
|
3574
|
+
}
|
|
3575
|
+
main().catch((error) => {
|
|
3576
|
+
console.error("[Guardian] Failed to start:", error);
|
|
3577
|
+
process.exit(1);
|
|
3578
|
+
});
|