@byok-sdk/cloud 0.5.0 → 0.6.1

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 {
@@ -7,13 +7,18 @@
7
7
  * so a revocation applied through the composite key is immediately visible to
8
8
  * `/byok/challenge` and `/byok/token` with no second copy to keep in sync.
9
9
  */
10
- import { type TenantId } from '@byok-sdk/core';
10
+ import { type PresenceStore, type TenantId, type TenantReadiness } from '@byok-sdk/core';
11
11
  import type { DeviceDirectory, DeviceRecord, DeviceRegistration } from '../ports';
12
12
  export declare class InMemoryDeviceDirectory implements DeviceDirectory {
13
13
  #private;
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[]>;
22
+ readiness(tenant: TenantId, presence: PresenceStore): Promise<TenantReadiness>;
18
23
  resolveByDeviceId(deviceId: string): Promise<DeviceRecord | undefined>;
19
24
  }
@@ -19,8 +19,10 @@ export { InMemoryPairingCodeStore } from './pairing-codes';
19
19
  export { InMemoryRequestReceiptStore } from './receipts';
20
20
  export { InMemoryProofRequestReceiptStore } from './proof-receipts';
21
21
  export { InMemoryTaskAttemptStore } from './task-attempts';
22
+ export { InMemoryTaskCancellationStore } from './task-cancellations';
22
23
  export { InMemoryActivityStore } from './activity';
23
24
  export { InMemoryApprovalTimelineStore } from './approval-timeline';
25
+ export { InMemoryAgentEgressStore } from './agent-egress';
24
26
  /**
25
27
  * The port bundle plus the byte proxy, in the shape `createInMemoryCoreStores`
26
28
  * already uses: the composition is an object with a `stores` field, not the
@@ -34,4 +36,4 @@ export interface InMemoryCloudComposition {
34
36
  readonly stores: CloudStores;
35
37
  readonly blobContentProxy: BlobContentProxy;
36
38
  }
37
- export declare function createInMemoryCloudStores(clock: Clock, crypto: CloudCrypto, objects: ObjectStore): InMemoryCloudComposition;
39
+ export declare function createInMemoryCloudStores(clock: Clock, crypto: CloudCrypto, objects: ObjectStore, mailbox: import('@byok-sdk/core').MailboxStore): InMemoryCloudComposition;
@@ -16,21 +16,42 @@
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
- constructor(clock: Clock);
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>;
37
+ getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
28
38
  claim(tenant: TenantId, input: {
29
39
  taskId: string;
30
40
  deviceId: string;
31
41
  }): Promise<TaskAttempt | undefined>;
32
42
  recordStatus(tenant: TenantId, input: {
33
- taskId: string;
34
- status: TaskAttemptStatus;
43
+ readonly taskId: string;
44
+ readonly status: TaskAttemptStatus;
45
+ readonly agentRef?: AgentRef;
46
+ readonly terminalCause?: string;
35
47
  }): Promise<TaskAttempt | undefined>;
36
48
  }
49
+ /** Shared mutable state for the task and cancellation reference ports. */
50
+ export declare class InMemoryTaskAttemptState {
51
+ #private;
52
+ readonly attempts: Map<string, TaskAttempt>;
53
+ constructor(clock: Clock);
54
+ now(): string;
55
+ /** Serialize every state-changing operation for one tenant/task key. */
56
+ mutate<T>(key: string, operation: () => T | Promise<T>): Promise<T>;
57
+ }
@@ -0,0 +1,9 @@
1
+ import { type MailboxStore, type TenantId } from '@byok-sdk/core';
2
+ import type { TaskCancellationMutation, TaskCancellationRequest, TaskCancellationStore } from '../ports';
3
+ import { InMemoryTaskAttemptState } from './task-attempts';
4
+ /** Failure-free reference composition of the atomic cancellation port. */
5
+ export declare class InMemoryTaskCancellationStore implements TaskCancellationStore {
6
+ #private;
7
+ constructor(state: InMemoryTaskAttemptState, mailbox: MailboxStore);
8
+ request(tenant: TenantId, input: TaskCancellationRequest): Promise<TaskCancellationMutation | undefined>;
9
+ }
@@ -34,7 +34,9 @@
34
34
  * because a composition backed by object storage physically cannot proxy
35
35
  * bytes (see the blobs section below).
36
36
  */
37
- import type { StorageReservation, TenantId } from '@byok-sdk/core';
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 {
@@ -65,6 +73,17 @@ export interface DeviceDirectory {
65
73
  get(tenant: TenantId, deviceId: string): Promise<DeviceRecord | undefined>;
66
74
  revoke(tenant: TenantId, deviceId: string): Promise<void>;
67
75
  list(tenant: TenantId): Promise<readonly DeviceRecord[]>;
76
+ /** Set-wise tenant observation; revoked devices never contribute presence. */
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>;
68
87
  /** Pre-tenant. Two callers only: `POST /byok/challenge` and `POST /byok/token`. Never exposed through the tenant facade. */
69
88
  resolveByDeviceId(deviceId: string): Promise<DeviceRecord | undefined>;
70
89
  }
@@ -115,16 +134,25 @@ export interface InboundDedupStore {
115
134
  */
116
135
  checkAndRecord(tenant: TenantId, deviceId: string, envelopeId: string): Promise<boolean>;
117
136
  }
118
- export declare const TASK_ATTEMPT_STATUSES: readonly ['offered', 'claimed', 'running', 'complete', 'failed', 'cancelled'];
137
+ export declare const TASK_ATTEMPT_STATUSES: readonly ['offered', 'claimed', 'running', 'cancel_requested', 'complete', 'failed', 'cancelled'];
119
138
  export type TaskAttemptStatus = (typeof TASK_ATTEMPT_STATUSES)[number];
120
139
  export interface TaskAttempt {
121
140
  readonly tenantId: TenantId;
122
141
  readonly taskId: string;
123
142
  /** The device the offer was addressed to. */
124
143
  readonly deviceId: string;
125
- /** 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. */
126
147
  readonly ownerDeviceId?: string;
127
148
  readonly status: TaskAttemptStatus;
149
+ /** Runtime-reported terminal cause from the first winning terminal. */
150
+ readonly terminalCause?: string;
151
+ /** Durable host cancellation authority. Its presence outranks later device terminal receipts. */
152
+ readonly cancellation?: {
153
+ readonly requestedAt: string;
154
+ readonly reason?: string;
155
+ };
128
156
  readonly updatedAt: string;
129
157
  }
130
158
  export interface TaskAttemptStore {
@@ -132,8 +160,23 @@ export interface TaskAttemptStore {
132
160
  open(tenant: TenantId, input: {
133
161
  readonly taskId: string;
134
162
  readonly deviceId: string;
163
+ readonly agentRef?: AgentRef;
135
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
+ }>;
136
177
  get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
178
+ /** Batch lookup used by mailbox projection; implementations must not turn one poll into N queries. */
179
+ getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
137
180
  /** First claim wins the ownership; a later claim by the same device is idempotent. No-op (returns `undefined`) for a task this tenant never offered. */
138
181
  claim(tenant: TenantId, input: {
139
182
  readonly taskId: string;
@@ -143,8 +186,28 @@ export interface TaskAttemptStore {
143
186
  recordStatus(tenant: TenantId, input: {
144
187
  readonly taskId: string;
145
188
  readonly status: TaskAttemptStatus;
189
+ readonly agentRef?: AgentRef;
190
+ readonly terminalCause?: string;
146
191
  }): Promise<TaskAttempt | undefined>;
147
192
  }
193
+ export interface TaskCancellationRequest {
194
+ readonly taskId: string;
195
+ readonly proposedMessageId: string;
196
+ readonly reason?: string;
197
+ readonly materialize: (seq: number, messageId: string) => MailboxBody | Promise<MailboxBody>;
198
+ }
199
+ export interface TaskCancellationMutation {
200
+ readonly attempt: TaskAttempt;
201
+ /** Absent only when the task was already complete or failed before cancellation. */
202
+ readonly message?: MailboxMessage;
203
+ }
204
+ /**
205
+ * One atomic authority for the cancellation tombstone plus its durable device
206
+ * delivery. Implementations must commit both or neither.
207
+ */
208
+ export interface TaskCancellationStore {
209
+ request(tenant: TenantId, input: TaskCancellationRequest): Promise<TaskCancellationMutation | undefined>;
210
+ }
148
211
  export interface RequestReceipt {
149
212
  readonly tenantId: TenantId;
150
213
  readonly key: string;
@@ -168,6 +231,22 @@ export interface RequestReceiptStore {
168
231
  }>;
169
232
  get(tenant: TenantId, key: string): Promise<RequestReceipt | undefined>;
170
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
+ }
171
250
  export interface ProofRequestReceipt {
172
251
  readonly tenantId: TenantId;
173
252
  readonly deviceId: string;
@@ -214,6 +293,35 @@ export interface BlobContent {
214
293
  readonly data: Uint8Array;
215
294
  readonly contentType: string;
216
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
+ };
217
325
  /**
218
326
  * The capability-minting half of blobs: what EVERY composition can honestly
219
327
  * provide, whoever holds the bytes.
@@ -262,7 +370,8 @@ export interface CloudBlobStore {
262
370
  export interface BlobContentProxy {
263
371
  verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
264
372
  writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
265
- 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>;
266
375
  }
267
376
  /**
268
377
  * Step 0 of the inbound gate. S3a ships an allow-all reference (a hosted
@@ -288,11 +397,13 @@ export interface CloudStores {
288
397
  readonly nonces: NonceStore;
289
398
  readonly dedup: InboundDedupStore;
290
399
  readonly tasks: TaskAttemptStore;
400
+ readonly cancellations: TaskCancellationStore;
291
401
  readonly receipts: RequestReceiptStore;
402
+ readonly egress: AgentEgressStore;
292
403
  readonly proofReceipts: ProofRequestReceiptStore;
293
404
  readonly blobs: CloudBlobStore;
294
405
  readonly rateLimiter: InboundRateLimiter;
295
406
  }
296
407
  /** Names of every port in {@link CloudStores}, in contract order. */
297
- export declare const CLOUD_STORE_NAMES: readonly ['activity', 'approvals', 'devices', 'pairingCodes', 'nonces', 'dedup', 'tasks', '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'];
298
409
  export type CloudStoreName = (typeof CLOUD_STORE_NAMES)[number];
@@ -21,10 +21,10 @@
21
21
  * presigned blob calls) are deliberately absent from this surface: a
22
22
  * device-facing handler must not be able to reach them.
23
23
  */
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 StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type TenantId } from '@byok-sdk/core';
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, 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,28 @@ 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>;
64
+ readiness(): Promise<TenantReadiness>;
60
65
  }
61
66
  export interface TenantBoundTaskAttempts {
62
67
  open(input: {
63
68
  readonly taskId: string;
64
69
  readonly deviceId: string;
70
+ readonly agentRef?: TaskAttempt['agentRef'];
65
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
+ }>;
66
80
  get(taskId: string): Promise<TaskAttempt | undefined>;
81
+ getMany(taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
67
82
  claim(input: {
68
83
  readonly taskId: string;
69
84
  readonly deviceId: string;
@@ -71,8 +86,13 @@ export interface TenantBoundTaskAttempts {
71
86
  recordStatus(input: {
72
87
  readonly taskId: string;
73
88
  readonly status: TaskAttemptStatus;
89
+ readonly agentRef?: TaskAttempt['agentRef'];
90
+ readonly terminalCause?: TaskAttempt['terminalCause'];
74
91
  }): Promise<TaskAttempt | undefined>;
75
92
  }
93
+ export interface TenantBoundTaskCancellations {
94
+ request(input: TaskCancellationRequest): Promise<TaskCancellationMutation | undefined>;
95
+ }
76
96
  export interface TenantBoundDedup {
77
97
  checkAndRecord(deviceId: string, envelopeId: string): Promise<boolean>;
78
98
  }
@@ -86,6 +106,13 @@ export interface TenantBoundReceipts {
86
106
  }>;
87
107
  get(key: string): Promise<RequestReceipt | undefined>;
88
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
+ }
89
116
  export interface TenantBoundBlobs {
90
117
  createUpload(reservation: StorageReservation): Promise<{
91
118
  readonly blobId: string;
@@ -113,8 +140,10 @@ export interface TenantStores {
113
140
  readonly approvals: TenantBoundApprovalTimeline;
114
141
  readonly devices: TenantBoundDevices;
115
142
  readonly tasks: TenantBoundTaskAttempts;
143
+ readonly cancellations: TenantBoundTaskCancellations;
116
144
  readonly dedup: TenantBoundDedup;
117
145
  readonly receipts: TenantBoundReceipts;
146
+ readonly egress: TenantBoundAgentEgress;
118
147
  readonly blobs: TenantBoundBlobs;
119
148
  readonly quota: TenantBoundQuota;
120
149
  readonly rateLimiter: TenantBoundRateLimiter;
@@ -1,4 +1,4 @@
1
- import { type BlobRef } 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[];
@@ -20,7 +22,15 @@ export interface TerminalResult {
20
22
  * `resultDocument` extractor.
21
23
  */
22
24
  readonly document?: unknown;
25
+ /**
26
+ * Device/runtime terminal observation copied from the canonical winning
27
+ * receipt. It is telemetry only — never cloud storage usage, billing, quota
28
+ * or entitlement authority.
29
+ */
30
+ readonly usage?: TerminalInferenceUsage;
23
31
  readonly reason?: string;
32
+ /** Terminal cause projection; currently the protocol's terminal reason. */
33
+ readonly terminalCause?: string;
24
34
  readonly retryable?: boolean;
25
35
  /** When the receipt store wrote the terminal fact — the first one, by its own first-write-wins rule. */
26
36
  readonly recordedAt: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/cloud",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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.5.0",
48
- "@byok-sdk/protocol": "0.5.0",
47
+ "@byok-sdk/core": "0.6.1",
48
+ "@byok-sdk/protocol": "0.6.1",
49
49
  "hono": "^4.12.30",
50
50
  "zod": "^4.4.3"
51
51
  }