fullcourtdefense-cli 1.22.8 → 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,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
+ }
package/dist/index.js CHANGED
@@ -49,6 +49,7 @@ const installCursorHook_1 = require("./commands/installCursorHook");
49
49
  const installClaudeHook_1 = require("./commands/installClaudeHook");
50
50
  const mcpGateway_1 = require("./commands/mcpGateway");
51
51
  const installAll_1 = require("./commands/installAll");
52
+ const ciProtect_1 = require("./commands/ciProtect");
52
53
  const onboard_1 = require("./commands/onboard");
53
54
  const uninstallAll_1 = require("./commands/uninstallAll");
54
55
  const verifyRemoved_1 = require("./commands/verifyRemoved");
@@ -178,6 +179,11 @@ function printHelp() {
178
179
  Use --schedule logon to upload inventory/posture at machine login.
179
180
  agent-ci CI/CD gate for agents and tools only. Fails builds on risky MCP,
180
181
  agent instruction, gateway, policy, or secret drift. Not a bot scan.
182
+ ci-protect One command for CI jobs that run AI agents (Claude Code, Copilot,
183
+ MCP tools): enrolls the PIPELINE as an ephemeral fleet endpoint
184
+ (auto-detected on GitHub Actions / GitLab CI), installs the runtime
185
+ hook + MCP gateway, and streams every tool call to the fleet
186
+ console. Auth: FCD_API_KEY secret (org API key).
181
187
  install-cursor-hook
182
188
  Installs a Cursor hook so EVERY agent action on this machine is
183
189
  checked against your org's Action Policies — in any repo/folder.
@@ -585,6 +591,21 @@ async function main() {
585
591
  await (0, discover_1.discoverCommand)(args, config);
586
592
  break;
587
593
  }
594
+ case 'ci-protect': {
595
+ const args = {
596
+ apiKey: flags['api-key'],
597
+ apiUrl: flags['api-url'],
598
+ provider: flags.provider,
599
+ repo: flags.repo,
600
+ workflow: flags.workflow,
601
+ runId: flags['run-id'],
602
+ runUrl: flags['run-url'],
603
+ hooks: flags.hooks,
604
+ gateway: flags.gateway,
605
+ };
606
+ await (0, ciProtect_1.ciProtectCommand)(args, config);
607
+ break;
608
+ }
588
609
  case 'agent-ci': {
589
610
  const args = {
590
611
  failOn: flags['fail-on'],
@@ -131,6 +131,22 @@ function friendlyOs() {
131
131
  function getMachineIdentity() {
132
132
  if (cached)
133
133
  return cached;
134
+ // Ephemeral fleet override: on CI runners the PIPELINE is the fleet identity,
135
+ // not the disposable runner hardware. `ci-protect` enrolls the pipeline and
136
+ // exports these envs (via GITHUB_ENV) so every subsequent hook/gateway
137
+ // process in the job attributes its events to the pipeline machine record.
138
+ const ciMachineId = (process.env.FCD_MACHINE_ID || '').trim();
139
+ if (ciMachineId) {
140
+ cached = {
141
+ machineId: ciMachineId.slice(0, 64),
142
+ user: 'ci',
143
+ hostname: (process.env.FCD_MACHINE_HOSTNAME || 'ci-pipeline').trim().toLowerCase().slice(0, 160) || 'ci-pipeline',
144
+ developerName: (process.env.FCD_DEVELOPER_NAME || 'ci@pipeline').trim().toLowerCase().slice(0, 160) || 'ci@pipeline',
145
+ platform: os.platform(),
146
+ osFriendly: 'CI runner',
147
+ };
148
+ return cached;
149
+ }
134
150
  const user = safe(() => os.userInfo().username) || process.env.USER || process.env.USERNAME || 'unknown-user';
135
151
  const hostname = normalizeHostname(safe(() => os.hostname()) || 'unknown-host');
136
152
  // Seed the hash with the OS-native id when available; fall back to a stable
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.22.8"
2
+ "version": "1.23.0"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.22.8",
3
+ "version": "1.23.0",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {