@roll-agent/relay-protocol 0.1.1 → 0.3.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.
Files changed (51) hide show
  1. package/README.md +134 -39
  2. package/dist/conformance.d.ts +571 -4
  3. package/dist/conformance.js +677 -8
  4. package/dist/control.d.ts +188 -0
  5. package/dist/control.js +180 -0
  6. package/dist/index.d.ts +3991 -287
  7. package/dist/index.js +676 -77
  8. package/dist/reference-adapter.d.ts +56 -0
  9. package/dist/reference-adapter.js +122 -0
  10. package/dist/schema/roll-relay-browser-session-v1.schema.json +23 -0
  11. package/dist/schema/roll-relay-control-v1.schema.json +127 -0
  12. package/dist/schema/roll-relay-protocol-v1.1.schema.json +3791 -0
  13. package/dist/schema/roll-relay-protocol-v1.schema.json +1 -1
  14. package/fixtures/control/v1.0/invalid-browser-session-trailing-whitespace.json +4 -0
  15. package/fixtures/control/v1.0/invalid-browser-session-uppercase-scheme.json +4 -0
  16. package/fixtures/control/v1.0/invalid-browser-session-url.json +4 -0
  17. package/fixtures/control/v1.0/invalid-browser-session-ws.json +4 -0
  18. package/fixtures/control/v1.0/invalid-control-extra-field.json +6 -0
  19. package/fixtures/control/v1.0/invalid-session-ready-wire-version.json +8 -0
  20. package/fixtures/control/v1.0/manifest.json +52 -0
  21. package/fixtures/control/v1.0/valid-browser-session.json +4 -0
  22. package/fixtures/control/v1.0/valid-session-error.json +5 -0
  23. package/fixtures/control/v1.0/valid-session-ready.json +8 -0
  24. package/fixtures/control/v1.0/valid-workspace-status.json +5 -0
  25. package/fixtures/v1.1/invalid-approval-projection-raw-input-leak.json +19 -0
  26. package/fixtures/v1.1/invalid-authentication-local-only.json +12 -0
  27. package/fixtures/v1.1/invalid-file-picker-local-only.json +12 -0
  28. package/fixtures/v1.1/invalid-interaction-cancelled-reason-leak.json +10 -0
  29. package/fixtures/v1.1/invalid-interaction-resolved-result-leak.json +18 -0
  30. package/fixtures/v1.1/invalid-legacy-approval-candidate.json +7 -0
  31. package/fixtures/v1.1/invalid-legacy-approval-respond.json +7 -0
  32. package/fixtures/v1.1/invalid-n-minus-one-device-connect.json +6 -0
  33. package/fixtures/v1.1/invalid-non-normal-sensitivity.json +21 -0
  34. package/fixtures/v1.1/invalid-runtime-event-tool-input-leak.json +22 -0
  35. package/fixtures/v1.1/invalid-runtime-event-tool-output-leak.json +21 -0
  36. package/fixtures/v1.1/invalid-user-input-projection-secret-leak.json +22 -0
  37. package/fixtures/v1.1/manifest.json +673 -0
  38. package/fixtures/v1.1/valid-device-connect.json +6 -0
  39. package/fixtures/v1.1/valid-interaction-cancelled.json +9 -0
  40. package/fixtures/v1.1/valid-interaction-candidate-user-input.json +25 -0
  41. package/fixtures/v1.1/valid-interaction-request-approval.json +17 -0
  42. package/fixtures/v1.1/valid-interaction-request-user-input.json +40 -0
  43. package/fixtures/v1.1/valid-interaction-resolved.json +9 -0
  44. package/fixtures/v1.1/valid-runtime-ack.json +5 -0
  45. package/fixtures/v1.1/valid-runtime-encrypted-interaction.json +10 -0
  46. package/fixtures/v1.1/valid-runtime-event-runtime-n-minus-one.json +16 -0
  47. package/fixtures/v1.1/valid-runtime-event.json +16 -0
  48. package/fixtures/v1.1/valid-runtime-gap.json +7 -0
  49. package/fixtures/v1.1/valid-runtime-request.json +10 -0
  50. package/fixtures/v1.1/valid-runtime-response.json +8 -0
  51. package/package.json +16 -3
@@ -0,0 +1,56 @@
1
+ import { type InteractionId, type RelayInteractionCancelledV11, type RelayInteractionRequestV11, type RelayInteractionResolvedV11, type RelayRequestId, type RelayRuntimeRequestV11, type WorkspaceId } from "./index.ts";
2
+ export declare const RELAY_REFERENCE_ADAPTER_ERROR_CODES: {
3
+ readonly protocolVersionUnsupported: "RELAY_REFERENCE_PROTOCOL_VERSION_UNSUPPORTED";
4
+ readonly invalidFrame: "RELAY_REFERENCE_INVALID_FRAME";
5
+ readonly interactionConflict: "RELAY_REFERENCE_INTERACTION_CONFLICT";
6
+ readonly interactionNotPending: "RELAY_REFERENCE_INTERACTION_NOT_PENDING";
7
+ readonly interactionTerminated: "RELAY_REFERENCE_INTERACTION_TERMINATED";
8
+ readonly interactionIdentityMismatch: "RELAY_REFERENCE_INTERACTION_IDENTITY_MISMATCH";
9
+ readonly candidateMethodMismatch: "RELAY_REFERENCE_CANDIDATE_METHOD_MISMATCH";
10
+ readonly requestIdUnavailable: "RELAY_REFERENCE_REQUEST_ID_UNAVAILABLE";
11
+ };
12
+ export type RelayReferenceAdapterErrorCode = (typeof RELAY_REFERENCE_ADAPTER_ERROR_CODES)[keyof typeof RELAY_REFERENCE_ADAPTER_ERROR_CODES];
13
+ export declare class RelayReferenceAdapterError extends Error {
14
+ readonly code: RelayReferenceAdapterErrorCode;
15
+ constructor(code: RelayReferenceAdapterErrorCode, message: string);
16
+ }
17
+ export interface RelayBrowserReferenceAdapterOptions {
18
+ /** Wire version is explicit so a legacy 1.0 connection cannot opt in accidentally. */
19
+ readonly protocolVersion: "1.1";
20
+ /** The embedding host owns Relay request ID allocation. */
21
+ readonly createRequestId?: () => RelayRequestId;
22
+ }
23
+ export type RelayInteractionMethodV11 = RelayInteractionRequestV11["method"];
24
+ export interface RelayInteractionCandidateInputV11 {
25
+ readonly workspaceId: WorkspaceId;
26
+ readonly interactionId: InteractionId;
27
+ readonly method: RelayInteractionMethodV11;
28
+ readonly candidate: unknown;
29
+ }
30
+ export type RelayReferenceAdapterReceiveResultV11 = {
31
+ readonly status: "pending" | "duplicate";
32
+ readonly request: RelayInteractionRequestV11;
33
+ } | {
34
+ readonly status: "resolved";
35
+ readonly request: RelayInteractionRequestV11;
36
+ readonly frame: RelayInteractionResolvedV11;
37
+ } | {
38
+ readonly status: "cancelled";
39
+ readonly request: RelayInteractionRequestV11;
40
+ readonly frame: RelayInteractionCancelledV11;
41
+ };
42
+ /**
43
+ * Browser-safe, in-memory correlation for Relay Wire 1.1 interaction frames.
44
+ * Authentication, responder policy, delivery retries, and persistence remain host responsibilities.
45
+ */
46
+ export declare class RelayBrowserReferenceAdapter {
47
+ private readonly createRequestId;
48
+ private readonly pending;
49
+ private readonly terminated;
50
+ constructor(options: RelayBrowserReferenceAdapterOptions);
51
+ receive(value: unknown): RelayReferenceAdapterReceiveResultV11;
52
+ createCandidate(input: RelayInteractionCandidateInputV11): RelayRuntimeRequestV11;
53
+ getPending(workspaceId: WorkspaceId, interactionId: InteractionId): RelayInteractionRequestV11 | undefined;
54
+ listPending(): readonly RelayInteractionRequestV11[];
55
+ }
56
+ export declare function createRelayBrowserReferenceAdapter(options: RelayBrowserReferenceAdapterOptions): RelayBrowserReferenceAdapter;
@@ -0,0 +1,122 @@
1
+ import { RELAY_REQUEST_METHODS_V11, relayInteractionCancelledSchemaV11, relayInteractionCandidateParamsSchemaV11, relayInteractionRequestSchemaV11, relayInteractionResolvedSchemaV11, relayRequestIdSchema, relayRuntimeRequestSchemaV11, parseRelayInteractionCandidateForRequestV11, } from "./index.js";
2
+ export const RELAY_REFERENCE_ADAPTER_ERROR_CODES = {
3
+ protocolVersionUnsupported: "RELAY_REFERENCE_PROTOCOL_VERSION_UNSUPPORTED",
4
+ invalidFrame: "RELAY_REFERENCE_INVALID_FRAME",
5
+ interactionConflict: "RELAY_REFERENCE_INTERACTION_CONFLICT",
6
+ interactionNotPending: "RELAY_REFERENCE_INTERACTION_NOT_PENDING",
7
+ interactionTerminated: "RELAY_REFERENCE_INTERACTION_TERMINATED",
8
+ interactionIdentityMismatch: "RELAY_REFERENCE_INTERACTION_IDENTITY_MISMATCH",
9
+ candidateMethodMismatch: "RELAY_REFERENCE_CANDIDATE_METHOD_MISMATCH",
10
+ requestIdUnavailable: "RELAY_REFERENCE_REQUEST_ID_UNAVAILABLE",
11
+ };
12
+ export class RelayReferenceAdapterError extends Error {
13
+ code;
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.name = "RelayReferenceAdapterError";
17
+ this.code = code;
18
+ }
19
+ }
20
+ function pendingKey(workspaceId, interactionId) {
21
+ return `${workspaceId}:${interactionId}`;
22
+ }
23
+ function hasSameInteractionIdentity(request, terminal) {
24
+ return (request.workspaceId === terminal.workspaceId &&
25
+ request.interactionId === terminal.interactionId &&
26
+ request.threadId === terminal.threadId &&
27
+ request.turnId === terminal.turnId &&
28
+ request.method === terminal.method);
29
+ }
30
+ function parseInteractionFrame(value) {
31
+ const request = relayInteractionRequestSchemaV11.safeParse(value);
32
+ if (request.success) {
33
+ return request.data;
34
+ }
35
+ const resolved = relayInteractionResolvedSchemaV11.safeParse(value);
36
+ if (resolved.success) {
37
+ return resolved.data;
38
+ }
39
+ const cancelled = relayInteractionCancelledSchemaV11.safeParse(value);
40
+ if (cancelled.success) {
41
+ return cancelled.data;
42
+ }
43
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.invalidFrame, "Expected a Relay Wire 1.1 interaction request, resolved, or cancelled frame");
44
+ }
45
+ function createBrowserRequestId() {
46
+ if (typeof globalThis.crypto?.randomUUID !== "function") {
47
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.requestIdUnavailable, "Relay candidate request IDs require crypto.randomUUID or an injected createRequestId");
48
+ }
49
+ return relayRequestIdSchema.parse(globalThis.crypto.randomUUID());
50
+ }
51
+ /**
52
+ * Browser-safe, in-memory correlation for Relay Wire 1.1 interaction frames.
53
+ * Authentication, responder policy, delivery retries, and persistence remain host responsibilities.
54
+ */
55
+ export class RelayBrowserReferenceAdapter {
56
+ createRequestId;
57
+ pending = new Map();
58
+ terminated = new Set();
59
+ constructor(options) {
60
+ if (options.protocolVersion !== "1.1") {
61
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.protocolVersionUnsupported, `Relay reference adapter requires Wire 1.1, received ${JSON.stringify(options.protocolVersion)}`);
62
+ }
63
+ this.createRequestId = options.createRequestId ?? createBrowserRequestId;
64
+ }
65
+ receive(value) {
66
+ const frame = parseInteractionFrame(value);
67
+ if (frame.type === "interaction.request") {
68
+ const key = pendingKey(frame.workspaceId, frame.interactionId);
69
+ if (this.terminated.has(key)) {
70
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.interactionTerminated, `Interaction ${frame.interactionId} has already terminated in this workspace`);
71
+ }
72
+ const existing = this.pending.get(key);
73
+ if (existing === undefined) {
74
+ this.pending.set(key, frame);
75
+ return { status: "pending", request: frame };
76
+ }
77
+ if (JSON.stringify(existing) === JSON.stringify(frame)) {
78
+ return { status: "duplicate", request: existing };
79
+ }
80
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.interactionConflict, `Interaction ${frame.interactionId} is already pending with different metadata`);
81
+ }
82
+ const key = pendingKey(frame.workspaceId, frame.interactionId);
83
+ const request = this.pending.get(key);
84
+ if (request === undefined) {
85
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.interactionNotPending, `Interaction ${frame.interactionId} is not pending in this workspace`);
86
+ }
87
+ if (!hasSameInteractionIdentity(request, frame)) {
88
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.interactionIdentityMismatch, `Interaction ${frame.interactionId} terminal frame does not match its pending request`);
89
+ }
90
+ this.pending.delete(key);
91
+ this.terminated.add(key);
92
+ return frame.type === "interaction.resolved"
93
+ ? { status: "resolved", request, frame }
94
+ : { status: "cancelled", request, frame };
95
+ }
96
+ createCandidate(input) {
97
+ const request = this.pending.get(pendingKey(input.workspaceId, input.interactionId));
98
+ if (request === undefined) {
99
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.interactionNotPending, `Interaction ${input.interactionId} is not pending in this workspace`);
100
+ }
101
+ if (request.method !== input.method) {
102
+ throw new RelayReferenceAdapterError(RELAY_REFERENCE_ADAPTER_ERROR_CODES.candidateMethodMismatch, `Candidate method ${input.method} does not match pending method ${request.method}`);
103
+ }
104
+ const params = relayInteractionCandidateParamsSchemaV11.parse(parseRelayInteractionCandidateForRequestV11(request, input.candidate));
105
+ return relayRuntimeRequestSchemaV11.parse({
106
+ type: "runtime.request",
107
+ requestId: this.createRequestId(),
108
+ workspaceId: request.workspaceId,
109
+ method: RELAY_REQUEST_METHODS_V11.interactionCandidate,
110
+ params,
111
+ });
112
+ }
113
+ getPending(workspaceId, interactionId) {
114
+ return this.pending.get(pendingKey(workspaceId, interactionId));
115
+ }
116
+ listPending() {
117
+ return [...this.pending.values()];
118
+ }
119
+ }
120
+ export function createRelayBrowserReferenceAdapter(options) {
121
+ return new RelayBrowserReferenceAdapter(options);
122
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "readOnly": true,
4
+ "type": "object",
5
+ "properties": {
6
+ "connectUrl": {
7
+ "type": "string",
8
+ "format": "uri",
9
+ "pattern": "^wss:\\/\\/\\S+$"
10
+ },
11
+ "expiresAt": {
12
+ "type": "string",
13
+ "format": "date-time",
14
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$"
15
+ }
16
+ },
17
+ "required": ["connectUrl", "expiresAt"],
18
+ "additionalProperties": false,
19
+ "$id": "urn:roll-agent:schema:relay-browser-session:1.0",
20
+ "title": "Roll Relay Browser Session Descriptor v1.0",
21
+ "description": "Short-lived single-use Browser WebSocket connection descriptor.",
22
+ "x-roll-control-version": "1.0"
23
+ }
@@ -0,0 +1,127 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "anyOf": [
4
+ {
5
+ "readOnly": true,
6
+ "type": "object",
7
+ "properties": {
8
+ "type": {
9
+ "type": "string",
10
+ "const": "session.ready"
11
+ },
12
+ "controlVersion": {
13
+ "type": "string",
14
+ "const": "1.0"
15
+ },
16
+ "relayProtocolVersion": {
17
+ "type": "string",
18
+ "const": "1.1"
19
+ },
20
+ "sessionId": {
21
+ "type": "string",
22
+ "minLength": 1
23
+ },
24
+ "workspaceId": {
25
+ "type": "string",
26
+ "format": "uuid",
27
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"
28
+ },
29
+ "workspaceStatus": {
30
+ "type": "string",
31
+ "enum": ["online", "offline"]
32
+ }
33
+ },
34
+ "required": [
35
+ "type",
36
+ "controlVersion",
37
+ "relayProtocolVersion",
38
+ "sessionId",
39
+ "workspaceId",
40
+ "workspaceStatus"
41
+ ],
42
+ "additionalProperties": false
43
+ },
44
+ {
45
+ "readOnly": true,
46
+ "type": "object",
47
+ "properties": {
48
+ "type": {
49
+ "type": "string",
50
+ "const": "workspace.status"
51
+ },
52
+ "workspaceId": {
53
+ "type": "string",
54
+ "format": "uuid",
55
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"
56
+ },
57
+ "status": {
58
+ "type": "string",
59
+ "enum": ["online", "offline"]
60
+ }
61
+ },
62
+ "required": ["type", "workspaceId", "status"],
63
+ "additionalProperties": false
64
+ },
65
+ {
66
+ "readOnly": true,
67
+ "type": "object",
68
+ "properties": {
69
+ "type": {
70
+ "type": "string",
71
+ "const": "session.error"
72
+ },
73
+ "code": {
74
+ "type": "string",
75
+ "enum": [
76
+ "WORKSPACE_NOT_FOUND",
77
+ "WORKSPACE_OFFLINE",
78
+ "CONTROLLER_CONFLICT",
79
+ "RELAY_PROTOCOL_VERSION_UNSUPPORTED",
80
+ "RATE_LIMITED",
81
+ "RELAY_INTERNAL_ERROR"
82
+ ]
83
+ },
84
+ "retryable": {
85
+ "type": "boolean"
86
+ }
87
+ },
88
+ "required": ["type", "code", "retryable"],
89
+ "additionalProperties": false
90
+ }
91
+ ],
92
+ "$id": "urn:roll-agent:schema:relay-control:1.0",
93
+ "title": "Roll Relay Browser Control v1.0",
94
+ "description": "Relay-to-Browser session and workspace control messages.",
95
+ "x-roll-control-version": "1.0",
96
+ "x-roll-browser-first-message-type": "session.ready",
97
+ "x-roll-browser-message-types-by-direction": {
98
+ "relay-to-peer": [
99
+ "session.ready",
100
+ "workspace.status",
101
+ "session.error",
102
+ "runtime.response",
103
+ "runtime.event",
104
+ "runtime.gap",
105
+ "interaction.request",
106
+ "interaction.resolved",
107
+ "interaction.cancelled"
108
+ ],
109
+ "peer-to-relay": ["runtime.request", "runtime.ack"]
110
+ },
111
+ "x-roll-companion-message-types-by-direction": {
112
+ "relay-to-peer": ["runtime.request", "runtime.ack"],
113
+ "peer-to-relay": [
114
+ "device.connect",
115
+ "runtime.response",
116
+ "runtime.event",
117
+ "runtime.gap",
118
+ "interaction.request",
119
+ "interaction.resolved",
120
+ "interaction.cancelled"
121
+ ]
122
+ },
123
+ "x-roll-wss-directions": {
124
+ "relayToPeer": "relay-to-peer",
125
+ "peerToRelay": "peer-to-relay"
126
+ }
127
+ }