fullcourtdefense-cli 1.26.7 → 1.26.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/hook.js +26 -14
- package/dist/commands/mcpGateway.js +1 -0
- package/dist/telemetry.d.ts +10 -0
- package/dist/telemetry.js +38 -1
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/hook.js
CHANGED
|
@@ -233,10 +233,14 @@ function extractPromptText(p) {
|
|
|
233
233
|
function buildToolCall(event, p) {
|
|
234
234
|
switch (event) {
|
|
235
235
|
case 'shell': {
|
|
236
|
-
const
|
|
236
|
+
const nested = (p.tool_input && typeof p.tool_input === 'object')
|
|
237
|
+
? p.tool_input
|
|
238
|
+
: undefined;
|
|
239
|
+
const command = str(p.command) || str(p.cmd) || str(p.commandLine)
|
|
240
|
+
|| str(nested?.command) || str(nested?.cmd);
|
|
237
241
|
if (!command)
|
|
238
242
|
return null;
|
|
239
|
-
return { toolName: 'shell', toolArgs: { command, cwd: str(p.cwd) || undefined } };
|
|
243
|
+
return { toolName: 'shell', toolArgs: { command, cwd: str(p.cwd) || str(nested?.cwd) || undefined } };
|
|
240
244
|
}
|
|
241
245
|
case 'mcp': {
|
|
242
246
|
const toolName = str(p.tool_name) || str(p.toolName) || str(p.name) || 'mcp';
|
|
@@ -288,6 +292,15 @@ function summarizeToolCall(toolName, toolArgs) {
|
|
|
288
292
|
}
|
|
289
293
|
return parts.join(' ').slice(0, 500);
|
|
290
294
|
}
|
|
295
|
+
function spoolCallEvent(call, rest) {
|
|
296
|
+
const { operation } = (0, actionPolicyEngine_1.inferToolContext)(call.toolName, call.toolArgs);
|
|
297
|
+
(0, telemetry_1.spoolEvent)({
|
|
298
|
+
...rest,
|
|
299
|
+
toolName: call.toolName,
|
|
300
|
+
operation: rest.operation || operation,
|
|
301
|
+
evidence: rest.evidence || (0, telemetry_1.evidenceFromToolArgs)(call.toolArgs),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
291
304
|
function safeJson(v) {
|
|
292
305
|
try {
|
|
293
306
|
return JSON.stringify(v);
|
|
@@ -409,18 +422,18 @@ function tryLocalPolicyEnforcement(ctx, call, detail, opts = {}) {
|
|
|
409
422
|
if (ctx.shadow) {
|
|
410
423
|
// Truth-in-reporting: the action RAN — 'warn' (advisory), never 'allow'
|
|
411
424
|
// (which hides the would-block from the console) and never 'block'.
|
|
412
|
-
(
|
|
425
|
+
spoolCallEvent(call, { decision: 'warn', reason: `[monitor] local policy would ${local.verdict} (offline): ${reason}`, offlineEnforced: true });
|
|
413
426
|
(0, telemetry_1.triggerFlush)(false);
|
|
414
427
|
ctx.respond(false, undefined, `[FullCourtDefense shadow] would ${local.verdict === 'block' ? 'block' : 'require approval for'} ${call.toolName} (offline, locally cached policy): ${reason}`);
|
|
415
428
|
}
|
|
416
429
|
const approvalNote = local.verdict === 'require_approval'
|
|
417
430
|
? ' This action requires human approval, which is not possible while the policy service is unreachable.'
|
|
418
431
|
: '';
|
|
419
|
-
(
|
|
432
|
+
spoolCallEvent(call, { decision: 'block', reason: `local policy (offline): ${reason}`, offlineEnforced: true });
|
|
420
433
|
(0, telemetry_1.triggerFlush)(true);
|
|
421
434
|
ctx.respond(true, `Blocked by FullCourtDefense${local.policyName ? ` (${local.policyName})` : ''} — enforced locally while the policy service is unreachable.${approvalNote}`, `FullCourtDefense blocked this ${ctx.event} using the locally cached org policy${local.policyName ? ` "${local.policyName}"` : ''} (${detail}).${approvalNote} Do not retry until the connection is restored or the policy allows it.`);
|
|
422
435
|
}
|
|
423
|
-
(
|
|
436
|
+
spoolCallEvent(call, { decision: 'allow', reason: `local policy allow (offline): ${detail}`, offlineEnforced: true });
|
|
424
437
|
(0, telemetry_1.triggerFlush)(false);
|
|
425
438
|
ctx.respond(false, undefined, `FullCourtDefense: policy service unreachable — this ${ctx.event} was checked against the locally cached org policies and allowed.`);
|
|
426
439
|
}
|
|
@@ -1017,7 +1030,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
1017
1030
|
async function runTaintApproveOnce(ctx, event, call, sessId, taintFinding) {
|
|
1018
1031
|
const { respond } = ctx;
|
|
1019
1032
|
const hardBlock = () => {
|
|
1020
|
-
(
|
|
1033
|
+
spoolCallEvent(call, { decision: 'block', reason: taintFinding.reason, ruleId: taintFinding.ruleId, offlineEnforced: true });
|
|
1021
1034
|
(0, telemetry_1.triggerFlush)(true);
|
|
1022
1035
|
respond(true, `Blocked by FullCourtDefense taint guard — ${taintFinding.reason}`, `FullCourtDefense blocked this ${event} locally (${taintFinding.ruleId}): ${taintFinding.reason} Do not retry.`);
|
|
1023
1036
|
return false;
|
|
@@ -1055,9 +1068,8 @@ async function runTaintApproveOnce(ctx, event, call, sessId, taintFinding) {
|
|
|
1055
1068
|
const outcome = await (0, taintLedger_1.waitForTaintApproval)(approval.id, timeoutMs, Math.max(750, Math.min(ctx.approvalPollMs, 2000)));
|
|
1056
1069
|
(0, hookIo_1.dbg)({ phase: 'taint_approve_once_outcome', approvalId: approval.id, outcome });
|
|
1057
1070
|
if (outcome === 'approved') {
|
|
1058
|
-
(
|
|
1071
|
+
spoolCallEvent(call, {
|
|
1059
1072
|
decision: 'allow',
|
|
1060
|
-
toolName: call.toolName,
|
|
1061
1073
|
reason: `Developer approved once after taint-guard hold (${taintFinding.ruleId}): ${taintFinding.reason}`,
|
|
1062
1074
|
ruleId: taintFinding.ruleId,
|
|
1063
1075
|
offlineEnforced: true,
|
|
@@ -1066,9 +1078,8 @@ async function runTaintApproveOnce(ctx, event, call, sessId, taintFinding) {
|
|
|
1066
1078
|
(0, notify_1.notifyOs)({ title: 'FullCourtDefense — approved once', message: `${call.toolName} -> ${targetText} will continue.` });
|
|
1067
1079
|
return true;
|
|
1068
1080
|
}
|
|
1069
|
-
(
|
|
1081
|
+
spoolCallEvent(call, {
|
|
1070
1082
|
decision: 'block',
|
|
1071
|
-
toolName: call.toolName,
|
|
1072
1083
|
reason: `${taintFinding.reason} Developer ${outcome === 'denied' ? 'denied the approve-once request' : 'did not respond to the approve-once request'}.`,
|
|
1073
1084
|
ruleId: taintFinding.ruleId,
|
|
1074
1085
|
offlineEnforced: true,
|
|
@@ -1087,6 +1098,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1087
1098
|
respond(false);
|
|
1088
1099
|
return;
|
|
1089
1100
|
}
|
|
1101
|
+
(0, telemetry_1.setVerdictToolArgs)(call.toolArgs);
|
|
1090
1102
|
const snapshot = await (0, localSafetySnapshot_1.loadLocalSafetySnapshot)({
|
|
1091
1103
|
apiUrl,
|
|
1092
1104
|
shieldId,
|
|
@@ -1124,7 +1136,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1124
1136
|
respond(false, undefined, `[FullCourtDefense shadow] would block ${call.toolName}: ${selfApproval.reason}`);
|
|
1125
1137
|
return;
|
|
1126
1138
|
}
|
|
1127
|
-
(
|
|
1139
|
+
spoolCallEvent(call, { decision: 'block', reason: selfApproval.reason, ruleId: 'local-taint-self-approval', offlineEnforced: true });
|
|
1128
1140
|
(0, telemetry_1.triggerFlush)(true);
|
|
1129
1141
|
respond(true, `Blocked by FullCourtDefense self-protection — ${selfApproval.reason}`, `FullCourtDefense blocked this ${event} locally (local-taint-self-approval): ${selfApproval.reason} Do not retry.`);
|
|
1130
1142
|
return;
|
|
@@ -1164,7 +1176,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1164
1176
|
(0, hookIo_1.dbg)({ phase: 'policy_skip_monitor', event, tool: call.toolName });
|
|
1165
1177
|
const detail = 'monitor mode — synchronous policy check skipped by design.';
|
|
1166
1178
|
if (!tryLocalPolicyEnforcement(ctx, call, detail, { countAsGateFailure: false })) {
|
|
1167
|
-
(
|
|
1179
|
+
spoolCallEvent(call, { decision: 'allow', reason: `monitor: ${detail}`, offlineEnforced: true });
|
|
1168
1180
|
(0, telemetry_1.triggerFlush)(false);
|
|
1169
1181
|
respond(false);
|
|
1170
1182
|
}
|
|
@@ -1210,7 +1222,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1210
1222
|
void fetch(gateUrl, { method: 'POST', headers, body: gateBody, signal: AbortSignal.timeout(4000) })
|
|
1211
1223
|
.then(() => (0, policyGateHealth_1.recordGateSuccess)())
|
|
1212
1224
|
.catch(() => {
|
|
1213
|
-
(
|
|
1225
|
+
spoolCallEvent(call, { decision: 'allow', reason: 'local policy allow (block mode, local-first); gate report failed — spooled', offlineEnforced: true });
|
|
1214
1226
|
(0, telemetry_1.triggerFlush)(false);
|
|
1215
1227
|
});
|
|
1216
1228
|
respond(false);
|
|
@@ -1218,7 +1230,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1218
1230
|
}
|
|
1219
1231
|
if (localVerdict.verdict === 'block') {
|
|
1220
1232
|
const reason = localVerdict.reason || `${call.toolName}: blocked by org Action Policy`;
|
|
1221
|
-
(
|
|
1233
|
+
spoolCallEvent(call, { decision: 'block', reason: `local policy (block mode, local-first): ${reason}`, offlineEnforced: true });
|
|
1222
1234
|
(0, telemetry_1.triggerFlush)(true);
|
|
1223
1235
|
respond(true, `Blocked by FullCourtDefense${localVerdict.policyName ? ` (${localVerdict.policyName})` : ''} — ${reason}`, `FullCourtDefense blocked this ${event} by org Action Policy${localVerdict.policyName ? ` "${localVerdict.policyName}"` : ''}: ${reason}. This is a policy decision, not an error — do not retry.`);
|
|
1224
1236
|
return;
|
|
@@ -1009,6 +1009,7 @@ class McpGatewayServer {
|
|
|
1009
1009
|
if (!toolName)
|
|
1010
1010
|
throw new Error('Missing MCP tool name.');
|
|
1011
1011
|
const toolArgs = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
1012
|
+
(0, telemetry_1.setVerdictToolArgs)(toolArgs);
|
|
1012
1013
|
let approvalActionId;
|
|
1013
1014
|
let operation = typeof params.operation === 'string' && params.operation.trim() ? params.operation.trim() : undefined;
|
|
1014
1015
|
// Tracks the downstream call state so the top-level catch can fail open
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -36,9 +36,17 @@ export interface SpoolEvent {
|
|
|
36
36
|
* - `fail_open` the hook crashed and allowed by contract
|
|
37
37
|
*/
|
|
38
38
|
export type VerdictPath = 'ipc' | 'local' | 'fail_open' | 'gateway';
|
|
39
|
+
/**
|
|
40
|
+
* Compact JSON of the command/path/query the agent attempted. The console
|
|
41
|
+
* Details column renders this as the labeled Command / Query / URL / File
|
|
42
|
+
* row. Truncated to 500 chars so it survives backend sanitize().
|
|
43
|
+
*/
|
|
44
|
+
export declare function evidenceFromToolArgs(toolArgs: Record<string, unknown> | undefined): string | undefined;
|
|
39
45
|
interface VerdictTiming {
|
|
40
46
|
startedAt: number;
|
|
41
47
|
path: VerdictPath;
|
|
48
|
+
/** Tool args for this evaluation — stamps `evidence` onto every spooled event. */
|
|
49
|
+
toolArgs?: Record<string, unknown>;
|
|
42
50
|
}
|
|
43
51
|
/** Run one hook evaluation with timing attached to everything it spools. */
|
|
44
52
|
export declare function markVerdictTiming<T>(timing: VerdictTiming, fn: () => T): T;
|
|
@@ -47,6 +55,8 @@ export declare function markVerdictTiming<T>(timing: VerdictTiming, fn: () => T)
|
|
|
47
55
|
* Keeps the original start time so latency still covers the full wait.
|
|
48
56
|
*/
|
|
49
57
|
export declare function setVerdictPath(path: VerdictPath): void;
|
|
58
|
+
/** Attach the current tool call's args so every spool in this evaluation carries the command. */
|
|
59
|
+
export declare function setVerdictToolArgs(toolArgs: Record<string, unknown>): void;
|
|
50
60
|
/**
|
|
51
61
|
* Restart the current evaluation's clock. Used by the MCP gateway right after
|
|
52
62
|
* the downstream tool returns: everything spooled from then on (response
|
package/dist/telemetry.js
CHANGED
|
@@ -33,8 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.evidenceFromToolArgs = evidenceFromToolArgs;
|
|
36
37
|
exports.markVerdictTiming = markVerdictTiming;
|
|
37
38
|
exports.setVerdictPath = setVerdictPath;
|
|
39
|
+
exports.setVerdictToolArgs = setVerdictToolArgs;
|
|
38
40
|
exports.restartVerdictTiming = restartVerdictTiming;
|
|
39
41
|
exports.spoolEvent = spoolEvent;
|
|
40
42
|
exports.flushSpool = flushSpool;
|
|
@@ -67,6 +69,35 @@ const MAX_BATCH = 200;
|
|
|
67
69
|
const MAX_SPOOL_EVENTS = 2_000;
|
|
68
70
|
/** Cheap pre-check: skip the trim parse entirely until the file is plausibly over cap. */
|
|
69
71
|
const SPOOL_TRIM_BYTES = 2 * 1024 * 1024;
|
|
72
|
+
/** Args keys that carry the action a human wants to see in the console. */
|
|
73
|
+
const ACTION_EVIDENCE_KEYS = [
|
|
74
|
+
'command', 'cmd', 'commandLine', 'script', 'query', 'sql', 'url',
|
|
75
|
+
'path', 'file_path', 'filePath', 'file', 'target',
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Compact JSON of the command/path/query the agent attempted. The console
|
|
79
|
+
* Details column renders this as the labeled Command / Query / URL / File
|
|
80
|
+
* row. Truncated to 500 chars so it survives backend sanitize().
|
|
81
|
+
*/
|
|
82
|
+
function evidenceFromToolArgs(toolArgs) {
|
|
83
|
+
if (!toolArgs)
|
|
84
|
+
return undefined;
|
|
85
|
+
const picked = {};
|
|
86
|
+
for (const key of ACTION_EVIDENCE_KEYS) {
|
|
87
|
+
const value = toolArgs[key];
|
|
88
|
+
if (typeof value === 'string' && value.trim()) {
|
|
89
|
+
picked[key] = value.trim().slice(0, 480);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (Object.keys(picked).length === 0)
|
|
93
|
+
return undefined;
|
|
94
|
+
try {
|
|
95
|
+
return JSON.stringify(picked).slice(0, 500);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return Object.values(picked)[0]?.slice(0, 500);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
70
101
|
/**
|
|
71
102
|
* Per-evaluation timing context.
|
|
72
103
|
*
|
|
@@ -93,6 +124,12 @@ function setVerdictPath(path) {
|
|
|
93
124
|
if (store)
|
|
94
125
|
store.path = path;
|
|
95
126
|
}
|
|
127
|
+
/** Attach the current tool call's args so every spool in this evaluation carries the command. */
|
|
128
|
+
function setVerdictToolArgs(toolArgs) {
|
|
129
|
+
const store = verdictTiming.getStore();
|
|
130
|
+
if (store)
|
|
131
|
+
store.toolArgs = toolArgs;
|
|
132
|
+
}
|
|
96
133
|
/**
|
|
97
134
|
* Restart the current evaluation's clock. Used by the MCP gateway right after
|
|
98
135
|
* the downstream tool returns: everything spooled from then on (response
|
|
@@ -125,7 +162,7 @@ function spoolEvent(event) {
|
|
|
125
162
|
itemId: event.itemId,
|
|
126
163
|
severity: event.severity,
|
|
127
164
|
source: event.source,
|
|
128
|
-
evidence: event.evidence,
|
|
165
|
+
evidence: event.evidence || evidenceFromToolArgs(timing?.toolArgs),
|
|
129
166
|
explanation: event.explanation,
|
|
130
167
|
policyHash: event.policyHash,
|
|
131
168
|
offlineEnforced: event.offlineEnforced,
|
package/dist/version.json
CHANGED