@hydra-acp/approver 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sam Magnuson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # hydra-acp-approver
2
+
3
+ Headless permission auto-responder extension for [hydra-acp](https://github.com/smagnuso/hydra-acp).
4
+
5
+ Attaches to every live hydra session and answers `session/request_permission` based on a JavaScript rule function you provide. When the rule matches, the approver wins the race and dismisses the permission prompt before any human client sees it. When the rule abstains, the request stays open so your interactive clients (slack, TUI, browser) can still answer it normally.
6
+
7
+ ## Install
8
+
9
+ From npm (recommended once published):
10
+
11
+ ```sh
12
+ npm install -g @hydra-acp/approver
13
+ ```
14
+
15
+ This drops a `hydra-acp-approver` binary on your PATH.
16
+
17
+ Or from source:
18
+
19
+ ```sh
20
+ git clone git@github.com:smagnuso/hydra-acp-approver.git ~/dev/hydra-acp-approver
21
+ cd ~/dev/hydra-acp-approver
22
+ npm install
23
+ npm run build
24
+ ```
25
+
26
+ Register the extension with hydra. If installed via npm:
27
+
28
+ ```sh
29
+ hydra-acp extensions add hydra-acp-approver --command hydra-acp-approver
30
+ ```
31
+
32
+ Or pointed at a local build:
33
+
34
+ ```sh
35
+ hydra-acp extensions add hydra-acp-approver \
36
+ --command node \
37
+ --args ~/dev/hydra-acp-approver/dist/index.js
38
+ ```
39
+
40
+ That writes the equivalent entry into `~/.hydra-acp/config.json`:
41
+
42
+ ```json
43
+ {
44
+ "extensions": {
45
+ "hydra-acp-approver": {
46
+ "command": ["node"],
47
+ "args": ["/home/you/dev/hydra-acp-approver/dist/index.js"],
48
+ "enabled": true
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ On `hydra-acp daemon start`, hydra spawns hydra-acp-approver as a managed
55
+ process with these env vars set: `HYDRA_ACP_DAEMON_URL`, `HYDRA_ACP_TOKEN`,
56
+ `HYDRA_ACP_WS_URL`. Stdout/stderr land in
57
+ `~/.hydra-acp/extensions/hydra-acp-approver.log`. Lifecycle is managed with
58
+ `hydra-acp extensions start|stop|restart hydra-acp-approver` and
59
+ `hydra-acp extensions logs hydra-acp-approver -f` to tail.
60
+
61
+ ## Configure
62
+
63
+ Drop a JS module at `~/.hydra-acp/approver.config.js` (override with `HYDRA_ACP_APPROVER_CONFIG`). Default-export a function that decides per request:
64
+
65
+ ```js
66
+ // ~/.hydra-acp/approver.config.js
67
+ export default function approve(req) {
68
+ // req.toolCall.kind is one of: "read", "edit", "execute", "search",
69
+ // "delete", "move", "fetch", "switch_mode", "think", "other".
70
+ const kind = req.toolCall?.kind;
71
+ if (["read", "search", "other", "execute"].includes(kind)) {
72
+ return req.options.find((o) => o.kind === "allow_once")?.optionId ?? null;
73
+ }
74
+ // Return null/undefined to abstain — the request stays open and a
75
+ // human client handles it as usual.
76
+ return null;
77
+ }
78
+ ```
79
+
80
+ > Prefer `allow_once` — agents typically cache `allow_always` choices locally and bypass the approver on subsequent identical calls.
81
+
82
+ ### Recommended starting point
83
+
84
+ Blanket-allow-everything-execute is convenient but a foot-gun. The rule below mirrors that ergonomics for `read`/`search`/`other` but guards `execute` with a danger list — any tool call whose serialized shape mentions `rm -rf /`, `dd of=/dev/...`, fork bombs, piping `curl` into `sh`, system-state changes (`shutdown`, `reboot`), and friends abstains instead, so an interactive client (Slack, TUI, browser) gets the prompt and a human decides.
85
+
86
+ Patterns are matched against `JSON.stringify(toolCall)`, so they catch whichever field the agent put the command in (`rawInput.command`, terminal blocks in `content`, the title, etc.). Abstaining is safe — the request stays open — so the list errs on the side of being broad.
87
+
88
+ ```js
89
+ // ~/.hydra-acp/approver.config.js
90
+ const SAFE_KINDS = new Set(["read", "search", "other"]);
91
+
92
+ const DANGEROUS_PATTERNS = [
93
+ // rm with recursive + force flags hitting /, /*, ~, $HOME, or a bare glob
94
+ /\brm\b[^\n]*\s-[a-zA-Z]*(rf|fr)[a-zA-Z]*\b[^\n]*(\s|=)(\/(?!\w)|\/\*|~|\$HOME|\*)(\s|"|'|\\|$)/,
95
+ /\brm\b[^\n]*--recursive\b[^\n]*--force\b[^\n]*(\s|=)(\/(?!\w)|\/\*|~|\$HOME|\*)/,
96
+ /\brm\b[^\n]*--force\b[^\n]*--recursive\b[^\n]*(\s|=)(\/(?!\w)|\/\*|~|\$HOME|\*)/,
97
+ /\bdd\b[^\n]*\bof=\/dev\/(sd|nvme|disk|hd|mmcblk|xvd|vd)\w*/i,
98
+ /\bmkfs(\.\w+)?\s+\/dev\//i,
99
+ /\bfdisk\s+\/dev\//i,
100
+ /\bparted\s+\/dev\//i,
101
+ /\bshred\b[^\n]*\/dev\//i,
102
+ /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, // fork bomb
103
+ />\s*\/dev\/(sd|nvme|disk|hd|mmcblk|xvd|vd)\w*/i,
104
+ />\s*\/etc\/(passwd|shadow|sudoers|hosts)\b/i,
105
+ /\b(shutdown|reboot|halt|poweroff)\b/i,
106
+ /\binit\s+[06]\b/,
107
+ /\bkill\s+-(?:9|KILL|SIGKILL)\s+1\b/,
108
+ /\b(curl|wget|fetch)\b[^\n|]*\|\s*(sudo\s+)?(sh|bash|zsh|fish)\b/i,
109
+ /\bchmod\s+-R\b[^\n]*\s\/(\s|$|"|')/,
110
+ /\bchown\s+-R\b[^\n]*\s\/(\s|$|"|')/,
111
+ /\bsudo\s+(rm|dd|mkfs|fdisk|parted|shred|chmod|chown|shutdown|reboot|halt|poweroff|init|userdel|groupdel)\b/i,
112
+ ];
113
+
114
+ function looksDangerous(toolCall) {
115
+ let blob;
116
+ try {
117
+ blob = JSON.stringify(toolCall);
118
+ } catch {
119
+ return true;
120
+ }
121
+ return DANGEROUS_PATTERNS.some((p) => p.test(blob));
122
+ }
123
+
124
+ function pickAllowOnce(options) {
125
+ return options.find((o) => o.kind === "allow_once")?.optionId ?? null;
126
+ }
127
+
128
+ export default function approve(req) {
129
+ const kind = req.toolCall?.kind;
130
+ if (SAFE_KINDS.has(kind)) {
131
+ return pickAllowOnce(req.options);
132
+ }
133
+ if (kind === "execute") {
134
+ if (looksDangerous(req.toolCall)) {
135
+ return null;
136
+ }
137
+ return pickAllowOnce(req.options);
138
+ }
139
+ return null;
140
+ }
141
+ ```
142
+
143
+ Treat this as a starting point, not a security boundary — pattern-based detection inevitably misses things, and an agent that can craft commands can probably evade any list you write. The win is "no permission prompts for the 99% case, human-in-the-loop for the obviously-irreversible 1%."
144
+
145
+ ### Request shape
146
+
147
+ ```ts
148
+ interface PermissionRequest {
149
+ sessionId: string;
150
+ toolCall: {
151
+ toolCallId: string;
152
+ name?: string;
153
+ title?: string;
154
+ kind?: string;
155
+ [k: string]: unknown;
156
+ };
157
+ options: ReadonlyArray<{
158
+ optionId: string;
159
+ name: string;
160
+ kind?: "allow_once" | "allow_always" | "reject_once" | "reject_always" | string;
161
+ }>;
162
+ cwd?: string;
163
+ agentId?: string;
164
+ }
165
+ ```
166
+
167
+ ### Return value
168
+
169
+ | Return | Behavior |
170
+ |--------------|------------------------------------------------------------------------------------------------------|
171
+ | `string` | An `optionId` from `req.options`. Approver responds with `{ outcome: { outcome: "selected", optionId } }` and wins the race against other attached clients. |
172
+ | `null` / `undefined` | Abstain. Approver doesn't respond; other attached clients (humans) see the prompt. |
173
+ | `Promise<...>` | Awaited. Same semantics on resolve. |
174
+ | Throw | Caught + logged + treated as abstain — safe-by-default if your rule has a bug. |
175
+
176
+ If `optionId` doesn't appear in `req.options` (typo, agent-specific renaming), the approver abstains and logs a warning.
177
+
178
+ ### Reload
179
+
180
+ Edits to `approver.config.js` are picked up automatically — the approver watches the file and re-imports it (with cache-busting) on every save. The next permission request after the reload completes uses the fresh rule.
181
+
182
+ If `fs.watch` is unreliable on your filesystem (NFS, some network mounts, certain container layouts), trigger a reload manually:
183
+
184
+ ```sh
185
+ hydra-acp extensions restart hydra-acp-approver
186
+ # or, lighter, just re-import the rule without bouncing the WS attaches:
187
+ kill -HUP $(cat ~/.hydra-acp/extensions/hydra-acp-approver.pid)
188
+ ```
189
+
190
+ Pending (already-abstained) requests are unaffected; new requests use the fresh rule.
191
+
192
+ ### Missing config
193
+
194
+ If `approver.config.js` doesn't exist, the approver defaults to **abstain on every request**. Installing the extension without writing a config has zero behavioral effect — the daemon broadcasts permission prompts to every attached client as before.
195
+
196
+ ## Environment
197
+
198
+ | Env var | Default | Purpose |
199
+ |---|---|---|
200
+ | `HYDRA_ACP_DAEMON_URL` | `http://127.0.0.1:8765` | Daemon HTTP endpoint (injected by hydra when run as an extension) |
201
+ | `HYDRA_ACP_TOKEN` | *(required)* | Daemon auth token (injected by hydra) |
202
+ | `HYDRA_ACP_WS_URL` | derived from daemon URL | Override WS endpoint |
203
+ | `HYDRA_ACP_APPROVER_CONFIG` | `~/.hydra-acp/approver.config.js` | Path to the rule module |
204
+ | `HYDRA_ACP_APPROVER_POLL_MS` | `2000` | Session-discovery poll interval |
205
+ | `DEBUG` | `false` | Verbose logging |
206
+
207
+ ## How it works
208
+
209
+ The hydra-acp daemon broadcasts each `session/request_permission` to every attached client simultaneously and resolves the original agent request on the first response (see `hydra-acp/src/core/session.ts` `handlePermissionRequest`). Losers receive a `session/permission_resolved` notification with the winning outcome.
210
+
211
+ The approver attaches as one more client. When the rule fn returns an `optionId`, it replies immediately and wins. When it abstains, it stashes the JSON-RPC `respond` callback keyed by `toolCallId`; when `permission_resolved` arrives for that id, it replies with `{ outcome: { outcome: "cancelled" } }` to close out its own pending promise (no side effect — the daemon already settled the original request).
212
+
213
+ This means: install the approver and any per-client approve lambdas can go. Centralize the policy in one place.
214
+
@@ -0,0 +1,192 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { readFileSync } from "node:fs";
3
+ import { WebSocket } from "ws";
4
+ import { logger } from "../util/log.js";
5
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
6
+ import { isNotification, isRequest, isResponse, } from "./protocol.js";
7
+ const log = logger("acp");
8
+ export class AcpAttach extends EventEmitter {
9
+ opts;
10
+ ws;
11
+ nextId = 1;
12
+ pending = new Map();
13
+ connected = false;
14
+ constructor(opts) {
15
+ super();
16
+ this.opts = opts;
17
+ }
18
+ get sessionId() {
19
+ return this.opts.sessionId;
20
+ }
21
+ get isConnected() {
22
+ return this.connected;
23
+ }
24
+ start() {
25
+ log.debug(`connecting ${this.opts.daemonWsUrl} for ${this.opts.sessionId}`);
26
+ const subprotocols = ["acp.v1", `hydra-acp-token.${this.opts.token}`];
27
+ let ws;
28
+ try {
29
+ ws = new WebSocket(this.opts.daemonWsUrl, subprotocols);
30
+ }
31
+ catch (err) {
32
+ this.emit("error", err);
33
+ return;
34
+ }
35
+ this.ws = ws;
36
+ ws.on("open", () => {
37
+ this.connected = true;
38
+ log.info(`ws open ${this.opts.sessionId}`);
39
+ void this.handshake()
40
+ .then(() => {
41
+ this.emit("open");
42
+ })
43
+ .catch((err) => {
44
+ this.emit("error", err);
45
+ try {
46
+ this.ws?.close();
47
+ }
48
+ catch {
49
+ void 0;
50
+ }
51
+ });
52
+ });
53
+ ws.on("message", (data, isBinary) => {
54
+ if (isBinary) {
55
+ return;
56
+ }
57
+ const text = data.toString("utf8");
58
+ try {
59
+ const parsed = JSON.parse(text);
60
+ this.onMessage(parsed);
61
+ }
62
+ catch (err) {
63
+ log.warn(`parse error on ${this.opts.sessionId}: ${err.message}; raw=${text.slice(0, 200)}`);
64
+ }
65
+ });
66
+ ws.on("error", (err) => {
67
+ log.warn(`ws error ${this.opts.sessionId}: ${err.message}`);
68
+ this.emit("error", err);
69
+ });
70
+ ws.on("close", (code, reason) => {
71
+ const hadError = code >= 4000 || code === 1006 || code === 1011;
72
+ const reasonText = reason.toString("utf8");
73
+ this.connected = false;
74
+ log.info(`ws closed ${this.opts.sessionId} code=${code}${reasonText ? ` reason=${reasonText}` : ""}`);
75
+ for (const [, p] of this.pending) {
76
+ p.reject(new Error("ws closed"));
77
+ }
78
+ this.pending.clear();
79
+ this.emit("close", { hadError });
80
+ });
81
+ }
82
+ stop() {
83
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
84
+ try {
85
+ this.ws.close();
86
+ }
87
+ catch {
88
+ void 0;
89
+ }
90
+ }
91
+ }
92
+ async request(method, params) {
93
+ const id = this.nextId++;
94
+ const msg = {
95
+ jsonrpc: "2.0",
96
+ id,
97
+ method,
98
+ ...(params !== undefined ? { params } : {}),
99
+ };
100
+ this.write(msg);
101
+ return new Promise((resolve, reject) => {
102
+ this.pending.set(id, {
103
+ resolve: (resp) => {
104
+ if (resp.error) {
105
+ reject(new Error(`${resp.error.code}: ${resp.error.message}`));
106
+ }
107
+ else {
108
+ resolve(resp.result);
109
+ }
110
+ },
111
+ reject,
112
+ });
113
+ });
114
+ }
115
+ notify(method, params) {
116
+ const msg = {
117
+ jsonrpc: "2.0",
118
+ method,
119
+ ...(params !== undefined ? { params } : {}),
120
+ };
121
+ this.write(msg);
122
+ }
123
+ reply(id, result) {
124
+ const msg = { jsonrpc: "2.0", id, result };
125
+ this.write(msg);
126
+ }
127
+ replyError(id, code, message) {
128
+ const msg = {
129
+ jsonrpc: "2.0",
130
+ id,
131
+ error: { code, message },
132
+ };
133
+ this.write(msg);
134
+ }
135
+ async handshake() {
136
+ try {
137
+ await this.request("initialize", {
138
+ protocolVersion: 1,
139
+ clientCapabilities: {
140
+ fs: { readTextFile: false, writeTextFile: false },
141
+ terminal: false,
142
+ },
143
+ });
144
+ }
145
+ catch (err) {
146
+ log.warn(`initialize failed for ${this.opts.sessionId}: ${err.message}`);
147
+ }
148
+ try {
149
+ // historyPolicy: "none" — the approver doesn't care about prior
150
+ // transcript content; we only want fresh permission requests.
151
+ const attachResult = await this.request("session/attach", {
152
+ sessionId: this.opts.sessionId,
153
+ historyPolicy: "none",
154
+ clientInfo: { name: "hydra-acp-approver", version: pkg.version },
155
+ });
156
+ log.info(`attached ${this.opts.sessionId}${attachResult.replayed !== undefined
157
+ ? ` replayed=${attachResult.replayed}`
158
+ : ""}`);
159
+ }
160
+ catch (err) {
161
+ log.warn(`session/attach failed for ${this.opts.sessionId}: ${err.message}`);
162
+ throw err;
163
+ }
164
+ }
165
+ write(msg) {
166
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
167
+ log.warn(`drop write to closed ws: ${JSON.stringify(msg)}`);
168
+ return;
169
+ }
170
+ this.ws.send(JSON.stringify(msg));
171
+ }
172
+ onMessage(m) {
173
+ if (isResponse(m)) {
174
+ const p = this.pending.get(m.id);
175
+ if (p) {
176
+ this.pending.delete(m.id);
177
+ p.resolve(m);
178
+ }
179
+ else {
180
+ log.debug(`unmatched response id=${String(m.id)}`);
181
+ }
182
+ this.emit("response", m);
183
+ }
184
+ else if (isRequest(m)) {
185
+ this.emit("request", m);
186
+ }
187
+ else if (isNotification(m)) {
188
+ this.emit("notification", m);
189
+ }
190
+ }
191
+ }
192
+ //# sourceMappingURL=attach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach.js","sourceRoot":"","sources":["../../src/acp/attach.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export function isRequest(m) {
2
+ return "method" in m && "id" in m;
3
+ }
4
+ export function isNotification(m) {
5
+ return "method" in m && !("id" in m);
6
+ }
7
+ export function isResponse(m) {
8
+ return !("method" in m) && "id" in m;
9
+ }
10
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/acp/protocol.ts"],"names":[],"mappings":"AA2BA,MAAM,UAAU,SAAS,CAAC,CAAiB;IACzC,OAAO,QAAQ,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,CAAiB;IAC9C,OAAO,QAAQ,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAiB;IAC1C,OAAO,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AACvC,CAAC"}
package/dist/bridge.js ADDED
@@ -0,0 +1,70 @@
1
+ import { AcpAttach } from "./acp/attach.js";
2
+ import { PermissionRouter } from "./permission.js";
3
+ import { logger } from "./util/log.js";
4
+ const log = logger("bridge");
5
+ // One bridge per discovered session. Opens a WS, attaches as a
6
+ // controller, hooks session/request_permission requests + the
7
+ // session/permission_resolved notification through PermissionRouter.
8
+ //
9
+ // `getRule` is a thunk rather than a baked-in value so SIGHUP reloads
10
+ // (which mutate the rule on the entry point's loader) are picked up
11
+ // without rebuilding every bridge. The router itself also has a
12
+ // setRule path, used when a reload happens after a bridge is live.
13
+ export class ApproverBridge {
14
+ opts;
15
+ attach;
16
+ router;
17
+ stopped = false;
18
+ constructor(opts) {
19
+ this.opts = opts;
20
+ this.attach = new AcpAttach({
21
+ sessionId: opts.meta.sessionId,
22
+ daemonWsUrl: opts.daemonWsUrl,
23
+ token: opts.token,
24
+ });
25
+ this.router = new PermissionRouter(opts.getRule(), opts.meta, log);
26
+ }
27
+ start() {
28
+ this.attach.on("request", (r) => this.onRequest(r));
29
+ this.attach.on("notification", (n) => this.onNotification(n));
30
+ this.attach.on("close", () => {
31
+ this.router.shutdown();
32
+ });
33
+ this.attach.on("error", (err) => {
34
+ log.warn(`attach error ${this.opts.meta.sessionId}: ${err.message}`);
35
+ });
36
+ this.attach.start();
37
+ }
38
+ stop() {
39
+ if (this.stopped) {
40
+ return;
41
+ }
42
+ this.stopped = true;
43
+ this.router.shutdown();
44
+ this.attach.stop();
45
+ }
46
+ // Pull a fresh rule from the loader after a SIGHUP reload.
47
+ refreshRule() {
48
+ this.router.setRule(this.opts.getRule());
49
+ }
50
+ onRequest(r) {
51
+ if (r.method !== "session/request_permission") {
52
+ // Anything else aimed at us is unexpected — reply with a JSON-RPC
53
+ // method-not-found so the daemon doesn't hold a pending promise.
54
+ this.attach.replyError(r.id, -32601, `method not implemented: ${r.method}`);
55
+ return;
56
+ }
57
+ const params = (r.params ?? {});
58
+ void this.router.onRequestPermission(r.id, params, (result) => {
59
+ this.attach.reply(r.id, result);
60
+ });
61
+ }
62
+ onNotification(n) {
63
+ if (n.method !== "session/permission_resolved") {
64
+ return;
65
+ }
66
+ const params = (n.params ?? {});
67
+ this.router.onPermissionResolved(params);
68
+ }
69
+ }
70
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAO5C,OAAO,EAAE,gBAAgB,EAAoB,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;AAS7B,+DAA+D;AAC/D,8DAA8D;AAC9D,qEAAqE;AACrE,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,gEAAgE;AAChE,mEAAmE;AACnE,MAAM,OAAO,cAAc;IAKI;IAJZ,MAAM,CAAY;IAClB,MAAM,CAAmB;IAClC,OAAO,GAAG,KAAK,CAAC;IAExB,YAA6B,IAAmB;QAAnB,SAAI,GAAJ,IAAI,CAAe;QAC9C,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC;YAC1B,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS;YAC9B,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,KAAK,EAAE,IAAI,CAAC,KAAK;SAClB,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACrE,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC9B,GAAG,CAAC,IAAI,CACN,gBAAgB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,GAAG,CAAC,OAAO,EAAE,CAC3D,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACvB,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;IAED,2DAA2D;IAC3D,WAAW;QACT,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3C,CAAC;IAEO,SAAS,CAAC,CAAiB;QACjC,IAAI,CAAC,CAAC,MAAM,KAAK,4BAA4B,EAAE,CAAC;YAC9C,kEAAkE;YAClE,iEAAiE;YACjE,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,2BAA2B,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;YAC5E,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAA4B,CAAC;QAC3D,KAAK,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;YAC5D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,cAAc,CAAC,CAAsB;QAC3C,IAAI,CAAC,CAAC,MAAM,KAAK,6BAA6B,EAAE,CAAC;YAC/C,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAA6B,CAAC;QAC5D,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;CACF"}
package/dist/config.js ADDED
@@ -0,0 +1,46 @@
1
+ import { homedir } from "node:os";
2
+ import { resolve } from "node:path";
3
+ function deriveWsUrl(httpUrl) {
4
+ if (httpUrl.startsWith("https://")) {
5
+ return "wss://" + httpUrl.slice("https://".length).replace(/\/$/, "") + "/acp";
6
+ }
7
+ if (httpUrl.startsWith("http://")) {
8
+ return "ws://" + httpUrl.slice("http://".length).replace(/\/$/, "") + "/acp";
9
+ }
10
+ throw new Error(`hydraDaemonUrl must start with http:// or https://: ${httpUrl}`);
11
+ }
12
+ function intEnv(name, fallback) {
13
+ const v = process.env[name];
14
+ if (!v) {
15
+ return fallback;
16
+ }
17
+ const n = Number.parseInt(v, 10);
18
+ return Number.isFinite(n) ? n : fallback;
19
+ }
20
+ const TRUTHY = new Set(["1", "true", "yes", "on", "t"]);
21
+ function boolEnv(name, fallback) {
22
+ const v = process.env[name];
23
+ if (v === undefined) {
24
+ return fallback;
25
+ }
26
+ return TRUTHY.has(v.toLowerCase());
27
+ }
28
+ export function loadConfig() {
29
+ const hydraDaemonUrl = process.env.HYDRA_ACP_DAEMON_URL ?? "http://127.0.0.1:8765";
30
+ const hydraToken = process.env.HYDRA_ACP_TOKEN ?? "";
31
+ if (!hydraToken) {
32
+ throw new Error("Missing HYDRA_ACP_TOKEN env var. When run as a hydra extension, hydra injects this automatically.");
33
+ }
34
+ const hydraWsUrl = process.env.HYDRA_ACP_WS_URL ?? deriveWsUrl(hydraDaemonUrl);
35
+ const ruleConfigPath = process.env.HYDRA_ACP_APPROVER_CONFIG ??
36
+ resolve(homedir(), ".hydra-acp", "approver.config.js");
37
+ return {
38
+ hydraDaemonUrl,
39
+ hydraWsUrl,
40
+ hydraToken,
41
+ hydraPollIntervalMs: intEnv("HYDRA_ACP_APPROVER_POLL_MS", 2000),
42
+ ruleConfigPath,
43
+ debug: boolEnv("DEBUG", false),
44
+ };
45
+ }
46
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAepC,SAAS,WAAW,CAAC,OAAe;IAClC,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC;IACjF,CAAC;IACD,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAClC,OAAO,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC;IAC/E,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,uDAAuD,OAAO,EAAE,CAAC,CAAC;AACpF,CAAC;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,QAAgB;IAC5C,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3C,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAExD,SAAS,OAAO,CAAC,IAAY,EAAE,QAAiB;IAC9C,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;QACpB,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,cAAc,GAClB,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,uBAAuB,CAAC;IAC9D,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,EAAE,CAAC;IACrD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,mGAAmG,CACpG,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,WAAW,CAAC,cAAc,CAAC,CAAC;IAC9D,MAAM,cAAc,GAClB,OAAO,CAAC,GAAG,CAAC,yBAAyB;QACrC,OAAO,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,oBAAoB,CAAC,CAAC;IAEzD,OAAO;QACL,cAAc;QACd,UAAU;QACV,UAAU;QACV,mBAAmB,EAAE,MAAM,CAAC,4BAA4B,EAAE,IAAI,CAAC;QAC/D,cAAc;QACd,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC;KAC/B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,82 @@
1
+ import { logger } from "./util/log.js";
2
+ const log = logger("discovery");
3
+ const DEFAULT_POLL_MS = 2_000;
4
+ export class HydraDiscovery {
5
+ opts;
6
+ timer;
7
+ known = new Map();
8
+ stopped = false;
9
+ inFlight = false;
10
+ constructor(opts) {
11
+ this.opts = opts;
12
+ }
13
+ start() {
14
+ log.info(`polling ${this.opts.daemonUrl}/v1/sessions every ${this.opts.pollIntervalMs ?? DEFAULT_POLL_MS}ms`);
15
+ void this.poll();
16
+ this.timer = setInterval(() => {
17
+ void this.poll();
18
+ }, this.opts.pollIntervalMs ?? DEFAULT_POLL_MS);
19
+ }
20
+ stop() {
21
+ this.stopped = true;
22
+ if (this.timer) {
23
+ clearInterval(this.timer);
24
+ this.timer = undefined;
25
+ }
26
+ }
27
+ async poll() {
28
+ if (this.stopped || this.inFlight) {
29
+ return;
30
+ }
31
+ this.inFlight = true;
32
+ try {
33
+ const r = await fetch(`${this.opts.daemonUrl}/v1/sessions`, {
34
+ headers: { Authorization: `Bearer ${this.opts.token}` },
35
+ });
36
+ if (!r.ok) {
37
+ log.warn(`daemon /v1/sessions returned ${r.status}`);
38
+ return;
39
+ }
40
+ const body = (await r.json());
41
+ const seen = new Map();
42
+ for (const s of body.sessions) {
43
+ if (s.status !== "live") {
44
+ continue;
45
+ }
46
+ seen.set(s.sessionId, s);
47
+ }
48
+ for (const [id, s] of seen) {
49
+ if (!this.known.has(id)) {
50
+ this.known.set(id, s);
51
+ try {
52
+ this.opts.onAdd(s);
53
+ }
54
+ catch (err) {
55
+ log.warn(`onAdd error for ${id}: ${err.message}`);
56
+ }
57
+ }
58
+ else {
59
+ this.known.set(id, s);
60
+ }
61
+ }
62
+ for (const id of [...this.known.keys()]) {
63
+ if (!seen.has(id)) {
64
+ this.known.delete(id);
65
+ try {
66
+ this.opts.onRemove(id);
67
+ }
68
+ catch (err) {
69
+ log.warn(`onRemove error for ${id}: ${err.message}`);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ catch (err) {
75
+ log.debug(`poll error: ${err.message}`);
76
+ }
77
+ finally {
78
+ this.inFlight = false;
79
+ }
80
+ }
81
+ }
82
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from "./config.js";
3
+ import { HydraDiscovery } from "./discovery.js";
4
+ import { ApproverBridge } from "./bridge.js";
5
+ import { ABSTAIN_RULE, loadRule } from "./rule.js";
6
+ import { logger, setDebug } from "./util/log.js";
7
+ import { watchConfigPath } from "./util/watch.js";
8
+ const log = logger("main");
9
+ async function main() {
10
+ const config = loadConfig();
11
+ setDebug(config.debug);
12
+ // The current rule function. SIGHUP-triggered reloads mutate this
13
+ // box; bridges re-read it on each request via a thunk so they always
14
+ // see the latest version.
15
+ let currentRule = ABSTAIN_RULE;
16
+ currentRule = await loadRule(config.ruleConfigPath);
17
+ const bridges = new Map();
18
+ const discovery = new HydraDiscovery({
19
+ daemonUrl: config.hydraDaemonUrl,
20
+ token: config.hydraToken,
21
+ pollIntervalMs: config.hydraPollIntervalMs,
22
+ onAdd: (session) => {
23
+ if (bridges.has(session.sessionId)) {
24
+ return;
25
+ }
26
+ log.info(`attaching to ${session.sessionId} agent=${session.agentId ?? "?"} cwd=${session.cwd}`);
27
+ const bridge = new ApproverBridge({
28
+ daemonWsUrl: config.hydraWsUrl,
29
+ token: config.hydraToken,
30
+ meta: {
31
+ sessionId: session.sessionId,
32
+ cwd: session.cwd,
33
+ ...(session.agentId !== undefined
34
+ ? { agentId: session.agentId }
35
+ : {}),
36
+ },
37
+ getRule: () => currentRule,
38
+ });
39
+ bridges.set(session.sessionId, bridge);
40
+ bridge.start();
41
+ },
42
+ onRemove: (sessionId) => {
43
+ const bridge = bridges.get(sessionId);
44
+ if (!bridge) {
45
+ return;
46
+ }
47
+ log.info(`detaching from ${sessionId}`);
48
+ bridges.delete(sessionId);
49
+ bridge.stop();
50
+ },
51
+ });
52
+ discovery.start();
53
+ // Reloads the rule function. Bridges call getRule() on every
54
+ // permission request, so they pick up the new function for the next
55
+ // request after reload completes. In-flight (stashed) responders
56
+ // keep their original abstain behavior and get closed out normally
57
+ // by permission_resolved when a human resolves them.
58
+ const reloadRule = (origin) => {
59
+ log.info(`${origin} — reloading rule from ${config.ruleConfigPath}`);
60
+ loadRule(config.ruleConfigPath)
61
+ .then((rule) => {
62
+ currentRule = rule;
63
+ for (const bridge of bridges.values()) {
64
+ bridge.refreshRule();
65
+ }
66
+ log.info("rule reload complete");
67
+ })
68
+ .catch((err) => {
69
+ log.warn(`rule reload failed: ${err.message}`);
70
+ });
71
+ };
72
+ process.on("SIGHUP", () => reloadRule("SIGHUP"));
73
+ // Auto-reload when the config file is edited. Watches the parent
74
+ // directory so it survives editor temp-file-then-rename and picks up
75
+ // the file even if it didn't exist at startup. SIGHUP stays as a
76
+ // manual fallback for setups where fs.watch is unreliable (NFS,
77
+ // network mounts, etc.).
78
+ const configWatcher = watchConfigPath({
79
+ path: config.ruleConfigPath,
80
+ onChange: () => reloadRule("config file changed"),
81
+ onError: (err) => log.warn(`config watcher error: ${err.message}`),
82
+ });
83
+ const shutdown = (sig) => {
84
+ log.info(`${sig} received — shutting down`);
85
+ configWatcher.stop();
86
+ discovery.stop();
87
+ for (const bridge of bridges.values()) {
88
+ bridge.stop();
89
+ }
90
+ // Give the WS closes a beat to flush, then exit.
91
+ setTimeout(() => process.exit(0), 200).unref();
92
+ };
93
+ process.on("SIGINT", () => shutdown("SIGINT"));
94
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
95
+ log.info(`hydra-acp-approver up; daemon=${config.hydraDaemonUrl} rule=${config.ruleConfigPath}`);
96
+ }
97
+ main().catch((err) => {
98
+ process.stderr.write(`hydra-acp-approver: ${err.message}\n`);
99
+ process.exit(1);
100
+ });
101
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ // Per-bridge router: decodes session/request_permission, calls the user
2
+ // rule fn, and either responds immediately (rule returned an optionId)
3
+ // or stashes the responder until session/permission_resolved arrives
4
+ // from the daemon.
5
+ //
6
+ // Rules can change at runtime via setRule (SIGHUP-driven reload from
7
+ // the entry point). Pending responders are unaffected by a reload —
8
+ // they remain stashed and get closed out when permission_resolved
9
+ // arrives or the WS drops.
10
+ export class PermissionRouter {
11
+ meta;
12
+ log;
13
+ pending = new Map();
14
+ rule;
15
+ constructor(rule, meta, log) {
16
+ this.meta = meta;
17
+ this.log = log;
18
+ this.rule = rule;
19
+ }
20
+ setRule(rule) {
21
+ this.rule = rule;
22
+ }
23
+ async onRequestPermission(id, params, respond) {
24
+ const req = buildRequest(params, this.meta);
25
+ let result;
26
+ try {
27
+ result = await this.rule(req);
28
+ }
29
+ catch (err) {
30
+ this.log.warn(`rule threw on toolCallId=${req.toolCall.toolCallId}: ${err.message}; abstaining`);
31
+ result = null;
32
+ }
33
+ if (result == null) {
34
+ this.stash(req.toolCall.toolCallId, respond);
35
+ this.log.info(`abstain id=${id} toolCallId=${req.toolCall.toolCallId} kind=${req.toolCall.kind ?? "?"}`);
36
+ return;
37
+ }
38
+ const opt = req.options.find((o) => o.optionId === result);
39
+ if (!opt) {
40
+ this.log.warn(`rule returned unknown optionId=${result} for toolCallId=${req.toolCall.toolCallId}; abstaining`);
41
+ this.stash(req.toolCall.toolCallId, respond);
42
+ return;
43
+ }
44
+ respond({ outcome: { outcome: "selected", optionId: result } });
45
+ this.log.info(`approved toolCallId=${req.toolCall.toolCallId} kind=${req.toolCall.kind ?? "?"} optionId=${result} (${opt.kind ?? "?"})`);
46
+ }
47
+ // Another controller answered the request first. Close out our stashed
48
+ // promise (if any) with a cancelled outcome so the JSON-RPC layer
49
+ // doesn't leak a pending request on the daemon side.
50
+ onPermissionResolved(params) {
51
+ const toolCallId = params.toolCall?.toolCallId;
52
+ if (!toolCallId) {
53
+ return;
54
+ }
55
+ const entry = this.pending.get(toolCallId);
56
+ if (!entry) {
57
+ return;
58
+ }
59
+ this.pending.delete(toolCallId);
60
+ entry.respond({ outcome: { outcome: "cancelled" } });
61
+ this.log.debug(`closed out abstained toolCallId=${toolCallId}`);
62
+ }
63
+ // Drop any stashed responders — called on WS close so we don't hold
64
+ // dangling JSON-RPC promises against a connection that's gone.
65
+ shutdown() {
66
+ if (this.pending.size > 0) {
67
+ this.log.debug(`dropping ${this.pending.size} pending responder(s) on shutdown`);
68
+ }
69
+ this.pending.clear();
70
+ }
71
+ stash(toolCallId, respond) {
72
+ this.pending.set(toolCallId, { toolCallId, respond });
73
+ }
74
+ }
75
+ function buildRequest(params, meta) {
76
+ const toolCall = (params.toolCall ?? {});
77
+ const options = Array.isArray(params.options) ? params.options : [];
78
+ return {
79
+ sessionId: meta.sessionId,
80
+ toolCall,
81
+ options,
82
+ ...(meta.cwd !== undefined ? { cwd: meta.cwd } : {}),
83
+ ...(meta.agentId !== undefined ? { agentId: meta.agentId } : {}),
84
+ };
85
+ }
86
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"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"}
package/dist/rule.js ADDED
@@ -0,0 +1,49 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { pathToFileURL } from "node:url";
3
+ import { logger } from "./util/log.js";
4
+ const log = logger("rule");
5
+ // The default rule when no config file is present (or when it fails to
6
+ // load): abstain on every request. Safe-by-default so a freshly
7
+ // installed extension never silently auto-approves anything.
8
+ export const ABSTAIN_RULE = () => null;
9
+ let loadCounter = 0;
10
+ // Loads (or reloads) the user's rule function from `path`. Each call
11
+ // re-imports with a fresh cache-busting query param so SIGHUP-driven
12
+ // reloads pick up edits without restarting the process.
13
+ //
14
+ // Returns ABSTAIN_RULE when the file is missing or fails to import;
15
+ // the caller stays running and human clients keep working as before.
16
+ export async function loadRule(path) {
17
+ try {
18
+ await stat(path);
19
+ }
20
+ catch (err) {
21
+ const e = err;
22
+ if (e.code === "ENOENT") {
23
+ log.info(`no rule config at ${path} — abstaining on every request (drop a JS file at that path to enable auto-approval)`);
24
+ return ABSTAIN_RULE;
25
+ }
26
+ log.warn(`stat ${path} failed: ${e.message}; abstaining`);
27
+ return ABSTAIN_RULE;
28
+ }
29
+ // Cache-bust the dynamic import so reloads see fresh source. Without
30
+ // the query param, Node's ESM loader returns the cached module
31
+ // forever and SIGHUP becomes a no-op.
32
+ loadCounter += 1;
33
+ const url = `${pathToFileURL(path).href}?v=${Date.now()}-${loadCounter}`;
34
+ try {
35
+ const mod = (await import(url));
36
+ const fn = mod.default;
37
+ if (typeof fn !== "function") {
38
+ log.warn(`${path} did not export a default function; abstaining`);
39
+ return ABSTAIN_RULE;
40
+ }
41
+ log.info(`loaded rule function from ${path}`);
42
+ return fn;
43
+ }
44
+ catch (err) {
45
+ log.warn(`import ${path} failed: ${err.message}; abstaining`);
46
+ return ABSTAIN_RULE;
47
+ }
48
+ }
49
+ //# sourceMappingURL=rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.js","sourceRoot":"","sources":["../src/rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AAwB3B,uEAAuE;AACvE,gEAAgE;AAChE,6DAA6D;AAC7D,MAAM,CAAC,MAAM,YAAY,GAAiB,GAAG,EAAE,CAAC,IAAI,CAAC;AAErD,IAAI,WAAW,GAAG,CAAC,CAAC;AAEpB,qEAAqE;AACrE,qEAAqE;AACrE,wDAAwD;AACxD,EAAE;AACF,oEAAoE;AACpE,qEAAqE;AACrE,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAA4B,CAAC;QACvC,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACxB,GAAG,CAAC,IAAI,CACN,qBAAqB,IAAI,sFAAsF,CAChH,CAAC;YACF,OAAO,YAAY,CAAC;QACtB,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC;QAC1D,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,qEAAqE;IACrE,+DAA+D;IAC/D,sCAAsC;IACtC,WAAW,IAAI,CAAC,CAAC;IACjB,MAAM,GAAG,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI,CAAC,GAAG,EAAE,IAAI,WAAW,EAAE,CAAC;IACzE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,CAA0B,CAAC;QACzD,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;QACvB,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,gDAAgD,CAAC,CAAC;YAClE,OAAO,YAAY,CAAC;QACtB,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC;QAC9C,OAAO,EAAkB,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,YAAa,GAAa,CAAC,OAAO,cAAc,CAAC,CAAC;QACzE,OAAO,YAAY,CAAC;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,46 @@
1
+ let debugEnabled = false;
2
+ export function setDebug(on) {
3
+ debugEnabled = on;
4
+ }
5
+ function emit(level, scope, args) {
6
+ const ts = new Date().toISOString();
7
+ const stream = level === "error" || level === "warn" ? process.stderr : process.stdout;
8
+ stream.write(`[${ts}] ${level} [${scope}] ${formatArgs(args)}\n`);
9
+ }
10
+ function formatArgs(args) {
11
+ return args
12
+ .map((a) => {
13
+ if (typeof a === "string") {
14
+ return a;
15
+ }
16
+ if (a instanceof Error) {
17
+ return a.stack ?? a.message;
18
+ }
19
+ try {
20
+ return JSON.stringify(a);
21
+ }
22
+ catch {
23
+ return String(a);
24
+ }
25
+ })
26
+ .join(" ");
27
+ }
28
+ export function logger(scope) {
29
+ return {
30
+ debug(...args) {
31
+ if (debugEnabled) {
32
+ emit("debug", scope, args);
33
+ }
34
+ },
35
+ info(...args) {
36
+ emit("info", scope, args);
37
+ },
38
+ warn(...args) {
39
+ emit("warn", scope, args);
40
+ },
41
+ error(...args) {
42
+ emit("error", scope, args);
43
+ },
44
+ };
45
+ }
46
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/util/log.ts"],"names":[],"mappings":"AAEA,IAAI,YAAY,GAAG,KAAK,CAAC;AAEzB,MAAM,UAAU,QAAQ,CAAC,EAAW;IAClC,YAAY,GAAG,EAAE,CAAC;AACpB,CAAC;AAED,SAAS,IAAI,CAAC,KAAY,EAAE,KAAa,EAAE,IAAe;IACxD,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,MAAM,MAAM,GAAG,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACvF,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK,KAAK,KAAK,KAAK,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,UAAU,CAAC,IAAe;IACjC,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,CAAC,YAAY,KAAK,EAAE,CAAC;YACvB,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;SACD,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AASD,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO;QACL,KAAK,CAAC,GAAG,IAAe;YACtB,IAAI,YAAY,EAAE,CAAC;gBACjB,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QACD,IAAI,CAAC,GAAG,IAAe;YACrB,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QACD,IAAI,CAAC,GAAG,IAAe;YACrB,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QACD,KAAK,CAAC,GAAG,IAAe;YACtB,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { realpathSync, watch } from "node:fs";
2
+ import { dirname, basename, resolve } from "node:path";
3
+ // Watch a config file by watching its parent directory and filtering on
4
+ // basename. This survives editors that write a temp file and rename
5
+ // over the target (vim, helix, JetBrains) — those flip the inode, which
6
+ // a direct `fs.watch(path)` would stop tracking. It also gracefully
7
+ // handles the file not existing yet at startup: as soon as a matching
8
+ // filename appears in the directory, the watcher fires.
9
+ //
10
+ // Symlinks: inotify on a directory only sees changes to entries in that
11
+ // directory; editing the target of a symlink that lives there does not
12
+ // touch the symlink itself, so a naive single watcher misses edits
13
+ // through the symlink. We resolve the realpath at startup and, when it
14
+ // differs from the literal path, watch both parents — the literal one
15
+ // to catch the symlink being created/swapped/removed, and the realpath
16
+ // one to catch edits to the actual file. If the symlink doesn't exist
17
+ // yet we just watch the literal parent and the realpath watcher is
18
+ // added later (on the next reload, the caller can recreate us).
19
+ export function watchConfigPath(opts) {
20
+ const literal = resolve(opts.path);
21
+ let real = literal;
22
+ try {
23
+ real = realpathSync(literal);
24
+ }
25
+ catch {
26
+ // file doesn't exist yet; watching the literal parent is enough.
27
+ }
28
+ const debounce = opts.debounceMs ?? 200;
29
+ let timer;
30
+ const watchers = [];
31
+ const trigger = () => {
32
+ if (timer) {
33
+ clearTimeout(timer);
34
+ }
35
+ timer = setTimeout(() => {
36
+ timer = undefined;
37
+ opts.onChange();
38
+ }, debounce);
39
+ };
40
+ const armParentWatcher = (filePath) => {
41
+ const dir = dirname(filePath);
42
+ const file = basename(filePath);
43
+ try {
44
+ const w = watch(dir, { persistent: false }, (_event, name) => {
45
+ if (name && name.toString() === file) {
46
+ trigger();
47
+ }
48
+ });
49
+ if (opts.onError) {
50
+ w.on("error", opts.onError);
51
+ }
52
+ watchers.push(w);
53
+ }
54
+ catch (err) {
55
+ if (opts.onError) {
56
+ opts.onError(err);
57
+ }
58
+ }
59
+ };
60
+ armParentWatcher(literal);
61
+ if (real !== literal) {
62
+ armParentWatcher(real);
63
+ }
64
+ return {
65
+ stop: () => {
66
+ if (timer) {
67
+ clearTimeout(timer);
68
+ }
69
+ for (const w of watchers) {
70
+ w.close();
71
+ }
72
+ },
73
+ };
74
+ }
75
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/util/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,EAAkB,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAevD,wEAAwE;AACxE,oEAAoE;AACpE,wEAAwE;AACxE,oEAAoE;AACpE,sEAAsE;AACtE,wDAAwD;AACxD,EAAE;AACF,wEAAwE;AACxE,uEAAuE;AACvE,mEAAmE;AACnE,uEAAuE;AACvE,sEAAsE;AACtE,uEAAuE;AACvE,sEAAsE;AACtE,mEAAmE;AACnE,gEAAgE;AAChE,MAAM,UAAU,eAAe,CAAC,IAAkB;IAChD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,IAAI,GAAG,OAAO,CAAC;IACnB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;IACnE,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC;IACxC,IAAI,KAAiC,CAAC;IACtC,MAAM,QAAQ,GAAgB,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,KAAK,EAAE,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;QACD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,KAAK,GAAG,SAAS,CAAC;YAClB,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,CAAC,EAAE,QAAQ,CAAC,CAAC;IACf,CAAC,CAAC;IACF,MAAM,gBAAgB,GAAG,CAAC,QAAgB,EAAQ,EAAE;QAClD,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;gBAC3D,IAAI,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC;oBACrC,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC,CAAC,CAAC;YACH,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjB,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YAC9B,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjB,IAAI,CAAC,OAAO,CAAC,GAAY,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1B,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IACD,OAAO;QACL,IAAI,EAAE,GAAS,EAAE;YACf,IAAI,KAAK,EAAE,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;gBACzB,CAAC,CAAC,KAAK,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@hydra-acp/approver",
3
+ "version": "0.1.0",
4
+ "description": "Headless permission auto-approver extension for hydra-acp.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "main": "dist/index.js",
17
+ "bin": {
18
+ "hydra-acp-approver": "dist/index.js"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p .",
22
+ "watch": "tsc -p . --watch",
23
+ "start": "node dist/index.js",
24
+ "dev": "tsc -p . && node dist/index.js",
25
+ "test": "node --test --import tsx 'test/**/*.test.ts'",
26
+ "lint": "tsc -p . --noEmit",
27
+ "prepublishOnly": "npm run build && npm test"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "dependencies": {
33
+ "ws": "^8.20.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.10.0",
37
+ "@types/ws": "^8.18.1",
38
+ "tsx": "^4.20.0",
39
+ "typescript": "^5.6.3"
40
+ }
41
+ }