fullcourtdefense-cli 1.27.1 → 1.27.2

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.
@@ -338,6 +338,32 @@ function spoolCallEvent(call, rest) {
338
338
  evidence: rest.evidence || (0, telemetry_1.evidenceFromToolArgs)(call.toolArgs),
339
339
  });
340
340
  }
341
+ /**
342
+ * A `log` verdict ("Warn only" in the console) is a real warning, not a silent
343
+ * allow: the action runs, the developer sees why it was flagged, and the fleet
344
+ * records it as Warned. Same delivery as a block — spool synchronously and let
345
+ * the detached flusher outlive a standalone hook process — so the warning is
346
+ * never lost on CI runners / containers where respond() ends the process.
347
+ */
348
+ function warnByPolicy(call, verdict, event, exitsProcess, source) {
349
+ const reason = verdict.reason || `${call.toolName}: flagged by org Action Policy`;
350
+ const named = verdict.policyName ? ` (${verdict.policyName})` : '';
351
+ if (source === 'local-first') {
352
+ // The gate path is recorded as Warned by the server itself (it saw the
353
+ // call); only the local verdict has nobody else to write the audit.
354
+ spoolCallEvent(call, {
355
+ decision: 'warn',
356
+ reason: `policy warn (local-first): ${reason}`,
357
+ ruleId: verdict.policyName ? `action-policy:${verdict.policyName}` : undefined,
358
+ offlineEnforced: true,
359
+ });
360
+ (0, telemetry_1.triggerFlush)(exitsProcess);
361
+ }
362
+ return {
363
+ userMsg: `FullCourtDefense warning${named} — ${reason}. The ${event} was allowed and recorded as Warned.`,
364
+ agentMsg: `FullCourtDefense warning${named}: ${reason}. This ${event} was allowed by a warn-only Action Policy and recorded as Warned — proceed, but mention the warning to the developer.`,
365
+ };
366
+ }
341
367
  function safeJson(v) {
342
368
  try {
343
369
  return JSON.stringify(v);
@@ -631,6 +657,11 @@ function tryLocalPolicyEnforcement(ctx, call, detail, opts = {}) {
631
657
  (0, telemetry_1.triggerFlush)(true);
632
658
  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.`);
633
659
  }
660
+ if (local.verdict === 'log') {
661
+ // Warn-only rule, offline: still a real warning + Warned in the fleet.
662
+ const warning = warnByPolicy(call, local, ctx.event, ctx.exitsProcess, 'local-first');
663
+ ctx.respond(false, warning.userMsg, `${warning.agentMsg} (policy service unreachable — evaluated from the locally cached org policies).`);
664
+ }
634
665
  spoolCallEvent(call, { decision: 'allow', reason: `local policy allow (offline): ${detail}`, offlineEnforced: true });
635
666
  (0, telemetry_1.triggerFlush)(false);
636
667
  ctx.respond(false, undefined, `FullCourtDefense: policy service unreachable — this ${ctx.event} was checked against the locally cached org policies and allowed.`);
@@ -1483,6 +1514,13 @@ async function enforceActionPolicy(ctx) {
1483
1514
  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.`);
1484
1515
  return;
1485
1516
  }
1517
+ if (localVerdict.verdict === 'log') {
1518
+ // "Warn only": runs, developer told why, fleet records Warned. No gate
1519
+ // round trip — the verdict is final and the audit rides the spool.
1520
+ const warning = warnByPolicy(call, localVerdict, event, ctx.exitsProcess, 'local-first');
1521
+ respond(false, warning.userMsg, warning.agentMsg);
1522
+ return;
1523
+ }
1486
1524
  // require_approval, developer scope + human present → IDE-native ask (no org queue).
1487
1525
  if (tryDeveloperConfirmation({ event, respond, shadow }, call, localVerdict, payload, 'local-first'))
1488
1526
  return;
@@ -1564,6 +1602,13 @@ async function enforceActionPolicy(ctx) {
1564
1602
  const policyName = result.actionPolicy?.policyName;
1565
1603
  (0, hookIo_1.dbg)({ phase: 'policy_verdict', event, tool: call.toolName, decision: result.decision, allowed: result.allowed, policyName });
1566
1604
  if (result.allowed && result.decision === 'allow') {
1605
+ if (result.actionPolicy?.verdict === 'log') {
1606
+ // The server allowed it under a warn-only rule; surface the warning and
1607
+ // record Warned (the server only records blocks/approvals itself).
1608
+ const warning = warnByPolicy(call, result.actionPolicy, event, ctx.exitsProcess, 'gate');
1609
+ respond(false, warning.userMsg, warning.agentMsg);
1610
+ return;
1611
+ }
1567
1612
  respond(false);
1568
1613
  return;
1569
1614
  }
@@ -1213,7 +1213,15 @@ class McpGatewayServer {
1213
1213
  (0, notify_1.notifyOs)({ title: 'FullCourtDefense — blocked by policy', message: `${toolName} blocked${local.policyName ? ` (${local.policyName})` : ''}.` });
1214
1214
  throw blockError(`FullCourtDefense blocked ${toolName} by org Action Policy${local.policyName ? ` "${local.policyName}"` : ''}: ${reason}. This is a policy decision, not an error — do not retry.`);
1215
1215
  }
1216
- if (local.verdict !== 'require_approval') {
1216
+ if (local.verdict === 'log') {
1217
+ // "Warn only": the call runs, the fleet records Warned (not a silent
1218
+ // allow). Final verdict — no gate round trip; the audit rides the spool.
1219
+ const reason = local.reason || `${toolName}: flagged by org Action Policy`;
1220
+ (0, telemetry_1.spoolEvent)({ decision: 'warn', toolName, operation: localOperation, reason: `policy warn (local-first): ${reason}`, ruleId: local.policyName ? `action-policy:${local.policyName}` : undefined, offlineEnforced: true });
1221
+ (0, telemetry_1.triggerFlush)(false);
1222
+ preflight = { allowed: true, decision: 'allow', operation: localOperation };
1223
+ }
1224
+ else if (local.verdict !== 'require_approval') {
1217
1225
  // Local allow: the server still records the action asynchronously; a
1218
1226
  // failed report spools (idempotent on eventId) for the next flush.
1219
1227
  // During a recently-down gate window, spool directly (no per-call
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.27.1"
2
+ "version": "1.27.2"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.27.1",
3
+ "version": "1.27.2",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {