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.
@@ -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
@@ -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
- sessionId: sessionId(payload),
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',
@@ -1,4 +1,5 @@
1
- export interface SpoolEvent {
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.34.17"
2
+ "version": "1.34.18"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.34.17",
3
+ "version": "1.34.18",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {