@roll-agent/companion 0.3.0 → 0.4.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 +12 -3
- package/dist/index.d.ts +1 -1
- package/dist/relay-bridge-v11.d.ts +11 -1
- package/dist/relay-bridge-v11.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -45,7 +45,8 @@ const workspace = new CompanionWorkspace({
|
|
|
45
45
|
client: runtime,
|
|
46
46
|
workspaceId,
|
|
47
47
|
interactionBroker,
|
|
48
|
-
|
|
48
|
+
// Official roll companion keeps Runtime approval as the single authority.
|
|
49
|
+
localApprovalPolicy: async () => "allow",
|
|
49
50
|
});
|
|
50
51
|
const bridge = new CompanionRelayBridgeV11({
|
|
51
52
|
deviceId: deviceIdSchema.parse(savedDeviceId),
|
|
@@ -57,6 +58,8 @@ const outbound = new OutboundCompanionRelayV11({
|
|
|
57
58
|
connectTransport: async () => ({
|
|
58
59
|
transport: await openAuthenticatedRelayTransport(),
|
|
59
60
|
responderContext: authenticatedRelaySession,
|
|
61
|
+
requestPolicy: async ({ workspaceId, method, responderContext, signal }) =>
|
|
62
|
+
authorizeRemoteRuntimeRequest(responderContext, { workspaceId, method, signal }),
|
|
60
63
|
responderPolicy: async ({ responderContext, signal }) =>
|
|
61
64
|
authorizeInteractionResponder(responderContext, { signal }),
|
|
62
65
|
}),
|
|
@@ -68,8 +71,14 @@ outbound.start();
|
|
|
68
71
|
`responderContext` 是宿主注入的不透明认证/会话状态;Companion 不解释它,也不因其存在就声称
|
|
69
72
|
Browser 已认证或 controller 已选出。Wire Schema 先拒绝畸形帧;合法 candidate 再经过宿主的
|
|
70
73
|
responder policy,然后按 Runtime 原始请求做 method-specific 校验。Approval 的 approve 候选
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
仍会经过低层库的本地 Approval Policy;官方 `roll companion` 将其固定为 allow,不增加第二套
|
|
75
|
+
权限事实源,也绝不把 Runtime deny 提升为可远程批准。自定义 Host 可以进一步收紧权限,但不能
|
|
76
|
+
放宽 Runtime 决议。User Input 候选按原始表单规范化,完整结果只回给 Runtime。
|
|
77
|
+
|
|
78
|
+
Wire 1.1 的 `requestPolicy` 是必填 Host authorizer。每个 `runtime.request` 都会在 request cache
|
|
79
|
+
查询和 Runtime dispatch 之前重新授权;相同 requestId 的重投也不会绕过当前策略。false、抛错
|
|
80
|
+
或连接 generation 中止都 fail closed 为固定 `REMOTE_REQUEST_DENIED`,本机策略原因不会进入
|
|
81
|
+
Relay。`interaction.candidate` 通过该全请求策略后,仍必须再经过 `responderPolicy`。
|
|
73
82
|
|
|
74
83
|
Wire 投影有意最小化:
|
|
75
84
|
|
package/dist/index.d.ts
CHANGED
|
@@ -12,5 +12,5 @@ export { CompanionApprovalRequestBroker, CompanionWorkspace, InvalidRelayRequest
|
|
|
12
12
|
export { CompanionRelayBridge, OutboundCompanionRelay, createWebSocketRelayTransport, relayEventMessage, } from "./relay-bridge.ts";
|
|
13
13
|
export { CompanionRelayBridgeV11, OutboundCompanionRelayV11, createWebSocketRelayTransportV11, } from "./relay-bridge-v11.ts";
|
|
14
14
|
export type { CompanionRelayBridgeOptions, OutboundCompanionRelayOptions, RelayPayloadCipher, RelayTransport, WebSocketLike, WebSocketMessageEventLike, } from "./relay-bridge.ts";
|
|
15
|
-
export type { CompanionRelayBridgeV11Options, CompanionRelayConnectionV11Options, CompanionWorkspaceV11Port, OutboundCompanionRelayV11Connection, OutboundCompanionRelayV11Options, RelayPayloadCipherV11, RelayTransportV11, WebSocketLikeV11, WebSocketMessageEventLikeV11, } from "./relay-bridge-v11.ts";
|
|
15
|
+
export type { CompanionRelayBridgeV11Options, CompanionRelayConnectionV11Options, CompanionWorkspaceV11Port, OutboundCompanionRelayV11Connection, OutboundCompanionRelayV11Options, RelayPayloadCipherV11, RemoteRequestPolicy, RelayTransportV11, WebSocketLikeV11, WebSocketMessageEventLikeV11, } from "./relay-bridge-v11.ts";
|
|
16
16
|
export type { CompanionRuntimeClient, CompanionWorkspaceOptions, LocalApprovalDecision, LocalApprovalPolicy, LocalApprovalPolicyContext, } from "./companion-workspace.ts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JsonValue } from "@roll-agent/protocol";
|
|
2
|
-
import { type DeviceId, type RelayEncryptedMessageV11, type RelayMessageV11, type RelayRuntimeRequestV11, type WorkspaceId } from "@roll-agent/relay-protocol";
|
|
2
|
+
import { type DeviceId, type RelayEncryptedMessageV11, type RelayMessageV11, type RelayRequestId, type RelayRequestMethodV11, type RelayRuntimeRequestV11, type WorkspaceId } from "@roll-agent/relay-protocol";
|
|
3
3
|
import type { RemoteInteractionCandidateContext, RemoteInteractionResponderPolicy } from "./interaction-broker.ts";
|
|
4
4
|
import { type CompanionRelayFrameEntryV11, type CompanionRelayFrameReplayV11 } from "./relay-frame-buffer.ts";
|
|
5
5
|
export interface RelayTransportV11 {
|
|
@@ -31,6 +31,7 @@ export interface CompanionRelayBridgeV11Options {
|
|
|
31
31
|
readonly maxRequestCacheEntries?: number;
|
|
32
32
|
}
|
|
33
33
|
export interface CompanionRelayConnectionV11Options {
|
|
34
|
+
readonly requestPolicy: RemoteRequestPolicy;
|
|
34
35
|
readonly responderPolicy: RemoteInteractionResponderPolicy;
|
|
35
36
|
/**
|
|
36
37
|
* Opaque host-owned authentication/session state. The Companion never interprets this value
|
|
@@ -38,6 +39,13 @@ export interface CompanionRelayConnectionV11Options {
|
|
|
38
39
|
*/
|
|
39
40
|
readonly responderContext: unknown;
|
|
40
41
|
}
|
|
42
|
+
export type RemoteRequestPolicy = (input: {
|
|
43
|
+
readonly workspaceId: WorkspaceId;
|
|
44
|
+
readonly requestId: RelayRequestId;
|
|
45
|
+
readonly method: RelayRequestMethodV11;
|
|
46
|
+
readonly responderContext: unknown;
|
|
47
|
+
readonly signal: AbortSignal;
|
|
48
|
+
}) => boolean | Promise<boolean>;
|
|
41
49
|
/**
|
|
42
50
|
* Explicit Relay Wire 1.1 bridge. The legacy `CompanionRelayBridge.connect(transport)` API remains
|
|
43
51
|
* a frozen Wire 1.0 path; callers must opt into this class to project typed Interactions.
|
|
@@ -62,6 +70,7 @@ export declare class CompanionRelayBridgeV11 {
|
|
|
62
70
|
private enqueueTask;
|
|
63
71
|
private failGeneration;
|
|
64
72
|
private handleMessage;
|
|
73
|
+
private isRemoteRequestAllowed;
|
|
65
74
|
private resolveRuntimeRequest;
|
|
66
75
|
private executeRuntimeRequest;
|
|
67
76
|
private settleRequest;
|
|
@@ -85,6 +94,7 @@ export interface WebSocketLikeV11 {
|
|
|
85
94
|
export declare function createWebSocketRelayTransportV11(socket: WebSocketLikeV11): RelayTransportV11;
|
|
86
95
|
export interface OutboundCompanionRelayV11Connection {
|
|
87
96
|
readonly transport: RelayTransportV11;
|
|
97
|
+
readonly requestPolicy: RemoteRequestPolicy;
|
|
88
98
|
readonly responderPolicy: RemoteInteractionResponderPolicy;
|
|
89
99
|
readonly responderContext: unknown;
|
|
90
100
|
}
|
package/dist/relay-bridge-v11.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as e,randomUUID as t}from"node:crypto";import{RollRpcError as r}from"@roll-agent/client-node";import{jsonValueSchema as s}from"@roll-agent/protocol";import{RELAY_ERROR_CODES as n,RELAY_MESSAGE_TYPES_V11 as o,RELAY_REQUEST_METHODS_V11 as i,canonicalizeRelayJson as a,classifyRelayAck as c,getRelayErrorRetryability as d,getRelayRequestMethodDispositionForVersion as p,parseRelayRequestParamsForVersion as h,parseRelayRequestResultForVersion as u,projectRelayOperationGetResultV11 as l,projectRelayThreadSnapshotV11 as m,relayEnvelopeIdSchema as y,relayMessageSchemaV11 as R,relayRuntimeRequestSchemaV11 as g}from"@roll-agent/relay-protocol";import{LocalApprovalDeniedError as q,LocalConfirmationRequiredError as f}from"./companion-workspace.js";import{materializeRelayFrameV11 as w}from"./relay-frame-buffer.js";class InvalidRelayRequestParamsV11Error extends Error{constructor(){super("Invalid Relay Wire 1.1 request params"),this.name="InvalidRelayRequestParamsV11Error"}}class LocalOnlyRelayRequestV11Error extends Error{constructor(e){super(`Relay method "${e}" is owned by the local Companion`),this.name="LocalOnlyRelayRequestV11Error"}}const v=1e4,k={code:n.encryptionRequired,message:"Encrypted Relay request required for this workspace",retryable:d(n.encryptionRequired)};function I(t){const r={workspaceId:t.workspaceId,method:t.method,params:t.params};return e("sha256").update(a(r)).digest("hex")}function C(e,t){let r=t;return e.method===i.threadOpen||e.method===i.threadSnapshot?r=m(t):e.method===i.operationGet&&(r=l(t)),s.parse(u("1.1",e.method,r))}function E(e){if(e instanceof InvalidRelayRequestParamsV11Error)return{code:n.invalidParams,message:"Invalid Relay request params",retryable:d(n.invalidParams)};if(e instanceof r)return{code:e.data?.rollCode??`JSON_RPC_${String(e.code)}`,message:"Runtime request failed",retryable:e.data?.retryable??!1};if(e instanceof q||e instanceof f||e instanceof Error&&("LocalApprovalDeniedError"===e.name||"LocalConfirmationRequiredError"===e.name)){const t=e instanceof q||"LocalApprovalDeniedError"===e.name?n.localApprovalDenied:n.localConfirmationRequired;return{code:t,message:t===n.localApprovalDenied?"Local approval denied":"Local confirmation required",retryable:d(t)}}return{code:n.companionError,message:"Companion request failed",retryable:d(n.companionError)}}export class CompanionRelayBridgeV11{deviceId;pairingToken;workspaces;ciphers;maxRequestCacheEntries;acknowledged=new Map;inFlightRequestCache=new Map;settledRequestCache=new Map;releaseWorkspaceSubscriptions;generation;releaseTransportSubscriptions=[];closed=!1;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??v,!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.onBufferedRelayFrameV11(t=>{const r=this.generation;void 0!==r&&this.sendFrame(r,e,t)}))}connect(e,t){if(this.closed)throw new Error("Companion Relay Wire 1.1 bridge is closed");this.releaseCurrentGeneration(!0);const r={transport:e,responderPolicy:t.responderPolicy,responderContext:t.responderContext,controller:new AbortController,advertisedThrough:new Map,queue:Promise.resolve()};this.generation=r,this.releaseTransportSubscriptions=[e.onMessage(e=>{this.handleMessage(e,r).catch(()=>{})}),e.onClose(()=>{this.generation===r&&this.releaseCurrentGeneration()})],this.enqueue(r,{type:o.deviceConnect,protocolVersion:"1.1",deviceId:this.deviceId,pairingToken:this.pairingToken});for(const[e,t]of this.workspaces){const s=t.replayRelayFramesV11(this.acknowledged.get(e)??-1);void 0!==s.gap&&this.enqueue(r,{type:o.runtimeGap,workspaceId:e,fromRelaySequence:s.gap.fromRelaySequence,throughRelaySequence:s.gap.throughRelaySequence,recovery:"thread.snapshot"});for(const t of s.frames)this.sendFrame(r,e,t)}}close(){if(this.closed)return;this.closed=!0,this.releaseCurrentGeneration(!0);for(const e of this.releaseWorkspaceSubscriptions)e();const e=new Error("Companion Relay Wire 1.1 bridge closed");for(const t of this.workspaces.values())t.closeRemoteInteractions?.(e)}enqueue(e,t){this.enqueueTask(e,async()=>{await e.transport.send(t),this.generation===e&&this.markMessageAdvertised(e,t)})}enqueueTask(e,t){e.queue=e.queue.then(async()=>{this.generation===e&&await t()}).catch(()=>{this.failGeneration(e)})}failGeneration(e){if(this.generation===e){this.releaseCurrentGeneration();try{e.transport.close()}catch{}}}async handleMessage(e,t,r=!1){if(this.generation!==t)return;const s=R.safeParse(e);if(!s.success)return;const i=s.data;if(i.type===o.runtimeEncrypted){if("request"!==i.payloadKind||void 0===i.requestId)return;const e=this.ciphers.get(i.workspaceId);if(void 0===e)return;try{const r=g.parse(await e.decrypt(i));if(r.workspaceId!==i.workspaceId||r.requestId!==i.requestId)return;await this.handleMessage(r,t,!0)}catch{this.sendRuntimeResponse(t,{type:o.runtimeResponse,requestId:i.requestId,workspaceId:i.workspaceId,error:{code:n.encryptedPayloadInvalid,message:"Encrypted Relay payload could not be authenticated or parsed",retryable:d(n.encryptedPayloadInvalid)}})}return}if(i.type===o.runtimeAck){const e=this.workspaces.get(i.workspaceId);if(void 0===e)return;const r=this.acknowledged.get(i.workspaceId)??-1;return void("advance"===c({acknowledgedThrough:r,advertisedThrough:t.advertisedThrough.get(i.workspaceId)??-1,incomingThrough:i.throughRelaySequence})&&(this.acknowledged.set(i.workspaceId,i.throughRelaySequence),e.acknowledgeRelayFramesV11(i.throughRelaySequence)))}if(i.type!==o.runtimeRequest)return;if(!r&&this.ciphers.has(i.workspaceId))return void this.sendRuntimeResponse(t,{type:o.runtimeResponse,requestId:i.requestId,workspaceId:i.workspaceId,error:k});const a=this.workspaces.get(i.workspaceId);if(void 0===a)return void this.sendRuntimeResponse(t,{type:o.runtimeResponse,requestId:i.requestId,workspaceId:i.workspaceId,error:{code:n.workspaceNotFound,message:"The requested workspace is not paired with this Companion",retryable:d(n.workspaceNotFound)}});const p=await this.resolveRuntimeRequest(i,a,t);this.generation===t&&this.sendRuntimeResponse(t,p)}resolveRuntimeRequest(e,t,r){const s=`${e.workspaceId}:${e.requestId}`,a=this.inFlightRequestCache.get(s),c=this.settledRequestCache.get(s),h=a??c;if(void 0!==h){const t=I(e);return h.fingerprint===t?(void 0!==c&&(this.settledRequestCache.delete(s),this.settledRequestCache.set(s,c)),h.response):Promise.resolve({type:o.runtimeResponse,requestId:e.requestId,workspaceId:e.workspaceId,error:{code:n.requestIdConflict,message:"Relay requestId was reused with different method or params",retryable:d(n.requestIdConflict)}})}if("mutation"!==p("1.1",e.method))return this.executeRuntimeRequest(e,t,r);const u={fingerprint:I(e),response:this.executeRuntimeRequest(e,t,r),generation:e.method===i.interactionCandidate?r:void 0};return this.inFlightRequestCache.set(s,u),u.response.then(()=>this.settleRequest(s,u),()=>this.settleRequest(s,u)).catch(()=>{}),u.response}async executeRuntimeRequest(e,t,r){try{if("local-only"===p("1.1",e.method))throw new LocalOnlyRelayRequestV11Error(e.method);let n;try{n=s.parse(h("1.1",e.method,e.params))}catch{throw new InvalidRelayRequestParamsV11Error}const i=g.parse({...e,params:n}),a={signal:r.controller.signal,responderPolicy:r.responderPolicy,responderContext:r.responderContext,workspaceId:e.workspaceId,requestId:e.requestId},c=await t.handleRemoteRequestV11(i,a);return{type:o.runtimeResponse,requestId:e.requestId,workspaceId:e.workspaceId,result:C(i,c)}}catch(t){return{type:o.runtimeResponse,requestId:e.requestId,workspaceId:e.workspaceId,error:E(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)}}sendFrame(e,r,n){const i=w(r,n),a=this.ciphers.get(r);void 0!==a?this.enqueueTask(e,async()=>{const c=await a.encrypt(s.parse(i));if(this.generation!==e)return;const d=i.type===o.runtimeEvent?"event":"interaction";await e.transport.send({type:o.runtimeEncrypted,workspaceId:r,envelopeId:y.parse(t()),payloadKind:d,relaySequence:n.relaySequence,algorithm:a.algorithm,nonce:c.nonce,ciphertext:c.ciphertext}),this.generation===e&&this.markAdvertised(e,r,n.relaySequence)}):this.enqueue(e,i)}sendRuntimeResponse(e,r){const n=this.ciphers.get(r.workspaceId);void 0!==n?this.enqueueTask(e,async()=>{const i=await n.encrypt(s.parse(r));this.generation===e&&await e.transport.send({type:o.runtimeEncrypted,workspaceId:r.workspaceId,envelopeId:y.parse(t()),payloadKind:"response",requestId:r.requestId,algorithm:n.algorithm,nonce:i.nonce,ciphertext:i.ciphertext})}):this.enqueue(e,r)}releaseCurrentGeneration(e=!1){const t=this.generation;if(this.generation=void 0,t?.controller.abort(new Error("Relay transport generation ended")),void 0!==t)for(const[e,r]of this.inFlightRequestCache)r.generation===t&&this.inFlightRequestCache.delete(e);for(const e of this.releaseTransportSubscriptions)e();this.releaseTransportSubscriptions=[],e&&t?.transport.close()}markMessageAdvertised(e,t){t.type!==o.runtimeGap?t.type!==o.runtimeEvent&&t.type!==o.interactionRequest&&t.type!==o.interactionResolved&&t.type!==o.interactionCancelled||this.markAdvertised(e,t.workspaceId,t.relaySequence):this.markAdvertised(e,t.workspaceId,t.throughRelaySequence)}markAdvertised(e,t,r){e.advertisedThrough.set(t,Math.max(e.advertisedThrough.get(t)??-1,r))}}export function createWebSocketRelayTransportV11(e){return{send(t){e.send(JSON.stringify(t))},onMessage(t){const r=e=>{if("string"==typeof e.data)try{t(JSON.parse(e.data))}catch{}};return e.addEventListener("message",r),()=>e.removeEventListener("message",r)},onClose:t=>(e.addEventListener("close",t),()=>e.removeEventListener("close",t)),close(){e.close()}}}export class OutboundCompanionRelayV11{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.transport.close();this.transport=e.transport,this.reconnectMs=this.minReconnectMs,e.transport.onClose(()=>{this.transport===e.transport&&(this.transport=void 0,this.scheduleReconnect())}),this.bridge.connect(e.transport,{responderPolicy:e.responderPolicy,responderContext:e.responderContext})}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)}}
|
|
1
|
+
import{createHash as e,randomUUID as t}from"node:crypto";import{RollRpcError as r}from"@roll-agent/client-node";import{jsonValueSchema as s}from"@roll-agent/protocol";import{RELAY_ERROR_CODES as n,RELAY_ERROR_CODES_V11 as o,RELAY_MESSAGE_TYPES_V11 as i,RELAY_REQUEST_METHODS_V11 as a,canonicalizeRelayJson as c,classifyRelayAck as d,getRelayErrorRetryability as p,getRelayErrorRetryabilityV11 as h,getRelayRequestMethodDispositionForVersion as u,parseRelayRequestParamsForVersion as l,parseRelayRequestResultForVersion as m,projectRelayOperationGetResultV11 as y,projectRelayThreadSnapshotV11 as R,relayEnvelopeIdSchema as g,relayMessageSchemaV11 as q,relayRuntimeRequestSchemaV11 as w}from"@roll-agent/relay-protocol";import{LocalApprovalDeniedError as f,LocalConfirmationRequiredError as I}from"./companion-workspace.js";import{materializeRelayFrameV11 as v}from"./relay-frame-buffer.js";class InvalidRelayRequestParamsV11Error extends Error{constructor(){super("Invalid Relay Wire 1.1 request params"),this.name="InvalidRelayRequestParamsV11Error"}}class LocalOnlyRelayRequestV11Error extends Error{constructor(e){super(`Relay method "${e}" is owned by the local Companion`),this.name="LocalOnlyRelayRequestV11Error"}}const k=1e4,C={code:n.encryptionRequired,message:"Encrypted Relay request required for this workspace",retryable:p(n.encryptionRequired)},E={code:o.remoteRequestDenied,message:"Remote request denied",retryable:h(o.remoteRequestDenied)};function b(t){const r={workspaceId:t.workspaceId,method:t.method,params:t.params};return e("sha256").update(c(r)).digest("hex")}function x(e,t){let r=t;return e.method===a.threadOpen||e.method===a.threadSnapshot?r=R(t):e.method===a.operationGet&&(r=y(t)),s.parse(m("1.1",e.method,r))}function M(e){if(e instanceof InvalidRelayRequestParamsV11Error)return{code:n.invalidParams,message:"Invalid Relay request params",retryable:p(n.invalidParams)};if(e instanceof r)return{code:e.data?.rollCode??`JSON_RPC_${String(e.code)}`,message:"Runtime request failed",retryable:e.data?.retryable??!1};if(e instanceof f||e instanceof I||e instanceof Error&&("LocalApprovalDeniedError"===e.name||"LocalConfirmationRequiredError"===e.name)){const t=e instanceof f||"LocalApprovalDeniedError"===e.name?n.localApprovalDenied:n.localConfirmationRequired;return{code:t,message:t===n.localApprovalDenied?"Local approval denied":"Local confirmation required",retryable:p(t)}}return{code:n.companionError,message:"Companion request failed",retryable:p(n.companionError)}}export class CompanionRelayBridgeV11{deviceId;pairingToken;workspaces;ciphers;maxRequestCacheEntries;acknowledged=new Map;inFlightRequestCache=new Map;settledRequestCache=new Map;releaseWorkspaceSubscriptions;generation;releaseTransportSubscriptions=[];closed=!1;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??k,!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.onBufferedRelayFrameV11(t=>{const r=this.generation;void 0!==r&&this.sendFrame(r,e,t)}))}connect(e,t){if(this.closed)throw new Error("Companion Relay Wire 1.1 bridge is closed");this.releaseCurrentGeneration(!0);const r={transport:e,requestPolicy:t.requestPolicy,responderPolicy:t.responderPolicy,responderContext:t.responderContext,controller:new AbortController,advertisedThrough:new Map,queue:Promise.resolve()};this.generation=r,this.releaseTransportSubscriptions=[e.onMessage(e=>{this.handleMessage(e,r).catch(()=>{})}),e.onClose(()=>{this.generation===r&&this.releaseCurrentGeneration()})],this.enqueue(r,{type:i.deviceConnect,protocolVersion:"1.1",deviceId:this.deviceId,pairingToken:this.pairingToken});for(const[e,t]of this.workspaces){const s=t.replayRelayFramesV11(this.acknowledged.get(e)??-1);void 0!==s.gap&&this.enqueue(r,{type:i.runtimeGap,workspaceId:e,fromRelaySequence:s.gap.fromRelaySequence,throughRelaySequence:s.gap.throughRelaySequence,recovery:"thread.snapshot"});for(const t of s.frames)this.sendFrame(r,e,t)}}close(){if(this.closed)return;this.closed=!0,this.releaseCurrentGeneration(!0);for(const e of this.releaseWorkspaceSubscriptions)e();const e=new Error("Companion Relay Wire 1.1 bridge closed");for(const t of this.workspaces.values())t.closeRemoteInteractions?.(e)}enqueue(e,t){this.enqueueTask(e,async()=>{await e.transport.send(t),this.generation===e&&this.markMessageAdvertised(e,t)})}enqueueTask(e,t){e.queue=e.queue.then(async()=>{this.generation===e&&await t()}).catch(()=>{this.failGeneration(e)})}failGeneration(e){if(this.generation===e){this.releaseCurrentGeneration();try{e.transport.close()}catch{}}}async handleMessage(e,t,r=!1){if(this.generation!==t)return;const s=q.safeParse(e);if(!s.success)return;const o=s.data;if(o.type===i.runtimeEncrypted){if("request"!==o.payloadKind||void 0===o.requestId)return;const e=this.ciphers.get(o.workspaceId);if(void 0===e)return;try{const r=w.parse(await e.decrypt(o));if(r.workspaceId!==o.workspaceId||r.requestId!==o.requestId)return;await this.handleMessage(r,t,!0)}catch{this.sendRuntimeResponse(t,{type:i.runtimeResponse,requestId:o.requestId,workspaceId:o.workspaceId,error:{code:n.encryptedPayloadInvalid,message:"Encrypted Relay payload could not be authenticated or parsed",retryable:p(n.encryptedPayloadInvalid)}})}return}if(o.type===i.runtimeAck){const e=this.workspaces.get(o.workspaceId);if(void 0===e)return;const r=this.acknowledged.get(o.workspaceId)??-1;return void("advance"===d({acknowledgedThrough:r,advertisedThrough:t.advertisedThrough.get(o.workspaceId)??-1,incomingThrough:o.throughRelaySequence})&&(this.acknowledged.set(o.workspaceId,o.throughRelaySequence),e.acknowledgeRelayFramesV11(o.throughRelaySequence)))}if(o.type!==i.runtimeRequest)return;if(!r&&this.ciphers.has(o.workspaceId))return void this.sendRuntimeResponse(t,{type:i.runtimeResponse,requestId:o.requestId,workspaceId:o.workspaceId,error:C});const a=await this.isRemoteRequestAllowed(o,t);if(this.generation!==t)return;if(!a)return void this.sendRuntimeResponse(t,{type:i.runtimeResponse,requestId:o.requestId,workspaceId:o.workspaceId,error:E});const c=this.workspaces.get(o.workspaceId);if(void 0===c)return void this.sendRuntimeResponse(t,{type:i.runtimeResponse,requestId:o.requestId,workspaceId:o.workspaceId,error:{code:n.workspaceNotFound,message:"The requested workspace is not paired with this Companion",retryable:p(n.workspaceNotFound)}});const h=await this.resolveRuntimeRequest(o,c,t);this.generation===t&&this.sendRuntimeResponse(t,h)}async isRemoteRequestAllowed(e,t){try{return await t.requestPolicy({workspaceId:e.workspaceId,requestId:e.requestId,method:e.method,responderContext:t.responderContext,signal:t.controller.signal})&&!t.controller.signal.aborted}catch{return!1}}resolveRuntimeRequest(e,t,r){const s=`${e.workspaceId}:${e.requestId}`,o=this.inFlightRequestCache.get(s),c=this.settledRequestCache.get(s),d=o??c;if(void 0!==d){const t=b(e);return d.fingerprint===t?(void 0!==c&&(this.settledRequestCache.delete(s),this.settledRequestCache.set(s,c)),d.response):Promise.resolve({type:i.runtimeResponse,requestId:e.requestId,workspaceId:e.workspaceId,error:{code:n.requestIdConflict,message:"Relay requestId was reused with different method or params",retryable:p(n.requestIdConflict)}})}if("mutation"!==u("1.1",e.method))return this.executeRuntimeRequest(e,t,r);const h={fingerprint:b(e),response:this.executeRuntimeRequest(e,t,r),generation:e.method===a.interactionCandidate?r:void 0};return this.inFlightRequestCache.set(s,h),h.response.then(()=>this.settleRequest(s,h),()=>this.settleRequest(s,h)).catch(()=>{}),h.response}async executeRuntimeRequest(e,t,r){try{if("local-only"===u("1.1",e.method))throw new LocalOnlyRelayRequestV11Error(e.method);let n;try{n=s.parse(l("1.1",e.method,e.params))}catch{throw new InvalidRelayRequestParamsV11Error}const o=w.parse({...e,params:n}),a={signal:r.controller.signal,responderPolicy:r.responderPolicy,responderContext:r.responderContext,workspaceId:e.workspaceId,requestId:e.requestId},c=await t.handleRemoteRequestV11(o,a);return{type:i.runtimeResponse,requestId:e.requestId,workspaceId:e.workspaceId,result:x(o,c)}}catch(t){return{type:i.runtimeResponse,requestId:e.requestId,workspaceId:e.workspaceId,error:M(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)}}sendFrame(e,r,n){const o=v(r,n),a=this.ciphers.get(r);void 0!==a?this.enqueueTask(e,async()=>{const c=await a.encrypt(s.parse(o));if(this.generation!==e)return;const d=o.type===i.runtimeEvent?"event":"interaction";await e.transport.send({type:i.runtimeEncrypted,workspaceId:r,envelopeId:g.parse(t()),payloadKind:d,relaySequence:n.relaySequence,algorithm:a.algorithm,nonce:c.nonce,ciphertext:c.ciphertext}),this.generation===e&&this.markAdvertised(e,r,n.relaySequence)}):this.enqueue(e,o)}sendRuntimeResponse(e,r){const n=this.ciphers.get(r.workspaceId);void 0!==n?this.enqueueTask(e,async()=>{const o=await n.encrypt(s.parse(r));this.generation===e&&await e.transport.send({type:i.runtimeEncrypted,workspaceId:r.workspaceId,envelopeId:g.parse(t()),payloadKind:"response",requestId:r.requestId,algorithm:n.algorithm,nonce:o.nonce,ciphertext:o.ciphertext})}):this.enqueue(e,r)}releaseCurrentGeneration(e=!1){const t=this.generation;if(this.generation=void 0,t?.controller.abort(new Error("Relay transport generation ended")),void 0!==t)for(const[e,r]of this.inFlightRequestCache)r.generation===t&&this.inFlightRequestCache.delete(e);for(const e of this.releaseTransportSubscriptions)e();this.releaseTransportSubscriptions=[],e&&t?.transport.close()}markMessageAdvertised(e,t){t.type!==i.runtimeGap?t.type!==i.runtimeEvent&&t.type!==i.interactionRequest&&t.type!==i.interactionResolved&&t.type!==i.interactionCancelled||this.markAdvertised(e,t.workspaceId,t.relaySequence):this.markAdvertised(e,t.workspaceId,t.throughRelaySequence)}markAdvertised(e,t,r){e.advertisedThrough.set(t,Math.max(e.advertisedThrough.get(t)??-1,r))}}export function createWebSocketRelayTransportV11(e){return{send(t){e.send(JSON.stringify(t))},onMessage(t){const r=e=>{if("string"==typeof e.data)try{t(JSON.parse(e.data))}catch{}};return e.addEventListener("message",r),()=>e.removeEventListener("message",r)},onClose:t=>(e.addEventListener("close",t),()=>e.removeEventListener("close",t)),close(){e.close()}}}export class OutboundCompanionRelayV11{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.transport.close();this.transport=e.transport,this.reconnectMs=this.minReconnectMs,e.transport.onClose(()=>{this.transport===e.transport&&(this.transport=void 0,this.scheduleReconnect())}),this.bridge.connect(e.transport,{requestPolicy:e.requestPolicy,responderPolicy:e.responderPolicy,responderContext:e.responderContext})}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)}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roll-agent/companion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"zod": "3.25.76",
|
|
34
|
-
"@roll-agent/client-node": "0.3.
|
|
35
|
-
"@roll-agent/protocol": "0.
|
|
36
|
-
"@roll-agent/
|
|
34
|
+
"@roll-agent/client-node": "0.3.1",
|
|
35
|
+
"@roll-agent/relay-protocol": "0.3.0",
|
|
36
|
+
"@roll-agent/protocol": "0.4.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^22.0.0"
|