@byok-sdk/cloud 0.17.0 → 0.18.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/cloud.d.ts +16 -2
- package/dist/inbound.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1646 -1568
- package/dist/index.js.map +1 -1
- package/dist/recurring.d.ts +121 -0
- package/dist/stores/in-memory/task-attempts.d.ts +4 -0
- package/dist/stores/ports.d.ts +5 -0
- package/dist/task-agent-message.d.ts +11 -0
- package/dist/tenant-stores.d.ts +4 -0
- package/dist/terminal-result.d.ts +9 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
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';
|
|
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, 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, AgentMessagePublishPayloadSchema, isTaskOfferType, MESSAGE_PAYLOAD_SCHEMAS, createEnvelope, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, 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, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MAILBOX_READ_AHEAD_MAX_SEQS, MAILBOX_READ_AHEAD_CAPABILITY, MessagesSendRequestSchema, 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,1731 @@ 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/
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 hosted Agent-memory mutation did not match the durable task/device/AgentRef binding. */
|
|
95
|
+
agent_memory_projection_task_mismatch: "agent_memory_projection_task_mismatch",
|
|
96
|
+
/** The embedder-owned grant/consent authority denied a hosted memory mutation. */
|
|
97
|
+
agent_memory_projection_authorization_denied: "agent_memory_projection_authorization_denied",
|
|
98
|
+
/** A redacted snapshot's decoded bytes did not match its declared hash or byte count. */
|
|
99
|
+
agent_memory_projection_hash_mismatch: "agent_memory_projection_hash_mismatch",
|
|
100
|
+
/** A writer epoch/source sequence already names a different immutable projection mutation. */
|
|
101
|
+
agent_memory_projection_replay_mismatch: "agent_memory_projection_replay_mismatch",
|
|
102
|
+
/** A hosted memory mutation came from an older writer epoch. */
|
|
103
|
+
agent_memory_projection_stale_epoch: "agent_memory_projection_stale_epoch",
|
|
104
|
+
/** A server-side erase fence prevents a deleted writer epoch from re-entering. */
|
|
105
|
+
agent_memory_projection_erased_epoch: "agent_memory_projection_erased_epoch",
|
|
106
|
+
/** A prior erase reached the protocol writer-epoch ceiling and cannot mint a later writer. */
|
|
107
|
+
agent_memory_projection_epoch_exhausted: "agent_memory_projection_epoch_exhausted",
|
|
108
|
+
/** A hosted memory mutation skipped or reset a required source sequence. */
|
|
109
|
+
agent_memory_projection_sequence_gap: "agent_memory_projection_sequence_gap"
|
|
42
110
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
}
|
|
111
|
+
var ByokCloudError = class extends Error {
|
|
112
|
+
code;
|
|
113
|
+
constructor(code, message, options) {
|
|
114
|
+
super(message, options);
|
|
115
|
+
this.name = "ByokCloudError";
|
|
116
|
+
this.code = code;
|
|
60
117
|
}
|
|
61
|
-
|
|
118
|
+
};
|
|
119
|
+
function isCloudError(value, code) {
|
|
120
|
+
if (!(value instanceof ByokCloudError)) return false;
|
|
121
|
+
return code === void 0 || value.code === code;
|
|
62
122
|
}
|
|
63
123
|
|
|
64
|
-
// src/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
124
|
+
// src/activity.ts
|
|
125
|
+
var DEFAULT_ACTIVITY_CAPACITY = 50;
|
|
126
|
+
var ActivityTaskIdSchema = z.string().min(1).max(200);
|
|
127
|
+
var SourceEnvelopeIdSchema = z.string().max(200).regex(/\S/, "sourceEnvelopeId must not be blank");
|
|
128
|
+
var ActivityAppendRequestSchema = z.object({
|
|
129
|
+
taskId: ActivityTaskIdSchema,
|
|
130
|
+
sourceEnvelopeId: SourceEnvelopeIdSchema,
|
|
131
|
+
batchSeq: z.number().int().nonnegative(),
|
|
132
|
+
events: z.array(AgentEventOrUnknownSchema).min(1),
|
|
133
|
+
dropped: z.number().int().nonnegative()
|
|
134
|
+
});
|
|
135
|
+
var TimelineEventSchema = z.object({
|
|
136
|
+
taskId: ActivityTaskIdSchema,
|
|
137
|
+
sourceEnvelopeId: SourceEnvelopeIdSchema,
|
|
138
|
+
batchSeq: z.number().int().nonnegative(),
|
|
139
|
+
eventIndex: z.number().int().nonnegative(),
|
|
140
|
+
/**
|
|
141
|
+
* The producer-declared loss attached to this source batch. It travels with
|
|
142
|
+
* every retained event so a replay includes it in the canonical batch
|
|
143
|
+
* binding rather than treating the aggregate as an incidental counter.
|
|
144
|
+
*/
|
|
145
|
+
sourceDropped: z.number().int().nonnegative().optional(),
|
|
146
|
+
receivedAt: z.iso.datetime(),
|
|
147
|
+
event: AgentEventOrUnknownSchema
|
|
148
|
+
});
|
|
149
|
+
function validateActivityAppend(input) {
|
|
150
|
+
if (!Number.isFinite(input.ttlMs) || input.ttlMs <= 0) {
|
|
151
|
+
throw new ByokCloudError(
|
|
152
|
+
"coordination_input_invalid",
|
|
153
|
+
`Activity ttl must be a positive number of milliseconds, received ${String(input.ttlMs)}.`
|
|
154
|
+
);
|
|
73
155
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.status = status;
|
|
81
|
-
this.runtime = runtime;
|
|
82
|
-
this.name = "SteerRejectedError";
|
|
156
|
+
const capacity = input.capacity ?? DEFAULT_ACTIVITY_CAPACITY;
|
|
157
|
+
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
158
|
+
throw new ByokCloudError(
|
|
159
|
+
"coordination_input_invalid",
|
|
160
|
+
`Activity capacity must be a positive integer, received ${String(capacity)}.`
|
|
161
|
+
);
|
|
83
162
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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;
|
|
163
|
+
const parsed = ActivityAppendRequestSchema.safeParse(input);
|
|
164
|
+
if (!parsed.success) {
|
|
165
|
+
throw new ByokCloudError(
|
|
166
|
+
"coordination_input_invalid",
|
|
167
|
+
"Activity batches require stable source identity, order, at least one valid event, and a non-negative dropped count."
|
|
168
|
+
);
|
|
107
169
|
}
|
|
170
|
+
return capacity;
|
|
108
171
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
172
|
+
function projectTimelineEvents(input, receivedAt) {
|
|
173
|
+
return input.events.map(
|
|
174
|
+
(event, eventIndex) => TimelineEventSchema.parse({
|
|
175
|
+
taskId: input.taskId,
|
|
176
|
+
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
177
|
+
batchSeq: input.batchSeq,
|
|
178
|
+
eventIndex,
|
|
179
|
+
sourceDropped: input.dropped,
|
|
180
|
+
receivedAt,
|
|
181
|
+
event
|
|
182
|
+
})
|
|
183
|
+
);
|
|
116
184
|
}
|
|
117
|
-
function
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
185
|
+
function activitySourceBatchState(entries, input) {
|
|
186
|
+
const existing = entries.filter((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId).sort((left, right) => left.eventIndex - right.eventIndex);
|
|
187
|
+
if (existing.length === 0) return "absent";
|
|
188
|
+
if (existing.length !== input.events.length) return "conflict";
|
|
189
|
+
for (const [eventIndex, event] of input.events.entries()) {
|
|
190
|
+
const stored = existing[eventIndex];
|
|
191
|
+
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)) {
|
|
192
|
+
return "conflict";
|
|
193
|
+
}
|
|
121
194
|
}
|
|
122
|
-
return
|
|
195
|
+
return "same";
|
|
123
196
|
}
|
|
124
|
-
function
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
};
|
|
197
|
+
function parseTimelineEvents(value) {
|
|
198
|
+
return z.array(TimelineEventSchema).parse(value);
|
|
199
|
+
}
|
|
200
|
+
function compareTimelineEvents(left, right) {
|
|
201
|
+
return left.batchSeq - right.batchSeq || left.eventIndex - right.eventIndex;
|
|
202
|
+
}
|
|
203
|
+
function activityCursor(entries) {
|
|
204
|
+
const last = entries.at(-1);
|
|
205
|
+
return last === void 0 ? void 0 : { batchSeq: last.batchSeq, eventIndex: last.eventIndex };
|
|
206
|
+
}
|
|
207
|
+
function activityTailKey(tenant, taskId) {
|
|
208
|
+
return tenantKey(tenant, taskId);
|
|
178
209
|
}
|
|
179
210
|
|
|
180
|
-
// src/
|
|
181
|
-
var
|
|
182
|
-
|
|
183
|
-
|
|
211
|
+
// src/coordination.ts
|
|
212
|
+
var DEFAULT_BOARD_CHANNEL_MAX_BYTES = 128;
|
|
213
|
+
var DEFAULT_BOARD_TITLE_MAX_BYTES = 512;
|
|
214
|
+
var DEFAULT_ACTIVITY_MAX_EVENTS = 50;
|
|
215
|
+
var DEFAULT_ACTIVITY_MAX_BYTES = 64 * 1024;
|
|
216
|
+
var DEFAULT_ACTIVITY_TTL_MS = 10 * 6e4;
|
|
217
|
+
var DEFAULT_PRESENCE_TTL_MS = 9e4;
|
|
218
|
+
var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
|
|
219
|
+
var DEFAULT_PRESENCE_DETAIL_MAX_BYTES = 512;
|
|
220
|
+
var encoder = new TextEncoder();
|
|
221
|
+
var DEFAULT_ACTIVITY_BOUNDS = {
|
|
222
|
+
maxEvents: DEFAULT_ACTIVITY_MAX_EVENTS,
|
|
223
|
+
maxBytes: DEFAULT_ACTIVITY_MAX_BYTES,
|
|
224
|
+
capacity: DEFAULT_ACTIVITY_CAPACITY,
|
|
225
|
+
ttlMs: DEFAULT_ACTIVITY_TTL_MS
|
|
226
|
+
};
|
|
227
|
+
function assertBoardLabels(channel, title, limits) {
|
|
228
|
+
if (channel.length === 0 || encoder.encode(channel).length > limits.channelMaxBytes) {
|
|
229
|
+
throw new ByokCloudError(
|
|
230
|
+
"coordination_input_invalid",
|
|
231
|
+
`Board channel must contain 1..${limits.channelMaxBytes} UTF-8 bytes.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (title.length === 0 || encoder.encode(title).length > limits.titleMaxBytes) {
|
|
235
|
+
throw new ByokCloudError(
|
|
236
|
+
"coordination_input_invalid",
|
|
237
|
+
`Board title must contain 1..${limits.titleMaxBytes} UTF-8 bytes.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
184
240
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
241
|
+
function assertTaskAttemptListLimit(limit) {
|
|
242
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
243
|
+
throw new ByokCloudError(
|
|
244
|
+
"coordination_input_invalid",
|
|
245
|
+
`Task attempt list limit must be a positive integer; received ${String(limit)}.`
|
|
246
|
+
);
|
|
188
247
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
248
|
+
}
|
|
249
|
+
function validateActivityEvents(events, dropped, bounds) {
|
|
250
|
+
if (!Number.isSafeInteger(dropped) || dropped < 0) {
|
|
251
|
+
throw new ByokCloudError(
|
|
252
|
+
"coordination_input_invalid",
|
|
253
|
+
"Activity dropped must be a non-negative integer."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (events.length === 0 || events.length > bounds.maxEvents) {
|
|
257
|
+
throw new ByokCloudError(
|
|
258
|
+
"activity_batch_too_large",
|
|
259
|
+
`Activity batch must contain 1..${bounds.maxEvents} events.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const encoded = JSON.stringify(events);
|
|
263
|
+
if (encoder.encode(encoded).length > bounds.maxBytes) {
|
|
264
|
+
throw new ByokCloudError(
|
|
265
|
+
"activity_batch_too_large",
|
|
266
|
+
`Activity batch exceeds ${bounds.maxBytes} UTF-8 bytes.`
|
|
267
|
+
);
|
|
193
268
|
}
|
|
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
269
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
270
|
+
function validateActivityBatch(input, bounds) {
|
|
271
|
+
validateActivityEvents(input.events, input.dropped, bounds);
|
|
272
|
+
validateActivityAppend({ ...input, ttlMs: bounds.ttlMs, capacity: bounds.capacity });
|
|
246
273
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
};
|
|
274
|
+
async function appendActivityEvents(activity, input, bounds) {
|
|
275
|
+
validateActivityBatch(input, bounds);
|
|
276
|
+
return activity.append({
|
|
277
|
+
taskId: input.taskId,
|
|
278
|
+
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
279
|
+
batchSeq: input.batchSeq,
|
|
280
|
+
events: input.events,
|
|
281
|
+
dropped: input.dropped,
|
|
282
|
+
ttlMs: bounds.ttlMs,
|
|
283
|
+
capacity: bounds.capacity
|
|
284
|
+
});
|
|
301
285
|
}
|
|
302
|
-
var
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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(),
|
|
286
|
+
var DEFAULT_APPROVAL_TIMELINE_CAPACITY = 50;
|
|
287
|
+
var DEFAULT_APPROVAL_TIMELINE_TTL_MS = 10 * 60 * 1e3;
|
|
288
|
+
var APPROVAL_SUMMARY_MAX_BYTES = 16 * 1024;
|
|
289
|
+
var NonBlankIdSchema = z.string().max(200).regex(/\S/, "value must not be blank");
|
|
290
|
+
var TaskIdSchema = z.string().min(1).max(200);
|
|
291
|
+
var ApprovalRequestedEventSchema = z.object({
|
|
292
|
+
type: z.literal("approval_requested"),
|
|
293
|
+
summary: z.string(),
|
|
294
|
+
approvalId: NonBlankIdSchema.optional()
|
|
295
|
+
}).strict();
|
|
296
|
+
var LocalApprovalResolutionEventSchema = z.object({
|
|
297
|
+
type: z.literal("approval_resolved"),
|
|
298
|
+
// Absent only when a daemon resolved the single outstanding request without
|
|
299
|
+
// native approval identity. No id is synthesized.
|
|
300
|
+
approvalId: NonBlankIdSchema.optional(),
|
|
301
|
+
decision: z.enum(["approve", "reject"]),
|
|
302
|
+
// A daemon's `task.approval_resolved` is pure wire observation; it cannot
|
|
303
|
+
// declare a host command payload.
|
|
304
|
+
resolvedBy: z.literal("local"),
|
|
305
|
+
at: z.iso.datetime({ offset: true })
|
|
306
|
+
}).strict();
|
|
307
|
+
var HostApprovalResolutionEventSchema = z.object({
|
|
308
|
+
type: z.literal("approval_resolved"),
|
|
309
|
+
approvalId: NonBlankIdSchema.optional(),
|
|
310
|
+
decision: z.literal("approve"),
|
|
311
|
+
resolvedBy: z.literal("host"),
|
|
312
|
+
at: z.iso.datetime({ offset: true })
|
|
313
|
+
}).strict();
|
|
314
|
+
var HostRejectResolutionEventSchema = z.object({
|
|
315
|
+
type: z.literal("approval_resolved"),
|
|
316
|
+
approvalId: NonBlankIdSchema.optional(),
|
|
317
|
+
decision: z.literal("reject"),
|
|
318
|
+
resolvedBy: z.literal("host"),
|
|
319
|
+
// Required even when the host deliberately omitted a wire reason. `null`
|
|
320
|
+
// records that complete payload, so a historical host reject with no field
|
|
321
|
+
// cannot be mistaken for a recoverable executable command.
|
|
322
|
+
reason: z.string().nullable(),
|
|
323
|
+
at: z.iso.datetime({ offset: true })
|
|
324
|
+
}).strict();
|
|
325
|
+
var ApprovalTimelineEventSchema = z.union([
|
|
326
|
+
ApprovalRequestedEventSchema,
|
|
327
|
+
LocalApprovalResolutionEventSchema,
|
|
328
|
+
HostApprovalResolutionEventSchema,
|
|
329
|
+
HostRejectResolutionEventSchema
|
|
330
|
+
]);
|
|
331
|
+
var ApprovalObservationSchema = z.object({
|
|
332
|
+
taskId: TaskIdSchema,
|
|
333
|
+
sourceEnvelopeId: NonBlankIdSchema,
|
|
334
|
+
revision: z.number().int().positive(),
|
|
486
335
|
receivedAt: z.iso.datetime(),
|
|
487
|
-
event:
|
|
336
|
+
event: ApprovalTimelineEventSchema
|
|
488
337
|
});
|
|
489
|
-
function
|
|
490
|
-
|
|
338
|
+
function validateApprovalTimelineAppend(input) {
|
|
339
|
+
const capacity = input.capacity ?? DEFAULT_APPROVAL_TIMELINE_CAPACITY;
|
|
340
|
+
const ttlMs = input.ttlMs ?? DEFAULT_APPROVAL_TIMELINE_TTL_MS;
|
|
341
|
+
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
491
342
|
throw new ByokCloudError(
|
|
492
343
|
"coordination_input_invalid",
|
|
493
|
-
`
|
|
344
|
+
`Approval timeline capacity must be a positive integer, received ${String(capacity)}.`
|
|
494
345
|
);
|
|
495
346
|
}
|
|
496
|
-
|
|
497
|
-
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
347
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
498
348
|
throw new ByokCloudError(
|
|
499
349
|
"coordination_input_invalid",
|
|
500
|
-
`
|
|
350
|
+
`Approval timeline ttl must be a positive number of milliseconds, received ${String(ttlMs)}.`
|
|
501
351
|
);
|
|
502
352
|
}
|
|
503
|
-
const parsed =
|
|
353
|
+
const parsed = z.object({
|
|
354
|
+
taskId: TaskIdSchema,
|
|
355
|
+
sourceEnvelopeId: NonBlankIdSchema,
|
|
356
|
+
event: ApprovalTimelineEventSchema
|
|
357
|
+
}).safeParse(input);
|
|
504
358
|
if (!parsed.success) {
|
|
505
359
|
throw new ByokCloudError(
|
|
506
360
|
"coordination_input_invalid",
|
|
507
|
-
"
|
|
361
|
+
"Approval observations require stable source identity and a valid native lifecycle event."
|
|
508
362
|
);
|
|
509
363
|
}
|
|
510
|
-
|
|
364
|
+
if (parsed.data.event.type === "approval_requested" && new TextEncoder().encode(parsed.data.event.summary).byteLength > APPROVAL_SUMMARY_MAX_BYTES) {
|
|
365
|
+
throw new ByokCloudError(
|
|
366
|
+
"coordination_input_invalid",
|
|
367
|
+
`Approval summary exceeds ${APPROVAL_SUMMARY_MAX_BYTES} UTF-8 bytes.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return { capacity, ttlMs, event: parsed.data.event };
|
|
511
371
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
}
|
|
372
|
+
function validateApprovalTimelineResolve(input) {
|
|
373
|
+
const validated = validateApprovalTimelineAppend(input);
|
|
374
|
+
const expected = z.object({
|
|
375
|
+
expectedSourceEnvelopeId: NonBlankIdSchema,
|
|
376
|
+
expectedRevision: z.number().int().positive()
|
|
377
|
+
}).safeParse(input);
|
|
378
|
+
if (!expected.success || validated.event.type !== "approval_resolved" || validated.event.resolvedBy !== "host") {
|
|
379
|
+
throw new ByokCloudError(
|
|
380
|
+
"coordination_input_invalid",
|
|
381
|
+
"Host approval resolution requires an exact pending source/revision and a host resolution event."
|
|
382
|
+
);
|
|
534
383
|
}
|
|
535
|
-
return
|
|
384
|
+
return validated;
|
|
536
385
|
}
|
|
537
|
-
function
|
|
538
|
-
return
|
|
386
|
+
function approvalTimelineKey(tenant, taskId) {
|
|
387
|
+
return tenantKey(tenant, taskId);
|
|
539
388
|
}
|
|
540
|
-
function
|
|
541
|
-
return
|
|
389
|
+
function parseApprovalObservations(value) {
|
|
390
|
+
return z.array(ApprovalObservationSchema).parse(value);
|
|
542
391
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
return last === void 0 ? void 0 : { batchSeq: last.batchSeq, eventIndex: last.eventIndex };
|
|
392
|
+
function approvalTimelineCursor(entries) {
|
|
393
|
+
return entries.at(-1)?.revision;
|
|
546
394
|
}
|
|
547
|
-
function
|
|
548
|
-
|
|
395
|
+
async function projectTerminalToReview(board, taskId) {
|
|
396
|
+
const item = await board.get(taskId);
|
|
397
|
+
if (item === void 0 || item.status !== "in_progress") return;
|
|
398
|
+
try {
|
|
399
|
+
await board.updateStatus({
|
|
400
|
+
itemId: taskId,
|
|
401
|
+
expectedStatus: "in_progress",
|
|
402
|
+
status: "in_review"
|
|
403
|
+
});
|
|
404
|
+
} catch (caught) {
|
|
405
|
+
if (isCoreConflictError(caught)) return;
|
|
406
|
+
throw caught;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function agentReliabilityKey(domain, deviceId, agentRef, id) {
|
|
410
|
+
const exact = AgentRefSchema.parse(agentRef);
|
|
411
|
+
return `${domain}:${JSON.stringify([deviceId, exact.agentId, exact.profileRevision, id])}`;
|
|
549
412
|
}
|
|
550
413
|
|
|
551
|
-
// src/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
);
|
|
414
|
+
// src/inbound.ts
|
|
415
|
+
async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume, alreadyDeduplicated) {
|
|
416
|
+
const device = await stores.devices.get(deviceId);
|
|
417
|
+
const attempt = await stores.tasks.get(taskId);
|
|
418
|
+
const binding = await stores.receipts.get(`agent-message-offer:${deviceId}:${taskId}`);
|
|
419
|
+
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" };
|
|
420
|
+
let frozen;
|
|
421
|
+
try {
|
|
422
|
+
frozen = JSON.parse(binding.body);
|
|
423
|
+
} catch {
|
|
424
|
+
return { outcome: "rejected" };
|
|
573
425
|
}
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
426
|
+
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" };
|
|
427
|
+
const priorDisposition = await readAgentMessageDisposition(stores, deviceId, taskId, payload);
|
|
428
|
+
if (priorDisposition !== void 0) return { outcome: "duplicate", disposition: priorDisposition };
|
|
429
|
+
const payloadBody = JSON.stringify(payload);
|
|
430
|
+
const reservation = await stores.tasks.reserveAgentMessage({
|
|
431
|
+
taskId,
|
|
432
|
+
deviceId,
|
|
433
|
+
messageId: payload.messageId,
|
|
434
|
+
payloadBody
|
|
435
|
+
});
|
|
436
|
+
if (reservation !== "reserved" && reservation !== "pending") return { outcome: "rejected" };
|
|
437
|
+
let decision;
|
|
438
|
+
if (consume === void 0) {
|
|
439
|
+
if (reservation === "pending") {
|
|
440
|
+
throw new Error("Agent message consumer is unavailable while reconciling an exact pending admission.");
|
|
441
|
+
}
|
|
442
|
+
decision = { outcome: "held", reasonCode: "consumer_unavailable" };
|
|
443
|
+
} else {
|
|
444
|
+
decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
|
|
579
445
|
}
|
|
446
|
+
const disposition = agentMessageDisposition(payload, decision);
|
|
447
|
+
const admission = await stores.tasks.finalizeAgentMessage({
|
|
448
|
+
taskId,
|
|
449
|
+
deviceId,
|
|
450
|
+
messageId: payload.messageId,
|
|
451
|
+
payloadBody,
|
|
452
|
+
terminalBody: JSON.stringify({ payload, disposition })
|
|
453
|
+
});
|
|
454
|
+
const terminal = admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
|
|
455
|
+
return terminal === void 0 ? { outcome: "rejected" } : { outcome: "accepted", disposition: terminal };
|
|
580
456
|
}
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
457
|
+
function agentMessageDisposition(payload, decision) {
|
|
458
|
+
return {
|
|
459
|
+
agentRef: payload.agentRef,
|
|
460
|
+
sessionRef: payload.sessionRef,
|
|
461
|
+
contract: payload.contract,
|
|
462
|
+
messageId: payload.messageId,
|
|
463
|
+
cursor: payload.cursor,
|
|
464
|
+
contentHash: payload.contentHash,
|
|
465
|
+
outcome: decision.outcome,
|
|
466
|
+
receiptId: crypto.randomUUID(),
|
|
467
|
+
...decision.reasonCode === void 0 ? {} : { reasonCode: decision.reasonCode }
|
|
468
|
+
};
|
|
588
469
|
}
|
|
589
|
-
function
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
)
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
470
|
+
async function readAgentMessageDisposition(stores, deviceId, taskId, payload) {
|
|
471
|
+
const admission = await stores.tasks.readAgentMessage({
|
|
472
|
+
taskId,
|
|
473
|
+
deviceId,
|
|
474
|
+
messageId: payload.messageId,
|
|
475
|
+
payloadBody: JSON.stringify(payload)
|
|
476
|
+
});
|
|
477
|
+
return admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
|
|
478
|
+
}
|
|
479
|
+
function parseAgentMessageDisposition(payload, terminalBody) {
|
|
480
|
+
const invalid = () => new Error("Invalid persisted Agent message disposition.");
|
|
481
|
+
let decoded;
|
|
482
|
+
try {
|
|
483
|
+
decoded = JSON.parse(terminalBody);
|
|
484
|
+
} catch {
|
|
485
|
+
throw invalid();
|
|
601
486
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
487
|
+
if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded)) throw invalid();
|
|
488
|
+
const record = decoded;
|
|
489
|
+
const storedPayload = AgentMessagePublishPayloadSchema.safeParse(record.payload);
|
|
490
|
+
const storedDisposition = AgentMessageDispositionPayloadSchema.safeParse(record.disposition);
|
|
491
|
+
if (!storedPayload.success || !storedDisposition.success || JSON.stringify(record.payload) !== JSON.stringify(payload)) throw invalid();
|
|
492
|
+
const disposition = storedDisposition.data;
|
|
493
|
+
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();
|
|
494
|
+
return disposition;
|
|
495
|
+
}
|
|
496
|
+
function terminalReceiptKey(taskId) {
|
|
497
|
+
return `task:${taskId}:terminal`;
|
|
498
|
+
}
|
|
499
|
+
function sameAgentRef(expected, actual) {
|
|
500
|
+
return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
|
|
501
|
+
}
|
|
502
|
+
function sameReliableEgress(expected, actual) {
|
|
503
|
+
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);
|
|
504
|
+
}
|
|
505
|
+
function contentReadCapability(surface) {
|
|
506
|
+
switch (surface) {
|
|
507
|
+
case "workspace":
|
|
508
|
+
return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
|
|
509
|
+
case "transcript":
|
|
510
|
+
return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
|
|
511
|
+
case "artifact":
|
|
512
|
+
return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
|
|
608
513
|
}
|
|
609
514
|
}
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
validateActivityAppend({ ...input, ttlMs: bounds.ttlMs, capacity: bounds.capacity });
|
|
515
|
+
function hasCapabilities(capabilities, required) {
|
|
516
|
+
return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
|
|
613
517
|
}
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
});
|
|
518
|
+
function contentRequestReceiptKey(deviceId, agentRef, requestId) {
|
|
519
|
+
return agentReliabilityKey("agent-content-request", deviceId, agentRef, requestId);
|
|
625
520
|
}
|
|
626
|
-
function
|
|
627
|
-
|
|
628
|
-
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
629
|
-
return match?.[1];
|
|
521
|
+
function contentReceiptKey(deviceId, agentRef, requestId) {
|
|
522
|
+
return agentReliabilityKey("agent-content-receipt", deviceId, agentRef, requestId);
|
|
630
523
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (deps.instanceProductId !== void 0 && claims.productId !== deps.instanceProductId) {
|
|
524
|
+
function matchesContentReadReceipt(request, receipt) {
|
|
525
|
+
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;
|
|
526
|
+
}
|
|
527
|
+
async function readContentRequest(stores, deviceId, agentRef, requestId) {
|
|
528
|
+
const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, agentRef, requestId));
|
|
529
|
+
if (stored === void 0) return void 0;
|
|
530
|
+
try {
|
|
531
|
+
return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
|
|
532
|
+
} catch {
|
|
641
533
|
return void 0;
|
|
642
534
|
}
|
|
643
|
-
return {
|
|
644
|
-
kind: "device",
|
|
645
|
-
tenantId: device.tenantId,
|
|
646
|
-
productId: device.productId,
|
|
647
|
-
deviceId: device.deviceId
|
|
648
|
-
};
|
|
649
535
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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;
|
|
536
|
+
function inboundAgentRef(envelope) {
|
|
537
|
+
switch (envelope.type) {
|
|
538
|
+
case "task.claim":
|
|
539
|
+
case "task.decline":
|
|
540
|
+
case "task.complete":
|
|
541
|
+
case "task.fail":
|
|
542
|
+
case "task.cancelled":
|
|
543
|
+
return envelope.payload.agentRef;
|
|
544
|
+
default:
|
|
545
|
+
return void 0;
|
|
745
546
|
}
|
|
746
547
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
548
|
+
function agentRefEchoRequired(envelope) {
|
|
549
|
+
return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
|
|
550
|
+
}
|
|
551
|
+
async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume, observer) {
|
|
552
|
+
if (envelope.v !== PROTOCOL_VERSION) return "rejected";
|
|
553
|
+
const outcome = await applyInboundGate(
|
|
554
|
+
stores,
|
|
555
|
+
deviceId,
|
|
556
|
+
envelope,
|
|
557
|
+
activityBounds,
|
|
558
|
+
agentMessageConsume
|
|
559
|
+
);
|
|
560
|
+
if (outcome === "accepted" && observer !== void 0) {
|
|
561
|
+
try {
|
|
562
|
+
observer.onInboundCommitted({ tenantId: stores.tenant, deviceId, envelope, outcome });
|
|
563
|
+
} catch {
|
|
754
564
|
}
|
|
755
565
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
return { body: void 0, tooLarge: true };
|
|
768
|
-
}
|
|
769
|
-
if (value.byteLength === 0) continue;
|
|
770
|
-
chunks.push(value);
|
|
771
|
-
total += value.byteLength;
|
|
566
|
+
return outcome;
|
|
567
|
+
}
|
|
568
|
+
async function applyInboundGate(stores, deviceId, envelope, activityBounds, agentMessageConsume) {
|
|
569
|
+
if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
|
|
570
|
+
if (envelope.type === "conn.hello") {
|
|
571
|
+
if (envelope.payload.deviceId !== deviceId) return "rejected";
|
|
572
|
+
if (envelope.payload.harnesses !== void 0 && !envelope.payload.capabilities.includes("custom-harness")) return "rejected";
|
|
573
|
+
if (!envelope.payload.protocolVersions.includes(PROTOCOL_VERSION)) return "rejected";
|
|
574
|
+
const device = await stores.devices.get(deviceId);
|
|
575
|
+
if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
|
|
576
|
+
return "rejected";
|
|
772
577
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
578
|
+
if (await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities, ...envelope.payload.harnesses === void 0 ? {} : { harnesses: envelope.payload.harnesses } }) === void 0) {
|
|
579
|
+
return "rejected";
|
|
580
|
+
}
|
|
581
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, void 0);
|
|
777
582
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
583
|
+
if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
|
|
584
|
+
if (envelope.type === "agent.message.publish") {
|
|
585
|
+
const message = await handleAgentMessagePublish(
|
|
586
|
+
stores,
|
|
587
|
+
deviceId,
|
|
588
|
+
envelope.task_id,
|
|
589
|
+
envelope.payload,
|
|
590
|
+
agentMessageConsume);
|
|
591
|
+
return message.outcome === "rejected" ? "rejected" : completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
|
|
783
592
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
593
|
+
if (envelope.type === "agent.egress.reliable") {
|
|
594
|
+
const device = await stores.devices.get(deviceId);
|
|
595
|
+
if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
596
|
+
return "rejected";
|
|
597
|
+
}
|
|
598
|
+
const outcome = await stores.egress.record({
|
|
599
|
+
deviceId,
|
|
600
|
+
payload: envelope.payload,
|
|
601
|
+
receiptId: crypto.randomUUID()
|
|
790
602
|
});
|
|
791
|
-
|
|
603
|
+
if (!outcome.created && !sameReliableEgress(outcome.record.payload, envelope.payload)) return "rejected";
|
|
604
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
|
|
792
605
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
606
|
+
if (envelope.type === "agent.content.receipt") {
|
|
607
|
+
const device = await stores.devices.get(deviceId);
|
|
608
|
+
if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
609
|
+
return "rejected";
|
|
610
|
+
}
|
|
611
|
+
const request = await readContentRequest(stores, deviceId, envelope.payload.agentRef, envelope.payload.requestId);
|
|
612
|
+
if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
|
|
613
|
+
const key = contentReceiptKey(deviceId, envelope.payload.agentRef, envelope.payload.requestId);
|
|
614
|
+
const body = JSON.stringify(envelope.payload);
|
|
615
|
+
const receipt = await stores.receipts.record({ key, body });
|
|
616
|
+
if (!receipt.created && receipt.receipt.body !== body) return "rejected";
|
|
617
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
|
|
801
618
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
619
|
+
const taskId = envelope.task_id;
|
|
620
|
+
if (taskId === void 0) return "rejected";
|
|
621
|
+
const attempt = await stores.tasks.get(taskId);
|
|
622
|
+
if (attempt === void 0 || attempt.deviceId !== deviceId) return "rejected";
|
|
623
|
+
if (attempt?.ownerDeviceId !== void 0 && attempt.ownerDeviceId !== deviceId) return "rejected";
|
|
624
|
+
if (envelope.type === "task.claim" && envelope.payload.deviceId !== deviceId) return "rejected";
|
|
625
|
+
if (envelope.type === "task.claim") {
|
|
626
|
+
const harnessId = envelope.payload.harnessId;
|
|
627
|
+
if (harnessId !== void 0 && envelope.payload.runtime !== void 0) return "rejected";
|
|
628
|
+
if (attempt.ownerDeviceId !== void 0 && (harnessId !== void 0 || attempt.claimedHarnessId !== void 0) && harnessId !== attempt.claimedHarnessId) return "rejected";
|
|
629
|
+
const device = await stores.devices.get(deviceId);
|
|
630
|
+
if (harnessId !== void 0 && (!device?.capabilities?.includes("custom-harness") || !device.harnesses?.some((harness) => harness.id === harnessId))) return "rejected";
|
|
631
|
+
const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
|
|
632
|
+
const offer = await stores.receipts.get(`task-offer:${messageId}`);
|
|
633
|
+
if (offer !== void 0) {
|
|
634
|
+
const sealed = JSON.parse(offer.body);
|
|
635
|
+
if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
|
|
636
|
+
if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
|
|
637
|
+
} else if (harnessId !== void 0) return "rejected";
|
|
808
638
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
}
|
|
639
|
+
let unclaimedInterruption = false;
|
|
640
|
+
if (envelope.type === "task.fail" && envelope.payload.recovery !== void 0) {
|
|
641
|
+
if (envelope.payload.reason !== "daemon_interrupted" || envelope.payload.retryable !== false) return "rejected";
|
|
642
|
+
const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
|
|
643
|
+
if (envelope.payload.recovery.offerId !== messageId) return "rejected";
|
|
644
|
+
const offer = await stores.receipts.get(`task-offer:${messageId}`);
|
|
645
|
+
if (offer === void 0) return "rejected";
|
|
646
|
+
const sealed = JSON.parse(offer.body);
|
|
647
|
+
const harnessId = envelope.payload.harnessId;
|
|
648
|
+
if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
|
|
649
|
+
if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
|
|
650
|
+
unclaimedInterruption = attempt.ownerDeviceId === void 0 && attempt.claimedHarnessId === void 0;
|
|
651
|
+
}
|
|
652
|
+
if ((envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled") && !unclaimedInterruption && envelope.payload.harnessId !== attempt.claimedHarnessId) return "rejected";
|
|
653
|
+
if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
|
|
654
|
+
return "rejected";
|
|
655
|
+
}
|
|
656
|
+
if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
|
|
869
657
|
try {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
658
|
+
validateActivityBatch(
|
|
659
|
+
{
|
|
660
|
+
taskId,
|
|
661
|
+
sourceEnvelopeId: envelope.id,
|
|
662
|
+
batchSeq: envelope.payload.seq,
|
|
663
|
+
events: envelope.payload.events,
|
|
664
|
+
dropped: 0
|
|
665
|
+
},
|
|
666
|
+
activityBounds
|
|
878
667
|
);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
return
|
|
668
|
+
} catch (caught) {
|
|
669
|
+
if (isCloudError(caught, "activity_batch_too_large") || isCloudError(caught, "coordination_input_invalid")) {
|
|
670
|
+
return "rejected";
|
|
882
671
|
}
|
|
883
|
-
|
|
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;
|
|
672
|
+
throw caught;
|
|
892
673
|
}
|
|
893
|
-
}
|
|
674
|
+
}
|
|
675
|
+
const approvalInput = approvalTimelineAppendInput(taskId, envelope);
|
|
676
|
+
if (approvalInput !== void 0) {
|
|
677
|
+
try {
|
|
678
|
+
validateApprovalTimelineAppend(approvalInput);
|
|
679
|
+
} catch (caught) {
|
|
680
|
+
if (isCloudError(caught, "coordination_input_invalid")) return "rejected";
|
|
681
|
+
throw caught;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const lifecycleOutcome = await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
|
|
685
|
+
if (lifecycleOutcome === "rejected") return "rejected";
|
|
686
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, attempt?.agentRef);
|
|
894
687
|
}
|
|
895
|
-
function
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
};
|
|
688
|
+
async function completeInboundEnvelope(stores, deviceId, envelopeId, agentRef) {
|
|
689
|
+
const duplicate = agentRef === void 0 ? await stores.dedup.checkAndRecord(deviceId, envelopeId) : await stores.dedup.checkAndRecordAgent(deviceId, agentRef, envelopeId);
|
|
690
|
+
return duplicate ? "duplicate" : "accepted";
|
|
904
691
|
}
|
|
905
|
-
function
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (contentLength !== void 0 && /^\d+$/u.test(contentLength) && BigInt(contentLength) > expectedBytes) {
|
|
918
|
-
return c.json({ error: "upload exceeds declared size" }, 413);
|
|
692
|
+
async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
|
|
693
|
+
switch (envelope.type) {
|
|
694
|
+
case "task.claim": {
|
|
695
|
+
const claimed = await stores.tasks.claim({
|
|
696
|
+
taskId,
|
|
697
|
+
deviceId,
|
|
698
|
+
...envelope.payload.runtime === void 0 ? {} : { runtime: envelope.payload.runtime },
|
|
699
|
+
...envelope.payload.harnessId === void 0 ? {} : { harnessId: envelope.payload.harnessId },
|
|
700
|
+
...envelope.payload.capabilities === void 0 ? {} : { capabilities: envelope.payload.capabilities }
|
|
701
|
+
});
|
|
702
|
+
if (claimed === void 0 || claimed.ownerDeviceId !== deviceId || claimed.claimedRuntime !== envelope.payload.runtime || claimed.claimedHarnessId !== envelope.payload.harnessId) return "rejected";
|
|
703
|
+
return;
|
|
919
704
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
705
|
+
case "task.started":
|
|
706
|
+
await stores.tasks.recordStatus({
|
|
707
|
+
taskId,
|
|
708
|
+
status: "running",
|
|
709
|
+
...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
|
|
710
|
+
});
|
|
711
|
+
return;
|
|
712
|
+
case "task.decline":
|
|
713
|
+
await recordTerminal(stores, taskId, envelope);
|
|
714
|
+
return;
|
|
715
|
+
case "task.progress":
|
|
716
|
+
if (envelope.payload.events.length > 0) {
|
|
717
|
+
await appendActivityEvents(
|
|
718
|
+
stores.activity,
|
|
719
|
+
{
|
|
720
|
+
taskId,
|
|
721
|
+
sourceEnvelopeId: envelope.id,
|
|
722
|
+
batchSeq: envelope.payload.seq,
|
|
723
|
+
events: envelope.payload.events,
|
|
724
|
+
dropped: 0
|
|
725
|
+
},
|
|
726
|
+
activityBounds
|
|
727
|
+
);
|
|
940
728
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
729
|
+
return;
|
|
730
|
+
case "task.await_approval":
|
|
731
|
+
await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
|
|
732
|
+
return;
|
|
733
|
+
case "task.approval_resolved":
|
|
734
|
+
await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
|
|
735
|
+
return;
|
|
736
|
+
case "task.complete":
|
|
737
|
+
await recordTerminal(stores, taskId, envelope);
|
|
738
|
+
return;
|
|
739
|
+
case "task.fail":
|
|
740
|
+
await recordTerminal(stores, taskId, envelope);
|
|
741
|
+
return;
|
|
742
|
+
case "task.cancelled":
|
|
743
|
+
await recordTerminal(stores, taskId, envelope);
|
|
744
|
+
return;
|
|
745
|
+
default:
|
|
746
|
+
return;
|
|
946
747
|
}
|
|
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
748
|
}
|
|
971
|
-
function
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
749
|
+
function approvalTimelineAppendInput(taskId, envelope) {
|
|
750
|
+
switch (envelope.type) {
|
|
751
|
+
case "task.await_approval":
|
|
752
|
+
return {
|
|
753
|
+
taskId,
|
|
754
|
+
sourceEnvelopeId: envelope.id,
|
|
755
|
+
event: {
|
|
756
|
+
type: "approval_requested",
|
|
757
|
+
summary: envelope.payload.summary,
|
|
758
|
+
...envelope.payload.approvalId === void 0 ? {} : { approvalId: envelope.payload.approvalId }
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
case "task.approval_resolved":
|
|
762
|
+
return {
|
|
763
|
+
taskId,
|
|
764
|
+
sourceEnvelopeId: envelope.id,
|
|
765
|
+
event: {
|
|
766
|
+
type: "approval_resolved",
|
|
767
|
+
approvalId: envelope.payload.approvalId,
|
|
768
|
+
decision: envelope.payload.decision,
|
|
769
|
+
resolvedBy: envelope.payload.resolvedBy,
|
|
770
|
+
at: envelope.payload.at
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
default:
|
|
774
|
+
return void 0;
|
|
979
775
|
}
|
|
980
|
-
|
|
981
|
-
|
|
776
|
+
}
|
|
777
|
+
async function recordTerminal(stores, taskId, envelope) {
|
|
778
|
+
const receipt = await stores.receipts.record({
|
|
779
|
+
key: terminalReceiptKey(taskId),
|
|
780
|
+
body: encodeEnvelope(envelope)
|
|
781
|
+
});
|
|
782
|
+
const winner = terminalReceiptBinding(taskId, receipt.receipt.body);
|
|
783
|
+
if (winner === void 0) {
|
|
784
|
+
throw new Error(`terminal receipt for ${taskId} is not a canonical terminal binding`);
|
|
982
785
|
}
|
|
983
|
-
|
|
984
|
-
|
|
786
|
+
const attempt = await stores.tasks.get(taskId);
|
|
787
|
+
if (attempt?.cancellation !== void 0) {
|
|
788
|
+
await stores.tasks.recordStatus({
|
|
789
|
+
taskId,
|
|
790
|
+
status: "cancelled",
|
|
791
|
+
...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
|
|
792
|
+
});
|
|
793
|
+
return;
|
|
985
794
|
}
|
|
986
|
-
|
|
795
|
+
const recordedAttempt = await stores.tasks.recordStatus({
|
|
796
|
+
taskId,
|
|
797
|
+
status: winner.status,
|
|
798
|
+
...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
|
|
799
|
+
...winner.cause === void 0 ? {} : { terminalCause: winner.cause }
|
|
800
|
+
});
|
|
801
|
+
if (recordedAttempt?.cancellation !== void 0 || recordedAttempt?.status !== winner.status) {
|
|
802
|
+
if (recordedAttempt?.cancellation !== void 0) {
|
|
803
|
+
await stores.tasks.recordStatus({
|
|
804
|
+
taskId,
|
|
805
|
+
status: "cancelled",
|
|
806
|
+
...recordedAttempt.agentRef === void 0 ? {} : { agentRef: recordedAttempt.agentRef }
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
await projectTerminalToReview(stores.board, taskId);
|
|
987
812
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
813
|
+
function terminalStatus(envelope) {
|
|
814
|
+
switch (envelope.type) {
|
|
815
|
+
case "task.complete":
|
|
816
|
+
return "complete";
|
|
817
|
+
case "task.fail":
|
|
818
|
+
case "task.decline":
|
|
819
|
+
return "failed";
|
|
820
|
+
case "task.cancelled":
|
|
821
|
+
return "cancelled";
|
|
822
|
+
default:
|
|
823
|
+
return void 0;
|
|
824
|
+
}
|
|
992
825
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
};
|
|
826
|
+
function terminalReceiptBinding(taskId, body) {
|
|
827
|
+
let envelope;
|
|
828
|
+
try {
|
|
829
|
+
envelope = decodeEnvelope(body);
|
|
830
|
+
} catch {
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
const status = terminalStatus(envelope);
|
|
834
|
+
if (status === void 0 || envelope.task_id !== taskId || encodeEnvelope(envelope) !== body) return void 0;
|
|
835
|
+
const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" || envelope.type === "task.decline" ? envelope.payload.reason : void 0;
|
|
836
|
+
return { status, ...cause === void 0 ? {} : { cause } };
|
|
1018
837
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
};
|
|
838
|
+
|
|
839
|
+
// src/task-agent-message.ts
|
|
840
|
+
var FrozenMessageBinding = z.object({
|
|
841
|
+
agentRef: AgentRefSchema,
|
|
842
|
+
sessionRef: z.string().min(1).optional(),
|
|
843
|
+
requirement: AgentMessageEgressRequirementSchema,
|
|
844
|
+
context: AgentMessageServerContextSchema
|
|
845
|
+
}).strict();
|
|
846
|
+
function sameRef(a, b) {
|
|
847
|
+
return a.agentId === b.agentId && a.profileRevision === b.profileRevision;
|
|
1030
848
|
}
|
|
1031
|
-
function
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
849
|
+
async function readTaskAgentMessage(stores, deviceId, taskId, expectedRef) {
|
|
850
|
+
const agentRef = AgentRefSchema.parse(expectedRef);
|
|
851
|
+
const attempt = await stores.tasks.get(taskId);
|
|
852
|
+
if (!attempt || attempt.tenantId !== stores.tenant || attempt.taskId !== taskId || attempt.deviceId !== deviceId || !attempt.agentRef || !sameRef(attempt.agentRef, agentRef)) return void 0;
|
|
853
|
+
const admission = await stores.tasks.readTaskAgentMessage({ taskId, deviceId });
|
|
854
|
+
if (!admission) return void 0;
|
|
855
|
+
const invalid = () => new Error("Invalid persisted task Agent message.");
|
|
856
|
+
const binding = await stores.receipts.get(`agent-message-offer:${deviceId}:${taskId}`);
|
|
857
|
+
if (!binding) throw invalid();
|
|
858
|
+
let body, frozenBody;
|
|
859
|
+
try {
|
|
860
|
+
body = JSON.parse(admission.payloadBody);
|
|
861
|
+
frozenBody = JSON.parse(binding.body);
|
|
862
|
+
} catch {
|
|
863
|
+
throw invalid();
|
|
864
|
+
}
|
|
865
|
+
const parsed = AgentMessagePublishPayloadSchema.safeParse(body);
|
|
866
|
+
const frozen = FrozenMessageBinding.safeParse(frozenBody);
|
|
867
|
+
if (!parsed.success || !frozen.success) throw invalid();
|
|
868
|
+
const payload = parsed.data;
|
|
869
|
+
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();
|
|
870
|
+
const disposition = admission.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
|
|
871
|
+
return { payload, context: frozen.data.context, ...disposition === void 0 ? {} : { disposition } };
|
|
872
|
+
}
|
|
873
|
+
var RecurringExecutionInputSchema = z.object({
|
|
874
|
+
taskId: z.string().min(1),
|
|
875
|
+
deviceId: z.string().min(1),
|
|
876
|
+
payload: TaskOfferForAgentWithEgressFreshPayloadSchema.required({ runtime: true, messageEgress: true, terminalProjection: true }),
|
|
877
|
+
agentMessageContext: AgentMessageServerContextSchema
|
|
878
|
+
}).strict();
|
|
879
|
+
|
|
880
|
+
// src/approval-control.ts
|
|
881
|
+
var StaleApprovalError = class extends Error {
|
|
882
|
+
constructor(taskId, requestedApprovalId, currentApprovalId) {
|
|
883
|
+
super(
|
|
884
|
+
`cannot resolve approval ${requestedApprovalId} for task ${taskId}: the currently pending approval is ${currentApprovalId ?? "(none recorded)"}`
|
|
885
|
+
);
|
|
886
|
+
this.taskId = taskId;
|
|
887
|
+
this.requestedApprovalId = requestedApprovalId;
|
|
888
|
+
this.currentApprovalId = currentApprovalId;
|
|
889
|
+
this.name = "StaleApprovalError";
|
|
890
|
+
}
|
|
891
|
+
taskId;
|
|
892
|
+
requestedApprovalId;
|
|
893
|
+
currentApprovalId;
|
|
894
|
+
};
|
|
895
|
+
function pendingApproval(tail) {
|
|
896
|
+
if (tail === void 0) return void 0;
|
|
897
|
+
let pending;
|
|
898
|
+
for (const entry of tail.entries) {
|
|
899
|
+
const event = entry.event;
|
|
900
|
+
if (event.type === "approval_requested") {
|
|
901
|
+
pending = {
|
|
902
|
+
sourceEnvelopeId: entry.sourceEnvelopeId,
|
|
903
|
+
revision: entry.revision,
|
|
904
|
+
...event.approvalId === void 0 ? {} : { approvalId: event.approvalId }
|
|
905
|
+
};
|
|
906
|
+
continue;
|
|
1042
907
|
}
|
|
1043
|
-
if (
|
|
1044
|
-
|
|
908
|
+
if (pending === void 0) continue;
|
|
909
|
+
if (pending.approvalId === void 0 || pending.approvalId === event.approvalId) {
|
|
910
|
+
pending = void 0;
|
|
1045
911
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
return c.json(response, 200);
|
|
1049
|
-
};
|
|
912
|
+
}
|
|
913
|
+
return pending;
|
|
1050
914
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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));
|
|
915
|
+
|
|
916
|
+
// src/steer-control.ts
|
|
917
|
+
function steerRejectionMessage(taskId, code, status, runtime) {
|
|
918
|
+
switch (code) {
|
|
919
|
+
case "task_terminal":
|
|
920
|
+
return `cannot steer task ${taskId}: task is already terminal (status ${status})`;
|
|
921
|
+
case "task_not_running":
|
|
922
|
+
return `cannot steer task ${taskId}: not running (status ${status})`;
|
|
923
|
+
case "steer_unsupported_runtime":
|
|
924
|
+
return `cannot steer task ${taskId}: claimed runtime ${runtime ?? "(unknown)"} does not support steering`;
|
|
925
|
+
}
|
|
1075
926
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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);
|
|
1148
|
-
}
|
|
1149
|
-
if (attempt < attempts - 1) await sleep(deps.longPollIntervalMs);
|
|
1150
|
-
}
|
|
1151
|
-
const response = {
|
|
1152
|
-
events: [],
|
|
1153
|
-
cursor: afterSeq,
|
|
1154
|
-
capabilities: CLOUD_PROTOCOL_CAPABILITIES
|
|
1155
|
-
};
|
|
1156
|
-
return c.json(response, 200);
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
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
|
-
);
|
|
1236
|
-
}
|
|
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
|
-
);
|
|
927
|
+
var SteerRejectedError = class extends Error {
|
|
928
|
+
constructor(taskId, code, status, runtime) {
|
|
929
|
+
super(steerRejectionMessage(taskId, code, status, runtime));
|
|
930
|
+
this.taskId = taskId;
|
|
931
|
+
this.code = code;
|
|
932
|
+
this.status = status;
|
|
933
|
+
this.runtime = runtime;
|
|
934
|
+
this.name = "SteerRejectedError";
|
|
1242
935
|
}
|
|
1243
|
-
|
|
936
|
+
taskId;
|
|
937
|
+
code;
|
|
938
|
+
status;
|
|
939
|
+
runtime;
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// src/crypto/web-crypto.ts
|
|
943
|
+
var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
944
|
+
var HEX_DIGITS = "0123456789abcdef";
|
|
945
|
+
function base64UrlEncode(bytes) {
|
|
946
|
+
let binary = "";
|
|
947
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
948
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
1244
949
|
}
|
|
1245
|
-
function
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
"Host approval resolution requires an exact pending source/revision and a host resolution event."
|
|
1255
|
-
);
|
|
950
|
+
function base64UrlDecode(value) {
|
|
951
|
+
const padded = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
952
|
+
try {
|
|
953
|
+
const binary = atob(padded);
|
|
954
|
+
const bytes = new Uint8Array(binary.length);
|
|
955
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
956
|
+
return bytes;
|
|
957
|
+
} catch {
|
|
958
|
+
return void 0;
|
|
1256
959
|
}
|
|
1257
|
-
return validated;
|
|
1258
|
-
}
|
|
1259
|
-
function approvalTimelineKey(tenant, taskId) {
|
|
1260
|
-
return tenantKey(tenant, taskId);
|
|
1261
960
|
}
|
|
1262
|
-
function
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
961
|
+
function toHex(bytes) {
|
|
962
|
+
let out = "";
|
|
963
|
+
for (const byte of bytes) {
|
|
964
|
+
out += HEX_DIGITS[byte >> 4 & 15];
|
|
965
|
+
out += HEX_DIGITS[byte & 15];
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
1267
968
|
}
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
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;
|
|
969
|
+
function subtle() {
|
|
970
|
+
const webCrypto = globalThis.crypto;
|
|
971
|
+
if (webCrypto?.subtle === void 0) {
|
|
972
|
+
throw new Error("@byok-sdk/cloud requires a WebCrypto implementation on globalThis.crypto");
|
|
1280
973
|
}
|
|
974
|
+
return webCrypto.subtle;
|
|
1281
975
|
}
|
|
1282
|
-
function
|
|
1283
|
-
|
|
1284
|
-
|
|
976
|
+
function createWebCrypto() {
|
|
977
|
+
return {
|
|
978
|
+
randomUuid() {
|
|
979
|
+
return globalThis.crypto.randomUUID();
|
|
980
|
+
},
|
|
981
|
+
randomToken(byteLength) {
|
|
982
|
+
return base64UrlEncode(globalThis.crypto.getRandomValues(new Uint8Array(byteLength)));
|
|
983
|
+
},
|
|
984
|
+
randomPairingCode(length) {
|
|
985
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(length));
|
|
986
|
+
let out = "";
|
|
987
|
+
for (const byte of bytes) {
|
|
988
|
+
out += PAIRING_CODE_ALPHABET[byte % PAIRING_CODE_ALPHABET.length];
|
|
989
|
+
}
|
|
990
|
+
return out;
|
|
991
|
+
},
|
|
992
|
+
async verifyEd25519(publicKeyBase64Url, message, signature) {
|
|
993
|
+
const signatureBytes = base64UrlDecode(signature);
|
|
994
|
+
if (signatureBytes === void 0) return false;
|
|
995
|
+
try {
|
|
996
|
+
const key = await subtle().importKey(
|
|
997
|
+
"jwk",
|
|
998
|
+
{ kty: "OKP", crv: "Ed25519", x: publicKeyBase64Url },
|
|
999
|
+
{ name: "Ed25519" },
|
|
1000
|
+
false,
|
|
1001
|
+
["verify"]
|
|
1002
|
+
);
|
|
1003
|
+
const messageBytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1004
|
+
return await subtle().verify({ name: "Ed25519" }, key, signatureBytes, messageBytes);
|
|
1005
|
+
} catch {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
async hmacSha256(secret, message) {
|
|
1010
|
+
const key = await subtle().importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1011
|
+
const signature = await subtle().sign("HMAC", key, new TextEncoder().encode(message));
|
|
1012
|
+
return base64UrlEncode(new Uint8Array(signature));
|
|
1013
|
+
},
|
|
1014
|
+
async sha256(data) {
|
|
1015
|
+
const digest = await subtle().digest("SHA-256", data);
|
|
1016
|
+
return `sha256:${toHex(new Uint8Array(digest))}`;
|
|
1017
|
+
},
|
|
1018
|
+
timingSafeEqual(left, right) {
|
|
1019
|
+
const leftBytes = base64UrlDecode(left);
|
|
1020
|
+
const rightBytes = base64UrlDecode(right);
|
|
1021
|
+
if (leftBytes === void 0 || rightBytes === void 0) return false;
|
|
1022
|
+
if (leftBytes.length !== rightBytes.length) return false;
|
|
1023
|
+
let diff = 0;
|
|
1024
|
+
for (let index = 0; index < leftBytes.length; index += 1) {
|
|
1025
|
+
diff |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
|
|
1026
|
+
}
|
|
1027
|
+
return diff === 0;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1285
1030
|
}
|
|
1286
1031
|
|
|
1287
|
-
// src/
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
frozen = JSON.parse(binding.body);
|
|
1296
|
-
} catch {
|
|
1297
|
-
return { outcome: "rejected" };
|
|
1032
|
+
// src/auth/tokens.ts
|
|
1033
|
+
var ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
1034
|
+
function encodeSegment(value) {
|
|
1035
|
+
return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
|
|
1036
|
+
}
|
|
1037
|
+
function createHmacTokenSigner(secret, clock) {
|
|
1038
|
+
async function signingKey(usage) {
|
|
1039
|
+
return globalThis.crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, [usage]);
|
|
1298
1040
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1041
|
+
async function signature(signingInput) {
|
|
1042
|
+
const key = await signingKey("sign");
|
|
1043
|
+
const bytes = await globalThis.crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signingInput));
|
|
1044
|
+
return base64UrlEncode(new Uint8Array(bytes));
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
async sign(claims, expiresInSeconds) {
|
|
1048
|
+
const issuedAt = Math.floor(clock.now().getTime() / 1e3);
|
|
1049
|
+
const header = encodeSegment({ alg: "HS256", typ: "JWT" });
|
|
1050
|
+
const payload = encodeSegment({
|
|
1051
|
+
deviceId: claims.deviceId,
|
|
1052
|
+
tenantId: claims.tenantId,
|
|
1053
|
+
productId: claims.productId,
|
|
1054
|
+
iat: issuedAt,
|
|
1055
|
+
exp: issuedAt + expiresInSeconds
|
|
1056
|
+
});
|
|
1057
|
+
const signingInput = `${header}.${payload}`;
|
|
1058
|
+
return `${signingInput}.${await signature(signingInput)}`;
|
|
1059
|
+
},
|
|
1060
|
+
async verify(token) {
|
|
1061
|
+
const parts = token.split(".");
|
|
1062
|
+
if (parts.length !== 3) return void 0;
|
|
1063
|
+
const [header, payload, provided] = parts;
|
|
1064
|
+
const key = await signingKey("verify");
|
|
1065
|
+
const providedBytes = base64UrlDecode(provided);
|
|
1066
|
+
if (providedBytes === void 0) return void 0;
|
|
1067
|
+
const ok = await globalThis.crypto.subtle.verify(
|
|
1068
|
+
"HMAC",
|
|
1069
|
+
key,
|
|
1070
|
+
providedBytes,
|
|
1071
|
+
new TextEncoder().encode(`${header}.${payload}`)
|
|
1072
|
+
);
|
|
1073
|
+
if (!ok) return void 0;
|
|
1074
|
+
const headerBytes = base64UrlDecode(header);
|
|
1075
|
+
const payloadBytes = base64UrlDecode(payload);
|
|
1076
|
+
if (headerBytes === void 0 || payloadBytes === void 0) return void 0;
|
|
1077
|
+
let decodedHeader;
|
|
1078
|
+
let decodedPayload;
|
|
1079
|
+
try {
|
|
1080
|
+
decodedHeader = JSON.parse(new TextDecoder().decode(headerBytes));
|
|
1081
|
+
decodedPayload = JSON.parse(new TextDecoder().decode(payloadBytes));
|
|
1082
|
+
} catch {
|
|
1083
|
+
return void 0;
|
|
1084
|
+
}
|
|
1085
|
+
const alg = decodedHeader?.alg;
|
|
1086
|
+
if (alg !== "HS256") return void 0;
|
|
1087
|
+
const { deviceId, tenantId: tenantId4, productId, exp } = decodedPayload;
|
|
1088
|
+
if (typeof deviceId !== "string" || typeof tenantId4 !== "string" || typeof productId !== "string") {
|
|
1089
|
+
return void 0;
|
|
1090
|
+
}
|
|
1091
|
+
if (typeof exp !== "number" || clock.now().getTime() / 1e3 >= exp) return void 0;
|
|
1092
|
+
return { deviceId, tenantId: tenantId4, productId };
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function verifyNonceSignature(crypto2, devicePublicKey, nonce, signature) {
|
|
1097
|
+
return crypto2.verifyEd25519(devicePublicKey, NONCE_SIGNING_DOMAIN + nonce, signature);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/auth/plane.ts
|
|
1101
|
+
var PAIRING_CODE_TTL_MS = 10 * 60 * 1e3;
|
|
1102
|
+
var PAIRING_CODE_LENGTH = 8;
|
|
1103
|
+
var DEVICE_IDENTITY_PROOF_KEY_ID = "identity";
|
|
1104
|
+
var DEVICE_IDENTITY_PROOF_KEY_EPOCH = 0;
|
|
1105
|
+
function createAuthPlane(deps) {
|
|
1106
|
+
const { stores, crypto: crypto2, clock, tokenSigner } = deps;
|
|
1107
|
+
const ttlSeconds = deps.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS;
|
|
1108
|
+
return {
|
|
1109
|
+
async createPairingCode(tenant, input) {
|
|
1110
|
+
const expiresAt = new Date(clock.now().getTime() + (input.ttlMs ?? PAIRING_CODE_TTL_MS)).toISOString();
|
|
1111
|
+
return stores.pairingCodes.issue(tenant, {
|
|
1112
|
+
code: crypto2.randomPairingCode(PAIRING_CODE_LENGTH),
|
|
1113
|
+
productId: input.productId,
|
|
1114
|
+
expiresAt
|
|
1115
|
+
});
|
|
1116
|
+
},
|
|
1117
|
+
async redeemAndRegister(input) {
|
|
1118
|
+
return stores.pairing.redeemAndRegister({
|
|
1119
|
+
pairingCode: input.pairingCode,
|
|
1120
|
+
deviceId: `dev_${crypto2.randomUuid()}`,
|
|
1121
|
+
deviceName: input.deviceName,
|
|
1122
|
+
devicePublicKey: input.devicePublicKey,
|
|
1123
|
+
proofKeyId: DEVICE_IDENTITY_PROOF_KEY_ID,
|
|
1124
|
+
proofKeyEpoch: DEVICE_IDENTITY_PROOF_KEY_EPOCH,
|
|
1125
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1126
|
+
});
|
|
1127
|
+
},
|
|
1128
|
+
async resolveDevice(deviceId) {
|
|
1129
|
+
const device = await stores.devices.resolveByDeviceId(deviceId);
|
|
1130
|
+
if (device === void 0 || device.revoked) return void 0;
|
|
1131
|
+
return device;
|
|
1132
|
+
},
|
|
1133
|
+
issueNonce(device) {
|
|
1134
|
+
return stores.nonces.issue(device.tenantId, device.deviceId);
|
|
1135
|
+
},
|
|
1136
|
+
consumeNonceIfValid(device, nonce) {
|
|
1137
|
+
return stores.nonces.consumeIfValid(device.tenantId, device.deviceId, nonce);
|
|
1138
|
+
},
|
|
1139
|
+
verifySignature(device, nonce, signature) {
|
|
1140
|
+
return verifyNonceSignature(crypto2, device.devicePublicKey, nonce, signature);
|
|
1141
|
+
},
|
|
1142
|
+
async mintAccessToken(device) {
|
|
1143
|
+
const accessToken = await tokenSigner.sign(
|
|
1144
|
+
{ deviceId: device.deviceId, tenantId: device.tenantId, productId: device.productId },
|
|
1145
|
+
ttlSeconds
|
|
1146
|
+
);
|
|
1147
|
+
return {
|
|
1148
|
+
accessToken,
|
|
1149
|
+
expiresAt: new Date(clock.now().getTime() + ttlSeconds * 1e3).toISOString()
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
var CLOUD_CAPABILITIES = {
|
|
1155
|
+
/** `GET /byok/events` long-poll receive (§8). */
|
|
1156
|
+
eventsLongPoll: "events.longpoll",
|
|
1157
|
+
/** `POST /byok/messages` batched send (§8.2). */
|
|
1158
|
+
messagesBatch: "messages.batch",
|
|
1159
|
+
/** The three bearer-authed blob routes: reserve/grant, explicit finalize, committed-only download (§7). */
|
|
1160
|
+
blobsPresigned: "blobs.presigned",
|
|
1161
|
+
/**
|
|
1162
|
+
* The two presigned `/byok/blobs/:id/content` routes — cloud carrying the
|
|
1163
|
+
* bytes itself.
|
|
1164
|
+
*
|
|
1165
|
+
* Split out of `blobs.presigned` because it was one capability describing two
|
|
1166
|
+
* separable facts. A composition whose bytes live in object storage mints
|
|
1167
|
+
* grants (`blobs.presigned`) but has no byte-proxy path at all, and saying so
|
|
1168
|
+
* by declaration is ADR-010's whole posture: a client reads what a deployment
|
|
1169
|
+
* serves, it never probes a `/content` route and interprets the status code.
|
|
1170
|
+
*
|
|
1171
|
+
* Spelled all-lowercase because core's declaration schema pins capability
|
|
1172
|
+
* names to `/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/` — the same reason the sibling
|
|
1173
|
+
* above reads `events.longpoll` and not `events.longPoll`.
|
|
1174
|
+
*/
|
|
1175
|
+
blobsContentProxy: "blobs.contentproxy",
|
|
1176
|
+
/** Board list/claim/unclaim/status routes. Polling is first-class under this declaration. */
|
|
1177
|
+
boardCoordination: "board.coordination",
|
|
1178
|
+
/** Additional SSE transport for the same board read model. */
|
|
1179
|
+
boardSse: "board.sse",
|
|
1180
|
+
/** Device-scoped five-level presence publication. */
|
|
1181
|
+
presenceHints: "presence.hints",
|
|
1182
|
+
/** Bounded task activity batch publication. */
|
|
1183
|
+
activityTail: "activity.tail",
|
|
1184
|
+
/** Request-bound device proof record manifest/read/write surface (S6). */
|
|
1185
|
+
truthRecords: "truth.records",
|
|
1186
|
+
/**
|
|
1187
|
+
* Tenant-scoped skill pack distribution: the manifest list plus the per-file
|
|
1188
|
+
* content route a paired device installs from.
|
|
1189
|
+
*
|
|
1190
|
+
* Hosted HTTP on purpose. A pack is DECLARATIVE CONTENT a device pulls after
|
|
1191
|
+
* reading this declaration — not a message — so `@byok-sdk/protocol` gains
|
|
1192
|
+
* nothing from it and the frozen v1 envelope stays untouched. Composition-
|
|
1193
|
+
* bound like `truth.records`: a deployment that declares it without supplying
|
|
1194
|
+
* a `SkillPackStore` is refused at construction rather than publishing two
|
|
1195
|
+
* routes it would then 404.
|
|
1196
|
+
*/
|
|
1197
|
+
skillPacks: "skills.pack",
|
|
1198
|
+
/** Optional, one-way redacted Agent-memory snapshot mutation route. */
|
|
1199
|
+
agentMemoryProjection: "agent.memory.projection"
|
|
1200
|
+
};
|
|
1201
|
+
var CapabilitiesResponseSchema = CapabilityDeclarationSchema;
|
|
1202
|
+
function fullCapabilityDeclaration(version = 1, options = {}) {
|
|
1203
|
+
return {
|
|
1204
|
+
schema: "byok-capabilities-v1",
|
|
1205
|
+
version,
|
|
1206
|
+
capabilities: Object.values(CLOUD_CAPABILITIES).filter((capability) => {
|
|
1207
|
+
if (capability === CLOUD_CAPABILITIES.truthRecords) return options.includeTruthRecords === true;
|
|
1208
|
+
if (capability === CLOUD_CAPABILITIES.skillPacks) return options.includeSkillPacks === true;
|
|
1209
|
+
if (capability === CLOUD_CAPABILITIES.agentMemoryProjection) {
|
|
1210
|
+
return options.includeAgentMemoryProjection === true;
|
|
1211
|
+
}
|
|
1212
|
+
return true;
|
|
1213
|
+
})
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function declares(declaration, capability) {
|
|
1217
|
+
return hasCapability(declaration, capability);
|
|
1218
|
+
}
|
|
1219
|
+
function extractBearerToken(header) {
|
|
1220
|
+
if (header === void 0) return void 0;
|
|
1221
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
1222
|
+
return match?.[1];
|
|
1223
|
+
}
|
|
1224
|
+
async function authenticateBearer(header, deps) {
|
|
1225
|
+
const token = extractBearerToken(header);
|
|
1226
|
+
if (token === void 0) return void 0;
|
|
1227
|
+
const claims = await deps.tokenSigner.verify(token);
|
|
1228
|
+
if (claims === void 0) return void 0;
|
|
1229
|
+
if (!isTenantId(claims.tenantId)) return void 0;
|
|
1230
|
+
const device = await deps.devices.get(tenantId(claims.tenantId), claims.deviceId);
|
|
1231
|
+
if (device === void 0 || device.revoked) return void 0;
|
|
1232
|
+
if (device.productId !== claims.productId) return void 0;
|
|
1233
|
+
if (deps.instanceProductId !== void 0 && claims.productId !== deps.instanceProductId) {
|
|
1234
|
+
return void 0;
|
|
1235
|
+
}
|
|
1236
|
+
return {
|
|
1237
|
+
kind: "device",
|
|
1238
|
+
tenantId: device.tenantId,
|
|
1239
|
+
productId: device.productId,
|
|
1240
|
+
deviceId: device.deviceId
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function tenantStoresFor(principal, root) {
|
|
1244
|
+
const tenant = principalTenant(principal);
|
|
1245
|
+
const { core, cloud } = root;
|
|
1246
|
+
return {
|
|
1247
|
+
tenant,
|
|
1248
|
+
principal,
|
|
1249
|
+
mailbox: {
|
|
1250
|
+
append: (input) => core.mailbox.append(tenant, input),
|
|
1251
|
+
readAfter: (query) => core.mailbox.readAfter(tenant, query),
|
|
1252
|
+
recordDelivery: (input) => core.mailbox.recordDelivery(tenant, input),
|
|
1253
|
+
advanceCursor: (input) => core.mailbox.advanceCursor(tenant, input),
|
|
1254
|
+
readCursor: (deviceId) => core.mailbox.readCursor(tenant, deviceId)
|
|
1255
|
+
},
|
|
1256
|
+
board: {
|
|
1257
|
+
create: (input) => core.board.create(tenant, input),
|
|
1258
|
+
get: (itemId) => core.board.get(tenant, itemId),
|
|
1259
|
+
list: (query) => core.board.list(tenant, query),
|
|
1260
|
+
claim: (input) => core.board.claim(tenant, input),
|
|
1261
|
+
unclaim: (input) => core.board.unclaim(tenant, input),
|
|
1262
|
+
updateStatus: (input) => core.board.updateStatus(tenant, input)
|
|
1263
|
+
},
|
|
1264
|
+
presence: {
|
|
1265
|
+
publish: (input) => core.presence.publish(tenant, input),
|
|
1266
|
+
read: (deviceId) => core.presence.read(tenant, deviceId),
|
|
1267
|
+
list: () => core.presence.list(tenant)
|
|
1268
|
+
},
|
|
1269
|
+
activity: {
|
|
1270
|
+
append: (input) => cloud.activity.append(tenant, input),
|
|
1271
|
+
read: (taskId) => cloud.activity.read(tenant, taskId)
|
|
1272
|
+
},
|
|
1273
|
+
approvals: {
|
|
1274
|
+
append: (input) => cloud.approvals.append(tenant, input),
|
|
1275
|
+
read: (taskId) => cloud.approvals.read(tenant, taskId)
|
|
1276
|
+
},
|
|
1277
|
+
devices: {
|
|
1278
|
+
get: (deviceId) => cloud.devices.get(tenant, deviceId),
|
|
1279
|
+
list: () => cloud.devices.list(tenant),
|
|
1280
|
+
revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId),
|
|
1281
|
+
recordCapabilities: (input) => principal.kind === "device" ? cloud.devices.recordCapabilities(tenant, {
|
|
1282
|
+
deviceId: principal.deviceId,
|
|
1283
|
+
capabilities: input.capabilities,
|
|
1284
|
+
...input.harnesses === void 0 ? {} : { harnesses: input.harnesses }
|
|
1285
|
+
}) : Promise.resolve(void 0),
|
|
1286
|
+
readiness: () => cloud.devices.readiness(tenant, core.presence)
|
|
1287
|
+
},
|
|
1288
|
+
tasks: {
|
|
1289
|
+
open: (input) => cloud.tasks.open(tenant, input),
|
|
1290
|
+
reserveAgentOffer: (input) => cloud.tasks.reserveAgentOffer(tenant, input),
|
|
1291
|
+
reserveAgentMessage: (input) => cloud.tasks.reserveAgentMessage(tenant, input),
|
|
1292
|
+
readTaskAgentMessage: (input) => cloud.tasks.readTaskAgentMessage(tenant, input),
|
|
1293
|
+
readAgentMessage: (input) => cloud.tasks.readAgentMessage(tenant, input),
|
|
1294
|
+
finalizeAgentMessage: (input) => cloud.tasks.finalizeAgentMessage(tenant, input),
|
|
1295
|
+
get: (taskId) => cloud.tasks.get(tenant, taskId),
|
|
1296
|
+
getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
|
|
1297
|
+
list: (query) => cloud.tasks.list(tenant, query),
|
|
1298
|
+
claim: (input) => cloud.tasks.claim(tenant, input),
|
|
1299
|
+
recordStatus: (input) => cloud.tasks.recordStatus(tenant, input)
|
|
1300
|
+
},
|
|
1301
|
+
cancellations: {
|
|
1302
|
+
request: (input) => cloud.cancellations.request(tenant, input)
|
|
1303
|
+
},
|
|
1304
|
+
dedup: {
|
|
1305
|
+
checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId),
|
|
1306
|
+
checkAndRecordAgent: (deviceId, agentRef, envelopeId) => cloud.dedup.checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId)
|
|
1307
|
+
},
|
|
1308
|
+
receipts: {
|
|
1309
|
+
record: (input) => cloud.receipts.record(tenant, input),
|
|
1310
|
+
get: (key) => cloud.receipts.get(tenant, key)
|
|
1311
|
+
},
|
|
1312
|
+
egress: {
|
|
1313
|
+
record: (input) => cloud.egress.record(tenant, input),
|
|
1314
|
+
get: (deviceId, agentRef, eventId) => cloud.egress.get(tenant, deviceId, agentRef, eventId)
|
|
1315
|
+
},
|
|
1316
|
+
blobs: {
|
|
1317
|
+
createUpload: (reservation) => cloud.blobs.createUpload(tenant, reservation),
|
|
1318
|
+
observeUpload: (blobId, reservation) => cloud.blobs.observeUpload(tenant, blobId, reservation),
|
|
1319
|
+
getDownloadUrl: (blobId) => cloud.blobs.getDownloadUrl(tenant, blobId)
|
|
1320
|
+
},
|
|
1321
|
+
quota: {
|
|
1322
|
+
readReservation: (reservationId) => core.quota.readReservation(tenant, reservationId),
|
|
1323
|
+
reserve: (input) => core.quota.reserve(tenant, input),
|
|
1324
|
+
finalizeReservation: (input) => core.quota.finalizeReservation(tenant, input),
|
|
1325
|
+
abortReservation: (reservationId) => core.quota.abortReservation(tenant, reservationId)
|
|
1326
|
+
},
|
|
1327
|
+
rateLimiter: {
|
|
1328
|
+
consume: (deviceId) => cloud.rateLimiter.consume(tenant, deviceId)
|
|
1314
1329
|
}
|
|
1315
|
-
decision = { outcome: "held", reasonCode: "consumer_unavailable" };
|
|
1316
|
-
} else {
|
|
1317
|
-
decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
|
|
1318
|
-
}
|
|
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
1330
|
};
|
|
1342
1331
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
deviceId,
|
|
1347
|
-
messageId: payload.messageId,
|
|
1348
|
-
payloadBody: JSON.stringify(payload)
|
|
1349
|
-
});
|
|
1350
|
-
return admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
|
|
1351
|
-
}
|
|
1352
|
-
function parseAgentMessageDisposition(payload, terminalBody) {
|
|
1332
|
+
|
|
1333
|
+
// src/handlers/shared.ts
|
|
1334
|
+
async function readJsonBody(c) {
|
|
1353
1335
|
try {
|
|
1354
|
-
|
|
1355
|
-
return decoded.disposition !== void 0 && JSON.stringify(decoded.payload) === JSON.stringify(payload) ? decoded.disposition : void 0;
|
|
1336
|
+
return await c.req.json();
|
|
1356
1337
|
} catch {
|
|
1357
1338
|
return void 0;
|
|
1358
1339
|
}
|
|
1359
1340
|
}
|
|
1360
|
-
function
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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;
|
|
1341
|
+
async function readBoundedJsonBody(c, maximum) {
|
|
1342
|
+
const declaredLength = c.req.header("content-length");
|
|
1343
|
+
if (declaredLength !== void 0) {
|
|
1344
|
+
const parsedLength = parseContentLength(declaredLength);
|
|
1345
|
+
if (parsedLength !== void 0 && parsedLength > BigInt(maximum)) {
|
|
1346
|
+
cancelBody(c.req.raw.body);
|
|
1347
|
+
return { body: void 0, tooLarge: true };
|
|
1348
|
+
}
|
|
1377
1349
|
}
|
|
1350
|
+
const stream = c.req.raw.body;
|
|
1351
|
+
if (stream === null) return { body: void 0, tooLarge: false };
|
|
1352
|
+
const reader = stream.getReader();
|
|
1353
|
+
const chunks = [];
|
|
1354
|
+
let total = 0;
|
|
1355
|
+
try {
|
|
1356
|
+
while (true) {
|
|
1357
|
+
const { done, value } = await reader.read();
|
|
1358
|
+
if (done) break;
|
|
1359
|
+
if (value.byteLength > maximum - total) {
|
|
1360
|
+
cancelBody(reader);
|
|
1361
|
+
return { body: void 0, tooLarge: true };
|
|
1362
|
+
}
|
|
1363
|
+
if (value.byteLength === 0) continue;
|
|
1364
|
+
chunks.push(value);
|
|
1365
|
+
total += value.byteLength;
|
|
1366
|
+
}
|
|
1367
|
+
} catch {
|
|
1368
|
+
return { body: void 0, tooLarge: false };
|
|
1369
|
+
} finally {
|
|
1370
|
+
reader.releaseLock();
|
|
1371
|
+
}
|
|
1372
|
+
const bytes = new Uint8Array(total);
|
|
1373
|
+
let offset = 0;
|
|
1374
|
+
for (const chunk of chunks) {
|
|
1375
|
+
bytes.set(chunk, offset);
|
|
1376
|
+
offset += chunk.byteLength;
|
|
1377
|
+
}
|
|
1378
|
+
return { body: parseJsonBytes(bytes), tooLarge: false };
|
|
1378
1379
|
}
|
|
1379
|
-
function
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
|
|
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;
|
|
1380
|
+
function cancelBody(body) {
|
|
1381
|
+
if (body === null) return;
|
|
1382
|
+
try {
|
|
1383
|
+
void Promise.resolve(body.cancel()).catch(() => {
|
|
1384
|
+
});
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1390
1387
|
}
|
|
1391
|
-
|
|
1392
|
-
const
|
|
1393
|
-
if (
|
|
1388
|
+
function parseContentLength(value) {
|
|
1389
|
+
const normalized = value.trim();
|
|
1390
|
+
if (!/^\d+$/u.test(normalized)) return void 0;
|
|
1394
1391
|
try {
|
|
1395
|
-
return
|
|
1392
|
+
return BigInt(normalized);
|
|
1396
1393
|
} catch {
|
|
1397
1394
|
return void 0;
|
|
1398
1395
|
}
|
|
1399
1396
|
}
|
|
1400
|
-
function
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
case "task.fail":
|
|
1406
|
-
case "task.cancelled":
|
|
1407
|
-
return envelope.payload.agentRef;
|
|
1408
|
-
default:
|
|
1409
|
-
return void 0;
|
|
1397
|
+
function parseJsonBytes(bytes) {
|
|
1398
|
+
try {
|
|
1399
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1400
|
+
} catch {
|
|
1401
|
+
return void 0;
|
|
1410
1402
|
}
|
|
1411
1403
|
}
|
|
1412
|
-
function
|
|
1413
|
-
|
|
1404
|
+
async function authenticateDevice(c, deps) {
|
|
1405
|
+
const device = await authenticateBearer(c.req.header("authorization"), deps.bearer);
|
|
1406
|
+
if (device === void 0) return void 0;
|
|
1407
|
+
return { device, stores: tenantStoresFor(device, deps.root) };
|
|
1414
1408
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
)
|
|
1424
|
-
|
|
1409
|
+
|
|
1410
|
+
// src/handlers/blobs.ts
|
|
1411
|
+
var BLOB_RESERVATION_TTL_MS = 15 * 60 * 1e3;
|
|
1412
|
+
var SignedUrlQuerySchema = z.object({
|
|
1413
|
+
sig: z.string().min(1),
|
|
1414
|
+
exp: z.coerce.number().finite()
|
|
1415
|
+
});
|
|
1416
|
+
function createBlobHandler(deps) {
|
|
1417
|
+
return async (c) => {
|
|
1418
|
+
const authenticated = await authenticateDevice(c, deps);
|
|
1419
|
+
if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
|
|
1420
|
+
const parsed = CreateBlobRequestSchema.safeParse(await readJsonBody(c));
|
|
1421
|
+
if (!parsed.success) return c.json({ error: "size, contentType, and contentHash are required" }, 400);
|
|
1422
|
+
if (parsed.data.size > deps.maxBlobSizeBytes) {
|
|
1423
|
+
return c.json({ error: `blob exceeds max size of ${deps.maxBlobSizeBytes} bytes` }, 413);
|
|
1424
|
+
}
|
|
1425
|
+
const reservationId = idempotencyKey(c);
|
|
1426
|
+
if (reservationId === void 0) {
|
|
1427
|
+
return c.json({ error: "Idempotency-Key header is required" }, 400);
|
|
1428
|
+
}
|
|
1425
1429
|
try {
|
|
1426
|
-
|
|
1427
|
-
|
|
1430
|
+
const reservation = await authenticated.stores.quota.reserve({
|
|
1431
|
+
reservationId,
|
|
1432
|
+
kind: "object",
|
|
1433
|
+
expectedBytes: BigInt(parsed.data.size),
|
|
1434
|
+
contentHash: contentHash(parsed.data.contentHash),
|
|
1435
|
+
contentType: parsed.data.contentType,
|
|
1436
|
+
ttlMs: BLOB_RESERVATION_TTL_MS
|
|
1437
|
+
});
|
|
1438
|
+
try {
|
|
1439
|
+
const { blobId, uploadUrl } = await authenticated.stores.blobs.createUpload(reservation);
|
|
1440
|
+
const response = { blobId, uploadUrl };
|
|
1441
|
+
return c.json(response, 200);
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
await authenticated.stores.quota.abortReservation(reservationId).catch(() => void 0);
|
|
1444
|
+
const rendered = renderStorageError(c, error);
|
|
1445
|
+
if (rendered !== void 0) return rendered;
|
|
1446
|
+
throw error;
|
|
1447
|
+
}
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
const rendered = renderStorageError(c, error);
|
|
1450
|
+
if (rendered !== void 0) return rendered;
|
|
1451
|
+
throw error;
|
|
1428
1452
|
}
|
|
1429
|
-
}
|
|
1430
|
-
return outcome;
|
|
1453
|
+
};
|
|
1431
1454
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
if (
|
|
1436
|
-
|
|
1437
|
-
if (
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1455
|
+
function finalizeBlobHandler(deps) {
|
|
1456
|
+
return async (c) => {
|
|
1457
|
+
const authenticated = await authenticateDevice(c, deps);
|
|
1458
|
+
if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
|
|
1459
|
+
const reservationId = idempotencyKey(c);
|
|
1460
|
+
if (reservationId === void 0) {
|
|
1461
|
+
return c.json({ error: "Idempotency-Key header is required" }, 400);
|
|
1462
|
+
}
|
|
1463
|
+
try {
|
|
1464
|
+
const reservation = await authenticated.stores.quota.readReservation(reservationId);
|
|
1465
|
+
if (reservation === void 0) {
|
|
1466
|
+
return c.json({ error: "storage_reservation_not_found" }, 404);
|
|
1467
|
+
}
|
|
1468
|
+
const blobId = c.req.param("id") ?? "";
|
|
1469
|
+
const observed = await authenticated.stores.blobs.observeUpload(
|
|
1470
|
+
blobId,
|
|
1471
|
+
reservation
|
|
1472
|
+
);
|
|
1473
|
+
if (observed === void 0) {
|
|
1474
|
+
await authenticated.stores.quota.abortReservation(reservationId);
|
|
1475
|
+
return c.json({ error: "storage_integrity_mismatch" }, 422);
|
|
1476
|
+
}
|
|
1477
|
+
await authenticated.stores.quota.finalizeReservation({
|
|
1478
|
+
reservationId,
|
|
1479
|
+
...observed
|
|
1480
|
+
});
|
|
1481
|
+
return c.body(null, 204);
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
const rendered = renderStorageError(c, error);
|
|
1484
|
+
if (rendered !== void 0) return rendered;
|
|
1485
|
+
throw error;
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function blobDownloadUrlHandler(deps) {
|
|
1490
|
+
return async (c) => {
|
|
1491
|
+
const authenticated = await authenticateDevice(c, deps);
|
|
1492
|
+
if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
|
|
1493
|
+
const downloadUrl = await authenticated.stores.blobs.getDownloadUrl(c.req.param("id") ?? "");
|
|
1494
|
+
if (downloadUrl === void 0) return c.json({ error: "blob not found" }, 404);
|
|
1495
|
+
const response = { downloadUrl };
|
|
1496
|
+
return c.json(response, 200);
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function blobUploadContentHandler(deps) {
|
|
1500
|
+
return async (c) => {
|
|
1501
|
+
const blobId = c.req.param("id") ?? "";
|
|
1502
|
+
const query = SignedUrlQuerySchema.safeParse({ sig: c.req.query("sig"), exp: c.req.query("exp") });
|
|
1503
|
+
if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "put", query.data.sig, query.data.exp)) {
|
|
1504
|
+
return c.json({ error: "invalid or expired signature" }, 401);
|
|
1441
1505
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1506
|
+
const expectedBytes = await deps.contentProxy.expectedUploadBytes(blobId);
|
|
1507
|
+
if (expectedBytes === void 0) return c.json({ error: "blob not found" }, 404);
|
|
1508
|
+
const absoluteCeiling = BigInt(deps.maxUploadBytes);
|
|
1509
|
+
if (expectedBytes > absoluteCeiling) return c.json({ error: "upload exceeds size limit" }, 413);
|
|
1510
|
+
const contentLength = c.req.header("content-length");
|
|
1511
|
+
if (contentLength !== void 0 && /^\d+$/u.test(contentLength) && BigInt(contentLength) > expectedBytes) {
|
|
1512
|
+
return c.json({ error: "upload exceeds declared size" }, 413);
|
|
1444
1513
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1514
|
+
const body = await readBoundedBody(c.req.raw.body, Number(expectedBytes));
|
|
1515
|
+
if (body.tooLarge) return c.json({ error: "upload exceeds declared size" }, 413);
|
|
1516
|
+
const result = await deps.contentProxy.writeContent(blobId, body.data);
|
|
1517
|
+
if (!result.ok) return c.json({ error: result.reason }, 422);
|
|
1518
|
+
return c.body(null, 204);
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
async function readBoundedBody(body, ceiling) {
|
|
1522
|
+
if (body === null) return { tooLarge: false, data: new Uint8Array() };
|
|
1523
|
+
const reader = body.getReader();
|
|
1524
|
+
const data = new Uint8Array(ceiling);
|
|
1525
|
+
let offset = 0;
|
|
1526
|
+
try {
|
|
1527
|
+
for (; ; ) {
|
|
1528
|
+
const next = await reader.read();
|
|
1529
|
+
if (next.done) break;
|
|
1530
|
+
const chunk = next.value;
|
|
1531
|
+
if (chunk.byteLength > ceiling - offset) {
|
|
1532
|
+
void reader.cancel().catch(() => void 0);
|
|
1533
|
+
return { tooLarge: true };
|
|
1534
|
+
}
|
|
1535
|
+
data.set(chunk, offset);
|
|
1536
|
+
offset += chunk.byteLength;
|
|
1461
1537
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
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);
|
|
1538
|
+
} finally {
|
|
1539
|
+
reader.releaseLock();
|
|
1469
1540
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1541
|
+
return { tooLarge: false, data: data.subarray(0, offset) };
|
|
1542
|
+
}
|
|
1543
|
+
function blobDownloadContentHandler(deps) {
|
|
1544
|
+
return async (c) => {
|
|
1545
|
+
const blobId = c.req.param("id") ?? "";
|
|
1546
|
+
const query = SignedUrlQuerySchema.safeParse({ sig: c.req.query("sig"), exp: c.req.query("exp") });
|
|
1547
|
+
if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "get", query.data.sig, query.data.exp)) {
|
|
1548
|
+
return c.json({ error: "invalid or expired signature" }, 401);
|
|
1474
1549
|
}
|
|
1475
|
-
const
|
|
1476
|
-
if (
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1550
|
+
const result = await deps.contentProxy.readContent(blobId);
|
|
1551
|
+
if (result === void 0) return c.json({ error: "blob not found" }, 404);
|
|
1552
|
+
if (!result.ok) return c.json({ error: result.code }, BLOB_READ_ERROR_HTTP_STATUS[result.code]);
|
|
1553
|
+
return c.body(new Uint8Array(result.content.data), 200, { "content-type": result.content.contentType });
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
var BLOB_READ_ERROR_HTTP_STATUS = {
|
|
1557
|
+
blob_upstream_unavailable: 502,
|
|
1558
|
+
blob_upstream_stream_interrupted: 502
|
|
1559
|
+
};
|
|
1560
|
+
function idempotencyKey(c) {
|
|
1561
|
+
const value = c.req.header("Idempotency-Key");
|
|
1562
|
+
if (value === void 0 || value.length === 0 || value.length > 200) return void 0;
|
|
1563
|
+
return value;
|
|
1564
|
+
}
|
|
1565
|
+
function renderStorageError(c, error) {
|
|
1566
|
+
if (!isCoreError(error)) return void 0;
|
|
1567
|
+
if (STORAGE_ERROR_CODES.includes(error.code)) {
|
|
1568
|
+
const code = error.code;
|
|
1569
|
+
return c.json({ error: code }, STORAGE_ERROR_HTTP_STATUS[code]);
|
|
1482
1570
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
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";
|
|
1571
|
+
if (error.code === "storage_reservation_not_found" || error.code === "object_not_found") {
|
|
1572
|
+
return c.json({ error: error.code }, 404);
|
|
1502
1573
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
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;
|
|
1574
|
+
if (error.code === "storage_entitlement_missing" || error.code === "object_state_invalid") {
|
|
1575
|
+
return c.json({ error: error.code }, 409);
|
|
1515
1576
|
}
|
|
1516
|
-
if (
|
|
1517
|
-
|
|
1518
|
-
return "rejected";
|
|
1577
|
+
if (error.code === "content_hash_invalid") {
|
|
1578
|
+
return c.json({ error: error.code }, 400);
|
|
1519
1579
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1580
|
+
return void 0;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/handlers/capabilities.ts
|
|
1584
|
+
function capabilitiesHandler(deps) {
|
|
1585
|
+
return async (c) => c.json(deps.declaration, 200);
|
|
1586
|
+
}
|
|
1587
|
+
var AUTH_JSON_BODY_MAX_BYTES = 16 * 1024;
|
|
1588
|
+
function pairHandler(deps) {
|
|
1589
|
+
return async (c) => {
|
|
1590
|
+
const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
|
|
1591
|
+
if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
|
|
1592
|
+
const parsed = PairRequestSchema.safeParse(body.body);
|
|
1593
|
+
if (!parsed.success) {
|
|
1594
|
+
return c.json(
|
|
1523
1595
|
{
|
|
1524
|
-
|
|
1525
|
-
sourceEnvelopeId: envelope.id,
|
|
1526
|
-
batchSeq: envelope.payload.seq,
|
|
1527
|
-
events: envelope.payload.events,
|
|
1528
|
-
dropped: 0
|
|
1596
|
+
error: "pairingCode, deviceName, and devicePublicKey are required strings; optional machineId must be 64 lowercase hex characters"
|
|
1529
1597
|
},
|
|
1530
|
-
|
|
1598
|
+
400
|
|
1531
1599
|
);
|
|
1532
|
-
} catch (caught) {
|
|
1533
|
-
if (isCloudError(caught, "activity_batch_too_large") || isCloudError(caught, "coordination_input_invalid")) {
|
|
1534
|
-
return "rejected";
|
|
1535
|
-
}
|
|
1536
|
-
throw caught;
|
|
1537
1600
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1601
|
+
const device = await deps.auth.redeemAndRegister(parsed.data);
|
|
1602
|
+
if (device === void 0) return c.json({ error: "invalid pairing code" }, 401);
|
|
1603
|
+
const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
|
|
1604
|
+
const response = PairResponseSchema.parse({
|
|
1605
|
+
deviceId: device.deviceId,
|
|
1606
|
+
accessToken,
|
|
1607
|
+
refreshHint: expiresAt,
|
|
1608
|
+
tenantId: device.tenantId
|
|
1609
|
+
});
|
|
1610
|
+
return c.json(response, 200);
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function challengeHandler(deps) {
|
|
1614
|
+
return async (c) => {
|
|
1615
|
+
const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
|
|
1616
|
+
if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
|
|
1617
|
+
const parsed = ChallengeRequestSchema.safeParse(body.body);
|
|
1618
|
+
if (!parsed.success) return c.json({ error: "deviceId is required" }, 400);
|
|
1619
|
+
const device = await deps.auth.resolveDevice(parsed.data.deviceId);
|
|
1620
|
+
if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
|
|
1621
|
+
const response = { nonce: await deps.auth.issueNonce(device) };
|
|
1622
|
+
return c.json(response, 200);
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function tokenHandler(deps) {
|
|
1626
|
+
return async (c) => {
|
|
1627
|
+
const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
|
|
1628
|
+
if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
|
|
1629
|
+
const parsed = TokenRequestSchema.safeParse(body.body);
|
|
1630
|
+
if (!parsed.success) return c.json({ error: "deviceId, nonce, and signature are required" }, 400);
|
|
1631
|
+
const { deviceId, nonce, signature } = parsed.data;
|
|
1632
|
+
const device = await deps.auth.resolveDevice(deviceId);
|
|
1633
|
+
if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
|
|
1634
|
+
if (!await deps.auth.verifySignature(device, nonce, signature)) {
|
|
1635
|
+
return c.json({ error: "invalid signature" }, 401);
|
|
1636
|
+
}
|
|
1637
|
+
if (!await deps.auth.consumeNonceIfValid(device, nonce)) {
|
|
1638
|
+
return c.json({ error: "invalid, expired, or already-used nonce" }, 401);
|
|
1639
|
+
}
|
|
1640
|
+
const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
|
|
1641
|
+
const response = { accessToken, expiresAt };
|
|
1642
|
+
return c.json(response, 200);
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
var CLOUD_PROTOCOL_CAPABILITIES = [
|
|
1646
|
+
"custom-harness",
|
|
1647
|
+
MAILBOX_READ_AHEAD_CAPABILITY,
|
|
1648
|
+
"result-document",
|
|
1649
|
+
// The kernel's inbound handles `task.approval_resolved` on both the plain
|
|
1650
|
+
// and the approval-timeline paths (`packages/cloud/src/inbound.ts:576,610`).
|
|
1651
|
+
// Without this flag `TaskRunner.sendApprovalResolved` returns early and a
|
|
1652
|
+
// locally-resolved approval is never reported on the wire at all.
|
|
1653
|
+
"approval_resolved",
|
|
1654
|
+
"agent-home-contract",
|
|
1655
|
+
"agent-home-projection",
|
|
1656
|
+
"agent-egress-policy",
|
|
1657
|
+
"agent-egress-reliable-ack",
|
|
1658
|
+
"agent-egress-fresh-session",
|
|
1659
|
+
// `handleAgentMessagePublish` accepts `agent.message_egress` gated on exactly
|
|
1660
|
+
// this capability (`packages/cloud/src/inbound.ts:124`) — the same inbound
|
|
1661
|
+
// family as the three egress flags above, omitted by the same drift.
|
|
1662
|
+
"agent-message-egress",
|
|
1663
|
+
"agent-content-workspace-read",
|
|
1664
|
+
"agent-content-transcript-read",
|
|
1665
|
+
"agent-content-artifact-read"
|
|
1666
|
+
];
|
|
1667
|
+
function sleep(ms) {
|
|
1668
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1669
|
+
}
|
|
1670
|
+
function eventsHandler(deps) {
|
|
1671
|
+
return async (c) => {
|
|
1672
|
+
const authenticated = await authenticateDevice(c, deps);
|
|
1673
|
+
if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
|
|
1674
|
+
const { device, stores } = authenticated;
|
|
1675
|
+
const cursorRaw = c.req.query("cursor");
|
|
1676
|
+
let cursor = 0;
|
|
1677
|
+
if (cursorRaw !== void 0) {
|
|
1678
|
+
const parsedCursor = Number(cursorRaw);
|
|
1679
|
+
if (!Number.isSafeInteger(parsedCursor) || parsedCursor < 0) {
|
|
1680
|
+
return c.json({ error: "invalid cursor" }, 400);
|
|
1681
|
+
}
|
|
1682
|
+
cursor = parsedCursor;
|
|
1546
1683
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
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";
|
|
1555
|
-
}
|
|
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;
|
|
1684
|
+
const afterRaw = c.req.query("afterSeq");
|
|
1685
|
+
const afterSeq = afterRaw === void 0 ? cursor : Number(afterRaw);
|
|
1686
|
+
if (!Number.isSafeInteger(afterSeq) || afterSeq < cursor || afterSeq - cursor > MAILBOX_READ_AHEAD_MAX_SEQS) {
|
|
1687
|
+
return c.json({ error: "invalid read-ahead offset" }, 400);
|
|
1568
1688
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
})
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
);
|
|
1689
|
+
const acked = await stores.mailbox.readCursor(device.deviceId);
|
|
1690
|
+
if (afterRaw !== void 0 && afterSeq > acked.deliveredSeq) return c.json({ error: "read offset exceeds delivered watermark" }, 409);
|
|
1691
|
+
if (cursor > acked.ackedSeq) {
|
|
1692
|
+
try {
|
|
1693
|
+
await stores.mailbox.advanceCursor({ deviceId: device.deviceId, ackedSeq: cursor });
|
|
1694
|
+
} catch (caught) {
|
|
1695
|
+
if (isCoreConflictError(caught, "mailbox_cursor_ahead_of_delivery")) {
|
|
1696
|
+
return c.json({ error: "cursor exceeds delivered watermark" }, 409);
|
|
1697
|
+
}
|
|
1698
|
+
throw caught;
|
|
1592
1699
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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 }
|
|
1700
|
+
}
|
|
1701
|
+
const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
|
|
1702
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1703
|
+
let scanCursor = afterSeq;
|
|
1704
|
+
while (true) {
|
|
1705
|
+
const page = await stores.mailbox.readAfter({
|
|
1706
|
+
deviceId: device.deviceId,
|
|
1707
|
+
afterSeq: scanCursor,
|
|
1708
|
+
limit: deps.pageLimit
|
|
1709
|
+
});
|
|
1710
|
+
if (cursor < page.recoverableFrom - 1) {
|
|
1711
|
+
return c.json({ error: "cursor_too_old", recoverableFrom: page.recoverableFrom }, 409);
|
|
1623
1712
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1713
|
+
if (page.messages.length === 0) break;
|
|
1714
|
+
const decoded = page.messages.map((message) => decodeEnvelope(message.body));
|
|
1715
|
+
const offeredTaskIds = decoded.flatMap(
|
|
1716
|
+
(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] : []
|
|
1717
|
+
);
|
|
1718
|
+
const attemptsByTaskId = Object.fromEntries(
|
|
1719
|
+
(await stores.tasks.getMany(offeredTaskIds)).map((attempt2) => [attempt2.taskId, attempt2])
|
|
1720
|
+
);
|
|
1721
|
+
const events = decoded.filter((event) => {
|
|
1722
|
+
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") {
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1725
|
+
return event.task_id === void 0 || attemptsByTaskId[event.task_id]?.cancellation === void 0;
|
|
1726
|
+
});
|
|
1727
|
+
if (events.length === 0 && page.hasMore) {
|
|
1728
|
+
scanCursor = page.nextSeq;
|
|
1729
|
+
continue;
|
|
1635
1730
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
-
});
|
|
1731
|
+
if (events.length === 0) break;
|
|
1732
|
+
const response2 = {
|
|
1733
|
+
events,
|
|
1734
|
+
cursor: page.nextSeq,
|
|
1735
|
+
capabilities: CLOUD_PROTOCOL_CAPABILITIES
|
|
1736
|
+
};
|
|
1737
|
+
await stores.mailbox.recordDelivery({
|
|
1738
|
+
deviceId: device.deviceId,
|
|
1739
|
+
deliveredSeq: response2.cursor
|
|
1740
|
+
});
|
|
1741
|
+
return c.json(response2, 200);
|
|
1742
|
+
}
|
|
1743
|
+
if (attempt < attempts - 1) await sleep(deps.longPollIntervalMs);
|
|
1672
1744
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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 } };
|
|
1745
|
+
const response = {
|
|
1746
|
+
events: [],
|
|
1747
|
+
cursor: afterSeq,
|
|
1748
|
+
capabilities: CLOUD_PROTOCOL_CAPABILITIES
|
|
1749
|
+
};
|
|
1750
|
+
return c.json(response, 200);
|
|
1751
|
+
};
|
|
1701
1752
|
}
|
|
1702
|
-
|
|
1703
|
-
// src/handlers/messages.ts
|
|
1704
1753
|
var MESSAGES_JSON_BODY_MAX_BYTES = 2 * 1024 * 1024;
|
|
1705
1754
|
function messagesHandler(deps) {
|
|
1706
1755
|
return async (c) => {
|
|
@@ -2661,6 +2710,13 @@ async function recordAgentHomeProjectionCompletion(receipts, tenant, deviceId, r
|
|
|
2661
2710
|
completedAt: storedCompletion.receipt.recordedAt
|
|
2662
2711
|
};
|
|
2663
2712
|
}
|
|
2713
|
+
function readDeviceTerminalReceipt(taskId, receipt) {
|
|
2714
|
+
const envelope = decodeEnvelope(receipt.body);
|
|
2715
|
+
if (envelope.task_id !== taskId || envelope.type !== "task.complete" && envelope.type !== "task.fail" && envelope.type !== "task.decline" && envelope.type !== "task.cancelled") {
|
|
2716
|
+
throw new ByokCloudError("terminal_receipt_unreadable", "Device terminal receipt identity or type is invalid.");
|
|
2717
|
+
}
|
|
2718
|
+
return { envelope, recordedAt: receipt.recordedAt };
|
|
2719
|
+
}
|
|
2664
2720
|
function projectTerminalResult(taskId, receipt) {
|
|
2665
2721
|
let envelope;
|
|
2666
2722
|
try {
|
|
@@ -3474,6 +3530,36 @@ function createByokCloud(options) {
|
|
|
3474
3530
|
throw new ByokCloudError("mailbox_receipt_mismatch", "Agent message disposition mailbox readback mismatched.");
|
|
3475
3531
|
}
|
|
3476
3532
|
}
|
|
3533
|
+
async function enqueueFreshAgentEgressOffer(tenant, deviceId, input) {
|
|
3534
|
+
await assertAgentCapabilities(tenant, deviceId, [
|
|
3535
|
+
AGENT_HOME_CONTRACT_CAPABILITY,
|
|
3536
|
+
AGENT_EGRESS_POLICY_CAPABILITY,
|
|
3537
|
+
AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
|
|
3538
|
+
AGENT_EGRESS_FRESH_SESSION_CAPABILITY,
|
|
3539
|
+
...input.payload.messageEgress === void 0 ? [] : [AGENT_MESSAGE_EGRESS_CAPABILITY],
|
|
3540
|
+
...input.payload.terminalProjection === void 0 ? [] : [TERMINAL_PROJECTION_SELECTION_CAPABILITY]
|
|
3541
|
+
]);
|
|
3542
|
+
const payload = TaskOfferForAgentWithEgressFreshPayloadSchema.parse(input.payload);
|
|
3543
|
+
if (payload.messageEgress === void 0 && input.agentMessageContext !== void 0) {
|
|
3544
|
+
throw new Error("agentMessageContext requires messageEgress");
|
|
3545
|
+
}
|
|
3546
|
+
const messageContext = payload.messageEgress === void 0 ? void 0 : AgentMessageServerContextSchema.parse(input.agentMessageContext);
|
|
3547
|
+
const enqueued = await enqueueTaskEnvelope(
|
|
3548
|
+
tenant,
|
|
3549
|
+
deviceId,
|
|
3550
|
+
input.taskId,
|
|
3551
|
+
payload.agentRef,
|
|
3552
|
+
(taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress_fresh", payload, { id: messageId, taskId, seq }),
|
|
3553
|
+
payload.messageEgress === void 0 ? void 0 : async (stores, taskId) => {
|
|
3554
|
+
const body = JSON.stringify({ agentRef: payload.agentRef, requirement: payload.messageEgress, context: messageContext });
|
|
3555
|
+
const recorded = await stores.receipts.record({ key: `agent-message-offer:${deviceId}:${taskId}`, body });
|
|
3556
|
+
if (!recorded.created && recorded.receipt.body !== body) {
|
|
3557
|
+
throw new ByokCloudError("agent_content_request_mismatch", `Task ${taskId} already has a different Agent message context.`);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
);
|
|
3561
|
+
return enqueued;
|
|
3562
|
+
}
|
|
3477
3563
|
return {
|
|
3478
3564
|
fetch: registry.fetch,
|
|
3479
3565
|
routes: registry.routes,
|
|
@@ -3545,35 +3631,11 @@ function createByokCloud(options) {
|
|
|
3545
3631
|
);
|
|
3546
3632
|
return enqueued;
|
|
3547
3633
|
},
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
AGENT_EGRESS_FRESH_SESSION_CAPABILITY,
|
|
3554
|
-
...input.payload.messageEgress === void 0 ? [] : [AGENT_MESSAGE_EGRESS_CAPABILITY],
|
|
3555
|
-
...input.payload.terminalProjection === void 0 ? [] : [TERMINAL_PROJECTION_SELECTION_CAPABILITY]
|
|
3556
|
-
]);
|
|
3557
|
-
const payload = TaskOfferForAgentWithEgressFreshPayloadSchema.parse(input.payload);
|
|
3558
|
-
if (payload.messageEgress === void 0 && input.agentMessageContext !== void 0) {
|
|
3559
|
-
throw new Error("agentMessageContext requires messageEgress");
|
|
3560
|
-
}
|
|
3561
|
-
const messageContext = payload.messageEgress === void 0 ? void 0 : AgentMessageServerContextSchema.parse(input.agentMessageContext);
|
|
3562
|
-
const enqueued = await enqueueTaskEnvelope(
|
|
3563
|
-
tenant,
|
|
3564
|
-
deviceId,
|
|
3565
|
-
input.taskId,
|
|
3566
|
-
payload.agentRef,
|
|
3567
|
-
(taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress_fresh", payload, { id: messageId, taskId, seq }),
|
|
3568
|
-
payload.messageEgress === void 0 ? void 0 : async (stores, taskId) => {
|
|
3569
|
-
const body = JSON.stringify({ agentRef: payload.agentRef, requirement: payload.messageEgress, context: messageContext });
|
|
3570
|
-
const recorded = await stores.receipts.record({ key: `agent-message-offer:${deviceId}:${taskId}`, body });
|
|
3571
|
-
if (!recorded.created && recorded.receipt.body !== body) {
|
|
3572
|
-
throw new ByokCloudError("agent_content_request_mismatch", `Task ${taskId} already has a different Agent message context.`);
|
|
3573
|
-
}
|
|
3574
|
-
}
|
|
3575
|
-
);
|
|
3576
|
-
return enqueued;
|
|
3634
|
+
enqueueFreshAgentEgressOffer,
|
|
3635
|
+
async submitRecurringExecution(tenant, input) {
|
|
3636
|
+
const validated = RecurringExecutionInputSchema.parse(input);
|
|
3637
|
+
if (typeof options.agentMessage?.consume !== "function") throw new Error("Recurring execution requires a registered Agent message consumer.");
|
|
3638
|
+
return enqueueFreshAgentEgressOffer(tenant, validated.deviceId, validated);
|
|
3577
3639
|
},
|
|
3578
3640
|
async enqueueAgentContentRead(tenant, deviceId, input) {
|
|
3579
3641
|
const payload = AgentContentReadPayloadSchema.parse(input.payload);
|
|
@@ -3752,6 +3814,17 @@ function createByokCloud(options) {
|
|
|
3752
3814
|
readTerminalReceipt(tenant, taskId) {
|
|
3753
3815
|
return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
|
|
3754
3816
|
},
|
|
3817
|
+
async readDeviceTerminal(tenant, taskId) {
|
|
3818
|
+
const receipt = await tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
|
|
3819
|
+
return receipt === void 0 ? void 0 : readDeviceTerminalReceipt(taskId, receipt);
|
|
3820
|
+
},
|
|
3821
|
+
readTaskAgentMessage(tenant, deviceId, taskId, agentRef) {
|
|
3822
|
+
return readTaskAgentMessage(tenantStoresFor(controlPlane(tenant), root), deviceId, taskId, agentRef);
|
|
3823
|
+
},
|
|
3824
|
+
readAgentMessageDisposition(tenant, deviceId, taskId, payload) {
|
|
3825
|
+
const parsed = AgentMessagePublishPayloadSchema.parse(payload);
|
|
3826
|
+
return readAgentMessageDisposition(tenantStoresFor(controlPlane(tenant), root), deviceId, taskId, parsed);
|
|
3827
|
+
},
|
|
3755
3828
|
readAgentEgress(tenant, deviceId, agentRef, eventId) {
|
|
3756
3829
|
return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, agentRef, eventId);
|
|
3757
3830
|
},
|
|
@@ -4406,6 +4479,10 @@ var InMemoryTaskAttemptStore = class {
|
|
|
4406
4479
|
return "reserved";
|
|
4407
4480
|
});
|
|
4408
4481
|
}
|
|
4482
|
+
async readTaskAgentMessage(tenant, input) {
|
|
4483
|
+
const admission = this.#state.messageAdmissions.get(tenantKey(tenant, input.taskId));
|
|
4484
|
+
return admission?.deviceId === input.deviceId ? admission : void 0;
|
|
4485
|
+
}
|
|
4409
4486
|
async readAgentMessage(tenant, input) {
|
|
4410
4487
|
const admission = this.#state.messageAdmissions.get(tenantKey(tenant, input.taskId));
|
|
4411
4488
|
return admission !== void 0 && admission.deviceId === input.deviceId && admission.messageId === input.messageId && admission.payloadBody === input.payloadBody ? admission : void 0;
|
|
@@ -5213,6 +5290,7 @@ var CLOUD_PORT_METHODS = {
|
|
|
5213
5290
|
"reserveAgentOffer",
|
|
5214
5291
|
"reserveAgentMessage",
|
|
5215
5292
|
"readAgentMessage",
|
|
5293
|
+
"readTaskAgentMessage",
|
|
5216
5294
|
"finalizeAgentMessage",
|
|
5217
5295
|
"get",
|
|
5218
5296
|
"getMany",
|
|
@@ -5247,6 +5325,6 @@ var CLOUD_PORT_INTERFACES = {
|
|
|
5247
5325
|
rateLimiter: "InboundRateLimiter"
|
|
5248
5326
|
};
|
|
5249
5327
|
|
|
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 };
|
|
5328
|
+
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, 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 };
|
|
5251
5329
|
//# sourceMappingURL=index.js.map
|
|
5252
5330
|
//# sourceMappingURL=index.js.map
|