@byok-sdk/cloud 0.6.0 → 0.7.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.
@@ -0,0 +1,12 @@
1
+ import { type Clock, type TenantId } from '@byok-sdk/core';
2
+ import type { AgentEgressRecord, AgentEgressStore } from '../ports';
3
+ /** Reference egress fact store. Duplicate event ids return, never overwrite, the first receipt. */
4
+ export declare class InMemoryAgentEgressStore implements AgentEgressStore {
5
+ #private;
6
+ constructor(clock: Clock);
7
+ record(tenant: TenantId, input: Omit<AgentEgressRecord, 'tenantId' | 'recordedAt'>): Promise<{
8
+ readonly record: AgentEgressRecord;
9
+ readonly created: boolean;
10
+ }>;
11
+ get(tenant: TenantId, deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
12
+ }
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import { type Clock, type ContentHash, type ObjectStore, type StorageReservation, type TenantId } from '@byok-sdk/core';
30
30
  import type { CloudCrypto } from '../../crypto/port';
31
- import type { BlobContent, BlobContentProxy, BlobObservation, BlobWriteResult, CloudBlobStore } from '../ports';
31
+ import type { BlobContentProxy, BlobObservation, BlobReadResult, BlobWriteResult, CloudBlobStore } from '../ports';
32
32
  /** How long a presigned upload/download URL stays valid. */
33
33
  export declare const BLOB_URL_TTL_MS: number;
34
34
  interface BlobRecord {
@@ -78,7 +78,13 @@ export declare class InMemoryBlobContentProxy implements BlobContentProxy {
78
78
  constructor(registry: InMemoryBlobRegistry);
79
79
  verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
80
80
  writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
81
- readContent(blobId: string): Promise<BlobContent | undefined>;
81
+ /**
82
+ * Never returns `{ok:false}`: this composition holds the bytes in the same
83
+ * process, so there is no upstream to be unreachable and no stream to be
84
+ * interrupted. Both `BlobReadErrorCode`s are structurally unreachable here
85
+ * — a proxy that fetches from object storage is where they become live.
86
+ */
87
+ readContent(blobId: string): Promise<BlobReadResult | undefined>;
82
88
  }
83
89
  /** Both halves over one registry. The only way to obtain either. */
84
90
  export interface InMemoryBlobs {
@@ -14,6 +14,10 @@ export declare class InMemoryDeviceDirectory implements DeviceDirectory {
14
14
  register(tenant: TenantId, input: DeviceRegistration): Promise<DeviceRecord>;
15
15
  get(tenant: TenantId, deviceId: string): Promise<DeviceRecord | undefined>;
16
16
  revoke(tenant: TenantId, deviceId: string): Promise<void>;
17
+ recordCapabilities(tenant: TenantId, input: {
18
+ readonly deviceId: string;
19
+ readonly capabilities: readonly string[];
20
+ }): Promise<DeviceRecord | undefined>;
17
21
  list(tenant: TenantId): Promise<readonly DeviceRecord[]>;
18
22
  readiness(tenant: TenantId, presence: PresenceStore): Promise<TenantReadiness>;
19
23
  resolveByDeviceId(deviceId: string): Promise<DeviceRecord | undefined>;
@@ -22,6 +22,7 @@ export { InMemoryTaskAttemptStore } from './task-attempts';
22
22
  export { InMemoryTaskCancellationStore } from './task-cancellations';
23
23
  export { InMemoryActivityStore } from './activity';
24
24
  export { InMemoryApprovalTimelineStore } from './approval-timeline';
25
+ export { InMemoryAgentEgressStore } from './agent-egress';
25
26
  /**
26
27
  * The port bundle plus the byte proxy, in the shape `createInMemoryCoreStores`
27
28
  * already uses: the composition is an object with a `stores` field, not the
@@ -16,14 +16,23 @@
16
16
  * unfalsifiable.
17
17
  */
18
18
  import { type Clock, type TenantId } from '@byok-sdk/core';
19
- import type { TaskAttempt, TaskAttemptStatus, TaskAttemptStore } from '../ports';
19
+ import type { AgentRef, TaskAttempt, TaskAttemptStatus, TaskAttemptStore } from '../ports';
20
20
  export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
21
21
  #private;
22
22
  constructor(clock: Clock, state?: InMemoryTaskAttemptState);
23
23
  open(tenant: TenantId, input: {
24
- taskId: string;
25
- deviceId: string;
24
+ readonly taskId: string;
25
+ readonly deviceId: string;
26
+ readonly agentRef?: AgentRef;
26
27
  }): Promise<TaskAttempt>;
28
+ reserveAgentOffer(tenant: TenantId, input: {
29
+ readonly taskId: string;
30
+ readonly deviceId: string;
31
+ readonly agentRef: AgentRef;
32
+ }): Promise<{
33
+ readonly attempt: TaskAttempt;
34
+ readonly created: boolean;
35
+ }>;
27
36
  get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
28
37
  getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
29
38
  claim(tenant: TenantId, input: {
@@ -31,8 +40,10 @@ export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
31
40
  deviceId: string;
32
41
  }): Promise<TaskAttempt | undefined>;
33
42
  recordStatus(tenant: TenantId, input: {
34
- taskId: string;
35
- status: TaskAttemptStatus;
43
+ readonly taskId: string;
44
+ readonly status: TaskAttemptStatus;
45
+ readonly agentRef?: AgentRef;
46
+ readonly terminalCause?: string;
36
47
  }): Promise<TaskAttempt | undefined>;
37
48
  }
38
49
  /** Shared mutable state for the task and cancellation reference ports. */
@@ -35,6 +35,8 @@
35
35
  * bytes (see the blobs section below).
36
36
  */
37
37
  import type { MailboxBody, MailboxMessage, PresenceStore, StorageReservation, TenantId, TenantReadiness } from '@byok-sdk/core';
38
+ import type { AgentEgressReliablePayload, AgentRef } from '@byok-sdk/protocol';
39
+ export type { AgentEgressReliablePayload, AgentRef } from '@byok-sdk/protocol';
38
40
  import type { ActivityStore } from '../activity';
39
41
  import type { ApprovalTimelineStore } from '../approval-timeline';
40
42
  export interface DeviceRecord {
@@ -49,6 +51,12 @@ export interface DeviceRecord {
49
51
  /** Current proof signing-key rotation generation. */
50
52
  readonly proofKeyEpoch: number;
51
53
  readonly revoked: boolean;
54
+ /**
55
+ * The latest capability snapshot written by an authenticated device
56
+ * handshake. This is deliberately separate from core presence: presence is
57
+ * lossy/TTL-bounded and cannot authorize Agent dispatch.
58
+ */
59
+ readonly capabilities?: readonly string[];
52
60
  }
53
61
  /** Everything `POST /byok/pair` knows at registration time. `tenantId` is the store's first parameter; `revoked` is the store's own to set. */
54
62
  export interface DeviceRegistration {
@@ -67,6 +75,15 @@ export interface DeviceDirectory {
67
75
  list(tenant: TenantId): Promise<readonly DeviceRecord[]>;
68
76
  /** Set-wise tenant observation; revoked devices never contribute presence. */
69
77
  readiness(tenant: TenantId, presence: PresenceStore): Promise<TenantReadiness>;
78
+ /**
79
+ * Persist a capability snapshot obtained from an authenticated device
80
+ * message. Implementations may return `undefined` for an unknown/revoked
81
+ * device; callers must fail closed in that case.
82
+ */
83
+ recordCapabilities(tenant: TenantId, input: {
84
+ readonly deviceId: string;
85
+ readonly capabilities: readonly string[];
86
+ }): Promise<DeviceRecord | undefined>;
70
87
  /** Pre-tenant. Two callers only: `POST /byok/challenge` and `POST /byok/token`. Never exposed through the tenant facade. */
71
88
  resolveByDeviceId(deviceId: string): Promise<DeviceRecord | undefined>;
72
89
  }
@@ -124,9 +141,13 @@ export interface TaskAttempt {
124
141
  readonly taskId: string;
125
142
  /** The device the offer was addressed to. */
126
143
  readonly deviceId: string;
127
- /** Set by `task.claim`, and only by the first one. Until then the task has no owner and the gate lets any of this tenant's devices through, matching the reference server. */
144
+ /** Exact Agent identity sealed when the strict Agent offer was opened. */
145
+ readonly agentRef?: AgentRef;
146
+ /** Set by `task.claim`, and only by the first one. Legacy attempts remain claimable by any tenant device until then; strict Agent attempts are target-device bound before this field is consulted. */
128
147
  readonly ownerDeviceId?: string;
129
148
  readonly status: TaskAttemptStatus;
149
+ /** Runtime-reported terminal cause from the first winning terminal. */
150
+ readonly terminalCause?: string;
130
151
  /** Durable host cancellation authority. Its presence outranks later device terminal receipts. */
131
152
  readonly cancellation?: {
132
153
  readonly requestedAt: string;
@@ -139,7 +160,20 @@ export interface TaskAttemptStore {
139
160
  open(tenant: TenantId, input: {
140
161
  readonly taskId: string;
141
162
  readonly deviceId: string;
163
+ readonly agentRef?: AgentRef;
142
164
  }): Promise<TaskAttempt>;
165
+ /**
166
+ * Atomically reserve one strict Agent offer. `created: false` means the task
167
+ * id already had durable authority and no caller may append another offer.
168
+ */
169
+ reserveAgentOffer(tenant: TenantId, input: {
170
+ readonly taskId: string;
171
+ readonly deviceId: string;
172
+ readonly agentRef: AgentRef;
173
+ }): Promise<{
174
+ readonly attempt: TaskAttempt;
175
+ readonly created: boolean;
176
+ }>;
143
177
  get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
144
178
  /** Batch lookup used by mailbox projection; implementations must not turn one poll into N queries. */
145
179
  getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
@@ -152,6 +186,8 @@ export interface TaskAttemptStore {
152
186
  recordStatus(tenant: TenantId, input: {
153
187
  readonly taskId: string;
154
188
  readonly status: TaskAttemptStatus;
189
+ readonly agentRef?: AgentRef;
190
+ readonly terminalCause?: string;
155
191
  }): Promise<TaskAttempt | undefined>;
156
192
  }
157
193
  export interface TaskCancellationRequest {
@@ -195,6 +231,22 @@ export interface RequestReceiptStore {
195
231
  }>;
196
232
  get(tenant: TenantId, key: string): Promise<RequestReceipt | undefined>;
197
233
  }
234
+ export interface AgentEgressRecord {
235
+ readonly tenantId: TenantId;
236
+ readonly deviceId: string;
237
+ readonly payload: AgentEgressReliablePayload;
238
+ /** Stable cloud-generated receipt identity echoed on every exact replay. */
239
+ readonly receiptId: string;
240
+ readonly recordedAt: string;
241
+ }
242
+ export interface AgentEgressStore {
243
+ /** First event-id write wins; callers reject mismatches rather than updating. */
244
+ record(tenant: TenantId, input: Omit<AgentEgressRecord, 'tenantId' | 'recordedAt'>): Promise<{
245
+ readonly record: AgentEgressRecord;
246
+ readonly created: boolean;
247
+ }>;
248
+ get(tenant: TenantId, deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
249
+ }
198
250
  export interface ProofRequestReceipt {
199
251
  readonly tenantId: TenantId;
200
252
  readonly deviceId: string;
@@ -241,6 +293,35 @@ export interface BlobContent {
241
293
  readonly data: Uint8Array;
242
294
  readonly contentType: string;
243
295
  }
296
+ /**
297
+ * How a byte-proxying read can fail while the blob itself is known to exist.
298
+ *
299
+ * The split is about WHERE the failure landed relative to the upstream
300
+ * response, because that is the only part a proxy can observe and the only
301
+ * part an operator can act on: `blob_upstream_unavailable` means nothing came
302
+ * back at all (the upstream was unreachable/refused before its response
303
+ * started), so a retry may succeed unchanged; `blob_upstream_stream_interrupted`
304
+ * means the response HAD started and died mid-transfer, so whatever the caller
305
+ * already has is a truncated prefix, not a short blob.
306
+ *
307
+ * Both are 502 on the wire (see `BLOB_READ_ERROR_HTTP_STATUS` in
308
+ * `handlers/blobs.ts`) — the CODE carries the distinction, not the status.
309
+ */
310
+ export declare const BLOB_READ_ERROR_CODES: readonly ['blob_upstream_unavailable', 'blob_upstream_stream_interrupted'];
311
+ export type BlobReadErrorCode = (typeof BLOB_READ_ERROR_CODES)[number];
312
+ /**
313
+ * The result of {@link BlobContentProxy.readContent}, in the same union idiom
314
+ * as {@link BlobWriteResult}. Note what is NOT in here: not-found stays
315
+ * `undefined` at the method's return type, so "no such blob" keeps its
316
+ * existing 404 meaning and never has to be spelled as a failure code.
317
+ */
318
+ export type BlobReadResult = {
319
+ readonly ok: true;
320
+ readonly content: BlobContent;
321
+ } | {
322
+ readonly ok: false;
323
+ readonly code: BlobReadErrorCode;
324
+ };
244
325
  /**
245
326
  * The capability-minting half of blobs: what EVERY composition can honestly
246
327
  * provide, whoever holds the bytes.
@@ -289,7 +370,8 @@ export interface CloudBlobStore {
289
370
  export interface BlobContentProxy {
290
371
  verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
291
372
  writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
292
- readContent(blobId: string): Promise<BlobContent | undefined>;
373
+ /** `undefined` = no such blob (404); a `{ok:false}` result = the blob exists but its bytes could not be proxied (502, distinguished by {@link BlobReadErrorCode}). */
374
+ readContent(blobId: string): Promise<BlobReadResult | undefined>;
293
375
  }
294
376
  /**
295
377
  * Step 0 of the inbound gate. S3a ships an allow-all reference (a hosted
@@ -317,10 +399,11 @@ export interface CloudStores {
317
399
  readonly tasks: TaskAttemptStore;
318
400
  readonly cancellations: TaskCancellationStore;
319
401
  readonly receipts: RequestReceiptStore;
402
+ readonly egress: AgentEgressStore;
320
403
  readonly proofReceipts: ProofRequestReceiptStore;
321
404
  readonly blobs: CloudBlobStore;
322
405
  readonly rateLimiter: InboundRateLimiter;
323
406
  }
324
407
  /** Names of every port in {@link CloudStores}, in contract order. */
325
- export declare const CLOUD_STORE_NAMES: readonly ['activity', 'approvals', 'devices', 'pairingCodes', 'nonces', 'dedup', 'tasks', 'cancellations', 'receipts', 'proofReceipts', 'blobs', 'rateLimiter'];
408
+ export declare const CLOUD_STORE_NAMES: readonly ['activity', 'approvals', 'devices', 'pairingCodes', 'nonces', 'dedup', 'tasks', 'cancellations', 'receipts', 'egress', 'proofReceipts', 'blobs', 'rateLimiter'];
326
409
  export type CloudStoreName = (typeof CLOUD_STORE_NAMES)[number];
@@ -24,7 +24,7 @@
24
24
  import { type BoardClaimInput, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type BoardStatusUpdateInput, type BoardUnclaimInput, type CoreStores, type MailboxAdvanceCursorInput, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type Principal, type PresenceHint, type PresenceHintInput, type TenantReadiness, type StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type TenantId } from '@byok-sdk/core';
25
25
  import type { ActivityAppendInput, ActivityTail } from './activity';
26
26
  import type { ApprovalTimelineAppendInput, ApprovalTimelineTail } from './approval-timeline';
27
- import type { BlobObservation, CloudStores, DeviceRecord, RequestReceipt, TaskCancellationMutation, TaskCancellationRequest, TaskAttempt, TaskAttemptStatus } from './stores/ports';
27
+ import type { BlobObservation, CloudStores, DeviceRecord, AgentEgressRecord, RequestReceipt, TaskCancellationMutation, TaskCancellationRequest, TaskAttempt, TaskAttemptStatus } from './stores/ports';
28
28
  export interface TenantBoundMailbox {
29
29
  append(input: MailboxAppendInput): Promise<MailboxMessage>;
30
30
  /** Pure read. Never advances the cursor — the daemon's next poll is the only ack. */
@@ -57,13 +57,26 @@ export interface TenantBoundDevices {
57
57
  get(deviceId: string): Promise<DeviceRecord | undefined>;
58
58
  list(): Promise<readonly DeviceRecord[]>;
59
59
  revoke(deviceId: string): Promise<void>;
60
+ /** Persist the authenticated device's own capability snapshot. */
61
+ recordCapabilities(input: {
62
+ readonly capabilities: readonly string[];
63
+ }): Promise<DeviceRecord | undefined>;
60
64
  readiness(): Promise<TenantReadiness>;
61
65
  }
62
66
  export interface TenantBoundTaskAttempts {
63
67
  open(input: {
64
68
  readonly taskId: string;
65
69
  readonly deviceId: string;
70
+ readonly agentRef?: TaskAttempt['agentRef'];
66
71
  }): Promise<TaskAttempt>;
72
+ reserveAgentOffer(input: {
73
+ readonly taskId: string;
74
+ readonly deviceId: string;
75
+ readonly agentRef: NonNullable<TaskAttempt['agentRef']>;
76
+ }): Promise<{
77
+ readonly attempt: TaskAttempt;
78
+ readonly created: boolean;
79
+ }>;
67
80
  get(taskId: string): Promise<TaskAttempt | undefined>;
68
81
  getMany(taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
69
82
  claim(input: {
@@ -73,6 +86,8 @@ export interface TenantBoundTaskAttempts {
73
86
  recordStatus(input: {
74
87
  readonly taskId: string;
75
88
  readonly status: TaskAttemptStatus;
89
+ readonly agentRef?: TaskAttempt['agentRef'];
90
+ readonly terminalCause?: TaskAttempt['terminalCause'];
76
91
  }): Promise<TaskAttempt | undefined>;
77
92
  }
78
93
  export interface TenantBoundTaskCancellations {
@@ -91,6 +106,13 @@ export interface TenantBoundReceipts {
91
106
  }>;
92
107
  get(key: string): Promise<RequestReceipt | undefined>;
93
108
  }
109
+ export interface TenantBoundAgentEgress {
110
+ record(input: Omit<AgentEgressRecord, 'tenantId' | 'recordedAt'>): Promise<{
111
+ readonly record: AgentEgressRecord;
112
+ readonly created: boolean;
113
+ }>;
114
+ get(deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
115
+ }
94
116
  export interface TenantBoundBlobs {
95
117
  createUpload(reservation: StorageReservation): Promise<{
96
118
  readonly blobId: string;
@@ -121,6 +143,7 @@ export interface TenantStores {
121
143
  readonly cancellations: TenantBoundTaskCancellations;
122
144
  readonly dedup: TenantBoundDedup;
123
145
  readonly receipts: TenantBoundReceipts;
146
+ readonly egress: TenantBoundAgentEgress;
124
147
  readonly blobs: TenantBoundBlobs;
125
148
  readonly quota: TenantBoundQuota;
126
149
  readonly rateLimiter: TenantBoundRateLimiter;
@@ -1,4 +1,4 @@
1
- import { type BlobRef, type TerminalInferenceUsage } from '@byok-sdk/protocol';
1
+ import { type AgentRef, type BlobRef, type TerminalInferenceUsage } from '@byok-sdk/protocol';
2
2
  import type { RequestReceipt } from './stores/ports';
3
3
  /**
4
4
  * The typed terminal read model — the hosted counterpart of the embedded
@@ -10,6 +10,8 @@ import type { RequestReceipt } from './stores/ports';
10
10
  export interface TerminalResult {
11
11
  readonly taskId: string;
12
12
  readonly state: 'complete' | 'failed' | 'cancelled';
13
+ /** Exact Agent identity echoed by the winning terminal, when Agent-bound. */
14
+ readonly agentRef?: AgentRef;
13
15
  readonly summary?: string;
14
16
  readonly sessionRef?: string;
15
17
  readonly artifactRefs?: readonly BlobRef[];
@@ -27,6 +29,8 @@ export interface TerminalResult {
27
29
  */
28
30
  readonly usage?: TerminalInferenceUsage;
29
31
  readonly reason?: string;
32
+ /** Terminal cause projection; currently the protocol's terminal reason. */
33
+ readonly terminalCause?: string;
30
34
  readonly retryable?: boolean;
31
35
  /** When the receipt store wrote the terminal fact — the first one, by its own first-write-wins rule. */
32
36
  readonly recordedAt: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/cloud",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "BYOK SDK hosted device surface: stateless frozen-v1 HTTP handlers over tenant-first @byok-sdk/core ports",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,8 +44,8 @@
44
44
  "clean": "rm -rf dist"
45
45
  },
46
46
  "dependencies": {
47
- "@byok-sdk/core": "0.6.0",
48
- "@byok-sdk/protocol": "0.6.0",
47
+ "@byok-sdk/core": "0.7.0",
48
+ "@byok-sdk/protocol": "0.7.0",
49
49
  "hono": "^4.12.30",
50
50
  "zod": "^4.4.3"
51
51
  }