@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 +21 -0
- package/README.md +258 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/hydra/client.js +61 -0
- package/dist/hydra/client.js.map +1 -0
- package/dist/hydra/ws.js +178 -0
- package/dist/hydra/ws.js.map +1 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/server/auth.js +104 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/http.js +128 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/routes-agents.js +14 -0
- package/dist/server/routes-agents.js.map +1 -0
- package/dist/server/routes-files.js +170 -0
- package/dist/server/routes-files.js.map +1 -0
- package/dist/server/routes-root.js +67 -0
- package/dist/server/routes-root.js.map +1 -0
- package/dist/server/routes-sessions.js +104 -0
- package/dist/server/routes-sessions.js.map +1 -0
- package/dist/server/title-cache.js +59 -0
- package/dist/server/title-cache.js.map +1 -0
- package/dist/server/ws-bridge.js +261 -0
- package/dist/server/ws-bridge.js.map +1 -0
- package/dist/ui/index.html +2151 -0
- package/dist/util/csrf.js +57 -0
- package/dist/util/csrf.js.map +1 -0
- package/dist/util/log.js +46 -0
- package/dist/util/log.js.map +1 -0
- package/dist/util/paths.js +24 -0
- package/dist/util/paths.js.map +1 -0
- package/package.json +63 -0
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":"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"}
|
|
@@ -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":"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"}
|
package/dist/hydra/ws.js
ADDED
|
@@ -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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"}
|