@roll-agent/companion 0.1.0 → 0.2.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 CHANGED
@@ -1,33 +1,54 @@
1
1
  # `@roll-agent/companion`
2
2
 
3
- 连接云端 Web UI 与用户本机 Roll 的 Local Companion / Relay 基础库。
3
+ 连接云端 Web UI 与用户本机 Roll 的 Local Companion / Relay bridge 基础库。
4
4
 
5
- 本包提供 Relay 消息 Schema、Workspace 生命周期 lease、事件 ACK/gap 缓冲、mutation
6
- 去重、出站重连和可插拔的 Workspace payload cipher。它不包含生产 Cloud Relay Server、
7
- Browser SDK、账号/设备存储、TLS、鉴权授权、本地确认 UI、HA 或监控。
5
+ 本包只安装在用户本机的 Node.js Host 中,例如 Remote-enabled Electron Main 或独立
6
+ Companion Daemon。Browser Web App、Cloud Relay Server、普通 `roll chat` 和只做本地 GUI
7
+ 的 Desktop App 都不安装它。
8
8
 
9
- ## 安装
9
+ 本包消费 `@roll-agent/relay-protocol` 的 Relay Wire 契约,提供 Workspace 生命周期
10
+ lease、本地 Approval Policy、事件 ACK/gap 缓冲、mutation 去重、出站重连和可插拔的
11
+ Workspace payload cipher。Wire Schema、ID、JSON Schema、fixtures 与版本注册表由
12
+ `@roll-agent/relay-protocol` 唯一维护;本包不再定义另一套协议。
13
+
14
+ 本包不包含生产 Cloud Relay Server、Browser SDK、账号/设备存储、TLS、鉴权授权、本地确认
15
+ UI、HA 或监控,也不提供 CLI、daemon 或自动启动机制。`npm install` 与
16
+ `npm install -g @roll-agent/core` 都不会启动 Companion;宿主必须在用户显式启用远程访问
17
+ 后完成配对,并显式启动 `OutboundCompanionRelay` 或向 `CompanionRelayBridge` 绑定
18
+ Transport。
19
+
20
+ ## 在 Local Companion Host 中安装
10
21
 
11
22
  ```bash
12
- pnpm add @roll-agent/companion @roll-agent/client-node
23
+ pnpm add @roll-agent/companion @roll-agent/client-node @roll-agent/relay-protocol
13
24
  ```
14
25
 
15
26
  ## 最小骨架
16
27
 
28
+ 下面是宿主接线骨架,不是可直接运行的完整 Companion 应用。`workspacePath`、设备/Workspace
29
+ 身份、`pairingToken`、认证 Transport、本机确认 UI 和进程生命周期都必须由宿主提供。
30
+
17
31
  ```ts
18
32
  import { RollNodeClient } from "@roll-agent/client-node";
19
33
  import {
34
+ CompanionApprovalRequestBroker,
20
35
  CompanionRelayBridge,
21
36
  CompanionWorkspace,
22
37
  OutboundCompanionRelay,
23
- deviceIdSchema,
24
- workspaceIdSchema,
25
38
  } from "@roll-agent/companion";
39
+ import { deviceIdSchema, workspaceIdSchema } from "@roll-agent/relay-protocol";
26
40
 
27
- const runtime = await RollNodeClient.start({ cwd: workspacePath });
41
+ const approvalRequestBroker = new CompanionApprovalRequestBroker();
42
+ const runtime = await RollNodeClient.start({
43
+ cwd: workspacePath,
44
+ serverRequestHandlers: {
45
+ "approval.request": approvalRequestBroker.handle,
46
+ },
47
+ });
28
48
  const workspaceId = workspaceIdSchema.parse(savedWorkspaceId);
29
49
  const workspace = new CompanionWorkspace({
30
50
  client: runtime,
51
+ approvalRequestBroker,
31
52
  localApprovalPolicy: async () => "require-local-confirmation",
32
53
  });
33
54
  const bridge = new CompanionRelayBridge({
@@ -43,9 +64,33 @@ const outbound = new OutboundCompanionRelay({
43
64
  outbound.start();
44
65
  ```
45
66
 
46
- Browser 与后台 Shell lease 由宿主显式接线;Turn 与 Approval lease 由
47
- `CompanionWorkspace` 自动维护。`require-local-confirmation` 只会以错误拒绝这次远程批准,
48
- 不会自动显示本机确认 UI。
67
+ Browser 与后台 Shell lease 由宿主显式接线;Turn lease 由 `CompanionWorkspace` 管理,
68
+ `"1.1"` Approval lease 由 `CompanionApprovalRequestBroker` 的 Handler 生命周期管理。
69
+ Browser 通过 Relay 专属 `approval.candidate` 提交候选决定;成功响应
70
+ `{ accepted: true }` 只表示本地 Broker 已接受候选,权威终态必须等待有序的
71
+ `approval.resolved` Event。候选批准必须重新经过本地 Policy,候选拒绝可以抢占仍在
72
+ 等待 Policy 的批准并以 deny-wins 收敛;并发批准仍会被拒绝。
73
+ `require-local-confirmation` 只会以错误拒绝这次远程批准,不会自动显示本机确认 UI。
74
+ Policy 会收到 `{ signal }`,Runtime 取消时该信号同步 abort。返回值严格校验为
75
+ `allow | deny | require-local-confirmation`;JavaScript Host 返回其他值时 fail-closed,
76
+ 不会把批准转发给 Runtime。未知本地异常对 Relay 统一脱敏为
77
+ `COMPANION_ERROR / Companion request failed`。
78
+
79
+ `approval.candidate` 的参数只有 `threadId`、`turnId`、`approvalId`、`decision` 与可选
80
+ `reason`,不复用 Runtime mutation 的 UUID `requestId`。Relay envelope 自己的
81
+ `requestId` 只负责重投和响应缓存。`"1.0"` 兼容连接继续走
82
+ `approval.required` + `approval.respond`;`"1.1"` 上调用后者会 fail closed。
83
+ 这里的 `"1.0"` / `"1.1"` 指 `RuntimeEventEnvelope.protocolVersion`,不是外层
84
+ Companion Relay `"1.0"`。Browser 必须按每个 Runtime Event 的协议版本选择审批方法:
85
+ Runtime `"1.0"` 走 `approval.respond`,Runtime `"1.1"` 才走 `approval.candidate`;
86
+ 滚动升级时不能仅凭 Relay 版本推断该能力。
87
+
88
+ 当前 `@roll-agent/relay-protocol` 只冻结已经存在的 Companion Relay Wire `"1.0"`。
89
+ `approval.candidate` 仍是该版本中的 Approval 专属候选方法;通用 typed interaction
90
+ request/result/cancelled 以及逻辑 `interactionId` 不属于当前 `"1.0"`,由
91
+ [#184](https://github.com/steveoon/roll-agent/issues/184)、
92
+ [#187](https://github.com/steveoon/roll-agent/issues/187) 和后续 Relay Wire version
93
+ 继续设计。
49
94
 
50
95
  为 Workspace 配置 `RelayPayloadCipher` 后,Runtime request、response 和 event 必须走
51
96
  `runtime.encrypted`。明文 request 不会进入 Runtime,而会收到加密的
@@ -54,6 +99,6 @@ Browser 与后台 Shell lease 由宿主显式接线;Turn 与 Approval lease
54
99
 
55
100
  ## 文档
56
101
 
57
- - [Companion Relay v1 参考](https://github.com/steveoon/roll-agent/blob/main/docs/companion-relay-v1-reference.md)
102
+ - [`@roll-agent/relay-protocol` Relay v1 参考](https://github.com/steveoon/roll-agent/blob/main/docs/companion-relay-v1-reference.md)
58
103
  - [使用自己的技术栈接入 Roll](https://github.com/steveoon/roll-agent/blob/main/docs/how-to-build-roll-runtime-ui.md)
59
104
  - [Runtime Protocol 架构与安全边界](https://github.com/steveoon/roll-agent/blob/main/docs/runtime-protocol-architecture.md)
@@ -1,20 +1,27 @@
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";
1
+ import { RUNTIME_SERVER_REQUEST_METHODS, type InitializeResult, type PendingApproval, type RuntimeEventEnvelope, type RuntimeMethod, type RuntimeMethodInput, type RuntimeMethodResult } from "@roll-agent/protocol";
2
+ import type { RollNodeClient, RuntimeServerRequestHandler } from "@roll-agent/client-node";
3
+ import { z } from "zod";
3
4
  import { CompanionEventBuffer, type CompanionEventBufferOptions, type EventBufferReplay } from "./event-buffer.ts";
4
5
  import { WorkspaceLeaseManager } from "./lease-manager.ts";
5
- import type { RelayRuntimeRequest } from "./relay-protocol.ts";
6
+ import { type RelayApprovalCandidateInput, type RelayApprovalCandidateParams, type RelayApprovalCandidateResult, type RelayRequestMethod, type RelayRuntimeRequest } from "@roll-agent/relay-protocol";
6
7
  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>;
8
+ export declare const localApprovalDecisionSchema: z.ZodEnum<["allow", "deny", "require-local-confirmation"]>;
9
+ export type LocalApprovalDecision = z.infer<typeof localApprovalDecisionSchema>;
10
+ export interface LocalApprovalPolicyContext {
11
+ readonly signal: AbortSignal;
12
+ }
13
+ export type LocalApprovalPolicy = (approval: PendingApproval, context: LocalApprovalPolicyContext) => LocalApprovalDecision | Promise<LocalApprovalDecision>;
9
14
  export interface CompanionRuntimeClient {
10
15
  request<TMethod extends RuntimeMethod>(method: TMethod, input: RuntimeMethodInput<TMethod>): Promise<RuntimeMethodResult<TMethod>>;
11
16
  onEvent(listener: (event: RuntimeEventEnvelope) => void): () => void;
17
+ getInitializationResult?(): Pick<InitializeResult, "protocolVersion">;
12
18
  close(): void;
13
19
  shutdown?(): Promise<unknown>;
14
20
  }
15
21
  export interface CompanionWorkspaceOptions extends CompanionEventBufferOptions {
16
22
  readonly client: CompanionRuntimeClient;
17
23
  readonly localApprovalPolicy: LocalApprovalPolicy;
24
+ readonly approvalRequestBroker?: CompanionApprovalRequestBroker;
18
25
  }
19
26
  export declare class LocalApprovalDeniedError extends Error {
20
27
  constructor(message: string);
@@ -22,14 +29,34 @@ export declare class LocalApprovalDeniedError extends Error {
22
29
  export declare class LocalConfirmationRequiredError extends Error {
23
30
  constructor(message: string);
24
31
  }
32
+ export declare class InvalidRelayRequestParamsError extends Error {
33
+ readonly method: RelayRequestMethod;
34
+ constructor(method: RelayRequestMethod);
35
+ }
36
+ export declare class CompanionApprovalRequestBroker {
37
+ readonly leases: WorkspaceLeaseManager;
38
+ readonly handle: RuntimeServerRequestHandler<typeof RUNTIME_SERVER_REQUEST_METHODS.approvalRequest>;
39
+ private readonly pendingRequests;
40
+ private localApprovalPolicy;
41
+ bindLocalApprovalPolicy(policy: LocalApprovalPolicy): () => void;
42
+ submitCandidate(params: RelayApprovalCandidateParams): Promise<RelayApprovalCandidateResult>;
43
+ private handleApprovalRequest;
44
+ private assertStillPending;
45
+ private resolvePending;
46
+ private rejectPending;
47
+ }
25
48
  export declare class CompanionWorkspace {
26
49
  readonly leases: WorkspaceLeaseManager;
27
50
  readonly events: CompanionEventBuffer;
28
51
  private readonly client;
29
52
  private readonly localApprovalPolicy;
53
+ private readonly protocolCapabilities;
54
+ private readonly approvalRequestBroker;
30
55
  private readonly approvals;
31
56
  private readonly eventListeners;
32
57
  private readonly releaseClientSubscription;
58
+ private readonly releaseApprovalPolicyBinding;
59
+ private closed;
33
60
  constructor(options: CompanionWorkspaceOptions);
34
61
  attachBrowser(clientId: string): () => void;
35
62
  detachBrowser(clientId: string): boolean;
@@ -42,6 +69,7 @@ export declare class CompanionWorkspace {
42
69
  acknowledge(throughRelaySequence: number): void;
43
70
  startTurn(input: RuntimeMethodInput<"turn.start">): Promise<RuntimeMethodResult<"turn.start">>;
44
71
  respondApproval(input: RuntimeMethodInput<"approval.respond">): Promise<RuntimeMethodResult<"approval.respond">>;
72
+ submitApprovalCandidate(input: RelayApprovalCandidateInput): Promise<RelayApprovalCandidateResult>;
45
73
  handleRemoteRequest(request: RelayRuntimeRequest): Promise<unknown>;
46
74
  closeIfIdle(): Promise<boolean>;
47
75
  private handleEvent;
@@ -1 +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}
1
+ import{RUNTIME_METHODS as e,RUNTIME_SERVER_REQUEST_METHODS as r,getRuntimeProtocolCapabilities as a,parseRuntimeMethodParams as t}from"@roll-agent/protocol";import{z as o}from"zod";import{CompanionEventBuffer as n}from"./event-buffer.js";import{WorkspaceLeaseManager as i}from"./lease-manager.js";import{RELAY_REQUEST_METHODS as s,relayApprovalCandidateParamsSchema as l}from"@roll-agent/relay-protocol";export const LOCAL_APPROVAL_DECISIONS=["allow","deny","require-local-confirmation"];export const localApprovalDecisionSchema=o.enum(LOCAL_APPROVAL_DECISIONS);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 InvalidRelayRequestParamsError extends Error{method;constructor(e){super(`Invalid params for Relay method "${e}"`),this.name="InvalidRelayRequestParamsError",this.method=e}}function p(e,r){try{return t(e,r)}catch{throw new InvalidRelayRequestParamsError(e)}}function c(e){const r=l.safeParse(e);if(!r.success)throw new InvalidRelayRequestParamsError(s.approvalCandidate);return r.data}function d(e){const r=localApprovalDecisionSchema.safeParse(e);if(!r.success)throw new LocalApprovalDeniedError("Local Companion policy returned an invalid approval decision");return r.data}function v(e){return e instanceof Error?e:new Error(void 0===e?"Runtime cancelled the approval request":String(e))}async function h(e,r,a){if(r.aborted)throw v(r.reason);let t=()=>{};const o=new Promise((e,r)=>{t=r}),n=()=>{t(v(r.reason))};r.addEventListener("abort",n,{once:!0});try{const t=Promise.resolve().then(()=>{if(r.aborted)throw v(r.reason);return a(e,{signal:r})});let n;try{n=await Promise.race([t,o])}catch{if(r.aborted)throw v(r.reason);throw new Error("Local Companion policy failed")}return d(n)}finally{r.removeEventListener("abort",n)}}export class CompanionApprovalRequestBroker{leases=new i;handle=(e,r)=>this.handleApprovalRequest(e,r);pendingRequests=new Map;localApprovalPolicy;bindLocalApprovalPolicy(e){if(void 0!==this.localApprovalPolicy)throw new Error("Companion approval request broker is already bound to a workspace");this.localApprovalPolicy=e;let r=!1;return()=>{r||(r=!0,this.localApprovalPolicy===e&&(this.localApprovalPolicy=void 0))}}async submitCandidate(e){const r=this.pendingRequests.get(e.approvalId);if(void 0===r)throw new LocalApprovalDeniedError(`Approval "${e.approvalId}" is no longer pending`);if(r.params.threadId!==e.threadId||r.params.approval.turnId!==e.turnId)throw new LocalApprovalDeniedError(`Approval "${e.approvalId}" does not belong to the requested thread and turn`);if("reject"===e.decision)return r.activeApproveController?.abort(new LocalApprovalDeniedError(`Approval "${e.approvalId}" approval candidate was superseded by rejection`)),this.assertStillPending(r),this.resolvePending(r,{decision:"reject",...void 0!==e.reason?{reason:e.reason}:{}}),{accepted:!0};if(void 0!==r.activeApproveController)throw new LocalApprovalDeniedError(`Approval "${e.approvalId}" already has an approval candidate in progress`);const a=new AbortController;r.activeApproveController=a;try{const e=this.localApprovalPolicy;if(void 0===e)throw new LocalApprovalDeniedError("Companion approval request broker is not bound to a local policy");const t=await h(r.params.approval,AbortSignal.any([r.signal,a.signal]),e);if(this.assertStillPending(r),"deny"===t)throw this.resolvePending(r,{decision:"reject",reason:"Local Companion policy denied remote approval"}),new LocalApprovalDeniedError("Local Companion policy denied remote approval");if("require-local-confirmation"===t)throw new LocalConfirmationRequiredError("Local confirmation is required before this approval can continue");return this.assertStillPending(r),this.resolvePending(r,{decision:"approve"}),{accepted:!0}}catch(a){throw a instanceof LocalConfirmationRequiredError||this.pendingRequests.get(e.approvalId)!==r||this.rejectPending(r,a instanceof Error?a:new LocalApprovalDeniedError("Local Companion policy failed")),a}finally{r.activeApproveController===a&&(r.activeApproveController=void 0)}}handleApprovalRequest(e,r){if(r.signal.aborted)return Promise.reject(v(r.signal.reason));const a=this.pendingRequests.get(e.approval.id);if(void 0!==a)return Promise.reject(new Error(`Approval "${e.approval.id}" is already pending for Runtime request `+JSON.stringify(a.requestId)));let t=()=>{},o=()=>{};const n=new Promise((e,r)=>{t=e,o=r}),i={params:e,requestId:r.requestId,signal:r.signal,resolve:t,reject:o,releaseLease:this.leases.acquire({kind:"approval",id:e.approval.id}),activeApproveController:void 0},s=()=>{this.rejectPending(i,v(r.signal.reason))};return this.pendingRequests.set(e.approval.id,i),r.signal.addEventListener("abort",s,{once:!0}),r.signal.aborted&&s(),n.finally(()=>{r.signal.removeEventListener("abort",s)})}assertStillPending(e){if(this.pendingRequests.get(e.params.approval.id)!==e)throw new LocalApprovalDeniedError(`Approval "${e.params.approval.id}" is no longer pending`)}resolvePending(e,r){this.pendingRequests.delete(e.params.approval.id),e.releaseLease(),e.resolve(r)}rejectPending(e,r){this.pendingRequests.get(e.params.approval.id)===e&&(this.pendingRequests.delete(e.params.approval.id),e.releaseLease(),e.reject(r))}}export class CompanionWorkspace{leases;events;client;localApprovalPolicy;protocolCapabilities;approvalRequestBroker;approvals=new Map;eventListeners=new Set;releaseClientSubscription;releaseApprovalPolicyBinding;closed=!1;constructor(e){if(this.client=e.client,this.localApprovalPolicy=e.localApprovalPolicy,this.protocolCapabilities=a(this.client.getInitializationResult?.().protocolVersion??"1.0"),this.approvalRequestBroker=e.approvalRequestBroker,this.protocolCapabilities.serverRequests&&void 0===this.approvalRequestBroker)throw new Error("The negotiated Runtime Protocol requires a CompanionApprovalRequestBroker registered during client initialization");this.events=new n({...void 0!==e.maxEvents?{maxEvents:e.maxEvents}:{},...void 0!==e.maxBytes?{maxBytes:e.maxBytes}:{}}),this.leases=this.approvalRequestBroker?.leases??new i;const r=this.approvalRequestBroker?.bindLocalApprovalPolicy(this.localApprovalPolicy);try{this.releaseClientSubscription=this.client.onEvent(e=>this.handleEvent(e))}catch(e){throw r?.(),e}this.releaseApprovalPolicyBinding=r}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),o=this.leases.acquire({kind:"turn",id:a.turnId});try{return await this.client.request(e.turnStart,a)}catch(e){throw o(),e}}async respondApproval(r){const a=t(e.approvalRespond,r);if(!this.protocolCapabilities.clientApprovalResponses)throw new LocalApprovalDeniedError("The negotiated Runtime Protocol must use the Relay approval.candidate method");const o=this.approvals.get(a.approvalId);if(void 0===o)throw new LocalApprovalDeniedError(`Approval "${a.approvalId}" is no longer pending`);if("approve"===a.decision){const e=await h(o.approval,o.controller.signal,this.localApprovalPolicy);if(o.controller.signal.aborted||this.approvals.get(a.approvalId)!==o)throw v(o.controller.signal.reason);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 n=await this.client.request(e.approvalRespond,a);return this.approvals.get(a.approvalId)===o&&(o.controller.abort(new LocalApprovalDeniedError(`Approval "${a.approvalId}" was resolved by another response`)),this.approvals.delete(a.approvalId),this.leases.release({kind:"approval",id:a.approvalId})),n}async submitApprovalCandidate(e){const r=c(e);if(!this.protocolCapabilities.serverRequests)throw new LocalApprovalDeniedError("Relay approval.candidate requires Runtime server-request capability");const a=this.approvalRequestBroker;if(void 0===a)throw new LocalApprovalDeniedError("Runtime server-request approval broker is unavailable");return a.submitCandidate(r)}async handleRemoteRequest(r){switch(r.method){case s.approvalCandidate:return this.submitApprovalCandidate(c(r.params));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,p(e.threadList,r.params));case e.threadCreate:return this.client.request(e.threadCreate,p(e.threadCreate,r.params));case e.threadOpen:return this.client.request(e.threadOpen,p(e.threadOpen,r.params));case e.threadSnapshot:return this.client.request(e.threadSnapshot,p(e.threadSnapshot,r.params));case e.threadRename:return this.client.request(e.threadRename,p(e.threadRename,r.params));case e.threadDelete:return this.client.request(e.threadDelete,p(e.threadDelete,r.params));case e.threadDetach:return this.client.request(e.threadDetach,p(e.threadDetach,r.params));case e.threadCapabilities:return this.client.request(e.threadCapabilities,p(e.threadCapabilities,r.params));case e.turnStart:return this.startTurn(p(e.turnStart,r.params));case e.turnCancel:return this.client.request(e.turnCancel,p(e.turnCancel,r.params));case e.approvalRespond:return this.respondApproval(p(e.approvalRespond,r.params));case e.operationGet:return this.client.request(e.operationGet,p(e.operationGet,r.params))}}async closeIfIdle(){if(this.closed)return!0;if(!this.leases.canStopRuntime())return!1;this.closed=!0;try{this.releaseClientSubscription()}finally{this.releaseApprovalPolicyBinding?.()}return void 0===this.client.shutdown?this.client.close():await this.client.shutdown(),!0}handleEvent(e){this.updateLeases(e);const r=this.events.append(e);for(const e of this.eventListeners)try{e(r)}catch{}}updateLeases(e){if(void 0===e.turnId||"turn.completed"!==e.event.type&&"turn.cancelled"!==e.event.type&&"turn.failed"!==e.event.type){if(this.protocolCapabilities.clientApprovalResponses&&"approval.required"===e.event.type){const r=e.event.approval,a=this.approvals.get(r.id);a?.controller.abort(new Error(`Approval "${r.id}" was superseded`)),this.approvals.set(r.id,{approval:r,controller:new AbortController}),this.leases.acquire({kind:"approval",id:r.id})}}else if(this.leases.release({kind:"turn",id:e.turnId}),this.protocolCapabilities.clientApprovalResponses)for(const[r,a]of this.approvals)a.approval.turnId===e.turnId&&(this.approvals.delete(r),a.controller.abort(new Error(`Turn ended with ${e.event.type}`)),this.leases.release({kind:"approval",id:r}))}}export function isRollNodeClient(e){return"getOutcomeUnknownTurnIds"in e}
package/dist/index.d.ts CHANGED
@@ -1,10 +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";
1
+ export { COMPANION_RELAY_PROTOCOL_VERSION, RELAY_REQUEST_METHODS, deviceIdSchema, workspaceIdSchema, relayRequestIdSchema, relayApprovalCandidateParamsSchema, relayApprovalCandidateResultSchema, relayDeviceConnectSchema, relayRuntimeRequestSchema, relayRuntimeResponseSchema, relayRuntimeEventSchema, relayAckSchema, relayGapSchema, relayEncryptedMessageSchema, relayMessageSchema, } from "./relay-protocol.ts";
2
+ export type { DeviceId, WorkspaceId, RelayRequestId, RelayRequestMethod, RelayApprovalCandidateInput, RelayApprovalCandidateParams, RelayApprovalCandidateResult, RelayMessage, RelayRuntimeRequest, RelayRuntimeResponse, RelayRuntimeEvent, RelayGap, RelayEncryptedMessage, } from "./relay-protocol.ts";
3
3
  export { CompanionEventBuffer, DEFAULT_COMPANION_MAX_BYTES, DEFAULT_COMPANION_MAX_EVENTS, } from "./event-buffer.ts";
4
4
  export type { BufferedRuntimeEvent, CompanionEventBufferOptions, EventBufferGap, EventBufferReplay, } from "./event-buffer.ts";
5
5
  export { COMPANION_LEASE_KINDS, WorkspaceLeaseManager } from "./lease-manager.ts";
6
6
  export type { CompanionLease, CompanionLeaseKind } from "./lease-manager.ts";
7
- export { CompanionWorkspace, LOCAL_APPROVAL_DECISIONS, LocalApprovalDeniedError, LocalConfirmationRequiredError, isRollNodeClient, } from "./companion-workspace.ts";
7
+ export { CompanionApprovalRequestBroker, CompanionWorkspace, InvalidRelayRequestParamsError, LOCAL_APPROVAL_DECISIONS, localApprovalDecisionSchema, LocalApprovalDeniedError, LocalConfirmationRequiredError, isRollNodeClient, } from "./companion-workspace.ts";
8
8
  export { CompanionRelayBridge, OutboundCompanionRelay, createWebSocketRelayTransport, relayEventMessage, } from "./relay-bridge.ts";
9
9
  export type { CompanionRelayBridgeOptions, OutboundCompanionRelayOptions, RelayPayloadCipher, RelayTransport, WebSocketLike, WebSocketMessageEventLike, } from "./relay-bridge.ts";
10
- export type { CompanionRuntimeClient, CompanionWorkspaceOptions, LocalApprovalDecision, LocalApprovalPolicy, } from "./companion-workspace.ts";
10
+ export type { CompanionRuntimeClient, CompanionWorkspaceOptions, LocalApprovalDecision, LocalApprovalPolicy, LocalApprovalPolicyContext, } from "./companion-workspace.ts";
package/dist/index.js CHANGED
@@ -1 +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";
1
+ export{COMPANION_RELAY_PROTOCOL_VERSION,RELAY_REQUEST_METHODS,deviceIdSchema,workspaceIdSchema,relayRequestIdSchema,relayApprovalCandidateParamsSchema,relayApprovalCandidateResultSchema,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{CompanionApprovalRequestBroker,CompanionWorkspace,InvalidRelayRequestParamsError,LOCAL_APPROVAL_DECISIONS,localApprovalDecisionSchema,LocalApprovalDeniedError,LocalConfirmationRequiredError,isRollNodeClient}from"./companion-workspace.js";export{CompanionRelayBridge,OutboundCompanionRelay,createWebSocketRelayTransport,relayEventMessage}from"./relay-bridge.js";
@@ -1 +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()]}}
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),r={lease:s,previous:this.leases.get(t),released:!1};return this.leases.set(t,r),()=>{if(r.released)return;if(r.released=!0,this.leases.get(t)!==r)return;let e=r.previous;for(;!0===e?.released;)e=e.previous;void 0===e?this.leases.delete(t):this.leases.set(t,e)}}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()].map(e=>e.lease)}}
@@ -1,10 +1,14 @@
1
1
  import { type JsonValue, type RuntimeEventEnvelope } from "@roll-agent/protocol";
2
2
  import { CompanionWorkspace } from "./companion-workspace.ts";
3
- import { type DeviceId, type RelayEncryptedMessage, type RelayMessage, type WorkspaceId } from "./relay-protocol.ts";
3
+ import { type DeviceId, type RelayEncryptedMessage, type RelayMessage, type WorkspaceId } from "@roll-agent/relay-protocol";
4
4
  export interface RelayTransport {
5
5
  send(message: RelayMessage): void | Promise<void>;
6
6
  onMessage(listener: (message: unknown) => void): () => void;
7
7
  onClose(listener: () => void): () => void;
8
+ /**
9
+ * Closes the transport and must eventually notify every currently registered
10
+ * `onClose` listener. `OutboundCompanionRelay` uses that notification to reconnect.
11
+ */
8
12
  close(): void;
9
13
  }
10
14
  export interface CompanionRelayBridgeOptions {
@@ -39,6 +43,7 @@ export declare class CompanionRelayBridge {
39
43
  close(): void;
40
44
  private enqueue;
41
45
  private enqueueTask;
46
+ private failTransportGeneration;
42
47
  private handleMessage;
43
48
  private resolveRuntimeRequest;
44
49
  private executeRuntimeRequest;
@@ -1 +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}}
1
+ import{createHash as e,randomUUID as t}from"node:crypto";import{RollRpcError as s}from"@roll-agent/client-node";import{jsonValueSchema as r}from"@roll-agent/protocol";import{CompanionWorkspace as n,InvalidRelayRequestParamsError as o,LocalApprovalDeniedError as i,LocalConfirmationRequiredError as a}from"./companion-workspace.js";import{COMPANION_RELAY_PROTOCOL_VERSION as c,RELAY_ERROR_CODES as d,canonicalizeRelayJson as p,classifyRelayAck as h,getRelayErrorRetryability as u,isRelayMutationRequestMethod as l,relayEnvelopeIdSchema as m,relayMessageSchema as g,relayRuntimeRequestSchema as q}from"@roll-agent/relay-protocol";const y=1e4,R={code:d.encryptionRequired,message:"Encrypted Relay request required for this workspace",retryable:u(d.encryptionRequired)};function v(t){const s={workspaceId:t.workspaceId,method:t.method,params:t.params};return e("sha256").update(p(s)).digest("hex")}function w(e){if(e instanceof o)return{code:d.invalidParams,message:"Invalid Relay request params",retryable:u(d.invalidParams)};if(e instanceof s)return{code:e.data?.rollCode??`JSON_RPC_${String(e.code)}`,message:e.message,retryable:e.data?.retryable??!1};if(e instanceof i||e instanceof a){const t=e instanceof i?d.localApprovalDenied:d.localConfirmationRequired;return{code:t,message:e.message,retryable:u(t)}}return{code:d.companionError,message:"Companion request failed",retryable:u(d.companionError)}}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??y,!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:c,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(()=>{this.failTransportGeneration(e)})}failTransportGeneration(e){if(this.transportGeneration===e){this.releaseCurrentTransport();try{e.transport.close()}catch{}}}async handleMessage(e,t,s=!1){if(this.transportGeneration!==t)return;const r=g.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=q.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:d.encryptedPayloadInvalid,message:"Encrypted Relay payload could not be authenticated or parsed",retryable:u(d.encryptedPayloadInvalid)}})}return}if("runtime.ack"===n.type){const e=this.workspaces.get(n.workspaceId);if(void 0===e)return;const s=this.acknowledged.get(n.workspaceId)??-1;return void("advance"===h({acknowledgedThrough:s,advertisedThrough:t.advertisedThrough.get(n.workspaceId)??-1,incomingThrough:n.throughRelaySequence})&&(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:R});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:d.workspaceNotFound,message:"The requested workspace is not paired with this Companion",retryable:u(d.workspaceNotFound)}});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=v(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:d.requestIdConflict,message:"Relay requestId was reused with different method or params",retryable:u(d.requestIdConflict)}})}if(!l(e.method))return this.executeRuntimeRequest(e,t);const i={fingerprint:v(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:r.parse(s)}}catch(t){return{type:"runtime.response",requestId:e.requestId,workspaceId:e.workspaceId,error:w(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,n){const o=this.ciphers.get(e);if(void 0===o)return void this.enqueue({type:"runtime.event",workspaceId:e,relaySequence:s,event:n});const i=this.transportGeneration;void 0!==i&&this.enqueueTask(i,async()=>{const a=await o.encrypt(r.parse(n));this.transportGeneration===i&&(await i.transport.send({type:"runtime.encrypted",workspaceId:e,envelopeId:m.parse(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 n=this.transportGeneration;void 0!==n&&this.enqueueTask(n,async()=>{const o=await s.encrypt(r.parse(e));this.transportGeneration===n&&await n.transport.send({type:"runtime.encrypted",workspaceId:e.workspaceId,envelopeId:m.parse(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}}
@@ -1,320 +1,6 @@
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>;
1
+ /**
2
+ * @deprecated Import Relay Wire schemas and types from `@roll-agent/relay-protocol`.
3
+ * This facade remains for one migration cycle so existing Companion consumers keep
4
+ * the same schema object identity.
5
+ */
6
+ export * from "@roll-agent/relay-protocol";
@@ -1 +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]);
1
+ export*from"@roll-agent/relay-protocol";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/companion",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,8 +27,9 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "zod": "3.25.76",
30
- "@roll-agent/client-node": "0.1.0",
31
- "@roll-agent/protocol": "0.1.0"
30
+ "@roll-agent/client-node": "0.2.0",
31
+ "@roll-agent/protocol": "0.2.0",
32
+ "@roll-agent/relay-protocol": "0.1.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^22.0.0"