@byok-sdk/server 0.5.0 → 0.6.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
@@ -3,7 +3,7 @@ import { type Envelope, type RuntimeId, type RuntimeInfo, type TaskState, type T
3
3
  import type { DeviceRegistry } from './auth';
4
4
  import { RateLimiter } from './rate-limiter';
5
5
  import type { TaskStore } from './task-store';
6
- import type { ByokServerEvent, DispatchInput, HubStats, MachineInfo, TaskHandle, TaskSnapshot } from './types';
6
+ import type { ByokServerEvent, AgentContentReadRequest, AgentEgressReceipt, DispatchInput, HubStats, MachineInfo, TaskHandle, TaskSnapshot } from './types';
7
7
  /**
8
8
  * The connection hub: tracks each device's live transport (WS or long-poll —
9
9
  * never both at once, see {@link takeOverAsLongPoll}), routes `dispatch()`'d
@@ -18,7 +18,8 @@ import type { ByokServerEvent, DispatchInput, HubStats, MachineInfo, TaskHandle,
18
18
  * transports (`ws-server.ts`'s WS message handler, `http.ts`'s
19
19
  * `POST /byok/messages`) call instead of reaching into per-type handlers
20
20
  * directly. Runs, in order: (1) type-allow — only `DAEMON_TO_SERVER_TYPES`
21
- * may pass, a server -> daemon type arriving inbound is rejected (P2); (2)
21
+ * plus the authenticated long-poll `conn.hello` snapshot may pass, a server
22
+ * -> daemon type arriving inbound is rejected (P2); (2)
22
23
  * ownership (N2) — an envelope for a task already owned by a *different*
23
24
  * device is dropped and logged, never force-failed (force-failing on an
24
25
  * authz mismatch would let an attacker who merely guesses a `taskId` kill
@@ -169,15 +170,11 @@ export type SteerRejectionCode = 'steer_unsupported_runtime' | 'task_not_running
169
170
  * Two reasons, either sufficient. First, scope: connection-level data is
170
171
  * discovery describing a daemon BUILD, not the per-runtime, per-task,
171
172
  * claim-time truth this gate needs — conflating the two is the original bug.
172
- * Second, reach: `conn.hello` is transport-shaped. A long-poll-only daemon
173
- * never sends one (sole sender: `ws-transport.ts:192`, `packages/client`), so
174
- * a connection-sourced snapshot is permanently `undefined` for an entire
175
- * transport and a fail-closed gate reading it disables steer across that whole
176
- * deployment surface a regression, not a safety property. The claim, by
177
- * contrast, is the message that establishes the task↔runtime binding on every
178
- * transport, so the gate's input now shares a lifecycle with the thing it
179
- * judges. Adding a connection-level fallback here would restore both defects
180
- * at once and is what this design exists to forbid.
173
+ * Second, lifecycle: the authenticated `conn.hello` snapshot on either
174
+ * transport describes the device build, while the claim establishes the
175
+ * exact task↔runtime binding. Only the latter shares a lifecycle with the
176
+ * thing this gate judges. Adding a connection-level fallback here would
177
+ * restore the scope defect and is what this design exists to forbid.
181
178
  */
182
179
  export declare class SteerRejectedError extends Error {
183
180
  readonly taskId: string;
@@ -214,6 +211,12 @@ export declare class ConnectionHub {
214
211
  private readonly longPollWaiters;
215
212
  private readonly runtimes;
216
213
  private readonly serverEvents;
214
+ /** First-write-wins reliable facts; the reference composition's bounded in-memory readback. */
215
+ private readonly agentEgressReceipts;
216
+ /** Accepted requests are the authority that later receipts/transfers must echo exactly. */
217
+ private readonly agentContentReadRequests;
218
+ /** Content-free explicit-read audit facts keyed by exact authenticated device/request identity. */
219
+ private readonly agentContentReceipts;
217
220
  /**
218
221
  * Per-task last-inbound-activity timestamp (epoch ms) — the task-lease
219
222
  * reaper's condition (c), see the "task-lease reaper" section below. Reset
@@ -276,7 +279,7 @@ export declare class ConnectionHub {
276
279
  * connection this hub never learns capabilities for simply reads back
277
280
  * `undefined` from {@link getDeviceCapabilities}.
278
281
  */
279
- registerConnection(deviceId: string, ws: WebSocket, runtimes: RuntimeInfo[] | undefined, capabilities?: readonly string[], configuredToolsets?: readonly ToolsetId[]): void;
282
+ registerConnection(deviceId: string, ws: WebSocket, runtimes: RuntimeInfo[] | undefined, capabilities?: readonly string[], configuredToolsets?: readonly ToolsetId[], clientVersion?: string): void;
280
283
  sendConnAck(deviceId: string, capabilities: string[]): void;
281
284
  /**
282
285
  * Reconnection procedure step 3 (§9): redeliver, in `seq` order, every
@@ -338,9 +341,10 @@ export declare class ConnectionHub {
338
341
  * taskStore lookup or dedup bookkeeping. See {@link handleRateLimited}
339
342
  * for what happens on exceed (never a silent drop).
340
343
  * 1. **type-allow (P2)** — only {@link DAEMON_TO_SERVER_TYPES} may pass; a
341
- * server -> daemon type (or anything unrecognized, e.g. a stale/future
342
- * `conn.hello` outside the handshake) arriving inbound is rejected
343
- * before it's dispatched or counted accepted.
344
+ * server -> daemon type arriving inbound is rejected before it's
345
+ * dispatched or counted accepted. `conn.hello` is the one non-task
346
+ * exception, and is accepted only from the bearer-authenticated
347
+ * long-poll route with an exact device/product/protocol match.
344
348
  * 2. **ownership (N2)** — an envelope for a task already owned by a
345
349
  * *different* device is dropped (logged), never force-failed:
346
350
  * force-failing on an authz mismatch would let an attacker who merely
@@ -360,7 +364,19 @@ export declare class ConnectionHub {
360
364
  * wire-level success even though no handler ran a second time; only
361
365
  * `rejected`/`rate_limited` (gate steps 0-2) are excluded from that count.
362
366
  */
363
- handleInbound(deviceId: string, envelope: Envelope): 'accepted' | 'duplicate' | 'rejected' | 'rate_limited';
367
+ handleInbound(deviceId: string, envelope: Envelope, authenticatedProductId?: string): 'accepted' | 'duplicate' | 'rejected' | 'rate_limited';
368
+ /**
369
+ * Store before acking. Replays must agree on every identity/cursor/hash
370
+ * field and receive the original receipt id; a same event id with changed
371
+ * facts is rejected rather than treated as an update.
372
+ */
373
+ private handleAgentEgressReliable;
374
+ private handleAgentContentReceipt;
375
+ private sendAgentEgressAck;
376
+ private sendAgentContentReceiptAck;
377
+ private agentEgressReceiptKey;
378
+ /** Record the authenticated long-poll equivalent of the WS opening frame. */
379
+ private registerLongPollHello;
364
380
  /**
365
381
  * M4 Phase 4 (part A): `deviceId` just exceeded its inbound-envelope rate
366
382
  * limit. Never a silent drop: counts the occurrence
@@ -733,6 +749,8 @@ export declare class ConnectionHub {
733
749
  /** Reap one lease-expired task through the exact same TaskStore/canTransition path — and terminal-event emission — as any other `task.fail` (see {@link applyOrFail}). */
734
750
  private reapTask;
735
751
  dispatch(input: DispatchInput): Promise<TaskHandle>;
752
+ /** Capability-gated control-plane read request; no request enters the outbox on omission. */
753
+ requestAgentContentRead(input: AgentContentReadRequest): Promise<void>;
736
754
  private buildTaskHandle;
737
755
  /** Idempotent: cancelling an already-terminal task is a no-op, not an error. */
738
756
  private cancelTask;
@@ -800,8 +818,7 @@ export declare class ConnectionHub {
800
818
  * not {@link getDeviceCapabilities}, not `ConnectionState.runtimes`, and
801
819
  * with no fallback to either when the snapshot is absent. See
802
820
  * {@link SteerRejectedError} for why a connection-sourced input is wrong
803
- * both in scope (describes a daemon build, not this task's runtime) and in
804
- * reach (absent entirely on long-poll-only daemons).
821
+ * in scope (it describes a daemon build, not this task's runtime).
805
822
  */
806
823
  private steerTask;
807
824
  private pickFirstConnectedDevice;
@@ -848,6 +865,8 @@ export declare class ConnectionHub {
848
865
  * only, never a correctness gate).
849
866
  */
850
867
  getDeviceCapabilities(deviceId: string): readonly string[] | undefined;
868
+ private hasDeviceCapabilities;
869
+ getAgentEgressReceipt(deviceId: string, eventId: string): AgentEgressReceipt | undefined;
851
870
  getTask(taskId: string): TaskSnapshot | undefined;
852
871
  listTasks(): TaskSnapshot[];
853
872
  /**
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ import type { Server as HttpServer } from 'node:http';
2
2
  import type { Hono } from 'hono';
3
3
  import { type TenantId } from './auth';
4
4
  import { type PairingCodeClaims, type PairingCodeInfo } from './pairing';
5
- import type { ByokServerEvent, CreateByokServerOptions, DispatchInput, HubStats, MachineInfo, TaskHandle, TaskSnapshot } from './types';
6
- export type { ByokServerEvent, CreateByokServerOptions, DispatchInput, HubStats, MachineInfo, ServerTaskEvent, TaskHandle, TaskResult, TaskSnapshot, } from './types';
5
+ import type { ByokServerEvent, AgentContentReadRequest, AgentEgressReceipt, CreateByokServerOptions, DispatchInput, HubStats, MachineInfo, TaskHandle, TaskSnapshot } from './types';
6
+ export type { ByokServerEvent, AgentContentReadRequest, AgentEgressReceipt, CreateByokServerOptions, DispatchInput, HubStats, MachineInfo, ServerTaskEvent, TaskHandle, TaskResult, TaskSnapshot, } from './types';
7
7
  export type { CreateTaskInput, TaskRecord, TaskStore } from './task-store';
8
8
  export { IllegalTaskTransitionError, InMemoryTaskStore } from './task-store';
9
9
  /**
@@ -60,10 +60,16 @@ export interface ByokServer {
60
60
  createPairingCode(claims: PairingCodeClaims): PairingCodeInfo;
61
61
  };
62
62
  dispatch(input: DispatchInput): Promise<TaskHandle>;
63
+ /** Enqueue one capability-gated, exact-identity content-read request. */
64
+ requestAgentContentRead(input: AgentContentReadRequest): Promise<void>;
63
65
  tasks: {
64
66
  get(taskId: string): TaskSnapshot | undefined;
65
67
  list(): TaskSnapshot[];
66
68
  };
69
+ /** Reference-server reliable egress receipt readback. */
70
+ egress: {
71
+ get(deviceId: string, eventId: string): AgentEgressReceipt | undefined;
72
+ };
67
73
  machines: {
68
74
  list(): MachineInfo[];
69
75
  };