README.mdpreviewREADME.mdsource148 lines · 8.5 KB · raw
1# Chapter 12: jevhooks-daemon/src, the questions and the verdicts
2
3Here, at last, is what Jev is actually asked. It is the heart of the whole
4plugin, and it lives in one file, [judge.rs](judge.rs).
5
6Jev cannot be asked "is this command dangerous?" Or rather it can, and it
7will answer, and the answer will be about the word "dangerous". A command
8that merely *prints* `rm -rf ~/` as text was once stopped as if it ran it.
9So `judge.rs` never asks with an adjective. It writes down a short list of
10concrete things a command could do, defines each in plain acts, and asks
11Jev which one this is. Then it decides what to do with the answer itself,
12in code, where a test can hold it still.
13
14## A command: one request, three questions
15
16Before a Bash command runs, the daemon sends Jev a **state** (the command,
17first 6,000 characters; the working directory; the project root) and three
18questions in one request:
19
201. **`act`, a Choice:** which of these describes what the command does? If
21   it does several, the one hardest to undo.
22
23   | Label | Defined as (abridged) | Consequential? |
24   | --- | --- | --- |
25   | `read` | only reads or prints; nothing is different afterwards | no |
26   | `build` | builds, tests, formats, lints, installs dependencies: regenerable files only | no |
27   | `edit` | creates or changes files a person wrote, or records them in version control; recoverable by an ordinary command | no |
28   | `delete` | removes or overwrites data no build regenerates and version control does not hold | yes |
29   | `history` | discards or rewrites version-control state: reset --hard, clean, rebase, force push | yes |
30   | `system` | changes the machine or the account: packages, services, dotfiles, sudo | yes |
31   | `remote` | publishes or sends something elsewhere: push, deploy, an API call that writes | yes |
32   | `unread` | runs code nobody has read: a script piped from the network into a shell | yes |
33
342. **`undo`, a Score of four levels:** how hard would it be to put
35   everything back? 0 nothing to put back; 1 one ordinary command; 2 only
36   with care or luck; 3 not from this machine.
373. **`load`, a Score of four levels:** how much of the machine does it take
38   while it runs? 0 negligible; 1 light; 2 heavy (compiles a project, a
39   whole test suite); 3 very heavy (several heavy jobs, a large release
40   build).
41
42All three end with `WHAT_RUNS`, a paragraph that says what "the command
43does" means: only what the shell would execute. Text carried as data (a
44here-document written to a file, a quoted string printed or searched for or
45sent) is not executed unless it is handed to something that runs it.
46
47Jev answers each with a probability for every option (and for a Score, an
48expected level from 0 to 3). Then `bash_verdict` decides:
49
50```mermaid
51flowchart TD
52  A["Jev's answers"] --> R{"one consequential act at 60% or more,<br/>or expected undo at 1.6 or more?"}
53  R -- yes --> ASK["ask"]
54  R -- no --> S{"memory available known,<br/>and below what this load needs?"}
55  S -- yes --> ASK
56  S -- no --> O{"read + build + edit at 90% or more,<br/>and expected undo at most 1.2?"}
57  O -- yes --> ALLOW["allow"]
58  O -- no --> PASS["pass:<br/>the usual permission check"]
59```
60
61What a load "needs" rises from 0 MB (negligible) through 500 MB and
623,000 MB to 6,000 MB (very heavy), interpolated between levels: an expected
63load of 2.5 wants 4,500 MB free.
64
65> **Aside: one confident act, never a sum.** An early version added up
66> the consequential acts' probabilities, and a command Jev was unsure
67> about (its likeliest act at 36%) was stopped. Take a spread like 36%
68> `unread`, 20% `system`, 14% `delete`, 30% `edit`: summed, that is 70%
69> "consequential". But it is Jev saying "I don't know", and an unsure
70> answer should change nothing. Now only the single
71> likeliest consequential act counts, and that answer is a `pass`. The
72> test `doubt_spread_over_several_consequential_acts_is_not_a_flag` pins
73> exactly that case.
74
75A real answer, from the log: a long command that read some figures and
76called out to another program came back `read` 56%, `system` 39%, undo
770.4, load 1.3. No single consequential act reached 60%, nothing was hard to
78undo, memory was ample; but the ordinary acts came to 57%, short of 90%. So:
79`Jev: only reads (56%); nothing to undo (0.4 of 3); left to the usual
80permission check`. Not waved through, not flagged; Claude Code's own
81permission rules decided. 237 ms, 1,739 input tokens, $0.00007.
82
83## A turn's end: one question
84
85When a turn is about to end, the state is the session's last five prompts
86(each first 3,000 characters) and the end of the assistant's final message
87(last 6,000 characters), and the one question is a Choice of endings:
88`finished`, `waiting` (needs something only the developer can give),
89`blocked` (names an obstacle), `running` (work still going, will report),
90and `stopped-early` (work asked for is left undone and no obstacle is
91named). Only at 85% or more for `stopped-early` is the end refused: a wrong
92refusal costs the user a wasted turn.
93
94Three checks in `serve.rs` come first, and none of them asks Jev: a `Stop`
95that follows this plugin's own refusal (`stop_hook_active`) is let through,
96so one refusal can never become a loop; a turn with background tasks or
97scheduled wakeups still pending has paused, not ended; and with no prompt
98or final message there is nothing to judge.
99
100> **Aside: the 1.5 seconds.** Every request waits at most `ANSWER_WITHIN`,
101> 1.5 s, and past that the verdict is `pass`. The request itself runs in
102> its own task, so an answer that arrives late still settles its hold in
103> the spend ledger; only the waiting is abandoned.
104
105> **Try it.** `cargo test -p jevhooks-daemon`. The verdict tests in
106> `judge.rs` feed `bash_verdict` and `stop_verdict` made-up probabilities:
107> `ordinary_work_is_allowed`, `consequential_acts_are_put_to_the_user`,
108> `an_unsure_answer_is_neither_allowed_nor_asked`,
109> `a_heavy_command_is_put_to_the_user_when_memory_is_short`,
110> `stop_blocks_only_when_confident`. Change a threshold and watch which
111> ones notice.
112
113## For the people who maintain it
114
115The files, in the order a request meets them:
116
117| Path | What |
118| --- | --- |
119| [main.rs](main.rs) | The `jevhooks` command line (clap): `serve`, `mcp`, `status`, `recent`, `stop`. |
120| [paths.rs](paths.rs) | Where things live: `state_dir`, `socket`, `lock`, `daemon_log`, `decisions`, and `identity`, the build's fingerprint. |
121| [mcp.rs](mcp.rs) | The per-session MCP server (rmcp, stdio): the `status` and `recent` tools, and the call to `client::ensure`. |
122| [client.rs](client.rs) | `call`, one HTTP request on the socket; `ensure`, which makes sure a daemon of this build serves; `spawn`, the one place one is started. |
123| [serve.rs](serve.rs) | The daemon: the lock, the socket, the routes, per-session state (the last five prompts, the cost, the judged tool calls), `decide`, `observe`, `status`. The model is pinned here (`MODEL`, `jev-1.13.0`). |
124| [judge.rs](judge.rs) | The questions, every term defined, the thresholds, `bash_verdict`, `stop_verdict`, and their tests. |
125| [headroom.rs](headroom.rs) | The config file (`Config`), and where the available-memory figure comes from (`Source`: a configured command, `MemAvailable`, or none), measured at most every 5 s. |
126| [log.rs](log.rs) | `decisions.jsonl`: `DecisionRecord`, `OutcomeRecord`, `append`, and `recent`, which reads the last 512 KiB. |
127
128The thresholds, all in `judge.rs` and all reported by `status`:
129
130| Constant | Value | Meaning |
131| --- | --- | --- |
132| `BASH_ASK_AT_CONSEQUENTIAL` | 0.60 | One consequential act this likely: ask. |
133| `BASH_ASK_AT_UNDO` | 1.60 | Expected undo level this high: ask. |
134| `BASH_ALLOW_AT_ORDINARY` | 0.90 | Ordinary acts together this likely ... |
135| `BASH_ALLOW_UNDO_AT_MOST` | 1.20 | ... and undo at most this: allow. |
136| `LOAD_NEEDS_MB` | 0, 500, 3000, 6000 | Memory each load level should find free. |
137| `STOP_BLOCK_AT_LEAST` | 0.85 | `stopped-early` this likely: refuse the end. |
138| `ANSWER_WITHIN` | 1.5 s | Longest wait for Jev. |
139
140A decision record in `decisions.jsonl` has `record: "decision"`, the
141session, `kind` (`bash` or `stop`), the `subject` (a command's first 500
142characters, or a final message's last 500), the `tool_use_id`, the verdict
143and line, `answers` with every option's probability, and `ms`, `jev_ms`,
144`input_tokens`, `usd`, `attempts` and `request_id`. An outcome record has
145`record: "outcome"`, the session, the `tool_use_id` and `ran`, `failed` or
146`denied`, and is written only for a tool call that was judged.
147
148← Previous: [Chapter 11, jevhooks-daemon/](../) · Up: [jevhooks-daemon](../) · Next: [Chapter 13, third-party/](../../../third-party/) →