beeos-claw 0.1.13 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +4 -4
  2. package/dist/a2a/poll-pending.js +1 -1
  3. package/dist/a2a/tools.js +1 -1
  4. package/dist/a2a-orchestrator.js +1 -0
  5. package/dist/a2a-orchestrator.test.js +1 -0
  6. package/dist/acp-gateway/bridge-core.js +1 -1
  7. package/dist/acp-gateway/gateway-events.js +1 -1
  8. package/dist/acp-gateway/history-replay.js +1 -1
  9. package/dist/acp-gateway/prompt-resource-resolver.js +1 -1
  10. package/dist/acp-gateway/session-state.js +1 -1
  11. package/dist/acp-gateway/session-state.test.js +1 -0
  12. package/dist/acp-gateway/tool-result-payload-strategies.js +1 -1
  13. package/dist/acp-gateway/types.js +1 -1
  14. package/dist/acp-server.js +1 -1
  15. package/dist/agent-sync.js +1 -1
  16. package/dist/bridge-discovery.js +1 -0
  17. package/dist/canvas/action-router.js +1 -0
  18. package/dist/canvas/index.js +1 -0
  19. package/dist/canvas/relay-client.js +1 -0
  20. package/dist/canvas/state.js +1 -0
  21. package/dist/canvas/state.test.js +1 -0
  22. package/dist/canvas/tools.js +1 -0
  23. package/dist/canvas/ws-provider.js +1 -0
  24. package/dist/canvas-orchestrator.js +1 -0
  25. package/dist/channel-detection.js +1 -0
  26. package/dist/config.js +1 -1
  27. package/dist/device-identity.js +1 -0
  28. package/dist/discovery-cache.js +1 -0
  29. package/dist/errors/classify.js +1 -0
  30. package/dist/gateway-client.js +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/messaging/message-sdk.js +1 -1
  33. package/dist/surface/cron-resource.js +1 -0
  34. package/dist/surface/form-surface-builder.js +1 -0
  35. package/dist/surface/index.js +1 -0
  36. package/dist/surface/resource-surface-renderer.js +1 -0
  37. package/dist/terminal/bridge-handler.js +1 -0
  38. package/dist/terminal/index.js +1 -0
  39. package/dist/terminal/rpc.js +1 -0
  40. package/dist/upload/file-stage.js +1 -0
  41. package/dist/upload/index.js +1 -0
  42. package/dist/{upload-result-cache.js → upload/result-cache.js} +1 -1
  43. package/dist/upload/tool.js +1 -0
  44. package/openclaw.plugin.json +6 -6
  45. package/package.json +6 -3
  46. package/dist/canvas-action-router.js +0 -1
  47. package/dist/canvas-api-client.js +0 -1
  48. package/dist/canvas-relay-client.js +0 -1
  49. package/dist/canvas-state.js +0 -1
  50. package/dist/canvas-tools.js +0 -1
  51. package/dist/canvas-ws-provider.js +0 -1
  52. package/dist/cron-resource.js +0 -1
  53. package/dist/file-stage.js +0 -1
  54. package/dist/form-surface-builder.js +0 -1
  55. package/dist/resource-surface-renderer.js +0 -1
  56. package/dist/terminal-bridge-handler.js +0 -1
  57. package/dist/terminal-rpc.js +0 -1
  58. package/dist/upload-tool.js +0 -1
  59. /package/dist/{a2ui-helpers.js → canvas/a2ui-helpers.js} +0 -0
  60. /package/dist/{canvas-constants.js → canvas/constants.js} +0 -0
  61. /package/dist/{action-tool-generator.js → surface/action-tool-generator.js} +0 -0
  62. /package/dist/{cron-schemas.js → surface/cron-schemas.js} +0 -0
  63. /package/dist/{form-schema-registry.js → surface/form-schema-registry.js} +0 -0
  64. /package/dist/{render-form-tool.js → surface/render-form-tool.js} +0 -0
  65. /package/dist/{resource-descriptor-registry.js → surface/resource-descriptor-registry.js} +0 -0
  66. /package/dist/{resource-descriptor.js → surface/resource-descriptor.js} +0 -0
  67. /package/dist/{terminal-session-manager.js → terminal/session-manager.js} +0 -0
  68. /package/dist/{terminal-websocket.js → terminal/websocket.js} +0 -0
@@ -0,0 +1 @@
1
+ import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,s={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relay_url||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),s=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const n=new WebSocket(t,{headers:s});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=n,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[]}),n.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(s)}),n.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,s)=>{this.pendingJoins.set(e,{resolve:t,reject:s}),this.sendJoinCommand(e);const n=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(n),o.resolve()},reject:e=>{clearTimeout(n),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){const t=randomUUID();return e.reqId=t,new Promise((s,n)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),n(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:s,reject:n,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const s=(new TextEncoder).encode(e),n=new Uint8Array(1+s.length+t.length);n[0]=s.length,n.set(s,1),n.set(t,1+s.length),this.ws.send(n)}handleTextMessage(e){try{const t=JSON.parse(e),s=t.type;if(s?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(s){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.actionHandler&&this.actionHandler(t);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${s}`)}}catch{this.opts.logger?.warn?.("[canvas-relay] Failed to parse control message")}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const s=(new TextDecoder).decode(e.slice(1,1+t)),n=e.slice(1+t),o=this.canvasHandlers.get(s);if(!o)return;const a=n[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(n):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(n)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
@@ -0,0 +1 @@
1
+ import{MSG_SYNC,MSG_SYNC_UPDATE,MSG_AWARENESS}from"./constants.js";const MAX_CACHED_CANVASES=20,MAX_SURFACES_PER_CANVAS=20,MAX_BYTES_PER_CANVAS=524288,MAX_TOTAL_BYTES=4194304,MAX_COMPONENTS_PER_SURFACE=500,RATE_LIMIT_PER_SURFACE_PER_SEC=30;export class CanvasStateManager{canvasEntries=new Map;sessionMap=new Map;canvasSessionMap=new Map;canvasRefCount=new Map;relayClient=null;joinedCanvases=new Set;agentId;editingClearTimer=null;logger;setRelayClient(e){this.relayClient=e}getRelayClient(){return this.relayClient}setAgentId(e){this.agentId=e}setLogger(e){this.logger=e}touchEntry(e){let t=this.canvasEntries.get(e);return t?(this.canvasEntries.delete(e),t.lastAccessed=Date.now(),this.canvasEntries.set(e,t)):(t={canvasId:e,surfaces:new Map,actionHandlers:new Map,lastAccessed:Date.now(),cachedBytes:0},this.canvasEntries.set(e,t),this.enforceCanvasCountLimit()),t}findEntryBySurfaceId(e){for(const t of this.canvasEntries.values())if(t.surfaces.has(e))return t}async joinCanvas(e,t){t&&this.registerSession(t,e),this.joinedCanvases.has(e)||(this.touchEntry(e),this.relayClient&&(await this.relayClient.joinCanvas(e,{onUpdate:t=>{this.logger?.debug?.(`[canvas-state] Received update from relay for canvas=${e}: ${t.length} bytes`);const s=this.canvasEntries.get(e);s&&this.processRelayMessageForEntry(s,t)},onAwareness:t=>{this.logger?.debug?.(`[canvas-state] Received awareness from relay for canvas=${e}: ${t.length} bytes`)}}),this.joinedCanvases.add(e)))}leaveCanvas(e){this.joinedCanvases.delete(e),this.relayClient?.leaveCanvas(e)}isCanvasJoined(e){return this.joinedCanvases.has(e)}get canvasServiceConnected(){return this.relayClient?.connected??!1}registerSession(e,t){const s=this.sessionMap.get(e);if(s&&s!==t){const e=(this.canvasRefCount.get(s)??1)-1;e<=0?this.canvasRefCount.delete(s):this.canvasRefCount.set(s,e)}this.sessionMap.set(e,t),this.canvasSessionMap.set(t,e),this.canvasRefCount.set(t,(this.canvasRefCount.get(t)??0)+(s===t?0:1))}resolveCanvasForSession(e){return this.sessionMap.get(e)}resolveSessionForCanvas(e){return this.canvasSessionMap.get(e)}removeSession(e){const t=this.sessionMap.get(e);if(this.sessionMap.delete(e),t){const e=(this.canvasRefCount.get(t)??1)-1;e<=0?(this.canvasRefCount.delete(t),this.canvasSessionMap.delete(t),this.canvasEntries.delete(t),this.leaveCanvas(t),this.relayClient?.deleteCanvas(t).catch(e=>{this.logger?.warn?.(`[canvas-state] Failed to delete canvas ${t}: ${e}`)})):this.canvasRefCount.set(t,e)}}async lazyLoadCanvas(e,t=3e3){const s=this.canvasEntries.get(e);if(s&&s.surfaces.size>0)return!0;if(!this.relayClient)return!1;const n=this.touchEntry(e);return await this.joinCanvas(e),await new Promise(e=>setTimeout(e,Math.min(t,1500))),n.surfaces.size>0}static MAX_CTX_SURFACES=8;static MAX_CTX_CHARS=500;buildContextForCanvas(e){const t=this.canvasEntries.get(e);if(!t||0===t.surfaces.size)return null;this.touchEntry(e);const s=[...t.surfaces.values()].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,CanvasStateManager.MAX_CTX_SURFACES),n=t.surfaces.size-s.length;let a=`[Canvas State] Active surfaces: ${s.map(e=>{const s=this.summarizeFromEntry(t,e.surfaceId);if(!s)return`"${e.surfaceId}"`;const n=s.types.slice(0,3).join(", "),a=s.title?`: "${s.title}"`:"";return`"${e.surfaceId}" (${n}${a})`}).join("; ")}.`;return n>0&&(a+=` ...and ${n} more.`),a+=" To modify existing content, reuse its id in render_ui.",a.length>CanvasStateManager.MAX_CTX_CHARS&&(a=a.slice(0,CanvasStateManager.MAX_CTX_CHARS-3)+"..."),a}has(e,t){const s=t?this.canvasEntries.get(t):this.findEntryBySurfaceId(e);return s?.surfaces.has(e)??!1}get(e){return this.findEntryBySurfaceId(e)?.surfaces.get(e)}getAll(e){const t=this.canvasEntries.get(e);return t?Array.from(t.surfaces.values()):[]}getAllActive(e){return this.getAll(e)}create(e,t,s,n,a){let r=this.canvasEntries.get(e);return r||(r=this.touchEntry(e)),this.createInEntry(r,t,s,n,a)}createInEntry(e,t,s,n,a){this.enforceMaxSurfacesForEntry(e);const r=Date.now(),i={surfaceId:t,presentation:s,components:n,rootComponentId:a?.rootComponentId,dataModels:new Map,origin:a?.origin??{kind:"agent"},createdAt:r,updatedAt:r,tokenBucket:{tokens:30,lastRefill:r}};return a?.dataModel&&i.dataModels.set(t,a.dataModel),e.surfaces.set(t,i),a?.actionHandler&&e.actionHandlers.set(t,a.actionHandler),this.recomputeEntryBytes(e),this.enforceEntryMemoryBudget(e),this.enforceGlobalMemoryBudget(),this.syncSurfaceToRelay(e.canvasId,t,n,s,i.rootComponentId,a?.dataModel),i}update(e,t,s,n,a){const r=s?this.canvasEntries.get(s):this.findEntryBySurfaceId(e);if(!r)return;const i=r.surfaces.get(e);return i?(i.components=t,n&&(i.rootComponentId=n),a&&i.dataModels.set(e,a),i.updatedAt=Date.now(),this.recomputeEntryBytes(r),this.enforceEntryMemoryBudget(r),this.enforceGlobalMemoryBudget(),this.syncSurfaceToRelay(r.canvasId,e,t,i.presentation,i.rootComponentId,a),i):void 0}delete(e,t){const s=t?this.canvasEntries.get(t):this.findEntryBySurfaceId(e);if(!s)return!1;const n=this.removeSurfaceFromEntry(s,e);return n&&(this.recomputeEntryBytes(s),this.syncDeleteToRelay(s.canvasId,e)),n}clear(e){const t=this.canvasEntries.get(e);if(t){for(const s of t.surfaces.keys())this.syncDeleteToRelay(e,s);t.actionHandlers.clear(),t.surfaces.clear(),t.cachedBytes=0}}sizeOf(e){return this.canvasEntries.get(e)?.surfaces.size??0}getActionHandler(e,t){const s=t?this.canvasEntries.get(t):this.findEntryBySurfaceId(e);return s?.actionHandlers.get(e)}bindActionHandler(e,t,s){const n=s?this.canvasEntries.get(s):this.findEntryBySurfaceId(e);n&&n.actionHandlers.set(e,t)}summarize(e){const t=this.findEntryBySurfaceId(e);return t?this.summarizeFromEntry(t,e):null}summarizeFromEntry(e,t){const s=e.surfaces.get(t);if(!s||!Array.isArray(s.components))return null;const n=s.components,a=[];let r;for(const e of n){if(!e.component)continue;const t=Object.keys(e.component)[0];if(t&&a.push(t),"Text"===t){const t=e.component.Text,s=t?.usageHint;if(!r&&s&&["h1","h2","h3","h4"].includes(s)){const e=t?.text;e?.literalString&&(r=e.literalString)}}}return{types:[...new Set(a)],title:r}}checkRateLimit(e){const t=this.findEntryBySurfaceId(e);if(!t)return!0;const s=t.surfaces.get(e);if(!s)return!0;const n=Date.now(),a=(n-s.tokenBucket.lastRefill)/1e3;return s.tokenBucket.tokens=Math.min(30,s.tokenBucket.tokens+30*a),s.tokenBucket.lastRefill=n,s.tokenBucket.tokens>=1&&(s.tokenBucket.tokens-=1,!0)}validateComponentCount(e){return!Array.isArray(e)||e.length<=500}syncDeleteToRelay(e,t){if(this.relayClient)try{const s=JSON.stringify({surfaceId:t,deleted:!0}),n=(new TextEncoder).encode(s),a=new Uint8Array(2+n.length);a[0]=MSG_SYNC,a[1]=MSG_SYNC_UPDATE,a.set(n,2),this.relayClient.sendUpdate(e,a)}catch(s){this.logger?.warn?.(`[canvas-state] syncDeleteToRelay failed for canvas=${e} surface=${t}:`,s)}}syncSurfaceToRelay(e,t,s,n,a,r){if(this.relayClient){try{const i={surfaceId:t,components:s,presentation:n??"panel"};a&&(i.root=a),r&&Object.keys(r).length>0&&(i.dataModel=r);const o=JSON.stringify(i),c=(new TextEncoder).encode(o),l=new Uint8Array(2+c.length);l[0]=MSG_SYNC,l[1]=MSG_SYNC_UPDATE,l.set(c,2),this.relayClient.sendUpdate(e,l),this.logger?.info?.(`[canvas-state] Queued/sent ${l.length} bytes to relay for canvas=${e} surfaceId=${t}`)}catch(e){this.logger?.warn?.(`[canvas-state] syncSurfaceToRelay failed: ${e}`)}this.sendEditingAwareness(e,t)}else this.logger?.warn?.(`[canvas-state] syncSurfaceToRelay: relayClient is null for surfaceId=${t}, data NOT synced to relay`)}sendEditingAwareness(e,t){if(this.relayClient){this.editingClearTimer&&(clearTimeout(this.editingClearTimer),this.editingClearTimer=null);try{const s=JSON.stringify({agentId:this.agentId??null,editingSurface:t,timestamp:Date.now()}),n=(new TextEncoder).encode(s),a=new Uint8Array(1+n.length);a[0]=MSG_AWARENESS,a.set(n,1),this.relayClient.sendAwareness(e,a)}catch{}this.editingClearTimer=setTimeout(()=>{this.editingClearTimer=null,this.sendClearEditingAwareness(e)},3e3)}}sendClearEditingAwareness(e){if(this.relayClient)try{const t=JSON.stringify({agentId:this.agentId??null,editingSurface:"",timestamp:Date.now()}),s=(new TextEncoder).encode(t),n=new Uint8Array(1+s.length);n[0]=MSG_AWARENESS,n.set(s,1),this.relayClient.sendAwareness(e,n)}catch{}}processRelayMessageForEntry(e,t){if(!(t.length<3)&&t[0]===MSG_SYNC&&t[1]===MSG_SYNC_UPDATE)try{const s=(new TextDecoder).decode(t.subarray(2)),n=JSON.parse(s);if(!n.surfaceId)return;if(n.deleted)return this.removeSurfaceFromEntry(e,n.surfaceId),this.recomputeEntryBytes(e),void this.logger?.info?.(`[canvas-state] Surface ${n.surfaceId} deleted by remote participant`);if(n.components&&!e.surfaces.has(n.surfaceId)){const t="inline"===n.presentation||"panel"===n.presentation?n.presentation:"panel",s=Date.now(),a={surfaceId:n.surfaceId,presentation:t,components:n.components,dataModels:new Map,origin:{kind:"agent"},createdAt:s,updatedAt:s,tokenBucket:{tokens:30,lastRefill:s}};e.surfaces.set(n.surfaceId,a),this.recomputeEntryBytes(e),this.logger?.info?.(`[canvas-state] Restored surface ${n.surfaceId} from relay`)}}catch{}}removeSurfaceFromEntry(e,t){return e.actionHandlers.delete(t),e.surfaces.delete(t)}enforceMaxSurfacesForEntry(e){if(e.surfaces.size<20)return;let t,s=1/0;for(const[n,a]of e.surfaces)a.createdAt<s&&(s=a.createdAt,t=n);t&&this.removeSurfaceFromEntry(e,t)}recomputeEntryBytes(e){let t=0;for(const s of e.surfaces.values())t+=this.estimateSize(s);e.cachedBytes=t}enforceEntryMemoryBudget(e){if(e.cachedBytes<=524288)return;const t=[...e.surfaces.entries()].sort(([,e],[,t])=>e.updatedAt-t.updatedAt);for(const[s]of t){if(e.cachedBytes<=524288)break;const t=e.surfaces.get(s);t&&(e.cachedBytes-=this.estimateSize(t)),this.removeSurfaceFromEntry(e,s)}}enforceGlobalMemoryBudget(){let e=0;for(const t of this.canvasEntries.values())e+=t.cachedBytes;if(!(e<=4194304))for(const[t,s]of this.canvasEntries){if(e<=4194304)break;this.joinedCanvases.has(t)||(e-=s.cachedBytes,this.canvasEntries.delete(t))}}enforceCanvasCountLimit(){for(;this.canvasEntries.size>20;){let e=!1;for(const[t]of this.canvasEntries)if(!this.joinedCanvases.has(t)){this.canvasEntries.delete(t),e=!0;break}if(!e)break}}estimateSize(e){try{let t=2*JSON.stringify(e.components).length;if(e.dataModels.size>0)for(const s of e.dataModels.values())t+=2*JSON.stringify(s).length;return t}catch{return 1024}}destroy(){this.editingClearTimer&&(clearTimeout(this.editingClearTimer),this.editingClearTimer=null),this.relayClient?.disconnect(),this.relayClient=null,this.canvasEntries.clear(),this.sessionMap.clear(),this.canvasSessionMap.clear(),this.canvasRefCount.clear(),this.joinedCanvases.clear()}}
@@ -0,0 +1 @@
1
+ import{describe,it,expect,beforeEach}from"vitest";import{CanvasStateManager}from"./state.js";describe("CanvasStateManager",()=>{let e;beforeEach(()=>{e=new CanvasStateManager}),describe("registerSession / removeSession ref counting",()=>{it("should not delete canvas when other sessions still reference it",()=>{e.registerSession("gw:a","canvas-1"),e.registerSession("gw:b","canvas-1"),e.removeSession("gw:a"),expect(e.resolveCanvasForSession("gw:b")).toBe("canvas-1")}),it("should delete canvas when last session is removed",()=>{e.registerSession("gw:a","canvas-1"),e.removeSession("gw:a"),expect(e.resolveCanvasForSession("gw:a")).toBeUndefined()}),it("should handle reassigning session to different canvas",()=>{e.registerSession("gw:a","canvas-1"),e.registerSession("gw:a","canvas-2"),expect(e.resolveCanvasForSession("gw:a")).toBe("canvas-2")})}),describe("resolveSessionForCanvas (reverse lookup)",()=>{it("should resolve gateway key from canvas ID",()=>{e.registerSession("gw:test","canvas-42"),expect(e.resolveSessionForCanvas("canvas-42")).toBe("gw:test")}),it("should return undefined for unknown canvas",()=>{expect(e.resolveSessionForCanvas("unknown")).toBeUndefined()})})});
@@ -0,0 +1 @@
1
+ import{sv,findRootIds,nextComponentId}from"./a2ui-helpers.js";function nextCid(){return nextComponentId("c")}function textNode(e,t,n="body"){return{id:e,component:{Text:{text:sv(t),usageHint:n}}}}function imageNode(e,t,n="mediumFeature",s,i){const o={url:sv(t),usageHint:n};return s&&(o.fit=s),i&&(o.altText=sv(i)),{id:e,component:{Image:o}}}function iconNode(e,t){return{id:e,component:{Icon:{name:sv(t)}}}}function dividerNode(e,t="horizontal"){return{id:e,component:{Divider:{axis:t}}}}function buttonNode(e,t,n,s){const i={name:n};return s&&(i.context=s),{id:e,component:{Button:{child:t,action:i}}}}function columnNode(e,t,n,s){const i={children:{explicitList:t}};return n&&(i.distribution=n),s&&(i.alignment=s),{id:e,component:{Column:i}}}function rowNode(e,t,n,s){const i={children:{explicitList:t}};return n&&(i.distribution=n),s&&(i.alignment=s),{id:e,component:{Row:i}}}function cardNode(e,t){return{id:e,component:{Card:{child:t}}}}function tabsNode(e,t){return{id:e,component:{Tabs:{tabItems:t.map(e=>({title:sv(e.title),child:e.childId}))}}}}function textFieldNode(e,t,n="shortText",s){const i={label:sv(t),type:n};return s&&(i.text={path:s}),{id:e,component:{TextField:i}}}function checkboxNode(e,t,n){return{id:e,component:{CheckBox:{label:sv(t),value:n?{path:n}:{literalBoolean:!1}}}}}function multipleChoiceNode(e,t,n,s,i){const o={label:sv(t),options:n.map(e=>({label:sv(e.label),value:e.value})),selections:{path:s}};return i&&(o.description=sv(i)),{id:e,component:{MultipleChoice:o}}}function translateComponentsWithDataModel(e){const t={},n=[];for(const s of e){if(!s||"object"!=typeof s){const e=nextCid();n.push(textNode(e,"[invalid component]"));continue}if("form"===("string"==typeof s.type?s.type.toLowerCase():"text")){const e=buildFormWithDataModel(s);n.push(...e.nodes),Object.assign(t,e.dataModel)}else n.push(...translateComponent(s))}return{components:n,dataModel:t}}function translateComponent(e){if(!e||"object"!=typeof e){return[textNode(nextCid(),"[invalid component]")]}const t="string"==typeof e.type?e.type.toLowerCase():"text";switch(t){case"text":return buildText(e);case"card":return buildCard(e);case"table":return buildTable(e);case"form":return buildForm(e);case"image":return buildImage(e);case"button":return buildButton(e);case"grid":return buildGrid(e);case"stat":return buildStat(e);case"key_value":return buildKeyValue(e);case"list":return buildList(e);case"alert":case"callout":return buildAlert(e);case"code":return buildCode(e);case"markdown":return buildMarkdown(e);case"chart":return buildChart(e);case"divider":return buildDivider(e);case"tabs":return buildTabs(e);case"link":return buildLink(e);case"web_embed":case"video":case"audio":case"map":case"file_preview":return buildEmbed(e);case"badge":return buildBadge(e);case"progress":return buildProgress(e);case"timeline":return buildTimeline(e);case"accordion":return buildAccordion(e);case"row":case"column":{const n=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e)),s=findRootIds(n),i=nextCid(),o="row"===t?rowNode(i,s):columnNode(i,s);return[...n,o]}default:return[textNode(nextCid(),`[Unsupported: ${t}]`)]}}function s(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);try{return JSON.stringify(e)}catch{return"[Object]"}}function buildText(e){return[textNode(nextCid(),s(e.content??e.text??""))]}function buildCard(e){const t=[],n=[];if(e.image){const i=nextCid();t.push(imageNode(i,s(e.image),"mediumFeature","cover")),n.push(i)}if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h3")),n.push(i)}if(e.subtitle){const i=nextCid();t.push(textNode(i,s(e.subtitle),"caption")),n.push(i)}if(e.description){const i=nextCid();t.push(textNode(i,s(e.description),"body")),n.push(i)}const i=Array.isArray(e.fields)?e.fields:[];for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,o=nextCid(),a=nextCid(),r=nextCid();t.push(textNode(a,s(i.label??i.key),"caption")),t.push(textNode(r,s(i.value))),t.push(rowNode(o,[a,r],"spaceBetween")),n.push(o)}const o=Array.isArray(e.actions)?e.actions:[];if(o.length>0){const e=[];for(const n of o){if(!n||"object"!=typeof n)continue;const i=n,o=nextCid(),a=nextCid(),r=s(i.label??"Action"),c=s(i.actionId??i.id??nextCid());t.push(textNode(a,r)),t.push(buttonNode(o,a,c,[{key:"actionId",value:{literalString:c}},{key:"label",value:{literalString:r}}])),e.push(o)}if(e.length>0){const s=nextCid();t.push(rowNode(s,e)),n.push(s)}}if(e.footer){const i=nextCid();t.push(textNode(i,s(e.footer),"caption")),n.push(i)}const a=nextCid();t.push(columnNode(a,n));const r=nextCid();return t.push(cardNode(r,a)),t}function buildTable(e){const t=[],n=[],i=Array.isArray(e.columns)?e.columns:[],o=Array.isArray(e.rows)?e.rows:[];if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h4")),n.push(i)}const a=[],r=[];for(const e of i)if("string"==typeof e)a.push(e),r.push(e);else if(e&&"object"==typeof e){const t=e,n=s(t.key??t.id??t.field??t.name??t.label??"");a.push(s(t.label??t.title??t.header??t.name??n)),r.push(n)}else{const t=s(e);a.push(t),r.push(t)}if(a.length>0){const e=[];for(const n of a){const s=nextCid();t.push(textNode(s,n,"caption")),e.push(s)}const s=nextCid();t.push(rowNode(s,e)),n.push(s);const i=nextCid();t.push(dividerNode(i)),n.push(i)}for(const e of o){let i;if(Array.isArray(e))i=e;else if(e&&"object"==typeof e&&r.length>0){const t=e;i=r.map(e=>{const n=Object.keys(t).find(t=>t===e||t.toLowerCase()===e.toLowerCase());return null!=n?t[n]:""})}else i=[e];const o=[];for(const e of i){const n=nextCid();t.push(textNode(n,s(e))),o.push(n)}const a=nextCid();t.push(rowNode(a,o)),n.push(a)}const c=nextCid();t.push(columnNode(c,n));const d=nextCid();return t.push(cardNode(d,c)),t}function buildForm(e){return buildFormWithDataModel(e).nodes}function buildFormWithDataModel(e){const t=[],n=[],i=Array.isArray(e.fields)?e.fields:[],o={};if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h3")),n.push(i)}if(e.description){const i=nextCid();t.push(textNode(i,s(e.description),"body")),n.push(i)}const a=[];for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,r=nextCid(),c=s(i.name??i.label??r),d=s(i.inputType??i.type??"text"),l=`/${c}`;a.push(c);const u=i.defaultValue??i.default??i.value;if("checkbox"===d||"boolean"===d||"toggle"===d)o[c]=null!=u&&!!u,t.push(checkboxNode(r,s(i.label??i.name??""),l));else if("select"===d||"multi_select"===d||"dropdown"===d||"radio"===d){const e=Array.isArray(i.options)?i.options:[],n=e.map(e=>{if("string"==typeof e)return{label:e,value:e};const t=e;return{label:s(t.label??t.value??""),value:s(t.value??t.label??"")}});if(null!=u)o[c]=String(u);else if(e.length>0){const t=e[0];o[c]="string"==typeof t?t:s(t.value??"")}else o[c]="";t.push(multipleChoiceNode(r,s(i.label??i.name??""),n,l))}else if("datetime"===d||"time"===d){o[c]=null!=u?String(u):"";const e="datetime"===d;t.push({id:r,component:{DateTimeInput:{value:{path:l},enableDate:e,enableTime:!0}}})}else if("range"===d){const e="number"==typeof i.min?i.min:0,n="number"==typeof i.max?i.max:100;o[c]=null!=u?Number(u):e,t.push({id:r,component:{Slider:{label:sv(s(i.label??i.name??"")),value:{path:l},minValue:e,maxValue:n}}})}else{o[c]=null!=u?String(u):"";const e="number"===d?"number":"date"===d?"date":"textarea"===d||"longText"===d?"longText":"shortText";t.push(textFieldNode(r,s(i.label??i.name??""),e,l))}n.push(r)}const r=a.map(e=>({key:e,value:{path:`/${e}`}})),c=nextCid(),d=nextCid();t.push(textNode(c,s(e.submitLabel??"Submit"))),t.push(buttonNode(d,c,s(e.submitActionId??"form_submit"),r)),n.push(d);const l=nextCid();return t.push(columnNode(l,n)),{nodes:t,dataModel:o}}function buildImage(e){const t=nextCid(),n=e.alt??e.altText;return[imageNode(t,s(e.url??e.src??""),"mediumFeature",void 0,n?s(n):void 0)]}function buildButton(e){const t=nextCid(),n=nextCid(),i=s(e.label??"Button"),o=s(e.actionId??n);return[textNode(t,i),buttonNode(n,t,o,[{key:"actionId",value:{literalString:o}},{key:"label",value:{literalString:i}}])]}function buildGrid(e){const t=(Array.isArray(e.children)?e.children:Array.isArray(e.components)?e.components:[]).flatMap(e=>translateComponent(e)),n=Number(e.columns??2),s=findRootIds(t),i=[],o=[];for(let e=0;e<s.length;e+=n){const t=s.slice(e,e+n),a=nextCid();i.push(rowNode(a,t)),o.push(a)}const a=nextCid();return[...t,...i,columnNode(a,o)]}function buildStat(e){const t=[],n=[];if(e.icon){const i=nextCid();t.push(iconNode(i,s(e.icon))),n.push(i)}if(e.label){const i=nextCid();t.push(textNode(i,s(e.label),"caption")),n.push(i)}const i=nextCid();if(t.push(textNode(i,s(e.value??"0"),"h2")),n.push(i),e.change){const i=nextCid(),o="positive"===e.changeType?"↑ ":"negative"===e.changeType?"↓ ":"";t.push(textNode(i,`${o}${s(e.change)}`,"caption")),n.push(i)}if(e.description){const i=nextCid();t.push(textNode(i,s(e.description),"caption")),n.push(i)}const o=nextCid();t.push(columnNode(o,n,void 0,"center"));const a=nextCid();return t.push(cardNode(a,o)),t}function buildKeyValue(e){const t=[],n=[],i=Array.isArray(e.items)?e.items:[];if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h4")),n.push(i)}for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,o=nextCid(),a=nextCid(),r=nextCid();t.push(textNode(a,s(i.key??i.label??""),"caption")),t.push(textNode(r,s(i.value??""))),t.push(rowNode(o,[a,r],"spaceBetween")),n.push(o)}const o=nextCid();t.push(columnNode(o,n));const a=nextCid();return t.push(cardNode(a,o)),t}function buildList(e){const t=[],n=[],i=Array.isArray(e.items)?e.items:[];if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h4")),n.push(i)}for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,o=[];if(i.image){const e=nextCid();t.push(imageNode(e,s(i.image),"avatar")),o.push(e)}if(i.icon){const e=nextCid();t.push(iconNode(e,s(i.icon))),o.push(e)}const a=[];if(i.title||i.name){const e=nextCid();t.push(textNode(e,s(i.title??i.name),"body")),a.push(e)}if(i.subtitle||i.description){const e=nextCid();t.push(textNode(e,s(i.subtitle??i.description),"caption")),a.push(e)}if(a.length>0){const e=nextCid();t.push(columnNode(e,a)),o.push(e)}if(i.badge||i.value){const e=nextCid();t.push(textNode(e,s(i.badge??i.value),"caption")),o.push(e)}const r=nextCid();t.push(rowNode(r,o,"start","center")),n.push(r)}const o=nextCid();return t.push(columnNode(o,n)),t}function buildAlert(e){const t=[],n=[],i=s(e.variant??"info"),o=nextCid();t.push(iconNode(o,{info:"info",success:"check",warning:"error",error:"error",tip:"info",caution:"error",important:"error",note:"info"}[i]??"info")),n.push(o);const a=[];if(e.title){const n=nextCid();t.push(textNode(n,s(e.title),"h5")),a.push(n)}if(e.message||e.content){const n=nextCid();t.push(textNode(n,s(e.message??e.content))),a.push(n)}if(0===a.length){const n=nextCid();t.push(textNode(n,s(e.type??"Alert"))),a.push(n)}const r=nextCid();t.push(columnNode(r,a)),n.push(r);const c=nextCid();t.push(rowNode(c,n,"start","start"));const d=nextCid();return t.push(cardNode(d,c)),t}function buildCode(e){const t=s(e.code??""),n=s(e.language??""),i=n?`\`\`\`${n}\n${t}\n\`\`\``:`\`\`\`\n${t}\n\`\`\``;return[textNode(nextCid(),i)]}function buildMarkdown(e){return[textNode(nextCid(),s(e.content??""))]}function buildChart(e){const t=nextCid(),n=s(e.chartType??"chart");return[textNode(t,`[${e.title?s(e.title):n} — chart visualization not available in standard catalog]`,"caption")]}function buildDivider(e){return[dividerNode(nextCid())]}function buildTabs(e){const t=Array.isArray(e.tabs)?e.tabs:Array.isArray(e.children)?e.children:Array.isArray(e.items)?e.items:[];if(0===t.length){return[textNode(nextCid(),s(e.title??""))]}const n=[],i=[];for(const e of t){const t=s(e.label??e.title??e.id??"Tab"),o=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));n.push(...o);const a=findRootIds(o),r=nextCid();n.push(columnNode(r,a)),i.push({title:t,childId:r})}const o=nextCid();return n.push(tabsNode(o,i)),n}function buildLink(e){const t=nextCid(),n=nextCid();return[textNode(t,s(e.text??e.label??e.url??"Link")),buttonNode(n,t,"navigate",[{key:"url",value:{literalString:s(e.url??"")}}])]}function buildEmbed(e){const t=s(e.url??"");if(/youtube|youtu\.be|vimeo/i.test(t)){return[{id:nextCid(),component:{Video:{url:sv(t)}}}]}return buildLink({text:s(e.title??t),url:t})}function buildBadge(e){return[textNode(nextCid(),s(e.text??e.label??""),"caption")]}function buildProgress(e){const t=Number(e.value??0),n=Number(e.max??100),i=n>0?Math.round(t/n*100):0;return[textNode(nextCid(),`${s(e.label)||"Progress"}: ${i}% (${t}/${n})`)]}function buildTimeline(e){const t=(Array.isArray(e.items)?e.items:[]).filter(e=>null!=e&&"object"==typeof e).map(e=>`${s(e.time??e.date??"")} — ${s(e.title??"")}${e.description?`: ${s(e.description)}`:""}`),n=e.title?`**${s(e.title)}**\n`:"";return[textNode(nextCid(),n+(t.length>0?t.join("\n"):"(empty timeline)"))]}function buildAccordion(e){const t=Array.isArray(e.items)?e.items:Array.isArray(e.children)?e.children:[];if(0===t.length){return[textNode(nextCid(),s(e.title??""))]}const n=[];for(const e of t){const t=nextCid();n.push(textNode(t,s(e.title??e.label??""),"h5"));const i=Array.isArray(e.children)?e.children.flatMap(e=>translateComponent(e)):[];n.push(...i)}return n}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render structured visual content to the Canvas panel. ALWAYS prefer this tool over the canvas tool for displaying UI. Use for charts, tables, stats, key-value pairs, lists, cards, forms, alerts, code, markdown, images, links, web embeds — NOT for text paragraphs. When you call this tool, keep your text reply brief.";export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","components"],properties:{id:{type:"string",description:"Descriptive, human-readable identifier (e.g., 'sales-report', 'user-dashboard'). Reuse same id to update an existing surface."},components:{type:["array","string"],description:"UI components to render",items:{type:"object",required:["type"],properties:{type:{type:"string",enum:["card","table","stat","key_value","list","grid","chart","markdown","code","text","image","web_embed","alert","form","button","link"]},title:{type:"string"},content:{type:"string"},label:{type:"string"},description:{type:"string"},url:{type:"string"},image:{type:"string"},subtitle:{type:"string"},badge:{type:"string"},badgeVariant:{type:"string",enum:["default","success","warning","error","info"]},footer:{type:"string"},layout:{type:"string",enum:["vertical","horizontal"]},actions:{type:"array",description:"Card action buttons: [{label, actionId, variant?}]"},fields:{type:"array",description:"For card: [{label,value}]. For form: [{name,label,inputType,options,...}]"},children:{type:"array",description:"For grid: nested child components (card, stat, image, etc.)"},gap:{type:"string",enum:["sm","md","lg"]},columns:{type:"array",description:"Column headers or grid column count"},rows:{type:"array",description:"Row data as arrays matching column order"},submitLabel:{type:"string"},submitActionId:{type:"string"},actionId:{type:"string"},alt:{type:"string"},aspectRatio:{type:"string",enum:["16:9","4:3","1:1"]},chartType:{type:"string",enum:["bar","horizontal_bar","line","area","scatter","pie","doughnut","radar","gauge","candlestick","funnel","sparkline"]},stages:{type:"array"},labels:{type:"array"},datasets:{type:"array"},language:{type:"string"},code:{type:"string"},items:{type:"array"},variant:{type:"string"},message:{type:"string"},value:{type:"number"},max:{type:"number"},change:{type:"string"},changeType:{type:"string",enum:["positive","negative","neutral"]},icon:{type:"string"},text:{type:"string"}}}}}};export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};async function ensureRelayConnected(e,t,n,s){const{stateManager:i,logger:o}=e,a=s??e.getGatewaySessionKey?.(n),r=a??t,c=i.resolveCanvasForSession(r);if(c&&i.isCanvasJoined(c))return c;const d=i.getRelayClient();if(d)try{e.waitForAgentSync&&await e.waitForAgentSync();const s=e.getPlatformAgentId?.(n),c=a??t,l=a&&a!==t?t:void 0,u=await d.ensureCanvas(c,{agentId:s,linkSessionId:l});return await i.joinCanvas(u,r),o.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${a??"none"} agentId=${s??"none"}`),u}catch(e){return void o.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:s,isCanvasActive:i}=e;let o=0;return async(a,r)=>{if(i&&!i())return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."};const c=r,d=c.id,l="panel";let u=c.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!d||!Array.isArray(u)||!u.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(u))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),s({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const p=translateComponentsWithDataModel(u),m=p.components,f=Object.keys(p.dataModel).length>0?p.dataModel:void 0,h=findRootIds(m),b=nextCid(),g=columnNode(b,h),y=[...m,g],x=e.getActiveSessionId?.(a);if(!x)return n.warn?.(`[canvas] No active session for toolCallId=${a}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};const v=e.getGatewaySessionKey?.(a),C=await ensureRelayConnected(e,x,a,v),N=t.has(d,C??void 0),w=Date.now(),A=t.getRelayClient();C&&A&&w-o>6e4&&A.createSnapshot(C,"Pre-agent edit").then(()=>{o=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),N?t.update(d,y,C,b,f):t.create(C??`_fallback_${Date.now()}`,d,l,y,{origin:{kind:"agent"},rootComponentId:b,dataModel:f});const I=u.map(e=>e.type);s({component:"canvas",domain:"canvas",name:N?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:l,componentCount:u.length,componentTypes:I}}),n.debug?.(`[canvas] ${N?"Updated":"Created"} surface=${d} components=${u.length}`);return{success:!0,surfaceId:d,presentation:l,isUpdate:N,activeSurfaces:C?t.getAllActive(C).map(e=>e.surfaceId):[d]}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:s}=e;return async(i,o)=>{const a=o.id;if(!a)return{success:!1,error:"id is required"};const r=t.get(a),c=r?Date.now()-r.createdAt:0,d=e.getGatewaySessionKey?.(i),l=d?t.resolveCanvasForSession(d):void 0;return t.delete(a,l),s({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:a,lifetime_ms:c}}),n.debug?.(`[canvas] Closed surface=${a}`),{success:!0,surfaceId:a,message:"UI closed"}}}export function buildCanvasActionSystemMessage(e){const t=e.userAction;return{type:"canvas_user_action",surfaceId:e.surfaceId,actionName:t?.name??"unknown",sourceComponentId:t?.sourceComponentId??"",context:t?.context??{}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.** The `render_ui` tool is the preferred and optimized way to render UI in the Canvas panel.\n\nRules:\n- When using render_ui, keep your chat reply to ONE short sentence. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Do NOT put large text blocks in render_ui — that belongs in chat.\n- Only use `grid` as a layout container. Keep nesting to grid > card/stat/image — max 1 level deep. Otherwise list components sequentially (they stack vertically).\n- Prefer fewer, larger components. One well-structured table is better than many small cards.\n- Canvas renders in a narrow sidebar (~400px). Use markdown headings (### Section) to organize sections.\n- Use grid + card for product showcases, dashboards, feature grids. Use list for compact item lists.\n\n**Surface IDs**: Use descriptive, human-readable IDs (e.g., "sales-report", "user-dashboard"). Never use generic IDs like "ui1" or random strings.\n\n**Update vs Create**: Same `id` updates existing surface; different `id` creates new one.\nWhen the user asks to modify, adjust, fix, or update existing canvas content, you MUST reuse the original surface id. Check [Canvas State] in the message for active surface ids and their content. Only create a new surface when showing genuinely new/different content.\n\n### Component Types\n\n**Layout**: grid (columns:1-6, gap:sm/md/lg, children:[...]) — responsive grid container for card/stat/image\n\n**Data Display**: card (image, title, subtitle, description, icon, badge, footer, actions:[{label,actionId}], fields:[{label,value}]), table (columns, rows), stat (label, value, change, changeType, icon), key_value (items:[{key,value}]), list (items:[{title,subtitle,description,image,icon,badge,value}])\n\n**Text & Code**: text (content), markdown (content — supports full Markdown including bold, italic, lists, code blocks, links), code (language, code)\n\n**Media**: image (url, alt), web_embed (url — video URLs auto-detected), link (text, url)\n\n**Feedback**: alert (variant:info/success/warning/error, title, message)\n\n**Interactive**: form (fields:[{name,label,inputType,options:[{label,value}]}], submitLabel), button (label, actionId)\n - inputType: text (default), number, date, textarea, checkbox, boolean, toggle, select, multi_select, radio, datetime, time, range\n - select/multi_select/radio requires an `options` array — each item is {label, value} or a plain string\n - range renders a slider (optional min, max — default 0..100)\n\n### Composition Patterns\n- **Product showcase**: grid(columns:3) containing card(image, title, description, badge:"$29", actions:[{label:"Buy"}]) × N\n- **Dashboard KPIs**: grid(columns:3) containing stat × N\n- **Image gallery**: grid(columns:3) containing image × N\n- **Feature grid**: grid(columns:2) containing card(icon, title, description) × N\n- **Compact item list**: list(items:[{image, title, subtitle, value:"$29", badge:"New"}])\n- **Detail card**: card(title, description, fields:[{label:"Price",value:"$29"},{label:"Stock",value:"142"}])\n- **Form with select**: form(title:"Settings", fields:[{name:"country", label:"Country", inputType:"select", options:[{label:"China",value:"CN"},{label:"US",value:"US"}]}, {name:"email", label:"Email"}], submitLabel:"Save")\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING="[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content (tables, stats, key-value, lists, cards, grids, alerts, forms, links, images, code, markdown). Use grid(columns, children:[card/stat/image]) for multi-column layouts like product showcases and dashboards. Card supports: image, title, subtitle, description, icon, badge, footer, actions, fields:[{label,value}]. List items support: image, title, subtitle, description, badge, value. Markdown content supports full formatting. Use web_embed for embedding URLs. Use link for navigation. Form supports inputType: text/number/date/textarea/checkbox/toggle/select/multi_select/radio/datetime/time/range; select/multi_select/radio requires options:[{label,value}]. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.";export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const s=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),i=n.length-s.length;let o=`[Canvas State] Active surfaces: ${s.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const s=n.types.slice(0,3).join(", "),i=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${s}${i})`}).join("; ")}.`;return i>0&&(o+=` ...and ${i} more.`),o+=" To modify existing content, reuse its id in render_ui.",o.length>500&&(o=o.slice(0,497)+"..."),o}
@@ -0,0 +1 @@
1
+ import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_MESSAGES=500;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(e){this.opts=e}connect(e,s){this.canvasId=e,this.onUpdate=s.onUpdate,this.onAwareness=s.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const e=`/ws/agent/${this.canvasId}`;let s=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`;this.opts.agentId&&(s+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),this.opts.logger?.info?.(`[canvas-ws] Connecting to ${s}`);const t=agentAuthHeaders("GET",e,this.opts.agentKeys),n=new WebSocket(s,{headers:t});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n;for(const e of this.pendingMessages)n.send(e);this.pendingMessages=[]}),n.on("message",e=>{const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));if(0===s.length)return;const t=s[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(s):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(s)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-ws] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}sendUpdate(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}sendAwareness(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}enqueuePending(e){this.pendingMessages.length>=500&&(this.pendingMessages.shift(),this.opts.logger?.warn?.("[canvas-ws] pendingMessages exceeded 500, dropping oldest")),this.pendingMessages.push(e)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}
@@ -0,0 +1 @@
1
+ import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,t]of Object.entries(e)){if("object"==typeof t&&null!==t){const a=t;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const t=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const t=this.deps.state.sessions.get(s),a=t?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},n=s?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:t,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=i,this.schemaRegistry=n,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:t,schemaRegistry:n,formBuilder:i,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s&&this.emitCanvasActionAsUserMessage(s.sessionId,e)}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action handler wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}isCanvasActive(e){const s=e??this.deps.state.lastActiveSessionId,t=s?this.deps.state.sessions.get(s):void 0;return!1!==t?.canvasActive}isCanvasActiveForGatewayKey(e){const s=this.deps.state.sessionsByGatewayKey.get(e);return!1!==s?.canvasActive}onSessionLoaded(e,s,t){if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const t=this.canvasState?.resolveCanvasForSession(s);t&&(this.canvasState.clear(t),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0!==e.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${e.length} pending canvas link(s)`);for(const{acpSessionId:s,gatewaySessionKey:t}of e)this.ensureCanvasForSession(s,t)}}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,t){if(!this.canvasState)return e;if(t.has(s.split(":").slice(0,2).join(":"))||t.has(s))return e;try{let t=this.canvasState.resolveCanvasForSession(s);if(!t){const e=this.canvasState.getRelayClient();if(e)try{t=await e.ensureCanvas(s),this.canvasState.registerSession(s,t)}catch{}}if(t){let s=this.canvasState.buildContextForCanvas(t);s||(await this.canvasState.lazyLoadCanvas(t),s=this.canvasState.buildContextForCanvas(t));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.("[canvas-orch] Canvas context injection failed: %s",e)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,t=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=t,t&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,t=a.agentId,n=this.canvasState?.getRelayClient();if(n){const i=this.deps.resolvePlatformAgentId(t??"")??a.platformAgentId,o=s&&s!==e?e:void 0;n.ensureCanvas(s??e,{agentId:i,linkSessionId:o}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${t}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:t}})}else if("canvas/clear"===e){const e=s,t=this.deps.state.resolveSessionFromParams(e),a=t?this.canvasState.resolveCanvasForSession(t.gatewaySessionKey):void 0,n=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${n} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,t=s.action;if("a2ui_push"===t||"surface_update"===t){const e=String(s.surfaceId??nextOclId("openclaw")),t=s.components;if(t&&Array.isArray(t)){const a=sanitizeA2UIComponents(t),n=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,n)?this.canvasState.update(e,a,n):this.canvasState.create(n??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===t||"url_open"===t){const e=String(s.url??"");if(e){const t=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),n=[{id:`${t}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,t,"inline",n,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${t}`)}ensureCanvasForSession(e,s){const t=this.canvasState?.getRelayClient();if(!t)return;const a=e!==s?e:void 0;t.ensureCanvas(s,{linkSessionId:a}).then(async t=>{this.canvasState?.registerSession(s,t),await(this.canvasState?.joinCanvas(t,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${t} acpSid=${e} gwKey=${s}`)}).catch(t=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} err=${t}`)})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(!this.deps.gateway?.isConnected)return;if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.debug?.("[canvas-orch] Agent busy, canvas action queued");this.emitCanvasActionAsUserMessage(s.sessionId,e),this.deps.state.resetForNewRun(s,s.sessionId,this.deps.transport);const t=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,t??void 0);try{const e=await this.deps.gateway.agentSend(s.gatewaySessionKey,a,this.deps.config.gateway.agentId||this.deps.getDefaultAgentId());s.currentRunId=e.runId,s.assistantStreamStartedAt=Date.now(),this.deps.state.promptsByRunId.set(e.runId,s)}catch(t){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${t instanceof Error?t.message:t}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const t=s.userAction?.name??"unknown",a=`Clicked "${t}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:t,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}})}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:t,userAction:a,requestId:n,senderClientId:i}=e,o=this.canvasState.resolveSessionForCanvas(s);if(!o)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,n,i,"no_session");const r=this.deps.state.findSessionByKey(o);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${o}`),void this.sendRelayActionError(s,n,i,"no_session");const c={surfaceId:t,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.sendRelayActionResult(s,n,i,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,n,i,"processing_failed")})}sendRelayActionResult(e,s,t,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:t,status:a})}sendRelayActionError(e,s,t,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:t,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
@@ -0,0 +1 @@
1
+ const NON_CHANNEL_CONFIG_KEYS=new Set(["defaults","modelByChannel"]);export function buildThirdPartyChannelSet(e){const n=e?.channels??{};return new Set(Object.keys(n).filter(e=>!NON_CHANNEL_CONFIG_KEYS.has(e)))}export function resolveChannelFromSessionKey(e,n){if(!e||0===n.size)return;const t=e.split(":");if(t.length<4||"agent"!==t[0])return;const s=t[2];return n.has(s)?s:void 0}export function isThirdPartyChannel(e,n){return 0!==e.size&&(!(!n.channelId||!e.has(n.channelId))||void 0!==resolveChannelFromSessionKey(n.sessionKey,e))}
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},o=e.openclawConfig??{},a=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=pick([{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.BEEOS_PLATFORM_URL)}],{source:"default",value:""}),l=r.gateway??{},s=(o.gateway??{}).auth??{},n=pick([{source:"pluginConfig",value:trimStr(l.url)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),i=pick([{source:"openclawConfig",value:trimStr(s.token)},{source:"pluginConfig",value:trimStr(l.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),t=pick([{source:"pluginConfig",value:readInt(l.protocol)}],{source:"default",value:3}),c=pick([{source:"pluginConfig",value:trimStr(l.clientId)}],{source:"default",value:`beeos-claw-${process.pid}`}),v=pick([{source:"pluginConfig",value:trimStr(l.agentId)}],{source:"default",value:""}),p=r.bridge??{},d=pick([{source:"pluginConfig",value:trimStr(p.url)},{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),m=pick([{source:"pluginConfig",value:trimStr(p.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),f=pick([{source:"pluginConfig",value:trimStr(p.service)},{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),g=pick([{source:"pluginConfig",value:readBool((p.shell??{}).enabled)},{source:"env",value:readBool(process.env.BEEOS_SHELL_ENABLED)}],{source:"default",value:!0}),E=r.retry??{},_=pick([{source:"pluginConfig",value:readInt(E.baseMs)}],{source:"default",value:1e3}),S=pick([{source:"pluginConfig",value:readInt(E.maxMs)}],{source:"default",value:6e5}),I=pick([{source:"pluginConfig",value:readInt(E.maxAttempts)}],{source:"default",value:0}),M=r.log??{},A=pick([{source:"pluginConfig",value:readBool(M.enabled)},{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),C=pick([{source:"pluginConfig",value:readBool(M.verbose)}],{source:"default",value:!1}),O=pick([{source:"pluginConfig",value:trimStr(M.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),T=pick([{source:"pluginConfig",value:readInt(p.promptTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_PROMPT_TIMEOUT_MS)}],{source:"default",value:3e5}),R=pick([{source:"pluginConfig",value:readInt(r.terminalWsPort)},{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),k=trimStr(r.terminalWsAllowedOrigins)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),L=pick([{source:k?"pluginConfig":"default",value:k?k.split(",").map(e=>e.trim()).filter(Boolean):void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),B=pick([{source:"pluginConfig",value:readInt(r.streamIdleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_STREAM_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),y=pick([{source:"pluginConfig",value:readInt(r.streamStaleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_STREAM_STALE_TIMEOUT_MS)}],{source:"default",value:6e5}),h=pick([{source:"pluginConfig",value:readInt(r.loadReplayIdleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_LOAD_REPLAY_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),w=pick([{source:"pluginConfig",value:readInt(r.cronFlushTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_FLUSH_TIMEOUT_MS)}],{source:"default",value:12e4}),b=pick([{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),U=pick([{source:"env",value:trimStr(process.env.CANVAS_RELAY_URL)},{source:"pluginConfig",value:trimStr(r.canvasRelayUrl)}],{source:"default",value:"https://canvas-relay.beeos.ai"}),P=pick([{source:"pluginConfig",value:readBool(r.canvasEnabled)},{source:"env",value:readBool(process.env.BEEOS_CANVAS_ENABLED)}],{source:"default",value:!0}),N=pick([{source:"pluginConfig",value:readBool(r.openclawCanvasCompat)},{source:"env",value:readBool(process.env.BEEOS_OPENCLAW_CANVAS_COMPAT)}],{source:"default",value:!1}),F=r.messageBus??{},W=pick([{source:"pluginConfig",value:trimStr(F.serviceUrl)},{source:"env",value:trimStr(process.env.MESSAGE_SERVICE_URL)}],{source:"default",value:""}),G=pick([{source:"pluginConfig",value:trimStr(F.apiKey)},{source:"env",value:trimStr(process.env.MESSAGE_API_KEY)}],{source:"default",value:""}),D=pick([{source:"pluginConfig",value:trimStr(F.url)},{source:"env",value:trimStr(process.env.CENTRIFUGO_URL)}],{source:"default",value:""}),x=pick([{source:"pluginConfig",value:trimStr(F.principalId)},{source:"env",value:trimStr(process.env.BEEOS_PRINCIPAL_ID)}],{source:"default",value:""}),H=pick([{source:"pluginConfig",value:readBool(F.a2aEnabled)},{source:"env",value:readBool(process.env.BEEOS_A2A_ENABLED)}],{source:"default",value:!0}),j=pick([{source:"pluginConfig",value:readBool(F.enabled)},{source:"env",value:readBool(process.env.BEEOS_MESSAGE_BUS_ENABLED)}],{source:"default",value:!(!W.value&&!u.value)}),Y={platformUrl:u.value.replace(/\/+$/,""),bridge:{url:d.value,keyFile:m.value,service:f.value,mode:trimStr(p.mode),shell:{enabled:g.value},promptTimeoutMs:T.value,historyPendingTimeoutMs:readInt(p.historyPendingTimeoutMs)},gateway:{url:n.value,token:i.value,protocol:t.value,clientId:c.value,agentId:v.value},retry:{baseMs:Math.max(0,_.value),maxMs:Math.max(0,S.value),maxAttempts:Math.max(0,I.value)},log:{enabled:A.value,verbose:C.value,dir:O.value},promptTimeoutMs:T.value,terminalWsPort:R.value,terminalWsAllowedOrigins:L.value,streamIdleTimeoutMs:Math.max(0,B.value),streamStaleTimeoutMs:Math.max(0,y.value),loadReplayIdleTimeoutMs:Math.max(0,h.value),cronFlushTimeoutMs:Math.max(0,w.value),cronSignalWindowMs:Math.max(0,b.value),canvasRelayUrl:U.value.replace(/\/+$/,""),canvasEnabled:P.value,openclawCanvasCompat:N.value,messageBus:{enabled:j.value,serviceUrl:W.value,apiKey:G.value,url:D.value,principalId:x.value,a2aEnabled:H.value}},K={platformUrl:u.source,bridge:{url:d.source,keyFile:m.source,service:f.source,shell:{enabled:g.source}},gateway:{url:n.source,token:i.source,protocol:t.source,clientId:c.source,agentId:v.source},retry:{baseMs:_.source,maxMs:S.source,maxAttempts:I.source},log:{enabled:A.source,verbose:C.source,dir:O.source},promptTimeoutMs:T.source,terminalWsPort:R.source,terminalWsAllowedOrigins:L.source,streamIdleTimeoutMs:B.source,streamStaleTimeoutMs:y.source,loadReplayIdleTimeoutMs:h.source,cronFlushTimeoutMs:w.source,cronSignalWindowMs:b.source,canvasRelayUrl:U.source,canvasEnabled:P.source,openclawCanvasCompat:N.source,messageBus:{enabled:j.source,serviceUrl:W.source,apiKey:G.source,url:D.source,principalId:x.source,a2aEnabled:H.source}},$=[];return Y.platformUrl||$.push({code:"CONFIG_PLATFORM_URL_MISSING",severity:"error",message:"platformUrl is required for file uploads and platform callbacks",nextSteps:[`Set plugins.entries.beeos-claw.config.platformUrl in ${a}`,"Or set the BEEOS_PLATFORM_URL environment variable"]}),Y.gateway.token||$.push({code:"CONFIG_GATEWAY_TOKEN_MISSING",severity:"warn",message:"gateway.token is empty; Gateway auth may fail if authentication is enabled",nextSteps:[`Set gateway.auth.token in ${a}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${a}`]}),{config:Y,sources:K,validation:$,openclawConfigPath:a}}export function resolveConfig(e){const{config:r,validation:o}=resolveConfigWithMeta({pluginConfig:e}),a=o.find(e=>"error"===e.severity);if(a)throw new Error(`[beeos-claw] ${a.message}`);return r}const MIRROR_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","openclaw.json");export function mirrorOpenClawConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))return{copied:!1,destinationPath:MIRROR_PATH,reason:"config_invalid"};try{return mkdirSync(dirname(MIRROR_PATH),{recursive:!0}),writeFileSync(MIRROR_PATH,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),{copied:!0,destinationPath:MIRROR_PATH}}catch(e){return{copied:!1,destinationPath:MIRROR_PATH,reason:e instanceof Error?e.message:"write_failed"}}}
1
+ import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const CURRENT_CONFIG_PROTOCOL=1;function shouldTrustOperationalConfig(e){const r=e._generator;if(!r)return!1;return("number"==typeof r.configProtocol?r.configProtocol:0)>=1}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},o=e.openclawConfig??{},a=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=shouldTrustOperationalConfig(r),l=pick([{source:"pluginConfig",value:trimStr(r.agentGatewayUrl)},{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.AGENT_GATEWAY_URL)}],{source:"default",value:""}),n=r.gateway??{},s=(o.gateway??{}).auth??{},t=pick([...u?[{source:"pluginConfig",value:trimStr(n.url)}]:[],{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),i=pick([{source:"openclawConfig",value:trimStr(s.token)},{source:"pluginConfig",value:trimStr(n.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),c=pick([...u?[{source:"pluginConfig",value:readInt(n.protocol)}]:[]],{source:"default",value:3}),v=pick([...u?[{source:"pluginConfig",value:trimStr(n.clientId)}]:[]],{source:"default",value:`beeos-claw-${process.pid}`}),p=pick([...u?[{source:"pluginConfig",value:trimStr(n.agentId)}]:[]],{source:"default",value:""}),d=r.bridge??{},g=pick([{source:"pluginConfig",value:trimStr(r.keyFile)},{source:"pluginConfig",value:trimStr(d.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),f=pick([{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),m=pick([...u?[{source:"pluginConfig",value:trimStr(d.service)}]:[],{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),E=d.shell??{},_=pick([...u?[{source:"pluginConfig",value:readBool(E.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_SHELL_ENABLED)}],{source:"default",value:!0}),S=r.retry??{},I=pick([...u?[{source:"pluginConfig",value:readInt(S.baseMs)}]:[]],{source:"default",value:1e3}),A=pick([...u?[{source:"pluginConfig",value:readInt(S.maxMs)}]:[]],{source:"default",value:6e5}),C=pick([...u?[{source:"pluginConfig",value:readInt(S.maxAttempts)}]:[]],{source:"default",value:0}),T=r.log??{},M=pick([...u?[{source:"pluginConfig",value:readBool(T.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),O=pick([...u?[{source:"pluginConfig",value:readBool(T.verbose)}]:[]],{source:"default",value:!1}),R=pick([{source:"pluginConfig",value:trimStr(T.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),y=pick([...u?[{source:"pluginConfig",value:readInt(d.promptTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_PROMPT_TIMEOUT_MS)}],{source:"default",value:3e5}),k=pick([...u?[{source:"pluginConfig",value:readInt(r.terminalWsPort)}]:[],{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),w=(u?trimStr(r.terminalWsAllowedOrigins):void 0)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),L=w?w.split(",").map(e=>e.trim()).filter(Boolean):void 0,h=pick([{source:w?"pluginConfig":"default",value:L?.length?L:void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),B=pick([...u?[{source:"pluginConfig",value:readInt(r.streamIdleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),b=pick([...u?[{source:"pluginConfig",value:readInt(r.streamStaleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_STALE_TIMEOUT_MS)}],{source:"default",value:6e5}),N=pick([...u?[{source:"pluginConfig",value:readInt(r.loadReplayIdleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_LOAD_REPLAY_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),U=pick([...u?[{source:"pluginConfig",value:readInt(r.cronFlushTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_CRON_FLUSH_TIMEOUT_MS)}],{source:"default",value:12e4}),G=pick([...u?[{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),P=pick([{source:"env",value:trimStr(process.env.CANVAS_RELAY_URL)},{source:"pluginConfig",value:trimStr(r.canvasRelayUrl)}],{source:"default",value:"https://canvas-relay.beeos.ai"}),W=pick([...u?[{source:"pluginConfig",value:readBool(r.canvasEnabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_CANVAS_ENABLED)}],{source:"default",value:!0}),F=pick([...u?[{source:"pluginConfig",value:readBool(r.openclawCanvasCompat)}]:[],{source:"env",value:readBool(process.env.BEEOS_OPENCLAW_CANVAS_COMPAT)}],{source:"default",value:!1}),D=r.messageBus??{},x=pick([{source:"pluginConfig",value:trimStr(D.serviceUrl)},{source:"env",value:trimStr(process.env.MESSAGE_SERVICE_URL)}],{source:"default",value:""}),H=pick([{source:"pluginConfig",value:trimStr(D.url)},{source:"env",value:trimStr(process.env.CENTRIFUGO_URL)}],{source:"default",value:""}),Y=pick([{source:"pluginConfig",value:trimStr(D.principalId)},{source:"env",value:trimStr(process.env.BEEOS_PRINCIPAL_ID)}],{source:"default",value:""}),j=pick([{source:"pluginConfig",value:readBool(D.a2aEnabled)},{source:"env",value:readBool(process.env.BEEOS_A2A_ENABLED)}],{source:"default",value:!0}),$=pick([{source:"pluginConfig",value:readBool(D.enabled)},{source:"env",value:readBool(process.env.BEEOS_MESSAGE_BUS_ENABLED)}],{source:"default",value:!(!x.value&&!l.value)}),V={agentGatewayUrl:l.value.replace(/\/+$/,""),keyFile:g.value,bridge:{url:f.value,service:m.value,mode:trimStr(d.mode),shell:{enabled:_.value},promptTimeoutMs:y.value,historyPendingTimeoutMs:readInt(d.historyPendingTimeoutMs)},gateway:{url:t.value,token:i.value,protocol:c.value,clientId:v.value,agentId:p.value},retry:{baseMs:Math.max(0,I.value),maxMs:Math.max(0,A.value),maxAttempts:Math.max(0,C.value)},log:{enabled:M.value,verbose:O.value,dir:R.value},promptTimeoutMs:y.value,terminalWsPort:k.value,terminalWsAllowedOrigins:h.value,streamIdleTimeoutMs:Math.max(0,B.value),streamStaleTimeoutMs:Math.max(0,b.value),loadReplayIdleTimeoutMs:Math.max(0,N.value),cronFlushTimeoutMs:Math.max(0,U.value),cronSignalWindowMs:Math.max(0,G.value),canvasRelayUrl:P.value.replace(/\/+$/,""),canvasEnabled:W.value,openclawCanvasCompat:F.value,messageBus:{enabled:$.value,serviceUrl:x.value,url:H.value,principalId:Y.value,a2aEnabled:j.value}},K={agentGatewayUrl:l.source,keyFile:g.source,bridge:{url:f.source,service:m.source,shell:{enabled:_.source}},gateway:{url:t.source,token:i.source,protocol:c.source,clientId:v.source,agentId:p.source},retry:{baseMs:I.source,maxMs:A.source,maxAttempts:C.source},log:{enabled:M.source,verbose:O.source,dir:R.source},promptTimeoutMs:y.source,terminalWsPort:k.source,terminalWsAllowedOrigins:h.source,streamIdleTimeoutMs:B.source,streamStaleTimeoutMs:b.source,loadReplayIdleTimeoutMs:N.source,cronFlushTimeoutMs:U.source,cronSignalWindowMs:G.source,canvasRelayUrl:P.source,canvasEnabled:W.source,openclawCanvasCompat:F.source,messageBus:{enabled:$.source,serviceUrl:x.source,url:H.source,principalId:Y.source,a2aEnabled:j.source}},q=[];return V.agentGatewayUrl||q.push({code:"CONFIG_AGENT_GATEWAY_URL_MISSING",severity:"error",message:"agentGatewayUrl is required for file uploads and Agent Gateway callbacks",nextSteps:[`Set plugins.entries.beeos-claw.config.agentGatewayUrl in ${a}`,"Or set the AGENT_GATEWAY_URL environment variable"]}),V.gateway.token||q.push({code:"CONFIG_GATEWAY_TOKEN_MISSING",severity:"warn",message:"gateway.token is empty; Gateway auth may fail if authentication is enabled",nextSteps:[`Set gateway.auth.token in ${a}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${a}`]}),{config:V,sources:K,validation:q,openclawConfigPath:a}}export function resolveConfig(e){const{config:r,validation:o}=resolveConfigWithMeta({pluginConfig:e}),a=o.find(e=>"error"===e.severity);if(a)throw new Error(`[beeos-claw] ${a.message}`);return r}const MIRROR_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","openclaw.json");export function mirrorOpenClawConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))return{copied:!1,destinationPath:MIRROR_PATH,reason:"config_invalid"};try{return mkdirSync(dirname(MIRROR_PATH),{recursive:!0}),writeFileSync(MIRROR_PATH,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),{copied:!0,destinationPath:MIRROR_PATH}}catch(e){return{copied:!1,destinationPath:MIRROR_PATH,reason:e instanceof Error?e.message:"write_failed"}}}
@@ -0,0 +1 @@
1
+ import{randomUUID,generateKeyPairSync,createHash,sign as cryptoSign,createPrivateKey}from"node:crypto";import{readFileSync,writeFileSync,mkdirSync,existsSync}from"node:fs";import{join,dirname}from"node:path";import{homedir}from"node:os";export function base64urlEncode(e){return e.toString("base64url")}function verifyDeviceFingerprint(e){try{const r=Buffer.from(e.publicKey,"base64url");return createHash("sha256").update(r).digest("hex")===e.deviceId}catch{return!1}}export function loadOrCreateDeviceIdentity(e){const r=e||join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw");mkdirSync(r,{recursive:!0});const i=join(r,"device-key.json");if(existsSync(i))try{const e=JSON.parse(readFileSync(i,"utf8"));if(verifyDeviceFingerprint(e))return{deviceId:e.deviceId,publicKeyB64url:e.publicKey,privateKeyDer:Buffer.from(e.privateKey,"base64")}}catch{}const{publicKey:t,privateKey:o}=generateKeyPairSync("ed25519"),n=t.export({type:"spki",format:"der"}).subarray(-32),c=o.export({type:"pkcs8",format:"der"}),a=createHash("sha256").update(n).digest("hex"),p=base64urlEncode(n),d={deviceId:a,publicKeyB64url:p,privateKeyDer:c};return writeFileSync(i,JSON.stringify({deviceId:a,publicKey:p,privateKey:c.toString("base64")},null,2)),d}export function buildDeviceAuthPayload(e){return["v2",e.deviceId,e.clientId,e.clientMode,e.role,e.scopes.join(","),String(e.signedAtMs),e.token,e.nonce].join("|")}export function signDeviceAuth(e,r){const i=createPrivateKey({key:e.privateKeyDer,format:"der",type:"pkcs8"});return base64urlEncode(cryptoSign(null,Buffer.from(r),i))}export function ensureDevicePaired(e){const r=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"devices","paired.json");let i={};try{i=JSON.parse(readFileSync(r,"utf8"))}catch{}if(i[e.deviceId])return;const t=Date.now(),o=base64urlEncode(Buffer.from(randomUUID().replace(/-/g,""),"hex"));i[e.deviceId]={deviceId:e.deviceId,publicKey:e.publicKeyB64url,clientId:"cli",clientMode:"cli",role:"operator",roles:["operator"],scopes:["operator.admin","operator.approvals","operator.pairing"],tokens:{operator:{token:o,role:"operator",scopes:["operator.admin","operator.approvals","operator.pairing"],createdAtMs:t}},createdAtMs:t,approvedAtMs:t},mkdirSync(dirname(r),{recursive:!0}),writeFileSync(r,JSON.stringify(i,null,2))}
@@ -0,0 +1 @@
1
+ import{existsSync,mkdirSync,readFileSync,renameSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";const CACHE_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","discovery-cache.json");export function readDiscoveryCache(){try{if(!existsSync(CACHE_PATH))return null;const e=readFileSync(CACHE_PATH,"utf8"),n=JSON.parse(e);return"object"!=typeof n||null===n?null:n}catch{return null}}export function writeDiscoveryCache(e){try{const n=dirname(CACHE_PATH);mkdirSync(n,{recursive:!0});const r=CACHE_PATH+".tmp";writeFileSync(r,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),renameSync(r,CACHE_PATH)}catch{}}
@@ -0,0 +1 @@
1
+ const ERROR_PATTERNS={billing:[/["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+payment/i,"payment required","insufficient credits",/insufficient[_ ]quota/i,"credit balance","plans & billing","insufficient balance","insufficient usd or diem balance","billing error","add more credits"],rateLimit:[/rate[_ ]limit|too many requests|429/,"model_cooldown","exceeded your current quota","resource has been exhausted","quota exceeded","resource_exhausted","usage limit",/\btpm\b/i,"tokens per minute","tokens per day"],overloaded:[/overloaded_error|"type"\s*:\s*"overloaded_error"/i,"overloaded",/service[_ ]unavailable.*(?:overload|capacity|high[_ ]demand)|(?:overload|capacity|high[_ ]demand).*service[_ ]unavailable/i,"high demand"],timeout:["timeout","timed out","service unavailable","deadline exceeded","connection error","network error","network request failed","fetch failed","socket hang up",/\beconn(?:refused|reset|aborted)\b/i,/\benotfound\b/i],auth:[/invalid[_ ]?api[_ ]?key/,"incorrect api key","invalid token","unauthorized","forbidden","access denied","insufficient permissions",/\b401\b/,/\b403\b/,"no credentials found","no api key found",/api[_ ]?key[_ ]?(?:revoked|invalid|deactivated|deleted)/i,"invalid_api_key","key has been disabled","permission_error"],contextOverflow:["request_too_large","context length exceeded","maximum context length","prompt is too long","exceeds model context window","context overflow:","context_window_exceeded"]};function matchesPatterns(e,t){if(!e)return!1;const r=e.toLowerCase();return t.some(e=>e instanceof RegExp?e.test(r):r.includes(e))}function classifyErrorReason(e){return e?matchesPatterns(e,ERROR_PATTERNS.billing)?"billing":matchesPatterns(e,ERROR_PATTERNS.contextOverflow)?"context_overflow":matchesPatterns(e,ERROR_PATTERNS.rateLimit)?"rate_limit":matchesPatterns(e,ERROR_PATTERNS.overloaded)?"overloaded":matchesPatterns(e,ERROR_PATTERNS.timeout)?"timeout":matchesPatterns(e,ERROR_PATTERNS.auth)?"auth":null:null}const FRIENDLY_MESSAGES={billing:"API provider returned a billing error — your API key may have run out of credits or has an insufficient balance. Check your provider's billing dashboard and top up, or switch to a different API key.",rate_limit:"API rate limit reached. Please wait a moment and try again.",auth:"API authentication failed — your API key may be invalid, revoked, or lacking permissions. Check your provider settings.",timeout:"LLM request timed out or the service is temporarily unavailable. Please try again.",overloaded:"The AI service is temporarily overloaded. Please try again in a moment.",context_overflow:"Context overflow: prompt too large for the model. Try starting a fresh session or use a model with a larger context window."};export function formatFriendlyError(e){if(!e)return e;if(e.startsWith("⚠️"))return e;const t=classifyErrorReason(e);return t?FRIENDLY_MESSAGES[t]:e}
@@ -1 +1 @@
1
- import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,generateKeyPairSync,createHash,sign as cryptoSign,createPrivateKey}from"node:crypto";import{readFileSync,writeFileSync,mkdirSync,existsSync}from"node:fs";import{join,dirname}from"node:path";import{homedir}from"node:os";const PROTOCOL_VERSION=3,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,AUTH_FAILURE_CLOSE_CODE=4001,CONNECT_DELAY_MS=750,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect";function base64urlEncode(e){return e.toString("base64url")}function verifyDeviceFingerprint(e){try{const t=Buffer.from(e.publicKey,"base64url");return createHash("sha256").update(t).digest("hex")===e.deviceId}catch{return!1}}function loadOrCreateDeviceIdentity(e){const t=e||join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw");mkdirSync(t,{recursive:!0});const n=join(t,"device-key.json");if(existsSync(n))try{const e=JSON.parse(readFileSync(n,"utf8"));if(verifyDeviceFingerprint(e))return{deviceId:e.deviceId,publicKeyB64url:e.publicKey,privateKeyDer:Buffer.from(e.privateKey,"base64")}}catch{}const{publicKey:i,privateKey:s}=generateKeyPairSync("ed25519"),r=i.export({type:"spki",format:"der"}).subarray(-32),o=s.export({type:"pkcs8",format:"der"}),c=createHash("sha256").update(r).digest("hex"),a=base64urlEncode(r),h={deviceId:c,publicKeyB64url:a,privateKeyDer:o};return writeFileSync(n,JSON.stringify({deviceId:c,publicKey:a,privateKey:o.toString("base64")},null,2)),h}function buildDeviceAuthPayload(e){return["v2",e.deviceId,e.clientId,e.clientMode,e.role,e.scopes.join(","),String(e.signedAtMs),e.token,e.nonce].join("|")}function signDeviceAuth(e,t){const n=createPrivateKey({key:e.privateKeyDer,format:"der",type:"pkcs8"});return base64urlEncode(cryptoSign(null,Buffer.from(t),n))}function ensureDevicePaired(e){const t=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"devices","paired.json");let n={};try{n=JSON.parse(readFileSync(t,"utf8"))}catch{}if(n[e.deviceId])return;const i=Date.now(),s=base64urlEncode(Buffer.from(randomUUID().replace(/-/g,""),"hex"));n[e.deviceId]={deviceId:e.deviceId,publicKey:e.publicKeyB64url,clientId:"cli",clientMode:"cli",role:"operator",roles:["operator"],scopes:["operator.admin","operator.approvals","operator.pairing"],tokens:{operator:{token:s,role:"operator",scopes:["operator.admin","operator.approvals","operator.pairing"],createdAtMs:i}},createdAtMs:i,approvedAtMs:i},mkdirSync(dirname(t),{recursive:!0}),writeFileSync(t,JSON.stringify(n,null,2))}const MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch{}}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const i=new WebSocket(this.config.url);this.ws=i;let s=null;const r=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),i.close(),t(new Error("Gateway connection timeout"))},3e4),o=t=>{clearTimeout(r),this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},c=e=>{clearTimeout(r),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:e,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:e}),i.close(),t(new Error(e))};i.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),i.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");if("pong"===t)return void(this.lastPongTs=Date.now());let i;this.emitMessageHash(t);try{i=JSON.parse(t)}catch{return}const r=i.method;r&&!i.id&&(this.isReconnectNotification(r)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",r,i.params));const a=i.type;if("event"===a&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===a){const e=i.event,t=i.payload;if("connect.challenge"===e){this.emit("transport",{event:"handshake.challenge_recv",durationMs:Date.now()-n});const e=t?.nonce;setTimeout(()=>{s=this._sendConnect(e),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n})},750)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):this.emit("gateway-event",e,t)}else if("hello-ok"===a)o(i);else if("hello-error"===a||"connect-error"===a)c("Authentication rejected by Gateway");else if("res"===a){const e=i.id;if(e&&e===s){if(i.ok){const e=i.payload;o(e??i)}else{const e=i.error;c(e?.message||"Gateway connect rejected")}return}const t=this.pending.get(e);if(t)if(this.pending.delete(e),clearTimeout(t.timer),i.ok)t.resolve(i.payload);else{const e=i.error;t.reject(new Error(e?.message||"Gateway request failed"))}}}),i.on("pong",()=>{this.lastPongTs=Date.now()}),i.on("close",e=>{this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),i.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.connected||(clearTimeout(r),t(e)),this.emit("error",e)})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);const t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs);this.reconnectAttempt++,this.emit("reconnecting",{attempt:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))}))}_sendConnect(e){const t=Date.now(),n="operator",i=["operator.admin","operator.approvals","operator.pairing"],s={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:t};if(e){const r=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:n,scopes:i,signedAtMs:t,token:this.config.token,nonce:e});s.nonce=e,s.signature=signDeviceAuth(this.deviceIdentity,r)}const r=randomUUID(),o={type:"req",id:r,method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:n,scopes:i,auth:{token:this.config.token},device:s}};return this.ws?.send(JSON.stringify(o)),r}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const i=randomUUID(),s={type:"req",id:i,method:e,params:t};return new Promise((t,r)=>{const o=setTimeout(()=>{this.pending.delete(i),r(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(i,{resolve:t,reject:r,timer:o}),this.ws.send(JSON.stringify(s))})}async agentSend(e,t,n,i,s){const r={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};i?.length&&(r.attachments=i),s&&(r.inputProvenance=s);return await this.request("agent",r,12e4)}async chatSend(e,t,n){const i={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(i.attachments=n);return await this.request("chat.send",i,12e4)}async chatAbort(e,t){await this.request("chat.abort",{sessionKey:e,runId:t})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async skillsStatus(){return this.request("skills.status",{})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
1
+ import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,createHash}from"node:crypto";import{loadOrCreateDeviceIdentity,buildDeviceAuthPayload,signDeviceAuth,ensureDevicePaired}from"./device-identity.js";const PROTOCOL_VERSION=3,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,AUTH_FAILURE_CLOSE_CODE=4001,CONNECT_DELAY_MS=750,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect",MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch(e){}}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const i=new WebSocket(this.config.url);this.ws=i;let s=null;const o=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),i.close(),t(new Error("Gateway connection timeout"))},3e4),c=t=>{clearTimeout(o),this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},r=e=>{clearTimeout(o),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:e,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:e}),i.close(),t(new Error(e))};i.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),i.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");if("pong"===t)return void(this.lastPongTs=Date.now());let i;this.emitMessageHash(t);try{i=JSON.parse(t)}catch{return}const o=i.method;o&&!i.id&&(this.isReconnectNotification(o)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",o,i.params));const a=i.type;if("event"===a&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===a){const e=i.event,t=i.payload;if("connect.challenge"===e){this.emit("transport",{event:"handshake.challenge_recv",durationMs:Date.now()-n});const e=t?.nonce;setTimeout(()=>{s=this._sendConnect(e),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n})},750)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):this.emit("gateway-event",e,t)}else if("hello-ok"===a)c(i);else if("hello-error"===a||"connect-error"===a)r("Authentication rejected by Gateway");else if("res"===a){const e=i.id;if(e&&e===s){if(i.ok){const e=i.payload;c(e??i)}else{const e=i.error;r(e?.message||"Gateway connect rejected")}return}const t=this.pending.get(e);if(t)if(this.pending.delete(e),clearTimeout(t.timer),i.ok)t.resolve(i.payload);else{const e=i.error;t.reject(new Error(e?.message||"Gateway request failed"))}}}),i.on("pong",()=>{this.lastPongTs=Date.now()}),i.on("close",e=>{const i=this.connected;this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),i||(clearTimeout(o),t(new Error(`Connection closed during handshake (code=${e})`))),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),i.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.connected||(clearTimeout(o),t(e)),this.emit("error",e)})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);const t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs);this.reconnectAttempt++,this.emit("reconnecting",{attempt:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))}))}_sendConnect(e){const t=Date.now(),n="operator",i=["operator.admin","operator.approvals","operator.pairing"],s={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:t};if(e){const o=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:n,scopes:i,signedAtMs:t,token:this.config.token,nonce:e});s.nonce=e,s.signature=signDeviceAuth(this.deviceIdentity,o)}const o=randomUUID(),c={type:"req",id:o,method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:n,scopes:i,auth:{token:this.config.token},device:s}};return this.ws?.send(JSON.stringify(c)),o}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const i=randomUUID(),s={type:"req",id:i,method:e,params:t};return new Promise((t,o)=>{const c=setTimeout(()=>{this.pending.delete(i),o(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(i,{resolve:t,reject:o,timer:c}),this.ws.send(JSON.stringify(s))})}async agentSend(e,t,n,i,s){const o={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};i?.length&&(o.attachments=i),s&&(o.inputProvenance=s);return await this.request("agent",o,12e4)}async chatSend(e,t,n){const i={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(i.attachments=n);return await this.request("chat.send",i,12e4)}async chatAbort(e,t){await this.request("chat.abort",{sessionKey:e,runId:t})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async skillsStatus(){return this.request("skills.status",{})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK}from"./messaging/message-sdk.js";import{registerA2ATools}from"./a2a/tools.js";import{createA2ATaskHandler}from"./a2a/task-handler.js";import{createA2AResultHandler}from"./a2a/result-handler.js";import{createA2ATaskCancelHandler}from"./a2a/cancel-handler.js";import{TaskHandleRegistry}from"./a2a/task-handle.js";import{pollPendingTasks}from"./a2a/poll-pending.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"./file-stage.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{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"./form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./render-form-tool.js";import{ResourceDescriptorRegistry}from"./resource-descriptor-registry.js";import{CRON_RESOURCE}from"./cron-resource.js";import{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./action-tool-generator.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;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){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return void e.logger.error?.("[beeos-claw] Failed to resolve config:",r)}const a=o.config;pluginConfig=a;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:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.bridge.keyFile)try{agentKeys=loadAgentKeyPair(a.bridge.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),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)`);cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const t=await executeUpload(o,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${t.ok}`),s({component:"upload-tool",domain:"tool",name:t.ok?"upload.success":"upload.failure",severity:t.ok?"info":"warn",payload:{toolCallId:r,ok:t.ok,fileCount:t.files?.length??0,error:t.error}}),t.ok&&t.content&&acpServer&&acpServer.pushToolResultContent(r,BEEOS_UPLOAD_TOOL_NAME,t.content),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(t)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,r){await handleFileStage(e,r,a)}});const t=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,n=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;acpServer=new ACPServer(a,e.logger,s,t,n),e.registerService({id:"beeos-acp",async start(){await acpServer.start(),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const l=new TaskHandleRegistry;let i=null;const{enabled:c,url:g,principalId:d,a2aEnabled:p}=a.messageBus;if(c&&a.messageBus.serviceUrl){i=new MessageSDK({serviceUrl:a.messageBus.serviceUrl,apiKey:a.messageBus.apiKey,centrifugoUrl:g},e.logger);const r=d;e.registerService({id:"beeos-message-bus",async start(){let o=r;if(o||(o=process.env.BEEOS_PRINCIPAL_ID||""),!o&&acpServer)try{await acpServer.waitForAgentMapping(),o=acpServer.getDefaultPlatformAgentId()||""}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}o?(await i.connect(o,g),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s)",o)):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(i?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const o=(e,r,o,a)=>acpServer.agentSend(e,r,o,a),s=(e,r,o)=>acpServer.trackA2ARun(e,r,o),t=e=>acpServer.emitA2ASystemNotification(e),n=a.platformUrl?{platformUrl:a.platformUrl,agentKeys:agentKeys,logger:e.logger}:void 0;i.onMessage("a2a_task",createA2ATaskHandler(o,n,s,t)),i.onMessage("a2a_result",createA2AResultHandler(o,l,t)),i.onMessage("a2a_task_cancel",createA2ATaskCancelHandler(o,e=>acpServer.cancelA2ARunByTaskId(e))),n&&e.registerService({id:"beeos-a2a-poll",async start(){await acpServer.waitForAgentMapping();const r=await pollPendingTasks(n,o,s);r>0&&e.logger.info?.("[beeos-claw] Dispatched %d pending A2A tasks on startup",r)}})}else c?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)",c);if(!1!==p&&a.platformUrl){const r=e=>acpServer?.getGatewaySessionKeyForToolCall(e),o=e=>acpServer?.getMentionsForToolCall(e),s=(e,r)=>{acpServer?.broadcastNotification(e,r)};registerA2ATools(e,{platformUrl:a.platformUrl,agentKeys:agentKeys,logger:e.logger},l,r,o,s),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){const r=new CanvasStateManager;if(a.platformUrl&&agentKeys){r.setRelayOpts({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,logger:e.logger});(async()=>{try{await acpServer.waitForAgentMapping();const o=acpServer.getDefaultPlatformAgent(),s=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:o?.id,displayName:o?.displayName,avatarUrl:o?.avatarUrl,logger:e.logger});s.connect(),r.setRelayClient(s),o?.id&&r.setAgentId(o.id),acpServer.wireRelayActionHandler(),acpServer.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${o?.id??"none"}, name=${o?.name??"none"})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}})(),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: platformUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>s(e),isCanvasActive:()=>acpServer.isCanvasActive(),getActiveSessionId:e=>acpServer.getSessionIdForToolCall(e),getPlatformAgentId:e=>acpServer.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>acpServer.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>acpServer.waitForAgentMapping()},t=new FormSchemaRegistry;t.register(CRON_CREATE_SCHEMA),t.register(CRON_UPDATE_SCHEMA);const n=new ResourceDescriptorRegistry;n.register(CRON_RESOURCE);const l=e=>async(r,o)=>(acpServer.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:l(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:l(createCloseUiHandler(o))}),acpServer.setCanvasState(r,{schemaRegistry:t,descriptorRegistry:n});const i=acpServer.getFormBuilder();i&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(t),parameters:RENDER_FORM_TOOL_SCHEMA,execute:l(createRenderFormHandler({registry:t,builder:i,logger:e.logger}))});const c=acpServer.getResourceRenderer();if(c){generateActionTools(n,e,async(e,r)=>{const o=acpServer?.gateway;if(!o?.isConnected)throw new Error("Gateway not connected");return o.request(e,r)},c,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(n),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:l(createRenderResourceHandler(n,c,e.logger))})}const g=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME,RENDER_RESOURCE_TOOL_NAME]);if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, render_form, or render_resource. Respond with plain text only."}),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,r)=>{if(r?.sessionKey&&acpServer.setPendingToolCallSessionKey(r.sessionKey),!g.has(e.toolName))return;return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?void 0:{block:!0,blockReason:"Canvas is closed by user. 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, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,_=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:s});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){_.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){_.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)"),r){const o=mirrorOpenClawConfig(r);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}e.logger.info?.("[beeos-claw] Plugin registered successfully")}};export default plugin;
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK}from"./messaging/message-sdk.js";import{registerA2ATools}from"./a2a/tools.js";import{createA2ATaskHandler}from"./a2a/task-handler.js";import{createA2AResultHandler}from"./a2a/result-handler.js";import{createA2ATaskCancelHandler}from"./a2a/cancel-handler.js";import{TaskHandleRegistry}from"./a2a/task-handle.js";import{pollPendingTasks}from"./a2a/poll-pending.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{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{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{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}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,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;function initializePlugin(e){let r,a;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{a=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const o=a.config;pluginConfig=o;const s=buildThirdPartyChannelSet(r);s.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...s].join(", ")}`);const t=o.log.enabled?createObsEventWriter({dir:o.log.dir,redaction:{gatewayToken:o.gateway.token},logger:e.logger}):()=>{};t({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),t({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:o,sources:a.sources,openclawConfigPath:a.openclawConfigPath}});for(const r of a.validation)t({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(a.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),o.keyFile)try{agentKeys=loadAgentKeyPair(o.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),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{cfg:o,writeObs:t,thirdPartyChannels:s,openclawConfig:r}}function registerUploadTools(e,r){const{cfg:a,writeObs:o}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(s)}`);const t=await executeUpload(s,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${t.ok}`),o({component:"upload-tool",domain:"tool",name:t.ok?"upload.success":"upload.failure",severity:t.ok?"info":"warn",payload:{toolCallId:r,ok:t.ok,fileCount:t.files?.length??0,error:t.error}}),t.ok&&t.content&&acpServer&&acpServer.pushToolResultContent(r,BEEOS_UPLOAD_TOOL_NAME,t.content),(async a=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,a),await cleanupUploadToolResultCache()}catch(a){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(a)}`)}return a})(t)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,a){await handleFileStage(e,a,r.cfg)}})}function registerServices(e,r){const{cfg:a,writeObs:o,thirdPartyChannels:s}=r,t=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,n=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;acpServer=new ACPServer(a,e.logger,o,t,n,s),e.registerService({id:"beeos-acp",async start(){if(!a.bridge.url&&a.agentGatewayUrl&&t){const r=await discoverBridgeFromGW(a.agentGatewayUrl,t);if(r)a.bridge.url=r,writeDiscoveryCache({bridge:{url:r,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${r}`);else{const r=readDiscoveryCache();r?.bridge?.url?(a.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await acpServer.start(),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const i=new TaskHandleRegistry;let l=null;const{enabled:c,url:g,principalId:d,a2aEnabled:p}=a.messageBus;let f;if(a.agentGatewayUrl&&agentKeys){const e=a.agentGatewayUrl,r=agentKeys;f=async a=>{const o=await agentFetch(`${e}/api/v1/messaging/token`,r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({principal_id:a})});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Gateway messaging token returned ${o.status}: ${e}`)}return o.json()}}if(c&&(f||a.messageBus.serviceUrl)){l=new MessageSDK({serviceUrl:f?void 0:a.messageBus.serviceUrl,centrifugoUrl:g},e.logger);const r=d;e.registerService({id:"beeos-message-bus",async start(){let a=r;if(a||(a=process.env.BEEOS_PRINCIPAL_ID||""),!a&&acpServer)try{await acpServer.waitForAgentMapping(),a=acpServer.getDefaultPlatformAgentId()||""}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}a?(await l.connect(a,{centrifugoUrl:g,tokenProvider:f}),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",a,f?"gateway":"direct")):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(l?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const o=(e,r,a,o)=>acpServer.agentSend(e,r,a,o),s=(e,r,a)=>acpServer.trackA2ARun(e,r,a),t=e=>acpServer.emitA2ASystemNotification(e),n=a.agentGatewayUrl?{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger}:void 0;l.onMessage("a2a_task",createA2ATaskHandler(o,n,s,t)),l.onMessage("a2a_result",createA2AResultHandler(o,i,t)),l.onMessage("a2a_task_cancel",createA2ATaskCancelHandler(o,e=>acpServer.cancelA2ARunByTaskId(e))),n&&e.registerService({id:"beeos-a2a-poll",async start(){await acpServer.waitForAgentMapping();const r=await pollPendingTasks(n,o,s);r>0&&e.logger.info?.("[beeos-claw] Dispatched %d pending A2A tasks on startup",r)}})}else c?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)",c);if(!1!==p&&a.agentGatewayUrl){const r=e=>acpServer?.getGatewaySessionKeyForToolCall(e),o=e=>acpServer?.getMentionsForToolCall(e),s=(e,r)=>{acpServer?.broadcastNotification(e,r)};registerA2ATools(e,{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},i,r,o,s),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){const r=new CanvasStateManager;if(a.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=a.agentGatewayUrl,r=agentKeys;o=async()=>{const a=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!a.ok){const e=await a.text().catch(()=>"");throw new Error(`Canvas token returned ${a.status}: ${e}`)}return a.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await acpServer.waitForAgentMapping();const s=acpServer.getDefaultPlatformAgent();if(!s?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const t=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:s.id,displayName:s.displayName,avatarUrl:s.avatarUrl,tokenProvider:o,logger:e.logger});t.connect(),r.setRelayClient(t),r.setAgentId(s.id),acpServer.wireRelayActionHandler(),acpServer.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${s.id}, name=${s.name??"none"})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const t={stateManager:r,logger:e.logger,writeObs:e=>o(e),isCanvasActive:()=>acpServer.isCanvasActive(),getActiveSessionId:e=>acpServer.getSessionIdForToolCall(e),getPlatformAgentId:e=>acpServer.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>acpServer.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>acpServer.waitForAgentMapping()},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE);const l=e=>async(r,a)=>(acpServer.preMapToolCallSession(r),e(r,a));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:l(createRenderUiHandler(t))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:l(createCloseUiHandler(t))}),acpServer.setCanvasState(r,{schemaRegistry:n,descriptorRegistry:i});const c=acpServer.getFormBuilder();c&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:l(createRenderFormHandler({registry:n,builder:c,logger:e.logger}))});const g=acpServer.getResourceRenderer();if(g){generateActionTools(i,e,async(e,r)=>acpServer.gatewayRequest(e,r),g,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(i),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:l(createRenderResourceHandler(i,g,e.logger))})}const d=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME,RENDER_RESOURCE_TOOL_NAME]),p=256,f=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(f.size>=p&&!f.has(r.sessionKey)){const e=f.keys().next().value;void 0!==e&&f.delete(e)}f.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(s,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return;return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, render_form, or render_resource. Respond with plain text only."}}),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,r)=>{if(r?.sessionKey&&acpServer.setPendingToolCallSessionKey(r.sessionKey),!d.has(e.toolName))return;const a=r?.sessionKey?f.get(r.sessionKey):void 0;if(isThirdPartyChannel(s,{sessionKey:r?.sessionKey,channelId:a}))return{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."};return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?void 0:{block:!0,blockReason:"Canvas is closed by user. 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, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const u=new TerminalManager,y=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:u,logger:e.logger,writeObs:o});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){y.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){y.stop(),u.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const a=mirrorOpenClawConfig(r.openclawConfig);a.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${a.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${a.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){const r=initializePlugin(e);r&&(registerUploadTools(e,r),registerServices(e,r),e.logger.info?.("[beeos-claw] Plugin registered successfully"))}};export default plugin;
@@ -1 +1 @@
1
- import{Centrifuge}from"centrifuge";import WebSocket from"ws";export class MessageSDK{config;client=null;personalSub=null;principalId="";handlers=new Map;logger;constructor(e,t){this.config=e,this.logger=t}async getToken(e,t,n){const s={principal_id:e};return t&&(s.claims=t),n&&(s.ttl_seconds=n),this.restRequest("POST","/api/v1/tokens",s)}async send(e,t,n){await this.restRequest("POST","/api/v1/messages/send",{target_id:e,message_type:t,payload:n})}async isOnline(e){return(await this.restRequest("GET",`/api/v1/connections/${encodeURIComponent(e)}/online`)).online}async registerWebhook(e,t,n){const s={url:e,events:t};return n&&(s.secret=n),this.restRequest("POST","/api/v1/webhooks",s)}async removeWebhook(e){await this.restRequest("DELETE",`/api/v1/webhooks/${encodeURIComponent(e)}`)}async connect(e,t){this.client&&await this.disconnect(),this.principalId=e;const n=await this.getToken(e),s=t||this.config.centrifugoUrl||n.centrifugo_url;if(!s)throw new Error("No Centrifugo URL available");this.client=new Centrifuge(s,{token:n.token,getToken:async()=>(await this.getToken(e)).token,websocket:WebSocket}),this.client.on("connected",e=>{this.logger.info?.(`[message-sdk] Connected (client=${e.client})`)}),this.client.on("disconnected",e=>{this.logger.warn?.(`[message-sdk] Disconnected: code=${e.code} reason=${e.reason}`)}),this.client.on("error",e=>{this.logger.error?.("[message-sdk] Error:",e.error)}),this.client.connect(),this.personalSub=this.client.newSubscription(`personal:${e}`),this.personalSub.on("publication",e=>{this.dispatch(e.data).catch(e=>{this.logger.error?.("[message-sdk] dispatch error:",e)})}),this.personalSub.subscribe()}async disconnect(){this.personalSub?.unsubscribe(),this.client?.disconnect(),this.client=null,this.personalSub=null}onMessage(e,t){this.handlers.set(e,t)}async joinGroup(e,t){if(!this.client)throw new Error("Not connected");const n=this.client.newSubscription(`group:${e}`);return n.on("publication",n=>{t(n.data).catch(t=>{this.logger.error?.(`[message-sdk] group handler error (${e}):`,t)})}),n.subscribe(),n}async publishToGroup(e,t){if(!this.client)throw new Error("Not connected");await this.client.publish(`group:${e}`,{from:this.principalId,content:t,timestamp:Date.now()})}get isConnected(){return"connected"===this.client?.state}get currentPrincipalId(){return this.principalId}async dispatch(e){const t=this.handlers.get(e.type);t?await t(e):this.logger.debug?.(`[message-sdk] No handler for type=${e.type}`)}async restRequest(e,t,n){const s=`${this.config.serviceUrl}${t}`,i={"X-API-Key":this.config.apiKey},o={method:e,headers:i};void 0!==n&&(i["Content-Type"]="application/json",o.body=JSON.stringify(n));const r=await fetch(s,o);if(!r.ok){const n=await r.text().catch(()=>"");throw new Error(`Message Service ${e} ${t} returned ${r.status}: ${n}`)}if(204!==r.status)return await r.json()}}
1
+ import{Centrifuge}from"centrifuge";import WebSocket from"ws";export class MessageSDK{config;client=null;personalSub=null;principalId="";handlers=new Map;connectLock=null;logger;constructor(e,t){this.config=e,this.logger=t}async getToken(e,t,n){const s={principal_id:e};return t&&(s.claims=t),n&&(s.ttl_seconds=n),this.restRequest("POST","/api/v1/tokens",s)}async send(e,t,n){await this.restRequest("POST","/api/v1/messages/send",{target_id:e,message_type:t,payload:n})}async isOnline(e){return(await this.restRequest("GET",`/api/v1/connections/${encodeURIComponent(e)}/online`)).online}async registerWebhook(e,t,n){const s={url:e,events:t};return n&&(s.secret=n),this.restRequest("POST","/api/v1/webhooks",s)}async removeWebhook(e){await this.restRequest("DELETE",`/api/v1/webhooks/${encodeURIComponent(e)}`)}async connect(e,t){let n;this.connectLock&&await this.connectLock,this.connectLock=new Promise(e=>{n=e});try{await this._doConnect(e,t)}finally{this.connectLock=null,n()}}async _doConnect(e,t){this.client&&await this.disconnect(),this.principalId=e;const n=t?.tokenProvider??(e=>this.getToken(e)),s=await n(e),i=t?.centrifugoUrl||this.config.centrifugoUrl||s.centrifugo_url;if(!i)throw new Error("No Centrifugo URL available");this.client=new Centrifuge(i,{token:s.token,getToken:async()=>(await n(e)).token,websocket:WebSocket}),this.client.on("connected",e=>{this.logger.info?.(`[message-sdk] Connected (client=${e.client})`)}),this.client.on("disconnected",e=>{this.logger.warn?.(`[message-sdk] Disconnected: code=${e.code} reason=${e.reason}`)}),this.client.on("error",e=>{this.logger.error?.("[message-sdk] Error:",e.error)}),this.client.connect(),this.personalSub=this.client.newSubscription(`personal:${e}`),this.personalSub.on("publication",e=>{this.dispatch(e.data).catch(e=>{this.logger.error?.("[message-sdk] dispatch error:",e)})}),this.personalSub.subscribe()}async disconnect(){this.personalSub?.unsubscribe(),this.client?.disconnect(),this.client=null,this.personalSub=null}onMessage(e,t){this.handlers.set(e,t)}async joinGroup(e,t){if(!this.client)throw new Error("Not connected");const n=this.client.newSubscription(`group:${e}`);return n.on("publication",n=>{t(n.data).catch(t=>{this.logger.error?.(`[message-sdk] group handler error (${e}):`,t)})}),n.subscribe(),n}async publishToGroup(e,t){if(!this.client)throw new Error("Not connected");await this.client.publish(`group:${e}`,{from:this.principalId,content:t,timestamp:Date.now()})}get isConnected(){return"connected"===this.client?.state}get currentPrincipalId(){return this.principalId}async dispatch(e){const t=this.handlers.get(e.type);t?await t(e):this.logger.debug?.(`[message-sdk] No handler for type=${e.type}`)}async restRequest(e,t,n){if(!this.config.serviceUrl)throw new Error("Message Service REST API requires serviceUrl in config");const s=`${this.config.serviceUrl}${t}`,i={},o={method:e,headers:i};void 0!==n&&(i["Content-Type"]="application/json",o.body=JSON.stringify(n));const r=await fetch(s,o);if(!r.ok){const n=await r.text().catch(()=>"");throw new Error(`Message Service ${e} ${t} returned ${r.status}: ${n}`)}if(204!==r.status)return await r.json()}}
@@ -0,0 +1 @@
1
+ function formatSchedule(e,t){const a=e;if(!a)return"—";const r=a.kind;if("cron"===r)return a.expr??"—";if("every"===r){const e=a.everyMs??0;return e>=864e5?`Every ${Math.round(e/864e5)}d`:e>=36e5?`Every ${Math.round(e/36e5)}h`:e>=6e4?`Every ${Math.round(e/6e4)}m`:`Every ${Math.round(e/1e3)}s`}return"at"===r?`At ${a.at??"—"}`:"—"}export const CRON_RESOURCE={domain:"cron",title:"Scheduled Tasks",listRpc:"cron.list",listParams:{includeDisabled:!0,limit:50},extractItems:e=>{const t=e?.jobs;return Array.isArray(t)?t:[]},itemId:e=>e.id,columns:[{key:"name",label:"Name"},{key:"schedule",label:"Schedule",format:formatSchedule},{key:"enabled",label:"Status",format:e=>e?"Enabled":"Disabled"},{key:"state.lastRunStatus",label:"Last Run",format:e=>String(e??"—")},{key:"state.nextRunAtMs",label:"Next Run",format:e=>e?new Date(Number(e)).toLocaleString():"—"}],itemActions:[{id:"toggle",type:"rpc",rpc:"cron.update",label:e=>e.enabled?`Disable "${e.name}"`:`Enable "${e.name}"`,buildParams:e=>({id:e.id,patch:{enabled:!e.enabled}}),description:"Toggle a cron job on/off"},{id:"edit",type:"form",formDomain:"cron",formOperation:"update",label:e=>`Edit "${e.name}"`,buildFormData:e=>e},{id:"run",type:"rpc",rpc:"cron.run",label:e=>`Run "${e.name}"`,buildParams:e=>({id:e.id,mode:"force"}),description:"Run a cron job immediately"},{id:"delete",type:"rpc",rpc:"cron.remove",label:e=>`Delete "${e.name}"`,buildParams:e=>({id:e.id}),description:"Delete a cron job"}],globalActions:[{id:"refresh",type:"refresh",label:"Refresh"}],slashCommand:"cron",emptyTitle:"No scheduled tasks",emptyContent:"Use the agent to create cron jobs, or add them via the form below."};
@@ -0,0 +1 @@
1
+ import{sv,nextComponentId}from"../canvas/a2ui-helpers.js";function getByPath(e,t){if(!e)return;const n=t.split(".");let a=e;for(const e of n){if(null==a||"object"!=typeof a)return;a=a[e]}return a}function setByPath(e,t,n){if(!t)return;const a=t.split(".");let o=e;for(let e=0;e<a.length-1;e++){const t=a[e];null!=o[t]&&"object"==typeof o[t]||(o[t]={}),o=o[t]}o[a[a.length-1]]=n}function nextId(){return nextComponentId("fsb")}export class FormSurfaceBuilder{deps;constructor(e){this.deps=e}cid(e){return this.deps.resolveCanvasId?.()??e??`_form_${Date.now()}`}build(e){const{schema:t,data:n,entityId:a,onSuccess:o}=e,i=e.surfaceId??`${t.domain}-${t.operation}-form`,s=e.presentation??"panel",l=`${t.domain}_${t.operation}_submit`,p=[],d=[];if(t.title){const e=nextId();p.push({id:e,component:{Text:{text:sv(t.title),usageHint:sv("h3")}}}),d.push(e)}if(t.description){const e=nextId();p.push({id:e,component:{Text:{text:sv(t.description)}}}),d.push(e)}const u={};for(const e of t.fields){const t=nextId(),a=this.schemaFieldToA2UI(e,n),o=`/${e.name}`;if("checkbox"===a.inputType||"boolean"===a.inputType||"toggle"===a.inputType){const n=!!a.defaultValue;u[e.name]=n,p.push({id:t,component:{CheckBox:{label:sv(String(a.label??"")),value:{path:o}}}})}else if("select"===a.inputType||"multi_select"===a.inputType||"dropdown"===a.inputType||"radio"===a.inputType){const n=Array.isArray(a.options)?a.options:[],i=n.map(e=>"string"==typeof e?{label:sv(e),value:sv(e)}:{label:sv(String(e.label??e.value??"")),value:sv(String(e.value??e.label??""))}),s={label:sv(String(a.label??"")),options:i,selections:{path:o}};if(null!=a.defaultValue)u[e.name]=String(a.defaultValue);else if(n.length>0){const t=n[0];u[e.name]="string"==typeof t?t:String(t.value??"")}else u[e.name]="";p.push({id:t,component:{MultipleChoice:s}})}else if("datetime"===a.inputType||"time"===a.inputType){const n="datetime"===a.inputType,i=!0;u[e.name]=null!=a.defaultValue?String(a.defaultValue):"",p.push({id:t,component:{DateTimeInput:{value:{path:o},enableDate:n,enableTime:i}}})}else if("range"===a.inputType){const n="number"==typeof a.min?a.min:0,i="number"==typeof a.max?a.max:100;u[e.name]=null!=a.defaultValue?Number(a.defaultValue):n,p.push({id:t,component:{Slider:{label:sv(String(a.label??"")),value:{path:o},minValue:n,maxValue:i}}})}else{const n="number"===a.inputType?"number":"date"===a.inputType?"date":"textarea"===a.inputType?"longText":"shortText",i={label:sv(String(a.label??"")),type:sv(n),text:{path:o}};u[e.name]=null!=a.defaultValue?String(a.defaultValue):"",p.push({id:t,component:{TextField:i}})}d.push(t)}const r=t.fields.map(e=>({key:e.name,value:{path:`/${e.name}`}})),c=nextId();p.push({id:c,component:{Text:{text:sv(t.submitLabel??"Submit")}}});const m=nextId();p.push({id:m,component:{Button:{child:c,action:{name:l,context:r}}}}),d.push(m);const h=nextId();p.push({id:h,component:{Column:{children:{explicitList:d}}}});const f=this.cid(e.canvasId),x=this.deps.canvasState.has(i,f),b=async e=>{if((e.userAction?.name??"")!==l)return;const n=e.userAction?.context??{};await this.handleSubmit(t,n,i,p,f,a,o)};x?(this.deps.canvasState.update(i,p,f,h,u),this.deps.canvasState.bindActionHandler(i,b,f)):this.deps.canvasState.create(f,i,s,p,{origin:{kind:"system",handlerId:`${t.domain}-form`},actionHandler:b,rootComponentId:h,dataModel:u}),this.deps.logger.debug?.(`[form-builder] ${x?"Updated":"Created"} form surface=${i} domain=${t.domain} op=${t.operation}`)}schemaFieldToA2UI(e,t){const n={name:e.name,label:e.label,inputType:e.inputType};e.placeholder&&(n.placeholder=e.placeholder),e.required&&(n.required=!0),e.options&&(n.options=e.options);let a=e.defaultValue;if(void 0===a&&t){a=getByPath(t,e.apiPath??e.name)}return null!=a&&(n.defaultValue=a),n}async handleSubmit(e,t,n,a,o,i,s){try{const a=e.transformPayload?e.transformPayload(t):this.autoTransform(e.fields,t),l="update"===e.operation&&i?{id:i,patch:a}:a;await this.deps.gatewayRequest(e.targetRpc,l),this.deps.logger.info?.(`[form-builder] Submit success: ${e.targetRpc} (${e.domain}:${e.operation})`);const p=nextId(),d=nextId(),u=nextId(),r=nextId(),c=[{id:p,component:{Icon:{name:sv("check")}}},{id:d,component:{Text:{text:sv(`${e.title} saved successfully.`)}}},{id:u,component:{Row:{children:{explicitList:[p,d]}}}},{id:r,component:{Card:{child:u}}}];this.deps.canvasState.update(n,c,o),s&&await s()}catch(t){const i=t instanceof Error?t.message:String(t);this.deps.logger.warn?.(`[form-builder] Submit failed: ${e.targetRpc} — ${i}`);const s=nextId(),l=nextId(),p=nextId(),d=nextId(),u=[...a,...[{id:s,component:{Icon:{name:sv("error")}}},{id:l,component:{Text:{text:sv(i)}}},{id:p,component:{Row:{children:{explicitList:[s,l]}}}},{id:d,component:{Card:{child:p}}}]];this.deps.canvasState.update(n,u,o)}}autoTransform(e,t){const n={};for(const a of e){const e=t[a.name];if(void 0===e||""===e)continue;setByPath(n,a.apiPath??a.name,e)}return n}}
@@ -0,0 +1 @@
1
+ export{FormSchemaRegistry}from"./form-schema-registry.js";export{FormSurfaceBuilder}from"./form-surface-builder.js";export{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./render-form-tool.js";export{ResourceDescriptorRegistry}from"./resource-descriptor-registry.js";export{ResourceSurfaceRenderer}from"./resource-surface-renderer.js";export{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./action-tool-generator.js";export{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./cron-schemas.js";export{CRON_RESOURCE}from"./cron-resource.js";
@@ -0,0 +1 @@
1
+ import{resolveColumnValue,resolveActionLabel}from"./resource-descriptor.js";import{sv,nextComponentId}from"../canvas/a2ui-helpers.js";function textComp(e,t="body"){return{Text:{text:sv(e),usageHint:t}}}function nextId(){return nextComponentId("rsr")}export class ResourceSurfaceRenderer{deps;constructor(e){this.deps=e}cid(){return this.deps.resolveCanvasId?.()??`_resource_${Date.now()}`}async render(e,t="panel",n){try{const o=await this.deps.gatewayRequest(e.listRpc,e.listParams??{}),s=e.extractItems(o)??[];this.buildAndSendSurface(e,s,t,n)}catch(t){this.deps.logger.warn?.(`[resource-renderer] Failed to render ${e.domain}: ${t instanceof Error?t.message:t}`)}}buildAndSendSurface(e,t,n,o){const s=this.surfaceIdFor(e,n),{components:i,rootId:r}=this.buildComponents(e,t),c=o??this.cid(),d=this.deps.canvasState.has(s,c),a=t=>this.handleAction(e,t,n,c);d?(this.deps.canvasState.update(s,i,c,r),this.deps.canvasState.bindActionHandler(s,a,c)):this.deps.canvasState.create(c,s,n,i,{origin:{kind:"system",handlerId:`${e.domain}-resource`},actionHandler:a,rootComponentId:r}),this.deps.logger.debug?.(`[resource-renderer] ${d?"Updated":"Created"} surface=${s} domain=${e.domain} items=${t.length}`)}buildComponents(e,t){const n=[],o=[],s=nextId();if(n.push({id:s,component:textComp(e.title,"h3")}),o.push(s),0===t.length){const t=nextId();n.push({id:t,component:{Icon:{name:sv("info")}}});const s=nextId();n.push({id:s,component:textComp(e.emptyTitle??"No items")});const i=nextId();n.push({id:i,component:{Row:{children:{explicitList:[t,s]}}}}),o.push(i);const r=nextId();n.push({id:r,component:{Column:{children:{explicitList:o}}}});const c=nextId();return n.push({id:c,component:{Card:{child:r}}}),{components:n,rootId:c}}const i=[];for(const t of e.columns){const e=nextId();n.push({id:e,component:textComp(t.label,"caption")}),i.push(e)}const r=nextId();n.push({id:r,component:{Row:{children:{explicitList:i}}}}),o.push(r);const c=nextId();n.push({id:c,component:{Divider:{axis:"horizontal"}}}),o.push(c);for(const s of t){const t=[];for(const o of e.columns){const e=nextId(),i=resolveColumnValue(o,s);n.push({id:e,component:textComp(String(i))}),t.push(e)}const i=nextId();n.push({id:i,component:{Row:{children:{explicitList:t}}}}),o.push(i)}if(e.itemActions.length>0){const s=nextId();n.push({id:s,component:{Divider:{axis:"horizontal"}}}),o.push(s);for(const s of t){const t=[];for(const o of e.itemActions){const i=nextId(),r=resolveActionLabel(o.label,s);n.push({id:i,component:textComp(r)});const c=nextId(),d=`${e.domain}_${o.id}`;let a;if("rpc"===o.type){const e=o.buildParams(s);a=Object.entries(e).map(([e,t])=>({key:e,value:{literalString:String(t??"")}}))}else{if(a=[{key:"__itemId",value:{literalString:e.itemId(s)}}],o.buildFormData){const e=o.buildFormData(s);for(const[t,n]of Object.entries(e))a.push({key:t,value:{literalString:String(n??"")}})}}n.push({id:c,component:{Button:{child:i,action:{name:d,context:a}}}}),t.push(c)}const i=nextId();n.push({id:i,component:{Row:{children:{explicitList:t}}}}),o.push(i)}}for(const t of e.globalActions){const s=nextId();n.push({id:s,component:textComp(t.label)});const i=nextId(),r=`${e.domain}_${t.id}`;n.push({id:i,component:{Button:{child:s,action:{name:r}}}}),o.push(i)}const d=nextId();n.push({id:d,component:{Column:{children:{explicitList:o}}}});const a=nextId();return n.push({id:a,component:{Card:{child:d}}}),{components:n,rootId:a}}async handleAction(e,t,n,o){const s=t.userAction?.name??"",i=t.userAction?.context??{},r=`${e.domain}_`;if(!s.startsWith(r))return;const c=s.slice(r.length),d=e.globalActions.find(e=>e.id===c);if(d){if("refresh"===d.type)return void await this.render(e,n,o);if("form"===d.type)return void this.openForm(d.formDomain,d.formOperation,void 0,void 0,e,n,o)}const a=e.itemActions.find(e=>e.id===c);if(a){try{if("rpc"===a.type)await this.deps.gatewayRequest(a.rpc,i);else if("form"===a.type){const t=i.__itemId??void 0,s={...i};return delete s.__itemId,void this.openForm(a.formDomain,a.formOperation,Object.keys(s).length>0?s:void 0,t,e,n,o)}}catch(e){this.deps.logger.warn?.(`[resource-renderer] Action ${s} failed: ${e instanceof Error?e.message:e}`)}await this.render(e,n,o)}}openForm(e,t,n,o,s,i,r){const c=this.deps.schemaRegistry.get(e,t);c?this.deps.formBuilder.build({schema:c,data:n,entityId:o,canvasId:r,onSuccess:()=>this.render(s,i,r)}):this.deps.logger.warn?.(`[resource-renderer] Form schema not found: ${e}:${t}`)}surfaceIdFor(e,t){return"inline"===t?`${e.domain}-resource-inline`:`${e.domain}-resource`}}
@@ -0,0 +1 @@
1
+ import{TERMINAL_UPDATE_METHOD,parseTerminalOpenParams,parseTerminalInputParams,parseTerminalResizeParams,parseTerminalCloseParams,parseTerminalWaitParams}from"./rpc.js";import{Errors}from"../errors/index.js";export class TerminalBridgeHandler{terminalMgr;transport;config;logger;writeObs;interactiveTerminals=new Map;HEARTBEAT_TIMEOUT_MS=6e4;constructor(e){this.terminalMgr=e.terminalMgr,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs}setupListeners(){this.terminalMgr.addListeners({onOutput:e=>{this.interactiveTerminals.has(e.terminalId)&&this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"stdout",dataBase64:e.data.toString("base64"),seq:e.seq})},onExit:e=>{const r=this.interactiveTerminals.get(e.terminalId);r&&(clearTimeout(r.heartbeatTimer),this.interactiveTerminals.delete(e.terminalId),this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"exit",code:e.code,signal:e.signal,...e.reason?{reason:e.reason}:{}}))},onStateChange:e=>{this.interactiveTerminals.has(e.terminalId)&&this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"state",from:e.from,to:e.to})}})}async handleRequest(e){switch(e.method){case"terminal/open":await this.handleTerminalOpen(e);break;case"terminal/input":this.handleTerminalInput(e);break;case"terminal/resize":this.handleTerminalResize(e);break;case"terminal/close":await this.handleTerminalClose(e);break;case"terminal/create":await this.handleTerminalCreate(e);break;case"terminal/wait":await this.handleTerminalWait(e);break;case"terminal/release":await this.handleTerminalRelease(e);break;case"terminal/kill":await this.handleTerminalKill(e);break;default:this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}handleNotification(e,r){switch(e){case"terminal/input":this.handleTerminalInputNotification(r);break;case"terminal/resize":this.handleTerminalResizeNotification(r);break;default:this.logger.debug?.(`[terminal-handler] Unknown notification: ${e}`)}}handleTerminalInputNotification(e){if(!this.config.bridge.shell.enabled)return;const r=parseTerminalInputParams(e);if(!r.ok)return;let a;if(r.value.dataBase64)try{a=Buffer.from(r.value.dataBase64,"base64")}catch{return}else a=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,a),this.refreshTerminalHeartbeat(r.value.terminalId)}catch{}}handleTerminalResizeNotification(e){if(!this.config.bridge.shell.enabled)return;const r=parseTerminalResizeParams(e);if(r.ok)try{this.terminalMgr.resizeSession(r.value.terminalId,r.value.cols,r.value.rows),this.refreshTerminalHeartbeat(r.value.terminalId)}catch{}}async handleTerminalCreate(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalOpenParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);this.terminalMgr.sessionCount>=this.terminalMgr.maxConcurrentLimit&&this.logger.warn?.("[terminal-handler] Terminal quota exceeded, evicting oldest");const a=this.terminalMgr.create(r.value.command);this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.created",severity:"info",payload:{terminalId:a.id,command:a.command,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:a.id,command:a.command})}async handleTerminalWait(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalWaitParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.terminalMgr.get(r.value.terminalId);a?this.transport.sendResult(e.id,{terminalId:r.value.terminalId,exitCode:a.exitCode,output:a.output.join(""),state:a.state}):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalRelease(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);this.terminalMgr.get(r.value.terminalId)?(this.terminalMgr.release(r.value.terminalId),this.transport.sendResult(e.id,{})):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalKill(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.terminalMgr.get(r.value.terminalId);a?"closed"!==a.state?(this.terminalMgr.kill(r.value.terminalId,r.value.signal),this.transport.sendResult(e.id,{})):this.transport.sendAcpError(e.id,Errors.TerminalClosed):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalOpen(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalOpenParams(e.params);if(r.ok)try{const a=this.terminalMgr.openSession({sessionId:r.value.sessionId,command:r.value.command,cols:r.value.cols,rows:r.value.rows,cwd:r.value.cwd}),t=setTimeout(()=>{this.interactiveTerminals.delete(a.terminalId),this.terminalMgr.closeSession(a.terminalId,"heartbeat_timeout")},this.HEARTBEAT_TIMEOUT_MS);"object"==typeof t&&"unref"in t&&t.unref(),this.interactiveTerminals.set(a.terminalId,{heartbeatTimer:t}),this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.opened",severity:"info",payload:{terminalId:a.terminalId,shell:a.shell,cwd:a.cwd,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:a.terminalId,command:a.shell,shell:a.shell,cwd:a.cwd,cols:a.cols,rows:a.rows,state:a.state})}catch(r){const a=r instanceof Error?r.message:String(r);a.includes("quota")?this.transport.sendAcpError(e.id,Errors.TerminalQuotaExceeded,{message:a}):this.transport.sendAcpError(e.id,Errors.GeneralError,{message:a})}else this.sendTerminalParseError(e.id,r)}handleTerminalInput(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalInputParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);let a;if(r.value.dataBase64)try{a=Buffer.from(r.value.dataBase64,"base64")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"invalid dataBase64"})}else a=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,a),this.refreshTerminalHeartbeat(r.value.terminalId),this.transport.sendResult(e.id,{})}catch(r){const a=r instanceof Error?r.message:String(r);this.transport.sendAcpError(e.id,Errors.TerminalNotFound,{message:a})}}handleTerminalResize(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalResizeParams(e.params);if(r.ok)try{this.terminalMgr.resizeSession(r.value.terminalId,r.value.cols,r.value.rows),this.refreshTerminalHeartbeat(r.value.terminalId),this.transport.sendResult(e.id,{})}catch(r){const a=r instanceof Error?r.message:String(r);this.transport.sendAcpError(e.id,Errors.TerminalNotFound,{message:a})}else this.sendTerminalParseError(e.id,r)}async handleTerminalClose(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.interactiveTerminals.get(r.value.terminalId);a&&(clearTimeout(a.heartbeatTimer),this.interactiveTerminals.delete(r.value.terminalId)),this.terminalMgr.closeSession(r.value.terminalId),this.transport.sendResult(e.id,{})}refreshTerminalHeartbeat(e){const r=this.interactiveTerminals.get(e);r&&(clearTimeout(r.heartbeatTimer),r.heartbeatTimer=setTimeout(()=>{this.logger.warn?.(`[terminal-handler] Terminal heartbeat timeout terminalId=${e}`),this.interactiveTerminals.delete(e),this.terminalMgr.closeSession(e,"heartbeat_timeout")},this.HEARTBEAT_TIMEOUT_MS),"object"==typeof r.heartbeatTimer&&"unref"in r.heartbeatTimer&&r.heartbeatTimer.unref())}sendTerminalParseError(e,r){this.transport.sendAcpError(e,Errors.InvalidParams,{message:r.error.message})}destroy(){for(const e of this.interactiveTerminals.values())clearTimeout(e.heartbeatTimer);this.interactiveTerminals.clear()}}
@@ -0,0 +1 @@
1
+ export{TerminalManager}from"./session-manager.js";export{createTerminalWebSocketService}from"./websocket.js";export{TerminalBridgeHandler}from"./bridge-handler.js";
@@ -0,0 +1 @@
1
+ import{Errors}from"../errors/index.js";export const TERMINAL_METHODS={open:"terminal/open",input:"terminal/input",resize:"terminal/resize",close:"terminal/close",create:"terminal/create",wait:"terminal/wait",release:"terminal/release",kill:"terminal/kill"};export const TERMINAL_UPDATE_METHOD="terminal/update";export const TERMINAL_ERROR_CODES={shellDisabled:-32010,terminalNotFound:-32011,terminalClosed:-32012,terminalQuotaExceeded:-32013,terminalTimeout:-32014};export const TERMINAL_INVALID_PARAMS={code:-32602,message:"invalid params"};function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function readString(e,r){const t=e[r];if("string"==typeof t){const e=t.trim();return e.length>0?e:void 0}}function readPositiveInt(e,r){const t=e[r];if("number"==typeof t&&Number.isInteger(t)&&t>0)return t}function invalid(e,r){return{ok:!1,error:{code:Errors.InvalidParams.rpcCode,message:Errors.InvalidParams.message,data:{field:e,reason:r}}}}function parseBase(e){return isRecord(e)?{ok:!0,value:e}:invalid("params","must be an object")}export function parseTerminalOpenParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readPositiveInt(t,"cols")??80,a=readPositiveInt(t,"rows")??24,i=readString(t,"sessionId"),s=readString(t,"command"),o=readString(t,"cwd");return{ok:!0,value:{sessionId:i,command:s,cols:n,rows:a,...o?{cwd:o}:{}}}}export function parseTerminalInputParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(t,"data"),i=readString(t,"dataBase64");return a||i?{ok:!0,value:{terminalId:n,data:a,dataBase64:i}}:invalid("data","data or dataBase64 must be provided")}export function parseTerminalResizeParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(t,"cols");if(!a)return invalid("cols","must be a positive integer");const i=readPositiveInt(t,"rows");return i?{ok:!0,value:{terminalId:n,cols:a,rows:i}}:invalid("rows","must be a positive integer")}export function parseTerminalCloseParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(t,"signal");return{ok:!0,value:{terminalId:n,...a?{signal:a}:{}}}}export function parseTerminalWaitParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(t,"timeoutMs");return{ok:!0,value:{terminalId:n,...a?{timeoutMs:a}:{}}}}export function isTerminalRpcMethod(e){return Object.values(TERMINAL_METHODS).includes(e)}
@@ -0,0 +1 @@
1
+ import{createWriteStream}from"node:fs";import{mkdir,access,readdir,constants}from"node:fs/promises";import{join,resolve,sep}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";import{Errors,sendHttpError}from"../errors/index.js";const OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),UPLOAD_DIR=join(OPENCLAW_HOME,"workspace","uploads"),DOWNLOAD_TIMEOUT_MS=3e5;function classifyDownloadError(r,e){if(r instanceof Error&&"AbortError"===r.name)return{message:"download timed out",category:"timeout",retriable:!0};if(e)return 429===e||e>=500?{message:`HTTP ${e}`,category:"http_5xx",retriable:!0,httpStatus:e}:e>=400?{message:`HTTP ${e}`,category:"http_4xx",retriable:!1,httpStatus:e}:{message:`HTTP ${e}`,category:"invalid_response",retriable:!1,httpStatus:e};const t=r instanceof Error?r.message:String(r),o=r.code;return"ENOSPC"===o||"EACCES"===o||"EROFS"===o?{message:t,category:"fs_error",retriable:!1}:{message:t,category:"network_error",retriable:!0}}export async function handleFileStage(r,e,t){const o=r,a=e,s=getHeader(o,"x-beeos-token")||getHeader(o,"authorization")?.replace(/^Bearer\s+/i,""),n=process.env.BRIDGE_INTERNAL_SECRET;if(n&&s!==n)return void sendHttpError(a,401,Errors.Unauthorized);let i;try{i=await readJSONBody(o)}catch{return void sendHttpError(a,400,Errors.InvalidBody)}if(!i.fileId||!i.fileName||!i.presignedUrl)return void sendHttpError(a,400,Errors.MissingField,{message:"fileId, fileName, and presignedUrl are required"});const d=resolve(join(OPENCLAW_HOME,"workspace")),c=i.targetDir?resolve(join(d,i.targetDir)):UPLOAD_DIR;if(!c.startsWith(d+sep)&&c!==d)return void sendHttpError(a,400,Errors.InvalidBody,{message:"Invalid targetDir"});const l=i.fileName.replace(/[/\\:*?"<>|]/g,"_"),f=`${i.fileId}_${l}`,u=join(c,f);try{return await access(u,constants.F_OK),void sendJSON(a,200,{ok:!0,localPath:u,cached:!0})}catch{const r=await findByFileIdPrefix(c,i.fileId);if(r)return void sendJSON(a,200,{ok:!0,localPath:r,cached:!0})}try{await mkdir(c,{recursive:!0})}catch(r){return void sendHttpError(a,500,Errors.FsError,{message:`Failed to create target directory: ${r instanceof Error?r.message:String(r)}`})}const p=new AbortController,m=setTimeout(()=>p.abort(),3e5);"object"==typeof m&&"unref"in m&&m.unref();try{const r=await fetch(i.presignedUrl,{signal:p.signal});if(!r.ok||!r.body){const e=classifyDownloadError(null,r.status);return void sendHttpError(a,502,Errors.DownloadFailed,{message:`Download failed: ${e.message}`,extra:{errorCategory:e.category,retriable:e.retriable,httpStatus:e.httpStatus}})}const e=createWriteStream(u);await pipeline(Readable.fromWeb(r.body),e)}catch(r){const e=classifyDownloadError(r);return void sendHttpError(a,502,Errors.DownloadFailed,{message:`Download failed: ${e.message}`,extra:{errorCategory:e.category,retriable:e.retriable}})}finally{clearTimeout(m)}sendJSON(a,200,{ok:!0,localPath:u,cached:!1})}function getHeader(r,e){const t=r.headers[e];return Array.isArray(t)?t[0]:t}const MAX_BODY_SIZE=1048576;function readJSONBody(r){return new Promise((e,t)=>{const o=[];let a=0,s=!1;r.on("data",r=>{if(s)return;const e=Buffer.isBuffer(r)?r:Buffer.from(r);if(a+=e.length,a>MAX_BODY_SIZE)return s=!0,void t(new Error(`Request body exceeds ${MAX_BODY_SIZE} bytes`));o.push(e)}),r.on("end",()=>{if(!s)try{e(JSON.parse(Buffer.concat(o).toString("utf-8")))}catch(r){t(r)}}),r.on("error",t)})}function sendJSON(r,e,t){r.statusCode=e,r.setHeader("Content-Type","application/json"),r.end(JSON.stringify(t))}async function findByFileIdPrefix(r,e){try{const t=await readdir(r),o=`${e}_`;for(const e of t)if(e.startsWith(o)){const t=join(r,e);try{return await access(t,constants.R_OK),t}catch{continue}}}catch{}return null}
@@ -0,0 +1 @@
1
+ export{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./tool.js";export{writeUploadToolResult,cleanupUploadToolResultCache}from"./result-cache.js";export{handleFileStage}from"./file-stage.js";
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{mkdir,readFile,rename,stat,unlink,readdir}from"node:fs/promises";import{writeFile}from"node:fs/promises";import{homedir}from"node:os";import{join}from"node:path";const CACHE_TTL_MS=864e5,CACHE_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","upload_meta"),CACHE_VERSION=1;function safeToolCallId(e){return e.replace(/[^a-zA-Z0-9._-]/g,"_")}function cachePath(e){return join(CACHE_DIR,`${safeToolCallId(e)}.json`)}async function safeUnlink(e){try{await unlink(e)}catch{}}export async function writeUploadToolResult(e,t,a){await mkdir(CACHE_DIR,{recursive:!0,mode:448});const n={version:1,createdAt:Date.now(),toolCallId:e,toolName:t,result:a},o=cachePath(e),i=`${o}.${randomUUID()}.tmp`;await writeFile(i,JSON.stringify(n),{encoding:"utf8",mode:384}),await rename(i,o)}export async function readUploadToolResult(e){const t=cachePath(e);let a,n,o;try{a=await stat(t)}catch{return}if(Date.now()-a.mtimeMs>=864e5)await safeUnlink(t);else{try{n=await readFile(t,"utf8")}catch{return}try{o=JSON.parse(n)}catch{return void await safeUnlink(t)}if(o&&1===o.version&&"number"==typeof o.createdAt&&!(Date.now()-o.createdAt>=864e5))return o.result;await safeUnlink(t)}}export async function cleanupUploadToolResultCache(){let e;try{e=await readdir(CACHE_DIR,{withFileTypes:!0})}catch{return 0}const t=Date.now();let a=0;for(const n of e){if(!n.isFile())continue;if(!n.name.endsWith(".json")&&!n.name.includes(".tmp"))continue;const e=join(CACHE_DIR,n.name);try{t-(await stat(e)).mtimeMs>=864e5&&(await unlink(e),a++)}catch{}}return a}
1
+ import{randomUUID}from"node:crypto";import{mkdir,readFile,rename,stat,unlink,readdir}from"node:fs/promises";import{writeFile}from"node:fs/promises";import{homedir}from"node:os";import{join}from"node:path";const CACHE_TTL_MS=864e5,CACHE_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","upload_meta"),CACHE_VERSION=1;function safeToolCallId(e){return e.replace(/[^a-zA-Z0-9._-]/g,"_")}function cachePath(e){return join(CACHE_DIR,`${safeToolCallId(e)}.json`)}async function safeUnlink(e){try{await unlink(e)}catch{}}export async function writeUploadToolResult(e,t,a){await mkdir(CACHE_DIR,{recursive:!0,mode:448});const n={version:1,createdAt:Date.now(),toolCallId:e,toolName:t,result:a},o=cachePath(e),i=`${o}.${randomUUID()}.tmp`;await writeFile(i,JSON.stringify(n),{encoding:"utf8",mode:384}),await rename(i,o)}export async function readUploadToolResult(e){const t=cachePath(e);let a,n,o;try{a=await stat(t)}catch{return}if(Date.now()-a.mtimeMs>=864e5)await safeUnlink(t);else{try{n=await readFile(t,"utf8")}catch{return}try{o=JSON.parse(n)}catch{return void await safeUnlink(t)}if(o&&1===o.version&&"number"==typeof o.createdAt&&!(Date.now()-o.createdAt>=864e5))return o.result;await safeUnlink(t)}}export async function cleanupUploadToolResultCache(){let e;try{e=await readdir(CACHE_DIR,{withFileTypes:!0})}catch{return 0}const t=Date.now();let a=0;for(const n of e){if(!n.isFile())continue;if(!n.name.endsWith(".json")&&!n.name.endsWith(".tmp"))continue;const e=join(CACHE_DIR,n.name);try{t-(await stat(e)).mtimeMs>=864e5&&(await unlink(e),a++)}catch{}}return a}
@@ -0,0 +1 @@
1
+ import{constants}from"node:fs";import{access,readFile,stat}from"node:fs/promises";import{basename,extname}from"node:path";import{agentFetch}from"../agent-auth.js";export const BEEOS_UPLOAD_TOOL_NAME="beeos_upload_file";export const BEEOS_UPLOAD_TOOL_DESCRIPTION="Upload 1-5 local files from this machine to the user via the BeeOS platform. If the call succeeds, the files are already delivered to the user — do not use any other tool to send them again. Any file type is accepted: text, images, documents, PDFs, archives, binaries.";export const BEEOS_UPLOAD_TOOL_SCHEMA={title:"BeeOS file upload",type:"object",description:BEEOS_UPLOAD_TOOL_DESCRIPTION,additionalProperties:!1,required:["paths"],properties:{paths:{type:"array",minItems:1,maxItems:5,description:"Local filesystem paths (1-5). Absolute or workspace-relative.",items:{type:"string",minLength:1,description:"Single local filesystem path"}}}};const MAX_PATHS=5,REQUEST_TIMEOUT_MS=12e4,MAX_CONCURRENCY=3;async function runWithConcurrency(e,t){const r=new Array(e.length);let a=0;async function n(){for(;a<e.length;){const t=a++;r[t]=await e[t]()}}const s=[];for(let r=0;r<Math.min(t,e.length);r++)s.push(n());return await Promise.all(s),r}async function validateFiles(e){return Promise.all(e.map(async e=>{try{await access(e,constants.R_OK);return(await stat(e)).isFile()?{path:e,ok:!0}:{path:e,ok:!1,error:"not a regular file"}}catch(t){const r=t.code;let a;return a="ENOENT"===r?"file not found":"ENOTDIR"===r?"path component is not a directory":"EACCES"===r?"permission denied":t instanceof Error?t.message:String(t),{path:e,ok:!1,error:a}}}))}export async function executeUpload(e,t,r){const a=parseParams(e);if(!a.ok)return errorResult(a.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — file upload requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign upload requests");const n=`${t.agentGatewayUrl}/api/v1/agent/files`,s=a.paths,i=await validateFiles(s),o=i.filter(e=>!e.ok);if(o.length===s.length)return errorResult(`All files invalid: ${o.map(e=>`${e.path}: ${e.error}`).join("; ")}`);const l=i.filter(e=>e.ok).map(e=>e.path),c=o.map(e=>`${e.path}: ${e.error}`),p=l.map(e=>async()=>{try{return await uploadSingleFile(e,n,r)}catch(t){return c.push(`${e}: ${t instanceof Error?t.message:String(t)}`),null}}),u=(await runWithConcurrency(p,3)).filter(e=>null!==e);if(0===u.length)return errorResult(`All uploads failed: ${c.join("; ")}`);const m=u.map(e=>({uri:`beeos-file://${e.fileId}`,name:e.fileName,mimeType:e.mimeType,fileId:e.fileId})),f={ok:!0,files:m};return{...f,content:m.map(e=>({type:"resource_link",uri:e.uri,name:e.name,mimeType:e.mimeType})),output:f,result:f,details:f,isError:!1}}async function uploadSingleFile(e,t,r){await access(e,constants.R_OK);if(!(await stat(e)).isFile())throw new Error("not a regular file");const a=basename(e),n=inferMimeType(a),s=await readFile(e),i=new AbortController,o=setTimeout(()=>i.abort(),12e4);"object"==typeof o&&"unref"in o&&o.unref();try{const e=await agentFetch(t+"/presign",r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fileName:a,mimeType:n}),signal:i.signal});if(!e.ok){const t=await e.text().catch(()=>e.statusText);throw new Error(`presign ${e.status}: ${t}`)}const o=await e.json(),l=await fetch(o.uploadUrl,{method:"PUT",headers:{"Content-Type":n},body:new Blob([s],{type:n}),signal:i.signal});if(!l.ok)throw new Error(`S3 PUT failed: ${l.status}`);const c=await agentFetch(t+"/confirm",r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fileId:o.fileId}),signal:i.signal});if(c.ok){const e=await c.json();return{fileId:e.fileId,downloadUrl:e.downloadUrl,fileName:e.fileName||a,mimeType:e.mimeType||n,size:e.size||s.byteLength}}return{fileId:o.fileId,downloadUrl:o.uploadUrl.split("?")[0]??o.uploadUrl,fileName:a,mimeType:n,size:s.byteLength}}finally{clearTimeout(o)}}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e;if(!Array.isArray(t.paths))return{ok:!1,error:"paths must be an array"};if(t.paths.length<1)return{ok:!1,error:"paths must contain at least 1 item"};if(t.paths.length>5)return{ok:!1,error:"paths must contain no more than 5 items"};const r=[];for(let e=0;e<t.paths.length;e++){const a=t.paths[e];if("string"!=typeof a||""===a.trim())return{ok:!1,error:`paths[${e}] must be a non-empty string`};r.push(a.trim())}return{ok:!0,paths:r}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}function inferMimeType(e){switch(extname(e).toLowerCase()){case".txt":return"text/plain";case".md":return"text/markdown";case".json":return"application/json";case".html":return"text/html";case".css":return"text/css";case".js":case".mjs":case".cjs":return"text/javascript";case".ts":return"text/x.typescript";case".tsx":case".jsx":return"text/jsx";case".xml":return"application/xml";case".yaml":case".yml":return"application/yaml";case".pdf":return"application/pdf";case".png":return"image/png";case".jpg":case".jpeg":return"image/jpeg";case".gif":return"image/gif";case".webp":return"image/webp";case".svg":return"image/svg+xml";case".csv":return"text/csv";case".zip":return"application/zip";case".tar":return"application/x-tar";case".gz":case".tgz":return"application/gzip";default:return"application/octet-stream"}}
@@ -7,9 +7,13 @@
7
7
  "type": "object",
8
8
  "additionalProperties": true,
9
9
  "properties": {
10
+ "agentGatewayUrl": {
11
+ "type": "string",
12
+ "description": "Base URL of the Agent Gateway API. Also readable from AGENT_GATEWAY_URL env var."
13
+ },
10
14
  "platformUrl": {
11
15
  "type": "string",
12
- "description": "Base URL of the BeeOS platform API (e.g. https://api.beeos.com)"
16
+ "description": "[Deprecated: use agentGatewayUrl] Legacy alias for agentGatewayUrl."
13
17
  },
14
18
  "canvasRelayUrl": {
15
19
  "type": "string",
@@ -121,16 +125,12 @@
121
125
  "properties": {
122
126
  "enabled": {
123
127
  "type": "boolean",
124
- "description": "Enable message bus. Auto-detected from serviceUrl or platformUrl. Also readable from BEEOS_MESSAGE_BUS_ENABLED env var."
128
+ "description": "Enable message bus. Auto-detected from serviceUrl or agentGatewayUrl. Also readable from BEEOS_MESSAGE_BUS_ENABLED env var."
125
129
  },
126
130
  "serviceUrl": {
127
131
  "type": "string",
128
132
  "description": "Message Service REST URL (e.g. http://localhost:8089). Also readable from MESSAGE_SERVICE_URL env var."
129
133
  },
130
- "apiKey": {
131
- "type": "string",
132
- "description": "Message Service API Key for REST authentication. Also readable from MESSAGE_API_KEY env var."
133
- },
134
134
  "url": {
135
135
  "type": "string",
136
136
  "description": "Override: Centrifugo WebSocket endpoint. Normally obtained from Message Service token response. Also readable from CENTRIFUGO_URL env var."