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.
- package/dist/actionIdentity.d.ts +8 -0
- package/dist/actionIdentity.js +15 -0
- package/dist/actionPolicyEngine.js +82 -3
- package/dist/commands/ciProtect.js +2 -0
- package/dist/commands/daemon.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/detectionData.d.ts +21 -0
- package/dist/detectionData.js +71 -0
- package/dist/detectionUpdates.d.ts +25 -0
- package/dist/detectionUpdates.js +209 -0
- package/dist/localDetectionUpdates.d.ts +2 -0
- package/dist/localDetectionUpdates.js +41 -0
- package/dist/runtimeConfig.js +2 -0
- package/dist/telemetry.d.ts +4 -1
- package/dist/telemetry.js +11 -0
- package/dist/version.json +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
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])
|
|
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
|
-
|
|
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
|
package/dist/commands/daemon.js
CHANGED
|
@@ -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.
|
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',
|
|
@@ -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,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();
|
package/dist/runtimeConfig.js
CHANGED
|
@@ -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];
|
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,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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.34.
|
|
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",
|