@workerdeck/server 3.1.0 → 3.3.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/build/index.d.mts CHANGED
@@ -2,6 +2,7 @@ import { IncomingMessage, Server, ServerResponse } from "node:http";
2
2
  import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, CostLedgerState, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, LocalShellSource, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SHELL_REFUSAL, SessionInstructions, SessionRunnerConfig, ShellDirectory, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
3
3
  import { JobQueue, QueueAdapter } from "@workerdeck/queue";
4
4
  import { ByModel, CreateSessionRequest, JobEvent, MessageAttachment, ModelRate, PricingTable, ProfileEngine, ProfileInfo, ProfileSpend, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig, ShellEndReason, ShellInfo, ShellOwner } from "@workerdeck/protocol";
5
+ import { RelayOp } from "@workerdeck/relay-client";
5
6
  //#region src/services/bridge.d.ts
6
7
  type BridgeHubOptions = {
7
8
  timeoutMs?: number;
@@ -154,6 +155,20 @@ type PeerServiceOptions = {
154
155
  maxHops?: number;
155
156
  };
156
157
  //#endregion
158
+ //#region src/services/peer-relay.d.ts
159
+ type RelayLinkOptions = {
160
+ url: string;
161
+ gateway: string;
162
+ key?: string;
163
+ keyFile?: string;
164
+ caFile?: string;
165
+ expose?: {
166
+ scope?: Record<string, string>;
167
+ allow?: RelayOp[];
168
+ };
169
+ log?: (message: string) => void;
170
+ };
171
+ //#endregion
157
172
  //#region src/services/profile-store.d.ts
158
173
  type ProfileStore = {
159
174
  list(): ProfileInfo[] | Promise<ProfileInfo[]>;
@@ -354,6 +369,7 @@ type WorkerServerOptions = {
354
369
  listSdkSessions?: SdkSessionLister;
355
370
  queue?: QueueServerOptions;
356
371
  peers?: PeerServiceOptions;
372
+ relay?: RelayLinkOptions;
357
373
  notifications?: SessionNotificationOptions;
358
374
  bridge?: BridgeHubOptions;
359
375
  parking?: {
@@ -430,6 +446,7 @@ type WorkerServer = {
430
446
  drain: (options?: DrainOptions) => Promise<DrainReport>;
431
447
  releaseSession: (id: string) => CarriedSession | undefined;
432
448
  adoptSession: (carried: CarriedSession) => boolean;
449
+ releaseRelay: () => void;
433
450
  close: () => Promise<void>;
434
451
  };
435
452
  //#endregion
@@ -536,5 +553,5 @@ declare class ProfileUsageTracker {
536
553
  usage(profile: string, now?: number): ProfileUsage | undefined;
537
554
  }
538
555
  //#endregion
539
- export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type CarriedSession, type DiagnosticSink, type EngineRunnerContext, type FileSessionStoreOptions, type JsonWriteOptions, MemorySessionStore, type NotificationErrorContext, type ParkedSessionRecord, type ProcessRow, type ProcessTable, type ProducedFile, ProducedFileStore, type ProfileStore, ProfileUsageTracker, type ProviderRunnerOptions, type QueueServerOptions, type ReloadPlan, SHELL_REFUSAL, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type ShellAgentWriteOption, type ShellDirectoryDeps, type ShellRegistry, type ShellRegistryOptions, type ShellSink, SpendLedger, type SpendRecord, type SpendStore, type StoredSessionRecord, type StoredShellIndex, type StoredShellRecord, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, isMissing, readJsonOr, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord, writeFileAtomic, writeJsonAtomic };
556
+ export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type CarriedSession, type DiagnosticSink, type EngineRunnerContext, type FileSessionStoreOptions, type JsonWriteOptions, MemorySessionStore, type NotificationErrorContext, type ParkedSessionRecord, type ProcessRow, type ProcessTable, type ProducedFile, ProducedFileStore, type ProfileStore, ProfileUsageTracker, type ProviderRunnerOptions, type QueueServerOptions, type RelayLinkOptions, type ReloadPlan, SHELL_REFUSAL, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type ShellAgentWriteOption, type ShellDirectoryDeps, type ShellRegistry, type ShellRegistryOptions, type ShellSink, SpendLedger, type SpendRecord, type SpendStore, type StoredSessionRecord, type StoredShellIndex, type StoredShellRecord, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, isMissing, readJsonOr, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord, writeFileAtomic, writeJsonAtomic };
540
557
  //# sourceMappingURL=index.d.mts.map
package/build/index.mjs CHANGED
@@ -10,6 +10,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
10
10
  import { chmod, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
11
11
  import { spawnSync } from "node:child_process";
12
12
  import xtermHeadless from "@xterm/headless";
13
+ import { connectRelay, parseRelayPeerId } from "@workerdeck/relay-client";
13
14
  //#region src/lifecycle.ts
14
15
  const SOCKET_CLOSE_GRACE_MS = 250;
15
16
  const CLOSE_DEADLINE_MS = 1e3;
@@ -4625,6 +4626,163 @@ function createFileSpendStore(path, onError) {
4625
4626
  };
4626
4627
  }
4627
4628
  //#endregion
4629
+ //#region src/services/peer-relay.ts
4630
+ const CARRIED = Symbol.for("workerdeck.relay.carried");
4631
+ const slots = globalThis;
4632
+ const UNAVAILABLE = "Remote gateways are unavailable right now; only sessions on this gateway are listed.";
4633
+ function expandHome(path) {
4634
+ return path === "~" || path.startsWith("~/") ? `${homedir()}${path.slice(1)}` : path;
4635
+ }
4636
+ function identityOf(options) {
4637
+ return JSON.stringify([
4638
+ options.url,
4639
+ options.gateway,
4640
+ options.keyFile ?? "",
4641
+ options.key ? "inline" : "",
4642
+ options.caFile ?? "",
4643
+ options.expose?.allow ?? null
4644
+ ]);
4645
+ }
4646
+ function remoteSummary(row) {
4647
+ return {
4648
+ id: `${row.gateway}:${row.id}`,
4649
+ gateway: row.gateway,
4650
+ engine: row.engine,
4651
+ status: row.status,
4652
+ title: row.title,
4653
+ project: row.project?.name,
4654
+ projectRoot: row.project?.root,
4655
+ cwd: row.cwd,
4656
+ profile: row.profile,
4657
+ model: row.model,
4658
+ contextUsage: row.contextUsage,
4659
+ lastActivityAt: row.lastActivityAt,
4660
+ pendingPermissionCount: row.pendingPermissionCount,
4661
+ allow: row.allow
4662
+ };
4663
+ }
4664
+ function remotePeek(gateway, peek) {
4665
+ const { allow: _allow, ...summary } = remoteSummary({
4666
+ ...peek,
4667
+ gateway,
4668
+ allow: []
4669
+ });
4670
+ return {
4671
+ ...summary,
4672
+ live: peek.live,
4673
+ checklist: peek.checklistItems,
4674
+ pendingApprovals: peek.pendingApprovals,
4675
+ recent: peek.recent
4676
+ };
4677
+ }
4678
+ async function readRelayKey(options) {
4679
+ if (options.key) return options.key;
4680
+ if (!options.keyFile) throw new Error("relay: set relay.keyFile (or WORKERDECK_RELAY_KEY) to the key `workerdeck relay enroll` printed");
4681
+ const key = (await readFile(expandHome(options.keyFile), "utf8")).trim();
4682
+ if (!key) throw new Error(`relay: ${options.keyFile} is empty`);
4683
+ return key;
4684
+ }
4685
+ function createRelayLink(options, peers, log) {
4686
+ const identity = identityOf(options);
4687
+ const exposed = options.expose?.scope;
4688
+ let connection;
4689
+ let closed = false;
4690
+ let released = false;
4691
+ const host = {
4692
+ snapshot: () => peers.relayEntries(exposed),
4693
+ peek: (_origin, sessionId, recent) => peers.relayPeek(sessionId, recent, exposed),
4694
+ send: (origin, sessionId, text) => peers.relaySend(origin, sessionId, text, exposed)
4695
+ };
4696
+ const carried = slots[CARRIED];
4697
+ if (carried) {
4698
+ delete slots[CARRIED];
4699
+ if (carried.identity === identity && carried.connection.state() !== "stopped") {
4700
+ carried.connection.setHost(host);
4701
+ connection = carried.connection;
4702
+ } else carried.connection.close();
4703
+ }
4704
+ if (!connection) (async () => {
4705
+ const key = await readRelayKey(options);
4706
+ const ca = options.caFile ? await readFile(expandHome(options.caFile)) : void 0;
4707
+ if (!closed) connection = connectRelay({
4708
+ url: options.url,
4709
+ gateway: options.gateway,
4710
+ key,
4711
+ ca,
4712
+ allow: options.expose?.allow,
4713
+ log
4714
+ }, host);
4715
+ })().catch((error) => log(error instanceof Error ? error.message : String(error)));
4716
+ const remoteTarget = parseRelayPeerId;
4717
+ const reachesRemote = async (from) => {
4718
+ const me = await peers.relaySender(from);
4719
+ return me.scope === void 0 || Object.keys(me.scope).length === 0;
4720
+ };
4721
+ return {
4722
+ directory: {
4723
+ list: async (from) => {
4724
+ const local = await peers.list(from);
4725
+ if (!connection || connection.state() !== "online" || !await reachesRemote(from)) return local;
4726
+ try {
4727
+ return [...local, ...(await connection.list(from)).map(remoteSummary)];
4728
+ } catch {
4729
+ return local;
4730
+ }
4731
+ },
4732
+ notice: async (from) => {
4733
+ if (!await reachesRemote(from)) return;
4734
+ return connection?.state() === "online" ? void 0 : UNAVAILABLE;
4735
+ },
4736
+ peek: async (from, sessionId, peekOptions) => {
4737
+ const target = remoteTarget(sessionId);
4738
+ if (!target) return peers.peek(from, sessionId, peekOptions);
4739
+ if (!connection || !await reachesRemote(from)) return;
4740
+ const peek = await connection.peek(from, target, peekOptions?.recent);
4741
+ return peek ? remotePeek(target.gateway, peek) : void 0;
4742
+ },
4743
+ send: async (from, sessionId, text, sendOptions) => {
4744
+ const target = remoteTarget(sessionId);
4745
+ if (!target) return peers.send(from, sessionId, text, sendOptions);
4746
+ if (!await reachesRemote(from)) return {
4747
+ delivered: false,
4748
+ reason: `no such session: ${sessionId}`
4749
+ };
4750
+ if (!connection || connection.state() !== "online") return {
4751
+ delivered: false,
4752
+ reason: "remote gateways are unavailable right now; try again later"
4753
+ };
4754
+ try {
4755
+ const result = await connection.send(from, target, text, sendOptions?.hops ?? peers.relayChain(from));
4756
+ return result.delivered ? {
4757
+ ...result,
4758
+ sessionId
4759
+ } : result;
4760
+ } catch (error) {
4761
+ return {
4762
+ delivered: false,
4763
+ reason: error instanceof Error ? error.message : String(error)
4764
+ };
4765
+ }
4766
+ }
4767
+ },
4768
+ nudge: () => connection?.nudge(),
4769
+ release: () => {
4770
+ if (connection && !closed) {
4771
+ released = true;
4772
+ slots[CARRIED] = {
4773
+ identity,
4774
+ connection
4775
+ };
4776
+ }
4777
+ },
4778
+ close: () => {
4779
+ closed = true;
4780
+ if (!released) connection?.close();
4781
+ connection = void 0;
4782
+ }
4783
+ };
4784
+ }
4785
+ //#endregion
4628
4786
  //#region src/services/peers.ts
4629
4787
  const DEFAULT_PER_MINUTE = 10;
4630
4788
  const DEFAULT_MAX_HOPS = 12;
@@ -4632,6 +4790,33 @@ const WINDOW_MS = 6e4;
4632
4790
  function visible(from, to) {
4633
4791
  return to.id !== from.id && scopeMatches(from.scope, to.scope);
4634
4792
  }
4793
+ function relayEntry(info, live) {
4794
+ const items = info.checklist;
4795
+ return {
4796
+ id: info.id,
4797
+ engine: info.engine,
4798
+ status: info.status,
4799
+ title: info.title,
4800
+ project: info.project ? {
4801
+ name: info.project.name,
4802
+ root: info.project.root
4803
+ } : void 0,
4804
+ cwd: info.cwd,
4805
+ profile: info.profile,
4806
+ model: info.model,
4807
+ permissionMode: info.permissionMode,
4808
+ contextUsage: info.contextUsage,
4809
+ checklist: items?.length ? {
4810
+ done: items.filter((item) => item.status === "completed").length,
4811
+ total: items.length
4812
+ } : void 0,
4813
+ numTurns: info.numTurns,
4814
+ createdAt: info.createdAt,
4815
+ lastActivityAt: info.lastActivityAt,
4816
+ pendingPermissionCount: info.pendingPermissionCount,
4817
+ live
4818
+ };
4819
+ }
4635
4820
  function createPeerService(deps) {
4636
4821
  const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
4637
4822
  const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
@@ -4659,6 +4844,19 @@ function createPeerService(deps) {
4659
4844
  if (!info) throw new Error(`unknown sender session: ${from}`);
4660
4845
  return info;
4661
4846
  };
4847
+ const recentOf = (sessionId, recent) => {
4848
+ const limit = Math.min(Math.max(recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
4849
+ const runner = registry().get(sessionId);
4850
+ const events = [];
4851
+ if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
4852
+ truncateResults: true,
4853
+ imageRefs: true
4854
+ })();
4855
+ return {
4856
+ runner,
4857
+ lines: recentLines(events, limit)
4858
+ };
4859
+ };
4662
4860
  const underRateLimit = (from, to) => {
4663
4861
  const key = `${from}->${to}`;
4664
4862
  const now = Date.now();
@@ -4679,19 +4877,13 @@ function createPeerService(deps) {
4679
4877
  const me = await sender(from);
4680
4878
  const info = await infoOf(sessionId);
4681
4879
  if (!info || !visible(me, info)) return;
4682
- const limit = Math.min(Math.max(options?.recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
4683
- const runner = registry().get(sessionId);
4684
- const events = [];
4685
- if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
4686
- truncateResults: true,
4687
- imageRefs: true
4688
- })();
4880
+ const { runner, lines } = recentOf(sessionId, options?.recent);
4689
4881
  return {
4690
4882
  ...peerSummary(info),
4691
4883
  live: runner !== void 0,
4692
4884
  checklist: info.checklist,
4693
4885
  pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
4694
- recent: recentLines(events, limit)
4886
+ recent: lines
4695
4887
  };
4696
4888
  };
4697
4889
  const send = async (from, sessionId, text, options) => {
@@ -4709,16 +4901,27 @@ function createPeerService(deps) {
4709
4901
  delivered: false,
4710
4902
  reason: `no such session: ${sessionId}`
4711
4903
  };
4904
+ const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
4905
+ return deliver(target, text, {
4906
+ kind: "peer",
4907
+ sessionId: from,
4908
+ name: me.title,
4909
+ engine: me.engine,
4910
+ hops
4911
+ }, from);
4912
+ };
4913
+ const deliver = async (target, text, origin, rateKey) => {
4914
+ const sessionId = target.id;
4712
4915
  if (target.status === "closed" || target.status === "failed") return {
4713
4916
  delivered: false,
4714
4917
  reason: `session ${sessionId} is ${target.status}`
4715
4918
  };
4716
- const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
4919
+ const hops = origin.hops ?? [];
4717
4920
  if (hops.length > maxHops) return {
4718
4921
  delivered: false,
4719
4922
  reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
4720
4923
  };
4721
- if (!underRateLimit(from, sessionId)) return {
4924
+ if (!underRateLimit(rateKey, sessionId)) return {
4722
4925
  delivered: false,
4723
4926
  reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
4724
4927
  };
@@ -4728,13 +4931,6 @@ function createPeerService(deps) {
4728
4931
  reason: `session ${sessionId} could not be woken`
4729
4932
  };
4730
4933
  const before = runner.info().status;
4731
- const origin = {
4732
- kind: "peer",
4733
- sessionId: from,
4734
- name: me.title,
4735
- engine: me.engine,
4736
- hops
4737
- };
4738
4934
  try {
4739
4935
  runner.sendMessage(text, void 0, { origin });
4740
4936
  } catch (error) {
@@ -4751,6 +4947,45 @@ function createPeerService(deps) {
4751
4947
  queued: before === "running" || before === "awaiting_approval"
4752
4948
  };
4753
4949
  };
4950
+ const exposedInfo = async (sessionId, exposed) => {
4951
+ const info = await infoOf(sessionId);
4952
+ return info && info.status !== "closed" && scopeMatches(exposed, info.scope) ? info : void 0;
4953
+ };
4954
+ const relayEntries = async (exposed) => {
4955
+ const live = new Set(registry().list().map((info) => info.id));
4956
+ return (await allSessions()).filter((info) => info.status !== "closed" && scopeMatches(exposed, info.scope)).map((info) => relayEntry(info, live.has(info.id)));
4957
+ };
4958
+ const relayPeek = async (sessionId, recent, exposed) => {
4959
+ const info = await exposedInfo(sessionId, exposed);
4960
+ if (!info) return;
4961
+ const { runner, lines } = recentOf(sessionId, recent);
4962
+ return {
4963
+ ...relayEntry(info, runner !== void 0),
4964
+ checklistItems: info.checklist,
4965
+ pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
4966
+ recent: lines
4967
+ };
4968
+ };
4969
+ const relaySend = async (origin, sessionId, text, exposed) => {
4970
+ if (text.length === 0 || text.length > maxChars) return {
4971
+ delivered: false,
4972
+ reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
4973
+ };
4974
+ const target = await exposedInfo(sessionId, exposed);
4975
+ if (!target) return {
4976
+ delivered: false,
4977
+ reason: `no such session: ${sessionId}`
4978
+ };
4979
+ const from = `${origin.gateway}:${origin.sessionId}`;
4980
+ return deliver(target, text, {
4981
+ kind: "peer",
4982
+ sessionId: from,
4983
+ hostId: origin.gateway,
4984
+ name: origin.name,
4985
+ engine: origin.engine,
4986
+ hops: origin.hops
4987
+ }, from);
4988
+ };
4754
4989
  const mentions = async (from, text) => {
4755
4990
  const tokens = scanPeerMentions(text);
4756
4991
  if (tokens.length === 0) return [];
@@ -4797,7 +5032,12 @@ function createPeerService(deps) {
4797
5032
  peek,
4798
5033
  send,
4799
5034
  mentions,
4800
- watch
5035
+ watch,
5036
+ relaySender: sender,
5037
+ relayChain: (from) => [...inbound.get(from) ?? [], from],
5038
+ relayEntries,
5039
+ relayPeek,
5040
+ relaySend
4801
5041
  };
4802
5042
  }
4803
5043
  //#endregion
@@ -5222,9 +5462,10 @@ function createWorkerServer(options = {}) {
5222
5462
  options: options.peers
5223
5463
  });
5224
5464
  const shellDirectory = shells ? createShellDirectory(shells, { runnerFor: (id) => registry.get(id) }) : void 0;
5225
- let ownPeers = peers;
5465
+ const relay = peers && options.relay ? createRelayLink(options.relay, peers, options.relay.log ?? ((message) => diagnose(new Error(message), "relay"))) : void 0;
5466
+ let ownPeers = relay?.directory ?? peers;
5226
5467
  let ownShells = shellDirectory;
5227
- if (peers) installPeerDirectory(peers);
5468
+ if (ownPeers) installPeerDirectory(ownPeers);
5228
5469
  installShellDirectory(shellDirectory);
5229
5470
  const factory = createSessionFactory({
5230
5471
  adapterFor,
@@ -5341,6 +5582,7 @@ function createWorkerServer(options = {}) {
5341
5582
  closeQueueSockets: queueSockets.clear,
5342
5583
  diagnose,
5343
5584
  releaseDirectories: () => {
5585
+ relay?.close();
5344
5586
  ownPeers = void 0;
5345
5587
  ownShells = void 0;
5346
5588
  }
@@ -5383,6 +5625,7 @@ function createWorkerServer(options = {}) {
5383
5625
  parking.adopt(runner, config);
5384
5626
  return true;
5385
5627
  },
5628
+ releaseRelay: () => relay?.release(),
5386
5629
  drain: lifecycle.drain,
5387
5630
  close: lifecycle.close
5388
5631
  };