fullcourtdefense-cli 1.6.2 → 1.6.4
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 -8
- package/dist/commands/installCursorHook.js +11 -3
- 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 = {};
|
|
@@ -232,7 +249,7 @@ async function hookCommand(args, config) {
|
|
|
232
249
|
const shadow = args.shadow === 'true';
|
|
233
250
|
const failClosed = args.failClosed === 'true';
|
|
234
251
|
const timeoutMs = Number(args.timeout) > 0 ? Number(args.timeout) : 8000;
|
|
235
|
-
const approvalMode = args.approvalMode === '
|
|
252
|
+
const approvalMode = args.approvalMode === 'wait' ? 'wait' : 'block';
|
|
236
253
|
const approvalTimeoutMs = Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 300000;
|
|
237
254
|
const approvalPollMs = Math.max(1000, Number(args.approvalPollMs) > 0 ? Number(args.approvalPollMs) : 3000);
|
|
238
255
|
const creds = (0, config_1.resolveCliCredentials)(config, {
|
|
@@ -316,7 +333,7 @@ async function hookCommand(args, config) {
|
|
|
316
333
|
respond(false);
|
|
317
334
|
}
|
|
318
335
|
async function enforceActionPolicy(ctx) {
|
|
319
|
-
const { event, payload, apiUrl, shieldId, shieldKey, shadow,
|
|
336
|
+
const { event, payload, apiUrl, shieldId, shieldKey, shadow, timeoutMs, respond } = ctx;
|
|
320
337
|
const call = buildToolCall(event, payload);
|
|
321
338
|
if (!call) {
|
|
322
339
|
respond(false);
|
|
@@ -347,20 +364,20 @@ async function enforceActionPolicy(ctx) {
|
|
|
347
364
|
clearTimeout(timer);
|
|
348
365
|
if (!resp.ok) {
|
|
349
366
|
dbg({ phase: 'policy_http_error', event, status: resp.status });
|
|
350
|
-
respond(
|
|
367
|
+
respond(false, undefined, degradedAllowMessage(event, `Backend returned HTTP ${resp.status}.`));
|
|
351
368
|
return;
|
|
352
369
|
}
|
|
353
370
|
const body = await resp.json().catch(() => ({}));
|
|
354
371
|
if (!body.success || !body.data) {
|
|
355
372
|
dbg({ phase: 'policy_no_data', event, error: body.error });
|
|
356
|
-
respond(
|
|
373
|
+
respond(false, undefined, degradedAllowMessage(event, body.error ? `Backend error: ${body.error}` : 'Backend returned no policy data.'));
|
|
357
374
|
return;
|
|
358
375
|
}
|
|
359
376
|
result = body.data;
|
|
360
377
|
}
|
|
361
378
|
catch (err) {
|
|
362
379
|
dbg({ phase: 'policy_exception', event, error: err instanceof Error ? err.message : String(err) });
|
|
363
|
-
respond(
|
|
380
|
+
respond(false, undefined, degradedAllowMessage(event, `Hook error: ${err instanceof Error ? err.message : String(err)}.`));
|
|
364
381
|
return;
|
|
365
382
|
}
|
|
366
383
|
const reason = result.actionPolicy?.reason
|
|
@@ -453,7 +470,8 @@ async function enforceShieldText(ctx) {
|
|
|
453
470
|
});
|
|
454
471
|
clearTimeout(timer);
|
|
455
472
|
if (!resp.ok) {
|
|
456
|
-
respond(
|
|
473
|
+
respond(false, undefined, degradedAllowMessage(event, `Backend returned HTTP ${resp.status}.`));
|
|
474
|
+
return;
|
|
457
475
|
}
|
|
458
476
|
const data = await resp.json().catch(() => ({}));
|
|
459
477
|
const sh = (data._shield || {});
|
|
@@ -468,6 +486,6 @@ async function enforceShieldText(ctx) {
|
|
|
468
486
|
respond(true, `Blocked by FullCourtDefense — ${reason}.`, `FullCourtDefense blocked this ${event} as "${reason}". Do not retry; revise to remove the flagged content.`);
|
|
469
487
|
}
|
|
470
488
|
catch (err) {
|
|
471
|
-
respond(
|
|
489
|
+
respond(false, undefined, degradedAllowMessage(event, `Hook error: ${err instanceof Error ? err.message : String(err)}.`));
|
|
472
490
|
}
|
|
473
491
|
}
|
|
@@ -123,6 +123,16 @@ async function installCursorHookCommand(args, config) {
|
|
|
123
123
|
});
|
|
124
124
|
const credFile = ensureHomeShield(creds);
|
|
125
125
|
const json = readHooksJson(file);
|
|
126
|
+
// Clear all previous FullCourtDefense-managed hooks first. This prevents old
|
|
127
|
+
// prompt/read/file entries from surviving when the installer narrows the event set.
|
|
128
|
+
for (const key of Object.keys(json.hooks)) {
|
|
129
|
+
const list = Array.isArray(json.hooks[key]) ? json.hooks[key] : [];
|
|
130
|
+
const kept = list.filter((x) => !isManaged(x));
|
|
131
|
+
if (kept.length)
|
|
132
|
+
json.hooks[key] = kept;
|
|
133
|
+
else
|
|
134
|
+
delete json.hooks[key];
|
|
135
|
+
}
|
|
126
136
|
for (const e of events) {
|
|
127
137
|
const { hookKey, flag, failClosedDefault, timeoutSec } = EVENT_MAP[e];
|
|
128
138
|
const failClosed = e === 'prompt' ? false : (args.failClosed !== undefined ? args.failClosed === 'true' : failClosedDefault);
|
|
@@ -133,9 +143,7 @@ async function installCursorHookCommand(args, config) {
|
|
|
133
143
|
if (failClosed)
|
|
134
144
|
entry.failClosed = true;
|
|
135
145
|
const list = Array.isArray(json.hooks[hookKey]) ? json.hooks[hookKey] : [];
|
|
136
|
-
|
|
137
|
-
const preserved = list.filter((x) => !isManaged(x));
|
|
138
|
-
json.hooks[hookKey] = [...preserved, entry];
|
|
146
|
+
json.hooks[hookKey] = [...list, entry];
|
|
139
147
|
}
|
|
140
148
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
141
149
|
fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n', 'utf8');
|
package/dist/version.json
CHANGED