fullcourtdefense-cli 1.26.3 → 1.26.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/daemon.js +40 -31
- package/dist/commands/hook.js +36 -102
- package/dist/commands/installClaudeHook.js +17 -3
- package/dist/commands/installCursorHook.js +24 -4
- package/dist/distress.d.ts +6 -0
- package/dist/distress.js +6 -0
- package/dist/hookIo.d.ts +43 -0
- package/dist/hookIo.js +188 -0
- package/dist/hookSlim.d.ts +32 -0
- package/dist/hookSlim.js +142 -0
- package/dist/index.js +4 -14
- package/dist/pollLoop.d.ts +72 -0
- package/dist/pollLoop.js +116 -0
- package/dist/verdictIpc.d.ts +12 -75
- package/dist/verdictIpc.js +30 -160
- package/dist/verdictIpcClient.d.ts +76 -0
- package/dist/verdictIpcClient.js +183 -0
- package/dist/version.json +1 -1
- package/package.json +3 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -59,6 +59,7 @@ const path = __importStar(require("path"));
|
|
|
59
59
|
const child_process_1 = require("child_process");
|
|
60
60
|
const config_1 = require("../config");
|
|
61
61
|
const distress_1 = require("../distress");
|
|
62
|
+
const pollLoop_1 = require("../pollLoop");
|
|
62
63
|
const selfTest_1 = require("../selfTest");
|
|
63
64
|
const daemonForensics_1 = require("../daemonForensics");
|
|
64
65
|
const securityAgents_1 = require("../securityAgents");
|
|
@@ -107,6 +108,14 @@ const RESCAN_INTERVAL_MS = envMs('FCD_DAEMON_RESCAN_MS', 5 * 60_000);
|
|
|
107
108
|
const HEARTBEAT_INTERVAL_MS = envMs('FCD_DAEMON_HEARTBEAT_MS', 5 * 60_000);
|
|
108
109
|
/** Bundle (mode / suspension / policy version) poll cadence. */
|
|
109
110
|
const BUNDLE_POLL_MS = envMs('FCD_DAEMON_BUNDLE_POLL_MS', 60_000);
|
|
111
|
+
/**
|
|
112
|
+
* Hard ceilings for one poll attempt (see startPollLoop). Generous multiples of
|
|
113
|
+
* the work each poll actually does — a bundle poll is a 1.5s fetch plus an 8s
|
|
114
|
+
* snapshot refresh, a heartbeat is a spool flush plus a log upload — so these
|
|
115
|
+
* only ever fire on a genuinely hung socket, never on a merely slow network.
|
|
116
|
+
*/
|
|
117
|
+
const BUNDLE_POLL_DEADLINE_MS = envMs('FCD_DAEMON_BUNDLE_DEADLINE_MS', 45_000);
|
|
118
|
+
const HEARTBEAT_DEADLINE_MS = envMs('FCD_DAEMON_HEARTBEAT_DEADLINE_MS', 120_000);
|
|
110
119
|
/** Delay before the one-time initial discovery sweep on a fresh machine. */
|
|
111
120
|
const INITIAL_DISCOVER_DELAY_MS = envMs('FCD_DAEMON_INITIAL_DISCOVER_MS', 2 * 60_000);
|
|
112
121
|
/** A discovery upload older than this is stale — the daemon catches up itself. */
|
|
@@ -1287,33 +1296,35 @@ async function runDaemon(args, config) {
|
|
|
1287
1296
|
// The bundle poll additionally honors the server's pollIntervalMs, and the
|
|
1288
1297
|
// next tick is scheduled only AFTER the previous poll finishes — a slow
|
|
1289
1298
|
// gate can never pile up overlapping polls.
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1299
|
+
//
|
|
1300
|
+
// Both loops run through startPollLoop, which bounds every attempt with a
|
|
1301
|
+
// hard deadline. A poll that never settles (socket dead without a FIN after
|
|
1302
|
+
// sleep / VPN change / proxy drop — `AbortSignal.timeout` does not always
|
|
1303
|
+
// rescue that) used to kill its loop for the life of the process. When that
|
|
1304
|
+
// happened to the bundle poll, remote actions — whose ONLY delivery path is
|
|
1305
|
+
// this poll — silently expired unacknowledged while heartbeats kept the
|
|
1306
|
+
// machine looking healthy. Abandoning the hung attempt restores delivery on
|
|
1307
|
+
// the next tick and leaves a `poll_stalled` signal in the fleet.
|
|
1308
|
+
const bundleLoop = (0, pollLoop_1.startPollLoop)({
|
|
1309
|
+
run: pollBundle,
|
|
1310
|
+
intervalMs: () => bundlePollBaseMs,
|
|
1311
|
+
deadlineMs: BUNDLE_POLL_DEADLINE_MS,
|
|
1312
|
+
onStalled: deadlineMs => {
|
|
1313
|
+
log(`Bundle poll did not finish within ${Math.round(deadlineMs / 1000)}s — abandoned; remote actions and policy updates were delayed. Next poll continues normally.`);
|
|
1314
|
+
(0, distress_1.reportDistress)('bundle', distress_1.DISTRESS.POLL_STALLED, `bundle poll exceeded ${deadlineMs}ms`);
|
|
1315
|
+
},
|
|
1316
|
+
onError: error => log(`Bundle poll error: ${error instanceof Error ? error.message : String(error)}`),
|
|
1317
|
+
});
|
|
1318
|
+
const heartbeatLoop = (0, pollLoop_1.startPollLoop)({
|
|
1319
|
+
run: heartbeat,
|
|
1320
|
+
intervalMs: () => HEARTBEAT_INTERVAL_MS,
|
|
1321
|
+
deadlineMs: HEARTBEAT_DEADLINE_MS,
|
|
1322
|
+
onStalled: deadlineMs => {
|
|
1323
|
+
log(`Heartbeat did not finish within ${Math.round(deadlineMs / 1000)}s — abandoned; spooled events stay on disk for the next tick.`);
|
|
1324
|
+
(0, distress_1.reportDistress)('heartbeat', distress_1.DISTRESS.POLL_STALLED, `heartbeat exceeded ${deadlineMs}ms`);
|
|
1325
|
+
},
|
|
1326
|
+
onError: error => log(`Heartbeat error: ${error instanceof Error ? error.message : String(error)}`),
|
|
1327
|
+
});
|
|
1317
1328
|
// PowerShell transcript retention (Windows): the Transcription policy FCD
|
|
1318
1329
|
// enables writes a file per session forever — prune anything older than the
|
|
1319
1330
|
// retention window once a day (plus once shortly after boot, so laptops
|
|
@@ -1337,10 +1348,8 @@ async function runDaemon(args, config) {
|
|
|
1337
1348
|
stopped = true;
|
|
1338
1349
|
log(`Received ${signal} — shutting down.`);
|
|
1339
1350
|
clearInterval(rescanTimer);
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
if (heartbeatTimer)
|
|
1343
|
-
clearTimeout(heartbeatTimer);
|
|
1351
|
+
bundleLoop.stop();
|
|
1352
|
+
heartbeatLoop.stop();
|
|
1344
1353
|
clearTimeout(transcriptPruneBootTimer);
|
|
1345
1354
|
clearInterval(transcriptPruneTimer);
|
|
1346
1355
|
clearTimeout(discoverCatchUpBootTimer);
|
package/dist/commands/hook.js
CHANGED
|
@@ -36,9 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.normalizeClaudePayload = normalizeClaudePayload;
|
|
37
37
|
exports.evaluateHookRequest = evaluateHookRequest;
|
|
38
38
|
exports.hookCommand = hookCommand;
|
|
39
|
-
const fs = __importStar(require("fs"));
|
|
40
39
|
const os = __importStar(require("os"));
|
|
41
|
-
const path = __importStar(require("path"));
|
|
42
40
|
const config_1 = require("../config");
|
|
43
41
|
const runtimeConfig_1 = require("../runtimeConfig");
|
|
44
42
|
const telemetry_1 = require("../telemetry");
|
|
@@ -51,73 +49,9 @@ const policyGateHealth_1 = require("../policyGateHealth");
|
|
|
51
49
|
const verdictIpc_1 = require("../verdictIpc");
|
|
52
50
|
const machineIdentity_1 = require("../machineIdentity");
|
|
53
51
|
const actionPolicyEngine_1 = require("../actionPolicyEngine");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* (raw payload + prompt text snippets). Default logging keeps operational
|
|
58
|
-
* metadata only — user prompt content must never sit in a plaintext file on
|
|
59
|
-
* a customer laptop unless support explicitly opts in.
|
|
60
|
-
*/
|
|
61
|
-
const HOOK_DEBUG_VERBOSE = process.env.FCD_HOOK_DEBUG === '1';
|
|
62
|
-
/** Rotate the hook log when it grows past this size (same scheme as daemon.log). */
|
|
63
|
-
const DEBUG_LOG_MAX_BYTES = 1_000_000;
|
|
64
|
-
// Checked once per (short-lived) hook process: unbounded hook.log growth was
|
|
65
|
-
// real customer disk churn — and every append to a huge file re-triggers AV
|
|
66
|
-
// scanning. One stat per process is enough for a hook run; the resident
|
|
67
|
-
// daemon (verdict IPC evaluation) re-checks every 500 writes so a long
|
|
68
|
-
// uptime can't outgrow the cap either.
|
|
69
|
-
let debugLogRotationChecked = false;
|
|
70
|
-
let debugLogWritesSinceCheck = 0;
|
|
71
|
-
/** Append a diagnostic line so we can see exactly what Cursor invoked + the verdict. */
|
|
72
|
-
function dbg(obj) {
|
|
73
|
-
try {
|
|
74
|
-
debugLogWritesSinceCheck += 1;
|
|
75
|
-
if (!debugLogRotationChecked || debugLogWritesSinceCheck >= 500) {
|
|
76
|
-
debugLogRotationChecked = true;
|
|
77
|
-
debugLogWritesSinceCheck = 0;
|
|
78
|
-
try {
|
|
79
|
-
if (fs.existsSync(DEBUG_LOG) && fs.statSync(DEBUG_LOG).size > DEBUG_LOG_MAX_BYTES) {
|
|
80
|
-
fs.renameSync(DEBUG_LOG, `${DEBUG_LOG}.1`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch { /* rotation is best-effort */ }
|
|
84
|
-
}
|
|
85
|
-
fs.appendFileSync(DEBUG_LOG, JSON.stringify({ t: new Date().toISOString(), ...obj }) + '\n');
|
|
86
|
-
}
|
|
87
|
-
catch { /* ignore */ }
|
|
88
|
-
}
|
|
89
|
-
function readStdin() {
|
|
90
|
-
return new Promise((resolve) => {
|
|
91
|
-
const stdin = process.stdin;
|
|
92
|
-
if (stdin.isTTY) {
|
|
93
|
-
resolve('');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
// Read raw BYTES (no setEncoding) and decode the whole payload as UTF-8 once.
|
|
97
|
-
// Per-chunk string decoding + concat corrupts multibyte chars (e.g. Hebrew),
|
|
98
|
-
// turning them into Latin-1 mojibake before they ever reach the backend.
|
|
99
|
-
const chunks = [];
|
|
100
|
-
let settled = false;
|
|
101
|
-
const finish = () => {
|
|
102
|
-
if (settled)
|
|
103
|
-
return;
|
|
104
|
-
settled = true;
|
|
105
|
-
let buf = Buffer.concat(chunks);
|
|
106
|
-
// Strip UTF-8 BOM at the byte level (Cursor on Windows prepends EF BB BF).
|
|
107
|
-
if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
|
|
108
|
-
buf = buf.subarray(3);
|
|
109
|
-
}
|
|
110
|
-
resolve(buf.toString('utf8'));
|
|
111
|
-
};
|
|
112
|
-
stdin.on('data', (chunk) => {
|
|
113
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
114
|
-
});
|
|
115
|
-
stdin.on('end', finish);
|
|
116
|
-
stdin.on('error', finish);
|
|
117
|
-
// Safety: if nothing arrives, don't hang forever.
|
|
118
|
-
setTimeout(finish, 2000).unref?.();
|
|
119
|
-
});
|
|
120
|
-
}
|
|
52
|
+
// Shared with the slim hook entry (hookSlim.ts) so both entries log to the
|
|
53
|
+
// same rotated hook.log and read stdin identically.
|
|
54
|
+
const hookIo_1 = require("../hookIo");
|
|
121
55
|
function inferEvent(explicit, payload) {
|
|
122
56
|
const e = (explicit || '').toLowerCase();
|
|
123
57
|
if (e) {
|
|
@@ -427,7 +361,7 @@ function respondDegraded(ctx, detail, toolName, authRejected = false, opts = {})
|
|
|
427
361
|
// Grace window: transient failure on a fail-closed machine — allow this
|
|
428
362
|
// action, but record distress so the episode is visible in the console.
|
|
429
363
|
(0, distress_1.reportDistress)('hook', authRejected ? distress_1.DISTRESS.AUTH_BROKEN : distress_1.DISTRESS.NETWORK_DOWN, `Policy gate failure ${health.consecutiveFailures}/3 (grace window, still allowing): ${detail}`);
|
|
430
|
-
dbg({ phase: 'policy_grace_allow', event: ctx.event, consecutiveFailures: health.consecutiveFailures, detail });
|
|
364
|
+
(0, hookIo_1.dbg)({ phase: 'policy_grace_allow', event: ctx.event, consecutiveFailures: health.consecutiveFailures, detail });
|
|
431
365
|
(0, telemetry_1.spoolEvent)({ decision: 'allow', toolName, reason: `degraded (grace ${health.consecutiveFailures}/3): ${detail}`, offlineEnforced: true });
|
|
432
366
|
(0, telemetry_1.triggerFlush)(false);
|
|
433
367
|
ctx.respond(false, undefined, degradedAllowMessage(ctx.event, detail));
|
|
@@ -469,7 +403,7 @@ function tryLocalPolicyEnforcement(ctx, call, detail, opts = {}) {
|
|
|
469
403
|
context.machineName = os.hostname();
|
|
470
404
|
const declaredOperations = (0, actionPolicyEngine_1.deriveToolOperations)(call.toolName);
|
|
471
405
|
const local = (0, actionPolicyEngine_1.evaluateActionPolicies)(policies, call.toolName, operation, context, declaredOperations);
|
|
472
|
-
dbg({ phase: 'policy_local_verdict', event: ctx.event, tool: call.toolName, operation, verdict: local.verdict, policyName: local.policyName, detail });
|
|
406
|
+
(0, hookIo_1.dbg)({ phase: 'policy_local_verdict', event: ctx.event, tool: call.toolName, operation, verdict: local.verdict, policyName: local.policyName, detail });
|
|
473
407
|
if (local.verdict === 'block' || local.verdict === 'require_approval') {
|
|
474
408
|
const reason = local.reason || `${call.toolName}: local policy ${local.verdict}`;
|
|
475
409
|
if (ctx.shadow) {
|
|
@@ -617,7 +551,7 @@ function spoolLocalFinding(input) {
|
|
|
617
551
|
*/
|
|
618
552
|
function spoolLocalWarnings(warnings, toolName, operation) {
|
|
619
553
|
for (const finding of warnings) {
|
|
620
|
-
dbg({ phase: 'local_deterministic_warn', tool: toolName, ruleId: finding.ruleId, itemId: finding.itemId });
|
|
554
|
+
(0, hookIo_1.dbg)({ phase: 'local_deterministic_warn', tool: toolName, ruleId: finding.ruleId, itemId: finding.itemId });
|
|
621
555
|
spoolLocalFinding({ finding, toolName, operation, decision: 'warn' });
|
|
622
556
|
}
|
|
623
557
|
}
|
|
@@ -695,7 +629,7 @@ async function hookCommandOuter(args, config, startedAt) {
|
|
|
695
629
|
let raw = '';
|
|
696
630
|
let stdinErr = '';
|
|
697
631
|
try {
|
|
698
|
-
raw = await readStdin();
|
|
632
|
+
raw = await (0, hookIo_1.readStdin)();
|
|
699
633
|
}
|
|
700
634
|
catch (e) {
|
|
701
635
|
stdinErr = e instanceof Error ? e.message : String(e);
|
|
@@ -709,7 +643,7 @@ async function hookCommandOuter(args, config, startedAt) {
|
|
|
709
643
|
if (process.env.FCD_VERDICT_IPC !== 'off') {
|
|
710
644
|
const ipc = await (0, verdictIpc_1.requestDaemonVerdict)(args, raw, undefined, startedAt);
|
|
711
645
|
if (ipc.outcome === 'verdict') {
|
|
712
|
-
dbg({ phase: 'ipc_verdict', event: args.event, exitCode: ipc.exitCode });
|
|
646
|
+
(0, hookIo_1.dbg)({ phase: 'ipc_verdict', event: args.event, exitCode: ipc.exitCode });
|
|
713
647
|
if (ipc.stderr) {
|
|
714
648
|
try {
|
|
715
649
|
process.stderr.write(ipc.stderr);
|
|
@@ -726,7 +660,7 @@ async function hookCommandOuter(args, config, startedAt) {
|
|
|
726
660
|
}
|
|
727
661
|
// Daemon down / busy / refused — evaluate locally as always. Logged so a
|
|
728
662
|
// machine that silently stopped using the fast path is diagnosable.
|
|
729
|
-
dbg({ phase: 'ipc_unavailable', event: args.event, detail: ipc.detail });
|
|
663
|
+
(0, hookIo_1.dbg)({ phase: 'ipc_unavailable', event: args.event, detail: ipc.detail });
|
|
730
664
|
}
|
|
731
665
|
await hookCommandInner(args, config, processIo(raw, stdinErr));
|
|
732
666
|
}
|
|
@@ -763,7 +697,7 @@ function failOpenOutcome(event, message) {
|
|
|
763
697
|
}
|
|
764
698
|
catch { /* never throw */ }
|
|
765
699
|
try {
|
|
766
|
-
dbg({ phase: 'hook_fail_open', event, error: message, suspended });
|
|
700
|
+
(0, hookIo_1.dbg)({ phase: 'hook_fail_open', event, error: message, suspended });
|
|
767
701
|
}
|
|
768
702
|
catch { /* never throw */ }
|
|
769
703
|
try {
|
|
@@ -855,7 +789,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
855
789
|
machineSuspended = true;
|
|
856
790
|
shadow = false;
|
|
857
791
|
}
|
|
858
|
-
dbg({ phase: 'mode_resolved', mode: bundle.mode, source: bundle.source, shadow, failClosed, suspended: machineSuspended });
|
|
792
|
+
(0, hookIo_1.dbg)({ phase: 'mode_resolved', mode: bundle.mode, source: bundle.source, shadow, failClosed, suspended: machineSuspended });
|
|
859
793
|
}
|
|
860
794
|
}
|
|
861
795
|
catch { /* keep the local default */ }
|
|
@@ -863,7 +797,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
863
797
|
// Monitor-first: no authoritative bundle (fresh/uncached/fetch failed) => report-only.
|
|
864
798
|
if (!modeResolved && !forceEnforce) {
|
|
865
799
|
shadow = true;
|
|
866
|
-
dbg({ phase: 'mode_default_monitor_first', shadow });
|
|
800
|
+
(0, hookIo_1.dbg)({ phase: 'mode_default_monitor_first', shadow });
|
|
867
801
|
}
|
|
868
802
|
if (forceEnforce)
|
|
869
803
|
shadow = false;
|
|
@@ -908,8 +842,8 @@ async function hookCommandInner(args, config, io) {
|
|
|
908
842
|
else {
|
|
909
843
|
event = inferEvent(args.event, payload);
|
|
910
844
|
}
|
|
911
|
-
dbg({ phase: 'invoke', event, argEvent: args.event, format: hookFormat, client, pid: process.pid,
|
|
912
|
-
isTTY, rawLen: raw.length, ...(HOOK_DEBUG_VERBOSE ? { rawPreview: raw.slice(0, 300) } : {}), stdinErr, parseErr,
|
|
845
|
+
(0, hookIo_1.dbg)({ phase: 'invoke', event, argEvent: args.event, format: hookFormat, client, pid: process.pid,
|
|
846
|
+
isTTY, rawLen: raw.length, ...(hookIo_1.HOOK_DEBUG_VERBOSE ? { rawPreview: raw.slice(0, 300) } : {}), stdinErr, parseErr,
|
|
913
847
|
argv: process.argv.slice(2), payloadKeys: Object.keys(payload) });
|
|
914
848
|
// Emit the verdict in the shape THIS client expects, then exit.
|
|
915
849
|
// Cursor: beforeSubmitPrompt blocks via { continue: false }; execution hooks
|
|
@@ -949,7 +883,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
949
883
|
if (agentMsg)
|
|
950
884
|
decision.agent_message = agentMsg;
|
|
951
885
|
}
|
|
952
|
-
dbg({ phase: 'verdict', event, format: hookFormat, blocked, decision });
|
|
886
|
+
(0, hookIo_1.dbg)({ phase: 'verdict', event, format: hookFormat, blocked, decision });
|
|
953
887
|
if (hookFormat === 'claude' && event === 'prompt' && blocked) {
|
|
954
888
|
// Claude Desktop/Code variants do not all honor the structured
|
|
955
889
|
// UserPromptSubmit decision consistently. Exit 2 is the documented hard
|
|
@@ -1001,7 +935,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
1001
935
|
// --- Shield text analysis for prompts (developer chat) ---
|
|
1002
936
|
if (event === 'prompt') {
|
|
1003
937
|
const text = extractPromptText(payload).trim();
|
|
1004
|
-
dbg({ phase: 'prompt_text', textLen: text.length, ...(HOOK_DEBUG_VERBOSE ? { textPreview: text.slice(0, 300) } : {}) });
|
|
938
|
+
(0, hookIo_1.dbg)({ phase: 'prompt_text', textLen: text.length, ...(hookIo_1.HOOK_DEBUG_VERBOSE ? { textPreview: text.slice(0, 300) } : {}) });
|
|
1005
939
|
if (!text)
|
|
1006
940
|
respond(false);
|
|
1007
941
|
// Record the developer's prompt so taint-tracking can later tell whether a
|
|
@@ -1020,7 +954,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
1020
954
|
spoolLocalWarnings(promptOutcome.warnings, 'prompt', 'prompt');
|
|
1021
955
|
const localBlock = promptOutcome.blockingFinding;
|
|
1022
956
|
if (localBlock) {
|
|
1023
|
-
dbg({ phase: 'local_deterministic_prompt_block', event, ruleId: localBlock.ruleId, category: localBlock.category });
|
|
957
|
+
(0, hookIo_1.dbg)({ phase: 'local_deterministic_prompt_block', event, ruleId: localBlock.ruleId, category: localBlock.category });
|
|
1024
958
|
if (shadow) {
|
|
1025
959
|
// Truth-in-reporting: this prompt RAN (shadow/monitor) — 'warn', never 'block'.
|
|
1026
960
|
spoolLocalFinding({ finding: localBlock, toolName: 'prompt', operation: 'prompt', decision: 'warn' });
|
|
@@ -1032,7 +966,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
1032
966
|
return;
|
|
1033
967
|
}
|
|
1034
968
|
if (localOnly) {
|
|
1035
|
-
dbg({ phase: 'local_deterministic_prompt_allow', event });
|
|
969
|
+
(0, hookIo_1.dbg)({ phase: 'local_deterministic_prompt_allow', event });
|
|
1036
970
|
respond(false);
|
|
1037
971
|
return;
|
|
1038
972
|
}
|
|
@@ -1094,9 +1028,9 @@ async function runTaintApproveOnce(ctx, event, call, sessId, taintFinding) {
|
|
|
1094
1028
|
+ `Deny: fullcourtdefense approve ${approval.id} --deny\n`
|
|
1095
1029
|
+ `(run "fullcourtdefense approve" to list; expires in ${Math.round(timeoutMs / 60000)} min)`,
|
|
1096
1030
|
});
|
|
1097
|
-
dbg({ phase: 'taint_approve_once_wait', event, tool: call.toolName, approvalId: approval.id, timeoutMs });
|
|
1031
|
+
(0, hookIo_1.dbg)({ phase: 'taint_approve_once_wait', event, tool: call.toolName, approvalId: approval.id, timeoutMs });
|
|
1098
1032
|
const outcome = await (0, taintLedger_1.waitForTaintApproval)(approval.id, timeoutMs, Math.max(750, Math.min(ctx.approvalPollMs, 2000)));
|
|
1099
|
-
dbg({ phase: 'taint_approve_once_outcome', approvalId: approval.id, outcome });
|
|
1033
|
+
(0, hookIo_1.dbg)({ phase: 'taint_approve_once_outcome', approvalId: approval.id, outcome });
|
|
1100
1034
|
if (outcome === 'approved') {
|
|
1101
1035
|
(0, telemetry_1.spoolEvent)({
|
|
1102
1036
|
decision: 'allow',
|
|
@@ -1146,7 +1080,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1146
1080
|
spoolLocalWarnings(toolOutcome.warnings, call.toolName, event);
|
|
1147
1081
|
const localBlock = toolOutcome.blockingFinding;
|
|
1148
1082
|
if (localBlock) {
|
|
1149
|
-
dbg({ phase: 'local_deterministic_block', event, tool: call.toolName, ruleId: localBlock.ruleId, category: localBlock.category });
|
|
1083
|
+
(0, hookIo_1.dbg)({ phase: 'local_deterministic_block', event, tool: call.toolName, ruleId: localBlock.ruleId, category: localBlock.category });
|
|
1150
1084
|
if (shadow) {
|
|
1151
1085
|
// Truth-in-reporting: this action RAN (shadow/monitor) — 'warn', never 'block'.
|
|
1152
1086
|
spoolLocalFinding({ finding: localBlock, toolName: call.toolName, operation: event, decision: 'warn' });
|
|
@@ -1162,7 +1096,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1162
1096
|
// state directory; otherwise a compromised agent could approve itself.
|
|
1163
1097
|
const selfApproval = (0, taintLedger_1.detectTaintSelfApproval)(event, call.toolName, call.toolArgs);
|
|
1164
1098
|
if (selfApproval) {
|
|
1165
|
-
dbg({ phase: 'taint_self_approval_block', event, tool: call.toolName });
|
|
1099
|
+
(0, hookIo_1.dbg)({ phase: 'taint_self_approval_block', event, tool: call.toolName });
|
|
1166
1100
|
if (shadow) {
|
|
1167
1101
|
respond(false, undefined, `[FullCourtDefense shadow] would block ${call.toolName}: ${selfApproval.reason}`);
|
|
1168
1102
|
return;
|
|
@@ -1180,7 +1114,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1180
1114
|
const sessId = sessionId(payload);
|
|
1181
1115
|
const taintFinding = (0, taintLedger_1.checkTaintedSink)(sessId, event, call.toolName, call.toolArgs);
|
|
1182
1116
|
if (taintFinding) {
|
|
1183
|
-
dbg({ phase: 'local_taint_block', event, tool: call.toolName, ruleId: taintFinding.ruleId, sink: taintFinding.sink.kind, targets: taintFinding.sink.targets });
|
|
1117
|
+
(0, hookIo_1.dbg)({ phase: 'local_taint_block', event, tool: call.toolName, ruleId: taintFinding.ruleId, sink: taintFinding.sink.kind, targets: taintFinding.sink.targets });
|
|
1184
1118
|
if (shadow) {
|
|
1185
1119
|
respond(false, undefined, `[FullCourtDefense shadow] would block ${call.toolName}: ${taintFinding.reason}`);
|
|
1186
1120
|
return;
|
|
@@ -1204,7 +1138,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1204
1138
|
// (detached flush child). Enforce machines keep the full timeout + retry:
|
|
1205
1139
|
// their verdict actually gates the action.
|
|
1206
1140
|
if (shadow) {
|
|
1207
|
-
dbg({ phase: 'policy_skip_monitor', event, tool: call.toolName });
|
|
1141
|
+
(0, hookIo_1.dbg)({ phase: 'policy_skip_monitor', event, tool: call.toolName });
|
|
1208
1142
|
const detail = 'monitor mode — synchronous policy check skipped by design.';
|
|
1209
1143
|
if (!tryLocalPolicyEnforcement(ctx, call, detail, { countAsGateFailure: false })) {
|
|
1210
1144
|
(0, telemetry_1.spoolEvent)({ decision: 'allow', toolName: call.toolName, reason: `monitor: ${detail}`, offlineEnforced: true });
|
|
@@ -1246,7 +1180,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1246
1180
|
localCtx.developerName = developerId();
|
|
1247
1181
|
localCtx.machineName = os.hostname();
|
|
1248
1182
|
const localVerdict = (0, actionPolicyEngine_1.evaluateActionPolicies)(ctx.localPolicies, call.toolName, localOp, localCtx, (0, actionPolicyEngine_1.deriveToolOperations)(call.toolName));
|
|
1249
|
-
dbg({ phase: 'policy_local_first', event, tool: call.toolName, operation: localOp, verdict: localVerdict.verdict, policyName: localVerdict.policyName });
|
|
1183
|
+
(0, hookIo_1.dbg)({ phase: 'policy_local_first', event, tool: call.toolName, operation: localOp, verdict: localVerdict.verdict, policyName: localVerdict.policyName });
|
|
1250
1184
|
if (localVerdict.verdict === 'allow') {
|
|
1251
1185
|
// Audit rides async: server records the action + its own verdict; if it
|
|
1252
1186
|
// is unreachable the spool keeps the trail (idempotent on eventId).
|
|
@@ -1275,7 +1209,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1275
1209
|
// skip does NOT refresh the failure window, so the next call after 60s
|
|
1276
1210
|
// probes the gate again (bounded), and a success resets the streak.
|
|
1277
1211
|
if ((0, policyGateHealth_1.gateRecentlyDown)()) {
|
|
1278
|
-
dbg({ phase: 'policy_skip_gate_down', event, tool: call.toolName });
|
|
1212
|
+
(0, hookIo_1.dbg)({ phase: 'policy_skip_gate_down', event, tool: call.toolName });
|
|
1279
1213
|
const detail = 'policy service recently unreachable — synchronous wait skipped (fast-probe window).';
|
|
1280
1214
|
if (!tryLocalPolicyEnforcement(ctx, call, detail, { countAsGateFailure: false })) {
|
|
1281
1215
|
respondDegraded(ctx, detail, call.toolName, false, { countAsGateFailure: false });
|
|
@@ -1293,9 +1227,9 @@ async function enforceActionPolicy(ctx) {
|
|
|
1293
1227
|
method: 'POST',
|
|
1294
1228
|
headers,
|
|
1295
1229
|
body: gateBody,
|
|
1296
|
-
}, gateTimeoutMs, (attempt, err) => dbg({ phase: 'policy_retry', event, attempt, error: err }), gateAttempts);
|
|
1230
|
+
}, gateTimeoutMs, (attempt, err) => (0, hookIo_1.dbg)({ phase: 'policy_retry', event, attempt, error: err }), gateAttempts);
|
|
1297
1231
|
if (!resp.ok) {
|
|
1298
|
-
dbg({ phase: 'policy_http_error', event, status: resp.status, failClosed: ctx.failClosed });
|
|
1232
|
+
(0, hookIo_1.dbg)({ phase: 'policy_http_error', event, status: resp.status, failClosed: ctx.failClosed });
|
|
1299
1233
|
const authRejected = resp.status === 401 || resp.status === 403;
|
|
1300
1234
|
const detail = authRejected
|
|
1301
1235
|
? `The policy service rejected this machine's credentials (HTTP ${resp.status}) — the shield key saved here is broken or revoked, not a network problem.`
|
|
@@ -1314,7 +1248,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1314
1248
|
}
|
|
1315
1249
|
const body = await resp.json().catch(() => ({}));
|
|
1316
1250
|
if (!body.success || !body.data) {
|
|
1317
|
-
dbg({ phase: 'policy_no_data', event, error: body.error, failClosed: ctx.failClosed });
|
|
1251
|
+
(0, hookIo_1.dbg)({ phase: 'policy_no_data', event, error: body.error, failClosed: ctx.failClosed });
|
|
1318
1252
|
const detail = body.error ? `Backend error: ${body.error}` : 'Backend returned no policy data.';
|
|
1319
1253
|
if (!tryLocalPolicyEnforcement(ctx, call, detail)) {
|
|
1320
1254
|
respondDegraded(ctx, detail, call.toolName);
|
|
@@ -1326,7 +1260,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1326
1260
|
(0, policyGateHealth_1.recordGateSuccess)();
|
|
1327
1261
|
}
|
|
1328
1262
|
catch (err) {
|
|
1329
|
-
dbg({ phase: 'policy_exception', event, error: err instanceof Error ? err.message : String(err), failClosed: ctx.failClosed });
|
|
1263
|
+
(0, hookIo_1.dbg)({ phase: 'policy_exception', event, error: err instanceof Error ? err.message : String(err), failClosed: ctx.failClosed });
|
|
1330
1264
|
const detail = `Hook error: ${err instanceof Error ? err.message : String(err)}.`;
|
|
1331
1265
|
if (!tryLocalPolicyEnforcement(ctx, call, detail)) {
|
|
1332
1266
|
respondDegraded(ctx, detail, call.toolName);
|
|
@@ -1337,7 +1271,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1337
1271
|
|| result.intentEvaluation?.reasons?.[0]
|
|
1338
1272
|
|| `${call.toolName} ${result.decision}ed by Action Policy.`;
|
|
1339
1273
|
const policyName = result.actionPolicy?.policyName;
|
|
1340
|
-
dbg({ phase: 'policy_verdict', event, tool: call.toolName, decision: result.decision, allowed: result.allowed, policyName });
|
|
1274
|
+
(0, hookIo_1.dbg)({ phase: 'policy_verdict', event, tool: call.toolName, decision: result.decision, allowed: result.allowed, policyName });
|
|
1341
1275
|
if (result.allowed && result.decision === 'allow') {
|
|
1342
1276
|
respond(false);
|
|
1343
1277
|
return;
|
|
@@ -1396,7 +1330,7 @@ async function waitForApproval(input) {
|
|
|
1396
1330
|
headers['x-shield-key'] = input.shieldKey;
|
|
1397
1331
|
const url = `${input.apiUrl}/api/agent-security/runtime/approvals/${encodeURIComponent(input.actionId)}?shieldId=${encodeURIComponent(input.shieldId)}`;
|
|
1398
1332
|
const deadline = Date.now() + input.timeoutMs;
|
|
1399
|
-
dbg({ phase: 'approval_wait_start', actionId: input.actionId, timeoutMs: input.timeoutMs });
|
|
1333
|
+
(0, hookIo_1.dbg)({ phase: 'approval_wait_start', actionId: input.actionId, timeoutMs: input.timeoutMs });
|
|
1400
1334
|
while (Date.now() <= deadline) {
|
|
1401
1335
|
try {
|
|
1402
1336
|
const resp = await fetch(url, { method: 'GET', headers, signal: AbortSignal.timeout(15000) });
|
|
@@ -1415,7 +1349,7 @@ async function waitForApproval(input) {
|
|
|
1415
1349
|
}
|
|
1416
1350
|
}
|
|
1417
1351
|
catch (err) {
|
|
1418
|
-
dbg({ phase: 'approval_poll_error', actionId: input.actionId, error: err instanceof Error ? err.message : String(err) });
|
|
1352
|
+
(0, hookIo_1.dbg)({ phase: 'approval_poll_error', actionId: input.actionId, error: err instanceof Error ? err.message : String(err) });
|
|
1419
1353
|
}
|
|
1420
1354
|
await new Promise((resolve) => setTimeout(resolve, Math.min(input.pollMs, Math.max(0, deadline - Date.now()))));
|
|
1421
1355
|
}
|
|
@@ -1431,7 +1365,7 @@ async function enforceShieldText(ctx) {
|
|
|
1431
1365
|
// deterministic rules already ran before this point; the audit trail rides
|
|
1432
1366
|
// the async spool. Enforce machines keep the full window: their verdict gates.
|
|
1433
1367
|
if (shadow) {
|
|
1434
|
-
dbg({ phase: 'shield_skip_monitor', event });
|
|
1368
|
+
(0, hookIo_1.dbg)({ phase: 'shield_skip_monitor', event });
|
|
1435
1369
|
(0, telemetry_1.spoolEvent)({ decision: 'allow', toolName: 'prompt', reason: 'monitor: synchronous Shield scan skipped by design.', offlineEnforced: true });
|
|
1436
1370
|
(0, telemetry_1.triggerFlush)(false);
|
|
1437
1371
|
respond(false);
|
|
@@ -1440,7 +1374,7 @@ async function enforceShieldText(ctx) {
|
|
|
1440
1374
|
// recently-failed gate is not re-probed on every prompt; the degraded stance
|
|
1441
1375
|
// resolves instantly instead of paying the full timeout per event.
|
|
1442
1376
|
if ((0, policyGateHealth_1.gateRecentlyDown)()) {
|
|
1443
|
-
dbg({ phase: 'shield_skip_gate_down', event });
|
|
1377
|
+
(0, hookIo_1.dbg)({ phase: 'shield_skip_gate_down', event });
|
|
1444
1378
|
respondDegraded(ctx, 'Shield service recently unreachable — synchronous wait skipped (fast-probe window).', 'prompt', false, { countAsGateFailure: false });
|
|
1445
1379
|
return;
|
|
1446
1380
|
}
|
|
@@ -1460,7 +1394,7 @@ async function enforceShieldText(ctx) {
|
|
|
1460
1394
|
method: 'POST',
|
|
1461
1395
|
headers,
|
|
1462
1396
|
body: JSON.stringify({ message: text }),
|
|
1463
|
-
}, gateTimeoutMs, (attempt, error) => dbg({ phase: 'shield_retry', event, attempt, error }), gateAttempts);
|
|
1397
|
+
}, gateTimeoutMs, (attempt, error) => (0, hookIo_1.dbg)({ phase: 'shield_retry', event, attempt, error }), gateAttempts);
|
|
1464
1398
|
if (!resp.ok) {
|
|
1465
1399
|
respondDegraded(ctx, `Backend returned HTTP ${resp.status}.`, 'prompt');
|
|
1466
1400
|
return;
|
|
@@ -41,6 +41,7 @@ const fs = __importStar(require("fs"));
|
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const config_1 = require("../config");
|
|
44
|
+
const hookIo_1 = require("../hookIo");
|
|
44
45
|
const restartNotice_1 = require("./restartNotice");
|
|
45
46
|
const COLOR = {
|
|
46
47
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -77,9 +78,23 @@ function isManagedCommand(cmd) {
|
|
|
77
78
|
return typeof cmd?.command === 'string'
|
|
78
79
|
&& (cmd.command.includes(MANAGED_MARKER) || cmd.command.includes(MANAGED_TAG));
|
|
79
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Prefer the SLIM hook entry (dist/hookSlim.js — Node-builtins-only module
|
|
83
|
+
* graph, near-spawn-floor cold start) and fall back to the full CLI entry on
|
|
84
|
+
* older dist layouts. Repair rewrites stale entries through this prefix too,
|
|
85
|
+
* which is how an enrolled fleet migrates to the slim hook on upgrade.
|
|
86
|
+
*/
|
|
87
|
+
function hookScriptPath() {
|
|
88
|
+
const cliScript = process.argv[1] || path.join(__dirname, '..', 'index.js');
|
|
89
|
+
// argv[1] first (normal CLI invocation), then this module's own dist
|
|
90
|
+
// location — argv[1] can be an npm shim or a daemon-driven repair helper.
|
|
91
|
+
return (0, hookIo_1.slimHookEntryPath)(cliScript)
|
|
92
|
+
|| (0, hookIo_1.slimHookEntryPath)(path.join(__dirname, '..', 'index.js'))
|
|
93
|
+
|| cliScript;
|
|
94
|
+
}
|
|
80
95
|
function hookInvocationPrefix() {
|
|
81
96
|
const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
82
|
-
return `${q(process.execPath)} ${q(
|
|
97
|
+
return `${q(process.execPath)} ${q(hookScriptPath())}`;
|
|
83
98
|
}
|
|
84
99
|
function repairManagedCommandPath(command) {
|
|
85
100
|
const marker = command.indexOf(' hook ');
|
|
@@ -189,12 +204,11 @@ function stripManaged(hooks) {
|
|
|
189
204
|
/** Build the absolute, shell-agnostic command that invokes this CLI's `hook`. */
|
|
190
205
|
function buildHookCommand(opts) {
|
|
191
206
|
const nodeExe = process.execPath;
|
|
192
|
-
const scriptPath = process.argv[1] || path.join(__dirname, '..', 'index.js');
|
|
193
207
|
const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
194
208
|
// No --event flag: the bridge detects the Claude payload (hook_event_name) itself.
|
|
195
209
|
// local-only affects prompt events only; tool events still use the complete
|
|
196
210
|
// Action Policy/approval path. This keeps prompt text on the endpoint.
|
|
197
|
-
let cmd = `${q(nodeExe)} ${q(
|
|
211
|
+
let cmd = `${q(nodeExe)} ${q(hookScriptPath())} hook --approval-mode wait --local-only true`;
|
|
198
212
|
if (opts.shadow)
|
|
199
213
|
cmd += ' --shadow true';
|
|
200
214
|
if (opts.failClosed)
|
|
@@ -41,6 +41,7 @@ const fs = __importStar(require("fs"));
|
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const config_1 = require("../config");
|
|
44
|
+
const hookIo_1 = require("../hookIo");
|
|
44
45
|
const restartNotice_1 = require("./restartNotice");
|
|
45
46
|
const COLOR = {
|
|
46
47
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -70,12 +71,29 @@ const EVENT_MAP = {
|
|
|
70
71
|
file: { hookKey: 'afterFileEdit', flag: 'file', timeoutSec: APPROVAL_CAPABLE_TIMEOUT_SEC },
|
|
71
72
|
read: { hookKey: 'beforeReadFile', flag: 'read', timeoutSec: APPROVAL_CAPABLE_TIMEOUT_SEC },
|
|
72
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* The script a hook command invokes: the SLIM entry (dist/hookSlim.js) when it
|
|
76
|
+
* exists — a Node-builtins-only module graph, so per-event cold start is the
|
|
77
|
+
* node spawn plus three small files instead of the whole CLI (the difference
|
|
78
|
+
* an EDR-heavy corporate machine feels on every docker command). Falls back
|
|
79
|
+
* to the full CLI entry when the slim file is absent (older dist layouts).
|
|
80
|
+
* The literal ` hook ` subcommand is kept in the command either way — it is
|
|
81
|
+
* the marker managed-entry repair keys on, and the slim entry ignores it.
|
|
82
|
+
*/
|
|
83
|
+
function hookScriptPath() {
|
|
84
|
+
const cliScript = process.argv[1] || path.join(__dirname, '..', 'index.js');
|
|
85
|
+
// Resolve the slim entry against argv[1] first (normal CLI invocation), then
|
|
86
|
+
// against this module's own dist location — argv[1] can be an npm shim or a
|
|
87
|
+
// helper script (daemon-driven repair) rather than dist/index.js.
|
|
88
|
+
return (0, hookIo_1.slimHookEntryPath)(cliScript)
|
|
89
|
+
|| (0, hookIo_1.slimHookEntryPath)(path.join(__dirname, '..', 'index.js'))
|
|
90
|
+
|| cliScript;
|
|
91
|
+
}
|
|
73
92
|
/** Build the absolute, shell-agnostic command that invokes this CLI's `hook`. */
|
|
74
93
|
function buildHookCommand(flag, opts) {
|
|
75
94
|
const nodeExe = process.execPath; // real node binary (handles Windows/Linux/mac)
|
|
76
|
-
const scriptPath = process.argv[1] || path.join(__dirname, '..', 'index.js');
|
|
77
95
|
const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
78
|
-
let cmd = `${q(nodeExe)} ${q(
|
|
96
|
+
let cmd = `${q(nodeExe)} ${q(hookScriptPath())} hook --event ${flag}`;
|
|
79
97
|
if (opts.shadow)
|
|
80
98
|
cmd += ' --shadow true';
|
|
81
99
|
if (opts.failClosed)
|
|
@@ -113,9 +131,11 @@ function isManaged(entry) {
|
|
|
113
131
|
}
|
|
114
132
|
function hookInvocationPrefix() {
|
|
115
133
|
const nodeExe = process.execPath;
|
|
116
|
-
const scriptPath = process.argv[1] || path.join(__dirname, '..', 'index.js');
|
|
117
134
|
const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
118
|
-
|
|
135
|
+
// Repair rewrites stale managed entries to the slim entry too — this is how
|
|
136
|
+
// an already-enrolled fleet migrates to the slim hook on upgrade, without a
|
|
137
|
+
// reinstall (protection repair runs from the daemon after every update).
|
|
138
|
+
return `${q(nodeExe)} ${q(hookScriptPath())}`;
|
|
119
139
|
}
|
|
120
140
|
function repairManagedCommandPath(command) {
|
|
121
141
|
const marker = command.indexOf(' hook ');
|
package/dist/distress.d.ts
CHANGED
|
@@ -48,6 +48,12 @@ export declare const DISTRESS: {
|
|
|
48
48
|
readonly MACHINE_KEYFILE_ACL_FAILED: "machine_keyfile_acl_failed";
|
|
49
49
|
/** Fail-closed engaged: hook is BLOCKING user actions because the policy gate is persistently unreachable. */
|
|
50
50
|
readonly HOOK_FAIL_CLOSED: "hook_fail_closed";
|
|
51
|
+
/**
|
|
52
|
+
* A daemon poll never settled and was abandoned at its deadline. Usually a
|
|
53
|
+
* socket that died without a FIN (sleep / VPN / proxy). Self-heals on the
|
|
54
|
+
* next tick — but while it lasted, remote actions were undeliverable.
|
|
55
|
+
*/
|
|
56
|
+
readonly POLL_STALLED: "poll_stalled";
|
|
51
57
|
/** Anything nobody predicted — reported via reportUnexpected(). */
|
|
52
58
|
readonly UNEXPECTED: "unexpected_error";
|
|
53
59
|
};
|
package/dist/distress.js
CHANGED
|
@@ -93,6 +93,12 @@ exports.DISTRESS = {
|
|
|
93
93
|
MACHINE_KEYFILE_ACL_FAILED: 'machine_keyfile_acl_failed',
|
|
94
94
|
/** Fail-closed engaged: hook is BLOCKING user actions because the policy gate is persistently unreachable. */
|
|
95
95
|
HOOK_FAIL_CLOSED: 'hook_fail_closed',
|
|
96
|
+
/**
|
|
97
|
+
* A daemon poll never settled and was abandoned at its deadline. Usually a
|
|
98
|
+
* socket that died without a FIN (sleep / VPN / proxy). Self-heals on the
|
|
99
|
+
* next tick — but while it lasted, remote actions were undeliverable.
|
|
100
|
+
*/
|
|
101
|
+
POLL_STALLED: 'poll_stalled',
|
|
96
102
|
/** Anything nobody predicted — reported via reportUnexpected(). */
|
|
97
103
|
UNEXPECTED: 'unexpected_error',
|
|
98
104
|
};
|
package/dist/hookIo.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook I/O leaf — the pieces of the hook runtime that BOTH entries share:
|
|
3
|
+
*
|
|
4
|
+
* - `dist/hookSlim.js` (the slim hook entry installed into IDE hook configs)
|
|
5
|
+
* - `dist/index.js hook` (the full CLI's hook command, and the daemon's
|
|
6
|
+
* in-process evaluator)
|
|
7
|
+
*
|
|
8
|
+
* COLD-START CONTRACT: this module is on the slim entry's module graph, which
|
|
9
|
+
* is loaded on EVERY IDE hook event. It may import Node built-ins ONLY —
|
|
10
|
+
* every additional module here is bytes the customer's EDR re-scans per
|
|
11
|
+
* docker command. `scripts/test-slim-hook.js` enforces the graph budget.
|
|
12
|
+
*/
|
|
13
|
+
export declare const HOOK_DEBUG_LOG: string;
|
|
14
|
+
/**
|
|
15
|
+
* Verbose diagnostics (FCD_HOOK_DEBUG=1) additionally log content previews
|
|
16
|
+
* (raw payload + prompt text snippets). Default logging keeps operational
|
|
17
|
+
* metadata only — user prompt content must never sit in a plaintext file on
|
|
18
|
+
* a customer laptop unless support explicitly opts in.
|
|
19
|
+
*/
|
|
20
|
+
export declare const HOOK_DEBUG_VERBOSE: boolean;
|
|
21
|
+
/** Append a diagnostic line so we can see exactly what the IDE invoked + the verdict. */
|
|
22
|
+
export declare function dbg(obj: Record<string, unknown>): void;
|
|
23
|
+
export declare function readStdin(): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* kebab-case CLI flags -> the camelCase HookArgs shape. ONE mapping shared by
|
|
26
|
+
* the full CLI entry (`index.ts` case 'hook') and the slim entry, so the
|
|
27
|
+
* daemon's IPC evaluator always receives identical argument shapes no matter
|
|
28
|
+
* which entry the IDE invoked.
|
|
29
|
+
*/
|
|
30
|
+
export declare function mapHookFlags(flags: Record<string, string>): Record<string, string | undefined>;
|
|
31
|
+
/**
|
|
32
|
+
* Minimal `--flag value` / `--flag=value` parser with the same semantics as
|
|
33
|
+
* the full CLI's parseArgs, restricted to what a hook command line carries.
|
|
34
|
+
* Positionals (the literal `hook` subcommand kept in installed commands for
|
|
35
|
+
* the managed-entry repair marker) are ignored.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseHookFlags(argv: string[]): Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* Absolute path of the slim hook entry that sits next to a given CLI script
|
|
40
|
+
* (`dist/index.js` -> `dist/hookSlim.js`). Installers prefer the slim entry
|
|
41
|
+
* when it exists; older installs (pre-slim dist) keep pointing at index.js.
|
|
42
|
+
*/
|
|
43
|
+
export declare function slimHookEntryPath(cliScriptPath: string): string | undefined;
|