yenop 0.0.0 → 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/LICENSE.md +105 -0
- package/README.md +243 -3
- package/dist/adapters/claude-code/hook.d.ts +43 -0
- package/dist/adapters/claude-code/hook.js +101 -0
- package/dist/adapters/claude-code/hook.js.map +1 -0
- package/dist/adapters/claude-code/install.d.ts +23 -0
- package/dist/adapters/claude-code/install.js +86 -0
- package/dist/adapters/claude-code/install.js.map +1 -0
- package/dist/adapters/codex/hook.d.ts +7 -0
- package/dist/adapters/codex/hook.js +134 -0
- package/dist/adapters/codex/hook.js.map +1 -0
- package/dist/adapters/codex/install.d.ts +18 -0
- package/dist/adapters/codex/install.js +66 -0
- package/dist/adapters/codex/install.js.map +1 -0
- package/dist/adapters/cursor/hook.d.ts +25 -0
- package/dist/adapters/cursor/hook.js +228 -0
- package/dist/adapters/cursor/hook.js.map +1 -0
- package/dist/adapters/cursor/install.d.ts +10 -0
- package/dist/adapters/cursor/install.js +57 -0
- package/dist/adapters/cursor/install.js.map +1 -0
- package/dist/adapters/gemini/hook.d.ts +20 -0
- package/dist/adapters/gemini/hook.js +232 -0
- package/dist/adapters/gemini/hook.js.map +1 -0
- package/dist/adapters/gemini/install.d.ts +21 -0
- package/dist/adapters/gemini/install.js +101 -0
- package/dist/adapters/gemini/install.js.map +1 -0
- package/dist/adapters/hooks/pipeline.d.ts +69 -0
- package/dist/adapters/hooks/pipeline.js +131 -0
- package/dist/adapters/hooks/pipeline.js.map +1 -0
- package/dist/adapters/hooks/registry.d.ts +8 -0
- package/dist/adapters/hooks/registry.js +14 -0
- package/dist/adapters/hooks/registry.js.map +1 -0
- package/dist/adapters/mcp/gateway.d.ts +64 -0
- package/dist/adapters/mcp/gateway.js +205 -0
- package/dist/adapters/mcp/gateway.js.map +1 -0
- package/dist/adapters/mcp/run.d.ts +12 -0
- package/dist/adapters/mcp/run.js +86 -0
- package/dist/adapters/mcp/run.js.map +1 -0
- package/dist/adapters/openai-agents/index.d.ts +74 -0
- package/dist/adapters/openai-agents/index.js +139 -0
- package/dist/adapters/openai-agents/index.js.map +1 -0
- package/dist/cli/demo.d.ts +5 -0
- package/dist/cli/demo.js +130 -0
- package/dist/cli/demo.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +746 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core/config.d.ts +50 -0
- package/dist/core/config.js +93 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/engine.d.ts +31 -0
- package/dist/core/engine.js +301 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/ids.d.ts +5 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +40 -0
- package/dist/core/index.js +73 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/policy.d.ts +41 -0
- package/dist/core/policy.js +205 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/receipts.d.ts +47 -0
- package/dist/core/receipts.js +137 -0
- package/dist/core/receipts.js.map +1 -0
- package/dist/core/report.d.ts +51 -0
- package/dist/core/report.js +125 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/shell.d.ts +105 -0
- package/dist/core/shell.js +921 -0
- package/dist/core/shell.js.map +1 -0
- package/dist/core/state.d.ts +52 -0
- package/dist/core/state.js +269 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/telemetry.d.ts +52 -0
- package/dist/core/telemetry.js +150 -0
- package/dist/core/telemetry.js.map +1 -0
- package/dist/core/tools.d.ts +5 -0
- package/dist/core/tools.js +59 -0
- package/dist/core/tools.js.map +1 -0
- package/dist/core/types.d.ts +213 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/daemon/client.d.ts +34 -0
- package/dist/daemon/client.js +138 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/fast.d.ts +9 -0
- package/dist/daemon/fast.js +43 -0
- package/dist/daemon/fast.js.map +1 -0
- package/dist/daemon/server.d.ts +38 -0
- package/dist/daemon/server.js +321 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/service.d.ts +26 -0
- package/dist/daemon/service.js +171 -0
- package/dist/daemon/service.js.map +1 -0
- package/dist/viewer/server.d.ts +52 -0
- package/dist/viewer/server.js +184 -0
- package/dist/viewer/server.js.map +1 -0
- package/package.json +59 -7
- package/policies/README.md +58 -0
- package/policies/approve/default.cedar +72 -0
- package/policies/permit/default.cedar +48 -0
- package/policies/schema.cedarschema +104 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Ertunç Varol
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,9 +1,249 @@
|
|
|
1
1
|
# Yenop
|
|
2
2
|
|
|
3
|
-
The layer
|
|
3
|
+
**The local-first execution authorization layer for AI agents. One set of rules across the agents you run, and your agents' actions stay on your machine.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An AI agent decides what it wants to do. Yenop decides what is allowed to happen. It sits between the agent and the systems it can touch, and on every action it returns one of three answers:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**ALLOW** · **ASK** a person · **DENY**
|
|
8
|
+
|
|
9
|
+
- **One layer across the agents you run.** Claude Code, Cursor, Codex CLI, Gemini CLI, agents built on the OpenAI Agents SDK, and any tool that speaks MCP, judged by the same policies and written to one verifiable record. No single agent vendor will ever govern its competitors; a layer outside all of them can.
|
|
10
|
+
- **It authorizes the action, not the reasoning.** Yenop does not decide whether an agent's thinking is safe. It decides whether the action the agent is about to execute is allowed, reading the actual command and file path, so there is no sentence to jailbreak. And it watches the whole run, so a harmful plan split across steps that each look harmless is still caught.
|
|
11
|
+
- **Local-first.** It runs next to the agent, on your machine. No cloud dependency, no account, and no agent data, source, prompts or secrets leaves the machine unless you choose to export the audit trail. That trail is a plain file you own, hash-chained so tampering is detectable.
|
|
12
|
+
|
|
13
|
+
Status: **pre-alpha, developer preview** (0.1.0). **Source-available, not open source** (see License). Works today with Claude Code, Cursor, Codex CLI and Gemini CLI hooks, the OpenAI Agents SDK, and an MCP gateway for any MCP client, each verified in a live session against the current release of that runtime. The set of runtimes is frozen for this preview; the next one is added when people using Yenop ask for it.
|
|
14
|
+
|
|
15
|
+
## See it in thirty seconds
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
yenop demo
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A scripted walk through what Yenop does, with no AI agent and nothing real touched: safe work passes silently, a secret-exfiltration attempt is blocked, a multi-step plan is caught by the run's history, a database dump is stopped at the MCP gateway, and Yenop refuses to let the agent switch it off. Every verdict is the real engine.
|
|
22
|
+
|
|
23
|
+
## Why this exists
|
|
24
|
+
|
|
25
|
+
Companies are giving agents real access: the shell, the repository, the database, the cloud. An agent is helpful, but it does what it is told, including by a web page or a document carrying a hidden instruction. And the newest models reason in ways you cannot read or log, so you cannot audit the thinking. You can only govern the actions. That is the one place left to put a control, and it is where Yenop sits.
|
|
26
|
+
|
|
27
|
+
Yenop's claim is deliberately narrow and provable: **a model's decision is never the final authorization decision.** Whatever the agent was convinced to attempt, the deed still has to pass a deterministic rule, and a person still signs off on the irreversible ones.
|
|
28
|
+
|
|
29
|
+
## Why another layer?
|
|
30
|
+
|
|
31
|
+
Your agent already has permissions. Cursor has run modes. Codex has a sandbox. MCP servers describe their own tools. Cloud platforms have IAM. So why install one more thing?
|
|
32
|
+
|
|
33
|
+
Because each of those controls part of the execution path, and each is owned by the party it governs. A runtime's permission system changes with its next release. A tool server's "read-only" hint is the server's word. A model's judgement is the thing being attacked. None of them is independent of the agent, and none of them is the same across the three agents your team runs on the same repository.
|
|
34
|
+
|
|
35
|
+
Yenop adds an independent enforcement boundary, outside the agent, that stays the same when the runtime, the model, or the tool server changes. One policy, one record, whichever agent acted. It does not replace those controls; it is the layer that is still there when they differ, update, or are talked around.
|
|
36
|
+
|
|
37
|
+
## Who this is for
|
|
38
|
+
|
|
39
|
+
Developers and security-minded teams giving coding agents, MCP tools, and autonomous workflows real access: a repository, credentials, a database, infrastructure. If an agent on your machine can reach something you would not want it to touch unasked, Yenop is for you.
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
From npm, once published:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install -g yenop
|
|
47
|
+
yenop init # sets up ~/.yenop and installs the hook for every agent it detects
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
From source, today:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install && npm run build && npm link
|
|
54
|
+
yenop init # detects Claude Code, Cursor, Codex, Gemini; installs each hook
|
|
55
|
+
yenop status # mode, layers, daemon, and which agents are covered
|
|
56
|
+
yenop explain Bash '{"command":"terraform destroy -auto-approve"}'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
From then on, every action an agent takes in this project passes through Yenop first:
|
|
60
|
+
|
|
61
|
+
| The agent tries | Yenop says |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `npm test`, editing a source file, reading the README | nothing; the agent's normal flow applies |
|
|
64
|
+
| `terraform destroy`, `DROP TABLE`, a forced push, writing outside the project, an MCP tool that writes | **ask**: the agent shows the exact action and waits for a person |
|
|
65
|
+
| a scanner, exploitation framework, credential cracker, or a reverse shell (`nmap`, `sqlmap`, `hashcat`, `bash -i >& /dev/tcp/...`) | **ask**: dual-use, so a person signs off |
|
|
66
|
+
| reading a private key or `.env`, piping a download into a shell, sending a credential over the network | **deny**: blocked, with the policy named |
|
|
67
|
+
| inline code handed to an interpreter (`python -c`, `eval`) that touches a protected file | **ask** or **deny**: opaque code is judged by what it names |
|
|
68
|
+
| a run that has been denied 20 times, or has made 1,000 calls | **deny**: the breaker halts the run |
|
|
69
|
+
|
|
70
|
+
Yenop only ever tightens. It never grants something the agent would have asked about on its own.
|
|
71
|
+
|
|
72
|
+
## Watch it happen
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
yenop viewer --open
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A read-only web page of the receipts, on this machine only, updating every couple of seconds: green allowed, amber needs a person, red blocked, each with the reason and the recorded answer. Off the decision path, writes nothing. Put it beside your agent during a demo.
|
|
79
|
+
|
|
80
|
+
Roll it out without breaking anyone's flow: **observe mode** records decisions without blocking. Run it for a week, read what would have been stopped, tune the policies, then switch to enforce.
|
|
81
|
+
|
|
82
|
+
## Read the week
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
yenop report # last 7 days, this project
|
|
86
|
+
yenop report --days 30 --all
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Counts by verdict, by rule, by runtime and by kind of action, from the receipts on this machine. The line that matters is the **ask approval rate**: of the calls held for a person, how many the person then allowed. Near 100 % means a rule asks too often; near 0 % means the asks were real catches. That is how the baseline gets tuned by evidence rather than opinion. `yenop feedback` opens the discussion board for the things a number cannot say.
|
|
90
|
+
|
|
91
|
+
**Telemetry is off by default and stays off unless you turn it on.** `yenop telemetry enable` sends the report's numbers once a day; `yenop telemetry status` prints the exact payload; `disable` stops it; `reset` issues a new random install id. What is sent is an allow-list, built in [`src/core/telemetry.ts`](src/core/telemetry.ts): counts by verdict, rule id, runtime and tool kind, the Yenop version, the operating system name and major version, which runtimes are hooked, and a random install id. Never a command, a path, a prompt, source, a hostname, a username, a project or tenant name, a receipt, or an MCP argument. A field not listed there does not leave the machine.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Every agent, one engine
|
|
96
|
+
|
|
97
|
+
Yenop turns each runtime's event into one canonical action, so the engine, the policies, the run state and the receipts never know which agent produced it. A rule written once applies to all of them.
|
|
98
|
+
|
|
99
|
+
**Claude Code.** A PreToolUse hook decides before each tool runs, and later events record whether the call ran, failed, or was refused. Silent on allow, a prompt on ask, a block on deny.
|
|
100
|
+
|
|
101
|
+
**Cursor.** `yenop init` writes `.cursor/hooks.json`. Cursor sends every tool call through `preToolUse` first, then through a specialised hook for shell and MCP calls that can ask a person, so Yenop judges everything at the first gate and lets an ask through to the second, where Cursor shows it as "Pending approval… Hook requested approval: Yenop: …", even in its Run Everything mode. File edits can only be allowed or denied there, so an edit that needs a person is blocked with a message rather than let through. Every hook is fail-closed, so a crash or timeout blocks. Verified in a live session against Cursor 3.21.18: allow, ask, deny on a secret read, and an attempt to edit its own hook file refused. Two facts about Cursor itself: its documentation describes the specialised hooks as the gates, but `preToolUse` fires first and a hook that stays silent there blocks the call, which is how the first version of this adapter failed, safely; and when a person clicks Skip on an approval, Cursor reports the command to hooks exactly as if it had run with no output, so on Cursor "approved, ran" in a receipt means "Cursor reported it complete", nothing more.
|
|
102
|
+
|
|
103
|
+
**Codex CLI.** `yenop init` writes `.codex/hooks.json`. Shell commands, `apply_patch` edits and MCP calls are judged, and a patch touching several files is judged by the strictest of them. Two facts about Codex itself: it has no way to ask a person from a hook, so a step that needs one is blocked with a message; and it ignores a hook that crashes or times out. Be precise about whose guarantee is whose: **Yenop's boundary is fail-closed**, a decision it cannot make is a deny; **Codex's runtime is fail-open**, it proceeds if the hook itself does not answer. Yenop answers with the one signal Codex always honours and is built to answer every time, but it cannot change Codex's design, and we say so rather than imply otherwise. Proven against a live model that tried to route around a blocked edit through an interpreter, and was stopped.
|
|
104
|
+
|
|
105
|
+
**Gemini CLI.** `yenop init` writes the hooks into `.gemini/settings.json`. Every built-in tool (shell, file reads and writes, `read_many_files`, `save_memory`, web fetch and search) and every MCP tool is judged before it runs, and Gemini honours all three answers: allow silently, **ask** by forcing its own confirmation prompt even in an auto-approve mode, deny with the reason handed to the model. Two facts about Gemini itself, read from its source rather than its docs: a hook that times out or fails to start is ignored, so Gemini's runtime is fail-open like Codex's, and Yenop answers with JSON on exit 0 for every verdict including its own failure, the one path Gemini honours without ambiguity; and Gemini skips a project's hooks in a folder it does not trust and shows new hooks for review, so `yenop status` reads both switches and says when nothing is enforced. Verified in a live session against Gemini CLI 0.60.0 with auto-approve on and no sandbox: allow silent, deny with the reason, ask shown as Gemini's own dialog with the run's history in it, and the ask comes back even after a person picked "Allow for this session", so Gemini's convenience switch never bypasses Yenop. The recorded events are the fixtures. One honest note: the model itself refused to read `.env` before any tool call; Yenop's deny was exercised on a `.npmrc`. A model's refusal is a mood, not a boundary, which is the point.
|
|
106
|
+
|
|
107
|
+
**OpenAI Agents SDK.** An agent you build with `@openai/agents` runs in your own process, so Yenop runs there too, as a pair of tool guardrails: the input guardrail decides, the output guardrail records what happened.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { tool } from "@openai/agents";
|
|
111
|
+
import { yenopGuardrails } from "yenop/openai-agents";
|
|
112
|
+
|
|
113
|
+
const guard = yenopGuardrails({
|
|
114
|
+
tools: { run_shell: { kind: "shell" }, write_file: { kind: "write" }, fetch_url: { kind: "web", readOnly: true } },
|
|
115
|
+
onAsk: async (decision) => askInTerminal(decision.message), // or Slack, or your approval UI
|
|
116
|
+
});
|
|
117
|
+
const runShell = tool({ name: "run_shell", /* ... */ inputGuardrails: guard.inputGuardrails, outputGuardrails: guard.outputGuardrails });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Your tools are your own functions, so `tools` says what each can do; one you leave out is judged as unknown and held for a person. A guardrail cannot ask anyone by itself: `onAsk` is where you route an ask, and without it an ask is refused, never let through. A deny reaches the agent as a rejection with the reason, so it can do something else, or as the SDK's tripwire exception with `onDeny: "throw"`. One SDK run is one Yenop run, so the run-level rules follow the agent across its calls. No dependency on the SDK: the types are structural, checked against `@openai/agents-core` 0.18.0, and verified with the real SDK: [`examples/openai-agents/`](examples/openai-agents/) is a forty-line agent whose `npm test` ran, whose `.npmrc` read was refused with the reason handed back to the model, and whose `rm -rf build` waited on a person in the terminal.
|
|
121
|
+
|
|
122
|
+
**Any MCP agent: the gateway.** Point the client at Yenop instead of at the tool server:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{ "mcpServers": {
|
|
126
|
+
"supabase": { "command": "yenop", "args": ["mcp", "--server", "supabase", "--", "npx", "-y", "@supabase/mcp-server"] }
|
|
127
|
+
} }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Yenop launches the real server, forwards every message, and runs a decision on each `tools/call`. Allowed calls go through untouched; blocked ones never reach the server, and the client gets a tool error with the reason. This is where the trusted-backend attacks land: an injected instruction telling the agent to dump a database through a Supabase server, or exfiltrate a repo through a GitHub server. The server would obey. The gateway does not. When a call needs a person, the gateway asks through the client using MCP elicitation, speaking both protocol generations; a client that cannot show forms gets `--on-ask block` or `--on-ask allow`. Under Claude Code the hook already gates MCP calls, so run the gateway there with `--on-ask allow`; it earns its keep with clients that have no hook.
|
|
131
|
+
|
|
132
|
+
## How it decides
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
action → budget breakers → permit policies → approval policies → receipt → allow / ask / deny
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Shell commands are parsed, not pattern-matched.** Yenop splits a command into programs and arguments, follows `sudo`, `xargs`, `bash -c`, `$(...)`, variables and redirects, ignores heredoc bodies and comments, and exposes facts like `context.shell.destructive`, `context.shell.secretPath`, `context.shell.opaque` and `context.shell.ops`. A README that mentions `rm -rf` is a write to a README; a public key is not a secret. Code handed to an interpreter is not read as shell but treated as opaque and judged by what it names, so obfuscation becomes the reason a person is asked. This is checked by property tests that generate about fourteen thousand disguised commands on every push, and by an adversarial harness that lets a real model try to find a route.
|
|
139
|
+
|
|
140
|
+
**Decisions look at the run, not only at the call.** A model that plans where nobody can see can split a harmful plan into steps that each look harmless: read a forum page, dump the environment, fetch a URL. Every run remembers two sticky facts, whether it has taken in **untrusted content** and whether it has touched **sensitive data**, and every call is described by its flow: does it reach an external host, send data out, change state. The baseline asks a person before any external network call once sensitive data is in play, and names the shape, untrusted input plus sensitive data plus a way out, as `lethal-trifecta` in the receipt. A run left idle expires, so a crashed session never taints a later one.
|
|
141
|
+
|
|
142
|
+
**An ask carries the run's history.** The person deciding cannot see the model's reasoning, so the prompt says which earlier steps made the run untrusted or sensitive and what it did just before. The receipt then records the answer, and whether the call went on to run or fail.
|
|
143
|
+
|
|
144
|
+
**Yenop protects itself.** An agent told "no" may be told, by whoever is manipulating it, to change the rules. So any change to Yenop's own files needs a person: the `.yenop` folders and policies, the mode, the daemon, and the hook registration files of every agent. Looking is free.
|
|
145
|
+
|
|
146
|
+
## Policies
|
|
147
|
+
|
|
148
|
+
Policies come in layers evaluated together: the **baseline** shipped in [`policies/`](policies/) and updated with each release, your **home** layer in `~/.yenop/policies/`, and a **project** layer committed with the code. Add a file to tighten. To loosen a baseline rule, switch it off by id with `"disabledPolicies": ["..."]`; the shipped files are never edited in place. Two folders, two questions: `permit/` asks "may this happen at all", `approve/` asks "must a person see it first". The vocabulary is frozen in [`policies/schema.cedarschema`](policies/schema.cedarschema) and every layer is validated against it on load. See [policies/README.md](policies/README.md).
|
|
149
|
+
|
|
150
|
+
**Fail closed.** Any evaluation error is a deny. A policy file that does not parse or does not match the vocabulary makes Yenop refuse every action in that project, naming the file and error, while keeping `yenop check`, `status` and `receipts` working so a person can fix it. A guard that cannot read its rules must not wave things through.
|
|
151
|
+
|
|
152
|
+
## Tamper-evident receipts
|
|
153
|
+
|
|
154
|
+
The receipts file is a hash chain: each line carries the hash of the line before it, so editing, deleting, or reordering any line is detectable.
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
yenop receipts --verify
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
reports an unbroken chain, or names the first broken line and exits non-zero. This turns the log from "trust us" into evidence a third party can check. It is local tamper-evidence, not a signature: it catches changes to the recorded history; anchoring the head against a wholesale rewrite is the control plane's job.
|
|
161
|
+
|
|
162
|
+
## What it protects against, and what it does not
|
|
163
|
+
|
|
164
|
+
A security tool should say both. Yenop governs agent execution on a machine you control.
|
|
165
|
+
|
|
166
|
+
**It protects against**
|
|
167
|
+
|
|
168
|
+
- tool misuse after a prompt injection: an agent convinced to act still has to pass a rule
|
|
169
|
+
- unauthorized destructive commands, and writes outside the project
|
|
170
|
+
- reads of secret files and credential stores, however the path is spelled or reached
|
|
171
|
+
- exfiltration paths: a credential over the network, a download piped into a shell, an outside call after sensitive data
|
|
172
|
+
- multi-step attack chains that look harmless one call at a time
|
|
173
|
+
- tool servers overreaching what they claimed to cover
|
|
174
|
+
- an agent editing Yenop's own rules or hook registration to switch it off
|
|
175
|
+
- runaway loops, by per-run budgets
|
|
176
|
+
|
|
177
|
+
**It does not claim to protect against**
|
|
178
|
+
|
|
179
|
+
- a person with root on the machine, or an attacker who already has it: they can remove Yenop like any other program
|
|
180
|
+
- a compromised operating system or a compromised Yenop binary
|
|
181
|
+
- vulnerabilities inside a tool that was legitimately allowed to run
|
|
182
|
+
- actions taken through a path Yenop is not hooked into, such as an agent Yenop does not yet support, or a runtime that ignores a hook by design (Codex, see above)
|
|
183
|
+
- a wholesale rewrite of the receipts file from scratch: the chain catches edits, deletions and reordering locally; anchoring the head against a full rewrite is the control plane's job
|
|
184
|
+
|
|
185
|
+
Yenop governs agent execution. It does not govern a compromised host.
|
|
186
|
+
|
|
187
|
+
## The daemon
|
|
188
|
+
|
|
189
|
+
Opening the policies, the Cedar engine and the state database costs about 100 ms from scratch. Yenop keeps a small resident service on `127.0.0.1` that holds everything warm and decides in about a millisecond.
|
|
190
|
+
|
|
191
|
+
| Path | Round trip | When |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| HTTP hook, posts straight to the daemon | ~1 ms | `yenop init --hook http` |
|
|
194
|
+
| Command hook, forwards over a raw socket | ~45 ms, almost all Node starting | default |
|
|
195
|
+
| Command hook, no daemon | ~120 ms, then it starts one for next time | fallback |
|
|
196
|
+
|
|
197
|
+
The command hook is the default because it cannot fail open: with no daemon it decides in-process. `yenop service install` hands the daemon to the operating system's supervisor (launchd on macOS, a systemd user service on Linux) so it stays up; do that before making the HTTP hook the default. The daemon serves every project, reloads within a second of a policy change, keeps its port and token across restarts, retires itself when Yenop is rebuilt, and binds only loopback behind a token.
|
|
198
|
+
|
|
199
|
+
## On-prem and offline
|
|
200
|
+
|
|
201
|
+
Yenop is built to run where you cannot reach a cloud. `npm run pack:offline` produces a self-contained bundle that installs on an air-gapped machine with only Node. Nothing it does requires a network. See [docs/on-prem.md](docs/on-prem.md) for the air-gapped install, running the daemon under a supervisor, platform support, and sending receipts to a SIEM.
|
|
202
|
+
|
|
203
|
+
## Try it safely
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
yenop playground # ~/yenop-playground: fake infra, .env, build output, enforcement on
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Open that folder in an agent and ask it to delete the build folder, run `terraform destroy`, or print the database password. Delete the folder when done.
|
|
210
|
+
|
|
211
|
+
## Formats and versions
|
|
212
|
+
|
|
213
|
+
| Thing | Version field | Where |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| Receipts | `v` and a `prev` hash on every line | `~/.yenop/receipts.jsonl` |
|
|
216
|
+
| Config files | `v` | `~/.yenop/config.json`, `<project>/.yenop/config.json` |
|
|
217
|
+
| State database | `PRAGMA user_version` | `~/.yenop/state.db` |
|
|
218
|
+
| Policy vocabulary | comment header | `policies/schema.cedarschema` |
|
|
219
|
+
|
|
220
|
+
A file newer than the running Yenop understands is an error, never a silent misread.
|
|
221
|
+
|
|
222
|
+
## Layout
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
src/core/ decision engine, Cedar evaluation, run state (SQLite), receipts (JSONL)
|
|
226
|
+
src/adapters/ one thin adapter per runtime, on a shared hook pipeline
|
|
227
|
+
src/daemon/ the resident decision service, its client, and the raw-socket fast path
|
|
228
|
+
src/viewer/ the read-only receipts page
|
|
229
|
+
src/cli/ the yenop command
|
|
230
|
+
policies/ the default policy pack and the vocabulary
|
|
231
|
+
fixtures/ hook events recorded from each runtime, replayed on every push
|
|
232
|
+
examples/ small agents with Yenop in front of them (OpenAI Agents SDK)
|
|
233
|
+
harness/ the adversarial harness: a real model as red-teamer
|
|
234
|
+
site/ yenop.com
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Design references: OWASP AISVS control group C09, the MCP specification, Cedar, AuthZEN, RFC 8693.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
Yenop is source-available under the [Functional Source License, Version 1.1, with Apache 2.0 as the future license](LICENSE.md) (FSL-1.1-ALv2).
|
|
242
|
+
|
|
243
|
+
- You may read, install, run, modify and redistribute it, including inside your company and for your customers' internal use.
|
|
244
|
+
- You may not offer it, or something substantially similar built from it, as a competing commercial product or service.
|
|
245
|
+
- Every release becomes Apache 2.0 two years after it is published.
|
|
246
|
+
|
|
247
|
+
Organization features, such as shared approvals, central policy, cross-runtime receipts and the enterprise build, are separate commercial products. "Yenop" is a trademark; forks must use another name.
|
|
8
248
|
|
|
9
249
|
https://yenop.com
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { DecisionRequest } from "../../core/types.js";
|
|
2
|
+
import { readStdin as readStdinShared, type HookEvent, type HookRunResult, type HookTranslator } from "../hooks/pipeline.js";
|
|
3
|
+
export interface ClaudeCodeHookInput {
|
|
4
|
+
session_id: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
permission_mode?: string;
|
|
7
|
+
hook_event_name?: string;
|
|
8
|
+
tool_name: string;
|
|
9
|
+
tool_input?: Record<string, unknown>;
|
|
10
|
+
tool_use_id?: string;
|
|
11
|
+
agent_id?: string;
|
|
12
|
+
agent_type?: string;
|
|
13
|
+
/** PermissionDenied only. */
|
|
14
|
+
denial_reason?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Events Claude Code sends after the decision, and what each says about the call. */
|
|
17
|
+
export declare const OUTCOME_EVENTS: Record<string, "ran" | "failed" | "denied">;
|
|
18
|
+
export declare function runIdOf(input: ClaudeCodeHookInput): string;
|
|
19
|
+
export interface ClaudeCodeHookOutput {
|
|
20
|
+
hookSpecificOutput: {
|
|
21
|
+
hookEventName: "PreToolUse";
|
|
22
|
+
permissionDecision: "allow" | "deny" | "ask";
|
|
23
|
+
permissionDecisionReason: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function toDecisionRequest(input: ClaudeCodeHookInput): DecisionRequest;
|
|
27
|
+
export type { HookRunResult };
|
|
28
|
+
/**
|
|
29
|
+
* The JSON Claude Code should receive, or null when Yenop has nothing to say
|
|
30
|
+
* (allow, or observe mode). Shared by the command hook and the daemon's HTTP hook.
|
|
31
|
+
*/
|
|
32
|
+
export declare function hookDecisionBody(effect: "allow" | "deny" | "ask", message: string, mode: "enforce" | "observe"): ClaudeCodeHookOutput | null;
|
|
33
|
+
/** Command-hook rendering: nothing on allow, JSON on ask, JSON plus exit 2 on deny. */
|
|
34
|
+
export declare function renderHookResult(effect: "allow" | "deny" | "ask", message: string, mode?: "enforce" | "observe"): HookRunResult;
|
|
35
|
+
/**
|
|
36
|
+
* Parse a Claude Code hook event. Throws on input that is not Claude Code's JSON, so the pipeline fails closed:
|
|
37
|
+
* a permission hook that cannot read its input must block, not shrug.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseClaudeCode(raw: string): HookEvent;
|
|
40
|
+
export declare const claudeCodeTranslator: HookTranslator;
|
|
41
|
+
/** Command hook entry point: the shared pipeline with the Claude Code translator. */
|
|
42
|
+
export declare function runHook(raw: string): Promise<HookRunResult>;
|
|
43
|
+
export declare const readStdin: typeof readStdinShared;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code PreToolUse adapter.
|
|
3
|
+
* stdin: the hook JSON Claude Code sends. stdout: a hook decision, or nothing to defer to Claude Code's own flow.
|
|
4
|
+
* Yenop only ever tightens: on allow it prints nothing, on ask it asks, on deny it blocks (exit 2 + JSON reason).
|
|
5
|
+
*/
|
|
6
|
+
import { classifyTool } from "../../core/tools.js";
|
|
7
|
+
import { runHookWith, safeUser, readStdin as readStdinShared } from "../hooks/pipeline.js";
|
|
8
|
+
/** Events Claude Code sends after the decision, and what each says about the call. */
|
|
9
|
+
export const OUTCOME_EVENTS = {
|
|
10
|
+
PostToolUse: "ran",
|
|
11
|
+
PostToolUseFailure: "failed",
|
|
12
|
+
PermissionDenied: "denied",
|
|
13
|
+
};
|
|
14
|
+
export function runIdOf(input) {
|
|
15
|
+
return `claude-code:${input.session_id}`;
|
|
16
|
+
}
|
|
17
|
+
export function toDecisionRequest(input) {
|
|
18
|
+
const req = {
|
|
19
|
+
runId: runIdOf(input),
|
|
20
|
+
principal: {
|
|
21
|
+
runtime: "claude-code",
|
|
22
|
+
agent: input.agent_type ? `subagent:${input.agent_type}` : "main",
|
|
23
|
+
user: safeUser(),
|
|
24
|
+
},
|
|
25
|
+
tool: classifyTool(input.tool_name),
|
|
26
|
+
args: input.tool_input ?? {},
|
|
27
|
+
};
|
|
28
|
+
if (input.cwd !== undefined)
|
|
29
|
+
req.cwd = input.cwd;
|
|
30
|
+
if (input.permission_mode !== undefined)
|
|
31
|
+
req.permissionMode = input.permission_mode;
|
|
32
|
+
if (input.tool_use_id !== undefined)
|
|
33
|
+
req.callId = input.tool_use_id;
|
|
34
|
+
return req;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The JSON Claude Code should receive, or null when Yenop has nothing to say
|
|
38
|
+
* (allow, or observe mode). Shared by the command hook and the daemon's HTTP hook.
|
|
39
|
+
*/
|
|
40
|
+
export function hookDecisionBody(effect, message, mode) {
|
|
41
|
+
if (mode === "observe" || effect === "allow")
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
hookSpecificOutput: {
|
|
45
|
+
hookEventName: "PreToolUse",
|
|
46
|
+
permissionDecision: effect,
|
|
47
|
+
permissionDecisionReason: `Yenop: ${message}`,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Command-hook rendering: nothing on allow, JSON on ask, JSON plus exit 2 on deny. */
|
|
52
|
+
export function renderHookResult(effect, message, mode = "enforce") {
|
|
53
|
+
const body = hookDecisionBody(effect, message, mode);
|
|
54
|
+
if (!body)
|
|
55
|
+
return { stdout: "", exitCode: 0 };
|
|
56
|
+
return { stdout: JSON.stringify(body), exitCode: effect === "deny" ? 2 : 0 };
|
|
57
|
+
}
|
|
58
|
+
function renderFromBody(body) {
|
|
59
|
+
if (!("hookSpecificOutput" in body))
|
|
60
|
+
return { stdout: "", exitCode: 0 };
|
|
61
|
+
return { stdout: JSON.stringify(body), exitCode: body.hookSpecificOutput.permissionDecision === "deny" ? 2 : 0 };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a Claude Code hook event. Throws on input that is not Claude Code's JSON, so the pipeline fails closed:
|
|
65
|
+
* a permission hook that cannot read its input must block, not shrug.
|
|
66
|
+
*/
|
|
67
|
+
export function parseClaudeCode(raw) {
|
|
68
|
+
const input = JSON.parse(raw); // throws → deny
|
|
69
|
+
if (typeof input.tool_name !== "string" || typeof input.session_id !== "string")
|
|
70
|
+
throw new Error("claude-code hook event without tool_name/session_id");
|
|
71
|
+
const outcome = OUTCOME_EVENTS[input.hook_event_name ?? ""];
|
|
72
|
+
if (outcome) {
|
|
73
|
+
if (!input.tool_use_id)
|
|
74
|
+
return { kind: "ignore" };
|
|
75
|
+
const ev = { kind: "outcome", runId: runIdOf(input), callId: input.tool_use_id, tool: input.tool_name, outcome };
|
|
76
|
+
if (input.denial_reason !== undefined)
|
|
77
|
+
ev.detail = input.denial_reason;
|
|
78
|
+
if (input.cwd !== undefined)
|
|
79
|
+
ev.cwd = input.cwd;
|
|
80
|
+
return ev;
|
|
81
|
+
}
|
|
82
|
+
if (input.hook_event_name !== undefined && input.hook_event_name !== "PreToolUse")
|
|
83
|
+
return { kind: "ignore" };
|
|
84
|
+
return { kind: "decision", event: "PreToolUse", askCapable: true, request: toDecisionRequest(input) };
|
|
85
|
+
}
|
|
86
|
+
export const claudeCodeTranslator = {
|
|
87
|
+
runtime: "claude-code",
|
|
88
|
+
parse: parseClaudeCode,
|
|
89
|
+
body: (effect, message, mode) => hookDecisionBody(effect, message, mode),
|
|
90
|
+
result: (body) => renderFromBody((body ?? {})),
|
|
91
|
+
failure: (reason) => ({
|
|
92
|
+
stdout: JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: `Yenop failed and refuses by default: ${reason}. Run "yenop status".` } }),
|
|
93
|
+
exitCode: 2,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
/** Command hook entry point: the shared pipeline with the Claude Code translator. */
|
|
97
|
+
export function runHook(raw) {
|
|
98
|
+
return runHookWith(claudeCodeTranslator, raw);
|
|
99
|
+
}
|
|
100
|
+
export const readStdin = readStdinShared;
|
|
101
|
+
//# sourceMappingURL=hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../../../src/adapters/claude-code/hook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Any command that ends in this is ours, whatever path the CLI lives at. */
|
|
2
|
+
export declare const YENOP_HOOK_MARKER = "hook claude-code";
|
|
3
|
+
/**
|
|
4
|
+
* The command Claude Code should run. When `yenop` on PATH is this very build (a global npm
|
|
5
|
+
* install or `npm link`), use the short form so the settings file carries no machine path.
|
|
6
|
+
* Otherwise fall back to node plus the absolute path of this build.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hookCommandFor(cliPath: string, runtime?: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Install the HTTP form of the hook: Claude Code posts straight to the daemon, no process spawn.
|
|
11
|
+
* The token lives in the settings file, which is why this form belongs in settings.local.json or the user file.
|
|
12
|
+
*/
|
|
13
|
+
export declare function installClaudeCodeHttpHook(settingsPath: string, port: number, token: string): {
|
|
14
|
+
changed: boolean;
|
|
15
|
+
path: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function installClaudeCodeHook(settingsPath: string, command: string): {
|
|
18
|
+
changed: boolean;
|
|
19
|
+
path: string;
|
|
20
|
+
};
|
|
21
|
+
/** PreToolUse decides. The others only report what became of the call, so they run without making the agent wait. */
|
|
22
|
+
export declare const DECISION_EVENT = "PreToolUse";
|
|
23
|
+
export declare const REPORT_EVENTS: string[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
/** Any command that ends in this is ours, whatever path the CLI lives at. */
|
|
5
|
+
export const YENOP_HOOK_MARKER = "hook claude-code";
|
|
6
|
+
/**
|
|
7
|
+
* The command Claude Code should run. When `yenop` on PATH is this very build (a global npm
|
|
8
|
+
* install or `npm link`), use the short form so the settings file carries no machine path.
|
|
9
|
+
* Otherwise fall back to node plus the absolute path of this build.
|
|
10
|
+
*/
|
|
11
|
+
export function hookCommandFor(cliPath, runtime = "claude-code") {
|
|
12
|
+
try {
|
|
13
|
+
const which = execFileSync("sh", ["-lc", "command -v yenop"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
14
|
+
if (which && realpathSync(which) === realpathSync(cliPath))
|
|
15
|
+
return `yenop hook ${runtime}`;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* not on PATH */
|
|
19
|
+
}
|
|
20
|
+
return `node ${JSON.stringify(cliPath)} hook ${runtime}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Add the Yenop PreToolUse hook to a Claude Code settings file.
|
|
24
|
+
* Project scope uses .claude/settings.local.json so the machine-specific command path is never committed.
|
|
25
|
+
* Idempotent: an existing Yenop entry is replaced, other hooks are left alone.
|
|
26
|
+
*/
|
|
27
|
+
function isOurs(h) {
|
|
28
|
+
if (h.type === "http")
|
|
29
|
+
return (h.url ?? "").includes("/hooks/claude-code");
|
|
30
|
+
return (h.command ?? "").trim().endsWith(YENOP_HOOK_MARKER);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Install the HTTP form of the hook: Claude Code posts straight to the daemon, no process spawn.
|
|
34
|
+
* The token lives in the settings file, which is why this form belongs in settings.local.json or the user file.
|
|
35
|
+
*/
|
|
36
|
+
export function installClaudeCodeHttpHook(settingsPath, port, token) {
|
|
37
|
+
const entry = {
|
|
38
|
+
type: "http",
|
|
39
|
+
url: `http://127.0.0.1:${port}/hooks/claude-code`,
|
|
40
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
41
|
+
timeout: 10,
|
|
42
|
+
statusMessage: "Yenop is checking this action",
|
|
43
|
+
};
|
|
44
|
+
return installEntry(settingsPath, entry);
|
|
45
|
+
}
|
|
46
|
+
export function installClaudeCodeHook(settingsPath, command) {
|
|
47
|
+
const entry = { type: "command", command, timeout: 15, statusMessage: "Yenop is checking this action" };
|
|
48
|
+
return installEntry(settingsPath, entry);
|
|
49
|
+
}
|
|
50
|
+
/** PreToolUse decides. The others only report what became of the call, so they run without making the agent wait. */
|
|
51
|
+
export const DECISION_EVENT = "PreToolUse";
|
|
52
|
+
export const REPORT_EVENTS = ["PostToolUse", "PostToolUseFailure", "PermissionDenied"];
|
|
53
|
+
function installEntry(settingsPath, entry) {
|
|
54
|
+
let settings = {};
|
|
55
|
+
if (existsSync(settingsPath)) {
|
|
56
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
mkdirSync(join(settingsPath, ".."), { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
settings.hooks ??= {};
|
|
62
|
+
let changed = false;
|
|
63
|
+
const put = (event, e) => {
|
|
64
|
+
const groups = (settings.hooks[event] ??= []);
|
|
65
|
+
const existing = groups.find((g) => g.hooks.some(isOurs));
|
|
66
|
+
if (existing) {
|
|
67
|
+
const same = existing.matcher === "*" && existing.hooks.length === 1 && JSON.stringify(existing.hooks[0]) === JSON.stringify(e);
|
|
68
|
+
if (same)
|
|
69
|
+
return;
|
|
70
|
+
existing.matcher = "*";
|
|
71
|
+
existing.hooks = [e];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
groups.push({ matcher: "*", hooks: [e] });
|
|
75
|
+
}
|
|
76
|
+
changed = true;
|
|
77
|
+
};
|
|
78
|
+
put(DECISION_EVENT, entry);
|
|
79
|
+
const { statusMessage: _quiet, ...report } = entry;
|
|
80
|
+
for (const ev of REPORT_EVENTS)
|
|
81
|
+
put(ev, { ...report, async: true });
|
|
82
|
+
if (changed)
|
|
83
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
84
|
+
return { changed, path: settingsPath };
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../../src/adapters/claude-code/install.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Mode } from "../../core/config.js";
|
|
2
|
+
import { type Effect, type HookEvent, type HookTranslator } from "../hooks/pipeline.js";
|
|
3
|
+
/** The files a patch touches, from its headers. Exported so a recorded apply_patch event can be checked against it. */
|
|
4
|
+
export declare function pathsInPatch(patch: string): string[];
|
|
5
|
+
export declare function parseCodex(raw: string): HookEvent;
|
|
6
|
+
export declare function codexBody(effect: Effect, message: string, mode: Mode): Record<string, unknown> | null;
|
|
7
|
+
export declare const codexTranslator: HookTranslator;
|