@byok-sdk/server 0.8.1 → 0.9.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/dist/hub.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { WebSocket } from 'ws';
2
- import { type Envelope, type AgentHomeProjectionCompletionRequest, type AgentHomeProjectionReadback, type RuntimeId, type RuntimeInfo, type TaskState, type ToolsetId } from '@byok-sdk/protocol';
2
+ import { type Envelope, type AgentMessagePublishPayload, type AgentMessageServerContext, type AgentHomeProjectionCompletionRequest, type AgentHomeProjectionReadback, type RuntimeId, type RuntimeInfo, type TaskState, type ToolsetId } from '@byok-sdk/protocol';
3
3
  import type { DeviceRegistry } from './auth';
4
4
  import { RateLimiter } from './rate-limiter';
5
5
  import type { TaskStore } from './task-store';
@@ -209,6 +209,7 @@ export declare class ConnectionHub {
209
209
  * working unchanged.
210
210
  */
211
211
  private readonly rateLimiter;
212
+ private readonly agentMessage?;
212
213
  private readonly connections;
213
214
  private readonly outboxes;
214
215
  /** Idempotency window per device (N3) — recent inbound envelope ids, capped at {@link DEDUP_RING_CAPACITY}. */
@@ -218,6 +219,9 @@ export declare class ConnectionHub {
218
219
  private readonly serverEvents;
219
220
  /** First-write-wins reliable facts; the reference composition's bounded in-memory readback. */
220
221
  private readonly agentEgressReceipts;
222
+ private readonly agentMessageRequirements;
223
+ private readonly agentMessageReceipts;
224
+ private readonly agentMessageTaskPayloads;
221
225
  /** Accepted requests are the authority that later receipts/transfers must echo exactly. */
222
226
  private readonly agentContentReadRequests;
223
227
  /** Content-free explicit-read audit facts keyed by exact authenticated device/request identity. */
@@ -267,7 +271,17 @@ export declare class ConnectionHub {
267
271
  * call site (this hub is constructed directly by several tests) keeps
268
272
  * working unchanged.
269
273
  */
270
- rateLimiter?: RateLimiter);
274
+ rateLimiter?: RateLimiter, agentMessage?: {
275
+ consume(input: {
276
+ readonly deviceId: string;
277
+ readonly taskId: string;
278
+ readonly context: AgentMessageServerContext;
279
+ readonly payload: AgentMessagePublishPayload;
280
+ }): {
281
+ readonly outcome: 'accepted' | 'held' | 'refused';
282
+ readonly reasonCode?: string;
283
+ };
284
+ } | undefined);
271
285
  /**
272
286
  * Stop the task-lease reaper's sweep timer — called by `ByokServer.stop()`
273
287
  * (`index.ts`) on shutdown. Idempotent: clearing an already-cleared
@@ -380,6 +394,8 @@ export declare class ConnectionHub {
380
394
  * facts is rejected rather than treated as an update.
381
395
  */
382
396
  private handleAgentEgressReliable;
397
+ private handleAgentMessagePublish;
398
+ private sendAgentMessageDisposition;
383
399
  private handleAgentContentReceipt;
384
400
  private sendAgentEgressAck;
385
401
  private sendAgentContentReceiptAck;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { mkdir, writeFile, readFile } from 'fs/promises';
5
5
  import { mkdtempSync, mkdirSync, existsSync, chmodSync } from 'fs';
6
6
  import { tmpdir } from 'os';
7
7
  import path, { dirname } from 'path';
8
- import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, AgentRefSchema, AgentEgressPolicySchema, DispatchSelectionSchema, RequiredToolsetsSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AgentContentReadPayloadSchema, AgentHomeProjectionPayloadSchema, AGENT_HOME_PROJECTION_CAPABILITY, AgentHomeProjectionReadbackSchema, AgentHomeProjectionCompletionRequestSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, PairResponseSchema, BYOK_CHALLENGE_PATH, ChallengeRequestSchema, BYOK_TOKEN_PATH, TokenRequestSchema, BYOK_BLOBS_PATH, CreateBlobRequestSchema, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendRequestSchema, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, PairResponseTenantIdSchema, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
8
+ import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentRefSchema, AgentEgressPolicySchema, AgentMessageEgressRequirementSchema, TerminalProjectionSelectionSchema, AgentMessageServerContextSchema, DispatchSelectionSchema, RequiredToolsetsSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, AgentContentReadPayloadSchema, AgentHomeProjectionPayloadSchema, AGENT_HOME_PROJECTION_CAPABILITY, AgentHomeProjectionReadbackSchema, AgentHomeProjectionCompletionRequestSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, PairResponseSchema, BYOK_CHALLENGE_PATH, ChallengeRequestSchema, BYOK_TOKEN_PATH, TokenRequestSchema, BYOK_BLOBS_PATH, CreateBlobRequestSchema, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendRequestSchema, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, PairResponseTenantIdSchema, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
9
9
  import { Hono } from 'hono';
10
10
  import { WebSocketServer } from 'ws';
11
11
  import { createRequire } from 'module';
@@ -540,11 +540,12 @@ var AgentHomeProjectionCompletionError = class extends Error {
540
540
  code;
541
541
  };
542
542
  var ConnectionHub = class {
543
- constructor(taskStore, devices, taskLeaseMs, rateLimiter = new RateLimiter()) {
543
+ constructor(taskStore, devices, taskLeaseMs, rateLimiter = new RateLimiter(), agentMessage) {
544
544
  this.taskStore = taskStore;
545
545
  this.devices = devices;
546
546
  this.taskLeaseMs = taskLeaseMs;
547
547
  this.rateLimiter = rateLimiter;
548
+ this.agentMessage = agentMessage;
548
549
  const sweepIntervalMs = Math.min(Math.max(taskLeaseMs, 10), MAX_LEASE_REAPER_SWEEP_INTERVAL_MS);
549
550
  this.leaseReaperTimer = setInterval(() => this.sweepLeases(), sweepIntervalMs);
550
551
  this.leaseReaperTimer.unref?.();
@@ -553,6 +554,7 @@ var ConnectionHub = class {
553
554
  devices;
554
555
  taskLeaseMs;
555
556
  rateLimiter;
557
+ agentMessage;
556
558
  connections = /* @__PURE__ */ new Map();
557
559
  outboxes = /* @__PURE__ */ new Map();
558
560
  /** Idempotency window per device (N3) — recent inbound envelope ids, capped at {@link DEDUP_RING_CAPACITY}. */
@@ -562,6 +564,9 @@ var ConnectionHub = class {
562
564
  serverEvents = new AsyncEventQueue();
563
565
  /** First-write-wins reliable facts; the reference composition's bounded in-memory readback. */
564
566
  agentEgressReceipts = /* @__PURE__ */ new Map();
567
+ agentMessageRequirements = /* @__PURE__ */ new Map();
568
+ agentMessageReceipts = /* @__PURE__ */ new Map();
569
+ agentMessageTaskPayloads = /* @__PURE__ */ new Map();
565
570
  /** Accepted requests are the authority that later receipts/transfers must echo exactly. */
566
571
  agentContentReadRequests = /* @__PURE__ */ new Map();
567
572
  /** Content-free explicit-read audit facts keyed by exact authenticated device/request identity. */
@@ -824,6 +829,9 @@ var ConnectionHub = class {
824
829
  if (envelope.type === "agent.egress.reliable") {
825
830
  return this.handleAgentEgressReliable(deviceId, envelope.payload);
826
831
  }
832
+ if (envelope.type === "agent.message.publish") {
833
+ return this.handleAgentMessagePublish(deviceId, envelope.task_id, envelope.payload);
834
+ }
827
835
  if (envelope.type === "agent.content.receipt") {
828
836
  return this.handleAgentContentReceipt(deviceId, envelope.payload);
829
837
  }
@@ -868,6 +876,40 @@ var ConnectionHub = class {
868
876
  this.sendAgentEgressAck(deviceId, receipt);
869
877
  return "accepted";
870
878
  }
879
+ handleAgentMessagePublish(deviceId, taskId, payload) {
880
+ if (!this.hasDeviceCapabilities(deviceId, [AGENT_MESSAGE_EGRESS_CAPABILITY])) return "rejected";
881
+ const binding = this.agentMessageRequirements.get(taskId);
882
+ const record = this.taskStore.get(taskId);
883
+ if (binding === void 0 || record === void 0 || record.deviceId !== deviceId || binding.deviceId !== deviceId || !sameAgentRef(binding.agentRef, payload.agentRef) || binding.sessionRef !== void 0 && binding.sessionRef !== payload.sessionRef || binding.requirement.contract !== payload.contract || binding.requirement.contentType !== payload.contentType || payload.byteCount > binding.requirement.maxBytes) return "rejected";
884
+ const taskPayload = this.agentMessageTaskPayloads.get(taskId);
885
+ if (taskPayload !== void 0 && JSON.stringify(taskPayload) !== JSON.stringify(payload)) return "rejected";
886
+ if (taskPayload === void 0) this.agentMessageTaskPayloads.set(taskId, payload);
887
+ const key = `${deviceId}\0${payload.messageId}`;
888
+ const previous = this.agentMessageReceipts.get(key);
889
+ if (previous !== void 0) {
890
+ if (JSON.stringify(previous.payload) !== JSON.stringify(payload)) return "rejected";
891
+ this.sendAgentMessageDisposition(deviceId, taskId, previous.disposition);
892
+ return "duplicate";
893
+ }
894
+ const decision = this.agentMessage?.consume({ deviceId, taskId, context: binding.context, payload }) ?? { outcome: "held", reasonCode: "consumer_unavailable" };
895
+ const disposition = {
896
+ agentRef: payload.agentRef,
897
+ sessionRef: payload.sessionRef,
898
+ contract: payload.contract,
899
+ messageId: payload.messageId,
900
+ cursor: payload.cursor,
901
+ contentHash: payload.contentHash,
902
+ outcome: decision.outcome,
903
+ receiptId: crypto.randomUUID(),
904
+ ...decision.reasonCode === void 0 ? {} : { reasonCode: decision.reasonCode }
905
+ };
906
+ this.agentMessageReceipts.set(key, { payload, disposition });
907
+ this.sendAgentMessageDisposition(deviceId, taskId, disposition);
908
+ return "accepted";
909
+ }
910
+ sendAgentMessageDisposition(deviceId, taskId, payload) {
911
+ this.sendToDevice(deviceId, "agent.message.disposition", payload, { taskId });
912
+ }
871
913
  handleAgentContentReceipt(deviceId, payload) {
872
914
  const capability = contentReadCapability(payload.surface);
873
915
  if (!this.hasDeviceCapabilities(deviceId, [capability, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) return "rejected";
@@ -1627,6 +1669,12 @@ var ConnectionHub = class {
1627
1669
  const sessionRef = "sessionRef" in input ? input.sessionRef : void 0;
1628
1670
  const agentRef = input.agentRef === void 0 ? void 0 : AgentRefSchema.parse(input.agentRef);
1629
1671
  const egressPolicy = input.egressPolicy === void 0 ? void 0 : AgentEgressPolicySchema.parse(input.egressPolicy);
1672
+ const messageEgress = input.messageEgress === void 0 ? void 0 : AgentMessageEgressRequirementSchema.parse(input.messageEgress);
1673
+ const terminalProjection = input.terminalProjection === void 0 ? void 0 : TerminalProjectionSelectionSchema.parse(input.terminalProjection);
1674
+ if (messageEgress === void 0 && input.agentMessageContext !== void 0) {
1675
+ throw new Error("agentMessageContext requires messageEgress");
1676
+ }
1677
+ const agentMessageContext = messageEgress === void 0 ? void 0 : AgentMessageServerContextSchema.parse(input.agentMessageContext);
1630
1678
  const dispatchSelection = input.dispatchSelection === void 0 ? void 0 : DispatchSelectionSchema.parse(input.dispatchSelection);
1631
1679
  const requiredToolsets = input.requiredToolsets === void 0 ? void 0 : RequiredToolsetsSchema.parse(input.requiredToolsets);
1632
1680
  const deviceId = input.deviceId ?? this.pickFirstConnectedDevice(requiredToolsets, agentRef !== void 0);
@@ -1636,6 +1684,9 @@ var ConnectionHub = class {
1636
1684
  if (egressPolicy !== void 0 && agentRef === void 0) {
1637
1685
  throw new Error("Agent egress policy requires an explicit AgentRef; legacy task dispatch cannot consume it");
1638
1686
  }
1687
+ if (messageEgress !== void 0 && egressPolicy === void 0) {
1688
+ throw new Error("Agent message egress requires the typed Agent egress offer path");
1689
+ }
1639
1690
  if (!deviceId || !this.connections.get(deviceId)?.connected) {
1640
1691
  throw new Error(
1641
1692
  deviceId ? `device ${deviceId} is not connected` : "no connected device to dispatch to (M0 does not queue tasks until a device connects)"
@@ -1661,6 +1712,12 @@ var ConnectionHub = class {
1661
1712
  freshAgentEgress ? `device ${deviceId} did not advertise Agent egress policy, reliable acknowledgement, and fresh-session capabilities; refusing before enqueue` : `device ${deviceId} did not advertise Agent egress policy and reliable acknowledgement capabilities; refusing before enqueue`
1662
1713
  );
1663
1714
  }
1715
+ if (messageEgress !== void 0 && !this.hasDeviceCapabilities(deviceId, [AGENT_MESSAGE_EGRESS_CAPABILITY])) {
1716
+ throw new Error(`device ${deviceId} did not advertise Agent message egress capability; refusing before enqueue`);
1717
+ }
1718
+ if (terminalProjection !== void 0 && !this.hasDeviceCapabilities(deviceId, [TERMINAL_PROJECTION_SELECTION_CAPABILITY])) {
1719
+ throw new Error(`device ${deviceId} did not advertise terminal projection selection capability; refusing before enqueue`);
1720
+ }
1664
1721
  if (dispatchSelection !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("dispatch-selection") ?? false)) {
1665
1722
  throw new Error(
1666
1723
  `device ${deviceId} did not advertise dispatch-selection capability; refusing authoritative provider/model dispatch`
@@ -1710,6 +1767,9 @@ var ConnectionHub = class {
1710
1767
  resolveResult = resolve;
1711
1768
  });
1712
1769
  this.runtimes.set(taskId, { queue, resolveResult, result });
1770
+ if (messageEgress !== void 0 && agentRef !== void 0) {
1771
+ this.agentMessageRequirements.set(taskId, { deviceId, agentRef, sessionRef, requirement: messageEgress, context: agentMessageContext });
1772
+ }
1713
1773
  queue.push({ kind: "state", state: record.state, at: record.createdAt });
1714
1774
  this.serverEvents.push({ kind: "task.created", taskId, at: record.createdAt });
1715
1775
  const commonOffer = {
@@ -1727,6 +1787,8 @@ var ConnectionHub = class {
1727
1787
  dispatchSelection,
1728
1788
  agentRef,
1729
1789
  egressPolicy,
1790
+ terminalProjection,
1791
+ ...messageEgress === void 0 ? {} : { messageEgress },
1730
1792
  ...requiredToolsets === void 0 ? {} : { requiredToolsets }
1731
1793
  };
1732
1794
  this.sendToDevice(
@@ -1744,6 +1806,8 @@ var ConnectionHub = class {
1744
1806
  sessionRef,
1745
1807
  agentRef,
1746
1808
  egressPolicy,
1809
+ terminalProjection,
1810
+ ...messageEgress === void 0 ? {} : { messageEgress },
1747
1811
  ...requiredToolsets === void 0 ? {} : { requiredToolsets }
1748
1812
  },
1749
1813
  { taskId, sessionRef }
@@ -1752,7 +1816,12 @@ var ConnectionHub = class {
1752
1816
  this.sendToDevice(
1753
1817
  deviceId,
1754
1818
  "task.offer_for_agent",
1755
- { ...commonOffer, agentRef, ...requiredToolsets === void 0 ? {} : { requiredToolsets } },
1819
+ {
1820
+ ...commonOffer,
1821
+ agentRef,
1822
+ ...terminalProjection === void 0 ? {} : { terminalProjection },
1823
+ ...requiredToolsets === void 0 ? {} : { requiredToolsets }
1824
+ },
1756
1825
  { taskId, sessionRef }
1757
1826
  );
1758
1827
  } else if (requiredToolsets === void 0) {
@@ -2045,7 +2114,7 @@ var ConnectionHub = class {
2045
2114
  const envelope = createEnvelope(type, payload, combinedOpts);
2046
2115
  const taskId = opts.taskId;
2047
2116
  const redeliverThroughTerminal = type === "task.cancel" || type === "task.reject";
2048
- const redeliverWithoutTask = type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
2117
+ const redeliverWithoutTask = type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection" || type === "agent.message.disposition";
2049
2118
  outbox.ring.push({ seq, taskId, envelope, redeliverThroughTerminal, redeliverWithoutTask });
2050
2119
  if (outbox.ring.length > OUTBOX_RING_CAPACITY) outbox.ring.shift();
2051
2120
  this.deliverToDevice(deviceId, envelope);
@@ -3151,7 +3220,7 @@ function createByokServer(opts) {
3151
3220
  const taskLeaseMs = opts.taskLeaseMs ?? DEFAULT_TASK_LEASE_MS;
3152
3221
  const rateLimiter = new RateLimiter(opts.rateLimit);
3153
3222
  const taskStore = opts.taskStore ?? new InMemoryTaskStore();
3154
- const hub = new ConnectionHub(taskStore, devices, taskLeaseMs, rateLimiter);
3223
+ const hub = new ConnectionHub(taskStore, devices, taskLeaseMs, rateLimiter, opts.agentMessage);
3155
3224
  const hono = buildHonoApp({
3156
3225
  pairing,
3157
3226
  devices,