@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/connections.d.ts +71 -0
- package/dist/device-selection.d.ts +30 -0
- package/dist/event-queue.d.ts +37 -2
- package/dist/index.d.ts +155 -88
- package/dist/index.js +1842 -3045
- package/dist/index.js.map +1 -1
- package/dist/rate-limiter.d.ts +38 -0
- package/dist/relay.d.ts +86 -0
- package/dist/snapshot.d.ts +64 -0
- package/dist/sqlite-support.d.ts +3 -3
- package/dist/stores/sqlite/__tests__/atomic-restart.test.d.ts +1 -0
- package/dist/stores/sqlite/__tests__/conformance.test.d.ts +1 -0
- package/dist/stores/sqlite/index.d.ts +140 -0
- package/dist/stores.d.ts +65 -0
- package/dist/task-handle.d.ts +61 -0
- package/dist/types.d.ts +167 -140
- package/package.json +6 -7
- package/dist/auth.d.ts +0 -218
- package/dist/blob-store.d.ts +0 -77
- package/dist/heartbeat.d.ts +0 -30
- package/dist/http.d.ts +0 -24
- package/dist/hub.d.ts +0 -931
- package/dist/ids.d.ts +0 -3
- package/dist/pairing.d.ts +0 -58
- package/dist/sqlite-blob-store.d.ts +0 -90
- package/dist/sqlite-task-store.d.ts +0 -124
- package/dist/task-store.d.ts +0 -127
- package/dist/ws-server.d.ts +0 -20
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 {
|
|
2
|
+
import type { TenantId, TokenSigner } from '@byok-sdk/cloud';
|
|
3
3
|
import type { RateLimiterOptions } from './rate-limiter';
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
/**
|
|
16
|
-
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* long
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
|
|
55
|
+
taskEventRetentionMs?: number;
|
|
44
56
|
/**
|
|
45
|
-
* M4 Phase 4 (part A): per-device inbound-envelope token bucket, enforced
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* the device
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
73
|
-
* app
|
|
74
|
-
* carrying no sensitive data (no device ids, no counts), just
|
|
75
|
-
* `{ok:true, uptimeMs}
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
-
*
|
|
83
|
+
* it.
|
|
81
84
|
*/
|
|
82
85
|
healthzRoute?: boolean;
|
|
83
|
-
/**
|
|
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
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
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 `
|
|
227
|
-
* that method's own doc comment for the full targeting/staleness
|
|
228
|
-
* semantics, including when this throws `StaleApprovalError` (exported
|
|
229
|
-
* from
|
|
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
|
-
/**
|
|
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
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
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
|
-
|
|
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
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* `
|
|
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
|
-
* `
|
|
326
|
-
* `SteerRejectedError`
|
|
327
|
-
*
|
|
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 (
|
|
343
|
-
*
|
|
344
|
-
* section, as opposed to `TaskHandle.events()` which is scoped
|
|
345
|
-
* Not part of the pinned wire contract; a server-embedder-facing
|
|
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
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
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
|
|
400
|
-
*
|
|
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
|
-
*
|
|
409
|
-
*
|
|
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).
|
|
423
|
-
*
|
|
424
|
-
*
|
|
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
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
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
|
|
448
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
48
|
-
"@byok-sdk/
|
|
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
|
-
"@
|
|
54
|
+
"@byok-sdk/conformance": "0.0.0"
|
|
56
55
|
}
|
|
57
56
|
}
|