jevgate 0.1.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 +27 -0
- package/jevgate.js +193 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# jevgate
|
|
2
|
+
|
|
3
|
+
Your agent stops to ask permission. jevgate answers in ~300 ms — with a
|
|
4
|
+
probability, a risk score, and an audit log. It escalates what it isn't sure
|
|
5
|
+
about. **It never denies.**
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx -y jevgate init # writes the PermissionRequest hook for Codex and Claude Code
|
|
9
|
+
jevgate login <token> # free token at https://jevgate.dev
|
|
10
|
+
jevgate status # plan, usage, endpoint
|
|
11
|
+
jevgate log # your last decisions, locally
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Codex runs a new hook only after you trust it once — open codex and accept the
|
|
15
|
+
jevgate hook when it asks. Claude Code only prompts outside auto mode; jevgate
|
|
16
|
+
answers those prompts.
|
|
17
|
+
|
|
18
|
+
How a decision is made: the request (the command, the task you gave the agent,
|
|
19
|
+
the working directory) goes to jevgate's endpoint, which asks Jev — TypeSafe's
|
|
20
|
+
decision-only model — four typed questions in one ~300 ms call: should this be
|
|
21
|
+
allowed, how risky is it (four levels), how clearly did you authorise it, and
|
|
22
|
+
what kind of action it is. jevgate allows only when risk is low with high
|
|
23
|
+
confidence and the action isn't an install or anything destructive. Anything
|
|
24
|
+
else, and any error or timeout, returns nothing — your harness then does
|
|
25
|
+
exactly what it does today.
|
|
26
|
+
|
|
27
|
+
Powered by Jev (TypeSafe). Not affiliated with TypeSafe, OpenAI, or Anthropic.
|
package/jevgate.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// jevgate — one binary, zero dependencies.
|
|
3
|
+
// jevgate init write the hooks into Claude Code and codex
|
|
4
|
+
// jevgate login store your token
|
|
5
|
+
// jevgate decide (called by the harness) stdin JSON → decision JSON or nothing
|
|
6
|
+
// jevgate status endpoint, token, usage
|
|
7
|
+
// jevgate log last decisions
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
const HOME = os.homedir();
|
|
13
|
+
const DIR = path.join(HOME, ".jevgate");
|
|
14
|
+
const CONFIG = path.join(DIR, "config.json");
|
|
15
|
+
const LOG = path.join(DIR, "decisions.jsonl");
|
|
16
|
+
const endpoint = () => process.env.JEVGATE_ENDPOINT || config().endpoint || "https://jevgate.dev";
|
|
17
|
+
const TIMEOUT_MS = Number(process.env.JEVGATE_TIMEOUT_MS || 2500);
|
|
18
|
+
// The hook runs on every permission request, so it must not pay npx's resolver
|
|
19
|
+
// each time. Write the absolute path of this very script: it is stable whether
|
|
20
|
+
// we were installed globally (npm i -g jevgate) or are running from npx's cache.
|
|
21
|
+
const SELF = new URL(import.meta.url).pathname;
|
|
22
|
+
const HOOK_CMD = `node "${SELF}" decide`;
|
|
23
|
+
|
|
24
|
+
const readJson = (p, fallback) => { try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return fallback; } };
|
|
25
|
+
const writeJson = (p, v) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(v, null, 2) + "\n"); };
|
|
26
|
+
const config = () => readJson(CONFIG, {});
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------- decide
|
|
29
|
+
async function decide() {
|
|
30
|
+
let input = "";
|
|
31
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
32
|
+
if (process.env.JEVGATE_DEBUG) { try { fs.mkdirSync(DIR, { recursive: true }); fs.appendFileSync(path.join(DIR, "raw-events.jsonl"), input.trim() + "\n"); } catch {} }
|
|
33
|
+
let event;
|
|
34
|
+
try { event = JSON.parse(input); } catch { appendLog({ ts: new Date().toISOString(), outcome: "bad_stdin", bytes: input.length, head: input.slice(0, 120) }); return; }
|
|
35
|
+
const cfg = config();
|
|
36
|
+
const token = process.env.JEVGATE_TOKEN || cfg.token;
|
|
37
|
+
if (!token) { appendLog({ ...meta(event, Date.now()), outcome: "no_token" }); return; } // not logged in → harness proceeds as usual
|
|
38
|
+
const started = Date.now();
|
|
39
|
+
const body = {
|
|
40
|
+
harness: detectHarness(event),
|
|
41
|
+
event: event.hook_event_name,
|
|
42
|
+
tool_name: event.tool_name,
|
|
43
|
+
tool_input: event.tool_input,
|
|
44
|
+
cwd: event.cwd,
|
|
45
|
+
permission_mode: event.permission_mode,
|
|
46
|
+
task: await recentUserPrompt(event.transcript_path) ?? codexTask(event.session_id),
|
|
47
|
+
};
|
|
48
|
+
const ctrl = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${endpoint()}/api/v1/decide`, {
|
|
53
|
+
method: "POST", signal: ctrl.signal,
|
|
54
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) { appendLog({ ...meta(event, started), outcome: "error", status: res.status }); return; }
|
|
58
|
+
result = await res.json();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
appendLog({ ...meta(event, started), outcome: "error", error: String(err?.name || err) });
|
|
61
|
+
return; // any failure → harness proceeds as usual
|
|
62
|
+
} finally { clearTimeout(timer); }
|
|
63
|
+
|
|
64
|
+
appendLog({ ...meta(event, started), ...result });
|
|
65
|
+
if (result.decision !== "allow") return; // ask/uncertain → say nothing, never deny
|
|
66
|
+
const reason = `jevgate: allow (p=${result.p_allow}, risk=${result.risk}) — ${result.kind}`;
|
|
67
|
+
const out = event.hook_event_name === "PermissionRequest"
|
|
68
|
+
? { hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "allow", message: reason } } }
|
|
69
|
+
: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", permissionDecisionReason: reason } };
|
|
70
|
+
process.stdout.write(JSON.stringify(out));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const meta = (event, started) => ({
|
|
74
|
+
ts: new Date().toISOString(), harness: detectHarness(event), event: event.hook_event_name,
|
|
75
|
+
tool: event.tool_name, cmd: summarize(event.tool_input), latency_ms: Date.now() - started,
|
|
76
|
+
});
|
|
77
|
+
const detectHarness = (e) => (e.session_id && e.turn_id) ? "codex" : "claude-code";
|
|
78
|
+
const summarize = (ti) => { if (!ti) return ""; const c = ti.command || ti.cmd || ti.file_path || ti.path || ""; return String(Array.isArray(c) ? c.join(" ") : c).slice(0, 160); };
|
|
79
|
+
function appendLog(rec) { try { fs.mkdirSync(DIR, { recursive: true }); fs.appendFileSync(LOG, JSON.stringify(rec) + "\n"); } catch {} }
|
|
80
|
+
|
|
81
|
+
// The latest user message: what the agent is trying to do. Read cheaply from the
|
|
82
|
+
// transcript when the harness gives one; Jev needs the task to judge the action.
|
|
83
|
+
async function recentUserPrompt(transcriptPath) {
|
|
84
|
+
if (!transcriptPath) return null;
|
|
85
|
+
try {
|
|
86
|
+
const text = fs.readFileSync(transcriptPath, "utf8");
|
|
87
|
+
const lines = text.split("\n").filter(Boolean).slice(-400);
|
|
88
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
89
|
+
let o; try { o = JSON.parse(lines[i]); } catch { continue; }
|
|
90
|
+
const m = o.message || o;
|
|
91
|
+
if ((o.type === "user" || m.role === "user") && m.content) {
|
|
92
|
+
const c = Array.isArray(m.content) ? m.content.map(x => x.text || "").join("\n") : String(m.content);
|
|
93
|
+
if (c.trim() && !c.startsWith("<")) return c.slice(0, 1500);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// codex's PermissionRequest payload carries no transcript, but the session
|
|
101
|
+
// rollout is on disk: ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<session_id>.jsonl.
|
|
102
|
+
// The latest real user message (harness-injected ones start with "<") is the task.
|
|
103
|
+
function codexTask(sessionId) {
|
|
104
|
+
if (!sessionId) return null;
|
|
105
|
+
try {
|
|
106
|
+
const root = path.join(HOME, ".codex", "sessions");
|
|
107
|
+
const day = (d) => path.join(root, String(d.getUTCFullYear()), String(d.getUTCMonth() + 1).padStart(2, "0"), String(d.getUTCDate()).padStart(2, "0"));
|
|
108
|
+
let file = null;
|
|
109
|
+
for (let back = 0; back < 3 && !file; back++) {
|
|
110
|
+
const dir = day(new Date(Date.now() - back * 864e5));
|
|
111
|
+
if (!fs.existsSync(dir)) continue;
|
|
112
|
+
const hit = fs.readdirSync(dir).find((f) => f.endsWith(`${sessionId}.jsonl`));
|
|
113
|
+
if (hit) file = path.join(dir, hit);
|
|
114
|
+
}
|
|
115
|
+
if (!file) return null;
|
|
116
|
+
const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-600);
|
|
117
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
118
|
+
let o; try { o = JSON.parse(lines[i]); } catch { continue; }
|
|
119
|
+
const pl = o.payload;
|
|
120
|
+
if (o.type !== "response_item" || !pl || pl.type !== "message" || pl.role !== "user") continue;
|
|
121
|
+
const text = (pl.content || []).filter((c) => c.type === "input_text").map((c) => c.text).join("\n").trim();
|
|
122
|
+
if (text && !text.startsWith("<")) return text.slice(0, 1500);
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------- init
|
|
129
|
+
function init() {
|
|
130
|
+
const results = [];
|
|
131
|
+
// Claude Code: PermissionRequest fires exactly when a permission prompt would
|
|
132
|
+
// appear. Allowing there skips the prompt; saying nothing shows it as usual.
|
|
133
|
+
const claude = path.join(HOME, ".claude", "settings.json");
|
|
134
|
+
if (fs.existsSync(path.dirname(claude))) {
|
|
135
|
+
const s = readJson(claude, {});
|
|
136
|
+
s.hooks ||= {};
|
|
137
|
+
results.push(addHook(s.hooks, "PermissionRequest", "*", 5) ? "claude-code: PermissionRequest hook added" : "claude-code: already installed");
|
|
138
|
+
writeJson(claude, s);
|
|
139
|
+
} else results.push("claude-code: not found (~/.claude)");
|
|
140
|
+
// codex: PermissionRequest fires only when codex would ask a human or Guardian.
|
|
141
|
+
const codex = path.join(HOME, ".codex", "hooks.json");
|
|
142
|
+
if (fs.existsSync(path.dirname(codex))) {
|
|
143
|
+
const h = readJson(codex, { hooks: {} });
|
|
144
|
+
h.hooks ||= {};
|
|
145
|
+
const added = addHook(h.hooks, "PermissionRequest", undefined, 5);
|
|
146
|
+
results.push(added ? "codex: PermissionRequest hook added" : "codex: already installed");
|
|
147
|
+
writeJson(codex, h);
|
|
148
|
+
// codex runs a hook only after you trust it once. It asks on the next start.
|
|
149
|
+
if (added) results.push("codex: open codex once and accept the jevgate hook when it asks (it verifies new hooks)");
|
|
150
|
+
} else results.push("codex: not found (~/.codex)");
|
|
151
|
+
for (const r of results) console.log(" " + r);
|
|
152
|
+
if (!config().token) console.log("\n next: jevgate login <token> (free at https://jevgate.dev)");
|
|
153
|
+
}
|
|
154
|
+
const isOurs = (h) => /jevgate(\.js)?"? decide\b/.test(h?.command || "");
|
|
155
|
+
// Idempotent: one jevgate group per event, always pointing at this script.
|
|
156
|
+
function addHook(hooks, eventName, matcher, timeout) {
|
|
157
|
+
hooks[eventName] ||= [];
|
|
158
|
+
const before = hooks[eventName].length;
|
|
159
|
+
hooks[eventName] = hooks[eventName].filter(g => !(g.hooks || []).some(isOurs));
|
|
160
|
+
const group = { hooks: [{ type: "command", command: HOOK_CMD, timeout }] };
|
|
161
|
+
if (matcher) group.matcher = matcher;
|
|
162
|
+
hooks[eventName].push(group);
|
|
163
|
+
return hooks[eventName].length > before; // true only when nothing of ours was there
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------- misc
|
|
167
|
+
function login(token) {
|
|
168
|
+
if (!token) { console.error("usage: jevgate login <token>"); process.exit(1); }
|
|
169
|
+
writeJson(CONFIG, { ...config(), token, endpoint: endpoint() });
|
|
170
|
+
console.log(" token saved to ~/.jevgate/config.json");
|
|
171
|
+
}
|
|
172
|
+
async function status() {
|
|
173
|
+
const cfg = config();
|
|
174
|
+
console.log(` endpoint ${endpoint()}`);
|
|
175
|
+
console.log(` token ${cfg.token ? cfg.token.slice(0, 8) + "…" : "(none — run: jevgate login <token>)"}`);
|
|
176
|
+
const n = fs.existsSync(LOG) ? fs.readFileSync(LOG, "utf8").split("\n").filter(Boolean) : [];
|
|
177
|
+
const allowed = n.filter(l => l.includes('"decision":"allow"')).length;
|
|
178
|
+
console.log(` decisions ${n.length} logged locally, ${allowed} allowed by jevgate`);
|
|
179
|
+
if (cfg.token) {
|
|
180
|
+
try {
|
|
181
|
+
const r = await fetch(`${endpoint()}/api/v1/me`, { headers: { authorization: `Bearer ${cfg.token}` } });
|
|
182
|
+
if (r.ok) { const me = await r.json(); console.log(` plan ${me.plan} ${me.used}/${me.limit === null ? "∞" : me.limit} this month`); }
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function log(n = 20) {
|
|
187
|
+
const lines = fs.existsSync(LOG) ? fs.readFileSync(LOG, "utf8").split("\n").filter(Boolean).slice(-n) : [];
|
|
188
|
+
for (const l of lines) { const r = JSON.parse(l); console.log(` ${r.ts.slice(11, 19)} ${(r.decision || r.outcome || "-").padEnd(6)} ${String(r.latency_ms).padStart(5)}ms ${r.harness.padEnd(11)} ${r.cmd}`); }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const [cmd, arg] = process.argv.slice(2);
|
|
192
|
+
({ decide, init, login: () => login(arg), status, log: () => log(Number(arg) || 20) }[cmd] ||
|
|
193
|
+
(() => console.log("usage: jevgate <init|login <token>|decide|status|log>")))();
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jevgate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Auto-approve the benign majority of agent tool calls in ~300ms, with a probability and an audit log. Escalates the rest. Never denies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codex",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"agent",
|
|
9
|
+
"permissions",
|
|
10
|
+
"hooks",
|
|
11
|
+
"approval",
|
|
12
|
+
"jev",
|
|
13
|
+
"typesafe"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://jevgate.dev",
|
|
16
|
+
"author": "remotehost <matt@remotehost.ai> (https://remotehost.ai)",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"jevgate": "./jevgate.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"jevgate.js",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|