fullcourtdefense-cli 1.34.17 → 1.34.18
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/actionIdentity.d.ts +8 -0
- package/dist/actionIdentity.js +15 -0
- package/dist/actionPolicyEngine.js +37 -0
- package/dist/commands/ciProtect.js +2 -0
- package/dist/commands/hook.js +3 -1
- package/dist/commands/mcpGateway.js +3 -0
- package/dist/commands/workloadProtect.js +2 -0
- package/dist/telemetry.d.ts +4 -1
- package/dist/telemetry.js +9 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Correlation metadata only. Never grants authority or changes a verdict. */
|
|
2
|
+
export interface ActionIdentity {
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
runId?: string;
|
|
5
|
+
instanceId?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function identityPart(value: unknown): string | undefined;
|
|
8
|
+
export declare function captureActionIdentity(payload?: Record<string, unknown>, env?: NodeJS.ProcessEnv): ActionIdentity;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.identityPart = identityPart;
|
|
4
|
+
exports.captureActionIdentity = captureActionIdentity;
|
|
5
|
+
function identityPart(value) {
|
|
6
|
+
return typeof value === 'string' && /^[A-Za-z0-9_.:@/-]{1,200}$/.test(value) ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function captureActionIdentity(payload = {}, env = process.env) {
|
|
9
|
+
return {
|
|
10
|
+
sessionId: identityPart(payload.conversation_id || payload.conversationId || payload.session_id || payload.sessionId
|
|
11
|
+
|| env.FCD_SESSION_ID || env.CLAUDE_CODE_SESSION_ID || env.CODEX_THREAD_ID),
|
|
12
|
+
runId: identityPart(env.FCD_RUN_ID || env.GITHUB_RUN_ID || env.CI_PIPELINE_ID),
|
|
13
|
+
instanceId: identityPart(env.FCD_WORKLOAD_INSTANCE_ID),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -7413,12 +7413,14 @@ function classifyShellWords(masked, raw, lead) {
|
|
|
7413
7413
|
return "SHELL";
|
|
7414
7414
|
}
|
|
7415
7415
|
function classifyShellCommandOperation(command, depth = 0) {
|
|
7416
|
+
if (provenPowerShellReadSequence(command)) return "read";
|
|
7416
7417
|
if (shellUrlActionText(command) !== command) return "write";
|
|
7417
7418
|
const segments = splitShellSegments(command);
|
|
7418
7419
|
if (segments.length === 0) return "SHELL";
|
|
7419
7420
|
return worstShellOp(segments.map((segment) => classifyShellSegment(segment, depth)), "read");
|
|
7420
7421
|
}
|
|
7421
7422
|
function provenCompoundDownload(command, detectedUrl) {
|
|
7423
|
+
if (command.includes(detectedUrl) && provenPowerShellReadSequence(command)) return true;
|
|
7422
7424
|
const segments = splitShellSegments(command).filter((segment) => /\b(?:https?|s3|gs|ftp|sftp|smb):\/\//i.test(segment));
|
|
7423
7425
|
if (!segments.length || !command.includes(detectedUrl)) return false;
|
|
7424
7426
|
return segments.every((segment) => {
|
|
@@ -7456,6 +7458,41 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
7456
7458
|
return urls === 1;
|
|
7457
7459
|
});
|
|
7458
7460
|
}
|
|
7461
|
+
function provenPowerShellReadSequence(command) {
|
|
7462
|
+
if (command.length > 65536 || !/(?:^|[;&\n])\s*\$[A-Za-z_][\w]*\s*=\s*(?:Invoke-RestMethod|Invoke-WebRequest|irm|iwr)\s/i.test(command)) return false;
|
|
7463
|
+
const segments = splitShellSegments(command);
|
|
7464
|
+
if (segments.length > 32 || /[`{}]/.test(command)) return false;
|
|
7465
|
+
const responses = /* @__PURE__ */ new Set();
|
|
7466
|
+
let requests = 0;
|
|
7467
|
+
for (let segment of segments) {
|
|
7468
|
+
let binding;
|
|
7469
|
+
const assignment = /^\$([A-Za-z_][\w]*)\s*=\s*(.+)$/s.exec(segment);
|
|
7470
|
+
if (assignment) {
|
|
7471
|
+
binding = assignment[1].toLowerCase();
|
|
7472
|
+
if (binding.startsWith("ps") || responses.has(binding)) return false;
|
|
7473
|
+
segment = assignment[2];
|
|
7474
|
+
}
|
|
7475
|
+
const property = /^\((Get-Content\s+.+\|\s*ConvertFrom-Json)\)\.[A-Za-z_][\w]*$/i.exec(segment);
|
|
7476
|
+
if (property) segment = property[1];
|
|
7477
|
+
const pipeline = splitShellSegments(segment, true);
|
|
7478
|
+
const lead = pipeline.shift() || "";
|
|
7479
|
+
if (pipeline.some((part) => !/^(?:ConvertTo-Json(?:\s+-Compress)?(?:\s+-Depth\s+\d{1,2})?|ConvertFrom-Json|Select-Object\s+[\w., -]+|Format-List|Format-Table|Out-Null)$/i.test(part))) return false;
|
|
7480
|
+
if (/^(?:Invoke-RestMethod|Invoke-WebRequest|irm|iwr)\s/i.test(lead)) {
|
|
7481
|
+
const url = lead.match(/https?:\/\/[^\s"'<>]+/i)?.[0];
|
|
7482
|
+
if (!url || !provenCompoundDownload(lead, url)) return false;
|
|
7483
|
+
requests++;
|
|
7484
|
+
} else if (/^\$[A-Za-z_][\w]*$/.test(lead)) {
|
|
7485
|
+
if (binding || !responses.has(lead.slice(1).toLowerCase())) return false;
|
|
7486
|
+
} else {
|
|
7487
|
+
if (binding) return false;
|
|
7488
|
+
if (pipeline.length === 0 && /^(?:git\s+status(?:\s+--short)?|cd\s+(?:[A-Za-z]:[\\/][\w./\\-]+|'[A-Za-z]:[\\/][^'$`\r\n]+'))$/i.test(lead)) continue;
|
|
7489
|
+
const file = /^Get-Content\s+(?:'([^'\r\n]+)'|"([^"$`\r\n]+)")((?:\s+-(?:Raw|Tail\s+\d+|TotalCount\s+\d+))*)$/i.exec(lead);
|
|
7490
|
+
if (!file || !/^[A-Za-z]:[\\/]/.test(file[1] || file[2]) || /[?*]/.test(file[1] || file[2])) return false;
|
|
7491
|
+
}
|
|
7492
|
+
if (binding) responses.add(binding);
|
|
7493
|
+
}
|
|
7494
|
+
return requests > 0;
|
|
7495
|
+
}
|
|
7459
7496
|
function isInternalHost(hostname) {
|
|
7460
7497
|
const host = hostname.toLowerCase();
|
|
7461
7498
|
if (!host || host === "localhost" || host === "::1" || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
@@ -4,6 +4,7 @@ exports.detectCiContext = detectCiContext;
|
|
|
4
4
|
exports.ciProtectCommand = ciProtectCommand;
|
|
5
5
|
const config_1 = require("../config");
|
|
6
6
|
const cliVersion_1 = require("../cliVersion");
|
|
7
|
+
const actionIdentity_1 = require("../actionIdentity");
|
|
7
8
|
const ephemeralStack_1 = require("./ephemeralStack");
|
|
8
9
|
const hostRuntime_1 = require("../hostRuntime");
|
|
9
10
|
/** Detect the pipeline from standard CI env vars (GitHub Actions, GitLab CI). */
|
|
@@ -111,6 +112,7 @@ async function ciProtectCommand(args, config) {
|
|
|
111
112
|
// (where the AI agent actually runs).
|
|
112
113
|
(0, ephemeralStack_1.exportIdentityEnv)({
|
|
113
114
|
FCD_MACHINE_ID: result.machineId,
|
|
115
|
+
FCD_RUN_ID: (0, actionIdentity_1.identityPart)(runId) || '',
|
|
114
116
|
FCD_DEVELOPER_NAME: `ci@${repo.toLowerCase()}`,
|
|
115
117
|
FCD_MACHINE_HOSTNAME: result.pipeline.key,
|
|
116
118
|
// No human at this keyboard: developer-scoped approvals route to the org
|
package/dist/commands/hook.js
CHANGED
|
@@ -50,6 +50,7 @@ const os = __importStar(require("os"));
|
|
|
50
50
|
const config_1 = require("../config");
|
|
51
51
|
const runtimeConfig_1 = require("../runtimeConfig");
|
|
52
52
|
const telemetry_1 = require("../telemetry");
|
|
53
|
+
const actionIdentity_1 = require("../actionIdentity");
|
|
53
54
|
const deterministicGuard_1 = require("./deterministicGuard");
|
|
54
55
|
const localSafetySnapshot_1 = require("../localSafetySnapshot");
|
|
55
56
|
const taintLedger_1 = require("./taintLedger");
|
|
@@ -1461,6 +1462,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
1461
1462
|
const hookFormat = claudeNormalized ? 'claude' : 'cursor';
|
|
1462
1463
|
const client = args.agentClient || (claudeNormalized ? claudeFormatClient() : 'cursor');
|
|
1463
1464
|
(0, telemetry_1.setVerdictClient)(client);
|
|
1465
|
+
(0, telemetry_1.setVerdictIdentity)(payload);
|
|
1464
1466
|
let event;
|
|
1465
1467
|
let ignoreEvent = false;
|
|
1466
1468
|
if (claudeNormalized) {
|
|
@@ -1897,7 +1899,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1897
1899
|
toolName: call.toolName,
|
|
1898
1900
|
toolArgs: call.toolArgs,
|
|
1899
1901
|
argsSummary: call.toolArgs,
|
|
1900
|
-
|
|
1902
|
+
...(0, actionIdentity_1.captureActionIdentity)(payload),
|
|
1901
1903
|
source: 'cursor_hook',
|
|
1902
1904
|
// Lets the server record the effective approval scope (developer-scoped rules degrade
|
|
1903
1905
|
// to org where nobody can answer an IDE prompt: CI, ephemeral workloads).
|
|
@@ -62,6 +62,7 @@ const runtimeConfig_1 = require("../runtimeConfig");
|
|
|
62
62
|
const sessionLimits_1 = require("../sessionLimits");
|
|
63
63
|
const telemetry_1 = require("../telemetry");
|
|
64
64
|
const notify_1 = require("../notify");
|
|
65
|
+
const actionIdentity_1 = require("../actionIdentity");
|
|
65
66
|
const fileWriteCanon_1 = require("../fileWriteCanon");
|
|
66
67
|
const blockExplanation_1 = require("../blockExplanation");
|
|
67
68
|
const distress_1 = require("../distress");
|
|
@@ -750,6 +751,7 @@ class AgentGuardApi {
|
|
|
750
751
|
}
|
|
751
752
|
async checkToolCall(input, timeoutMs) {
|
|
752
753
|
const result = await this.post('/api/agent-security/runtime/check-tool-call', {
|
|
754
|
+
...(0, actionIdentity_1.captureActionIdentity)(),
|
|
753
755
|
shieldId: this.config.shieldId,
|
|
754
756
|
agentName: this.config.agentName,
|
|
755
757
|
toolName: input.toolName,
|
|
@@ -772,6 +774,7 @@ class AgentGuardApi {
|
|
|
772
774
|
}
|
|
773
775
|
async recordToolCall(input) {
|
|
774
776
|
await this.post('/api/agent-security/runtime/tool-call', {
|
|
777
|
+
...(0, actionIdentity_1.captureActionIdentity)(),
|
|
775
778
|
shieldId: this.config.shieldId,
|
|
776
779
|
agentName: this.config.agentName,
|
|
777
780
|
toolName: input.toolName,
|
|
@@ -41,6 +41,7 @@ const os = __importStar(require("os"));
|
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const config_1 = require("../config");
|
|
43
43
|
const cliVersion_1 = require("../cliVersion");
|
|
44
|
+
const actionIdentity_1 = require("../actionIdentity");
|
|
44
45
|
const ephemeralStack_1 = require("./ephemeralStack");
|
|
45
46
|
const hostRuntime_1 = require("../hostRuntime");
|
|
46
47
|
/**
|
|
@@ -173,6 +174,7 @@ async function workloadProtectCommand(args, config) {
|
|
|
173
174
|
// declare "no human here" so approvals never wait on a prompt nobody sees.
|
|
174
175
|
const identityEnv = {
|
|
175
176
|
FCD_MACHINE_ID: result.machineId,
|
|
177
|
+
FCD_WORKLOAD_INSTANCE_ID: (0, actionIdentity_1.identityPart)(instanceId) || '',
|
|
176
178
|
FCD_DEVELOPER_NAME: `workload@${result.workload.name.toLowerCase()}`,
|
|
177
179
|
FCD_MACHINE_HOSTNAME: result.workload.key,
|
|
178
180
|
FCD_EPHEMERAL: '1',
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ActionIdentity } from './actionIdentity';
|
|
2
|
+
export interface SpoolEvent extends ActionIdentity {
|
|
2
3
|
agentClient?: string;
|
|
3
4
|
eventId: string;
|
|
4
5
|
type: 'verdict';
|
|
@@ -53,6 +54,7 @@ export type VerdictPath = 'ipc' | 'local' | 'fail_open' | 'gateway';
|
|
|
53
54
|
*/
|
|
54
55
|
export declare function evidenceFromToolArgs(toolArgs: Record<string, unknown> | undefined): string | undefined;
|
|
55
56
|
interface VerdictTiming {
|
|
57
|
+
identity?: ActionIdentity;
|
|
56
58
|
agentClient?: string;
|
|
57
59
|
startedAt: number;
|
|
58
60
|
path: VerdictPath;
|
|
@@ -67,6 +69,7 @@ export declare function markVerdictTiming<T>(timing: VerdictTiming, fn: () => T)
|
|
|
67
69
|
*/
|
|
68
70
|
export declare function setVerdictPath(path: VerdictPath): void;
|
|
69
71
|
export declare function setVerdictClient(client: string): void;
|
|
72
|
+
export declare function setVerdictIdentity(payload: Record<string, unknown>): void;
|
|
70
73
|
/** Attach the current tool call's args so every spool in this evaluation carries the command. */
|
|
71
74
|
export declare function setVerdictToolArgs(toolArgs: Record<string, unknown>): void;
|
|
72
75
|
/**
|
package/dist/telemetry.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.evidenceFromToolArgs = evidenceFromToolArgs;
|
|
|
37
37
|
exports.markVerdictTiming = markVerdictTiming;
|
|
38
38
|
exports.setVerdictPath = setVerdictPath;
|
|
39
39
|
exports.setVerdictClient = setVerdictClient;
|
|
40
|
+
exports.setVerdictIdentity = setVerdictIdentity;
|
|
40
41
|
exports.setVerdictToolArgs = setVerdictToolArgs;
|
|
41
42
|
exports.restartVerdictTiming = restartVerdictTiming;
|
|
42
43
|
exports.spoolEvent = spoolEvent;
|
|
@@ -49,6 +50,7 @@ const fs = __importStar(require("fs"));
|
|
|
49
50
|
const os = __importStar(require("os"));
|
|
50
51
|
const path = __importStar(require("path"));
|
|
51
52
|
const machineIdentity_1 = require("./machineIdentity");
|
|
53
|
+
const actionIdentity_1 = require("./actionIdentity");
|
|
52
54
|
const localDetectionUpdates_1 = require("./localDetectionUpdates");
|
|
53
55
|
/**
|
|
54
56
|
* Local-first telemetry: every enforcement decision is appended to an on-disk
|
|
@@ -163,6 +165,11 @@ function setVerdictClient(client) {
|
|
|
163
165
|
if (store)
|
|
164
166
|
store.agentClient = client;
|
|
165
167
|
}
|
|
168
|
+
function setVerdictIdentity(payload) {
|
|
169
|
+
const store = verdictTiming.getStore();
|
|
170
|
+
if (store)
|
|
171
|
+
store.identity = (0, actionIdentity_1.captureActionIdentity)(payload);
|
|
172
|
+
}
|
|
166
173
|
/** Attach the current tool call's args so every spool in this evaluation carries the command. */
|
|
167
174
|
function setVerdictToolArgs(toolArgs) {
|
|
168
175
|
const store = verdictTiming.getStore();
|
|
@@ -188,6 +195,8 @@ function spoolEvent(event) {
|
|
|
188
195
|
// onboard) — the backend treats missing fields as "no sample".
|
|
189
196
|
const timing = verdictTiming.getStore();
|
|
190
197
|
const full = {
|
|
198
|
+
...(0, actionIdentity_1.captureActionIdentity)(),
|
|
199
|
+
...timing?.identity,
|
|
191
200
|
eventId: crypto.randomUUID(),
|
|
192
201
|
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
193
202
|
type: 'verdict',
|
package/dist/version.json
CHANGED