@hydra-acp/browser 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,258 @@
1
+ # hydra-acp-browser
2
+
3
+ A browser-based UI for [hydra-acp](https://github.com/smagnuso/hydra-acp)
4
+ sessions. Runs as a hydra extension (or standalone) and serves a small
5
+ single-page app on localhost that lists live sessions, mirrors them in real
6
+ time, and lets you prompt, approve permission requests, switch modes/models,
7
+ create fresh sessions, kill old ones, and browse the project files of any
8
+ session — all from a phone or laptop browser.
9
+
10
+ The hydra master token never leaves the machine; the browser authenticates
11
+ with a separate per-host authkey instead.
12
+
13
+ ## How it works
14
+
15
+ ```
16
+ hydra REST +-------------------+ browser
17
+ /v1/sessions <---------- | | ----> GET /
18
+ | hydra-acp-browser | <----> /ws?session=<id>
19
+ hydra WSS <----------> | |
20
+ /acp +-------------------+
21
+ |
22
+ ~/.hydra-acp-browser/
23
+ authkey
24
+ link
25
+ ```
26
+
27
+ The extension exposes:
28
+
29
+ - **HTTP routes** at `/api/sessions` (GET list, POST create), `/api/agents`,
30
+ `/api/kill`, `/api/files/list`, `/api/files/read`, `/api/health`.
31
+ - **A WebSocket bridge** at `/ws?session=<id>`. Each browser tab gets its
32
+ own attach to hydra's `/acp`; ACP frames flow through unchanged in
33
+ the upstream→browser direction. Browser→upstream traffic is
34
+ method-whitelisted (`session/prompt`, `session/cancel`, `session/set_mode`,
35
+ `session/set_model`, plus permission responses) so a tab can't issue
36
+ arbitrary admin calls.
37
+
38
+ ## Setup
39
+
40
+ 1. **Install or build.**
41
+
42
+ From npm (recommended once published):
43
+
44
+ ```sh
45
+ npm install -g @hydra-acp/browser
46
+ ```
47
+
48
+ This drops an `hydra-acp-browser` binary on your PATH.
49
+
50
+ Or from source:
51
+
52
+ ```sh
53
+ git clone https://github.com/smagnuso/hydra-acp-browser.git ~/dev/hydra-acp-browser
54
+ cd ~/dev/hydra-acp-browser
55
+ npm install
56
+ npm run build
57
+ ```
58
+
59
+ 2. **Run as a hydra extension (recommended).** Register the extension
60
+ with hydra. If installed via npm:
61
+
62
+ ```sh
63
+ hydra-acp extensions add hydra-acp-browser --command hydra-acp-browser
64
+ ```
65
+
66
+ Or pointed at a local build:
67
+
68
+ ```sh
69
+ hydra-acp extensions add hydra-acp-browser \
70
+ --command node \
71
+ --args ~/dev/hydra-acp-browser/dist/index.js
72
+ ```
73
+
74
+ That writes the equivalent entry into `~/.hydra-acp/config.json`:
75
+
76
+ ```json
77
+ {
78
+ "extensions": {
79
+ "hydra-acp-browser": {
80
+ "command": ["node"],
81
+ "args": ["/home/you/dev/hydra-acp-browser/dist/index.js"],
82
+ "enabled": true
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ `extensions add` is config-only — it doesn't spawn anything yet.
89
+ Either bounce the daemon, or, if the daemon is already running,
90
+ kick the extension into life:
91
+
92
+ ```sh
93
+ hydra-acp extensions start hydra-acp-browser
94
+ ```
95
+
96
+ On startup, hydra spawns hydra-acp-browser with these env vars set:
97
+ `HYDRA_ACP_DAEMON_URL`, `HYDRA_ACP_TOKEN`, `HYDRA_ACP_WS_URL`. The
98
+ first launch generates `~/.hydra-acp-browser/authkey` and writes
99
+ the open URL (with `?authkey=…`) to `~/.hydra-acp-browser/link`.
100
+ Stdout/stderr land in `~/.hydra-acp/extensions/hydra-acp-browser.log`.
101
+ Lifecycle is managed with
102
+ `hydra-acp extensions start|stop|restart hydra-acp-browser` —
103
+ `restart` is the right call after `npm run build`. Tail the log
104
+ with `hydra-acp extensions logs hydra-acp-browser -f` (the open URL
105
+ shows up there on first launch).
106
+
107
+ 3. **Run standalone (alternative).** Set `HYDRA_TOKEN` in
108
+ `~/.hydra-acp-browser.conf` (or export `HYDRA_ACP_TOKEN`), then:
109
+
110
+ ```sh
111
+ npm start
112
+ ```
113
+
114
+ 4. **Open the browser** to the URL printed on stderr. The first request
115
+ sets a cookie; subsequent requests are authenticated by the cookie
116
+ alone. The URL is also at `~/.hydra-acp-browser/link` for convenience.
117
+
118
+ ## HTTPS
119
+
120
+ Optional on `127.0.0.1`, **required** for any non-loopback bind (the server
121
+ refuses otherwise — same rule as the hydra daemon). The simplest setup
122
+ is a self-signed cert in `~/.hydra-acp-browser/tls/`.
123
+
124
+ 1. **Generate cert + key.** ECDSA P-256, 5-year validity, with a SAN
125
+ covering loopback. Add any extra hostnames you'll hit it from
126
+ (Tailscale name, LAN IP, etc.) to the SAN inline:
127
+
128
+ ```sh
129
+ mkdir -p ~/.hydra-acp-browser/tls && chmod 700 ~/.hydra-acp-browser/tls
130
+ cd ~/.hydra-acp-browser/tls
131
+
132
+ SAN='subjectAltName=DNS:localhost,DNS:'"$(hostname)"',IP:127.0.0.1,IP:::1'
133
+ # ^ add ,DNS:my.tailnet.ts.net or ,IP:100.64.x.y if needed.
134
+
135
+ openssl req -x509 \
136
+ -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
137
+ -sha256 -days 1825 -nodes \
138
+ -keyout key.pem -out cert.pem \
139
+ -subj "/CN=hydra-acp-browser" \
140
+ -addext "$SAN" \
141
+ -addext "extendedKeyUsage=serverAuth"
142
+ chmod 600 key.pem cert.pem
143
+ ```
144
+
145
+ Verify the SAN landed:
146
+
147
+ ```sh
148
+ openssl x509 -in cert.pem -noout -text | grep -A1 'Subject Alternative Name'
149
+ ```
150
+
151
+ The cert's CN doesn't matter to modern browsers — only the SAN does.
152
+ Skipping `-addext "subjectAltName=…"` will make every browser reject
153
+ the cert with `NET::ERR_CERT_COMMON_NAME_INVALID`.
154
+
155
+ 2. **Wire into config.** Append to `~/.hydra-acp-browser.conf`:
156
+
157
+ ```sh
158
+ BROWSER_TLS_CERT=~/.hydra-acp-browser/tls/cert.pem
159
+ BROWSER_TLS_KEY=~/.hydra-acp-browser/tls/key.pem
160
+ ```
161
+
162
+ To expose beyond loopback, also set:
163
+
164
+ ```sh
165
+ BROWSER_HOST=0.0.0.0
166
+ BROWSER_ALLOWED_HOSTS=mybox,mybox.tailnet.ts.net,100.64.1.5
167
+ ```
168
+
169
+ Every entry in `BROWSER_ALLOWED_HOSTS` must also be in the cert's SAN.
170
+
171
+ 3. **Apply** with `hydra-acp extensions restart hydra-acp-browser`. The
172
+ log line should now read `listening on https://…` and the
173
+ `Open: https://…/?authkey=…` URL is what you load. The auth cookie
174
+ carries `Secure` automatically when serving HTTPS.
175
+
176
+ 4. **Trust the cert.** Self-signed certs trip browser warnings.
177
+ - **Click-through:** open the URL, accept the warning. Per-site only.
178
+ - **Linux Chrome/Chromium:**
179
+ `certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n hydra-acp-browser -i ~/.hydra-acp-browser/tls/cert.pem`
180
+ - **macOS:** double-click `cert.pem`, add to System keychain, set
181
+ "Always Trust" in Get Info.
182
+ - **iOS:** AirDrop/email `cert.pem` to the device, install profile
183
+ (Settings → General → VPN & Device Management), then enable under
184
+ Settings → General → About → Certificate Trust Settings.
185
+
186
+ If you're already on Tailscale, [`tailscale cert`](https://tailscale.com/kb/1153/enabling-https)
187
+ issues a real Let's Encrypt cert for `<host>.tailnet.ts.net` — strictly
188
+ better than self-signed (no trust prompts, ~30 s setup). Drop the
189
+ output paths into `BROWSER_TLS_CERT` / `BROWSER_TLS_KEY` and skip
190
+ step 4.
191
+
192
+ If you flip-flop between HTTP and HTTPS, the `Secure` cookie set under
193
+ HTTPS won't be sent over plain HTTP. Run
194
+ `hydra-acp-browser --rotate-authkey` to start fresh.
195
+
196
+ ## Configuration keys
197
+
198
+ `~/.hydra-acp-browser.conf` (KEY=VALUE). All keys are optional unless noted.
199
+
200
+ | Key | Default | Notes |
201
+ |------------------------------|----------------------------------------|-------|
202
+ | `BROWSER_HOST` | `127.0.0.1` | Bind host. Non-loopback requires TLS. |
203
+ | `BROWSER_PORT` | `9099` | Listen port. |
204
+ | `BROWSER_TLS_CERT` | (none) | If set with `BROWSER_TLS_KEY`, listen on HTTPS. |
205
+ | `BROWSER_TLS_KEY` | (none) | Path to TLS key. |
206
+ | `BROWSER_AUTHKEY_FILE` | `~/.hydra-acp-browser/authkey` | Where the browser-side authkey lives. |
207
+ | `BROWSER_LINK_FILE` | `~/.hydra-acp-browser/link` | URL written for convenience. |
208
+ | `BROWSER_ALLOWED_HOSTS` | empty | Comma-sep extra Host values for DNS-rebind allowlist (e.g. Tailscale name). |
209
+ | `BROWSER_FILE_MAX_BYTES` | `262144` | Upper bound for `/api/files/read`. |
210
+ | `HYDRA_DAEMON_URL` | from env / `http://127.0.0.1:8765` | `HYDRA_ACP_DAEMON_URL` env wins. |
211
+ | `HYDRA_WS_URL` | derived | `HYDRA_ACP_WS_URL` env wins. |
212
+ | `HYDRA_TOKEN` | (required) | Same precedence as the slack ext. |
213
+ | `DEBUG` | `false` | Verbose logging. |
214
+
215
+ ## Security
216
+
217
+ - **Authkey vs. hydra token.** The browser only ever sees a per-host
218
+ authkey (32 bytes, hex). The hydra master token stays on the server.
219
+ - **Loopback or TLS.** The server refuses to bind a non-loopback host
220
+ unless `BROWSER_TLS_CERT` and `BROWSER_TLS_KEY` are configured —
221
+ mirrors hydra's daemon.
222
+ - **DNS-rebind protection.** The `Host` header must match
223
+ `127.0.0.1[:port]`, `localhost[:port]`, or an entry in
224
+ `BROWSER_ALLOWED_HOSTS`.
225
+ - **CSRF.** State-changing requests check `Origin` (against
226
+ `<scheme>://<allowed-host>:<port>`) and `Sec-Fetch-Site`
227
+ (`same-origin` / `none` only).
228
+ - **CSP.** The HTML response carries a per-request nonce; only
229
+ `'self'` and the matching nonce are allowed for scripts/styles.
230
+ - **Rate limit.** 10 failed auth attempts in 15 min from a single
231
+ remote IP triggers a `429` until the window rolls.
232
+ - **WS method whitelist.** A compromised tab can only send
233
+ `session/prompt`, `session/cancel`, `session/set_mode`,
234
+ `session/set_model`, plus responses to permission requests it has
235
+ actually been forwarded.
236
+ - **`fs/*` reverse calls** from agents are rejected at the bridge so a
237
+ tab can't accidentally expose the user's filesystem to the agent
238
+ via this surface.
239
+
240
+ ## Tests
241
+
242
+ ```sh
243
+ npm test
244
+ ```
245
+
246
+ Runs the auth, CSRF, file-traversal, and bridge-whitelist tests
247
+ under the built-in Node test runner.
248
+
249
+ ## Status
250
+
251
+ Experimental. v1 covers list / chat / tool calls / permissions /
252
+ session create / kill / file browse / mode + model picker. Out of scope:
253
+ multi-user UI, image upload from the browser into the agent,
254
+ transcript search.
255
+
256
+ ## License
257
+
258
+ MIT.
package/dist/config.js ADDED
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { expandHome, paths } from "./util/paths.js";
3
+ const TRUTHY = new Set(["1", "true", "yes", "on", "t"]);
4
+ function parseEnvFile(text) {
5
+ const out = new Map();
6
+ for (const rawLine of text.split(/\r?\n/)) {
7
+ const line = rawLine.trim();
8
+ if (!line || line.startsWith("#")) {
9
+ continue;
10
+ }
11
+ const eq = line.indexOf("=");
12
+ if (eq === -1) {
13
+ continue;
14
+ }
15
+ const key = line.slice(0, eq).trim();
16
+ let val = line.slice(eq + 1).trim();
17
+ if ((val.startsWith('"') && val.endsWith('"')) ||
18
+ (val.startsWith("'") && val.endsWith("'"))) {
19
+ val = val.slice(1, -1);
20
+ }
21
+ out.set(key, val);
22
+ }
23
+ return out;
24
+ }
25
+ function deriveWsUrl(httpUrl) {
26
+ if (httpUrl.startsWith("https://")) {
27
+ return ("wss://" + httpUrl.slice("https://".length).replace(/\/$/, "") + "/acp");
28
+ }
29
+ if (httpUrl.startsWith("http://")) {
30
+ return ("ws://" + httpUrl.slice("http://".length).replace(/\/$/, "") + "/acp");
31
+ }
32
+ throw new Error(`hydraDaemonUrl must start with http:// or https://: ${httpUrl}`);
33
+ }
34
+ function bool(map, key, fallback) {
35
+ const v = map.get(key);
36
+ if (v === undefined) {
37
+ return fallback;
38
+ }
39
+ return TRUTHY.has(v.toLowerCase());
40
+ }
41
+ function intVal(map, key, fallback) {
42
+ const v = map.get(key);
43
+ if (v === undefined || v.length === 0) {
44
+ return fallback;
45
+ }
46
+ const n = Number.parseInt(v, 10);
47
+ return Number.isFinite(n) ? n : fallback;
48
+ }
49
+ function commaList(map, key) {
50
+ const v = map.get(key);
51
+ if (!v) {
52
+ return [];
53
+ }
54
+ return v
55
+ .split(",")
56
+ .map((s) => s.trim())
57
+ .filter((s) => s.length > 0);
58
+ }
59
+ export function loadConfig(path = paths.configFile()) {
60
+ let text = "";
61
+ try {
62
+ text = readFileSync(path, "utf8");
63
+ }
64
+ catch {
65
+ // Config file is optional; defaults + env vars cover the required keys.
66
+ }
67
+ const map = parseEnvFile(text);
68
+ const hydraDaemonUrl = process.env.HYDRA_ACP_DAEMON_URL ??
69
+ map.get("HYDRA_DAEMON_URL") ??
70
+ "http://127.0.0.1:8765";
71
+ const hydraToken = process.env.HYDRA_ACP_TOKEN ?? map.get("HYDRA_TOKEN") ?? "";
72
+ if (!hydraToken) {
73
+ throw new Error("Missing HYDRA_ACP_TOKEN env var (or HYDRA_TOKEN config key). When run as a hydra extension, hydra injects this automatically; otherwise set it in ~/.hydra-acp-browser.conf.");
74
+ }
75
+ const hydraWsUrl = process.env.HYDRA_ACP_WS_URL ??
76
+ map.get("HYDRA_WS_URL") ??
77
+ deriveWsUrl(hydraDaemonUrl);
78
+ const tlsCert = map.get("BROWSER_TLS_CERT");
79
+ const tlsKey = map.get("BROWSER_TLS_KEY");
80
+ let tls;
81
+ if (tlsCert && tlsKey) {
82
+ tls = { cert: expandHome(tlsCert), key: expandHome(tlsKey) };
83
+ }
84
+ else if (tlsCert || tlsKey) {
85
+ throw new Error("BROWSER_TLS_CERT and BROWSER_TLS_KEY must both be set or both omitted.");
86
+ }
87
+ return {
88
+ browserHost: map.get("BROWSER_HOST") ?? "127.0.0.1",
89
+ browserPort: intVal(map, "BROWSER_PORT", 9099),
90
+ tls,
91
+ authkeyFile: expandHome(map.get("BROWSER_AUTHKEY_FILE") ?? paths.authkeyFile()),
92
+ linkFile: expandHome(map.get("BROWSER_LINK_FILE") ?? paths.linkFile()),
93
+ allowedHosts: commaList(map, "BROWSER_ALLOWED_HOSTS"),
94
+ fileMaxBytes: intVal(map, "BROWSER_FILE_MAX_BYTES", 256 * 1024),
95
+ hydraDaemonUrl,
96
+ hydraWsUrl,
97
+ hydraToken,
98
+ debug: bool(map, "DEBUG", false),
99
+ };
100
+ }
101
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAqBpD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAExD,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAClC,SAAS;QACX,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;YACd,SAAS;QACX,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IACE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC1C,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAC1C,CAAC;YACD,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,CACL,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CACxE,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAClC,OAAO,CACL,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CACtE,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,KAAK,CACb,uDAAuD,OAAO,EAAE,CACjE,CAAC;AACJ,CAAC;AAED,SAAS,IAAI,CAAC,GAAwB,EAAE,GAAW,EAAE,QAAiB;IACpE,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,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,SAAS,MAAM,CACb,GAAwB,EACxB,GAAW,EACX,QAAgB;IAEhB,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtC,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,SAAS,SAAS,CAAC,GAAwB,EAAE,GAAW;IACtD,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,CAAC;SACL,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAe,KAAK,CAAC,UAAU,EAAE;IAC1D,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;IAC1E,CAAC;IACD,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAE/B,MAAM,cAAc,GAClB,OAAO,CAAC,GAAG,CAAC,oBAAoB;QAChC,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAC3B,uBAAuB,CAAC;IAC1B,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;IAC9D,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,8KAA8K,CAC/K,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,CAAC,gBAAgB;QAC5B,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC;QACvB,WAAW,CAAC,cAAc,CAAC,CAAC;IAE9B,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC1C,IAAI,GAA0B,CAAC;IAC/B,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;QACtB,GAAG,GAAG,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;IAC/D,CAAC;SAAM,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;IACJ,CAAC;IAED,OAAO;QACL,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,WAAW;QACnD,WAAW,EAAE,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC;QAC9C,GAAG;QACH,WAAW,EAAE,UAAU,CACrB,GAAG,CAAC,GAAG,CAAC,sBAAsB,CAAC,IAAI,KAAK,CAAC,WAAW,EAAE,CACvD;QACD,QAAQ,EAAE,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACtE,YAAY,EAAE,SAAS,CAAC,GAAG,EAAE,uBAAuB,CAAC;QACrD,YAAY,EAAE,MAAM,CAAC,GAAG,EAAE,wBAAwB,EAAE,GAAG,GAAG,IAAI,CAAC;QAC/D,cAAc;QACd,UAAU;QACV,UAAU;QACV,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC;KACjC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { logger } from "../util/log.js";
2
+ const log = logger("hydra-rest");
3
+ export class HydraRestClient {
4
+ baseUrl;
5
+ token;
6
+ constructor(baseUrl, token) {
7
+ this.baseUrl = baseUrl;
8
+ this.token = token;
9
+ }
10
+ async json(method, path, body) {
11
+ const init = {
12
+ method,
13
+ headers: {
14
+ Authorization: `Bearer ${this.token}`,
15
+ "Content-Type": "application/json",
16
+ },
17
+ };
18
+ if (body !== undefined) {
19
+ init.body = JSON.stringify(body);
20
+ }
21
+ const r = await fetch(`${this.baseUrl}${path}`, init);
22
+ if (!r.ok) {
23
+ const text = await r.text().catch(() => "");
24
+ log.warn(`${method} ${path} → ${r.status} ${text.slice(0, 200)}`);
25
+ throw new HydraRestError(r.status, `${method} ${path}: ${r.status}`);
26
+ }
27
+ if (r.status === 204) {
28
+ return undefined;
29
+ }
30
+ return (await r.json());
31
+ }
32
+ async health() {
33
+ return this.json("GET", "/v1/health");
34
+ }
35
+ async listSessions(opts) {
36
+ const qs = new URLSearchParams();
37
+ if (opts?.cwd) {
38
+ qs.set("cwd", opts.cwd);
39
+ }
40
+ if (opts?.all) {
41
+ qs.set("all", "true");
42
+ }
43
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
44
+ return this.json("GET", `/v1/sessions${suffix}`);
45
+ }
46
+ async deleteSession(sessionId) {
47
+ await this.json("DELETE", `/v1/sessions/${encodeURIComponent(sessionId)}`);
48
+ }
49
+ async listAgents() {
50
+ return this.json("GET", "/v1/agents");
51
+ }
52
+ }
53
+ export class HydraRestError extends Error {
54
+ status;
55
+ constructor(status, message) {
56
+ super(message);
57
+ this.status = status;
58
+ this.name = "HydraRestError";
59
+ }
60
+ }
61
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/hydra/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAqBjC,MAAM,OAAO,eAAe;IAEP;IACA;IAFnB,YACmB,OAAe,EACf,KAAa;QADb,YAAO,GAAP,OAAO,CAAQ;QACf,UAAK,GAAL,KAAK,CAAQ;IAC7B,CAAC;IAEI,KAAK,CAAC,IAAI,CAChB,MAAc,EACd,IAAY,EACZ,IAAc;QAEd,MAAM,IAAI,GAAgB;YACxB,MAAM;YACN,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE;gBACrC,cAAc,EAAE,kBAAkB;aACnC;SACF,CAAC;QACF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC5C,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YAClE,MAAM,IAAI,cAAc,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,MAAM,IAAI,IAAI,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,SAAc,CAAC;QACxB,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAM,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,IAGlB;QACC,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;QACjC,IAAI,IAAI,EAAE,GAAG,EAAE,CAAC;YACd,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,IAAI,EAAE,GAAG,EAAE,CAAC;YACd,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACxB,CAAC;QACD,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,eAAe,MAAM,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,SAAiB;QACnC,MAAM,IAAI,CAAC,IAAI,CACb,QAAQ,EACR,gBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAChD,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,UAAU;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAErB;IADlB,YACkB,MAAc,EAC9B,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,WAAM,GAAN,MAAM,CAAQ;QAI9B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,178 @@
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 log = logger("hydra-ws");
6
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
7
+ export function isRequest(m) {
8
+ return "method" in m && "id" in m;
9
+ }
10
+ export function isNotification(m) {
11
+ return "method" in m && !("id" in m);
12
+ }
13
+ export function isResponse(m) {
14
+ return !("method" in m) && "id" in m;
15
+ }
16
+ // Thin wrapper around an outbound WSS connection to hydra's `/acp` endpoint.
17
+ // Authenticates via the `hydra-acp-token.<token>` subprotocol (alongside
18
+ // `acp.v1`), exposes JSON-RPC request/notify primitives and an event stream
19
+ // for inbound traffic. Handshake (initialize + session/attach or session/new)
20
+ // is the caller's responsibility — see ws-bridge.ts and routes-sessions.ts.
21
+ export class UpstreamConnection extends EventEmitter {
22
+ opts;
23
+ ws;
24
+ nextId = 1;
25
+ pending = new Map();
26
+ connected = false;
27
+ closed = false;
28
+ constructor(opts) {
29
+ super();
30
+ this.opts = opts;
31
+ }
32
+ get isConnected() {
33
+ return this.connected;
34
+ }
35
+ get clientName() {
36
+ return this.opts.clientName ?? "hydra-acp-browser";
37
+ }
38
+ get clientVersion() {
39
+ return this.opts.clientVersion ?? pkg.version;
40
+ }
41
+ start() {
42
+ log.debug(`connecting ${this.opts.daemonWsUrl}`);
43
+ const subprotocols = ["acp.v1", `hydra-acp-token.${this.opts.token}`];
44
+ let ws;
45
+ try {
46
+ ws = new WebSocket(this.opts.daemonWsUrl, subprotocols);
47
+ }
48
+ catch (err) {
49
+ this.emit("error", err);
50
+ return;
51
+ }
52
+ this.ws = ws;
53
+ ws.on("open", () => {
54
+ this.connected = true;
55
+ this.emit("open");
56
+ });
57
+ ws.on("message", (data, isBinary) => {
58
+ if (isBinary) {
59
+ return;
60
+ }
61
+ const text = data.toString("utf8");
62
+ try {
63
+ const parsed = JSON.parse(text);
64
+ this.onMessage(parsed);
65
+ }
66
+ catch (err) {
67
+ log.warn(`parse error: ${err.message}; raw=${text.slice(0, 200)}`);
68
+ }
69
+ });
70
+ ws.on("error", (err) => {
71
+ log.warn(`ws error: ${err.message}`);
72
+ this.emit("error", err);
73
+ });
74
+ ws.on("close", (code, reason) => {
75
+ this.connected = false;
76
+ this.closed = true;
77
+ const reasonText = reason.toString("utf8");
78
+ for (const [, p] of this.pending) {
79
+ p.reject(new Error("ws closed"));
80
+ }
81
+ this.pending.clear();
82
+ this.emit("close", { code, reason: reasonText });
83
+ });
84
+ }
85
+ stop() {
86
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
87
+ try {
88
+ this.ws.close();
89
+ }
90
+ catch {
91
+ void 0;
92
+ }
93
+ }
94
+ }
95
+ async request(method, params) {
96
+ const id = this.nextId++;
97
+ const msg = {
98
+ jsonrpc: "2.0",
99
+ id,
100
+ method,
101
+ ...(params !== undefined ? { params } : {}),
102
+ };
103
+ this.write(msg);
104
+ return new Promise((resolve, reject) => {
105
+ this.pending.set(id, {
106
+ resolve: (resp) => {
107
+ if (resp.error) {
108
+ reject(new Error(`${resp.error.code}: ${resp.error.message}`));
109
+ }
110
+ else {
111
+ resolve(resp.result);
112
+ }
113
+ },
114
+ reject,
115
+ });
116
+ });
117
+ }
118
+ notify(method, params) {
119
+ const msg = {
120
+ jsonrpc: "2.0",
121
+ method,
122
+ ...(params !== undefined ? { params } : {}),
123
+ };
124
+ this.write(msg);
125
+ }
126
+ reply(id, result) {
127
+ const msg = { jsonrpc: "2.0", id, result };
128
+ this.write(msg);
129
+ }
130
+ replyError(id, code, message) {
131
+ const msg = {
132
+ jsonrpc: "2.0",
133
+ id,
134
+ error: { code, message },
135
+ };
136
+ this.write(msg);
137
+ }
138
+ // Send a frame as-is. Used by the WS bridge to forward browser-originated
139
+ // JSON-RPC messages to hydra unchanged (after method-whitelist validation).
140
+ sendRaw(frame) {
141
+ this.write(frame);
142
+ }
143
+ write(msg) {
144
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
145
+ if (!this.closed) {
146
+ log.warn(`drop write to closed ws: ${JSON.stringify(msg).slice(0, 200)}`);
147
+ }
148
+ return;
149
+ }
150
+ this.ws.send(JSON.stringify(msg));
151
+ }
152
+ onMessage(m) {
153
+ if (isResponse(m)) {
154
+ const p = this.pending.get(m.id);
155
+ if (p) {
156
+ this.pending.delete(m.id);
157
+ p.resolve(m);
158
+ }
159
+ this.emit("response", m);
160
+ }
161
+ else if (isRequest(m)) {
162
+ this.emit("request", m);
163
+ }
164
+ else if (isNotification(m)) {
165
+ this.emit("notification", m);
166
+ }
167
+ }
168
+ }
169
+ export async function runInitialize(conn, opts = {}) {
170
+ await conn.request("initialize", {
171
+ protocolVersion: opts.protocolVersion ?? 1,
172
+ clientCapabilities: opts.clientCapabilities ?? {
173
+ fs: { readTextFile: false, writeTextFile: false },
174
+ terminal: false,
175
+ },
176
+ });
177
+ }
178
+ //# sourceMappingURL=ws.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.js","sourceRoot":"","sources":["../../src/hydra/ws.ts"],"names":[],"mappings":"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"}