@byok-sdk/protocol 0.1.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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/agent-event.d.ts +146 -0
- package/dist/blob.d.ts +24 -0
- package/dist/codec.d.ts +116 -0
- package/dist/envelope.d.ts +333 -0
- package/dist/errors.d.ts +25 -0
- package/dist/http-api.d.ts +725 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +432 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +646 -0
- package/dist/permission.d.ts +39 -0
- package/dist/task-state.d.ts +21 -0
- package/dist/version.d.ts +70 -0
- package/package.json +49 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { PROTOCOL_VERSION, CAPABILITY_FLAGS } from './version';
|
|
2
|
+
export type { CapabilityFlag } from './version';
|
|
3
|
+
export { BlobRefSchema, CONTENT_HASH_RE } from './blob';
|
|
4
|
+
export type { BlobRef } from './blob';
|
|
5
|
+
export { PermissionPolicySchema, PERMISSION_MODES } from './permission';
|
|
6
|
+
export type { PermissionPolicy, PermissionMode } from './permission';
|
|
7
|
+
export { AgentEventSchema, UnknownAgentEventSchema, AgentEventOrUnknownSchema, KNOWN_AGENT_EVENT_TYPES, isKnownAgentEvent, partitionAgentEvents, } from './agent-event';
|
|
8
|
+
export type { AgentEvent, UnknownAgentEvent, AgentEventOrUnknown } from './agent-event';
|
|
9
|
+
export { TASK_STATES, TASK_TRANSITIONS, canTransition } from './task-state';
|
|
10
|
+
export type { TaskState } from './task-state';
|
|
11
|
+
export { MESSAGE_TYPES, MESSAGE_PAYLOAD_SCHEMAS, SERVER_TO_DAEMON_TYPES, DAEMON_TO_SERVER_TYPES, RuntimeIdSchema, RuntimeInfoSchema, RuntimeCapabilitiesSchema, ConnHelloPayloadSchema, ConnAckPayloadSchema, TaskOfferPayloadSchema, TaskApprovePayloadSchema, TaskRejectPayloadSchema, TaskCancelPayloadSchema, TaskSteerPayloadSchema, TaskClaimPayloadSchema, TaskStartedPayloadSchema, TaskDeclinePayloadSchema, TaskProgressPayloadSchema, TaskArtifactPayloadSchema, TaskAwaitApprovalPayloadSchema, TaskCompletePayloadSchema, TaskFailPayloadSchema, TaskCancelledPayloadSchema, TaskApprovalResolvedPayloadSchema, } from './messages';
|
|
12
|
+
export type { MessageType, RuntimeId, RuntimeInfo, RuntimeCapabilities, ConnHelloPayload, ConnAckPayload, TaskOfferPayload, TaskApprovePayload, TaskRejectPayload, TaskCancelPayload, TaskSteerPayload, TaskClaimPayload, TaskStartedPayload, TaskDeclinePayload, TaskProgressPayload, TaskArtifactPayload, TaskAwaitApprovalPayload, TaskCompletePayload, TaskFailPayload, TaskCancelledPayload, TaskApprovalResolvedPayload, } from './messages';
|
|
13
|
+
export { EnvelopeSchema, isServerToDaemonType } from './envelope';
|
|
14
|
+
export type { Envelope } from './envelope';
|
|
15
|
+
export { ProtocolError, EnvelopeParseError, UnknownMessageTypeError, EnvelopeValidationError, } from './errors';
|
|
16
|
+
export { encodeEnvelope, decodeEnvelope, createEnvelope, parseMessage } from './codec';
|
|
17
|
+
export type { CreateEnvelopeOptions } from './codec';
|
|
18
|
+
export { PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, ChallengeResponseSchema, TokenRequestSchema, TokenResponseSchema, CreateBlobRequestSchema, CreateBlobResponseSchema, BlobDownloadUrlResponseSchema, EventsPollQuerySchema, EventsPollResponseSchema, MessagesSendRequestSchema, MessagesSendResponseSchema, MAX_MESSAGES_PER_BATCH, } from './http-api';
|
|
19
|
+
export type { PairRequest, PairResponse, ChallengeRequest, ChallengeResponse, TokenRequest, TokenResponse, CreateBlobRequest, CreateBlobResponse, BlobDownloadUrlResponse, EventsPollQuery, EventsPollResponse, MessagesSendRequest, MessagesSendResponse, } from './http-api';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// src/version.ts
|
|
4
|
+
var PROTOCOL_VERSION = 1;
|
|
5
|
+
var CAPABILITY_FLAGS = [
|
|
6
|
+
"steer",
|
|
7
|
+
"blob-upload",
|
|
8
|
+
"interactive-approval",
|
|
9
|
+
"approval_resolved",
|
|
10
|
+
"approval-targeting"
|
|
11
|
+
];
|
|
12
|
+
var CONTENT_HASH_RE = /^sha256:[0-9a-f]{64}$/;
|
|
13
|
+
var BlobRefSchema = z.object({
|
|
14
|
+
blobId: z.string(),
|
|
15
|
+
contentHash: z.string().regex(CONTENT_HASH_RE, 'contentHash must be "sha256:<64 lowercase hex>"'),
|
|
16
|
+
size: z.number().int().nonnegative(),
|
|
17
|
+
contentType: z.string(),
|
|
18
|
+
url: z.string().optional()
|
|
19
|
+
});
|
|
20
|
+
var PERMISSION_MODES = ["auto", "confirm", "readonly", "plan"];
|
|
21
|
+
var PermissionPolicySchema = z.object({
|
|
22
|
+
mode: z.enum(PERMISSION_MODES),
|
|
23
|
+
allowTools: z.array(z.string()).optional(),
|
|
24
|
+
denyTools: z.array(z.string()).optional(),
|
|
25
|
+
workspaceRoot: z.string().optional(),
|
|
26
|
+
network: z.boolean().optional()
|
|
27
|
+
}).strict();
|
|
28
|
+
var AgentEventSchema = z.discriminatedUnion("type", [
|
|
29
|
+
z.object({ type: z.literal("progress"), text: z.string() }),
|
|
30
|
+
z.object({ type: z.literal("tool_use"), tool: z.string(), input: z.unknown().optional() }),
|
|
31
|
+
z.object({ type: z.literal("tool_result"), tool: z.string(), output: z.unknown().optional() }),
|
|
32
|
+
z.object({ type: z.literal("artifact"), name: z.string(), contentType: z.string() }),
|
|
33
|
+
z.object({ type: z.literal("needs_approval"), summary: z.string() }),
|
|
34
|
+
z.object({ type: z.literal("turn_end") }),
|
|
35
|
+
z.object({ type: z.literal("error"), message: z.string() }),
|
|
36
|
+
/**
|
|
37
|
+
* Token usage reported by a runtime turn — maps from codex
|
|
38
|
+
* `turn.completed.usage` and claude `result` usage (adapters wired up in a
|
|
39
|
+
* later wave; this is just the variant + schema). All fields optional
|
|
40
|
+
* because runtimes report different subsets.
|
|
41
|
+
*/
|
|
42
|
+
z.object({
|
|
43
|
+
type: z.literal("usage"),
|
|
44
|
+
inputTokens: z.number().int().nonnegative().optional(),
|
|
45
|
+
cachedInputTokens: z.number().int().nonnegative().optional(),
|
|
46
|
+
outputTokens: z.number().int().nonnegative().optional(),
|
|
47
|
+
reasoningTokens: z.number().int().nonnegative().optional(),
|
|
48
|
+
totalTokens: z.number().int().nonnegative().optional()
|
|
49
|
+
})
|
|
50
|
+
]);
|
|
51
|
+
var KNOWN_AGENT_EVENT_TYPES = z.toJSONSchema(AgentEventSchema).oneOf.map((branch) => branch.properties.type.const);
|
|
52
|
+
var UnknownAgentEventSchema = z.object({ type: z.string() }).passthrough().refine(
|
|
53
|
+
(event) => !KNOWN_AGENT_EVENT_TYPES.includes(event.type),
|
|
54
|
+
"type refers to a known AgentEvent variant; malformed known variants must fail through AgentEventSchema, not fall back to unknown-tolerance"
|
|
55
|
+
);
|
|
56
|
+
var AgentEventOrUnknownSchema = z.union([AgentEventSchema, UnknownAgentEventSchema]);
|
|
57
|
+
function isKnownAgentEvent(event) {
|
|
58
|
+
return KNOWN_AGENT_EVENT_TYPES.includes(event.type);
|
|
59
|
+
}
|
|
60
|
+
function partitionAgentEvents(events) {
|
|
61
|
+
const known = [];
|
|
62
|
+
const unknown = [];
|
|
63
|
+
for (const event of events) {
|
|
64
|
+
if (isKnownAgentEvent(event)) {
|
|
65
|
+
known.push(event);
|
|
66
|
+
} else {
|
|
67
|
+
unknown.push(event);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { known, unknown };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/task-state.ts
|
|
74
|
+
var TASK_STATES = [
|
|
75
|
+
"Offered",
|
|
76
|
+
"Claimed",
|
|
77
|
+
"Running",
|
|
78
|
+
"AwaitApproval",
|
|
79
|
+
"Complete",
|
|
80
|
+
"Failed",
|
|
81
|
+
"Cancelled"
|
|
82
|
+
];
|
|
83
|
+
var TASK_TRANSITIONS = {
|
|
84
|
+
Offered: ["Claimed", "Cancelled", "Failed"],
|
|
85
|
+
Claimed: ["Running", "Failed", "Cancelled"],
|
|
86
|
+
Running: ["AwaitApproval", "Complete", "Failed", "Cancelled"],
|
|
87
|
+
AwaitApproval: ["Running", "Failed", "Cancelled"],
|
|
88
|
+
Complete: [],
|
|
89
|
+
Failed: [],
|
|
90
|
+
Cancelled: []
|
|
91
|
+
};
|
|
92
|
+
function canTransition(from, to) {
|
|
93
|
+
return TASK_TRANSITIONS[from].includes(to);
|
|
94
|
+
}
|
|
95
|
+
var MAX_INLINE_BYTES = 64 * 1024;
|
|
96
|
+
function isWithinInlineByteLimit(value) {
|
|
97
|
+
return new TextEncoder().encode(value).length <= MAX_INLINE_BYTES;
|
|
98
|
+
}
|
|
99
|
+
var RuntimeIdSchema = z.enum(["pi", "claude", "codex"]);
|
|
100
|
+
var RuntimeCapabilitiesSchema = z.object({
|
|
101
|
+
steer: z.boolean().optional(),
|
|
102
|
+
resume: z.boolean().optional(),
|
|
103
|
+
approvalInteractive: z.boolean().optional(),
|
|
104
|
+
permissionModes: z.array(z.string()).optional()
|
|
105
|
+
});
|
|
106
|
+
var RuntimeInfoSchema = z.object({
|
|
107
|
+
id: RuntimeIdSchema,
|
|
108
|
+
version: z.string().optional(),
|
|
109
|
+
authPresent: z.boolean().optional(),
|
|
110
|
+
/** Optional: older daemons omit this entirely (pre-freeze addition). */
|
|
111
|
+
capabilities: RuntimeCapabilitiesSchema.optional()
|
|
112
|
+
});
|
|
113
|
+
var ConnHelloPayloadSchema = z.object({
|
|
114
|
+
protocolVersions: z.array(z.number().int()),
|
|
115
|
+
capabilities: z.array(z.string()),
|
|
116
|
+
deviceId: z.string(),
|
|
117
|
+
productId: z.string(),
|
|
118
|
+
/** Runtimes detected on this device (M1 gap #4; replaces `agents`). */
|
|
119
|
+
runtimes: z.array(RuntimeInfoSchema).optional(),
|
|
120
|
+
/**
|
|
121
|
+
* Last `seq` this device has seen from the server (M1 redelivery cursor).
|
|
122
|
+
* Omitted on a device's first-ever connection. The server replays any
|
|
123
|
+
* server->daemon envelopes with `seq > cursor` it still holds — see
|
|
124
|
+
* docs/protocol.md "At-least-once delivery".
|
|
125
|
+
*/
|
|
126
|
+
cursor: z.number().int().optional()
|
|
127
|
+
});
|
|
128
|
+
var ConnAckPayloadSchema = z.object({
|
|
129
|
+
protocolVersion: z.number().int(),
|
|
130
|
+
capabilities: z.array(z.string()),
|
|
131
|
+
serverTime: z.iso.datetime({ offset: true })
|
|
132
|
+
});
|
|
133
|
+
var InstructionBlobRefSchema = z.object({ blobRef: BlobRefSchema }).strict();
|
|
134
|
+
var TaskOfferPayloadSchema = z.object({
|
|
135
|
+
instruction: z.union([z.string(), InstructionBlobRefSchema]),
|
|
136
|
+
policy: PermissionPolicySchema,
|
|
137
|
+
runtime: RuntimeIdSchema.optional(),
|
|
138
|
+
sessionRef: z.string().optional(),
|
|
139
|
+
workspaceHint: z.string().optional(),
|
|
140
|
+
limits: z.object({
|
|
141
|
+
maxDurationMs: z.number().int().positive().optional(),
|
|
142
|
+
maxTokens: z.number().int().positive().optional()
|
|
143
|
+
}).optional()
|
|
144
|
+
});
|
|
145
|
+
var TaskApprovePayloadSchema = z.object({
|
|
146
|
+
approvalId: z.string().optional()
|
|
147
|
+
});
|
|
148
|
+
var TaskRejectPayloadSchema = z.object({
|
|
149
|
+
reason: z.string().optional(),
|
|
150
|
+
approvalId: z.string().optional()
|
|
151
|
+
});
|
|
152
|
+
var TaskCancelPayloadSchema = z.object({
|
|
153
|
+
reason: z.string().optional()
|
|
154
|
+
});
|
|
155
|
+
var TaskSteerPayloadSchema = z.object({
|
|
156
|
+
text: z.string()
|
|
157
|
+
});
|
|
158
|
+
var TaskClaimPayloadSchema = z.object({
|
|
159
|
+
deviceId: z.string(),
|
|
160
|
+
agentId: z.string().optional(),
|
|
161
|
+
runtime: RuntimeIdSchema.optional(),
|
|
162
|
+
capabilities: RuntimeCapabilitiesSchema.optional()
|
|
163
|
+
});
|
|
164
|
+
var TaskStartedPayloadSchema = z.object({});
|
|
165
|
+
var TaskDeclinePayloadSchema = z.object({
|
|
166
|
+
reason: z.string(),
|
|
167
|
+
retryable: z.boolean().optional()
|
|
168
|
+
});
|
|
169
|
+
var TaskProgressPayloadSchema = z.object({
|
|
170
|
+
seq: z.number().int(),
|
|
171
|
+
events: z.array(AgentEventOrUnknownSchema)
|
|
172
|
+
});
|
|
173
|
+
var TaskArtifactPayloadSchema = z.object({
|
|
174
|
+
name: z.string(),
|
|
175
|
+
contentType: z.string(),
|
|
176
|
+
inline: z.string().refine(isWithinInlineByteLimit, {
|
|
177
|
+
message: "inline artifact payload exceeds 64KB limit"
|
|
178
|
+
}).optional(),
|
|
179
|
+
blobRef: BlobRefSchema.optional()
|
|
180
|
+
});
|
|
181
|
+
var TaskAwaitApprovalPayloadSchema = z.object({
|
|
182
|
+
summary: z.string(),
|
|
183
|
+
approvalId: z.string().optional()
|
|
184
|
+
});
|
|
185
|
+
var TaskCompletePayloadSchema = z.object({
|
|
186
|
+
summary: z.string(),
|
|
187
|
+
sessionRef: z.string(),
|
|
188
|
+
artifactRefs: z.array(BlobRefSchema).optional()
|
|
189
|
+
});
|
|
190
|
+
var TaskFailPayloadSchema = z.object({
|
|
191
|
+
reason: z.string(),
|
|
192
|
+
retryable: z.boolean().optional()
|
|
193
|
+
});
|
|
194
|
+
var TaskCancelledPayloadSchema = z.object({
|
|
195
|
+
reason: z.string().optional()
|
|
196
|
+
});
|
|
197
|
+
var TaskApprovalResolvedPayloadSchema = z.object({
|
|
198
|
+
approvalId: z.string(),
|
|
199
|
+
decision: z.enum(["approve", "reject"]),
|
|
200
|
+
resolvedBy: z.enum(["local"]),
|
|
201
|
+
at: z.iso.datetime({ offset: true })
|
|
202
|
+
});
|
|
203
|
+
var MESSAGE_PAYLOAD_SCHEMAS = {
|
|
204
|
+
"conn.hello": ConnHelloPayloadSchema,
|
|
205
|
+
"conn.ack": ConnAckPayloadSchema,
|
|
206
|
+
"task.offer": TaskOfferPayloadSchema,
|
|
207
|
+
"task.approve": TaskApprovePayloadSchema,
|
|
208
|
+
"task.reject": TaskRejectPayloadSchema,
|
|
209
|
+
"task.cancel": TaskCancelPayloadSchema,
|
|
210
|
+
"task.steer": TaskSteerPayloadSchema,
|
|
211
|
+
"task.claim": TaskClaimPayloadSchema,
|
|
212
|
+
"task.started": TaskStartedPayloadSchema,
|
|
213
|
+
"task.decline": TaskDeclinePayloadSchema,
|
|
214
|
+
"task.progress": TaskProgressPayloadSchema,
|
|
215
|
+
"task.artifact": TaskArtifactPayloadSchema,
|
|
216
|
+
"task.await_approval": TaskAwaitApprovalPayloadSchema,
|
|
217
|
+
"task.complete": TaskCompletePayloadSchema,
|
|
218
|
+
"task.fail": TaskFailPayloadSchema,
|
|
219
|
+
"task.cancelled": TaskCancelledPayloadSchema,
|
|
220
|
+
"task.approval_resolved": TaskApprovalResolvedPayloadSchema
|
|
221
|
+
};
|
|
222
|
+
var MESSAGE_TYPES = Object.keys(MESSAGE_PAYLOAD_SCHEMAS);
|
|
223
|
+
var SERVER_TO_DAEMON_TYPES = [
|
|
224
|
+
"conn.ack",
|
|
225
|
+
"task.offer",
|
|
226
|
+
"task.approve",
|
|
227
|
+
"task.reject",
|
|
228
|
+
"task.cancel",
|
|
229
|
+
"task.steer"
|
|
230
|
+
];
|
|
231
|
+
var DAEMON_TO_SERVER_TYPES = [
|
|
232
|
+
"task.claim",
|
|
233
|
+
"task.started",
|
|
234
|
+
"task.decline",
|
|
235
|
+
"task.progress",
|
|
236
|
+
"task.artifact",
|
|
237
|
+
"task.await_approval",
|
|
238
|
+
"task.complete",
|
|
239
|
+
"task.fail",
|
|
240
|
+
"task.cancelled",
|
|
241
|
+
"task.approval_resolved"
|
|
242
|
+
];
|
|
243
|
+
var REQUIRED_TASK_ID = z.string().min(1);
|
|
244
|
+
var OPTIONAL_TASK_ID = REQUIRED_TASK_ID.optional();
|
|
245
|
+
var REQUIRED_SEQ = z.number().int();
|
|
246
|
+
var OPTIONAL_SEQ = REQUIRED_SEQ.optional();
|
|
247
|
+
function envelopeShape(type, taskId, seq) {
|
|
248
|
+
return z.object({
|
|
249
|
+
v: z.number().int(),
|
|
250
|
+
id: z.uuid(),
|
|
251
|
+
ts: z.iso.datetime({ offset: true }),
|
|
252
|
+
type: z.literal(type),
|
|
253
|
+
task_id: taskId,
|
|
254
|
+
session_ref: z.string().optional(),
|
|
255
|
+
seq,
|
|
256
|
+
payload: MESSAGE_PAYLOAD_SCHEMAS[type]
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
var EnvelopeSchema = z.discriminatedUnion("type", [
|
|
260
|
+
// conn.* — task_id stays optional (no task routing). conn.hello is
|
|
261
|
+
// daemon -> server (no cursor to satisfy on this leg); conn.ack is
|
|
262
|
+
// server -> daemon and therefore carries the required redelivery `seq`.
|
|
263
|
+
envelopeShape("conn.hello", OPTIONAL_TASK_ID, OPTIONAL_SEQ),
|
|
264
|
+
envelopeShape("conn.ack", OPTIONAL_TASK_ID, REQUIRED_SEQ),
|
|
265
|
+
// task.* server -> daemon: task_id required (routing key) + seq required
|
|
266
|
+
// (per-device redelivery cursor).
|
|
267
|
+
envelopeShape("task.offer", REQUIRED_TASK_ID, REQUIRED_SEQ),
|
|
268
|
+
envelopeShape("task.approve", REQUIRED_TASK_ID, REQUIRED_SEQ),
|
|
269
|
+
envelopeShape("task.reject", REQUIRED_TASK_ID, REQUIRED_SEQ),
|
|
270
|
+
envelopeShape("task.cancel", REQUIRED_TASK_ID, REQUIRED_SEQ),
|
|
271
|
+
envelopeShape("task.steer", REQUIRED_TASK_ID, REQUIRED_SEQ),
|
|
272
|
+
// task.* daemon -> server: task_id required (routing key); envelope-level
|
|
273
|
+
// seq is not required in this direction in M1 (no daemon->server
|
|
274
|
+
// redelivery cursor yet — see docs/protocol.md).
|
|
275
|
+
envelopeShape("task.claim", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
276
|
+
envelopeShape("task.started", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
277
|
+
envelopeShape("task.decline", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
278
|
+
envelopeShape("task.progress", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
279
|
+
envelopeShape("task.artifact", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
280
|
+
envelopeShape("task.await_approval", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
281
|
+
envelopeShape("task.complete", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
282
|
+
envelopeShape("task.fail", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
283
|
+
envelopeShape("task.cancelled", REQUIRED_TASK_ID, OPTIONAL_SEQ),
|
|
284
|
+
envelopeShape("task.approval_resolved", REQUIRED_TASK_ID, OPTIONAL_SEQ)
|
|
285
|
+
]);
|
|
286
|
+
function isServerToDaemonType(type) {
|
|
287
|
+
return SERVER_TO_DAEMON_TYPES.includes(type);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/errors.ts
|
|
291
|
+
var ProtocolError = class extends Error {
|
|
292
|
+
constructor(message, options) {
|
|
293
|
+
super(message, options);
|
|
294
|
+
this.name = "ProtocolError";
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var EnvelopeParseError = class extends ProtocolError {
|
|
298
|
+
constructor(message, cause) {
|
|
299
|
+
super(message, { cause });
|
|
300
|
+
this.name = "EnvelopeParseError";
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
var UnknownMessageTypeError = class extends ProtocolError {
|
|
304
|
+
type;
|
|
305
|
+
constructor(type) {
|
|
306
|
+
super(`Unknown message type: ${String(type)}`);
|
|
307
|
+
this.name = "UnknownMessageTypeError";
|
|
308
|
+
this.type = type;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var EnvelopeValidationError = class extends ProtocolError {
|
|
312
|
+
issues;
|
|
313
|
+
constructor(message, issues) {
|
|
314
|
+
super(message, { cause: issues });
|
|
315
|
+
this.name = "EnvelopeValidationError";
|
|
316
|
+
this.issues = issues;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/codec.ts
|
|
321
|
+
var MESSAGE_TYPE_SET = new Set(MESSAGE_TYPES);
|
|
322
|
+
function decodeText(input) {
|
|
323
|
+
return typeof input === "string" ? input : new TextDecoder("utf-8").decode(input);
|
|
324
|
+
}
|
|
325
|
+
function parseMessage(data) {
|
|
326
|
+
const result = EnvelopeSchema.safeParse(data);
|
|
327
|
+
if (result.success) {
|
|
328
|
+
return result.data;
|
|
329
|
+
}
|
|
330
|
+
const typeValue = typeof data === "object" && data !== null && !Array.isArray(data) ? data.type : void 0;
|
|
331
|
+
if (typeof typeValue !== "string" || !MESSAGE_TYPE_SET.has(typeValue)) {
|
|
332
|
+
throw new UnknownMessageTypeError(typeValue);
|
|
333
|
+
}
|
|
334
|
+
throw new EnvelopeValidationError(
|
|
335
|
+
`Envelope failed validation for type "${typeValue}"`,
|
|
336
|
+
result.error
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
function decodeEnvelope(line) {
|
|
340
|
+
const text = decodeText(line).replace(/[\r\n]+$/, "");
|
|
341
|
+
let json;
|
|
342
|
+
try {
|
|
343
|
+
json = JSON.parse(text);
|
|
344
|
+
} catch (cause) {
|
|
345
|
+
throw new EnvelopeParseError("Envelope line is not valid JSON", cause);
|
|
346
|
+
}
|
|
347
|
+
return parseMessage(json);
|
|
348
|
+
}
|
|
349
|
+
function encodeEnvelope(env) {
|
|
350
|
+
return `${JSON.stringify(env)}
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
function createEnvelope(type, payload, ...rest) {
|
|
354
|
+
const opts = rest[0] ?? {};
|
|
355
|
+
const envelope = {
|
|
356
|
+
v: opts.v ?? PROTOCOL_VERSION,
|
|
357
|
+
id: opts.id ?? crypto.randomUUID(),
|
|
358
|
+
ts: opts.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
359
|
+
type,
|
|
360
|
+
...opts.taskId !== void 0 ? { task_id: opts.taskId } : {},
|
|
361
|
+
...opts.sessionRef !== void 0 ? { session_ref: opts.sessionRef } : {},
|
|
362
|
+
...opts.seq !== void 0 ? { seq: opts.seq } : {},
|
|
363
|
+
payload
|
|
364
|
+
};
|
|
365
|
+
const result = EnvelopeSchema.safeParse(envelope);
|
|
366
|
+
if (!result.success) {
|
|
367
|
+
throw new EnvelopeValidationError(`createEnvelope built an invalid envelope for type "${type}"`, result.error);
|
|
368
|
+
}
|
|
369
|
+
return result.data;
|
|
370
|
+
}
|
|
371
|
+
var PairRequestSchema = z.object({
|
|
372
|
+
pairingCode: z.string(),
|
|
373
|
+
deviceName: z.string(),
|
|
374
|
+
/** Ed25519 public key, base64url-encoded. Private key stays device-local (OS keychain or 0600 file). */
|
|
375
|
+
devicePublicKey: z.string()
|
|
376
|
+
});
|
|
377
|
+
var PairResponseSchema = z.object({
|
|
378
|
+
deviceId: z.string(),
|
|
379
|
+
/** JWT, ~1h lifetime. */
|
|
380
|
+
accessToken: z.string(),
|
|
381
|
+
/** Opaque hint for when/how to renew (e.g. an ISO timestamp); not itself a credential. */
|
|
382
|
+
refreshHint: z.string().optional()
|
|
383
|
+
});
|
|
384
|
+
var ChallengeRequestSchema = z.object({
|
|
385
|
+
deviceId: z.string()
|
|
386
|
+
});
|
|
387
|
+
var ChallengeResponseSchema = z.object({
|
|
388
|
+
/** One-time value the client must sign with its device private key. */
|
|
389
|
+
nonce: z.string()
|
|
390
|
+
});
|
|
391
|
+
var TokenRequestSchema = z.object({
|
|
392
|
+
deviceId: z.string(),
|
|
393
|
+
nonce: z.string(),
|
|
394
|
+
/** Ed25519 signature over `nonce`, base64url-encoded. */
|
|
395
|
+
signature: z.string()
|
|
396
|
+
});
|
|
397
|
+
var TokenResponseSchema = z.object({
|
|
398
|
+
accessToken: z.string(),
|
|
399
|
+
expiresAt: z.iso.datetime({ offset: true })
|
|
400
|
+
});
|
|
401
|
+
var CreateBlobRequestSchema = z.object({
|
|
402
|
+
size: z.number().int().nonnegative(),
|
|
403
|
+
contentType: z.string(),
|
|
404
|
+
contentHash: z.string().regex(CONTENT_HASH_RE, 'contentHash must be "sha256:<64 lowercase hex>"')
|
|
405
|
+
});
|
|
406
|
+
var CreateBlobResponseSchema = z.object({
|
|
407
|
+
blobId: z.string(),
|
|
408
|
+
uploadUrl: z.string()
|
|
409
|
+
});
|
|
410
|
+
var BlobDownloadUrlResponseSchema = z.object({
|
|
411
|
+
downloadUrl: z.string()
|
|
412
|
+
});
|
|
413
|
+
var EventsPollQuerySchema = z.object({
|
|
414
|
+
/** Last `seq` this client has seen; omitted on a client's first-ever poll. Never negative — `seq` is a monotonically increasing counter starting at 1. */
|
|
415
|
+
cursor: z.number().int().nonnegative().optional()
|
|
416
|
+
});
|
|
417
|
+
var EventsPollResponseSchema = z.object({
|
|
418
|
+
events: z.array(EnvelopeSchema),
|
|
419
|
+
cursor: z.number().int()
|
|
420
|
+
});
|
|
421
|
+
var MAX_MESSAGES_PER_BATCH = 256;
|
|
422
|
+
var MessagesSendRequestSchema = z.object({
|
|
423
|
+
messages: z.array(EnvelopeSchema).max(MAX_MESSAGES_PER_BATCH)
|
|
424
|
+
});
|
|
425
|
+
var MessagesSendResponseSchema = z.object({
|
|
426
|
+
accepted: z.number().int().nonnegative(),
|
|
427
|
+
rejected: z.number().int().nonnegative().optional()
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
export { AgentEventOrUnknownSchema, AgentEventSchema, BlobDownloadUrlResponseSchema, BlobRefSchema, CAPABILITY_FLAGS, CONTENT_HASH_RE, ChallengeRequestSchema, ChallengeResponseSchema, ConnAckPayloadSchema, ConnHelloPayloadSchema, CreateBlobRequestSchema, CreateBlobResponseSchema, DAEMON_TO_SERVER_TYPES, EnvelopeParseError, EnvelopeSchema, EnvelopeValidationError, EventsPollQuerySchema, EventsPollResponseSchema, KNOWN_AGENT_EVENT_TYPES, MAX_MESSAGES_PER_BATCH, MESSAGE_PAYLOAD_SCHEMAS, MESSAGE_TYPES, MessagesSendRequestSchema, MessagesSendResponseSchema, PERMISSION_MODES, PROTOCOL_VERSION, PairRequestSchema, PairResponseSchema, PermissionPolicySchema, ProtocolError, RuntimeCapabilitiesSchema, RuntimeIdSchema, RuntimeInfoSchema, SERVER_TO_DAEMON_TYPES, TASK_STATES, TASK_TRANSITIONS, TaskApprovalResolvedPayloadSchema, TaskApprovePayloadSchema, TaskArtifactPayloadSchema, TaskAwaitApprovalPayloadSchema, TaskCancelPayloadSchema, TaskCancelledPayloadSchema, TaskClaimPayloadSchema, TaskCompletePayloadSchema, TaskDeclinePayloadSchema, TaskFailPayloadSchema, TaskOfferPayloadSchema, TaskProgressPayloadSchema, TaskRejectPayloadSchema, TaskStartedPayloadSchema, TaskSteerPayloadSchema, TokenRequestSchema, TokenResponseSchema, UnknownAgentEventSchema, UnknownMessageTypeError, canTransition, createEnvelope, decodeEnvelope, encodeEnvelope, isKnownAgentEvent, isServerToDaemonType, parseMessage, partitionAgentEvents };
|
|
431
|
+
//# sourceMappingURL=index.js.map
|
|
432
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/version.ts","../src/blob.ts","../src/permission.ts","../src/agent-event.ts","../src/task-state.ts","../src/messages.ts","../src/envelope.ts","../src/errors.ts","../src/codec.ts","../src/http-api.ts"],"names":["z"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Wire protocol version. Bump on breaking (non-additive) changes to the envelope\n * or message shapes. Additive changes (new optional fields, new message types)\n * do not require a bump — servers negotiate the highest common version and\n * daemons/servers must ignore unknown fields and unknown message types.\n *\n * FROZEN v1 (end of M2 — see docs/protocol.md \"Freeze rule\"): the pi, claude,\n * and codex runtime adapters have all exercised the wire, and every M1/M2\n * protocol gap has been closed. `PROTOCOL_VERSION` stays `1` from here\n * forward; it does not bump for additive changes (new optional fields, new\n * message types, new `AgentEvent` variants, new capability flags) — only for\n * a breaking one (changing, removing, or retyping anything that already\n * exists).\n *\n * IMPORTANT: changing this constant, or changing/removing/retyping any\n * already-frozen schema in this package, requires a DELIBERATE update to the\n * committed golden fixtures in `src/__tests__/golden/` (`v1.frozen.json`,\n * `v1.envelopes.ndjson`) — see `src/__tests__/freeze-guard.test.ts`, which\n * fails loudly on exactly that kind of drift. A passing freeze-guard run\n * after such a change means either (a) the change was genuinely additive and\n * the golden was regenerated with justification, or (b) this constant was\n * bumped alongside a new golden generation for the new version — never a\n * silent edit to either file to make the test pass.\n */\nexport const PROTOCOL_VERSION = 1;\n\n/**\n * Capability flags exchanged during the connection handshake (`conn.hello` /\n * `conn.ack`). Additional flags may be introduced without a protocol version\n * bump; unrecognized flags must be ignored by both sides.\n *\n * `interactive-approval` is RESERVED as of this addition: it gates the\n * (currently unexercised) approval seam — a server must not route an\n * approval-requiring policy to a daemon that hasn't advertised this flag. No\n * bundled runtime adapter emits it yet; that's expected until interactive\n * approval is actually wired up in a later wave.\n *\n * `approval_resolved` (additive-minor): a SERVER-advertised flag meaning\n * \"I understand the `task.approval_resolved` message\" (`messages.ts`). This\n * is the N/N-1 answer for that new daemon -> server message: an old server's\n * `CAPABILITY_FLAGS`/`conn.ack.capabilities` never includes it, so a new\n * daemon talking to an old server never sends `task.approval_resolved` at\n * all (see `packages/client`'s `task-runner.ts`) and falls back to the\n * pre-existing implicit-resume inference\n * (`ConnectionHub.resumeIfImplicitlyApproved`, `packages/server/src/hub.ts`)\n * unconditionally, exactly as before this flag existed. Unlike\n * `interactive-approval`, this one IS exercised the moment both sides\n * support it — there is no reserved/dormant period for it.\n */\n/**\n * `approval-targeting` (M5, additive-minor): unlike `approval_resolved`\n * above, this flag is purely INFORMATIONAL/semantic, not a functional gate.\n * `task.await_approval`/`task.approve`/`task.reject` all carry their new\n * `approvalId` field UNCONDITIONALLY on both sides once each peer is\n * upgraded -- the wire is tolerant (a plain, non-`.strict()` `z.object()`\n * field, `messages.ts`), so no version/capability negotiation is needed just\n * to send it safely; an older peer that doesn't recognize the field simply\n * never reads it. Receivers decide whether to apply exact-match targeting\n * by FIELD PRESENCE on the specific message at hand (does this particular\n * `task.approve`/`task.reject`/`onApprovalResolved` payload carry an\n * `approvalId`, and does a stored one exist to compare it against?), never\n * by checking this flag -- see `ConnectionHub.approveTask`/`rejectTask`/\n * `onApprovalResolved` and `TaskRunner.handleApprove`/`handleReject`\n * (`packages/client`'s `task-runner.ts`). This flag exists only so each side\n * can advertise, and an embedder/operator can observe (`ConnectionHub.\n * getDeviceCapabilities`), whether the OTHER side is new enough to\n * participate in targeting at all -- the same N/N-1-safe shape as every\n * other flag here, just consumed for observability instead of gating.\n */\nexport const CAPABILITY_FLAGS = [\n 'steer',\n 'blob-upload',\n 'interactive-approval',\n 'approval_resolved',\n 'approval-targeting',\n] as const;\n\nexport type CapabilityFlag = (typeof CAPABILITY_FLAGS)[number];\n","import { z } from 'zod';\n\n/**\n * Canonical `contentHash` format (finding F9): `sha256:` followed by exactly\n * 64 lowercase hex characters (a SHA-256 digest). Pinned here — the single\n * source of truth both `BlobRefSchema` and `CreateBlobRequestSchema`\n * (`http-api.ts`) validate against — rather than left as a bare `z.string()`\n * that silently accepted any prefix (or none) and left the server to\n * reconcile the mismatch with an ad hoc normalization step. No compat shim:\n * the wire is pre-freeze (`v` stays `1`), so this is a straight tightening,\n * not a migration.\n */\nexport const CONTENT_HASH_RE = /^sha256:[0-9a-f]{64}$/;\n\n/**\n * Reference to a large payload that was pushed out-of-band (presigned PUT) or\n * is fetchable out-of-band (presigned GET), rather than inlined in an envelope.\n */\nexport const BlobRefSchema = z.object({\n blobId: z.string(),\n contentHash: z.string().regex(CONTENT_HASH_RE, 'contentHash must be \"sha256:<64 lowercase hex>\"'),\n size: z.number().int().nonnegative(),\n contentType: z.string(),\n url: z.string().optional(),\n});\n\nexport type BlobRef = z.infer<typeof BlobRefSchema>;\n","import { z } from 'zod';\n\nexport const PERMISSION_MODES = ['auto', 'confirm', 'readonly', 'plan'] as const;\n\nexport type PermissionMode = (typeof PERMISSION_MODES)[number];\n\n/**\n * Policy the server proposes for a task. The daemon/runtime adapter maps this\n * onto the concrete runtime's flags; anything that can't be expressed exactly\n * must fail closed (deny) rather than silently widen the grant.\n *\n * `.strict()`: this is control/security data, so per the freeze rule's\n * observability-vs-control asymmetry (docs/protocol.md \"Freeze rule\") an\n * unrecognized field must be REJECTED, not silently stripped-and-ignored the\n * way an ordinary payload's unknown field is (plain `z.object()`'s default\n * behavior). Without `.strict()`, a policy carrying a future constraint this\n * schema doesn't know about yet would parse successfully with that\n * constraint silently discarded — exactly the silent-widening failure mode\n * this type's own doc comment above warns against, since a stripped\n * constraint is indistinguishable from a constraint that was never sent.\n *\n * Consequence: adding a new field to this schema post-freeze is therefore a\n * BREAKING change requiring a `PROTOCOL_VERSION` bump — unlike the general\n * \"a new optional field on an existing payload is non-breaking\" rule the\n * freeze rule grants every other schema. That's intentional: a new\n * security/control constraint must force a conscious version bump so an\n * unupgraded peer can never silently ignore it, rather than being added the\n * same low-friction way a harmless observability field would be.\n */\nexport const PermissionPolicySchema = z\n .object({\n mode: z.enum(PERMISSION_MODES),\n allowTools: z.array(z.string()).optional(),\n denyTools: z.array(z.string()).optional(),\n workspaceRoot: z.string().optional(),\n network: z.boolean().optional(),\n })\n .strict();\n\nexport type PermissionPolicy = z.infer<typeof PermissionPolicySchema>;\n","import { z } from 'zod';\n\n/**\n * Normalized event shape that every runtime adapter (pi / claude / codex)\n * translates its native JSONL output into. This is the interior of a\n * `task.progress` payload's `events` array.\n */\nexport const AgentEventSchema = z.discriminatedUnion('type', [\n z.object({ type: z.literal('progress'), text: z.string() }),\n z.object({ type: z.literal('tool_use'), tool: z.string(), input: z.unknown().optional() }),\n z.object({ type: z.literal('tool_result'), tool: z.string(), output: z.unknown().optional() }),\n z.object({ type: z.literal('artifact'), name: z.string(), contentType: z.string() }),\n z.object({ type: z.literal('needs_approval'), summary: z.string() }),\n z.object({ type: z.literal('turn_end') }),\n z.object({ type: z.literal('error'), message: z.string() }),\n /**\n * Token usage reported by a runtime turn — maps from codex\n * `turn.completed.usage` and claude `result` usage (adapters wired up in a\n * later wave; this is just the variant + schema). All fields optional\n * because runtimes report different subsets.\n */\n z.object({\n type: z.literal('usage'),\n inputTokens: z.number().int().nonnegative().optional(),\n cachedInputTokens: z.number().int().nonnegative().optional(),\n outputTokens: z.number().int().nonnegative().optional(),\n reasoningTokens: z.number().int().nonnegative().optional(),\n totalTokens: z.number().int().nonnegative().optional(),\n }),\n]);\n\nexport type AgentEvent = z.infer<typeof AgentEventSchema>;\n\n/**\n * Known AgentEvent variant type discriminators — DERIVED directly from\n * {@link AgentEventSchema}'s own discriminated-union variants via\n * `z.toJSONSchema`, rather than hand-maintained as a second literal list.\n * This used to be a standalone array kept in sync with the schema above by\n * hand (dual authority); the freeze guard\n * (`__tests__/freeze-guard.test.ts`'s \"dual-authority cross-check\") already\n * asserted the two matched using this EXACT SAME `z.toJSONSchema` extraction\n * mechanism, which is why deriving it this way is safe: the guard already\n * proved this extraction produces the identical set the hand-written list\n * held. With the derivation below, the two can no longer drift apart at\n * all — there is only one authority now, {@link AgentEventSchema} itself.\n * The freeze guard test is kept anyway (now definitionally true rather than\n * a live check) as a regression net in case a future refactor reintroduces a\n * hand-written list.\n *\n * Exported (not module-private) so {@link isKnownAgentEvent} /\n * {@link partitionAgentEvents} (and the freeze guard) can check against the\n * exact same set without each reaching into zod's discriminated-union\n * internals directly. `z.toJSONSchema`'s output shape here (`.oneOf[].\n * properties.type.const`) is a public, documented zod v4 API — not\n * reaching into `._def`/internal fields — same as the freeze guard already\n * relies on.\n */\nexport const KNOWN_AGENT_EVENT_TYPES: readonly string[] = (\n z.toJSONSchema(AgentEventSchema) as unknown as { oneOf: Array<{ properties: { type: { const: string } } }> }\n).oneOf.map((branch) => branch.properties.type.const);\n\n/**\n * Pre-freeze compatibility widening (the freeze blocker this schema fixes):\n * an unknown-type event — one a future runtime/protocol minor version\n * introduces — parses as an opaque passthrough placeholder instead of\n * hard-failing the entire `task.progress` batch it arrived in. Without this,\n * `TaskProgressPayloadSchema.events: z.array(AgentEventSchema)` would throw\n * on the whole array the moment one event had an unrecognized `type`, which\n * made the wire's \"additive new variants are non-breaking\" promise false for\n * the installed base — and unfixable post-freeze.\n *\n * The `.refine` guard is load-bearing, not decorative: it excludes every\n * KNOWN type literal, so a *malformed* known variant (e.g. `progress`\n * missing `text`) still fails validation instead of silently matching this\n * fallback. Tolerance is only for unknown TYPES, never for malformed known\n * ones — see {@link AgentEventOrUnknownSchema}, which is what actually\n * combines this with {@link AgentEventSchema} for real use.\n *\n * Deliberately asymmetric with envelope-level control/security fields\n * (`instruction`, `policy` — see `messages.ts`/`permission.ts`), which stay\n * fail-closed on unknown shapes with no equivalent widening: this tolerance\n * applies only to observability data (agent progress events), never to\n * control/security surfaces. That asymmetry is the freeze rule.\n */\nexport const UnknownAgentEventSchema = z\n .object({ type: z.string() })\n .passthrough()\n .refine(\n (event) => !KNOWN_AGENT_EVENT_TYPES.includes(event.type),\n 'type refers to a known AgentEvent variant; malformed known variants must fail through AgentEventSchema, not fall back to unknown-tolerance',\n );\n\nexport type UnknownAgentEvent = z.infer<typeof UnknownAgentEventSchema>;\n\n/**\n * The actual element schema for `TaskProgressPayloadSchema.events`\n * (`messages.ts`): a known, fully-typed {@link AgentEvent} OR an opaque\n * unknown-type placeholder. `z.union` (not `discriminatedUnion`) is required\n * here because the fallback branch matches on \"not one of the known\n * literals\", which a discriminated union can't express directly.\n */\nexport const AgentEventOrUnknownSchema = z.union([AgentEventSchema, UnknownAgentEventSchema]);\n\nexport type AgentEventOrUnknown = z.infer<typeof AgentEventOrUnknownSchema>;\n\n/**\n * Type guard distinguishing a known, fully-typed {@link AgentEvent} from an\n * {@link UnknownAgentEvent} passthrough placeholder.\n */\nexport function isKnownAgentEvent(event: AgentEventOrUnknown): event is AgentEvent {\n return KNOWN_AGENT_EVENT_TYPES.includes(event.type);\n}\n\n/**\n * Split a `task.progress` events array into known (typed, actionable) and\n * unknown (opaque, safe-to-skip) events. Consumers should process `known`\n * and skip `unknown` rather than throwing on it — that's the point of the\n * pre-freeze tolerance above.\n */\nexport function partitionAgentEvents(events: readonly AgentEventOrUnknown[]): {\n known: AgentEvent[];\n unknown: UnknownAgentEvent[];\n} {\n const known: AgentEvent[] = [];\n const unknown: UnknownAgentEvent[] = [];\n for (const event of events) {\n if (isKnownAgentEvent(event)) {\n known.push(event);\n } else {\n unknown.push(event);\n }\n }\n return { known, unknown };\n}\n","export const TASK_STATES = [\n 'Offered',\n 'Claimed',\n 'Running',\n 'AwaitApproval',\n 'Complete',\n 'Failed',\n 'Cancelled',\n] as const;\n\nexport type TaskState = (typeof TASK_STATES)[number];\n\n/**\n * Legal state transitions for a task. `Complete` / `Failed` / `Cancelled` are\n * terminal (no outgoing edges). `Running` and `AwaitApproval` form a loop:\n * the daemon can request approval mid-run and resume once the server\n * approves (or fail/cancel out of the approval wait).\n *\n * `Offered -> Failed` (M1 gap #5, \"Declined vs. Failed\"): a daemon that\n * declines an offer pre-claim (`task.decline`) reports it through the\n * existing `Failed` state rather than a new `Declined` state. A decline and\n * a post-claim failure are the same outcome from the dispatcher's point of\n * view — this attempt produced no result, `reason`/`retryable` say why and\n * whether retrying elsewhere makes sense — so reusing `Failed` keeps the\n * state machine minimal instead of forking every terminal-state consumer\n * into \"Failed or Declined, handle both\". See docs/protocol.md for the full\n * writeup.\n */\nexport const TASK_TRANSITIONS: Readonly<Record<TaskState, readonly TaskState[]>> = {\n Offered: ['Claimed', 'Cancelled', 'Failed'],\n Claimed: ['Running', 'Failed', 'Cancelled'],\n Running: ['AwaitApproval', 'Complete', 'Failed', 'Cancelled'],\n AwaitApproval: ['Running', 'Failed', 'Cancelled'],\n Complete: [],\n Failed: [],\n Cancelled: [],\n};\n\n/** Whether `from -> to` is a legal transition per {@link TASK_TRANSITIONS}. */\nexport function canTransition(from: TaskState, to: TaskState): boolean {\n return TASK_TRANSITIONS[from].includes(to);\n}\n","import { z } from 'zod';\nimport { BlobRefSchema } from './blob';\nimport { PermissionPolicySchema } from './permission';\nimport { AgentEventOrUnknownSchema } from './agent-event';\n\n/** Max size of an inlined artifact payload, per the delivery-model spec (<=64KB). */\nconst MAX_INLINE_BYTES = 64 * 1024;\n\nfunction isWithinInlineByteLimit(value: string): boolean {\n return new TextEncoder().encode(value).length <= MAX_INLINE_BYTES;\n}\n\n// ---------------------------------------------------------------------------\n// conn.* — connection handshake\n// ---------------------------------------------------------------------------\n\nexport const RuntimeIdSchema = z.enum(['pi', 'claude', 'codex']);\nexport type RuntimeId = z.infer<typeof RuntimeIdSchema>;\n\n/**\n * Per-runtime feature flags reported in `conn.hello.runtimes[].capabilities`\n * (pre-freeze addition). Distinct from the connection-level `CAPABILITY_FLAGS`\n * (`version.ts`) / `conn.hello.capabilities` array: those are protocol-level\n * flags negotiated for the whole connection, while this is what one specific\n * detected runtime (pi/claude/codex) supports. The whole field is optional\n * end-to-end — older daemons omit `capabilities` entirely — and every field\n * inside it is itself optional, since detection can be partial.\n *\n * Per-tool allow/deny lists are deliberately NOT included here (noise).\n * `permissionModes` mirrors `PERMISSION_MODES` (`permission.ts`) but is kept\n * as a bare `string[]` rather than `z.enum(PERMISSION_MODES)`: this is a\n * runtime's self-reported observability data, not a control/security field,\n * so — per the freeze rule (tolerate unknown for observability, fail closed\n * for control/security; see `agent-event.ts`'s unknown-variant tolerance for\n * the same asymmetry applied to `task.progress` events) — it stays tolerant\n * of a mode string a newer runtime might report that this schema doesn't\n * enumerate yet, rather than rejecting the whole `conn.hello`.\n *\n * Unrecognized keys inside `capabilities` itself, by contrast, are silently\n * stripped (zod's default object behavior — same as every other payload\n * schema in this file) rather than passed through: this is a closed, typed\n * shape consumers can rely on, and a genuinely new capability flag gets added\n * here explicitly rather than round-tripped opaquely.\n */\nexport const RuntimeCapabilitiesSchema = z.object({\n steer: z.boolean().optional(),\n resume: z.boolean().optional(),\n approvalInteractive: z.boolean().optional(),\n permissionModes: z.array(z.string()).optional(),\n});\nexport type RuntimeCapabilities = z.infer<typeof RuntimeCapabilitiesSchema>;\n\n/**\n * Runtime detection info reported in `conn.hello`. Supersedes the M0\n * `agents: unknown` field (M1 gap #4): typed, so the server no longer has to\n * best-effort-normalize an untyped blob.\n */\nexport const RuntimeInfoSchema = z.object({\n id: RuntimeIdSchema,\n version: z.string().optional(),\n authPresent: z.boolean().optional(),\n /** Optional: older daemons omit this entirely (pre-freeze addition). */\n capabilities: RuntimeCapabilitiesSchema.optional(),\n});\nexport type RuntimeInfo = z.infer<typeof RuntimeInfoSchema>;\n\n/** daemon -> server: opening handshake. */\nexport const ConnHelloPayloadSchema = z.object({\n protocolVersions: z.array(z.number().int()),\n capabilities: z.array(z.string()),\n deviceId: z.string(),\n productId: z.string(),\n /** Runtimes detected on this device (M1 gap #4; replaces `agents`). */\n runtimes: z.array(RuntimeInfoSchema).optional(),\n /**\n * Last `seq` this device has seen from the server (M1 redelivery cursor).\n * Omitted on a device's first-ever connection. The server replays any\n * server->daemon envelopes with `seq > cursor` it still holds — see\n * docs/protocol.md \"At-least-once delivery\".\n */\n cursor: z.number().int().optional(),\n});\nexport type ConnHelloPayload = z.infer<typeof ConnHelloPayloadSchema>;\n\n/** server -> daemon: handshake acknowledgement. */\nexport const ConnAckPayloadSchema = z.object({\n protocolVersion: z.number().int(),\n capabilities: z.array(z.string()),\n serverTime: z.iso.datetime({ offset: true }),\n});\nexport type ConnAckPayload = z.infer<typeof ConnAckPayloadSchema>;\n\n// ---------------------------------------------------------------------------\n// server -> daemon: task.*\n//\n// Every type in this section carries a *required* envelope `task_id` (M1 gap\n// #1) and a *required* envelope `seq` — a per-device monotonic counter the\n// daemon uses as a redelivery cursor (M1 gap; see `conn.hello.cursor` above\n// and docs/protocol.md \"At-least-once delivery\"). None of these payloads\n// duplicate `taskId` at the payload level (M1 gap #7): the envelope's\n// `task_id` is the single routing key.\n// ---------------------------------------------------------------------------\n\n/**\n * The out-of-band-reference form of `TaskOfferPayload.instruction` (the\n * alternative to an inlined string). `.strict()`: like `PermissionPolicySchema`\n * (`permission.ts`), this is control data — it's the task instruction itself,\n * the thing that authorizes what work gets done — so per the freeze rule's\n * observability-vs-control asymmetry (docs/protocol.md \"Freeze rule\") an\n * unrecognized field here must be REJECTED, not silently stripped the way an\n * ordinary payload's unknown field is. Without `.strict()`, a hypothetical\n * future field riding along next to `blobRef` (e.g. a routing/priority\n * override) would be silently discarded instead of failing the parse —\n * exactly the kind of silent reinterpretation the freeze rule forbids for\n * control/security payloads. Consequence: adding a field to this shape\n * post-freeze is a BREAKING change (version bump required), not the usual\n * non-breaking additive-optional-field case — same as `PermissionPolicySchema`.\n */\nconst InstructionBlobRefSchema = z.object({ blobRef: BlobRefSchema }).strict();\n\n/**\n * server -> daemon: offer a task for a device to claim.\n *\n * `taskId` used to be duplicated here; it is now carried only by the\n * envelope's `task_id` (M1 gap #7 — single source of truth for routing).\n */\nexport const TaskOfferPayloadSchema = z.object({\n instruction: z.union([z.string(), InstructionBlobRefSchema]),\n policy: PermissionPolicySchema,\n runtime: RuntimeIdSchema.optional(),\n sessionRef: z.string().optional(),\n workspaceHint: z.string().optional(),\n limits: z\n .object({\n maxDurationMs: z.number().int().positive().optional(),\n maxTokens: z.number().int().positive().optional(),\n })\n .optional(),\n});\nexport type TaskOfferPayload = z.infer<typeof TaskOfferPayloadSchema>;\n\n/**\n * server -> daemon: approve a pending `task.await_approval` request.\n *\n * Semantics (M1 gap #3): the server's own state is authoritative on its own\n * action — calling the server-side `approve()` API moves the task record\n * `AwaitApproval -> Running` immediately. This wire message is a best-effort\n * *notification* telling the daemon to resume the paused runtime session; the\n * daemon does not send a dedicated ack. Its outcome is observable through the\n * task's existing message stream (e.g. `task.progress` resuming, or\n * `task.fail`/`task.cancelled` if resuming turns out to be impossible) — no\n * new ack message type is introduced. See docs/protocol.md \"Approval flow\".\n *\n * `approvalId` (M5, additive-minor — docs/protocol.md §5.3): OPTIONAL target\n * identity for the SPECIFIC pending approval this decision resolves, rather\n * than \"whichever one is currently pending\" (the pre-M5 behavior, and still\n * what happens when this field is absent — a legacy server that never\n * learned an id, or one talking to a legacy daemon). When present, the\n * daemon compares it against its own currently-dispatched approval id\n * (`ActiveTask.pendingApprovalId`, `packages/client`'s `task-runner.ts`) and\n * treats a mismatch as a stale, audit-only no-op instead of resolving\n * whatever happens to be pending right now — see `TaskRunner.handleApprove`.\n */\nexport const TaskApprovePayloadSchema = z.object({\n approvalId: z.string().optional(),\n});\nexport type TaskApprovePayload = z.infer<typeof TaskApprovePayloadSchema>;\n\n/**\n * server -> daemon: reject a pending `task.await_approval` request.\n *\n * Same best-effort-notification semantics as `task.approve` (M1 gap #3): the\n * server moves its own record `AwaitApproval -> Failed` immediately; this\n * message just tells the daemon to stop, and the daemon reports the outcome\n * via its existing `task.fail` terminal message.\n *\n * `approvalId` (M5, additive-minor — docs/protocol.md §5.3): same optional\n * targeting semantics as `TaskApprovePayloadSchema.approvalId` above, applied\n * to the reject path (`TaskRunner.handleReject`).\n */\nexport const TaskRejectPayloadSchema = z.object({\n reason: z.string().optional(),\n approvalId: z.string().optional(),\n});\nexport type TaskRejectPayload = z.infer<typeof TaskRejectPayloadSchema>;\n\n/**\n * server -> daemon: cancel a task in any non-terminal state.\n *\n * Same best-effort-notification semantics (M1 gap #3): the server moves its\n * own record to `Cancelled` immediately on its own action and does not wait\n * for a daemon ack; this message just tells the daemon to stop local work.\n * The daemon reports the outcome via the explicit `task.cancelled` terminal\n * message (M1 gap #6) — not `task.fail`.\n */\nexport const TaskCancelPayloadSchema = z.object({\n reason: z.string().optional(),\n});\nexport type TaskCancelPayload = z.infer<typeof TaskCancelPayloadSchema>;\n\n/** server -> daemon: inject steering text into a running task. */\nexport const TaskSteerPayloadSchema = z.object({\n text: z.string(),\n});\nexport type TaskSteerPayload = z.infer<typeof TaskSteerPayloadSchema>;\n\n// ---------------------------------------------------------------------------\n// daemon -> server: task.*\n//\n// Every type in this section carries a *required* envelope `task_id` (M1 gap\n// #1 — these all route by task id). Envelope `seq` (the redelivery cursor)\n// stays optional in this direction: M1 only specifies at-least-once\n// server->daemon redelivery, not a daemon->server one (see\n// docs/protocol.md).\n// ---------------------------------------------------------------------------\n\n/**\n * daemon -> server: claim an offered task (idempotent CAS on the server\n * side). `taskId` used to be duplicated here; it is now carried only by the\n * envelope's `task_id` (M1 gap #7).\n *\n * Claiming no longer implies the task is `Running` (M1 gap #2) — see\n * `task.started`.\n *\n * `runtime` (M5, additive-minor — docs/protocol.md §3.1): the ACTUAL\n * adapter this device selected for the task, distinct from `task.offer`'s\n * own `runtime` (the merely REQUESTED one, `TaskOfferPayloadSchema.runtime`\n * above). When an offer names no runtime the daemon auto-selects (pi-first —\n * `TaskRunner.pickAdapter`, `packages/client`'s `task-runner.ts`), and before\n * this field existed the server had no way to learn which adapter actually\n * ran — `TaskSnapshot.runtime` (`packages/server`'s `types.ts`) only ever\n * recorded what was requested. Plain optional property on this already-\n * tolerant `z.object()`: an old server simply never reads it, so this needed\n * no version bump and no emission gating (same shape as `approvalId` on\n * `task.await_approval`/`task.approve`/`task.reject`, §5.3) — a new daemon\n * sends it unconditionally, regardless of whether the connected server is\n * new enough to store it.\n *\n * `capabilities` (S0/D-4, additive-minor — docs/protocol.md §2.4): the\n * TASK-level capability authority — what the claiming adapter reported about\n * itself at the moment it took this task, reusing `RuntimeCapabilitiesSchema`\n * (above) rather than introducing a second capability shape. Same source of\n * truth as `conn.hello.runtimes[].capabilities`, different scope: `conn.hello`\n * is CONNECTION-level discovery (\"what could this device run\"), which no\n * server-side control decision may read, because it is transport-shaped — a\n * long-poll-only daemon never sends `conn.hello` at all — and it describes a\n * device, not a task. This field is task-shaped: it shares a lifecycle with\n * the task↔runtime binding the claim itself establishes, so a control gate\n * (`steerTask()`, `packages/server`'s `hub.ts`) can key off it and stay\n * correct across reconnects, adapter-set changes, and transports. Plain\n * optional property on this already-tolerant `z.object()`, exactly like\n * `runtime` above: an old daemon simply omits it, and a consumer that gates on\n * it must fail closed on absence rather than assume a default.\n */\nexport const TaskClaimPayloadSchema = z.object({\n deviceId: z.string(),\n agentId: z.string().optional(),\n runtime: RuntimeIdSchema.optional(),\n capabilities: RuntimeCapabilitiesSchema.optional(),\n});\nexport type TaskClaimPayload = z.infer<typeof TaskClaimPayloadSchema>;\n\n/**\n * daemon -> server: explicit `Claimed -> Running` transition (M1 gap #2).\n * A `task.claim` no longer implies the task started running; the daemon\n * sends this once it has actually started the runtime session for the task.\n */\nexport const TaskStartedPayloadSchema = z.object({});\nexport type TaskStartedPayload = z.infer<typeof TaskStartedPayloadSchema>;\n\n/**\n * daemon -> server: decline an offer *before* claiming it (M1 gap #5) — e.g.\n * no compatible/available runtime, or the offered policy exceeds this\n * device's ceiling. Fail-closed rejections must use this instead of silently\n * dropping the offer.\n *\n * Decision (see docs/protocol.md \"Declined vs. Failed\" for the full\n * writeup): declining does *not* introduce a new `Declined` terminal state.\n * It maps onto the existing `Failed` state via a new `Offered -> Failed`\n * transition. `reason`/`retryable` intentionally mirror `TaskFailPayload`\n * exactly, because a pre-claim decline and a post-claim failure are the same\n * outcome from the dispatcher's point of view (this attempt produced no\n * result; here's whether retrying — e.g. offering to a different device —\n * makes sense), and keeping the state machine minimal avoids forking every\n * terminal-state consumer into \"Failed or Declined, handle both\".\n */\nexport const TaskDeclinePayloadSchema = z.object({\n reason: z.string(),\n retryable: z.boolean().optional(),\n});\nexport type TaskDeclinePayload = z.infer<typeof TaskDeclinePayloadSchema>;\n\n/**\n * daemon -> server: batch of normalized agent events.\n *\n * `events` elements are known-or-unknown (`AgentEventOrUnknownSchema` —\n * `agent-event.ts`), not bare `AgentEventSchema`: pre-freeze, an unrecognized\n * event `type` must not fail the whole batch, since a peer running a newer\n * minor version may have emitted an additive event variant this schema\n * doesn't know about yet. See `agent-event.ts` for the full rationale and\n * `partitionAgentEvents`/`isKnownAgentEvent` for how consumers should skip\n * unknowns instead of choking on them.\n */\nexport const TaskProgressPayloadSchema = z.object({\n seq: z.number().int(),\n events: z.array(AgentEventOrUnknownSchema),\n});\nexport type TaskProgressPayload = z.infer<typeof TaskProgressPayloadSchema>;\n\n/** daemon -> server: an artifact produced by the task, inline or by blob ref. */\nexport const TaskArtifactPayloadSchema = z.object({\n name: z.string(),\n contentType: z.string(),\n inline: z\n .string()\n .refine(isWithinInlineByteLimit, {\n message: 'inline artifact payload exceeds 64KB limit',\n })\n .optional(),\n blobRef: BlobRefSchema.optional(),\n});\nexport type TaskArtifactPayload = z.infer<typeof TaskArtifactPayloadSchema>;\n\n/**\n * daemon -> server: task is blocked on an out-of-band approval.\n *\n * `approvalId` (M5, additive-minor — docs/protocol.md §5.3): the daemon's own\n * locally-generated identity for THIS SPECIFIC pending approval\n * (`ApprovalRegistry`, `packages/client`'s `approvals.ts`) — included\n * unconditionally by an M5+ daemon, regardless of whether the connected\n * server has advertised the `approval-targeting` capability flag\n * (`version.ts`; see that flag's own doc comment for why no emission gating\n * is needed here — it's a tolerant `z.object()` field, so an older server\n * simply ignores it). Optional purely for wire tolerance with a pre-M5\n * daemon build that never set it at all: a server that never learns an id\n * for a task's current approval can't target a later `approve`/`reject`\n * decision and falls back to resolving \"whichever approval is currently\n * pending\" — the same behavior every server had before this field existed.\n */\nexport const TaskAwaitApprovalPayloadSchema = z.object({\n summary: z.string(),\n approvalId: z.string().optional(),\n});\nexport type TaskAwaitApprovalPayload = z.infer<typeof TaskAwaitApprovalPayloadSchema>;\n\n/** daemon -> server: task finished successfully. */\nexport const TaskCompletePayloadSchema = z.object({\n summary: z.string(),\n sessionRef: z.string(),\n artifactRefs: z.array(BlobRefSchema).optional(),\n});\nexport type TaskCompletePayload = z.infer<typeof TaskCompletePayloadSchema>;\n\n/** daemon -> server: task failed. */\nexport const TaskFailPayloadSchema = z.object({\n reason: z.string(),\n retryable: z.boolean().optional(),\n});\nexport type TaskFailPayload = z.infer<typeof TaskFailPayloadSchema>;\n\n/**\n * daemon -> server: task ended in the `Cancelled` state (M1 gap #6) — either\n * in response to a server-sent `task.cancel`, or a cancellation the daemon\n * observed/decided locally (e.g. a local stop action) that the server didn't\n * initiate. This is the canonical way to report a `Cancelled` outcome; it\n * supersedes the M0 convention of `task.fail({ reason: 'cancelled' })`.\n *\n * This is deliberately its own message rather than folded into `task.fail`\n * (decision: prefer the explicit message — see docs/protocol.md) because\n * `Cancelled` is semantically distinct from `Failed`: one is an intentional\n * stop, the other an error. Overloading `task.fail` with a magic\n * `reason: 'cancelled'` string convention hid that distinction on the wire.\n *\n * Dual-purpose on receipt: if the server already moved its own record to\n * `Cancelled` (it initiated the cancel — M1 gap #3's \"server state is\n * authoritative\" rule), this is an idempotent no-op ack. If the server\n * hasn't yet (a locally-observed cancellation), this is the authoritative\n * trigger that moves `Claimed`/`Running`/`AwaitApproval -> Cancelled`.\n */\nexport const TaskCancelledPayloadSchema = z.object({\n reason: z.string().optional(),\n});\nexport type TaskCancelledPayload = z.infer<typeof TaskCancelledPayloadSchema>;\n\n/**\n * daemon -> server: a pending `task.await_approval` was resolved entirely\n * LOCALLY on the device — the local control-socket `approvals.resolve` RPC,\n * a fail-closed `requestApproval` timeout, or a fail-closed eviction/finish\n * rejection (see `packages/client`'s `task-runner.ts`/`approvals.ts`) —\n * *without* a wire `task.approve`/`task.reject` ever having been exchanged\n * for it. This is the additive-minor answer to a gap the M4 Phase 3 approval\n * work left open (see the \"Deferred additive candidate\" note this schema\n * resolves, `docs/protocol.md`): today the server only learns of a local\n * resolution IMPLICITLY, after the fact, once the daemon's next\n * `task.progress`/`task.artifact`/`task.complete` proves the task already\n * moved on (`ConnectionHub.resumeIfImplicitlyApproved`,\n * `packages/server/src/hub.ts`) — a window in which a SaaS-side\n * `TaskHandle.approve()`/`.reject()` can independently decide (and win) the\n * server's own authoritative record before that evidence ever arrives. This\n * message lets the daemon report the local resolution explicitly and\n * immediately, narrowing that window from \"until the next progress message\"\n * down to ordinary network latency; the implicit-inference path stays as-is,\n * unconditionally, as the compatibility fallback for an old server that\n * never advertises the `approval_resolved` capability flag (`version.ts`) or\n * an old daemon that predates this message entirely.\n *\n * Observability-class tolerance applies (not control/security — see the\n * freeze rule's asymmetry, `docs/protocol.md`): this is a daemon reporting\n * what it already did locally, not a payload that grants/denies anything on\n * its own — the receiving server's own state machine (`TASK_TRANSITIONS`,\n * `task-state.ts`) is still what decides whether the reported resolution is\n * legal to apply. Plain `z.object()` (not `.strict()`), same as every other\n * non-control payload in this file.\n *\n * `resolvedBy` is a single-value enum (`'local'`) rather than a bare string:\n * deliberately future-proof (a later wave could add e.g. `'operator-cli'` as\n * a DISTINCT value without a version bump — a new enum member is additive,\n * same as a new message type or capability flag), while still being a closed,\n * typed shape today rather than an open string a typo could silently widen.\n */\nexport const TaskApprovalResolvedPayloadSchema = z.object({\n approvalId: z.string(),\n decision: z.enum(['approve', 'reject']),\n resolvedBy: z.enum(['local']),\n at: z.iso.datetime({ offset: true }),\n});\nexport type TaskApprovalResolvedPayload = z.infer<typeof TaskApprovalResolvedPayloadSchema>;\n\n// ---------------------------------------------------------------------------\n// registry — single source of truth mapping message type -> payload schema\n// ---------------------------------------------------------------------------\n\nexport const MESSAGE_PAYLOAD_SCHEMAS = {\n 'conn.hello': ConnHelloPayloadSchema,\n 'conn.ack': ConnAckPayloadSchema,\n 'task.offer': TaskOfferPayloadSchema,\n 'task.approve': TaskApprovePayloadSchema,\n 'task.reject': TaskRejectPayloadSchema,\n 'task.cancel': TaskCancelPayloadSchema,\n 'task.steer': TaskSteerPayloadSchema,\n 'task.claim': TaskClaimPayloadSchema,\n 'task.started': TaskStartedPayloadSchema,\n 'task.decline': TaskDeclinePayloadSchema,\n 'task.progress': TaskProgressPayloadSchema,\n 'task.artifact': TaskArtifactPayloadSchema,\n 'task.await_approval': TaskAwaitApprovalPayloadSchema,\n 'task.complete': TaskCompletePayloadSchema,\n 'task.fail': TaskFailPayloadSchema,\n 'task.cancelled': TaskCancelledPayloadSchema,\n 'task.approval_resolved': TaskApprovalResolvedPayloadSchema,\n} as const;\n\nexport type MessageType = keyof typeof MESSAGE_PAYLOAD_SCHEMAS;\n\nexport const MESSAGE_TYPES = Object.keys(MESSAGE_PAYLOAD_SCHEMAS) as MessageType[];\n\n/**\n * Message types the server sends to the daemon. Used by {@link EnvelopeSchema}\n * (`envelope.ts`) to decide which branches require envelope `seq` (M1\n * redelivery cursor).\n */\nexport const SERVER_TO_DAEMON_TYPES = [\n 'conn.ack',\n 'task.offer',\n 'task.approve',\n 'task.reject',\n 'task.cancel',\n 'task.steer',\n] as const satisfies readonly MessageType[];\n\n/**\n * Message types the daemon sends to the server — the flip side of\n * {@link SERVER_TO_DAEMON_TYPES}. `conn.hello` is deliberately excluded: it's\n * only ever valid as the first frame of a WS handshake (`ws-server.ts`), not\n * as ongoing inbound traffic through `ConnectionHub.handleInbound`.\n *\n * Used by `handleInbound` (`@byok-sdk/server`'s `hub.ts`) as the type-allow gate\n * for every inbound envelope, WS and `POST /byok/messages` alike (finding\n * P2): a `type` outside this set — a server -> daemon type arriving inbound,\n * or anything unrecognized — is rejected before it's dispatched to any\n * handler or counted `accepted` on the `/byok/messages` wire.\n */\nexport const DAEMON_TO_SERVER_TYPES = [\n 'task.claim',\n 'task.started',\n 'task.decline',\n 'task.progress',\n 'task.artifact',\n 'task.await_approval',\n 'task.complete',\n 'task.fail',\n 'task.cancelled',\n 'task.approval_resolved',\n] as const satisfies readonly MessageType[];\n","import { z } from 'zod';\nimport { MESSAGE_PAYLOAD_SCHEMAS, SERVER_TO_DAEMON_TYPES, type MessageType } from './messages';\n\nconst REQUIRED_TASK_ID = z.string().min(1);\nconst OPTIONAL_TASK_ID = REQUIRED_TASK_ID.optional();\nconst REQUIRED_SEQ = z.number().int();\nconst OPTIONAL_SEQ = REQUIRED_SEQ.optional();\n\n/**\n * Build the full envelope schema for one message type. Field order here\n * matches the spec's documented shape (`v, id, ts, type, task_id?,\n * session_ref?, seq?, payload`) so parsed output has predictable key order.\n *\n * `taskId`/`seq` are passed in as concrete schemas (not a boolean flag)\n * so each call site's return type is precisely the required-or-optional\n * shape for that branch — `z.infer<typeof EnvelopeSchema>` then reflects the\n * per-message-type requiredness rather than collapsing it to `string |\n * undefined` for every branch.\n */\nfunction envelopeShape<\n T extends MessageType,\n TaskId extends z.ZodTypeAny,\n Seq extends z.ZodTypeAny,\n>(type: T, taskId: TaskId, seq: Seq) {\n return z.object({\n v: z.number().int(),\n id: z.uuid(),\n ts: z.iso.datetime({ offset: true }),\n type: z.literal(type),\n task_id: taskId,\n session_ref: z.string().optional(),\n seq,\n payload: MESSAGE_PAYLOAD_SCHEMAS[type],\n });\n}\n\n/**\n * The wire envelope: common transport fields plus a `payload` whose shape is\n * determined by `type`. Unknown top-level fields are tolerated (stripped) for\n * forward-compat; unknown `type` values do not match any branch below and are\n * handled explicitly by {@link parseMessage} in `codec.ts`.\n *\n * Two cross-cutting requiredness rules, fixed at M1 (see docs/protocol.md\n * \"M0 -> M1 breaking changes\"):\n *\n * - `task_id` is REQUIRED for every `task.*` type (they all route by task id)\n * and stays optional for `conn.*` (M1 gap #1).\n * - `seq` is REQUIRED for every type the *server* sends to the daemon — a\n * per-device monotonic counter used as a redelivery cursor — and stays\n * optional for daemon -> server types (M1 redelivery cursor; see\n * `conn.hello.cursor` in `messages.ts`).\n */\nexport const EnvelopeSchema = z.discriminatedUnion('type', [\n // conn.* — task_id stays optional (no task routing). conn.hello is\n // daemon -> server (no cursor to satisfy on this leg); conn.ack is\n // server -> daemon and therefore carries the required redelivery `seq`.\n envelopeShape('conn.hello', OPTIONAL_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('conn.ack', OPTIONAL_TASK_ID, REQUIRED_SEQ),\n\n // task.* server -> daemon: task_id required (routing key) + seq required\n // (per-device redelivery cursor).\n envelopeShape('task.offer', REQUIRED_TASK_ID, REQUIRED_SEQ),\n envelopeShape('task.approve', REQUIRED_TASK_ID, REQUIRED_SEQ),\n envelopeShape('task.reject', REQUIRED_TASK_ID, REQUIRED_SEQ),\n envelopeShape('task.cancel', REQUIRED_TASK_ID, REQUIRED_SEQ),\n envelopeShape('task.steer', REQUIRED_TASK_ID, REQUIRED_SEQ),\n\n // task.* daemon -> server: task_id required (routing key); envelope-level\n // seq is not required in this direction in M1 (no daemon->server\n // redelivery cursor yet — see docs/protocol.md).\n envelopeShape('task.claim', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.started', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.decline', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.progress', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.artifact', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.await_approval', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.complete', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.fail', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.cancelled', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n envelopeShape('task.approval_resolved', REQUIRED_TASK_ID, OPTIONAL_SEQ),\n]);\n\nexport type Envelope = z.infer<typeof EnvelopeSchema>;\n\n/** `true` for every message type the server sends to the daemon (envelope `seq` is required for these). */\nexport function isServerToDaemonType(type: MessageType): boolean {\n return (SERVER_TO_DAEMON_TYPES as readonly MessageType[]).includes(type);\n}\n","import type { ZodError } from 'zod';\n\n/** Base class for all protocol decode/validation errors. */\nexport class ProtocolError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ProtocolError';\n }\n}\n\n/** The input was not valid JSON at all (only thrown by `decodeEnvelope`). */\nexport class EnvelopeParseError extends ProtocolError {\n constructor(message: string, cause?: unknown) {\n super(message, { cause });\n this.name = 'EnvelopeParseError';\n }\n}\n\n/**\n * The `type` field did not match any known message type. This is distinct\n * from {@link EnvelopeValidationError} on purpose: a daemon/server on an\n * older minor version should catch this specifically and skip the message\n * instead of treating it as a bug, since a newer peer may have introduced an\n * additive message type it doesn't understand yet.\n */\nexport class UnknownMessageTypeError extends ProtocolError {\n public readonly type: unknown;\n\n constructor(type: unknown) {\n super(`Unknown message type: ${String(type)}`);\n this.name = 'UnknownMessageTypeError';\n this.type = type;\n }\n}\n\n/** The `type` field was recognized but the envelope/payload failed schema validation. */\nexport class EnvelopeValidationError extends ProtocolError {\n public readonly issues: ZodError;\n\n constructor(message: string, issues: ZodError) {\n super(message, { cause: issues });\n this.name = 'EnvelopeValidationError';\n this.issues = issues;\n }\n}\n","import type { z } from 'zod';\nimport { EnvelopeSchema, type Envelope } from './envelope';\nimport { MESSAGE_TYPES, MESSAGE_PAYLOAD_SCHEMAS, type MessageType } from './messages';\nimport { PROTOCOL_VERSION } from './version';\nimport { EnvelopeParseError, EnvelopeValidationError, UnknownMessageTypeError } from './errors';\n\nconst MESSAGE_TYPE_SET = new Set<string>(MESSAGE_TYPES);\n\nfunction decodeText(input: string | Uint8Array): string {\n return typeof input === 'string' ? input : new TextDecoder('utf-8').decode(input);\n}\n\n/**\n * Validate an already-parsed JS value as an {@link Envelope}, narrowing\n * `payload` by `type`. Throws {@link UnknownMessageTypeError} when `type`\n * isn't a recognized message type (safe for the caller to skip/ignore), or\n * {@link EnvelopeValidationError} when a recognized type fails schema\n * validation.\n */\nexport function parseMessage(data: unknown): Envelope {\n const result = EnvelopeSchema.safeParse(data);\n if (result.success) {\n return result.data;\n }\n\n const typeValue =\n typeof data === 'object' && data !== null && !Array.isArray(data)\n ? (data as Record<string, unknown>).type\n : undefined;\n\n if (typeof typeValue !== 'string' || !MESSAGE_TYPE_SET.has(typeValue)) {\n throw new UnknownMessageTypeError(typeValue);\n }\n\n throw new EnvelopeValidationError(\n `Envelope failed validation for type \"${typeValue}\"`,\n result.error,\n );\n}\n\n/**\n * Decode a single NDJSON line into a validated {@link Envelope}. Accepts a\n * string or raw bytes (e.g. a WebSocket binary frame) — isomorphic, no\n * stream handling required of the caller.\n */\nexport function decodeEnvelope(line: string | Uint8Array): Envelope {\n const text = decodeText(line).replace(/[\\r\\n]+$/, '');\n let json: unknown;\n try {\n json = JSON.parse(text);\n } catch (cause) {\n throw new EnvelopeParseError('Envelope line is not valid JSON', cause);\n }\n return parseMessage(json);\n}\n\n/** Encode an {@link Envelope} as a single-line NDJSON string (trailing `\\n` included). */\nexport function encodeEnvelope(env: Envelope): string {\n return `${JSON.stringify(env)}\\n`;\n}\n\n// ---------------------------------------------------------------------------\n// createEnvelope — finding F1: `taskId`/`seq` used to be uniformly optional\n// regardless of `type`, so e.g. `createEnvelope('task.offer', payload)` (no\n// `taskId`, no `seq`) type-checked cleanly and only failed much later, at\n// runtime, wherever the resulting envelope happened to get decoded — often\n// on the *other* side of the wire, several hops from the actual mistake.\n//\n// `EnvelopeShapeOptions` is the per-type source of truth for `taskId`/`seq`\n// requiredness, deliberately parallel to `envelope.ts`'s own\n// `envelopeShape()` calls (which encode the identical rule at the schema\n// level — see docs/protocol.md §1.1/§1.2): every `task.*` type requires\n// `taskId`; every type the *server* sends to the daemon additionally\n// requires `seq`. `createEnvelope`'s `opts` parameter is conditionally\n// required/optional per `T` from this map (`RequiredKeys`/`CreateEnvelopeArgs`\n// below), so a call site missing a required field is a compile error, not a\n// runtime surprise. The constructed envelope is also validated against\n// {@link EnvelopeSchema} before being returned, throwing\n// {@link EnvelopeValidationError} on failure — a second, runtime-level net\n// for whatever the type system can't catch (e.g. a payload widened via `as`).\n// ---------------------------------------------------------------------------\n\ninterface EnvelopeShapeOptions {\n 'conn.hello': { taskId?: string; seq?: number };\n 'conn.ack': { taskId?: string; seq: number };\n 'task.offer': { taskId: string; seq: number };\n 'task.approve': { taskId: string; seq: number };\n 'task.reject': { taskId: string; seq: number };\n 'task.cancel': { taskId: string; seq: number };\n 'task.steer': { taskId: string; seq: number };\n 'task.claim': { taskId: string; seq?: number };\n 'task.started': { taskId: string; seq?: number };\n 'task.decline': { taskId: string; seq?: number };\n 'task.progress': { taskId: string; seq?: number };\n 'task.artifact': { taskId: string; seq?: number };\n 'task.await_approval': { taskId: string; seq?: number };\n 'task.complete': { taskId: string; seq?: number };\n 'task.fail': { taskId: string; seq?: number };\n 'task.cancelled': { taskId: string; seq?: number };\n 'task.approval_resolved': { taskId: string; seq?: number };\n}\n\ninterface EnvelopeCommonOptions {\n id?: string;\n ts?: string;\n v?: number;\n /** Always optional regardless of `type` (docs/protocol.md §1.3). */\n sessionRef?: string;\n}\n\n/** Public options shape for `createEnvelope<T>` — conditionally required `taskId`/`seq` per `EnvelopeShapeOptions[T]`, plus the always-optional common fields. Defaults to the full `MessageType` union (a loose, all-optional-ish shape) when `T` isn't pinned, which is also what `createEnvelope`'s own implementation uses internally to read `opts` without fighting the per-call-site conditional. */\nexport type CreateEnvelopeOptions<T extends MessageType = MessageType> = EnvelopeCommonOptions &\n EnvelopeShapeOptions[T];\n\n/** `never` unless every key of `T` is optional — i.e. whether `createEnvelope`'s `opts` argument can be omitted entirely for a given message type. */\ntype RequiredKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? never : K }[keyof T];\n\n/** The rest-parameter shape for `createEnvelope`'s 3rd argument: present-and-optional when `T` needs nothing, present-and-required when it needs `taskId` and/or `seq`. */\ntype CreateEnvelopeArgs<T extends MessageType> = RequiredKeys<EnvelopeShapeOptions[T]> extends never\n ? [opts?: CreateEnvelopeOptions<T>]\n : [opts: CreateEnvelopeOptions<T>];\n\ntype PayloadOf<T extends MessageType> = z.infer<(typeof MESSAGE_PAYLOAD_SCHEMAS)[T]>;\n\n/**\n * Build a well-formed {@link Envelope}, filling `v`/`id`/`ts` with defaults.\n * `opts` (`taskId`/`seq`) is required or optional depending on `type` — see\n * the module doc above — and the constructed envelope is validated against\n * {@link EnvelopeSchema} before being returned, throwing\n * {@link EnvelopeValidationError} if it doesn't satisfy the schema.\n */\nexport function createEnvelope<T extends MessageType>(\n type: T,\n payload: PayloadOf<T>,\n ...rest: CreateEnvelopeArgs<T>\n): Extract<Envelope, { type: T }> {\n // `rest[0]`'s precise per-T type doesn't survive being read back out\n // inside a generic function body (a well-known TS limitation, not a\n // soundness gap: every concrete instantiation of `T` at the call site was\n // already checked against `CreateEnvelopeArgs<T>` above) — `opts` is\n // treated as the loose, common shape here, and the schema validation below\n // is what actually guards this function's output either way.\n const opts = (rest[0] ?? {}) as CreateEnvelopeOptions;\n const envelope = {\n v: opts.v ?? PROTOCOL_VERSION,\n id: opts.id ?? crypto.randomUUID(),\n ts: opts.ts ?? new Date().toISOString(),\n type,\n ...(opts.taskId !== undefined ? { task_id: opts.taskId } : {}),\n ...(opts.sessionRef !== undefined ? { session_ref: opts.sessionRef } : {}),\n ...(opts.seq !== undefined ? { seq: opts.seq } : {}),\n payload,\n };\n\n const result = EnvelopeSchema.safeParse(envelope);\n if (!result.success) {\n throw new EnvelopeValidationError(`createEnvelope built an invalid envelope for type \"${type}\"`, result.error);\n }\n return result.data as Extract<Envelope, { type: T }>;\n}\n","import { z } from 'zod';\nimport { CONTENT_HASH_RE } from './blob';\nimport { EnvelopeSchema } from './envelope';\n\n/**\n * HTTP-side request/response shapes for the reference server's auth and blob\n * endpoints (M1 Part B). These are plain HTTP bodies, not wire envelopes —\n * kept in a separate module from `envelope.ts`/`messages.ts` because they\n * never travel over the WSS connection. Documented in full in\n * docs/protocol.md (\"Auth flows\", \"Blob flows\", \"Long-poll fallback\").\n *\n * The wire protocol version (`v:1`) is unaffected by any of this: pairing,\n * token renewal, and blob transfer are out-of-band HTTP calls that happen\n * before/alongside the WSS connection, not envelope types.\n */\n\n// ---------------------------------------------------------------------------\n// POST /byok/pair (v2) — one-time device pairing. An out-of-band pairing\n// code (minted by the SaaS's own auth/device-flow UI) plus a freshly\n// generated Ed25519 device keypair (private key never leaves the device)\n// register the device and mint its first access token.\n// ---------------------------------------------------------------------------\n\nexport const PairRequestSchema = z.object({\n pairingCode: z.string(),\n deviceName: z.string(),\n /** Ed25519 public key, base64url-encoded. Private key stays device-local (OS keychain or 0600 file). */\n devicePublicKey: z.string(),\n});\nexport type PairRequest = z.infer<typeof PairRequestSchema>;\n\nexport const PairResponseSchema = z.object({\n deviceId: z.string(),\n /** JWT, ~1h lifetime. */\n accessToken: z.string(),\n /** Opaque hint for when/how to renew (e.g. an ISO timestamp); not itself a credential. */\n refreshHint: z.string().optional(),\n});\nexport type PairResponse = z.infer<typeof PairResponseSchema>;\n\n// ---------------------------------------------------------------------------\n// POST /byok/challenge + POST /byok/token — token renewal without re-pairing.\n// Two-step challenge/response proves possession of the device private key\n// without ever transmitting it: the server hands out a one-time nonce, the\n// client signs it locally with the device key, and trades the signature for\n// a fresh access token.\n// ---------------------------------------------------------------------------\n\nexport const ChallengeRequestSchema = z.object({\n deviceId: z.string(),\n});\nexport type ChallengeRequest = z.infer<typeof ChallengeRequestSchema>;\n\nexport const ChallengeResponseSchema = z.object({\n /** One-time value the client must sign with its device private key. */\n nonce: z.string(),\n});\nexport type ChallengeResponse = z.infer<typeof ChallengeResponseSchema>;\n\nexport const TokenRequestSchema = z.object({\n deviceId: z.string(),\n nonce: z.string(),\n /** Ed25519 signature over `nonce`, base64url-encoded. */\n signature: z.string(),\n});\nexport type TokenRequest = z.infer<typeof TokenRequestSchema>;\n\nexport const TokenResponseSchema = z.object({\n accessToken: z.string(),\n expiresAt: z.iso.datetime({ offset: true }),\n});\nexport type TokenResponse = z.infer<typeof TokenResponseSchema>;\n\n/**\n * Revocation is server-side only (dashboard/API call on the SaaS's own\n * device registry) — there is no wire message for it. A revoked device's\n * next `/byok/challenge` or `/byok/token` call (or WSS connect) gets a 401;\n * the daemon's only recourse is to re-run `/byok/pair` from scratch.\n */\n\n// ---------------------------------------------------------------------------\n// Blob endpoints — presigned upload/download. Authed (bearer access token).\n// `BlobRef` (`blob.ts`) is unchanged; these are the HTTP calls that produce\n// the presigned URLs a `BlobRef` points at.\n// ---------------------------------------------------------------------------\n\n/** POST /byok/blobs request: declare a blob before uploading it. `contentHash` must be the canonical `sha256:<64 lowercase hex>` form (finding F9) — the server rejects anything else outright, no normalization. */\nexport const CreateBlobRequestSchema = z.object({\n size: z.number().int().nonnegative(),\n contentType: z.string(),\n contentHash: z.string().regex(CONTENT_HASH_RE, 'contentHash must be \"sha256:<64 lowercase hex>\"'),\n});\nexport type CreateBlobRequest = z.infer<typeof CreateBlobRequestSchema>;\n\n/** POST /byok/blobs response: presigned PUT target for the declared blob. */\nexport const CreateBlobResponseSchema = z.object({\n blobId: z.string(),\n uploadUrl: z.string(),\n});\nexport type CreateBlobResponse = z.infer<typeof CreateBlobResponseSchema>;\n\n/** GET /byok/blobs/:id/url response: presigned GET target for an existing blob. */\nexport const BlobDownloadUrlResponseSchema = z.object({\n downloadUrl: z.string(),\n});\nexport type BlobDownloadUrlResponse = z.infer<typeof BlobDownloadUrlResponseSchema>;\n\n// ---------------------------------------------------------------------------\n// GET /byok/events?cursor=N — long-poll fallback for environments where WSS\n// is unavailable. Authed; holds the request open ~50s waiting for new\n// events, same at-least-once/cursor semantics as the WSS redelivery path\n// (see docs/protocol.md \"At-least-once delivery\").\n// ---------------------------------------------------------------------------\n\nexport const EventsPollQuerySchema = z.object({\n /** Last `seq` this client has seen; omitted on a client's first-ever poll. Never negative — `seq` is a monotonically increasing counter starting at 1. */\n cursor: z.number().int().nonnegative().optional(),\n});\nexport type EventsPollQuery = z.infer<typeof EventsPollQuerySchema>;\n\nexport const EventsPollResponseSchema = z.object({\n events: z.array(EnvelopeSchema),\n cursor: z.number().int(),\n});\nexport type EventsPollResponse = z.infer<typeof EventsPollResponseSchema>;\n\n// ---------------------------------------------------------------------------\n// POST /byok/messages — finding F6: long-poll is now a full transport, not\n// receive-only. While a device is long-polling for S->D traffic (§8), it has\n// no live WS to carry its own D->S envelopes (task.claim, task.progress,\n// task.complete, etc.) — this endpoint is that path: a batch of envelopes,\n// authed the same way as every other bearer-authed route, routed through the\n// identical inbound handling a WS connection's messages get. See\n// docs/protocol.md §8.\n// ---------------------------------------------------------------------------\n\n/**\n * Batch size ceiling for a single `POST /byok/messages` call — generous for\n * normal redelivery-catchup bursts, but bounded so one request can't force\n * the server to process an unbounded batch. Exported (not just a local\n * const) so the client's own outbound drain (`ConnectionManager.drainOutbox`,\n * finding P1) can chunk against the exact same number instead of a\n * hard-coded, driftable copy of it.\n */\nexport const MAX_MESSAGES_PER_BATCH = 256;\n\nexport const MessagesSendRequestSchema = z.object({\n messages: z.array(EnvelopeSchema).max(MAX_MESSAGES_PER_BATCH),\n});\nexport type MessagesSendRequest = z.infer<typeof MessagesSendRequestSchema>;\n\n/**\n * `accepted` counts every envelope `ConnectionHub.handleInbound` returned\n * `'accepted'` *or* `'duplicate'` for (finding P2) — a dedup'd replay is a\n * wire-level success (§9's idempotency window), even though no handler ran\n * for it a second time. `rejected` (a type outside `DAEMON_TO_SERVER_TYPES`,\n * or an ownership mismatch — N2) is a separate, additive count: omitted\n * entirely when zero, so a batch with nothing rejected keeps the pre-P2\n * `{ accepted }` shape callers already depend on.\n */\nexport const MessagesSendResponseSchema = z.object({\n accepted: z.number().int().nonnegative(),\n rejected: z.number().int().nonnegative().optional(),\n});\nexport type MessagesSendResponse = z.infer<typeof MessagesSendResponseSchema>;\n"]}
|