fullcourtdefense-cli 1.15.0 → 1.15.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/README.md CHANGED
@@ -26,6 +26,20 @@ fullcourtdefense onboard --token <fleet-enrollment-token>
26
26
 
27
27
  Then restart your AI clients (Cursor, Claude, VS Code, ...) so they pick up the wrapped configs. The machine reports to your org's AI Fleet in monitor-first mode until an admin enables enforcement.
28
28
 
29
+ Onboarding is a resumable local transaction. Its non-secret status journal is
30
+ stored at `~/.fullcourtdefense/onboarding.json`; use `--resume` after an
31
+ interruption and `--repair` to re-run completed protection installation steps.
32
+ Discovery upload and its daily schedule are recorded as optional telemetry,
33
+ separate from required protection. Useful MDM options:
34
+
35
+ ```bash
36
+ fullcourtdefense onboard --dry-run true --json true # preflight + final JSON status
37
+ fullcourtdefense onboard --resume # continue a failed transaction
38
+ fullcourtdefense onboard --resume --repair # reinstall protection surfaces
39
+ fullcourtdefense onboard --schedule false # do not install daily discovery
40
+ fullcourtdefense onboard --no-daemon true # do not enable optional self-healing
41
+ ```
42
+
29
43
  Manual step-by-step equivalent:
30
44
 
31
45
  ```bash
@@ -57,7 +71,7 @@ fullcourtdefense init
57
71
  ## Command Guide
58
72
 
59
73
  - `fullcourtdefense help` — shows the full onboarding flow and command reference.
60
- - `fullcourtdefense onboard --token <token>` — one command: doctor + login + install-all + verification checklist (exits non-zero if a required surface failed — safe for MDM scripts).
74
+ - `fullcourtdefense onboard --token <token>` — resumable machine transaction: preflight + login + protection + optional discovery + verification. `--json true` prints final machine-readable status; exits non-zero only when a critical step fails.
61
75
  - `fullcourtdefense doctor` — confirms outbound HTTPS to FullCourtDefense is open before scanning.
62
76
  - `fullcourtdefense login --token <token>` — enrolls this machine with a fleet token and saves per-machine Shield credentials (no copy/paste).
63
77
  - `fullcourtdefense install-all` — wraps every configured MCP server, installs IDE hooks and terminal guards, uploads discovery, schedules daily rescans.
@@ -7,4 +7,6 @@ export interface DaemonArgs extends ProtectAllArgs {
7
7
  /** Suppress OS toasts (still logs). */
8
8
  quiet?: string;
9
9
  }
10
+ /** Whether the daemon has been registered to start automatically. */
11
+ export declare function isDaemonAutostartInstalled(): boolean;
10
12
  export declare function daemonCommand(args: DaemonArgs, config: BotGuardConfig): Promise<void>;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isDaemonAutostartInstalled = isDaemonAutostartInstalled;
36
37
  exports.daemonCommand = daemonCommand;
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
@@ -43,6 +44,7 @@ const mcpGateway_1 = require("./mcpGateway");
43
44
  const runtimeConfig_1 = require("../runtimeConfig");
44
45
  const telemetry_1 = require("../telemetry");
45
46
  const notify_1 = require("../notify");
47
+ const integrity_1 = require("../integrity");
46
48
  const COLOR = {
47
49
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
48
50
  red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
@@ -266,14 +268,20 @@ async function runDaemon(args, config) {
266
268
  if (!creds.shieldId)
267
269
  return;
268
270
  try {
271
+ const integrity = (0, integrity_1.getLocalIntegrityReport)({ requireDaemon: true });
269
272
  const result = await (0, telemetry_1.flushSpool)({
270
273
  apiUrl: creds.apiUrl,
271
274
  shieldId: creds.shieldId,
272
275
  shieldKey: creds.shieldKey,
273
276
  heartbeat: true,
277
+ integrityOk: integrity.ok,
278
+ integrityReasons: integrity.reasons,
279
+ integrityCheckedAt: integrity.checkedAt,
274
280
  });
275
281
  if (result && result.accepted > 0)
276
282
  log(`Heartbeat: flushed ${result.accepted} spooled event(s).`);
283
+ if (!integrity.ok)
284
+ log(`Integrity warning: ${integrity.reasons.join(', ')}.`);
277
285
  }
278
286
  catch { /* spool stays on disk for the next tick */ }
279
287
  };
@@ -387,6 +395,20 @@ function systemdUnitPath() {
387
395
  const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
388
396
  return path.join(base, 'systemd', 'user', SYSTEMD_UNIT);
389
397
  }
398
+ /** Whether the daemon has been registered to start automatically. */
399
+ function isDaemonAutostartInstalled() {
400
+ if (process.platform === 'win32') {
401
+ const result = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', TASK_NAME], {
402
+ stdio: 'ignore',
403
+ timeout: 5_000,
404
+ windowsHide: true,
405
+ });
406
+ return result.status === 0;
407
+ }
408
+ if (process.platform === 'darwin')
409
+ return fs.existsSync(launchdPlistPath());
410
+ return fs.existsSync(systemdUnitPath());
411
+ }
390
412
  function installLinux() {
391
413
  const unit = `[Unit]
392
414
  Description=FullCourtDefense resident daemon (config watch + heartbeat)
@@ -3,6 +3,16 @@ import { InstallAllArgs } from './installAll';
3
3
  export interface OnboardArgs extends InstallAllArgs {
4
4
  /** Fleet enrollment token (or FCD_ENROLL_TOKEN env). Optional when already enrolled. */
5
5
  token?: string;
6
+ /** Continue a persisted onboarding transaction. */
7
+ resume?: string;
8
+ /** Re-run completed install steps to repair changed machine state. */
9
+ repair?: string;
10
+ /** Report the transaction plan without changing local configuration. */
11
+ dryRun?: string;
12
+ /** Print the final transaction status as JSON. */
13
+ json?: string;
14
+ /** Do not enable optional self-healing protection. */
15
+ noDaemon?: string;
6
16
  }
7
17
  /**
8
18
  * `fullcourtdefense onboard` — the one-command path from a fresh machine to a
@@ -40,6 +40,7 @@ const path = __importStar(require("path"));
40
40
  const config_1 = require("../config");
41
41
  const login_1 = require("./login");
42
42
  const installAll_1 = require("./installAll");
43
+ const discover_1 = require("./discover");
43
44
  const discoverPaths_1 = require("./discoverPaths");
44
45
  const discoverSchedule_1 = require("./discoverSchedule");
45
46
  const windowsAudit_1 = require("./windowsAudit");
@@ -47,6 +48,9 @@ const shellGuard_1 = require("./shellGuard");
47
48
  const cmdGuard_1 = require("./cmdGuard");
48
49
  const posixShellGuard_1 = require("./posixShellGuard");
49
50
  const notify_1 = require("../notify");
51
+ const daemon_1 = require("./daemon");
52
+ const machineIdentity_1 = require("../machineIdentity");
53
+ const onboardingJournal_1 = require("./onboardingJournal");
50
54
  const GREEN = '\x1b[32m';
51
55
  const RED = '\x1b[31m';
52
56
  const YELLOW = '\x1b[33m';
@@ -70,6 +74,41 @@ async function checkBackendReachable(apiUrl) {
70
74
  return { label: 'Backend reachable', ok: false, detail: error instanceof Error ? error.message : String(error) };
71
75
  }
72
76
  }
77
+ async function checkEnrollmentCredential(apiUrl, token) {
78
+ try {
79
+ const identity = (0, machineIdentity_1.getMachineIdentity)();
80
+ const resp = await fetch(`${apiUrl}/api/cli/enroll/preflight`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json', 'x-fleet-token': token },
83
+ body: JSON.stringify({
84
+ machineId: identity.machineId,
85
+ hostname: identity.hostname,
86
+ developerName: identity.developerName,
87
+ platform: identity.platform,
88
+ osFriendly: identity.osFriendly,
89
+ }),
90
+ signal: AbortSignal.timeout(10_000),
91
+ });
92
+ const body = await resp.json().catch(() => ({}));
93
+ if (resp.ok && body.success) {
94
+ const status = body.data?.enrollmentStatus === 'already_enrolled' ? 'existing device can be re-enrolled safely' : 'new device is eligible';
95
+ return { label: 'Enrollment credential', ok: true, detail: status };
96
+ }
97
+ return { label: 'Enrollment credential', ok: false, detail: body.error || `HTTP ${resp.status}` };
98
+ }
99
+ catch (error) {
100
+ return { label: 'Enrollment credential', ok: false, detail: error instanceof Error ? error.message : String(error) };
101
+ }
102
+ }
103
+ function checkCompatibility() {
104
+ const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] || '0', 10);
105
+ const supportedPlatform = ['win32', 'darwin', 'linux'].includes(process.platform);
106
+ if (nodeMajor < 18)
107
+ return { label: 'CLI compatibility', ok: false, detail: `Node.js ${process.versions.node}; Node 18+ is required` };
108
+ if (!supportedPlatform)
109
+ return { label: 'CLI compatibility', ok: false, detail: `${process.platform} is not supported for local protection` };
110
+ return { label: 'CLI compatibility', ok: true, detail: `Node ${process.versions.node} on ${process.platform}` };
111
+ }
73
112
  function readTextSafe(file) {
74
113
  try {
75
114
  return fs.readFileSync(file, 'utf8');
@@ -154,40 +193,161 @@ function checkTerminalGuards() {
154
193
  * Exits non-zero when a required surface failed, so MDM scripts can gate on it.
155
194
  */
156
195
  async function onboardCommand(args, config) {
196
+ const journal = (0, onboardingJournal_1.loadOnboardingJournal)();
197
+ const dryRun = args.dryRun === 'true';
198
+ const repair = args.repair === 'true';
199
+ const json = args.json === 'true';
200
+ const journalFile = (0, onboardingJournal_1.onboardingJournalPath)();
201
+ const persist = () => (0, onboardingJournal_1.saveOnboardingJournal)(journal, journalFile);
202
+ const mark = (name, status, detail, error) => {
203
+ (0, onboardingJournal_1.updateOnboardingStep)(journal, name, status, detail, error);
204
+ persist();
205
+ };
206
+ const report = (nextAction) => {
207
+ journal.nextAction = nextAction;
208
+ persist();
209
+ if (json)
210
+ console.log(JSON.stringify({ ok: !Object.values(journal.steps).some(step => step.critical && step.status === 'failed'), journalPath: journalFile, ...journal }));
211
+ };
157
212
  console.log(`\n${BOLD}\x1b[36mFullCourtDefense — machine onboarding${RESET}`);
158
213
  console.log(`${DIM}Connectivity -> enrollment -> protection -> verification. One command.${RESET}\n`);
159
214
  const creds = (0, config_1.resolveCliCredentials)(config, { apiUrl: args.apiUrl });
160
215
  const apiUrl = creds.apiUrl;
216
+ const token = (args.token || process.env.FCD_ENROLL_TOKEN || '').trim();
161
217
  // 1. Connectivity.
162
- console.log(`${BOLD}[1/4] Checking connectivity…${RESET}`);
218
+ console.log(`${BOLD}[1/6] Checking compatibility and connectivity…${RESET}`);
219
+ mark('preflight', 'running');
163
220
  const reachable = await checkBackendReachable(apiUrl);
221
+ const compatible = checkCompatibility();
222
+ printCheck(compatible);
164
223
  printCheck(reachable);
165
- if (!reachable.ok) {
166
- console.log(`\n${RED}Cannot reach the FullCourtDefense backend.${RESET} Allow outbound TCP 443 to ${apiUrl}, check proxy settings, then re-run.`);
167
- process.exit(1);
224
+ if (!reachable.ok || !compatible.ok) {
225
+ const detail = !reachable.ok ? reachable.detail : compatible.detail;
226
+ mark('preflight', 'failed', undefined, detail);
227
+ report(!reachable.ok
228
+ ? `Restore connectivity to ${apiUrl}, then run fullcourtdefense onboard --resume.`
229
+ : 'Install Node.js 18 or later, then run fullcourtdefense onboard --resume.');
230
+ process.exitCode = 1;
231
+ return;
232
+ }
233
+ if (token) {
234
+ const enrollmentCredential = await checkEnrollmentCredential(apiUrl, token);
235
+ printCheck(enrollmentCredential);
236
+ if (!enrollmentCredential.ok) {
237
+ mark('preflight', 'failed', undefined, enrollmentCredential.detail);
238
+ report('Request a valid enrollment code from your administrator, then run fullcourtdefense onboard --resume.');
239
+ process.exitCode = 1;
240
+ return;
241
+ }
168
242
  }
243
+ mark('preflight', 'completed');
169
244
  // 2. Enrollment.
170
- console.log(`\n${BOLD}[2/4] Enrolling this machine…${RESET}`);
171
- const token = (args.token || process.env.FCD_ENROLL_TOKEN || '').trim();
245
+ console.log(`\n${BOLD}[2/6] Enrolling this machine…${RESET}`);
246
+ mark('enrollment', 'running');
172
247
  const alreadyEnrolled = Boolean(creds.shieldId && creds.shieldKey);
173
- if (token) {
248
+ if (dryRun) {
249
+ mark('enrollment', 'skipped', alreadyEnrolled ? 'would reuse existing per-machine Shield credentials' : 'would enroll with supplied token');
250
+ }
251
+ else if (token) {
174
252
  await (0, login_1.loginCommand)({ token, apiUrl: args.apiUrl }, config);
253
+ mark('enrollment', 'completed', 'machine enrolled');
175
254
  }
176
255
  else if (alreadyEnrolled) {
177
256
  console.log(` ${GREEN}✓${RESET} Already enrolled ${DIM}(shield ${creds.shieldId})${RESET} — pass --token to re-enroll.`);
257
+ mark('enrollment', 'completed', `reused shield ${creds.shieldId}`);
178
258
  }
179
259
  else {
180
260
  console.log(`\n${RED}No fleet enrollment token.${RESET} Ask an org admin for one (AI Fleet -> Settings -> Fleet enrollment token), then run:`);
181
261
  console.log(` ${BOLD}fullcourtdefense onboard --token <fleet-enrollment-token>${RESET}`);
182
- process.exit(1);
262
+ mark('enrollment', 'failed', undefined, 'no fleet enrollment token or saved machine credentials');
263
+ report('Get a fleet enrollment token, then run fullcourtdefense onboard --resume --token <token>.');
264
+ process.exitCode = 1;
265
+ return;
183
266
  }
184
267
  // Re-read config: login just wrote fresh shield credentials to ~/.fullcourtdefense.yml.
185
268
  const freshConfig = (0, config_1.loadConfig)();
186
269
  // 3. Protection.
187
- console.log(`\n${BOLD}[3/4] Installing protection (install-all)…${RESET}`);
188
- await (0, installAll_1.installAllCommand)({ ...args, apiUrl }, freshConfig);
270
+ console.log(`\n${BOLD}[3/6] Installing protection (install-all)…${RESET}`);
271
+ if (dryRun) {
272
+ mark('protection', 'skipped', 'would install MCP gateways, IDE hooks, and terminal guards');
273
+ }
274
+ else if (journal.steps.protection.status === 'completed' && !repair) {
275
+ console.log(` ${GREEN}✓${RESET} Reusing completed protection step ${DIM}(use --repair to reinstall)${RESET}`);
276
+ }
277
+ else {
278
+ mark('protection', 'running');
279
+ try {
280
+ // Discovery and scheduling are deliberately separate optional transaction
281
+ // steps. install-all remains the compatibility-preserving protection owner.
282
+ await (0, installAll_1.installAllCommand)({
283
+ ...args,
284
+ apiUrl,
285
+ discover: 'false',
286
+ schedule: 'false',
287
+ autoProtect: args.noDaemon === 'true' ? 'false' : args.autoProtect,
288
+ }, freshConfig);
289
+ mark('protection', 'completed');
290
+ }
291
+ catch (error) {
292
+ const detail = error instanceof Error ? error.message : String(error);
293
+ mark('protection', 'failed', undefined, detail);
294
+ report('Fix the protection error, then run fullcourtdefense onboard --resume --repair.');
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+ }
299
+ console.log(`\n${BOLD}[4/6] Installing background protection…${RESET}`);
300
+ if (dryRun || args.noDaemon === 'true') {
301
+ mark('daemon', 'skipped', dryRun ? 'would register the resident daemon' : 'disabled by --no-daemon true');
302
+ }
303
+ else {
304
+ mark('daemon', 'running');
305
+ try {
306
+ await (0, daemon_1.daemonCommand)({ ...args, install: 'true', quiet: 'true' }, freshConfig);
307
+ if ((0, daemon_1.isDaemonAutostartInstalled)()) {
308
+ mark('daemon', 'completed', 'resident daemon autostart registered');
309
+ }
310
+ else {
311
+ mark('daemon', 'failed', undefined, 'daemon autostart was not registered; run an elevated terminal and retry `fullcourtdefense daemon --install true`');
312
+ }
313
+ }
314
+ catch (error) {
315
+ mark('daemon', 'failed', undefined, error instanceof Error ? error.message : String(error));
316
+ }
317
+ }
318
+ // Discovery and scheduling are useful fleet telemetry, but must never mark a
319
+ // device as unprotected or prevent a repair of the enforcement surfaces.
320
+ console.log(`\n${BOLD}[5/6] Recording optional discovery and schedule…${RESET}`);
321
+ if (dryRun || args.discover === 'false') {
322
+ mark('discovery', 'skipped', dryRun ? 'would upload desktop discovery' : 'disabled by --discover false');
323
+ }
324
+ else {
325
+ mark('discovery', 'running');
326
+ try {
327
+ await (0, discover_1.discoverCommand)({ surface: 'all', upload: 'true', apiUrl, userEmail: args.developerName }, freshConfig);
328
+ mark('discovery', 'completed');
329
+ }
330
+ catch (error) {
331
+ mark('discovery', 'failed', undefined, error instanceof Error ? error.message : String(error));
332
+ }
333
+ }
334
+ if (dryRun || args.schedule === 'false') {
335
+ mark('schedule', 'skipped', dryRun ? 'would install daily discovery schedule' : 'disabled by --schedule false');
336
+ }
337
+ else {
338
+ mark('schedule', 'running');
339
+ try {
340
+ const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
341
+ (0, discoverSchedule_1.installDailyDiscoverSchedule)({ userEmail: args.developerName, hour: Number.isFinite(hour) ? hour : 9, surface: 'all', trigger: 'daily' });
342
+ mark('schedule', 'completed');
343
+ }
344
+ catch (error) {
345
+ mark('schedule', 'failed', undefined, error instanceof Error ? error.message : String(error));
346
+ }
347
+ }
189
348
  // 4. Verification.
190
- console.log(`\n${BOLD}[4/4] Verifying protection surfaces…${RESET}`);
349
+ console.log(`\n${BOLD}[6/6] Verifying protection surfaces…${RESET}`);
350
+ mark('verification', 'running');
191
351
  const postCreds = (0, config_1.resolveCliCredentials)((0, config_1.loadConfig)(), { apiUrl });
192
352
  const checks = [
193
353
  reachable,
@@ -200,6 +360,12 @@ async function onboardCommand(args, config) {
200
360
  checkClaudeHooks(),
201
361
  checkMcpGateways(),
202
362
  ...checkTerminalGuards(),
363
+ {
364
+ label: 'Resident daemon autostart',
365
+ ok: (0, daemon_1.isDaemonAutostartInstalled)(),
366
+ optional: true,
367
+ detail: (0, daemon_1.isDaemonAutostartInstalled)() ? undefined : 'run fullcourtdefense daemon --install true from an elevated terminal',
368
+ },
203
369
  {
204
370
  label: 'Daily discovery schedule',
205
371
  ok: (0, discoverSchedule_1.isDiscoverScheduleInstalled)(),
@@ -217,9 +383,13 @@ async function onboardCommand(args, config) {
217
383
  console.log(`${GREEN}${BOLD}This machine is protected.${RESET} It reports to your org's AI Fleet (monitor-first until an admin enables enforcement).`);
218
384
  console.log(`${DIM}Fleet view: ${dashboard}${RESET}`);
219
385
  console.log(`${DIM}Restart your AI clients (Cursor, Claude, VS Code, …) so they pick up the wrapped configs.${RESET}`);
386
+ mark('verification', 'completed');
387
+ report('Onboarding complete. Restart AI clients to load protected configurations.');
220
388
  }
221
389
  else {
222
390
  console.log(`${YELLOW}${BOLD}Onboarding finished with ${requiredFailures.length} unresolved surface(s).${RESET} Fix the ✗ lines above and re-run ${BOLD}fullcourtdefense onboard${RESET}.`);
223
- process.exit(1);
391
+ mark('verification', 'failed', undefined, `${requiredFailures.length} critical protection surfaces unresolved`);
392
+ report('Fix the failed required surfaces, then run fullcourtdefense onboard --resume --repair.');
393
+ process.exitCode = 1;
224
394
  }
225
395
  }
@@ -0,0 +1,20 @@
1
+ export type OnboardingStepName = 'preflight' | 'enrollment' | 'protection' | 'daemon' | 'discovery' | 'schedule' | 'verification';
2
+ export type OnboardingStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
3
+ export interface OnboardingStep {
4
+ status: OnboardingStepStatus;
5
+ critical: boolean;
6
+ updatedAt: string;
7
+ error?: string;
8
+ detail?: string;
9
+ }
10
+ export interface OnboardingJournal {
11
+ version: 1;
12
+ updatedAt: string;
13
+ nextAction: string;
14
+ steps: Record<OnboardingStepName, OnboardingStep>;
15
+ }
16
+ export declare function onboardingJournalPath(home?: string): string;
17
+ export declare function createOnboardingJournal(): OnboardingJournal;
18
+ export declare function loadOnboardingJournal(file?: string): OnboardingJournal;
19
+ export declare function saveOnboardingJournal(journal: OnboardingJournal, file?: string): void;
20
+ export declare function updateOnboardingStep(journal: OnboardingJournal, name: OnboardingStepName, status: OnboardingStepStatus, detail?: string, error?: string): void;
@@ -0,0 +1,100 @@
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.onboardingJournalPath = onboardingJournalPath;
37
+ exports.createOnboardingJournal = createOnboardingJournal;
38
+ exports.loadOnboardingJournal = loadOnboardingJournal;
39
+ exports.saveOnboardingJournal = saveOnboardingJournal;
40
+ exports.updateOnboardingStep = updateOnboardingStep;
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const STEP_DEFINITIONS = [
45
+ ['preflight', true],
46
+ ['enrollment', true],
47
+ ['protection', true],
48
+ ['daemon', false],
49
+ ['discovery', false],
50
+ ['schedule', false],
51
+ ['verification', true],
52
+ ];
53
+ function onboardingJournalPath(home = os.homedir()) {
54
+ return path.join(home, '.fullcourtdefense', 'onboarding.json');
55
+ }
56
+ function createOnboardingJournal() {
57
+ const now = new Date().toISOString();
58
+ return {
59
+ version: 1,
60
+ updatedAt: now,
61
+ nextAction: 'Run fullcourtdefense onboard --resume to begin.',
62
+ steps: Object.fromEntries(STEP_DEFINITIONS.map(([name, critical]) => [
63
+ name,
64
+ { status: 'pending', critical, updatedAt: now },
65
+ ])),
66
+ };
67
+ }
68
+ function loadOnboardingJournal(file = onboardingJournalPath()) {
69
+ try {
70
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
71
+ if (parsed.version === 1 && parsed.steps) {
72
+ const fallback = createOnboardingJournal();
73
+ return {
74
+ ...fallback,
75
+ ...parsed,
76
+ steps: { ...fallback.steps, ...parsed.steps },
77
+ };
78
+ }
79
+ }
80
+ catch {
81
+ // A missing or malformed journal must never prevent repair.
82
+ }
83
+ return createOnboardingJournal();
84
+ }
85
+ function saveOnboardingJournal(journal, file = onboardingJournalPath()) {
86
+ journal.updatedAt = new Date().toISOString();
87
+ fs.mkdirSync(path.dirname(file), { recursive: true });
88
+ const temporary = `${file}.${process.pid}.tmp`;
89
+ fs.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
90
+ fs.renameSync(temporary, file);
91
+ }
92
+ function updateOnboardingStep(journal, name, status, detail, error) {
93
+ journal.steps[name] = {
94
+ ...journal.steps[name],
95
+ status,
96
+ updatedAt: new Date().toISOString(),
97
+ ...(detail ? { detail } : {}),
98
+ ...(error ? { error } : {}),
99
+ };
100
+ }
package/dist/config.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface BotGuardConfig {
4
4
  apiUrl?: string;
5
5
  shieldId?: string;
6
6
  shieldKey?: string;
7
+ /** Windows DPAPI-protected shield key. Preferred over plaintext shieldKey. */
8
+ shieldKeyDpapi?: string;
7
9
  scan?: {
8
10
  endpoint?: string;
9
11
  description?: string;
package/dist/config.js CHANGED
@@ -45,6 +45,7 @@ exports.resolveSystemPrompt = resolveSystemPrompt;
45
45
  const fs = __importStar(require("fs"));
46
46
  const os = __importStar(require("os"));
47
47
  const path = __importStar(require("path"));
48
+ const child_process_1 = require("child_process");
48
49
  const CONFIG_FILENAMES = [
49
50
  '.fullcourtdefense.yml',
50
51
  '.fullcourtdefense.yaml',
@@ -137,6 +138,7 @@ function readConfigFile(filePath) {
137
138
  apiUrl: raw.apiUrl ? interpolateEnv(String(raw.apiUrl)) : undefined,
138
139
  shieldId: raw.shieldId ? interpolateEnv(String(raw.shieldId)) : undefined,
139
140
  shieldKey: raw.shieldKey ? interpolateEnv(String(raw.shieldKey)) : undefined,
141
+ shieldKeyDpapi: raw.shieldKeyDpapi ? String(raw.shieldKeyDpapi) : undefined,
140
142
  scan: raw.scan
141
143
  ? {
142
144
  endpoint: raw.scan.endpoint,
@@ -152,6 +154,33 @@ function readConfigFile(filePath) {
152
154
  : undefined,
153
155
  };
154
156
  }
157
+ function powershellDpapi(script, value) {
158
+ if (process.platform !== 'win32' || !value)
159
+ return undefined;
160
+ try {
161
+ return (0, child_process_1.execFileSync)('powershell.exe', [
162
+ '-NoProfile',
163
+ '-NonInteractive',
164
+ '-ExecutionPolicy', 'Bypass',
165
+ '-Command', script,
166
+ ], {
167
+ encoding: 'utf8',
168
+ stdio: ['ignore', 'pipe', 'ignore'],
169
+ env: { ...process.env, FCD_DPAPI_VALUE: value },
170
+ timeout: 5_000,
171
+ windowsHide: true,
172
+ }).trim() || undefined;
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
178
+ function protectShieldKeyForCurrentWindowsUser(value) {
179
+ return powershellDpapi('$secure=ConvertTo-SecureString -String $env:FCD_DPAPI_VALUE -AsPlainText -Force; ConvertFrom-SecureString -SecureString $secure', value);
180
+ }
181
+ function unprotectShieldKeyForCurrentWindowsUser(value) {
182
+ return powershellDpapi('$secure=ConvertTo-SecureString -String $env:FCD_DPAPI_VALUE; $ptr=[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure); try {[Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)} finally {[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)}', value);
183
+ }
155
184
  function mergeConfig(base, override) {
156
185
  return {
157
186
  ...base,
@@ -213,6 +242,7 @@ function resolveCliCredentials(config, overrides = {}) {
213
242
  || process.env.FULLCOURTDEFENSE_SHIELD_ID
214
243
  || process.env.AGENTGUARD_SHIELD_ID,
215
244
  shieldKey: overrides.shieldKey
245
+ || unprotectShieldKeyForCurrentWindowsUser(config.shieldKeyDpapi || '')
216
246
  || config.shieldKey
217
247
  || process.env.FCD_SHIELD_KEY
218
248
  || process.env.FULLCOURTDEFENSE_SHIELD_KEY
@@ -266,8 +296,21 @@ function writeSetupConfig(target, input) {
266
296
  setTopLevel('apiKey', input.apiKey);
267
297
  setTopLevel('organizationId', input.organizationId);
268
298
  setTopLevel('shieldId', input.shieldId);
269
- setTopLevel('shieldKey', input.shieldKey);
299
+ const protectedShieldKey = input.shieldKey && process.platform === 'win32'
300
+ ? protectShieldKeyForCurrentWindowsUser(input.shieldKey)
301
+ : undefined;
302
+ if (protectedShieldKey) {
303
+ setTopLevel('shieldKeyDpapi', protectedShieldKey);
304
+ // Remove legacy plaintext key on a successful Windows DPAPI migration.
305
+ const plaintextIndex = lines.findIndex(line => /^shieldKey:/.test(line.trim()));
306
+ if (plaintextIndex >= 0)
307
+ lines.splice(plaintextIndex, 1);
308
+ }
309
+ else {
310
+ setTopLevel('shieldKey', input.shieldKey);
311
+ }
270
312
  setTopLevel('apiUrl', input.apiUrl);
313
+ fs.mkdirSync(path.dirname(target), { recursive: true });
271
314
  fs.writeFileSync(target, `${lines.filter((line, idx, arr) => !(line === '' && arr[idx - 1] === '')).join('\n').trim()}\n`, 'utf-8');
272
315
  return target;
273
316
  }
package/dist/index.js CHANGED
@@ -359,6 +359,8 @@ function printHelp() {
359
359
  $ fullcourtdefense login --token <fleet-enrollment-token>
360
360
  $ fullcourtdefense install-all
361
361
  $ fullcourtdefense install-all --auto-protect true --upload
362
+ $ fullcourtdefense onboard --token <fleet-enrollment-token> --resume
363
+ $ fullcourtdefense onboard --dry-run true --json true
362
364
  $ fullcourtdefense discover --surface all --upload --schedule logon
363
365
  $ fullcourtdefense install-mcp-gateway --clients cursor,codex --mcp-command npm --mcp-args "run mcp"
364
366
  $ fullcourtdefense install-codex-mcp-gateway --mcp-command npm --mcp-args "run mcp"
@@ -634,6 +636,8 @@ async function main() {
634
636
  shellGuard: flags['shell-guard'],
635
637
  cmdGuard: flags['cmd-guard'],
636
638
  posixGuard: flags['posix-guard'],
639
+ schedule: flags.schedule,
640
+ scheduleHour: flags['schedule-hour'],
637
641
  };
638
642
  await (0, installAll_1.installAllCommand)(args, config);
639
643
  break;
@@ -650,6 +654,13 @@ async function main() {
650
654
  shellGuard: flags['shell-guard'],
651
655
  cmdGuard: flags['cmd-guard'],
652
656
  posixGuard: flags['posix-guard'],
657
+ schedule: flags.schedule,
658
+ scheduleHour: flags['schedule-hour'],
659
+ resume: flags.resume,
660
+ repair: flags.repair,
661
+ dryRun: flags['dry-run'],
662
+ json: flags.json,
663
+ noDaemon: flags['no-daemon'],
653
664
  };
654
665
  await (0, onboard_1.onboardCommand)(args, config);
655
666
  break;
@@ -0,0 +1,21 @@
1
+ export type IntegrityReason = 'mcp_gateway_missing' | 'cursor_hook_changed' | 'claude_hook_changed' | 'daemon_not_running' | 'runtime_bundle_stale';
2
+ export interface LocalIntegrityReport {
3
+ ok: boolean;
4
+ reasons: IntegrityReason[];
5
+ checkedAt: string;
6
+ protectedMcpConfigs: number;
7
+ discoveredMcpConfigs: number;
8
+ daemonRunning: boolean;
9
+ }
10
+ export declare function isDaemonRunning(): boolean;
11
+ /**
12
+ * Local-only verification of the protection surfaces that a user-mode agent can
13
+ * observe. It deliberately reports gaps instead of claiming EDR-grade tamper
14
+ * resistance: unsupported clients and non-interactive shells remain visible
15
+ * product limitations.
16
+ */
17
+ export declare function getLocalIntegrityReport(options?: {
18
+ requireDaemon?: boolean;
19
+ runtimeBundleFetchedAt?: number;
20
+ runtimeBundleMaxAgeMs?: number;
21
+ }): LocalIntegrityReport;
@@ -0,0 +1,115 @@
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.isDaemonRunning = isDaemonRunning;
37
+ exports.getLocalIntegrityReport = getLocalIntegrityReport;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const discoverPaths_1 = require("./commands/discoverPaths");
42
+ function readText(file) {
43
+ try {
44
+ return fs.readFileSync(file, 'utf8');
45
+ }
46
+ catch {
47
+ return '';
48
+ }
49
+ }
50
+ function hasMcpServers(text) {
51
+ return /"mcpServers"|\[mcp_servers\]|"servers"\s*:/.test(text)
52
+ && /"command"|command\s*=|"url"\s*:/.test(text);
53
+ }
54
+ function isPidAlive(pid) {
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ }
59
+ catch (error) {
60
+ return error.code === 'EPERM';
61
+ }
62
+ }
63
+ function isDaemonRunning() {
64
+ try {
65
+ const pid = Number(readText(path.join(os.homedir(), '.fullcourtdefense', 'daemon.pid')).trim());
66
+ return Number.isFinite(pid) && pid > 0 && isPidAlive(pid);
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ /**
73
+ * Local-only verification of the protection surfaces that a user-mode agent can
74
+ * observe. It deliberately reports gaps instead of claiming EDR-grade tamper
75
+ * resistance: unsupported clients and non-interactive shells remain visible
76
+ * product limitations.
77
+ */
78
+ function getLocalIntegrityReport(options = {}) {
79
+ const reasons = [];
80
+ let discoveredMcpConfigs = 0;
81
+ let protectedMcpConfigs = 0;
82
+ for (const candidate of (0, discoverPaths_1.candidateConfigPaths)(process.cwd())) {
83
+ const text = readText(candidate.path);
84
+ if (!text || !hasMcpServers(text))
85
+ continue;
86
+ discoveredMcpConfigs++;
87
+ if (text.includes('mcp-gateway'))
88
+ protectedMcpConfigs++;
89
+ }
90
+ if (protectedMcpConfigs < discoveredMcpConfigs)
91
+ reasons.push('mcp_gateway_missing');
92
+ const home = os.homedir();
93
+ const cursorHooks = readText(path.join(home, '.cursor', 'hooks.json'));
94
+ if (cursorHooks && !(/fcd-managed|fullcourtdefense|botguard/i.test(cursorHooks))) {
95
+ reasons.push('cursor_hook_changed');
96
+ }
97
+ const claudeHooks = readText(path.join(home, '.claude', 'settings.json'));
98
+ if (claudeHooks && !(/fcd-managed|fullcourtdefense|botguard/i.test(claudeHooks))) {
99
+ reasons.push('claude_hook_changed');
100
+ }
101
+ const daemonRunning = isDaemonRunning();
102
+ if (options.requireDaemon && !daemonRunning)
103
+ reasons.push('daemon_not_running');
104
+ if (options.runtimeBundleFetchedAt && Date.now() - options.runtimeBundleFetchedAt > (options.runtimeBundleMaxAgeMs ?? 5 * 60_000)) {
105
+ reasons.push('runtime_bundle_stale');
106
+ }
107
+ return {
108
+ ok: reasons.length === 0,
109
+ reasons,
110
+ checkedAt: new Date().toISOString(),
111
+ protectedMcpConfigs,
112
+ discoveredMcpConfigs,
113
+ daemonRunning,
114
+ };
115
+ }
@@ -29,6 +29,9 @@ export interface FlushInput {
29
29
  agentVersion?: string;
30
30
  /** Whether protection points still look intact (tamper signal). */
31
31
  integrityOk?: boolean;
32
+ /** Machine-readable reasons for partial protection, never prompt/tool content. */
33
+ integrityReasons?: string[];
34
+ integrityCheckedAt?: string;
32
35
  timeoutMs?: number;
33
36
  }
34
37
  /** Drain the spool to the backend in one batch (+ optional heartbeat). Returns accepted count. */
package/dist/telemetry.js CHANGED
@@ -138,6 +138,8 @@ async function flushSpool(input) {
138
138
  ? {
139
139
  agentVersion: input.agentVersion,
140
140
  integrityOk: input.integrityOk,
141
+ integrityReasons: input.integrityReasons,
142
+ integrityCheckedAt: input.integrityCheckedAt,
141
143
  coverage: 'hooks',
142
144
  hostname: identity.hostname,
143
145
  // Windows-only: current PowerShell audit coverage (ScriptBlock
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.15.0"
2
+ "version": "1.15.1"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.15.0",
3
+ "version": "1.15.1",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -27,9 +27,13 @@
27
27
  "test:per-server-agent-name": "npm run build && node scripts/test-per-server-agent-name.js",
28
28
  "test:remote-mcp-gateway": "npm run build && node scripts/test-remote-mcp-gateway.js",
29
29
  "test:e2e-onboarding-personas": "npm run build && node scripts/test-e2e-onboarding-personas.js",
30
+ "test:onboarding-transaction": "npm run build && node scripts/test-onboarding-transaction.js",
30
31
  "test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
31
32
  "test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
32
33
  "test:daemon-e2e": "npm run build && node scripts/test-daemon-e2e.js",
34
+ "test:integrity": "npm run build && node scripts/test-integrity.js",
35
+ "test:dpapi-config": "npm run build && node scripts/test-dpapi-config.js",
36
+ "build:msi": "powershell -NoProfile -ExecutionPolicy Bypass -File installer/windows/Build-Msi.ps1",
33
37
  "prepublishOnly": "npm run build"
34
38
  },
35
39
  "keywords": [