@byok-sdk/server 0.8.0 → 0.9.0-rc.1

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, 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,20 +1669,34 @@ 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
- const deviceId = input.deviceId ?? this.pickFirstConnectedDevice(requiredToolsets);
1680
+ const deviceId = input.deviceId ?? this.pickFirstConnectedDevice(requiredToolsets, agentRef !== void 0);
1633
1681
  if (agentRef !== void 0 && input.deviceId === void 0) {
1634
1682
  throw new Error("Agent-bound dispatch requires an explicit deviceId for capability admission");
1635
1683
  }
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)"
1642
1693
  );
1643
1694
  }
1695
+ if (agentRef === void 0 && this.getDeviceCapabilities(deviceId)?.includes(STRICT_AGENT_ONLY_CAPABILITY) === true) {
1696
+ throw new Error(
1697
+ `device ${deviceId} advertises strict-agent-only; legacy task dispatch is refused before enqueue`
1698
+ );
1699
+ }
1644
1700
  if (agentRef !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("agent-home-contract") ?? false)) {
1645
1701
  throw new Error(
1646
1702
  `device ${deviceId} did not advertise agent-home-contract capability; refusing Agent-bound dispatch`
@@ -1656,6 +1712,12 @@ var ConnectionHub = class {
1656
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`
1657
1713
  );
1658
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
+ }
1659
1721
  if (dispatchSelection !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("dispatch-selection") ?? false)) {
1660
1722
  throw new Error(
1661
1723
  `device ${deviceId} did not advertise dispatch-selection capability; refusing authoritative provider/model dispatch`
@@ -1705,6 +1767,9 @@ var ConnectionHub = class {
1705
1767
  resolveResult = resolve;
1706
1768
  });
1707
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
+ }
1708
1773
  queue.push({ kind: "state", state: record.state, at: record.createdAt });
1709
1774
  this.serverEvents.push({ kind: "task.created", taskId, at: record.createdAt });
1710
1775
  const commonOffer = {
@@ -1722,6 +1787,8 @@ var ConnectionHub = class {
1722
1787
  dispatchSelection,
1723
1788
  agentRef,
1724
1789
  egressPolicy,
1790
+ terminalProjection,
1791
+ ...messageEgress === void 0 ? {} : { messageEgress },
1725
1792
  ...requiredToolsets === void 0 ? {} : { requiredToolsets }
1726
1793
  };
1727
1794
  this.sendToDevice(
@@ -1739,6 +1806,8 @@ var ConnectionHub = class {
1739
1806
  sessionRef,
1740
1807
  agentRef,
1741
1808
  egressPolicy,
1809
+ terminalProjection,
1810
+ ...messageEgress === void 0 ? {} : { messageEgress },
1742
1811
  ...requiredToolsets === void 0 ? {} : { requiredToolsets }
1743
1812
  },
1744
1813
  { taskId, sessionRef }
@@ -1747,7 +1816,12 @@ var ConnectionHub = class {
1747
1816
  this.sendToDevice(
1748
1817
  deviceId,
1749
1818
  "task.offer_for_agent",
1750
- { ...commonOffer, agentRef, ...requiredToolsets === void 0 ? {} : { requiredToolsets } },
1819
+ {
1820
+ ...commonOffer,
1821
+ agentRef,
1822
+ ...terminalProjection === void 0 ? {} : { terminalProjection },
1823
+ ...requiredToolsets === void 0 ? {} : { requiredToolsets }
1824
+ },
1751
1825
  { taskId, sessionRef }
1752
1826
  );
1753
1827
  } else if (requiredToolsets === void 0) {
@@ -2005,9 +2079,10 @@ var ConnectionHub = class {
2005
2079
  }
2006
2080
  this.sendToDevice(record.deviceId, "task.steer", { text }, { taskId });
2007
2081
  }
2008
- pickFirstConnectedDevice(requiredToolsets) {
2082
+ pickFirstConnectedDevice(requiredToolsets, allowStrictAgentOnly = false) {
2009
2083
  for (const [deviceId, conn] of this.connections) {
2010
2084
  if (!conn.connected) continue;
2085
+ if (!allowStrictAgentOnly && conn.capabilities?.includes(STRICT_AGENT_ONLY_CAPABILITY)) continue;
2011
2086
  if (requiredToolsets === void 0) return deviceId;
2012
2087
  if (!conn.capabilities?.includes("toolset-selection")) continue;
2013
2088
  if (conn.configuredToolsets === void 0) continue;
@@ -2039,7 +2114,7 @@ var ConnectionHub = class {
2039
2114
  const envelope = createEnvelope(type, payload, combinedOpts);
2040
2115
  const taskId = opts.taskId;
2041
2116
  const redeliverThroughTerminal = type === "task.cancel" || type === "task.reject";
2042
- 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";
2043
2118
  outbox.ring.push({ seq, taskId, envelope, redeliverThroughTerminal, redeliverWithoutTask });
2044
2119
  if (outbox.ring.length > OUTBOX_RING_CAPACITY) outbox.ring.shift();
2045
2120
  this.deliverToDevice(deviceId, envelope);
@@ -3145,7 +3220,7 @@ function createByokServer(opts) {
3145
3220
  const taskLeaseMs = opts.taskLeaseMs ?? DEFAULT_TASK_LEASE_MS;
3146
3221
  const rateLimiter = new RateLimiter(opts.rateLimit);
3147
3222
  const taskStore = opts.taskStore ?? new InMemoryTaskStore();
3148
- const hub = new ConnectionHub(taskStore, devices, taskLeaseMs, rateLimiter);
3223
+ const hub = new ConnectionHub(taskStore, devices, taskLeaseMs, rateLimiter, opts.agentMessage);
3149
3224
  const hono = buildHonoApp({
3150
3225
  pairing,
3151
3226
  devices,