@roll-agent/relay-protocol 0.0.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.
@@ -0,0 +1,429 @@
1
+ import { RELAY_ERROR_CODES, RELAY_ERROR_RETRYABILITY, RELAY_PROTOCOL_VERSION, RELAY_REQUEST_METHOD_DISPOSITIONS, RELAY_REQUEST_METHOD_VALUES, RELAY_REQUEST_REPLAY_DISPOSITIONS, RELAY_REQUEST_METHODS, classifyRelayAck, classifyRelayRequestReplay, getRelayRequestMethodDisposition, negotiateRelayProtocolVersion, parseRelayRequestParams, parseRelayRequestResult, relayMessageSchema, relayRuntimeRequestSchema, } from "./index.js";
2
+ const IDS = {
3
+ runtime: "00000000-0000-4000-8000-000000000501",
4
+ thread: "00000000-0000-4000-8000-000000000502",
5
+ turn: "00000000-0000-4000-8000-000000000503",
6
+ approval: "00000000-0000-4000-8000-000000000504",
7
+ request: "00000000-0000-4000-8000-000000000505",
8
+ secondRequest: "00000000-0000-4000-8000-000000000509",
9
+ device: "00000000-0000-4000-8000-000000000506",
10
+ workspace: "00000000-0000-4000-8000-000000000507",
11
+ secondWorkspace: "00000000-0000-4000-8000-000000000510",
12
+ envelope: "00000000-0000-4000-8000-000000000508",
13
+ };
14
+ export const RELAY_FRAME_CONFORMANCE_CASES = [
15
+ {
16
+ id: "device-connect",
17
+ fixtureName: "valid-device-connect.json",
18
+ valid: true,
19
+ frame: {
20
+ type: "device.connect",
21
+ protocolVersion: "1.0",
22
+ deviceId: IDS.device,
23
+ pairingToken: "pairing-token-with-sufficient-length",
24
+ },
25
+ },
26
+ {
27
+ id: "runtime-request",
28
+ fixtureName: "valid-runtime-request.json",
29
+ valid: true,
30
+ frame: {
31
+ type: "runtime.request",
32
+ requestId: IDS.request,
33
+ workspaceId: IDS.workspace,
34
+ method: "thread.snapshot",
35
+ params: { threadId: IDS.thread, limit: 100 },
36
+ },
37
+ },
38
+ {
39
+ id: "runtime-response",
40
+ fixtureName: "valid-runtime-response.json",
41
+ valid: true,
42
+ frame: {
43
+ type: "runtime.response",
44
+ requestId: IDS.request,
45
+ workspaceId: IDS.workspace,
46
+ result: { accepted: true },
47
+ },
48
+ },
49
+ {
50
+ id: "runtime-event",
51
+ fixtureName: "valid-runtime-event.json",
52
+ valid: true,
53
+ frame: {
54
+ type: "runtime.event",
55
+ workspaceId: IDS.workspace,
56
+ relaySequence: 0,
57
+ event: {
58
+ protocolVersion: "1.0",
59
+ runtimeInstanceId: IDS.runtime,
60
+ sequence: 0,
61
+ timestamp: "2026-07-30T00:00:00.000Z",
62
+ threadId: IDS.thread,
63
+ turnId: IDS.turn,
64
+ event: { type: "turn.completed" },
65
+ },
66
+ },
67
+ },
68
+ {
69
+ id: "runtime-ack",
70
+ fixtureName: "valid-runtime-ack.json",
71
+ valid: true,
72
+ frame: {
73
+ type: "runtime.ack",
74
+ workspaceId: IDS.workspace,
75
+ throughRelaySequence: 0,
76
+ },
77
+ },
78
+ {
79
+ id: "runtime-gap",
80
+ fixtureName: "valid-runtime-gap.json",
81
+ valid: true,
82
+ frame: {
83
+ type: "runtime.gap",
84
+ workspaceId: IDS.workspace,
85
+ fromRelaySequence: 0,
86
+ throughRelaySequence: 2,
87
+ recovery: "thread.snapshot",
88
+ },
89
+ },
90
+ {
91
+ id: "runtime-encrypted",
92
+ fixtureName: "valid-runtime-encrypted.json",
93
+ valid: true,
94
+ frame: {
95
+ type: "runtime.encrypted",
96
+ workspaceId: IDS.workspace,
97
+ envelopeId: IDS.envelope,
98
+ payloadKind: "request",
99
+ requestId: IDS.request,
100
+ algorithm: "example-aead",
101
+ nonce: "opaque-nonce",
102
+ ciphertext: "opaque-ciphertext",
103
+ },
104
+ },
105
+ {
106
+ id: "unknown-version",
107
+ fixtureName: "invalid-unknown-version.json",
108
+ valid: false,
109
+ frame: {
110
+ type: "device.connect",
111
+ protocolVersion: "9.9",
112
+ deviceId: IDS.device,
113
+ pairingToken: "pairing-token-with-sufficient-length",
114
+ },
115
+ },
116
+ {
117
+ id: "unknown-message",
118
+ fixtureName: "invalid-unknown-message.json",
119
+ valid: false,
120
+ frame: {
121
+ type: "runtime.unknown",
122
+ workspaceId: IDS.workspace,
123
+ },
124
+ },
125
+ {
126
+ id: "unknown-method",
127
+ fixtureName: "invalid-unknown-method.json",
128
+ valid: false,
129
+ frame: {
130
+ type: "runtime.request",
131
+ requestId: IDS.request,
132
+ workspaceId: IDS.workspace,
133
+ method: "runtime.futureMethod",
134
+ params: {},
135
+ },
136
+ },
137
+ {
138
+ id: "invalid-id",
139
+ fixtureName: "invalid-request-id.json",
140
+ valid: false,
141
+ frame: {
142
+ type: "runtime.request",
143
+ requestId: "not-a-uuid",
144
+ workspaceId: IDS.workspace,
145
+ method: "thread.list",
146
+ params: {},
147
+ },
148
+ },
149
+ {
150
+ id: "extra-field",
151
+ fixtureName: "invalid-extra-field.json",
152
+ valid: false,
153
+ frame: {
154
+ type: "runtime.ack",
155
+ workspaceId: IDS.workspace,
156
+ throughRelaySequence: 0,
157
+ unexpected: true,
158
+ },
159
+ },
160
+ {
161
+ id: "invalid-gap-recovery",
162
+ fixtureName: "invalid-gap-recovery.json",
163
+ valid: false,
164
+ frame: {
165
+ type: "runtime.gap",
166
+ workspaceId: IDS.workspace,
167
+ fromRelaySequence: 0,
168
+ throughRelaySequence: 2,
169
+ recovery: "event.replay",
170
+ },
171
+ },
172
+ {
173
+ id: "encrypted-metadata-leak",
174
+ fixtureName: "invalid-encrypted-metadata-leak.json",
175
+ valid: false,
176
+ frame: {
177
+ type: "runtime.encrypted",
178
+ workspaceId: IDS.workspace,
179
+ envelopeId: IDS.envelope,
180
+ payloadKind: "request",
181
+ requestId: IDS.request,
182
+ algorithm: "example-aead",
183
+ nonce: "opaque-nonce",
184
+ ciphertext: "opaque-ciphertext",
185
+ plaintext: { secret: "must-not-be-visible" },
186
+ },
187
+ },
188
+ ];
189
+ export const RELAY_NEGOTIATION_CONFORMANCE_CASES = [
190
+ { id: "same-version", peerVersions: ["1.0"], expected: "1.0" },
191
+ { id: "unknown-before-supported", peerVersions: ["9.9", "1.0"], expected: "1.0" },
192
+ { id: "unknown-only", peerVersions: ["9.9"], expected: undefined },
193
+ ];
194
+ export const RELAY_METHOD_REGISTRY_CONFORMANCE_CASES = [
195
+ ...RELAY_REQUEST_METHOD_VALUES.map((value) => ({
196
+ id: `method-disposition-${value}`,
197
+ value,
198
+ expected: RELAY_REQUEST_METHOD_DISPOSITIONS[value],
199
+ })),
200
+ {
201
+ id: "unknown-method-not-recognized",
202
+ value: "runtime.futureMethod",
203
+ expected: undefined,
204
+ },
205
+ ];
206
+ export const RELAY_METHOD_CONFORMANCE_CASES = [
207
+ {
208
+ id: "approval-candidate-params",
209
+ method: RELAY_REQUEST_METHODS.approvalCandidate,
210
+ kind: "params",
211
+ valid: true,
212
+ value: {
213
+ threadId: IDS.thread,
214
+ turnId: IDS.turn,
215
+ approvalId: IDS.approval,
216
+ decision: "approve",
217
+ },
218
+ },
219
+ {
220
+ id: "approval-candidate-empty-reason",
221
+ method: RELAY_REQUEST_METHODS.approvalCandidate,
222
+ kind: "params",
223
+ valid: false,
224
+ value: {
225
+ threadId: IDS.thread,
226
+ turnId: IDS.turn,
227
+ approvalId: IDS.approval,
228
+ decision: "reject",
229
+ reason: "",
230
+ },
231
+ },
232
+ {
233
+ id: "approval-candidate-result",
234
+ method: RELAY_REQUEST_METHODS.approvalCandidate,
235
+ kind: "result",
236
+ valid: true,
237
+ value: { accepted: true },
238
+ },
239
+ ];
240
+ const replayRequest = relayRuntimeRequestSchema.parse(RELAY_FRAME_CONFORMANCE_CASES.find((entry) => entry.id === "runtime-request")?.frame);
241
+ export const RELAY_REPLAY_CONFORMANCE_CASES = [
242
+ {
243
+ id: "same-request-different-key-order",
244
+ existing: replayRequest,
245
+ candidate: relayRuntimeRequestSchema.parse({
246
+ ...replayRequest,
247
+ params: { limit: 100, threadId: IDS.thread },
248
+ }),
249
+ expected: {
250
+ disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.replay,
251
+ },
252
+ },
253
+ {
254
+ id: "same-id-different-payload",
255
+ existing: replayRequest,
256
+ candidate: relayRuntimeRequestSchema.parse({
257
+ ...replayRequest,
258
+ params: { threadId: IDS.thread, limit: 10 },
259
+ }),
260
+ expected: {
261
+ disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.conflict,
262
+ errorCode: RELAY_ERROR_CODES.requestIdConflict,
263
+ },
264
+ },
265
+ {
266
+ id: "same-id-different-method",
267
+ existing: replayRequest,
268
+ candidate: relayRuntimeRequestSchema.parse({
269
+ ...replayRequest,
270
+ method: RELAY_REQUEST_METHODS.threadOpen,
271
+ }),
272
+ expected: {
273
+ disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.conflict,
274
+ errorCode: RELAY_ERROR_CODES.requestIdConflict,
275
+ },
276
+ },
277
+ {
278
+ id: "different-request-id",
279
+ existing: replayRequest,
280
+ candidate: relayRuntimeRequestSchema.parse({
281
+ ...replayRequest,
282
+ requestId: IDS.secondRequest,
283
+ }),
284
+ expected: {
285
+ disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.new,
286
+ },
287
+ },
288
+ {
289
+ id: "different-workspace-id",
290
+ existing: replayRequest,
291
+ candidate: relayRuntimeRequestSchema.parse({
292
+ ...replayRequest,
293
+ workspaceId: IDS.secondWorkspace,
294
+ }),
295
+ expected: {
296
+ disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.new,
297
+ },
298
+ },
299
+ ];
300
+ export const RELAY_ACK_CONFORMANCE_CASES = [
301
+ {
302
+ id: "initial-advance",
303
+ acknowledgedThrough: -1,
304
+ advertisedThrough: 0,
305
+ incomingThrough: 0,
306
+ expected: "advance",
307
+ },
308
+ {
309
+ id: "advance-contiguous-prefix",
310
+ acknowledgedThrough: 0,
311
+ advertisedThrough: 2,
312
+ incomingThrough: 2,
313
+ expected: "advance",
314
+ },
315
+ {
316
+ id: "equal-duplicate-ack",
317
+ acknowledgedThrough: 2,
318
+ advertisedThrough: 2,
319
+ incomingThrough: 2,
320
+ expected: "duplicate",
321
+ },
322
+ {
323
+ id: "stale-duplicate-ack",
324
+ acknowledgedThrough: 2,
325
+ advertisedThrough: 2,
326
+ incomingThrough: 1,
327
+ expected: "duplicate",
328
+ },
329
+ {
330
+ id: "ack-beyond-advertised",
331
+ acknowledgedThrough: 0,
332
+ advertisedThrough: 1,
333
+ incomingThrough: 2,
334
+ expected: "invalid",
335
+ },
336
+ {
337
+ id: "fractional-ack",
338
+ acknowledgedThrough: 0,
339
+ advertisedThrough: 1,
340
+ incomingThrough: 0.5,
341
+ expected: "invalid",
342
+ },
343
+ {
344
+ id: "ack-below-sentinel",
345
+ acknowledgedThrough: -2,
346
+ advertisedThrough: 0,
347
+ incomingThrough: 0,
348
+ expected: "invalid",
349
+ },
350
+ ];
351
+ export const RELAY_ENCRYPTED_VISIBLE_METADATA_FIELDS = [
352
+ "type",
353
+ "workspaceId",
354
+ "envelopeId",
355
+ "payloadKind",
356
+ "requestId",
357
+ "relaySequence",
358
+ "algorithm",
359
+ "nonce",
360
+ "ciphertext",
361
+ ];
362
+ export const RELAY_ERROR_CONFORMANCE_CASES = Object.values(RELAY_ERROR_CODES).map((code) => ({
363
+ id: `error-retryability-${code}`,
364
+ code,
365
+ retryable: RELAY_ERROR_RETRYABILITY[code],
366
+ }));
367
+ export const runtimeRelayProtocolConformanceAdapter = {
368
+ validateFrame: (value) => relayMessageSchema.safeParse(value).success,
369
+ negotiate: negotiateRelayProtocolVersion,
370
+ getRequestMethodDisposition: getRelayRequestMethodDisposition,
371
+ validateMethodValue(method, kind, value) {
372
+ try {
373
+ if (kind === "params") {
374
+ parseRelayRequestParams(method, value);
375
+ }
376
+ else {
377
+ parseRelayRequestResult(method, value);
378
+ }
379
+ return true;
380
+ }
381
+ catch {
382
+ return false;
383
+ }
384
+ },
385
+ classifyReplay: classifyRelayRequestReplay,
386
+ classifyAck: classifyRelayAck,
387
+ getErrorRetryability: (code) => RELAY_ERROR_RETRYABILITY[code],
388
+ };
389
+ export function runRelayProtocolConformance(adapter) {
390
+ const failures = [];
391
+ for (const entry of RELAY_FRAME_CONFORMANCE_CASES) {
392
+ recordFailure(failures, entry.id, entry.valid, adapter.validateFrame(entry.frame));
393
+ }
394
+ for (const entry of RELAY_NEGOTIATION_CONFORMANCE_CASES) {
395
+ recordFailure(failures, entry.id, entry.expected ?? "unsupported", adapter.negotiate(entry.peerVersions) ?? "unsupported");
396
+ }
397
+ for (const entry of RELAY_METHOD_REGISTRY_CONFORMANCE_CASES) {
398
+ recordFailure(failures, entry.id, entry.expected ?? "unrecognized", adapter.getRequestMethodDisposition(entry.value) ?? "unrecognized");
399
+ }
400
+ for (const entry of RELAY_METHOD_CONFORMANCE_CASES) {
401
+ recordFailure(failures, entry.id, entry.valid, adapter.validateMethodValue(entry.method, entry.kind, entry.value));
402
+ }
403
+ for (const entry of RELAY_REPLAY_CONFORMANCE_CASES) {
404
+ const actual = adapter.classifyReplay(entry.existing, entry.candidate);
405
+ recordFailure(failures, `${entry.id}-disposition`, entry.expected.disposition, actual.disposition);
406
+ recordFailure(failures, `${entry.id}-error-code`, "errorCode" in entry.expected ? entry.expected.errorCode : "none", actual.errorCode ?? "none");
407
+ }
408
+ for (const entry of RELAY_ACK_CONFORMANCE_CASES) {
409
+ recordFailure(failures, entry.id, entry.expected, adapter.classifyAck(entry));
410
+ }
411
+ for (const entry of RELAY_ERROR_CONFORMANCE_CASES) {
412
+ recordFailure(failures, entry.id, entry.retryable, adapter.getErrorRetryability(entry.code));
413
+ }
414
+ return {
415
+ protocolVersion: RELAY_PROTOCOL_VERSION,
416
+ passed: failures.length === 0,
417
+ failures,
418
+ };
419
+ }
420
+ function recordFailure(failures, caseId, expected, actual) {
421
+ if (expected === actual) {
422
+ return;
423
+ }
424
+ failures.push({
425
+ caseId,
426
+ expected: String(expected),
427
+ actual: String(actual),
428
+ });
429
+ }