@roll-agent/companion 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.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # `@roll-agent/companion`
2
+
3
+ 连接云端 Web UI 与用户本机 Roll 的 Local Companion / Relay 基础库。
4
+
5
+ 本包提供 Relay 消息 Schema、Workspace 生命周期 lease、事件 ACK/gap 缓冲、mutation
6
+ 去重、出站重连和可插拔的 Workspace payload cipher。它不包含生产 Cloud Relay Server、
7
+ Browser SDK、账号/设备存储、TLS、鉴权授权、本地确认 UI、HA 或监控。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ pnpm add @roll-agent/companion @roll-agent/client-node
13
+ ```
14
+
15
+ ## 最小骨架
16
+
17
+ ```ts
18
+ import { RollNodeClient } from "@roll-agent/client-node";
19
+ import {
20
+ CompanionRelayBridge,
21
+ CompanionWorkspace,
22
+ OutboundCompanionRelay,
23
+ deviceIdSchema,
24
+ workspaceIdSchema,
25
+ } from "@roll-agent/companion";
26
+
27
+ const runtime = await RollNodeClient.start({ cwd: workspacePath });
28
+ const workspaceId = workspaceIdSchema.parse(savedWorkspaceId);
29
+ const workspace = new CompanionWorkspace({
30
+ client: runtime,
31
+ localApprovalPolicy: async () => "require-local-confirmation",
32
+ });
33
+ const bridge = new CompanionRelayBridge({
34
+ deviceId: deviceIdSchema.parse(savedDeviceId),
35
+ pairingToken,
36
+ workspaces: new Map([[workspaceId, workspace]]),
37
+ });
38
+ const outbound = new OutboundCompanionRelay({
39
+ bridge,
40
+ connectTransport: openAuthenticatedRelayTransport,
41
+ });
42
+
43
+ outbound.start();
44
+ ```
45
+
46
+ Browser 与后台 Shell lease 由宿主显式接线;Turn 与 Approval lease 由
47
+ `CompanionWorkspace` 自动维护。`require-local-confirmation` 只会以错误拒绝这次远程批准,
48
+ 不会自动显示本机确认 UI。
49
+
50
+ 为 Workspace 配置 `RelayPayloadCipher` 后,Runtime request、response 和 event 必须走
51
+ `runtime.encrypted`。明文 request 不会进入 Runtime,而会收到加密的
52
+ `RELAY_ENCRYPTION_REQUIRED`(`retryable: false`)响应;cipher、密钥管理和 Browser 端
53
+ 实现由宿主提供。
54
+
55
+ ## 文档
56
+
57
+ - [Companion Relay v1 参考](https://github.com/steveoon/roll-agent/blob/main/docs/companion-relay-v1-reference.md)
58
+ - [使用自己的技术栈接入 Roll](https://github.com/steveoon/roll-agent/blob/main/docs/how-to-build-roll-runtime-ui.md)
59
+ - [Runtime Protocol 架构与安全边界](https://github.com/steveoon/roll-agent/blob/main/docs/runtime-protocol-architecture.md)
@@ -0,0 +1,50 @@
1
+ import { type PendingApproval, type RuntimeEventEnvelope, type RuntimeMethod, type RuntimeMethodInput, type RuntimeMethodResult } from "@roll-agent/protocol";
2
+ import type { RollNodeClient } from "@roll-agent/client-node";
3
+ import { CompanionEventBuffer, type CompanionEventBufferOptions, type EventBufferReplay } from "./event-buffer.ts";
4
+ import { WorkspaceLeaseManager } from "./lease-manager.ts";
5
+ import type { RelayRuntimeRequest } from "./relay-protocol.ts";
6
+ export declare const LOCAL_APPROVAL_DECISIONS: readonly ["allow", "deny", "require-local-confirmation"];
7
+ export type LocalApprovalDecision = (typeof LOCAL_APPROVAL_DECISIONS)[number];
8
+ export type LocalApprovalPolicy = (approval: PendingApproval) => LocalApprovalDecision | Promise<LocalApprovalDecision>;
9
+ export interface CompanionRuntimeClient {
10
+ request<TMethod extends RuntimeMethod>(method: TMethod, input: RuntimeMethodInput<TMethod>): Promise<RuntimeMethodResult<TMethod>>;
11
+ onEvent(listener: (event: RuntimeEventEnvelope) => void): () => void;
12
+ close(): void;
13
+ shutdown?(): Promise<unknown>;
14
+ }
15
+ export interface CompanionWorkspaceOptions extends CompanionEventBufferOptions {
16
+ readonly client: CompanionRuntimeClient;
17
+ readonly localApprovalPolicy: LocalApprovalPolicy;
18
+ }
19
+ export declare class LocalApprovalDeniedError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ export declare class LocalConfirmationRequiredError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ export declare class CompanionWorkspace {
26
+ readonly leases: WorkspaceLeaseManager;
27
+ readonly events: CompanionEventBuffer;
28
+ private readonly client;
29
+ private readonly localApprovalPolicy;
30
+ private readonly approvals;
31
+ private readonly eventListeners;
32
+ private readonly releaseClientSubscription;
33
+ constructor(options: CompanionWorkspaceOptions);
34
+ attachBrowser(clientId: string): () => void;
35
+ detachBrowser(clientId: string): boolean;
36
+ acquireBackgroundShellLease(sessionId: string): () => void;
37
+ onBufferedEvent(listener: (entry: {
38
+ readonly relaySequence: number;
39
+ readonly event: RuntimeEventEnvelope;
40
+ }) => void): () => void;
41
+ replay(afterRelaySequence?: number): EventBufferReplay;
42
+ acknowledge(throughRelaySequence: number): void;
43
+ startTurn(input: RuntimeMethodInput<"turn.start">): Promise<RuntimeMethodResult<"turn.start">>;
44
+ respondApproval(input: RuntimeMethodInput<"approval.respond">): Promise<RuntimeMethodResult<"approval.respond">>;
45
+ handleRemoteRequest(request: RelayRuntimeRequest): Promise<unknown>;
46
+ closeIfIdle(): Promise<boolean>;
47
+ private handleEvent;
48
+ private updateLeases;
49
+ }
50
+ export declare function isRollNodeClient(client: CompanionRuntimeClient): client is RollNodeClient;
@@ -0,0 +1 @@
1
+ import{RUNTIME_METHODS as e,parseRuntimeMethodParams as t}from"@roll-agent/protocol";import{CompanionEventBuffer as r}from"./event-buffer.js";import{WorkspaceLeaseManager as a}from"./lease-manager.js";export const LOCAL_APPROVAL_DECISIONS=["allow","deny","require-local-confirmation"];export class LocalApprovalDeniedError extends Error{constructor(e){super(e),this.name="LocalApprovalDeniedError"}}export class LocalConfirmationRequiredError extends Error{constructor(e){super(e),this.name="LocalConfirmationRequiredError"}}export class CompanionWorkspace{leases=new a;events;client;localApprovalPolicy;approvals=new Map;eventListeners=new Set;releaseClientSubscription;constructor(e){this.client=e.client,this.localApprovalPolicy=e.localApprovalPolicy,this.events=new r({...void 0!==e.maxEvents?{maxEvents:e.maxEvents}:{},...void 0!==e.maxBytes?{maxBytes:e.maxBytes}:{}}),this.releaseClientSubscription=this.client.onEvent(e=>this.handleEvent(e))}attachBrowser(e){return this.leases.acquire({kind:"client",id:e})}detachBrowser(e){return this.leases.releaseClient(e)}acquireBackgroundShellLease(e){return this.leases.acquire({kind:"shell-session",id:e})}onBufferedEvent(e){return this.eventListeners.add(e),()=>{this.eventListeners.delete(e)}}replay(e=-1){return this.events.replay(e)}acknowledge(e){this.events.acknowledge(e)}async startTurn(r){const a=t(e.turnStart,r);this.leases.acquire({kind:"turn",id:a.turnId});try{return await this.client.request(e.turnStart,a)}catch(e){throw this.leases.release({kind:"turn",id:a.turnId}),e}}async respondApproval(r){const a=t(e.approvalRespond,r),n=this.approvals.get(a.approvalId);if(void 0===n)throw new LocalApprovalDeniedError(`Approval "${a.approvalId}" is no longer pending`);if("approve"===a.decision){const e=await this.localApprovalPolicy(n);if("deny"===e)throw new LocalApprovalDeniedError("Local Companion policy denied remote approval");if("require-local-confirmation"===e)throw new LocalConfirmationRequiredError("Local confirmation is required before this approval can continue")}const s=await this.client.request(e.approvalRespond,a);return this.approvals.delete(a.approvalId),this.leases.release({kind:"approval",id:a.approvalId}),s}async handleRemoteRequest(r){switch(r.method){case e.initialize:throw new LocalApprovalDeniedError("initialize is owned by the local Companion and cannot be relayed");case e.threadList:return this.client.request(e.threadList,t(e.threadList,r.params));case e.threadCreate:return this.client.request(e.threadCreate,t(e.threadCreate,r.params));case e.threadOpen:return this.client.request(e.threadOpen,t(e.threadOpen,r.params));case e.threadSnapshot:return this.client.request(e.threadSnapshot,t(e.threadSnapshot,r.params));case e.threadRename:return this.client.request(e.threadRename,t(e.threadRename,r.params));case e.threadDelete:return this.client.request(e.threadDelete,t(e.threadDelete,r.params));case e.threadDetach:return this.client.request(e.threadDetach,t(e.threadDetach,r.params));case e.threadCapabilities:return this.client.request(e.threadCapabilities,t(e.threadCapabilities,r.params));case e.turnStart:return this.startTurn(t(e.turnStart,r.params));case e.turnCancel:return this.client.request(e.turnCancel,t(e.turnCancel,r.params));case e.approvalRespond:return this.respondApproval(t(e.approvalRespond,r.params));case e.operationGet:return this.client.request(e.operationGet,t(e.operationGet,r.params))}}async closeIfIdle(){return!!this.leases.canStopRuntime()&&(this.releaseClientSubscription(),void 0===this.client.shutdown?this.client.close():await this.client.shutdown(),!0)}handleEvent(e){this.updateLeases(e);const t=this.events.append(e);for(const e of this.eventListeners)e(t)}updateLeases(e){if(void 0===e.turnId||"turn.completed"!==e.event.type&&"turn.cancelled"!==e.event.type&&"turn.failed"!==e.event.type){if("approval.required"===e.event.type){const t=e.event.approval;this.approvals.set(t.id,t),this.leases.acquire({kind:"approval",id:t.id})}}else{this.leases.release({kind:"turn",id:e.turnId});for(const[t,r]of this.approvals)r.turnId===e.turnId&&(this.approvals.delete(t),this.leases.release({kind:"approval",id:t}))}}}export function isRollNodeClient(e){return"getOutcomeUnknownTurnIds"in e}
@@ -0,0 +1,35 @@
1
+ import type { RuntimeEventEnvelope } from "@roll-agent/protocol";
2
+ export declare const DEFAULT_COMPANION_MAX_EVENTS = 10000;
3
+ export declare const DEFAULT_COMPANION_MAX_BYTES: number;
4
+ export interface BufferedRuntimeEvent {
5
+ readonly relaySequence: number;
6
+ readonly event: RuntimeEventEnvelope;
7
+ }
8
+ export interface EventBufferGap {
9
+ readonly fromRelaySequence: number;
10
+ readonly throughRelaySequence: number;
11
+ }
12
+ export interface EventBufferReplay {
13
+ readonly events: readonly BufferedRuntimeEvent[];
14
+ readonly gap: EventBufferGap | undefined;
15
+ }
16
+ export interface CompanionEventBufferOptions {
17
+ readonly maxEvents?: number;
18
+ readonly maxBytes?: number;
19
+ }
20
+ export declare class CompanionEventBuffer {
21
+ private readonly maxEvents;
22
+ private readonly maxBytes;
23
+ private readonly entries;
24
+ private nextSequence;
25
+ private retainedBytes;
26
+ private droppedThroughSequence;
27
+ constructor(options?: CompanionEventBufferOptions);
28
+ append(event: RuntimeEventEnvelope): BufferedRuntimeEvent;
29
+ replay(afterRelaySequence?: number): EventBufferReplay;
30
+ acknowledge(throughRelaySequence: number): void;
31
+ get size(): number;
32
+ get bytes(): number;
33
+ get highestRelaySequence(): number;
34
+ private enforceLimits;
35
+ }
@@ -0,0 +1 @@
1
+ export const DEFAULT_COMPANION_MAX_EVENTS=1e4;export const DEFAULT_COMPANION_MAX_BYTES=16777216;export class CompanionEventBuffer{maxEvents;maxBytes;entries=[];nextSequence=0;retainedBytes=0;droppedThroughSequence=-1;constructor(e={}){if(this.maxEvents=e.maxEvents??1e4,this.maxBytes=e.maxBytes??16777216,!Number.isInteger(this.maxEvents)||this.maxEvents<1)throw new Error("maxEvents must be a positive integer");if(!Number.isInteger(this.maxBytes)||this.maxBytes<1)throw new Error("maxBytes must be a positive integer")}append(e){const t={relaySequence:this.nextSequence,event:e,bytes:Buffer.byteLength(JSON.stringify(e),"utf8")};return this.nextSequence+=1,this.entries.push(t),this.retainedBytes+=t.bytes,this.enforceLimits(),{relaySequence:t.relaySequence,event:e}}replay(e=-1){if(!Number.isInteger(e)||e<-1)throw new Error("afterRelaySequence must be an integer greater than or equal to -1");const t=e<this.droppedThroughSequence?{fromRelaySequence:e+1,throughRelaySequence:this.droppedThroughSequence}:void 0;return{events:this.entries.filter(t=>t.relaySequence>e).map(({relaySequence:e,event:t})=>({relaySequence:e,event:t})),gap:t}}acknowledge(e){if(!Number.isInteger(e)||e<-1)throw new Error("throughRelaySequence must be an integer greater than or equal to -1");if(!(e>this.highestRelaySequence))for(;void 0!==this.entries[0]&&this.entries[0].relaySequence<=e;){const e=this.entries.shift();void 0!==e&&(this.retainedBytes-=e.bytes,this.droppedThroughSequence=Math.max(this.droppedThroughSequence,e.relaySequence))}}get size(){return this.entries.length}get bytes(){return this.retainedBytes}get highestRelaySequence(){return this.nextSequence-1}enforceLimits(){for(;this.entries.length>this.maxEvents||this.retainedBytes>this.maxBytes&&this.entries.length>0;){const e=this.entries.shift();void 0!==e&&(this.retainedBytes-=e.bytes,this.droppedThroughSequence=e.relaySequence)}}}
@@ -0,0 +1,10 @@
1
+ export { COMPANION_RELAY_PROTOCOL_VERSION, deviceIdSchema, workspaceIdSchema, relayRequestIdSchema, relayDeviceConnectSchema, relayRuntimeRequestSchema, relayRuntimeResponseSchema, relayRuntimeEventSchema, relayAckSchema, relayGapSchema, relayEncryptedMessageSchema, relayMessageSchema, } from "./relay-protocol.ts";
2
+ export type { DeviceId, WorkspaceId, RelayRequestId, RelayMessage, RelayRuntimeRequest, RelayRuntimeResponse, RelayRuntimeEvent, RelayGap, RelayEncryptedMessage, } from "./relay-protocol.ts";
3
+ export { CompanionEventBuffer, DEFAULT_COMPANION_MAX_BYTES, DEFAULT_COMPANION_MAX_EVENTS, } from "./event-buffer.ts";
4
+ export type { BufferedRuntimeEvent, CompanionEventBufferOptions, EventBufferGap, EventBufferReplay, } from "./event-buffer.ts";
5
+ export { COMPANION_LEASE_KINDS, WorkspaceLeaseManager } from "./lease-manager.ts";
6
+ export type { CompanionLease, CompanionLeaseKind } from "./lease-manager.ts";
7
+ export { CompanionWorkspace, LOCAL_APPROVAL_DECISIONS, LocalApprovalDeniedError, LocalConfirmationRequiredError, isRollNodeClient, } from "./companion-workspace.ts";
8
+ export { CompanionRelayBridge, OutboundCompanionRelay, createWebSocketRelayTransport, relayEventMessage, } from "./relay-bridge.ts";
9
+ export type { CompanionRelayBridgeOptions, OutboundCompanionRelayOptions, RelayPayloadCipher, RelayTransport, WebSocketLike, WebSocketMessageEventLike, } from "./relay-bridge.ts";
10
+ export type { CompanionRuntimeClient, CompanionWorkspaceOptions, LocalApprovalDecision, LocalApprovalPolicy, } from "./companion-workspace.ts";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export{COMPANION_RELAY_PROTOCOL_VERSION,deviceIdSchema,workspaceIdSchema,relayRequestIdSchema,relayDeviceConnectSchema,relayRuntimeRequestSchema,relayRuntimeResponseSchema,relayRuntimeEventSchema,relayAckSchema,relayGapSchema,relayEncryptedMessageSchema,relayMessageSchema}from"./relay-protocol.js";export{CompanionEventBuffer,DEFAULT_COMPANION_MAX_BYTES,DEFAULT_COMPANION_MAX_EVENTS}from"./event-buffer.js";export{COMPANION_LEASE_KINDS,WorkspaceLeaseManager}from"./lease-manager.js";export{CompanionWorkspace,LOCAL_APPROVAL_DECISIONS,LocalApprovalDeniedError,LocalConfirmationRequiredError,isRollNodeClient}from"./companion-workspace.js";export{CompanionRelayBridge,OutboundCompanionRelay,createWebSocketRelayTransport,relayEventMessage}from"./relay-bridge.js";
@@ -0,0 +1,15 @@
1
+ export declare const COMPANION_LEASE_KINDS: readonly ["client", "turn", "shell-session", "approval"];
2
+ export type CompanionLeaseKind = (typeof COMPANION_LEASE_KINDS)[number];
3
+ export interface CompanionLease {
4
+ readonly kind: CompanionLeaseKind;
5
+ readonly id: string;
6
+ }
7
+ export declare class WorkspaceLeaseManager {
8
+ private readonly leases;
9
+ acquire(lease: CompanionLease): () => void;
10
+ release(lease: CompanionLease): boolean;
11
+ releaseClient(clientId: string): boolean;
12
+ has(lease: CompanionLease): boolean;
13
+ canStopRuntime(): boolean;
14
+ list(): readonly CompanionLease[];
15
+ }
@@ -0,0 +1 @@
1
+ export const COMPANION_LEASE_KINDS=["client","turn","shell-session","approval"];function e(e){return`${e.kind}:${e.id}`}export class WorkspaceLeaseManager{leases=new Map;acquire(s){const t=e(s);return this.leases.set(t,s),()=>{this.leases.delete(t)}}release(s){return this.leases.delete(e(s))}releaseClient(e){return this.release({kind:"client",id:e})}has(s){return this.leases.has(e(s))}canStopRuntime(){return 0===this.leases.size}list(){return[...this.leases.values()]}}
@@ -0,0 +1,85 @@
1
+ import { type JsonValue, type RuntimeEventEnvelope } from "@roll-agent/protocol";
2
+ import { CompanionWorkspace } from "./companion-workspace.ts";
3
+ import { type DeviceId, type RelayEncryptedMessage, type RelayMessage, type WorkspaceId } from "./relay-protocol.ts";
4
+ export interface RelayTransport {
5
+ send(message: RelayMessage): void | Promise<void>;
6
+ onMessage(listener: (message: unknown) => void): () => void;
7
+ onClose(listener: () => void): () => void;
8
+ close(): void;
9
+ }
10
+ export interface CompanionRelayBridgeOptions {
11
+ readonly deviceId: DeviceId;
12
+ readonly pairingToken: string;
13
+ readonly workspaces: ReadonlyMap<WorkspaceId, CompanionWorkspace>;
14
+ readonly ciphers?: ReadonlyMap<WorkspaceId, RelayPayloadCipher>;
15
+ readonly maxRequestCacheEntries?: number;
16
+ }
17
+ export interface RelayPayloadCipher {
18
+ readonly algorithm: string;
19
+ encrypt(value: JsonValue): Promise<{
20
+ readonly nonce: string;
21
+ readonly ciphertext: string;
22
+ }>;
23
+ decrypt(message: RelayEncryptedMessage): Promise<JsonValue>;
24
+ }
25
+ export declare class CompanionRelayBridge {
26
+ private readonly deviceId;
27
+ private readonly pairingToken;
28
+ private readonly workspaces;
29
+ private readonly ciphers;
30
+ private readonly maxRequestCacheEntries;
31
+ private readonly acknowledged;
32
+ private readonly inFlightRequestCache;
33
+ private readonly settledRequestCache;
34
+ private readonly releaseWorkspaceSubscriptions;
35
+ private transportGeneration;
36
+ private releaseTransportSubscriptions;
37
+ constructor(options: CompanionRelayBridgeOptions);
38
+ connect(transport: RelayTransport): void;
39
+ close(): void;
40
+ private enqueue;
41
+ private enqueueTask;
42
+ private handleMessage;
43
+ private resolveRuntimeRequest;
44
+ private executeRuntimeRequest;
45
+ private settleRequest;
46
+ private sendRuntimeEvent;
47
+ private sendRuntimeResponse;
48
+ private releaseCurrentTransport;
49
+ private markAdvertised;
50
+ }
51
+ export interface WebSocketMessageEventLike {
52
+ readonly data: unknown;
53
+ }
54
+ export interface WebSocketLike {
55
+ send(data: string): void;
56
+ close(): void;
57
+ addEventListener(type: "message", listener: (event: WebSocketMessageEventLike) => void): void;
58
+ addEventListener(type: "close", listener: () => void): void;
59
+ removeEventListener(type: "message", listener: (event: WebSocketMessageEventLike) => void): void;
60
+ removeEventListener(type: "close", listener: () => void): void;
61
+ }
62
+ export declare function createWebSocketRelayTransport(socket: WebSocketLike): RelayTransport;
63
+ export interface OutboundCompanionRelayOptions {
64
+ readonly bridge: CompanionRelayBridge;
65
+ readonly connectTransport: () => Promise<RelayTransport>;
66
+ readonly minReconnectMs?: number;
67
+ readonly maxReconnectMs?: number;
68
+ }
69
+ export declare class OutboundCompanionRelay {
70
+ private readonly bridge;
71
+ private readonly connectTransport;
72
+ private readonly minReconnectMs;
73
+ private readonly maxReconnectMs;
74
+ private reconnectMs;
75
+ private timer;
76
+ private transport;
77
+ private stopped;
78
+ private connecting;
79
+ constructor(options: OutboundCompanionRelayOptions);
80
+ start(): void;
81
+ stop(): void;
82
+ private connect;
83
+ private scheduleReconnect;
84
+ }
85
+ export declare function relayEventMessage(workspaceId: WorkspaceId, relaySequence: number, event: RuntimeEventEnvelope): RelayMessage;
@@ -0,0 +1 @@
1
+ import{createHash as e,randomUUID as t}from"node:crypto";import{RollRpcError as s}from"@roll-agent/client-node";import{RUNTIME_METHODS as r,jsonValueSchema as n}from"@roll-agent/protocol";import{CompanionWorkspace as o}from"./companion-workspace.js";import{COMPANION_RELAY_PROTOCOL_VERSION as i,relayMessageSchema as a,relayRuntimeRequestSchema as c}from"./relay-protocol.js";const h=1e4,p={code:"RELAY_ENCRYPTION_REQUIRED",message:"Encrypted Relay request required for this workspace",retryable:!1},d=new Set([r.threadCreate,r.threadRename,r.threadDelete,r.threadDetach,r.turnStart,r.turnCancel,r.approvalRespond]);function u(t){return e("sha256").update(JSON.stringify({workspaceId:t.workspaceId,method:t.method,params:t.params})).digest("hex")}function l(e){return e instanceof s?{code:e.data?.rollCode??`JSON_RPC_${String(e.code)}`,message:e.message,retryable:e.data?.retryable??!1}:{code:e instanceof Error?e.name:"COMPANION_ERROR",message:e instanceof Error?e.message:String(e),retryable:!1}}export class CompanionRelayBridge{deviceId;pairingToken;workspaces;ciphers;maxRequestCacheEntries;acknowledged=new Map;inFlightRequestCache=new Map;settledRequestCache=new Map;releaseWorkspaceSubscriptions;transportGeneration;releaseTransportSubscriptions=[];constructor(e){if(this.deviceId=e.deviceId,this.pairingToken=e.pairingToken,this.workspaces=e.workspaces,this.ciphers=e.ciphers??new Map,this.maxRequestCacheEntries=e.maxRequestCacheEntries??h,!Number.isInteger(this.maxRequestCacheEntries)||this.maxRequestCacheEntries<1)throw new Error("maxRequestCacheEntries must be a positive integer");this.releaseWorkspaceSubscriptions=[...this.workspaces.entries()].map(([e,t])=>t.onBufferedEvent(t=>{void 0!==this.transportGeneration&&this.sendRuntimeEvent(e,t.relaySequence,t.event)}))}connect(e){this.releaseCurrentTransport(!0);const t={transport:e,advertisedThrough:new Map,queue:Promise.resolve()};this.transportGeneration=t,this.releaseTransportSubscriptions=[e.onMessage(e=>{this.handleMessage(e,t).catch(()=>{})}),e.onClose(()=>{this.transportGeneration===t&&this.releaseCurrentTransport()})],this.enqueue({type:"device.connect",protocolVersion:i,deviceId:this.deviceId,pairingToken:this.pairingToken});for(const[e,t]of this.workspaces){const s=t.replay(this.acknowledged.get(e)??-1);void 0!==s.gap&&this.enqueue({type:"runtime.gap",workspaceId:e,fromRelaySequence:s.gap.fromRelaySequence,throughRelaySequence:s.gap.throughRelaySequence,recovery:"thread.snapshot"});for(const t of s.events)this.sendRuntimeEvent(e,t.relaySequence,t.event)}}close(){this.releaseCurrentTransport(!0);for(const e of this.releaseWorkspaceSubscriptions)e()}enqueue(e){const t=this.transportGeneration;void 0!==t&&this.enqueueTask(t,async()=>{await t.transport.send(e),this.transportGeneration===t&&("runtime.event"===e.type?this.markAdvertised(t,e.workspaceId,e.relaySequence):"runtime.gap"===e.type&&this.markAdvertised(t,e.workspaceId,e.throughRelaySequence))})}enqueueTask(e,t){e.queue=e.queue.then(async()=>{this.transportGeneration===e&&await t()}).catch(()=>{})}async handleMessage(e,t,s=!1){if(this.transportGeneration!==t)return;const r=a.safeParse(e);if(!r.success)return;const n=r.data;if("runtime.encrypted"===n.type){if("request"!==n.payloadKind||void 0===n.requestId)return;const e=this.ciphers.get(n.workspaceId);if(void 0===e)return;try{const s=c.parse(await e.decrypt(n));if(s.workspaceId!==n.workspaceId||s.requestId!==n.requestId)return;await this.handleMessage(s,t,!0)}catch{this.sendRuntimeResponse({type:"runtime.response",requestId:n.requestId,workspaceId:n.workspaceId,error:{code:"ENCRYPTED_PAYLOAD_INVALID",message:"Encrypted Relay payload could not be authenticated or parsed",retryable:!1}})}return}if("runtime.ack"===n.type){const e=this.workspaces.get(n.workspaceId);if(void 0===e||n.throughRelaySequence>(t.advertisedThrough.get(n.workspaceId)??-1))return;const s=this.acknowledged.get(n.workspaceId)??-1;return void(n.throughRelaySequence>s&&(this.acknowledged.set(n.workspaceId,n.throughRelaySequence),e.acknowledge(n.throughRelaySequence)))}if("runtime.request"!==n.type)return;if(!s&&this.ciphers.has(n.workspaceId))return void this.sendRuntimeResponse({type:"runtime.response",requestId:n.requestId,workspaceId:n.workspaceId,error:p});const o=this.workspaces.get(n.workspaceId);if(void 0===o)return void this.sendRuntimeResponse({type:"runtime.response",requestId:n.requestId,workspaceId:n.workspaceId,error:{code:"WORKSPACE_NOT_FOUND",message:"The requested workspace is not paired with this Companion",retryable:!1}});const i=await this.resolveRuntimeRequest(n,o);this.sendRuntimeResponse(i)}resolveRuntimeRequest(e,t){const s=`${e.workspaceId}:${e.requestId}`,r=this.inFlightRequestCache.get(s),n=this.settledRequestCache.get(s),o=r??n;if(void 0!==o){const t=u(e);return o.fingerprint===t?(void 0!==n&&(this.settledRequestCache.delete(s),this.settledRequestCache.set(s,n)),o.response):Promise.resolve({type:"runtime.response",requestId:e.requestId,workspaceId:e.workspaceId,error:{code:"RELAY_REQUEST_ID_CONFLICT",message:"Relay requestId was reused with different method or params",retryable:!1}})}if(!d.has(e.method))return this.executeRuntimeRequest(e,t);const i={fingerprint:u(e),response:this.executeRuntimeRequest(e,t)};return this.inFlightRequestCache.set(s,i),i.response.then(()=>this.settleRequest(s,i),()=>this.settleRequest(s,i)).catch(()=>{}),i.response}async executeRuntimeRequest(e,t){try{const s=await t.handleRemoteRequest(e);return{type:"runtime.response",requestId:e.requestId,workspaceId:e.workspaceId,result:n.parse(s)}}catch(t){return{type:"runtime.response",requestId:e.requestId,workspaceId:e.workspaceId,error:l(t)}}}settleRequest(e,t){if(this.inFlightRequestCache.get(e)===t)for(this.inFlightRequestCache.delete(e),this.settledRequestCache.set(e,t);this.settledRequestCache.size>this.maxRequestCacheEntries;){const e=this.settledRequestCache.keys().next().value;if(void 0===e)return;this.settledRequestCache.delete(e)}}sendRuntimeEvent(e,s,r){const o=this.ciphers.get(e);if(void 0===o)return void this.enqueue({type:"runtime.event",workspaceId:e,relaySequence:s,event:r});const i=this.transportGeneration;void 0!==i&&this.enqueueTask(i,async()=>{const a=await o.encrypt(n.parse(r));this.transportGeneration===i&&(await i.transport.send({type:"runtime.encrypted",workspaceId:e,envelopeId:t(),payloadKind:"event",relaySequence:s,algorithm:o.algorithm,nonce:a.nonce,ciphertext:a.ciphertext}),this.transportGeneration===i&&this.markAdvertised(i,e,s))})}sendRuntimeResponse(e){const s=this.ciphers.get(e.workspaceId);if(void 0===s)return void this.enqueue(e);const r=this.transportGeneration;void 0!==r&&this.enqueueTask(r,async()=>{const o=await s.encrypt(n.parse(e));this.transportGeneration===r&&await r.transport.send({type:"runtime.encrypted",workspaceId:e.workspaceId,envelopeId:t(),payloadKind:"response",requestId:e.requestId,algorithm:s.algorithm,nonce:o.nonce,ciphertext:o.ciphertext})})}releaseCurrentTransport(e=!1){const t=this.transportGeneration;this.transportGeneration=void 0;for(const e of this.releaseTransportSubscriptions)e();this.releaseTransportSubscriptions=[],e&&t?.transport.close()}markAdvertised(e,t,s){e.advertisedThrough.set(t,Math.max(e.advertisedThrough.get(t)??-1,s))}}export function createWebSocketRelayTransport(e){return{send(t){e.send(JSON.stringify(t))},onMessage(t){const s=e=>{if("string"==typeof e.data)try{t(JSON.parse(e.data))}catch{}};return e.addEventListener("message",s),()=>e.removeEventListener("message",s)},onClose:t=>(e.addEventListener("close",t),()=>e.removeEventListener("close",t)),close(){e.close()}}}export class OutboundCompanionRelay{bridge;connectTransport;minReconnectMs;maxReconnectMs;reconnectMs;timer;transport;stopped=!0;connecting=!1;constructor(e){this.bridge=e.bridge,this.connectTransport=e.connectTransport,this.minReconnectMs=e.minReconnectMs??500,this.maxReconnectMs=e.maxReconnectMs??3e4,this.reconnectMs=this.minReconnectMs}start(){this.stopped&&(this.stopped=!1,this.connect().catch(()=>{}))}stop(){this.stopped=!0,void 0!==this.timer&&(clearTimeout(this.timer),this.timer=void 0),this.transport?.close(),this.transport=void 0,this.bridge.close()}async connect(){if(!this.stopped&&!this.connecting){this.connecting=!0;try{const e=await this.connectTransport();if(this.stopped)return void e.close();this.transport=e,this.reconnectMs=this.minReconnectMs,e.onClose(()=>{this.transport===e&&(this.transport=void 0,this.scheduleReconnect())}),this.bridge.connect(e)}catch{this.scheduleReconnect()}finally{this.connecting=!1}}}scheduleReconnect(){if(this.stopped||void 0!==this.timer)return;const e=this.reconnectMs;this.reconnectMs=Math.min(2*this.reconnectMs,this.maxReconnectMs),this.timer=setTimeout(()=>{this.timer=void 0,this.connect().catch(()=>{})},e)}}export function relayEventMessage(e,t,s){return{type:"runtime.event",workspaceId:e,relaySequence:t,event:s}}
@@ -0,0 +1,320 @@
1
+ import { z } from "zod/v4";
2
+ export declare const COMPANION_RELAY_PROTOCOL_VERSION: "1.0";
3
+ export declare const deviceIdSchema: z.core.$ZodBranded<z.ZodString, "DeviceId">;
4
+ export declare const workspaceIdSchema: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
5
+ export declare const relayRequestIdSchema: z.core.$ZodBranded<z.ZodString, "RelayRequestId">;
6
+ export declare const relayDeviceConnectSchema: z.ZodReadonly<z.ZodObject<{
7
+ type: z.ZodLiteral<"device.connect">;
8
+ protocolVersion: z.ZodLiteral<"1.0">;
9
+ deviceId: z.core.$ZodBranded<z.ZodString, "DeviceId">;
10
+ pairingToken: z.ZodString;
11
+ }, z.core.$strict>>;
12
+ export declare const relayRuntimeRequestSchema: z.ZodReadonly<z.ZodObject<{
13
+ type: z.ZodLiteral<"runtime.request">;
14
+ requestId: z.core.$ZodBranded<z.ZodString, "RelayRequestId">;
15
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
16
+ method: z.ZodEnum<{
17
+ initialize: "initialize";
18
+ "thread.list": "thread.list";
19
+ "thread.create": "thread.create";
20
+ "thread.open": "thread.open";
21
+ "thread.snapshot": "thread.snapshot";
22
+ "thread.rename": "thread.rename";
23
+ "thread.delete": "thread.delete";
24
+ "thread.detach": "thread.detach";
25
+ "thread.capabilities": "thread.capabilities";
26
+ "turn.start": "turn.start";
27
+ "turn.cancel": "turn.cancel";
28
+ "approval.respond": "approval.respond";
29
+ "operation.get": "operation.get";
30
+ }>;
31
+ params: z.ZodJSONSchema;
32
+ }, z.core.$strict>>;
33
+ export declare const relayRuntimeResponseSchema: z.ZodReadonly<z.ZodObject<{
34
+ type: z.ZodLiteral<"runtime.response">;
35
+ requestId: z.core.$ZodBranded<z.ZodString, "RelayRequestId">;
36
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
37
+ result: z.ZodOptional<z.ZodJSONSchema>;
38
+ error: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
39
+ code: z.ZodString;
40
+ message: z.ZodString;
41
+ retryable: z.ZodBoolean;
42
+ }, z.core.$strict>>>;
43
+ }, z.core.$strict>>;
44
+ export declare const relayRuntimeEventSchema: z.ZodReadonly<z.ZodObject<{
45
+ type: z.ZodLiteral<"runtime.event">;
46
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
47
+ relaySequence: z.ZodNumber;
48
+ event: z.ZodReadonly<z.ZodObject<{
49
+ protocolVersion: z.ZodLiteral<"1.0">;
50
+ runtimeInstanceId: z.core.$ZodBranded<z.ZodString, "RuntimeInstanceId">;
51
+ sequence: z.ZodNumber;
52
+ timestamp: z.ZodString;
53
+ threadId: z.core.$ZodBranded<z.ZodString, "ThreadId">;
54
+ turnId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "TurnId">>;
55
+ event: z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
56
+ type: z.ZodLiteral<"turn.started">;
57
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
58
+ type: z.ZodLiteral<"message.started">;
59
+ streamId: z.core.$ZodBranded<z.ZodString, "StreamId">;
60
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
61
+ type: z.ZodLiteral<"message.delta">;
62
+ streamId: z.core.$ZodBranded<z.ZodString, "StreamId">;
63
+ delta: z.ZodString;
64
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
65
+ type: z.ZodLiteral<"message.completed">;
66
+ streamId: z.core.$ZodBranded<z.ZodString, "StreamId">;
67
+ text: z.ZodString;
68
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
69
+ type: z.ZodLiteral<"reasoning.summary.delta">;
70
+ reasoningId: z.ZodString;
71
+ delta: z.ZodString;
72
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
73
+ type: z.ZodLiteral<"tool.started">;
74
+ toolCallId: z.ZodString;
75
+ agentName: z.ZodString;
76
+ toolName: z.ZodString;
77
+ input: z.ZodJSONSchema;
78
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
79
+ type: z.ZodLiteral<"tool.output">;
80
+ toolCallId: z.ZodString;
81
+ agentName: z.ZodString;
82
+ toolName: z.ZodString;
83
+ stream: z.ZodEnum<{
84
+ stdout: "stdout";
85
+ stderr: "stderr";
86
+ }>;
87
+ delta: z.ZodString;
88
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
89
+ type: z.ZodLiteral<"tool.completed">;
90
+ toolCallId: z.ZodString;
91
+ agentName: z.ZodString;
92
+ toolName: z.ZodString;
93
+ operationId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "OperationId">>;
94
+ outcome: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
95
+ kind: z.ZodEnum<{
96
+ success: "success";
97
+ user_rejected: "user_rejected";
98
+ policy_denied: "policy_denied";
99
+ invalid_input: "invalid_input";
100
+ cancelled: "cancelled";
101
+ tool_failed: "tool_failed";
102
+ }>;
103
+ reason: z.ZodOptional<z.ZodString>;
104
+ }, z.core.$strict>>>;
105
+ display: z.ZodJSONSchema;
106
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
107
+ type: z.ZodLiteral<"approval.required">;
108
+ approval: z.ZodReadonly<z.ZodObject<{
109
+ id: z.core.$ZodBranded<z.ZodString, "ApprovalId">;
110
+ turnId: z.core.$ZodBranded<z.ZodString, "TurnId">;
111
+ agentName: z.ZodString;
112
+ toolName: z.ZodString;
113
+ preview: z.ZodJSONSchema;
114
+ reason: z.ZodOptional<z.ZodString>;
115
+ }, z.core.$strict>>;
116
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
117
+ type: z.ZodLiteral<"turn.completed">;
118
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
119
+ type: z.ZodLiteral<"turn.cancelled">;
120
+ reason: z.ZodString;
121
+ message: z.ZodString;
122
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
123
+ type: z.ZodLiteral<"turn.failed">;
124
+ stage: z.ZodEnum<{
125
+ bootstrap: "bootstrap";
126
+ plan: "plan";
127
+ execute: "execute";
128
+ }>;
129
+ message: z.ZodString;
130
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
131
+ type: z.ZodLiteral<"capabilities.changed">;
132
+ }, z.core.$strict>>]>;
133
+ }, z.core.$strict>>;
134
+ }, z.core.$strict>>;
135
+ export declare const relayAckSchema: z.ZodReadonly<z.ZodObject<{
136
+ type: z.ZodLiteral<"runtime.ack">;
137
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
138
+ throughRelaySequence: z.ZodNumber;
139
+ }, z.core.$strict>>;
140
+ export declare const relayGapSchema: z.ZodReadonly<z.ZodObject<{
141
+ type: z.ZodLiteral<"runtime.gap">;
142
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
143
+ fromRelaySequence: z.ZodNumber;
144
+ throughRelaySequence: z.ZodNumber;
145
+ recovery: z.ZodLiteral<"thread.snapshot">;
146
+ }, z.core.$strict>>;
147
+ export declare const relayEncryptedMessageSchema: z.ZodReadonly<z.ZodObject<{
148
+ type: z.ZodLiteral<"runtime.encrypted">;
149
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
150
+ envelopeId: z.ZodString;
151
+ payloadKind: z.ZodEnum<{
152
+ event: "event";
153
+ request: "request";
154
+ response: "response";
155
+ }>;
156
+ requestId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "RelayRequestId">>;
157
+ relaySequence: z.ZodOptional<z.ZodNumber>;
158
+ algorithm: z.ZodString;
159
+ nonce: z.ZodString;
160
+ ciphertext: z.ZodString;
161
+ }, z.core.$strict>>;
162
+ export declare const relayMessageSchema: z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
163
+ type: z.ZodLiteral<"device.connect">;
164
+ protocolVersion: z.ZodLiteral<"1.0">;
165
+ deviceId: z.core.$ZodBranded<z.ZodString, "DeviceId">;
166
+ pairingToken: z.ZodString;
167
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
168
+ type: z.ZodLiteral<"runtime.request">;
169
+ requestId: z.core.$ZodBranded<z.ZodString, "RelayRequestId">;
170
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
171
+ method: z.ZodEnum<{
172
+ initialize: "initialize";
173
+ "thread.list": "thread.list";
174
+ "thread.create": "thread.create";
175
+ "thread.open": "thread.open";
176
+ "thread.snapshot": "thread.snapshot";
177
+ "thread.rename": "thread.rename";
178
+ "thread.delete": "thread.delete";
179
+ "thread.detach": "thread.detach";
180
+ "thread.capabilities": "thread.capabilities";
181
+ "turn.start": "turn.start";
182
+ "turn.cancel": "turn.cancel";
183
+ "approval.respond": "approval.respond";
184
+ "operation.get": "operation.get";
185
+ }>;
186
+ params: z.ZodJSONSchema;
187
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
188
+ type: z.ZodLiteral<"runtime.response">;
189
+ requestId: z.core.$ZodBranded<z.ZodString, "RelayRequestId">;
190
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
191
+ result: z.ZodOptional<z.ZodJSONSchema>;
192
+ error: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
193
+ code: z.ZodString;
194
+ message: z.ZodString;
195
+ retryable: z.ZodBoolean;
196
+ }, z.core.$strict>>>;
197
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
198
+ type: z.ZodLiteral<"runtime.event">;
199
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
200
+ relaySequence: z.ZodNumber;
201
+ event: z.ZodReadonly<z.ZodObject<{
202
+ protocolVersion: z.ZodLiteral<"1.0">;
203
+ runtimeInstanceId: z.core.$ZodBranded<z.ZodString, "RuntimeInstanceId">;
204
+ sequence: z.ZodNumber;
205
+ timestamp: z.ZodString;
206
+ threadId: z.core.$ZodBranded<z.ZodString, "ThreadId">;
207
+ turnId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "TurnId">>;
208
+ event: z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
209
+ type: z.ZodLiteral<"turn.started">;
210
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
211
+ type: z.ZodLiteral<"message.started">;
212
+ streamId: z.core.$ZodBranded<z.ZodString, "StreamId">;
213
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
214
+ type: z.ZodLiteral<"message.delta">;
215
+ streamId: z.core.$ZodBranded<z.ZodString, "StreamId">;
216
+ delta: z.ZodString;
217
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
218
+ type: z.ZodLiteral<"message.completed">;
219
+ streamId: z.core.$ZodBranded<z.ZodString, "StreamId">;
220
+ text: z.ZodString;
221
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
222
+ type: z.ZodLiteral<"reasoning.summary.delta">;
223
+ reasoningId: z.ZodString;
224
+ delta: z.ZodString;
225
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
226
+ type: z.ZodLiteral<"tool.started">;
227
+ toolCallId: z.ZodString;
228
+ agentName: z.ZodString;
229
+ toolName: z.ZodString;
230
+ input: z.ZodJSONSchema;
231
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
232
+ type: z.ZodLiteral<"tool.output">;
233
+ toolCallId: z.ZodString;
234
+ agentName: z.ZodString;
235
+ toolName: z.ZodString;
236
+ stream: z.ZodEnum<{
237
+ stdout: "stdout";
238
+ stderr: "stderr";
239
+ }>;
240
+ delta: z.ZodString;
241
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
242
+ type: z.ZodLiteral<"tool.completed">;
243
+ toolCallId: z.ZodString;
244
+ agentName: z.ZodString;
245
+ toolName: z.ZodString;
246
+ operationId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "OperationId">>;
247
+ outcome: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
248
+ kind: z.ZodEnum<{
249
+ success: "success";
250
+ user_rejected: "user_rejected";
251
+ policy_denied: "policy_denied";
252
+ invalid_input: "invalid_input";
253
+ cancelled: "cancelled";
254
+ tool_failed: "tool_failed";
255
+ }>;
256
+ reason: z.ZodOptional<z.ZodString>;
257
+ }, z.core.$strict>>>;
258
+ display: z.ZodJSONSchema;
259
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
260
+ type: z.ZodLiteral<"approval.required">;
261
+ approval: z.ZodReadonly<z.ZodObject<{
262
+ id: z.core.$ZodBranded<z.ZodString, "ApprovalId">;
263
+ turnId: z.core.$ZodBranded<z.ZodString, "TurnId">;
264
+ agentName: z.ZodString;
265
+ toolName: z.ZodString;
266
+ preview: z.ZodJSONSchema;
267
+ reason: z.ZodOptional<z.ZodString>;
268
+ }, z.core.$strict>>;
269
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
270
+ type: z.ZodLiteral<"turn.completed">;
271
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
272
+ type: z.ZodLiteral<"turn.cancelled">;
273
+ reason: z.ZodString;
274
+ message: z.ZodString;
275
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
276
+ type: z.ZodLiteral<"turn.failed">;
277
+ stage: z.ZodEnum<{
278
+ bootstrap: "bootstrap";
279
+ plan: "plan";
280
+ execute: "execute";
281
+ }>;
282
+ message: z.ZodString;
283
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
284
+ type: z.ZodLiteral<"capabilities.changed">;
285
+ }, z.core.$strict>>]>;
286
+ }, z.core.$strict>>;
287
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
288
+ type: z.ZodLiteral<"runtime.ack">;
289
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
290
+ throughRelaySequence: z.ZodNumber;
291
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
292
+ type: z.ZodLiteral<"runtime.gap">;
293
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
294
+ fromRelaySequence: z.ZodNumber;
295
+ throughRelaySequence: z.ZodNumber;
296
+ recovery: z.ZodLiteral<"thread.snapshot">;
297
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
298
+ type: z.ZodLiteral<"runtime.encrypted">;
299
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
300
+ envelopeId: z.ZodString;
301
+ payloadKind: z.ZodEnum<{
302
+ event: "event";
303
+ request: "request";
304
+ response: "response";
305
+ }>;
306
+ requestId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "RelayRequestId">>;
307
+ relaySequence: z.ZodOptional<z.ZodNumber>;
308
+ algorithm: z.ZodString;
309
+ nonce: z.ZodString;
310
+ ciphertext: z.ZodString;
311
+ }, z.core.$strict>>]>;
312
+ export type DeviceId = z.infer<typeof deviceIdSchema>;
313
+ export type WorkspaceId = z.infer<typeof workspaceIdSchema>;
314
+ export type RelayRequestId = z.infer<typeof relayRequestIdSchema>;
315
+ export type RelayMessage = z.infer<typeof relayMessageSchema>;
316
+ export type RelayRuntimeRequest = z.infer<typeof relayRuntimeRequestSchema>;
317
+ export type RelayRuntimeResponse = z.infer<typeof relayRuntimeResponseSchema>;
318
+ export type RelayRuntimeEvent = z.infer<typeof relayRuntimeEventSchema>;
319
+ export type RelayGap = z.infer<typeof relayGapSchema>;
320
+ export type RelayEncryptedMessage = z.infer<typeof relayEncryptedMessageSchema>;
@@ -0,0 +1 @@
1
+ import{z as e}from"zod/v4";import{RUNTIME_METHODS as t,jsonValueSchema as r,runtimeEventEnvelopeSchema as n}from"@roll-agent/protocol";export const COMPANION_RELAY_PROTOCOL_VERSION="1.0";export const deviceIdSchema=e.string().uuid().brand();export const workspaceIdSchema=e.string().uuid().brand();export const relayRequestIdSchema=e.string().uuid().brand();export const relayDeviceConnectSchema=e.object({type:e.literal("device.connect"),protocolVersion:e.literal("1.0"),deviceId:deviceIdSchema,pairingToken:e.string().min(16)}).strict().readonly();export const relayRuntimeRequestSchema=e.object({type:e.literal("runtime.request"),requestId:relayRequestIdSchema,workspaceId:workspaceIdSchema,method:e.enum(Object.values(t)),params:r}).strict().readonly();export const relayRuntimeResponseSchema=e.object({type:e.literal("runtime.response"),requestId:relayRequestIdSchema,workspaceId:workspaceIdSchema,result:r.optional(),error:e.object({code:e.string().min(1),message:e.string(),retryable:e.boolean()}).strict().readonly().optional()}).strict().readonly();export const relayRuntimeEventSchema=e.object({type:e.literal("runtime.event"),workspaceId:workspaceIdSchema,relaySequence:e.number().int().nonnegative(),event:n}).strict().readonly();export const relayAckSchema=e.object({type:e.literal("runtime.ack"),workspaceId:workspaceIdSchema,throughRelaySequence:e.number().int().min(-1)}).strict().readonly();export const relayGapSchema=e.object({type:e.literal("runtime.gap"),workspaceId:workspaceIdSchema,fromRelaySequence:e.number().int().nonnegative(),throughRelaySequence:e.number().int().nonnegative(),recovery:e.literal("thread.snapshot")}).strict().readonly();export const relayEncryptedMessageSchema=e.object({type:e.literal("runtime.encrypted"),workspaceId:workspaceIdSchema,envelopeId:e.string().uuid(),payloadKind:e.enum(["request","response","event"]),requestId:relayRequestIdSchema.optional(),relaySequence:e.number().int().nonnegative().optional(),algorithm:e.string().min(1),nonce:e.string().min(1),ciphertext:e.string().min(1)}).strict().readonly();export const relayMessageSchema=e.discriminatedUnion("type",[relayDeviceConnectSchema,relayRuntimeRequestSchema,relayRuntimeResponseSchema,relayRuntimeEventSchema,relayAckSchema,relayGapSchema,relayEncryptedMessageSchema]);
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@roll-agent/companion",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/steveoon/roll-agent"
8
+ },
9
+ "homepage": "https://github.com/steveoon/roll-agent",
10
+ "bugs": {
11
+ "url": "https://github.com/steveoon/roll-agent/issues"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22.6.0"
27
+ },
28
+ "dependencies": {
29
+ "zod": "3.25.76",
30
+ "@roll-agent/client-node": "0.0.0",
31
+ "@roll-agent/protocol": "0.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0"
35
+ },
36
+ "scripts": {
37
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/obfuscate.mjs",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "node --experimental-strip-types --test \"src/**/*.test.ts\"",
40
+ "clean": "rm -rf dist"
41
+ }
42
+ }