@byok-sdk/server 0.12.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 +1838 -3370
- 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 -146
- package/package.json +6 -7
- package/dist/auth.d.ts +0 -218
- package/dist/blob-store.d.ts +0 -89
- package/dist/heartbeat.d.ts +0 -30
- package/dist/http.d.ts +0 -24
- package/dist/hub.d.ts +0 -947
- package/dist/ids.d.ts +0 -3
- package/dist/pairing.d.ts +0 -87
- package/dist/sqlite-blob-store.d.ts +0 -94
- package/dist/sqlite-task-store.d.ts +0 -124
- package/dist/task-store.d.ts +0 -127
- package/dist/ws-server.d.ts +0 -23
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,91 +20,92 @@ export interface CreateByokServerOptions {
|
|
|
12
20
|
* mismatched daemon is rejected at handshake time.
|
|
13
21
|
*/
|
|
14
22
|
productId: string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* long
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
55
|
+
taskEventRetentionMs?: number;
|
|
50
56
|
/**
|
|
51
|
-
* M4 Phase 4 (part A): per-device inbound-envelope token bucket, enforced
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* the device
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
79
|
-
* app
|
|
80
|
-
* carrying no sensitive data (no device ids, no counts), just
|
|
81
|
-
* `{ok:true, uptimeMs}
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
-
*
|
|
83
|
+
* it.
|
|
87
84
|
*/
|
|
88
85
|
healthzRoute?: boolean;
|
|
89
|
-
/**
|
|
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
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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 `
|
|
233
|
-
* that method's own doc comment for the full targeting/staleness
|
|
234
|
-
* semantics, including when this throws `StaleApprovalError` (exported
|
|
235
|
-
* 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.
|
|
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
|
-
/**
|
|
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
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
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
|
-
|
|
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
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* `
|
|
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
|
-
* `
|
|
332
|
-
* `SteerRejectedError`
|
|
333
|
-
*
|
|
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 (
|
|
349
|
-
*
|
|
350
|
-
* section, as opposed to `TaskHandle.events()` which is scoped
|
|
351
|
-
* 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.
|
|
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
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
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
|
|
406
|
-
*
|
|
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
|
-
*
|
|
415
|
-
*
|
|
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).
|
|
429
|
-
*
|
|
430
|
-
*
|
|
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
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
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
|
|
454
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
}
|