fullcourtdefense-cli 1.22.7 → 1.23.0

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.
@@ -0,0 +1,7 @@
1
+ export declare function backupManifestPath(): string;
2
+ /** Every recorded backup path (absent/corrupt manifest = empty list). */
3
+ export declare function readBackupManifest(): string[];
4
+ /** Record one backup file path. Deduped, capped, never throws. */
5
+ export declare function recordConfigBackup(backupPath: string): void;
6
+ /** Drop entries whose backup file no longer exists (post-cleanup compaction). */
7
+ export declare function pruneBackupManifest(): void;
@@ -0,0 +1,108 @@
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.backupManifestPath = backupManifestPath;
37
+ exports.readBackupManifest = readBackupManifest;
38
+ exports.recordConfigBackup = recordConfigBackup;
39
+ exports.pruneBackupManifest = pruneBackupManifest;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ /**
44
+ * Ledger of every `*.fcd-backup-*` config safety copy the CLI creates.
45
+ *
46
+ * MCP-config edits back the original file up next to it — including
47
+ * PROJECT-LOCAL configs (a repo's .cursor/mcp.json), which live in places no
48
+ * directory scan can enumerate. Those backups can embed live credentials, so
49
+ * `uninstall` / `verify-removed` must be able to find every one of them: each
50
+ * backup path is recorded here at creation time and the manifest is the
51
+ * authoritative list at cleanup time (union-ed with the known-dir scan for
52
+ * backups created by older CLI versions).
53
+ *
54
+ * Best-effort by design: a manifest write failure must never break a protect
55
+ * operation, and cleanup falls back to the directory scan.
56
+ */
57
+ const MANIFEST_NAME = 'backup-manifest.json';
58
+ const MAX_ENTRIES = 2000;
59
+ function backupManifestPath() {
60
+ return path.join(os.homedir(), '.fullcourtdefense', MANIFEST_NAME);
61
+ }
62
+ /** Every recorded backup path (absent/corrupt manifest = empty list). */
63
+ function readBackupManifest() {
64
+ try {
65
+ const raw = JSON.parse(fs.readFileSync(backupManifestPath(), 'utf8'));
66
+ const entries = Array.isArray(raw?.backups) ? raw.backups : [];
67
+ return entries.filter((entry) => typeof entry === 'string' && entry.length > 0);
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ /** Record one backup file path. Deduped, capped, never throws. */
74
+ function recordConfigBackup(backupPath) {
75
+ try {
76
+ const existing = readBackupManifest();
77
+ if (existing.includes(backupPath))
78
+ return;
79
+ const entries = [...existing, backupPath].slice(-MAX_ENTRIES);
80
+ const file = backupManifestPath();
81
+ fs.mkdirSync(path.dirname(file), { recursive: true });
82
+ fs.writeFileSync(file, `${JSON.stringify({ backups: entries }, null, 2)}\n`, 'utf8');
83
+ }
84
+ catch { /* best effort — cleanup still has the directory scan */ }
85
+ }
86
+ /** Drop entries whose backup file no longer exists (post-cleanup compaction). */
87
+ function pruneBackupManifest() {
88
+ try {
89
+ const existing = readBackupManifest();
90
+ const remaining = existing.filter(entry => {
91
+ try {
92
+ return fs.existsSync(entry);
93
+ }
94
+ catch {
95
+ return true;
96
+ }
97
+ });
98
+ if (remaining.length === existing.length)
99
+ return;
100
+ const file = backupManifestPath();
101
+ if (remaining.length === 0) {
102
+ fs.rmSync(file, { force: true });
103
+ return;
104
+ }
105
+ fs.writeFileSync(file, `${JSON.stringify({ backups: remaining }, null, 2)}\n`, 'utf8');
106
+ }
107
+ catch { /* best effort */ }
108
+ }
@@ -0,0 +1,42 @@
1
+ import { BotGuardConfig } from '../config';
2
+ /**
3
+ * `fullcourtdefense ci-protect` — one-command runtime protection for CI jobs.
4
+ *
5
+ * A CI runner is born, runs one job with an AI agent inside, and dies — no
6
+ * human to click a consent dialog, no durable disk for a fleet token. So the
7
+ * flow differs from laptop onboarding in exactly two ways:
8
+ *
9
+ * 1. Auth is the org API key (FCD_API_KEY repo secret) — the admin who put
10
+ * it there consented for the pipeline.
11
+ * 2. Fleet identity is the PIPELINE (provider/repo/workflow), not the
12
+ * runner: the backend converges every run onto one machine record, and
13
+ * we export FCD_MACHINE_ID so the hooks installed here attribute all
14
+ * events to that pipeline record.
15
+ *
16
+ * Everything else is the exact laptop stack: same Claude-format hook, same
17
+ * MCP gateway, same vendored policy engine enforcing offline.
18
+ */
19
+ export interface CiProtectArgs {
20
+ apiKey?: string;
21
+ apiUrl?: string;
22
+ provider?: string;
23
+ repo?: string;
24
+ workflow?: string;
25
+ runId?: string;
26
+ runUrl?: string;
27
+ /** 'false' => skip installing the Claude-format runtime hook. */
28
+ hooks?: string;
29
+ /** 'false' => skip wrapping MCP client configs with the gateway. */
30
+ gateway?: string;
31
+ }
32
+ interface CiContext {
33
+ provider: string;
34
+ repo?: string;
35
+ workflow?: string;
36
+ runId?: string;
37
+ runUrl?: string;
38
+ }
39
+ /** Detect the pipeline from standard CI env vars (GitHub Actions, GitLab CI). */
40
+ export declare function detectCiContext(env?: NodeJS.ProcessEnv): CiContext | undefined;
41
+ export declare function ciProtectCommand(args: CiProtectArgs, config: BotGuardConfig): Promise<void>;
42
+ export {};
@@ -0,0 +1,191 @@
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.detectCiContext = detectCiContext;
37
+ exports.ciProtectCommand = ciProtectCommand;
38
+ const fs = __importStar(require("fs"));
39
+ const config_1 = require("../config");
40
+ const installClaudeHook_1 = require("./installClaudeHook");
41
+ const mcpGateway_1 = require("./mcpGateway");
42
+ /** Detect the pipeline from standard CI env vars (GitHub Actions, GitLab CI). */
43
+ function detectCiContext(env = process.env) {
44
+ if (env.GITHUB_ACTIONS === 'true') {
45
+ const repo = env.GITHUB_REPOSITORY || undefined;
46
+ const runId = env.GITHUB_RUN_ID || undefined;
47
+ return {
48
+ provider: 'github-actions',
49
+ repo,
50
+ workflow: env.GITHUB_WORKFLOW || undefined,
51
+ runId,
52
+ runUrl: repo && runId ? `${env.GITHUB_SERVER_URL || 'https://github.com'}/${repo}/actions/runs/${runId}` : undefined,
53
+ };
54
+ }
55
+ if (env.GITLAB_CI === 'true') {
56
+ return {
57
+ provider: 'gitlab-ci',
58
+ repo: env.CI_PROJECT_PATH || undefined,
59
+ workflow: env.CI_JOB_NAME || env.CI_PIPELINE_NAME || undefined,
60
+ runId: env.CI_PIPELINE_ID || undefined,
61
+ runUrl: env.CI_PIPELINE_URL || undefined,
62
+ };
63
+ }
64
+ return undefined;
65
+ }
66
+ /**
67
+ * Best-effort log streaming to the dashboard's CI Pipelines page — the admin
68
+ * watches this job's protection steps live without opening GitHub. Never
69
+ * throws and never blocks the job for more than a few seconds.
70
+ */
71
+ function ciLogSender(apiUrl, apiKey, ctx) {
72
+ return async (lines, status) => {
73
+ try {
74
+ await fetch(`${apiUrl}/api/cli/ci-log`, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
77
+ body: JSON.stringify({ ...ctx, lines, status }),
78
+ signal: AbortSignal.timeout(4000),
79
+ });
80
+ }
81
+ catch { /* log delivery is best-effort */ }
82
+ };
83
+ }
84
+ async function ciProtectCommand(args, config) {
85
+ const creds = (0, config_1.resolveCliCredentials)(config, { apiUrl: args.apiUrl });
86
+ const apiUrl = creds.apiUrl;
87
+ const apiKey = (args.apiKey || process.env.FCD_API_KEY || '').trim();
88
+ if (!apiKey) {
89
+ throw new Error('An organization API key is required. Add FCD_API_KEY to the repo/pipeline secrets '
90
+ + '(create one in the dashboard: Workspace → API keys) or pass --api-key.');
91
+ }
92
+ const detected = detectCiContext();
93
+ const provider = (args.provider || detected?.provider || 'generic-ci').trim();
94
+ const repo = (args.repo || detected?.repo || '').trim();
95
+ const workflow = (args.workflow || detected?.workflow || '').trim();
96
+ if (!repo || !workflow) {
97
+ throw new Error('Could not detect the pipeline identity. On GitHub Actions / GitLab CI it is auto-detected; '
98
+ + 'elsewhere pass --repo <org/repo> --workflow <name>.');
99
+ }
100
+ const runId = (args.runId || detected?.runId || '').trim() || undefined;
101
+ const runUrl = (args.runUrl || detected?.runUrl || '').trim() || undefined;
102
+ console.log('\x1b[1m\x1b[36mFullCourtDefense — CI runtime protection\x1b[0m');
103
+ console.log(` Pipeline: ${provider}/${repo}/${workflow}`);
104
+ if (runId)
105
+ console.log(` Run: ${runId}`);
106
+ const sendLog = ciLogSender(apiUrl, apiKey, { provider, repo, workflow, runId, runUrl });
107
+ const resp = await fetch(`${apiUrl}/api/cli/enroll/ci`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
110
+ body: JSON.stringify({
111
+ provider,
112
+ repo,
113
+ workflow,
114
+ runId,
115
+ runUrl,
116
+ cliVersion: process.env.npm_package_version,
117
+ }),
118
+ });
119
+ const data = (await resp.json().catch(() => ({})));
120
+ if (!resp.ok || data.success === false || !data.data) {
121
+ const reason = data.error || `CI enrollment failed (HTTP ${resp.status})`;
122
+ await sendLog([{ level: 'error', message: `Enrollment failed: ${reason}` }], 'failed');
123
+ throw new Error(reason);
124
+ }
125
+ const result = data.data;
126
+ const savedPath = (0, config_1.saveSetupConfig)({
127
+ organizationId: result.organizationId,
128
+ shieldId: result.shieldId,
129
+ shieldKey: result.shieldKey,
130
+ apiUrl,
131
+ });
132
+ // Attribute every subsequent hook/gateway event in this job to the PIPELINE
133
+ // machine record: current process + GITHUB_ENV for the steps that follow
134
+ // (where the AI agent actually runs).
135
+ const identityEnv = {
136
+ FCD_MACHINE_ID: result.machineId,
137
+ FCD_DEVELOPER_NAME: `ci@${repo.toLowerCase()}`,
138
+ FCD_MACHINE_HOSTNAME: result.pipeline.key,
139
+ };
140
+ for (const [key, value] of Object.entries(identityEnv))
141
+ process.env[key] = value;
142
+ if (process.env.GITHUB_ENV) {
143
+ try {
144
+ fs.appendFileSync(process.env.GITHUB_ENV, Object.entries(identityEnv).map(([k, v]) => `${k}=${v}\n`).join(''));
145
+ }
146
+ catch { /* non-GitHub runner with a stray env var — identity still set for this process */ }
147
+ }
148
+ console.log(`\x1b[32m✓ ${result.reused ? 'Pipeline re-enrolled' : 'Pipeline enrolled'}\x1b[0m ${result.shieldName}`);
149
+ console.log(` Mode: ${result.enforcementMode} · Config: ${savedPath}`);
150
+ // Same protection stack as laptops. The Claude-format hook covers Claude
151
+ // Code, VS Code agent mode, and Copilot CLI — the common CI agents.
152
+ if (args.hooks !== 'false') {
153
+ console.log('\n\x1b[1mInstalling runtime hook (Claude Code / VS Code / Copilot CLI)…\x1b[0m');
154
+ const hookArgs = {
155
+ shieldId: result.shieldId,
156
+ shieldKey: result.shieldKey,
157
+ apiUrl,
158
+ events: 'tools,prompt',
159
+ };
160
+ try {
161
+ await (0, installClaudeHook_1.installClaudeHookCommand)(hookArgs, config);
162
+ await sendLog([{ level: 'success', message: 'Runtime hook installed (Claude Code / VS Code / Copilot CLI)' }]);
163
+ }
164
+ catch (error) {
165
+ const reason = error instanceof Error ? error.message : String(error);
166
+ console.log(`Hook install skipped: ${reason}`);
167
+ await sendLog([{ level: 'warn', message: `Runtime hook install skipped: ${reason}` }]);
168
+ }
169
+ }
170
+ if (args.gateway !== 'false') {
171
+ console.log('\n\x1b[1mWrapping MCP client configs with the gateway…\x1b[0m');
172
+ const gatewayArgs = {
173
+ shieldId: result.shieldId,
174
+ shieldKey: result.shieldKey,
175
+ apiUrl,
176
+ clients: 'all',
177
+ };
178
+ try {
179
+ await (0, mcpGateway_1.protectAllCommand)(gatewayArgs, config);
180
+ await sendLog([{ level: 'success', message: 'MCP client configs wrapped with the FullCourtDefense gateway' }]);
181
+ }
182
+ catch (error) {
183
+ const reason = error instanceof Error ? error.message : String(error);
184
+ console.log(`MCP gateway wrap skipped: ${reason}`);
185
+ await sendLog([{ level: 'warn', message: `MCP gateway wrap skipped: ${reason}` }]);
186
+ }
187
+ }
188
+ console.log('\n\x1b[32mDone.\x1b[0m This job\'s AI tool calls are now policy-checked and streamed to the fleet console.');
189
+ console.log(`Pipeline appears in AI Fleet → Machines → CI pipelines as \x1b[1m${repo} · ${workflow}\x1b[0m.`);
190
+ await sendLog([{ level: 'success', message: `Protection active (${result.enforcementMode} mode) — this job's AI tool calls are policy-checked and recorded` }], 'succeeded');
191
+ }
@@ -1187,6 +1187,8 @@ async function runDaemon(args, config) {
1187
1187
  const result = (0, windowsAudit_1.pruneTranscripts)();
1188
1188
  if (result.pruned > 0)
1189
1189
  log(`Transcript retention: pruned ${result.pruned} transcript file(s) older than ${(0, windowsAudit_1.transcriptRetentionDays)()} day(s).`);
1190
+ if ((result.prunedBySize || 0) > 0)
1191
+ log(`Transcript retention: pruned ${result.prunedBySize} more transcript file(s) (oldest first) to stay under the ${(0, windowsAudit_1.transcriptMaxMb)()}MB size budget.`);
1190
1192
  }
1191
1193
  catch { /* never let housekeeping hurt the daemon */ }
1192
1194
  };
@@ -258,9 +258,18 @@ function disabled(options) {
258
258
  function isEnabled(itemId, options) {
259
259
  return !disabled(options).has(itemId);
260
260
  }
261
+ /**
262
+ * Items that WARN by default (catalog-level default action). Applies only when
263
+ * the snapshot carries no explicit action for the item — an org override to
264
+ * 'block' ships explicitly in the snapshot and wins. Keeps offline machines
265
+ * and stale snapshots on the warn-first rollout instead of hard-blocking.
266
+ */
267
+ const DEFAULT_WARN_BUILT_INS = new Set(['env_workspace']);
261
268
  function actionFor(itemId, options) {
262
269
  const action = options?.itemActions?.[itemId];
263
- return action === 'warn' || action === 'mask' ? action : 'block';
270
+ if (action === 'warn' || action === 'mask' || action === 'block')
271
+ return action;
272
+ return DEFAULT_WARN_BUILT_INS.has(itemId) ? 'warn' : 'block';
264
273
  }
265
274
  function builtIn(itemId, categoryId, category, ruleId, reason, findingEvidence, explanation, options) {
266
275
  if (!isEnabled(itemId, options))
@@ -396,8 +405,14 @@ function containsSensitiveCredentialPath(value) {
396
405
  if (check.re.test(text))
397
406
  return { itemId: check.itemId, label: check.label };
398
407
  }
399
- if (traversal && /(?:^|\/)\.env(?:\.[a-z0-9_-]+)?(?:$|[/?#\s'"])/.test(text)) {
400
- return { itemId: 'env_traversal', label: 'environment file via path traversal' };
408
+ if (/(?:^|\/)\.env(?:\.[a-z0-9_-]+)?(?:$|[/?#\s'"])/.test(text)) {
409
+ // Traversal to a .env outside the workspace stays a hard block
410
+ // (env_traversal); a plain workspace .env is a separate warn-by-default
411
+ // item (env_workspace) — agents legitimately read it during development,
412
+ // but the fleet should still see every touch.
413
+ return traversal
414
+ ? { itemId: 'env_traversal', label: 'environment file via path traversal' }
415
+ : { itemId: 'env_workspace', label: 'workspace environment file' };
401
416
  }
402
417
  return undefined;
403
418
  }
@@ -1097,6 +1097,11 @@ async function discoverCommand(args, config) {
1097
1097
  printAgentFilesReport(agentFiles, silent);
1098
1098
  if (posture)
1099
1099
  printPostureReport(posture, silent);
1100
+ // Explicit scope boundary: a broad security/credential report during an
1101
+ // active incident can reasonably be over-read as endpoint protection.
1102
+ if (!silent) {
1103
+ console.log(`${COLOR.gray}Scope: this discovery inventories AI agents, MCP servers, credentials, and machine posture. It is NOT antivirus/EDR — binaries, running processes, and quarantined files are not scanned. Keep your endpoint protection (e.g. Microsoft Defender) active alongside it.${COLOR.reset}`);
1104
+ }
1100
1105
  if (uploadRequested) {
1101
1106
  console.log('');
1102
1107
  await maybeUpload();
@@ -47,6 +47,7 @@ const taintLedger_1 = require("./taintLedger");
47
47
  const notify_1 = require("../notify");
48
48
  const distress_1 = require("../distress");
49
49
  const policyGateHealth_1 = require("../policyGateHealth");
50
+ const machineIdentity_1 = require("../machineIdentity");
50
51
  const actionPolicyEngine_1 = require("../actionPolicyEngine");
51
52
  const DEBUG_LOG = path.join(os.homedir(), '.fullcourtdefense-hook.log');
52
53
  /**
@@ -533,7 +534,16 @@ function machineMetadata(client = 'cursor') {
533
534
  username = os.userInfo().username;
534
535
  }
535
536
  catch { /* ignore */ }
537
+ // Stable fleet machine identity: lets the backend attribute this decision to
538
+ // THIS machine exactly (timeline + behavior baseline), instead of matching
539
+ // by hostname. Best-effort — identity must never break the hook.
540
+ let machineId;
541
+ try {
542
+ machineId = (0, machineIdentity_1.getMachineIdentity)().machineId;
543
+ }
544
+ catch { /* ignore */ }
536
545
  return {
546
+ ...(machineId ? { machineId } : {}),
537
547
  developerName: developerId(),
538
548
  machineName: os.hostname(),
539
549
  os: `${os.platform()} ${os.release()}`,
@@ -44,7 +44,8 @@ async function installAllCommand(args, config) {
44
44
  // Prompt secrets are checked locally; prompt text never leaves the machine.
45
45
  // No failClosed flag: the offline stance is server-authoritative (from
46
46
  // the runtime bundle), unified with the Claude/VS Code hook below.
47
- events: 'prompt,shell,mcp',
47
+ // file + read close the "agent reads ~/.aws/credentials silently" hole.
48
+ events: 'prompt,shell,mcp,file,read',
48
49
  };
49
50
  try {
50
51
  await (0, installCursorHook_1.installCursorHookCommand)(hookArgs, config);
@@ -72,9 +73,11 @@ async function installAllCommand(args, config) {
72
73
  }
73
74
  // Windows-wide command auditing — PowerShell ScriptBlock Logging + Transcription.
74
75
  // Covers commands typed in ANY PowerShell (inside or outside the IDE), not just
75
- // agent-driven actions. Requires ONE admin (UAC) approval; failure is a warning,
76
- // never an install blocker missing coverage shows on the fleet dashboard.
77
- if (process.platform === 'win32' && args.windowsAudit !== 'false') {
76
+ // agent-driven actions. OFF BY DEFAULT: it flips machine-wide HKLM policy and
77
+ // transcription writes sizeable plaintext logs to ProgramData that footprint
78
+ // is an explicit opt-in (--windows-audit true, or org policy), not a side
79
+ // effect of installing protection. Requires ONE admin (UAC) approval.
80
+ if (process.platform === 'win32' && args.windowsAudit === 'true') {
78
81
  console.log('\n\x1b[1mEnabling Windows PowerShell audit logging (ScriptBlock Logging + Transcription)…\x1b[0m');
79
82
  try {
80
83
  const current = (0, windowsAudit_1.getWindowsAuditStatus)();
@@ -97,6 +100,9 @@ async function installAllCommand(args, config) {
97
100
  console.log(`\x1b[33m⚠ Warning: PowerShell audit logging step failed (${error instanceof Error ? error.message : String(error)}). Installation continues.\x1b[0m`);
98
101
  }
99
102
  }
103
+ else if (process.platform === 'win32' && args.windowsAudit !== 'false') {
104
+ console.log('\n\x1b[2mPowerShell audit logging (ScriptBlock Logging + Transcription) is OFF by default — it changes machine-wide policy and writes command transcripts to disk. Opt in with --windows-audit true, or later via: fullcourtdefense windows-audit --enable true\x1b[0m');
105
+ }
100
106
  // Interactive PowerShell guard — blocks dangerous commands TYPED in a
101
107
  // terminal (inside or outside the IDE) using the same rule families the IDE
102
108
  // hooks enforce, from the local cache (no admin needed, no network hot path).
@@ -147,7 +147,7 @@ function repairCursorManagedHooks() {
147
147
  const json = readHooksJson(before.file);
148
148
  let changed = false;
149
149
  if (before.managedEntries === 0) {
150
- for (const event of ['prompt', 'shell', 'mcp']) {
150
+ for (const event of ['prompt', 'shell', 'mcp', 'file', 'read']) {
151
151
  const { hookKey, flag, timeoutSec } = EVENT_MAP[event];
152
152
  const list = Array.isArray(json.hooks[hookKey]) ? json.hooks[hookKey] : [];
153
153
  list.push({
@@ -218,7 +218,11 @@ async function installCursorHookCommand(args, config) {
218
218
  const projectScope = args.project === 'true';
219
219
  const shadow = args.shadow === 'true';
220
220
  const file = hooksJsonPath(projectScope);
221
- const requested = (args.events || 'prompt,shell,mcp')
221
+ // file + read are in the default set: without them a Cursor agent can read
222
+ // ~/.aws/credentials or write a poisoned config with no hook ever firing —
223
+ // the exact coverage hole customers assume is closed. Opt out with
224
+ // --events prompt,shell,mcp.
225
+ const requested = (args.events || 'prompt,shell,mcp,file,read')
222
226
  .split(/[\s,]+/).map((e) => e.trim().toLowerCase()).filter(Boolean);
223
227
  const events = requested.filter((e) => EVENT_MAP[e]);
224
228
  if (events.length === 0) {
@@ -63,6 +63,8 @@ const telemetry_1 = require("../telemetry");
63
63
  const notify_1 = require("../notify");
64
64
  const distress_1 = require("../distress");
65
65
  const actionPolicyEngine_1 = require("../actionPolicyEngine");
66
+ const machineIdentity_1 = require("../machineIdentity");
67
+ const backupManifest_1 = require("../backupManifest");
66
68
  const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
67
69
  const MANAGED_SERVER_NAME = 'agentguard-gateway';
68
70
  const INSTALL_GATEWAY_ARGS = { includeShieldKey: false };
@@ -230,7 +232,15 @@ function resolveGatewayConfig(args, config, defaults = {}) {
230
232
  }
231
233
  function machineMetadata(developerName, agentClient = 'cursor') {
232
234
  const sessionId = runtimeSessionId();
235
+ // Exact fleet machine attribution for the backend's runtime decision mirror
236
+ // (timeline + behavior baseline). Best-effort — never break the gateway.
237
+ let machineId;
238
+ try {
239
+ machineId = (0, machineIdentity_1.getMachineIdentity)().machineId;
240
+ }
241
+ catch { /* ignore */ }
233
242
  return {
243
+ ...(machineId ? { machineId } : {}),
234
244
  developerName: developerName || machineScopedUser(),
235
245
  machineName: os.hostname(),
236
246
  os: `${os.platform()} ${os.release()}`,
@@ -1621,6 +1631,9 @@ function backupFile(file) {
1621
1631
  try {
1622
1632
  const bak = `${file}.fcd-backup-${Date.now()}`;
1623
1633
  fs.copyFileSync(file, bak);
1634
+ // Backups can embed live credentials and land in project-local dirs no
1635
+ // scan can enumerate — record each one so uninstall can delete them all.
1636
+ (0, backupManifest_1.recordConfigBackup)(bak);
1624
1637
  }
1625
1638
  catch { /* best effort */ }
1626
1639
  }
@@ -15,6 +15,8 @@ export interface OnboardArgs extends InstallAllArgs {
15
15
  json?: string;
16
16
  /** Do not enable optional self-healing protection. */
17
17
  noDaemon?: string;
18
+ /** 'true' => skip the interactive trust disclosure (automation/MDM; the MSI consents via its own dialog). */
19
+ acceptDisclosure?: string;
18
20
  }
19
21
  /**
20
22
  * `fullcourtdefense onboard` — the one-command path from a fresh machine to a
@@ -63,6 +63,50 @@ const YELLOW = '\x1b[33m';
63
63
  const BOLD = '\x1b[1m';
64
64
  const DIM = '\x1b[2m';
65
65
  const RESET = '\x1b[0m';
66
+ /**
67
+ * The trust disclosure — the npm/CLI twin of the MSI EnrollmentDlg text
68
+ * (FullCourtDefense.wxs). A user installing from npm must see the same trust
69
+ * boundary the Windows installer shows BEFORE anything touches the machine:
70
+ * what gets installed, what runs in the background, and what data leaves the
71
+ * device. Keep both texts in sync when either changes.
72
+ *
73
+ * Consent model: interactive terminals must confirm; `--accept-disclosure
74
+ * true` / FCD_ACCEPT_DISCLOSURE=1 skip the prompt for automation and MDM;
75
+ * non-interactive runs (MSI custom action — consent was the Install click)
76
+ * print the text and continue, because a hidden prompt would hang the install.
77
+ * Returns false when the user explicitly declined.
78
+ */
79
+ async function confirmTrustDisclosure(args) {
80
+ console.log(`${BOLD}Before continuing, understand what FullCourtDefense sets up on this machine:${RESET}`);
81
+ console.log(`${DIM} - background protection tasks (resident daemon + watchdog) that start with your session`);
82
+ console.log(' - IDE/terminal hooks, and MCP tool traffic routed through a local security gateway');
83
+ console.log(" - agent/tool activity (commands, tool-call argument summaries) sent to your organization's");
84
+ console.log(' FullCourtDefense console for policy decisions and audit');
85
+ console.log(' - intended for organization-managed devices');
86
+ console.log(` - removed with \`fullcourtdefense uninstall\`${process.platform === 'win32' ? ' (MSI installs: Windows Settings > Apps)' : ''}${RESET}`);
87
+ console.log('');
88
+ const accepted = args.acceptDisclosure === 'true'
89
+ || process.env.FCD_ACCEPT_DISCLOSURE === '1'
90
+ || process.env.FCD_ACCEPT_DISCLOSURE === 'true';
91
+ if (accepted) {
92
+ console.log(`${DIM}Disclosure accepted via --accept-disclosure / FCD_ACCEPT_DISCLOSURE.${RESET}\n`);
93
+ return true;
94
+ }
95
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
96
+ // Non-interactive (MSI custom action, CI, MDM) — consent happened upstream
97
+ // (installer dialog / management policy); prompting here would hang.
98
+ console.log(`${DIM}Non-interactive run — continuing (pass --accept-disclosure true to make consent explicit in automation).${RESET}\n`);
99
+ return true;
100
+ }
101
+ const readline = await Promise.resolve().then(() => __importStar(require('readline')));
102
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
103
+ const answer = await new Promise(resolve => {
104
+ rl.question(`${BOLD}Continue? [y/N] ${RESET}`, resolve);
105
+ });
106
+ rl.close();
107
+ console.log('');
108
+ return /^y(es)?$/i.test(answer.trim());
109
+ }
66
110
  function printCheck(check) {
67
111
  const mark = check.ok ? `${GREEN}✓${RESET}` : check.optional ? `${YELLOW}○${RESET}` : `${RED}✗${RESET}`;
68
112
  const detail = check.detail ? ` ${DIM}${check.detail}${RESET}` : '';
@@ -193,10 +237,10 @@ function checkTerminalGuards() {
193
237
  const audit = (0, windowsAudit_1.getWindowsAuditStatus)();
194
238
  const auditOk = audit.scriptBlockLogging && audit.transcription;
195
239
  checks.push({
196
- label: 'PowerShell audit logging',
240
+ label: 'PowerShell audit logging (opt-in)',
197
241
  ok: auditOk,
198
242
  optional: true,
199
- detail: auditOk ? undefined : 'run windows-audit --enable true (needs one UAC approval)',
243
+ detail: auditOk ? undefined : 'off by default — enable with windows-audit --enable true (one UAC approval; writes transcripts to ProgramData)',
200
244
  });
201
245
  }
202
246
  else {
@@ -234,6 +278,13 @@ async function onboardCommand(args, config) {
234
278
  };
235
279
  console.log(`\n${BOLD}\x1b[36mFullCourtDefense — machine onboarding${RESET}`);
236
280
  console.log(`${DIM}Connectivity -> enrollment -> protection -> verification. One command.${RESET}\n`);
281
+ // Trust disclosure first — nothing touches the machine before consent.
282
+ // Dry runs report the plan without changing anything, so no prompt there.
283
+ if (!dryRun && !(await confirmTrustDisclosure(args))) {
284
+ console.log(`${YELLOW}Onboarding cancelled — nothing was installed or changed on this machine.${RESET}`);
285
+ process.exitCode = 1;
286
+ return;
287
+ }
237
288
  // Onboarding is a persisted transaction: completed steps are reused on any
238
289
  // re-run, so `--resume` is an explicit alias that also surfaces the journal.
239
290
  const resume = args.resume === 'true' || args.resume === '';