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.
- package/dist/commands/hook.js +45 -0
- package/dist/commands/mcpGateway.js +9 -1
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/hook.js
CHANGED
|
@@ -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
|
|
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