@smeltjs/core 0.1.0 → 0.2.0

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.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/args.d.ts +12 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +61 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config.d.ts +22 -0
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +32 -1
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/hooks.d.ts +153 -0
  11. package/dist/cli/hooks.d.ts.map +1 -0
  12. package/dist/cli/hooks.js +1180 -0
  13. package/dist/cli/hooks.js.map +1 -0
  14. package/dist/cli/init.d.ts +3 -1
  15. package/dist/cli/init.d.ts.map +1 -1
  16. package/dist/cli/init.js +6 -0
  17. package/dist/cli/init.js.map +1 -1
  18. package/dist/cli/run.d.ts +1 -1
  19. package/dist/cli/run.d.ts.map +1 -1
  20. package/dist/cli/run.js +19 -0
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/hooks/guard-core.d.ts +164 -0
  23. package/dist/hooks/guard-core.d.ts.map +1 -0
  24. package/dist/hooks/guard-core.js +513 -0
  25. package/dist/hooks/guard-core.js.map +1 -0
  26. package/dist/hooks/shim.d.ts +85 -0
  27. package/dist/hooks/shim.d.ts.map +1 -0
  28. package/dist/hooks/shim.js +107 -0
  29. package/dist/hooks/shim.js.map +1 -0
  30. package/dist/hooks/shims/claude-code.d.ts +23 -0
  31. package/dist/hooks/shims/claude-code.d.ts.map +1 -0
  32. package/dist/hooks/shims/claude-code.js +61 -0
  33. package/dist/hooks/shims/claude-code.js.map +1 -0
  34. package/dist/hooks/shims/cline.d.ts +17 -0
  35. package/dist/hooks/shims/cline.d.ts.map +1 -0
  36. package/dist/hooks/shims/cline.js +39 -0
  37. package/dist/hooks/shims/cline.js.map +1 -0
  38. package/dist/hooks/shims/codex.d.ts +23 -0
  39. package/dist/hooks/shims/codex.d.ts.map +1 -0
  40. package/dist/hooks/shims/codex.js +56 -0
  41. package/dist/hooks/shims/codex.js.map +1 -0
  42. package/dist/hooks/shims/cursor.d.ts +19 -0
  43. package/dist/hooks/shims/cursor.d.ts.map +1 -0
  44. package/dist/hooks/shims/cursor.js +47 -0
  45. package/dist/hooks/shims/cursor.js.map +1 -0
  46. package/dist/hooks/shims/gemini.d.ts +23 -0
  47. package/dist/hooks/shims/gemini.d.ts.map +1 -0
  48. package/dist/hooks/shims/gemini.js +53 -0
  49. package/dist/hooks/shims/gemini.js.map +1 -0
  50. package/dist/hooks/shims/grok.d.ts +18 -0
  51. package/dist/hooks/shims/grok.d.ts.map +1 -0
  52. package/dist/hooks/shims/grok.js +37 -0
  53. package/dist/hooks/shims/grok.js.map +1 -0
  54. package/dist/hooks/shims/hermes.d.ts +22 -0
  55. package/dist/hooks/shims/hermes.d.ts.map +1 -0
  56. package/dist/hooks/shims/hermes.js +50 -0
  57. package/dist/hooks/shims/hermes.js.map +1 -0
  58. package/dist/net/policy.d.ts.map +1 -1
  59. package/dist/net/policy.js +1 -0
  60. package/dist/net/policy.js.map +1 -1
  61. package/package.json +12 -4
@@ -0,0 +1,513 @@
1
+ import { readSync, statSync, existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import process from 'node:process';
5
+ /**
6
+ * The guard core — one zero-dependency node script, shared by every harness shim.
7
+ *
8
+ * Contract (KOT-212): stdin carries one JSON request
9
+ * `{ tool, input: { path?, command?, offsetLimited? } }`, stdout answers with one
10
+ * JSON decision `{ action: "allow" | "deny", reason?, suggestion? }`, and the exit
11
+ * code is always 0 — the decision lives in the JSON, and each shim translates it
12
+ * into its harness's own schema (exit 2, `permissionDecision`, `cancel:true`, …).
13
+ *
14
+ * Two properties are load-bearing and guarded:
15
+ *
16
+ * - **Fail open, loudly.** Malformed stdin, a malformed config, an unstatable path —
17
+ * every degenerate input produces `{"action":"allow"}` plus a warning on stderr.
18
+ * A guard that can brick a session on bad input is worse than no guard; the agent
19
+ * loses nothing but the optimization, and the warning says so.
20
+ * - **No library import on any path.** This module imports node builtins only —
21
+ * never `../index.ts`, never a planner, never web-tree-sitter. The allow case is
22
+ * a stat and an exit; the research note
23
+ * (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard
24
+ * at tens of milliseconds, and loading grammar machinery here would spend that
25
+ * budget before deciding anything. The smelt run itself is only ever paid by the
26
+ * *replacement* command the model runs after a deny (or the rewritten command).
27
+ *
28
+ * Config: the nearest `smelt.config.json` (walking up from the cwd, same discovery
29
+ * as the CLI) may carry a `hooks` block — `thresholdBytes` and `enforcement` — plus
30
+ * the `defaultBudgetBytes` the suggested command quotes. This file reads that config
31
+ * with its own tolerant reader instead of importing `cli/config.ts`: the CLI's strict
32
+ * parser sits on the planner import graph, and a *guard* must fail open where the CLI
33
+ * correctly refuses. `test/hooks-guard-core.test.ts` pins the two readers to the same
34
+ * key names and defaults, so they cannot drift apart silently.
35
+ */
36
+ /** Deny threshold when no config says otherwise: reads at or under this pass untouched. */
37
+ export const DEFAULT_THRESHOLD_BYTES = 8192;
38
+ /**
39
+ * The `--budget` the suggested replacement command quotes when no config carries
40
+ * `defaultBudgetBytes`. A suggestion default, not a smelt default: the CLI itself
41
+ * still refuses to run without an explicit budget from a flag or the config.
42
+ */
43
+ export const DEFAULT_SUGGESTION_BUDGET_BYTES = 8000;
44
+ /** The `hooks.enforcement` values `smelt.config.json` may carry. Deny is the default. */
45
+ export const ENFORCEMENT_MODES = ['deny', 'rewrite'];
46
+ /** The config file this guard discovers, by the same name the CLI uses. */
47
+ export const GUARD_CONFIG_FILE_NAME = 'smelt.config.json';
48
+ /**
49
+ * The runnable CLI name every reason and suggestion quotes. A local (non-global)
50
+ * `npm install @smeltjs/core` puts no `smelt` on anyone's PATH — the installer wires
51
+ * every shim as `node "<dist path>"` for exactly that reason — so a suggestion
52
+ * saying bare `smelt` would exit 127 the moment the model (or a rewrite-mode
53
+ * harness) ran it. When this module's sibling `cli/bin.js` exists — the shipped
54
+ * `dist/` layout every real run executes from — the command names it through `node`
55
+ * explicitly; the bare name is only the fallback for layouts where the sibling is
56
+ * absent (the source tree under the test runner).
57
+ */
58
+ export function smeltCliCommand() {
59
+ try {
60
+ const bin = join(dirname(fileURLToPath(import.meta.url)), '..', 'cli', 'bin.js');
61
+ if (existsSync(bin))
62
+ return `node ${shellQuote(bin)}`;
63
+ }
64
+ catch {
65
+ // fall through to the PATH name
66
+ }
67
+ return 'smelt';
68
+ }
69
+ const SMELT_CLI = smeltCliCommand();
70
+ export const DEFAULT_GUARD_SETTINGS = {
71
+ thresholdBytes: DEFAULT_THRESHOLD_BYTES,
72
+ enforcement: 'deny',
73
+ budgetBytes: DEFAULT_SUGGESTION_BUDGET_BYTES,
74
+ persistentStore: false,
75
+ };
76
+ /**
77
+ * Read the nearest `smelt.config.json`'s guard-relevant fields, tolerantly.
78
+ *
79
+ * Tolerant is a deliberate divergence from the CLI: `smelt` refuses a malformed
80
+ * config because a silently skipped setting is a setting the user believed was in
81
+ * force — but this code runs inside somebody's *session*, before every Read, and a
82
+ * guard that turns a config typo into a hard-down harness has failed worse than the
83
+ * typo. So: any unreadable or ill-typed field falls back to its default, and `warn`
84
+ * receives one line saying which file and why — visible in the harness's hook debug
85
+ * output, never fatal.
86
+ */
87
+ export function readGuardSettings(cwd, warn) {
88
+ const path = findGuardConfigFile(cwd);
89
+ if (path === undefined)
90
+ return DEFAULT_GUARD_SETTINGS;
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
94
+ }
95
+ catch (cause) {
96
+ warn(`smelt guard: ${path} is not readable JSON ` +
97
+ `(${cause instanceof Error ? cause.message : String(cause)}) — ` +
98
+ `guarding with defaults instead. \`smelt\` itself will refuse this config.`);
99
+ return DEFAULT_GUARD_SETTINGS;
100
+ }
101
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
102
+ warn(`smelt guard: ${path} is not a JSON object — guarding with defaults instead.`);
103
+ return DEFAULT_GUARD_SETTINGS;
104
+ }
105
+ const fields = parsed;
106
+ const hooks = typeof fields['hooks'] === 'object' &&
107
+ fields['hooks'] !== null &&
108
+ !Array.isArray(fields['hooks'])
109
+ ? fields['hooks']
110
+ : {};
111
+ return {
112
+ thresholdBytes: positiveInteger(hooks['thresholdBytes'], DEFAULT_THRESHOLD_BYTES, `${path}: hooks.thresholdBytes`, warn),
113
+ enforcement: enforcementMode(hooks['enforcement'], `${path}: hooks.enforcement`, warn),
114
+ budgetBytes: positiveInteger(fields['defaultBudgetBytes'], DEFAULT_SUGGESTION_BUDGET_BYTES, `${path}: defaultBudgetBytes`, warn),
115
+ persistentStore: isDirectoryStore(fields['store']),
116
+ };
117
+ }
118
+ /** True for a well-formed `{"kind":"directory","path":…}` store block; no warning
119
+ * otherwise — an absent or memory store is a valid (just non-persistent) choice. */
120
+ function isDirectoryStore(value) {
121
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
122
+ return false;
123
+ const fields = value;
124
+ return fields['kind'] === 'directory' && typeof fields['path'] === 'string';
125
+ }
126
+ function positiveInteger(value, fallback, what, warn) {
127
+ if (value === undefined)
128
+ return fallback;
129
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0)
130
+ return value;
131
+ warn(`smelt guard: ${what} must be a positive integer, got ${JSON.stringify(value)} — ` +
132
+ `using ${String(fallback)}.`);
133
+ return fallback;
134
+ }
135
+ function enforcementMode(value, what, warn) {
136
+ if (value === undefined)
137
+ return 'deny';
138
+ if (value === 'deny' || value === 'rewrite')
139
+ return value;
140
+ warn(`smelt guard: ${what} must be "deny" or "rewrite", got ${JSON.stringify(value)} — ` +
141
+ `using "deny".`);
142
+ return 'deny';
143
+ }
144
+ /** The same upward walk `cli/config.ts` does, re-implemented to keep this module tiny. */
145
+ export function findGuardConfigFile(cwd) {
146
+ let dir = resolve(cwd);
147
+ for (;;) {
148
+ const candidate = join(dir, GUARD_CONFIG_FILE_NAME);
149
+ if (existsSync(candidate))
150
+ return candidate;
151
+ const parent = dirname(dir);
152
+ if (parent === dir)
153
+ return undefined;
154
+ dir = parent;
155
+ }
156
+ }
157
+ /** Parse one stdin request. `undefined` means malformed — the caller allows and warns. */
158
+ export function parseGuardRequest(text) {
159
+ let parsed;
160
+ try {
161
+ parsed = JSON.parse(text);
162
+ }
163
+ catch {
164
+ return undefined;
165
+ }
166
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
167
+ return undefined;
168
+ const fields = parsed;
169
+ if (typeof fields['tool'] !== 'string')
170
+ return undefined;
171
+ const input = fields['input'];
172
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
173
+ return undefined;
174
+ const inputFields = input;
175
+ const path = inputFields['path'];
176
+ const command = inputFields['command'];
177
+ const offsetLimited = inputFields['offsetLimited'];
178
+ if (path !== undefined && typeof path !== 'string')
179
+ return undefined;
180
+ if (command !== undefined && typeof command !== 'string')
181
+ return undefined;
182
+ if (offsetLimited !== undefined && typeof offsetLimited !== 'boolean')
183
+ return undefined;
184
+ return {
185
+ tool: fields['tool'],
186
+ input: {
187
+ ...(path === undefined ? {} : { path }),
188
+ ...(command === undefined ? {} : { command }),
189
+ ...(offsetLimited === undefined ? {} : { offsetLimited }),
190
+ },
191
+ };
192
+ }
193
+ const ALLOW = { action: 'allow' };
194
+ /**
195
+ * The decision, pure given a stat function — so tests exercise every branch without
196
+ * a filesystem, and the script wires `statSync` in.
197
+ *
198
+ * The shape of the rules, from the research note (§ 5, "the ~8 KB threshold,
199
+ * validated", with both amendments):
200
+ *
201
+ * - **Read**: stat the exact target; deny only above the threshold, and never when
202
+ * the read is already windowed (`offsetLimited`) — a windowed read of a huge file
203
+ * is an economy move.
204
+ * - **Bash**: only a *simple* command whose subject is a statable named file can be
205
+ * judged pre-run. `cat <file>` above the threshold is denied with the faithful
206
+ * replacement; `grep`/`rg` output is unknowable pre-run, so it passes in deny
207
+ * mode and is wrapped (` | smelt --budget … --focus <pattern>`) only under
208
+ * `hooks.enforcement: "rewrite"`. Pipelines, redirects, substitutions — anything
209
+ * this parser cannot be sure about — pass untouched. Fail open, always.
210
+ */
211
+ export function decide(request, settings, cwd, statFile = statFileReal) {
212
+ if (request.tool === 'Read') {
213
+ return decideRead(request.input, settings, cwd, statFile);
214
+ }
215
+ if (request.tool === 'Bash') {
216
+ return decideBash(request.input, settings, cwd, statFile);
217
+ }
218
+ return ALLOW;
219
+ }
220
+ function statFileReal(path) {
221
+ try {
222
+ const stat = statSync(path);
223
+ return { size: stat.size, isFile: stat.isFile() };
224
+ }
225
+ catch {
226
+ return undefined;
227
+ }
228
+ }
229
+ function decideRead(input, settings, cwd, statFile) {
230
+ if (input.path === undefined)
231
+ return ALLOW;
232
+ if (input.offsetLimited === true)
233
+ return ALLOW; // already windowed — an economy move
234
+ const path = isAbsolute(input.path) ? input.path : resolve(cwd, input.path);
235
+ const stat = statFile(path);
236
+ if (stat === undefined || !stat.isFile)
237
+ return ALLOW; // let the tool surface its own error
238
+ if (stat.size <= settings.thresholdBytes)
239
+ return ALLOW;
240
+ return denyOversized(path, stat.size, settings, 'Reading it raw');
241
+ }
242
+ function decideBash(input, settings, cwd, statFile) {
243
+ if (input.command === undefined)
244
+ return ALLOW;
245
+ const command = input.command.trim();
246
+ // A command already using smelt is the model doing the right thing — including the
247
+ // exact replacement a previous deny suggested. Never intercept it (and never wrap a
248
+ // wrapped pipeline a second time).
249
+ if (/(^|[\s/"'=])smelt($|[\s"'])/.test(command))
250
+ return ALLOW;
251
+ const words = simpleCommandWords(command);
252
+ if (words === undefined || words.length === 0)
253
+ return ALLOW; // not simple — unknowable pre-run
254
+ const program = words[0].split('/').at(-1);
255
+ if (program === 'cat') {
256
+ const files = words.slice(1).filter((word) => word !== '--' && !word.startsWith('-'));
257
+ for (const file of files) {
258
+ const path = isAbsolute(file) ? file : resolve(cwd, file);
259
+ const stat = statFile(path);
260
+ if (stat === undefined || !stat.isFile)
261
+ continue;
262
+ if (stat.size > settings.thresholdBytes) {
263
+ const decision = denyOversized(path, stat.size, settings, `\`${command}\``);
264
+ // The suggestion is only a *faithful* replacement when cat named exactly this
265
+ // one file; `cat a b` replaced by `smelt a` would silently drop b, so the
266
+ // multi-file case keeps the reason (the model decides), drops the suggestion
267
+ // (nothing may auto-substitute it), and says out loud that the named
268
+ // replacement covers only the oversized file — a model following the reason
269
+ // verbatim must not silently drop the others.
270
+ return files.length === 1
271
+ ? decision
272
+ : {
273
+ action: 'deny',
274
+ reason: `${decision.reason ?? ''} Note: \`${command}\` names ` +
275
+ `${String(files.length)} files and the replacement above covers only ` +
276
+ `${path} — read the other file(s) separately (cat is fine for the ones ` +
277
+ `under the threshold).`,
278
+ };
279
+ }
280
+ }
281
+ return ALLOW;
282
+ }
283
+ if ((program === 'grep' || program === 'rg') && settings.enforcement === 'rewrite') {
284
+ const pattern = searchPattern(words);
285
+ if (pattern === undefined)
286
+ return ALLOW;
287
+ // Deliberately no `--focus` on the wrap: a plain grep's every output line contains
288
+ // the searched pattern, so focusing on it would protect the entire output — zero
289
+ // elisions exactly when the output is large, plus an over-budget exit. The wrap
290
+ // lets smelt's lexical planner keep the head and tail and collapse the middle
291
+ // into retrievable markers instead.
292
+ const wrapped = `${command} | ${SMELT_CLI} --budget ${String(settings.budgetBytes)}`;
293
+ return {
294
+ action: 'deny',
295
+ reason: `smelt guard (rewrite mode): \`${program}\` output size is unknowable before it runs, ` +
296
+ `so pipe it through smelt instead. Run exactly: ${wrapped} — output within the ` +
297
+ `budget passes through untouched; past it, elided regions leave <<smelt/v1 …>> ` +
298
+ `markers. ${retrieveSentence(settings)}`,
299
+ suggestion: wrapped,
300
+ };
301
+ }
302
+ return ALLOW;
303
+ }
304
+ /**
305
+ * The one sentence about getting elided bytes back — honest about the store: the
306
+ * retrieval promise is only made when a persistent store is configured, because a
307
+ * memory store dies with the process and `retrieve` then refuses (`resolveStoreRun`).
308
+ */
309
+ function retrieveSentence(settings) {
310
+ return settings.persistentStore
311
+ ? `\`${SMELT_CLI} retrieve <hash>\` prints any marker's bytes back, byte for byte.`
312
+ : `\`${SMELT_CLI} retrieve <hash>\` can print a marker's bytes back once a persistent ` +
313
+ `store is configured ({"store":{"kind":"directory","path":…}} in smelt.config.json — ` +
314
+ `\`smelt hooks install\` writes one); without it the elided bytes die with the ` +
315
+ `smelt process.`;
316
+ }
317
+ /** The deny everything above the threshold gets: steering text plus the exact command. */
318
+ function denyOversized(path, size, settings, what) {
319
+ const replacement = `${SMELT_CLI} ${shellQuote(path)} --budget ${String(settings.budgetBytes)}`;
320
+ return {
321
+ action: 'deny',
322
+ reason: `smelt guard: ${path} is ${String(size)} bytes — over the ${String(settings.thresholdBytes)}-byte ` +
323
+ `threshold (smelt.config.json hooks.thresholdBytes). ${what} would spend context on bytes ` +
324
+ `the task may not need. Run instead: ${replacement} --focus <what you are looking for> ` +
325
+ `(repeat --focus per term; focused regions survive verbatim). Elided regions leave ` +
326
+ `<<smelt/v1 …>> markers — ${retrieveSentence(settings)} A windowed read (offset/limit) ` +
327
+ `of just the lines you need is also fine.`,
328
+ suggestion: replacement,
329
+ };
330
+ }
331
+ /**
332
+ * Split a command into words IF it is one simple command: no pipes, no logic, no
333
+ * redirects, no substitutions, no expansions this code would have to model. Anything
334
+ * else returns `undefined` and the caller allows — the guard judges only what it can
335
+ * see whole.
336
+ */
337
+ export function simpleCommandWords(command) {
338
+ const words = [];
339
+ let current = '';
340
+ let started = false;
341
+ let i = 0;
342
+ const push = () => {
343
+ if (started)
344
+ words.push(current);
345
+ current = '';
346
+ started = false;
347
+ };
348
+ while (i < command.length) {
349
+ const ch = command[i];
350
+ if ('|&;<>()`$\\\n*?~{}!'.includes(ch))
351
+ return undefined; // shell would interpret it
352
+ if (ch === "'" || ch === '"') {
353
+ const quote = ch;
354
+ i += 1;
355
+ started = true;
356
+ while (i < command.length && command[i] !== quote) {
357
+ if (quote === '"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\')) {
358
+ return undefined; // expansions inside double quotes — not simple
359
+ }
360
+ current += command[i];
361
+ i += 1;
362
+ }
363
+ if (i >= command.length)
364
+ return undefined; // unterminated quote
365
+ i += 1;
366
+ continue;
367
+ }
368
+ if (ch === ' ' || ch === '\t') {
369
+ push();
370
+ i += 1;
371
+ continue;
372
+ }
373
+ current += ch;
374
+ started = true;
375
+ i += 1;
376
+ }
377
+ push();
378
+ return words;
379
+ }
380
+ /**
381
+ * The pattern a grep/rg invocation searches for: an explicit `-e`/`--regexp` value if
382
+ * given, else the first word that is not a flag or a flag's value. `undefined` when
383
+ * the parse is not sure — and unsure means allow, like everything else here.
384
+ */
385
+ export function searchPattern(words) {
386
+ const takesValue = new Set([
387
+ '-e',
388
+ '--regexp',
389
+ '-f',
390
+ '--file',
391
+ '-m',
392
+ '--max-count',
393
+ '-A',
394
+ '--after-context',
395
+ '-B',
396
+ '--before-context',
397
+ '-C',
398
+ '--context',
399
+ '-d',
400
+ '--directories',
401
+ '-D',
402
+ '--devices',
403
+ '--include',
404
+ '--exclude',
405
+ '--exclude-dir',
406
+ '-t',
407
+ '--type',
408
+ '-T',
409
+ '--type-not',
410
+ '-g',
411
+ '--glob',
412
+ '--iglob',
413
+ '-j',
414
+ '--threads',
415
+ '--color',
416
+ '--colour',
417
+ ]);
418
+ let i = 1;
419
+ while (i < words.length) {
420
+ const word = words[i];
421
+ if (word === '--')
422
+ return words[i + 1];
423
+ if (word === '-e' || word === '--regexp')
424
+ return words[i + 1];
425
+ if (word.startsWith('--') && word.includes('=')) {
426
+ i += 1;
427
+ continue;
428
+ }
429
+ if (word.startsWith('-') && word.length > 1) {
430
+ i += takesValue.has(word) ? 2 : 1;
431
+ continue;
432
+ }
433
+ return word;
434
+ }
435
+ return undefined;
436
+ }
437
+ /** Single-quote a value for `sh` unless it is plainly safe bare. */
438
+ export function shellQuote(value) {
439
+ if (/^[A-Za-z0-9_./:=-]+$/.test(value))
440
+ return value;
441
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
442
+ }
443
+ /* ------------------------------------------------------------------------------------
444
+ * The script half: `node dist/hooks/guard-core.js` — stdin request, stdout decision.
445
+ * ---------------------------------------------------------------------------------- */
446
+ /** True when this module is the file node was asked to run, not an import. */
447
+ export function isMainModule(moduleUrl) {
448
+ const entry = process.argv[1];
449
+ if (entry === undefined)
450
+ return false;
451
+ try {
452
+ return pathToFileURL(entry).href === moduleUrl;
453
+ }
454
+ catch {
455
+ return false;
456
+ }
457
+ }
458
+ /** Every byte of fd 0 to EOF, retrying EAGAIN — the same shape `cli/bin.ts` uses. */
459
+ export function readAllOfStdin() {
460
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
461
+ const chunks = [];
462
+ const chunk = Buffer.alloc(1 << 16);
463
+ for (;;) {
464
+ let bytesRead;
465
+ try {
466
+ bytesRead = readSync(0, chunk, 0, chunk.length, null);
467
+ }
468
+ catch (error) {
469
+ const code = error.code;
470
+ if (code === 'EAGAIN') {
471
+ Atomics.wait(sleeper, 0, 0, 10);
472
+ continue;
473
+ }
474
+ if (code === 'EOF')
475
+ break;
476
+ throw error;
477
+ }
478
+ if (bytesRead === 0)
479
+ break;
480
+ chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));
481
+ }
482
+ return Buffer.concat(chunks).toString('utf8');
483
+ }
484
+ /**
485
+ * Run the guard over stdin and print the decision. Exit code 0 in every case —
486
+ * including every failure, which becomes an allow with a stderr warning. A guard
487
+ * subprocess that exits non-zero on its own bug would look like a policy decision
488
+ * to some harnesses, and "my hook crashed" must never read as "denied".
489
+ */
490
+ export function runGuardMain() {
491
+ let decision = ALLOW;
492
+ try {
493
+ const raw = readAllOfStdin();
494
+ const request = parseGuardRequest(raw);
495
+ if (request === undefined) {
496
+ process.stderr.write(`smelt guard: stdin was not a {tool, input} JSON request — allowing the call. ` +
497
+ `A malformed hook wiring must never brick the session; fix the shim, not the model.\n`);
498
+ }
499
+ else {
500
+ const settings = readGuardSettings(process.cwd(), (text) => process.stderr.write(`${text}\n`));
501
+ decision = decide(request, settings, process.cwd());
502
+ }
503
+ }
504
+ catch (error) {
505
+ process.stderr.write(`smelt guard: unexpected error — allowing the call. ` +
506
+ `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
507
+ decision = ALLOW;
508
+ }
509
+ process.stdout.write(`${JSON.stringify(decision)}\n`);
510
+ }
511
+ if (isMainModule(import.meta.url))
512
+ runGuardMain();
513
+ //# sourceMappingURL=guard-core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard-core.js","sourceRoot":"","sources":["../../src/hooks/guard-core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ import { decide, isMainModule, parseGuardRequest } from './guard-core.ts';
2
+ import type { GuardDecision, GuardRequest, GuardSettings } from './guard-core.ts';
3
+ /**
4
+ * The shim runtime — the one loop every per-harness shim runs.
5
+ *
6
+ * A shim is a thin adapter: it maps its harness's native PreToolUse stdin schema
7
+ * onto a {@link GuardRequest}, hands that to the guard core, and renders the
8
+ * {@link GuardDecision} back in the harness's native output schema. All policy —
9
+ * threshold, windows, suggestion rendering, enforcement mode — lives in
10
+ * `guard-core.ts`; a shim that grew an opinion of its own would be a second guard
11
+ * that drifts from the first.
12
+ *
13
+ * The enforcement gate lives here, once, because it is the same for every harness:
14
+ *
15
+ * - **deny mode (the default)**: a deny is rendered as the harness's deny, with the
16
+ * guard's steering reason. The transcript stays truthful — the model sees what it
17
+ * asked for refused and why, and learns to run the replacement (and
18
+ * `smelt retrieve`) itself.
19
+ * - **rewrite mode (`hooks.enforcement: "rewrite"` in smelt.config.json)**: when the
20
+ * denied call is a Bash command, the guard produced a faithful executable
21
+ * `suggestion`, and this harness can modify tool input, the shim substitutes the
22
+ * suggestion instead of denying. A harness that cannot rewrite falls back to the
23
+ * deny — with the exact command in the reason — so rewrite mode never silently
24
+ * weakens into nothing. A substitution is always announced: in the decision
25
+ * reason where the harness's rewrite schema carries one (Claude Code, Codex),
26
+ * and on stderr where it does not (Gemini, Cursor, Hermes, the opencode
27
+ * plugin) — a rewrite nobody can see would change what the model asked for
28
+ * without telling anyone. And a shim never rewrites in deny mode: a silent
29
+ * rewrite changes what the model asked for without telling it, which is this
30
+ * project's signature failure shape applied to the harness (founder ruling,
31
+ * KOT-212).
32
+ */
33
+ /** What one harness shim supplies. Everything else is {@link runShim}'s. */
34
+ export interface ShimAdapter {
35
+ /** Map the harness's raw stdin JSON to a guard request; `undefined` = pass through. */
36
+ readonly toRequest: (raw: unknown) => GuardRequest | undefined;
37
+ /**
38
+ * The working directory tool-relative paths resolve against, read from the raw
39
+ * payload when the harness sends one (Claude Code's hook stdin carries `cwd`: the
40
+ * *session's* Bash cwd, which after a `cd` differs from this hook process's own).
41
+ * `undefined` falls back to the shim process's cwd.
42
+ */
43
+ readonly cwd?: (raw: unknown) => string | undefined;
44
+ /** Render an allow. Most harnesses: empty stdout, exit 0. */
45
+ readonly pass: () => ShimOutput;
46
+ /** Render a deny, in the harness's schema. */
47
+ readonly deny: (raw: unknown, request: GuardRequest, decision: GuardDecision) => ShimOutput;
48
+ /**
49
+ * Render an input rewrite (only ever called for a Bash-shaped request whose
50
+ * decision carries a faithful `suggestion`, and only in rewrite mode). Omit on
51
+ * harnesses whose hook schema cannot modify tool input — they deny instead. A
52
+ * schema with no reason channel must announce the substitution via `stderr`.
53
+ */
54
+ readonly rewrite?: (raw: unknown, request: GuardRequest, decision: GuardDecision) => ShimOutput;
55
+ }
56
+ /** What a shim writes and how it exits. `stdout` is a complete JSON line or ''. */
57
+ export interface ShimOutput {
58
+ readonly stdout: string;
59
+ /** Written to the shim process's stderr — the announcement channel for harnesses
60
+ * whose rewrite schema has no reason field. Never a decision by itself. */
61
+ readonly stderr?: string;
62
+ readonly exitCode: number;
63
+ }
64
+ /** The pure half of the shim loop, for tests: raw harness input → rendered output. */
65
+ export declare function renderShimDecision(adapter: ShimAdapter, raw: unknown, settings: GuardSettings, cwd: string, statFile?: Parameters<typeof decide>[3]): ShimOutput;
66
+ /**
67
+ * The whole shim as a process: read the harness's stdin, decide, render, exit.
68
+ * Fail open exactly like the guard core — unparseable stdin passes with a stderr
69
+ * warning, and an unexpected error passes too. A shim crash must never read as a
70
+ * policy decision.
71
+ */
72
+ export declare function runShimMain(adapter: ShimAdapter): void;
73
+ /**
74
+ * Map one `{tool name, tool input}` pair — the shape every surveyed harness feeds its
75
+ * pre-tool hook, under varying key spellings — onto a {@link GuardRequest}.
76
+ * `undefined` for tools the guard has no opinion on: pass through.
77
+ */
78
+ export declare function toolCallRequest(toolName: unknown, toolInput: unknown, names: {
79
+ readonly read: readonly string[];
80
+ readonly bash: readonly string[];
81
+ }): GuardRequest | undefined;
82
+ /** The raw stdin as a keyed record, or an empty record when it is not object-shaped. */
83
+ export declare function asRecord(raw: unknown): Record<string, unknown>;
84
+ export { isMainModule, parseGuardRequest };
85
+ //# sourceMappingURL=shim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shim.d.ts","sourceRoot":"","sources":["../../src/hooks/shim.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,MAAM,EACN,YAAY,EACZ,iBAAiB,EAGlB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,YAAY,GAAG,SAAS,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACpD,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,UAAU,CAAC;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,KAAK,UAAU,CAAC;IAC5F;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,KAAK,UAAU,CAAC;CACjG;AAED,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;+EAC2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,GACtC,UAAU,CAkBZ;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CA+BtD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC5E,YAAY,GAAG,SAAS,CAoB1B;AAiBD,wFAAwF;AACxF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9D;AAED,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAC"}