@bobobowis/jev-kit 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.
- package/README.md +71 -0
- package/dist/cli/hook.js +7 -4
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +6 -1
- package/dist/guards/bash-gate.d.ts +38 -2
- package/dist/guards/bash-gate.js +47 -4
- package/dist/guards/index.d.ts +1 -0
- package/dist/guards/index.js +1 -0
- package/dist/guards/mcp-gate.d.ts +23 -0
- package/dist/guards/mcp-gate.js +52 -0
- package/dist/hooks/handle.d.ts +16 -2
- package/dist/hooks/handle.js +19 -6
- package/dist/hooks/harness.d.ts +5 -0
- package/dist/hooks/harness.js +11 -3
- package/dist/setup/index.d.ts +11 -2
- package/dist/setup/index.js +16 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,46 @@ const r = await askFileBool("src/billing.ts", "Does this file round money anywhe
|
|
|
78
78
|
console.log(r.answer, r.noul); // your script, or the agent, never loads the file
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
## Recipes: the lab's levels 1 to 5
|
|
82
|
+
|
|
83
|
+
The package doesn't export the use cases from levels 1 to 5: they are examples with the lab's own
|
|
84
|
+
categories built in, so copy the one you need. Each one is a single short file that uses only what
|
|
85
|
+
the package exports. Copy it into your project, point its imports at the package, and change the
|
|
86
|
+
options and criteria to your own:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// In the lab
|
|
90
|
+
import { jev } from "../../core/client.ts";
|
|
91
|
+
import { noul } from "../../core/helpers.ts";
|
|
92
|
+
import type { NoulAnswer } from "../../core/types.ts";
|
|
93
|
+
import { CONFIDENCE } from "./confidence.ts"; // or { normalized } from "./normalize.ts"
|
|
94
|
+
|
|
95
|
+
// In your project
|
|
96
|
+
import { jev, noul, type NoulAnswer } from "@bobobowis/jev-kit";
|
|
97
|
+
import { CONFIDENCE, normalized } from "@bobobowis/jev-kit/policy";
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| Level | Recipe | Use it for |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| 1 Single decisions | [`injectionGate`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level01/injection-gate.ts) | A prompt injection gate in front of anything that sends user text to an LLM |
|
|
103
|
+
| | [`urgentGate`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level01/urgency-gate.ts) | Pinging a human only when a message is urgent, with your threshold |
|
|
104
|
+
| | [`classifyTicket`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level01/ticket-classifier.ts) | Routing a ticket to one department, with its confidence |
|
|
105
|
+
| 2 Multiple choice | [`triageTicket`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level02/support-triage.ts) | Route, priority, and a human handoff for a support ticket in one call |
|
|
106
|
+
| | [`screenResume`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level02/resume-screening.ts) | Seniority and fit of a resume against a job description |
|
|
107
|
+
| | [`qualifySponsorForm`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level02/sponsor-qualification.ts) | Qualifying inbound forms or leads by intent and product |
|
|
108
|
+
| 3 Composite scoring | [`ticketPriority`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level03/ticket-priority.ts) | A weighted priority from several scores |
|
|
109
|
+
| | [`codeReviewRisk`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level03/code-review-risk.ts) | The risk of a diff in CI, before a human reviews it |
|
|
110
|
+
| | [`ideaVerdict`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level03/idea-verdict.ts) | Kill, fix, or ship on a pitch or proposal |
|
|
111
|
+
| 4 Confidence gating | [`gateShellCommand`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level04/shell-command-gate.ts) | Allowing a shell command, or asking a human when Jev is unsure |
|
|
112
|
+
| | [`routeAccountAction`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level04/account-actions.ts) | Running an account action on its own, asking for a confirmation before a transfer, and handing off to a human when unsure |
|
|
113
|
+
| | [`checkCitation`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level04/citation-check.ts) | Checking that a quote supports a claim, for RAG answers |
|
|
114
|
+
| 5 Routing | [`routeIntent`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level05/intent-router.ts) | Sending a message to the right handler, and to reasoning only when it needs it |
|
|
115
|
+
| | [`routeModel`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level05/model-router.ts) | The cheapest model and the effort a task needs |
|
|
116
|
+
| | [`routeAgent`](https://github.com/bobobowis/ten-levels-of-jev/blob/main/apps/ten-levels/src/levels/level05/agent-router.ts) | Picking which coding agent takes a task |
|
|
117
|
+
|
|
118
|
+
Levels 6 to 10 need no copying: they ship as `/guards`, `/compact`, `/files`, and `/tools`, and
|
|
119
|
+
behind `jev guard`, `jev setup`, and `jev mcp`.
|
|
120
|
+
|
|
81
121
|
## The jev command
|
|
82
122
|
|
|
83
123
|
Results go to stdout as JSON, diagnostics to stderr, so stdout pipes straight into `jq`. Every
|
|
@@ -146,6 +186,7 @@ else alone:
|
|
|
146
186
|
jev setup --harness claude --guards bash,write,result --tools ask_jev # .claude/settings.json, .mcp.json
|
|
147
187
|
jev setup --harness codex --guards bash,write --compact # a marked block in .codex/config.toml
|
|
148
188
|
jev setup --harness claude --level 6 --dry-run # print, write nothing
|
|
189
|
+
jev setup --harness claude --guards bash,write,mcp --ask --allow-read-only # the Level 4 ladder, below
|
|
149
190
|
```
|
|
150
191
|
|
|
151
192
|
Each run sets the whole selection and replaces only what an earlier run wrote. A settings file that
|
|
@@ -204,6 +245,36 @@ echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command"
|
|
|
204
245
|
| jev hook --harness claude --event PreToolUse --guards bash
|
|
205
246
|
```
|
|
206
247
|
|
|
248
|
+
### Ask, don't guess: the Level 4 ladder (Claude Code)
|
|
249
|
+
|
|
250
|
+
A block or a pass is two answers. Level 4 adds the third: when Jev is unsure, a human decides. Two
|
|
251
|
+
flags put it on the bash gate and the MCP gate, and both work on their own:
|
|
252
|
+
|
|
253
|
+
- **`--ask`**: short of a block, the bash gate asks you instead of passing when Jev is unsure (effect
|
|
254
|
+
confidence under 0.5), picks irreversible below the block line, leans destructive (0.4 or more),
|
|
255
|
+
or sees the command reach outside the machine (publish, deploy, push, `curl … | sh`).
|
|
256
|
+
- **`--allow-read-only`**: a command or MCP call Jev is sure only reads (above 0.9, no destructive
|
|
257
|
+
or outward lean) is approved, so Claude Code doesn't prompt you for `git status` or a `SELECT`.
|
|
258
|
+
|
|
259
|
+
The **`mcp` guard** runs the same ladder before any tool from another MCP server (`mcp__<server>__*`;
|
|
260
|
+
Jev's own tools are skipped). Destructive, outward, and unsure calls ask you; it never denies,
|
|
261
|
+
because you connected the server, so you decide. It is opt-in: `--level 6` and `jev hook` without
|
|
262
|
+
`--guards` leave it off.
|
|
263
|
+
|
|
264
|
+
What it did on live Jev, with both flags:
|
|
265
|
+
|
|
266
|
+
| Call | Verdict |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `git status`, `grep -rn TODO`, `Neon run_sql SELECT count(*)`, `clerk list_users` | approved: read only (1.00) |
|
|
269
|
+
| `pnpm test`, `git commit`, `mv`, `Neon create_branch`, `Neon run_sql UPDATE ...` | your usual permissions |
|
|
270
|
+
| `npm publish`, `kubectl apply`, `git push`, `curl … \| sh`, `slack send_message` | asks: reaches outside |
|
|
271
|
+
| `git rebase main`, `psql -f migrate.sql` | asks: Jev is unsure (0.27, 0.48) |
|
|
272
|
+
| `Neon run_sql DROP TABLE`, `Neon delete_branch` | asks: destructive |
|
|
273
|
+
| `git push --force`, `git reset --hard`, `rm -rf` | denied, as without the flags |
|
|
274
|
+
|
|
275
|
+
Codex hooks read only deny, so there the flags and the `mcp` guard would do nothing; `jev setup`
|
|
276
|
+
refuses them for Codex rather than write hooks that don't act.
|
|
277
|
+
|
|
207
278
|
Hooks fail open: a missing key, a Jev error, or a timeout (8 s by default) allows the action and
|
|
208
279
|
reports an error. They never exit 2. The contracts they rely on are in
|
|
209
280
|
[docs/NOTES-claude.md](docs/NOTES-claude.md) and [docs/NOTES-codex.md](docs/NOTES-codex.md).
|
package/dist/cli/hook.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* action. Never exits 2, which both harnesses read as "block".
|
|
5
5
|
*/
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { ALL_GUARDS, runHarnessHook, HOOK_LINES } from "../hooks/index.js";
|
|
7
|
+
import { ALL_GUARDS, LEVEL6_GUARDS, runHarnessHook, HOOK_LINES } from "../hooks/index.js";
|
|
8
8
|
import { deciderFor, list, readAll, withDeadline } from "./common.js";
|
|
9
9
|
export const HOOK_HELP = `Usage: jev hook --harness claude|codex --event <HookEvent> [options]
|
|
10
10
|
|
|
@@ -13,7 +13,9 @@ Reads the hook payload on stdin and answers on stdout. Exit 0 always, except 1 f
|
|
|
13
13
|
Options:
|
|
14
14
|
--harness <name> claude or codex
|
|
15
15
|
--event <name> PreToolUse, PostToolUse, UserPromptSubmit, Stop, PreCompact
|
|
16
|
-
--guards <list> bash,write,result (default:
|
|
16
|
+
--guards <list> bash,write,result,mcp (default: bash,write,result)
|
|
17
|
+
--ask the bash and mcp gates ask you when Jev is unsure or leans destructive (Claude Code)
|
|
18
|
+
--allow-read-only the bash and mcp gates approve what Jev is sure only reads (Claude Code)
|
|
17
19
|
--compact run the Level 7 advisor on UserPromptSubmit and Stop
|
|
18
20
|
--lines <n,n,n> notice, recommend, request lines in context tokens (default ${HOOK_LINES.notice},${HOOK_LINES.recommend},${HOOK_LINES.request})
|
|
19
21
|
--window <tokens> the model's context window, for percentages
|
|
@@ -27,6 +29,7 @@ export async function hook(argv, io) {
|
|
|
27
29
|
args: argv, strict: true,
|
|
28
30
|
options: {
|
|
29
31
|
harness: { type: "string" }, event: { type: "string" }, guards: { type: "string" }, compact: { type: "boolean" },
|
|
32
|
+
ask: { type: "boolean" }, "allow-read-only": { type: "boolean" },
|
|
30
33
|
lines: { type: "string" }, window: { type: "string" }, "timeout-ms": { type: "string" }, mock: { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
31
34
|
},
|
|
32
35
|
}));
|
|
@@ -48,7 +51,7 @@ export async function hook(argv, io) {
|
|
|
48
51
|
io.stderr.write(`jev hook: --event is required\n`);
|
|
49
52
|
return 1;
|
|
50
53
|
}
|
|
51
|
-
const guards = values.guards ? list(values.guards) :
|
|
54
|
+
const guards = values.guards ? list(values.guards) : LEVEL6_GUARDS;
|
|
52
55
|
const unknown = guards.filter((g) => !ALL_GUARDS.includes(g));
|
|
53
56
|
if (unknown.length) {
|
|
54
57
|
io.stderr.write(`jev hook: unknown guard ${unknown.join(", ")}; known: ${ALL_GUARDS.join(", ")}\n`);
|
|
@@ -74,7 +77,7 @@ export async function hook(argv, io) {
|
|
|
74
77
|
reporter.report("error", { hook: values.event, message: `unreadable hook payload: ${err?.message ?? err}` });
|
|
75
78
|
return 0;
|
|
76
79
|
}
|
|
77
|
-
const response = await withDeadline(runHarnessHook(values.event, payload, { harness, guards, compact: values.compact, lines, window, decide, report: (k, p) => reporter.report(k, p) })
|
|
80
|
+
const response = await withDeadline(runHarnessHook(values.event, payload, { harness, guards, ladder: { ask: values.ask, allowReadOnly: values["allow-read-only"] }, compact: values.compact, lines, window, decide, report: (k, p) => reporter.report(k, p) })
|
|
78
81
|
.catch((err) => { reporter.report("error", { hook: values.event, message: err?.message ?? String(err) }); return null; }), timeoutMs, () => { reporter.report("error", { hook: values.event, message: `timed out after ${timeoutMs} ms; allowed` }); return null; });
|
|
79
82
|
if (response)
|
|
80
83
|
io.stdout.write(JSON.stringify(response));
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type Io } from "./common.ts";
|
|
2
|
-
export declare const SETUP_HELP = "Usage: jev setup --harness claude|codex [options]\n\nWrites or merges the Jev entries into the project's agent config, leaving everything else alone.\nClaude Code: .mcp.json and .claude/settings.json. Codex: a marked block in .codex/config.toml.\nEach run sets the whole selection: running it again replaces only what it set up before.\n\nOptions:\n --harness <name> claude or codex\n --guards <list> bash,write,result: the Level 6 hooks\n --tools <list> Jev tools to serve over MCP, for example ask_jev,ask_jev_files\n --compact the Level 7 advisor hooks\n --level <n> a lab level: 6 guards, 7 advisor, 8 to 10 their tools\n --out <dir> the project directory (default: the current one)\n --command <cmd> how to run jev in the hooks and the MCP entry (default: node and this package's bin)\n --hook-args <args> extra jev hook flags, for example \"--lines 60000,120000,180000\"\n --dry-run print what would change, write nothing\n";
|
|
2
|
+
export declare const SETUP_HELP = "Usage: jev setup --harness claude|codex [options]\n\nWrites or merges the Jev entries into the project's agent config, leaving everything else alone.\nClaude Code: .mcp.json and .claude/settings.json. Codex: a marked block in .codex/config.toml.\nEach run sets the whole selection: running it again replaces only what it set up before.\n\nOptions:\n --harness <name> claude or codex\n --guards <list> bash,write,result: the Level 6 hooks; mcp: the MCP gate on other servers' tools (Claude Code)\n --ask the bash and mcp gates ask you when Jev is unsure or leans destructive (Claude Code)\n --allow-read-only the bash and mcp gates approve what Jev is sure only reads (Claude Code)\n --tools <list> Jev tools to serve over MCP, for example ask_jev,ask_jev_files\n --compact the Level 7 advisor hooks\n --level <n> a lab level: 6 guards, 7 advisor, 8 to 10 their tools\n --out <dir> the project directory (default: the current one)\n --command <cmd> how to run jev in the hooks and the MCP entry (default: node and this package's bin)\n --hook-args <args> extra jev hook flags, for example \"--lines 60000,120000,180000\"\n --dry-run print what would change, write nothing\n";
|
|
3
3
|
export declare function setup(argv: string[], io: Io): Promise<number>;
|
package/dist/cli/setup.js
CHANGED
|
@@ -14,7 +14,9 @@ Each run sets the whole selection: running it again replaces only what it set up
|
|
|
14
14
|
|
|
15
15
|
Options:
|
|
16
16
|
--harness <name> claude or codex
|
|
17
|
-
--guards <list> bash,write,result: the Level 6 hooks
|
|
17
|
+
--guards <list> bash,write,result: the Level 6 hooks; mcp: the MCP gate on other servers' tools (Claude Code)
|
|
18
|
+
--ask the bash and mcp gates ask you when Jev is unsure or leans destructive (Claude Code)
|
|
19
|
+
--allow-read-only the bash and mcp gates approve what Jev is sure only reads (Claude Code)
|
|
18
20
|
--tools <list> Jev tools to serve over MCP, for example ask_jev,ask_jev_files
|
|
19
21
|
--compact the Level 7 advisor hooks
|
|
20
22
|
--level <n> a lab level: 6 guards, 7 advisor, 8 to 10 their tools
|
|
@@ -30,6 +32,7 @@ export async function setup(argv, io) {
|
|
|
30
32
|
args: argv, strict: true,
|
|
31
33
|
options: {
|
|
32
34
|
harness: { type: "string" }, guards: { type: "string" }, tools: { type: "string" }, compact: { type: "boolean" },
|
|
35
|
+
ask: { type: "boolean" }, "allow-read-only": { type: "boolean" },
|
|
33
36
|
level: { type: "string" }, out: { type: "string" }, command: { type: "string" }, "hook-args": { type: "string" },
|
|
34
37
|
"dry-run": { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
35
38
|
},
|
|
@@ -54,6 +57,8 @@ export async function setup(argv, io) {
|
|
|
54
57
|
guards: values.guards ? list(values.guards) : undefined,
|
|
55
58
|
tools: values.tools ? list(values.tools) : undefined,
|
|
56
59
|
compact: values.compact,
|
|
60
|
+
ask: values.ask,
|
|
61
|
+
allowReadOnly: values["allow-read-only"],
|
|
57
62
|
level: values.level !== undefined ? Number(values.level) : undefined,
|
|
58
63
|
};
|
|
59
64
|
const changes = plan({
|
|
@@ -6,7 +6,17 @@ export declare const BASH_QUESTIONS: {
|
|
|
6
6
|
export interface BashGateAnswers {
|
|
7
7
|
effect: ChoiceAnswer;
|
|
8
8
|
destructive_intent: NoulAnswer;
|
|
9
|
+
/** Asked only with a ladder that asks. */
|
|
10
|
+
outward?: NoulAnswer;
|
|
9
11
|
}
|
|
12
|
+
/** With `ask`, one more question: a command can change nothing here and still publish or deploy. */
|
|
13
|
+
export declare const BASH_LADDER_QUESTIONS: {
|
|
14
|
+
outward: import("../core/types.ts").NoulQuestion;
|
|
15
|
+
effect: import("../core/types.ts").ChoiceQuestion;
|
|
16
|
+
destructive_intent: import("../core/types.ts").NoulQuestion;
|
|
17
|
+
};
|
|
18
|
+
/** The outward Noul at which a laddered command asks. */
|
|
19
|
+
export declare const BASH_OUTWARD = 0.5;
|
|
10
20
|
export interface BashThresholds {
|
|
11
21
|
/** Confidence the irreversible pick needs before it blocks on its own. */
|
|
12
22
|
irreversible: number;
|
|
@@ -14,16 +24,42 @@ export interface BashThresholds {
|
|
|
14
24
|
destructive: number;
|
|
15
25
|
}
|
|
16
26
|
export declare const BASH_THRESHOLDS: BashThresholds;
|
|
27
|
+
/**
|
|
28
|
+
* What a hook does with a call, on the Level 4 ladder: deny it, ask the user, approve it without
|
|
29
|
+
* the harness's own prompt, or pass it to the harness's usual permissions.
|
|
30
|
+
*/
|
|
31
|
+
export type Verdict = "deny" | "ask" | "allow" | "pass";
|
|
32
|
+
export interface Ladder {
|
|
33
|
+
/** Ask the user, instead of passing, when Jev is unsure or leans destructive without blocking. */
|
|
34
|
+
ask?: boolean;
|
|
35
|
+
/** Approve what Jev is sure only reads, so the harness doesn't prompt for it. */
|
|
36
|
+
allowReadOnly?: boolean;
|
|
37
|
+
/** Below this confidence a human decides. */
|
|
38
|
+
floor?: number;
|
|
39
|
+
/** Above this confidence a read-only pick is approved. */
|
|
40
|
+
bar?: number;
|
|
41
|
+
}
|
|
17
42
|
export interface GateDecision {
|
|
18
43
|
block: boolean;
|
|
19
44
|
reason: string;
|
|
45
|
+
/** Set when a ladder was given; `block` is then `verdict === "deny"`. */
|
|
46
|
+
verdict?: Verdict;
|
|
20
47
|
}
|
|
21
48
|
/** What the agent reads after every block. The block is the answer, not a puzzle to route around. */
|
|
22
49
|
export declare const BLOCK_NOTICE = "This block is final. Do not try to work around it with another command, another tool, a different path, or an encoding that does the same thing. Stop and tell the user what was blocked and why.";
|
|
23
50
|
/** Block or allow. A hook has no third option, so the middle ground allows and says why. */
|
|
24
51
|
export declare function gateBash(a: BashGateAnswers, t?: BashThresholds): GateDecision;
|
|
52
|
+
/**
|
|
53
|
+
* The bash gate on the Level 4 ladder. Blocks stay blocks. Below them, an irreversible pick, a
|
|
54
|
+
* destructive lean, or confidence under the floor asks the user; a read-only pick above the bar is
|
|
55
|
+
* approved. Without `ask` or `allowReadOnly` this is gateBash.
|
|
56
|
+
*/
|
|
57
|
+
export declare function bashVerdict(a: BashGateAnswers, ladder?: Ladder, t?: BashThresholds): GateDecision;
|
|
25
58
|
export type Decide = (state: State, questions: Questions) => Promise<{
|
|
26
59
|
answers: Record<string, unknown>;
|
|
27
60
|
}>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
61
|
+
/**
|
|
62
|
+
* A: one call before the command runs. `decide` defaults to the shared client; the extension passes a reporting one.
|
|
63
|
+
* With a ladder the decision carries a verdict.
|
|
64
|
+
*/
|
|
65
|
+
export declare function gateBashCommand(command: string, cwd: string, decide?: Decide, ladder?: Ladder): Promise<GateDecision>;
|
package/dist/guards/bash-gate.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { jev } from "../core/client.js";
|
|
6
6
|
import { choice, noul } from "../core/helpers.js";
|
|
7
|
+
import { CONFIDENCE } from "../policy/index.js";
|
|
7
8
|
export const BASH_QUESTIONS = {
|
|
8
9
|
effect: choice("What does running `command` in `cwd` do to the machine?", {
|
|
9
10
|
read_only: "Lists, reads, searches, tests, builds into a scratch directory; nothing durable changes",
|
|
@@ -15,6 +16,16 @@ export const BASH_QUESTIONS = {
|
|
|
15
16
|
false: "Reading, listing, testing, installing, generating, or editing in place",
|
|
16
17
|
}),
|
|
17
18
|
};
|
|
19
|
+
/** With `ask`, one more question: a command can change nothing here and still publish or deploy. */
|
|
20
|
+
export const BASH_LADDER_QUESTIONS = {
|
|
21
|
+
...BASH_QUESTIONS,
|
|
22
|
+
outward: noul("Does `command` reach people or systems outside this machine: publish, deploy, push to production, send, or run code downloaded from the internet?", {
|
|
23
|
+
true: "npm publish, kubectl apply, terraform apply, a migration against a remote database, curl ... | sh, sending email",
|
|
24
|
+
false: "Works only on this machine and this repository, or only downloads dependencies through the package manager",
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
/** The outward Noul at which a laddered command asks. */
|
|
28
|
+
export const BASH_OUTWARD = 0.5;
|
|
18
29
|
export const BASH_THRESHOLDS = { irreversible: 0.6, destructive: 0.7 };
|
|
19
30
|
/** What the agent reads after every block. The block is the answer, not a puzzle to route around. */
|
|
20
31
|
export const BLOCK_NOTICE = "This block is final. Do not try to work around it with another command, another tool, a different path, or an encoding that does the same thing. Stop and tell the user what was blocked and why.";
|
|
@@ -28,8 +39,40 @@ export function gateBash(a, t = BASH_THRESHOLDS) {
|
|
|
28
39
|
}
|
|
29
40
|
return { block: false, reason: `${a.effect.choice} (${a.effect.confidence.toFixed(2)}), destructive ${a.destructive_intent.noul.toFixed(2)}` };
|
|
30
41
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
/** Bash read below the destructive line, where a ladder asks rather than passes. */
|
|
43
|
+
const LEANS_DESTRUCTIVE = 0.4;
|
|
44
|
+
/** The most destructive intent a command may show and still be approved as read-only. */
|
|
45
|
+
const READ_ONLY_INTENT = 0.1;
|
|
46
|
+
/**
|
|
47
|
+
* The bash gate on the Level 4 ladder. Blocks stay blocks. Below them, an irreversible pick, a
|
|
48
|
+
* destructive lean, or confidence under the floor asks the user; a read-only pick above the bar is
|
|
49
|
+
* approved. Without `ask` or `allowReadOnly` this is gateBash.
|
|
50
|
+
*/
|
|
51
|
+
export function bashVerdict(a, ladder = {}, t = BASH_THRESHOLDS) {
|
|
52
|
+
const d = gateBash(a, t);
|
|
53
|
+
if (d.block)
|
|
54
|
+
return { ...d, verdict: "deny" };
|
|
55
|
+
const floor = ladder.floor ?? CONFIDENCE.REVIEW_FLOOR, bar = ladder.bar ?? CONFIDENCE.DESTRUCTIVE_BAR;
|
|
56
|
+
if (ladder.ask) {
|
|
57
|
+
if (a.effect.confidence < floor)
|
|
58
|
+
return { block: false, verdict: "ask", reason: `Jev is unsure what this does (${a.effect.choice} at ${a.effect.confidence.toFixed(2)})` };
|
|
59
|
+
if (a.effect.choice === "irreversible")
|
|
60
|
+
return { block: false, verdict: "ask", reason: `may be irreversible (${a.effect.confidence.toFixed(2)})` };
|
|
61
|
+
if (a.destructive_intent.noul >= LEANS_DESTRUCTIVE)
|
|
62
|
+
return { block: false, verdict: "ask", reason: `leans destructive (${a.destructive_intent.noul.toFixed(2)})` };
|
|
63
|
+
if (a.outward && a.outward.noul >= BASH_OUTWARD)
|
|
64
|
+
return { block: false, verdict: "ask", reason: `reaches outside this machine (${a.outward.noul.toFixed(2)}): it publishes, deploys, sends, or runs downloaded code` };
|
|
65
|
+
}
|
|
66
|
+
if (ladder.allowReadOnly && a.effect.choice === "read_only" && a.effect.confidence > bar && a.destructive_intent.noul < READ_ONLY_INTENT && (a.outward?.noul ?? 0) < BASH_OUTWARD) {
|
|
67
|
+
return { block: false, verdict: "allow", reason: `read only (${a.effect.confidence.toFixed(2)})` };
|
|
68
|
+
}
|
|
69
|
+
return { ...d, verdict: "pass" };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A: one call before the command runs. `decide` defaults to the shared client; the extension passes a reporting one.
|
|
73
|
+
* With a ladder the decision carries a verdict.
|
|
74
|
+
*/
|
|
75
|
+
export async function gateBashCommand(command, cwd, decide = (s, q) => jev.systemOne(s, q), ladder) {
|
|
76
|
+
const { answers } = await decide({ command, cwd }, ladder?.ask ? BASH_LADDER_QUESTIONS : BASH_QUESTIONS);
|
|
77
|
+
return ladder ? bashVerdict(answers, ladder) : gateBash(answers);
|
|
35
78
|
}
|
package/dist/guards/index.d.ts
CHANGED
package/dist/guards/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ChoiceAnswer, NoulAnswer } from "../core/types.ts";
|
|
2
|
+
import type { Decide, GateDecision, Ladder } from "./bash-gate.ts";
|
|
3
|
+
export declare const MCP_QUESTIONS: {
|
|
4
|
+
effect: import("../core/types.ts").ChoiceQuestion;
|
|
5
|
+
outward: import("../core/types.ts").NoulQuestion;
|
|
6
|
+
};
|
|
7
|
+
export interface McpGateAnswers {
|
|
8
|
+
effect: ChoiceAnswer;
|
|
9
|
+
outward: NoulAnswer;
|
|
10
|
+
}
|
|
11
|
+
/** The outward Noul at which a call asks whatever its effect. */
|
|
12
|
+
export declare const MCP_OUTWARD = 0.5;
|
|
13
|
+
/** Inputs are cut to this many characters before they go to Jev. */
|
|
14
|
+
export declare const MCP_INPUT_CHARS = 4000;
|
|
15
|
+
/** Ask, approve, or pass; `block` is always false. */
|
|
16
|
+
export declare function mcpVerdict(a: McpGateAnswers, ladder?: Ladder): GateDecision;
|
|
17
|
+
/** `mcp__<server>__<tool>`, split; null for a name that isn't an MCP tool. */
|
|
18
|
+
export declare function mcpToolName(name: string): {
|
|
19
|
+
server: string;
|
|
20
|
+
tool: string;
|
|
21
|
+
} | null;
|
|
22
|
+
/** One call before the tool runs. */
|
|
23
|
+
export declare function gateMcpCall(name: string, input: unknown, decide?: Decide, ladder?: Ladder): Promise<GateDecision>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP gate: the Level 4 account-actions ladder over an agent's MCP tool calls.
|
|
3
|
+
* Before a tool from a connected server runs: does it only read, change something that can be
|
|
4
|
+
* changed back, or destroy or reach outside? Destructive and outward calls ask the user, never run
|
|
5
|
+
* on their own; an unsure call asks too; a call Jev is sure only reads can be approved. It never
|
|
6
|
+
* denies: the user connected the server, so the user decides.
|
|
7
|
+
*/
|
|
8
|
+
import { jev } from "../core/client.js";
|
|
9
|
+
import { choice, noul } from "../core/helpers.js";
|
|
10
|
+
import { CONFIDENCE } from "../policy/index.js";
|
|
11
|
+
export const MCP_QUESTIONS = {
|
|
12
|
+
effect: choice("What does calling the MCP tool `tool` with `input` do?", {
|
|
13
|
+
read_only: "Reads, lists, searches, describes, explains, or fetches; nothing changes",
|
|
14
|
+
reversible: "Creates or updates something that can be edited back or deleted again: a draft, a branch, a record",
|
|
15
|
+
destructive: "Deletes, drops, revokes, resets, overwrites, or restores over existing data, with no way back",
|
|
16
|
+
}),
|
|
17
|
+
outward: noul("Does calling `tool` with `input` reach people or systems outside this work: send a message, publish, deploy, invite, charge money?", {
|
|
18
|
+
true: "Sends email or chat, posts, publishes, deploys to production, changes billing or access for others",
|
|
19
|
+
false: "Stays within the user's own data and environments",
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
/** The outward Noul at which a call asks whatever its effect. */
|
|
23
|
+
export const MCP_OUTWARD = 0.5;
|
|
24
|
+
/** Inputs are cut to this many characters before they go to Jev. */
|
|
25
|
+
export const MCP_INPUT_CHARS = 4000;
|
|
26
|
+
/** Ask, approve, or pass; `block` is always false. */
|
|
27
|
+
export function mcpVerdict(a, ladder = {}) {
|
|
28
|
+
const floor = ladder.floor ?? CONFIDENCE.REVIEW_FLOOR, bar = ladder.bar ?? CONFIDENCE.DESTRUCTIVE_BAR;
|
|
29
|
+
const c = a.effect.confidence.toFixed(2);
|
|
30
|
+
if (a.effect.confidence < floor)
|
|
31
|
+
return { block: false, verdict: "ask", reason: `Jev is unsure what this call does (${a.effect.choice} at ${c})` };
|
|
32
|
+
if (a.effect.choice === "destructive")
|
|
33
|
+
return { block: false, verdict: "ask", reason: `destructive (${c}): there is no way back` };
|
|
34
|
+
if (a.outward.noul >= MCP_OUTWARD)
|
|
35
|
+
return { block: false, verdict: "ask", reason: `reaches outside (${a.outward.noul.toFixed(2)}): it sends, publishes, deploys, or charges` };
|
|
36
|
+
if (ladder.allowReadOnly && a.effect.choice === "read_only" && a.effect.confidence > bar)
|
|
37
|
+
return { block: false, verdict: "allow", reason: `read only (${c})` };
|
|
38
|
+
return { block: false, verdict: "pass", reason: `${a.effect.choice} (${c}), outward ${a.outward.noul.toFixed(2)}` };
|
|
39
|
+
}
|
|
40
|
+
/** `mcp__<server>__<tool>`, split; null for a name that isn't an MCP tool. */
|
|
41
|
+
export function mcpToolName(name) {
|
|
42
|
+
const m = name.match(/^mcp__(.+?)__(.+)$/);
|
|
43
|
+
return m ? { server: m[1], tool: m[2] } : null;
|
|
44
|
+
}
|
|
45
|
+
/** One call before the tool runs. */
|
|
46
|
+
export async function gateMcpCall(name, input, decide = (s, q) => jev.systemOne(s, q), ladder = {}) {
|
|
47
|
+
const parsed = mcpToolName(name);
|
|
48
|
+
const json = JSON.stringify(input ?? {});
|
|
49
|
+
const state = { tool: parsed?.tool ?? name, server: parsed?.server ?? "", input: json.length > MCP_INPUT_CHARS ? `${json.slice(0, MCP_INPUT_CHARS)}…` : json };
|
|
50
|
+
const { answers } = await decide(state, MCP_QUESTIONS);
|
|
51
|
+
return mcpVerdict(answers, ladder);
|
|
52
|
+
}
|
package/dist/hooks/handle.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* the compaction advisor make the same decisions under pi, Claude Code, and Codex.
|
|
5
5
|
*
|
|
6
6
|
* shell before a shell command runs: the bash gate
|
|
7
|
+
* mcp-call before an MCP tool runs: the MCP gate
|
|
7
8
|
* write before a file is created or changed: the write gate
|
|
8
9
|
* read-result after a read returns: the result screen
|
|
9
10
|
* shell-result after a shell command returns: the result screen
|
|
@@ -12,10 +13,13 @@
|
|
|
12
13
|
* Every handler fails open: a Jev error allows the action and reports an `error` event.
|
|
13
14
|
*/
|
|
14
15
|
import type { Decider } from "../events/index.ts";
|
|
16
|
+
import { type Ladder, type Verdict } from "../guards/bash-gate.ts";
|
|
15
17
|
import { type Evaluation, type TurnInput } from "../compact/evaluate.ts";
|
|
16
18
|
import type { CompactAction } from "../compact/should-compact.ts";
|
|
17
|
-
export type Guard = "bash" | "write" | "result";
|
|
19
|
+
export type Guard = "bash" | "write" | "result" | "mcp";
|
|
18
20
|
export declare const ALL_GUARDS: Guard[];
|
|
21
|
+
/** Level 6's guards, and what runs when no guards are named. The MCP gate is opt-in. */
|
|
22
|
+
export declare const LEVEL6_GUARDS: Guard[];
|
|
19
23
|
interface Base {
|
|
20
24
|
cwd: string;
|
|
21
25
|
/** The harness's own name for the event, used in reports: tool_call, PreToolUse, ... */
|
|
@@ -25,6 +29,10 @@ export type HookEvent = (Base & {
|
|
|
25
29
|
kind: "shell";
|
|
26
30
|
tool: string;
|
|
27
31
|
command: string;
|
|
32
|
+
}) | (Base & {
|
|
33
|
+
kind: "mcp-call";
|
|
34
|
+
tool: string;
|
|
35
|
+
input: unknown;
|
|
28
36
|
}) | (Base & {
|
|
29
37
|
kind: "write";
|
|
30
38
|
tool: string;
|
|
@@ -43,6 +51,8 @@ export interface HookOutcome {
|
|
|
43
51
|
/** Deny the action; `reason` is what the agent reads. */
|
|
44
52
|
block?: boolean;
|
|
45
53
|
reason?: string;
|
|
54
|
+
/** Before a call, short of a block: ask the user, or approve without the harness's prompt. */
|
|
55
|
+
verdict?: Exclude<Verdict, "deny" | "pass">;
|
|
46
56
|
/** For results: a banner to show above, or alongside, the content. */
|
|
47
57
|
banner?: string;
|
|
48
58
|
/** For turn ends: a message for the agent's next turn. */
|
|
@@ -53,10 +63,14 @@ export interface HookOutcome {
|
|
|
53
63
|
export interface HookContext {
|
|
54
64
|
decide: Decider;
|
|
55
65
|
report(kind: string, payload: Record<string, unknown>): void;
|
|
56
|
-
/** Which guards are on. Defaults to
|
|
66
|
+
/** Which guards are on. Defaults to Level 6's. */
|
|
57
67
|
guards?: Guard[];
|
|
68
|
+
/** The Level 4 ladder for the bash and MCP gates: when to ask, when to approve. */
|
|
69
|
+
ladder?: Ladder;
|
|
58
70
|
}
|
|
59
71
|
/** The text the agent reads when a guard blocks. */
|
|
60
72
|
export declare const blockMessage: (what: string, reason: string) => string;
|
|
73
|
+
/** The text the user reads when a guard asks. */
|
|
74
|
+
export declare const askMessage: (what: string, reason: string) => string;
|
|
61
75
|
export declare function handleHook(event: HookEvent, ctx: HookContext): Promise<HookOutcome>;
|
|
62
76
|
export {};
|
package/dist/hooks/handle.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import { BLOCK_NOTICE, gateBashCommand } from "../guards/bash-gate.js";
|
|
2
|
+
import { gateMcpCall } from "../guards/mcp-gate.js";
|
|
2
3
|
import { gateWriteCall } from "../guards/write-gate.js";
|
|
3
4
|
import { screenToolResult } from "../guards/result-screen.js";
|
|
4
5
|
import { evaluateCompaction } from "../compact/evaluate.js";
|
|
5
|
-
export const ALL_GUARDS = ["bash", "write", "result"];
|
|
6
|
+
export const ALL_GUARDS = ["bash", "write", "result", "mcp"];
|
|
7
|
+
/** Level 6's guards, and what runs when no guards are named. The MCP gate is opt-in. */
|
|
8
|
+
export const LEVEL6_GUARDS = ["bash", "write", "result"];
|
|
6
9
|
const DEFAULT_HOOK = {
|
|
7
|
-
shell: "tool_call", write: "tool_call", "read-result": "tool_result", "shell-result": "tool_result", "turn-end": "turn_end",
|
|
10
|
+
shell: "tool_call", "mcp-call": "tool_call", write: "tool_call", "read-result": "tool_result", "shell-result": "tool_result", "turn-end": "turn_end",
|
|
8
11
|
};
|
|
9
12
|
/** The text the agent reads when a guard blocks. */
|
|
10
13
|
export const blockMessage = (what, reason) => `jev-guard blocked this ${what}: ${reason}. ${BLOCK_NOTICE}`;
|
|
14
|
+
/** The text the user reads when a guard asks. */
|
|
15
|
+
export const askMessage = (what, reason) => `jev-guard asks before this ${what}: ${reason}.`;
|
|
16
|
+
const laddered = (verdict, what, reason) => verdict === "ask" ? { verdict: "ask", reason: askMessage(what, reason) } : verdict === "allow" ? { verdict: "allow", reason: `jev-guard approved this ${what}: ${reason}.` } : {};
|
|
11
17
|
export async function handleHook(event, ctx) {
|
|
12
|
-
const on = (g) => (ctx.guards ??
|
|
18
|
+
const on = (g) => (ctx.guards ?? LEVEL6_GUARDS).includes(g);
|
|
13
19
|
const hook = event.hook ?? DEFAULT_HOOK[event.kind];
|
|
14
20
|
try {
|
|
15
21
|
switch (event.kind) {
|
|
16
22
|
case "shell": {
|
|
17
23
|
if (!on("bash"))
|
|
18
24
|
return {};
|
|
19
|
-
const d = await gateBashCommand(event.command, event.cwd, (s, q) => ctx.decide(`${hook} ${event.tool}`, s, q));
|
|
20
|
-
ctx.report("hook", { hook, tool: event.tool, command: event.command, block: d.block, reason: d.reason });
|
|
21
|
-
return d.block ? { block: true, reason: blockMessage("command", d.reason) } :
|
|
25
|
+
const d = await gateBashCommand(event.command, event.cwd, (s, q) => ctx.decide(`${hook} ${event.tool}`, s, q), ctx.ladder);
|
|
26
|
+
ctx.report("hook", { hook, tool: event.tool, command: event.command, block: d.block, reason: d.reason, ...(d.verdict ? { verdict: d.verdict } : {}) });
|
|
27
|
+
return d.block ? { block: true, reason: blockMessage("command", d.reason) } : laddered(d.verdict, "command", d.reason);
|
|
28
|
+
}
|
|
29
|
+
case "mcp-call": {
|
|
30
|
+
if (!on("mcp"))
|
|
31
|
+
return {};
|
|
32
|
+
const d = await gateMcpCall(event.tool, event.input, (s, q) => ctx.decide(`${hook} ${event.tool}`, s, q), ctx.ladder);
|
|
33
|
+
ctx.report("hook", { hook, tool: event.tool, block: false, verdict: d.verdict, reason: d.reason });
|
|
34
|
+
return laddered(d.verdict, `${event.tool} call`, d.reason);
|
|
22
35
|
}
|
|
23
36
|
case "write": {
|
|
24
37
|
if (!on("write"))
|
package/dist/hooks/harness.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* they differ in tool names and where context usage lives.
|
|
5
5
|
*
|
|
6
6
|
* PreToolUse Bash: bash gate. Write, Edit, MultiEdit, NotebookEdit, apply_patch: write gate. Deny on block.
|
|
7
|
+
* mcp__*: the MCP gate, except the jev server's own tools. With a ladder, Claude Code
|
|
8
|
+
* also gets ask and allow; Codex only reads deny, so there they pass.
|
|
7
9
|
* PostToolUse Read, Bash: result screen. The banner goes back as additionalContext.
|
|
8
10
|
* UserPromptSubmit Level 7: record the request; inject a waiting tier message once.
|
|
9
11
|
* Stop Level 7: the four questions, the tier saved for the next prompt.
|
|
@@ -11,6 +13,7 @@
|
|
|
11
13
|
*/
|
|
12
14
|
import type { Decider } from "../events/index.ts";
|
|
13
15
|
import type { Lines } from "../compact/should-compact.ts";
|
|
16
|
+
import type { Ladder } from "../guards/bash-gate.ts";
|
|
14
17
|
import { type Guard, type HookContext } from "./handle.ts";
|
|
15
18
|
export type Harness = "claude" | "codex";
|
|
16
19
|
/** Lines for real sessions, whose system prompt alone passes the lab's demo lines. */
|
|
@@ -18,6 +21,8 @@ export declare const HOOK_LINES: Lines;
|
|
|
18
21
|
export interface HarnessHookOptions {
|
|
19
22
|
harness: Harness;
|
|
20
23
|
guards?: Guard[];
|
|
24
|
+
/** When the bash and MCP gates ask or approve instead of passing. */
|
|
25
|
+
ladder?: Ladder;
|
|
21
26
|
/** Run the Level 7 advisor on UserPromptSubmit and Stop. */
|
|
22
27
|
compact?: boolean;
|
|
23
28
|
lines?: Lines;
|
package/dist/hooks/harness.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LEVEL6_GUARDS, handleHook } from "./handle.js";
|
|
2
2
|
import { readState, stateDir, writeState } from "./state.js";
|
|
3
3
|
import { claudeUsage, codexUsage } from "./transcript.js";
|
|
4
4
|
/** Lines for real sessions, whose system prompt alone passes the lab's demo lines. */
|
|
@@ -40,9 +40,12 @@ function resultText(tool, response) {
|
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
42
|
const deny = (reason) => ({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } });
|
|
43
|
+
const decision = (permissionDecision, reason) => ({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision, permissionDecisionReason: reason } });
|
|
43
44
|
const context = (event, text) => ({ hookSpecificOutput: { hookEventName: event, additionalContext: text } });
|
|
44
45
|
export async function runHarnessHook(event, payload, opts) {
|
|
45
|
-
const ctx = { decide: opts.decide, report: opts.report, guards: opts.guards ??
|
|
46
|
+
const ctx = { decide: opts.decide, report: opts.report, guards: opts.guards ?? LEVEL6_GUARDS, ladder: opts.ladder };
|
|
47
|
+
/** Codex hooks honor deny only; an ask or allow there passes to Codex's own approvals. */
|
|
48
|
+
const pre = (r) => r.block ? deny(r.reason) : r.verdict && opts.harness === "claude" ? decision(r.verdict, r.reason) : null;
|
|
46
49
|
const cwd = String(payload?.cwd || process.cwd());
|
|
47
50
|
const tool = String(payload?.tool_name ?? "");
|
|
48
51
|
const session = String(payload?.session_id ?? "default");
|
|
@@ -50,7 +53,12 @@ export async function runHarnessHook(event, payload, opts) {
|
|
|
50
53
|
case "PreToolUse": {
|
|
51
54
|
if (tool === "Bash") {
|
|
52
55
|
const r = await handleHook({ kind: "shell", tool, command: String(payload?.tool_input?.command ?? ""), cwd, hook: event }, ctx);
|
|
53
|
-
return
|
|
56
|
+
return pre(r);
|
|
57
|
+
}
|
|
58
|
+
if (tool.startsWith("mcp__")) {
|
|
59
|
+
if (tool.startsWith("mcp__jev__"))
|
|
60
|
+
return null; // Jev's own tools only read
|
|
61
|
+
return pre(await handleHook({ kind: "mcp-call", tool, input: payload?.tool_input, cwd, hook: event }, ctx));
|
|
54
62
|
}
|
|
55
63
|
const targets = writeTargets(tool, payload?.tool_input);
|
|
56
64
|
if (!targets)
|
package/dist/setup/index.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export interface SetupOptions {
|
|
|
8
8
|
tools?: string[];
|
|
9
9
|
/** Run the Level 7 advisor hooks. */
|
|
10
10
|
compact?: boolean;
|
|
11
|
+
/** The bash and MCP gates ask the user when Jev is unsure or leans destructive. Claude Code only. */
|
|
12
|
+
ask?: boolean;
|
|
13
|
+
/** The bash and MCP gates approve what Jev is sure only reads. Claude Code only. */
|
|
14
|
+
allowReadOnly?: boolean;
|
|
11
15
|
/** A lab level: 6 means the guards, 7 the advisor and should_i_compact, 8 to 10 their tools. */
|
|
12
16
|
level?: number;
|
|
13
17
|
/** How the harness runs this package's bin. Defaults to node and its absolute path. */
|
|
@@ -35,8 +39,13 @@ export interface Resolved {
|
|
|
35
39
|
guards: Guard[];
|
|
36
40
|
tools: string[];
|
|
37
41
|
compact: boolean;
|
|
42
|
+
ask?: boolean;
|
|
43
|
+
allowReadOnly?: boolean;
|
|
38
44
|
}
|
|
39
|
-
|
|
45
|
+
type Selection = Pick<SetupOptions, "guards" | "tools" | "compact" | "level" | "ask" | "allowReadOnly"> & {
|
|
46
|
+
harness?: SetupHarness;
|
|
47
|
+
};
|
|
48
|
+
export declare function resolveSelection(o: Selection): Resolved;
|
|
40
49
|
interface HookGroup {
|
|
41
50
|
matcher?: string;
|
|
42
51
|
hooks: {
|
|
@@ -61,7 +70,7 @@ export declare function codexBlock(sel: Resolved, command: string[], extra?: str
|
|
|
61
70
|
*/
|
|
62
71
|
export declare function planCodex(o: SetupOptions): FileChange[];
|
|
63
72
|
/** One line for people: what the selection turns on. */
|
|
64
|
-
export declare function describeSelection(o:
|
|
73
|
+
export declare function describeSelection(o: Selection): string;
|
|
65
74
|
export declare function plan(o: SetupOptions): FileChange[];
|
|
66
75
|
export declare function apply(changes: FileChange[]): void;
|
|
67
76
|
export {};
|
package/dist/setup/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { ALL_GUARDS } from "../hooks/handle.js";
|
|
10
|
+
import { ALL_GUARDS, LEVEL6_GUARDS } from "../hooks/handle.js";
|
|
11
11
|
import { LEVEL_TOOLS, selectTools } from "../tools/sets.js";
|
|
12
12
|
export class SetupError extends Error {
|
|
13
13
|
}
|
|
@@ -32,7 +32,7 @@ export function resolveSelection(o) {
|
|
|
32
32
|
let compact = o.compact ?? false;
|
|
33
33
|
if (o.level !== undefined) {
|
|
34
34
|
if (o.level === 6)
|
|
35
|
-
|
|
35
|
+
LEVEL6_GUARDS.forEach((g) => guards.add(g));
|
|
36
36
|
else if (LEVEL_TOOLS[o.level]) {
|
|
37
37
|
LEVEL_TOOLS[o.level].forEach((t) => tools.add(t));
|
|
38
38
|
if (o.level === 7)
|
|
@@ -52,20 +52,30 @@ export function resolveSelection(o) {
|
|
|
52
52
|
}
|
|
53
53
|
if (!guards.size && !tools.size && !compact)
|
|
54
54
|
throw new SetupError("nothing to set up: pass --guards, --tools, --compact, or --level");
|
|
55
|
-
|
|
55
|
+
const ladder = o.ask || o.allowReadOnly;
|
|
56
|
+
if (ladder && !guards.has("bash") && !guards.has("mcp"))
|
|
57
|
+
throw new SetupError("--ask and --allow-read-only apply to the bash and mcp guards; add one of them to --guards");
|
|
58
|
+
if (o.harness === "codex" && (ladder || guards.has("mcp"))) {
|
|
59
|
+
throw new SetupError("Codex hooks can only deny, so the mcp guard, --ask, and --allow-read-only are Claude Code only");
|
|
60
|
+
}
|
|
61
|
+
return { guards: [...guards], tools: [...tools], compact, ...(o.ask ? { ask: true } : {}), ...(o.allowReadOnly ? { allowReadOnly: true } : {}) };
|
|
56
62
|
}
|
|
57
63
|
/** The hook entries, by event, that a selection needs. Same shape for Claude Code and Codex. */
|
|
58
64
|
export function hookGroups(harness, sel, command, extra = [], timeoutSec = 10) {
|
|
59
65
|
const run = (event, flags) => ({ type: "command", command: [...command, "hook", "--harness", harness, "--event", event, ...flags, ...extra].map(quote).join(" "), timeout: timeoutSec });
|
|
60
66
|
const out = {};
|
|
61
67
|
const guardFlag = (gs) => ["--guards", gs.join(",")];
|
|
68
|
+
const ladder = [...(sel.ask ? ["--ask"] : []), ...(sel.allowReadOnly ? ["--allow-read-only"] : [])];
|
|
62
69
|
const pre = [];
|
|
63
70
|
if (sel.guards.includes("bash"))
|
|
64
71
|
pre.push("Bash");
|
|
65
72
|
if (sel.guards.includes("write"))
|
|
66
73
|
pre.push(...(harness === "codex" ? ["apply_patch"] : ["Write", "Edit", "MultiEdit", "NotebookEdit"]));
|
|
74
|
+
const preGuards = sel.guards.filter((g) => g === "bash" || g === "write");
|
|
67
75
|
if (pre.length)
|
|
68
|
-
out.PreToolUse = [{ matcher: pre.join("|"), hooks: [run("PreToolUse", guardFlag(sel.guards.
|
|
76
|
+
out.PreToolUse = [{ matcher: pre.join("|"), hooks: [run("PreToolUse", [...guardFlag(preGuards), ...(sel.guards.includes("bash") ? ladder : [])])] }];
|
|
77
|
+
if (sel.guards.includes("mcp"))
|
|
78
|
+
(out.PreToolUse ??= []).push({ matcher: "mcp__.*", hooks: [run("PreToolUse", [...guardFlag(["mcp"]), ...ladder])] });
|
|
69
79
|
if (sel.guards.includes("result"))
|
|
70
80
|
out.PostToolUse = [{ matcher: harness === "codex" ? "Bash" : "Read|Bash", hooks: [run("PostToolUse", guardFlag(["result"]))] }];
|
|
71
81
|
if (sel.compact) {
|
|
@@ -191,6 +201,8 @@ export function describeSelection(o) {
|
|
|
191
201
|
const sel = resolveSelection(o);
|
|
192
202
|
const parts = [
|
|
193
203
|
sel.guards.length ? `guards ${sel.guards.join(",")}` : "",
|
|
204
|
+
sel.ask ? "ask when unsure" : "",
|
|
205
|
+
sel.allowReadOnly ? "approve read only" : "",
|
|
194
206
|
sel.compact ? "compaction advisor" : "",
|
|
195
207
|
sel.tools.length ? `tools ${sel.tools.join(",")}` : "",
|
|
196
208
|
].filter(Boolean);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobobowis/jev-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Jev, TypeSafe AI's System One decision model, as a typed client, guards, file reads, agent tools, hooks, an MCP server, and a CLI. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|