@harness-fe/runtime 4.0.0-next.4 → 4.0.0-next.6

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/client.js CHANGED
@@ -123,7 +123,7 @@ export class RuntimeClient {
123
123
  this.recorder = new RrwebRecorder((chunk) => this.sendEvent(EVENT_NAME.RRWEB, chunk), { checkoutEveryNms: opts.rrwebCheckoutEveryNms });
124
124
  }
125
125
  start() {
126
- const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
126
+ const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
127
127
  this.ctx.capture.install((name, payload) => this.sendEvent(name, payload), { daemonUrl });
128
128
  this.recorder.start();
129
129
  this.connect();
@@ -134,7 +134,7 @@ export class RuntimeClient {
134
134
  this.ws?.close();
135
135
  }
136
136
  connect() {
137
- const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
137
+ const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
138
138
  try {
139
139
  this.ws = new WebSocket(url);
140
140
  }
@@ -1,15 +1,12 @@
1
1
  /**
2
- * Convert the runtime's `mcpUrl` (a WebSocket URL the plugin gave us) into
3
- * the dashboard URL the same daemon serves.
2
+ * Convert the runtime's `mcpUrl` (the gateway WebSocket URL the plugin gave us)
3
+ * into the console URL the same gateway serves: `<http>://<host>:<port>/console`,
4
+ * deep-linking to `/console/sessions/:id` when a sessionId is given.
4
5
  *
5
- * The daemon binds one HTTP+WS port; the dashboard lives at
6
- * `<http-scheme>://<host>:<port>/dashboard/`. The token, if any, is
7
- * carried in the query string so the browser is pre-authenticated on
8
- * first hit (after which mcp-server hands it off to a cookie — see
9
- * `packages/mcp-server/src/dashboardSpa.ts`).
10
- *
11
- * Optionally deep-links into a session's detail page when `sessionId` is
12
- * provided.
6
+ * This is a **pure shortcut** it carries NO token. Opening it is plain
7
+ * navigation; the console authorizes the viewer on its own (admin sign-in or a
8
+ * pasted read token). The runtime's token is write-only and must never be used
9
+ * as a console auth grant, so it's deliberately omitted.
13
10
  */
14
11
  export interface DashboardUrlInput {
15
12
  mcpUrl: string;
@@ -10,11 +10,7 @@ export function deriveDashboardUrl(input) {
10
10
  }
11
11
  const httpScheme = url.protocol === 'wss:' ? 'https:' : 'http:';
12
12
  const path = input.sessionId
13
- ? `/dashboard/sessions/${encodeURIComponent(input.sessionId)}`
14
- : '/dashboard/';
15
- const token = url.searchParams.get('token');
16
- const search = token ? `?token=${encodeURIComponent(token)}` : '';
17
- // Build manually so we don't leak any extra query/hash from the WS URL
18
- // (rare, but be defensive — the agent only ever sees what we hand it).
19
- return `${httpScheme}//${url.host}${path}${search}`;
13
+ ? `/console/sessions/${encodeURIComponent(input.sessionId)}`
14
+ : '/console';
15
+ return `${httpScheme}//${url.host}${path}`;
20
16
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "4.0.0-next.4";
1
+ export declare const VERSION = "4.0.0-next.6";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit by hand.
2
2
  // Sourced from package.json at build time so the runtime reports its real version.
3
- export const VERSION = '4.0.0-next.4';
3
+ export const VERSION = '4.0.0-next.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-fe/runtime",
3
- "version": "4.0.0-next.4",
3
+ "version": "4.0.0-next.6",
4
4
  "description": "Browser-side SDK injected into the dev page. Connects to the MCP server via WebSocket and executes commands.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,13 +30,15 @@
30
30
  "dependencies": {
31
31
  "@zumer/snapdom": "^2.12.0",
32
32
  "rrweb": "2.0.0-alpha.4",
33
- "@harness-fe/protocol": "4.0.0-next.4",
34
- "@harness-fe/sandbox": "^3.2.0"
33
+ "@harness-fe/protocol": "4.0.0-next.6",
34
+ "@harness-fe/sandbox": "^4.0.0-next.6"
35
35
  },
36
36
  "devDependencies": {
37
37
  "happy-dom": "^20.9.0",
38
38
  "typescript": "^5.6.0",
39
- "vitest": "^2.1.9"
39
+ "vitest": "^2.1.9",
40
+ "@harness-fe/core": "4.0.0-next.6",
41
+ "@harness-fe/gateway": "4.0.0-next.6"
40
42
  },
41
43
  "publishConfig": {
42
44
  "access": "public"
package/src/client.ts CHANGED
@@ -190,7 +190,7 @@ export class RuntimeClient {
190
190
 
191
191
 
192
192
  start(): void {
193
- const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
193
+ const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
194
194
  this.ctx.capture.install(
195
195
  (name, payload) => this.sendEvent(name, payload),
196
196
  { daemonUrl },
@@ -206,7 +206,7 @@ export class RuntimeClient {
206
206
  }
207
207
 
208
208
  private connect(): void {
209
- const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
209
+ const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
210
210
  try {
211
211
  this.ws = new WebSocket(url);
212
212
  } catch (err) {
@@ -1,55 +1,36 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { deriveDashboardUrl } from './dashboardUrl.js';
3
3
 
4
- describe('deriveDashboardUrl', () => {
5
- it('swaps ws:// to http:// and points at /dashboard/', () => {
6
- expect(deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729' })).toBe(
7
- 'http://127.0.0.1:47729/dashboard/',
4
+ describe('deriveDashboardUrl (pure shortcut — no token)', () => {
5
+ it('swaps ws:// to http:// and points at /console', () => {
6
+ expect(deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws' })).toBe(
7
+ 'http://127.0.0.1:47729/console',
8
8
  );
9
9
  });
10
10
 
11
11
  it('swaps wss:// to https:// (production / LAN with TLS)', () => {
12
- expect(deriveDashboardUrl({ mcpUrl: 'wss://harness.lan:47729' })).toBe(
13
- 'https://harness.lan:47729/dashboard/',
12
+ expect(deriveDashboardUrl({ mcpUrl: 'wss://harness.lan:47729/ws' })).toBe(
13
+ 'https://harness.lan:47729/console',
14
14
  );
15
15
  });
16
16
 
17
- it('carries the token query through verbatim', () => {
17
+ it('deep-links to /console/sessions/:id when sessionId is provided', () => {
18
18
  expect(
19
- deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729?token=abc' }),
20
- ).toBe('http://127.0.0.1:47729/dashboard/?token=abc');
21
- });
22
-
23
- it('URL-encodes the token (defensive against weird HARNESS_FE_TOKEN values)', () => {
24
- expect(
25
- deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729?token=a%20b%26c' }),
26
- ).toBe('http://127.0.0.1:47729/dashboard/?token=a%20b%26c');
27
- });
28
-
29
- it('deep-links to /dashboard/sessions/:id when sessionId is provided', () => {
30
- expect(
31
- deriveDashboardUrl({
32
- mcpUrl: 'ws://127.0.0.1:47729?token=abc',
33
- sessionId: 'sess-1',
34
- }),
35
- ).toBe('http://127.0.0.1:47729/dashboard/sessions/sess-1?token=abc');
19
+ deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=abc', sessionId: 'sess-1' }),
20
+ ).toBe('http://127.0.0.1:47729/console/sessions/sess-1');
36
21
  });
37
22
 
38
23
  it('URL-encodes the session id', () => {
39
24
  expect(
40
- deriveDashboardUrl({
41
- mcpUrl: 'ws://127.0.0.1:47729',
42
- sessionId: 'a/b c',
43
- }),
44
- ).toBe('http://127.0.0.1:47729/dashboard/sessions/a%2Fb%20c');
25
+ deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws', sessionId: 'a/b c' }),
26
+ ).toBe('http://127.0.0.1:47729/console/sessions/a%2Fb%20c');
45
27
  });
46
28
 
47
- it('strips other query/hash from the WS URLonly token is forwarded', () => {
48
- expect(
49
- deriveDashboardUrl({
50
- mcpUrl: 'ws://127.0.0.1:47729/?token=abc&other=secret#hash',
51
- }),
52
- ).toBe('http://127.0.0.1:47729/dashboard/?token=abc');
29
+ it('never carries the runtime tokenit is a navigation shortcut, not an auth grant', () => {
30
+ const url = deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=secret&other=x#h', sessionId: 'sess-1' });
31
+ expect(url).toBe('http://127.0.0.1:47729/console/sessions/sess-1');
32
+ expect(url).not.toContain('token');
33
+ expect(url).not.toContain('secret');
53
34
  });
54
35
 
55
36
  it('returns undefined for empty or invalid input', () => {
@@ -1,15 +1,12 @@
1
1
  /**
2
- * Convert the runtime's `mcpUrl` (a WebSocket URL the plugin gave us) into
3
- * the dashboard URL the same daemon serves.
2
+ * Convert the runtime's `mcpUrl` (the gateway WebSocket URL the plugin gave us)
3
+ * into the console URL the same gateway serves: `<http>://<host>:<port>/console`,
4
+ * deep-linking to `/console/sessions/:id` when a sessionId is given.
4
5
  *
5
- * The daemon binds one HTTP+WS port; the dashboard lives at
6
- * `<http-scheme>://<host>:<port>/dashboard/`. The token, if any, is
7
- * carried in the query string so the browser is pre-authenticated on
8
- * first hit (after which mcp-server hands it off to a cookie — see
9
- * `packages/mcp-server/src/dashboardSpa.ts`).
10
- *
11
- * Optionally deep-links into a session's detail page when `sessionId` is
12
- * provided.
6
+ * This is a **pure shortcut** it carries NO token. Opening it is plain
7
+ * navigation; the console authorizes the viewer on its own (admin sign-in or a
8
+ * pasted read token). The runtime's token is write-only and must never be used
9
+ * as a console auth grant, so it's deliberately omitted.
13
10
  */
14
11
  export interface DashboardUrlInput {
15
12
  mcpUrl: string;
@@ -26,11 +23,7 @@ export function deriveDashboardUrl(input: DashboardUrlInput): string | undefined
26
23
  }
27
24
  const httpScheme = url.protocol === 'wss:' ? 'https:' : 'http:';
28
25
  const path = input.sessionId
29
- ? `/dashboard/sessions/${encodeURIComponent(input.sessionId)}`
30
- : '/dashboard/';
31
- const token = url.searchParams.get('token');
32
- const search = token ? `?token=${encodeURIComponent(token)}` : '';
33
- // Build manually so we don't leak any extra query/hash from the WS URL
34
- // (rare, but be defensive — the agent only ever sees what we hand it).
35
- return `${httpScheme}//${url.host}${path}${search}`;
26
+ ? `/console/sessions/${encodeURIComponent(input.sessionId)}`
27
+ : '/console';
28
+ return `${httpScheme}//${url.host}${path}`;
36
29
  }
@@ -220,12 +220,13 @@ describe('installOverlay', () => {
220
220
 
221
221
  it('shows the "Open dashboard" button only when the client has an mcpUrl', () => {
222
222
  setupDom();
223
- installOverlay(makeFakeClient({ mcpUrl: 'ws://127.0.0.1:47729?token=demo' }));
223
+ installOverlay(makeFakeClient({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=demo' }));
224
224
  const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
225
225
  const btn = root.querySelector('[data-role=open-dashboard]') as HTMLButtonElement;
226
226
  expect(btn.style.display).toBe('');
227
- expect(btn.title).toContain('http://127.0.0.1:47729/dashboard/sessions/');
228
- expect(btn.title).toContain('token=demo');
227
+ expect(btn.title).toContain('http://127.0.0.1:47729/console/sessions/');
228
+ // The overlay link is a pure shortcut — it must NOT carry the runtime token.
229
+ expect(btn.title).not.toContain('token=');
229
230
  });
230
231
 
231
232
  it('hides the "Open dashboard" button when mcpUrl is missing', () => {
@@ -238,7 +239,7 @@ describe('installOverlay', () => {
238
239
 
239
240
  it('clicking "Open dashboard" calls window.open with the derived URL in a new tab', () => {
240
241
  setupDom();
241
- installOverlay(makeFakeClient({ mcpUrl: 'wss://harness.lan:8443?token=t' }));
242
+ installOverlay(makeFakeClient({ mcpUrl: 'wss://harness.lan:8443/ws?token=t' }));
242
243
  const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
243
244
  const calls: Array<{ url: string; target: string; features: string }> = [];
244
245
  (globalThis.window as unknown as { open: typeof window.open }).open = ((
@@ -252,7 +253,7 @@ describe('installOverlay', () => {
252
253
  const btn = root.querySelector('[data-role=open-dashboard]') as HTMLButtonElement;
253
254
  btn.click();
254
255
  expect(calls).toHaveLength(1);
255
- expect(calls[0].url).toBe('https://harness.lan:8443/dashboard/sessions/sess-12345-abcdef-9876?token=t');
256
+ expect(calls[0].url).toBe('https://harness.lan:8443/console/sessions/sess-12345-abcdef-9876');
256
257
  expect(calls[0].target).toBe('_blank');
257
258
  expect(calls[0].features).toMatch(/noopener/);
258
259
  });
@@ -24,15 +24,15 @@ vi.mock('rrweb', () => ({
24
24
  import { mkdtempSync, rmSync } from 'node:fs';
25
25
  import { tmpdir } from 'node:os';
26
26
  import { join } from 'node:path';
27
- import { Bridge } from '../../daemon/src/bridge.js';
28
- import { JsonlStore } from '../../daemon/src/store/index.js';
27
+ import { InProcessCoreClient, JsonlStore, type StoreEvent } from '@harness-fe/core';
28
+ import { createGateway, Policy, type GatewayHandle } from '@harness-fe/gateway';
29
29
  import { RuntimeClient } from './client.js';
30
30
  import { getCaptureStore } from './capture.js';
31
- import type { StoreEvent } from '../../daemon/src/store/index.js';
32
31
  import type { NetworkEntry, StorageEntry, WsEntry } from '@harness-fe/protocol';
33
32
 
34
33
  interface Env {
35
- bridge: Bridge;
34
+ core: InProcessCoreClient;
35
+ gw: GatewayHandle;
36
36
  store: JsonlStore;
37
37
  dir: string;
38
38
  port: number;
@@ -59,9 +59,12 @@ async function rmDirWithRetry(dir: string, attempts = 5): Promise<void> {
59
59
  async function setup(): Promise<Env> {
60
60
  const dir = mkdtempSync(join(tmpdir(), 'harness-rt-e2e-'));
61
61
  const store = new JsonlStore(dir);
62
- const bridge = new Bridge({ port: 0, host: '127.0.0.1', store, taskStore: null, autoPurge: { enabled: false } });
63
- await bridge.start();
64
- const port = bridge.getBoundPort();
62
+ // New architecture: an in-process core behind the gateway front door; the
63
+ // runtime connects to the gateway's /ws (Open policy → local principal).
64
+ const core = new InProcessCoreClient({ store, taskStore: null, autoPurge: { enabled: false } });
65
+ await core.start();
66
+ const gw = createGateway({ coreClient: core, policy: new Policy({ mode: 'open' }) });
67
+ const port = await gw.listen(0, '127.0.0.1');
65
68
  if (!port) throw new Error('no port');
66
69
 
67
70
  // happy-dom keeps singletons across tests — reset the patch state so this
@@ -70,7 +73,7 @@ async function setup(): Promise<Env> {
70
73
 
71
74
  const client = new RuntimeClient({
72
75
  projectId: 'rt-e2e',
73
- mcpUrl: `ws://127.0.0.1:${port}`,
76
+ mcpUrl: `ws://127.0.0.1:${port}/ws`,
74
77
  });
75
78
  client.start();
76
79
 
@@ -88,7 +91,7 @@ async function setup(): Promise<Env> {
88
91
  throw new Error('runtime-client never connected');
89
92
  }
90
93
 
91
- return { bridge, store, dir, port, client, sessionId: client.sessionId };
94
+ return { core, gw, store, dir, port, client, sessionId: client.sessionId };
92
95
  }
93
96
 
94
97
  beforeEach(async () => {
@@ -98,7 +101,8 @@ beforeEach(async () => {
98
101
  afterEach(async () => {
99
102
  if (!env) return;
100
103
  env.client.stop();
101
- await env.bridge.stop();
104
+ await env.gw.close();
105
+ await env.core.stop();
102
106
  // close() drains the async write queue — must await, else rmSync races
103
107
  // file writes and the dir-recursive-rm trips ENOTEMPTY on Linux CI.
104
108
  await env.store.close();
@@ -186,7 +190,7 @@ describe('RuntimeClient E2E — patched WebSocket flows to bridge', () => {
186
190
  (window as unknown as { WebSocket: typeof WebSocket }).WebSocket = FakeWS as unknown as typeof WebSocket;
187
191
  cap.install(
188
192
  (name, payload) => e.client.sendEvent(name, payload),
189
- { daemonUrl: `ws://127.0.0.1:${e.port}` },
193
+ { daemonUrl: `ws://127.0.0.1:${e.port}/ws` },
190
194
  );
191
195
 
192
196
  try {
@@ -245,7 +249,7 @@ describe('RuntimeClient E2E — patched fetch initiator round-trip', () => {
245
249
  cap.dispose();
246
250
  cap.install(
247
251
  (name, payload) => e.client.sendEvent(name, payload),
248
- { daemonUrl: `ws://127.0.0.1:${e.port}` },
252
+ { daemonUrl: `ws://127.0.0.1:${e.port}/ws` },
249
253
  );
250
254
 
251
255
  try {
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit by hand.
2
2
  // Sourced from package.json at build time so the runtime reports its real version.
3
- export const VERSION = '4.0.0-next.4';
3
+ export const VERSION = '4.0.0-next.6';