@byok-sdk/cloud 0.17.0 → 0.19.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { BOARD_STATUSES, CapabilityDeclarationSchema, NONCE_SIGNING_DOMAIN, hasCapability, isTenantId, tenantId, principalTenant, parseDeviceProofEnvelope, deviceProofSigningInput, contentHash, parseCapabilityDeclaration, ByokCoreError, tenantKey, PRESENCE_LEVELS, createInMemoryCoreStores, authenticateDeviceAssertion, assertCapability, isCoreConflictError, isCoreError, DEVICE_PROOF_HEADER, TRUTH_RECORD_KINDS, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS } from '@byok-sdk/core';
1
+ import { BOARD_STATUSES, CapabilityDeclarationSchema, NONCE_SIGNING_DOMAIN, hasCapability, isTenantId, tenantId, principalTenant, parseDeviceProofEnvelope, deviceProofSigningInput, contentHash, parseCapabilityDeclaration, ByokCoreError, tenantKey, PRESENCE_LEVELS, createInMemoryCoreStores, authenticateDeviceAssertion, authenticateTaskAssertion, assertCapability, isCoreConflictError, isCoreError, DEVICE_PROOF_HEADER, TRUTH_RECORD_KINDS, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS } from '@byok-sdk/core';
2
2
  export { DEVICE_PROOF_HEADER, NONCE_SIGNING_DOMAIN, isTenantId, tenantId } from '@byok-sdk/core';
3
- import { AgentEventOrUnknownSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, AgentHomeProjectionCompletionRequestSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_AGENT_MEMORY_PROJECTIONS_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, isTaskOfferType, MESSAGE_PAYLOAD_SCHEMAS, createEnvelope, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, TaskOfferForAgentWithEgressFreshPayloadSchema, AgentMessageServerContextSchema, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AgentMemoryProjectionMutationSchema, AgentMemoryProjectionReceiptSchema, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionEraseResultSchema, HarnessIdSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AgentRefSchema, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MAILBOX_READ_AHEAD_MAX_SEQS, MAILBOX_READ_AHEAD_CAPABILITY, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
4
3
  import { z } from 'zod';
4
+ import { AgentEventOrUnknownSchema, AgentMessageServerContextSchema, AgentMessageEgressRequirementSchema, AgentRefSchema, TaskOfferForAgentWithEgressFreshPayloadSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, AgentHomeProjectionCompletionRequestSchema, AgentInputPreparationPayloadSchema, InputPreparationCompletionRequestSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_AGENT_MEMORY_PROJECTIONS_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, BYOK_INPUT_PREPARATION_COMPLETION_ROUTE, BYOK_INPUT_PREPARATION_STATUS_ROUTE, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, AgentMessagePublishPayloadSchema, isTaskOfferType, MESSAGE_PAYLOAD_SCHEMAS, createEnvelope, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, TaskOfferForAgentWithEgressPayloadSchema, AGENT_INPUT_PREPARATION_CAPABILITY, TaskOfferPreparedPayloadSchema, TaskOfferForAgentPayloadSchema, AgentMemoryProjectionMutationSchema, AgentMemoryProjectionReceiptSchema, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionEraseResultSchema, HarnessIdSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MAILBOX_READ_AHEAD_MAX_SEQS, MAILBOX_READ_AHEAD_CAPABILITY, MessagesSendRequestSchema, InputPreparationStatusQuerySchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentMessageDispositionPayloadSchema, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
5
5
  import { Hono } from 'hono';
6
6
 
7
7
  // src/index.ts
@@ -25,1682 +25,1778 @@ async function hashOfferBytes(bytes) {
25
25
  return `sha256:${Array.from(hash, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
26
26
  }
27
27
 
28
- // src/approval-control.ts
29
- var StaleApprovalError = class extends Error {
30
- constructor(taskId, requestedApprovalId, currentApprovalId) {
31
- super(
32
- `cannot resolve approval ${requestedApprovalId} for task ${taskId}: the currently pending approval is ${currentApprovalId ?? "(none recorded)"}`
33
- );
34
- this.taskId = taskId;
35
- this.requestedApprovalId = requestedApprovalId;
36
- this.currentApprovalId = currentApprovalId;
37
- this.name = "StaleApprovalError";
38
- }
39
- taskId;
40
- requestedApprovalId;
41
- currentApprovalId;
28
+ // src/errors.ts
29
+ var CLOUD_ERROR_CODES = {
30
+ /** A pairing code that is unknown, expired, or already redeemed (§6.1). */
31
+ pairing_code_invalid: "pairing_code_invalid",
32
+ /** The composition handed a device row whose tenant is not a mintable `TenantId`. */
33
+ device_tenant_invalid: "device_tenant_invalid",
34
+ /**
35
+ * The mailbox committed a row `seq` that disagrees with the delivery `seq`
36
+ * its body factory baked into the envelope. Loud rather than silent: those
37
+ * two numbers ARE the daemon's redelivery cursor.
38
+ */
39
+ mailbox_seq_mismatch: "mailbox_seq_mismatch",
40
+ /** A capability declaration the host supplied that core refused. */
41
+ capability_declaration_invalid: "capability_declaration_invalid",
42
+ /**
43
+ * The declaration names a capability this composition cannot serve, so the
44
+ * deployment would publish a surface it does not have (ADR-010).
45
+ *
46
+ * Construction-time and fatal. A client learns what a deployment supports by
47
+ * READING the declaration and is entitled to act on it without probing, so a
48
+ * declaration that over-states is not a degraded deployment — it is a
49
+ * deployment whose one honest interface lies.
50
+ */
51
+ capability_over_declared: "capability_over_declared",
52
+ /** Host-supplied board labels or coordination input exceeded the explicit contract. */
53
+ coordination_input_invalid: "coordination_input_invalid",
54
+ /**
55
+ * A terminal receipt whose stored body is not a terminal envelope — either
56
+ * undecodable or a non-terminal type. Whatever wrote that row broke the
57
+ * receipt-store contract, so the typed read model fails closed instead of
58
+ * projecting a best-effort shape.
59
+ */
60
+ terminal_receipt_unreadable: "terminal_receipt_unreadable",
61
+ /** A progress/activity batch exceeded the configured event or byte ceiling. */
62
+ activity_batch_too_large: "activity_batch_too_large",
63
+ /** Host control-plane task lookup is tenant-closed and found no task. */
64
+ task_not_found: "task_not_found",
65
+ /**
66
+ * A host `approveTask`/`rejectTask` names a task with no approval to
67
+ * resolve: the durable timeline holds no unresolved `approval_requested`
68
+ * (including a tail that has expired or was never written), the attempt has
69
+ * already reached a terminal status, or no device ever claimed it so there
70
+ * is nothing to notify. Fail-closed by construction — cloud never invents a
71
+ * pending approval to make the call succeed.
72
+ */
73
+ task_not_awaiting_approval: "task_not_awaiting_approval",
74
+ /** Durable target-device capability is absent, revoked, or unavailable. */
75
+ agent_capability_missing: "agent_capability_missing",
76
+ /** Inbound Agent identity did not exactly match the offered identity. */
77
+ agent_ref_mismatch: "agent_ref_mismatch",
78
+ /** A strict Agent task id already names a durable attempt and cannot be re-enqueued. */
79
+ agent_task_already_exists: "agent_task_already_exists",
80
+ /** A first-write-wins Agent control record was replayed with a different body. */
81
+ agent_content_request_mismatch: "agent_content_request_mismatch",
82
+ /** A durable mailbox receipt id resolved to an envelope other than its exact acknowledgement. */
83
+ mailbox_receipt_mismatch: "mailbox_receipt_mismatch",
84
+ /** A task-free Agent-home request id already names a different immutable desired projection. */
85
+ agent_home_projection_request_conflict: "agent_home_projection_request_conflict",
86
+ /** A direct Agent-home completion did not identify a stored desired request for this exact device. */
87
+ agent_home_projection_request_not_found: "agent_home_projection_request_not_found",
88
+ /** A direct Agent-home completion changed the first durable terminal outcome. */
89
+ agent_home_projection_completion_conflict: "agent_home_projection_completion_conflict",
90
+ /** A task-free completion did not exactly echo its immutable desired projection binding. */
91
+ agent_home_projection_receipt_mismatch: "agent_home_projection_receipt_mismatch",
92
+ /** A receipt-store row at the projection namespace violated the frozen projection schema. */
93
+ agent_home_projection_receipt_invalid: "agent_home_projection_receipt_invalid",
94
+ /** A remote input-preparation request id already names a different immutable desired request. */
95
+ input_preparation_request_conflict: "input_preparation_request_conflict",
96
+ /** A device completion did not identify a stored desired request for this exact device. */
97
+ input_preparation_request_not_found: "input_preparation_request_not_found",
98
+ /** A device completion changed the first durable terminal outcome. */
99
+ input_preparation_completion_conflict: "input_preparation_completion_conflict",
100
+ /** A completion did not exactly echo its immutable desired request/Agent/policy binding. */
101
+ input_preparation_receipt_mismatch: "input_preparation_receipt_mismatch",
102
+ /** A receipt-store row at the preparation namespace violated the frozen schema. */
103
+ input_preparation_receipt_invalid: "input_preparation_receipt_invalid",
104
+ /** A hosted Agent-memory mutation did not match the durable task/device/AgentRef binding. */
105
+ agent_memory_projection_task_mismatch: "agent_memory_projection_task_mismatch",
106
+ /** The embedder-owned grant/consent authority denied a hosted memory mutation. */
107
+ agent_memory_projection_authorization_denied: "agent_memory_projection_authorization_denied",
108
+ /** A redacted snapshot's decoded bytes did not match its declared hash or byte count. */
109
+ agent_memory_projection_hash_mismatch: "agent_memory_projection_hash_mismatch",
110
+ /** A writer epoch/source sequence already names a different immutable projection mutation. */
111
+ agent_memory_projection_replay_mismatch: "agent_memory_projection_replay_mismatch",
112
+ /** A hosted memory mutation came from an older writer epoch. */
113
+ agent_memory_projection_stale_epoch: "agent_memory_projection_stale_epoch",
114
+ /** A server-side erase fence prevents a deleted writer epoch from re-entering. */
115
+ agent_memory_projection_erased_epoch: "agent_memory_projection_erased_epoch",
116
+ /** A prior erase reached the protocol writer-epoch ceiling and cannot mint a later writer. */
117
+ agent_memory_projection_epoch_exhausted: "agent_memory_projection_epoch_exhausted",
118
+ /** A hosted memory mutation skipped or reset a required source sequence. */
119
+ agent_memory_projection_sequence_gap: "agent_memory_projection_sequence_gap"
42
120
  };
43
- function pendingApproval(tail) {
44
- if (tail === void 0) return void 0;
45
- let pending;
46
- for (const entry of tail.entries) {
47
- const event = entry.event;
48
- if (event.type === "approval_requested") {
49
- pending = {
50
- sourceEnvelopeId: entry.sourceEnvelopeId,
51
- revision: entry.revision,
52
- ...event.approvalId === void 0 ? {} : { approvalId: event.approvalId }
53
- };
54
- continue;
55
- }
56
- if (pending === void 0) continue;
57
- if (pending.approvalId === void 0 || pending.approvalId === event.approvalId) {
58
- pending = void 0;
59
- }
121
+ var ByokCloudError = class extends Error {
122
+ code;
123
+ constructor(code, message, options) {
124
+ super(message, options);
125
+ this.name = "ByokCloudError";
126
+ this.code = code;
60
127
  }
61
- return pending;
128
+ };
129
+ function isCloudError(value, code) {
130
+ if (!(value instanceof ByokCloudError)) return false;
131
+ return code === void 0 || value.code === code;
62
132
  }
63
133
 
64
- // src/steer-control.ts
65
- function steerRejectionMessage(taskId, code, status, runtime) {
66
- switch (code) {
67
- case "task_terminal":
68
- return `cannot steer task ${taskId}: task is already terminal (status ${status})`;
69
- case "task_not_running":
70
- return `cannot steer task ${taskId}: not running (status ${status})`;
71
- case "steer_unsupported_runtime":
72
- return `cannot steer task ${taskId}: claimed runtime ${runtime ?? "(unknown)"} does not support steering`;
134
+ // src/activity.ts
135
+ var DEFAULT_ACTIVITY_CAPACITY = 50;
136
+ var ActivityTaskIdSchema = z.string().min(1).max(200);
137
+ var SourceEnvelopeIdSchema = z.string().max(200).regex(/\S/, "sourceEnvelopeId must not be blank");
138
+ var ActivityAppendRequestSchema = z.object({
139
+ taskId: ActivityTaskIdSchema,
140
+ sourceEnvelopeId: SourceEnvelopeIdSchema,
141
+ batchSeq: z.number().int().nonnegative(),
142
+ events: z.array(AgentEventOrUnknownSchema).min(1),
143
+ dropped: z.number().int().nonnegative()
144
+ });
145
+ var TimelineEventSchema = z.object({
146
+ taskId: ActivityTaskIdSchema,
147
+ sourceEnvelopeId: SourceEnvelopeIdSchema,
148
+ batchSeq: z.number().int().nonnegative(),
149
+ eventIndex: z.number().int().nonnegative(),
150
+ /**
151
+ * The producer-declared loss attached to this source batch. It travels with
152
+ * every retained event so a replay includes it in the canonical batch
153
+ * binding rather than treating the aggregate as an incidental counter.
154
+ */
155
+ sourceDropped: z.number().int().nonnegative().optional(),
156
+ receivedAt: z.iso.datetime(),
157
+ event: AgentEventOrUnknownSchema
158
+ });
159
+ function validateActivityAppend(input) {
160
+ if (!Number.isFinite(input.ttlMs) || input.ttlMs <= 0) {
161
+ throw new ByokCloudError(
162
+ "coordination_input_invalid",
163
+ `Activity ttl must be a positive number of milliseconds, received ${String(input.ttlMs)}.`
164
+ );
73
165
  }
74
- }
75
- var SteerRejectedError = class extends Error {
76
- constructor(taskId, code, status, runtime) {
77
- super(steerRejectionMessage(taskId, code, status, runtime));
78
- this.taskId = taskId;
79
- this.code = code;
80
- this.status = status;
81
- this.runtime = runtime;
82
- this.name = "SteerRejectedError";
166
+ const capacity = input.capacity ?? DEFAULT_ACTIVITY_CAPACITY;
167
+ if (!Number.isSafeInteger(capacity) || capacity <= 0) {
168
+ throw new ByokCloudError(
169
+ "coordination_input_invalid",
170
+ `Activity capacity must be a positive integer, received ${String(capacity)}.`
171
+ );
83
172
  }
84
- taskId;
85
- code;
86
- status;
87
- runtime;
88
- };
89
-
90
- // src/crypto/web-crypto.ts
91
- var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
92
- var HEX_DIGITS = "0123456789abcdef";
93
- function base64UrlEncode(bytes) {
94
- let binary = "";
95
- for (const byte of bytes) binary += String.fromCharCode(byte);
96
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
97
- }
98
- function base64UrlDecode(value) {
99
- const padded = value.replaceAll("-", "+").replaceAll("_", "/");
100
- try {
101
- const binary = atob(padded);
102
- const bytes = new Uint8Array(binary.length);
103
- for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
104
- return bytes;
105
- } catch {
106
- return void 0;
173
+ const parsed = ActivityAppendRequestSchema.safeParse(input);
174
+ if (!parsed.success) {
175
+ throw new ByokCloudError(
176
+ "coordination_input_invalid",
177
+ "Activity batches require stable source identity, order, at least one valid event, and a non-negative dropped count."
178
+ );
107
179
  }
180
+ return capacity;
108
181
  }
109
- function toHex(bytes) {
110
- let out = "";
111
- for (const byte of bytes) {
112
- out += HEX_DIGITS[byte >> 4 & 15];
113
- out += HEX_DIGITS[byte & 15];
114
- }
115
- return out;
182
+ function projectTimelineEvents(input, receivedAt) {
183
+ return input.events.map(
184
+ (event, eventIndex) => TimelineEventSchema.parse({
185
+ taskId: input.taskId,
186
+ sourceEnvelopeId: input.sourceEnvelopeId,
187
+ batchSeq: input.batchSeq,
188
+ eventIndex,
189
+ sourceDropped: input.dropped,
190
+ receivedAt,
191
+ event
192
+ })
193
+ );
116
194
  }
117
- function subtle() {
118
- const webCrypto = globalThis.crypto;
119
- if (webCrypto?.subtle === void 0) {
120
- throw new Error("@byok-sdk/cloud requires a WebCrypto implementation on globalThis.crypto");
195
+ function activitySourceBatchState(entries, input) {
196
+ const existing = entries.filter((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId).sort((left, right) => left.eventIndex - right.eventIndex);
197
+ if (existing.length === 0) return "absent";
198
+ if (existing.length !== input.events.length) return "conflict";
199
+ for (const [eventIndex, event] of input.events.entries()) {
200
+ const stored = existing[eventIndex];
201
+ if (stored === void 0 || stored.taskId !== input.taskId || stored.batchSeq !== input.batchSeq || stored.eventIndex !== eventIndex || stored.sourceDropped !== input.dropped || JSON.stringify(stored.event) !== JSON.stringify(event)) {
202
+ return "conflict";
203
+ }
121
204
  }
122
- return webCrypto.subtle;
205
+ return "same";
123
206
  }
124
- function createWebCrypto() {
125
- return {
126
- randomUuid() {
127
- return globalThis.crypto.randomUUID();
128
- },
129
- randomToken(byteLength) {
130
- return base64UrlEncode(globalThis.crypto.getRandomValues(new Uint8Array(byteLength)));
131
- },
132
- randomPairingCode(length) {
133
- const bytes = globalThis.crypto.getRandomValues(new Uint8Array(length));
134
- let out = "";
135
- for (const byte of bytes) {
136
- out += PAIRING_CODE_ALPHABET[byte % PAIRING_CODE_ALPHABET.length];
137
- }
138
- return out;
139
- },
140
- async verifyEd25519(publicKeyBase64Url, message, signature) {
141
- const signatureBytes = base64UrlDecode(signature);
142
- if (signatureBytes === void 0) return false;
143
- try {
144
- const key = await subtle().importKey(
145
- "jwk",
146
- { kty: "OKP", crv: "Ed25519", x: publicKeyBase64Url },
147
- { name: "Ed25519" },
148
- false,
149
- ["verify"]
150
- );
151
- const messageBytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
152
- return await subtle().verify({ name: "Ed25519" }, key, signatureBytes, messageBytes);
153
- } catch {
154
- return false;
155
- }
156
- },
157
- async hmacSha256(secret, message) {
158
- const key = await subtle().importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
159
- const signature = await subtle().sign("HMAC", key, new TextEncoder().encode(message));
160
- return base64UrlEncode(new Uint8Array(signature));
161
- },
162
- async sha256(data) {
163
- const digest = await subtle().digest("SHA-256", data);
164
- return `sha256:${toHex(new Uint8Array(digest))}`;
165
- },
166
- timingSafeEqual(left, right) {
167
- const leftBytes = base64UrlDecode(left);
168
- const rightBytes = base64UrlDecode(right);
169
- if (leftBytes === void 0 || rightBytes === void 0) return false;
170
- if (leftBytes.length !== rightBytes.length) return false;
171
- let diff = 0;
172
- for (let index = 0; index < leftBytes.length; index += 1) {
173
- diff |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
174
- }
175
- return diff === 0;
176
- }
177
- };
207
+ function parseTimelineEvents(value) {
208
+ return z.array(TimelineEventSchema).parse(value);
209
+ }
210
+ function compareTimelineEvents(left, right) {
211
+ return left.batchSeq - right.batchSeq || left.eventIndex - right.eventIndex;
212
+ }
213
+ function activityCursor(entries) {
214
+ const last = entries.at(-1);
215
+ return last === void 0 ? void 0 : { batchSeq: last.batchSeq, eventIndex: last.eventIndex };
216
+ }
217
+ function activityTailKey(tenant, taskId) {
218
+ return tenantKey(tenant, taskId);
178
219
  }
179
220
 
180
- // src/auth/tokens.ts
181
- var ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
182
- function encodeSegment(value) {
183
- return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
221
+ // src/coordination.ts
222
+ var DEFAULT_BOARD_CHANNEL_MAX_BYTES = 128;
223
+ var DEFAULT_BOARD_TITLE_MAX_BYTES = 512;
224
+ var DEFAULT_ACTIVITY_MAX_EVENTS = 50;
225
+ var DEFAULT_ACTIVITY_MAX_BYTES = 64 * 1024;
226
+ var DEFAULT_ACTIVITY_TTL_MS = 10 * 6e4;
227
+ var DEFAULT_PRESENCE_TTL_MS = 9e4;
228
+ var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
229
+ var DEFAULT_PRESENCE_DETAIL_MAX_BYTES = 512;
230
+ var encoder = new TextEncoder();
231
+ var DEFAULT_ACTIVITY_BOUNDS = {
232
+ maxEvents: DEFAULT_ACTIVITY_MAX_EVENTS,
233
+ maxBytes: DEFAULT_ACTIVITY_MAX_BYTES,
234
+ capacity: DEFAULT_ACTIVITY_CAPACITY,
235
+ ttlMs: DEFAULT_ACTIVITY_TTL_MS
236
+ };
237
+ function assertBoardLabels(channel, title, limits) {
238
+ if (channel.length === 0 || encoder.encode(channel).length > limits.channelMaxBytes) {
239
+ throw new ByokCloudError(
240
+ "coordination_input_invalid",
241
+ `Board channel must contain 1..${limits.channelMaxBytes} UTF-8 bytes.`
242
+ );
243
+ }
244
+ if (title.length === 0 || encoder.encode(title).length > limits.titleMaxBytes) {
245
+ throw new ByokCloudError(
246
+ "coordination_input_invalid",
247
+ `Board title must contain 1..${limits.titleMaxBytes} UTF-8 bytes.`
248
+ );
249
+ }
184
250
  }
185
- function createHmacTokenSigner(secret, clock) {
186
- async function signingKey(usage) {
187
- return globalThis.crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, [usage]);
251
+ function assertTaskAttemptListLimit(limit) {
252
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
253
+ throw new ByokCloudError(
254
+ "coordination_input_invalid",
255
+ `Task attempt list limit must be a positive integer; received ${String(limit)}.`
256
+ );
188
257
  }
189
- async function signature(signingInput) {
190
- const key = await signingKey("sign");
191
- const bytes = await globalThis.crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signingInput));
192
- return base64UrlEncode(new Uint8Array(bytes));
258
+ }
259
+ function validateActivityEvents(events, dropped, bounds) {
260
+ if (!Number.isSafeInteger(dropped) || dropped < 0) {
261
+ throw new ByokCloudError(
262
+ "coordination_input_invalid",
263
+ "Activity dropped must be a non-negative integer."
264
+ );
265
+ }
266
+ if (events.length === 0 || events.length > bounds.maxEvents) {
267
+ throw new ByokCloudError(
268
+ "activity_batch_too_large",
269
+ `Activity batch must contain 1..${bounds.maxEvents} events.`
270
+ );
271
+ }
272
+ const encoded = JSON.stringify(events);
273
+ if (encoder.encode(encoded).length > bounds.maxBytes) {
274
+ throw new ByokCloudError(
275
+ "activity_batch_too_large",
276
+ `Activity batch exceeds ${bounds.maxBytes} UTF-8 bytes.`
277
+ );
193
278
  }
194
- return {
195
- async sign(claims, expiresInSeconds) {
196
- const issuedAt = Math.floor(clock.now().getTime() / 1e3);
197
- const header = encodeSegment({ alg: "HS256", typ: "JWT" });
198
- const payload = encodeSegment({
199
- deviceId: claims.deviceId,
200
- tenantId: claims.tenantId,
201
- productId: claims.productId,
202
- iat: issuedAt,
203
- exp: issuedAt + expiresInSeconds
204
- });
205
- const signingInput = `${header}.${payload}`;
206
- return `${signingInput}.${await signature(signingInput)}`;
207
- },
208
- async verify(token) {
209
- const parts = token.split(".");
210
- if (parts.length !== 3) return void 0;
211
- const [header, payload, provided] = parts;
212
- const key = await signingKey("verify");
213
- const providedBytes = base64UrlDecode(provided);
214
- if (providedBytes === void 0) return void 0;
215
- const ok = await globalThis.crypto.subtle.verify(
216
- "HMAC",
217
- key,
218
- providedBytes,
219
- new TextEncoder().encode(`${header}.${payload}`)
220
- );
221
- if (!ok) return void 0;
222
- const headerBytes = base64UrlDecode(header);
223
- const payloadBytes = base64UrlDecode(payload);
224
- if (headerBytes === void 0 || payloadBytes === void 0) return void 0;
225
- let decodedHeader;
226
- let decodedPayload;
227
- try {
228
- decodedHeader = JSON.parse(new TextDecoder().decode(headerBytes));
229
- decodedPayload = JSON.parse(new TextDecoder().decode(payloadBytes));
230
- } catch {
231
- return void 0;
232
- }
233
- const alg = decodedHeader?.alg;
234
- if (alg !== "HS256") return void 0;
235
- const { deviceId, tenantId: tenantId4, productId, exp } = decodedPayload;
236
- if (typeof deviceId !== "string" || typeof tenantId4 !== "string" || typeof productId !== "string") {
237
- return void 0;
238
- }
239
- if (typeof exp !== "number" || clock.now().getTime() / 1e3 >= exp) return void 0;
240
- return { deviceId, tenantId: tenantId4, productId };
241
- }
242
- };
243
279
  }
244
- function verifyNonceSignature(crypto2, devicePublicKey, nonce, signature) {
245
- return crypto2.verifyEd25519(devicePublicKey, NONCE_SIGNING_DOMAIN + nonce, signature);
280
+ function validateActivityBatch(input, bounds) {
281
+ validateActivityEvents(input.events, input.dropped, bounds);
282
+ validateActivityAppend({ ...input, ttlMs: bounds.ttlMs, capacity: bounds.capacity });
246
283
  }
247
-
248
- // src/auth/plane.ts
249
- var PAIRING_CODE_TTL_MS = 10 * 60 * 1e3;
250
- var PAIRING_CODE_LENGTH = 8;
251
- var DEVICE_IDENTITY_PROOF_KEY_ID = "identity";
252
- var DEVICE_IDENTITY_PROOF_KEY_EPOCH = 0;
253
- function createAuthPlane(deps) {
254
- const { stores, crypto: crypto2, clock, tokenSigner } = deps;
255
- const ttlSeconds = deps.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS;
256
- return {
257
- async createPairingCode(tenant, input) {
258
- const expiresAt = new Date(clock.now().getTime() + (input.ttlMs ?? PAIRING_CODE_TTL_MS)).toISOString();
259
- return stores.pairingCodes.issue(tenant, {
260
- code: crypto2.randomPairingCode(PAIRING_CODE_LENGTH),
261
- productId: input.productId,
262
- expiresAt
263
- });
264
- },
265
- async redeemAndRegister(input) {
266
- return stores.pairing.redeemAndRegister({
267
- pairingCode: input.pairingCode,
268
- deviceId: `dev_${crypto2.randomUuid()}`,
269
- deviceName: input.deviceName,
270
- devicePublicKey: input.devicePublicKey,
271
- proofKeyId: DEVICE_IDENTITY_PROOF_KEY_ID,
272
- proofKeyEpoch: DEVICE_IDENTITY_PROOF_KEY_EPOCH,
273
- ...input.machineId === void 0 ? {} : { machineId: input.machineId }
274
- });
275
- },
276
- async resolveDevice(deviceId) {
277
- const device = await stores.devices.resolveByDeviceId(deviceId);
278
- if (device === void 0 || device.revoked) return void 0;
279
- return device;
280
- },
281
- issueNonce(device) {
282
- return stores.nonces.issue(device.tenantId, device.deviceId);
283
- },
284
- consumeNonceIfValid(device, nonce) {
285
- return stores.nonces.consumeIfValid(device.tenantId, device.deviceId, nonce);
286
- },
287
- verifySignature(device, nonce, signature) {
288
- return verifyNonceSignature(crypto2, device.devicePublicKey, nonce, signature);
289
- },
290
- async mintAccessToken(device) {
291
- const accessToken = await tokenSigner.sign(
292
- { deviceId: device.deviceId, tenantId: device.tenantId, productId: device.productId },
293
- ttlSeconds
294
- );
295
- return {
296
- accessToken,
297
- expiresAt: new Date(clock.now().getTime() + ttlSeconds * 1e3).toISOString()
298
- };
299
- }
300
- };
284
+ async function appendActivityEvents(activity, input, bounds) {
285
+ validateActivityBatch(input, bounds);
286
+ return activity.append({
287
+ taskId: input.taskId,
288
+ sourceEnvelopeId: input.sourceEnvelopeId,
289
+ batchSeq: input.batchSeq,
290
+ events: input.events,
291
+ dropped: input.dropped,
292
+ ttlMs: bounds.ttlMs,
293
+ capacity: bounds.capacity
294
+ });
301
295
  }
302
- var CLOUD_CAPABILITIES = {
303
- /** `GET /byok/events` long-poll receive (§8). */
304
- eventsLongPoll: "events.longpoll",
305
- /** `POST /byok/messages` batched send (§8.2). */
306
- messagesBatch: "messages.batch",
307
- /** The three bearer-authed blob routes: reserve/grant, explicit finalize, committed-only download (§7). */
308
- blobsPresigned: "blobs.presigned",
309
- /**
310
- * The two presigned `/byok/blobs/:id/content` routes — cloud carrying the
311
- * bytes itself.
312
- *
313
- * Split out of `blobs.presigned` because it was one capability describing two
314
- * separable facts. A composition whose bytes live in object storage mints
315
- * grants (`blobs.presigned`) but has no byte-proxy path at all, and saying so
316
- * by declaration is ADR-010's whole posture: a client reads what a deployment
317
- * serves, it never probes a `/content` route and interprets the status code.
318
- *
319
- * Spelled all-lowercase because core's declaration schema pins capability
320
- * names to `/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/` — the same reason the sibling
321
- * above reads `events.longpoll` and not `events.longPoll`.
322
- */
323
- blobsContentProxy: "blobs.contentproxy",
324
- /** Board list/claim/unclaim/status routes. Polling is first-class under this declaration. */
325
- boardCoordination: "board.coordination",
326
- /** Additional SSE transport for the same board read model. */
327
- boardSse: "board.sse",
328
- /** Device-scoped five-level presence publication. */
329
- presenceHints: "presence.hints",
330
- /** Bounded task activity batch publication. */
331
- activityTail: "activity.tail",
332
- /** Request-bound device proof record manifest/read/write surface (S6). */
333
- truthRecords: "truth.records",
334
- /**
335
- * Tenant-scoped skill pack distribution: the manifest list plus the per-file
336
- * content route a paired device installs from.
337
- *
338
- * Hosted HTTP on purpose. A pack is DECLARATIVE CONTENT a device pulls after
339
- * reading this declaration — not a message — so `@byok-sdk/protocol` gains
340
- * nothing from it and the frozen v1 envelope stays untouched. Composition-
341
- * bound like `truth.records`: a deployment that declares it without supplying
342
- * a `SkillPackStore` is refused at construction rather than publishing two
343
- * routes it would then 404.
344
- */
345
- skillPacks: "skills.pack",
346
- /** Optional, one-way redacted Agent-memory snapshot mutation route. */
347
- agentMemoryProjection: "agent.memory.projection"
348
- };
349
- var CapabilitiesResponseSchema = CapabilityDeclarationSchema;
350
- function fullCapabilityDeclaration(version = 1, options = {}) {
351
- return {
352
- schema: "byok-capabilities-v1",
353
- version,
354
- capabilities: Object.values(CLOUD_CAPABILITIES).filter((capability) => {
355
- if (capability === CLOUD_CAPABILITIES.truthRecords) return options.includeTruthRecords === true;
356
- if (capability === CLOUD_CAPABILITIES.skillPacks) return options.includeSkillPacks === true;
357
- if (capability === CLOUD_CAPABILITIES.agentMemoryProjection) {
358
- return options.includeAgentMemoryProjection === true;
359
- }
360
- return true;
361
- })
362
- };
363
- }
364
- function declares(declaration, capability) {
365
- return hasCapability(declaration, capability);
366
- }
367
-
368
- // src/errors.ts
369
- var CLOUD_ERROR_CODES = {
370
- /** A pairing code that is unknown, expired, or already redeemed (§6.1). */
371
- pairing_code_invalid: "pairing_code_invalid",
372
- /** The composition handed a device row whose tenant is not a mintable `TenantId`. */
373
- device_tenant_invalid: "device_tenant_invalid",
374
- /**
375
- * The mailbox committed a row `seq` that disagrees with the delivery `seq`
376
- * its body factory baked into the envelope. Loud rather than silent: those
377
- * two numbers ARE the daemon's redelivery cursor.
378
- */
379
- mailbox_seq_mismatch: "mailbox_seq_mismatch",
380
- /** A capability declaration the host supplied that core refused. */
381
- capability_declaration_invalid: "capability_declaration_invalid",
382
- /**
383
- * The declaration names a capability this composition cannot serve, so the
384
- * deployment would publish a surface it does not have (ADR-010).
385
- *
386
- * Construction-time and fatal. A client learns what a deployment supports by
387
- * READING the declaration and is entitled to act on it without probing, so a
388
- * declaration that over-states is not a degraded deployment — it is a
389
- * deployment whose one honest interface lies.
390
- */
391
- capability_over_declared: "capability_over_declared",
392
- /** Host-supplied board labels or coordination input exceeded the explicit contract. */
393
- coordination_input_invalid: "coordination_input_invalid",
394
- /**
395
- * A terminal receipt whose stored body is not a terminal envelope — either
396
- * undecodable or a non-terminal type. Whatever wrote that row broke the
397
- * receipt-store contract, so the typed read model fails closed instead of
398
- * projecting a best-effort shape.
399
- */
400
- terminal_receipt_unreadable: "terminal_receipt_unreadable",
401
- /** A progress/activity batch exceeded the configured event or byte ceiling. */
402
- activity_batch_too_large: "activity_batch_too_large",
403
- /** Host control-plane task lookup is tenant-closed and found no task. */
404
- task_not_found: "task_not_found",
405
- /**
406
- * A host `approveTask`/`rejectTask` names a task with no approval to
407
- * resolve: the durable timeline holds no unresolved `approval_requested`
408
- * (including a tail that has expired or was never written), the attempt has
409
- * already reached a terminal status, or no device ever claimed it so there
410
- * is nothing to notify. Fail-closed by construction — cloud never invents a
411
- * pending approval to make the call succeed.
412
- */
413
- task_not_awaiting_approval: "task_not_awaiting_approval",
414
- /** Durable target-device capability is absent, revoked, or unavailable. */
415
- agent_capability_missing: "agent_capability_missing",
416
- /** Inbound Agent identity did not exactly match the offered identity. */
417
- agent_ref_mismatch: "agent_ref_mismatch",
418
- /** A strict Agent task id already names a durable attempt and cannot be re-enqueued. */
419
- agent_task_already_exists: "agent_task_already_exists",
420
- /** A first-write-wins Agent control record was replayed with a different body. */
421
- agent_content_request_mismatch: "agent_content_request_mismatch",
422
- /** A durable mailbox receipt id resolved to an envelope other than its exact acknowledgement. */
423
- mailbox_receipt_mismatch: "mailbox_receipt_mismatch",
424
- /** A task-free Agent-home request id already names a different immutable desired projection. */
425
- agent_home_projection_request_conflict: "agent_home_projection_request_conflict",
426
- /** A direct Agent-home completion did not identify a stored desired request for this exact device. */
427
- agent_home_projection_request_not_found: "agent_home_projection_request_not_found",
428
- /** A direct Agent-home completion changed the first durable terminal outcome. */
429
- agent_home_projection_completion_conflict: "agent_home_projection_completion_conflict",
430
- /** A task-free completion did not exactly echo its immutable desired projection binding. */
431
- agent_home_projection_receipt_mismatch: "agent_home_projection_receipt_mismatch",
432
- /** A receipt-store row at the projection namespace violated the frozen projection schema. */
433
- agent_home_projection_receipt_invalid: "agent_home_projection_receipt_invalid",
434
- /** A hosted Agent-memory mutation did not match the durable task/device/AgentRef binding. */
435
- agent_memory_projection_task_mismatch: "agent_memory_projection_task_mismatch",
436
- /** The embedder-owned grant/consent authority denied a hosted memory mutation. */
437
- agent_memory_projection_authorization_denied: "agent_memory_projection_authorization_denied",
438
- /** A redacted snapshot's decoded bytes did not match its declared hash or byte count. */
439
- agent_memory_projection_hash_mismatch: "agent_memory_projection_hash_mismatch",
440
- /** A writer epoch/source sequence already names a different immutable projection mutation. */
441
- agent_memory_projection_replay_mismatch: "agent_memory_projection_replay_mismatch",
442
- /** A hosted memory mutation came from an older writer epoch. */
443
- agent_memory_projection_stale_epoch: "agent_memory_projection_stale_epoch",
444
- /** A server-side erase fence prevents a deleted writer epoch from re-entering. */
445
- agent_memory_projection_erased_epoch: "agent_memory_projection_erased_epoch",
446
- /** A prior erase reached the protocol writer-epoch ceiling and cannot mint a later writer. */
447
- agent_memory_projection_epoch_exhausted: "agent_memory_projection_epoch_exhausted",
448
- /** A hosted memory mutation skipped or reset a required source sequence. */
449
- agent_memory_projection_sequence_gap: "agent_memory_projection_sequence_gap"
450
- };
451
- var ByokCloudError = class extends Error {
452
- code;
453
- constructor(code, message, options) {
454
- super(message, options);
455
- this.name = "ByokCloudError";
456
- this.code = code;
457
- }
458
- };
459
- function isCloudError(value, code) {
460
- if (!(value instanceof ByokCloudError)) return false;
461
- return code === void 0 || value.code === code;
462
- }
463
-
464
- // src/activity.ts
465
- var DEFAULT_ACTIVITY_CAPACITY = 50;
466
- var ActivityTaskIdSchema = z.string().min(1).max(200);
467
- var SourceEnvelopeIdSchema = z.string().max(200).regex(/\S/, "sourceEnvelopeId must not be blank");
468
- var ActivityAppendRequestSchema = z.object({
469
- taskId: ActivityTaskIdSchema,
470
- sourceEnvelopeId: SourceEnvelopeIdSchema,
471
- batchSeq: z.number().int().nonnegative(),
472
- events: z.array(AgentEventOrUnknownSchema).min(1),
473
- dropped: z.number().int().nonnegative()
474
- });
475
- var TimelineEventSchema = z.object({
476
- taskId: ActivityTaskIdSchema,
477
- sourceEnvelopeId: SourceEnvelopeIdSchema,
478
- batchSeq: z.number().int().nonnegative(),
479
- eventIndex: z.number().int().nonnegative(),
480
- /**
481
- * The producer-declared loss attached to this source batch. It travels with
482
- * every retained event so a replay includes it in the canonical batch
483
- * binding rather than treating the aggregate as an incidental counter.
484
- */
485
- sourceDropped: z.number().int().nonnegative().optional(),
296
+ var DEFAULT_APPROVAL_TIMELINE_CAPACITY = 50;
297
+ var DEFAULT_APPROVAL_TIMELINE_TTL_MS = 10 * 60 * 1e3;
298
+ var APPROVAL_SUMMARY_MAX_BYTES = 16 * 1024;
299
+ var NonBlankIdSchema = z.string().max(200).regex(/\S/, "value must not be blank");
300
+ var TaskIdSchema = z.string().min(1).max(200);
301
+ var ApprovalRequestedEventSchema = z.object({
302
+ type: z.literal("approval_requested"),
303
+ summary: z.string(),
304
+ approvalId: NonBlankIdSchema.optional()
305
+ }).strict();
306
+ var LocalApprovalResolutionEventSchema = z.object({
307
+ type: z.literal("approval_resolved"),
308
+ // Absent only when a daemon resolved the single outstanding request without
309
+ // native approval identity. No id is synthesized.
310
+ approvalId: NonBlankIdSchema.optional(),
311
+ decision: z.enum(["approve", "reject"]),
312
+ // A daemon's `task.approval_resolved` is pure wire observation; it cannot
313
+ // declare a host command payload.
314
+ resolvedBy: z.literal("local"),
315
+ at: z.iso.datetime({ offset: true })
316
+ }).strict();
317
+ var HostApprovalResolutionEventSchema = z.object({
318
+ type: z.literal("approval_resolved"),
319
+ approvalId: NonBlankIdSchema.optional(),
320
+ decision: z.literal("approve"),
321
+ resolvedBy: z.literal("host"),
322
+ at: z.iso.datetime({ offset: true })
323
+ }).strict();
324
+ var HostRejectResolutionEventSchema = z.object({
325
+ type: z.literal("approval_resolved"),
326
+ approvalId: NonBlankIdSchema.optional(),
327
+ decision: z.literal("reject"),
328
+ resolvedBy: z.literal("host"),
329
+ // Required even when the host deliberately omitted a wire reason. `null`
330
+ // records that complete payload, so a historical host reject with no field
331
+ // cannot be mistaken for a recoverable executable command.
332
+ reason: z.string().nullable(),
333
+ at: z.iso.datetime({ offset: true })
334
+ }).strict();
335
+ var ApprovalTimelineEventSchema = z.union([
336
+ ApprovalRequestedEventSchema,
337
+ LocalApprovalResolutionEventSchema,
338
+ HostApprovalResolutionEventSchema,
339
+ HostRejectResolutionEventSchema
340
+ ]);
341
+ var ApprovalObservationSchema = z.object({
342
+ taskId: TaskIdSchema,
343
+ sourceEnvelopeId: NonBlankIdSchema,
344
+ revision: z.number().int().positive(),
486
345
  receivedAt: z.iso.datetime(),
487
- event: AgentEventOrUnknownSchema
346
+ event: ApprovalTimelineEventSchema
488
347
  });
489
- function validateActivityAppend(input) {
490
- if (!Number.isFinite(input.ttlMs) || input.ttlMs <= 0) {
348
+ function validateApprovalTimelineAppend(input) {
349
+ const capacity = input.capacity ?? DEFAULT_APPROVAL_TIMELINE_CAPACITY;
350
+ const ttlMs = input.ttlMs ?? DEFAULT_APPROVAL_TIMELINE_TTL_MS;
351
+ if (!Number.isSafeInteger(capacity) || capacity <= 0) {
491
352
  throw new ByokCloudError(
492
353
  "coordination_input_invalid",
493
- `Activity ttl must be a positive number of milliseconds, received ${String(input.ttlMs)}.`
354
+ `Approval timeline capacity must be a positive integer, received ${String(capacity)}.`
494
355
  );
495
356
  }
496
- const capacity = input.capacity ?? DEFAULT_ACTIVITY_CAPACITY;
497
- if (!Number.isSafeInteger(capacity) || capacity <= 0) {
357
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
498
358
  throw new ByokCloudError(
499
359
  "coordination_input_invalid",
500
- `Activity capacity must be a positive integer, received ${String(capacity)}.`
360
+ `Approval timeline ttl must be a positive number of milliseconds, received ${String(ttlMs)}.`
501
361
  );
502
362
  }
503
- const parsed = ActivityAppendRequestSchema.safeParse(input);
363
+ const parsed = z.object({
364
+ taskId: TaskIdSchema,
365
+ sourceEnvelopeId: NonBlankIdSchema,
366
+ event: ApprovalTimelineEventSchema
367
+ }).safeParse(input);
504
368
  if (!parsed.success) {
505
369
  throw new ByokCloudError(
506
370
  "coordination_input_invalid",
507
- "Activity batches require stable source identity, order, at least one valid event, and a non-negative dropped count."
371
+ "Approval observations require stable source identity and a valid native lifecycle event."
508
372
  );
509
373
  }
510
- return capacity;
374
+ if (parsed.data.event.type === "approval_requested" && new TextEncoder().encode(parsed.data.event.summary).byteLength > APPROVAL_SUMMARY_MAX_BYTES) {
375
+ throw new ByokCloudError(
376
+ "coordination_input_invalid",
377
+ `Approval summary exceeds ${APPROVAL_SUMMARY_MAX_BYTES} UTF-8 bytes.`
378
+ );
379
+ }
380
+ return { capacity, ttlMs, event: parsed.data.event };
511
381
  }
512
- function projectTimelineEvents(input, receivedAt) {
513
- return input.events.map(
514
- (event, eventIndex) => TimelineEventSchema.parse({
515
- taskId: input.taskId,
516
- sourceEnvelopeId: input.sourceEnvelopeId,
517
- batchSeq: input.batchSeq,
518
- eventIndex,
519
- sourceDropped: input.dropped,
520
- receivedAt,
521
- event
522
- })
523
- );
524
- }
525
- function activitySourceBatchState(entries, input) {
526
- const existing = entries.filter((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId).sort((left, right) => left.eventIndex - right.eventIndex);
527
- if (existing.length === 0) return "absent";
528
- if (existing.length !== input.events.length) return "conflict";
529
- for (const [eventIndex, event] of input.events.entries()) {
530
- const stored = existing[eventIndex];
531
- if (stored === void 0 || stored.taskId !== input.taskId || stored.batchSeq !== input.batchSeq || stored.eventIndex !== eventIndex || stored.sourceDropped !== input.dropped || JSON.stringify(stored.event) !== JSON.stringify(event)) {
532
- return "conflict";
533
- }
382
+ function validateApprovalTimelineResolve(input) {
383
+ const validated = validateApprovalTimelineAppend(input);
384
+ const expected = z.object({
385
+ expectedSourceEnvelopeId: NonBlankIdSchema,
386
+ expectedRevision: z.number().int().positive()
387
+ }).safeParse(input);
388
+ if (!expected.success || validated.event.type !== "approval_resolved" || validated.event.resolvedBy !== "host") {
389
+ throw new ByokCloudError(
390
+ "coordination_input_invalid",
391
+ "Host approval resolution requires an exact pending source/revision and a host resolution event."
392
+ );
534
393
  }
535
- return "same";
394
+ return validated;
536
395
  }
537
- function parseTimelineEvents(value) {
538
- return z.array(TimelineEventSchema).parse(value);
396
+ function approvalTimelineKey(tenant, taskId) {
397
+ return tenantKey(tenant, taskId);
539
398
  }
540
- function compareTimelineEvents(left, right) {
541
- return left.batchSeq - right.batchSeq || left.eventIndex - right.eventIndex;
399
+ function parseApprovalObservations(value) {
400
+ return z.array(ApprovalObservationSchema).parse(value);
542
401
  }
543
- function activityCursor(entries) {
544
- const last = entries.at(-1);
545
- return last === void 0 ? void 0 : { batchSeq: last.batchSeq, eventIndex: last.eventIndex };
402
+ function approvalTimelineCursor(entries) {
403
+ return entries.at(-1)?.revision;
546
404
  }
547
- function activityTailKey(tenant, taskId) {
548
- return tenantKey(tenant, taskId);
405
+ async function projectTerminalToReview(board, taskId) {
406
+ const item = await board.get(taskId);
407
+ if (item === void 0 || item.status !== "in_progress") return;
408
+ try {
409
+ await board.updateStatus({
410
+ itemId: taskId,
411
+ expectedStatus: "in_progress",
412
+ status: "in_review"
413
+ });
414
+ } catch (caught) {
415
+ if (isCoreConflictError(caught)) return;
416
+ throw caught;
417
+ }
418
+ }
419
+ function agentReliabilityKey(domain, deviceId, agentRef, id) {
420
+ const exact = AgentRefSchema.parse(agentRef);
421
+ return `${domain}:${JSON.stringify([deviceId, exact.agentId, exact.profileRevision, id])}`;
549
422
  }
550
423
 
551
- // src/coordination.ts
552
- var DEFAULT_BOARD_CHANNEL_MAX_BYTES = 128;
553
- var DEFAULT_BOARD_TITLE_MAX_BYTES = 512;
554
- var DEFAULT_ACTIVITY_MAX_EVENTS = 50;
555
- var DEFAULT_ACTIVITY_MAX_BYTES = 64 * 1024;
556
- var DEFAULT_ACTIVITY_TTL_MS = 10 * 6e4;
557
- var DEFAULT_PRESENCE_TTL_MS = 9e4;
558
- var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
559
- var DEFAULT_PRESENCE_DETAIL_MAX_BYTES = 512;
560
- var encoder = new TextEncoder();
561
- var DEFAULT_ACTIVITY_BOUNDS = {
562
- maxEvents: DEFAULT_ACTIVITY_MAX_EVENTS,
563
- maxBytes: DEFAULT_ACTIVITY_MAX_BYTES,
564
- capacity: DEFAULT_ACTIVITY_CAPACITY,
565
- ttlMs: DEFAULT_ACTIVITY_TTL_MS
566
- };
567
- function assertBoardLabels(channel, title, limits) {
568
- if (channel.length === 0 || encoder.encode(channel).length > limits.channelMaxBytes) {
569
- throw new ByokCloudError(
570
- "coordination_input_invalid",
571
- `Board channel must contain 1..${limits.channelMaxBytes} UTF-8 bytes.`
572
- );
424
+ // src/inbound.ts
425
+ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume, alreadyDeduplicated) {
426
+ const device = await stores.devices.get(deviceId);
427
+ const attempt = await stores.tasks.get(taskId);
428
+ const binding = await stores.receipts.get(`agent-message-offer:${deviceId}:${taskId}`);
429
+ if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_MESSAGE_EGRESS_CAPABILITY]) || attempt === void 0 || attempt.deviceId !== deviceId || binding === void 0 || attempt.agentRef === void 0 || !sameAgentRef(attempt.agentRef, payload.agentRef)) return { outcome: "rejected" };
430
+ let frozen;
431
+ try {
432
+ frozen = JSON.parse(binding.body);
433
+ } catch {
434
+ return { outcome: "rejected" };
573
435
  }
574
- if (title.length === 0 || encoder.encode(title).length > limits.titleMaxBytes) {
575
- throw new ByokCloudError(
576
- "coordination_input_invalid",
577
- `Board title must contain 1..${limits.titleMaxBytes} UTF-8 bytes.`
578
- );
436
+ if (!sameAgentRef(frozen.agentRef, payload.agentRef) || frozen.context === void 0 || frozen.requirement?.contract !== payload.contract || frozen.sessionRef !== void 0 && frozen.sessionRef !== payload.sessionRef || frozen.requirement.contentType !== payload.contentType || typeof frozen.requirement.maxBytes !== "number" || payload.byteCount > frozen.requirement.maxBytes) return { outcome: "rejected" };
437
+ const priorDisposition = await readAgentMessageDisposition(stores, deviceId, taskId, payload);
438
+ if (priorDisposition !== void 0) return { outcome: "duplicate", disposition: priorDisposition };
439
+ const payloadBody = JSON.stringify(payload);
440
+ const reservation = await stores.tasks.reserveAgentMessage({
441
+ taskId,
442
+ deviceId,
443
+ messageId: payload.messageId,
444
+ payloadBody
445
+ });
446
+ if (reservation !== "reserved" && reservation !== "pending") return { outcome: "rejected" };
447
+ let decision;
448
+ if (consume === void 0) {
449
+ if (reservation === "pending") {
450
+ throw new Error("Agent message consumer is unavailable while reconciling an exact pending admission.");
451
+ }
452
+ decision = { outcome: "held", reasonCode: "consumer_unavailable" };
453
+ } else {
454
+ decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
579
455
  }
456
+ const disposition = agentMessageDisposition(payload, decision);
457
+ const admission = await stores.tasks.finalizeAgentMessage({
458
+ taskId,
459
+ deviceId,
460
+ messageId: payload.messageId,
461
+ payloadBody,
462
+ terminalBody: JSON.stringify({ payload, disposition })
463
+ });
464
+ const terminal = admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
465
+ return terminal === void 0 ? { outcome: "rejected" } : { outcome: "accepted", disposition: terminal };
580
466
  }
581
- function assertTaskAttemptListLimit(limit) {
582
- if (!Number.isSafeInteger(limit) || limit <= 0) {
583
- throw new ByokCloudError(
584
- "coordination_input_invalid",
585
- `Task attempt list limit must be a positive integer; received ${String(limit)}.`
586
- );
587
- }
467
+ function agentMessageDisposition(payload, decision) {
468
+ return {
469
+ agentRef: payload.agentRef,
470
+ sessionRef: payload.sessionRef,
471
+ contract: payload.contract,
472
+ messageId: payload.messageId,
473
+ cursor: payload.cursor,
474
+ contentHash: payload.contentHash,
475
+ outcome: decision.outcome,
476
+ receiptId: crypto.randomUUID(),
477
+ ...decision.reasonCode === void 0 ? {} : { reasonCode: decision.reasonCode }
478
+ };
588
479
  }
589
- function validateActivityEvents(events, dropped, bounds) {
590
- if (!Number.isSafeInteger(dropped) || dropped < 0) {
591
- throw new ByokCloudError(
592
- "coordination_input_invalid",
593
- "Activity dropped must be a non-negative integer."
594
- );
595
- }
596
- if (events.length === 0 || events.length > bounds.maxEvents) {
597
- throw new ByokCloudError(
598
- "activity_batch_too_large",
599
- `Activity batch must contain 1..${bounds.maxEvents} events.`
600
- );
480
+ async function readAgentMessageDisposition(stores, deviceId, taskId, payload) {
481
+ const admission = await stores.tasks.readAgentMessage({
482
+ taskId,
483
+ deviceId,
484
+ messageId: payload.messageId,
485
+ payloadBody: JSON.stringify(payload)
486
+ });
487
+ return admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
488
+ }
489
+ function parseAgentMessageDisposition(payload, terminalBody) {
490
+ const invalid = () => new Error("Invalid persisted Agent message disposition.");
491
+ let decoded;
492
+ try {
493
+ decoded = JSON.parse(terminalBody);
494
+ } catch {
495
+ throw invalid();
601
496
  }
602
- const encoded = JSON.stringify(events);
603
- if (encoder.encode(encoded).length > bounds.maxBytes) {
604
- throw new ByokCloudError(
605
- "activity_batch_too_large",
606
- `Activity batch exceeds ${bounds.maxBytes} UTF-8 bytes.`
607
- );
497
+ if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded)) throw invalid();
498
+ const record = decoded;
499
+ const storedPayload = AgentMessagePublishPayloadSchema.safeParse(record.payload);
500
+ const storedDisposition = AgentMessageDispositionPayloadSchema.safeParse(record.disposition);
501
+ if (!storedPayload.success || !storedDisposition.success || JSON.stringify(record.payload) !== JSON.stringify(payload)) throw invalid();
502
+ const disposition = storedDisposition.data;
503
+ if (!sameAgentRef(payload.agentRef, disposition.agentRef) || disposition.sessionRef !== payload.sessionRef || disposition.contract !== payload.contract || disposition.messageId !== payload.messageId || disposition.cursor !== payload.cursor || disposition.contentHash !== payload.contentHash) throw invalid();
504
+ return disposition;
505
+ }
506
+ function terminalReceiptKey(taskId) {
507
+ return `task:${taskId}:terminal`;
508
+ }
509
+ function sameAgentRef(expected, actual) {
510
+ return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
511
+ }
512
+ function sameReliableEgress(expected, actual) {
513
+ return sameAgentRef(expected.agentRef, actual.agentRef) && expected.sessionRef === actual.sessionRef && expected.policyRevision === actual.policyRevision && expected.eventId === actual.eventId && expected.cursor === actual.cursor && expected.contentHash === actual.contentHash && expected.byteCount === actual.byteCount && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
514
+ }
515
+ function contentReadCapability(surface) {
516
+ switch (surface) {
517
+ case "workspace":
518
+ return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
519
+ case "transcript":
520
+ return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
521
+ case "artifact":
522
+ return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
608
523
  }
609
524
  }
610
- function validateActivityBatch(input, bounds) {
611
- validateActivityEvents(input.events, input.dropped, bounds);
612
- validateActivityAppend({ ...input, ttlMs: bounds.ttlMs, capacity: bounds.capacity });
525
+ function hasCapabilities(capabilities, required) {
526
+ return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
613
527
  }
614
- async function appendActivityEvents(activity, input, bounds) {
615
- validateActivityBatch(input, bounds);
616
- return activity.append({
617
- taskId: input.taskId,
618
- sourceEnvelopeId: input.sourceEnvelopeId,
619
- batchSeq: input.batchSeq,
620
- events: input.events,
621
- dropped: input.dropped,
622
- ttlMs: bounds.ttlMs,
623
- capacity: bounds.capacity
624
- });
528
+ function contentRequestReceiptKey(deviceId, agentRef, requestId) {
529
+ return agentReliabilityKey("agent-content-request", deviceId, agentRef, requestId);
625
530
  }
626
- function extractBearerToken(header) {
627
- if (header === void 0) return void 0;
628
- const match = /^Bearer\s+(.+)$/i.exec(header);
629
- return match?.[1];
531
+ function contentReceiptKey(deviceId, agentRef, requestId) {
532
+ return agentReliabilityKey("agent-content-receipt", deviceId, agentRef, requestId);
630
533
  }
631
- async function authenticateBearer(header, deps) {
632
- const token = extractBearerToken(header);
633
- if (token === void 0) return void 0;
634
- const claims = await deps.tokenSigner.verify(token);
635
- if (claims === void 0) return void 0;
636
- if (!isTenantId(claims.tenantId)) return void 0;
637
- const device = await deps.devices.get(tenantId(claims.tenantId), claims.deviceId);
638
- if (device === void 0 || device.revoked) return void 0;
639
- if (device.productId !== claims.productId) return void 0;
640
- if (deps.instanceProductId !== void 0 && claims.productId !== deps.instanceProductId) {
534
+ function matchesContentReadReceipt(request, receipt) {
535
+ return request.requestId === receipt.requestId && request.surface === receipt.surface && request.actor.kind === receipt.actor.kind && request.actor.id === receipt.actor.id && sameAgentRef(request.agentRef, receipt.agentRef) && request.sessionRef === receipt.sessionRef && request.runtime === receipt.runtime && request.cwd === receipt.cwd && request.policyRevision === receipt.policyRevision && request.target === receipt.target && request.mimeType === receipt.mimeType && request.decodeAs === receipt.decodeAs;
536
+ }
537
+ async function readContentRequest(stores, deviceId, agentRef, requestId) {
538
+ const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, agentRef, requestId));
539
+ if (stored === void 0) return void 0;
540
+ try {
541
+ return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
542
+ } catch {
641
543
  return void 0;
642
544
  }
643
- return {
644
- kind: "device",
645
- tenantId: device.tenantId,
646
- productId: device.productId,
647
- deviceId: device.deviceId
648
- };
649
545
  }
650
- function tenantStoresFor(principal, root) {
651
- const tenant = principalTenant(principal);
652
- const { core, cloud } = root;
653
- return {
654
- tenant,
655
- principal,
656
- mailbox: {
657
- append: (input) => core.mailbox.append(tenant, input),
658
- readAfter: (query) => core.mailbox.readAfter(tenant, query),
659
- recordDelivery: (input) => core.mailbox.recordDelivery(tenant, input),
660
- advanceCursor: (input) => core.mailbox.advanceCursor(tenant, input),
661
- readCursor: (deviceId) => core.mailbox.readCursor(tenant, deviceId)
662
- },
663
- board: {
664
- create: (input) => core.board.create(tenant, input),
665
- get: (itemId) => core.board.get(tenant, itemId),
666
- list: (query) => core.board.list(tenant, query),
667
- claim: (input) => core.board.claim(tenant, input),
668
- unclaim: (input) => core.board.unclaim(tenant, input),
669
- updateStatus: (input) => core.board.updateStatus(tenant, input)
670
- },
671
- presence: {
672
- publish: (input) => core.presence.publish(tenant, input),
673
- read: (deviceId) => core.presence.read(tenant, deviceId),
674
- list: () => core.presence.list(tenant)
675
- },
676
- activity: {
677
- append: (input) => cloud.activity.append(tenant, input),
678
- read: (taskId) => cloud.activity.read(tenant, taskId)
679
- },
680
- approvals: {
681
- append: (input) => cloud.approvals.append(tenant, input),
682
- read: (taskId) => cloud.approvals.read(tenant, taskId)
683
- },
684
- devices: {
685
- get: (deviceId) => cloud.devices.get(tenant, deviceId),
686
- list: () => cloud.devices.list(tenant),
687
- revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId),
688
- recordCapabilities: (input) => principal.kind === "device" ? cloud.devices.recordCapabilities(tenant, {
689
- deviceId: principal.deviceId,
690
- capabilities: input.capabilities,
691
- ...input.harnesses === void 0 ? {} : { harnesses: input.harnesses }
692
- }) : Promise.resolve(void 0),
693
- readiness: () => cloud.devices.readiness(tenant, core.presence)
694
- },
695
- tasks: {
696
- open: (input) => cloud.tasks.open(tenant, input),
697
- reserveAgentOffer: (input) => cloud.tasks.reserveAgentOffer(tenant, input),
698
- reserveAgentMessage: (input) => cloud.tasks.reserveAgentMessage(tenant, input),
699
- readAgentMessage: (input) => cloud.tasks.readAgentMessage(tenant, input),
700
- finalizeAgentMessage: (input) => cloud.tasks.finalizeAgentMessage(tenant, input),
701
- get: (taskId) => cloud.tasks.get(tenant, taskId),
702
- getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
703
- list: (query) => cloud.tasks.list(tenant, query),
704
- claim: (input) => cloud.tasks.claim(tenant, input),
705
- recordStatus: (input) => cloud.tasks.recordStatus(tenant, input)
706
- },
707
- cancellations: {
708
- request: (input) => cloud.cancellations.request(tenant, input)
709
- },
710
- dedup: {
711
- checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId),
712
- checkAndRecordAgent: (deviceId, agentRef, envelopeId) => cloud.dedup.checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId)
713
- },
714
- receipts: {
715
- record: (input) => cloud.receipts.record(tenant, input),
716
- get: (key) => cloud.receipts.get(tenant, key)
717
- },
718
- egress: {
719
- record: (input) => cloud.egress.record(tenant, input),
720
- get: (deviceId, agentRef, eventId) => cloud.egress.get(tenant, deviceId, agentRef, eventId)
721
- },
722
- blobs: {
723
- createUpload: (reservation) => cloud.blobs.createUpload(tenant, reservation),
724
- observeUpload: (blobId, reservation) => cloud.blobs.observeUpload(tenant, blobId, reservation),
725
- getDownloadUrl: (blobId) => cloud.blobs.getDownloadUrl(tenant, blobId)
726
- },
727
- quota: {
728
- readReservation: (reservationId) => core.quota.readReservation(tenant, reservationId),
729
- reserve: (input) => core.quota.reserve(tenant, input),
730
- finalizeReservation: (input) => core.quota.finalizeReservation(tenant, input),
731
- abortReservation: (reservationId) => core.quota.abortReservation(tenant, reservationId)
732
- },
733
- rateLimiter: {
734
- consume: (deviceId) => cloud.rateLimiter.consume(tenant, deviceId)
735
- }
736
- };
737
- }
738
-
739
- // src/handlers/shared.ts
740
- async function readJsonBody(c) {
741
- try {
742
- return await c.req.json();
743
- } catch {
744
- return void 0;
546
+ function inboundAgentRef(envelope) {
547
+ switch (envelope.type) {
548
+ case "task.claim":
549
+ case "task.decline":
550
+ case "task.complete":
551
+ case "task.fail":
552
+ case "task.cancelled":
553
+ return envelope.payload.agentRef;
554
+ default:
555
+ return void 0;
745
556
  }
746
557
  }
747
- async function readBoundedJsonBody(c, maximum) {
748
- const declaredLength = c.req.header("content-length");
749
- if (declaredLength !== void 0) {
750
- const parsedLength = parseContentLength(declaredLength);
751
- if (parsedLength !== void 0 && parsedLength > BigInt(maximum)) {
752
- cancelBody(c.req.raw.body);
753
- return { body: void 0, tooLarge: true };
558
+ function agentRefEchoRequired(envelope) {
559
+ return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
560
+ }
561
+ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume, observer) {
562
+ if (envelope.v !== PROTOCOL_VERSION) return "rejected";
563
+ const outcome = await applyInboundGate(
564
+ stores,
565
+ deviceId,
566
+ envelope,
567
+ activityBounds,
568
+ agentMessageConsume
569
+ );
570
+ if (outcome === "accepted" && observer !== void 0) {
571
+ try {
572
+ observer.onInboundCommitted({ tenantId: stores.tenant, deviceId, envelope, outcome });
573
+ } catch {
754
574
  }
755
575
  }
756
- const stream = c.req.raw.body;
757
- if (stream === null) return { body: void 0, tooLarge: false };
758
- const reader = stream.getReader();
759
- const chunks = [];
760
- let total = 0;
761
- try {
762
- while (true) {
763
- const { done, value } = await reader.read();
764
- if (done) break;
765
- if (value.byteLength > maximum - total) {
766
- cancelBody(reader);
767
- return { body: void 0, tooLarge: true };
768
- }
769
- if (value.byteLength === 0) continue;
770
- chunks.push(value);
771
- total += value.byteLength;
576
+ return outcome;
577
+ }
578
+ async function applyInboundGate(stores, deviceId, envelope, activityBounds, agentMessageConsume) {
579
+ if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
580
+ if (envelope.type === "conn.hello") {
581
+ if (envelope.payload.deviceId !== deviceId) return "rejected";
582
+ if (envelope.payload.harnesses !== void 0 && !envelope.payload.capabilities.includes("custom-harness")) return "rejected";
583
+ if (!envelope.payload.protocolVersions.includes(PROTOCOL_VERSION)) return "rejected";
584
+ const device = await stores.devices.get(deviceId);
585
+ if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
586
+ return "rejected";
772
587
  }
773
- } catch {
774
- return { body: void 0, tooLarge: false };
775
- } finally {
776
- reader.releaseLock();
588
+ if (await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities, ...envelope.payload.harnesses === void 0 ? {} : { harnesses: envelope.payload.harnesses } }) === void 0) {
589
+ return "rejected";
590
+ }
591
+ return completeInboundEnvelope(stores, deviceId, envelope.id, void 0);
777
592
  }
778
- const bytes = new Uint8Array(total);
779
- let offset = 0;
780
- for (const chunk of chunks) {
781
- bytes.set(chunk, offset);
782
- offset += chunk.byteLength;
593
+ if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
594
+ if (envelope.type === "agent.message.publish") {
595
+ const message = await handleAgentMessagePublish(
596
+ stores,
597
+ deviceId,
598
+ envelope.task_id,
599
+ envelope.payload,
600
+ agentMessageConsume);
601
+ return message.outcome === "rejected" ? "rejected" : completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
783
602
  }
784
- return { body: parseJsonBytes(bytes), tooLarge: false };
785
- }
786
- function cancelBody(body) {
787
- if (body === null) return;
788
- try {
789
- void Promise.resolve(body.cancel()).catch(() => {
603
+ if (envelope.type === "agent.egress.reliable") {
604
+ const device = await stores.devices.get(deviceId);
605
+ if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
606
+ return "rejected";
607
+ }
608
+ const outcome = await stores.egress.record({
609
+ deviceId,
610
+ payload: envelope.payload,
611
+ receiptId: crypto.randomUUID()
790
612
  });
791
- } catch {
613
+ if (!outcome.created && !sameReliableEgress(outcome.record.payload, envelope.payload)) return "rejected";
614
+ return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
792
615
  }
793
- }
794
- function parseContentLength(value) {
795
- const normalized = value.trim();
796
- if (!/^\d+$/u.test(normalized)) return void 0;
797
- try {
798
- return BigInt(normalized);
799
- } catch {
800
- return void 0;
616
+ if (envelope.type === "agent.content.receipt") {
617
+ const device = await stores.devices.get(deviceId);
618
+ if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
619
+ return "rejected";
620
+ }
621
+ const request = await readContentRequest(stores, deviceId, envelope.payload.agentRef, envelope.payload.requestId);
622
+ if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
623
+ const key = contentReceiptKey(deviceId, envelope.payload.agentRef, envelope.payload.requestId);
624
+ const body = JSON.stringify(envelope.payload);
625
+ const receipt = await stores.receipts.record({ key, body });
626
+ if (!receipt.created && receipt.receipt.body !== body) return "rejected";
627
+ return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
801
628
  }
802
- }
803
- function parseJsonBytes(bytes) {
804
- try {
805
- return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
806
- } catch {
807
- return void 0;
629
+ const taskId = envelope.task_id;
630
+ if (taskId === void 0) return "rejected";
631
+ const attempt = await stores.tasks.get(taskId);
632
+ if (attempt === void 0 || attempt.deviceId !== deviceId) return "rejected";
633
+ if (attempt?.ownerDeviceId !== void 0 && attempt.ownerDeviceId !== deviceId) return "rejected";
634
+ if (envelope.type === "task.claim" && envelope.payload.deviceId !== deviceId) return "rejected";
635
+ if (envelope.type === "task.claim") {
636
+ const harnessId = envelope.payload.harnessId;
637
+ if (harnessId !== void 0 && envelope.payload.runtime !== void 0) return "rejected";
638
+ if (attempt.ownerDeviceId !== void 0 && (harnessId !== void 0 || attempt.claimedHarnessId !== void 0) && harnessId !== attempt.claimedHarnessId) return "rejected";
639
+ const device = await stores.devices.get(deviceId);
640
+ if (harnessId !== void 0 && (!device?.capabilities?.includes("custom-harness") || !device.harnesses?.some((harness) => harness.id === harnessId))) return "rejected";
641
+ const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
642
+ const offer = await stores.receipts.get(`task-offer:${messageId}`);
643
+ if (offer !== void 0) {
644
+ const sealed = JSON.parse(offer.body);
645
+ if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
646
+ if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
647
+ } else if (harnessId !== void 0) return "rejected";
808
648
  }
809
- }
810
- async function authenticateDevice(c, deps) {
811
- const device = await authenticateBearer(c.req.header("authorization"), deps.bearer);
812
- if (device === void 0) return void 0;
813
- return { device, stores: tenantStoresFor(device, deps.root) };
814
- }
815
-
816
- // src/handlers/blobs.ts
817
- var BLOB_RESERVATION_TTL_MS = 15 * 60 * 1e3;
818
- var SignedUrlQuerySchema = z.object({
819
- sig: z.string().min(1),
820
- exp: z.coerce.number().finite()
821
- });
822
- function createBlobHandler(deps) {
823
- return async (c) => {
824
- const authenticated = await authenticateDevice(c, deps);
825
- if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
826
- const parsed = CreateBlobRequestSchema.safeParse(await readJsonBody(c));
827
- if (!parsed.success) return c.json({ error: "size, contentType, and contentHash are required" }, 400);
828
- if (parsed.data.size > deps.maxBlobSizeBytes) {
829
- return c.json({ error: `blob exceeds max size of ${deps.maxBlobSizeBytes} bytes` }, 413);
830
- }
831
- const reservationId = idempotencyKey(c);
832
- if (reservationId === void 0) {
833
- return c.json({ error: "Idempotency-Key header is required" }, 400);
834
- }
835
- try {
836
- const reservation = await authenticated.stores.quota.reserve({
837
- reservationId,
838
- kind: "object",
839
- expectedBytes: BigInt(parsed.data.size),
840
- contentHash: contentHash(parsed.data.contentHash),
841
- contentType: parsed.data.contentType,
842
- ttlMs: BLOB_RESERVATION_TTL_MS
843
- });
844
- try {
845
- const { blobId, uploadUrl } = await authenticated.stores.blobs.createUpload(reservation);
846
- const response = { blobId, uploadUrl };
847
- return c.json(response, 200);
848
- } catch (error) {
849
- await authenticated.stores.quota.abortReservation(reservationId).catch(() => void 0);
850
- const rendered = renderStorageError(c, error);
851
- if (rendered !== void 0) return rendered;
852
- throw error;
853
- }
854
- } catch (error) {
855
- const rendered = renderStorageError(c, error);
856
- if (rendered !== void 0) return rendered;
857
- throw error;
858
- }
859
- };
860
- }
861
- function finalizeBlobHandler(deps) {
862
- return async (c) => {
863
- const authenticated = await authenticateDevice(c, deps);
864
- if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
865
- const reservationId = idempotencyKey(c);
866
- if (reservationId === void 0) {
867
- return c.json({ error: "Idempotency-Key header is required" }, 400);
868
- }
649
+ let unclaimedInterruption = false;
650
+ if (envelope.type === "task.fail" && envelope.payload.recovery !== void 0) {
651
+ if (envelope.payload.reason !== "daemon_interrupted" || envelope.payload.retryable !== false) return "rejected";
652
+ const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
653
+ if (envelope.payload.recovery.offerId !== messageId) return "rejected";
654
+ const offer = await stores.receipts.get(`task-offer:${messageId}`);
655
+ if (offer === void 0) return "rejected";
656
+ const sealed = JSON.parse(offer.body);
657
+ const harnessId = envelope.payload.harnessId;
658
+ if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
659
+ if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
660
+ unclaimedInterruption = attempt.ownerDeviceId === void 0 && attempt.claimedHarnessId === void 0;
661
+ }
662
+ if ((envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled") && !unclaimedInterruption && envelope.payload.harnessId !== attempt.claimedHarnessId) return "rejected";
663
+ if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
664
+ return "rejected";
665
+ }
666
+ if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
869
667
  try {
870
- const reservation = await authenticated.stores.quota.readReservation(reservationId);
871
- if (reservation === void 0) {
872
- return c.json({ error: "storage_reservation_not_found" }, 404);
873
- }
874
- const blobId = c.req.param("id") ?? "";
875
- const observed = await authenticated.stores.blobs.observeUpload(
876
- blobId,
877
- reservation
668
+ validateActivityBatch(
669
+ {
670
+ taskId,
671
+ sourceEnvelopeId: envelope.id,
672
+ batchSeq: envelope.payload.seq,
673
+ events: envelope.payload.events,
674
+ dropped: 0
675
+ },
676
+ activityBounds
878
677
  );
879
- if (observed === void 0) {
880
- await authenticated.stores.quota.abortReservation(reservationId);
881
- return c.json({ error: "storage_integrity_mismatch" }, 422);
678
+ } catch (caught) {
679
+ if (isCloudError(caught, "activity_batch_too_large") || isCloudError(caught, "coordination_input_invalid")) {
680
+ return "rejected";
882
681
  }
883
- await authenticated.stores.quota.finalizeReservation({
884
- reservationId,
885
- ...observed
886
- });
887
- return c.body(null, 204);
888
- } catch (error) {
889
- const rendered = renderStorageError(c, error);
890
- if (rendered !== void 0) return rendered;
891
- throw error;
682
+ throw caught;
892
683
  }
893
- };
684
+ }
685
+ const approvalInput = approvalTimelineAppendInput(taskId, envelope);
686
+ if (approvalInput !== void 0) {
687
+ try {
688
+ validateApprovalTimelineAppend(approvalInput);
689
+ } catch (caught) {
690
+ if (isCloudError(caught, "coordination_input_invalid")) return "rejected";
691
+ throw caught;
692
+ }
693
+ }
694
+ const lifecycleOutcome = await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
695
+ if (lifecycleOutcome === "rejected") return "rejected";
696
+ return completeInboundEnvelope(stores, deviceId, envelope.id, attempt?.agentRef);
894
697
  }
895
- function blobDownloadUrlHandler(deps) {
896
- return async (c) => {
897
- const authenticated = await authenticateDevice(c, deps);
898
- if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
899
- const downloadUrl = await authenticated.stores.blobs.getDownloadUrl(c.req.param("id") ?? "");
900
- if (downloadUrl === void 0) return c.json({ error: "blob not found" }, 404);
901
- const response = { downloadUrl };
902
- return c.json(response, 200);
903
- };
698
+ async function completeInboundEnvelope(stores, deviceId, envelopeId, agentRef) {
699
+ const duplicate = agentRef === void 0 ? await stores.dedup.checkAndRecord(deviceId, envelopeId) : await stores.dedup.checkAndRecordAgent(deviceId, agentRef, envelopeId);
700
+ return duplicate ? "duplicate" : "accepted";
904
701
  }
905
- function blobUploadContentHandler(deps) {
906
- return async (c) => {
907
- const blobId = c.req.param("id") ?? "";
908
- const query = SignedUrlQuerySchema.safeParse({ sig: c.req.query("sig"), exp: c.req.query("exp") });
909
- if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "put", query.data.sig, query.data.exp)) {
910
- return c.json({ error: "invalid or expired signature" }, 401);
911
- }
912
- const expectedBytes = await deps.contentProxy.expectedUploadBytes(blobId);
913
- if (expectedBytes === void 0) return c.json({ error: "blob not found" }, 404);
914
- const absoluteCeiling = BigInt(deps.maxUploadBytes);
915
- if (expectedBytes > absoluteCeiling) return c.json({ error: "upload exceeds size limit" }, 413);
916
- const contentLength = c.req.header("content-length");
917
- if (contentLength !== void 0 && /^\d+$/u.test(contentLength) && BigInt(contentLength) > expectedBytes) {
918
- return c.json({ error: "upload exceeds declared size" }, 413);
702
+ async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
703
+ switch (envelope.type) {
704
+ case "task.claim": {
705
+ const claimed = await stores.tasks.claim({
706
+ taskId,
707
+ deviceId,
708
+ ...envelope.payload.runtime === void 0 ? {} : { runtime: envelope.payload.runtime },
709
+ ...envelope.payload.harnessId === void 0 ? {} : { harnessId: envelope.payload.harnessId },
710
+ ...envelope.payload.capabilities === void 0 ? {} : { capabilities: envelope.payload.capabilities }
711
+ });
712
+ if (claimed === void 0 || claimed.ownerDeviceId !== deviceId || claimed.claimedRuntime !== envelope.payload.runtime || claimed.claimedHarnessId !== envelope.payload.harnessId) return "rejected";
713
+ return;
919
714
  }
920
- const body = await readBoundedBody(c.req.raw.body, Number(expectedBytes));
921
- if (body.tooLarge) return c.json({ error: "upload exceeds declared size" }, 413);
922
- const result = await deps.contentProxy.writeContent(blobId, body.data);
923
- if (!result.ok) return c.json({ error: result.reason }, 422);
924
- return c.body(null, 204);
925
- };
926
- }
927
- async function readBoundedBody(body, ceiling) {
928
- if (body === null) return { tooLarge: false, data: new Uint8Array() };
929
- const reader = body.getReader();
930
- const data = new Uint8Array(ceiling);
931
- let offset = 0;
932
- try {
933
- for (; ; ) {
934
- const next = await reader.read();
935
- if (next.done) break;
936
- const chunk = next.value;
937
- if (chunk.byteLength > ceiling - offset) {
938
- void reader.cancel().catch(() => void 0);
939
- return { tooLarge: true };
715
+ case "task.started":
716
+ await stores.tasks.recordStatus({
717
+ taskId,
718
+ status: "running",
719
+ ...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
720
+ });
721
+ return;
722
+ case "task.decline":
723
+ await recordTerminal(stores, taskId, envelope);
724
+ return;
725
+ case "task.progress":
726
+ if (envelope.payload.events.length > 0) {
727
+ await appendActivityEvents(
728
+ stores.activity,
729
+ {
730
+ taskId,
731
+ sourceEnvelopeId: envelope.id,
732
+ batchSeq: envelope.payload.seq,
733
+ events: envelope.payload.events,
734
+ dropped: 0
735
+ },
736
+ activityBounds
737
+ );
940
738
  }
941
- data.set(chunk, offset);
942
- offset += chunk.byteLength;
943
- }
944
- } finally {
945
- reader.releaseLock();
739
+ return;
740
+ case "task.await_approval":
741
+ await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
742
+ return;
743
+ case "task.approval_resolved":
744
+ await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
745
+ return;
746
+ case "task.complete":
747
+ await recordTerminal(stores, taskId, envelope);
748
+ return;
749
+ case "task.fail":
750
+ await recordTerminal(stores, taskId, envelope);
751
+ return;
752
+ case "task.cancelled":
753
+ await recordTerminal(stores, taskId, envelope);
754
+ return;
755
+ default:
756
+ return;
946
757
  }
947
- return { tooLarge: false, data: data.subarray(0, offset) };
948
- }
949
- function blobDownloadContentHandler(deps) {
950
- return async (c) => {
951
- const blobId = c.req.param("id") ?? "";
952
- const query = SignedUrlQuerySchema.safeParse({ sig: c.req.query("sig"), exp: c.req.query("exp") });
953
- if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "get", query.data.sig, query.data.exp)) {
954
- return c.json({ error: "invalid or expired signature" }, 401);
955
- }
956
- const result = await deps.contentProxy.readContent(blobId);
957
- if (result === void 0) return c.json({ error: "blob not found" }, 404);
958
- if (!result.ok) return c.json({ error: result.code }, BLOB_READ_ERROR_HTTP_STATUS[result.code]);
959
- return c.body(new Uint8Array(result.content.data), 200, { "content-type": result.content.contentType });
960
- };
961
- }
962
- var BLOB_READ_ERROR_HTTP_STATUS = {
963
- blob_upstream_unavailable: 502,
964
- blob_upstream_stream_interrupted: 502
965
- };
966
- function idempotencyKey(c) {
967
- const value = c.req.header("Idempotency-Key");
968
- if (value === void 0 || value.length === 0 || value.length > 200) return void 0;
969
- return value;
970
758
  }
971
- function renderStorageError(c, error) {
972
- if (!isCoreError(error)) return void 0;
973
- if (STORAGE_ERROR_CODES.includes(error.code)) {
974
- const code = error.code;
975
- return c.json({ error: code }, STORAGE_ERROR_HTTP_STATUS[code]);
976
- }
977
- if (error.code === "storage_reservation_not_found" || error.code === "object_not_found") {
978
- return c.json({ error: error.code }, 404);
979
- }
980
- if (error.code === "storage_entitlement_missing" || error.code === "object_state_invalid") {
981
- return c.json({ error: error.code }, 409);
759
+ function approvalTimelineAppendInput(taskId, envelope) {
760
+ switch (envelope.type) {
761
+ case "task.await_approval":
762
+ return {
763
+ taskId,
764
+ sourceEnvelopeId: envelope.id,
765
+ event: {
766
+ type: "approval_requested",
767
+ summary: envelope.payload.summary,
768
+ ...envelope.payload.approvalId === void 0 ? {} : { approvalId: envelope.payload.approvalId }
769
+ }
770
+ };
771
+ case "task.approval_resolved":
772
+ return {
773
+ taskId,
774
+ sourceEnvelopeId: envelope.id,
775
+ event: {
776
+ type: "approval_resolved",
777
+ approvalId: envelope.payload.approvalId,
778
+ decision: envelope.payload.decision,
779
+ resolvedBy: envelope.payload.resolvedBy,
780
+ at: envelope.payload.at
781
+ }
782
+ };
783
+ default:
784
+ return void 0;
982
785
  }
983
- if (error.code === "content_hash_invalid") {
984
- return c.json({ error: error.code }, 400);
786
+ }
787
+ async function recordTerminal(stores, taskId, envelope) {
788
+ const receipt = await stores.receipts.record({
789
+ key: terminalReceiptKey(taskId),
790
+ body: encodeEnvelope(envelope)
791
+ });
792
+ const winner = terminalReceiptBinding(taskId, receipt.receipt.body);
793
+ if (winner === void 0) {
794
+ throw new Error(`terminal receipt for ${taskId} is not a canonical terminal binding`);
985
795
  }
986
- return void 0;
796
+ const attempt = await stores.tasks.get(taskId);
797
+ if (attempt?.cancellation !== void 0) {
798
+ await stores.tasks.recordStatus({
799
+ taskId,
800
+ status: "cancelled",
801
+ ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
802
+ });
803
+ return;
804
+ }
805
+ const recordedAttempt = await stores.tasks.recordStatus({
806
+ taskId,
807
+ status: winner.status,
808
+ ...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
809
+ ...winner.cause === void 0 ? {} : { terminalCause: winner.cause }
810
+ });
811
+ if (recordedAttempt?.cancellation !== void 0 || recordedAttempt?.status !== winner.status) {
812
+ if (recordedAttempt?.cancellation !== void 0) {
813
+ await stores.tasks.recordStatus({
814
+ taskId,
815
+ status: "cancelled",
816
+ ...recordedAttempt.agentRef === void 0 ? {} : { agentRef: recordedAttempt.agentRef }
817
+ });
818
+ }
819
+ return;
820
+ }
821
+ await projectTerminalToReview(stores.board, taskId);
987
822
  }
988
-
989
- // src/handlers/capabilities.ts
990
- function capabilitiesHandler(deps) {
991
- return async (c) => c.json(deps.declaration, 200);
823
+ function terminalStatus(envelope) {
824
+ switch (envelope.type) {
825
+ case "task.complete":
826
+ return "complete";
827
+ case "task.fail":
828
+ case "task.decline":
829
+ return "failed";
830
+ case "task.cancelled":
831
+ return "cancelled";
832
+ default:
833
+ return void 0;
834
+ }
992
835
  }
993
- var AUTH_JSON_BODY_MAX_BYTES = 16 * 1024;
994
- function pairHandler(deps) {
995
- return async (c) => {
996
- const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
997
- if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
998
- const parsed = PairRequestSchema.safeParse(body.body);
999
- if (!parsed.success) {
1000
- return c.json(
1001
- {
1002
- error: "pairingCode, deviceName, and devicePublicKey are required strings; optional machineId must be 64 lowercase hex characters"
1003
- },
1004
- 400
1005
- );
1006
- }
1007
- const device = await deps.auth.redeemAndRegister(parsed.data);
1008
- if (device === void 0) return c.json({ error: "invalid pairing code" }, 401);
1009
- const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
1010
- const response = PairResponseSchema.parse({
1011
- deviceId: device.deviceId,
1012
- accessToken,
1013
- refreshHint: expiresAt,
1014
- tenantId: device.tenantId
1015
- });
1016
- return c.json(response, 200);
1017
- };
836
+ function terminalReceiptBinding(taskId, body) {
837
+ let envelope;
838
+ try {
839
+ envelope = decodeEnvelope(body);
840
+ } catch {
841
+ return void 0;
842
+ }
843
+ const status = terminalStatus(envelope);
844
+ if (status === void 0 || envelope.task_id !== taskId || encodeEnvelope(envelope) !== body) return void 0;
845
+ const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" || envelope.type === "task.decline" ? envelope.payload.reason : void 0;
846
+ return { status, ...cause === void 0 ? {} : { cause } };
1018
847
  }
1019
- function challengeHandler(deps) {
1020
- return async (c) => {
1021
- const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
1022
- if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
1023
- const parsed = ChallengeRequestSchema.safeParse(body.body);
1024
- if (!parsed.success) return c.json({ error: "deviceId is required" }, 400);
1025
- const device = await deps.auth.resolveDevice(parsed.data.deviceId);
1026
- if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
1027
- const response = { nonce: await deps.auth.issueNonce(device) };
1028
- return c.json(response, 200);
1029
- };
848
+
849
+ // src/task-agent-message.ts
850
+ var FrozenMessageBinding = z.object({
851
+ agentRef: AgentRefSchema,
852
+ sessionRef: z.string().min(1).optional(),
853
+ requirement: AgentMessageEgressRequirementSchema,
854
+ context: AgentMessageServerContextSchema
855
+ }).strict();
856
+ function sameRef(a, b) {
857
+ return a.agentId === b.agentId && a.profileRevision === b.profileRevision;
1030
858
  }
1031
- function tokenHandler(deps) {
1032
- return async (c) => {
1033
- const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
1034
- if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
1035
- const parsed = TokenRequestSchema.safeParse(body.body);
1036
- if (!parsed.success) return c.json({ error: "deviceId, nonce, and signature are required" }, 400);
1037
- const { deviceId, nonce, signature } = parsed.data;
1038
- const device = await deps.auth.resolveDevice(deviceId);
1039
- if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
1040
- if (!await deps.auth.verifySignature(device, nonce, signature)) {
1041
- return c.json({ error: "invalid signature" }, 401);
859
+ async function readTaskAgentMessage(stores, deviceId, taskId, expectedRef) {
860
+ const agentRef = AgentRefSchema.parse(expectedRef);
861
+ const attempt = await stores.tasks.get(taskId);
862
+ if (!attempt || attempt.tenantId !== stores.tenant || attempt.taskId !== taskId || attempt.deviceId !== deviceId || !attempt.agentRef || !sameRef(attempt.agentRef, agentRef)) return void 0;
863
+ const admission = await stores.tasks.readTaskAgentMessage({ taskId, deviceId });
864
+ if (!admission) return void 0;
865
+ const invalid = () => new Error("Invalid persisted task Agent message.");
866
+ const binding = await stores.receipts.get(`agent-message-offer:${deviceId}:${taskId}`);
867
+ if (!binding) throw invalid();
868
+ let body, frozenBody;
869
+ try {
870
+ body = JSON.parse(admission.payloadBody);
871
+ frozenBody = JSON.parse(binding.body);
872
+ } catch {
873
+ throw invalid();
874
+ }
875
+ const parsed = AgentMessagePublishPayloadSchema.safeParse(body);
876
+ const frozen = FrozenMessageBinding.safeParse(frozenBody);
877
+ if (!parsed.success || !frozen.success) throw invalid();
878
+ const payload = parsed.data;
879
+ if (admission.messageId !== payload.messageId || JSON.stringify(payload) !== admission.payloadBody || !sameRef(payload.agentRef, agentRef) || !sameRef(frozen.data.agentRef, agentRef) || payload.contract !== frozen.data.requirement.contract || payload.contentType !== frozen.data.requirement.contentType || frozen.data.sessionRef !== void 0 && payload.sessionRef !== frozen.data.sessionRef || payload.byteCount > frozen.data.requirement.maxBytes) throw invalid();
880
+ const disposition = admission.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
881
+ return { payload, context: frozen.data.context, ...disposition === void 0 ? {} : { disposition } };
882
+ }
883
+ var RecurringExecutionInputSchema = z.object({
884
+ taskId: z.string().min(1),
885
+ deviceId: z.string().min(1),
886
+ payload: TaskOfferForAgentWithEgressFreshPayloadSchema.required({ runtime: true, messageEgress: true, terminalProjection: true }),
887
+ agentMessageContext: AgentMessageServerContextSchema
888
+ }).strict();
889
+
890
+ // src/approval-control.ts
891
+ var StaleApprovalError = class extends Error {
892
+ constructor(taskId, requestedApprovalId, currentApprovalId) {
893
+ super(
894
+ `cannot resolve approval ${requestedApprovalId} for task ${taskId}: the currently pending approval is ${currentApprovalId ?? "(none recorded)"}`
895
+ );
896
+ this.taskId = taskId;
897
+ this.requestedApprovalId = requestedApprovalId;
898
+ this.currentApprovalId = currentApprovalId;
899
+ this.name = "StaleApprovalError";
900
+ }
901
+ taskId;
902
+ requestedApprovalId;
903
+ currentApprovalId;
904
+ };
905
+ function pendingApproval(tail) {
906
+ if (tail === void 0) return void 0;
907
+ let pending;
908
+ for (const entry of tail.entries) {
909
+ const event = entry.event;
910
+ if (event.type === "approval_requested") {
911
+ pending = {
912
+ sourceEnvelopeId: entry.sourceEnvelopeId,
913
+ revision: entry.revision,
914
+ ...event.approvalId === void 0 ? {} : { approvalId: event.approvalId }
915
+ };
916
+ continue;
1042
917
  }
1043
- if (!await deps.auth.consumeNonceIfValid(device, nonce)) {
1044
- return c.json({ error: "invalid, expired, or already-used nonce" }, 401);
918
+ if (pending === void 0) continue;
919
+ if (pending.approvalId === void 0 || pending.approvalId === event.approvalId) {
920
+ pending = void 0;
1045
921
  }
1046
- const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
1047
- const response = { accessToken, expiresAt };
1048
- return c.json(response, 200);
1049
- };
922
+ }
923
+ return pending;
1050
924
  }
1051
- var CLOUD_PROTOCOL_CAPABILITIES = [
1052
- "custom-harness",
1053
- MAILBOX_READ_AHEAD_CAPABILITY,
1054
- "result-document",
1055
- // The kernel's inbound handles `task.approval_resolved` on both the plain
1056
- // and the approval-timeline paths (`packages/cloud/src/inbound.ts:576,610`).
1057
- // Without this flag `TaskRunner.sendApprovalResolved` returns early and a
1058
- // locally-resolved approval is never reported on the wire at all.
1059
- "approval_resolved",
1060
- "agent-home-contract",
1061
- "agent-home-projection",
1062
- "agent-egress-policy",
1063
- "agent-egress-reliable-ack",
1064
- "agent-egress-fresh-session",
1065
- // `handleAgentMessagePublish` accepts `agent.message_egress` gated on exactly
1066
- // this capability (`packages/cloud/src/inbound.ts:124`) — the same inbound
1067
- // family as the three egress flags above, omitted by the same drift.
1068
- "agent-message-egress",
1069
- "agent-content-workspace-read",
1070
- "agent-content-transcript-read",
1071
- "agent-content-artifact-read"
1072
- ];
1073
- function sleep(ms) {
1074
- return new Promise((resolve) => setTimeout(resolve, ms));
925
+
926
+ // src/steer-control.ts
927
+ function steerRejectionMessage(taskId, code, status, runtime) {
928
+ switch (code) {
929
+ case "task_terminal":
930
+ return `cannot steer task ${taskId}: task is already terminal (status ${status})`;
931
+ case "task_not_running":
932
+ return `cannot steer task ${taskId}: not running (status ${status})`;
933
+ case "steer_unsupported_runtime":
934
+ return `cannot steer task ${taskId}: claimed runtime ${runtime ?? "(unknown)"} does not support steering`;
935
+ }
1075
936
  }
1076
- function eventsHandler(deps) {
1077
- return async (c) => {
1078
- const authenticated = await authenticateDevice(c, deps);
1079
- if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1080
- const { device, stores } = authenticated;
1081
- const cursorRaw = c.req.query("cursor");
1082
- let cursor = 0;
1083
- if (cursorRaw !== void 0) {
1084
- const parsedCursor = Number(cursorRaw);
1085
- if (!Number.isSafeInteger(parsedCursor) || parsedCursor < 0) {
1086
- return c.json({ error: "invalid cursor" }, 400);
1087
- }
1088
- cursor = parsedCursor;
1089
- }
1090
- const afterRaw = c.req.query("afterSeq");
1091
- const afterSeq = afterRaw === void 0 ? cursor : Number(afterRaw);
1092
- if (!Number.isSafeInteger(afterSeq) || afterSeq < cursor || afterSeq - cursor > MAILBOX_READ_AHEAD_MAX_SEQS) {
1093
- return c.json({ error: "invalid read-ahead offset" }, 400);
1094
- }
1095
- const acked = await stores.mailbox.readCursor(device.deviceId);
1096
- if (afterRaw !== void 0 && afterSeq > acked.deliveredSeq) return c.json({ error: "read offset exceeds delivered watermark" }, 409);
1097
- if (cursor > acked.ackedSeq) {
1098
- try {
1099
- await stores.mailbox.advanceCursor({ deviceId: device.deviceId, ackedSeq: cursor });
1100
- } catch (caught) {
1101
- if (isCoreConflictError(caught, "mailbox_cursor_ahead_of_delivery")) {
1102
- return c.json({ error: "cursor exceeds delivered watermark" }, 409);
1103
- }
1104
- throw caught;
1105
- }
1106
- }
1107
- const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
1108
- for (let attempt = 0; attempt < attempts; attempt += 1) {
1109
- let scanCursor = afterSeq;
1110
- while (true) {
1111
- const page = await stores.mailbox.readAfter({
1112
- deviceId: device.deviceId,
1113
- afterSeq: scanCursor,
1114
- limit: deps.pageLimit
1115
- });
1116
- if (cursor < page.recoverableFrom - 1) {
1117
- return c.json({ error: "cursor_too_old", recoverableFrom: page.recoverableFrom }, 409);
1118
- }
1119
- if (page.messages.length === 0) break;
1120
- const decoded = page.messages.map((message) => decodeEnvelope(message.body));
1121
- const offeredTaskIds = decoded.flatMap(
1122
- (event) => (event.type === "task.offer" || event.type === "task.offer_with_toolsets" || event.type === "task.offer_for_agent" || event.type === "task.offer_for_agent_with_egress" || event.type === "task.offer_for_agent_with_egress_fresh") && event.task_id !== void 0 ? [event.task_id] : []
1123
- );
1124
- const attemptsByTaskId = Object.fromEntries(
1125
- (await stores.tasks.getMany(offeredTaskIds)).map((attempt2) => [attempt2.taskId, attempt2])
1126
- );
1127
- const events = decoded.filter((event) => {
1128
- if (event.type !== "task.offer" && event.type !== "task.offer_with_toolsets" && event.type !== "task.offer_for_agent" && event.type !== "task.offer_for_agent_with_egress" && event.type !== "task.offer_for_agent_with_egress_fresh") {
1129
- return true;
1130
- }
1131
- return event.task_id === void 0 || attemptsByTaskId[event.task_id]?.cancellation === void 0;
1132
- });
1133
- if (events.length === 0 && page.hasMore) {
1134
- scanCursor = page.nextSeq;
1135
- continue;
1136
- }
1137
- if (events.length === 0) break;
1138
- const response2 = {
1139
- events,
1140
- cursor: page.nextSeq,
1141
- capabilities: CLOUD_PROTOCOL_CAPABILITIES
1142
- };
1143
- await stores.mailbox.recordDelivery({
1144
- deviceId: device.deviceId,
1145
- deliveredSeq: response2.cursor
1146
- });
1147
- return c.json(response2, 200);
937
+ var SteerRejectedError = class extends Error {
938
+ constructor(taskId, code, status, runtime) {
939
+ super(steerRejectionMessage(taskId, code, status, runtime));
940
+ this.taskId = taskId;
941
+ this.code = code;
942
+ this.status = status;
943
+ this.runtime = runtime;
944
+ this.name = "SteerRejectedError";
945
+ }
946
+ taskId;
947
+ code;
948
+ status;
949
+ runtime;
950
+ };
951
+
952
+ // src/crypto/web-crypto.ts
953
+ var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
954
+ var HEX_DIGITS = "0123456789abcdef";
955
+ function base64UrlEncode(bytes) {
956
+ let binary = "";
957
+ for (const byte of bytes) binary += String.fromCharCode(byte);
958
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
959
+ }
960
+ function base64UrlDecode(value) {
961
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/");
962
+ try {
963
+ const binary = atob(padded);
964
+ const bytes = new Uint8Array(binary.length);
965
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
966
+ return bytes;
967
+ } catch {
968
+ return void 0;
969
+ }
970
+ }
971
+ function toHex(bytes) {
972
+ let out = "";
973
+ for (const byte of bytes) {
974
+ out += HEX_DIGITS[byte >> 4 & 15];
975
+ out += HEX_DIGITS[byte & 15];
976
+ }
977
+ return out;
978
+ }
979
+ function subtle() {
980
+ const webCrypto = globalThis.crypto;
981
+ if (webCrypto?.subtle === void 0) {
982
+ throw new Error("@byok-sdk/cloud requires a WebCrypto implementation on globalThis.crypto");
983
+ }
984
+ return webCrypto.subtle;
985
+ }
986
+ function createWebCrypto() {
987
+ return {
988
+ randomUuid() {
989
+ return globalThis.crypto.randomUUID();
990
+ },
991
+ randomToken(byteLength) {
992
+ return base64UrlEncode(globalThis.crypto.getRandomValues(new Uint8Array(byteLength)));
993
+ },
994
+ randomPairingCode(length) {
995
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(length));
996
+ let out = "";
997
+ for (const byte of bytes) {
998
+ out += PAIRING_CODE_ALPHABET[byte % PAIRING_CODE_ALPHABET.length];
1148
999
  }
1149
- if (attempt < attempts - 1) await sleep(deps.longPollIntervalMs);
1000
+ return out;
1001
+ },
1002
+ async verifyEd25519(publicKeyBase64Url, message, signature) {
1003
+ const signatureBytes = base64UrlDecode(signature);
1004
+ if (signatureBytes === void 0) return false;
1005
+ try {
1006
+ const key = await subtle().importKey(
1007
+ "jwk",
1008
+ { kty: "OKP", crv: "Ed25519", x: publicKeyBase64Url },
1009
+ { name: "Ed25519" },
1010
+ false,
1011
+ ["verify"]
1012
+ );
1013
+ const messageBytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
1014
+ return await subtle().verify({ name: "Ed25519" }, key, signatureBytes, messageBytes);
1015
+ } catch {
1016
+ return false;
1017
+ }
1018
+ },
1019
+ async hmacSha256(secret, message) {
1020
+ const key = await subtle().importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1021
+ const signature = await subtle().sign("HMAC", key, new TextEncoder().encode(message));
1022
+ return base64UrlEncode(new Uint8Array(signature));
1023
+ },
1024
+ async sha256(data) {
1025
+ const digest = await subtle().digest("SHA-256", data);
1026
+ return `sha256:${toHex(new Uint8Array(digest))}`;
1027
+ },
1028
+ timingSafeEqual(left, right) {
1029
+ const leftBytes = base64UrlDecode(left);
1030
+ const rightBytes = base64UrlDecode(right);
1031
+ if (leftBytes === void 0 || rightBytes === void 0) return false;
1032
+ if (leftBytes.length !== rightBytes.length) return false;
1033
+ let diff = 0;
1034
+ for (let index = 0; index < leftBytes.length; index += 1) {
1035
+ diff |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
1036
+ }
1037
+ return diff === 0;
1150
1038
  }
1151
- const response = {
1152
- events: [],
1153
- cursor: afterSeq,
1154
- capabilities: CLOUD_PROTOCOL_CAPABILITIES
1155
- };
1156
- return c.json(response, 200);
1157
1039
  };
1158
1040
  }
1159
- var DEFAULT_APPROVAL_TIMELINE_CAPACITY = 50;
1160
- var DEFAULT_APPROVAL_TIMELINE_TTL_MS = 10 * 60 * 1e3;
1161
- var APPROVAL_SUMMARY_MAX_BYTES = 16 * 1024;
1162
- var NonBlankIdSchema = z.string().max(200).regex(/\S/, "value must not be blank");
1163
- var TaskIdSchema = z.string().min(1).max(200);
1164
- var ApprovalRequestedEventSchema = z.object({
1165
- type: z.literal("approval_requested"),
1166
- summary: z.string(),
1167
- approvalId: NonBlankIdSchema.optional()
1168
- }).strict();
1169
- var LocalApprovalResolutionEventSchema = z.object({
1170
- type: z.literal("approval_resolved"),
1171
- // Absent only when a daemon resolved the single outstanding request without
1172
- // native approval identity. No id is synthesized.
1173
- approvalId: NonBlankIdSchema.optional(),
1174
- decision: z.enum(["approve", "reject"]),
1175
- // A daemon's `task.approval_resolved` is pure wire observation; it cannot
1176
- // declare a host command payload.
1177
- resolvedBy: z.literal("local"),
1178
- at: z.iso.datetime({ offset: true })
1179
- }).strict();
1180
- var HostApprovalResolutionEventSchema = z.object({
1181
- type: z.literal("approval_resolved"),
1182
- approvalId: NonBlankIdSchema.optional(),
1183
- decision: z.literal("approve"),
1184
- resolvedBy: z.literal("host"),
1185
- at: z.iso.datetime({ offset: true })
1186
- }).strict();
1187
- var HostRejectResolutionEventSchema = z.object({
1188
- type: z.literal("approval_resolved"),
1189
- approvalId: NonBlankIdSchema.optional(),
1190
- decision: z.literal("reject"),
1191
- resolvedBy: z.literal("host"),
1192
- // Required even when the host deliberately omitted a wire reason. `null`
1193
- // records that complete payload, so a historical host reject with no field
1194
- // cannot be mistaken for a recoverable executable command.
1195
- reason: z.string().nullable(),
1196
- at: z.iso.datetime({ offset: true })
1197
- }).strict();
1198
- var ApprovalTimelineEventSchema = z.union([
1199
- ApprovalRequestedEventSchema,
1200
- LocalApprovalResolutionEventSchema,
1201
- HostApprovalResolutionEventSchema,
1202
- HostRejectResolutionEventSchema
1203
- ]);
1204
- var ApprovalObservationSchema = z.object({
1205
- taskId: TaskIdSchema,
1206
- sourceEnvelopeId: NonBlankIdSchema,
1207
- revision: z.number().int().positive(),
1208
- receivedAt: z.iso.datetime(),
1209
- event: ApprovalTimelineEventSchema
1210
- });
1211
- function validateApprovalTimelineAppend(input) {
1212
- const capacity = input.capacity ?? DEFAULT_APPROVAL_TIMELINE_CAPACITY;
1213
- const ttlMs = input.ttlMs ?? DEFAULT_APPROVAL_TIMELINE_TTL_MS;
1214
- if (!Number.isSafeInteger(capacity) || capacity <= 0) {
1215
- throw new ByokCloudError(
1216
- "coordination_input_invalid",
1217
- `Approval timeline capacity must be a positive integer, received ${String(capacity)}.`
1218
- );
1219
- }
1220
- if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
1221
- throw new ByokCloudError(
1222
- "coordination_input_invalid",
1223
- `Approval timeline ttl must be a positive number of milliseconds, received ${String(ttlMs)}.`
1224
- );
1225
- }
1226
- const parsed = z.object({
1227
- taskId: TaskIdSchema,
1228
- sourceEnvelopeId: NonBlankIdSchema,
1229
- event: ApprovalTimelineEventSchema
1230
- }).safeParse(input);
1231
- if (!parsed.success) {
1232
- throw new ByokCloudError(
1233
- "coordination_input_invalid",
1234
- "Approval observations require stable source identity and a valid native lifecycle event."
1235
- );
1041
+
1042
+ // src/auth/tokens.ts
1043
+ var ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
1044
+ function encodeSegment(value) {
1045
+ return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
1046
+ }
1047
+ function createHmacTokenSigner(secret, clock) {
1048
+ async function signingKey(usage) {
1049
+ return globalThis.crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, [usage]);
1236
1050
  }
1237
- if (parsed.data.event.type === "approval_requested" && new TextEncoder().encode(parsed.data.event.summary).byteLength > APPROVAL_SUMMARY_MAX_BYTES) {
1238
- throw new ByokCloudError(
1239
- "coordination_input_invalid",
1240
- `Approval summary exceeds ${APPROVAL_SUMMARY_MAX_BYTES} UTF-8 bytes.`
1241
- );
1051
+ async function signature(signingInput) {
1052
+ const key = await signingKey("sign");
1053
+ const bytes = await globalThis.crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signingInput));
1054
+ return base64UrlEncode(new Uint8Array(bytes));
1242
1055
  }
1243
- return { capacity, ttlMs, event: parsed.data.event };
1056
+ return {
1057
+ async sign(claims, expiresInSeconds) {
1058
+ const issuedAt = Math.floor(clock.now().getTime() / 1e3);
1059
+ const header = encodeSegment({ alg: "HS256", typ: "JWT" });
1060
+ const payload = encodeSegment({
1061
+ deviceId: claims.deviceId,
1062
+ tenantId: claims.tenantId,
1063
+ productId: claims.productId,
1064
+ iat: issuedAt,
1065
+ exp: issuedAt + expiresInSeconds
1066
+ });
1067
+ const signingInput = `${header}.${payload}`;
1068
+ return `${signingInput}.${await signature(signingInput)}`;
1069
+ },
1070
+ async verify(token) {
1071
+ const parts = token.split(".");
1072
+ if (parts.length !== 3) return void 0;
1073
+ const [header, payload, provided] = parts;
1074
+ const key = await signingKey("verify");
1075
+ const providedBytes = base64UrlDecode(provided);
1076
+ if (providedBytes === void 0) return void 0;
1077
+ const ok = await globalThis.crypto.subtle.verify(
1078
+ "HMAC",
1079
+ key,
1080
+ providedBytes,
1081
+ new TextEncoder().encode(`${header}.${payload}`)
1082
+ );
1083
+ if (!ok) return void 0;
1084
+ const headerBytes = base64UrlDecode(header);
1085
+ const payloadBytes = base64UrlDecode(payload);
1086
+ if (headerBytes === void 0 || payloadBytes === void 0) return void 0;
1087
+ let decodedHeader;
1088
+ let decodedPayload;
1089
+ try {
1090
+ decodedHeader = JSON.parse(new TextDecoder().decode(headerBytes));
1091
+ decodedPayload = JSON.parse(new TextDecoder().decode(payloadBytes));
1092
+ } catch {
1093
+ return void 0;
1094
+ }
1095
+ const alg = decodedHeader?.alg;
1096
+ if (alg !== "HS256") return void 0;
1097
+ const { deviceId, tenantId: tenantId4, productId, exp } = decodedPayload;
1098
+ if (typeof deviceId !== "string" || typeof tenantId4 !== "string" || typeof productId !== "string") {
1099
+ return void 0;
1100
+ }
1101
+ if (typeof exp !== "number" || clock.now().getTime() / 1e3 >= exp) return void 0;
1102
+ return { deviceId, tenantId: tenantId4, productId };
1103
+ }
1104
+ };
1244
1105
  }
1245
- function validateApprovalTimelineResolve(input) {
1246
- const validated = validateApprovalTimelineAppend(input);
1247
- const expected = z.object({
1248
- expectedSourceEnvelopeId: NonBlankIdSchema,
1249
- expectedRevision: z.number().int().positive()
1250
- }).safeParse(input);
1251
- if (!expected.success || validated.event.type !== "approval_resolved" || validated.event.resolvedBy !== "host") {
1252
- throw new ByokCloudError(
1253
- "coordination_input_invalid",
1254
- "Host approval resolution requires an exact pending source/revision and a host resolution event."
1255
- );
1256
- }
1257
- return validated;
1106
+ function verifyNonceSignature(crypto2, devicePublicKey, nonce, signature) {
1107
+ return crypto2.verifyEd25519(devicePublicKey, NONCE_SIGNING_DOMAIN + nonce, signature);
1258
1108
  }
1259
- function approvalTimelineKey(tenant, taskId) {
1260
- return tenantKey(tenant, taskId);
1109
+
1110
+ // src/auth/plane.ts
1111
+ var PAIRING_CODE_TTL_MS = 10 * 60 * 1e3;
1112
+ var PAIRING_CODE_LENGTH = 8;
1113
+ var DEVICE_IDENTITY_PROOF_KEY_ID = "identity";
1114
+ var DEVICE_IDENTITY_PROOF_KEY_EPOCH = 0;
1115
+ function createAuthPlane(deps) {
1116
+ const { stores, crypto: crypto2, clock, tokenSigner } = deps;
1117
+ const ttlSeconds = deps.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS;
1118
+ return {
1119
+ async createPairingCode(tenant, input) {
1120
+ const expiresAt = new Date(clock.now().getTime() + (input.ttlMs ?? PAIRING_CODE_TTL_MS)).toISOString();
1121
+ return stores.pairingCodes.issue(tenant, {
1122
+ code: crypto2.randomPairingCode(PAIRING_CODE_LENGTH),
1123
+ productId: input.productId,
1124
+ expiresAt
1125
+ });
1126
+ },
1127
+ async redeemAndRegister(input) {
1128
+ return stores.pairing.redeemAndRegister({
1129
+ pairingCode: input.pairingCode,
1130
+ deviceId: `dev_${crypto2.randomUuid()}`,
1131
+ deviceName: input.deviceName,
1132
+ devicePublicKey: input.devicePublicKey,
1133
+ proofKeyId: DEVICE_IDENTITY_PROOF_KEY_ID,
1134
+ proofKeyEpoch: DEVICE_IDENTITY_PROOF_KEY_EPOCH,
1135
+ ...input.machineId === void 0 ? {} : { machineId: input.machineId }
1136
+ });
1137
+ },
1138
+ async resolveDevice(deviceId) {
1139
+ const device = await stores.devices.resolveByDeviceId(deviceId);
1140
+ if (device === void 0 || device.revoked) return void 0;
1141
+ return device;
1142
+ },
1143
+ issueNonce(device) {
1144
+ return stores.nonces.issue(device.tenantId, device.deviceId);
1145
+ },
1146
+ consumeNonceIfValid(device, nonce) {
1147
+ return stores.nonces.consumeIfValid(device.tenantId, device.deviceId, nonce);
1148
+ },
1149
+ verifySignature(device, nonce, signature) {
1150
+ return verifyNonceSignature(crypto2, device.devicePublicKey, nonce, signature);
1151
+ },
1152
+ async mintAccessToken(device) {
1153
+ const accessToken = await tokenSigner.sign(
1154
+ { deviceId: device.deviceId, tenantId: device.tenantId, productId: device.productId },
1155
+ ttlSeconds
1156
+ );
1157
+ return {
1158
+ accessToken,
1159
+ expiresAt: new Date(clock.now().getTime() + ttlSeconds * 1e3).toISOString()
1160
+ };
1161
+ }
1162
+ };
1261
1163
  }
1262
- function parseApprovalObservations(value) {
1263
- return z.array(ApprovalObservationSchema).parse(value);
1164
+ var CLOUD_CAPABILITIES = {
1165
+ /** `GET /byok/events` long-poll receive (§8). */
1166
+ eventsLongPoll: "events.longpoll",
1167
+ /** `POST /byok/messages` batched send (§8.2). */
1168
+ messagesBatch: "messages.batch",
1169
+ /** The three bearer-authed blob routes: reserve/grant, explicit finalize, committed-only download (§7). */
1170
+ blobsPresigned: "blobs.presigned",
1171
+ /**
1172
+ * The two presigned `/byok/blobs/:id/content` routes — cloud carrying the
1173
+ * bytes itself.
1174
+ *
1175
+ * Split out of `blobs.presigned` because it was one capability describing two
1176
+ * separable facts. A composition whose bytes live in object storage mints
1177
+ * grants (`blobs.presigned`) but has no byte-proxy path at all, and saying so
1178
+ * by declaration is ADR-010's whole posture: a client reads what a deployment
1179
+ * serves, it never probes a `/content` route and interprets the status code.
1180
+ *
1181
+ * Spelled all-lowercase because core's declaration schema pins capability
1182
+ * names to `/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/` — the same reason the sibling
1183
+ * above reads `events.longpoll` and not `events.longPoll`.
1184
+ */
1185
+ blobsContentProxy: "blobs.contentproxy",
1186
+ /** Board list/claim/unclaim/status routes. Polling is first-class under this declaration. */
1187
+ boardCoordination: "board.coordination",
1188
+ /** Additional SSE transport for the same board read model. */
1189
+ boardSse: "board.sse",
1190
+ /** Device-scoped five-level presence publication. */
1191
+ presenceHints: "presence.hints",
1192
+ /** Bounded task activity batch publication. */
1193
+ activityTail: "activity.tail",
1194
+ /** Request-bound device proof record manifest/read/write surface (S6). */
1195
+ truthRecords: "truth.records",
1196
+ /**
1197
+ * Tenant-scoped skill pack distribution: the manifest list plus the per-file
1198
+ * content route a paired device installs from.
1199
+ *
1200
+ * Hosted HTTP on purpose. A pack is DECLARATIVE CONTENT a device pulls after
1201
+ * reading this declaration — not a message — so `@byok-sdk/protocol` gains
1202
+ * nothing from it and the frozen v1 envelope stays untouched. Composition-
1203
+ * bound like `truth.records`: a deployment that declares it without supplying
1204
+ * a `SkillPackStore` is refused at construction rather than publishing two
1205
+ * routes it would then 404.
1206
+ */
1207
+ skillPacks: "skills.pack",
1208
+ /** Optional, one-way redacted Agent-memory snapshot mutation route. */
1209
+ agentMemoryProjection: "agent.memory.projection",
1210
+ /**
1211
+ * Remote authenticated runtime input preparation (C07 G4): the host control
1212
+ * plane enqueues `agent.input.preparation` and the device discharges it
1213
+ * through the completion/status pair.
1214
+ *
1215
+ * Informational like `host-mcp-task-context`, not route-gating. Both routes
1216
+ * mount unconditionally (`cloud.ts`) because the completion route is the
1217
+ * DEVICE's only way to discharge a mailbox row this same deployment handed
1218
+ * it — withholding it would strand the device's redelivery cursor rather
1219
+ * than degrade a feature. For the same reason the completion route asserts
1220
+ * no DEVICE capability either: the device-level `agent-input-preparation`
1221
+ * flag gates ADMISSION (`enqueueInputPreparation`) and nothing else, so an
1222
+ * unconfigured device is refused a row instead of being handed one whose
1223
+ * only honest answer — `input_preparation_unconfigured` — it could not then
1224
+ * record. What declaring THIS capability promises is that this deployment's
1225
+ * host side actually drives the lane, which is what a Host reads before
1226
+ * calling `enqueueInputPreparation` instead of probing.
1227
+ */
1228
+ inputPreparation: "agent.input.preparation",
1229
+ /**
1230
+ * Contract §8.1's capability gate for task-scoped tool authority
1231
+ * (`byok-task-assertion-v1`), deployment-level channel.
1232
+ *
1233
+ * The SAME literal name `@byok-sdk/protocol`'s
1234
+ * `HOST_MCP_TASK_CONTEXT_CAPABILITY` carries on the device-level channel —
1235
+ * §8.1 requires both to be in place, and a name spelled differently in the
1236
+ * two would make "both" unverifiable.
1237
+ *
1238
+ * Unlike its siblings above this capability mounts no route in this package:
1239
+ * what a deployment promises by declaring it is that its host side actually
1240
+ * VERIFIES task assertions (`authenticateHostedTaskAssertion` plus the frozen
1241
+ * offer check that only the host can make). That is why it is default-off
1242
+ * below and not derivable from anything this package can inspect — §8.2(1):
1243
+ * the capability is declared only once it is completely implemented.
1244
+ */
1245
+ hostMcpTaskContext: "host-mcp-task-context"
1246
+ };
1247
+ var CapabilitiesResponseSchema = CapabilityDeclarationSchema;
1248
+ function fullCapabilityDeclaration(version = 1, options = {}) {
1249
+ return {
1250
+ schema: "byok-capabilities-v1",
1251
+ version,
1252
+ capabilities: Object.values(CLOUD_CAPABILITIES).filter((capability) => {
1253
+ if (capability === CLOUD_CAPABILITIES.truthRecords) return options.includeTruthRecords === true;
1254
+ if (capability === CLOUD_CAPABILITIES.skillPacks) return options.includeSkillPacks === true;
1255
+ if (capability === CLOUD_CAPABILITIES.agentMemoryProjection) {
1256
+ return options.includeAgentMemoryProjection === true;
1257
+ }
1258
+ if (capability === CLOUD_CAPABILITIES.hostMcpTaskContext) {
1259
+ return options.includeHostMcpTaskContext === true;
1260
+ }
1261
+ return true;
1262
+ })
1263
+ };
1264
1264
  }
1265
- function approvalTimelineCursor(entries) {
1266
- return entries.at(-1)?.revision;
1265
+ function declares(declaration, capability) {
1266
+ return hasCapability(declaration, capability);
1267
+ }
1268
+ function extractBearerToken(header) {
1269
+ if (header === void 0) return void 0;
1270
+ const match = /^Bearer\s+(.+)$/i.exec(header);
1271
+ return match?.[1];
1272
+ }
1273
+ async function authenticateBearer(header, deps) {
1274
+ const token = extractBearerToken(header);
1275
+ if (token === void 0) return void 0;
1276
+ const claims = await deps.tokenSigner.verify(token);
1277
+ if (claims === void 0) return void 0;
1278
+ if (!isTenantId(claims.tenantId)) return void 0;
1279
+ const device = await deps.devices.get(tenantId(claims.tenantId), claims.deviceId);
1280
+ if (device === void 0 || device.revoked) return void 0;
1281
+ if (device.productId !== claims.productId) return void 0;
1282
+ if (deps.instanceProductId !== void 0 && claims.productId !== deps.instanceProductId) {
1283
+ return void 0;
1284
+ }
1285
+ return {
1286
+ kind: "device",
1287
+ tenantId: device.tenantId,
1288
+ productId: device.productId,
1289
+ deviceId: device.deviceId
1290
+ };
1291
+ }
1292
+ function tenantStoresFor(principal, root) {
1293
+ const tenant = principalTenant(principal);
1294
+ const { core, cloud } = root;
1295
+ return {
1296
+ tenant,
1297
+ principal,
1298
+ mailbox: {
1299
+ append: (input) => core.mailbox.append(tenant, input),
1300
+ readAfter: (query) => core.mailbox.readAfter(tenant, query),
1301
+ recordDelivery: (input) => core.mailbox.recordDelivery(tenant, input),
1302
+ advanceCursor: (input) => core.mailbox.advanceCursor(tenant, input),
1303
+ readCursor: (deviceId) => core.mailbox.readCursor(tenant, deviceId)
1304
+ },
1305
+ board: {
1306
+ create: (input) => core.board.create(tenant, input),
1307
+ get: (itemId) => core.board.get(tenant, itemId),
1308
+ list: (query) => core.board.list(tenant, query),
1309
+ claim: (input) => core.board.claim(tenant, input),
1310
+ unclaim: (input) => core.board.unclaim(tenant, input),
1311
+ updateStatus: (input) => core.board.updateStatus(tenant, input)
1312
+ },
1313
+ presence: {
1314
+ publish: (input) => core.presence.publish(tenant, input),
1315
+ read: (deviceId) => core.presence.read(tenant, deviceId),
1316
+ list: () => core.presence.list(tenant)
1317
+ },
1318
+ activity: {
1319
+ append: (input) => cloud.activity.append(tenant, input),
1320
+ read: (taskId) => cloud.activity.read(tenant, taskId)
1321
+ },
1322
+ approvals: {
1323
+ append: (input) => cloud.approvals.append(tenant, input),
1324
+ read: (taskId) => cloud.approvals.read(tenant, taskId)
1325
+ },
1326
+ devices: {
1327
+ get: (deviceId) => cloud.devices.get(tenant, deviceId),
1328
+ list: () => cloud.devices.list(tenant),
1329
+ revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId),
1330
+ recordCapabilities: (input) => principal.kind === "device" ? cloud.devices.recordCapabilities(tenant, {
1331
+ deviceId: principal.deviceId,
1332
+ capabilities: input.capabilities,
1333
+ ...input.harnesses === void 0 ? {} : { harnesses: input.harnesses }
1334
+ }) : Promise.resolve(void 0),
1335
+ readiness: () => cloud.devices.readiness(tenant, core.presence)
1336
+ },
1337
+ tasks: {
1338
+ open: (input) => cloud.tasks.open(tenant, input),
1339
+ reserveAgentOffer: (input) => cloud.tasks.reserveAgentOffer(tenant, input),
1340
+ reserveAgentMessage: (input) => cloud.tasks.reserveAgentMessage(tenant, input),
1341
+ readTaskAgentMessage: (input) => cloud.tasks.readTaskAgentMessage(tenant, input),
1342
+ readAgentMessage: (input) => cloud.tasks.readAgentMessage(tenant, input),
1343
+ finalizeAgentMessage: (input) => cloud.tasks.finalizeAgentMessage(tenant, input),
1344
+ get: (taskId) => cloud.tasks.get(tenant, taskId),
1345
+ getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
1346
+ list: (query) => cloud.tasks.list(tenant, query),
1347
+ claim: (input) => cloud.tasks.claim(tenant, input),
1348
+ recordStatus: (input) => cloud.tasks.recordStatus(tenant, input)
1349
+ },
1350
+ cancellations: {
1351
+ request: (input) => cloud.cancellations.request(tenant, input)
1352
+ },
1353
+ dedup: {
1354
+ checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId),
1355
+ checkAndRecordAgent: (deviceId, agentRef, envelopeId) => cloud.dedup.checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId)
1356
+ },
1357
+ receipts: {
1358
+ record: (input) => cloud.receipts.record(tenant, input),
1359
+ get: (key) => cloud.receipts.get(tenant, key)
1360
+ },
1361
+ egress: {
1362
+ record: (input) => cloud.egress.record(tenant, input),
1363
+ get: (deviceId, agentRef, eventId) => cloud.egress.get(tenant, deviceId, agentRef, eventId)
1364
+ },
1365
+ blobs: {
1366
+ createUpload: (reservation) => cloud.blobs.createUpload(tenant, reservation),
1367
+ observeUpload: (blobId, reservation) => cloud.blobs.observeUpload(tenant, blobId, reservation),
1368
+ getDownloadUrl: (blobId) => cloud.blobs.getDownloadUrl(tenant, blobId)
1369
+ },
1370
+ quota: {
1371
+ readReservation: (reservationId) => core.quota.readReservation(tenant, reservationId),
1372
+ reserve: (input) => core.quota.reserve(tenant, input),
1373
+ finalizeReservation: (input) => core.quota.finalizeReservation(tenant, input),
1374
+ abortReservation: (reservationId) => core.quota.abortReservation(tenant, reservationId)
1375
+ },
1376
+ rateLimiter: {
1377
+ consume: (deviceId) => cloud.rateLimiter.consume(tenant, deviceId)
1378
+ }
1379
+ };
1267
1380
  }
1268
- async function projectTerminalToReview(board, taskId) {
1269
- const item = await board.get(taskId);
1270
- if (item === void 0 || item.status !== "in_progress") return;
1381
+
1382
+ // src/handlers/shared.ts
1383
+ async function readJsonBody(c) {
1271
1384
  try {
1272
- await board.updateStatus({
1273
- itemId: taskId,
1274
- expectedStatus: "in_progress",
1275
- status: "in_review"
1276
- });
1277
- } catch (caught) {
1278
- if (isCoreConflictError(caught)) return;
1279
- throw caught;
1385
+ return await c.req.json();
1386
+ } catch {
1387
+ return void 0;
1280
1388
  }
1281
1389
  }
1282
- function agentReliabilityKey(domain, deviceId, agentRef, id) {
1283
- const exact = AgentRefSchema.parse(agentRef);
1284
- return `${domain}:${JSON.stringify([deviceId, exact.agentId, exact.profileRevision, id])}`;
1285
- }
1286
-
1287
- // src/inbound.ts
1288
- async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume, alreadyDeduplicated) {
1289
- const device = await stores.devices.get(deviceId);
1290
- const attempt = await stores.tasks.get(taskId);
1291
- const binding = await stores.receipts.get(`agent-message-offer:${deviceId}:${taskId}`);
1292
- if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_MESSAGE_EGRESS_CAPABILITY]) || attempt === void 0 || attempt.deviceId !== deviceId || binding === void 0 || attempt.agentRef === void 0 || !sameAgentRef(attempt.agentRef, payload.agentRef)) return { outcome: "rejected" };
1293
- let frozen;
1390
+ async function readBoundedJsonBody(c, maximum) {
1391
+ const declaredLength = c.req.header("content-length");
1392
+ if (declaredLength !== void 0) {
1393
+ const parsedLength = parseContentLength(declaredLength);
1394
+ if (parsedLength !== void 0 && parsedLength > BigInt(maximum)) {
1395
+ cancelBody(c.req.raw.body);
1396
+ return { body: void 0, tooLarge: true };
1397
+ }
1398
+ }
1399
+ const stream = c.req.raw.body;
1400
+ if (stream === null) return { body: void 0, tooLarge: false };
1401
+ const reader = stream.getReader();
1402
+ const chunks = [];
1403
+ let total = 0;
1294
1404
  try {
1295
- frozen = JSON.parse(binding.body);
1405
+ while (true) {
1406
+ const { done, value } = await reader.read();
1407
+ if (done) break;
1408
+ if (value.byteLength > maximum - total) {
1409
+ cancelBody(reader);
1410
+ return { body: void 0, tooLarge: true };
1411
+ }
1412
+ if (value.byteLength === 0) continue;
1413
+ chunks.push(value);
1414
+ total += value.byteLength;
1415
+ }
1296
1416
  } catch {
1297
- return { outcome: "rejected" };
1417
+ return { body: void 0, tooLarge: false };
1418
+ } finally {
1419
+ reader.releaseLock();
1298
1420
  }
1299
- if (!sameAgentRef(frozen.agentRef, payload.agentRef) || frozen.context === void 0 || frozen.requirement?.contract !== payload.contract || frozen.sessionRef !== void 0 && frozen.sessionRef !== payload.sessionRef || frozen.requirement.contentType !== payload.contentType || typeof frozen.requirement.maxBytes !== "number" || payload.byteCount > frozen.requirement.maxBytes) return { outcome: "rejected" };
1300
- const priorDisposition = await readAgentMessageDisposition(stores, deviceId, taskId, payload);
1301
- if (priorDisposition !== void 0) return { outcome: "duplicate", disposition: priorDisposition };
1302
- const payloadBody = JSON.stringify(payload);
1303
- const reservation = await stores.tasks.reserveAgentMessage({
1304
- taskId,
1305
- deviceId,
1306
- messageId: payload.messageId,
1307
- payloadBody
1308
- });
1309
- if (reservation !== "reserved" && reservation !== "pending") return { outcome: "rejected" };
1310
- let decision;
1311
- if (consume === void 0) {
1312
- if (reservation === "pending") {
1313
- throw new Error("Agent message consumer is unavailable while reconciling an exact pending admission.");
1314
- }
1315
- decision = { outcome: "held", reasonCode: "consumer_unavailable" };
1316
- } else {
1317
- decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
1421
+ const bytes = new Uint8Array(total);
1422
+ let offset = 0;
1423
+ for (const chunk of chunks) {
1424
+ bytes.set(chunk, offset);
1425
+ offset += chunk.byteLength;
1318
1426
  }
1319
- const disposition = agentMessageDisposition(payload, decision);
1320
- const admission = await stores.tasks.finalizeAgentMessage({
1321
- taskId,
1322
- deviceId,
1323
- messageId: payload.messageId,
1324
- payloadBody,
1325
- terminalBody: JSON.stringify({ payload, disposition })
1326
- });
1327
- const terminal = admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
1328
- return terminal === void 0 ? { outcome: "rejected" } : { outcome: "accepted", disposition: terminal };
1329
- }
1330
- function agentMessageDisposition(payload, decision) {
1331
- return {
1332
- agentRef: payload.agentRef,
1333
- sessionRef: payload.sessionRef,
1334
- contract: payload.contract,
1335
- messageId: payload.messageId,
1336
- cursor: payload.cursor,
1337
- contentHash: payload.contentHash,
1338
- outcome: decision.outcome,
1339
- receiptId: crypto.randomUUID(),
1340
- ...decision.reasonCode === void 0 ? {} : { reasonCode: decision.reasonCode }
1341
- };
1342
- }
1343
- async function readAgentMessageDisposition(stores, deviceId, taskId, payload) {
1344
- const admission = await stores.tasks.readAgentMessage({
1345
- taskId,
1346
- deviceId,
1347
- messageId: payload.messageId,
1348
- payloadBody: JSON.stringify(payload)
1349
- });
1350
- return admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
1427
+ return { body: parseJsonBytes(bytes), tooLarge: false };
1351
1428
  }
1352
- function parseAgentMessageDisposition(payload, terminalBody) {
1429
+ function cancelBody(body) {
1430
+ if (body === null) return;
1353
1431
  try {
1354
- const decoded = JSON.parse(terminalBody);
1355
- return decoded.disposition !== void 0 && JSON.stringify(decoded.payload) === JSON.stringify(payload) ? decoded.disposition : void 0;
1432
+ void Promise.resolve(body.cancel()).catch(() => {
1433
+ });
1356
1434
  } catch {
1357
- return void 0;
1358
1435
  }
1359
1436
  }
1360
- function terminalReceiptKey(taskId) {
1361
- return `task:${taskId}:terminal`;
1362
- }
1363
- function sameAgentRef(expected, actual) {
1364
- return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
1365
- }
1366
- function sameReliableEgress(expected, actual) {
1367
- return sameAgentRef(expected.agentRef, actual.agentRef) && expected.sessionRef === actual.sessionRef && expected.policyRevision === actual.policyRevision && expected.eventId === actual.eventId && expected.cursor === actual.cursor && expected.contentHash === actual.contentHash && expected.byteCount === actual.byteCount && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
1368
- }
1369
- function contentReadCapability(surface) {
1370
- switch (surface) {
1371
- case "workspace":
1372
- return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
1373
- case "transcript":
1374
- return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
1375
- case "artifact":
1376
- return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
1437
+ function parseContentLength(value) {
1438
+ const normalized = value.trim();
1439
+ if (!/^\d+$/u.test(normalized)) return void 0;
1440
+ try {
1441
+ return BigInt(normalized);
1442
+ } catch {
1443
+ return void 0;
1377
1444
  }
1378
1445
  }
1379
- function hasCapabilities(capabilities, required) {
1380
- return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
1381
- }
1382
- function contentRequestReceiptKey(deviceId, agentRef, requestId) {
1383
- return agentReliabilityKey("agent-content-request", deviceId, agentRef, requestId);
1384
- }
1385
- function contentReceiptKey(deviceId, agentRef, requestId) {
1386
- return agentReliabilityKey("agent-content-receipt", deviceId, agentRef, requestId);
1387
- }
1388
- function matchesContentReadReceipt(request, receipt) {
1389
- return request.requestId === receipt.requestId && request.surface === receipt.surface && request.actor.kind === receipt.actor.kind && request.actor.id === receipt.actor.id && sameAgentRef(request.agentRef, receipt.agentRef) && request.sessionRef === receipt.sessionRef && request.runtime === receipt.runtime && request.cwd === receipt.cwd && request.policyRevision === receipt.policyRevision && request.target === receipt.target && request.mimeType === receipt.mimeType && request.decodeAs === receipt.decodeAs;
1390
- }
1391
- async function readContentRequest(stores, deviceId, agentRef, requestId) {
1392
- const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, agentRef, requestId));
1393
- if (stored === void 0) return void 0;
1446
+ function parseJsonBytes(bytes) {
1394
1447
  try {
1395
- return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
1448
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1396
1449
  } catch {
1397
1450
  return void 0;
1398
1451
  }
1399
1452
  }
1400
- function inboundAgentRef(envelope) {
1401
- switch (envelope.type) {
1402
- case "task.claim":
1403
- case "task.decline":
1404
- case "task.complete":
1405
- case "task.fail":
1406
- case "task.cancelled":
1407
- return envelope.payload.agentRef;
1408
- default:
1409
- return void 0;
1410
- }
1453
+ async function authenticateDevice(c, deps) {
1454
+ const device = await authenticateBearer(c.req.header("authorization"), deps.bearer);
1455
+ if (device === void 0) return void 0;
1456
+ return { device, stores: tenantStoresFor(device, deps.root) };
1411
1457
  }
1412
- function agentRefEchoRequired(envelope) {
1413
- return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
1458
+
1459
+ // src/handlers/blobs.ts
1460
+ var BLOB_RESERVATION_TTL_MS = 15 * 60 * 1e3;
1461
+ var SignedUrlQuerySchema = z.object({
1462
+ sig: z.string().min(1),
1463
+ exp: z.coerce.number().finite()
1464
+ });
1465
+ function createBlobHandler(deps) {
1466
+ return async (c) => {
1467
+ const authenticated = await authenticateDevice(c, deps);
1468
+ if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1469
+ const parsed = CreateBlobRequestSchema.safeParse(await readJsonBody(c));
1470
+ if (!parsed.success) return c.json({ error: "size, contentType, and contentHash are required" }, 400);
1471
+ if (parsed.data.size > deps.maxBlobSizeBytes) {
1472
+ return c.json({ error: `blob exceeds max size of ${deps.maxBlobSizeBytes} bytes` }, 413);
1473
+ }
1474
+ const reservationId = idempotencyKey(c);
1475
+ if (reservationId === void 0) {
1476
+ return c.json({ error: "Idempotency-Key header is required" }, 400);
1477
+ }
1478
+ try {
1479
+ const reservation = await authenticated.stores.quota.reserve({
1480
+ reservationId,
1481
+ kind: "object",
1482
+ expectedBytes: BigInt(parsed.data.size),
1483
+ contentHash: contentHash(parsed.data.contentHash),
1484
+ contentType: parsed.data.contentType,
1485
+ ttlMs: BLOB_RESERVATION_TTL_MS
1486
+ });
1487
+ try {
1488
+ const { blobId, uploadUrl } = await authenticated.stores.blobs.createUpload(reservation);
1489
+ const response = { blobId, uploadUrl };
1490
+ return c.json(response, 200);
1491
+ } catch (error) {
1492
+ await authenticated.stores.quota.abortReservation(reservationId).catch(() => void 0);
1493
+ const rendered = renderStorageError(c, error);
1494
+ if (rendered !== void 0) return rendered;
1495
+ throw error;
1496
+ }
1497
+ } catch (error) {
1498
+ const rendered = renderStorageError(c, error);
1499
+ if (rendered !== void 0) return rendered;
1500
+ throw error;
1501
+ }
1502
+ };
1414
1503
  }
1415
- async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume, observer) {
1416
- if (envelope.v !== PROTOCOL_VERSION) return "rejected";
1417
- const outcome = await applyInboundGate(
1418
- stores,
1419
- deviceId,
1420
- envelope,
1421
- activityBounds,
1422
- agentMessageConsume
1423
- );
1424
- if (outcome === "accepted" && observer !== void 0) {
1504
+ function finalizeBlobHandler(deps) {
1505
+ return async (c) => {
1506
+ const authenticated = await authenticateDevice(c, deps);
1507
+ if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1508
+ const reservationId = idempotencyKey(c);
1509
+ if (reservationId === void 0) {
1510
+ return c.json({ error: "Idempotency-Key header is required" }, 400);
1511
+ }
1425
1512
  try {
1426
- observer.onInboundCommitted({ tenantId: stores.tenant, deviceId, envelope, outcome });
1427
- } catch {
1513
+ const reservation = await authenticated.stores.quota.readReservation(reservationId);
1514
+ if (reservation === void 0) {
1515
+ return c.json({ error: "storage_reservation_not_found" }, 404);
1516
+ }
1517
+ const blobId = c.req.param("id") ?? "";
1518
+ const observed = await authenticated.stores.blobs.observeUpload(
1519
+ blobId,
1520
+ reservation
1521
+ );
1522
+ if (observed === void 0) {
1523
+ await authenticated.stores.quota.abortReservation(reservationId);
1524
+ return c.json({ error: "storage_integrity_mismatch" }, 422);
1525
+ }
1526
+ await authenticated.stores.quota.finalizeReservation({
1527
+ reservationId,
1528
+ ...observed
1529
+ });
1530
+ return c.body(null, 204);
1531
+ } catch (error) {
1532
+ const rendered = renderStorageError(c, error);
1533
+ if (rendered !== void 0) return rendered;
1534
+ throw error;
1428
1535
  }
1429
- }
1430
- return outcome;
1536
+ };
1431
1537
  }
1432
- async function applyInboundGate(stores, deviceId, envelope, activityBounds, agentMessageConsume) {
1433
- if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
1434
- if (envelope.type === "conn.hello") {
1435
- if (envelope.payload.deviceId !== deviceId) return "rejected";
1436
- if (envelope.payload.harnesses !== void 0 && !envelope.payload.capabilities.includes("custom-harness")) return "rejected";
1437
- if (!envelope.payload.protocolVersions.includes(PROTOCOL_VERSION)) return "rejected";
1438
- const device = await stores.devices.get(deviceId);
1439
- if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
1440
- return "rejected";
1538
+ function blobDownloadUrlHandler(deps) {
1539
+ return async (c) => {
1540
+ const authenticated = await authenticateDevice(c, deps);
1541
+ if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1542
+ const downloadUrl = await authenticated.stores.blobs.getDownloadUrl(c.req.param("id") ?? "");
1543
+ if (downloadUrl === void 0) return c.json({ error: "blob not found" }, 404);
1544
+ const response = { downloadUrl };
1545
+ return c.json(response, 200);
1546
+ };
1547
+ }
1548
+ function blobUploadContentHandler(deps) {
1549
+ return async (c) => {
1550
+ const blobId = c.req.param("id") ?? "";
1551
+ const query = SignedUrlQuerySchema.safeParse({ sig: c.req.query("sig"), exp: c.req.query("exp") });
1552
+ if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "put", query.data.sig, query.data.exp)) {
1553
+ return c.json({ error: "invalid or expired signature" }, 401);
1441
1554
  }
1442
- if (await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities, ...envelope.payload.harnesses === void 0 ? {} : { harnesses: envelope.payload.harnesses } }) === void 0) {
1443
- return "rejected";
1555
+ const expectedBytes = await deps.contentProxy.expectedUploadBytes(blobId);
1556
+ if (expectedBytes === void 0) return c.json({ error: "blob not found" }, 404);
1557
+ const absoluteCeiling = BigInt(deps.maxUploadBytes);
1558
+ if (expectedBytes > absoluteCeiling) return c.json({ error: "upload exceeds size limit" }, 413);
1559
+ const contentLength = c.req.header("content-length");
1560
+ if (contentLength !== void 0 && /^\d+$/u.test(contentLength) && BigInt(contentLength) > expectedBytes) {
1561
+ return c.json({ error: "upload exceeds declared size" }, 413);
1444
1562
  }
1445
- return completeInboundEnvelope(stores, deviceId, envelope.id, void 0);
1446
- }
1447
- if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
1448
- if (envelope.type === "agent.message.publish") {
1449
- const message = await handleAgentMessagePublish(
1450
- stores,
1451
- deviceId,
1452
- envelope.task_id,
1453
- envelope.payload,
1454
- agentMessageConsume);
1455
- return message.outcome === "rejected" ? "rejected" : completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
1456
- }
1457
- if (envelope.type === "agent.egress.reliable") {
1458
- const device = await stores.devices.get(deviceId);
1459
- if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
1460
- return "rejected";
1563
+ const body = await readBoundedBody(c.req.raw.body, Number(expectedBytes));
1564
+ if (body.tooLarge) return c.json({ error: "upload exceeds declared size" }, 413);
1565
+ const result = await deps.contentProxy.writeContent(blobId, body.data);
1566
+ if (!result.ok) return c.json({ error: result.reason }, 422);
1567
+ return c.body(null, 204);
1568
+ };
1569
+ }
1570
+ async function readBoundedBody(body, ceiling) {
1571
+ if (body === null) return { tooLarge: false, data: new Uint8Array() };
1572
+ const reader = body.getReader();
1573
+ const data = new Uint8Array(ceiling);
1574
+ let offset = 0;
1575
+ try {
1576
+ for (; ; ) {
1577
+ const next = await reader.read();
1578
+ if (next.done) break;
1579
+ const chunk = next.value;
1580
+ if (chunk.byteLength > ceiling - offset) {
1581
+ void reader.cancel().catch(() => void 0);
1582
+ return { tooLarge: true };
1583
+ }
1584
+ data.set(chunk, offset);
1585
+ offset += chunk.byteLength;
1461
1586
  }
1462
- const outcome = await stores.egress.record({
1463
- deviceId,
1464
- payload: envelope.payload,
1465
- receiptId: crypto.randomUUID()
1466
- });
1467
- if (!outcome.created && !sameReliableEgress(outcome.record.payload, envelope.payload)) return "rejected";
1468
- return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
1587
+ } finally {
1588
+ reader.releaseLock();
1469
1589
  }
1470
- if (envelope.type === "agent.content.receipt") {
1471
- const device = await stores.devices.get(deviceId);
1472
- if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
1473
- return "rejected";
1590
+ return { tooLarge: false, data: data.subarray(0, offset) };
1591
+ }
1592
+ function blobDownloadContentHandler(deps) {
1593
+ return async (c) => {
1594
+ const blobId = c.req.param("id") ?? "";
1595
+ const query = SignedUrlQuerySchema.safeParse({ sig: c.req.query("sig"), exp: c.req.query("exp") });
1596
+ if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "get", query.data.sig, query.data.exp)) {
1597
+ return c.json({ error: "invalid or expired signature" }, 401);
1474
1598
  }
1475
- const request = await readContentRequest(stores, deviceId, envelope.payload.agentRef, envelope.payload.requestId);
1476
- if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
1477
- const key = contentReceiptKey(deviceId, envelope.payload.agentRef, envelope.payload.requestId);
1478
- const body = JSON.stringify(envelope.payload);
1479
- const receipt = await stores.receipts.record({ key, body });
1480
- if (!receipt.created && receipt.receipt.body !== body) return "rejected";
1481
- return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
1599
+ const result = await deps.contentProxy.readContent(blobId);
1600
+ if (result === void 0) return c.json({ error: "blob not found" }, 404);
1601
+ if (!result.ok) return c.json({ error: result.code }, BLOB_READ_ERROR_HTTP_STATUS[result.code]);
1602
+ return c.body(new Uint8Array(result.content.data), 200, { "content-type": result.content.contentType });
1603
+ };
1604
+ }
1605
+ var BLOB_READ_ERROR_HTTP_STATUS = {
1606
+ blob_upstream_unavailable: 502,
1607
+ blob_upstream_stream_interrupted: 502
1608
+ };
1609
+ function idempotencyKey(c) {
1610
+ const value = c.req.header("Idempotency-Key");
1611
+ if (value === void 0 || value.length === 0 || value.length > 200) return void 0;
1612
+ return value;
1613
+ }
1614
+ function renderStorageError(c, error) {
1615
+ if (!isCoreError(error)) return void 0;
1616
+ if (STORAGE_ERROR_CODES.includes(error.code)) {
1617
+ const code = error.code;
1618
+ return c.json({ error: code }, STORAGE_ERROR_HTTP_STATUS[code]);
1482
1619
  }
1483
- const taskId = envelope.task_id;
1484
- if (taskId === void 0) return "rejected";
1485
- const attempt = await stores.tasks.get(taskId);
1486
- if (attempt === void 0 || attempt.deviceId !== deviceId) return "rejected";
1487
- if (attempt?.ownerDeviceId !== void 0 && attempt.ownerDeviceId !== deviceId) return "rejected";
1488
- if (envelope.type === "task.claim" && envelope.payload.deviceId !== deviceId) return "rejected";
1489
- if (envelope.type === "task.claim") {
1490
- const harnessId = envelope.payload.harnessId;
1491
- if (harnessId !== void 0 && envelope.payload.runtime !== void 0) return "rejected";
1492
- if (attempt.ownerDeviceId !== void 0 && (harnessId !== void 0 || attempt.claimedHarnessId !== void 0) && harnessId !== attempt.claimedHarnessId) return "rejected";
1493
- const device = await stores.devices.get(deviceId);
1494
- if (harnessId !== void 0 && (!device?.capabilities?.includes("custom-harness") || !device.harnesses?.some((harness) => harness.id === harnessId))) return "rejected";
1495
- const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
1496
- const offer = await stores.receipts.get(`task-offer:${messageId}`);
1497
- if (offer !== void 0) {
1498
- const sealed = JSON.parse(offer.body);
1499
- if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
1500
- if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
1501
- } else if (harnessId !== void 0) return "rejected";
1620
+ if (error.code === "storage_reservation_not_found" || error.code === "object_not_found") {
1621
+ return c.json({ error: error.code }, 404);
1502
1622
  }
1503
- let unclaimedInterruption = false;
1504
- if (envelope.type === "task.fail" && envelope.payload.recovery !== void 0) {
1505
- if (envelope.payload.reason !== "daemon_interrupted" || envelope.payload.retryable !== false) return "rejected";
1506
- const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
1507
- if (envelope.payload.recovery.offerId !== messageId) return "rejected";
1508
- const offer = await stores.receipts.get(`task-offer:${messageId}`);
1509
- if (offer === void 0) return "rejected";
1510
- const sealed = JSON.parse(offer.body);
1511
- const harnessId = envelope.payload.harnessId;
1512
- if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
1513
- if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
1514
- unclaimedInterruption = attempt.ownerDeviceId === void 0 && attempt.claimedHarnessId === void 0;
1623
+ if (error.code === "storage_entitlement_missing" || error.code === "object_state_invalid") {
1624
+ return c.json({ error: error.code }, 409);
1515
1625
  }
1516
- if ((envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled") && !unclaimedInterruption && envelope.payload.harnessId !== attempt.claimedHarnessId) return "rejected";
1517
- if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
1518
- return "rejected";
1626
+ if (error.code === "content_hash_invalid") {
1627
+ return c.json({ error: error.code }, 400);
1519
1628
  }
1520
- if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
1521
- try {
1522
- validateActivityBatch(
1523
- {
1524
- taskId,
1525
- sourceEnvelopeId: envelope.id,
1526
- batchSeq: envelope.payload.seq,
1527
- events: envelope.payload.events,
1528
- dropped: 0
1629
+ return void 0;
1630
+ }
1631
+
1632
+ // src/handlers/capabilities.ts
1633
+ function capabilitiesHandler(deps) {
1634
+ return async (c) => c.json(deps.declaration, 200);
1635
+ }
1636
+ var AUTH_JSON_BODY_MAX_BYTES = 16 * 1024;
1637
+ function pairHandler(deps) {
1638
+ return async (c) => {
1639
+ const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
1640
+ if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
1641
+ const parsed = PairRequestSchema.safeParse(body.body);
1642
+ if (!parsed.success) {
1643
+ return c.json(
1644
+ {
1645
+ error: "pairingCode, deviceName, and devicePublicKey are required strings; optional machineId must be 64 lowercase hex characters"
1529
1646
  },
1530
- activityBounds
1647
+ 400
1531
1648
  );
1532
- } catch (caught) {
1533
- if (isCloudError(caught, "activity_batch_too_large") || isCloudError(caught, "coordination_input_invalid")) {
1534
- return "rejected";
1535
- }
1536
- throw caught;
1537
1649
  }
1538
- }
1539
- const approvalInput = approvalTimelineAppendInput(taskId, envelope);
1540
- if (approvalInput !== void 0) {
1541
- try {
1542
- validateApprovalTimelineAppend(approvalInput);
1543
- } catch (caught) {
1544
- if (isCloudError(caught, "coordination_input_invalid")) return "rejected";
1545
- throw caught;
1650
+ const device = await deps.auth.redeemAndRegister(parsed.data);
1651
+ if (device === void 0) return c.json({ error: "invalid pairing code" }, 401);
1652
+ const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
1653
+ const response = PairResponseSchema.parse({
1654
+ deviceId: device.deviceId,
1655
+ accessToken,
1656
+ refreshHint: expiresAt,
1657
+ tenantId: device.tenantId
1658
+ });
1659
+ return c.json(response, 200);
1660
+ };
1661
+ }
1662
+ function challengeHandler(deps) {
1663
+ return async (c) => {
1664
+ const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
1665
+ if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
1666
+ const parsed = ChallengeRequestSchema.safeParse(body.body);
1667
+ if (!parsed.success) return c.json({ error: "deviceId is required" }, 400);
1668
+ const device = await deps.auth.resolveDevice(parsed.data.deviceId);
1669
+ if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
1670
+ const response = { nonce: await deps.auth.issueNonce(device) };
1671
+ return c.json(response, 200);
1672
+ };
1673
+ }
1674
+ function tokenHandler(deps) {
1675
+ return async (c) => {
1676
+ const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
1677
+ if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
1678
+ const parsed = TokenRequestSchema.safeParse(body.body);
1679
+ if (!parsed.success) return c.json({ error: "deviceId, nonce, and signature are required" }, 400);
1680
+ const { deviceId, nonce, signature } = parsed.data;
1681
+ const device = await deps.auth.resolveDevice(deviceId);
1682
+ if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
1683
+ if (!await deps.auth.verifySignature(device, nonce, signature)) {
1684
+ return c.json({ error: "invalid signature" }, 401);
1546
1685
  }
1547
- }
1548
- const lifecycleOutcome = await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
1549
- if (lifecycleOutcome === "rejected") return "rejected";
1550
- return completeInboundEnvelope(stores, deviceId, envelope.id, attempt?.agentRef);
1686
+ if (!await deps.auth.consumeNonceIfValid(device, nonce)) {
1687
+ return c.json({ error: "invalid, expired, or already-used nonce" }, 401);
1688
+ }
1689
+ const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
1690
+ const response = { accessToken, expiresAt };
1691
+ return c.json(response, 200);
1692
+ };
1551
1693
  }
1552
- async function completeInboundEnvelope(stores, deviceId, envelopeId, agentRef) {
1553
- const duplicate = agentRef === void 0 ? await stores.dedup.checkAndRecord(deviceId, envelopeId) : await stores.dedup.checkAndRecordAgent(deviceId, agentRef, envelopeId);
1554
- return duplicate ? "duplicate" : "accepted";
1694
+ var CLOUD_PROTOCOL_CAPABILITIES = [
1695
+ "custom-harness",
1696
+ MAILBOX_READ_AHEAD_CAPABILITY,
1697
+ "result-document",
1698
+ // The kernel's inbound handles `task.approval_resolved` on both the plain
1699
+ // and the approval-timeline paths (`packages/cloud/src/inbound.ts:576,610`).
1700
+ // Without this flag `TaskRunner.sendApprovalResolved` returns early and a
1701
+ // locally-resolved approval is never reported on the wire at all.
1702
+ "approval_resolved",
1703
+ "agent-home-contract",
1704
+ "agent-home-projection",
1705
+ "agent-egress-policy",
1706
+ "agent-egress-reliable-ack",
1707
+ "agent-egress-fresh-session",
1708
+ // `handleAgentMessagePublish` accepts `agent.message_egress` gated on exactly
1709
+ // this capability (`packages/cloud/src/inbound.ts:124`) — the same inbound
1710
+ // family as the three egress flags above, omitted by the same drift.
1711
+ "agent-message-egress",
1712
+ "agent-content-workspace-read",
1713
+ "agent-content-transcript-read",
1714
+ "agent-content-artifact-read"
1715
+ ];
1716
+ function sleep(ms) {
1717
+ return new Promise((resolve) => setTimeout(resolve, ms));
1555
1718
  }
1556
- async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
1557
- switch (envelope.type) {
1558
- case "task.claim": {
1559
- const claimed = await stores.tasks.claim({
1560
- taskId,
1561
- deviceId,
1562
- ...envelope.payload.runtime === void 0 ? {} : { runtime: envelope.payload.runtime },
1563
- ...envelope.payload.harnessId === void 0 ? {} : { harnessId: envelope.payload.harnessId },
1564
- ...envelope.payload.capabilities === void 0 ? {} : { capabilities: envelope.payload.capabilities }
1565
- });
1566
- if (claimed === void 0 || claimed.ownerDeviceId !== deviceId || claimed.claimedRuntime !== envelope.payload.runtime || claimed.claimedHarnessId !== envelope.payload.harnessId) return "rejected";
1567
- return;
1719
+ function eventsHandler(deps) {
1720
+ return async (c) => {
1721
+ const authenticated = await authenticateDevice(c, deps);
1722
+ if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1723
+ const { device, stores } = authenticated;
1724
+ const cursorRaw = c.req.query("cursor");
1725
+ let cursor = 0;
1726
+ if (cursorRaw !== void 0) {
1727
+ const parsedCursor = Number(cursorRaw);
1728
+ if (!Number.isSafeInteger(parsedCursor) || parsedCursor < 0) {
1729
+ return c.json({ error: "invalid cursor" }, 400);
1730
+ }
1731
+ cursor = parsedCursor;
1568
1732
  }
1569
- case "task.started":
1570
- await stores.tasks.recordStatus({
1571
- taskId,
1572
- status: "running",
1573
- ...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
1574
- });
1575
- return;
1576
- case "task.decline":
1577
- await recordTerminal(stores, taskId, envelope);
1578
- return;
1579
- case "task.progress":
1580
- if (envelope.payload.events.length > 0) {
1581
- await appendActivityEvents(
1582
- stores.activity,
1583
- {
1584
- taskId,
1585
- sourceEnvelopeId: envelope.id,
1586
- batchSeq: envelope.payload.seq,
1587
- events: envelope.payload.events,
1588
- dropped: 0
1589
- },
1590
- activityBounds
1591
- );
1733
+ const afterRaw = c.req.query("afterSeq");
1734
+ const afterSeq = afterRaw === void 0 ? cursor : Number(afterRaw);
1735
+ if (!Number.isSafeInteger(afterSeq) || afterSeq < cursor || afterSeq - cursor > MAILBOX_READ_AHEAD_MAX_SEQS) {
1736
+ return c.json({ error: "invalid read-ahead offset" }, 400);
1737
+ }
1738
+ const acked = await stores.mailbox.readCursor(device.deviceId);
1739
+ if (afterRaw !== void 0 && afterSeq > acked.deliveredSeq) return c.json({ error: "read offset exceeds delivered watermark" }, 409);
1740
+ if (cursor > acked.ackedSeq) {
1741
+ try {
1742
+ await stores.mailbox.advanceCursor({ deviceId: device.deviceId, ackedSeq: cursor });
1743
+ } catch (caught) {
1744
+ if (isCoreConflictError(caught, "mailbox_cursor_ahead_of_delivery")) {
1745
+ return c.json({ error: "cursor exceeds delivered watermark" }, 409);
1746
+ }
1747
+ throw caught;
1592
1748
  }
1593
- return;
1594
- case "task.await_approval":
1595
- await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
1596
- return;
1597
- case "task.approval_resolved":
1598
- await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
1599
- return;
1600
- case "task.complete":
1601
- await recordTerminal(stores, taskId, envelope);
1602
- return;
1603
- case "task.fail":
1604
- await recordTerminal(stores, taskId, envelope);
1605
- return;
1606
- case "task.cancelled":
1607
- await recordTerminal(stores, taskId, envelope);
1608
- return;
1609
- default:
1610
- return;
1611
- }
1612
- }
1613
- function approvalTimelineAppendInput(taskId, envelope) {
1614
- switch (envelope.type) {
1615
- case "task.await_approval":
1616
- return {
1617
- taskId,
1618
- sourceEnvelopeId: envelope.id,
1619
- event: {
1620
- type: "approval_requested",
1621
- summary: envelope.payload.summary,
1622
- ...envelope.payload.approvalId === void 0 ? {} : { approvalId: envelope.payload.approvalId }
1749
+ }
1750
+ const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
1751
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1752
+ let scanCursor = afterSeq;
1753
+ while (true) {
1754
+ const page = await stores.mailbox.readAfter({
1755
+ deviceId: device.deviceId,
1756
+ afterSeq: scanCursor,
1757
+ limit: deps.pageLimit
1758
+ });
1759
+ if (cursor < page.recoverableFrom - 1) {
1760
+ return c.json({ error: "cursor_too_old", recoverableFrom: page.recoverableFrom }, 409);
1623
1761
  }
1624
- };
1625
- case "task.approval_resolved":
1626
- return {
1627
- taskId,
1628
- sourceEnvelopeId: envelope.id,
1629
- event: {
1630
- type: "approval_resolved",
1631
- approvalId: envelope.payload.approvalId,
1632
- decision: envelope.payload.decision,
1633
- resolvedBy: envelope.payload.resolvedBy,
1634
- at: envelope.payload.at
1762
+ if (page.messages.length === 0) break;
1763
+ const decoded = page.messages.map((message) => decodeEnvelope(message.body));
1764
+ const offeredTaskIds = decoded.flatMap(
1765
+ (event) => isTaskOfferType(event.type) && event.task_id !== void 0 ? [event.task_id] : []
1766
+ );
1767
+ const attemptsByTaskId = Object.fromEntries(
1768
+ (await stores.tasks.getMany(offeredTaskIds)).map((attempt2) => [attempt2.taskId, attempt2])
1769
+ );
1770
+ const events = decoded.filter((event) => {
1771
+ if (!isTaskOfferType(event.type)) return true;
1772
+ return event.task_id === void 0 || attemptsByTaskId[event.task_id]?.cancellation === void 0;
1773
+ });
1774
+ if (events.length === 0 && page.hasMore) {
1775
+ scanCursor = page.nextSeq;
1776
+ continue;
1635
1777
  }
1636
- };
1637
- default:
1638
- return void 0;
1639
- }
1640
- }
1641
- async function recordTerminal(stores, taskId, envelope) {
1642
- const receipt = await stores.receipts.record({
1643
- key: terminalReceiptKey(taskId),
1644
- body: encodeEnvelope(envelope)
1645
- });
1646
- const winner = terminalReceiptBinding(taskId, receipt.receipt.body);
1647
- if (winner === void 0) {
1648
- throw new Error(`terminal receipt for ${taskId} is not a canonical terminal binding`);
1649
- }
1650
- const attempt = await stores.tasks.get(taskId);
1651
- if (attempt?.cancellation !== void 0) {
1652
- await stores.tasks.recordStatus({
1653
- taskId,
1654
- status: "cancelled",
1655
- ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
1656
- });
1657
- return;
1658
- }
1659
- const recordedAttempt = await stores.tasks.recordStatus({
1660
- taskId,
1661
- status: winner.status,
1662
- ...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
1663
- ...winner.cause === void 0 ? {} : { terminalCause: winner.cause }
1664
- });
1665
- if (recordedAttempt?.cancellation !== void 0 || recordedAttempt?.status !== winner.status) {
1666
- if (recordedAttempt?.cancellation !== void 0) {
1667
- await stores.tasks.recordStatus({
1668
- taskId,
1669
- status: "cancelled",
1670
- ...recordedAttempt.agentRef === void 0 ? {} : { agentRef: recordedAttempt.agentRef }
1671
- });
1778
+ if (events.length === 0) break;
1779
+ const response2 = {
1780
+ events,
1781
+ cursor: page.nextSeq,
1782
+ capabilities: CLOUD_PROTOCOL_CAPABILITIES
1783
+ };
1784
+ await stores.mailbox.recordDelivery({
1785
+ deviceId: device.deviceId,
1786
+ deliveredSeq: response2.cursor
1787
+ });
1788
+ return c.json(response2, 200);
1789
+ }
1790
+ if (attempt < attempts - 1) await sleep(deps.longPollIntervalMs);
1672
1791
  }
1673
- return;
1674
- }
1675
- await projectTerminalToReview(stores.board, taskId);
1676
- }
1677
- function terminalStatus(envelope) {
1678
- switch (envelope.type) {
1679
- case "task.complete":
1680
- return "complete";
1681
- case "task.fail":
1682
- case "task.decline":
1683
- return "failed";
1684
- case "task.cancelled":
1685
- return "cancelled";
1686
- default:
1687
- return void 0;
1688
- }
1689
- }
1690
- function terminalReceiptBinding(taskId, body) {
1691
- let envelope;
1692
- try {
1693
- envelope = decodeEnvelope(body);
1694
- } catch {
1695
- return void 0;
1696
- }
1697
- const status = terminalStatus(envelope);
1698
- if (status === void 0 || envelope.task_id !== taskId || encodeEnvelope(envelope) !== body) return void 0;
1699
- const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" || envelope.type === "task.decline" ? envelope.payload.reason : void 0;
1700
- return { status, ...cause === void 0 ? {} : { cause } };
1792
+ const response = {
1793
+ events: [],
1794
+ cursor: afterSeq,
1795
+ capabilities: CLOUD_PROTOCOL_CAPABILITIES
1796
+ };
1797
+ return c.json(response, 200);
1798
+ };
1701
1799
  }
1702
-
1703
- // src/handlers/messages.ts
1704
1800
  var MESSAGES_JSON_BODY_MAX_BYTES = 2 * 1024 * 1024;
1705
1801
  function messagesHandler(deps) {
1706
1802
  return async (c) => {
@@ -1775,6 +1871,63 @@ function agentHomeProjectionCompletionHandler(deps) {
1775
1871
  }
1776
1872
  };
1777
1873
  }
1874
+ function mapCloudError(c, error) {
1875
+ if (isCloudError(error, "input_preparation_request_not_found")) {
1876
+ return c.json({ error: error.code }, 404);
1877
+ }
1878
+ if (isCloudError(error, "input_preparation_receipt_mismatch") || isCloudError(error, "input_preparation_receipt_invalid")) {
1879
+ return c.json({ error: error.code }, 422);
1880
+ }
1881
+ if (isCloudError(error, "input_preparation_request_conflict") || isCloudError(error, "input_preparation_completion_conflict")) {
1882
+ return c.json({ error: error.code }, 409);
1883
+ }
1884
+ throw error;
1885
+ }
1886
+ function inputPreparationCompletionHandler(deps) {
1887
+ return async (c) => {
1888
+ const authenticated = await authenticateDevice(c, deps);
1889
+ if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1890
+ const parsed = InputPreparationCompletionRequestSchema.safeParse(await readJsonBody(c));
1891
+ if (!parsed.success) return c.json({ error: "invalid input preparation completion" }, 422);
1892
+ const requestId = c.req.param("requestId");
1893
+ if (requestId === void 0 || requestId !== parsed.data.requestId) {
1894
+ return c.json({ error: "input_preparation_receipt_mismatch" }, 422);
1895
+ }
1896
+ try {
1897
+ return c.json(
1898
+ await deps.complete(authenticated.stores, authenticated.device.deviceId, parsed.data),
1899
+ 200
1900
+ );
1901
+ } catch (error) {
1902
+ return mapCloudError(c, error);
1903
+ }
1904
+ };
1905
+ }
1906
+ function inputPreparationStatusHandler(deps) {
1907
+ return async (c) => {
1908
+ const authenticated = await authenticateDevice(c, deps);
1909
+ if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1910
+ const requestId = c.req.param("requestId");
1911
+ if (requestId === void 0 || requestId.length === 0) {
1912
+ return c.json({ error: "invalid input preparation request id" }, 422);
1913
+ }
1914
+ const query = InputPreparationStatusQuerySchema.safeParse({
1915
+ agentId: c.req.query("agentId"),
1916
+ profileRevision: c.req.query("profileRevision")
1917
+ });
1918
+ if (!query.success) return c.json({ error: "invalid input preparation status query" }, 422);
1919
+ try {
1920
+ const readback = await deps.status(authenticated.stores, authenticated.device.deviceId, {
1921
+ requestId,
1922
+ agentRef: { agentId: query.data.agentId, profileRevision: query.data.profileRevision }
1923
+ });
1924
+ if (readback === void 0) return c.json({ error: "input_preparation_request_not_found" }, 404);
1925
+ return c.json(readback, 200);
1926
+ } catch (error) {
1927
+ return mapCloudError(c, error);
1928
+ }
1929
+ };
1930
+ }
1778
1931
  var DEFAULT_MAX_AGENT_MEMORY_PROJECTION_REQUEST_BYTES = 1 * 1024 * 1024;
1779
1932
  function agentMemoryProjectionHandler(deps) {
1780
1933
  return async (c) => {
@@ -2661,6 +2814,135 @@ async function recordAgentHomeProjectionCompletion(receipts, tenant, deviceId, r
2661
2814
  completedAt: storedCompletion.receipt.recordedAt
2662
2815
  };
2663
2816
  }
2817
+ function inputPreparationRequestKey(deviceId, agentRef, requestId) {
2818
+ return agentReliabilityKey("agent-input-preparation-request", deviceId, agentRef, requestId);
2819
+ }
2820
+ function inputPreparationCompletionKey(deviceId, agentRef, requestId) {
2821
+ return agentReliabilityKey("agent-input-preparation-completion", deviceId, agentRef, requestId);
2822
+ }
2823
+ function sameInputPreparationRequest(expected, actual) {
2824
+ return canonicalRequestBody(expected) === canonicalRequestBody(actual);
2825
+ }
2826
+ function canonicalRequestBody(payload) {
2827
+ return JSON.stringify(AgentInputPreparationPayloadSchema.parse(payload));
2828
+ }
2829
+ function receiptMatchesInputPreparation(request, receipt) {
2830
+ return receipt.requestId === request.requestId && receipt.agentRef.agentId === request.agentRef.agentId && receipt.agentRef.profileRevision === request.agentRef.profileRevision && receipt.profileId === request.profileId && receipt.policyRevision === request.policyRevision;
2831
+ }
2832
+ function statusInputMatchesInputPreparation(request, input) {
2833
+ return input.requestId === request.requestId && input.agentRef.agentId === request.agentRef.agentId && input.agentRef.profileRevision === request.agentRef.profileRevision;
2834
+ }
2835
+ function parseRequestBody2(body) {
2836
+ try {
2837
+ return AgentInputPreparationPayloadSchema.parse(JSON.parse(body));
2838
+ } catch (error) {
2839
+ throw new ByokCloudError(
2840
+ "input_preparation_receipt_invalid",
2841
+ "Stored input-preparation request is not a valid immutable desired fact.",
2842
+ { cause: error }
2843
+ );
2844
+ }
2845
+ }
2846
+ function parseCompletionBody2(body) {
2847
+ try {
2848
+ return InputPreparationCompletionRequestSchema.parse(JSON.parse(body));
2849
+ } catch (error) {
2850
+ throw new ByokCloudError(
2851
+ "input_preparation_receipt_invalid",
2852
+ "Stored input-preparation completion is not a valid immutable receipt fact.",
2853
+ { cause: error }
2854
+ );
2855
+ }
2856
+ }
2857
+ function terminalReadback(tenant, deviceId, request, completion, completedAt) {
2858
+ const common = {
2859
+ tenantId: tenant,
2860
+ deviceId,
2861
+ requestId: request.requestId,
2862
+ agentRef: request.agentRef,
2863
+ profileId: request.profileId,
2864
+ policyRevision: request.policyRevision,
2865
+ completedAt
2866
+ };
2867
+ return completion.outcome === "prepared" ? { ...common, status: "prepared", receipt: completion.receipt } : { ...common, status: "rejected", reason: completion.reason };
2868
+ }
2869
+ async function readInputPreparationStatus(receipts, tenant, deviceId, input) {
2870
+ const storedRequest = await receipts.get(inputPreparationRequestKey(deviceId, input.agentRef, input.requestId));
2871
+ if (storedRequest === void 0) return void 0;
2872
+ const request = parseRequestBody2(storedRequest.body);
2873
+ if (!statusInputMatchesInputPreparation(request, input)) {
2874
+ throw new ByokCloudError(
2875
+ "input_preparation_request_conflict",
2876
+ `Input-preparation request ${input.requestId} does not match its immutable desired fact.`
2877
+ );
2878
+ }
2879
+ const storedCompletion = await receipts.get(
2880
+ inputPreparationCompletionKey(deviceId, request.agentRef, input.requestId)
2881
+ );
2882
+ if (storedCompletion === void 0) {
2883
+ return {
2884
+ tenantId: tenant,
2885
+ deviceId,
2886
+ requestId: request.requestId,
2887
+ agentRef: request.agentRef,
2888
+ profileId: request.profileId,
2889
+ policyRevision: request.policyRevision,
2890
+ status: "pending"
2891
+ };
2892
+ }
2893
+ const completion = parseCompletionBody2(storedCompletion.body);
2894
+ if (!receiptMatchesInputPreparation(request, completion)) {
2895
+ throw new ByokCloudError(
2896
+ "input_preparation_receipt_mismatch",
2897
+ `Input-preparation completion ${input.requestId} does not match its immutable desired fact.`
2898
+ );
2899
+ }
2900
+ return terminalReadback(tenant, deviceId, request, completion, storedCompletion.recordedAt);
2901
+ }
2902
+ async function recordInputPreparationCompletion(receipts, tenant, deviceId, receiptInput) {
2903
+ const receipt = InputPreparationCompletionRequestSchema.parse(receiptInput);
2904
+ const storedRequest = await receipts.get(
2905
+ inputPreparationRequestKey(deviceId, receipt.agentRef, receipt.requestId)
2906
+ );
2907
+ if (storedRequest === void 0) {
2908
+ throw new ByokCloudError(
2909
+ "input_preparation_request_not_found",
2910
+ `Input-preparation request ${receipt.requestId} was not found for this device.`
2911
+ );
2912
+ }
2913
+ const request = parseRequestBody2(storedRequest.body);
2914
+ if (!receiptMatchesInputPreparation(request, receipt)) {
2915
+ throw new ByokCloudError(
2916
+ "input_preparation_receipt_mismatch",
2917
+ `Input-preparation completion ${receipt.requestId} does not exactly match its desired fact.`
2918
+ );
2919
+ }
2920
+ const body = JSON.stringify(receipt);
2921
+ const storedCompletion = await receipts.record({
2922
+ key: inputPreparationCompletionKey(deviceId, request.agentRef, receipt.requestId),
2923
+ body
2924
+ });
2925
+ if (!storedCompletion.created && storedCompletion.receipt.body !== body) {
2926
+ throw new ByokCloudError(
2927
+ "input_preparation_completion_conflict",
2928
+ `Input-preparation request ${receipt.requestId} already has a different terminal completion.`
2929
+ );
2930
+ }
2931
+ return terminalReadback(
2932
+ tenant,
2933
+ deviceId,
2934
+ request,
2935
+ parseCompletionBody2(storedCompletion.receipt.body),
2936
+ storedCompletion.receipt.recordedAt
2937
+ );
2938
+ }
2939
+ function readDeviceTerminalReceipt(taskId, receipt) {
2940
+ const envelope = decodeEnvelope(receipt.body);
2941
+ if (envelope.task_id !== taskId || envelope.type !== "task.complete" && envelope.type !== "task.fail" && envelope.type !== "task.decline" && envelope.type !== "task.cancelled") {
2942
+ throw new ByokCloudError("terminal_receipt_unreadable", "Device terminal receipt identity or type is invalid.");
2943
+ }
2944
+ return { envelope, recordedAt: receipt.recordedAt };
2945
+ }
2664
2946
  function projectTerminalResult(taskId, receipt) {
2665
2947
  let envelope;
2666
2948
  try {
@@ -2830,6 +3112,19 @@ function createByokCloud(options) {
2830
3112
  complete: (stores, deviceId, receipt) => completeAgentHomeProjectionFromStores(stores, deviceId, receipt)
2831
3113
  })
2832
3114
  );
3115
+ const inputPreparationRouteDeps = {
3116
+ ...deviceRouteDeps,
3117
+ complete: (stores, deviceId, receipt) => completeInputPreparationFromStores(stores, deviceId, receipt),
3118
+ status: (stores, deviceId, input) => readInputPreparationStatus(stores.receipts, stores.tenant, deviceId, input)
3119
+ };
3120
+ registry.register(
3121
+ { method: "PUT", path: BYOK_INPUT_PREPARATION_COMPLETION_ROUTE, class: "device" },
3122
+ inputPreparationCompletionHandler(inputPreparationRouteDeps)
3123
+ );
3124
+ registry.register(
3125
+ { method: "GET", path: BYOK_INPUT_PREPARATION_STATUS_ROUTE, class: "device" },
3126
+ inputPreparationStatusHandler(inputPreparationRouteDeps)
3127
+ );
2833
3128
  if (declares(declaration, CLOUD_CAPABILITIES.boardCoordination)) {
2834
3129
  const boardDeps = {
2835
3130
  ...deviceRouteDeps,
@@ -3340,6 +3635,25 @@ function createByokCloud(options) {
3340
3635
  receipt
3341
3636
  );
3342
3637
  }
3638
+ async function getInputPreparationStatus(tenant, deviceId, input) {
3639
+ return readInputPreparationStatus(
3640
+ tenantStoresFor(controlPlane(tenant), root).receipts,
3641
+ tenant,
3642
+ deviceId,
3643
+ input
3644
+ );
3645
+ }
3646
+ async function completeInputPreparation(tenant, deviceId, receiptInput) {
3647
+ return completeInputPreparationFromStores(
3648
+ tenantStoresFor(controlPlane(tenant), root),
3649
+ deviceId,
3650
+ receiptInput
3651
+ );
3652
+ }
3653
+ async function completeInputPreparationFromStores(stores, deviceId, receiptInput) {
3654
+ const receipt = InputPreparationCompletionRequestSchema.parse(receiptInput);
3655
+ return recordInputPreparationCompletion(stores.receipts, stores.tenant, deviceId, receipt);
3656
+ }
3343
3657
  async function commitAgentMemoryProjectionFromStores(stores, deviceId, mutationInput, redactedBytes) {
3344
3658
  const mutation = AgentMemoryProjectionCommitRequestSchema.parse(mutationInput);
3345
3659
  const attempt = await stores.tasks.get(mutation.taskId);
@@ -3474,6 +3788,36 @@ function createByokCloud(options) {
3474
3788
  throw new ByokCloudError("mailbox_receipt_mismatch", "Agent message disposition mailbox readback mismatched.");
3475
3789
  }
3476
3790
  }
3791
+ async function enqueueFreshAgentEgressOffer(tenant, deviceId, input) {
3792
+ await assertAgentCapabilities(tenant, deviceId, [
3793
+ AGENT_HOME_CONTRACT_CAPABILITY,
3794
+ AGENT_EGRESS_POLICY_CAPABILITY,
3795
+ AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
3796
+ AGENT_EGRESS_FRESH_SESSION_CAPABILITY,
3797
+ ...input.payload.messageEgress === void 0 ? [] : [AGENT_MESSAGE_EGRESS_CAPABILITY],
3798
+ ...input.payload.terminalProjection === void 0 ? [] : [TERMINAL_PROJECTION_SELECTION_CAPABILITY]
3799
+ ]);
3800
+ const payload = TaskOfferForAgentWithEgressFreshPayloadSchema.parse(input.payload);
3801
+ if (payload.messageEgress === void 0 && input.agentMessageContext !== void 0) {
3802
+ throw new Error("agentMessageContext requires messageEgress");
3803
+ }
3804
+ const messageContext = payload.messageEgress === void 0 ? void 0 : AgentMessageServerContextSchema.parse(input.agentMessageContext);
3805
+ const enqueued = await enqueueTaskEnvelope(
3806
+ tenant,
3807
+ deviceId,
3808
+ input.taskId,
3809
+ payload.agentRef,
3810
+ (taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress_fresh", payload, { id: messageId, taskId, seq }),
3811
+ payload.messageEgress === void 0 ? void 0 : async (stores, taskId) => {
3812
+ const body = JSON.stringify({ agentRef: payload.agentRef, requirement: payload.messageEgress, context: messageContext });
3813
+ const recorded = await stores.receipts.record({ key: `agent-message-offer:${deviceId}:${taskId}`, body });
3814
+ if (!recorded.created && recorded.receipt.body !== body) {
3815
+ throw new ByokCloudError("agent_content_request_mismatch", `Task ${taskId} already has a different Agent message context.`);
3816
+ }
3817
+ }
3818
+ );
3819
+ return enqueued;
3820
+ }
3477
3821
  return {
3478
3822
  fetch: registry.fetch,
3479
3823
  routes: registry.routes,
@@ -3516,45 +3860,30 @@ function createByokCloud(options) {
3516
3860
  (taskId, seq, messageId) => createEnvelope("task.offer_for_agent", payload, { id: messageId, taskId, seq })
3517
3861
  );
3518
3862
  },
3519
- async enqueueAgentEgressOffer(tenant, deviceId, input) {
3863
+ async enqueuePreparedOffer(tenant, deviceId, input) {
3520
3864
  await assertAgentCapabilities(tenant, deviceId, [
3521
3865
  AGENT_HOME_CONTRACT_CAPABILITY,
3522
- AGENT_EGRESS_POLICY_CAPABILITY,
3523
- AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
3524
- ...input.payload.messageEgress === void 0 ? [] : [AGENT_MESSAGE_EGRESS_CAPABILITY],
3866
+ AGENT_INPUT_PREPARATION_CAPABILITY,
3525
3867
  ...input.payload.terminalProjection === void 0 ? [] : [TERMINAL_PROJECTION_SELECTION_CAPABILITY]
3526
3868
  ]);
3527
- const payload = TaskOfferForAgentWithEgressPayloadSchema.parse(input.payload);
3528
- if (payload.messageEgress === void 0 && input.agentMessageContext !== void 0) {
3529
- throw new Error("agentMessageContext requires messageEgress");
3530
- }
3531
- const messageContext = payload.messageEgress === void 0 ? void 0 : AgentMessageServerContextSchema.parse(input.agentMessageContext);
3532
- const enqueued = await enqueueTaskEnvelope(
3869
+ const payload = TaskOfferPreparedPayloadSchema.parse(input.payload);
3870
+ return enqueueTaskEnvelope(
3533
3871
  tenant,
3534
3872
  deviceId,
3535
3873
  input.taskId,
3536
3874
  payload.agentRef,
3537
- (taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress", payload, { id: messageId, taskId, seq }),
3538
- payload.messageEgress === void 0 ? void 0 : async (stores, taskId) => {
3539
- const body = JSON.stringify({ agentRef: payload.agentRef, sessionRef: payload.sessionRef, requirement: payload.messageEgress, context: messageContext });
3540
- const recorded = await stores.receipts.record({ key: `agent-message-offer:${deviceId}:${taskId}`, body });
3541
- if (!recorded.created && recorded.receipt.body !== body) {
3542
- throw new ByokCloudError("agent_content_request_mismatch", `Task ${taskId} already has a different Agent message context.`);
3543
- }
3544
- }
3875
+ (taskId, seq, messageId) => createEnvelope("task.offer_prepared", payload, { id: messageId, taskId, seq })
3545
3876
  );
3546
- return enqueued;
3547
3877
  },
3548
- async enqueueFreshAgentEgressOffer(tenant, deviceId, input) {
3878
+ async enqueueAgentEgressOffer(tenant, deviceId, input) {
3549
3879
  await assertAgentCapabilities(tenant, deviceId, [
3550
3880
  AGENT_HOME_CONTRACT_CAPABILITY,
3551
3881
  AGENT_EGRESS_POLICY_CAPABILITY,
3552
3882
  AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
3553
- AGENT_EGRESS_FRESH_SESSION_CAPABILITY,
3554
3883
  ...input.payload.messageEgress === void 0 ? [] : [AGENT_MESSAGE_EGRESS_CAPABILITY],
3555
3884
  ...input.payload.terminalProjection === void 0 ? [] : [TERMINAL_PROJECTION_SELECTION_CAPABILITY]
3556
3885
  ]);
3557
- const payload = TaskOfferForAgentWithEgressFreshPayloadSchema.parse(input.payload);
3886
+ const payload = TaskOfferForAgentWithEgressPayloadSchema.parse(input.payload);
3558
3887
  if (payload.messageEgress === void 0 && input.agentMessageContext !== void 0) {
3559
3888
  throw new Error("agentMessageContext requires messageEgress");
3560
3889
  }
@@ -3564,9 +3893,9 @@ function createByokCloud(options) {
3564
3893
  deviceId,
3565
3894
  input.taskId,
3566
3895
  payload.agentRef,
3567
- (taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress_fresh", payload, { id: messageId, taskId, seq }),
3896
+ (taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress", payload, { id: messageId, taskId, seq }),
3568
3897
  payload.messageEgress === void 0 ? void 0 : async (stores, taskId) => {
3569
- const body = JSON.stringify({ agentRef: payload.agentRef, requirement: payload.messageEgress, context: messageContext });
3898
+ const body = JSON.stringify({ agentRef: payload.agentRef, sessionRef: payload.sessionRef, requirement: payload.messageEgress, context: messageContext });
3570
3899
  const recorded = await stores.receipts.record({ key: `agent-message-offer:${deviceId}:${taskId}`, body });
3571
3900
  if (!recorded.created && recorded.receipt.body !== body) {
3572
3901
  throw new ByokCloudError("agent_content_request_mismatch", `Task ${taskId} already has a different Agent message context.`);
@@ -3575,6 +3904,12 @@ function createByokCloud(options) {
3575
3904
  );
3576
3905
  return enqueued;
3577
3906
  },
3907
+ enqueueFreshAgentEgressOffer,
3908
+ async submitRecurringExecution(tenant, input) {
3909
+ const validated = RecurringExecutionInputSchema.parse(input);
3910
+ if (typeof options.agentMessage?.consume !== "function") throw new Error("Recurring execution requires a registered Agent message consumer.");
3911
+ return enqueueFreshAgentEgressOffer(tenant, validated.deviceId, validated);
3912
+ },
3578
3913
  async enqueueAgentContentRead(tenant, deviceId, input) {
3579
3914
  const payload = AgentContentReadPayloadSchema.parse(input.payload);
3580
3915
  await assertAgentCapabilities(tenant, deviceId, [
@@ -3668,6 +4003,58 @@ function createByokCloud(options) {
3668
4003
  },
3669
4004
  getAgentHomeProjectionStatus,
3670
4005
  completeAgentHomeProjection,
4006
+ async enqueueInputPreparation(tenant, deviceId, input) {
4007
+ const payload = AgentInputPreparationPayloadSchema.parse(input);
4008
+ await assertAgentCapabilities(tenant, deviceId, [
4009
+ AGENT_HOME_CONTRACT_CAPABILITY,
4010
+ AGENT_INPUT_PREPARATION_CAPABILITY
4011
+ ]);
4012
+ const stores = tenantStoresFor(controlPlane(tenant), root);
4013
+ const requestBody = JSON.stringify(payload);
4014
+ const persisted = await stores.receipts.record({
4015
+ key: inputPreparationRequestKey(deviceId, payload.agentRef, payload.requestId),
4016
+ body: requestBody
4017
+ });
4018
+ const persistedPayload = AgentInputPreparationPayloadSchema.parse(JSON.parse(persisted.receipt.body));
4019
+ if (!sameInputPreparationRequest(payload, persistedPayload)) {
4020
+ throw new ByokCloudError(
4021
+ "input_preparation_request_conflict",
4022
+ `Input-preparation request ${payload.requestId} already exists with a different immutable desired body.`
4023
+ );
4024
+ }
4025
+ const messageId = await agentControlEnvelopeId(
4026
+ "byok:agent-input-preparation",
4027
+ tenant,
4028
+ deviceId,
4029
+ payload.agentRef,
4030
+ payload.requestId
4031
+ );
4032
+ const control = await enqueueAgentControlEnvelope(
4033
+ tenant,
4034
+ deviceId,
4035
+ messageId,
4036
+ (seq) => createEnvelope("agent.input.preparation", payload, { id: messageId, seq })
4037
+ );
4038
+ if (control.envelope.type !== "agent.input.preparation" || !sameInputPreparationRequest(payload, control.envelope.payload)) {
4039
+ throw new ByokCloudError(
4040
+ "input_preparation_request_conflict",
4041
+ `Mailbox request ${payload.requestId} does not match its immutable input-preparation fact.`
4042
+ );
4043
+ }
4044
+ const status = await getInputPreparationStatus(tenant, deviceId, {
4045
+ requestId: payload.requestId,
4046
+ agentRef: payload.agentRef
4047
+ });
4048
+ if (status === void 0) {
4049
+ throw new ByokCloudError(
4050
+ "input_preparation_receipt_invalid",
4051
+ `Input-preparation request ${payload.requestId} disappeared after durable allocation.`
4052
+ );
4053
+ }
4054
+ return { ...control, status };
4055
+ },
4056
+ getInputPreparationStatus,
4057
+ completeInputPreparation,
3671
4058
  eraseAgentMemoryProjection,
3672
4059
  approveTask(tenant, taskId, opts) {
3673
4060
  return resolveApproval(tenant, taskId, "approve", opts);
@@ -3752,6 +4139,17 @@ function createByokCloud(options) {
3752
4139
  readTerminalReceipt(tenant, taskId) {
3753
4140
  return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
3754
4141
  },
4142
+ async readDeviceTerminal(tenant, taskId) {
4143
+ const receipt = await tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
4144
+ return receipt === void 0 ? void 0 : readDeviceTerminalReceipt(taskId, receipt);
4145
+ },
4146
+ readTaskAgentMessage(tenant, deviceId, taskId, agentRef) {
4147
+ return readTaskAgentMessage(tenantStoresFor(controlPlane(tenant), root), deviceId, taskId, agentRef);
4148
+ },
4149
+ readAgentMessageDisposition(tenant, deviceId, taskId, payload) {
4150
+ const parsed = AgentMessagePublishPayloadSchema.parse(payload);
4151
+ return readAgentMessageDisposition(tenantStoresFor(controlPlane(tenant), root), deviceId, taskId, parsed);
4152
+ },
3755
4153
  readAgentEgress(tenant, deviceId, agentRef, eventId) {
3756
4154
  return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, agentRef, eventId);
3757
4155
  },
@@ -4406,6 +4804,10 @@ var InMemoryTaskAttemptStore = class {
4406
4804
  return "reserved";
4407
4805
  });
4408
4806
  }
4807
+ async readTaskAgentMessage(tenant, input) {
4808
+ const admission = this.#state.messageAdmissions.get(tenantKey(tenant, input.taskId));
4809
+ return admission?.deviceId === input.deviceId ? admission : void 0;
4810
+ }
4409
4811
  async readAgentMessage(tenant, input) {
4410
4812
  const admission = this.#state.messageAdmissions.get(tenantKey(tenant, input.taskId));
4411
4813
  return admission !== void 0 && admission.deviceId === input.deviceId && admission.messageId === input.messageId && admission.payloadBody === input.payloadBody ? admission : void 0;
@@ -5065,6 +5467,28 @@ function authenticateHostedDeviceAssertion(input, deps) {
5065
5467
  ...deps.maxLifetimeMs === void 0 ? {} : { maxLifetimeMs: deps.maxLifetimeMs }
5066
5468
  });
5067
5469
  }
5470
+ function authenticateHostedTaskAssertion(input, deps) {
5471
+ return authenticateTaskAssertion(input, {
5472
+ verifier: {
5473
+ verify: ({ publicKey, signingInput, signature }) => deps.crypto.verifyEd25519(publicKey, signingInput, signature)
5474
+ },
5475
+ lookupDevice: async (deviceId) => {
5476
+ const row = await deps.devices.resolveByDeviceId(deviceId);
5477
+ if (row === void 0) return void 0;
5478
+ return {
5479
+ tenantId: row.tenantId,
5480
+ productId: row.productId,
5481
+ deviceId: row.deviceId,
5482
+ publicKeyJwkX: row.devicePublicKey,
5483
+ revoked: row.revoked
5484
+ };
5485
+ },
5486
+ replay: deps.replay,
5487
+ expected: deps.expected,
5488
+ now: deps.clock.now(),
5489
+ ...deps.maxLifetimeMs === void 0 ? {} : { maxLifetimeMs: deps.maxLifetimeMs }
5490
+ });
5491
+ }
5068
5492
  var BoardFeedItemSchema = z.object({
5069
5493
  tenantId: z.string(),
5070
5494
  itemId: z.string(),
@@ -5213,6 +5637,7 @@ var CLOUD_PORT_METHODS = {
5213
5637
  "reserveAgentOffer",
5214
5638
  "reserveAgentMessage",
5215
5639
  "readAgentMessage",
5640
+ "readTaskAgentMessage",
5216
5641
  "finalizeAgentMessage",
5217
5642
  "get",
5218
5643
  "getMany",
@@ -5247,6 +5672,6 @@ var CLOUD_PORT_INTERFACES = {
5247
5672
  rateLimiter: "InboundRateLimiter"
5248
5673
  };
5249
5674
 
5250
- export { ACCESS_TOKEN_TTL_SECONDS, AGENT_HOME_CONTRACT_CAPABILITY, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_READ_ERROR_CODES, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryAgentEgressStore, InMemoryAgentMemoryProjectionAuthorizer, InMemoryAgentMemoryProjectionStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, StaleApprovalError, SteerRejectedError, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, activitySourceBatchState, agentHomeProjectionCompletionKey, agentHomeProjectionRequestKey, approvalTimelineCursor, assertTaskAttemptListLimit, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, pendingApproval, projectTerminalResult, projectTimelineEvents, readAgentHomeProjectionStatus, recordAgentHomeProjectionCompletion, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, validateApprovalTimelineResolve, verifyNonceSignature };
5675
+ export { ACCESS_TOKEN_TTL_SECONDS, AGENT_HOME_CONTRACT_CAPABILITY, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_READ_ERROR_CODES, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryAgentEgressStore, InMemoryAgentMemoryProjectionAuthorizer, InMemoryAgentMemoryProjectionStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, RecurringExecutionInputSchema, StaleApprovalError, SteerRejectedError, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, activitySourceBatchState, agentHomeProjectionCompletionKey, agentHomeProjectionRequestKey, approvalTimelineCursor, assertTaskAttemptListLimit, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, authenticateHostedTaskAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, inputPreparationCompletionKey, inputPreparationRequestKey, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, pendingApproval, projectTerminalResult, projectTimelineEvents, readAgentHomeProjectionStatus, readInputPreparationStatus, recordAgentHomeProjectionCompletion, recordInputPreparationCompletion, routeKey, sameInputPreparationRequest, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, validateApprovalTimelineResolve, verifyNonceSignature };
5251
5676
  //# sourceMappingURL=index.js.map
5252
5677
  //# sourceMappingURL=index.js.map