fullcourtdefense-cli 1.6.3 → 1.6.5
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 +62 -9
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/hook.js
CHANGED
|
@@ -135,7 +135,21 @@ function buildToolCall(event, p) {
|
|
|
135
135
|
}
|
|
136
136
|
case 'mcp': {
|
|
137
137
|
const toolName = str(p.tool_name) || str(p.toolName) || str(p.name) || 'mcp';
|
|
138
|
-
|
|
138
|
+
let rawInput = p.tool_input ?? p.arguments ?? p.input ?? p.params;
|
|
139
|
+
// Cursor sends `tool_input` as a JSON-ENCODED STRING (e.g. '{"command":"DROP ..."}').
|
|
140
|
+
// Parse it so the real args (command/query/sql/...) are visible to the policy engine.
|
|
141
|
+
// Without this, args collapse to { value: "<json string>" }, no operation verb is
|
|
142
|
+
// inferred, and the operation falls back to the tool NAME — so a tool called
|
|
143
|
+
// `drop_production_database` would match a DROP rule for EVERY command (SELECT included).
|
|
144
|
+
if (typeof rawInput === 'string') {
|
|
145
|
+
const trimmed = rawInput.trim();
|
|
146
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
147
|
+
try {
|
|
148
|
+
rawInput = JSON.parse(trimmed);
|
|
149
|
+
}
|
|
150
|
+
catch { /* keep the raw string */ }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
139
153
|
const toolArgs = rawInput && typeof rawInput === 'object' ? rawInput : { value: rawInput };
|
|
140
154
|
return { toolName, toolArgs };
|
|
141
155
|
}
|
|
@@ -177,6 +191,9 @@ function safeJson(v) {
|
|
|
177
191
|
return String(v);
|
|
178
192
|
}
|
|
179
193
|
}
|
|
194
|
+
function degradedAllowMessage(event, detail) {
|
|
195
|
+
return `FullCourtDefense policy gate unavailable; allowed ${event} in degraded mode. ${detail}`.trim();
|
|
196
|
+
}
|
|
180
197
|
/** Per-event attribution that maps onto the existing Shield runtime headers (prompt path). */
|
|
181
198
|
function attributionFor(event, p) {
|
|
182
199
|
const h = {};
|
|
@@ -205,6 +222,37 @@ function developerId() {
|
|
|
205
222
|
return 'unknown-developer';
|
|
206
223
|
}
|
|
207
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Normalize an identity component — MUST stay byte-for-byte identical to the MCP
|
|
227
|
+
* gateway's `safeIdentityPart` (mcpGateway.ts) so the hook and the gateway build
|
|
228
|
+
* the SAME per-server agent identity for the same server.
|
|
229
|
+
*/
|
|
230
|
+
function safeIdentityPart(value) {
|
|
231
|
+
return value
|
|
232
|
+
.trim()
|
|
233
|
+
.replace(/@.*$/, '')
|
|
234
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
235
|
+
.replace(/^-+|-+$/g, '')
|
|
236
|
+
.toLowerCase() || 'local-user';
|
|
237
|
+
}
|
|
238
|
+
/** Extract the MCP server name Cursor reports for a beforeMCPExecution payload. */
|
|
239
|
+
function mcpServerName(p) {
|
|
240
|
+
return str(p.mcp_server_name) || str(p.server_name) || str(p.serverName) || str(p.mcpServer) || undefined;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Runtime agent identity for a tool call. For MCP calls we mirror the gateway's
|
|
244
|
+
* per-server identity (`<developer>-cursor-<server>`) so a per-agent policy created
|
|
245
|
+
* for a gateway-observed agent ALSO matches the same server when it flows through
|
|
246
|
+
* the in-IDE hook. Non-MCP events keep the collapsed per-developer identity.
|
|
247
|
+
*/
|
|
248
|
+
function agentNameForCall(event, p) {
|
|
249
|
+
if (event === 'mcp') {
|
|
250
|
+
const server = mcpServerName(p);
|
|
251
|
+
if (server)
|
|
252
|
+
return `${safeIdentityPart(developerId())}-cursor-${safeIdentityPart(server)}`;
|
|
253
|
+
}
|
|
254
|
+
return `cursor-${developerId()}`;
|
|
255
|
+
}
|
|
208
256
|
/** Cursor conversation/session id when present, else a stable per-machine id. */
|
|
209
257
|
function sessionId(p) {
|
|
210
258
|
const fromPayload = p.conversation_id || p.conversationId || p.session_id || p.sessionId;
|
|
@@ -232,7 +280,7 @@ async function hookCommand(args, config) {
|
|
|
232
280
|
const shadow = args.shadow === 'true';
|
|
233
281
|
const failClosed = args.failClosed === 'true';
|
|
234
282
|
const timeoutMs = Number(args.timeout) > 0 ? Number(args.timeout) : 8000;
|
|
235
|
-
const approvalMode = args.approvalMode === '
|
|
283
|
+
const approvalMode = args.approvalMode === 'wait' ? 'wait' : 'block';
|
|
236
284
|
const approvalTimeoutMs = Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 300000;
|
|
237
285
|
const approvalPollMs = Math.max(1000, Number(args.approvalPollMs) > 0 ? Number(args.approvalPollMs) : 3000);
|
|
238
286
|
const creds = (0, config_1.resolveCliCredentials)(config, {
|
|
@@ -316,7 +364,7 @@ async function hookCommand(args, config) {
|
|
|
316
364
|
respond(false);
|
|
317
365
|
}
|
|
318
366
|
async function enforceActionPolicy(ctx) {
|
|
319
|
-
const { event, payload, apiUrl, shieldId, shieldKey, shadow,
|
|
367
|
+
const { event, payload, apiUrl, shieldId, shieldKey, shadow, timeoutMs, respond } = ctx;
|
|
320
368
|
const call = buildToolCall(event, payload);
|
|
321
369
|
if (!call) {
|
|
322
370
|
respond(false);
|
|
@@ -334,12 +382,16 @@ async function enforceActionPolicy(ctx) {
|
|
|
334
382
|
headers,
|
|
335
383
|
body: JSON.stringify({
|
|
336
384
|
shieldId,
|
|
337
|
-
|
|
385
|
+
// For MCP calls this mirrors the gateway's per-server identity
|
|
386
|
+
// (`<developer>-cursor-<server>`), so a per-agent policy matches the same
|
|
387
|
+
// server on BOTH the gateway path and this in-IDE hook path.
|
|
388
|
+
agentName: agentNameForCall(event, payload),
|
|
338
389
|
toolName: call.toolName,
|
|
339
390
|
toolArgs: call.toolArgs,
|
|
340
391
|
argsSummary: summarizeToolCall(call.toolName, call.toolArgs),
|
|
341
392
|
sessionId: sessionId(payload),
|
|
342
393
|
source: 'cursor_hook',
|
|
394
|
+
...(event === 'mcp' && mcpServerName(payload) ? { mcpServer: mcpServerName(payload) } : {}),
|
|
343
395
|
...machineMetadata(),
|
|
344
396
|
}),
|
|
345
397
|
signal: controller.signal,
|
|
@@ -347,20 +399,20 @@ async function enforceActionPolicy(ctx) {
|
|
|
347
399
|
clearTimeout(timer);
|
|
348
400
|
if (!resp.ok) {
|
|
349
401
|
dbg({ phase: 'policy_http_error', event, status: resp.status });
|
|
350
|
-
respond(
|
|
402
|
+
respond(false, undefined, degradedAllowMessage(event, `Backend returned HTTP ${resp.status}.`));
|
|
351
403
|
return;
|
|
352
404
|
}
|
|
353
405
|
const body = await resp.json().catch(() => ({}));
|
|
354
406
|
if (!body.success || !body.data) {
|
|
355
407
|
dbg({ phase: 'policy_no_data', event, error: body.error });
|
|
356
|
-
respond(
|
|
408
|
+
respond(false, undefined, degradedAllowMessage(event, body.error ? `Backend error: ${body.error}` : 'Backend returned no policy data.'));
|
|
357
409
|
return;
|
|
358
410
|
}
|
|
359
411
|
result = body.data;
|
|
360
412
|
}
|
|
361
413
|
catch (err) {
|
|
362
414
|
dbg({ phase: 'policy_exception', event, error: err instanceof Error ? err.message : String(err) });
|
|
363
|
-
respond(
|
|
415
|
+
respond(false, undefined, degradedAllowMessage(event, `Hook error: ${err instanceof Error ? err.message : String(err)}.`));
|
|
364
416
|
return;
|
|
365
417
|
}
|
|
366
418
|
const reason = result.actionPolicy?.reason
|
|
@@ -453,7 +505,8 @@ async function enforceShieldText(ctx) {
|
|
|
453
505
|
});
|
|
454
506
|
clearTimeout(timer);
|
|
455
507
|
if (!resp.ok) {
|
|
456
|
-
respond(
|
|
508
|
+
respond(false, undefined, degradedAllowMessage(event, `Backend returned HTTP ${resp.status}.`));
|
|
509
|
+
return;
|
|
457
510
|
}
|
|
458
511
|
const data = await resp.json().catch(() => ({}));
|
|
459
512
|
const sh = (data._shield || {});
|
|
@@ -468,6 +521,6 @@ async function enforceShieldText(ctx) {
|
|
|
468
521
|
respond(true, `Blocked by FullCourtDefense — ${reason}.`, `FullCourtDefense blocked this ${event} as "${reason}". Do not retry; revise to remove the flagged content.`);
|
|
469
522
|
}
|
|
470
523
|
catch (err) {
|
|
471
|
-
respond(
|
|
524
|
+
respond(false, undefined, degradedAllowMessage(event, `Hook error: ${err instanceof Error ? err.message : String(err)}.`));
|
|
472
525
|
}
|
|
473
526
|
}
|
package/dist/version.json
CHANGED