fullcourtdefense-cli 1.15.4 → 1.15.6

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.
@@ -266,6 +266,7 @@ async function runDaemon(args, config) {
266
266
  await (0, mcpGateway_1.protectAllCommand)({ ...args, dryRun: undefined }, config);
267
267
  quietUntil = Date.now() + SELF_WRITE_QUIET_MS;
268
268
  log('Re-protection pass complete.');
269
+ await uploadLogTail();
269
270
  if (!quiet) {
270
271
  (0, notify_1.notifyOs)({
271
272
  title: 'FullCourtDefense re-protected this machine',
@@ -329,6 +330,28 @@ async function runDaemon(args, config) {
329
330
  }
330
331
  return targets.length;
331
332
  };
333
+ // Ship the REAL daemon log tail to the control plane so the dashboard's
334
+ // machine page can show live progress (protect-all passes, cache clears,
335
+ // discovery sweeps). Best-effort — never blocks or fails the daemon.
336
+ const uploadLogTail = async () => {
337
+ if (!creds.shieldId)
338
+ return;
339
+ try {
340
+ const raw = fs.readFileSync(logFile(), 'utf8');
341
+ const lines = raw.split(/\r?\n/).filter(line => line.trim()).slice(-80);
342
+ const identity = (0, machineIdentity_1.getMachineIdentity)();
343
+ await fetch(`${creds.apiUrl}/api/cli/machines/log`, {
344
+ method: 'POST',
345
+ headers: {
346
+ 'Content-Type': 'application/json',
347
+ ...(creds.shieldKey ? { 'x-shield-key': creds.shieldKey } : {}),
348
+ },
349
+ body: JSON.stringify({ shieldId: creds.shieldId, machineId: identity.machineId, lines }),
350
+ signal: AbortSignal.timeout(8_000),
351
+ });
352
+ }
353
+ catch { /* log shipping is best-effort */ }
354
+ };
332
355
  const reportMachineAction = async (actionId, status, detail) => {
333
356
  if (!creds.shieldId)
334
357
  return;
@@ -359,10 +382,16 @@ async function runDaemon(args, config) {
359
382
  executingActionIds.add(action.id);
360
383
  await reportMachineAction(action.id, 'running');
361
384
  log(`Remote action started: ${action.type} (${action.id}).`);
385
+ await uploadLogTail();
362
386
  try {
363
387
  let resultSummary = '';
364
388
  if (action.type === 'health_check') {
389
+ log('Health check: verifying daemon + protection surfaces…');
390
+ await uploadLogTail();
365
391
  const integrity = (0, integrity_1.getLocalIntegrityReport)({ requireDaemon: true });
392
+ log(integrity.ok
393
+ ? `Health check: all protection points healthy (${integrity.protectedMcpConfigs}/${integrity.discoveredMcpConfigs} MCP configs wrapped).`
394
+ : `Health check: issues found — ${integrity.reasons.join(', ')}.`);
366
395
  resultSummary = integrity.ok
367
396
  ? 'Daemon and required AgentGuard protection points are healthy.'
368
397
  : `Health check found: ${integrity.reasons.join(', ')}`;
@@ -374,21 +403,28 @@ async function runDaemon(args, config) {
374
403
  throw new Error('Shield not configured on this machine.');
375
404
  const shieldId = creds.shieldId;
376
405
  const identity = (0, machineIdentity_1.getMachineIdentity)();
377
- const bundle = await (0, runtimeConfig_1.getRuntimeBundle)({
406
+ log('Policy refresh: clearing Local Safety snapshot cache…');
407
+ (0, localSafetySnapshot_1.clearLocalSafetySnapshotCache)({
378
408
  apiUrl: creds.apiUrl,
379
409
  shieldId,
380
- shieldKey: creds.shieldKey,
381
410
  developerName: identity.developerName,
382
411
  machineName: identity.hostname,
383
- force: true,
384
- ttlMs: 0,
385
412
  });
386
- (0, localSafetySnapshot_1.clearLocalSafetySnapshotCache)({
413
+ await uploadLogTail();
414
+ log('Policy refresh: pulling latest policy bundle from control plane…');
415
+ const bundle = await (0, runtimeConfig_1.getRuntimeBundle)({
387
416
  apiUrl: creds.apiUrl,
388
417
  shieldId,
418
+ shieldKey: creds.shieldKey,
389
419
  developerName: identity.developerName,
390
420
  machineName: identity.hostname,
421
+ machineId: identity.machineId,
422
+ force: true,
423
+ ttlMs: 0,
391
424
  });
425
+ log(bundle.policyHash
426
+ ? `Policy refresh: bundle applied (hash ${bundle.policyHash.slice(0, 12)}…, ${bundle.policyCount ?? 0} policies).`
427
+ : 'Policy refresh: bundle applied from control plane.');
392
428
  resultSummary = bundle.policyHash
393
429
  ? `Policy bundle refreshed (hash ${bundle.policyHash.slice(0, 12)}…).`
394
430
  : 'Policy bundle refreshed from the control plane.';
@@ -396,25 +432,41 @@ async function runDaemon(args, config) {
396
432
  else if (action.type === 'repair_protection') {
397
433
  if (suspended)
398
434
  throw new Error('Machine is suspended; resume it before repairing protection.');
435
+ log('Repair protection: running protect-all (IDE hooks + MCP gateways)…');
436
+ await uploadLogTail();
399
437
  await (0, mcpGateway_1.protectAllCommand)({ ...args, dryRun: undefined }, config);
438
+ log('Repair protection: verifying hooks, gateways and daemon integrity…');
400
439
  const verification = (0, integrity_1.getLocalIntegrityReport)({ requireDaemon: true });
401
440
  if (!verification.ok) {
402
441
  throw new Error(`Repair completed but verification still reports: ${verification.reasons.join(', ')}`);
403
442
  }
443
+ log(`Repair protection: verified (${verification.protectedMcpConfigs}/${verification.discoveredMcpConfigs} MCP configs wrapped).`);
404
444
  resultSummary = 'AgentGuard hooks, gateways, and protection configuration were repaired and verified.';
405
445
  }
406
446
  else if (action.type === 'discovery_scan') {
407
- // Full surface sweep (MCP + secrets + agent-files + posture) so a remote
408
- // "Run discovery" refreshes BOTH the discovery and the posture scan
409
- // timestamps in the dashboard not just the MCP inventory.
410
- const result = (0, child_process_1.spawnSync)(process.execPath, [cliEntry(), 'discover', '--upload', '--surface', 'all', '--silent'], {
411
- encoding: 'utf8',
412
- windowsHide: true,
413
- timeout: 300_000,
414
- });
415
- if (result.status !== 0) {
416
- throw new Error((result.stderr || result.stdout || 'Discovery command failed').trim().slice(0, 500));
447
+ log('Discovery scan: starting full surface sweep (MCP + secrets + agent files + posture)…');
448
+ await uploadLogTail();
449
+ const logPump = setInterval(() => { void uploadLogTail(); }, 15_000);
450
+ try {
451
+ const exitCode = await new Promise((resolve, reject) => {
452
+ const child = (0, child_process_1.spawn)(process.execPath, [cliEntry(), 'discover', '--upload', '--surface', 'all', '--silent'], {
453
+ windowsHide: true,
454
+ stdio: 'ignore',
455
+ });
456
+ const timer = setTimeout(() => {
457
+ child.kill();
458
+ reject(new Error('Discovery command timed out after 5 minutes'));
459
+ }, 300_000);
460
+ child.on('error', error => { clearTimeout(timer); reject(error); });
461
+ child.on('close', code => { clearTimeout(timer); resolve(code ?? 1); });
462
+ });
463
+ if (exitCode !== 0)
464
+ throw new Error(`Discovery command failed with exit code ${exitCode}`);
417
465
  }
466
+ finally {
467
+ clearInterval(logPump);
468
+ }
469
+ log('Discovery scan: upload complete — dashboard discovery + posture timestamps will refresh.');
418
470
  resultSummary = 'Discovery + posture scan completed and uploaded.';
419
471
  }
420
472
  await reportMachineAction(action.id, 'succeeded', { resultSummary });
@@ -425,6 +477,10 @@ async function runDaemon(args, config) {
425
477
  await reportMachineAction(action.id, 'failed', { error: message });
426
478
  log(`Remote action failed: ${action.type}: ${message}`);
427
479
  }
480
+ finally {
481
+ executingActionIds.delete(action.id);
482
+ await uploadLogTail();
483
+ }
428
484
  };
429
485
  const pollBundle = async () => {
430
486
  if (!creds.shieldId)
@@ -437,6 +493,7 @@ async function runDaemon(args, config) {
437
493
  shieldKey: creds.shieldKey,
438
494
  developerName: identity.developerName,
439
495
  machineName: identity.hostname,
496
+ machineId: identity.machineId,
440
497
  force: true,
441
498
  });
442
499
  if (bundle.suspended && !suspended) {
@@ -480,6 +537,7 @@ async function runDaemon(args, config) {
480
537
  log(`Heartbeat: flushed ${result.accepted} spooled event(s).`);
481
538
  if (!integrity.ok)
482
539
  log(`Integrity warning: ${integrity.reasons.join(', ')}.`);
540
+ await uploadLogTail();
483
541
  }
484
542
  catch { /* spool stays on disk for the next tick */ }
485
543
  };
@@ -39,6 +39,8 @@ export interface FetchBundleInput {
39
39
  shieldKey?: string;
40
40
  developerName?: string;
41
41
  machineName?: string;
42
+ /** Exact fleet machine ID — makes the server's pending-action lookup deterministic. */
43
+ machineId?: string;
42
44
  /** Override cache TTL (ms). */
43
45
  ttlMs?: number;
44
46
  /** Force a network refresh regardless of cache freshness. */
@@ -83,6 +83,8 @@ async function getRuntimeBundle(input) {
83
83
  params.set('developerName', input.developerName);
84
84
  if (input.machineName)
85
85
  params.set('machineName', input.machineName);
86
+ if (input.machineId)
87
+ params.set('machineId', input.machineId);
86
88
  const resp = await fetch(`${input.apiUrl}/api/cli/bundle?${params.toString()}`, { method: 'GET', headers, signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS) });
87
89
  // 304 Not Modified — cache is still valid; refresh its timestamp.
88
90
  if (resp.status === 304 && cached) {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.15.4"
2
+ "version": "1.15.6"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.15.4",
3
+ "version": "1.15.6",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {