@super-harness/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mert
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,176 @@
1
+ # @super-harness/server
2
+
3
+ The super-line binding for a `@super-harness/core` Harness, shipped as a
4
+ super-line **plugin**. Two exports, two adoption modes:
5
+
6
+ - **`harness(engine)`** — a `SuperLinePlugin` a host adds to its own
7
+ `createSuperLineServer` `plugins:` array. The primary story: one server, one
8
+ socket, one auth, one collections backend for the host AND the harness.
9
+ - **`serve(engine, config)`** — the standalone host, built from the same
10
+ pieces: it owns the collections backend, a default query-param
11
+ `authenticate`, `identify`, and mounts `plugins: [harness(engine), ...]`.
12
+
13
+ The tree rides **collections**, not the request surface: structural state is
14
+ `harness.threads`/`nodes`/`tools`/`membership` rows (declared by
15
+ `harnessContract()` in `@super-harness/shared`), and the token stream rides
16
+ ephemeral per-thread room events that are never persisted per-token. Clients
17
+ assemble the live tree with `subscribeTree` from `@super-harness/shared` (or
18
+ `@super-harness/react`).
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pnpm add @super-harness/server @super-harness/core @super-harness/shared \
24
+ @mastra/core @super-line/core @super-line/server \
25
+ @super-line/collections-memory @super-line/collections-sqlite
26
+ ```
27
+
28
+ All `@super-line/*` packages are **peers** — your app owns one core instance
29
+ shared by host and library. What you need:
30
+
31
+ | Peer | Version | When |
32
+ | --- | --- | --- |
33
+ | `@super-line/core` | `^0.11.0` | always |
34
+ | `@super-line/server` | `^0.11.0` | always |
35
+ | `@super-line/collections-memory` | `^0.1.0` | always (serve's `memory` backend; cheap) |
36
+ | `@super-line/collections-sqlite` | `^0.1.0` | always (serve's default backend) |
37
+ | `@super-line/collections-pglite` | `^0.1.0` | **optional** — only for `storage: { type: 'pglite' }` |
38
+ | `@mastra/core` | `^1.49.0-alpha.2` | always (the engine's peer) |
39
+
40
+ A transport is the host's choice — e.g. `@super-line/transport-websocket`
41
+ (`^0.6.0`). Composing hosts that bring their own backend only need the
42
+ collections package they actually use.
43
+
44
+ ## Composition — the harness as a plugin in your server
45
+
46
+ If the app runs (or should run) its own super-line server, add the harness
47
+ beside its surface. Four obligations (see `examples/composed-host` for the
48
+ runnable version, `composition.test.ts` for the e2e):
49
+
50
+ ```ts
51
+ import { z } from 'zod'
52
+ import { defineContract, defineSurface } from '@super-line/core'
53
+ import { createSuperLineServer } from '@super-line/server'
54
+ import { memoryCollections } from '@super-line/collections-memory'
55
+ import { webSocketServerTransport } from '@super-line/transport-websocket'
56
+ import { harnessContract } from '@super-harness/shared'
57
+ import { harness } from '@super-harness/server'
58
+
59
+ // 1. Merge the contract fragment: harnessContract() contributes the harness
60
+ // surface (on `shared`) + the four harness.* collections.
61
+ const hostContract = defineContract({
62
+ plugins: [harnessContract()],
63
+ shared: defineSurface({
64
+ clientToServer: {
65
+ 'demo.echo': { input: z.object({ text: z.string() }), output: z.object({ echoed: z.string() }) },
66
+ },
67
+ }),
68
+ roles: { user: {} },
69
+ })
70
+
71
+ const srv = createSuperLineServer(hostContract, {
72
+ transports: [webSocketServerTransport({ server: httpServer, path: '/ws' })],
73
+ // 2. ONE collections backend, host-owned — serves the harness collections
74
+ // beside any of the host's own.
75
+ collections: memoryCollections(),
76
+ // 3. authenticate ctx carries userId (+resourceId); identify returns it.
77
+ authenticate: (h) => ({ role: 'user' as const, ctx: { userId: userIdFrom(h) } }),
78
+ identify: (conn) => (conn.ctx as { userId: string }).userId,
79
+ // 4. Add the plugin. harness.* handler keys are subtracted from implement().
80
+ plugins: [harness(engine)],
81
+ })
82
+
83
+ // The host implements only its own surface — harness.* is owned by the plugin.
84
+ srv.implement({ shared: { 'demo.echo': async ({ text }) => ({ echoed: text.toUpperCase() }) }, user: {} })
85
+ ```
86
+
87
+ **`identify` is load-bearing.** The harness collections' RLS keys on
88
+ `ctx.userId`, and super-line's principal is `identify(conn) ?? conn.id` — a
89
+ random connection id. A host that skips `identify` gets a working request
90
+ surface and a **silently empty tree**: every membership-gated read is denied.
91
+ With `@super-line/plugin-auth`, `identify` comes from the session and the
92
+ identity becomes the principal for free (see `examples/auth`).
93
+
94
+ Client side: one `createSuperLineClient(hostContract)` handed to
95
+ `createHarnessClient({ client })` from `@super-harness/react` in borrowed mode — its
96
+ `close()` detaches listeners, never the host's socket.
97
+
98
+ ## Standalone — `serve()`
99
+
100
+ The harness-only contract on its own server, same pieces pre-wired:
101
+
102
+ ```ts
103
+ import { createServer } from 'node:http'
104
+ import { webSocketServerTransport } from '@super-line/transport-websocket'
105
+ import { serve } from '@super-harness/server'
106
+
107
+ const httpServer = createServer()
108
+ const { server, close } = await serve(engine, {
109
+ storage: { type: 'sqlite', file: './harness.db' }, // default; 'memory' for tests
110
+ transports: [webSocketServerTransport({ server: httpServer, path: '/super-line' })],
111
+ // authenticate?: (handshake) => ({ role: 'user', ctx: { userId, resourceId? } })
112
+ // plugin?: { roleFor, defaultRole } — membership role policy (see below)
113
+ // plugins?: [inspector(), auth()] — extra super-line plugins beside harness()
114
+ })
115
+ httpServer.listen(4111)
116
+ ```
117
+
118
+ The storage union picks the collections backend:
119
+
120
+ - `{ type: 'sqlite', file? }` — default; owns its own file. Needs
121
+ `better-sqlite3`'s native build.
122
+ - `{ type: 'memory' }` — tests/dev.
123
+ - `{ type: 'pglite', pgUrl, electricUrl? }` — multi-node: central Postgres for
124
+ writes + Electric-synced per-node replicas for fan-out. Loads
125
+ `@super-line/collections-pglite` (the optional peer) on demand. Viable even
126
+ though writes round-trip PG → Electric → replica, because tokens are
127
+ ephemeral — only low-frequency structural rows hit the backend.
128
+
129
+ The default `authenticate` trusts `userId`/`resourceId` query params — replace
130
+ it (or compose `@super-line/plugin-auth` via `plugins:`) for anything
131
+ non-local. `close()` tears the server down and disposes the plugin's bus
132
+ subscription; the transports/http server are the caller's to close.
133
+
134
+ ## What the plugin contributes
135
+
136
+ `harness(engine, opts)` returns `{ policies, handlers, setup }`:
137
+
138
+ - **Policies** — membership-based RLS over the four collections.
139
+ `harness.nodes`/`tools` read `isIn('threadId', joinedThreads(principal))`;
140
+ `harness.threads` reads `eq('resourceId', ctx.resourceId)` (the sidebar
141
+ list), falling back to membership when the connection carries no
142
+ `resourceId`; `harness.membership` reads `eq('userId', principal)`. Client
143
+ writes are all **deny** — the plugin co-writes server-authoritatively,
144
+ bypassing policy.
145
+ - **Handlers** — the `harness.*` requests (send/resume/abort/approve/
146
+ switchMode/listModes/thread CRUD/join), subtracted from the host's
147
+ `implement()`. `harness.join` adds the connection to the thread room and
148
+ inserts a membership row.
149
+ - **`setup(ctx)`** — subscribes the engine bus. Token deltas
150
+ (`reasoning`/`text`/`argsText`) broadcast to the per-thread room
151
+ (`harness:thread:{id}`) — ephemeral, never persisted — and fold into a
152
+ per-thread Projector; structural events fold through the Projector into the
153
+ collections writer (`collectionsTreeSink`), which lands the final strings on
154
+ the row at `node_end`/`tool_end`. Session signals (`suspended`,
155
+ `approvalRequired`, `modeChanged`, `followUpQueued`) broadcast; a
156
+ suspension's `{resumeSchema, request}` also parks on the node row
157
+ (`pendingResume`) so a mid-turn reload rebuilds the prompt. Thread metadata
158
+ persists as row updates (thread-list reactivity is `harness.threads` row
159
+ deltas, not events); `thread_deleted` cascades node/tool/membership rows.
160
+
161
+ ### Roles: viewer vs operator
162
+
163
+ The viewer/operator split is a **`role` column on `harness.membership`** —
164
+ per-user, per-thread — NOT a connection role. Control ops (send/resume/abort/
165
+ approve/switchMode/rename/delete) reject a `viewer` membership with
166
+ `FORBIDDEN`; read ops don't, so a viewer still sees the full live tree. The
167
+ default join role is `operator` (every joiner can drive — preserves
168
+ single-user behavior); pass `roleFor(ctx)` or `defaultRole` in
169
+ `HarnessPluginOptions` to hand out `viewer` memberships.
170
+
171
+ ## Auth
172
+
173
+ The plugin is auth-agnostic: it reads `ctx.userId` however the host's
174
+ `authenticate` supplies it — query-param dev auth, `@super-line/plugin-auth`,
175
+ or a custom scheme. `resourceId` (optional) scopes the thread list. See
176
+ `examples/auth` for the plugin-auth pairing.
package/dist/index.cjs ADDED
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ collectionsTreeSink: () => collectionsTreeSink,
34
+ harness: () => harness,
35
+ harnessContract: () => import_shared3.harnessContract,
36
+ serve: () => serve
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/serve.ts
41
+ var import_server = require("@super-line/server");
42
+ var import_collections_memory = require("@super-line/collections-memory");
43
+ var import_collections_sqlite = require("@super-line/collections-sqlite");
44
+ var import_shared2 = require("@super-harness/shared");
45
+
46
+ // src/plugin.ts
47
+ var import_core = require("@super-line/core");
48
+ var import_core2 = require("@super-harness/core");
49
+ var import_shared = require("@super-harness/shared");
50
+
51
+ // src/sink.ts
52
+ var isCode = (e, code) => e?.code === code;
53
+ function collectionsTreeSink(opts) {
54
+ const { collections, threadId } = opts;
55
+ const nodesCol = () => collections(opts.nodes);
56
+ const toolsCol = () => collections(opts.tools);
57
+ const threadsCol = () => collections(opts.threads);
58
+ const known = /* @__PURE__ */ new Set();
59
+ const lastWritten = /* @__PURE__ */ new Map();
60
+ const chains = /* @__PURE__ */ new Map();
61
+ const run = (key, fn) => {
62
+ const prev = chains.get(key) ?? Promise.resolve();
63
+ chains.set(
64
+ key,
65
+ prev.then(fn).catch((e) => console.error("[harness] collection write failed", key, e))
66
+ );
67
+ };
68
+ const upsert = (handle, ns, id, row) => {
69
+ const key = `${ns}\0${id}`;
70
+ const serialized = JSON.stringify(row);
71
+ if (lastWritten.get(key) === serialized) return;
72
+ lastWritten.set(key, serialized);
73
+ run(key, async () => {
74
+ if (known.has(key)) return handle.update(row);
75
+ try {
76
+ await handle.insert(row);
77
+ known.add(key);
78
+ } catch (e) {
79
+ if (!isCode(e, "CONFLICT")) throw e;
80
+ known.add(key);
81
+ await handle.update(row);
82
+ }
83
+ });
84
+ };
85
+ let threadRow = { id: threadId, turns: [], todos: [], createdAt: 0, updatedAt: 0 };
86
+ const flushThread = () => upsert(threadsCol(), opts.threads, threadId, threadRow);
87
+ const pending = /* @__PURE__ */ new Map();
88
+ const pendingPayload = /* @__PURE__ */ new Map();
89
+ const nodeRowOf = (node) => {
90
+ const terminal = node.status !== "running";
91
+ const parkedTool = pending.get(node.nodeId);
92
+ const stillPending = parkedTool !== void 0 && !isSettled(node.tools[parkedTool]?.status);
93
+ if (parkedTool !== void 0 && !stillPending) pending.delete(node.nodeId);
94
+ return {
95
+ id: node.nodeId,
96
+ threadId,
97
+ parentNodeId: node.parentNodeId,
98
+ depth: node.depth,
99
+ agentType: node.agentType,
100
+ task: node.task,
101
+ status: node.status,
102
+ reasoning: terminal ? node.reasoning : "",
103
+ text: terminal ? node.text : "",
104
+ toolOrder: node.toolOrder,
105
+ childOrder: node.childOrder,
106
+ usage: node.usage,
107
+ durationMs: node.durationMs,
108
+ error: node.error,
109
+ textOffset: node.textOffset,
110
+ pendingResume: stillPending ? pendingPayload.get(node.nodeId) : void 0
111
+ };
112
+ };
113
+ const toolRowOf = (node, toolCallId) => {
114
+ const t = node.tools[toolCallId];
115
+ const streaming = t.status === "input-streaming";
116
+ return {
117
+ id: t.toolCallId,
118
+ threadId,
119
+ nodeId: node.nodeId,
120
+ toolName: t.toolName,
121
+ status: t.status,
122
+ argsText: streaming ? "" : t.argsText,
123
+ args: t.args,
124
+ result: t.result,
125
+ isError: t.isError,
126
+ textOffset: t.textOffset
127
+ };
128
+ };
129
+ return {
130
+ writeNode: (node) => {
131
+ upsert(nodesCol(), opts.nodes, node.nodeId, nodeRowOf(node));
132
+ for (const tid of node.toolOrder) upsert(toolsCol(), opts.tools, tid, toolRowOf(node, tid));
133
+ },
134
+ writeThread: (doc) => {
135
+ threadRow = { ...threadRow, turns: doc.turns, todos: doc.todos ?? [] };
136
+ flushThread();
137
+ },
138
+ setThreadMeta: (meta) => {
139
+ threadRow = {
140
+ ...threadRow,
141
+ resourceId: meta.resourceId ?? threadRow.resourceId,
142
+ title: meta.title ?? threadRow.title,
143
+ createdAt: threadRow.createdAt || meta.createdAt || 0,
144
+ updatedAt: meta.updatedAt ?? threadRow.updatedAt
145
+ };
146
+ flushThread();
147
+ },
148
+ setPending: (nodeId, toolCallId, payload) => {
149
+ pending.set(nodeId, toolCallId);
150
+ pendingPayload.set(nodeId, payload);
151
+ }
152
+ };
153
+ }
154
+ function isSettled(status) {
155
+ return status === "output-available" || status === "error";
156
+ }
157
+
158
+ // src/plugin.ts
159
+ function harness(engine, opts = {}) {
160
+ let pctx;
161
+ const P = () => {
162
+ if (!pctx) throw new Error("harness plugin: setup() has not run yet");
163
+ return pctx;
164
+ };
165
+ const col = (name) => P().collection(name);
166
+ const room = (threadId) => P().room((0, import_shared.harnessThreadRoom)(threadId));
167
+ const sinks = /* @__PURE__ */ new Map();
168
+ const projectors = /* @__PURE__ */ new Map();
169
+ const sinkFor = (threadId) => {
170
+ let s = sinks.get(threadId);
171
+ if (!s) {
172
+ s = collectionsTreeSink({
173
+ collections: (n) => col(n),
174
+ threadId,
175
+ nodes: import_shared.HARNESS_NODES,
176
+ tools: import_shared.HARNESS_TOOLS,
177
+ threads: import_shared.HARNESS_THREADS
178
+ });
179
+ sinks.set(threadId, s);
180
+ }
181
+ return s;
182
+ };
183
+ const projectorFor = (threadId) => {
184
+ let p = projectors.get(threadId);
185
+ if (!p) {
186
+ p = new import_core2.Projector(sinkFor(threadId));
187
+ projectors.set(threadId, p);
188
+ }
189
+ return p;
190
+ };
191
+ const joined = async (principal) => {
192
+ const rows = await col(import_shared.HARNESS_MEMBERSHIP).snapshot({ filter: (0, import_core.eq)("userId", principal) });
193
+ return rows.map((r) => r.threadId);
194
+ };
195
+ const roleOf = async (threadId, userId) => {
196
+ const row = await col(import_shared.HARNESS_MEMBERSHIP).read((0, import_shared.membershipId)(threadId, userId));
197
+ return row?.role;
198
+ };
199
+ const requireDriver = async (threadId, ctx) => {
200
+ if (await roleOf(threadId, ctx.userId) === "viewer") {
201
+ throw new import_core.SuperLineError("FORBIDDEN", "viewer role cannot drive the thread");
202
+ }
203
+ };
204
+ const ok = { ok: true };
205
+ const attempt = async (fn) => {
206
+ try {
207
+ await fn();
208
+ return ok;
209
+ } catch (e) {
210
+ console.error("[harness] request failed", e);
211
+ return { ok: false };
212
+ }
213
+ };
214
+ const purgeThread = async (threadId) => {
215
+ projectors.delete(threadId);
216
+ sinks.delete(threadId);
217
+ for (const c of [import_shared.HARNESS_NODES, import_shared.HARNESS_TOOLS, import_shared.HARNESS_MEMBERSHIP]) {
218
+ const rows = await col(c).snapshot({ filter: (0, import_core.eq)("threadId", threadId) }).catch(() => []);
219
+ for (const r of rows) await col(c).delete(r.id).catch(() => {
220
+ });
221
+ }
222
+ await col(import_shared.HARNESS_THREADS).delete(threadId).catch(() => {
223
+ });
224
+ };
225
+ const onBus = (threadId, e) => {
226
+ switch (e.type) {
227
+ case "reasoning_delta":
228
+ room(threadId).broadcast("harness.reasoningDelta", { threadId, nodeId: e.nodeId, text: e.text });
229
+ projectorFor(threadId).emit(e);
230
+ return;
231
+ case "text_delta":
232
+ room(threadId).broadcast("harness.textDelta", { threadId, nodeId: e.nodeId, text: e.text });
233
+ projectorFor(threadId).emit(e);
234
+ return;
235
+ case "tool_input_delta":
236
+ room(threadId).broadcast("harness.toolInputDelta", {
237
+ threadId,
238
+ nodeId: e.nodeId,
239
+ toolCallId: e.toolCallId,
240
+ argsTextDelta: e.argsTextDelta
241
+ });
242
+ projectorFor(threadId).emit(e);
243
+ return;
244
+ case "suspended":
245
+ room(threadId).broadcast("harness.suspended", {
246
+ threadId,
247
+ nodeId: e.nodeId,
248
+ toolCallId: e.toolCallId,
249
+ toolName: e.toolName,
250
+ request: e.request,
251
+ resumeSchema: e.resumeSchema
252
+ });
253
+ if (e.nodeId && e.toolCallId) sinkFor(threadId).setPending(e.nodeId, e.toolCallId, { resumeSchema: e.resumeSchema, request: e.request });
254
+ return;
255
+ case "approval_required":
256
+ room(threadId).broadcast("harness.approvalRequired", {
257
+ threadId,
258
+ nodeId: e.nodeId,
259
+ toolCallId: e.toolCallId,
260
+ toolName: e.toolName,
261
+ args: e.args
262
+ });
263
+ return;
264
+ case "mode_changed":
265
+ room(threadId).broadcast("harness.modeChanged", { threadId, modeId: e.modeId, previousModeId: e.previousModeId });
266
+ return;
267
+ case "follow_up_queued":
268
+ room(threadId).broadcast("harness.followUpQueued", { threadId, count: e.count });
269
+ return;
270
+ case "thread_created":
271
+ sinkFor(threadId).setThreadMeta({ resourceId: e.resourceId, title: e.title, createdAt: now(), updatedAt: now() });
272
+ return;
273
+ case "thread_renamed":
274
+ sinkFor(threadId).setThreadMeta({ title: e.title, updatedAt: now() });
275
+ return;
276
+ case "thread_deleted":
277
+ void purgeThread(threadId);
278
+ return;
279
+ case "tree_changed":
280
+ return;
281
+ default:
282
+ projectorFor(threadId).emit(e);
283
+ }
284
+ };
285
+ const buildHandlers = (ctx) => {
286
+ pctx = ctx;
287
+ return {
288
+ "harness.join": async ({ threadId }, connCtx, conn) => {
289
+ const c = connCtx;
290
+ room(threadId).add(conn);
291
+ const role = opts.roleFor?.(c) ?? opts.defaultRole ?? "operator";
292
+ const row = { id: (0, import_shared.membershipId)(threadId, c.userId), threadId, userId: c.userId, role, joinedAt: now() };
293
+ await col(import_shared.HARNESS_MEMBERSHIP).insert(row).catch((e) => {
294
+ if (e?.code !== "CONFLICT") console.error("[harness] membership insert failed", e);
295
+ });
296
+ return ok;
297
+ },
298
+ "harness.sendMessage": async ({ threadId, message, files }, connCtx) => {
299
+ await requireDriver(threadId, connCtx);
300
+ void engine.sendMessage({ threadId, content: message, files }).catch((e) => console.error("[harness] run failed", e));
301
+ return ok;
302
+ },
303
+ "harness.resumeMessage": async ({ threadId, toolCallId, resumeData }, connCtx) => {
304
+ await requireDriver(threadId, connCtx);
305
+ try {
306
+ void engine.resume({ threadId, toolCallId, resumeData }).catch((e) => console.error("[harness] resume failed", e));
307
+ return ok;
308
+ } catch (e) {
309
+ console.error("[harness] resume rejected", e);
310
+ return { ok: false };
311
+ }
312
+ },
313
+ "harness.abort": async ({ threadId }, connCtx) => {
314
+ await requireDriver(threadId, connCtx);
315
+ engine.abort(threadId);
316
+ return ok;
317
+ },
318
+ "harness.respondToApproval": async (input, connCtx) => {
319
+ await requireDriver(input.threadId, connCtx);
320
+ return attempt(() => engine.respondToApproval(input));
321
+ },
322
+ "harness.switchMode": async ({ threadId, modeId }, connCtx) => {
323
+ await requireDriver(threadId, connCtx);
324
+ return attempt(() => engine.switchMode(threadId, modeId));
325
+ },
326
+ "harness.listModes": async () => ({
327
+ modes: engine.listModes().map((m) => ({ id: m.id, name: m.name, description: m.description })),
328
+ defaultModeId: engine.defaultModeId
329
+ }),
330
+ // A connection-pinned resourceId is AUTHORITATIVE (the host's authenticate
331
+ // validated it — honoring the request's would let any client enumerate or
332
+ // write another tenant's scope). Hosts that leave ctx.resourceId unset opt
333
+ // into request-level scoping instead: clients switch resources per call,
334
+ // no reconnect. Note this covers the REQUEST surface only — a
335
+ // harness.threads collection subscription's read filter is frozen per
336
+ // connection from ctx, so live sidebar reactivity follows ctx, not requests.
337
+ "harness.listThreads": async (input, connCtx) => ({
338
+ threads: await engine.threads.list(connCtx.resourceId ?? input.resourceId)
339
+ }),
340
+ "harness.createThread": async (input, connCtx) => ({
341
+ threadId: (await engine.threads.create({ ...input, resourceId: connCtx.resourceId ?? input.resourceId })).id
342
+ }),
343
+ "harness.renameThread": async ({ threadId, title }, connCtx) => {
344
+ await requireDriver(threadId, connCtx);
345
+ return attempt(() => engine.threads.rename(threadId, title));
346
+ },
347
+ "harness.deleteThread": async ({ threadId }, connCtx) => {
348
+ await requireDriver(threadId, connCtx);
349
+ return attempt(() => engine.threads.delete(threadId));
350
+ }
351
+ };
352
+ };
353
+ return {
354
+ name: "harness",
355
+ policies: {
356
+ [import_shared.HARNESS_MEMBERSHIP]: { read: (principal) => (0, import_core.eq)("userId", principal) },
357
+ [import_shared.HARNESS_NODES]: { read: async (principal) => (0, import_core.isIn)("threadId", await joined(principal)) },
358
+ [import_shared.HARNESS_TOOLS]: { read: async (principal) => (0, import_core.isIn)("threadId", await joined(principal)) },
359
+ // Thread LIST = your resource's threads (sidebar) UNIONED with threads you
360
+ // joined. Membership must never be masked by resourceId: a client-minted
361
+ // thread (join + send, no createThread) has no resourceId on its row, and
362
+ // an eq-only filter would hide the caller's OWN thread — empty turns.
363
+ [import_shared.HARNESS_THREADS]: {
364
+ read: async (principal, ctx) => {
365
+ const rid = ctx?.resourceId;
366
+ const mine = (0, import_core.isIn)("id", await joined(principal));
367
+ return rid ? (0, import_core.or)((0, import_core.eq)("resourceId", rid), mine) : mine;
368
+ }
369
+ }
370
+ },
371
+ handlers: buildHandlers,
372
+ setup: (ctx) => {
373
+ pctx = ctx;
374
+ const unsub = engine.subscribe(onBus);
375
+ return () => unsub();
376
+ }
377
+ };
378
+ }
379
+ function now() {
380
+ return Date.now();
381
+ }
382
+
383
+ // src/serve.ts
384
+ async function collectionsFor(storage) {
385
+ switch (storage.type) {
386
+ case "memory":
387
+ return (0, import_collections_memory.memoryCollections)();
388
+ case "pglite": {
389
+ const { pgliteCollections } = await import("@super-line/collections-pglite");
390
+ return pgliteCollections({ pgUrl: storage.pgUrl, electricUrl: storage.electricUrl });
391
+ }
392
+ default:
393
+ return (0, import_collections_sqlite.sqliteCollections)({ file: storage.file ?? "./harness.db" });
394
+ }
395
+ }
396
+ var defaultAuthenticate = (h) => {
397
+ const q = h?.query ?? {};
398
+ return { role: "user", ctx: { userId: q.userId ?? q.resourceId ?? "local", resourceId: q.resourceId } };
399
+ };
400
+ async function serve(engine, config = {}) {
401
+ const collections = await collectionsFor(config.storage ?? { type: "sqlite" });
402
+ const server = (0, import_server.createSuperLineServer)(import_shared2.contract, {
403
+ transports: config.transports ?? [],
404
+ authenticate: config.authenticate ?? defaultAuthenticate,
405
+ identify: (conn) => conn.ctx.userId,
406
+ collections,
407
+ plugins: [harness(engine, config.plugin), ...config.plugins ?? []],
408
+ adapter: config.adapter
409
+ });
410
+ server.implement({});
411
+ return { server, close: () => void server.close() };
412
+ }
413
+
414
+ // src/index.ts
415
+ var import_shared3 = require("@super-harness/shared");
416
+ // Annotate the CommonJS export names for ESM import in node:
417
+ 0 && (module.exports = {
418
+ collectionsTreeSink,
419
+ harness,
420
+ harnessContract,
421
+ serve
422
+ });
@@ -0,0 +1,74 @@
1
+ import { ServerTransport, Adapter } from '@super-line/core';
2
+ import { SuperLinePlugin, createSuperLineServer } from '@super-line/server';
3
+ import { Harness, TreeSink } from '@super-harness/core';
4
+ import { MemberRole, HarnessSurface } from '@super-harness/shared';
5
+ export { harnessContract } from '@super-harness/shared';
6
+
7
+ interface HarnessCtx {
8
+ userId: string;
9
+ resourceId?: string;
10
+ roles?: string[];
11
+ }
12
+ interface HarnessPluginOptions {
13
+ roleFor?: (ctx: HarnessCtx) => MemberRole;
14
+ defaultRole?: MemberRole;
15
+ }
16
+ declare function harness(engine: Harness, opts?: HarnessPluginOptions): SuperLinePlugin<HarnessSurface>;
17
+
18
+ type HarnessStorage = {
19
+ type: 'memory';
20
+ } | {
21
+ type: 'sqlite';
22
+ file?: string;
23
+ } | {
24
+ type: 'pglite';
25
+ pgUrl: string;
26
+ electricUrl?: string;
27
+ };
28
+ interface ServeConfig {
29
+ storage?: HarnessStorage;
30
+ transports?: ServerTransport[];
31
+ adapter?: Adapter;
32
+ authenticate?: (handshake: unknown) => {
33
+ role: 'user';
34
+ ctx: HarnessCtx;
35
+ };
36
+ plugin?: HarnessPluginOptions;
37
+ plugins?: SuperLinePlugin[];
38
+ }
39
+ interface HarnessServer {
40
+ server: ReturnType<typeof createSuperLineServer>;
41
+ close(): void;
42
+ }
43
+ declare function serve(engine: Harness, config?: ServeConfig): Promise<HarnessServer>;
44
+
45
+ interface CollectionHandleLike {
46
+ insert(row: unknown): Promise<void>;
47
+ update(row: unknown): Promise<void>;
48
+ delete(id: string): Promise<void>;
49
+ snapshot(query?: unknown): Promise<unknown[]>;
50
+ }
51
+ interface Collections {
52
+ (name: string): CollectionHandleLike;
53
+ }
54
+ interface CollectionsTreeSink extends TreeSink {
55
+ setThreadMeta(meta: {
56
+ resourceId?: string;
57
+ title?: string;
58
+ createdAt?: number;
59
+ updatedAt?: number;
60
+ }): void;
61
+ setPending(nodeId: string, toolCallId: string, payload: {
62
+ resumeSchema?: string;
63
+ request?: unknown;
64
+ }): void;
65
+ }
66
+ declare function collectionsTreeSink(opts: {
67
+ collections: Collections;
68
+ threadId: string;
69
+ nodes: string;
70
+ tools: string;
71
+ threads: string;
72
+ }): CollectionsTreeSink;
73
+
74
+ export { type CollectionHandleLike, type Collections, type CollectionsTreeSink, type HarnessCtx, type HarnessPluginOptions, type HarnessServer, type HarnessStorage, type ServeConfig, collectionsTreeSink, harness, serve };
@@ -0,0 +1,74 @@
1
+ import { ServerTransport, Adapter } from '@super-line/core';
2
+ import { SuperLinePlugin, createSuperLineServer } from '@super-line/server';
3
+ import { Harness, TreeSink } from '@super-harness/core';
4
+ import { MemberRole, HarnessSurface } from '@super-harness/shared';
5
+ export { harnessContract } from '@super-harness/shared';
6
+
7
+ interface HarnessCtx {
8
+ userId: string;
9
+ resourceId?: string;
10
+ roles?: string[];
11
+ }
12
+ interface HarnessPluginOptions {
13
+ roleFor?: (ctx: HarnessCtx) => MemberRole;
14
+ defaultRole?: MemberRole;
15
+ }
16
+ declare function harness(engine: Harness, opts?: HarnessPluginOptions): SuperLinePlugin<HarnessSurface>;
17
+
18
+ type HarnessStorage = {
19
+ type: 'memory';
20
+ } | {
21
+ type: 'sqlite';
22
+ file?: string;
23
+ } | {
24
+ type: 'pglite';
25
+ pgUrl: string;
26
+ electricUrl?: string;
27
+ };
28
+ interface ServeConfig {
29
+ storage?: HarnessStorage;
30
+ transports?: ServerTransport[];
31
+ adapter?: Adapter;
32
+ authenticate?: (handshake: unknown) => {
33
+ role: 'user';
34
+ ctx: HarnessCtx;
35
+ };
36
+ plugin?: HarnessPluginOptions;
37
+ plugins?: SuperLinePlugin[];
38
+ }
39
+ interface HarnessServer {
40
+ server: ReturnType<typeof createSuperLineServer>;
41
+ close(): void;
42
+ }
43
+ declare function serve(engine: Harness, config?: ServeConfig): Promise<HarnessServer>;
44
+
45
+ interface CollectionHandleLike {
46
+ insert(row: unknown): Promise<void>;
47
+ update(row: unknown): Promise<void>;
48
+ delete(id: string): Promise<void>;
49
+ snapshot(query?: unknown): Promise<unknown[]>;
50
+ }
51
+ interface Collections {
52
+ (name: string): CollectionHandleLike;
53
+ }
54
+ interface CollectionsTreeSink extends TreeSink {
55
+ setThreadMeta(meta: {
56
+ resourceId?: string;
57
+ title?: string;
58
+ createdAt?: number;
59
+ updatedAt?: number;
60
+ }): void;
61
+ setPending(nodeId: string, toolCallId: string, payload: {
62
+ resumeSchema?: string;
63
+ request?: unknown;
64
+ }): void;
65
+ }
66
+ declare function collectionsTreeSink(opts: {
67
+ collections: Collections;
68
+ threadId: string;
69
+ nodes: string;
70
+ tools: string;
71
+ threads: string;
72
+ }): CollectionsTreeSink;
73
+
74
+ export { type CollectionHandleLike, type Collections, type CollectionsTreeSink, type HarnessCtx, type HarnessPluginOptions, type HarnessServer, type HarnessStorage, type ServeConfig, collectionsTreeSink, harness, serve };
package/dist/index.js ADDED
@@ -0,0 +1,389 @@
1
+ // src/serve.ts
2
+ import { createSuperLineServer } from "@super-line/server";
3
+ import { memoryCollections } from "@super-line/collections-memory";
4
+ import { sqliteCollections } from "@super-line/collections-sqlite";
5
+ import { contract } from "@super-harness/shared";
6
+
7
+ // src/plugin.ts
8
+ import { SuperLineError, eq, isIn, or } from "@super-line/core";
9
+ import { Projector } from "@super-harness/core";
10
+ import {
11
+ HARNESS_THREADS,
12
+ HARNESS_NODES,
13
+ HARNESS_TOOLS,
14
+ HARNESS_MEMBERSHIP,
15
+ membershipId,
16
+ harnessThreadRoom
17
+ } from "@super-harness/shared";
18
+
19
+ // src/sink.ts
20
+ var isCode = (e, code) => e?.code === code;
21
+ function collectionsTreeSink(opts) {
22
+ const { collections, threadId } = opts;
23
+ const nodesCol = () => collections(opts.nodes);
24
+ const toolsCol = () => collections(opts.tools);
25
+ const threadsCol = () => collections(opts.threads);
26
+ const known = /* @__PURE__ */ new Set();
27
+ const lastWritten = /* @__PURE__ */ new Map();
28
+ const chains = /* @__PURE__ */ new Map();
29
+ const run = (key, fn) => {
30
+ const prev = chains.get(key) ?? Promise.resolve();
31
+ chains.set(
32
+ key,
33
+ prev.then(fn).catch((e) => console.error("[harness] collection write failed", key, e))
34
+ );
35
+ };
36
+ const upsert = (handle, ns, id, row) => {
37
+ const key = `${ns}\0${id}`;
38
+ const serialized = JSON.stringify(row);
39
+ if (lastWritten.get(key) === serialized) return;
40
+ lastWritten.set(key, serialized);
41
+ run(key, async () => {
42
+ if (known.has(key)) return handle.update(row);
43
+ try {
44
+ await handle.insert(row);
45
+ known.add(key);
46
+ } catch (e) {
47
+ if (!isCode(e, "CONFLICT")) throw e;
48
+ known.add(key);
49
+ await handle.update(row);
50
+ }
51
+ });
52
+ };
53
+ let threadRow = { id: threadId, turns: [], todos: [], createdAt: 0, updatedAt: 0 };
54
+ const flushThread = () => upsert(threadsCol(), opts.threads, threadId, threadRow);
55
+ const pending = /* @__PURE__ */ new Map();
56
+ const pendingPayload = /* @__PURE__ */ new Map();
57
+ const nodeRowOf = (node) => {
58
+ const terminal = node.status !== "running";
59
+ const parkedTool = pending.get(node.nodeId);
60
+ const stillPending = parkedTool !== void 0 && !isSettled(node.tools[parkedTool]?.status);
61
+ if (parkedTool !== void 0 && !stillPending) pending.delete(node.nodeId);
62
+ return {
63
+ id: node.nodeId,
64
+ threadId,
65
+ parentNodeId: node.parentNodeId,
66
+ depth: node.depth,
67
+ agentType: node.agentType,
68
+ task: node.task,
69
+ status: node.status,
70
+ reasoning: terminal ? node.reasoning : "",
71
+ text: terminal ? node.text : "",
72
+ toolOrder: node.toolOrder,
73
+ childOrder: node.childOrder,
74
+ usage: node.usage,
75
+ durationMs: node.durationMs,
76
+ error: node.error,
77
+ textOffset: node.textOffset,
78
+ pendingResume: stillPending ? pendingPayload.get(node.nodeId) : void 0
79
+ };
80
+ };
81
+ const toolRowOf = (node, toolCallId) => {
82
+ const t = node.tools[toolCallId];
83
+ const streaming = t.status === "input-streaming";
84
+ return {
85
+ id: t.toolCallId,
86
+ threadId,
87
+ nodeId: node.nodeId,
88
+ toolName: t.toolName,
89
+ status: t.status,
90
+ argsText: streaming ? "" : t.argsText,
91
+ args: t.args,
92
+ result: t.result,
93
+ isError: t.isError,
94
+ textOffset: t.textOffset
95
+ };
96
+ };
97
+ return {
98
+ writeNode: (node) => {
99
+ upsert(nodesCol(), opts.nodes, node.nodeId, nodeRowOf(node));
100
+ for (const tid of node.toolOrder) upsert(toolsCol(), opts.tools, tid, toolRowOf(node, tid));
101
+ },
102
+ writeThread: (doc) => {
103
+ threadRow = { ...threadRow, turns: doc.turns, todos: doc.todos ?? [] };
104
+ flushThread();
105
+ },
106
+ setThreadMeta: (meta) => {
107
+ threadRow = {
108
+ ...threadRow,
109
+ resourceId: meta.resourceId ?? threadRow.resourceId,
110
+ title: meta.title ?? threadRow.title,
111
+ createdAt: threadRow.createdAt || meta.createdAt || 0,
112
+ updatedAt: meta.updatedAt ?? threadRow.updatedAt
113
+ };
114
+ flushThread();
115
+ },
116
+ setPending: (nodeId, toolCallId, payload) => {
117
+ pending.set(nodeId, toolCallId);
118
+ pendingPayload.set(nodeId, payload);
119
+ }
120
+ };
121
+ }
122
+ function isSettled(status) {
123
+ return status === "output-available" || status === "error";
124
+ }
125
+
126
+ // src/plugin.ts
127
+ function harness(engine, opts = {}) {
128
+ let pctx;
129
+ const P = () => {
130
+ if (!pctx) throw new Error("harness plugin: setup() has not run yet");
131
+ return pctx;
132
+ };
133
+ const col = (name) => P().collection(name);
134
+ const room = (threadId) => P().room(harnessThreadRoom(threadId));
135
+ const sinks = /* @__PURE__ */ new Map();
136
+ const projectors = /* @__PURE__ */ new Map();
137
+ const sinkFor = (threadId) => {
138
+ let s = sinks.get(threadId);
139
+ if (!s) {
140
+ s = collectionsTreeSink({
141
+ collections: (n) => col(n),
142
+ threadId,
143
+ nodes: HARNESS_NODES,
144
+ tools: HARNESS_TOOLS,
145
+ threads: HARNESS_THREADS
146
+ });
147
+ sinks.set(threadId, s);
148
+ }
149
+ return s;
150
+ };
151
+ const projectorFor = (threadId) => {
152
+ let p = projectors.get(threadId);
153
+ if (!p) {
154
+ p = new Projector(sinkFor(threadId));
155
+ projectors.set(threadId, p);
156
+ }
157
+ return p;
158
+ };
159
+ const joined = async (principal) => {
160
+ const rows = await col(HARNESS_MEMBERSHIP).snapshot({ filter: eq("userId", principal) });
161
+ return rows.map((r) => r.threadId);
162
+ };
163
+ const roleOf = async (threadId, userId) => {
164
+ const row = await col(HARNESS_MEMBERSHIP).read(membershipId(threadId, userId));
165
+ return row?.role;
166
+ };
167
+ const requireDriver = async (threadId, ctx) => {
168
+ if (await roleOf(threadId, ctx.userId) === "viewer") {
169
+ throw new SuperLineError("FORBIDDEN", "viewer role cannot drive the thread");
170
+ }
171
+ };
172
+ const ok = { ok: true };
173
+ const attempt = async (fn) => {
174
+ try {
175
+ await fn();
176
+ return ok;
177
+ } catch (e) {
178
+ console.error("[harness] request failed", e);
179
+ return { ok: false };
180
+ }
181
+ };
182
+ const purgeThread = async (threadId) => {
183
+ projectors.delete(threadId);
184
+ sinks.delete(threadId);
185
+ for (const c of [HARNESS_NODES, HARNESS_TOOLS, HARNESS_MEMBERSHIP]) {
186
+ const rows = await col(c).snapshot({ filter: eq("threadId", threadId) }).catch(() => []);
187
+ for (const r of rows) await col(c).delete(r.id).catch(() => {
188
+ });
189
+ }
190
+ await col(HARNESS_THREADS).delete(threadId).catch(() => {
191
+ });
192
+ };
193
+ const onBus = (threadId, e) => {
194
+ switch (e.type) {
195
+ case "reasoning_delta":
196
+ room(threadId).broadcast("harness.reasoningDelta", { threadId, nodeId: e.nodeId, text: e.text });
197
+ projectorFor(threadId).emit(e);
198
+ return;
199
+ case "text_delta":
200
+ room(threadId).broadcast("harness.textDelta", { threadId, nodeId: e.nodeId, text: e.text });
201
+ projectorFor(threadId).emit(e);
202
+ return;
203
+ case "tool_input_delta":
204
+ room(threadId).broadcast("harness.toolInputDelta", {
205
+ threadId,
206
+ nodeId: e.nodeId,
207
+ toolCallId: e.toolCallId,
208
+ argsTextDelta: e.argsTextDelta
209
+ });
210
+ projectorFor(threadId).emit(e);
211
+ return;
212
+ case "suspended":
213
+ room(threadId).broadcast("harness.suspended", {
214
+ threadId,
215
+ nodeId: e.nodeId,
216
+ toolCallId: e.toolCallId,
217
+ toolName: e.toolName,
218
+ request: e.request,
219
+ resumeSchema: e.resumeSchema
220
+ });
221
+ if (e.nodeId && e.toolCallId) sinkFor(threadId).setPending(e.nodeId, e.toolCallId, { resumeSchema: e.resumeSchema, request: e.request });
222
+ return;
223
+ case "approval_required":
224
+ room(threadId).broadcast("harness.approvalRequired", {
225
+ threadId,
226
+ nodeId: e.nodeId,
227
+ toolCallId: e.toolCallId,
228
+ toolName: e.toolName,
229
+ args: e.args
230
+ });
231
+ return;
232
+ case "mode_changed":
233
+ room(threadId).broadcast("harness.modeChanged", { threadId, modeId: e.modeId, previousModeId: e.previousModeId });
234
+ return;
235
+ case "follow_up_queued":
236
+ room(threadId).broadcast("harness.followUpQueued", { threadId, count: e.count });
237
+ return;
238
+ case "thread_created":
239
+ sinkFor(threadId).setThreadMeta({ resourceId: e.resourceId, title: e.title, createdAt: now(), updatedAt: now() });
240
+ return;
241
+ case "thread_renamed":
242
+ sinkFor(threadId).setThreadMeta({ title: e.title, updatedAt: now() });
243
+ return;
244
+ case "thread_deleted":
245
+ void purgeThread(threadId);
246
+ return;
247
+ case "tree_changed":
248
+ return;
249
+ default:
250
+ projectorFor(threadId).emit(e);
251
+ }
252
+ };
253
+ const buildHandlers = (ctx) => {
254
+ pctx = ctx;
255
+ return {
256
+ "harness.join": async ({ threadId }, connCtx, conn) => {
257
+ const c = connCtx;
258
+ room(threadId).add(conn);
259
+ const role = opts.roleFor?.(c) ?? opts.defaultRole ?? "operator";
260
+ const row = { id: membershipId(threadId, c.userId), threadId, userId: c.userId, role, joinedAt: now() };
261
+ await col(HARNESS_MEMBERSHIP).insert(row).catch((e) => {
262
+ if (e?.code !== "CONFLICT") console.error("[harness] membership insert failed", e);
263
+ });
264
+ return ok;
265
+ },
266
+ "harness.sendMessage": async ({ threadId, message, files }, connCtx) => {
267
+ await requireDriver(threadId, connCtx);
268
+ void engine.sendMessage({ threadId, content: message, files }).catch((e) => console.error("[harness] run failed", e));
269
+ return ok;
270
+ },
271
+ "harness.resumeMessage": async ({ threadId, toolCallId, resumeData }, connCtx) => {
272
+ await requireDriver(threadId, connCtx);
273
+ try {
274
+ void engine.resume({ threadId, toolCallId, resumeData }).catch((e) => console.error("[harness] resume failed", e));
275
+ return ok;
276
+ } catch (e) {
277
+ console.error("[harness] resume rejected", e);
278
+ return { ok: false };
279
+ }
280
+ },
281
+ "harness.abort": async ({ threadId }, connCtx) => {
282
+ await requireDriver(threadId, connCtx);
283
+ engine.abort(threadId);
284
+ return ok;
285
+ },
286
+ "harness.respondToApproval": async (input, connCtx) => {
287
+ await requireDriver(input.threadId, connCtx);
288
+ return attempt(() => engine.respondToApproval(input));
289
+ },
290
+ "harness.switchMode": async ({ threadId, modeId }, connCtx) => {
291
+ await requireDriver(threadId, connCtx);
292
+ return attempt(() => engine.switchMode(threadId, modeId));
293
+ },
294
+ "harness.listModes": async () => ({
295
+ modes: engine.listModes().map((m) => ({ id: m.id, name: m.name, description: m.description })),
296
+ defaultModeId: engine.defaultModeId
297
+ }),
298
+ // A connection-pinned resourceId is AUTHORITATIVE (the host's authenticate
299
+ // validated it — honoring the request's would let any client enumerate or
300
+ // write another tenant's scope). Hosts that leave ctx.resourceId unset opt
301
+ // into request-level scoping instead: clients switch resources per call,
302
+ // no reconnect. Note this covers the REQUEST surface only — a
303
+ // harness.threads collection subscription's read filter is frozen per
304
+ // connection from ctx, so live sidebar reactivity follows ctx, not requests.
305
+ "harness.listThreads": async (input, connCtx) => ({
306
+ threads: await engine.threads.list(connCtx.resourceId ?? input.resourceId)
307
+ }),
308
+ "harness.createThread": async (input, connCtx) => ({
309
+ threadId: (await engine.threads.create({ ...input, resourceId: connCtx.resourceId ?? input.resourceId })).id
310
+ }),
311
+ "harness.renameThread": async ({ threadId, title }, connCtx) => {
312
+ await requireDriver(threadId, connCtx);
313
+ return attempt(() => engine.threads.rename(threadId, title));
314
+ },
315
+ "harness.deleteThread": async ({ threadId }, connCtx) => {
316
+ await requireDriver(threadId, connCtx);
317
+ return attempt(() => engine.threads.delete(threadId));
318
+ }
319
+ };
320
+ };
321
+ return {
322
+ name: "harness",
323
+ policies: {
324
+ [HARNESS_MEMBERSHIP]: { read: (principal) => eq("userId", principal) },
325
+ [HARNESS_NODES]: { read: async (principal) => isIn("threadId", await joined(principal)) },
326
+ [HARNESS_TOOLS]: { read: async (principal) => isIn("threadId", await joined(principal)) },
327
+ // Thread LIST = your resource's threads (sidebar) UNIONED with threads you
328
+ // joined. Membership must never be masked by resourceId: a client-minted
329
+ // thread (join + send, no createThread) has no resourceId on its row, and
330
+ // an eq-only filter would hide the caller's OWN thread — empty turns.
331
+ [HARNESS_THREADS]: {
332
+ read: async (principal, ctx) => {
333
+ const rid = ctx?.resourceId;
334
+ const mine = isIn("id", await joined(principal));
335
+ return rid ? or(eq("resourceId", rid), mine) : mine;
336
+ }
337
+ }
338
+ },
339
+ handlers: buildHandlers,
340
+ setup: (ctx) => {
341
+ pctx = ctx;
342
+ const unsub = engine.subscribe(onBus);
343
+ return () => unsub();
344
+ }
345
+ };
346
+ }
347
+ function now() {
348
+ return Date.now();
349
+ }
350
+
351
+ // src/serve.ts
352
+ async function collectionsFor(storage) {
353
+ switch (storage.type) {
354
+ case "memory":
355
+ return memoryCollections();
356
+ case "pglite": {
357
+ const { pgliteCollections } = await import("@super-line/collections-pglite");
358
+ return pgliteCollections({ pgUrl: storage.pgUrl, electricUrl: storage.electricUrl });
359
+ }
360
+ default:
361
+ return sqliteCollections({ file: storage.file ?? "./harness.db" });
362
+ }
363
+ }
364
+ var defaultAuthenticate = (h) => {
365
+ const q = h?.query ?? {};
366
+ return { role: "user", ctx: { userId: q.userId ?? q.resourceId ?? "local", resourceId: q.resourceId } };
367
+ };
368
+ async function serve(engine, config = {}) {
369
+ const collections = await collectionsFor(config.storage ?? { type: "sqlite" });
370
+ const server = createSuperLineServer(contract, {
371
+ transports: config.transports ?? [],
372
+ authenticate: config.authenticate ?? defaultAuthenticate,
373
+ identify: (conn) => conn.ctx.userId,
374
+ collections,
375
+ plugins: [harness(engine, config.plugin), ...config.plugins ?? []],
376
+ adapter: config.adapter
377
+ });
378
+ server.implement({});
379
+ return { server, close: () => void server.close() };
380
+ }
381
+
382
+ // src/index.ts
383
+ import { harnessContract } from "@super-harness/shared";
384
+ export {
385
+ collectionsTreeSink,
386
+ harness,
387
+ harnessContract,
388
+ serve
389
+ };
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@super-harness/server",
3
+ "version": "0.1.0",
4
+ "description": "Batteries-included super-line binding for Super Harness: the harness() plugin and standalone serve() expose a core Harness over a super-line server with durable harness.* collections, the wire contract, and HITL suspension/approval broadcast.",
5
+ "keywords": [
6
+ "agent",
7
+ "agents",
8
+ "multi-agent",
9
+ "ai",
10
+ "llm",
11
+ "language-model",
12
+ "tool-calling",
13
+ "orchestration",
14
+ "supervisor",
15
+ "subagent",
16
+ "streaming",
17
+ "harness",
18
+ "mastra",
19
+ "super-line",
20
+ "typescript"
21
+ ],
22
+ "homepage": "https://mertdogar.github.io/super-harness/",
23
+ "bugs": "https://github.com/mertdogar/super-harness/issues",
24
+ "license": "MIT",
25
+ "author": "Mert Dogar",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/mertdogar/super-harness.git",
29
+ "directory": "packages/server"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "CHANGELOG.md"
34
+ ],
35
+ "type": "module",
36
+ "sideEffects": false,
37
+ "main": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "exports": {
40
+ ".": {
41
+ "import": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/index.d.cts",
47
+ "default": "./dist/index.cjs"
48
+ }
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "dependencies": {
55
+ "@super-harness/shared": "^0.1.0",
56
+ "@super-harness/core": "^0.1.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@mastra/core": "^1.49.0-alpha.2",
60
+ "@super-line/collections-memory": "^0.1.0",
61
+ "@super-line/collections-pglite": "^0.1.0",
62
+ "@super-line/collections-sqlite": "^0.1.0",
63
+ "@super-line/core": "^0.11.0",
64
+ "@super-line/server": "^0.11.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@super-line/collections-pglite": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "@electric-sql/pglite": "^0.5.4",
73
+ "@libsql/client": "^0.17.4",
74
+ "@mastra/core": "1.49.0-alpha.2",
75
+ "@super-line/client": "^0.9.0",
76
+ "@super-line/collections-memory": "^0.1.0",
77
+ "@super-line/collections-pglite": "^0.1.0",
78
+ "@super-line/collections-sqlite": "^0.1.0",
79
+ "@super-line/core": "^0.11.0",
80
+ "@super-line/server": "^0.11.0",
81
+ "@super-line/transport-websocket": "^0.6.0",
82
+ "vitest": "^4.1.2",
83
+ "zod": "^4.4.3"
84
+ },
85
+ "engines": {
86
+ "node": ">=18"
87
+ },
88
+ "scripts": {
89
+ "build": "tsup",
90
+ "test": "vitest run",
91
+ "test:watch": "vitest"
92
+ }
93
+ }