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.
@@ -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
- const rawInput = p.tool_input ?? p.arguments ?? p.input ?? p.params;
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 === 'block' ? 'block' : 'wait';
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, failClosed, timeoutMs, respond } = ctx;
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
- agentName: `cursor-${developerId()}`,
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(failClosed, failClosed ? `FullCourtDefense policy gate unavailable (HTTP ${resp.status}) — blocked by fail-closed policy.` : undefined, `FullCourtDefense policy gate returned HTTP ${resp.status}.`);
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(failClosed, failClosed ? 'FullCourtDefense policy gate error blocked by fail-closed policy.' : undefined, body.error);
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(failClosed, failClosed ? 'FullCourtDefense policy gate unreachable — blocked by fail-closed policy.' : undefined, `FullCourtDefense hook error: ${err instanceof Error ? err.message : String(err)}.`);
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(failClosed, failClosed ? `FullCourtDefense gate unavailable (HTTP ${resp.status}) — blocked by fail-closed policy.` : undefined, `FullCourtDefense gate returned HTTP ${resp.status}.`);
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(failClosed, failClosed ? 'FullCourtDefense gate unreachable — blocked by fail-closed policy.' : undefined, `FullCourtDefense hook error: ${err instanceof Error ? err.message : String(err)}.`);
524
+ respond(false, undefined, degradedAllowMessage(event, `Hook error: ${err instanceof Error ? err.message : String(err)}.`));
472
525
  }
473
526
  }
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.6.3"
2
+ "version": "1.6.5"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {