fullcourtdefense-cli 1.26.4 → 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/hookIo.js ADDED
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HOOK_DEBUG_VERBOSE = exports.HOOK_DEBUG_LOG = void 0;
37
+ exports.dbg = dbg;
38
+ exports.readStdin = readStdin;
39
+ exports.mapHookFlags = mapHookFlags;
40
+ exports.parseHookFlags = parseHookFlags;
41
+ exports.slimHookEntryPath = slimHookEntryPath;
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ /**
46
+ * Hook I/O leaf — the pieces of the hook runtime that BOTH entries share:
47
+ *
48
+ * - `dist/hookSlim.js` (the slim hook entry installed into IDE hook configs)
49
+ * - `dist/index.js hook` (the full CLI's hook command, and the daemon's
50
+ * in-process evaluator)
51
+ *
52
+ * COLD-START CONTRACT: this module is on the slim entry's module graph, which
53
+ * is loaded on EVERY IDE hook event. It may import Node built-ins ONLY —
54
+ * every additional module here is bytes the customer's EDR re-scans per
55
+ * docker command. `scripts/test-slim-hook.js` enforces the graph budget.
56
+ */
57
+ exports.HOOK_DEBUG_LOG = path.join(os.homedir(), '.fullcourtdefense-hook.log');
58
+ /**
59
+ * Verbose diagnostics (FCD_HOOK_DEBUG=1) additionally log content previews
60
+ * (raw payload + prompt text snippets). Default logging keeps operational
61
+ * metadata only — user prompt content must never sit in a plaintext file on
62
+ * a customer laptop unless support explicitly opts in.
63
+ */
64
+ exports.HOOK_DEBUG_VERBOSE = process.env.FCD_HOOK_DEBUG === '1';
65
+ /** Rotate the hook log when it grows past this size (same scheme as daemon.log). */
66
+ const DEBUG_LOG_MAX_BYTES = 1_000_000;
67
+ // Checked once per (short-lived) hook process: unbounded hook.log growth was
68
+ // real customer disk churn — and every append to a huge file re-triggers AV
69
+ // scanning. One stat per process is enough for a hook run; the resident
70
+ // daemon (verdict IPC evaluation) re-checks every 500 writes so a long
71
+ // uptime can't outgrow the cap either.
72
+ let debugLogRotationChecked = false;
73
+ let debugLogWritesSinceCheck = 0;
74
+ /** Append a diagnostic line so we can see exactly what the IDE invoked + the verdict. */
75
+ function dbg(obj) {
76
+ try {
77
+ debugLogWritesSinceCheck += 1;
78
+ if (!debugLogRotationChecked || debugLogWritesSinceCheck >= 500) {
79
+ debugLogRotationChecked = true;
80
+ debugLogWritesSinceCheck = 0;
81
+ try {
82
+ if (fs.existsSync(exports.HOOK_DEBUG_LOG) && fs.statSync(exports.HOOK_DEBUG_LOG).size > DEBUG_LOG_MAX_BYTES) {
83
+ fs.renameSync(exports.HOOK_DEBUG_LOG, `${exports.HOOK_DEBUG_LOG}.1`);
84
+ }
85
+ }
86
+ catch { /* rotation is best-effort */ }
87
+ }
88
+ fs.appendFileSync(exports.HOOK_DEBUG_LOG, JSON.stringify({ t: new Date().toISOString(), ...obj }) + '\n');
89
+ }
90
+ catch { /* ignore */ }
91
+ }
92
+ function readStdin() {
93
+ return new Promise((resolve) => {
94
+ const stdin = process.stdin;
95
+ if (stdin.isTTY) {
96
+ resolve('');
97
+ return;
98
+ }
99
+ // Read raw BYTES (no setEncoding) and decode the whole payload as UTF-8 once.
100
+ // Per-chunk string decoding + concat corrupts multibyte chars (e.g. Hebrew),
101
+ // turning them into Latin-1 mojibake before they ever reach the backend.
102
+ const chunks = [];
103
+ let settled = false;
104
+ const finish = () => {
105
+ if (settled)
106
+ return;
107
+ settled = true;
108
+ let buf = Buffer.concat(chunks);
109
+ // Strip UTF-8 BOM at the byte level (Cursor on Windows prepends EF BB BF).
110
+ if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
111
+ buf = buf.subarray(3);
112
+ }
113
+ resolve(buf.toString('utf8'));
114
+ };
115
+ stdin.on('data', (chunk) => {
116
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
117
+ });
118
+ stdin.on('end', finish);
119
+ stdin.on('error', finish);
120
+ // Safety: if nothing arrives, don't hang forever.
121
+ setTimeout(finish, 2000).unref?.();
122
+ });
123
+ }
124
+ /**
125
+ * kebab-case CLI flags -> the camelCase HookArgs shape. ONE mapping shared by
126
+ * the full CLI entry (`index.ts` case 'hook') and the slim entry, so the
127
+ * daemon's IPC evaluator always receives identical argument shapes no matter
128
+ * which entry the IDE invoked.
129
+ */
130
+ function mapHookFlags(flags) {
131
+ return {
132
+ event: flags.event,
133
+ apiUrl: flags['api-url'],
134
+ shieldId: flags['shield-id'],
135
+ shieldKey: flags['shield-key'],
136
+ shadow: flags.shadow,
137
+ enforce: flags.enforce,
138
+ failClosed: flags['fail-closed'],
139
+ localOnly: flags['local-only'],
140
+ timeout: flags.timeout,
141
+ approvalMode: flags['approval-mode'],
142
+ approvalTimeoutMs: flags['approval-timeout-ms'],
143
+ approvalPollMs: flags['approval-poll-ms'],
144
+ };
145
+ }
146
+ /**
147
+ * Minimal `--flag value` / `--flag=value` parser with the same semantics as
148
+ * the full CLI's parseArgs, restricted to what a hook command line carries.
149
+ * Positionals (the literal `hook` subcommand kept in installed commands for
150
+ * the managed-entry repair marker) are ignored.
151
+ */
152
+ function parseHookFlags(argv) {
153
+ const flags = {};
154
+ for (let i = 0; i < argv.length; i++) {
155
+ const arg = argv[i];
156
+ if (!arg.startsWith('-'))
157
+ continue;
158
+ const key = arg.startsWith('--') ? arg.slice(2) : arg.slice(1);
159
+ const eqIdx = key.indexOf('=');
160
+ if (eqIdx !== -1) {
161
+ flags[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
162
+ continue;
163
+ }
164
+ const next = argv[i + 1];
165
+ if (next && !next.startsWith('-')) {
166
+ flags[key] = next;
167
+ i++;
168
+ }
169
+ else {
170
+ flags[key] = 'true';
171
+ }
172
+ }
173
+ return flags;
174
+ }
175
+ /**
176
+ * Absolute path of the slim hook entry that sits next to a given CLI script
177
+ * (`dist/index.js` -> `dist/hookSlim.js`). Installers prefer the slim entry
178
+ * when it exists; older installs (pre-slim dist) keep pointing at index.js.
179
+ */
180
+ function slimHookEntryPath(cliScriptPath) {
181
+ try {
182
+ const candidate = path.join(path.dirname(cliScriptPath), 'hookSlim.js');
183
+ return fs.existsSync(candidate) ? candidate : undefined;
184
+ }
185
+ catch {
186
+ return undefined;
187
+ }
188
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SLIM HOOK ENTRY — what the IDE actually spawns on every hook event.
4
+ *
5
+ * The per-event tax on a hook used to be loading the ENTIRE CLI (dist/index.js
6
+ * and its transitive graph) just to ask the resident daemon one question over
7
+ * the verdict pipe. On a normal laptop that is tens of milliseconds; on a
8
+ * locked-down corporate machine whose EDR re-scans every loaded file, it is
9
+ * the 1-5s "sticky" pause a docker-heavy customer feels dozens of times an
10
+ * hour. This entry exists to make the common path (daemon up, IPC verdict)
11
+ * load almost nothing:
12
+ *
13
+ * node boot -> hookSlim.js -> hookIo.js + verdictIpcClient.js (3 small files,
14
+ * Node built-ins only) -> named-pipe verdict -> print -> exit.
15
+ *
16
+ * FALLBACK PARITY: when the daemon is unreachable, this entry lazily requires
17
+ * the full hook evaluation (`commands/hook.evaluateHookRequest`) — literally
18
+ * the same code the full CLI runs — so behavior with the daemon down is
19
+ * byte-identical to `index.js hook`. The slim win applies exactly where it
20
+ * matters: the warm, common case.
21
+ *
22
+ * FAIL-OPEN CONTRACT (same as the full hook): any unexpected error must never
23
+ * surface as a stack trace, a nonzero exit, or a blocked command. The ONE
24
+ * exception is the admin kill-switch — a crash on a suspended machine still
25
+ * denies, so a broken install can never unlock a machine an admin suspended.
26
+ *
27
+ * COLD-START CONTRACT: static imports here are restricted to the two leaves
28
+ * below (Node built-ins only). Everything else loads lazily, and ONLY on the
29
+ * fallback path. `scripts/test-slim-hook.js` fails CI if the warm-path module
30
+ * graph grows.
31
+ */
32
+ export {};
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * SLIM HOOK ENTRY — what the IDE actually spawns on every hook event.
5
+ *
6
+ * The per-event tax on a hook used to be loading the ENTIRE CLI (dist/index.js
7
+ * and its transitive graph) just to ask the resident daemon one question over
8
+ * the verdict pipe. On a normal laptop that is tens of milliseconds; on a
9
+ * locked-down corporate machine whose EDR re-scans every loaded file, it is
10
+ * the 1-5s "sticky" pause a docker-heavy customer feels dozens of times an
11
+ * hour. This entry exists to make the common path (daemon up, IPC verdict)
12
+ * load almost nothing:
13
+ *
14
+ * node boot -> hookSlim.js -> hookIo.js + verdictIpcClient.js (3 small files,
15
+ * Node built-ins only) -> named-pipe verdict -> print -> exit.
16
+ *
17
+ * FALLBACK PARITY: when the daemon is unreachable, this entry lazily requires
18
+ * the full hook evaluation (`commands/hook.evaluateHookRequest`) — literally
19
+ * the same code the full CLI runs — so behavior with the daemon down is
20
+ * byte-identical to `index.js hook`. The slim win applies exactly where it
21
+ * matters: the warm, common case.
22
+ *
23
+ * FAIL-OPEN CONTRACT (same as the full hook): any unexpected error must never
24
+ * surface as a stack trace, a nonzero exit, or a blocked command. The ONE
25
+ * exception is the admin kill-switch — a crash on a suspended machine still
26
+ * denies, so a broken install can never unlock a machine an admin suspended.
27
+ *
28
+ * COLD-START CONTRACT: static imports here are restricted to the two leaves
29
+ * below (Node built-ins only). Everything else loads lazily, and ONLY on the
30
+ * fallback path. `scripts/test-slim-hook.js` fails CI if the warm-path module
31
+ * graph grows.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ const hookIo_1 = require("./hookIo");
35
+ const verdictIpcClient_1 = require("./verdictIpcClient");
36
+ function emitFailOpen(event, message) {
37
+ // Mirror of the full hook's failOpenOutcome, reachable only when even the
38
+ // lazy fallback could not load (corrupt install). Best effort all the way
39
+ // down; the kill-switch check and spool are each allowed to fail alone.
40
+ let suspended = false;
41
+ try {
42
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
43
+ suspended = require('./runtimeConfig').isSuspendedByCache();
44
+ }
45
+ catch { /* runtime cache unreadable — stay fail-open */ }
46
+ try {
47
+ (0, hookIo_1.dbg)({ phase: 'slim_fail_open', event, error: message.slice(0, 300), suspended });
48
+ }
49
+ catch { /* never throw */ }
50
+ try {
51
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
52
+ const telemetry = require('./telemetry');
53
+ telemetry.setVerdictPath('fail_open');
54
+ telemetry.spoolEvent({
55
+ decision: suspended ? 'block' : 'allow',
56
+ toolName: event || 'unknown',
57
+ operation: `${event || 'unknown'}:hook_error`,
58
+ reason: suspended
59
+ ? `Hook error on a SUSPENDED machine — denied (kill-switch holds): ${message.slice(0, 200)}`
60
+ : `Hook error failed open: ${message.slice(0, 200)}`,
61
+ offlineEnforced: true,
62
+ ...(suspended ? { severity: 'critical', source: 'backend' } : {}),
63
+ });
64
+ telemetry.triggerFlush(true);
65
+ }
66
+ catch { /* telemetry unavailable — the verdict below still stands */ }
67
+ const verdict = suspended
68
+ ? {
69
+ continue: false,
70
+ permission: 'deny',
71
+ user_message: 'This machine is suspended by your security team — all AI agent actions are blocked. Contact your admin to resume it.',
72
+ agent_message: 'FullCourtDefense: this machine is suspended by an administrator. Stop — do not retry any action until it is resumed.',
73
+ }
74
+ : { continue: true, permission: 'allow' };
75
+ try {
76
+ process.stdout.write(JSON.stringify(verdict) + '\n');
77
+ }
78
+ catch { /* stdout gone */ }
79
+ process.exit(0);
80
+ }
81
+ async function main() {
82
+ const startedAt = Date.now();
83
+ const flags = (0, hookIo_1.parseHookFlags)(process.argv.slice(2));
84
+ const args = (0, hookIo_1.mapHookFlags)(flags);
85
+ try {
86
+ const raw = await (0, hookIo_1.readStdin)();
87
+ // FAST PATH: the resident daemon computes the verdict with everything warm.
88
+ // Bounded hard (connect ~250ms, response ~2.5s) — any non-verdict outcome
89
+ // falls through to the full evaluation below. FCD_VERDICT_IPC=off opts out.
90
+ if (process.env.FCD_VERDICT_IPC !== 'off') {
91
+ const ipc = await (0, verdictIpcClient_1.requestDaemonVerdict)(args, raw, undefined, startedAt);
92
+ if (ipc.outcome === 'verdict') {
93
+ (0, hookIo_1.dbg)({ phase: 'ipc_verdict', event: args.event, exitCode: ipc.exitCode, entry: 'slim' });
94
+ if (ipc.stderr) {
95
+ try {
96
+ process.stderr.write(ipc.stderr);
97
+ }
98
+ catch { /* stderr gone */ }
99
+ }
100
+ if (ipc.stdout) {
101
+ try {
102
+ process.stdout.write(ipc.stdout);
103
+ }
104
+ catch { /* stdout gone */ }
105
+ }
106
+ process.exit(ipc.exitCode ?? 0);
107
+ }
108
+ (0, hookIo_1.dbg)({ phase: 'ipc_unavailable', event: args.event, detail: ipc.detail, entry: 'slim' });
109
+ }
110
+ // FALLBACK: daemon down/busy — load the full evaluation lazily. This is
111
+ // the exact code `index.js hook` runs (including its own fail-open and
112
+ // kill-switch handling), so daemon-down behavior is identical to the
113
+ // pre-slim hook. evaluateHookRequest never rejects by contract.
114
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
115
+ const { evaluateHookRequest } = require('./commands/hook');
116
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
117
+ const { loadConfig } = require('./config');
118
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
119
+ const { markVerdictTiming } = require('./telemetry');
120
+ const result = await markVerdictTiming({ startedAt, path: 'local' }, () => evaluateHookRequest(args, raw, loadConfig()));
121
+ if (result.stderr) {
122
+ try {
123
+ process.stderr.write(result.stderr);
124
+ }
125
+ catch { /* stderr gone */ }
126
+ }
127
+ if (result.stdout) {
128
+ try {
129
+ process.stdout.write(result.stdout);
130
+ }
131
+ catch { /* stdout gone */ }
132
+ }
133
+ process.exit(result.exitCode);
134
+ }
135
+ catch (err) {
136
+ emitFailOpen(args.event, err instanceof Error ? err.message : String(err));
137
+ }
138
+ }
139
+ // Main-guarded so importing this module (tests, tooling) never runs the hook.
140
+ if (require.main === module) {
141
+ void main();
142
+ }
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
44
44
  // touching this file.
45
45
  const config_1 = require("./config");
46
46
  const hook_1 = require("./commands/hook");
47
+ const hookIo_1 = require("./hookIo");
47
48
  const fs = __importStar(require("fs"));
48
49
  const path = __importStar(require("path"));
49
50
  function readCliVersion() {
@@ -679,20 +680,9 @@ async function main() {
679
680
  break;
680
681
  }
681
682
  case 'hook': {
682
- const args = {
683
- event: flags.event,
684
- apiUrl: flags['api-url'],
685
- shieldId: flags['shield-id'],
686
- shieldKey: flags['shield-key'],
687
- shadow: flags.shadow,
688
- enforce: flags.enforce,
689
- failClosed: flags['fail-closed'],
690
- localOnly: flags['local-only'],
691
- timeout: flags.timeout,
692
- approvalMode: flags['approval-mode'],
693
- approvalTimeoutMs: flags['approval-timeout-ms'],
694
- approvalPollMs: flags['approval-poll-ms'],
695
- };
683
+ // ONE flag mapping shared with the slim entry (hookSlim.ts) — the daemon
684
+ // IPC evaluator must see identical argument shapes from both entries.
685
+ const args = (0, hookIo_1.mapHookFlags)(flags);
696
686
  await (0, hook_1.hookCommand)(args, config);
697
687
  break;
698
688
  }
@@ -1,82 +1,19 @@
1
1
  /**
2
- * Daemon verdict IPC — the resident-agent pattern real endpoint products use
3
- * (one resident process; per-event interceptors are thin clients that ask it
4
- * over local IPC), applied to the IDE hook path.
2
+ * Daemon verdict IPC — SERVER side (runs inside the resident daemon).
5
3
  *
6
- * Why: every IDE hook event spawns a fresh Node process. The spawn itself is
7
- * unavoidable (the IDE invokes a command), but everything AFTER it — config
8
- * parse, credential resolution, bundle read, snapshot load, policy evaluation,
9
- * possible network — used to repeat per event. The daemon is already resident
10
- * with all of that warm in memory, so the spawned hook becomes a THIN CLIENT:
11
- * it forwards `{args, stdin}` over a named pipe (Windows) / Unix domain socket
12
- * and prints the verdict the daemon computed in single-digit milliseconds.
4
+ * The resident-agent pattern real endpoint products use: one resident process;
5
+ * per-event interceptors are thin clients that ask it over local IPC. The
6
+ * daemon is already resident with config, credentials, bundle, and snapshot
7
+ * warm in memory, so a spawned hook becomes a THIN CLIENT that forwards
8
+ * `{args, stdin}` over a named pipe (Windows) / Unix domain socket and prints
9
+ * the verdict the daemon computed in single-digit milliseconds.
13
10
  *
14
- * Fallback contract: ANY failure — pipe missing, handshake error, slow or
15
- * busy daemon (response cap ~2.5s) — makes the hook silently fall back to its
16
- * full local evaluation, identical to before this feature existed. Local
17
- * re-evaluation after a partial daemon evaluation is safe: verdict
18
- * computation is read-mostly (worst case a duplicate telemetry event), and
19
- * enforcement is never weakened by evaluating twice. The cap guarantees a
20
- * busy daemon (blocking schtasks call, GC pause) can never make a hook
21
- * SLOWER than the pre-IPC status quo.
22
- *
23
- * Security: named pipes are reachable by other local users by default, so the
24
- * server requires a per-boot random token stored under ~/.fullcourtdefense
25
- * (user-profile ACL). A client that cannot read the token cannot get verdicts.
26
- * The protocol is line-delimited JSON — one request line, one response line.
27
- */
28
- export declare const VERDICT_IPC_PROTOCOL_VERSION = 1;
29
- export interface VerdictRequest {
30
- v: number;
31
- token: string;
32
- /** HookArgs as parsed by the CLI entry (all-string values). */
33
- args: Record<string, string | undefined>;
34
- /** Raw stdin payload exactly as the IDE piped it (BOM already stripped). */
35
- stdin: string;
36
- /**
37
- * Client's `Date.now()` at hook start, so the daemon-written telemetry event
38
- * carries the wall time the DEVELOPER waited (process spawn + pipe + verdict),
39
- * not just the daemon's own evaluation slice. Optional on purpose: during a
40
- * staged rollout an old client omits it (daemon falls back to its own clock)
41
- * and an old daemon ignores it — same-machine clock, so no skew.
42
- */
43
- t0?: number;
44
- }
45
- export interface VerdictResponse {
46
- v: number;
47
- /** Present on evaluation success. */
48
- stdout?: string;
49
- stderr?: string;
50
- exitCode?: number;
51
- /** Present when the daemon could not evaluate — client falls back locally. */
52
- error?: string;
53
- }
54
- /** Result of a client attempt. Anything but 'verdict' → evaluate locally. */
55
- export interface VerdictClientResult {
56
- outcome: 'verdict' | 'unavailable';
57
- stdout?: string;
58
- stderr?: string;
59
- exitCode?: number;
60
- detail?: string;
61
- }
62
- export declare function verdictTokenFile(): string;
63
- export declare function verdictPipePath(): string;
64
- /**
65
- * Ask the resident daemon for a verdict. Bounded hard: connect budget ~250ms
66
- * (a missing pipe fails in <1ms with ENOENT — the common daemon-down case
67
- * costs nothing), response capped at ~2.5s. A verdict that legitimately needs
68
- * longer than that (synchronous approval wait, no-cache gate call) is exactly
69
- * the case where the IPC saves nothing — the local evaluation path handles it
70
- * with its own bounded waits. Never throws.
71
- *
72
- * Burst behavior: an agent firing many commands at once (docker compose +
73
- * exec + build in flight together) means many hooks connecting to the ONE
74
- * pipe simultaneously — some connects lose the race (EBUSY / connect
75
- * timeout) while the daemon is demonstrably alive (token present). Those
76
- * get ONE short jittered retry; hard failures (ENOENT stale pipe, refused)
77
- * still fall back to local evaluation instantly.
11
+ * The client half lives in `verdictIpcClient.ts` — a Node-builtins-only leaf
12
+ * shared with the slim hook entry (`hookSlim.ts`), whose module graph must
13
+ * stay tiny because it loads on every IDE hook event. This file re-exports
14
+ * the client API so existing imports keep working.
78
15
  */
79
- export declare function requestDaemonVerdict(args: Record<string, string | undefined>, stdin: string, responseTimeoutMs?: number, clientStartedAt?: number): Promise<VerdictClientResult>;
16
+ export { VERDICT_IPC_PROTOCOL_VERSION, VerdictRequest, VerdictResponse, VerdictClientResult, requestDaemonVerdict, verdictPipePath, verdictTokenFile, } from './verdictIpcClient';
80
17
  export interface VerdictServerHandle {
81
18
  close(): void;
82
19
  }