@byok-sdk/server 0.11.0 → 0.13.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/types.d.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  import type { AgentContentReadPayload, AgentHomeProjectionPayload, AgentHomeProjectionReadback, AgentEventOrUnknown, AgentEgressPolicy, AgentEgressReliablePayload, AgentMessageEgressRequirement, AgentMessageServerContext, AgentMessagePublishPayload, AgentRef, BlobRef, DispatchSelection, PermissionPolicy, RuntimeCapabilities, RuntimeId, RuntimeInfo, TaskApprovalResolvedPayload, TaskArtifactPayload, TaskState, ToolsetId, TerminalProjectionSelection } from '@byok-sdk/protocol';
2
- import type { BlobStore } from './blob-store';
2
+ import type { TenantId, TokenSigner } from '@byok-sdk/cloud';
3
3
  import type { RateLimiterOptions } from './rate-limiter';
4
- import type { TaskStore } from './task-store';
5
- import type { TokenSigner } from './auth';
4
+ /** Mutually-exclusive storage authority for the embedded reference server. */
5
+ export type ByokServerStorage = {
6
+ readonly kind: 'memory';
7
+ } | {
8
+ readonly kind: 'sqlite';
9
+ /** File-backed SQLite database. `:memory:` is accepted for tests only. */
10
+ readonly path: string;
11
+ /** Lifetime of signed blob upload/download URLs. Default 15 minutes. */
12
+ readonly urlTtlMs?: number;
13
+ };
6
14
  /** Options for {@link createByokServer}. */
7
15
  export interface CreateByokServerOptions {
8
16
  /**
@@ -12,85 +20,92 @@ export interface CreateByokServerOptions {
12
20
  * mismatched daemon is rejected at handshake time.
13
21
  */
14
22
  productId: string;
15
- /** WS-native ping interval, ms (§ heartbeat). Default 30s. */
16
- heartbeatIntervalMs?: number;
23
+ /**
24
+ * Embedded storage authority. Defaults to process-local memory. SQLite is
25
+ * selected explicitly and fails closed if it cannot be opened; it never
26
+ * falls back to memory.
27
+ */
28
+ storage?: ByokServerStorage;
17
29
  /** How long `GET /byok/events` holds an empty poll open before returning, ms (§8). Default ~50s; override for tests. */
18
30
  longPollHoldMs?: number;
19
31
  /** Per-product blob size ceiling in bytes (§7). Default 100MB. */
20
32
  maxBlobSizeBytes?: number;
21
- /** Override the reference {@link BlobStore} (e.g. a real object-store-backed implementation, or `sqlite-blob-store.ts`'s `SqliteBlobStore` for a persistent single-node deployment). */
22
- blobStore?: BlobStore;
23
- /** Override the reference {@link TaskStore} (e.g. `sqlite-task-store.ts`'s `SqliteTaskStore` for a persistent single-node deployment). Defaults to an in-memory store that loses all task state on restart. */
24
- taskStore?: TaskStore;
25
33
  /** Override the reference {@link TokenSigner} (e.g. an org-wide/KMS-backed signer). */
26
34
  tokenSigner?: TokenSigner;
27
35
  /**
28
- * How long a `Claimed`/`Running`/`AwaitApproval` task may sit with no
29
- * inbound `task.*` activity from its owning device while that device is
30
- * dark (disconnected, or long-poll-silent) before the server reaps it to
31
- * `Failed(retryable: true, reason: 'lease-expired')` — no new task state,
32
- * no new wire message; the embedder is expected to re-dispatch as a
33
- * brand-new task, same as any other retryable failure. Deliberately
34
- * generous — it exists purely as a backstop for a device that never
35
- * reconnects at all (M1's redelivery, docs/protocol.md §9, already covers
36
- * "came back within the window"), so it must stay far larger than any
37
- * realistic task duration or it will race and fail perfectly healthy
38
- * long-running tasks. A task on a *connected*, actively-progressing
39
- * device is never touched regardless of this value — see
40
- * `ConnectionHub`'s lease-reaper doc comment (`hub.ts`) for the full
41
- * design and its accepted residual risk. Default 30 minutes.
36
+ * How many {@link ServerTaskEvent}s one task's `TaskHandle.events()` buffer
37
+ * retains before the OLDEST are dropped and a single
38
+ * `{ kind: 'error', reason: 'events_truncated' }` marker is appended. The
39
+ * feed is a notification relay, not a second record of what happened — the
40
+ * durable facts stay in the cloud stores (`tasks.get`, `result()`) — so a
41
+ * consumer that stops reading costs bounded memory rather than unbounded
42
+ * growth. Default 1000.
43
+ */
44
+ taskEventBufferLimit?: number;
45
+ /**
46
+ * How long after a task reaches a terminal its relay buffer and terminal
47
+ * promise are RETAINED before being reclaimed, ms. A late `events()` reader
48
+ * within this window still replays the whole feed; past it, the durable read
49
+ * model (`tasks.get`) is the only answer. Default 5 minutes.
50
+ *
51
+ * Retention never decides when a feed ENDS: {@link TaskHandle.events}
52
+ * completes at the terminal event itself, whenever that happens, so a
53
+ * `for await` over it is never left waiting on this timer.
42
54
  */
43
- taskLeaseMs?: number;
55
+ taskEventRetentionMs?: number;
44
56
  /**
45
- * M4 Phase 4 (part A): per-device inbound-envelope token bucket, enforced
46
- * by `ConnectionHub.handleInbound` (`hub.ts`) — the single choke point
47
- * both WS (`ws-server.ts`) and long-poll (`POST /byok/messages`, `http.ts`)
48
- * inbound traffic passes through. Defaults: 50 msg/s sustained, burst 100
49
- * (see `rate-limiter.ts`'s own defaults). Exceeding it never drops
50
- * silently: it counts in `ConnectionHub.stats()`'s `rateLimitEvents`, and
51
- * emits a `device.rate_limited` {@link ByokServerEvent} — see that
52
- * variant's own doc comment for the per-transport enforcement shape (WS
53
- * close vs. long-poll 429). Blob upload/download routes (`http.ts`) are
54
- * deliberately NOT covered by this same bucket — see that file's own
55
- * comment on why a shared limiter didn't drop in cleanly there.
57
+ * M4 Phase 4 (part A): per-device inbound-envelope token bucket, enforced at
58
+ * step 0 of the cloud kernel's inbound gate (`@byok-sdk/cloud`'s
59
+ * `inbound.ts`) — the single choke point every daemon -> server envelope
60
+ * passes through, debited BEFORE the type-allow check so a flood of
61
+ * garbage-typed envelopes costs the same budget as a flood of well-formed
62
+ * ones. Defaults: 50 msg/s sustained, burst 100 (see `rate-limiter.ts`).
56
63
  *
57
- * Honest caveat (no code change changes this — it's an inherent property
58
- * of an abrupt WS close, not something rate limiting adds): a
59
- * flood-triggered 1008 close is not special. Envelopes the daemon's own
60
- * WS transport already handed off to its socket write between the moment
61
- * the device exceeded budget and the close actually landing share the
62
- * ordinary at-most-once exposure of ANY abrupt WS disconnect (network
63
- * blip, server restart, etc.) — the wire's at-least-once guarantee
64
- * (docs/protocol.md §9) is specified for the server->daemon direction
65
- * only; daemon->server has no redelivery cursor to begin with, so this
66
- * was already true before rate limiting existed. A flood just makes that
67
- * pre-existing window more likely to have something in flight at the
68
- * exact moment of a close.
64
+ * Exceeding it never drops silently: the occurrence counts in
65
+ * {@link HubStats.rateLimitEvents} (per REFUSED envelope), and the first
66
+ * refusal of an episode emits a `device.rate_limited`
67
+ * {@link ByokServerEvent} — coalesced per episode, re-armed only by a later
68
+ * successful consume by the same device. Enforcement on the wire is a
69
+ * whole-request `429` from `POST /byok/messages`; `GET /byok/events` is not
70
+ * on this bucket, and neither are the blob upload/download routes.
69
71
  */
70
72
  rateLimit?: RateLimiterOptions;
71
73
  /**
72
- * M4 Phase 4 (part B.2): opt-in `GET /healthz` liveness route on the Hono
73
- * app (`http.ts`) — deliberately unauthenticated (no bearer check) and
74
- * carrying no sensitive data (no device ids, no counts), just
75
- * `{ok:true, uptimeMs}`; see `http.ts`'s own comment on that route for the
76
- * full auth-posture rationale. Default `false` (no route mounted at all).
77
- * `ConnectionHub.stats()` (richer, in-process-only detail) is never
74
+ * M4 Phase 4 (part B.2): opt-in `GET /healthz` liveness route layered on the
75
+ * Hono app in front of the kernel — deliberately unauthenticated (no bearer
76
+ * check) and carrying no sensitive data (no device ids, no counts), just
77
+ * `{ok:true, uptimeMs}`, because a container orchestrator's liveness probe
78
+ * must not need a device credential. Server-local rather than a kernel route
79
+ * because it reports deployment liveness, not coordination. Default `false`
80
+ * (no route mounted at all). `ByokServer.stats()` (richer detail) is never
78
81
  * exposed over HTTP by this SDK regardless of this flag — an embedder that
79
82
  * wants that surfaced remotely builds its own authenticated route around
80
- * `stats()`.
83
+ * it.
81
84
  */
82
85
  healthzRoute?: boolean;
83
- /** Product-owned, authenticated task destination consumer. */
86
+ /**
87
+ * Product-owned, authenticated task destination consumer.
88
+ *
89
+ * The cloud kernel's admission shape verbatim (`ByokCloudOptions.agentMessage`,
90
+ * `@byok-sdk/cloud`): async, and carrying the tenant the message was
91
+ * authenticated under. This façade forwards the hook to the kernel unchanged
92
+ * rather than wrapping a second shape around it — one contract, documented in
93
+ * one place. Delivery is at least once: the product must durably deduplicate
94
+ * exact message identity and return its original decision on replay, including
95
+ * concurrent calls and retries after an uncertain commit.
96
+ * A one-shot break for embedders (WP3B §6); there is no adapter.
97
+ */
84
98
  agentMessage?: {
85
99
  consume(input: {
100
+ readonly tenant: TenantId;
86
101
  readonly deviceId: string;
87
102
  readonly taskId: string;
88
103
  readonly context: AgentMessageServerContext;
89
104
  readonly payload: AgentMessagePublishPayload;
90
- }): {
105
+ }): Promise<{
91
106
  readonly outcome: 'accepted' | 'held' | 'refused';
92
107
  readonly reasonCode?: string;
93
- };
108
+ }>;
94
109
  };
95
110
  }
96
111
  /** Input to {@link ByokServer.dispatch}. */
@@ -174,10 +189,10 @@ export interface TaskResult {
174
189
  * no `resultDocument` extractor configured and a pre-`result-document`
175
190
  * daemon build that has no notion of the field at all.
176
191
  *
177
- * Persisted WITH `summary`/`artifactRefs` rather than beside them: the
178
- * whole `TaskResult` is stored as a single `result_json` document
179
- * (`sqlite-task-store.ts`), so this field reaches durable storage with
180
- * exact parity and introduces no second authority for it.
192
+ * Not stored by this package at all: the durable fact is the first terminal
193
+ * receipt the kernel recorded (`readTerminalReceipt`/`readTaskResult`,
194
+ * `@byok-sdk/cloud`), and every field here is projected off it on demand, so
195
+ * there is no second authority for it to drift from.
181
196
  */
182
197
  document?: unknown;
183
198
  }
@@ -223,10 +238,13 @@ export interface TaskHandle {
223
238
  * M5 (approval targeting, docs/protocol.md §5.3): `opts.approvalId`
224
239
  * targets a SPECIFIC pending approval rather than "whichever one is
225
240
  * currently pending" (the default when `opts` is omitted, unchanged from
226
- * pre-M5). Thin wrapper over `ConnectionHub.approveTask` (`hub.ts`) — see
227
- * that method's own doc comment for the full targeting/staleness
228
- * semantics, including when this throws `StaleApprovalError` (exported
229
- * from the package index for a caller to catch/inspect).
241
+ * pre-M5). Thin wrapper over `ByokCloud.approveTask` (`@byok-sdk/cloud`) —
242
+ * see that method's own doc comment for the full targeting/staleness
243
+ * semantics, including when this throws `StaleApprovalError` (re-exported
244
+ * from this package's index for a caller to catch/inspect). The host's
245
+ * decision is authoritative immediately: it is recorded on the task's
246
+ * durable approval timeline, so the task reads `Running` again without
247
+ * waiting for the runtime to report back.
230
248
  */
231
249
  approve(opts?: {
232
250
  approvalId?: string;
@@ -251,24 +269,28 @@ export interface MachineInfo {
251
269
  /** Logical toolset IDs reported by the current daemon; omission means legacy/unknown. */
252
270
  configuredToolsets?: ToolsetId[];
253
271
  }
254
- /** Snapshot of a task as tracked by the in-memory {@link TaskStore}. */
272
+ /**
273
+ * Projection of one task, read back from the cloud kernel's durable authority
274
+ * (`TaskAttempt` plus its terminal receipt and approval timeline,
275
+ * `@byok-sdk/cloud`) on every call — never a mirrored record this package
276
+ * maintains alongside it.
277
+ *
278
+ * Deliberately smaller than it used to be: `instruction`, `runtime`, `policy`
279
+ * and `requiredToolsets` were DISPATCH INPUT the host already holds and the
280
+ * kernel does not persist (ADR-028 — an attempt records ownership and
281
+ * disposition, not the request that produced it). A host that wants them back
282
+ * keeps its own map keyed by `taskId`; this snapshot never re-derives them.
283
+ */
255
284
  export interface TaskSnapshot {
256
285
  taskId: string;
257
- state: TaskState;
258
- instruction: string;
259
286
  /**
260
- * The REQUESTED runtime — `DispatchInput.runtime`, forwarded verbatim into
261
- * `task.offer.runtime`. Set once at `dispatch()` time and never touched
262
- * again afterward, regardless of what the daemon actually ends up running.
263
- * `undefined` means "no preference was expressed" (the daemon auto-selects,
264
- * pi-first) — NOT "the daemon ran no runtime". Contrast with
265
- * {@link claimedRuntime}, the ACTUAL runtime the daemon reports having
266
- * picked; see that field's own doc comment for the full requested-vs-
267
- * claimed distinction (docs/protocol.md §3.1).
287
+ * The wire {@link TaskState} this attempt projects to. Derived, in this
288
+ * order: an accepted host cancellation is `Cancelled` whatever the runtime
289
+ * later says; a terminal attempt is its own terminal; otherwise an
290
+ * unresolved approval on the task's timeline is `AwaitApproval`; otherwise
291
+ * the attempt's own coarse status.
268
292
  */
269
- runtime?: RuntimeId;
270
- policy: PermissionPolicy;
271
- requiredToolsets?: ToolsetId[];
293
+ state: TaskState;
272
294
  deviceId?: string;
273
295
  sessionRef?: string;
274
296
  /** Exact Agent identity for an Agent-bound task; absent for legacy tasks. */
@@ -278,22 +300,24 @@ export interface TaskSnapshot {
278
300
  result?: TaskResult;
279
301
  /**
280
302
  * M5 (approval targeting, docs/protocol.md §5.3): the daemon-reported
281
- * `approvalId` for the CURRENT `AwaitApproval` cycle, if this server has
282
- * learned one (`ConnectionHub.onAwaitApproval`, `hub.ts`) — `undefined`
283
- * whenever the task isn't currently awaiting approval, OR it is but no id
284
- * was ever reported for it (a legacy daemon). Cleared centrally the
285
- * instant the task LEAVES `AwaitApproval` (`ConnectionHub`'s
286
- * `transitionTask`), so a later `AwaitApproval` cycle for the same task
287
- * never inherits a stale id from a previous one. `approveTask`/
288
- * `rejectTask` compare an operator-supplied target id against this field
289
- * to decide whether a decision is stale (`StaleApprovalError`) — see
290
- * `hub.ts` for the full mechanism.
303
+ * `approvalId` for the CURRENT `AwaitApproval` cycle, if one is pending —
304
+ * `undefined` whenever the task isn't currently awaiting approval, OR it is
305
+ * but no id was ever reported for it (a legacy daemon).
306
+ *
307
+ * DERIVED, not stored: it is `pendingApproval()`'s fold over the task's
308
+ * durable approval timeline (`@byok-sdk/cloud`), the same single authority
309
+ * the kernel's `approveTask`/`rejectTask` staleness gate reads. A resolution
310
+ * — reported by the daemon, or recorded by this façade when the host
311
+ * resolves one — clears the slot there, so a later `AwaitApproval` cycle can
312
+ * never inherit a stale id and this projection can never disagree with the
313
+ * gate.
291
314
  */
292
315
  pendingApprovalId?: string;
293
316
  /**
294
317
  * M5 (claimed runtime, docs/protocol.md §3.1): the ACTUAL adapter the
295
318
  * daemon reports having selected for this task (`task.claim.runtime`,
296
- * `ConnectionHub.onClaim` — `hub.ts`) — covers both the explicit-runtime
319
+ * snapshotted by the kernel's inbound gate at the `offered -> claimed`
320
+ * ownership CAS) — covers both the explicit-runtime
297
321
  * path (echoes {@link runtime}) and the auto-select/pi-first path (a value
298
322
  * where {@link runtime} is `undefined`, since no preference was ever
299
323
  * requested). `undefined` until the first `task.claim` for this task
@@ -302,29 +326,30 @@ export interface TaskSnapshot {
302
326
  * `Offered -> Claimed` transition, and never modified again afterward — a
303
327
  * retried/idempotent claim from the same device is a no-op that never
304
328
  * reaches `onClaim`'s patch at all (see `onClaim`'s own doc comment), so
305
- * this can never be silently overwritten by a redelivered claim.
329
+ * this can never be silently overwritten by a redelivered claim. Read
330
+ * straight off `TaskAttempt.claimedRuntime` (`@byok-sdk/cloud`).
306
331
  */
307
332
  claimedRuntime?: RuntimeId;
308
333
  /**
309
334
  * S0/D-4 (runtime-honest control surface): the capability block the
310
335
  * CLAIMING adapter reported for itself on its own `task.claim`
311
336
  * (`TaskClaimPayload.capabilities`, `@byok-sdk/protocol`), snapshotted at the
312
- * exact moment of the `Offered -> Claimed` transition
313
- * (`ConnectionHub.onClaim`, `hub.ts`).
337
+ * exact moment of the `Offered -> Claimed` transition and read straight off
338
+ * `TaskAttempt.claimedRuntimeCapabilities` (`@byok-sdk/cloud`).
314
339
  *
315
340
  * Sourced from the claim and from nothing else. The connection-level
316
341
  * `conn.hello.runtimes[].capabilities` is discovery data — it describes a
317
342
  * device rather than the adapter that claimed this task — so it is never
318
- * read here or by the gate; see
319
- * `SteerRejectedError` (`hub.ts`) for the full argument.
343
+ * read here or by the gate; see `SteerRejectedError`
344
+ * (`@byok-sdk/cloud`'s `steer-control.ts`) for the full argument.
320
345
  *
321
346
  * A SNAPSHOT, deliberately — not a live read of anything: the same device
322
347
  * can reconnect later with a different adapter set (a runtime upgraded,
323
348
  * removed, or newly installed mid-task), and a task that is already running
324
349
  * must keep being judged against what was true when it was claimed.
325
- * `ConnectionHub.steerTask` is the consumer: it fails closed with a
326
- * `SteerRejectedError` (`hub.ts`) unless this snapshot says `steer === true`,
327
- * BEFORE any `task.steer` envelope exists.
350
+ * `ByokCloud.steerTask` is the consumer: it fails closed with a
351
+ * `SteerRejectedError` unless this snapshot says `steer === true`, BEFORE
352
+ * any `task.steer` envelope exists.
328
353
  *
329
354
  * `undefined` means "this server does not know" — never "supported" and
330
355
  * never "unsupported as a fact". It stays `undefined` when the claim carried
@@ -339,20 +364,20 @@ export interface TaskSnapshot {
339
364
  claimedRuntimeCapabilities?: RuntimeCapabilities;
340
365
  }
341
366
  /**
342
- * Cross-cutting server event feed (device connects/disconnects, task
343
- * creation/state changes) — the "event hub" from the plan's 服务端参考实现
344
- * section, as opposed to `TaskHandle.events()` which is scoped to one task.
345
- * Not part of the pinned wire contract; a server-embedder-facing convenience.
367
+ * Cross-cutting server event feed (task creation/state changes, approval
368
+ * resolutions, rate-limit episodes) — the "event hub" from the plan's
369
+ * 服务端参考实现 section, as opposed to `TaskHandle.events()` which is scoped
370
+ * to one task. Not part of the pinned wire contract; a server-embedder-facing
371
+ * convenience.
372
+ *
373
+ * `device.connected` / `device.disconnected` are deliberately GONE (WP3B §1.2
374
+ * option A). Both were edges of a live WebSocket registration; over the
375
+ * long-poll transport the only honest connection signals are a device's own
376
+ * `conn.hello` and its polling, and synthesising edges out of a TTL would
377
+ * publish transitions no device ever made. `machines.list()` reports the
378
+ * observation itself instead.
346
379
  */
347
380
  export type ByokServerEvent = {
348
- kind: 'device.connected';
349
- deviceId: string;
350
- at: string;
351
- } | {
352
- kind: 'device.disconnected';
353
- deviceId: string;
354
- at: string;
355
- } | {
356
381
  kind: 'task.created';
357
382
  taskId: string;
358
383
  at: string;
@@ -377,10 +402,10 @@ export type ByokServerEvent = {
377
402
  * pending approval entirely locally (M4 Phase 3's local `approvals.resolve`
378
403
  * control-socket path) — no wire `task.approve`/`task.reject` ever reached
379
404
  * the server for it. This fires when daemon-originated task traffic
380
- * (`task.progress`/`task.artifact`/`task.complete`) for a task the server's
381
- * own record still has as `AwaitApproval` proves, after the fact, that the
382
- * approval was resolved on the device — see `ConnectionHub`'s
383
- * `resumeIfImplicitlyApproved` (hub.ts) for the state-machine side of this.
405
+ * (`task.progress`/`task.artifact`) for a task whose approval timeline still
406
+ * shows an unresolved request proves, after the fact, that the approval was
407
+ * resolved on the device. The façade records that resolution on the same
408
+ * timeline, so the read model resumes from the one authority.
384
409
  * Deliberately NOT a wire message (no `packages/protocol` change) — a
385
410
  * first-class `task.approval_resolved` wire notification is a deferred
386
411
  * v1.1 candidate; this is purely an embedder-facing observability signal
@@ -396,8 +421,8 @@ export type ByokServerEvent = {
396
421
  * M4 (additive-minor): the EXPLICIT counterpart to
397
422
  * `task.approval_resolved_implicit` above — fires when the daemon reports
398
423
  * a locally-resolved approval via the wire `task.approval_resolved`
399
- * message (`ConnectionHub.onApprovalResolved`, `hub.ts`) rather than the
400
- * server having to infer it from later task traffic. Carries the same
424
+ * message rather than the server having to infer it from later task
425
+ * traffic. Carries the same
401
426
  * `approvalId`/`decision`/`resolvedBy` the daemon reported, so an embedder
402
427
  * can render/audit exactly what was resolved and by which path, not just
403
428
  * that a resolution happened. `resolvedBy` is currently always `'local'`
@@ -405,10 +430,8 @@ export type ByokServerEvent = {
405
430
  * enum today, future-proofed for an additional value later without a
406
431
  * version bump). Mutually exclusive with `task.approval_resolved_implicit`
407
432
  * for the same resolution: whichever mechanism the server processes first
408
- * performs the actual `AwaitApproval -> Running` transition, and the other
409
- * is already a no-op by the time it would otherwise run — see
410
- * `onApprovalResolved`'s own doc comment (`hub.ts`) for the full
411
- * relationship.
433
+ * clears the pending slot on the approval timeline, and the other finds
434
+ * nothing left to clear by the time it would otherwise run.
412
435
  */
413
436
  | ({
414
437
  kind: 'task.approval_resolved';
@@ -419,24 +442,23 @@ export type ByokServerEvent = {
419
442
  * REPORTING device advertised the `approval-targeting` capability flag
420
443
  * (`version.ts`) in its `conn.hello` — an observability-only signal
421
444
  * (see that flag's own doc comment: it never gates matching, which is
422
- * always decided by field presence on the specific message). `false`
423
- * for a legacy daemon, or one whose connection capabilities this hub
424
- * never recorded (see `ConnectionHub.getDeviceCapabilities`).
445
+ * always decided by field presence on the specific message). Always
446
+ * `false` now: that flag was a property of the device's LIVE WebSocket
447
+ * registration, which no longer exists, and the durable capability list
448
+ * is a device-BUILD fact rather than a per-report one.
425
449
  */
426
450
  targeted: boolean;
427
451
  } & Pick<TaskApprovalResolvedPayload, 'approvalId' | 'decision' | 'resolvedBy'>)
428
452
  /**
429
453
  * M4 Phase 4 (part A): `deviceId` exceeded its inbound-envelope rate limit
430
- * (`CreateByokServerOptions.rateLimit`, enforced in
431
- * `ConnectionHub.handleInbound`, `hub.ts`) — fired for every envelope that
432
- * arrives once the bucket is empty, not just the first. Never a silent
433
- * drop: this event fires AND the occurrence is counted in
434
- * `ConnectionHub.stats()`'s `rateLimitEvents`. Per-transport enforcement
435
- * differs (both still emit this same event): a WS connection is closed
436
- * (policy-violation close code) right after, so the client's existing
437
- * backoff+reconnect (protocol §9's redelivery covers the rest); a
438
- * long-poll device has no live connection to close, so `POST
439
- * /byok/messages` (`http.ts`) instead answers that request with HTTP 429.
454
+ * (`CreateByokServerOptions.rateLimit`, enforced at step 0 of the cloud
455
+ * kernel's inbound gate) — fired ONCE PER EPISODE, not once per refused
456
+ * envelope: the first refusal emits it, and only a later successful consume
457
+ * by the same device re-arms it, so a flood is one event and a device that
458
+ * recovers and floods again is a second, distinct one. Never a silent drop
459
+ * either way: every refused envelope counts in
460
+ * {@link HubStats.rateLimitEvents}, and the request that carried it is
461
+ * answered `429` by `POST /byok/messages`.
440
462
  */
441
463
  | {
442
464
  kind: 'device.rate_limited';
@@ -444,25 +466,30 @@ export type ByokServerEvent = {
444
466
  at: string;
445
467
  };
446
468
  /**
447
- * Plain, serializable in-process snapshot returned by
448
- * `ConnectionHub.stats()` (`hub.ts`) — M4 Phase 4 (part B.1). Deliberately
469
+ * Plain, serializable snapshot returned by `ByokServer.stats()` — M4 Phase 4
470
+ * (part B.1).
471
+ *
472
+ * `envelopesOut` went with the in-process outbox that produced it: server ->
473
+ * daemon envelopes are durable mailbox rows owned by the cloud kernel now, and
474
+ * a counter here would be a second, weaker authority over a fact the mailbox
475
+ * already holds exactly.
476
+ *
477
+ * Deliberately
449
478
  * NOT exposed over HTTP by this SDK (see `CreateByokServerOptions.healthzRoute`'s
450
479
  * doc comment): an embedder that wants any of this surfaced remotely builds
451
480
  * its own authenticated route around `ByokServer.stats()`.
452
481
  */
453
482
  export interface HubStats {
454
- /** Devices with a currently-live WS or long-poll connection. */
483
+ /** Devices this server has observed alive and not since forgotten — see {@link MachineInfo.connected}. */
455
484
  connectedDeviceCount: number;
456
485
  /** Every {@link TaskState} mapped to how many known tasks currently sit in it. */
457
486
  taskCountsByState: Record<TaskState, number>;
458
- /** Total inbound daemon->server envelopes {@link ConnectionHub.handleInbound} has ever been called with (every outcome, including rejected/rate-limited). */
487
+ /** Total inbound daemon->server envelopes the cloud kernel's inbound gate has been handed (every outcome, including rejected/rate-limited), counted at the gate's own step 0. */
459
488
  envelopesIn: number;
460
- /** Total server->daemon envelopes ever constructed via {@link ConnectionHub}'s single outbound choke point (`sendToDevice`), regardless of whether a live transport was available to flush them immediately. */
461
- envelopesOut: number;
462
489
  /** Inbound envelopes recognized as an already-seen `(deviceId, id)` pair (N3) — a no-op wire-level success, counted here for observability. */
463
490
  dedupDrops: number;
464
491
  /** Inbound envelopes rejected for exceeding a device's rate limit — see `device.rate_limited` on {@link ByokServerEvent}. */
465
492
  rateLimitEvents: number;
466
- /** Milliseconds since this `ConnectionHub` was constructed. */
493
+ /** Milliseconds since `createByokServer` returned this instance. */
467
494
  uptimeMs: number;
468
495
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/server",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "BYOK SDK server: in-memory M0 reference implementation of the SaaS-side coordinator",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,14 +44,13 @@
44
44
  "clean": "rm -rf dist"
45
45
  },
46
46
  "dependencies": {
47
- "@byok-sdk/core": "0.11.0",
48
- "@byok-sdk/protocol": "0.11.0",
47
+ "@byok-sdk/cloud": "0.13.0",
48
+ "@byok-sdk/core": "0.13.0",
49
+ "@byok-sdk/protocol": "0.13.0",
49
50
  "@hono/node-server": "^2.0.10",
50
- "hono": "^4.12.30",
51
- "jose": "^6.2.3",
52
- "ws": "^8.21.1"
51
+ "hono": "^4.12.30"
53
52
  },
54
53
  "devDependencies": {
55
- "@types/ws": "^8.18.1"
54
+ "@byok-sdk/conformance": "0.0.0"
56
55
  }
57
56
  }