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/commands/hook.js +36 -102
- package/dist/commands/installClaudeHook.js +17 -3
- package/dist/commands/installCursorHook.js +24 -4
- 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/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 +2 -1
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 {};
|
package/dist/hookSlim.js
ADDED
|
@@ -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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
}
|
package/dist/verdictIpc.d.ts
CHANGED
|
@@ -1,82 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon verdict IPC —
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
}
|