fullcourtdefense-cli 1.15.5 → 1.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/daemon.js +141 -20
- package/dist/commands/discover.js +4 -0
- package/dist/commands/mcpGateway.js +24 -4
- package/dist/discoveryMarker.d.ts +2 -0
- package/dist/discoveryMarker.js +64 -0
- package/dist/machineIdentity.js +8 -2
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -47,6 +47,7 @@ const telemetry_1 = require("../telemetry");
|
|
|
47
47
|
const notify_1 = require("../notify");
|
|
48
48
|
const integrity_1 = require("../integrity");
|
|
49
49
|
const machineIdentity_1 = require("../machineIdentity");
|
|
50
|
+
const discoveryMarker_1 = require("../discoveryMarker");
|
|
50
51
|
const COLOR = {
|
|
51
52
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
52
53
|
red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
@@ -69,6 +70,8 @@ const RESCAN_INTERVAL_MS = envMs('FCD_DAEMON_RESCAN_MS', 5 * 60_000);
|
|
|
69
70
|
const HEARTBEAT_INTERVAL_MS = envMs('FCD_DAEMON_HEARTBEAT_MS', 5 * 60_000);
|
|
70
71
|
/** Bundle (mode / suspension / policy version) poll cadence. */
|
|
71
72
|
const BUNDLE_POLL_MS = envMs('FCD_DAEMON_BUNDLE_POLL_MS', 60_000);
|
|
73
|
+
/** Delay before the one-time initial discovery sweep on a fresh machine. */
|
|
74
|
+
const INITIAL_DISCOVER_DELAY_MS = envMs('FCD_DAEMON_INITIAL_DISCOVER_MS', 2 * 60_000);
|
|
72
75
|
/** Rotate the daemon log when it grows past this size. */
|
|
73
76
|
const LOG_MAX_BYTES = 1_000_000;
|
|
74
77
|
function daemonDir() {
|
|
@@ -129,6 +132,21 @@ function stopPid(pid) {
|
|
|
129
132
|
function logFile() {
|
|
130
133
|
return path.join(daemonDir(), 'daemon.log');
|
|
131
134
|
}
|
|
135
|
+
/** Spawn `discover --upload --surface all --silent` and wait for it to finish. */
|
|
136
|
+
function runDiscoverSweep(timeoutMs = 300_000) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const child = (0, child_process_1.spawn)(process.execPath, [cliEntry(), 'discover', '--upload', '--surface', 'all', '--silent'], {
|
|
139
|
+
windowsHide: true,
|
|
140
|
+
stdio: 'ignore',
|
|
141
|
+
});
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
child.kill();
|
|
144
|
+
reject(new Error(`Discovery command timed out after ${Math.round(timeoutMs / 60_000)} minutes`));
|
|
145
|
+
}, timeoutMs);
|
|
146
|
+
child.on('error', error => { clearTimeout(timer); reject(error); });
|
|
147
|
+
child.on('close', code => { clearTimeout(timer); resolve(code ?? 1); });
|
|
148
|
+
});
|
|
149
|
+
}
|
|
132
150
|
function cliEntry() {
|
|
133
151
|
return path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
|
|
134
152
|
}
|
|
@@ -193,8 +211,12 @@ function acquirePidLock() {
|
|
|
193
211
|
// No meta = pre-1.15.4 build (never wrote one) → treated as older.
|
|
194
212
|
if (compareVersions(cliVersion(), runningVersion) > 0) {
|
|
195
213
|
log(`Superseding older daemon (pid ${existing}, version ${runningVersion || 'unknown'}) with ${cliVersion() || 'this build'}.`);
|
|
196
|
-
if (!stopPid(existing))
|
|
197
|
-
|
|
214
|
+
if (!stopPid(existing)) {
|
|
215
|
+
// Elevated daemons (e.g. spawned by an elevated MSI install) cannot be
|
|
216
|
+
// stopped from a normal-user CLI — surface the real reason.
|
|
217
|
+
log(`Could not stop daemon pid ${existing} — it may be running elevated. Run this command from an elevated terminal, or reboot so the logon autostart takes over.`);
|
|
218
|
+
return false; // yield rather than double-run
|
|
219
|
+
}
|
|
198
220
|
}
|
|
199
221
|
else {
|
|
200
222
|
return false;
|
|
@@ -266,6 +288,7 @@ async function runDaemon(args, config) {
|
|
|
266
288
|
await (0, mcpGateway_1.protectAllCommand)({ ...args, dryRun: undefined }, config);
|
|
267
289
|
quietUntil = Date.now() + SELF_WRITE_QUIET_MS;
|
|
268
290
|
log('Re-protection pass complete.');
|
|
291
|
+
await uploadLogTail();
|
|
269
292
|
if (!quiet) {
|
|
270
293
|
(0, notify_1.notifyOs)({
|
|
271
294
|
title: 'FullCourtDefense re-protected this machine',
|
|
@@ -329,6 +352,28 @@ async function runDaemon(args, config) {
|
|
|
329
352
|
}
|
|
330
353
|
return targets.length;
|
|
331
354
|
};
|
|
355
|
+
// Ship the REAL daemon log tail to the control plane so the dashboard's
|
|
356
|
+
// machine page can show live progress (protect-all passes, cache clears,
|
|
357
|
+
// discovery sweeps). Best-effort — never blocks or fails the daemon.
|
|
358
|
+
const uploadLogTail = async () => {
|
|
359
|
+
if (!creds.shieldId)
|
|
360
|
+
return;
|
|
361
|
+
try {
|
|
362
|
+
const raw = fs.readFileSync(logFile(), 'utf8');
|
|
363
|
+
const lines = raw.split(/\r?\n/).filter(line => line.trim()).slice(-80);
|
|
364
|
+
const identity = (0, machineIdentity_1.getMachineIdentity)();
|
|
365
|
+
await fetch(`${creds.apiUrl}/api/cli/machines/log`, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: {
|
|
368
|
+
'Content-Type': 'application/json',
|
|
369
|
+
...(creds.shieldKey ? { 'x-shield-key': creds.shieldKey } : {}),
|
|
370
|
+
},
|
|
371
|
+
body: JSON.stringify({ shieldId: creds.shieldId, machineId: identity.machineId, lines }),
|
|
372
|
+
signal: AbortSignal.timeout(8_000),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch { /* log shipping is best-effort */ }
|
|
376
|
+
};
|
|
332
377
|
const reportMachineAction = async (actionId, status, detail) => {
|
|
333
378
|
if (!creds.shieldId)
|
|
334
379
|
return;
|
|
@@ -359,10 +404,16 @@ async function runDaemon(args, config) {
|
|
|
359
404
|
executingActionIds.add(action.id);
|
|
360
405
|
await reportMachineAction(action.id, 'running');
|
|
361
406
|
log(`Remote action started: ${action.type} (${action.id}).`);
|
|
407
|
+
await uploadLogTail();
|
|
362
408
|
try {
|
|
363
409
|
let resultSummary = '';
|
|
364
410
|
if (action.type === 'health_check') {
|
|
411
|
+
log('Health check: verifying daemon + protection surfaces…');
|
|
412
|
+
await uploadLogTail();
|
|
365
413
|
const integrity = (0, integrity_1.getLocalIntegrityReport)({ requireDaemon: true });
|
|
414
|
+
log(integrity.ok
|
|
415
|
+
? `Health check: all protection points healthy (${integrity.protectedMcpConfigs}/${integrity.discoveredMcpConfigs} MCP configs wrapped).`
|
|
416
|
+
: `Health check: issues found — ${integrity.reasons.join(', ')}.`);
|
|
366
417
|
resultSummary = integrity.ok
|
|
367
418
|
? 'Daemon and required AgentGuard protection points are healthy.'
|
|
368
419
|
: `Health check found: ${integrity.reasons.join(', ')}`;
|
|
@@ -374,21 +425,28 @@ async function runDaemon(args, config) {
|
|
|
374
425
|
throw new Error('Shield not configured on this machine.');
|
|
375
426
|
const shieldId = creds.shieldId;
|
|
376
427
|
const identity = (0, machineIdentity_1.getMachineIdentity)();
|
|
377
|
-
|
|
428
|
+
log('Policy refresh: clearing Local Safety snapshot cache…');
|
|
429
|
+
(0, localSafetySnapshot_1.clearLocalSafetySnapshotCache)({
|
|
378
430
|
apiUrl: creds.apiUrl,
|
|
379
431
|
shieldId,
|
|
380
|
-
shieldKey: creds.shieldKey,
|
|
381
432
|
developerName: identity.developerName,
|
|
382
433
|
machineName: identity.hostname,
|
|
383
|
-
force: true,
|
|
384
|
-
ttlMs: 0,
|
|
385
434
|
});
|
|
386
|
-
(
|
|
435
|
+
await uploadLogTail();
|
|
436
|
+
log('Policy refresh: pulling latest policy bundle from control plane…');
|
|
437
|
+
const bundle = await (0, runtimeConfig_1.getRuntimeBundle)({
|
|
387
438
|
apiUrl: creds.apiUrl,
|
|
388
439
|
shieldId,
|
|
440
|
+
shieldKey: creds.shieldKey,
|
|
389
441
|
developerName: identity.developerName,
|
|
390
442
|
machineName: identity.hostname,
|
|
443
|
+
machineId: identity.machineId,
|
|
444
|
+
force: true,
|
|
445
|
+
ttlMs: 0,
|
|
391
446
|
});
|
|
447
|
+
log(bundle.policyHash
|
|
448
|
+
? `Policy refresh: bundle applied (hash ${bundle.policyHash.slice(0, 12)}…, ${bundle.policyCount ?? 0} policies).`
|
|
449
|
+
: 'Policy refresh: bundle applied from control plane.');
|
|
392
450
|
resultSummary = bundle.policyHash
|
|
393
451
|
? `Policy bundle refreshed (hash ${bundle.policyHash.slice(0, 12)}…).`
|
|
394
452
|
: 'Policy bundle refreshed from the control plane.';
|
|
@@ -396,25 +454,30 @@ async function runDaemon(args, config) {
|
|
|
396
454
|
else if (action.type === 'repair_protection') {
|
|
397
455
|
if (suspended)
|
|
398
456
|
throw new Error('Machine is suspended; resume it before repairing protection.');
|
|
457
|
+
log('Repair protection: running protect-all (IDE hooks + MCP gateways)…');
|
|
458
|
+
await uploadLogTail();
|
|
399
459
|
await (0, mcpGateway_1.protectAllCommand)({ ...args, dryRun: undefined }, config);
|
|
460
|
+
log('Repair protection: verifying hooks, gateways and daemon integrity…');
|
|
400
461
|
const verification = (0, integrity_1.getLocalIntegrityReport)({ requireDaemon: true });
|
|
401
462
|
if (!verification.ok) {
|
|
402
463
|
throw new Error(`Repair completed but verification still reports: ${verification.reasons.join(', ')}`);
|
|
403
464
|
}
|
|
465
|
+
log(`Repair protection: verified (${verification.protectedMcpConfigs}/${verification.discoveredMcpConfigs} MCP configs wrapped).`);
|
|
404
466
|
resultSummary = 'AgentGuard hooks, gateways, and protection configuration were repaired and verified.';
|
|
405
467
|
}
|
|
406
468
|
else if (action.type === 'discovery_scan') {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
});
|
|
415
|
-
if (result.status !== 0) {
|
|
416
|
-
throw new Error((result.stderr || result.stdout || 'Discovery command failed').trim().slice(0, 500));
|
|
469
|
+
log('Discovery scan: starting full surface sweep (MCP + secrets + agent files + posture)…');
|
|
470
|
+
await uploadLogTail();
|
|
471
|
+
const logPump = setInterval(() => { void uploadLogTail(); }, 15_000);
|
|
472
|
+
try {
|
|
473
|
+
const exitCode = await runDiscoverSweep();
|
|
474
|
+
if (exitCode !== 0)
|
|
475
|
+
throw new Error(`Discovery command failed with exit code ${exitCode}`);
|
|
417
476
|
}
|
|
477
|
+
finally {
|
|
478
|
+
clearInterval(logPump);
|
|
479
|
+
}
|
|
480
|
+
log('Discovery scan: upload complete — dashboard discovery + posture timestamps will refresh.');
|
|
418
481
|
resultSummary = 'Discovery + posture scan completed and uploaded.';
|
|
419
482
|
}
|
|
420
483
|
await reportMachineAction(action.id, 'succeeded', { resultSummary });
|
|
@@ -425,6 +488,10 @@ async function runDaemon(args, config) {
|
|
|
425
488
|
await reportMachineAction(action.id, 'failed', { error: message });
|
|
426
489
|
log(`Remote action failed: ${action.type}: ${message}`);
|
|
427
490
|
}
|
|
491
|
+
finally {
|
|
492
|
+
executingActionIds.delete(action.id);
|
|
493
|
+
await uploadLogTail();
|
|
494
|
+
}
|
|
428
495
|
};
|
|
429
496
|
const pollBundle = async () => {
|
|
430
497
|
if (!creds.shieldId)
|
|
@@ -481,6 +548,7 @@ async function runDaemon(args, config) {
|
|
|
481
548
|
log(`Heartbeat: flushed ${result.accepted} spooled event(s).`);
|
|
482
549
|
if (!integrity.ok)
|
|
483
550
|
log(`Integrity warning: ${integrity.reasons.join(', ')}.`);
|
|
551
|
+
await uploadLogTail();
|
|
484
552
|
}
|
|
485
553
|
catch { /* spool stays on disk for the next tick */ }
|
|
486
554
|
};
|
|
@@ -492,6 +560,34 @@ async function runDaemon(args, config) {
|
|
|
492
560
|
// One protective pass at startup so a machine that drifted while the daemon
|
|
493
561
|
// was down converges immediately.
|
|
494
562
|
await reprotect(['startup pass']);
|
|
563
|
+
// Fresh machines have never uploaded an inventory (MSI/onboard defers the
|
|
564
|
+
// initial discovery to keep setup fast), so the dashboard shows "Never" for
|
|
565
|
+
// discovery + posture until the daily scheduled job fires — up to 24h later.
|
|
566
|
+
// Run ONE full sweep shortly after the first daemon boot instead, then leave
|
|
567
|
+
// a marker so subsequent boots skip it (the daily job owns refreshes).
|
|
568
|
+
let initialDiscoverTimer;
|
|
569
|
+
if (creds.shieldId && !(0, discoveryMarker_1.hasDiscoveryUploadMarker)()) {
|
|
570
|
+
log(`Initial discovery: no prior inventory upload found — full sweep scheduled in ${Math.round(INITIAL_DISCOVER_DELAY_MS / 60_000)} min.`);
|
|
571
|
+
initialDiscoverTimer = setTimeout(async () => {
|
|
572
|
+
if (stopped)
|
|
573
|
+
return;
|
|
574
|
+
log('Initial discovery: starting full surface sweep (MCP + secrets + agent files + posture)…');
|
|
575
|
+
await uploadLogTail();
|
|
576
|
+
try {
|
|
577
|
+
const exitCode = await runDiscoverSweep();
|
|
578
|
+
if (exitCode !== 0)
|
|
579
|
+
throw new Error(`discover exited with code ${exitCode}`);
|
|
580
|
+
// The discover child writes the upload marker itself on success, so
|
|
581
|
+
// subsequent boots skip this. On failure the next daemon start
|
|
582
|
+
// retries, and the daily scheduled job remains the backstop.
|
|
583
|
+
log('Initial discovery: upload complete — dashboard discovery + posture timestamps are now fresh.');
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
log(`Initial discovery failed (will retry on next daemon start): ${error.message}`);
|
|
587
|
+
}
|
|
588
|
+
await uploadLogTail();
|
|
589
|
+
}, INITIAL_DISCOVER_DELAY_MS);
|
|
590
|
+
}
|
|
495
591
|
const rescanTimer = setInterval(() => {
|
|
496
592
|
const count = refreshWatchTargets();
|
|
497
593
|
log(`Rescan: watching ${count} config file(s).`);
|
|
@@ -506,6 +602,8 @@ async function runDaemon(args, config) {
|
|
|
506
602
|
clearInterval(rescanTimer);
|
|
507
603
|
clearInterval(bundleTimer);
|
|
508
604
|
clearInterval(heartbeatTimer);
|
|
605
|
+
if (initialDiscoverTimer)
|
|
606
|
+
clearTimeout(initialDiscoverTimer);
|
|
509
607
|
if (debounceTimer)
|
|
510
608
|
clearTimeout(debounceTimer);
|
|
511
609
|
for (const watcher of watchers.values())
|
|
@@ -546,7 +644,7 @@ function isWindowsRunKeyInstalled() {
|
|
|
546
644
|
/** Launch the daemon right now, outside our own process tree. WMI process
|
|
547
645
|
* creation escapes the Windows Installer job object, which would otherwise
|
|
548
646
|
* kill the daemon the moment an MSI custom action finishes. */
|
|
549
|
-
function startDaemonNowWindows(vbs) {
|
|
647
|
+
function startDaemonNowWindows(vbs, viaTask = false) {
|
|
550
648
|
try {
|
|
551
649
|
const existing = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
552
650
|
if (Number.isFinite(existing) && existing > 0 && isPidAlive(existing)) {
|
|
@@ -560,6 +658,15 @@ function startDaemonNowWindows(vbs) {
|
|
|
560
658
|
}
|
|
561
659
|
}
|
|
562
660
|
catch { /* not running */ }
|
|
661
|
+
if (viaTask) {
|
|
662
|
+
// Start through the scheduled task so the daemon runs with the task's
|
|
663
|
+
// LIMITED (non-elevated) token. Launching directly from an elevated MSI
|
|
664
|
+
// custom action would leave an elevated daemon that a normal-user CLI can
|
|
665
|
+
// never stop or supersede.
|
|
666
|
+
const run = (0, child_process_1.spawnSync)('schtasks', ['/Run', '/TN', TASK_NAME], { stdio: 'ignore', windowsHide: true, timeout: 20_000 });
|
|
667
|
+
if (run.status === 0)
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
563
670
|
const escaped = vbs.replace(/'/g, "''");
|
|
564
671
|
(0, child_process_1.spawnSync)('powershell', [
|
|
565
672
|
'-NoProfile', '-NonInteractive', '-Command',
|
|
@@ -574,7 +681,8 @@ function installWindows() {
|
|
|
574
681
|
'/Create', '/TN', TASK_NAME, '/TR', `wscript.exe "${vbs}"`,
|
|
575
682
|
'/SC', 'ONLOGON', '/F', '/RL', 'LIMITED',
|
|
576
683
|
], { stdio: 'ignore', windowsHide: true });
|
|
577
|
-
|
|
684
|
+
const taskOk = task.status === 0;
|
|
685
|
+
let ok = taskOk;
|
|
578
686
|
if (!ok) {
|
|
579
687
|
// Fallback: per-user Run key — no elevation needed, runs at every logon.
|
|
580
688
|
const reg = (0, child_process_1.spawnSync)('reg', [
|
|
@@ -584,7 +692,7 @@ function installWindows() {
|
|
|
584
692
|
ok = reg.status === 0;
|
|
585
693
|
}
|
|
586
694
|
if (ok)
|
|
587
|
-
startDaemonNowWindows(vbs);
|
|
695
|
+
startDaemonNowWindows(vbs, taskOk);
|
|
588
696
|
return ok;
|
|
589
697
|
}
|
|
590
698
|
function uninstallWindows() {
|
|
@@ -758,6 +866,19 @@ async function daemonCommand(args, config) {
|
|
|
758
866
|
const ok = process.platform === 'win32' ? uninstallWindows()
|
|
759
867
|
: process.platform === 'darwin' ? uninstallMacos()
|
|
760
868
|
: uninstallLinux();
|
|
869
|
+
// Uninstall must also STOP the resident daemon — otherwise it keeps running
|
|
870
|
+
// (heartbeats, log writes) until the next reboot and holds ~/.fullcourtdefense.
|
|
871
|
+
const meta = readDaemonMeta();
|
|
872
|
+
if (meta?.pid && meta.pid !== process.pid && isPidAlive(meta.pid)) {
|
|
873
|
+
const stopped = stopPid(meta.pid);
|
|
874
|
+
console.log(stopped
|
|
875
|
+
? `${COLOR.green}Stopped the running daemon (pid ${meta.pid}).${COLOR.reset}`
|
|
876
|
+
: `${COLOR.yellow}Could not stop the running daemon (pid ${meta.pid}) — it may be running elevated. Stop it from an elevated terminal: taskkill /PID ${meta.pid} /F${COLOR.reset}`);
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
fs.unlinkSync(metaFile());
|
|
880
|
+
}
|
|
881
|
+
catch { /* may not exist */ }
|
|
761
882
|
console.log(ok
|
|
762
883
|
? `${COLOR.green}Removed the FullCourtDefense daemon autostart.${COLOR.reset}`
|
|
763
884
|
: `${COLOR.yellow}No daemon autostart found (or removal failed).${COLOR.reset}`);
|
|
@@ -50,6 +50,7 @@ const discoverAgentFiles_1 = require("./discoverAgentFiles");
|
|
|
50
50
|
const discoverBlastRadius_1 = require("./discoverBlastRadius");
|
|
51
51
|
const discoverSecrets_1 = require("./discoverSecrets");
|
|
52
52
|
const windowsAudit_1 = require("./windowsAudit");
|
|
53
|
+
const discoveryMarker_1 = require("../discoveryMarker");
|
|
53
54
|
const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
|
|
54
55
|
function parseSurfaces(args) {
|
|
55
56
|
const raw = (args.surface || args.type || 'mcp').toLowerCase().trim();
|
|
@@ -696,6 +697,9 @@ async function upload(servers, host, clientCoverage, apiUrl, auth, connectorName
|
|
|
696
697
|
throw new Error(data.error || `Upload failed (${resp.status})`);
|
|
697
698
|
}
|
|
698
699
|
const ingested = data.data?.ingested ?? servers.length;
|
|
700
|
+
// Any successful inventory upload satisfies the daemon's "initial discovery"
|
|
701
|
+
// requirement — onboard-time, manual, scheduled, or remote-action sweeps all count.
|
|
702
|
+
(0, discoveryMarker_1.writeDiscoveryUploadMarker)('discover_upload');
|
|
699
703
|
const postureNote = extras?.posture ? ` · machine score ${extras.posture.score}/100 (${extras.posture.grade})` : '';
|
|
700
704
|
console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) from ${host.hostname} to your AI Inventory${postureNote}.${COLOR.reset}`);
|
|
701
705
|
}
|
|
@@ -1336,6 +1336,22 @@ function extractWrappedDownstream(entry) {
|
|
|
1336
1336
|
}
|
|
1337
1337
|
return { kind: 'stdio', command, args: downstreamArgs };
|
|
1338
1338
|
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Like extractWrappedDownstream, but peels NESTED gateway wraps until the real
|
|
1341
|
+
* server surfaces. Old CLIs compared the wrapper against their own node path, so
|
|
1342
|
+
* an npm install and an MSI install could wrap each other's wraps repeatedly —
|
|
1343
|
+
* unwrapping must restore the original server no matter how deep that went.
|
|
1344
|
+
*/
|
|
1345
|
+
function extractFullyUnwrappedDownstream(entry) {
|
|
1346
|
+
let downstream = extractWrappedDownstream(entry);
|
|
1347
|
+
for (let depth = 0; depth < 20 && downstream && downstream.kind === 'stdio' && downstream.args.includes('mcp-gateway'); depth++) {
|
|
1348
|
+
const inner = extractWrappedDownstream({ args: downstream.args });
|
|
1349
|
+
if (!inner)
|
|
1350
|
+
break;
|
|
1351
|
+
downstream = inner;
|
|
1352
|
+
}
|
|
1353
|
+
return downstream;
|
|
1354
|
+
}
|
|
1339
1355
|
/** All server maps inside a parsed JSON config, across every known client shape. */
|
|
1340
1356
|
function collectJsonServerMaps(json) {
|
|
1341
1357
|
const maps = [];
|
|
@@ -1535,7 +1551,7 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
|
|
|
1535
1551
|
// the original downstream server from the existing wrapper args and
|
|
1536
1552
|
// rewrap with the fixed matched-pair credential handling.
|
|
1537
1553
|
if (isGatewayWrappedEntry(entry) && wrappedEntryNeedsHeal(entry)) {
|
|
1538
|
-
const downstream =
|
|
1554
|
+
const downstream = extractFullyUnwrappedDownstream(entry);
|
|
1539
1555
|
if (downstream) {
|
|
1540
1556
|
const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
|
|
1541
1557
|
entry.command = nodeExe;
|
|
@@ -1612,7 +1628,7 @@ function unwrapJsonConfigFile(file, dryRun) {
|
|
|
1612
1628
|
}
|
|
1613
1629
|
if (!isGatewayWrappedEntry(entry))
|
|
1614
1630
|
continue;
|
|
1615
|
-
const downstream =
|
|
1631
|
+
const downstream = extractFullyUnwrappedDownstream(entry);
|
|
1616
1632
|
if (!downstream)
|
|
1617
1633
|
continue;
|
|
1618
1634
|
if (downstream.kind === 'http') {
|
|
@@ -1731,7 +1747,11 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
|
|
|
1731
1747
|
if (section.cmdLine === -1 || !section.command) {
|
|
1732
1748
|
continue;
|
|
1733
1749
|
}
|
|
1734
|
-
|
|
1750
|
+
// Command-agnostic like the JSON path: a wrap made by ANY CLI install (npm node,
|
|
1751
|
+
// MSI runtime node, different nvm version) must be recognized — comparing against
|
|
1752
|
+
// THIS process's node path made one install re-wrap another install's wrap and
|
|
1753
|
+
// left unprotect-all unable to remove it.
|
|
1754
|
+
const wrappedAlready = (section.args || []).includes('mcp-gateway');
|
|
1735
1755
|
if (mode === 'wrap') {
|
|
1736
1756
|
if (section.name === MANAGED_SERVER_NAME || wrappedAlready) {
|
|
1737
1757
|
stats.skippedManaged.push(section.name);
|
|
@@ -1752,7 +1772,7 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
|
|
|
1752
1772
|
else {
|
|
1753
1773
|
if (!wrappedAlready)
|
|
1754
1774
|
continue;
|
|
1755
|
-
const downstream =
|
|
1775
|
+
const downstream = extractFullyUnwrappedDownstream({ args: section.args });
|
|
1756
1776
|
// Codex TOML wraps are always stdio (remote URLs never get wrapped into TOML).
|
|
1757
1777
|
if (!downstream || downstream.kind !== 'stdio')
|
|
1758
1778
|
continue;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.hasDiscoveryUploadMarker = hasDiscoveryUploadMarker;
|
|
37
|
+
exports.writeDiscoveryUploadMarker = writeDiscoveryUploadMarker;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Marker recording that this machine has uploaded at least one full discovery
|
|
43
|
+
* inventory. The daemon uses it to decide whether a fresh machine still needs
|
|
44
|
+
* its one-time initial sweep (MSI/onboard defer discovery to keep setup fast,
|
|
45
|
+
* which otherwise leaves the dashboard showing "Never" until the daily job).
|
|
46
|
+
*/
|
|
47
|
+
function markerFile() {
|
|
48
|
+
return path.join(os.homedir(), '.fullcourtdefense', 'initial-discover.json');
|
|
49
|
+
}
|
|
50
|
+
function hasDiscoveryUploadMarker() {
|
|
51
|
+
try {
|
|
52
|
+
return fs.existsSync(markerFile());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function writeDiscoveryUploadMarker(trigger) {
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(path.dirname(markerFile()), { recursive: true });
|
|
61
|
+
fs.writeFileSync(markerFile(), JSON.stringify({ completedAt: new Date().toISOString(), trigger }, null, 2), 'utf8');
|
|
62
|
+
}
|
|
63
|
+
catch { /* marker is best-effort */ }
|
|
64
|
+
}
|
package/dist/machineIdentity.js
CHANGED
|
@@ -77,11 +77,17 @@ function normalizeHostname(raw) {
|
|
|
77
77
|
function rawStableId() {
|
|
78
78
|
const platform = os.platform();
|
|
79
79
|
if (platform === 'win32') {
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// MachineGuid lives only in the 64-bit registry view. When this process is
|
|
81
|
+
// 32-bit (e.g. spawned by an MSI custom action via SysWOW64 PowerShell),
|
|
82
|
+
// a plain query is redirected to WOW6432Node and finds nothing — which
|
|
83
|
+
// would silently change the machineId and enroll a DUPLICATE fleet record.
|
|
84
|
+
// Query the 64-bit view first, then fall back for true 32-bit Windows.
|
|
85
|
+
const queryMachineGuid = (extraArgs) => safe(() => {
|
|
86
|
+
const out = (0, child_process_1.execFileSync)('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid', ...extraArgs], { encoding: 'utf8', windowsHide: true, timeout: 4000 });
|
|
82
87
|
const match = out.match(/MachineGuid\s+REG_SZ\s+([\w-]+)/i);
|
|
83
88
|
return match ? match[1] : undefined;
|
|
84
89
|
});
|
|
90
|
+
return queryMachineGuid(['/reg:64']) || queryMachineGuid([]);
|
|
85
91
|
}
|
|
86
92
|
if (platform === 'darwin') {
|
|
87
93
|
return safe(() => {
|
package/dist/version.json
CHANGED