@byok-sdk/cloud 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,12 +14,46 @@ export declare const ActivityAppendRequestSchema: z.ZodObject<{
14
14
  tool: z.ZodString;
15
15
  input: z.ZodOptional<z.ZodUnknown>;
16
16
  toolCallId: z.ZodOptional<z.ZodString>;
17
+ spill: z.ZodOptional<z.ZodObject<{
18
+ field: z.ZodEnum<{
19
+ input: "input";
20
+ output: "output";
21
+ }>;
22
+ totalBytes: z.ZodNumber;
23
+ omittedBytes: z.ZodNumber;
24
+ contentType: z.ZodLiteral<"application/json">;
25
+ blob: z.ZodOptional<z.ZodObject<{
26
+ blobId: z.ZodString;
27
+ contentHash: z.ZodString;
28
+ size: z.ZodNumber;
29
+ contentType: z.ZodString;
30
+ url: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strip>>;
32
+ unstoredReason: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strict>>;
17
34
  }, z.core.$strip>, z.ZodObject<{
18
35
  type: z.ZodLiteral<"tool_result">;
19
36
  tool: z.ZodString;
20
37
  output: z.ZodOptional<z.ZodUnknown>;
21
38
  toolCallId: z.ZodOptional<z.ZodString>;
22
39
  isError: z.ZodOptional<z.ZodBoolean>;
40
+ spill: z.ZodOptional<z.ZodObject<{
41
+ field: z.ZodEnum<{
42
+ input: "input";
43
+ output: "output";
44
+ }>;
45
+ totalBytes: z.ZodNumber;
46
+ omittedBytes: z.ZodNumber;
47
+ contentType: z.ZodLiteral<"application/json">;
48
+ blob: z.ZodOptional<z.ZodObject<{
49
+ blobId: z.ZodString;
50
+ contentHash: z.ZodString;
51
+ size: z.ZodNumber;
52
+ contentType: z.ZodString;
53
+ url: z.ZodOptional<z.ZodString>;
54
+ }, z.core.$strip>>;
55
+ unstoredReason: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strict>>;
23
57
  }, z.core.$strip>, z.ZodObject<{
24
58
  type: z.ZodLiteral<"artifact">;
25
59
  name: z.ZodString;
@@ -49,6 +83,7 @@ export declare const TimelineEventSchema: z.ZodObject<{
49
83
  sourceEnvelopeId: z.ZodString;
50
84
  batchSeq: z.ZodNumber;
51
85
  eventIndex: z.ZodNumber;
86
+ sourceDropped: z.ZodOptional<z.ZodNumber>;
52
87
  receivedAt: z.ZodISODateTime;
53
88
  event: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodObject<{
54
89
  type: z.ZodLiteral<"progress">;
@@ -58,12 +93,46 @@ export declare const TimelineEventSchema: z.ZodObject<{
58
93
  tool: z.ZodString;
59
94
  input: z.ZodOptional<z.ZodUnknown>;
60
95
  toolCallId: z.ZodOptional<z.ZodString>;
96
+ spill: z.ZodOptional<z.ZodObject<{
97
+ field: z.ZodEnum<{
98
+ input: "input";
99
+ output: "output";
100
+ }>;
101
+ totalBytes: z.ZodNumber;
102
+ omittedBytes: z.ZodNumber;
103
+ contentType: z.ZodLiteral<"application/json">;
104
+ blob: z.ZodOptional<z.ZodObject<{
105
+ blobId: z.ZodString;
106
+ contentHash: z.ZodString;
107
+ size: z.ZodNumber;
108
+ contentType: z.ZodString;
109
+ url: z.ZodOptional<z.ZodString>;
110
+ }, z.core.$strip>>;
111
+ unstoredReason: z.ZodOptional<z.ZodString>;
112
+ }, z.core.$strict>>;
61
113
  }, z.core.$strip>, z.ZodObject<{
62
114
  type: z.ZodLiteral<"tool_result">;
63
115
  tool: z.ZodString;
64
116
  output: z.ZodOptional<z.ZodUnknown>;
65
117
  toolCallId: z.ZodOptional<z.ZodString>;
66
118
  isError: z.ZodOptional<z.ZodBoolean>;
119
+ spill: z.ZodOptional<z.ZodObject<{
120
+ field: z.ZodEnum<{
121
+ input: "input";
122
+ output: "output";
123
+ }>;
124
+ totalBytes: z.ZodNumber;
125
+ omittedBytes: z.ZodNumber;
126
+ contentType: z.ZodLiteral<"application/json">;
127
+ blob: z.ZodOptional<z.ZodObject<{
128
+ blobId: z.ZodString;
129
+ contentHash: z.ZodString;
130
+ size: z.ZodNumber;
131
+ contentType: z.ZodString;
132
+ url: z.ZodOptional<z.ZodString>;
133
+ }, z.core.$strip>>;
134
+ unstoredReason: z.ZodOptional<z.ZodString>;
135
+ }, z.core.$strict>>;
67
136
  }, z.core.$strip>, z.ZodObject<{
68
137
  type: z.ZodLiteral<"artifact">;
69
138
  name: z.ZodString;
@@ -111,11 +180,23 @@ export interface ActivityAppendInput {
111
180
  readonly capacity?: number;
112
181
  }
113
182
  export interface ActivityStore {
183
+ /**
184
+ * `sourceEnvelopeId` is the per-envelope idempotency authority. Repeating
185
+ * its exact canonical batch returns the existing tail; changing any batch
186
+ * fact under that identity is rejected rather than appended as a second
187
+ * observation.
188
+ */
114
189
  append(tenant: TenantId, input: ActivityAppendInput): Promise<ActivityTail>;
115
190
  read(tenant: TenantId, taskId: string): Promise<ActivityTail | undefined>;
116
191
  }
117
192
  export declare function validateActivityAppend(input: ActivityAppendInput): number;
118
193
  export declare function projectTimelineEvents(input: ActivityAppendInput, receivedAt: string): readonly TimelineEvent[];
194
+ /**
195
+ * Whether one live tail already contains this source envelope's canonical
196
+ * batch. `conflict` includes a partially retained source: its missing events
197
+ * are not evidence that a different batch may reuse the same source identity.
198
+ */
199
+ export declare function activitySourceBatchState(entries: readonly TimelineEvent[], input: ActivityAppendInput): 'absent' | 'same' | 'conflict';
119
200
  export declare function parseTimelineEvents(value: unknown): readonly TimelineEvent[];
120
201
  export declare function compareTimelineEvents(left: TimelineEvent, right: TimelineEvent): number;
121
202
  export declare function activityCursor(entries: readonly TimelineEvent[]): ActivityCursor | undefined;
@@ -14,8 +14,8 @@ export interface AgentHomeProjectionReceiptInput {
14
14
  readonly agentRef: AgentHomeProjectionPayload['agentRef'];
15
15
  readonly projectionHash: AgentHomeProjectionPayload['projectionHash'];
16
16
  }
17
- export declare function agentHomeProjectionRequestKey(deviceId: string, requestId: string): string;
18
- export declare function agentHomeProjectionCompletionKey(deviceId: string, requestId: string): string;
17
+ export declare function agentHomeProjectionRequestKey(deviceId: string, agentRef: AgentHomeProjectionPayload['agentRef'], requestId: string): string;
18
+ export declare function agentHomeProjectionCompletionKey(deviceId: string, agentRef: AgentHomeProjectionPayload['agentRef'], requestId: string): string;
19
19
  export declare function sameAgentHomeProjectionRequest(expected: AgentHomeProjectionPayload, actual: AgentHomeProjectionPayload): boolean;
20
20
  export declare function receiptMatchesAgentHomeProjection(request: AgentHomeProjectionPayload, receipt: AgentHomeProjectionCompletionRequest): boolean;
21
21
  export declare function statusInputMatchesAgentHomeProjection(request: AgentHomeProjectionPayload, input: AgentHomeProjectionReceiptInput): boolean;
@@ -0,0 +1,9 @@
1
+ import { type AgentRef } from '@byok-sdk/protocol';
2
+ /**
3
+ * Exact, unambiguous storage identity for one Agent-scoped reliability fact.
4
+ *
5
+ * The device remains explicit because one Agent is placed on at most one
6
+ * device today, while one device may run many Agents. JSON supplies a stable
7
+ * length-delimited encoding without inventing a Placement authority.
8
+ */
9
+ export declare function agentReliabilityKey(domain: string, deviceId: string, agentRef: AgentRef, id: string): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Host-side approval resolution: the one rule that decides WHICH approval a
3
+ * `cloud.approveTask`/`rejectTask` call is allowed to resolve.
4
+ *
5
+ * `@byok-sdk/server` keeps this as a single mutable slot on its task record
6
+ * (`TaskSnapshot.pendingApprovalId`, `packages/server/src/hub.ts`) that
7
+ * `task.await_approval` overwrites and `task.approval_resolved` clears. Cloud
8
+ * holds no session state and no second store: the SAME two observations are
9
+ * already durable in the `ApprovalTimelineStore` tail
10
+ * (`approval-timeline.ts`), appended by the inbound gate, so the slot is
11
+ * DERIVED from that tail on each call rather than mirrored into a record that
12
+ * could then disagree with it.
13
+ *
14
+ * The derivation is a fold over the tail's entries in revision order, holding
15
+ * exactly one slot — deliberately the server's shape, not a set of concurrently
16
+ * pending approvals:
17
+ *
18
+ * - `approval_requested` SETS the slot, superseding whatever it held. A
19
+ * daemon that dispatched a fresh approval without this cloud ever seeing
20
+ * the previous one resolved has moved on, and the newest request is what
21
+ * an operator is being asked about (`hub.ts:1399`).
22
+ * - `approval_resolved(X)` CLEARS the slot when the slot's own id is `X`, or
23
+ * when the slot has no id at all (a pre-M5 daemon never reported one, so
24
+ * the resolution can only be about the single outstanding request). A
25
+ * resolution naming some OTHER id is about an approval already superseded
26
+ * and leaves the current slot standing (`hub.ts:1569`).
27
+ *
28
+ * Two ways there is legitimately nothing pending: the tail has no unresolved
29
+ * request, or the tail is gone entirely — it is a bounded, TTL'd observation
30
+ * ring, not an authority that keeps facts forever. Both answer `undefined`,
31
+ * and the callers fail closed on it. A dropped-then-expired approval is not
32
+ * silently approved.
33
+ */
34
+ import type { ApprovalTimelineTail } from './approval-timeline';
35
+ /**
36
+ * M5 (approval targeting): thrown by `ByokCloud.approveTask`/`rejectTask` when
37
+ * the caller supplied an `approvalId` that does NOT match the approval this
38
+ * task currently has pending — the caller is targeting a SPECIFIC approval the
39
+ * daemon has already superseded with a newer one.
40
+ *
41
+ * Distinct from the `task_not_awaiting_approval` `ByokCloudError` (there
42
+ * is no pending approval at all, checked FIRST so it still wins when both would
43
+ * apply): this error means the task genuinely IS awaiting an approval, just not
44
+ * the one the caller thinks it is. Thrown before any mailbox row is allocated —
45
+ * a stale-id call has zero side effects.
46
+ *
47
+ * A distinct class rather than a `CloudErrorCode`, unlike the rest of
48
+ * this package's failures, because it carries the two ids the caller needs to
49
+ * re-target: a code alone cannot say what the pending approval actually is.
50
+ */
51
+ export declare class StaleApprovalError extends Error {
52
+ readonly taskId: string;
53
+ readonly requestedApprovalId: string;
54
+ readonly currentApprovalId: string | undefined;
55
+ constructor(taskId: string, requestedApprovalId: string, currentApprovalId: string | undefined);
56
+ }
57
+ /** The one approval a task currently has outstanding. */
58
+ export interface PendingApproval {
59
+ /**
60
+ * The daemon's own id for this approval. ABSENT for a pre-M5 daemon that
61
+ * reported `task.await_approval` without one — the approval is still pending
62
+ * and still resolvable, it just cannot be targeted, so a caller-supplied
63
+ * `approvalId` proceeds untargeted exactly as it does on the server.
64
+ */
65
+ readonly approvalId?: string;
66
+ /** Immutable daemon envelope identity for this exact request observation. */
67
+ readonly sourceEnvelopeId: string;
68
+ /** Monotonic timeline revision for this exact request observation. */
69
+ readonly revision: number;
70
+ }
71
+ /**
72
+ * The task's current pending approval, or `undefined` when it has none — see
73
+ * this module's own doc comment for the fold and why it holds one slot.
74
+ */
75
+ export declare function pendingApproval(tail: ApprovalTimelineTail | undefined): PendingApproval | undefined;
@@ -3,44 +3,66 @@ import { z } from 'zod';
3
3
  export declare const DEFAULT_APPROVAL_TIMELINE_CAPACITY = 50;
4
4
  export declare const DEFAULT_APPROVAL_TIMELINE_TTL_MS: number;
5
5
  export declare const APPROVAL_SUMMARY_MAX_BYTES: number;
6
- export declare const ApprovalTimelineEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
6
+ export declare const ApprovalTimelineEventSchema: z.ZodUnion<readonly [z.ZodObject<{
7
7
  type: z.ZodLiteral<"approval_requested">;
8
8
  summary: z.ZodString;
9
9
  approvalId: z.ZodOptional<z.ZodString>;
10
- }, z.core.$strip>, z.ZodObject<{
10
+ }, z.core.$strict>, z.ZodObject<{
11
11
  type: z.ZodLiteral<"approval_resolved">;
12
- approvalId: z.ZodString;
12
+ approvalId: z.ZodOptional<z.ZodString>;
13
13
  decision: z.ZodEnum<{
14
14
  approve: "approve";
15
15
  reject: "reject";
16
16
  }>;
17
- resolvedBy: z.ZodEnum<{
18
- local: "local";
19
- }>;
17
+ resolvedBy: z.ZodLiteral<"local">;
18
+ at: z.ZodISODateTime;
19
+ }, z.core.$strict>, z.ZodObject<{
20
+ type: z.ZodLiteral<"approval_resolved">;
21
+ approvalId: z.ZodOptional<z.ZodString>;
22
+ decision: z.ZodLiteral<"approve">;
23
+ resolvedBy: z.ZodLiteral<"host">;
20
24
  at: z.ZodISODateTime;
21
- }, z.core.$strip>], "type">;
25
+ }, z.core.$strict>, z.ZodObject<{
26
+ type: z.ZodLiteral<"approval_resolved">;
27
+ approvalId: z.ZodOptional<z.ZodString>;
28
+ decision: z.ZodLiteral<"reject">;
29
+ resolvedBy: z.ZodLiteral<"host">;
30
+ reason: z.ZodNullable<z.ZodString>;
31
+ at: z.ZodISODateTime;
32
+ }, z.core.$strict>]>;
22
33
  export type ApprovalTimelineEvent = Readonly<z.infer<typeof ApprovalTimelineEventSchema>>;
23
34
  export declare const ApprovalObservationSchema: z.ZodObject<{
24
35
  taskId: z.ZodString;
25
36
  sourceEnvelopeId: z.ZodString;
26
37
  revision: z.ZodNumber;
27
38
  receivedAt: z.ZodISODateTime;
28
- event: z.ZodDiscriminatedUnion<[z.ZodObject<{
39
+ event: z.ZodUnion<readonly [z.ZodObject<{
29
40
  type: z.ZodLiteral<"approval_requested">;
30
41
  summary: z.ZodString;
31
42
  approvalId: z.ZodOptional<z.ZodString>;
32
- }, z.core.$strip>, z.ZodObject<{
43
+ }, z.core.$strict>, z.ZodObject<{
33
44
  type: z.ZodLiteral<"approval_resolved">;
34
- approvalId: z.ZodString;
45
+ approvalId: z.ZodOptional<z.ZodString>;
35
46
  decision: z.ZodEnum<{
36
47
  approve: "approve";
37
48
  reject: "reject";
38
49
  }>;
39
- resolvedBy: z.ZodEnum<{
40
- local: "local";
41
- }>;
50
+ resolvedBy: z.ZodLiteral<"local">;
51
+ at: z.ZodISODateTime;
52
+ }, z.core.$strict>, z.ZodObject<{
53
+ type: z.ZodLiteral<"approval_resolved">;
54
+ approvalId: z.ZodOptional<z.ZodString>;
55
+ decision: z.ZodLiteral<"approve">;
56
+ resolvedBy: z.ZodLiteral<"host">;
42
57
  at: z.ZodISODateTime;
43
- }, z.core.$strip>], "type">;
58
+ }, z.core.$strict>, z.ZodObject<{
59
+ type: z.ZodLiteral<"approval_resolved">;
60
+ approvalId: z.ZodOptional<z.ZodString>;
61
+ decision: z.ZodLiteral<"reject">;
62
+ resolvedBy: z.ZodLiteral<"host">;
63
+ reason: z.ZodNullable<z.ZodString>;
64
+ at: z.ZodISODateTime;
65
+ }, z.core.$strict>]>;
44
66
  }, z.core.$strip>;
45
67
  export type ApprovalObservation = Readonly<z.infer<typeof ApprovalObservationSchema>>;
46
68
  export interface ApprovalTimelineTail {
@@ -59,8 +81,31 @@ export interface ApprovalTimelineAppendInput {
59
81
  readonly ttlMs?: number;
60
82
  readonly capacity?: number;
61
83
  }
84
+ /**
85
+ * One host decision against the exact unresolved request it observed.
86
+ *
87
+ * The expected source/revision pair is the durable request identity. A
88
+ * resolver must not turn a stale read into a decision for a request which has
89
+ * since been superseded, and it must not split one logical decision into
90
+ * several mailbox controls. Implementations serialize this comparison and
91
+ * append under the timeline's per-task authority.
92
+ */
93
+ export interface ApprovalTimelineResolvePendingInput extends ApprovalTimelineAppendInput {
94
+ readonly expectedSourceEnvelopeId: string;
95
+ readonly expectedRevision: number;
96
+ }
97
+ /** Result of a conditional host-decision append. Logical conflicts are data, not transport failures. */
98
+ export type ApprovalTimelineResolvePendingResult = {
99
+ readonly status: 'applied' | 'replayed';
100
+ readonly tail: ApprovalTimelineTail;
101
+ } | {
102
+ readonly status: 'conflict' | 'superseded' | 'absent';
103
+ readonly tail?: ApprovalTimelineTail;
104
+ };
62
105
  export interface ApprovalTimelineStore {
63
106
  append(tenant: TenantId, input: ApprovalTimelineAppendInput): Promise<ApprovalTimelineTail>;
107
+ /** Atomically append one host resolution only if its request remains current. */
108
+ resolvePending(tenant: TenantId, input: ApprovalTimelineResolvePendingInput): Promise<ApprovalTimelineResolvePendingResult>;
64
109
  read(tenant: TenantId, taskId: string): Promise<ApprovalTimelineTail | undefined>;
65
110
  }
66
111
  export interface ValidatedApprovalTimelineAppend {
@@ -69,6 +114,7 @@ export interface ValidatedApprovalTimelineAppend {
69
114
  readonly event: ApprovalTimelineEvent;
70
115
  }
71
116
  export declare function validateApprovalTimelineAppend(input: ApprovalTimelineAppendInput): ValidatedApprovalTimelineAppend;
117
+ export declare function validateApprovalTimelineResolve(input: ApprovalTimelineResolvePendingInput): ValidatedApprovalTimelineAppend;
72
118
  export declare function approvalTimelineKey(tenant: TenantId, taskId: string): string;
73
119
  export declare function parseApprovalObservations(value: unknown): readonly ApprovalObservation[];
74
120
  export declare function approvalTimelineCursor(entries: readonly ApprovalObservation[]): number | undefined;
@@ -20,6 +20,23 @@ import type { TokenSigner } from './tokens';
20
20
  export interface BearerAuthDeps {
21
21
  readonly tokenSigner: TokenSigner;
22
22
  readonly devices: DeviceDirectory;
23
+ /**
24
+ * The ONE product this deployment serves, when it serves exactly one.
25
+ *
26
+ * Two deployment shapes, two explicit authorities — not a default and a
27
+ * fallback. A multi-product hosted control plane leaves this absent, and the
28
+ * row is the whole product authority (`device.productId === claims.productId`
29
+ * below). A single-product instance supplies it — that is the embedded shape,
30
+ * where `createByokServer({ productId })` always has one — and the token must
31
+ * then name THAT product as well, so a device paired through a code minted
32
+ * for another product cannot reach this instance's bearer-authed routes at
33
+ * all.
34
+ *
35
+ * Absent it, a cross-product device whose token and row agree with each other
36
+ * authenticates on every `device` route the deployment mounts, which is
37
+ * precisely the drift this closes for the embedded shape.
38
+ */
39
+ readonly instanceProductId?: string;
23
40
  }
24
41
  export declare function extractBearerToken(header: string | undefined): string | undefined;
25
42
  export declare function authenticateBearer(header: string | undefined, deps: BearerAuthDeps): Promise<DevicePrincipal | undefined>;
package/dist/cloud.d.ts CHANGED
@@ -17,13 +17,14 @@
17
17
  import { type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type CapabilityDeclaration, type Clock, type CoreStores, type PresenceHint, type SkillPackStore, type TenantId, type TenantReadiness } from '@byok-sdk/core';
18
18
  import type { ActivityTail } from './activity';
19
19
  import type { ApprovalTimelineTail } from './approval-timeline';
20
- import { type Envelope, type AgentContentReadPayload, type AgentMessagePublishPayload, type AgentMessageServerContext, type AgentHomeProjectionCompletionRequest, type AgentHomeProjectionPayload, type AgentHomeProjectionReadback, type AgentMemoryProjectionEraseResult, type TaskOfferPayload, type TaskOfferForAgentPayload, type TaskOfferForAgentWithEgressPayload, type TaskOfferForAgentWithEgressFreshPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
20
+ import { type Envelope, type AgentRef, type AgentContentReadPayload, type AgentMessagePublishPayload, type AgentMessageServerContext, type AgentHomeProjectionCompletionRequest, type AgentHomeProjectionPayload, type AgentHomeProjectionReadback, type AgentMemoryProjectionEraseResult, type TaskOfferPayload, type TaskSteerPayload, type TaskOfferForAgentPayload, type TaskOfferForAgentWithEgressPayload, type TaskOfferForAgentWithEgressFreshPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
21
21
  import type { TokenSigner } from './auth/tokens';
22
22
  import type { CloudCrypto } from './crypto/port';
23
23
  import { type RouteDescriptor } from './router/registry';
24
+ import { type ByokCloudObserver } from './inbound';
24
25
  import { type AgentHomeProjectionReceiptInput } from './agent-home-projections';
25
26
  import type { AgentMemoryProjectionAuthorizer, AgentMemoryProjectionStore } from './agent-memory-projection';
26
- import type { BlobContentProxy, CloudStores, DeviceRecord, AgentEgressRecord, PairingCodeInfo, RequestReceipt, TaskAttempt } from './stores/ports';
27
+ import type { BlobContentProxy, CloudStores, DeviceRecord, AgentEgressRecord, PairingCodeInfo, RequestReceipt, TaskAttempt, TaskAttemptListQuery, TaskAttemptPage } from './stores/ports';
27
28
  import { type TerminalResult } from './terminal-result';
28
29
  import type { TruthCommitter, TruthObjectDownloads } from './truth/contract';
29
30
  /** Matches the reference server's ceiling (§7). */
@@ -67,7 +68,24 @@ export interface ByokCloudOptions {
67
68
  readonly capabilities: CapabilityDeclaration;
68
69
  /** Recorded on the control-plane principal every host-side call is made under. */
69
70
  readonly operatorId?: string;
70
- /** Product-owned consumer; destination lookup is keyed by authenticated task context, never model input. */
71
+ /**
72
+ * The ONE product this deployment serves, when it serves exactly one — the
73
+ * embedded shape. Supplying it makes every bearer-authed route additionally
74
+ * require the token to name this product; omitting it leaves the device row
75
+ * as the whole product authority. Two deployment shapes, two explicit
76
+ * authorities: see `BearerAuthDeps.instanceProductId` (`auth/bearer.ts`).
77
+ */
78
+ readonly instanceProductId?: string;
79
+ /**
80
+ * Product-owned consumer; destination lookup uses authenticated task context.
81
+ * Delivery is at least once, including concurrent calls and retries after a
82
+ * process restart or an uncertain commit. The product must durably deduplicate
83
+ * the exact tenant/device/task/AgentRef/message identity and payload, producing
84
+ * one logical effect and the same outcome/reasonCode on every exact replay.
85
+ * A thrown error leaves admission pending for transport retry; only a returned
86
+ * outcome is a terminal product decision. Do not use callback invocation count
87
+ * as execution authority.
88
+ */
71
89
  readonly agentMessage?: {
72
90
  consume(input: {
73
91
  readonly tenant: TenantId;
@@ -80,6 +98,14 @@ export interface ByokCloudOptions {
80
98
  readonly reasonCode?: string;
81
99
  }>;
82
100
  };
101
+ /**
102
+ * Post-commit relay for envelopes this cloud durably accepted. Read-only by
103
+ * construction: it is told after the write, it returns `void`, and a throw
104
+ * from it is swallowed — the outcome is already fixed. Distinct from
105
+ * {@link ByokCloudOptions.agentMessage}, which is admission and runs BEFORE
106
+ * a write, deciding whether it happens at all.
107
+ */
108
+ readonly observer?: ByokCloudObserver;
83
109
  /** Embedder-owned grant and consent authority for optional hosted Agent-memory projection. */
84
110
  readonly agentMemoryProjectionAuthorizer?: AgentMemoryProjectionAuthorizer;
85
111
  /** Durable snapshot + immutable metering receipt authority for optional hosted Agent-memory projection. */
@@ -152,6 +178,20 @@ export interface AgentContentReadInput {
152
178
  export type AgentHomeProjectionInput = AgentHomeProjectionPayload;
153
179
  /** Exact request identity a host must echo to read back durable projection status. */
154
180
  export type AgentHomeProjectionStatusInput = AgentHomeProjectionReceiptInput;
181
+ /** Optional targeting for {@link ByokCloud.approveTask}. */
182
+ export interface ApproveTaskOptions {
183
+ /**
184
+ * Resolve THIS specific approval rather than whichever one is pending.
185
+ * A mismatch against the task's current pending approval throws
186
+ * {@link StaleApprovalError} instead of resolving the wrong thing.
187
+ */
188
+ readonly approvalId?: string;
189
+ }
190
+ /** Optional targeting and refusal cause for {@link ByokCloud.rejectTask}. */
191
+ export interface RejectTaskOptions extends ApproveTaskOptions {
192
+ /** Carried to the runtime verbatim; absent stays absent on the wire. */
193
+ readonly reason?: string;
194
+ }
155
195
  export interface EnqueuedAgentControl {
156
196
  readonly seq: number;
157
197
  readonly envelope: Envelope;
@@ -223,11 +263,82 @@ export interface ByokCloud {
223
263
  eraseAgentMemoryProjection(tenant: TenantId, agentId: string): Promise<AgentMemoryProjectionEraseResult>;
224
264
  /** Host control plane: durably request cancellation by tenant/task id. Idempotent. */
225
265
  cancelTask(tenant: TenantId, taskId: string, reason?: string): Promise<TaskAttempt>;
266
+ /**
267
+ * Host control plane: resolve the approval a paused task is waiting on, by
268
+ * enqueueing `task.approve` to the device that claimed it.
269
+ *
270
+ * WHICH approval is a derived fact, not a stored one — see
271
+ * `approval-control.ts` for the fold over the durable timeline that produces
272
+ * it, and why cloud adds no second authority for it. Gate order mirrors the
273
+ * reference server (`packages/server/src/hub.ts`), and every gate is
274
+ * evaluated in full before a mailbox row is allocated, so a refused call has
275
+ * zero side effects:
276
+ *
277
+ * 1. no such task for this tenant -> `task_not_found`;
278
+ * 2. nothing to resolve -> `task_not_awaiting_approval`, which covers a
279
+ * terminal attempt, an unclaimed one, and a timeline with no unresolved
280
+ * request (expired or never written). Checked BEFORE targeting, so it
281
+ * still wins when both would apply;
282
+ * 3. `opts.approvalId` names an approval this task has already superseded
283
+ * -> {@link StaleApprovalError}. When the daemon never reported an id
284
+ * at all, the call proceeds untargeted, exactly as on the server.
285
+ *
286
+ * Delivery is a best-effort notification, the same contract the wire message
287
+ * has always had (`TaskApprovePayloadSchema`, `@byok-sdk/protocol`): the
288
+ * enqueued envelope is durable, the runtime's response to it is observable
289
+ * only through that task's own later messages. The outgoing `approvalId` is
290
+ * the caller's when given, else the pending one, else omitted.
291
+ */
292
+ approveTask(tenant: TenantId, taskId: string, opts?: ApproveTaskOptions): Promise<EnqueuedAgentControl>;
293
+ /**
294
+ * Host control plane: refuse the approval a paused task is waiting on, by
295
+ * enqueueing `task.reject` to the device that claimed it. Same gates, same
296
+ * targeting, and the same best-effort delivery contract as
297
+ * {@link ByokCloud.approveTask} — `opts.reason` is carried to the runtime
298
+ * verbatim and is never synthesized when absent.
299
+ */
300
+ rejectTask(tenant: TenantId, taskId: string, opts?: RejectTaskOptions): Promise<EnqueuedAgentControl>;
301
+ /**
302
+ * Host control plane: inject steering text into a RUNNING task, by enqueueing
303
+ * `task.steer` to the device that claimed it.
304
+ *
305
+ * Gated on the claim-time capability snapshot and on nothing else — see
306
+ * `steer-control.ts` for why that is the only admissible input and why an
307
+ * absent snapshot refuses. Gate order mirrors the reference server
308
+ * (`ConnectionHub.steerTask`, `packages/server/src/hub.ts`), and every gate is
309
+ * evaluated in full before a mailbox row is allocated, so a refused call has
310
+ * zero side effects:
311
+ *
312
+ * 1. no such task for this tenant -> `task_not_found` ({@link ByokCloudError});
313
+ * 2. terminal attempt -> {@link SteerRejectedError} `task_terminal`,
314
+ * checked BEFORE the running check so a steer racing a terminal
315
+ * transition always resolves terminal-first;
316
+ * 3. not running (or running with no owning device) ->
317
+ * {@link SteerRejectedError} `task_not_running`;
318
+ * 4. the claim snapshot does not positively say `steer: true` ->
319
+ * {@link SteerRejectedError} `steer_unsupported_runtime`, including when
320
+ * there is no snapshot at all.
321
+ *
322
+ * Delivery is the same best-effort notification every control message has:
323
+ * the enqueued envelope is durable, and whether the runtime acted on it is
324
+ * observable only through that task's own later messages.
325
+ */
326
+ steerTask(tenant: TenantId, taskId: string, payload: TaskSteerPayload): Promise<EnqueuedAgentControl>;
226
327
  readTaskAttempt(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
328
+ /**
329
+ * Host control plane: one bounded page of this tenant's task attempts,
330
+ * keyset-paged by `taskId` — see {@link TaskAttemptListQuery} for why the key
331
+ * is the task id and not a timestamp, and why the walk terminates on an
332
+ * absent `nextCursor` rather than on an empty page.
333
+ *
334
+ * A non-positive or non-integer `limit` is rejected
335
+ * (`coordination_input_invalid`), never defaulted: the host names the bound.
336
+ */
337
+ listTaskAttempts(tenant: TenantId, query: TaskAttemptListQuery): Promise<TaskAttemptPage>;
227
338
  /** The recorded terminal for a task — the first one, re-encoded canonically under the frozen v1 codec (see `recordTerminal`, `inbound.ts`: the stored body is `encodeEnvelope` of the zod-parsed envelope, not the device's original byte sequence). */
228
339
  readTerminalReceipt(tenant: TenantId, taskId: string): Promise<RequestReceipt | undefined>;
229
- /** Exact durable egress fact and receipt selected by (tenant, device, event id). */
230
- readAgentEgress(tenant: TenantId, deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
340
+ /** Exact durable egress fact and receipt selected by (tenant, device, AgentRef, event id). */
341
+ readAgentEgress(tenant: TenantId, deviceId: string, agentRef: AgentRef, eventId: string): Promise<AgentEgressRecord | undefined>;
231
342
  /**
232
343
  * Host control plane: the same first terminal, decoded into the typed read
233
344
  * model ({@link TerminalResult}) so a host reads result fields, not envelope
@@ -20,6 +20,8 @@ export interface InMemoryByokCloudOptions {
20
20
  readonly tokenSigner?: TokenSigner;
21
21
  readonly capabilities?: CapabilityDeclaration;
22
22
  readonly operatorId?: string;
23
+ /** Single-product instance authority; absent leaves the device row as the whole product authority. */
24
+ readonly instanceProductId?: string;
23
25
  readonly maxBlobSizeBytes?: number;
24
26
  readonly longPollHoldMs?: number;
25
27
  readonly longPollIntervalMs?: number;
@@ -51,6 +53,8 @@ export interface InMemoryByokCloudOptions {
51
53
  readonly skillPacks?: SkillPackStore;
52
54
  readonly skillPackPageLimit?: number;
53
55
  readonly agentMessage?: ByokCloudOptions['agentMessage'];
56
+ /** Post-commit relay; absent means nothing is watching. */
57
+ readonly observer?: ByokCloudOptions['observer'];
54
58
  readonly agentMemoryProjectionAuthorizer?: AgentMemoryProjectionAuthorizer;
55
59
  readonly agentMemoryProjectionStore?: AgentMemoryProjectionStore;
56
60
  }
@@ -21,6 +21,14 @@ export declare function assertBoardLabels(channel: string, title: string, limits
21
21
  readonly channelMaxBytes: number;
22
22
  readonly titleMaxBytes: number;
23
23
  }): void;
24
+ /**
25
+ * The page bound on {@link TaskAttemptStore.list}. Fail-closed rather than
26
+ * defaulted or clamped: a caller that asks for `0`, `-1`, `2.5`, or `Infinity`
27
+ * has a bug, and answering it with a silently substituted limit hides that bug
28
+ * behind a plausible page. Enforced in the STORE rather than only at the
29
+ * façade, because the port is reachable directly by a host composition.
30
+ */
31
+ export declare function assertTaskAttemptListLimit(limit: number): void;
24
32
  export declare function validateActivityEvents(events: readonly AgentEventOrUnknown[], dropped: number, bounds: ActivityBounds): void;
25
33
  export declare function validateActivityBatch(input: Omit<ActivityAppendInput, 'ttlMs' | 'capacity'>, bounds: ActivityBounds): void;
26
34
  export declare function appendActivityEvents(activity: TenantBoundActivity, input: {
package/dist/errors.d.ts CHANGED
@@ -43,6 +43,15 @@ export declare const CLOUD_ERROR_CODES: {
43
43
  readonly activity_batch_too_large: 'activity_batch_too_large';
44
44
  /** Host control-plane task lookup is tenant-closed and found no task. */
45
45
  readonly task_not_found: 'task_not_found';
46
+ /**
47
+ * A host `approveTask`/`rejectTask` names a task with no approval to
48
+ * resolve: the durable timeline holds no unresolved `approval_requested`
49
+ * (including a tail that has expired or was never written), the attempt has
50
+ * already reached a terminal status, or no device ever claimed it so there
51
+ * is nothing to notify. Fail-closed by construction — cloud never invents a
52
+ * pending approval to make the call succeed.
53
+ */
54
+ readonly task_not_awaiting_approval: 'task_not_awaiting_approval';
46
55
  /** Durable target-device capability is absent, revoked, or unavailable. */
47
56
  readonly agent_capability_missing: 'agent_capability_missing';
48
57
  /** Inbound Agent identity did not exactly match the offered identity. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `GET /byok/events?cursor=N` — the long-poll receive half (§8).
3
3
  *
4
- * Three properties the daemon's transport depends on, reproduced against the
4
+ * Four properties the daemon's transport depends on, reproduced against the
5
5
  * core mailbox rather than an in-process outbox:
6
6
  *
7
7
  * - **Reading is not acknowledging.** `readAfter` never moves the cursor. The
@@ -19,6 +19,12 @@
19
19
  * registered waiter: a waiter map is exactly the cross-request state a
20
20
  * stateless handler may not keep (S3.5 boxes 14-15), and a second cloud
21
21
  * instance would not see it anyway.
22
+ * - **A lost cursor fails closed.** A cursor below the mailbox's retained
23
+ * window is answered `409 { error: 'cursor_too_old', recoverableFrom }`, the
24
+ * same body the reference server returns (`packages/server/src/http.ts:386`)
25
+ * and the only one the daemon's transport can turn into a resync. Serving
26
+ * such a caller a 200 would hand it a partial tail it has no way to tell
27
+ * apart from a complete one.
22
28
  */
23
29
  import type { Context } from 'hono';
24
30
  import { type DeviceRouteDeps } from './shared';
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import type { Context } from 'hono';
23
23
  import { type AgentContentReceiptPayload, type AgentMessageDispositionPayload, type AgentMessagePublishPayload } from '@byok-sdk/protocol';
24
+ import { type ByokCloudObserver } from '../inbound';
24
25
  import type { ActivityBounds } from '../coordination';
25
26
  import type { AgentEgressRecord } from '../stores/ports';
26
27
  import type { TenantStores } from '../tenant-stores';
@@ -48,5 +49,11 @@ export interface MessagesRouteDeps extends DeviceRouteDeps {
48
49
  }>;
49
50
  };
50
51
  readonly appendAgentMessageDisposition: (stores: TenantStores, deviceId: string, taskId: string, payload: AgentMessageDispositionPayload) => Promise<void>;
52
+ /**
53
+ * Post-commit relay. Batch order IS notification order: the loop below
54
+ * awaits one envelope's gate before starting the next, so an observer sees
55
+ * committed envelopes in the order the device sent them.
56
+ */
57
+ readonly observer?: ByokCloudObserver;
51
58
  }
52
59
  export declare function messagesHandler(deps: MessagesRouteDeps): (c: Context) => Promise<Response>;