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.
- package/dist/commands/ciProtect.d.ts +42 -0
- package/dist/commands/ciProtect.js +191 -0
- package/dist/index.js +21 -0
- package/dist/machineIdentity.js +16 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -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'],
|
package/dist/machineIdentity.js
CHANGED
|
@@ -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