@harness-fe/mcp-server 4.0.0-next.1 → 4.0.0-next.3
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/dist/bin.d.ts +2 -0
- package/dist/bin.js +15 -0
- package/dist/daemon.d.ts +3 -3
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +49 -15
- package/dist/mcpHttp.d.ts +2 -2
- package/dist/mcpHttp.js +8 -2
- package/package.json +5 -7
- package/src/bin.ts +19 -0
- package/src/daemon.ts +3 -3
- package/src/experimental.test.ts +2 -2
- package/src/index.ts +4 -4
- package/src/mcp.ts +51 -19
- package/src/mcpHttp.test.ts +3 -3
- package/src/mcpHttp.ts +10 -4
- package/src/mcpLayer.e2e.test.ts +2 -2
- package/src/newCapabilities.e2e.test.ts +3 -3
- package/dist/auth.d.ts +0 -53
- package/dist/auth.js +0 -212
- package/dist/bridge.d.ts +0 -323
- package/dist/bridge.js +0 -1618
- package/dist/cli.d.ts +0 -18
- package/dist/cli.js +0 -293
- package/dist/dashboardApi.d.ts +0 -40
- package/dist/dashboardApi.js +0 -142
- package/dist/dashboardSpa.d.ts +0 -18
- package/dist/dashboardSpa.js +0 -180
- package/dist/dashboardUrl.d.ts +0 -13
- package/dist/dashboardUrl.js +0 -18
- package/dist/eventsHandler.d.ts +0 -24
- package/dist/eventsHandler.js +0 -114
- package/dist/identity.d.ts +0 -74
- package/dist/identity.js +0 -101
- package/dist/openBrowser.d.ts +0 -33
- package/dist/openBrowser.js +0 -63
- package/dist/remoteBridge.d.ts +0 -61
- package/dist/remoteBridge.js +0 -307
- package/dist/replayCreate.d.ts +0 -36
- package/dist/replayCreate.js +0 -156
- package/dist/replayViewer.d.ts +0 -20
- package/dist/replayViewer.js +0 -168
- package/dist/sessionRouter.d.ts +0 -45
- package/dist/sessionRouter.js +0 -88
- package/dist/store/JsonMemoryStore.d.ts +0 -52
- package/dist/store/JsonMemoryStore.js +0 -119
- package/dist/store/JsonTaskStore.d.ts +0 -21
- package/dist/store/JsonTaskStore.js +0 -53
- package/dist/store/JsonlStore.d.ts +0 -128
- package/dist/store/JsonlStore.js +0 -1172
- package/dist/store/MemoryEventStore.d.ts +0 -47
- package/dist/store/MemoryEventStore.js +0 -111
- package/dist/store/WriteQueue.d.ts +0 -51
- package/dist/store/WriteQueue.js +0 -142
- package/dist/store/index.d.ts +0 -6
- package/dist/store/index.js +0 -5
- package/dist/store/types.d.ts +0 -427
- package/dist/store/types.js +0 -19
- package/dist/visitorTimeline.d.ts +0 -24
- package/dist/visitorTimeline.js +0 -68
- package/src/auth.test.ts +0 -90
- package/src/auth.ts +0 -248
- package/src/bridge-auth.test.ts +0 -196
- package/src/bridge.test.ts +0 -1708
- package/src/bridge.ts +0 -1854
- package/src/cli.ts +0 -338
- package/src/dashboardApi.test.ts +0 -235
- package/src/dashboardApi.ts +0 -184
- package/src/dashboardSpa.test.ts +0 -239
- package/src/dashboardSpa.ts +0 -195
- package/src/dashboardUrl.test.ts +0 -46
- package/src/dashboardUrl.ts +0 -28
- package/src/eventsHandler.test.ts +0 -247
- package/src/eventsHandler.ts +0 -136
- package/src/identity.test.ts +0 -86
- package/src/identity.ts +0 -116
- package/src/openBrowser.test.ts +0 -103
- package/src/openBrowser.ts +0 -81
- package/src/remoteBridge.test.ts +0 -119
- package/src/remoteBridge.ts +0 -404
- package/src/replay.test.ts +0 -271
- package/src/replayCreate.ts +0 -194
- package/src/replayViewer.ts +0 -173
- package/src/sessionRouter.ts +0 -119
- package/src/store/JsonMemoryStore.test.ts +0 -175
- package/src/store/JsonMemoryStore.ts +0 -128
- package/src/store/JsonTaskStore.test.ts +0 -212
- package/src/store/JsonTaskStore.ts +0 -59
- package/src/store/JsonlStore.test.ts +0 -1538
- package/src/store/JsonlStore.ts +0 -1325
- package/src/store/MemoryEventStore.test.ts +0 -119
- package/src/store/MemoryEventStore.ts +0 -151
- package/src/store/WriteQueue.ts +0 -165
- package/src/store/identityTagging.test.ts +0 -67
- package/src/store/index.ts +0 -29
- package/src/store/types.ts +0 -532
- package/src/visitorTimeline.test.ts +0 -197
- package/src/visitorTimeline.ts +0 -89
package/dist/auth.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Token-based auth for the bridge's HTTP + WS surfaces.
|
|
3
|
-
*
|
|
4
|
-
* Loopback (127.*, localhost, ::1) — auth disabled by default; the daemon
|
|
5
|
-
* trusts everything that can reach the loopback socket. As soon as the
|
|
6
|
-
* daemon is bound to a non-loopback host (e.g. 0.0.0.0 for LAN debugging),
|
|
7
|
-
* the CLI requires a token and this module enforces it on every HTTP route
|
|
8
|
-
* and WS upgrade.
|
|
9
|
-
*
|
|
10
|
-
* Why a single module: dashboard / replay viewer / events handler /
|
|
11
|
-
* MCP HTTP transport all live behind the same bridge HTTP server. Bridge
|
|
12
|
-
* wraps requests with `isAuthorized` once, so individual handlers never
|
|
13
|
-
* see unauthenticated traffic and don't carry auth code.
|
|
14
|
-
*/
|
|
15
|
-
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
16
|
-
export const DEFAULT_COOKIE_NAME = 'harness_fe_token';
|
|
17
|
-
export const DEFAULT_LOGIN_PATH = '/__auth';
|
|
18
|
-
const WS_SUBPROTOCOL_PREFIX = 'harness-fe.token.';
|
|
19
|
-
export function isAuthEnabled(opts) {
|
|
20
|
-
return !!(opts.token || opts.authorize);
|
|
21
|
-
}
|
|
22
|
-
/** Pull token from header / cookie / query / WS subprotocol (first match wins). */
|
|
23
|
-
export function extractToken(req, opts = {}) {
|
|
24
|
-
const auth = req.headers.authorization;
|
|
25
|
-
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
|
26
|
-
const v = auth.slice(7).trim();
|
|
27
|
-
if (v)
|
|
28
|
-
return v;
|
|
29
|
-
}
|
|
30
|
-
const cookieName = opts.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
31
|
-
const cookies = parseCookieHeader(req.headers.cookie);
|
|
32
|
-
if (cookies[cookieName])
|
|
33
|
-
return decodeURIComponent(cookies[cookieName]);
|
|
34
|
-
const url = req.url ?? '';
|
|
35
|
-
const qi = url.indexOf('?');
|
|
36
|
-
if (qi >= 0) {
|
|
37
|
-
const params = new URLSearchParams(url.slice(qi + 1));
|
|
38
|
-
const t = params.get('token');
|
|
39
|
-
if (t)
|
|
40
|
-
return t;
|
|
41
|
-
}
|
|
42
|
-
const subproto = req.headers['sec-websocket-protocol'];
|
|
43
|
-
if (typeof subproto === 'string') {
|
|
44
|
-
for (const p of subproto.split(',')) {
|
|
45
|
-
const trimmed = p.trim();
|
|
46
|
-
if (trimmed.startsWith(WS_SUBPROTOCOL_PREFIX)) {
|
|
47
|
-
return trimmed.slice(WS_SUBPROTOCOL_PREFIX.length);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Constant-time token compare. Hashing both sides first means we always
|
|
55
|
-
* compare equal-length buffers, sidestepping the length-leak that a raw
|
|
56
|
-
* timingSafeEqual on user input would have.
|
|
57
|
-
*/
|
|
58
|
-
export function verifyToken(provided, expected) {
|
|
59
|
-
if (!provided || !expected)
|
|
60
|
-
return false;
|
|
61
|
-
const a = createHash('sha256').update(provided).digest();
|
|
62
|
-
const b = createHash('sha256').update(expected).digest();
|
|
63
|
-
return timingSafeEqual(a, b);
|
|
64
|
-
}
|
|
65
|
-
/** True if request is allowed (auth disabled, custom predicate accepts, or token matches). */
|
|
66
|
-
export function isAuthorized(req, opts) {
|
|
67
|
-
if (!isAuthEnabled(opts))
|
|
68
|
-
return true;
|
|
69
|
-
// Custom predicate wins when supplied. Hosts that embed the daemon pass
|
|
70
|
-
// their own check here (e.g. JWT verification reading from a cookie).
|
|
71
|
-
if (opts.authorize)
|
|
72
|
-
return opts.authorize(req);
|
|
73
|
-
return verifyToken(extractToken(req, opts), opts.token);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Write a 401 response. Browsers (Accept: text/html) get a minimal login
|
|
77
|
-
* form they can post the token through; everything else gets JSON.
|
|
78
|
-
*/
|
|
79
|
-
export function sendUnauthorized(req, res, opts) {
|
|
80
|
-
// Custom-authorize mode is for host apps that own their own login UX —
|
|
81
|
-
// the built-in token form is never the right answer there. Always 401
|
|
82
|
-
// as JSON and let the host redirect.
|
|
83
|
-
const wantsLoginForm = !opts.authorize;
|
|
84
|
-
const accept = (req.headers.accept ?? '').toLowerCase();
|
|
85
|
-
const wantsHtml = accept.includes('text/html') && wantsLoginForm;
|
|
86
|
-
if (wantsHtml) {
|
|
87
|
-
res.statusCode = 401;
|
|
88
|
-
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
89
|
-
res.setHeader('cache-control', 'no-store');
|
|
90
|
-
res.end(renderLoginPage(opts, req.url ?? '/'));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
res.statusCode = 401;
|
|
94
|
-
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
95
|
-
res.setHeader('www-authenticate', 'Bearer realm="harness-fe"');
|
|
96
|
-
res.end(JSON.stringify({
|
|
97
|
-
error: 'unauthorized',
|
|
98
|
-
message: 'Missing or invalid token. Provide Authorization: Bearer <token>, ?token=<token>, or the harness_fe_token cookie.',
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Handle POST {loginPath}: read form body, verify token, set cookie, 303 → next.
|
|
103
|
-
*/
|
|
104
|
-
export async function handleLoginPost(req, res, opts) {
|
|
105
|
-
if (!isAuthEnabled(opts) || opts.authorize) {
|
|
106
|
-
// Auth disabled, or the host owns auth via a custom predicate — the
|
|
107
|
-
// built-in login form isn't meaningful here. Redirect home.
|
|
108
|
-
res.statusCode = 303;
|
|
109
|
-
res.setHeader('location', '/');
|
|
110
|
-
res.end();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const chunks = [];
|
|
114
|
-
let total = 0;
|
|
115
|
-
const MAX = 4096;
|
|
116
|
-
for await (const c of req) {
|
|
117
|
-
const buf = c;
|
|
118
|
-
total += buf.length;
|
|
119
|
-
if (total > MAX) {
|
|
120
|
-
res.statusCode = 413;
|
|
121
|
-
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
122
|
-
res.end('payload too large');
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
chunks.push(buf);
|
|
126
|
-
}
|
|
127
|
-
const body = Buffer.concat(chunks).toString('utf8');
|
|
128
|
-
const form = new URLSearchParams(body);
|
|
129
|
-
const token = form.get('token') ?? '';
|
|
130
|
-
const next = safeNext(form.get('next') ?? '/');
|
|
131
|
-
if (!verifyToken(token, opts.token)) {
|
|
132
|
-
res.statusCode = 401;
|
|
133
|
-
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
134
|
-
res.setHeader('cache-control', 'no-store');
|
|
135
|
-
res.end(renderLoginPage(opts, next, 'Invalid token. Try again.'));
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const cookieName = opts.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
139
|
-
// 30 days. HttpOnly so JS can't read it; SameSite=Lax so cross-tab nav works.
|
|
140
|
-
const cookie = `${cookieName}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`;
|
|
141
|
-
res.statusCode = 303;
|
|
142
|
-
res.setHeader('set-cookie', cookie);
|
|
143
|
-
res.setHeader('location', next);
|
|
144
|
-
res.end();
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Allow only same-origin relative paths as the post-login redirect. Anything
|
|
148
|
-
* else degrades to "/" so a crafted form can't redirect to an external site.
|
|
149
|
-
*/
|
|
150
|
-
function safeNext(next) {
|
|
151
|
-
if (typeof next !== 'string')
|
|
152
|
-
return '/';
|
|
153
|
-
if (!next.startsWith('/'))
|
|
154
|
-
return '/';
|
|
155
|
-
if (next.startsWith('//'))
|
|
156
|
-
return '/';
|
|
157
|
-
return next;
|
|
158
|
-
}
|
|
159
|
-
function parseCookieHeader(raw) {
|
|
160
|
-
if (!raw)
|
|
161
|
-
return {};
|
|
162
|
-
const out = {};
|
|
163
|
-
for (const part of raw.split(';')) {
|
|
164
|
-
const eq = part.indexOf('=');
|
|
165
|
-
if (eq < 0)
|
|
166
|
-
continue;
|
|
167
|
-
const k = part.slice(0, eq).trim();
|
|
168
|
-
const v = part.slice(eq + 1).trim();
|
|
169
|
-
if (k)
|
|
170
|
-
out[k] = v;
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
function escapeHtml(s) {
|
|
175
|
-
return s.replace(/[&<>"']/g, (c) => {
|
|
176
|
-
switch (c) {
|
|
177
|
-
case '&': return '&';
|
|
178
|
-
case '<': return '<';
|
|
179
|
-
case '>': return '>';
|
|
180
|
-
case '"': return '"';
|
|
181
|
-
default: return ''';
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
function renderLoginPage(opts, next, error) {
|
|
186
|
-
const loginPath = opts.loginPath ?? DEFAULT_LOGIN_PATH;
|
|
187
|
-
const safeN = escapeHtml(safeNext(next));
|
|
188
|
-
const errBlock = error
|
|
189
|
-
? `<p style="color:#c0392b;margin:0 0 12px">${escapeHtml(error)}</p>`
|
|
190
|
-
: '';
|
|
191
|
-
return `<!doctype html>
|
|
192
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
193
|
-
<title>harness-fe — sign in</title>
|
|
194
|
-
<style>
|
|
195
|
-
body{font:14px/1.4 -apple-system,BlinkMacSystemFont,system-ui,sans-serif;background:#fafafa;color:#222;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
|
196
|
-
form{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:24px;max-width:360px;width:100%;box-shadow:0 4px 12px rgba(0,0,0,.04)}
|
|
197
|
-
h1{font-size:16px;margin:0 0 12px}
|
|
198
|
-
input[type=password]{display:block;width:100%;box-sizing:border-box;padding:10px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;margin-bottom:12px}
|
|
199
|
-
button{display:block;width:100%;padding:10px;background:#111;color:#fff;border:0;border-radius:6px;font-size:14px;cursor:pointer}
|
|
200
|
-
.muted{color:#666;font-size:12px;margin-top:12px}
|
|
201
|
-
</style></head>
|
|
202
|
-
<body>
|
|
203
|
-
<form method="post" action="${escapeHtml(loginPath)}" autocomplete="off">
|
|
204
|
-
<h1>harness-fe</h1>
|
|
205
|
-
${errBlock}
|
|
206
|
-
<input type="password" name="token" placeholder="token" autofocus required>
|
|
207
|
-
<input type="hidden" name="next" value="${safeN}">
|
|
208
|
-
<button type="submit">Sign in</button>
|
|
209
|
-
<p class="muted">Paste the token from the daemon startup banner.</p>
|
|
210
|
-
</form>
|
|
211
|
-
</body></html>`;
|
|
212
|
-
}
|
package/dist/bridge.d.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WS bridge — accepts connections from vite-plugin and runtime-client.
|
|
3
|
-
*
|
|
4
|
-
* Protocol: see @harness-fe/protocol.
|
|
5
|
-
*
|
|
6
|
-
* Responsibilities:
|
|
7
|
-
* - Handshake: `hello` frame → register peer in SessionRouter, reply `hello.ack`
|
|
8
|
-
* - sendCommand(): forward a CommandFrame to the target tab, return a
|
|
9
|
-
* Promise that resolves when the matching ResponseFrame arrives
|
|
10
|
-
* - onEvent(): broadcast event frames to subscribers (mcp tools / future
|
|
11
|
-
* recorder)
|
|
12
|
-
*/
|
|
13
|
-
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
14
|
-
import { type AuthOptions } from './auth.js';
|
|
15
|
-
import { type Principal } from './identity.js';
|
|
16
|
-
import { type ConsentPolicy, type EventFrame, type HttpBatch, type TabInfo, type Task, type TaskStatus } from '@harness-fe/protocol';
|
|
17
|
-
import { SessionRouter, type PeerSession } from './sessionRouter.js';
|
|
18
|
-
import { type IStore, type ITaskStore, type IMemoryStore, type RetentionPolicy } from './store/index.js';
|
|
19
|
-
/**
|
|
20
|
-
* Surface used by the stdio MCP layer. Same shape whether the underlying
|
|
21
|
-
* implementation is an in-process `Bridge` (leader) or a `RemoteBridge`
|
|
22
|
-
* proxying over WS to another daemon (follower).
|
|
23
|
-
*
|
|
24
|
-
* All methods are async so the same call site works in both modes.
|
|
25
|
-
*/
|
|
26
|
-
export interface IBridge {
|
|
27
|
-
sendCommand(command: string, args: unknown, opts?: SendCommandOptions): Promise<unknown>;
|
|
28
|
-
listTabs(): Promise<TabInfo[]>;
|
|
29
|
-
listTasks(filter?: {
|
|
30
|
-
status?: TaskStatus | 'all';
|
|
31
|
-
limit?: number;
|
|
32
|
-
}): Promise<Task[]>;
|
|
33
|
-
claimTask(id: string, principal?: Principal): Promise<Task | undefined>;
|
|
34
|
-
resolveTask(id: string, note?: string, principal?: Principal): Promise<Task | undefined>;
|
|
35
|
-
getMemoryStore(): IMemoryStore;
|
|
36
|
-
/**
|
|
37
|
-
* Base URL (e.g. http://127.0.0.1:47729) where the replay viewer is reachable.
|
|
38
|
-
* Returns undefined when the bridge does not serve HTTP (e.g. follower mode).
|
|
39
|
-
*/
|
|
40
|
-
getViewerBaseUrl(): string | undefined;
|
|
41
|
-
/**
|
|
42
|
-
* The configured auth token, or undefined if auth is disabled. Used to
|
|
43
|
-
* compose URLs that the user (or agent) can hit without an extra
|
|
44
|
-
* authentication step.
|
|
45
|
-
*/
|
|
46
|
-
getAuthToken(): string | undefined;
|
|
47
|
-
/**
|
|
48
|
-
* Read an attachment PNG for a task. Returns base64-encoded PNG or null.
|
|
49
|
-
* The task must exist in the in-memory map so we can look up its projectId.
|
|
50
|
-
*/
|
|
51
|
-
getTaskAttachmentData(taskId: string, attachmentId: string): Promise<string | null>;
|
|
52
|
-
}
|
|
53
|
-
export interface SendCommandOptions {
|
|
54
|
-
tabId?: string;
|
|
55
|
-
timeoutMs?: number;
|
|
56
|
-
target?: 'runtime-client' | 'vite-plugin';
|
|
57
|
-
projectId?: string;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Default data directory for all persistence stores, keyed by the port the
|
|
61
|
-
* daemon listens on. Identity of a daemon = its listening address; same
|
|
62
|
-
* port → same on-disk store; different ports → independent stores.
|
|
63
|
-
*
|
|
64
|
-
* This lets users opt into isolation simply by configuring a different
|
|
65
|
-
* `--port` in their `mcp.json`, and lets multiple IDEs targeting the same
|
|
66
|
-
* port automatically share state through the existing leader/follower
|
|
67
|
-
* mechanism in `cli.ts`. No cwd / project-root detection involved.
|
|
68
|
-
*/
|
|
69
|
-
export declare function defaultDataDir(port: number): string;
|
|
70
|
-
export interface BridgeOptions {
|
|
71
|
-
port?: number;
|
|
72
|
-
/** Bind address. Default 127.0.0.1 (no remote exposure). */
|
|
73
|
-
host?: string;
|
|
74
|
-
/**
|
|
75
|
-
* Token-based auth applied to every HTTP route and WS upgrade. Empty
|
|
76
|
-
* token disables auth (only valid when bound to a loopback host).
|
|
77
|
-
*/
|
|
78
|
-
auth?: AuthOptions;
|
|
79
|
-
/**
|
|
80
|
-
* Browser-consent policy for control commands (4.0 · P2). When omitted it
|
|
81
|
-
* defaults to `session` while auth is enabled (non-loopback / exposed) and
|
|
82
|
-
* `off` on loopback — so solo dev keeps its zero-friction flow and any
|
|
83
|
-
* exposed daemon prompts the user before an agent drives the page.
|
|
84
|
-
*/
|
|
85
|
-
consent?: ConsentPolicy;
|
|
86
|
-
/**
|
|
87
|
-
* Override the host used when building outbound URLs (dashboard links,
|
|
88
|
-
* replay viewer URLs). When omitted and `host` is `0.0.0.0` / `::`, the
|
|
89
|
-
* first non-internal IPv4 address from the OS network interfaces is
|
|
90
|
-
* used so other LAN devices can follow the links. For loopback binds the
|
|
91
|
-
* literal `host` is reused.
|
|
92
|
-
*/
|
|
93
|
-
publicHost?: string;
|
|
94
|
-
/**
|
|
95
|
-
* Store instance for JSONL persistence. If omitted, a default JsonlStore
|
|
96
|
-
* is created at `dataDir` (or `defaultDataDir(port)` when `dataDir` is
|
|
97
|
-
* also omitted). Pass null to disable persistence.
|
|
98
|
-
*/
|
|
99
|
-
store?: IStore | null;
|
|
100
|
-
/**
|
|
101
|
-
* Task store instance for JSON task persistence. If omitted, a default
|
|
102
|
-
* JsonTaskStore is created at `dataDir`. Pass null to disable task
|
|
103
|
-
* persistence (useful in tests).
|
|
104
|
-
*/
|
|
105
|
-
taskStore?: ITaskStore | null;
|
|
106
|
-
/**
|
|
107
|
-
* Memory store instance for agent memory persistence. If omitted, a default
|
|
108
|
-
* JsonMemoryStore is created at `dataDir`. Pass null to disable memory
|
|
109
|
-
* persistence (useful in tests).
|
|
110
|
-
*/
|
|
111
|
-
memoryStore?: IMemoryStore | null;
|
|
112
|
-
/**
|
|
113
|
-
* Root data directory for task attachment binaries. Defaults to the same
|
|
114
|
-
* `~/.harness/data` directory used by the stores. Override in tests.
|
|
115
|
-
*/
|
|
116
|
-
attachmentsDataDir?: string;
|
|
117
|
-
/**
|
|
118
|
-
* Root data directory for the default stores (when `store` / `taskStore`
|
|
119
|
-
* / `memoryStore` are not supplied). When omitted, computed from `port`
|
|
120
|
-
* via `defaultDataDir(port)`. Set explicitly to point all stores at a
|
|
121
|
-
* non-default location (useful for tests or migration).
|
|
122
|
-
*/
|
|
123
|
-
dataDir?: string;
|
|
124
|
-
/**
|
|
125
|
-
* Optional friendly label surfaced in the startup banner and (later) the
|
|
126
|
-
* dashboard title. Purely cosmetic — has no effect on data isolation,
|
|
127
|
-
* routing, or auth. Identity is always the listening port.
|
|
128
|
-
*/
|
|
129
|
-
label?: string;
|
|
130
|
-
/**
|
|
131
|
-
* Automatic retention policy enforcement.
|
|
132
|
-
*
|
|
133
|
-
* Without this, manual `session.purge` MCP calls are the only thing that
|
|
134
|
-
* trims the on-disk store — so a long-running daemon will eventually fill
|
|
135
|
-
* the user's disk. Default: run `store.purge()` once shortly after start
|
|
136
|
-
* and every hour thereafter. Set `enabled: false` for tests / one-shot runs.
|
|
137
|
-
*/
|
|
138
|
-
autoPurge?: {
|
|
139
|
-
enabled?: boolean;
|
|
140
|
-
/** Period between purges in ms. Default 1 hour. */
|
|
141
|
-
intervalMs?: number;
|
|
142
|
-
/** Override the retention policy. Default uses store's built-in defaults. */
|
|
143
|
-
policy?: RetentionPolicy;
|
|
144
|
-
/** Skip the startup purge (still runs the periodic timer). Default false. */
|
|
145
|
-
skipInitial?: boolean;
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
export type EventListener = (event: EventFrame, session: PeerSession) => void;
|
|
149
|
-
export declare class Bridge implements IBridge {
|
|
150
|
-
readonly router: SessionRouter;
|
|
151
|
-
readonly store: IStore | null;
|
|
152
|
-
readonly taskStore: ITaskStore | null;
|
|
153
|
-
readonly memoryStore: IMemoryStore;
|
|
154
|
-
private wss?;
|
|
155
|
-
private httpServer?;
|
|
156
|
-
/**
|
|
157
|
-
* Optional HTTP handler invoked for non-WebSocket requests. Set via
|
|
158
|
-
* `setHttpHandler()`. Allows higher layers (e.g. replay viewer) to serve
|
|
159
|
-
* routes on the same port as the WS bridge without coupling Bridge to them.
|
|
160
|
-
*/
|
|
161
|
-
private httpHandler?;
|
|
162
|
-
private sockets;
|
|
163
|
-
private pending;
|
|
164
|
-
private eventListeners;
|
|
165
|
-
private tasks;
|
|
166
|
-
private opts;
|
|
167
|
-
private auth;
|
|
168
|
-
/** Browser-consent policy pushed to runtime clients in hello.ack (4.0 · P2). */
|
|
169
|
-
private readonly consentPolicy;
|
|
170
|
-
private publicHostOverride;
|
|
171
|
-
private readonly attachDataDir;
|
|
172
|
-
private autoPurgeOpts;
|
|
173
|
-
/** Set by start() when auto-purge is enabled; cleared by stop(). */
|
|
174
|
-
private autoPurgeTimer?;
|
|
175
|
-
/**
|
|
176
|
-
* Map from connectionId → buildId (for build-plugin connections)
|
|
177
|
-
* or sessionId (for runtime-client connections).
|
|
178
|
-
*/
|
|
179
|
-
private connToStoreId;
|
|
180
|
-
/** Caller identity per connection (4.0 · P1). Resolved at WS upgrade. */
|
|
181
|
-
private connToPrincipal;
|
|
182
|
-
/**
|
|
183
|
-
* Identity attributed to MCP-driven writes (task claim/resolve). The MCP
|
|
184
|
-
* tool layer is a daemon-wide singleton today with no per-call caller
|
|
185
|
-
* context, so stdio/HTTP agents collapse to one principal. P4 (per-session
|
|
186
|
-
* MCP transport) replaces this with the real per-call principal.
|
|
187
|
-
*/
|
|
188
|
-
private readonly defaultPrincipal;
|
|
189
|
-
/** Connections that already logged a "no store session" warning. */
|
|
190
|
-
private warnedNoSession;
|
|
191
|
-
/**
|
|
192
|
-
* Grace period timers: projectId → timer handle.
|
|
193
|
-
* When a build plugin disconnects, a 30-second timer is started.
|
|
194
|
-
* If the same project reconnects within that window, the timer is cancelled.
|
|
195
|
-
*/
|
|
196
|
-
private graceTimers;
|
|
197
|
-
/**
|
|
198
|
-
* Pending build end info: projectId → { buildId, closedAt }.
|
|
199
|
-
* Tracks builds waiting for the grace period to expire.
|
|
200
|
-
*/
|
|
201
|
-
private pendingEndBuild;
|
|
202
|
-
/**
|
|
203
|
-
* Dashboard SPA subscribers — connections that sent `hello` with
|
|
204
|
-
* role: 'dashboard-client'. Receive `dashboard.update` frames whenever
|
|
205
|
-
* session state changes; never receive commands and never send events.
|
|
206
|
-
*/
|
|
207
|
-
private dashboardSubscribers;
|
|
208
|
-
/** Debounce per-session 'session.update' broadcasts so chatty rrweb chunks don't spam subscribers. */
|
|
209
|
-
private dashboardDebounceTimers;
|
|
210
|
-
/** Optional friendly label (HARNESS_FE_LABEL). Cosmetic only. */
|
|
211
|
-
readonly label: string | undefined;
|
|
212
|
-
constructor(opts?: BridgeOptions);
|
|
213
|
-
/**
|
|
214
|
-
* Returns the memory store instance for use by mcp.ts and other callers.
|
|
215
|
-
*/
|
|
216
|
-
getMemoryStore(): IMemoryStore;
|
|
217
|
-
private loadTasks;
|
|
218
|
-
private persistTasks;
|
|
219
|
-
/**
|
|
220
|
-
* Load tasks for a specific project from the task store into the in-memory map.
|
|
221
|
-
* Called when a project connects so its tasks are available immediately.
|
|
222
|
-
*/
|
|
223
|
-
private loadTasksForProject;
|
|
224
|
-
private taskDedupKey;
|
|
225
|
-
/**
|
|
226
|
-
* Register an HTTP request handler that runs for non-WebSocket requests on
|
|
227
|
-
* the same port. Only one handler is supported; later calls replace prior
|
|
228
|
-
* ones. WS upgrades bypass this handler.
|
|
229
|
-
*/
|
|
230
|
-
setHttpHandler(handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>): void;
|
|
231
|
-
/**
|
|
232
|
-
* Insert a handler that runs *before* the main HTTP handler. Return `true`
|
|
233
|
-
* if the request was consumed (no further processing); return `false` to
|
|
234
|
-
* fall through to the existing handler. Allows mcpHttp.ts to mount on
|
|
235
|
-
* `/mcp` without owning the whole HTTP surface.
|
|
236
|
-
*/
|
|
237
|
-
prependHttpHandler(handler: (req: IncomingMessage, res: ServerResponse) => Promise<boolean> | boolean): void;
|
|
238
|
-
start(): Promise<void>;
|
|
239
|
-
stop(): Promise<void>;
|
|
240
|
-
/**
|
|
241
|
-
* Run `store.purge()` defensively. Errors are logged but never bubble out
|
|
242
|
-
* — the daemon must continue serving even if disk is full or files are
|
|
243
|
-
* locked.
|
|
244
|
-
*/
|
|
245
|
-
private runAutoPurge;
|
|
246
|
-
/** Expose the bound port (useful when port:0 was passed for tests). */
|
|
247
|
-
getBoundPort(): number | undefined;
|
|
248
|
-
getViewerBaseUrl(): string | undefined;
|
|
249
|
-
getAuthToken(): string | undefined;
|
|
250
|
-
/** Daemon auth options — used by the MCP layer to identify the per-call principal (4.0 · P4). */
|
|
251
|
-
getAuthOptions(): AuthOptions;
|
|
252
|
-
/**
|
|
253
|
-
* Broadcast a `dashboard.update` frame to every subscribed dashboard SPA.
|
|
254
|
-
*
|
|
255
|
-
* `kind: 'session.update'` is debounced per-sessionId (200ms) so chatty
|
|
256
|
-
* rrweb chunk appends don't spam every subscriber. Other kinds fire
|
|
257
|
-
* immediately because they represent rare state transitions (new
|
|
258
|
-
* session, session closed, export created).
|
|
259
|
-
*/
|
|
260
|
-
notifyDashboard(payload: {
|
|
261
|
-
kind: 'session.new' | 'session.update' | 'session.closed' | 'project.update' | 'export.new';
|
|
262
|
-
sessionId?: string;
|
|
263
|
-
projectId?: string;
|
|
264
|
-
}): void;
|
|
265
|
-
private flushDashboardUpdate;
|
|
266
|
-
/**
|
|
267
|
-
* Host string used when handing out URLs that other machines need to
|
|
268
|
-
* reach. Loopback binds keep the literal address; wildcard binds
|
|
269
|
-
* (0.0.0.0 / ::) prefer the first non-internal IPv4. Explicit
|
|
270
|
-
* `publicHost` always wins.
|
|
271
|
-
*/
|
|
272
|
-
private getPublicHost;
|
|
273
|
-
onEvent(listener: EventListener): () => void;
|
|
274
|
-
/**
|
|
275
|
-
* Handle an HTTP-batch POST /events request (Edge Runtime path).
|
|
276
|
-
*
|
|
277
|
-
* Stateless: each call is a self-contained hello+events sequence.
|
|
278
|
-
* The hello is used to register the peer (or look up the existing session)
|
|
279
|
-
* and the events are persisted to the session timeline — same paths as the
|
|
280
|
-
* WS handler.
|
|
281
|
-
*/
|
|
282
|
-
handleHttpBatch(hello: HttpBatch['hello'], events: HttpBatch['events']): void;
|
|
283
|
-
listTabs(): Promise<TabInfo[]>;
|
|
284
|
-
listTasks(filter?: {
|
|
285
|
-
status?: TaskStatus | 'all';
|
|
286
|
-
limit?: number;
|
|
287
|
-
}): Promise<Task[]>;
|
|
288
|
-
claimTask(id: string, principal?: Principal): Promise<Task | undefined>;
|
|
289
|
-
getTaskAttachmentData(taskId: string, attachmentId: string): Promise<string | null>;
|
|
290
|
-
resolveTask(id: string, note?: string, principal?: Principal): Promise<Task | undefined>;
|
|
291
|
-
private persistTaskEvent;
|
|
292
|
-
private recordTask;
|
|
293
|
-
/**
|
|
294
|
-
* Write attachment data to disk and return persisted pointer objects.
|
|
295
|
-
* Drops attachments if the total decoded size exceeds 4 MB.
|
|
296
|
-
*/
|
|
297
|
-
private writeTaskAttachments;
|
|
298
|
-
/**
|
|
299
|
-
* Read an attachment from disk for a given task.
|
|
300
|
-
* Returns the base64 data if found, null otherwise.
|
|
301
|
-
*/
|
|
302
|
-
readTaskAttachment(projectId: string, taskId: string, attachmentId: string): string | null;
|
|
303
|
-
/**
|
|
304
|
-
* Send a command to a specific tab and await its response.
|
|
305
|
-
* `tabId` falls back to the most-recent active tab if omitted.
|
|
306
|
-
*/
|
|
307
|
-
sendCommand(command: string, args: unknown, opts?: SendCommandOptions): Promise<unknown>;
|
|
308
|
-
/**
|
|
309
|
-
* Returns true if there is an active build for the given projectId.
|
|
310
|
-
* Checks both in-memory grace period builds and the store.
|
|
311
|
-
*/
|
|
312
|
-
private hasActiveBuild;
|
|
313
|
-
private onConnection;
|
|
314
|
-
private handleFrame;
|
|
315
|
-
/**
|
|
316
|
-
* Runtime → daemon query dispatcher (0.5+). Whitelisted methods only.
|
|
317
|
-
* Owner check: tasks.update / tasks.get / tasks.delete refuse to touch
|
|
318
|
-
* tasks whose `visitorId` doesn't match the caller's `peer.visitorId`.
|
|
319
|
-
*/
|
|
320
|
-
private handleQuery;
|
|
321
|
-
private handleMcpCall;
|
|
322
|
-
private invokeMcpMethod;
|
|
323
|
-
}
|