@byok-sdk/server 0.12.0 → 0.14.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,91 +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;
17
- /** Deadline for one authenticated socket to present a valid `conn.hello`. Default 5s. */
18
- webSocketHelloTimeoutMs?: number;
19
- /** Global cap for authenticated sockets that have not completed `conn.hello`. Default 32. */
20
- maxPendingWebSockets?: number;
21
- /** Maximum inbound WS message bytes before envelope decoding. Default accommodates the largest protocol document envelope. */
22
- maxWebSocketPayloadBytes?: 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;
23
29
  /** How long `GET /byok/events` holds an empty poll open before returning, ms (§8). Default ~50s; override for tests. */
24
30
  longPollHoldMs?: number;
25
31
  /** Per-product blob size ceiling in bytes (§7). Default 100MB. */
26
32
  maxBlobSizeBytes?: number;
27
- /** 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). */
28
- blobStore?: BlobStore;
29
- /** 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. */
30
- taskStore?: TaskStore;
31
33
  /** Override the reference {@link TokenSigner} (e.g. an org-wide/KMS-backed signer). */
32
34
  tokenSigner?: TokenSigner;
33
35
  /**
34
- * How long a `Claimed`/`Running`/`AwaitApproval` task may sit with no
35
- * inbound `task.*` activity from its owning device while that device is
36
- * dark (disconnected, or long-poll-silent) before the server reaps it to
37
- * `Failed(retryable: true, reason: 'lease-expired')` — no new task state,
38
- * no new wire message; the embedder is expected to re-dispatch as a
39
- * brand-new task, same as any other retryable failure. Deliberately
40
- * generous — it exists purely as a backstop for a device that never
41
- * reconnects at all (M1's redelivery, docs/protocol.md §9, already covers
42
- * "came back within the window"), so it must stay far larger than any
43
- * realistic task duration or it will race and fail perfectly healthy
44
- * long-running tasks. A task on a *connected*, actively-progressing
45
- * device is never touched regardless of this value — see
46
- * `ConnectionHub`'s lease-reaper doc comment (`hub.ts`) for the full
47
- * 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.
48
54
  */
49
- taskLeaseMs?: number;
55
+ taskEventRetentionMs?: number;
50
56
  /**
51
- * M4 Phase 4 (part A): per-device inbound-envelope token bucket, enforced
52
- * by `ConnectionHub.handleInbound` (`hub.ts`) — the single choke point
53
- * both WS (`ws-server.ts`) and long-poll (`POST /byok/messages`, `http.ts`)
54
- * inbound traffic passes through. Defaults: 50 msg/s sustained, burst 100
55
- * (see `rate-limiter.ts`'s own defaults). Exceeding it never drops
56
- * silently: it counts in `ConnectionHub.stats()`'s `rateLimitEvents`, and
57
- * emits a `device.rate_limited` {@link ByokServerEvent} — see that
58
- * variant's own doc comment for the per-transport enforcement shape (WS
59
- * close vs. long-poll 429). Blob upload/download routes (`http.ts`) are
60
- * deliberately NOT covered by this same bucket — see that file's own
61
- * 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`).
62
63
  *
63
- * Honest caveat (no code change changes this — it's an inherent property
64
- * of an abrupt WS close, not something rate limiting adds): a
65
- * flood-triggered 1008 close is not special. Envelopes the daemon's own
66
- * WS transport already handed off to its socket write between the moment
67
- * the device exceeded budget and the close actually landing share the
68
- * ordinary at-most-once exposure of ANY abrupt WS disconnect (network
69
- * blip, server restart, etc.) — the wire's at-least-once guarantee
70
- * (docs/protocol.md §9) is specified for the server->daemon direction
71
- * only; daemon->server has no redelivery cursor to begin with, so this
72
- * was already true before rate limiting existed. A flood just makes that
73
- * pre-existing window more likely to have something in flight at the
74
- * 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.
75
71
  */
76
72
  rateLimit?: RateLimiterOptions;
77
73
  /**
78
- * M4 Phase 4 (part B.2): opt-in `GET /healthz` liveness route on the Hono
79
- * app (`http.ts`) — deliberately unauthenticated (no bearer check) and
80
- * carrying no sensitive data (no device ids, no counts), just
81
- * `{ok:true, uptimeMs}`; see `http.ts`'s own comment on that route for the
82
- * full auth-posture rationale. Default `false` (no route mounted at all).
83
- * `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
84
81
  * exposed over HTTP by this SDK regardless of this flag — an embedder that
85
82
  * wants that surfaced remotely builds its own authenticated route around
86
- * `stats()`.
83
+ * it.
87
84
  */
88
85
  healthzRoute?: boolean;
89
- /** 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
+ */
90
98
  agentMessage?: {
91
99
  consume(input: {
100
+ readonly tenant: TenantId;
92
101
  readonly deviceId: string;
93
102
  readonly taskId: string;
94
103
  readonly context: AgentMessageServerContext;
95
104
  readonly payload: AgentMessagePublishPayload;
96
- }): {
105
+ }): Promise<{
97
106
  readonly outcome: 'accepted' | 'held' | 'refused';
98
107
  readonly reasonCode?: string;
99
- };
108
+ }>;
100
109
  };
101
110
  }
102
111
  /** Input to {@link ByokServer.dispatch}. */
@@ -180,10 +189,10 @@ export interface TaskResult {
180
189
  * no `resultDocument` extractor configured and a pre-`result-document`
181
190
  * daemon build that has no notion of the field at all.
182
191
  *
183
- * Persisted WITH `summary`/`artifactRefs` rather than beside them: the
184
- * whole `TaskResult` is stored as a single `result_json` document
185
- * (`sqlite-task-store.ts`), so this field reaches durable storage with
186
- * 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.
187
196
  */
188
197
  document?: unknown;
189
198
  }
@@ -229,10 +238,13 @@ export interface TaskHandle {
229
238
  * M5 (approval targeting, docs/protocol.md §5.3): `opts.approvalId`
230
239
  * targets a SPECIFIC pending approval rather than "whichever one is
231
240
  * currently pending" (the default when `opts` is omitted, unchanged from
232
- * pre-M5). Thin wrapper over `ConnectionHub.approveTask` (`hub.ts`) — see
233
- * that method's own doc comment for the full targeting/staleness
234
- * semantics, including when this throws `StaleApprovalError` (exported
235
- * 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.
236
248
  */
237
249
  approve(opts?: {
238
250
  approvalId?: string;
@@ -257,24 +269,28 @@ export interface MachineInfo {
257
269
  /** Logical toolset IDs reported by the current daemon; omission means legacy/unknown. */
258
270
  configuredToolsets?: ToolsetId[];
259
271
  }
260
- /** 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
+ */
261
284
  export interface TaskSnapshot {
262
285
  taskId: string;
263
- state: TaskState;
264
- instruction: string;
265
286
  /**
266
- * The REQUESTED runtime — `DispatchInput.runtime`, forwarded verbatim into
267
- * `task.offer.runtime`. Set once at `dispatch()` time and never touched
268
- * again afterward, regardless of what the daemon actually ends up running.
269
- * `undefined` means "no preference was expressed" (the daemon auto-selects,
270
- * pi-first) — NOT "the daemon ran no runtime". Contrast with
271
- * {@link claimedRuntime}, the ACTUAL runtime the daemon reports having
272
- * picked; see that field's own doc comment for the full requested-vs-
273
- * 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.
274
292
  */
275
- runtime?: RuntimeId;
276
- policy: PermissionPolicy;
277
- requiredToolsets?: ToolsetId[];
293
+ state: TaskState;
278
294
  deviceId?: string;
279
295
  sessionRef?: string;
280
296
  /** Exact Agent identity for an Agent-bound task; absent for legacy tasks. */
@@ -284,22 +300,24 @@ export interface TaskSnapshot {
284
300
  result?: TaskResult;
285
301
  /**
286
302
  * M5 (approval targeting, docs/protocol.md §5.3): the daemon-reported
287
- * `approvalId` for the CURRENT `AwaitApproval` cycle, if this server has
288
- * learned one (`ConnectionHub.onAwaitApproval`, `hub.ts`) — `undefined`
289
- * whenever the task isn't currently awaiting approval, OR it is but no id
290
- * was ever reported for it (a legacy daemon). Cleared centrally the
291
- * instant the task LEAVES `AwaitApproval` (`ConnectionHub`'s
292
- * `transitionTask`), so a later `AwaitApproval` cycle for the same task
293
- * never inherits a stale id from a previous one. `approveTask`/
294
- * `rejectTask` compare an operator-supplied target id against this field
295
- * to decide whether a decision is stale (`StaleApprovalError`) — see
296
- * `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.
297
314
  */
298
315
  pendingApprovalId?: string;
299
316
  /**
300
317
  * M5 (claimed runtime, docs/protocol.md §3.1): the ACTUAL adapter the
301
318
  * daemon reports having selected for this task (`task.claim.runtime`,
302
- * `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
303
321
  * path (echoes {@link runtime}) and the auto-select/pi-first path (a value
304
322
  * where {@link runtime} is `undefined`, since no preference was ever
305
323
  * requested). `undefined` until the first `task.claim` for this task
@@ -308,29 +326,30 @@ export interface TaskSnapshot {
308
326
  * `Offered -> Claimed` transition, and never modified again afterward — a
309
327
  * retried/idempotent claim from the same device is a no-op that never
310
328
  * reaches `onClaim`'s patch at all (see `onClaim`'s own doc comment), so
311
- * 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`).
312
331
  */
313
332
  claimedRuntime?: RuntimeId;
314
333
  /**
315
334
  * S0/D-4 (runtime-honest control surface): the capability block the
316
335
  * CLAIMING adapter reported for itself on its own `task.claim`
317
336
  * (`TaskClaimPayload.capabilities`, `@byok-sdk/protocol`), snapshotted at the
318
- * exact moment of the `Offered -> Claimed` transition
319
- * (`ConnectionHub.onClaim`, `hub.ts`).
337
+ * exact moment of the `Offered -> Claimed` transition and read straight off
338
+ * `TaskAttempt.claimedRuntimeCapabilities` (`@byok-sdk/cloud`).
320
339
  *
321
340
  * Sourced from the claim and from nothing else. The connection-level
322
341
  * `conn.hello.runtimes[].capabilities` is discovery data — it describes a
323
342
  * device rather than the adapter that claimed this task — so it is never
324
- * read here or by the gate; see
325
- * `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.
326
345
  *
327
346
  * A SNAPSHOT, deliberately — not a live read of anything: the same device
328
347
  * can reconnect later with a different adapter set (a runtime upgraded,
329
348
  * removed, or newly installed mid-task), and a task that is already running
330
349
  * must keep being judged against what was true when it was claimed.
331
- * `ConnectionHub.steerTask` is the consumer: it fails closed with a
332
- * `SteerRejectedError` (`hub.ts`) unless this snapshot says `steer === true`,
333
- * 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.
334
353
  *
335
354
  * `undefined` means "this server does not know" — never "supported" and
336
355
  * never "unsupported as a fact". It stays `undefined` when the claim carried
@@ -345,20 +364,20 @@ export interface TaskSnapshot {
345
364
  claimedRuntimeCapabilities?: RuntimeCapabilities;
346
365
  }
347
366
  /**
348
- * Cross-cutting server event feed (device connects/disconnects, task
349
- * creation/state changes) — the "event hub" from the plan's 服务端参考实现
350
- * section, as opposed to `TaskHandle.events()` which is scoped to one task.
351
- * 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.
352
379
  */
353
380
  export type ByokServerEvent = {
354
- kind: 'device.connected';
355
- deviceId: string;
356
- at: string;
357
- } | {
358
- kind: 'device.disconnected';
359
- deviceId: string;
360
- at: string;
361
- } | {
362
381
  kind: 'task.created';
363
382
  taskId: string;
364
383
  at: string;
@@ -383,10 +402,10 @@ export type ByokServerEvent = {
383
402
  * pending approval entirely locally (M4 Phase 3's local `approvals.resolve`
384
403
  * control-socket path) — no wire `task.approve`/`task.reject` ever reached
385
404
  * the server for it. This fires when daemon-originated task traffic
386
- * (`task.progress`/`task.artifact`/`task.complete`) for a task the server's
387
- * own record still has as `AwaitApproval` proves, after the fact, that the
388
- * approval was resolved on the device — see `ConnectionHub`'s
389
- * `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.
390
409
  * Deliberately NOT a wire message (no `packages/protocol` change) — a
391
410
  * first-class `task.approval_resolved` wire notification is a deferred
392
411
  * v1.1 candidate; this is purely an embedder-facing observability signal
@@ -402,8 +421,8 @@ export type ByokServerEvent = {
402
421
  * M4 (additive-minor): the EXPLICIT counterpart to
403
422
  * `task.approval_resolved_implicit` above — fires when the daemon reports
404
423
  * a locally-resolved approval via the wire `task.approval_resolved`
405
- * message (`ConnectionHub.onApprovalResolved`, `hub.ts`) rather than the
406
- * 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
407
426
  * `approvalId`/`decision`/`resolvedBy` the daemon reported, so an embedder
408
427
  * can render/audit exactly what was resolved and by which path, not just
409
428
  * that a resolution happened. `resolvedBy` is currently always `'local'`
@@ -411,10 +430,8 @@ export type ByokServerEvent = {
411
430
  * enum today, future-proofed for an additional value later without a
412
431
  * version bump). Mutually exclusive with `task.approval_resolved_implicit`
413
432
  * for the same resolution: whichever mechanism the server processes first
414
- * performs the actual `AwaitApproval -> Running` transition, and the other
415
- * is already a no-op by the time it would otherwise run — see
416
- * `onApprovalResolved`'s own doc comment (`hub.ts`) for the full
417
- * 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.
418
435
  */
419
436
  | ({
420
437
  kind: 'task.approval_resolved';
@@ -425,24 +442,23 @@ export type ByokServerEvent = {
425
442
  * REPORTING device advertised the `approval-targeting` capability flag
426
443
  * (`version.ts`) in its `conn.hello` — an observability-only signal
427
444
  * (see that flag's own doc comment: it never gates matching, which is
428
- * always decided by field presence on the specific message). `false`
429
- * for a legacy daemon, or one whose connection capabilities this hub
430
- * 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.
431
449
  */
432
450
  targeted: boolean;
433
451
  } & Pick<TaskApprovalResolvedPayload, 'approvalId' | 'decision' | 'resolvedBy'>)
434
452
  /**
435
453
  * M4 Phase 4 (part A): `deviceId` exceeded its inbound-envelope rate limit
436
- * (`CreateByokServerOptions.rateLimit`, enforced in
437
- * `ConnectionHub.handleInbound`, `hub.ts`) — fired for every envelope that
438
- * arrives once the bucket is empty, not just the first. Never a silent
439
- * drop: this event fires AND the occurrence is counted in
440
- * `ConnectionHub.stats()`'s `rateLimitEvents`. Per-transport enforcement
441
- * differs (both still emit this same event): a WS connection is closed
442
- * (policy-violation close code) right after, so the client's existing
443
- * backoff+reconnect (protocol §9's redelivery covers the rest); a
444
- * long-poll device has no live connection to close, so `POST
445
- * /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`.
446
462
  */
447
463
  | {
448
464
  kind: 'device.rate_limited';
@@ -450,25 +466,30 @@ export type ByokServerEvent = {
450
466
  at: string;
451
467
  };
452
468
  /**
453
- * Plain, serializable in-process snapshot returned by
454
- * `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
455
478
  * NOT exposed over HTTP by this SDK (see `CreateByokServerOptions.healthzRoute`'s
456
479
  * doc comment): an embedder that wants any of this surfaced remotely builds
457
480
  * its own authenticated route around `ByokServer.stats()`.
458
481
  */
459
482
  export interface HubStats {
460
- /** 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}. */
461
484
  connectedDeviceCount: number;
462
485
  /** Every {@link TaskState} mapped to how many known tasks currently sit in it. */
463
486
  taskCountsByState: Record<TaskState, number>;
464
- /** 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. */
465
488
  envelopesIn: number;
466
- /** 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. */
467
- envelopesOut: number;
468
489
  /** Inbound envelopes recognized as an already-seen `(deviceId, id)` pair (N3) — a no-op wire-level success, counted here for observability. */
469
490
  dedupDrops: number;
470
491
  /** Inbound envelopes rejected for exceeding a device's rate limit — see `device.rate_limited` on {@link ByokServerEvent}. */
471
492
  rateLimitEvents: number;
472
- /** Milliseconds since this `ConnectionHub` was constructed. */
493
+ /** Milliseconds since `createByokServer` returned this instance. */
473
494
  uptimeMs: number;
474
495
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/server",
3
- "version": "0.12.0",
3
+ "version": "0.14.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.12.0",
48
- "@byok-sdk/protocol": "0.12.0",
47
+ "@byok-sdk/cloud": "0.14.0",
48
+ "@byok-sdk/core": "0.14.0",
49
+ "@byok-sdk/protocol": "0.14.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
  }