fullcourtdefense-cli 1.34.16 → 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
+ }
@@ -5739,10 +5739,11 @@ function parse3(input, options) {
5739
5739
  }
5740
5740
 
5741
5741
  // src/actionPolicyEngine.ts
5742
+ var import_detectionData = require("./detectionData");
5742
5743
  function isFirestoreQueryUrl(target) {
5743
5744
  try {
5744
5745
  const url = new URL(target);
5745
- return url.origin === "https://firestore.googleapis.com" && !url.username && !url.password && !url.search && !url.hash && /^\/v1\/projects\/[^/]+\/databases\/[^/]+\/documents(?:\/[^/]+)*:runQuery$/.test(url.pathname);
5746
+ return (0, import_detectionData.getDetectionData)().firestoreQueryOrigins.includes(url.origin) && !url.username && !url.password && !url.search && !url.hash && /^\/v1\/projects\/[^/]+\/databases\/[^/]+\/documents(?:\/[^/]+)*:runQuery$/.test(url.pathname);
5746
5747
  } catch {
5747
5748
  return false;
5748
5749
  }
@@ -5871,7 +5872,7 @@ function provenJavaScriptReadUrls(code) {
5871
5872
  const options = ev(node.arguments[0]);
5872
5873
  if (options.kind !== "object" || options.fields.size !== 1) return fail();
5873
5874
  const scopes = options.fields.get("scopes");
5874
- if (scopes?.kind !== "array" || scopes.items.length !== 1 || string(scopes.items[0]) !== "https://www.googleapis.com/auth/datastore") return fail();
5875
+ if (scopes?.kind !== "array" || scopes.items.length !== 1 || !(0, import_detectionData.getDetectionData)().googleAuthScopes.includes(string(scopes.items[0]))) return fail();
5875
5876
  return object([["getClient", callable("google.getClient")]]);
5876
5877
  }
5877
5878
  case "ReturnStatement":
@@ -7007,7 +7008,45 @@ function extractUrl(args, _argsText) {
7007
7008
  return "";
7008
7009
  }
7009
7010
  function shellUrlActionText(command) {
7010
- const segments = splitShellSegments(command);
7011
+ if (command.length > 65536) return command;
7012
+ const originalSegments = splitShellSegments(command);
7013
+ const localPaths = /* @__PURE__ */ new Map();
7014
+ const pathAssignments = /* @__PURE__ */ new Set();
7015
+ let expandedSize = command.length;
7016
+ let expansionOverflow = false;
7017
+ const expand = (original, value) => {
7018
+ expandedSize += Math.max(0, value.length - original.length);
7019
+ if (expandedSize > 65536) {
7020
+ expansionOverflow = true;
7021
+ return original;
7022
+ }
7023
+ return value;
7024
+ };
7025
+ const segments = originalSegments.map((segment, index) => {
7026
+ const assignment = /^\$([A-Za-z_][\w]*)\s*=\s*(?:'([^']*)'|"([^"$`]*)")$/.exec(segment);
7027
+ if (assignment) {
7028
+ const name = assignment[1].toLowerCase();
7029
+ const value = assignment[2] ?? assignment[3];
7030
+ if (localPaths.has(name) || localPaths.size >= 32 || value.length > 4096 || !/^(?:[A-Za-z]:[\\/]|\/(?!\/))[\w ./\\-]+$/.test(value)) return segment;
7031
+ localPaths.set(name, value);
7032
+ pathAssignments.add(index);
7033
+ return segment;
7034
+ }
7035
+ const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$`]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/.exec(segment);
7036
+ const prefix = literal2?.[0] || "";
7037
+ const tail = segment.slice(prefix.length).replace(
7038
+ /"\$([A-Za-z_][\w]*)([\\/][\w ./\\-]*)?"/g,
7039
+ (match, name, suffix) => {
7040
+ const value = localPaths.get(name.toLowerCase());
7041
+ return value ? expand(match, `"${value}${suffix || ""}"`) : match;
7042
+ }
7043
+ ).replace(/(?<=\s)\$([A-Za-z_][\w]*)(?=\s|$)/g, (match, name) => {
7044
+ const value = localPaths.get(name.toLowerCase());
7045
+ return value ? expand(match, `"${value}"`) : match;
7046
+ });
7047
+ return prefix + tail;
7048
+ });
7049
+ if (expansionOverflow) return command;
7011
7050
  const filtered = segments.map((segment) => {
7012
7051
  const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$`]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/;
7013
7052
  const piped = segment.match(literal2);
@@ -7040,7 +7079,10 @@ function shellUrlActionText(command) {
7040
7079
  return tail;
7041
7080
  });
7042
7081
  const onlyStoredData = segments.every((segment, index) => {
7082
+ if (pathAssignments.has(index)) return true;
7043
7083
  if (filtered[index] !== segment) return true;
7084
+ const directory = /^New-Item\s+-ItemType\s+Directory\s+(?:-Force\s+)?-(?:LiteralPath|Path)\s+(?:'([^']*)'|"([^"$`]*)")\s*(?:\|\s*Out-Null)?$/i.exec(segment);
7085
+ if (directory && /^(?:[A-Za-z]:[\\/]|\/(?!\/))[\w ./\\-]+$/.test(directory[1] ?? directory[2])) return true;
7044
7086
  const words = maskQuotedSpans(segment);
7045
7087
  if (!/[$`|;&(){}<>]/.test(segment) && /^(?:(?:cd|pushd|set-location)\s+[^\r\n]+|(?:pwd|popd|get-location)|git\s+status(?:\s+[^\r\n]+)?)$/i.test(segment)) return true;
7046
7088
  return !/[$`]/.test(segment) && /^\(Get-Content\s+(?:[A-Za-z]:[\\/])?[\w./\\-]+\s+-Raw\)\.Replace\("",\s*""\)\s*\|\s*Set-Content\s+(?:[A-Za-z]:[\\/])?[\w./\\-]+\s+-Encoding\s+utf8\s*$/i.test(words);
@@ -7371,12 +7413,14 @@ function classifyShellWords(masked, raw, lead) {
7371
7413
  return "SHELL";
7372
7414
  }
7373
7415
  function classifyShellCommandOperation(command, depth = 0) {
7416
+ if (provenPowerShellReadSequence(command)) return "read";
7374
7417
  if (shellUrlActionText(command) !== command) return "write";
7375
7418
  const segments = splitShellSegments(command);
7376
7419
  if (segments.length === 0) return "SHELL";
7377
7420
  return worstShellOp(segments.map((segment) => classifyShellSegment(segment, depth)), "read");
7378
7421
  }
7379
7422
  function provenCompoundDownload(command, detectedUrl) {
7423
+ if (command.includes(detectedUrl) && provenPowerShellReadSequence(command)) return true;
7380
7424
  const segments = splitShellSegments(command).filter((segment) => /\b(?:https?|s3|gs|ftp|sftp|smb):\/\//i.test(segment));
7381
7425
  if (!segments.length || !command.includes(detectedUrl)) return false;
7382
7426
  return segments.every((segment) => {
@@ -7414,6 +7458,41 @@ function provenCompoundDownload(command, detectedUrl) {
7414
7458
  return urls === 1;
7415
7459
  });
7416
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
+ }
7417
7496
  function isInternalHost(hostname) {
7418
7497
  const host = hostname.toLowerCase();
7419
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
@@ -75,6 +75,7 @@ const telemetry_1 = require("../telemetry");
75
75
  const notify_1 = require("../notify");
76
76
  const integrity_1 = require("../integrity");
77
77
  const machineIdentity_1 = require("../machineIdentity");
78
+ const localDetectionUpdates_1 = require("../localDetectionUpdates");
78
79
  const discoveryMarker_1 = require("../discoveryMarker");
79
80
  const selfUpdate_1 = require("../selfUpdate");
80
81
  const cmdGuard_1 = require("./cmdGuard");
@@ -1204,6 +1205,7 @@ async function runDaemon(args, config) {
1204
1205
  const pollBundle = async () => {
1205
1206
  if (!creds.shieldId)
1206
1207
  return;
1208
+ void localDetectionUpdates_1.localDetectionUpdates.refresh();
1207
1209
  if (!creds.shieldKey) {
1208
1210
  // Credential-broken machine: stay reachable via the key-less signed-
1209
1211
  // action channel while credential recovery keeps retrying.
@@ -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',
@@ -0,0 +1,21 @@
1
+ export declare const DETECTION_SCHEMA = 1;
2
+ export declare const MAX_DETECTION_BYTES = 16384;
3
+ export declare const DETECTION_SIGNATURE_DOMAIN = "fullcourtdefense/detection-data/v1\n";
4
+ export declare const DETECTION_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALTPVDAPBFn0XNHo+y3GFHQ8Inr/OM7z1vqUixWL8R00=\n-----END PUBLIC KEY-----";
5
+ export interface DetectionData {
6
+ schema: 1;
7
+ revision: number;
8
+ /** These capabilities never execute data, and never contain a policy verdict. */
9
+ googleAuthScopes: string[];
10
+ firestoreQueryOrigins: string[];
11
+ }
12
+ export declare const BUNDLED_DETECTION_DATA: Readonly<DetectionData>;
13
+ export declare function getDetectionData(): Readonly<DetectionData>;
14
+ export declare function validateDetectionData(value: unknown): DetectionData;
15
+ export interface DetectionEnvelope {
16
+ payload: string;
17
+ signature: string;
18
+ }
19
+ export declare function verifyDetectionEnvelope(raw: string, now?: number, publicKey?: string): DetectionData;
20
+ /** Callers must authenticate envelopes before activation; always copy/freeze to avoid mutation. */
21
+ export declare function activateDetectionData(data: DetectionData): boolean;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUNDLED_DETECTION_DATA = exports.DETECTION_PUBLIC_KEY = exports.DETECTION_SIGNATURE_DOMAIN = exports.MAX_DETECTION_BYTES = exports.DETECTION_SCHEMA = void 0;
4
+ exports.getDetectionData = getDetectionData;
5
+ exports.validateDetectionData = validateDetectionData;
6
+ exports.verifyDetectionEnvelope = verifyDetectionEnvelope;
7
+ exports.activateDetectionData = activateDetectionData;
8
+ /** Pure, bounded detection vocabulary. Vendored with the policy engine into CLI/demo. */
9
+ const crypto_1 = require("crypto");
10
+ exports.DETECTION_SCHEMA = 1;
11
+ exports.MAX_DETECTION_BYTES = 16_384;
12
+ exports.DETECTION_SIGNATURE_DOMAIN = 'fullcourtdefense/detection-data/v1\n';
13
+ exports.DETECTION_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
14
+ MCowBQYDK2VwAyEALTPVDAPBFn0XNHo+y3GFHQ8Inr/OM7z1vqUixWL8R00=
15
+ -----END PUBLIC KEY-----`;
16
+ exports.BUNDLED_DETECTION_DATA = Object.freeze({
17
+ schema: 1, revision: 1,
18
+ googleAuthScopes: Object.freeze(['https://www.googleapis.com/auth/datastore']),
19
+ firestoreQueryOrigins: Object.freeze(['https://firestore.googleapis.com']),
20
+ });
21
+ let active = exports.BUNDLED_DETECTION_DATA;
22
+ function getDetectionData() { return active; }
23
+ function exactKeys(value, keys) {
24
+ return !!value && typeof value === 'object' && !Array.isArray(value)
25
+ && Object.keys(value).length === keys.length && keys.every(key => Object.prototype.hasOwnProperty.call(value, key));
26
+ }
27
+ function validateDetectionData(value) {
28
+ const data = value;
29
+ if (!exactKeys(data, ['schema', 'revision', 'googleAuthScopes', 'firestoreQueryOrigins'])
30
+ || data.schema !== exports.DETECTION_SCHEMA || !Number.isSafeInteger(data.revision) || data.revision < 1)
31
+ throw new Error('Unsupported detection schema/revision');
32
+ const list = (items, valid) => {
33
+ if (!Array.isArray(items) || items.length < 1 || items.length > 32
34
+ || items.some(item => typeof item !== 'string' || item.length > 200 || !valid(item))
35
+ || new Set(items).size !== items.length)
36
+ throw new Error('Invalid detection vocabulary');
37
+ return [...items];
38
+ };
39
+ // Scope URLs are authorization metadata, not destinations. Restrict to the provider namespace.
40
+ const scopes = list(data.googleAuthScopes, s => /^https:\/\/www\.googleapis\.com\/auth\/[a-z][a-z0-9._-]*$/.test(s));
41
+ // Only Firestore service origins can use the fixed structuredQuery body/path proof.
42
+ // No wildcard host, arbitrary URL, regex, flag, verdict, or shell pattern is supported.
43
+ const origins = list(data.firestoreQueryOrigins, s => /^https:\/\/firestore(?:\.[a-z][a-z0-9-]{0,40})?\.googleapis\.com$/.test(s));
44
+ return { schema: 1, revision: data.revision, googleAuthScopes: scopes, firestoreQueryOrigins: origins };
45
+ }
46
+ function verifyDetectionEnvelope(raw, now = Date.now(), publicKey = exports.DETECTION_PUBLIC_KEY) {
47
+ if (Buffer.byteLength(raw) > exports.MAX_DETECTION_BYTES)
48
+ throw new Error('Detection update too large');
49
+ const envelope = JSON.parse(raw);
50
+ if (!exactKeys(envelope, ['payload', 'signature']) || typeof envelope.payload !== 'string'
51
+ || typeof envelope.signature !== 'string' || !/^[A-Za-z0-9+/]{86}==$/.test(envelope.signature))
52
+ throw new Error('Invalid detection envelope');
53
+ if (!(0, crypto_1.verify)(null, Buffer.from(exports.DETECTION_SIGNATURE_DOMAIN + envelope.payload), (0, crypto_1.createPublicKey)(publicKey), Buffer.from(envelope.signature, 'base64')))
54
+ throw new Error('Invalid detection signature');
55
+ const payload = JSON.parse(envelope.payload);
56
+ if (!exactKeys(payload, ['issuedAt', 'expiresAt', 'data']) || !Number.isSafeInteger(payload.issuedAt)
57
+ || !Number.isSafeInteger(payload.expiresAt) || payload.issuedAt > now + 60_000
58
+ || payload.expiresAt <= now || payload.expiresAt <= payload.issuedAt
59
+ || payload.expiresAt - payload.issuedAt > 90 * 86400_000)
60
+ throw new Error('Detection update outside validity window');
61
+ return validateDetectionData(payload.data);
62
+ }
63
+ /** Callers must authenticate envelopes before activation; always copy/freeze to avoid mutation. */
64
+ function activateDetectionData(data) {
65
+ const valid = validateDetectionData(data);
66
+ if (valid.revision <= active.revision)
67
+ return false;
68
+ active = Object.freeze({ ...valid, googleAuthScopes: Object.freeze(valid.googleAuthScopes),
69
+ firestoreQueryOrigins: Object.freeze(valid.firestoreQueryOrigins) });
70
+ return true;
71
+ }
@@ -0,0 +1,25 @@
1
+ export declare const DETECTION_UPDATE_URL = "https://storage.googleapis.com/fullcourtdefense-cli-releases/detection/stable.json";
2
+ export interface DetectionUpdateStatus {
3
+ revision: number;
4
+ bundledRevision: number;
5
+ source: 'bundled' | 'update';
6
+ lastCheckedAt?: string;
7
+ state: 'bundled' | 'current' | 'unavailable' | 'rejected';
8
+ }
9
+ export declare class DetectionUpdates {
10
+ private readonly cachePath;
11
+ private readonly publicKey;
12
+ private readonly channel;
13
+ private nextCheck;
14
+ private nextLocalRead;
15
+ private pending?;
16
+ private status;
17
+ constructor(cachePath: string, publicKey?: string, channel?: 'stable' | 'staging');
18
+ getStatus(): DetectionUpdateStatus;
19
+ private readCache;
20
+ private cachedData;
21
+ loadLocal(): void;
22
+ /** Call only from a background worker. Coalesces concurrent calls and backs off on failure. */
23
+ refresh(): Promise<void>;
24
+ private download;
25
+ }
@@ -0,0 +1,209 @@
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.DetectionUpdates = exports.DETECTION_UPDATE_URL = void 0;
37
+ /** Background-only transport. Commands use authenticated local data; never fetch here on a verdict path. */
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const crypto_1 = require("crypto");
41
+ const detectionData_1 = require("./detectionData");
42
+ exports.DETECTION_UPDATE_URL = 'https://storage.googleapis.com/fullcourtdefense-cli-releases/detection/stable.json';
43
+ class DetectionUpdates {
44
+ cachePath;
45
+ publicKey;
46
+ channel;
47
+ nextCheck = 0;
48
+ nextLocalRead = 0;
49
+ pending;
50
+ status = { revision: detectionData_1.BUNDLED_DETECTION_DATA.revision,
51
+ bundledRevision: detectionData_1.BUNDLED_DETECTION_DATA.revision, source: 'bundled', state: 'bundled' };
52
+ constructor(cachePath, publicKey = detectionData_1.DETECTION_PUBLIC_KEY, channel = 'stable') {
53
+ this.cachePath = cachePath;
54
+ this.publicKey = publicKey;
55
+ this.channel = channel;
56
+ }
57
+ getStatus() {
58
+ const revision = (0, detectionData_1.getDetectionData)().revision;
59
+ return { ...this.status, revision, source: revision > detectionData_1.BUNDLED_DETECTION_DATA.revision ? 'update' : 'bundled' };
60
+ }
61
+ readCache() {
62
+ try {
63
+ if (fs.statSync(this.cachePath).size > detectionData_1.MAX_DETECTION_BYTES)
64
+ return undefined;
65
+ return fs.readFileSync(this.cachePath, 'utf8');
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ }
71
+ cachedData(raw) {
72
+ // Expiration gates first acceptance, not continued offline use. Authenticate
73
+ // the signed validity period before using the last verified cache after restart.
74
+ const payload = JSON.parse(JSON.parse(raw).payload);
75
+ return (0, detectionData_1.verifyDetectionEnvelope)(raw, Math.min(Date.now(), payload.issuedAt), this.publicKey);
76
+ }
77
+ loadLocal() {
78
+ if (Date.now() < this.nextLocalRead)
79
+ return;
80
+ this.nextLocalRead = Date.now() + 30_000;
81
+ const raw = this.readCache();
82
+ if (!raw)
83
+ return;
84
+ try {
85
+ (0, detectionData_1.activateDetectionData)(this.cachedData(raw));
86
+ }
87
+ catch {
88
+ this.status.state = 'rejected';
89
+ }
90
+ }
91
+ /** Call only from a background worker. Coalesces concurrent calls and backs off on failure. */
92
+ refresh() {
93
+ if (this.pending)
94
+ return this.pending;
95
+ if (Date.now() < this.nextCheck)
96
+ return Promise.resolve();
97
+ this.nextCheck = Date.now() + 60_000 + Math.floor(Math.random() * 15_000);
98
+ this.pending = this.download().finally(() => { this.pending = undefined; });
99
+ return this.pending;
100
+ }
101
+ async download() {
102
+ this.status.lastCheckedAt = new Date().toISOString();
103
+ let raw;
104
+ try {
105
+ const response = await fetch(this.channel === 'staging' ? exports.DETECTION_UPDATE_URL.replace('/stable.json', '/staging.json') : exports.DETECTION_UPDATE_URL, { redirect: 'error', signal: AbortSignal.timeout(3000) });
106
+ if (!response.ok || !response.body)
107
+ throw new Error('Unavailable');
108
+ const reader = response.body.getReader();
109
+ const chunks = [];
110
+ let size = 0;
111
+ try {
112
+ for (;;) {
113
+ const { done, value } = await reader.read();
114
+ if (done)
115
+ break;
116
+ size += value.byteLength;
117
+ if (size > detectionData_1.MAX_DETECTION_BYTES)
118
+ throw new Error('Oversized');
119
+ chunks.push(value);
120
+ }
121
+ }
122
+ finally {
123
+ await reader.cancel().catch(() => { });
124
+ }
125
+ raw = Buffer.concat(chunks).toString('utf8');
126
+ }
127
+ catch {
128
+ this.status.state = 'unavailable';
129
+ this.nextCheck = Date.now() + 5 * 60_000;
130
+ return;
131
+ }
132
+ let lock;
133
+ let temporary;
134
+ try {
135
+ const data = (0, detectionData_1.verifyDetectionEnvelope)(raw, Date.now(), this.publicKey);
136
+ fs.mkdirSync(path.dirname(this.cachePath), { recursive: true });
137
+ const lockPath = this.cachePath + '.lock';
138
+ try {
139
+ const stat = fs.statSync(lockPath);
140
+ const pid = stat.size <= 30 ? Number(fs.readFileSync(lockPath, 'utf8')) : NaN;
141
+ if (Number.isSafeInteger(pid) && pid > 0) {
142
+ try {
143
+ process.kill(pid, 0);
144
+ }
145
+ catch (error) {
146
+ if (error.code === 'ESRCH')
147
+ fs.unlinkSync(lockPath);
148
+ }
149
+ }
150
+ else if (Date.now() - stat.mtimeMs > 10 * 60_000)
151
+ fs.unlinkSync(lockPath);
152
+ }
153
+ catch { /* absent lock or another writer; exclusive creation decides */ }
154
+ lock = fs.openSync(lockPath, 'wx', 0o600);
155
+ fs.writeFileSync(lock, String(process.pid));
156
+ const currentRaw = this.readCache();
157
+ let cached;
158
+ try {
159
+ cached = currentRaw ? this.cachedData(currentRaw) : undefined;
160
+ }
161
+ catch { /* recover corrupt cache */ }
162
+ const cachedRevision = cached?.revision ?? 0;
163
+ const currentRevision = Math.max(cachedRevision, (0, detectionData_1.getDetectionData)().revision);
164
+ if (data.revision < currentRevision)
165
+ throw new Error('Revision downgrade');
166
+ if (cached && data.revision === cached.revision && JSON.stringify(data) !== JSON.stringify(cached))
167
+ throw new Error('Cached revision reused for different content');
168
+ if (data.revision === (0, detectionData_1.getDetectionData)().revision && JSON.stringify(data) !== JSON.stringify((0, detectionData_1.getDetectionData)()))
169
+ throw new Error('Revision reused for different content');
170
+ if (data.revision > currentRevision) {
171
+ temporary = this.cachePath + '.' + (0, crypto_1.randomUUID)() + '.tmp';
172
+ const fd = fs.openSync(temporary, 'wx', 0o600);
173
+ try {
174
+ fs.writeFileSync(fd, raw);
175
+ fs.fsyncSync(fd);
176
+ }
177
+ finally {
178
+ fs.closeSync(fd);
179
+ }
180
+ fs.renameSync(temporary, this.cachePath);
181
+ temporary = undefined;
182
+ (0, detectionData_1.activateDetectionData)(data);
183
+ }
184
+ else if (cached)
185
+ (0, detectionData_1.activateDetectionData)(cached);
186
+ this.status.state = 'current';
187
+ }
188
+ catch {
189
+ this.status.state = 'rejected';
190
+ this.nextCheck = Date.now() + 5 * 60_000;
191
+ }
192
+ finally {
193
+ if (temporary) {
194
+ try {
195
+ fs.unlinkSync(temporary);
196
+ }
197
+ catch { }
198
+ }
199
+ if (lock !== undefined) {
200
+ fs.closeSync(lock);
201
+ try {
202
+ fs.unlinkSync(this.cachePath + '.lock');
203
+ }
204
+ catch { }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ exports.DetectionUpdates = DetectionUpdates;
@@ -0,0 +1,2 @@
1
+ import { DetectionUpdates } from './detectionUpdates';
2
+ export declare const localDetectionUpdates: DetectionUpdates;
@@ -0,0 +1,41 @@
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.localDetectionUpdates = void 0;
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const detectionUpdates_1 = require("./detectionUpdates");
40
+ exports.localDetectionUpdates = new detectionUpdates_1.DetectionUpdates(path.join(os.homedir(), '.fullcourtdefense', 'detection-update.json'));
41
+ exports.localDetectionUpdates.loadLocal();
@@ -46,6 +46,7 @@ const os = __importStar(require("os"));
46
46
  const path = __importStar(require("path"));
47
47
  const distress_1 = require("./distress");
48
48
  const sessionLimits_1 = require("./sessionLimits");
49
+ const localDetectionUpdates_1 = require("./localDetectionUpdates");
49
50
  const CACHE_PATH = path.join(os.homedir(), '.fullcourtdefense-runtime.json');
50
51
  const DEFAULT_TTL_MS = 60_000;
51
52
  const REFRESH_TIMEOUT_MS = 1_500; // tight: the hook must stay fast
@@ -216,6 +217,7 @@ function sanitizeBundlePolicies(value) {
216
217
  * or a 'default' marker so the caller can apply its local fallback.
217
218
  */
218
219
  async function getRuntimeBundle(input) {
220
+ localDetectionUpdates_1.localDetectionUpdates.loadLocal();
219
221
  const ttl = input.ttlMs ?? DEFAULT_TTL_MS;
220
222
  const cache = readCacheFile();
221
223
  const cached = cache[input.shieldId];
@@ -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,8 @@ 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");
54
+ const localDetectionUpdates_1 = require("./localDetectionUpdates");
52
55
  /**
53
56
  * Local-first telemetry: every enforcement decision is appended to an on-disk
54
57
  * spool (instant, offline-safe) and flushed to the backend in batches. Critical
@@ -162,6 +165,11 @@ function setVerdictClient(client) {
162
165
  if (store)
163
166
  store.agentClient = client;
164
167
  }
168
+ function setVerdictIdentity(payload) {
169
+ const store = verdictTiming.getStore();
170
+ if (store)
171
+ store.identity = (0, actionIdentity_1.captureActionIdentity)(payload);
172
+ }
165
173
  /** Attach the current tool call's args so every spool in this evaluation carries the command. */
166
174
  function setVerdictToolArgs(toolArgs) {
167
175
  const store = verdictTiming.getStore();
@@ -187,6 +195,8 @@ function spoolEvent(event) {
187
195
  // onboard) — the backend treats missing fields as "no sample".
188
196
  const timing = verdictTiming.getStore();
189
197
  const full = {
198
+ ...(0, actionIdentity_1.captureActionIdentity)(),
199
+ ...timing?.identity,
190
200
  eventId: crypto.randomUUID(),
191
201
  occurredAt: event.occurredAt || new Date().toISOString(),
192
202
  type: 'verdict',
@@ -355,6 +365,7 @@ async function flushSpoolLocked(input) {
355
365
  heartbeat: input.heartbeat
356
366
  ? {
357
367
  agentVersion: input.agentVersion,
368
+ detectionUpdates: localDetectionUpdates_1.localDetectionUpdates.getStatus(),
358
369
  integrityOk: input.integrityOk,
359
370
  integrityReasons: input.integrityReasons,
360
371
  integrityCheckedAt: input.integrityCheckedAt,
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.34.16"
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.16",
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": {
@@ -96,7 +96,7 @@
96
96
  "test:ide-fp-corpus:hook": "npm run build && node scripts/test-ide-fp-corpus.js --hook",
97
97
  "test:msi-payload-deps": "npm run build && node scripts/test-msi-payload-deps.js",
98
98
  "build:msi": "powershell -NoProfile -ExecutionPolicy Bypass -File installer/windows/Build-Msi.ps1",
99
- "prepublishOnly": "npm run build && node scripts/test-msi-payload-deps.js && node scripts/test-shell-parity.js && node scripts/test-terminal-mcp-ask.js"
99
+ "prepublishOnly": "npm run build && node scripts/check-detection-baseline.js && node scripts/test-detection-updates.js && node scripts/test-detection-cache.js && node scripts/test-msi-payload-deps.js && node scripts/test-shell-parity.js && node scripts/test-terminal-mcp-ask.js"
100
100
  },
101
101
  "keywords": [
102
102
  "llm",