anyagent-bridge 0.5.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/.env.example +81 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/bin/anyagent-bridge.js +127 -0
- package/client/index.html +525 -0
- package/config.example.json +69 -0
- package/docs/INSTALL.md +138 -0
- package/docs/ROADMAP.md +168 -0
- package/docs/SECURITY.md +85 -0
- package/docs/WALKTHROUGH.md +82 -0
- package/docs/screenshots/.gitkeep +3 -0
- package/docs/screenshots/01-startup-banner.png +0 -0
- package/docs/screenshots/02-terminal-view.png +0 -0
- package/docs/screenshots/03-agent-running.png +0 -0
- package/docs/screenshots/04-mobile.png +0 -0
- package/package.json +57 -0
- package/server/auth/index.js +20 -0
- package/server/auth/manager.js +448 -0
- package/server/auth/oauth.js +154 -0
- package/server/auth/providers/github.js +59 -0
- package/server/auth/providers/google.js +44 -0
- package/server/auth/sessions.js +160 -0
- package/server/auth/store.js +135 -0
- package/server/auth/totp.js +140 -0
- package/server/index.js +1779 -0
- package/server/safety/audit.js +139 -0
- package/server/safety/clientip.js +73 -0
- package/server/safety/index.js +17 -0
- package/server/safety/manager.js +507 -0
- package/server/safety/redact.js +153 -0
- package/server/safety/sandbox.js +130 -0
- package/server/tunnel/adapters/cloudflare-quick.js +40 -0
- package/server/tunnel/adapters/cloudflared-named.js +49 -0
- package/server/tunnel/adapters/devtunnel.js +54 -0
- package/server/tunnel/adapters/tailscale.js +42 -0
- package/server/tunnel/base-adapter.js +185 -0
- package/server/tunnel/detect.js +65 -0
- package/server/tunnel/index.js +15 -0
- package/server/tunnel/manager.js +321 -0
- package/server/tunnel/registry.js +31 -0
- package/test/stage4-boot.js +98 -0
- package/test/stage4-smoke.js +267 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
|
+
<meta name="theme-color" content="#0d1117" />
|
|
7
|
+
<title>AnyAgent Bridge</title>
|
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0d1117;
|
|
12
|
+
--bar: #161b22;
|
|
13
|
+
--border: #30363d;
|
|
14
|
+
--fg: #c9d1d9;
|
|
15
|
+
--muted: #8b949e;
|
|
16
|
+
--accent: #2f81f7;
|
|
17
|
+
--green: #3fb950;
|
|
18
|
+
--red: #f85149;
|
|
19
|
+
--yellow: #d29922;
|
|
20
|
+
}
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
html, body {
|
|
23
|
+
margin: 0; padding: 0; height: 100%;
|
|
24
|
+
background: var(--bg); color: var(--fg);
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
#app { display: flex; flex-direction: column; height: 100dvh; }
|
|
29
|
+
|
|
30
|
+
#bar {
|
|
31
|
+
display: flex; align-items: center; gap: 8px;
|
|
32
|
+
padding: 8px 10px;
|
|
33
|
+
background: var(--bar);
|
|
34
|
+
border-bottom: 1px solid var(--border);
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
flex: 0 0 auto;
|
|
37
|
+
}
|
|
38
|
+
#bar .title {
|
|
39
|
+
font-weight: 600; font-size: 14px; letter-spacing: .2px;
|
|
40
|
+
margin-right: 4px; white-space: nowrap;
|
|
41
|
+
}
|
|
42
|
+
#bar .title .dot { color: var(--accent); }
|
|
43
|
+
select, button, input {
|
|
44
|
+
font-family: inherit; font-size: 13px;
|
|
45
|
+
background: #0d1117; color: var(--fg);
|
|
46
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
47
|
+
padding: 6px 9px; outline: none;
|
|
48
|
+
}
|
|
49
|
+
select:focus, button:focus, input:focus { border-color: var(--accent); }
|
|
50
|
+
button {
|
|
51
|
+
cursor: pointer; background: #21262d; transition: background .15s, border-color .15s;
|
|
52
|
+
}
|
|
53
|
+
button:hover { background: #30363d; }
|
|
54
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
|
|
55
|
+
button.primary:hover { background: #4493f8; }
|
|
56
|
+
button:disabled { opacity: .5; cursor: not-allowed; }
|
|
57
|
+
|
|
58
|
+
.spacer { flex: 1 1 auto; }
|
|
59
|
+
|
|
60
|
+
#status {
|
|
61
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
62
|
+
font-size: 12px; color: var(--muted); white-space: nowrap;
|
|
63
|
+
}
|
|
64
|
+
#status .led {
|
|
65
|
+
width: 9px; height: 9px; border-radius: 50%;
|
|
66
|
+
background: var(--muted); box-shadow: 0 0 0 0 rgba(0,0,0,0);
|
|
67
|
+
}
|
|
68
|
+
#status.connected .led { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
69
|
+
#status.connecting .led { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
|
|
70
|
+
#status.disconnected .led { background: var(--red); }
|
|
71
|
+
|
|
72
|
+
#termwrap {
|
|
73
|
+
flex: 1 1 auto; position: relative;
|
|
74
|
+
padding: 6px 4px 4px 8px; min-height: 0;
|
|
75
|
+
background: var(--bg);
|
|
76
|
+
}
|
|
77
|
+
#terminal { width: 100%; height: 100%; }
|
|
78
|
+
|
|
79
|
+
/* Token gate overlay */
|
|
80
|
+
#gate {
|
|
81
|
+
position: fixed; inset: 0; z-index: 50;
|
|
82
|
+
background: rgba(13,17,23,.96);
|
|
83
|
+
display: flex; align-items: center; justify-content: center;
|
|
84
|
+
padding: 20px;
|
|
85
|
+
}
|
|
86
|
+
#gate .card {
|
|
87
|
+
background: var(--bar); border: 1px solid var(--border);
|
|
88
|
+
border-radius: 12px; padding: 24px; width: 100%; max-width: 380px;
|
|
89
|
+
box-shadow: 0 12px 40px rgba(0,0,0,.5);
|
|
90
|
+
}
|
|
91
|
+
#gate h1 { margin: 0 0 4px; font-size: 18px; }
|
|
92
|
+
#gate h1 .dot { color: var(--accent); }
|
|
93
|
+
#gate p { margin: 0 0 16px; color: var(--muted); font-size: 13px; line-height: 1.5; }
|
|
94
|
+
#gate label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
|
95
|
+
#gate input { width: 100%; margin-bottom: 12px; }
|
|
96
|
+
#gate button { width: 100%; }
|
|
97
|
+
#gate .err { color: var(--red); font-size: 12px; margin: 0 0 10px; min-height: 16px; }
|
|
98
|
+
#gate .divider { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 11px; margin: 14px 0 10px; }
|
|
99
|
+
#gate .divider::before, #gate .divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
|
|
100
|
+
#gate .oauthBtn { margin-bottom: 8px; background: #21262d; }
|
|
101
|
+
#gate .oauthBtn:hover { background: #30363d; }
|
|
102
|
+
|
|
103
|
+
@media (max-width: 640px) {
|
|
104
|
+
#bar .title { width: 100%; margin-bottom: 2px; }
|
|
105
|
+
select, button, input { font-size: 12px; padding: 7px 9px; }
|
|
106
|
+
#status { margin-left: auto; }
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
109
|
+
</head>
|
|
110
|
+
<body>
|
|
111
|
+
<div id="app">
|
|
112
|
+
<div id="bar">
|
|
113
|
+
<span class="title">AnyAgent<span class="dot">·</span>Bridge</span>
|
|
114
|
+
<select id="agentSel" title="Agent"></select>
|
|
115
|
+
<button id="startBtn">Start</button>
|
|
116
|
+
<select id="projectSel" title="Project" style="display:none"></select>
|
|
117
|
+
<span class="spacer"></span>
|
|
118
|
+
<span id="status" class="disconnected"><span class="led"></span><span id="statusText">disconnected</span></span>
|
|
119
|
+
</div>
|
|
120
|
+
<div id="termwrap"><div id="terminal"></div></div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div id="gate">
|
|
124
|
+
<div class="card">
|
|
125
|
+
<h1>AnyAgent<span class="dot">·</span>Bridge</h1>
|
|
126
|
+
<p id="gateMsg">Enter the access token printed in the server console to connect.</p>
|
|
127
|
+
<p class="err" id="gateErr"></p>
|
|
128
|
+
<label for="tokenInput">Access token</label>
|
|
129
|
+
<input id="tokenInput" type="password" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="paste token…" />
|
|
130
|
+
<div id="totpRow" style="display:none">
|
|
131
|
+
<label for="totpInput">2FA code</label>
|
|
132
|
+
<input id="totpInput" type="text" inputmode="numeric" autocomplete="one-time-code" spellcheck="false" placeholder="6-digit code" />
|
|
133
|
+
</div>
|
|
134
|
+
<button class="primary" id="gateBtn">Connect</button>
|
|
135
|
+
<div id="oauthRow" style="display:none">
|
|
136
|
+
<div class="divider">or</div>
|
|
137
|
+
<button class="oauthBtn" id="oauthGoogle" data-provider="google" style="display:none">Continue with Google</button>
|
|
138
|
+
<button class="oauthBtn" id="oauthGithub" data-provider="github" style="display:none">Continue with GitHub</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
144
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
145
|
+
<script>
|
|
146
|
+
(function () {
|
|
147
|
+
"use strict";
|
|
148
|
+
|
|
149
|
+
// ---------- DOM ----------
|
|
150
|
+
const $ = (id) => document.getElementById(id);
|
|
151
|
+
const gate = $("gate"), gateBtn = $("gateBtn"), gateErr = $("gateErr"), tokenInput = $("tokenInput");
|
|
152
|
+
const gateMsg = $("gateMsg"), totpRow = $("totpRow"), totpInput = $("totpInput");
|
|
153
|
+
const oauthRow = $("oauthRow"), oauthGoogle = $("oauthGoogle"), oauthGithub = $("oauthGithub");
|
|
154
|
+
const agentSel = $("agentSel"), startBtn = $("startBtn"), projectSel = $("projectSel");
|
|
155
|
+
const statusEl = $("status"), statusText = $("statusText");
|
|
156
|
+
|
|
157
|
+
// ---------- State ----------
|
|
158
|
+
let token = null;
|
|
159
|
+
let ws = null;
|
|
160
|
+
let term = null;
|
|
161
|
+
let fitAddon = null;
|
|
162
|
+
let connected = false;
|
|
163
|
+
let manualClose = false;
|
|
164
|
+
let reconnectTimer = null;
|
|
165
|
+
let reconnectDelay = 1000; // backoff base
|
|
166
|
+
const MAX_RECONNECT_DELAY = 15000;
|
|
167
|
+
let pingTimer = null;
|
|
168
|
+
|
|
169
|
+
// ---------- Status UI ----------
|
|
170
|
+
function setStatus(state, text) {
|
|
171
|
+
statusEl.className = state;
|
|
172
|
+
statusText.textContent = text || state;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------- Terminal ----------
|
|
176
|
+
function initTerminal() {
|
|
177
|
+
if (term) return;
|
|
178
|
+
term = new Terminal({
|
|
179
|
+
cursorBlink: true,
|
|
180
|
+
fontFamily: 'Menlo, Monaco, "Cascadia Code", "Courier New", monospace',
|
|
181
|
+
fontSize: window.innerWidth < 640 ? 12 : 13,
|
|
182
|
+
scrollback: 10000,
|
|
183
|
+
theme: {
|
|
184
|
+
background: "#0d1117",
|
|
185
|
+
foreground: "#c9d1d9",
|
|
186
|
+
cursor: "#2f81f7",
|
|
187
|
+
cursorAccent: "#0d1117",
|
|
188
|
+
selectionBackground: "#264f78",
|
|
189
|
+
black: "#484f58", red: "#ff7b72", green: "#3fb950", yellow: "#d29922",
|
|
190
|
+
blue: "#58a6ff", magenta: "#bc8cff", cyan: "#39c5cf", white: "#b1bac4",
|
|
191
|
+
brightBlack: "#6e7681", brightRed: "#ffa198", brightGreen: "#56d364",
|
|
192
|
+
brightYellow: "#e3b341", brightBlue: "#79c0ff", brightMagenta: "#d2a8ff",
|
|
193
|
+
brightCyan: "#56d4dd", brightWhite: "#f0f6fc"
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
fitAddon = new FitAddon.FitAddon();
|
|
197
|
+
term.loadAddon(fitAddon);
|
|
198
|
+
term.open($("terminal"));
|
|
199
|
+
safeFit();
|
|
200
|
+
|
|
201
|
+
// keystrokes -> server
|
|
202
|
+
term.onData((data) => {
|
|
203
|
+
send({ type: "input", data });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
207
|
+
// Some mobile browsers need a delayed refit after the keyboard toggles
|
|
208
|
+
window.addEventListener("orientationchange", () => setTimeout(onResize, 300));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function safeFit() {
|
|
212
|
+
try { fitAddon && fitAddon.fit(); } catch (_) {}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let resizeRaf = null;
|
|
216
|
+
function onResize() {
|
|
217
|
+
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
|
218
|
+
resizeRaf = requestAnimationFrame(() => {
|
|
219
|
+
safeFit();
|
|
220
|
+
if (connected && term) {
|
|
221
|
+
send({ type: "resize", cols: term.cols, rows: term.rows });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------- WebSocket ----------
|
|
227
|
+
function wsUrl() {
|
|
228
|
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
229
|
+
// With a JS-held session/static token, pass it as ?token=. For cookie-based
|
|
230
|
+
// sessions (OAuth) there is no JS token — the browser sends the session
|
|
231
|
+
// cookie on the upgrade automatically, so connect with no query.
|
|
232
|
+
const q = token ? `?token=${encodeURIComponent(token)}` : "";
|
|
233
|
+
return `${proto}//${location.host}/ws${q}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function connect() {
|
|
237
|
+
clearTimeout(reconnectTimer);
|
|
238
|
+
manualClose = false;
|
|
239
|
+
setStatus("connecting", "connecting…");
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
ws = new WebSocket(wsUrl());
|
|
243
|
+
} catch (e) {
|
|
244
|
+
scheduleReconnect();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
ws.onopen = () => {
|
|
249
|
+
connected = true;
|
|
250
|
+
reconnectDelay = 1000;
|
|
251
|
+
setStatus("connected", "connected");
|
|
252
|
+
// initialize PTY with current geometry
|
|
253
|
+
send({ type: "init", cols: term ? term.cols : 80, rows: term ? term.rows : 24 });
|
|
254
|
+
startPing();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
ws.onmessage = (ev) => {
|
|
258
|
+
let msg;
|
|
259
|
+
try { msg = JSON.parse(ev.data); } catch (_) { return; }
|
|
260
|
+
handle(msg);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
ws.onclose = (ev) => {
|
|
264
|
+
connected = false;
|
|
265
|
+
stopPing();
|
|
266
|
+
if (manualClose) { setStatus("disconnected", "disconnected"); return; }
|
|
267
|
+
// 1008 / 4401-style auth failures: surface to the gate
|
|
268
|
+
if (ev.code === 1008 || ev.code === 4401) {
|
|
269
|
+
showGate("Authentication failed — check the token.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
setStatus("disconnected", "reconnecting…");
|
|
273
|
+
scheduleReconnect();
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
ws.onerror = () => {
|
|
277
|
+
// onclose will follow; just reflect state
|
|
278
|
+
if (!connected) setStatus("disconnected", "connection error");
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function scheduleReconnect() {
|
|
283
|
+
clearTimeout(reconnectTimer);
|
|
284
|
+
reconnectTimer = setTimeout(() => {
|
|
285
|
+
if (!manualClose) connect();
|
|
286
|
+
}, reconnectDelay);
|
|
287
|
+
reconnectDelay = Math.min(reconnectDelay * 1.7, MAX_RECONNECT_DELAY);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function send(obj) {
|
|
291
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
292
|
+
ws.send(JSON.stringify(obj));
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function startPing() {
|
|
299
|
+
stopPing();
|
|
300
|
+
pingTimer = setInterval(() => send({ type: "ping" }), 25000);
|
|
301
|
+
}
|
|
302
|
+
function stopPing() {
|
|
303
|
+
if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------- Server -> client messages ----------
|
|
307
|
+
function handle(msg) {
|
|
308
|
+
switch (msg.type) {
|
|
309
|
+
case "ready":
|
|
310
|
+
// session established; nothing required, but resync size
|
|
311
|
+
if (connected && term) send({ type: "resize", cols: term.cols, rows: term.rows });
|
|
312
|
+
break;
|
|
313
|
+
case "output":
|
|
314
|
+
if (term && typeof msg.data === "string") term.write(msg.data);
|
|
315
|
+
break;
|
|
316
|
+
case "exit":
|
|
317
|
+
if (term) term.write("\r\n\x1b[33m[process exited]\x1b[0m\r\n");
|
|
318
|
+
break;
|
|
319
|
+
case "pong":
|
|
320
|
+
break;
|
|
321
|
+
case "error":
|
|
322
|
+
if (term) term.write(`\r\n\x1b[31m[error] ${msg.message || ""}\x1b[0m\r\n`);
|
|
323
|
+
if (/auth/i.test(msg.message || "")) showGate(msg.message);
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
// ignore unknown (forward-compat: detached, etc.)
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------- REST: agents + projects ----------
|
|
332
|
+
async function api(path) {
|
|
333
|
+
// Always include cookies (cookie-based OAuth sessions); add the Bearer header
|
|
334
|
+
// too when we hold a JS token (static token / token-login session).
|
|
335
|
+
const headers = token ? { Authorization: "Bearer " + token } : {};
|
|
336
|
+
const res = await fetch(path, { credentials: "include", headers });
|
|
337
|
+
if (res.status === 401 || res.status === 403) {
|
|
338
|
+
const err = new Error("unauthorized"); err.unauthorized = true; throw err;
|
|
339
|
+
}
|
|
340
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
341
|
+
return res.json();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function loadAgents() {
|
|
345
|
+
let list = [];
|
|
346
|
+
try {
|
|
347
|
+
const data = await api("/api/agents");
|
|
348
|
+
list = Array.isArray(data) ? data : (data.agents || []);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
if (e.unauthorized) throw e;
|
|
351
|
+
// leave empty; Start disabled
|
|
352
|
+
}
|
|
353
|
+
agentSel.innerHTML = "";
|
|
354
|
+
if (!list.length) {
|
|
355
|
+
const opt = document.createElement("option");
|
|
356
|
+
opt.textContent = "(no agents)"; opt.value = "";
|
|
357
|
+
agentSel.appendChild(opt);
|
|
358
|
+
startBtn.disabled = true;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
startBtn.disabled = false;
|
|
362
|
+
for (const a of list) {
|
|
363
|
+
const opt = document.createElement("option");
|
|
364
|
+
opt.value = a.id;
|
|
365
|
+
opt.textContent = a.name || a.id;
|
|
366
|
+
agentSel.appendChild(opt);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function loadProjects() {
|
|
371
|
+
let list = [];
|
|
372
|
+
try {
|
|
373
|
+
const data = await api("/api/projects");
|
|
374
|
+
list = Array.isArray(data) ? data : (data.projects || []);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
if (e.unauthorized) throw e;
|
|
377
|
+
}
|
|
378
|
+
if (!list.length) { projectSel.style.display = "none"; return; }
|
|
379
|
+
projectSel.innerHTML = "";
|
|
380
|
+
const none = document.createElement("option");
|
|
381
|
+
none.value = ""; none.textContent = "Project…";
|
|
382
|
+
projectSel.appendChild(none);
|
|
383
|
+
for (const p of list) {
|
|
384
|
+
const opt = document.createElement("option");
|
|
385
|
+
opt.value = p.path || p.name || "";
|
|
386
|
+
opt.textContent = p.name || p.path || "project";
|
|
387
|
+
projectSel.appendChild(opt);
|
|
388
|
+
}
|
|
389
|
+
projectSel.style.display = "";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------- Start agent ----------
|
|
393
|
+
startBtn.addEventListener("click", () => {
|
|
394
|
+
const agentId = agentSel.value;
|
|
395
|
+
if (!agentId) return;
|
|
396
|
+
send({ type: "startAgent", agentId });
|
|
397
|
+
if (term) term.focus();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ---------- Login gate ----------
|
|
401
|
+
function showGate(errMsg) {
|
|
402
|
+
setStatus("disconnected", "disconnected");
|
|
403
|
+
manualClose = true;
|
|
404
|
+
if (ws) { try { ws.close(); } catch (_) {} ws = null; }
|
|
405
|
+
gateErr.textContent = errMsg || "";
|
|
406
|
+
gate.style.display = "flex";
|
|
407
|
+
setTimeout(() => tokenInput.focus(), 50);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function hideGate() { gate.style.display = "none"; }
|
|
411
|
+
|
|
412
|
+
// Exchange a token (+ optional 2FA code) for a session (also sets an httpOnly
|
|
413
|
+
// session cookie). Returns { token } | { needTotp } | { error }.
|
|
414
|
+
async function tokenLogin(tokenValue, totpCode) {
|
|
415
|
+
let res, data;
|
|
416
|
+
try {
|
|
417
|
+
res = await fetch("/api/auth/login", {
|
|
418
|
+
method: "POST", credentials: "include",
|
|
419
|
+
headers: { "Content-Type": "application/json" },
|
|
420
|
+
body: JSON.stringify({ token: tokenValue, totp: totpCode || undefined })
|
|
421
|
+
});
|
|
422
|
+
data = await res.json().catch(() => ({}));
|
|
423
|
+
} catch (e) { return { error: "Network error — is the server running?" }; }
|
|
424
|
+
if (res.status === 429) return { error: (data && data.message) || "Too many attempts. Try later." };
|
|
425
|
+
if (data && data.needTotp) return { needTotp: true };
|
|
426
|
+
if (data && data.ok && data.token) return { token: data.token };
|
|
427
|
+
return { error: (data && (data.reason || data.message)) || "Invalid token." };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function startSession() {
|
|
431
|
+
try { await loadAgents(); await loadProjects(); }
|
|
432
|
+
catch (e) { if (e.unauthorized) { showGate("Authentication failed."); return false; } }
|
|
433
|
+
hideGate(); initTerminal(); connect(); return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Resume an existing cookie session (set by OAuth, or a prior token login).
|
|
437
|
+
async function cookieResume() {
|
|
438
|
+
try {
|
|
439
|
+
const res = await fetch("/api/auth/me", { credentials: "include" });
|
|
440
|
+
if (res.ok) { token = null; return await startSession(); }
|
|
441
|
+
} catch (_) {}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function submitToken() {
|
|
446
|
+
const v = tokenInput.value.trim();
|
|
447
|
+
if (!v) { gateErr.textContent = "Token required."; return; }
|
|
448
|
+
const code = totpRow.style.display !== "none" ? totpInput.value.trim() : "";
|
|
449
|
+
gateBtn.disabled = true; gateErr.textContent = "";
|
|
450
|
+
const r = await tokenLogin(v, code);
|
|
451
|
+
gateBtn.disabled = false;
|
|
452
|
+
if (r.needTotp) {
|
|
453
|
+
totpRow.style.display = "";
|
|
454
|
+
gateMsg.textContent = "Two-factor required — enter your 6-digit code.";
|
|
455
|
+
setTimeout(() => totpInput.focus(), 30);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (r.error) { gateErr.textContent = r.error; return; }
|
|
459
|
+
token = r.token;
|
|
460
|
+
await startSession();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
gateBtn.addEventListener("click", submitToken);
|
|
464
|
+
tokenInput.addEventListener("keydown", (e) => { if (e.key === "Enter") submitToken(); });
|
|
465
|
+
totpInput.addEventListener("keydown", (e) => { if (e.key === "Enter") submitToken(); });
|
|
466
|
+
|
|
467
|
+
function wireOauth(btn) {
|
|
468
|
+
btn.addEventListener("click", () => { window.location.href = "/api/auth/oauth/" + btn.dataset.provider + "/start"; });
|
|
469
|
+
}
|
|
470
|
+
wireOauth(oauthGoogle);
|
|
471
|
+
wireOauth(oauthGithub);
|
|
472
|
+
|
|
473
|
+
async function loadLoginConfig() {
|
|
474
|
+
try {
|
|
475
|
+
const res = await fetch("/api/auth/config", { credentials: "include" });
|
|
476
|
+
if (!res.ok) return;
|
|
477
|
+
const cfg = await res.json();
|
|
478
|
+
const o = (cfg.methods && cfg.methods.oauth) || {};
|
|
479
|
+
let any = false;
|
|
480
|
+
if (o.google) { oauthGoogle.style.display = ""; any = true; }
|
|
481
|
+
if (o.github) { oauthGithub.style.display = ""; any = true; }
|
|
482
|
+
if (any) oauthRow.style.display = "";
|
|
483
|
+
} catch (_) {}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ---------- Bootstrap ----------
|
|
487
|
+
(async function boot() {
|
|
488
|
+
const params = new URLSearchParams(location.search);
|
|
489
|
+
const authError = params.get("auth_error");
|
|
490
|
+
|
|
491
|
+
await loadLoginConfig();
|
|
492
|
+
|
|
493
|
+
// 1) explicit ?token= bootstrap link from the server console
|
|
494
|
+
const t = params.get("token");
|
|
495
|
+
if (t) {
|
|
496
|
+
params.delete("token");
|
|
497
|
+
history.replaceState(null, "", location.pathname + (params.toString() ? "?" + params.toString() : ""));
|
|
498
|
+
tokenInput.value = t;
|
|
499
|
+
const r = await tokenLogin(t, "");
|
|
500
|
+
if (r.needTotp) {
|
|
501
|
+
showGate("");
|
|
502
|
+
totpRow.style.display = "";
|
|
503
|
+
gateMsg.textContent = "Two-factor required — enter your 6-digit code.";
|
|
504
|
+
setTimeout(() => totpInput.focus(), 30);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (r.token) { token = r.token; await startSession(); return; }
|
|
508
|
+
showGate(r.error || "Invalid token.");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 2) existing cookie session (OAuth return, or a remembered login)
|
|
513
|
+
if (await cookieResume()) {
|
|
514
|
+
if (authError) history.replaceState(null, "", location.pathname);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 3) otherwise, prompt
|
|
519
|
+
showGate(authError ? ("Login failed: " + authError) : "");
|
|
520
|
+
if (authError) history.replaceState(null, "", location.pathname);
|
|
521
|
+
})();
|
|
522
|
+
})();
|
|
523
|
+
</script>
|
|
524
|
+
</body>
|
|
525
|
+
</html>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"host": "127.0.0.1",
|
|
3
|
+
"port": 3001,
|
|
4
|
+
"shell": null,
|
|
5
|
+
"auth": {
|
|
6
|
+
"token": null,
|
|
7
|
+
"sessionTtlHours": 12,
|
|
8
|
+
"sessionSecret": null,
|
|
9
|
+
"requireLogin": false,
|
|
10
|
+
"totp": { "enabled": true, "issuer": "AnyAgent Bridge", "label": "operator" },
|
|
11
|
+
"oauth": {
|
|
12
|
+
"enabled": false,
|
|
13
|
+
"callbackBaseUrl": null,
|
|
14
|
+
"claimFirstUser": true,
|
|
15
|
+
"google": { "clientId": null, "clientSecret": null, "allowedEmails": [] },
|
|
16
|
+
"github": { "clientId": null, "clientSecret": null, "allowedLogins": [] }
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"agents": [
|
|
20
|
+
{ "id": "claude", "name": "Claude Code", "command": "claude" },
|
|
21
|
+
{ "id": "codex", "name": "Codex", "command": "codex" }
|
|
22
|
+
],
|
|
23
|
+
"projects": [],
|
|
24
|
+
"allowedPaths": [],
|
|
25
|
+
"sessionTimeoutDays": 7,
|
|
26
|
+
"tunnel": {
|
|
27
|
+
"enabled": false,
|
|
28
|
+
"provider": "devtunnel",
|
|
29
|
+
"urlTimeoutMs": 30000,
|
|
30
|
+
"killGraceMs": 4000,
|
|
31
|
+
"restart": { "maxPerWindow": 5, "windowMs": 10000, "backoffMs": 5000, "backoffMaxMs": 60000 },
|
|
32
|
+
|
|
33
|
+
"devtunnel": { "tunnelId": null, "allowAnonymous": true, "extraArgs": [] },
|
|
34
|
+
"cloudflare-quick": { "extraArgs": [] },
|
|
35
|
+
"tailscale": { "extraArgs": [] },
|
|
36
|
+
"cloudflared-named": { "tunnelName": null, "hostname": null, "extraArgs": [] }
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"safety": {
|
|
40
|
+
"enabled": false,
|
|
41
|
+
"trustProxy": false,
|
|
42
|
+
|
|
43
|
+
"sandbox": {
|
|
44
|
+
"enabled": false,
|
|
45
|
+
"image": null,
|
|
46
|
+
"network": "bridge",
|
|
47
|
+
"mountMode": "rw",
|
|
48
|
+
"workdir": "/workspace",
|
|
49
|
+
"shell": null,
|
|
50
|
+
"memory": "2g",
|
|
51
|
+
"cpus": "2",
|
|
52
|
+
"pidsLimit": 512,
|
|
53
|
+
"noNewPrivileges": true,
|
|
54
|
+
"readOnlyRootfs": false,
|
|
55
|
+
"dropAllCaps": false,
|
|
56
|
+
"runAsHostUser": false,
|
|
57
|
+
"envPassthrough": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
58
|
+
"onDockerMissing": "host",
|
|
59
|
+
"onMissingProject": "host",
|
|
60
|
+
"extraArgs": []
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"killSwitch": { "enabled": true, "lockOnPanic": true, "stopTunnelOnPanic": true, "persistLock": true },
|
|
64
|
+
|
|
65
|
+
"audit": { "enabled": false, "dir": null, "includeReads": false, "maxFileBytes": 10485760, "retentionDays": 30 },
|
|
66
|
+
|
|
67
|
+
"redaction": { "liveStream": false, "auditAlways": true, "maxHoldBytes": 8192 }
|
|
68
|
+
}
|
|
69
|
+
}
|