beeos-claw 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{describe,expect,it,vi}from"vitest";import{ACPServer}from"./acp-server.js";const bridgeMock=vi.hoisted(()=>({instances:[]}));function makeConfig(){return{agentGatewayUrl:"https://agent-gw.example.test",keyFile:"/tmp/test.key",bridge:{url:"wss://bridge.example.test",service:"acp",shell:{enabled:!1}},gateway:{url:"ws://127.0.0.1:18789",token:"test-token",protocol:3,clientId:"test-client",agentId:"main"},retry:{baseMs:1,maxMs:10,maxAttempts:0},log:{enabled:!1,verbose:!1,dir:"/tmp"},promptTimeoutMs:1e3,terminalWsPort:0,terminalWsAllowedOrigins:[],streamIdleTimeoutMs:1e3,streamStaleTimeoutMs:1e3,cronSignalWindowMs:1e3,canvasRelayUrl:"",canvasEnabled:!1,openclawCanvasCompat:!1,messageBus:{enabled:!1,serviceUrl:"",url:"",principalId:"",a2aEnabled:!1}}}vi.mock("@beeos-ai/bridge-client",()=>({BridgeClient:class{state="connected";send=vi.fn(()=>Promise.reject(new Error("ws closing")));connect=vi.fn(async()=>{});close=vi.fn(async()=>{});constructor(){bridgeMock.instances.push(this)}on(e,t){return this}}})),vi.mock("./gateway-client.js",()=>({GatewayClient:class{get isConnected(){return!0}on(e,t){return this}async start(){}stop(){}request(){return Promise.resolve({})}agentSend(){return Promise.resolve({})}}})),describe("ACPServer bridge shim",()=>{it("handles BridgeClient.send rejection without creating an unhandled rejection",async()=>{bridgeMock.instances.length=0;const e=vi.fn();process.once("unhandledRejection",e);const t={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},n=new ACPServer(makeConfig(),t);await n.start(),n.sendBridgeMessage({jsonrpc:"2.0",method:"session/update",params:{}}),await new Promise(e=>setImmediate(e)),process.removeListener("unhandledRejection",e),expect(bridgeMock.instances[0]?.send).toHaveBeenCalledTimes(1),expect(t.warn).toHaveBeenCalledWith("[acp-server] bridge.send failed: ws closing"),expect(e).not.toHaveBeenCalled()})});
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;messageSdk=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance,t.channelId,t.inboundMessageId).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n,i,a){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const r=this.config.gateway.agentId||this.core.getDefaultAgentId();let o,c;if(s?(o=this.state.findSessionByKey(s),c=s,o?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${o.sessionId}`):(o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: created session for key=${s} sid=${o.sessionId}`))):(c=`agent:${r}:a2a:${randomUUID()}`,o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created A2A session key=${c}`)),i&&o&&(o.channelId=i),a&&o&&(o.inboundMessageId=a),o&&!o.promptDone&&(void 0!==o.pendingPromptId||void 0!==o.currentRunId||void 0!==o.gatewayRequestId))return(o.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n,channelId:i,inboundMessageId:a}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",o.pendingAgentSends.length);const l=await this.gateway.agentSend(c,e,r,t,n);return l?.runId&&(o?(this.state.resetForNewRun(o,o.sessionId,this.transport),o.currentRunId=l.runId,o.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(l.runId,o)):this.state.promptsByRunId.set(l.runId,{gatewaySessionKey:c,channelId:i,inboundMessageId:a})),l}setMessageSdk(e,t){this.messageSdk=e,this.msgPrincipalId=t}async publishAgentReply(e,t,s,n){const i=this.messageSdk,a=this.msgPrincipalId;if(i&&a){if(e&&i.sendToChannel)try{return await i.sendToChannel(e,s,n,{inReplyTo:t}),void this.logger.info?.(`[acp-server] ${s} published channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendToChannel failed channel=${e}: ${t instanceof Error?t.message:String(t)}; attempting persistent fallback`)}if(e&&i.sendChannelMessage)try{return await i.sendChannelMessage(e,{type:s,payload:n,inReplyTo:t,senderId:a}),void this.logger.info?.(`[acp-server] ${s} persisted channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendChannelMessage failed channel=${e}: ${t instanceof Error?t.message:String(t)}; falling back to personal channel`)}await i.sendToPersonal(a,s,n)}}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const r=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:r,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>this.core.completePrompt(e,t),failPrompt:(e,t,s)=>this.core.failPrompt(e,t,s),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageSdk:()=>{const e=this.messageSdk;return e?.sendToChannel?{sendToChannel:e.sendToChannel.bind(e)}:null},getMsgPrincipalId:()=>this.msgPrincipalId??null,gatewayRequest:(e,t)=>{const s=this.gateway;return s?.isConnected?s.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache,onStreamDelta:(e,t)=>{if(!this.messageSdk||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(n,"agent_reply_delta",i):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply_delta",i)).catch(e=>this.logger.debug?.(`[acp-server] agent_reply_delta publish failed: ${e}`))}}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"} principal=${this.msgPrincipalId} hasSDK=${!!this.messageSdk}`),!n)return void this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`);const r={session_key:n,text:t};this.publishAgentReply(i,a,"agent_reply",r).catch(e=>this.logger.warn?.(`[acp-server] agent_reply publish failed: ${e instanceof Error?e.message:JSON.stringify(e)}`))},this.core.onRunFailed=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"}`),!n)return;const r={session_key:n,error:t||"execution failed"};this.publishAgentReply(i,a,"agent_reply",r).catch(e=>this.logger.warn?.(`[acp-server] agent_reply error publish failed: ${e instanceof Error?e.message:String(e)}`))},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(e){const t=Date.now();this.logger.info?.("[acp-server] start() begin"),e&&(this.config.bridge.url=e);const s=this.config.bridge.url,n=this.config.keyFile;if(s&&!BridgeClientCtor)try{const e=Date.now(),t=await import("@beeos-ai/bridge-client");BridgeClientCtor=t.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-e}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}const i=Date.now();if(await this.connectGateway(),this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-i}ms`),s&&n&&BridgeClientCtor){Date.now();await this.startBridgeMode(s,n)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-t}ms`)}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});this.bridgeInstance=s,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n;const i=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-i}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-i}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentSyncInFlight)return;const t=()=>{if(e+1>=6)return void this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — waiting for Gateway reconnect");const t=e<2?1e3*(e+1):Math.min(5e3*2**(e-2),12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>this.tryAgentSync(e+1),t)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0,streaming:!0},agentFetch:this.agentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}finally{this.agentSyncInFlight=!1}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.gateway.on("sessionMessage",e=>this.core.handleSessionMessage(e)),this.gateway.on("response",e=>this.core.handleGatewayResponse(e));const t=this.gateway;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const s=this.canvasOrch.schemaRegistry;s&&import("node:fs/promises").then(async({readFile:n})=>{try{const i=`${t}/skill.json`,a=await n(i,"utf-8"),r=JSON.parse(a);if(r.formSchemas?.length){for(const e of r.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${r.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
1
+ import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;messageSdk=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance,t.channelId,t.inboundMessageId).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n,i,r){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const a=this.config.gateway.agentId||this.core.getDefaultAgentId();let o,c;if(s?(o=this.state.findSessionByKey(s),c=s,o?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${o.sessionId}`):(o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: created session for key=${s} sid=${o.sessionId}`))):(c=`agent:${a}:a2a:${randomUUID()}`,o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created A2A session key=${c}`)),i&&o&&(o.channelId=i),r&&o&&(o.inboundMessageId=r),o&&!o.promptDone&&(void 0!==o.pendingPromptId||void 0!==o.currentRunId||void 0!==o.gatewayRequestId))return(o.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n,channelId:i,inboundMessageId:r}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",o.pendingAgentSends.length);const l=await this.gateway.agentSend(c,e,a,t,n);return l?.runId&&(o?(this.state.resetForNewRun(o,o.sessionId,this.transport),o.currentRunId=l.runId,o.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(l.runId,o)):this.state.promptsByRunId.set(l.runId,{gatewaySessionKey:c,channelId:i,inboundMessageId:r})),l}setMessageSdk(e,t){this.messageSdk=e,this.msgPrincipalId=t}async publishAgentReply(e,t,s,n){const i=this.messageSdk,r=this.msgPrincipalId;if(i&&r){if(e&&i.sendToChannel)try{return await i.sendToChannel(e,s,n,{inReplyTo:t}),void this.logger.info?.(`[acp-server] ${s} published channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendToChannel failed channel=${e}: ${t instanceof Error?t.message:String(t)}; attempting persistent fallback`)}if(e&&i.sendChannelMessage)try{return await i.sendChannelMessage(e,{type:s,payload:n,inReplyTo:t,senderId:r}),void this.logger.info?.(`[acp-server] ${s} persisted channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendChannelMessage failed channel=${e}: ${t instanceof Error?t.message:String(t)}; falling back to personal channel`)}await i.sendToPersonal(r,s,n)}}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,r){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=r??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const a=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:a,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>this.core.completePrompt(e,t),failPrompt:(e,t,s)=>this.core.failPrompt(e,t,s),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageSdk:()=>{const e=this.messageSdk;return e?.sendToChannel?{sendToChannel:e.sendToChannel.bind(e)}:null},getMsgPrincipalId:()=>this.msgPrincipalId??null,gatewayRequest:(e,t)=>{const s=this.gateway;return s?.isConnected?s.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache,onStreamDelta:(e,t)=>{if(!this.messageSdk||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(n,"agent_reply_delta",i):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply_delta",i)).catch(e=>this.logger.debug?.(`[acp-server] agent_reply_delta publish failed: ${e}`))}}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,r=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} in_reply_to=${r??"-"} principal=${this.msgPrincipalId} hasSDK=${!!this.messageSdk}`),!n)return void this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`);const a={session_key:n,text:t};this.publishAgentReply(i,r,"agent_reply",a).catch(e=>this.logger.warn?.(`[acp-server] agent_reply publish failed: ${e instanceof Error?e.message:JSON.stringify(e)}`))},this.core.onRunFailed=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,r=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i} in_reply_to=${r??"-"}`),!n)return;const a={session_key:n,error:t||"execution failed"};this.publishAgentReply(i,r,"agent_reply",a).catch(e=>this.logger.warn?.(`[acp-server] agent_reply error publish failed: ${e instanceof Error?e.message:String(e)}`))},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(e){const t=Date.now();this.logger.info?.("[acp-server] start() begin"),e&&(this.config.bridge.url=e);const s=this.config.bridge.url,n=this.config.keyFile;if(s&&!BridgeClientCtor)try{const e=Date.now(),t=await import("@beeos-ai/bridge-client");BridgeClientCtor=t.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-e}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}const i=Date.now();if(await this.connectGateway(),this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-i}ms`),s&&n&&BridgeClientCtor){Date.now();await this.startBridgeMode(s,n)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-t}ms`)}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});this.bridgeInstance=s,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});const n={readyState:WebSocket.OPEN,send:(e,t)=>{s.send(e).then(()=>t?.(),e=>{const s=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${s.message}`),t?.(s)})},close:()=>{s.close().catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{s.close(!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n;const i=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-i}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-i}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentSyncInFlight)return;const t=()=>{if(e+1>=6)return void this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — waiting for Gateway reconnect");const t=e<2?1e3*(e+1):Math.min(5e3*2**(e-2),12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(e+1)},t)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0,streaming:!0},agentFetch:this.agentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}finally{this.agentSyncInFlight=!1}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.gateway.on("sessionMessage",e=>this.core.handleSessionMessage(e)),this.gateway.on("response",e=>this.core.handleGatewayResponse(e));const t=this.gateway;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const s=this.canvasOrch.schemaRegistry;s&&import("node:fs/promises").then(async({readFile:n})=>{try{const i=`${t}/skill.json`,r=await n(i,"utf-8"),a=JSON.parse(r);if(a.formSchemas?.length){for(const e of a.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null);const e=this.bridgeInstance;if(this.client){if(this.client.removeAllListeners(),e)try{await e.close()}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}else this.client.close();this.client=null}this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK,createMessagingTokenProvider,extractChatPrompt}from"@beeos-ai/message-sdk";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToMessagingIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async o=>{const{createPrivateKey:r,sign:a}=await import("node:crypto"),t=r({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(a(null,Buffer.from(o),t))}}}function initializePlugin(e){let o,r;try{o=e.runtime?.config?.loadConfig?.()}catch{}try{r=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:o})}catch(o){return e.logger.error?.("[beeos-claw] Failed to resolve config:",o),null}const a=r.config,t=buildThirdPartyChannelSet(o);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:r.sources,openclawConfigPath:r.openclawConfigPath}});for(const o of r.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:o.severity,summary:o.message,error:{code:o.code,message:o.message,nextSteps:o.nextSteps}}),"error"===o.severity?e.logger.error?.(`[beeos-claw] ${o.message}`):e.logger.warn?.(`[beeos-claw] ${o.message}`);if(r.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.keyFile)try{agentKeys=loadAgentKeyPair(a.keyFile);const o=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${o})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${o}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&a.agentGatewayUrl&&(setUploadDeps({gatewayUrl:a.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:a,writeObs:s,thirdPartyChannels:t,openclawConfig:o}}function registerUploadTools(e,o,r){const{cfg:a,writeObs:t}=o;cleanupUploadToolResultCache().catch(o=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(o)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(o,r,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} params=${JSON.stringify(r)}`);const n=await executeUpload(r,a,agentKeys,s);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:o,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),(async r=>{try{await writeUploadToolResult(o,BEEOS_UPLOAD_TOOL_NAME,r),await cleanupUploadToolResultCache()}catch(r){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${o}: ${String(r)}`)}return r})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,r){await handleFileStage(e,r,o.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,o){handleStatusRequest(e,o,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}})}function registerServices(e,o,r){const{cfg:a,writeObs:t,thirdPartyChannels:s}=o,n=agentKeys?(e,o)=>agentFetch(e,agentKeys,o):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0,l=new ACPServer(a,e.logger,t,n,i,s);r.current=l,e.registerService({id:"beeos-acp",async start(){const o=Date.now(),r=_pluginRegisteredAt?o-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${r}ms, bridge.url=${a.bridge.url||"(empty)"}`),!a.bridge.url&&a.agentGatewayUrl&&n){const o=Date.now(),r=await discoverBridgeFromGW(a.agentGatewayUrl,n);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-o}ms result=${r?.url??"null"}`),r)a.bridge.url=r.url,writeDiscoveryCache({bridge:{url:r.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${r.url}`);else{const o=readDiscoveryCache();o?.bridge?.url?(a.bridge.url=o.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${o.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(a.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-o}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:u,a2aEnabled:f}=a.messageBus;let y;if(a.agentGatewayUrl&&agentKeys){const e=agentKeyPairToMessagingIdentity(agentKeys);y=createMessagingTokenProvider({agentGatewayUrl:a.agentGatewayUrl,identity:e})}if(d&&(y||a.messageBus.serviceUrl)){c=new MessageSDK({serviceUrl:y?void 0:a.messageBus.serviceUrl,centrifugoUrl:p},e.logger);const o=u,r=async o=>{c.onceConnected(()=>{l.setMessageSdk(c,o),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",o,y?"gateway":"direct")}),await c.connect(o,{centrifugoUrl:p,tokenProvider:y})};e.registerService({id:"beeos-message-bus",async start(){let a=o;if(a||(a=process.env.BEEOS_PRINCIPAL_ID||""),!a&&l)try{await l.waitForAgentMapping(),a=l.getDefaultPlatformAgent()?.instanceId||""}catch(o){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",o)}a?await r(a):l?(e.logger.info?.("[beeos-claw] Message bus deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgent()?.instanceId;o?r(o).catch(o=>{e.logger.error?.("[beeos-claw] Deferred message bus connect failed:",o)}):e.logger.warn?.("[beeos-claw] Agent mapping ready but no principalId, message bus stays disabled")})):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const t=async(o,r)=>{const a=extractChatPrompt(r,{source:o,localAgentId:l.getLocalAgentId()});a&&(e.logger.debug?.(`[beeos-claw] ${o} received (sessionKey=%s, contextId=%s)`,a.sessionKey,a.contextId??"-"),await l.agentSend(a.message,a.files,a.sessionKey,void 0,a.channelId,a.messageId))};c.onMessage("chat_message",async e=>{await t("chat_message",e)});let s=!1;c.onMessage("agent_request",async o=>{s||(e.logger.warn?.("[beeos-claw] Received legacy 'agent_request' envelope — this path is deprecated and will be removed once all upstreams migrate to 'chat_message' (L0 AgentInvocationService)."),s=!0),await t("agent_request",o)})}else d?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",d);if(!1!==f&&a.agentGatewayUrl){const o=e=>l.getGatewaySessionKeyForToolCall(e),r=e=>l.getMentionsForToolCall(e),t=(e,o)=>{l.broadcastNotification(e,o)};registerA2ATools(e,{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,o,r,t),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const o=sharedCanvasState;if(a.agentGatewayUrl&&agentKeys){let r;o.setLogger(e.logger);{const e=a.agentGatewayUrl,o=agentKeys;r=async()=>{const r=await agentFetch(`${e}/api/v1/canvas/token`,o,{method:"POST"});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Canvas token returned ${r.status}: ${e}`)}return r.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const t=l.getDefaultPlatformAgent();if(!t?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const s=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:r,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),o.setRelayClient(s),o.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${o}`)}},stop(){o.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const r={stateManager:o,logger:e.logger,writeObs:e=>t(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry,i=buildCronSchemas(s);n.register(i.create),n.register(i.update);const g=new ResourceDescriptorRegistry;g.register(CRON_RESOURCE);const c=e=>async(o,r,a)=>(l.preMapToolCallSession(o),e(o,r));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:c(createRenderUiHandler(r))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:c(createCloseUiHandler(r))}),l.setCanvasState(o,{schemaRegistry:n,descriptorRegistry:g});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:c(createRenderFormHandler({registry:n,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(g,e,async(e,o)=>l.gatewayRequest(e,o),p,e.logger)}const u='\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.',f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),y=256,m=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,o)=>{if(o?.sessionKey){if(m.size>=y&&!m.has(o.sessionKey)){const e=m.keys().next().value;void 0!==e&&m.delete(e)}m.set(o.sessionKey,o.channelId)}if(isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:o?.channelId}))return;if(!o?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(o.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,o)=>{if(o?.sessionKey&&l.setPendingToolCallSessionKey(o.sessionKey),!f.has(e.toolName))return;return isThirdPartyChannel(s,{sessionKey:o?.sessionKey})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:o?.sessionKey?l.isCanvasActiveForGatewayKey(o.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){b.stop(),m.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),o.openclawConfig){const r=mirrorOpenClawConfig(o.openclawConfig);r.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${r.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${r.reason}`)}}const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(e.registrationMode&&"full"!==e.registrationMode)e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`);else{_initializing=!0;try{const o=initializePlugin(e);if(!o)return;const r={current:null};registerUploadTools(e,o,r),registerServices(e,o,r),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}}};export default plugin;
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK,createMessagingTokenProvider,extractChatPrompt}from"@beeos-ai/message-sdk";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToMessagingIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async o=>{const{createPrivateKey:r,sign:a}=await import("node:crypto"),t=r({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(a(null,Buffer.from(o),t))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",r=>{const a=r instanceof Error?r.message:String(r);e.logger.error?.("[beeos-claw] unhandledRejection:",r),o({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:a,error:{code:"unhandled_rejection",message:a}})}))}function initializePlugin(e){let o,r;try{o=e.runtime?.config?.loadConfig?.()}catch{}try{r=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:o})}catch(o){return e.logger.error?.("[beeos-claw] Failed to resolve config:",o),null}const a=r.config,t=buildThirdPartyChannelSet(o);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};installUnhandledRejectionObserver(e,s),s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:r.sources,openclawConfigPath:r.openclawConfigPath}});for(const o of r.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:o.severity,summary:o.message,error:{code:o.code,message:o.message,nextSteps:o.nextSteps}}),"error"===o.severity?e.logger.error?.(`[beeos-claw] ${o.message}`):e.logger.warn?.(`[beeos-claw] ${o.message}`);if(r.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.keyFile)try{agentKeys=loadAgentKeyPair(a.keyFile);const o=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${o})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${o}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&a.agentGatewayUrl&&(setUploadDeps({gatewayUrl:a.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:a,writeObs:s,thirdPartyChannels:t,openclawConfig:o}}function registerUploadTools(e,o,r){const{cfg:a,writeObs:t}=o;cleanupUploadToolResultCache().catch(o=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(o)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(o,r,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} params=${JSON.stringify(r)}`);const n=await executeUpload(r,a,agentKeys,s);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:o,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),(async r=>{try{await writeUploadToolResult(o,BEEOS_UPLOAD_TOOL_NAME,r),await cleanupUploadToolResultCache()}catch(r){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${o}: ${String(r)}`)}return r})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,r){await handleFileStage(e,r,o.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,o){handleStatusRequest(e,o,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}})}function registerServices(e,o,r){const{cfg:a,writeObs:t,thirdPartyChannels:s}=o,n=agentKeys?(e,o)=>agentFetch(e,agentKeys,o):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0,l=new ACPServer(a,e.logger,t,n,i,s);r.current=l,e.registerService({id:"beeos-acp",async start(){const o=Date.now(),r=_pluginRegisteredAt?o-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${r}ms, bridge.url=${a.bridge.url||"(empty)"}`),!a.bridge.url&&a.agentGatewayUrl&&n){const o=Date.now(),r=await discoverBridgeFromGW(a.agentGatewayUrl,n);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-o}ms result=${r?.url??"null"}`),r)a.bridge.url=r.url,writeDiscoveryCache({bridge:{url:r.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${r.url}`);else{const o=readDiscoveryCache();o?.bridge?.url?(a.bridge.url=o.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${o.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(a.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-o}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:u,a2aEnabled:f}=a.messageBus;let y;if(a.agentGatewayUrl&&agentKeys){const e=agentKeyPairToMessagingIdentity(agentKeys);y=createMessagingTokenProvider({agentGatewayUrl:a.agentGatewayUrl,identity:e})}if(d&&(y||a.messageBus.serviceUrl)){c=new MessageSDK({serviceUrl:y?void 0:a.messageBus.serviceUrl,centrifugoUrl:p},e.logger);const o=u,r=async o=>{c.onceConnected(()=>{l.setMessageSdk(c,o),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",o,y?"gateway":"direct")}),await c.connect(o,{centrifugoUrl:p,tokenProvider:y})};e.registerService({id:"beeos-message-bus",async start(){let a=o;if(a||(a=process.env.BEEOS_PRINCIPAL_ID||""),!a&&l)try{await l.waitForAgentMapping(),a=l.getDefaultPlatformAgent()?.instanceId||""}catch(o){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",o)}a?await r(a):l?(e.logger.info?.("[beeos-claw] Message bus deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgent()?.instanceId;o?r(o).catch(o=>{e.logger.error?.("[beeos-claw] Deferred message bus connect failed:",o)}):e.logger.warn?.("[beeos-claw] Agent mapping ready but no principalId, message bus stays disabled")})):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const t=async(o,r)=>{const a=extractChatPrompt(r,{source:o,localAgentId:l.getLocalAgentId()});a&&(e.logger.debug?.(`[beeos-claw] ${o} received (sessionKey=%s, contextId=%s)`,a.sessionKey,a.contextId??"-"),await l.agentSend(a.message,a.files,a.sessionKey,void 0,a.channelId,a.messageId))};c.onMessage("chat_message",async e=>{await t("chat_message",e)});let s=!1;c.onMessage("agent_request",async o=>{s||(e.logger.warn?.("[beeos-claw] Received legacy 'agent_request' envelope — this path is deprecated and will be removed once all upstreams migrate to 'chat_message' (L0 AgentInvocationService)."),s=!0),await t("agent_request",o)})}else d?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",d);if(!1!==f&&a.agentGatewayUrl){const o=e=>l.getGatewaySessionKeyForToolCall(e),r=e=>l.getMentionsForToolCall(e),t=(e,o)=>{l.broadcastNotification(e,o)};registerA2ATools(e,{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,o,r,t),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const o=sharedCanvasState;if(a.agentGatewayUrl&&agentKeys){let r;o.setLogger(e.logger);{const e=a.agentGatewayUrl,o=agentKeys;r=async()=>{const r=await agentFetch(`${e}/api/v1/canvas/token`,o,{method:"POST"});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Canvas token returned ${r.status}: ${e}`)}return r.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const t=l.getDefaultPlatformAgent();if(!t?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const s=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:r,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),o.setRelayClient(s),o.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${o}`)}},stop(){o.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const r={stateManager:o,logger:e.logger,writeObs:e=>t(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry,i=buildCronSchemas(s);n.register(i.create),n.register(i.update);const g=new ResourceDescriptorRegistry;g.register(CRON_RESOURCE);const c=e=>async(o,r,a)=>(l.preMapToolCallSession(o),e(o,r));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:c(createRenderUiHandler(r))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:c(createCloseUiHandler(r))}),l.setCanvasState(o,{schemaRegistry:n,descriptorRegistry:g});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:c(createRenderFormHandler({registry:n,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(g,e,async(e,o)=>l.gatewayRequest(e,o),p,e.logger)}const u='\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.',f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),y=256,m=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,o)=>{if(o?.sessionKey){if(m.size>=y&&!m.has(o.sessionKey)){const e=m.keys().next().value;void 0!==e&&m.delete(e)}m.set(o.sessionKey,o.channelId)}if(isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:o?.channelId}))return;if(!o?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(o.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,o)=>{if(o?.sessionKey&&l.setPendingToolCallSessionKey(o.sessionKey),!f.has(e.toolName))return;return isThirdPartyChannel(s,{sessionKey:o?.sessionKey})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:o?.sessionKey?l.isCanvasActiveForGatewayKey(o.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){b.stop(),m.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),o.openclawConfig){const r=mirrorOpenClawConfig(o.openclawConfig);r.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${r.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${r.reason}`)}}const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(e.registrationMode&&"full"!==e.registrationMode)e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`);else{_initializing=!0;try{const o=initializePlugin(e);if(!o)return;const r={current:null};registerUploadTools(e,o,r),registerServices(e,o,r),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}}};export default plugin;
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";const HEARTBEAT_TIMEOUT_MS=6e4;export function createTerminalWebSocketService(e){const{config:t,terminalMgr:n,logger:o,writeObs:r}=e,i=r??(()=>{});let a=null;const c=new Set;function s(e){return e.replace(/^https?:\/\//,"").replace(/:\d+$/,"").replace(/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i,"localhost")}function l(e,r){const a=function(e){const t=e.headers["x-forwarded-for"];return("string"==typeof t?t.split(",")[0]?.trim():void 0)||e.socket?.remoteAddress||"unknown"}(r),l=r.headers.origin;if(!function(e){if(!t.allowedOrigins||0===t.allowedOrigins.length)return!0;if(!e)return!1;const n=s(e);return t.allowedOrigins.some(e=>s(e)===n)}(l))return i({component:"terminal-ws",domain:"auth",name:"origin_rejected",severity:"warn",payload:{origin:l,clientIdentity:a}}),void e.close(1008,"Origin not allowed");const d=function(e){try{return new URL(e.url??"/","ws://localhost").searchParams.get("token")??void 0}catch{return}}(r);if(!t.token||d!==t.token)return i({component:"terminal-ws",domain:"auth",name:"token_rejected",severity:"warn",payload:{clientIdentity:a}}),void e.close(1008,"Unauthorized");const f=n.create(),m={ws:e,terminalId:f.id,clientIdentity:a};c.add(m),i({component:"terminal-ws",domain:"lifecycle",name:"client.connected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}});const u=setInterval(()=>{const t=n.get(f.id);if(t){if(t.output.length>0){const n=t.output.splice(0).join("");n&&e.readyState===WebSocket.OPEN&&e.send(n)}"closed"===t.state&&(clearInterval(u),e.readyState===WebSocket.OPEN&&e.close(1e3,`exit:${t.exitCode??0}`))}else clearInterval(u)},50);let p=Date.now();const w=setInterval(()=>{Date.now()-p>6e4&&(o?.warn?.(`[terminal-ws] heartbeat timeout terminalId=${f.id}`),e.close(1001,"Heartbeat timeout"))},3e4);"object"==typeof w&&"unref"in w&&w.unref(),e.on("message",(t,o)=>{p=Date.now();const r=Buffer.isBuffer(t)?t.toString("utf-8"):"string"==typeof t?t:Buffer.from(t).toString("utf-8");if(o)n.write(f.id,r);else try{const t=JSON.parse(r),o=t.action??t.type;if("heartbeat"===o)p=Date.now(),e.send(JSON.stringify({type:"heartbeat_ack"}));else if("resize"===o){const e="number"==typeof t.cols?t.cols:80,o="number"==typeof t.rows?t.rows:24;n.resize(f.id,e,o)}else"input"===o&&"string"==typeof t.data&&n.write(f.id,t.data)}catch{n.write(f.id,r)}}),e.on("close",()=>{clearInterval(u),clearInterval(w),c.delete(m),n.release(f.id),i({component:"terminal-ws",domain:"lifecycle",name:"client.disconnected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}})})}return{start(){a=new WebSocketServer({port:t.port}),a.on("connection",l),o?.info?.(`[terminal-ws] Listening on ws://0.0.0.0:${t.port}`),i({component:"terminal-ws",domain:"lifecycle",name:"server.started",severity:"info",payload:{port:t.port}})},stop(){for(const e of c)try{e.ws.close(1001,"Server shutting down")}catch{}c.clear(),a?.close(),a=null}}}
1
+ import{WebSocketServer,WebSocket}from"ws";const HEARTBEAT_TIMEOUT_MS=6e4;export function createTerminalWebSocketService(e){const{config:t,terminalMgr:n,logger:r,writeObs:o}=e,i=o??(()=>{});let a=null;const s=new Set;function c(e){return e.replace(/^https?:\/\//,"").replace(/:\d+$/,"").replace(/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i,"localhost")}function l(e,o){const a=function(e){const t=e.headers["x-forwarded-for"];return("string"==typeof t?t.split(",")[0]?.trim():void 0)||e.socket?.remoteAddress||"unknown"}(o),l=o.headers.origin;if(!function(e){if(!t.allowedOrigins||0===t.allowedOrigins.length)return!0;if(!e)return!1;const n=c(e);return t.allowedOrigins.some(e=>c(e)===n)}(l))return i({component:"terminal-ws",domain:"auth",name:"origin_rejected",severity:"warn",payload:{origin:l,clientIdentity:a}}),void e.close(1008,"Origin not allowed");const d=function(e){try{return new URL(e.url??"/","ws://localhost").searchParams.get("token")??void 0}catch{return}}(o);if(!t.token||d!==t.token)return i({component:"terminal-ws",domain:"auth",name:"token_rejected",severity:"warn",payload:{clientIdentity:a}}),void e.close(1008,"Unauthorized");const m=n.create(),f={ws:e,terminalId:m.id,clientIdentity:a};s.add(f),i({component:"terminal-ws",domain:"lifecycle",name:"client.connected",severity:"info",payload:{terminalId:m.id,clientIdentity:a}});const p=setInterval(()=>{const t=n.get(m.id);if(t){if(t.output.length>0){const n=t.output.splice(0).join("");n&&e.readyState===WebSocket.OPEN&&e.send(n)}"closed"===t.state&&(clearInterval(p),e.readyState===WebSocket.OPEN&&e.close(1e3,`exit:${t.exitCode??0}`))}else clearInterval(p)},50);let u=Date.now();const y=setInterval(()=>{Date.now()-u>6e4&&(r?.warn?.(`[terminal-ws] heartbeat timeout terminalId=${m.id}`),e.close(1001,"Heartbeat timeout"))},3e4);"object"==typeof y&&"unref"in y&&y.unref(),e.on("message",(t,r)=>{u=Date.now();const o=Buffer.isBuffer(t)?t.toString("utf-8"):"string"==typeof t?t:Buffer.from(t).toString("utf-8");if(r)n.write(m.id,o);else try{const t=JSON.parse(o),r=t.action??t.type;if("heartbeat"===r)u=Date.now(),e.send(JSON.stringify({type:"heartbeat_ack"}));else if("resize"===r){const e="number"==typeof t.cols?t.cols:80,r="number"==typeof t.rows?t.rows:24;n.resize(m.id,e,r)}else"input"===r&&"string"==typeof t.data&&n.write(m.id,t.data)}catch{n.write(m.id,o)}}),e.on("close",()=>{clearInterval(p),clearInterval(y),s.delete(f),n.release(m.id),i({component:"terminal-ws",domain:"lifecycle",name:"client.disconnected",severity:"info",payload:{terminalId:m.id,clientIdentity:a}})})}return{start(){if(null!==a)return void r?.info?.(`[terminal-ws] start() called while already running on :${t.port}; no-op`);let e;try{e=new WebSocketServer({port:t.port})}catch(e){return r?.warn?.(`[terminal-ws] sync bind failure on :${t.port}: ${e.message??e}`),void i({component:"terminal-ws",domain:"lifecycle",name:"server.bind_failed",severity:"warn",payload:{port:t.port,error:String(e.message??e),kind:"sync"}})}e.on("error",n=>{const o=n?.code??"UNKNOWN";r?.warn?.(`[terminal-ws] async listen error on :${t.port}: code=${o} msg=${n?.message??n}`),i({component:"terminal-ws",domain:"lifecycle",name:"server.bind_failed",severity:"warn",payload:{port:t.port,code:o,error:String(n?.message??n),kind:"async"}});try{e.close()}catch{}a===e&&(a=null)}),e.on("connection",l),a=e,r?.info?.(`[terminal-ws] Listening on ws://0.0.0.0:${t.port}`),i({component:"terminal-ws",domain:"lifecycle",name:"server.started",severity:"info",payload:{port:t.port}})},stop(){for(const e of s)try{e.ws.close(1001,"Server shutting down")}catch{}s.clear();try{a?.close()}catch{}a=null}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
5
  "license": "MIT",
6
6
  "author": "BeeOS <dev@beeos.ai>",
@@ -33,6 +33,7 @@
33
33
  "clean": "rm -rf dist",
34
34
  "build": "npm run clean && tsc -p tsconfig.json",
35
35
  "build:release": "npm run build && npm run minify",
36
+ "lint": "eslint \"src/**/*.ts\"",
36
37
  "minify": "find dist -name '*.js' -exec terser {} --compress drop_console=true,drop_debugger=true --mangle --output {} \\; && find dist -name '*.d.ts' -delete && find dist -name '*.d.ts.map' -delete",
37
38
  "test": "vitest run",
38
39
  "test:watch": "vitest"
@@ -48,6 +49,9 @@
48
49
  "@types/adm-zip": "^0.5.8",
49
50
  "@types/node": "^22.0.0",
50
51
  "@types/ws": "^8.18.1",
52
+ "@typescript-eslint/eslint-plugin": "^8.59.2",
53
+ "@typescript-eslint/parser": "^8.59.2",
54
+ "eslint": "^9.39.4",
51
55
  "terser": "^5.39.0",
52
56
  "typescript": "^5.6.3",
53
57
  "vitest": "^4.1.0"