beeos-claw 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present BeeOS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # beeos-claw
2
+
3
+ OpenClaw plugin that bridges the [BeeOS](https://beeos.ai) platform with the local OpenClaw Gateway. Provides ACP (Agent Communication Protocol) bridge, file transfer, terminal access, and observability.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install beeos-claw
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Add to your OpenClaw configuration (`~/.openclaw/openclaw.json`):
14
+
15
+ ```json
16
+ {
17
+ "plugins": {
18
+ "entries": {
19
+ "beeos-claw": {
20
+ "package": "beeos-claw",
21
+ "enabled": true,
22
+ "config": {
23
+ "platformUrl": "https://api.beeos.ai",
24
+ "bridge": {
25
+ "url": "wss://bridge.beeos.ai",
26
+ "keyFile": "~/.openclaw/bridge-key.json"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Connection Mode
36
+
37
+ The agent initiates an outbound WebSocket connection to the Bridge server (Phone-Home model). No inbound ports are required — the agent never listens as an ACP server.
38
+
39
+ Requires both `bridge.url` and `bridge.keyFile` to be configured.
40
+
41
+ ## Configuration
42
+
43
+ Configuration is resolved from three sources in priority order:
44
+
45
+ 1. **Plugin config** — `plugins.entries.beeos-claw.config` in `openclaw.json`
46
+ 2. **Environment variables** — prefixed with `BEEOS_` or `BRIDGE_`
47
+ 3. **Defaults**
48
+
49
+ ### Platform
50
+
51
+ | Config Key | Env Var | Default | Description |
52
+ |---|---|---|---|
53
+ | `platformUrl` | `BEEOS_PLATFORM_URL` | — | **(Required)** Base URL of the BeeOS platform API |
54
+
55
+ ### Bridge
56
+
57
+ | Config Key | Env Var | Default | Description |
58
+ |---|---|---|---|
59
+ | `bridge.url` | `BRIDGE_URL` | — | Bridge server WebSocket URL |
60
+ | `bridge.keyFile` | `BRIDGE_KEY_FILE` | — | Path to Ed25519 key file for Bridge authentication. Auto-generates if file doesn't exist. |
61
+ | `bridge.service` | `BRIDGE_SERVICE` | `acp` | Service type for Bridge registration. One of: `acp`, `screen`, `desktop`, `shell` |
62
+ | `bridge.mode` | — | — | Bridge operational mode |
63
+ | `bridge.shell.enabled` | `BEEOS_SHELL_ENABLED` | `true` | Enable web-SSH / terminal bridge support |
64
+ | `bridge.promptTimeoutMs` | `BEEOS_PROMPT_TIMEOUT_MS` | `300000` | Timeout for LLM prompt responses (ms) |
65
+ | `bridge.historyPendingTimeoutMs` | — | — | Timeout for history replay pending state (ms) |
66
+
67
+ ### Gateway (OpenClaw Gateway Connection)
68
+
69
+ | Config Key | Env Var | Default | Description |
70
+ |---|---|---|---|
71
+ | `gateway.url` | `BEEOS_GATEWAY_URL` | `ws://127.0.0.1:18789` | WebSocket URL of the local OpenClaw Gateway |
72
+ | `gateway.token` | `BEEOS_GATEWAY_TOKEN` | — | Gateway authentication token. Also read from `openclaw.json` at `gateway.auth.token`. |
73
+ | `gateway.protocol` | — | `3` | Gateway protocol version |
74
+ | `gateway.clientId` | — | `beeos-claw-{pid}` | Client ID for the Gateway connection |
75
+ | `gateway.agentId` | — | `main` | Agent ID to use |
76
+
77
+ ### Terminal
78
+
79
+ | Config Key | Env Var | Default | Description |
80
+ |---|---|---|---|
81
+ | `terminalWsPort` | `BEEOS_TERMINAL_WS_PORT` | `18801` | Port for the terminal WebSocket server |
82
+ | `terminalWsAllowedOrigins` | `BEEOS_TERMINAL_WS_ALLOWED_ORIGINS` | `localhost,127.0.0.1,::1` | Comma-separated allowed origins for terminal WS |
83
+
84
+ ### Reconnect
85
+
86
+ | Config Key | Env Var | Default | Description |
87
+ |---|---|---|---|
88
+ | `retry.baseMs` | — | `1000` | Base delay for exponential backoff reconnect (ms) |
89
+ | `retry.maxMs` | — | `600000` | Maximum reconnect delay (ms) |
90
+ | `retry.maxAttempts` | — | `0` | Maximum reconnect attempts. `0` = unlimited. |
91
+
92
+ ### Logging & Observability
93
+
94
+ | Config Key | Env Var | Default | Description |
95
+ |---|---|---|---|
96
+ | `log.enabled` | `BEEOS_LOG_ENABLED` | `true` | Enable structured JSONL observability logs |
97
+ | `log.verbose` | — | `false` | Enable verbose debug logging |
98
+ | `log.dir` | — | `~/.openclaw/beeos-logs` | Directory for rolling JSONL log files |
99
+
100
+ ### Advanced Timeouts
101
+
102
+ | Config Key | Env Var | Default | Description |
103
+ |---|---|---|---|
104
+ | `streamIdleTimeoutMs` | `BEEOS_STREAM_IDLE_TIMEOUT_MS` | `15000` | Idle timeout for streaming responses (ms) |
105
+ | `streamStaleTimeoutMs` | `BEEOS_STREAM_STALE_TIMEOUT_MS` | `600000` | Stale timeout for abandoned streams (ms) |
106
+ | `loadReplayIdleTimeoutMs` | `BEEOS_LOAD_REPLAY_IDLE_TIMEOUT_MS` | `15000` | Idle timeout for history replay loading (ms) |
107
+ | `cronFlushTimeoutMs` | `BEEOS_CRON_FLUSH_TIMEOUT_MS` | `120000` | Timeout for cron flush operations (ms) |
108
+ | `cronSignalWindowMs` | `BEEOS_CRON_SIGNAL_WINDOW_MS` | `60000` | Window for cron signal batching (ms) |
109
+
110
+ ## Full Configuration Example
111
+
112
+ ```json
113
+ {
114
+ "plugins": {
115
+ "entries": {
116
+ "beeos-claw": {
117
+ "package": "beeos-claw",
118
+ "enabled": true,
119
+ "config": {
120
+ "platformUrl": "https://api.beeos.ai",
121
+ "bridge": {
122
+ "url": "wss://bridge.beeos.ai",
123
+ "keyFile": "/home/agent/.openclaw/bridge-key.json",
124
+ "service": "acp",
125
+ "shell": {
126
+ "enabled": true
127
+ },
128
+ "promptTimeoutMs": 300000
129
+ },
130
+ "gateway": {
131
+ "url": "ws://127.0.0.1:18789",
132
+ "protocol": 3,
133
+ "agentId": "main"
134
+ },
135
+ "retry": {
136
+ "baseMs": 1000,
137
+ "maxMs": 600000,
138
+ "maxAttempts": 0
139
+ },
140
+ "log": {
141
+ "enabled": true,
142
+ "verbose": false,
143
+ "dir": "/home/agent/.openclaw/beeos-logs"
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## Features
153
+
154
+ ### ACP Bridge
155
+
156
+ Connects local OpenClaw agents to the BeeOS platform via the Agent Communication Protocol. Handles prompt routing, streaming responses, session history replay, and tool call forwarding.
157
+
158
+ ### File Transfer
159
+
160
+ Provides the `beeos_upload_file` tool for agents to upload files from the sandbox to BeeOS platform storage. Also supports file staging via the `POST /beeos/files/stage` HTTP endpoint for downloading files from the platform into the agent sandbox.
161
+
162
+ ### Terminal Bridge
163
+
164
+ When `bridge.shell.enabled` is `true`, the plugin starts a WebSocket server for web-SSH / terminal access. Remote users can open terminal sessions through the Bridge connection.
165
+
166
+ ### Observability
167
+
168
+ When `log.enabled` is `true`, the plugin writes structured JSONL logs covering:
169
+ - Plugin lifecycle events (startup, config resolution)
170
+ - ACP message flow (prompts, responses, tool calls)
171
+ - Bridge connection events (connect, disconnect, auth)
172
+ - Upload tool execution results
173
+ - Terminal session events
174
+
175
+ Logs are written to rolling files in `log.dir` with automatic rotation and sensitive data redaction (tokens, large base64 payloads).
176
+
177
+ ## Requirements
178
+
179
+ - Node.js >= 18
180
+ - OpenClaw runtime
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1 @@
1
+ import{sanitizePromptPayload}from"../message-filter.js";import{MessageQueue}from"../utils/mq.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory}from"./history-replay.js";import{appendHistoryEntry}from"./local-session-history.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const MAIN_SESSION_KEY="agent:main:main",DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const i=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return i&&!i.includes("/")&&SLASH_COMMAND_NAME_RE.test(i)?i.toLowerCase():void 0}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;initialized=!1;handleTerminalRequest=null;handleTerminalNotification=null;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendError(e.id,-32601,`Method not found: ${e.method}`)}}catch(t){this.transport.sendError(e.id,-32603,t instanceof Error?t.message:"Internal error")}}handleNotification(e){if("session/cancel"===e.method)this.handleSessionCancel(void 0,e.params);else e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=MAIN_SESSION_KEY,s="string"==typeof e.params?.cwd?e.params.cwd:".";let i=this.state.sessions.get(t);i?i.cwd=s:(i={sessionId:t,gatewaySessionKey:t,createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:s},this.state.sessions.set(t,i),this.state.sessionsByGatewayKey.set(t,i)),this.transport.sendResult(e.id,{sessionId:t,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:t}}),this.fetchHistoryInBackground(t)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendError(e.id,-32602,"sessionId required");let s=this.state.sessions.get(t);s||(s={sessionId:t,gatewaySessionKey:MAIN_SESSION_KEY,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(t,s),this.state.sessionsByGatewayKey.set(s.gatewaySessionKey,s));try{const e=this.getGateway(),i=await loadHistory(e,s.gatewaySessionKey,50);for(const e of i){const i=e.dedupKey??`${e.role}:${e.content.slice(0,64)}`;if(s.chatReplayDedupKeys.has(i))continue;s.chatReplayDedupKeys.add(i);const a=e.updateType??("user"===e.role?"user_message_chunk":"agent_message_chunk");"tool_call"===a&&e.toolCallId?this.transport.sendSessionUpdate(t,{sessionUpdate:"tool_call",toolCallId:e.toolCallId,title:e.toolName??"tool",kind:"other",status:"completed"}):this.transport.sendSessionUpdate(t,{sessionUpdate:a,content:{type:"text",text:e.content}})}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}s.assistantMq&&!s.assistantMq.closed&&await this.replayCurrentAssistantStream(s),this.transport.sendResult(e.id,{sessionId:t}),this.pushSkillsList().catch(()=>{})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const t=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,s=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let i=0;for(;Date.now()<s&&(e.assistantStreamOwnerRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const s=await Promise.race([e.assistantMq.read(i),new Promise(e=>setTimeout(()=>e(null),t))]);if(!s||0===s.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t),i++}i>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:i}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)t.push({sessionId:e.sessionId,title:e.title||e.sessionId,cwd:e.cwd,updatedAt:e.createdAt});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){t.find(t=>this.state.sessions.get(t.sessionId)?.gatewaySessionKey===e.sessionKey)||t.push({sessionId:e.sessionKey,title:e.title||e.sessionKey,cwd:e.cwd,updatedAt:e.updatedAt})}}}catch{}this.transport.sendResult(e.id,{sessions:t})}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,i=t?this.state.sessions.get(t):void 0,a=this.getGateway();if(i&&a?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await a.sessionsPatch(i.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,i=t?this.state.sessions.get(t):void 0,a=this.getGateway();if(i&&a?.isConnected&&s)try{await a.sessionsPatch(i.gatewaySessionKey,{model:s})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:s??"default"})}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendError(e.id,-32602,"sessionId and prompt required");let i=this.state.sessions.get(t);i||(i={sessionId:t,gatewaySessionKey:MAIN_SESSION_KEY,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(t,i),this.state.sessionsByGatewayKey.set(i.gatewaySessionKey,i));const a=this.getGateway();if(!a?.isConnected)return void this.transport.sendError(e.id,-32001,"Gateway not connected");let o=extractPromptBlocks(sanitizePromptPayload(s));const n=buildResolutionPlan(o);if(n.refs.length>0)try{const e=await resolveResources(n,this.writeObs);o=applyResolutions(o,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const r=toGatewayPrompt(o);if("error"in r)return void this.transport.sendError(e.id,-32602,r.error);const{message:d,attachments:l}=r,m=resolveStandaloneSlashCommandName(o,d)??resolveSlashCmd(o,d),c="string"==typeof m,p=c?"chat.send":"agent",h=c?d:withUserMessagePrefix(d);c&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${t} command=/${m}`),i.currentRunId&&this.state.promptsByRunId.delete(i.currentRunId),i.currentRunId=void 0,i.assistantStreamOwnerRunId=void 0,i.pendingPromptId=e.id,i.promptDone=!1,i.gatewayMethod=p,appendHistoryEntry(i.gatewaySessionKey,{ts:Date.now(),role:"user",content:d}),i.assistantMq?.close(),i.assistantMq=new MessageQueue,i.chatReplayDedupKeys.clear(),i.hasAgentAssistantStream=!1,i.hasAgentThinkingStream=!1,i.hasAgentToolStartStream=!1,i.hasAgentToolResultStream=!1,i.chatAssistantTextSoFar=void 0,i.agentAssistantTextSoFar=void 0,this.emitPromptAsUserSessionUpdates(i.sessionId,o),this.transport.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(i);try{const e=l.length>0?l.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let t;t=c?await a.chatSend(i.gatewaySessionKey,h,e):await a.agentSend(i.gatewaySessionKey,h,this.config.gateway.agentId||"main",e),i.currentRunId=t.runId,i.assistantStreamOwnerRunId=t.runId,i.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(t.runId,i)}catch(t){this.state.clearPromptTimeout(i),void 0!==i.pendingPromptId&&(i.pendingPromptId=void 0,i.assistantMq?.close(),this.transport.sendError(e.id,-32001,t instanceof Error?t.message:"failed to send prompt to gateway"))}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,i=t.mimeType??t.mime_type??e.mimeType??e.mime_type,a=t.text??e.text,o=t.data??e.data,n=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!a&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...i?{mimeType:i}:{},...a?{text:a}:{},...o?{data:o}:{},...n?{fileName:n}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendError(e,-32602,"sessionId is required"));const i=this.state.sessions.get(s);if(!i)return void(void 0!==e&&this.transport.sendError(e,-32602,"unknown sessionId"));const a=this.getGateway();a?.isConnected&&i.currentRunId&&a.chatAbort(i.gatewaySessionKey,i.currentRunId).catch(()=>{}),this.completePrompt(i,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,-32022,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.gatewayRequestId=void 0,e.assistantMq?.close()}completePrompt(e,t){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendResult(e.pendingPromptId,{stopReason:t}),this.cleanupPrompt(e))}failPrompt(e,t,s){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendError(e.pendingPromptId,t,s),this.cleanupPrompt(e))}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,-32001,"Gateway disconnected")}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,-32021,s)}}pushToolResultContent(e,t,s){if(0===s.length)return;const i=this.state.findAnyActiveSession();if(!i)return;const a=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(a,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:a}})}isCronCorrelatedRun(e){return this.state.cronRunIds.has(e)}}
@@ -0,0 +1 @@
1
+ import{stripTransportMetadata}from"../message-filter.js";import{appendHistoryEntry}from"./local-session-history.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS,DEFAULT_CRON_FLUSH_TIMEOUT_MS,DEFAULT_CRON_SIGNAL_WINDOW_MS}from"./types.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"!=typeof t)return;return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")||void 0}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendToolCallSpacer(t){const e={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"}};this.transport.sendSessionUpdate(t.sessionId,e),t.assistantMq?.push(e)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const t=this.state.toolCallIdMap.get(n);return t||(this.state.toolCallIdMap.set(n,n),n)}return`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return void(this.state.isCronCandidate(t.runId,e)&&this.handleUncorrelatedCronChatEvent(t))}const s=e.sessionId,n=t.message;if(n){this.touchStreamActivity(e);if("assistant"===n.role)for(const t of n.content){const n=t;if(!isRecord(n))continue;const o=asString(n.type);if(o){if("thinking"===o){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=`chat:thinking:${t}`;if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t}};this.transport.sendSessionUpdate(s,a)&&e.assistantMq?.push(a);continue}if("toolCall"===o){if(this.sendToolCallSpacer(e),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=isRecord(n.arguments)?n.arguments:isRecord(n.args)?n.args:{},i=`chat:tool_start:${t}:${JSON.stringify(a)}`;if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const r={sessionUpdate:"tool_call",toolCallId:t,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]};this.transport.sendSessionUpdate(s,r)&&e.assistantMq?.push(r);continue}if("toolResult"===o){if(this.sendToolCallSpacer(e),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(n.result)||Array.isArray(n.result)?JSON.stringify(n.result,null,2):void 0);if(!a)continue;const i=`chat:tool_result:${t}:${a}`;if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const r={sessionUpdate:"tool_call_update",toolCallId:t,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]};this.transport.sendSessionUpdate(s,r)&&e.assistantMq?.push(r);continue}if("text"===o&&n.text){if(e.hasAgentAssistantStream)continue;const t=stripTransportMetadata(n.text);if(!t)continue;const o=e.chatAssistantTextSoFar??"";let a;if(e.chatAssistantTextSoFar=t,t.startsWith(o))a=t.slice(o.length);else{if(o.startsWith(t))continue;a=t}if(!a)continue;const i={sessionUpdate:"agent_message_chunk",content:{type:"text",text:a}};this.transport.sendSessionUpdate(s,i),e.assistantMq?.push(i)}}}}else"delta"===t.state&&this.touchStreamActivity(e);this.maybeCompletePromptFromChatState(e,t),"final"===t.state?this.completePrompt(e,t.stopReason||"end_turn"):"aborted"===t.state||"cancelled"===t.state||"cancel"===t.state?this.completePrompt(e,"cancelled"):"error"===t.state&&this.failPrompt(e,-32021,t.errorMessage||"Agent error")}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0)??this.state.findAnyActiveSession();if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return this.state.isCronCandidate(t.runId,e)?void this.handleUncorrelatedCronAgentEvent(t):void 0}const s=e.sessionId;this.touchStreamActivity(e);const n=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});o&&n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}});const a=[],i=t.data;i&&(Array.isArray(i.content)?a.push(...i.content):isRecord(i.content)?a.push(i.content):"string"==typeof i.type&&a.push(i));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=isRecord(t.arguments)?t.arguments:isRecord(t.args)?t.args:{};n({sessionUpdate:"tool_call",toolCallId:s,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]});continue}if("toolResult"===o){this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=normalizeText(t.text)??normalizeText(t.result)??(isRecord(t.result)||Array.isArray(t.result)?JSON.stringify(t.result,null,2):void 0);a&&n({sessionUpdate:"tool_call_update",toolCallId:s,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]});continue}}appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:o??""})}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=t.data?.delta||t.data?.text;if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:n,entryType:"thought"})}}else if("tool"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("start"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),o=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",a=isRecord(s?.arguments)?s.arguments:isRecord(s?.args)?s.args:{};n({sessionUpdate:"tool_call",toolCallId:t,title:o,kind:"other",status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]})}else if("result"===o)this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const o=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool";n({sessionUpdate:"tool_call",toolCallId:this.resolveToolCallId(e,{...s,...t},"start"),title:o,kind:"other",status:"in_progress"})}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){this.flushCronBufferForRun(t.runId);const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"end_turn")}else if("cancelled"===n||"cancel"===n){const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"cancelled")}else if("error"===n){const t=s?.error,n=("string"==typeof s?.message?s.message:void 0)??("string"==typeof t?.message?t.message:void 0)??"gateway lifecycle error";this.failPrompt(e,-32021,n)}}}async handleToolResult(t,e){const s=e.data,n=s?.toolCall,o=("string"==typeof n?.id?n.id:void 0)??("string"==typeof s?.toolCallId?s.toolCallId:void 0),a=(o?this.state.toolCallIdMap.get(o):void 0)??o??`tc_${e.runId}_${e.seq}`,i=("string"==typeof n?.name?n.name:void 0)??("string"==typeof s?.name?s.name:void 0)??"tool",r="string"==typeof s?.text?s.text:void 0,l=o??a;let c;try{c=await resolveToolResultBlocks({toolCallId:l,toolName:i,rawText:r,rawResult:e.data})}catch{c=r?[{type:"content",content:{type:"text",text:r}}]:[]}0===c.length&&r&&(c=[{type:"content",content:{type:"text",text:r}}]);for(const e of c)if(e.content){const s={sessionUpdate:"agent_message_chunk",content:e.content};this.transport.sendSessionUpdate(t.sessionId,s),t.assistantMq?.push(s)}if(c.length>0){const e={sessionUpdate:"tool_call_update",toolCallId:a,title:i,kind:"other",status:"completed"};this.transport.sendSessionUpdate(t.sessionId,e)&&t.assistantMq?.push(e),appendHistoryEntry(t.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:r??"",entryType:"tool_result",toolCallId:a,toolName:i})}}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t,s="string"==typeof e?.runId?e.runId:void 0;if(this.state.lastCronSignalAtMs=Date.now(),s){this.state.cronRunIds.add(s);const t=this.state.cronRunIdTimers.get(s);t&&clearTimeout(t);const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS,n=setTimeout(()=>{this.state.cronRunIds.delete(s),this.state.cronRunIdTimers.delete(s)},e);"object"==typeof n&&"unref"in n&&n.unref(),this.state.cronRunIdTimers.set(s,n)}this.pushCronWebhook(e,s);const n=this.state.findAnyActiveSession();if(!n)return;const o=("string"==typeof e?.summary?e.summary:"")||("string"==typeof e?.text?e.text:"")||JSON.stringify(t),a=s?`run:${s}`:`signal:${Date.now()}`;this.bufferCronText(a,`[Cron] ${o}\n`,n.sessionId)}pushCronWebhook(t,e){if(!this.config.platformUrl||!this.agentFetchFn)return;const s=`${this.config.platformUrl}/api/v1/agent/webhooks/cron`,n=("string"==typeof t?.summary?t.summary:void 0)??("string"==typeof t?.text?t.text:void 0)??"",o={jobId:e??"",name:("string"==typeof t?.name?t.name:void 0)??("string"==typeof t?.jobName?t.jobName:void 0)??"",content:n,summary:n,type:"cron",action:"string"==typeof t?.action?t.action:"fired",status:"string"==typeof t?.status?t.status:""};this.agentFetchFn(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).catch(t=>{this.logger.warn?.(`[gateway-events] cron webhook push failed: ${t instanceof Error?t.message:t}`)})}bufferCronText(t,e,s){const n=this.state.cronTextByKey.get(t)??"";this.state.cronTextByKey.set(t,n+e),this.scheduleCronFlush(t,s)}scheduleCronFlush(t,e){const s=this.state.cronFlushTimersByKey.get(t);s&&clearTimeout(s);const n=this.config.cronFlushTimeoutMs??DEFAULT_CRON_FLUSH_TIMEOUT_MS,o=setTimeout(()=>{this.state.cronFlushTimersByKey.delete(t),this.flushCronBufferByKey(t,e,"timeout")},n);"object"==typeof o&&"unref"in o&&o.unref(),this.state.cronFlushTimersByKey.set(t,o)}flushCronBufferByKey(t,e,s){const n=this.state.cronFlushTimersByKey.get(t);n&&(clearTimeout(n),this.state.cronFlushTimersByKey.delete(t));const o=(this.state.cronTextByKey.get(t)??"").trim();if(this.state.cronTextByKey.delete(t),!o)return void("timeout"===s&&this.clearCronStreamTracking(t));const a=this.state.getCronRequestId(t),i={sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}};this.transport.sendCronSessionUpdate(e,i,{requestId:a,messageType:"cron",timestamp:Date.now()});const r=this.state.sessions.get(e);r?.assistantMq?.push(i),"timeout"===s&&this.clearCronStreamTracking(t)}flushCronBufferForRun(t){if(!t)return;const e=`run:${t}`;if(!this.state.cronTextByKey.has(e))return;const s=this.state.findAnyActiveSession();s&&this.flushCronBufferByKey(e,s.sessionId,"end")}clearCronStreamTracking(t){this.state.cronRequestIdsByKey.delete(t),this.state.cronTextByKey.delete(t);const e=this.state.cronFlushTimersByKey.get(t);if(e&&(clearTimeout(e),this.state.cronFlushTimersByKey.delete(t)),t.startsWith("run:")){const e=t.slice(4);if(e){this.state.cronRunIds.delete(e);const t=this.state.cronRunIdTimers.get(e);t&&(clearTimeout(t),this.state.cronRunIdTimers.delete(e))}}}handleUncorrelatedCronAgentEvent(t){if("lifecycle"===t.stream){const e=t.data,s="string"==typeof e?.phase?e.phase:void 0;if("end"===s){this.flushCronBufferForRun(t.runId);const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}else if("cancelled"===s||"cancel"===s||"error"===s){const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}return}if("assistant"!==t.stream)return;const e=this.state.findAnyActiveSession();if(!e)return;const s=("string"==typeof t.data?.delta?t.data.delta:void 0)??("string"==typeof t.data?.text?t.data.text:void 0);if(!s)return;const n=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`;this.bufferCronText(n,s,e.sessionId)}handleUncorrelatedCronChatEvent(t){if("user"!==t.message?.role)return;const e=this.state.findAnyActiveSession();if(e&&t.message?.content)for(const s of t.message.content)if("text"===s.type&&s.text){const n=stripTransportMetadata(s.text);if(!n)continue;const o=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`,a=this.state.getCronRequestId(o),i={sessionUpdate:"user_message_chunk",content:{type:"text",text:n}};this.transport.sendCronSessionUpdate(e.sessionId,i,{requestId:a,messageType:"cron",timestamp:Date.now()})}}}
@@ -0,0 +1 @@
1
+ import{readLocalHistory}from"./local-session-history.js";export async function loadHistory(t,e,o=50){if(t?.isConnected)try{const r=parseGatewayHistory(await t.chatHistory(e,o));if(r.length>0)return r}catch{}const r=readLocalHistory(e,o);return r.length>0?r.map(t=>{let e;switch(t.entryType){case"thought":e="agent_thought_chunk";break;case"tool_call":e="tool_call";break;case"tool_result":e="tool_call_update";break;default:e="user"===t.role?"user_message_chunk":"agent_message_chunk"}return{role:t.role,content:t.content,dedupKey:t.dedupKey,updateType:e,toolCallId:t.toolCallId,toolName:t.toolName}}):[]}function parseGatewayHistory(t){if(!t||"object"!=typeof t)return[];const e=t.messages;if(!Array.isArray(e))return[];const o=[];for(const t of e){if(!t||"object"!=typeof t)continue;const e=t,r="string"!=typeof e.role||"user"!==e.role&&"assistant"!==e.role?"assistant":e.role,s="string"==typeof e.content?e.content:"";s&&o.push({role:r,content:s})}return o}
@@ -0,0 +1 @@
1
+ import{mkdirSync,existsSync,appendFileSync,openSync,readSync,closeSync,statSync}from"node:fs";import{join}from"node:path";import{homedir}from"node:os";const STATE_DIR=process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),HISTORY_DIR=join(STATE_DIR,"beeos-claw","session_history"),TAIL_CHUNK_BYTES=65536,TAIL_MAX_BYTES=1048576,TAIL_MAX_LINES=6400;function sessionFilePath(n){const e=n.replace(/[^a-zA-Z0-9._-]/g,"_");return join(HISTORY_DIR,`${e}.jsonl`)}function ensureDir(){existsSync(HISTORY_DIR)||mkdirSync(HISTORY_DIR,{recursive:!0})}export function appendHistoryEntry(n,e,t){try{ensureDir();const t=JSON.stringify(e)+"\n";appendFileSync(sessionFilePath(n),t,"utf8")}catch(e){t?.debug?.(`[local-history] write failed sessionKey=${n}: ${e instanceof Error?e.message:e}`)}}export function readLocalHistory(n,e=50){const t=sessionFilePath(n);if(!existsSync(t))return[];try{const n=readTailLines(t),r=[];for(const t of n){try{r.push(JSON.parse(t))}catch{}if(r.length>=e)break}return r.reverse()}catch{return[]}}function readTailLines(n){let e,t;try{e=statSync(n).size}catch{return[]}if(e<=0)return[];try{t=openSync(n,"r");let r=e,s=1048576,o="";const i=[];for(;r>0&&s>0&&i.length<6400;){const n=Math.min(65536,r,s),e=r-n,c=Buffer.allocUnsafe(n),a=readSync(t,c,0,n,e);if(a<=0)break;r=e,s-=a;const l=(c.toString("utf-8",0,a)+o).split("\n");o=l.shift()??"";for(let n=l.length-1;n>=0;n--){const e=l[n]?.trim();if(e&&(i.push(e),i.length>=6400))break}}const c=o.trim();return c&&i.length<6400&&i.push(c),i}catch{return[]}finally{if(void 0!==t)try{closeSync(t)}catch{}}}
@@ -0,0 +1 @@
1
+ import{basename}from"node:path";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const SLASH_COMMAND_RE=/^\/([a-z0-9_-]+)(?::|\s|$)/i;export function resolveSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const r=t.trim();if(!r||r.includes("\n")||!r.startsWith("/"))return;const i=r.match(SLASH_COMMAND_RE);return i?.[1]?.toLowerCase()}export function extractPromptBlocks(e){const t=[];if("string"==typeof e)t.push({type:"text",text:e});else if(Array.isArray(e))for(const r of e){const e=asBlock(r);e&&t.push(e)}else if(isRecord(e))if(Array.isArray(e.content))for(const r of e.content){const e=asBlock(r);e&&t.push(e)}else if("string"==typeof e.type){const r=asBlock(e);r&&t.push(r)}return t}export function toGatewayPrompt(e){const t=[],r=[];for(const i of e)switch(i.type){case"text":{const e=i.text?.trim();e&&t.push(e);break}case"image":{const e=i.mimeType??i.mime_type??"application/octet-stream",a=i.fileName??i.file_name??i.name??(i.uri?resolveUriFileName(i.uri):void 0);if(!i.data&&!i.uri)return{error:"image block requires data or uri"};const s=[`mime=${e}`];i.data&&(s.push(`size=${i.data.length}`),r.push({type:"image",content:i.data,mimeType:e,fileName:a})),i.uri&&s.push(`uri=${i.uri}`),t.push(`[image ${s.join(" ")}]`);break}case"file":{const e=i.mimeType??i.mime_type??"application/octet-stream",a=i.fileName??i.file_name??i.filename??i.name??(i.uri?resolveUriFileName(i.uri):void 0),s=i.text?.trim(),n=s?Buffer.from(s,"utf-8").toString("base64"):void 0;if(!i.data&&!n&&!i.uri)return{error:"file block requires data, text, or uri"};const o=[`mime=${e}`];a&&o.push(`name=${a}`),i.data?(o.push(`size=${i.data.length}`),r.push({type:"file",content:i.data,mimeType:e,fileName:a})):n&&(o.push(`text=${truncate(s,80)}`),r.push({type:"file",content:n,mimeType:e,fileName:a})),i.uri&&o.push(`uri=${i.uri}`),t.push(`[file ${o.join(" ")}]`);break}case"resource_link":{if(!i.uri)return{error:"resource_link block requires uri"};const e=i.title??i.name??"resource";t.push(`[resource_link title=${e} uri=${i.uri}]`);break}case"resource":{const e=i.resource??{},a=asStr(e.uri)??i.uri;if(!a)return{error:"resource block requires uri"};const s=asStr(e.mimeType)??asStr(e.mime_type)??i.mimeType??i.mime_type,n=asStr(e.text)??i.text,o=asStr(e.data)??i.data,m=asStr(e.fileName)??asStr(e.file_name)??asStr(e.name)??i.fileName??i.file_name??i.name??resolveUriFileName(a);o?r.push({type:"file",content:o,mimeType:s,fileName:m}):n&&r.push({type:"file",content:Buffer.from(n,"utf-8").toString("base64"),mimeType:s??"text/plain",fileName:m});const u=n?`text=${truncate(n,120)}`:o?`size=${o.length}`:"";t.push(`[resource uri=${a}${s?` mime=${s}`:""} ${u}]`.trim());break}}return 0===t.length?{message:"(empty prompt)",attachments:[]}:{message:t.join("\n"),attachments:r}}function asBlock(e){return isRecord(e)&&"string"==typeof e.type?e:null}function truncate(e,t){return e.length<=t?e:`${e.slice(0,t)}...`}export function resolveUriFileName(e){try{const t=new URL(e),r=basename(t.pathname);if(r&&"."!==r&&"/"!==r&&"\\"!==r)return decodeURIComponent(r)}catch{}const t=basename(e);if(t&&"."!==t&&"/"!==t&&"\\"!==t)return t}
@@ -0,0 +1 @@
1
+ import{createWriteStream}from"node:fs";import{mkdir,access,readdir,constants}from"node:fs/promises";import{join,basename}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";import{createHash}from"node:crypto";const OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),RESOURCE_CACHE_DIR=join(OPENCLAW_HOME,"workspace","resource_cache"),DOWNLOAD_TIMEOUT_MS=12e4,MAX_CONCURRENT_DOWNLOADS=3;export function buildResolutionPlan(e){const r=[],t=new Set;for(let o=0;o<e.length;o++){const n=e[o];if("resource_link"!==n.type||!n.uri)continue;const a=n.uri;if(!a.startsWith("http://")&&!a.startsWith("https://"))continue;const i=deriveFileId(a);r.push({blockIndex:o,uri:a,name:n.title??n.name,fileId:i}),t.add(i)}return{refs:r,fileIds:t}}export async function resolveResources(e,r){const t=new Map;if(0===e.refs.length)return t;await mkdir(RESOURCE_CACHE_DIR,{recursive:!0}).catch(()=>{});const o=e.refs.map(e=>async()=>{const o=await resolveOne(e,r);t.set(e.uri,o)});return await runWithConcurrency(o,3),t}export function applyResolutions(e,r){return e.map(e=>{if("resource_link"!==e.type||!e.uri)return e;const t=r.get(e.uri);return t?.localPath?{...e,_resolvedLocalPath:t.localPath}:e})}function deriveFileId(e){return createHash("sha256").update(e).digest("hex").slice(0,16)}async function resolveOne(e,r){const{uri:t,fileId:o,name:n}=e,a=await findCachedFile(o);if(a)return r?.({component:"resource-resolver",domain:"file",name:"cache_hit",severity:"debug",payload:{uri:t,fileId:o,localPath:a}}),{uri:t,fileId:o,localPath:a,cached:!0};const i=(n||extractFileName(t)||`resource_${o}`).replace(/[/\\:*?"<>|]/g,"_"),c=join(RESOURCE_CACHE_DIR,`${o}_${i}`),s=new AbortController,l=setTimeout(()=>s.abort(),12e4);"object"==typeof l&&"unref"in l&&l.unref();try{const e=await fetch(t,{signal:s.signal});if(!e.ok||!e.body){const n=classifyHttpError(e.status);return r?.({component:"resource-resolver",domain:"file",name:"download_failed",severity:"warn",payload:{uri:t,fileId:o,status:e.status,category:n}}),{uri:t,fileId:o,localPath:null,cached:!1,error:`HTTP ${e.status}`,errorCategory:n,retriable:"http_5xx"===n}}const n=createWriteStream(c);return await pipeline(Readable.fromWeb(e.body),n),r?.({component:"resource-resolver",domain:"file",name:"download_ok",severity:"debug",payload:{uri:t,fileId:o,localPath:c}}),{uri:t,fileId:o,localPath:c,cached:!1}}catch(e){const n=e instanceof Error&&"AbortError"===e.name?"timeout":"network_error",a=e instanceof Error?e.message:String(e);return r?.({component:"resource-resolver",domain:"file",name:"download_failed",severity:"warn",payload:{uri:t,fileId:o,error:a,category:n}}),{uri:t,fileId:o,localPath:null,cached:!1,error:a,errorCategory:n,retriable:!0}}finally{clearTimeout(l)}}async function findCachedFile(e){try{const r=await readdir(RESOURCE_CACHE_DIR),t=`${e}_`;for(const e of r)if(e.startsWith(t)){const r=join(RESOURCE_CACHE_DIR,e);try{return await access(r,constants.R_OK),r}catch{continue}}}catch{}return null}function extractFileName(e){try{const r=new URL(e),t=basename(r.pathname);if(t&&"."!==t&&"/"!==t)return decodeURIComponent(t)}catch{}}function classifyHttpError(e){return e>=400&&e<500?"http_4xx":e>=500?"http_5xx":"invalid_response"}async function runWithConcurrency(e,r){let t=0;async function o(){for(;t<e.length;){const r=t++;await e[r]()}}const n=[];for(let t=0;t<Math.min(r,e.length);t++)n.push(o());await Promise.all(n)}
@@ -0,0 +1 @@
1
+ import{randomUUID}from"node:crypto";export class AcpGatewaySessionState{sessions=new Map;promptsByRunId=new Map;sessionsByGatewayKey=new Map;toolCallIdMap=new Map;nextToolCallSeq=0;cronRunIds=new Set;cronRunIdTimers=new Map;lastCronSignalAtMs=0;cronTextByKey=new Map;cronFlushTimersByKey=new Map;cronRequestIdsByKey=new Map;sessionGcTimer=null;MAX_IDLE_SESSION_MS=864e5;SESSION_GC_INTERVAL_MS=3e5;logger;constructor(e){this.logger=e}findSessionByKey(e){return this.sessionsByGatewayKey.get(e)}findAnyActiveSession(){for(const e of this.sessions.values())if(void 0!==e.pendingPromptId)return e;return this.sessions.values().next().value}isCurrentStreamOwner(e,s){return!s||!!e.assistantStreamOwnerRunId&&s===e.assistantStreamOwnerRunId}clearPromptTimeout(e){e.promptTimeoutTimer&&(clearTimeout(e.promptTimeoutTimer),e.promptTimeoutTimer=void 0)}clearStreamIdleTimer(e){e.streamIdleTimer&&(clearTimeout(e.streamIdleTimer),e.streamIdleTimer=void 0)}extractAgentAssistantDelta(e,s){if(s.delta)return s.delta;const t=s.text;if(!t)return;const i=e.agentAssistantTextSoFar??"";if(e.agentAssistantTextSoFar=t,t.startsWith(i)){return t.slice(i.length)||void 0}return i.startsWith(t)?void 0:t}isCronCandidate(e,s){return!(!e||!this.cronRunIds.has(e))||Date.now()-this.lastCronSignalAtMs<=s&&(e&&this.cronRunIds.add(e),!0)}resolveCronStreamKey(e){if(e.runId)return`run:${e.runId}`}getCronRequestId(e){let s=this.cronRequestIdsByKey.get(e);return s||(s=randomUUID(),this.cronRequestIdsByKey.set(e,s)),s}startSessionGc(e){this.sessionGcTimer=setInterval(()=>this.gcSessions(e),this.SESSION_GC_INTERVAL_MS),"object"==typeof this.sessionGcTimer&&"unref"in this.sessionGcTimer&&this.sessionGcTimer.unref()}gcSessions(e){const s=Date.now()-this.MAX_IDLE_SESSION_MS;let t=0;for(const[i,r]of this.sessions){if(void 0!==r.pendingPromptId)continue;(r.lastStreamActivityTs??r.createdAt)<s&&(e(r),this.sessions.delete(i),this.sessionsByGatewayKey.delete(r.gatewaySessionKey),t++)}const i=new Set(this.promptsByRunId.keys());for(const[e,s]of this.toolCallIdMap){const t=s.split("_")[1];t&&!i.has(t)&&this.toolCallIdMap.delete(e)}t>0&&this.logger.debug?.(`[session-state] GC evicted ${t} idle sessions, remaining=${this.sessions.size}`)}destroy(){this.sessionGcTimer&&(clearInterval(this.sessionGcTimer),this.sessionGcTimer=null);for(const e of this.sessions.values())this.clearPromptTimeout(e),this.clearStreamIdleTimer(e),e.assistantMq?.close();for(const e of this.cronFlushTimersByKey.values())clearTimeout(e);this.cronFlushTimersByKey.clear(),this.cronTextByKey.clear(),this.cronRequestIdsByKey.clear();for(const e of this.cronRunIdTimers.values())clearTimeout(e);this.cronRunIdTimers.clear(),this.cronRunIds.clear(),this.promptsByRunId.clear(),this.toolCallIdMap.clear(),this.sessionsByGatewayKey.clear(),this.sessions.clear()}}
@@ -0,0 +1 @@
1
+ import{readUploadToolResult}from"../upload-result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload-tool.js";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const strategies=[{name:"beeos_upload_file_cache",canHandle:t=>t.toolName===BEEOS_UPLOAD_TOOL_NAME,resolve:strategyUploadCache},{name:"passthrough_gateway_result",canHandle:()=>!0,resolve:async t=>strategyPassthrough(t)}];export function registerToolResultStrategy(t,e){if(e?.prepend)strategies.unshift(t);else{const e=Math.max(0,strategies.length-1);strategies.splice(e,0,t)}}export function getToolResultStrategies(){return strategies}export async function resolveToolResultBlocks(t){for(const e of strategies)if(e.canHandle(t))try{const n=await e.resolve(t);if(n&&n.length>0)return n}catch{}return[]}async function strategyUploadCache(t){if(t.toolName!==BEEOS_UPLOAD_TOOL_NAME)return null;let e;try{e=await readUploadToolResult(t.toolCallId)}catch{return null}if(!e)return null;const n=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,n??e)}function strategyPassthrough(t){if(t.rawResult&&isRecord(t.rawResult)){const e=buildToolResultSessionContentBlocks(t.toolName,t.rawResult);if(e.length>0)return e}const e=t.rawText?.trim();if(!e)return[];try{const n=JSON.parse(e);if(isRecord(n)){const e=buildToolResultSessionContentBlocks(t.toolName,n);if(e.length>0)return e}}catch{}return[{type:"content",content:{type:"text",text:e}}]}function isUploadPayload(t){return!!isRecord(t)&&(!0===t.ok||!1===t.ok||"files"in t||Array.isArray(t.content)&&t.content.length>0)}function pickUploadPayload(t){if(isRecord(t)){for(const e of["output","result","details"]){const n=t[e];if(isUploadPayload(n))return n;if(isRecord(n))for(const t of["output","result","details"])if(isUploadPayload(n[t]))return n[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const n=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&n.push({type:"content",content:{type:"text",text:e.trim()}}),n;const o=e.content??e.result;if(Array.isArray(o)){for(const t of o){const e=toContentBlock(t);e&&n.push(e)}if(n.length>0)return n}if(Array.isArray(e.files)){for(const t of e.files){if(!isRecord(t))continue;const e=asStr(t.fileId)??asStr(t.file_id),o=e?`beeos-file://${e}`:asStr(t.uri)??asStr(t.downloadUrl)??asStr(t.url);if(!o)continue;const r=asStr(t.name)??asStr(t.fileName)??asStr(t.file_name),s=asStr(t.mimeType)??asStr(t.mime_type);n.push({type:"content",content:{type:"resource_link",uri:o,...r?{name:r}:{},...s?{mimeType:s}:{}}})}if(n.length>0)return n}const r=asStr(e.output)??asStr(e.text)??asStr(e.result);return r&&n.push({type:"content",content:{type:"text",text:r}}),n}function toContentBlock(t){if(!isRecord(t))return"string"==typeof t&&t.trim()?{type:"content",content:{type:"text",text:t.trim()}}:null;const e=asStr(t.type);if(!e)return null;if("text"===e){const e=asStr(t.text);return e?{type:"content",content:{type:"text",text:e}}:null}if("resource_link"===e){const e=asStr(t.uri);return e?{type:"content",content:{type:"resource_link",uri:e,...t.name?{name:String(t.name)}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{},...t.title?{title:String(t.title)}:{}}}:null}if("image"===e){const e=asStr(t.data),n=asStr(t.uri);return e||n?{type:"content",content:{type:"image",...e?{data:e}:{},...n?{uri:n}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
@@ -0,0 +1 @@
1
+ export class AcpTransport{debugIndex=0;sendBridgeMessage;obs;forward;constructor(e,t,r){this.sendBridgeMessage=e,this.obs=t??(()=>{}),this.forward=r??{forwardThinking:!0,forwardToolCalls:!0}}get forwardControl(){return this.forward}nextMeta(){return{_debug_index:this.debugIndex++,_ts:Date.now()}}sendResult(e,t){const r=this.nextMeta();this.detectProtocolError(e,t,r._debug_index);const s={jsonrpc:"2.0",id:e,result:this.attachMeta(t,r),_debug_index:r._debug_index,_ts:r._ts};this.sendBridgeMessage(s),this.obs({component:"acp-transport",domain:"reply",name:"rpc.result",severity:"debug",payload:{id:e,_debug_index:r._debug_index}})}sendError(e,t,r,s){const o=this.nextMeta(),n={code:t,message:r};n.data=void 0!==s?this.attachMeta(s,o):{_meta:o};const d={jsonrpc:"2.0",id:e,error:n,_debug_index:o._debug_index,_ts:o._ts};this.sendBridgeMessage(d),this.obs({component:"acp-transport",domain:"reply",name:"rpc.error",severity:"warn",payload:{id:e,code:t,message:r,_debug_index:o._debug_index}})}sendNotification(e,t){const r=this.nextMeta(),s={jsonrpc:"2.0",method:e,params:t,_debug_index:r._debug_index,_ts:r._ts};this.sendBridgeMessage(s)}sendSessionUpdate(e,t){const r=t,s=r?.sessionUpdate;return!("agent_thought_chunk"===s&&!this.forward.forwardThinking)&&(!!("tool_call"!==s&&"tool_call_update"!==s||this.forward.forwardToolCalls)&&(this.sendNotification("session/update",{sessionId:e,update:t}),!0))}sendCronSessionUpdate(e,t,r){return this.sendNotification("session/update",{sessionId:e,update:t,_meta:{...this.nextMeta(),...r}}),!0}detectProtocolError(e,t,r){if(!t||"object"!=typeof t)return;const s=t,o="error"in s&&null!=s.error,n="ok"in s&&!1===s.ok;(o||n)&&this.obs({component:"acp-transport",domain:"reply",name:"protocol.error_in_result",severity:"warn",payload:{id:e,_debug_index:r,hasError:o,notOk:n,errorSummary:o?"string"==typeof s.error?s.error.slice(0,200):JSON.stringify(s.error).slice(0,200):void 0}})}attachMeta(e,t){return!e||"object"!=typeof e||Array.isArray(e)?e:{...e,_meta:t}}}
@@ -0,0 +1 @@
1
+ export const ACP_PROTOCOL_VERSION=1;export const DEFAULT_PROMPT_TIMEOUT_MS=18e5;export const DEFAULT_STREAM_IDLE_TIMEOUT_MS=15e3;export const DEFAULT_STREAM_STALE_TIMEOUT_MS=6e5;export const DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS=15e3;export const DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS=6e5;export const DEFAULT_CRON_FLUSH_TIMEOUT_MS=12e4;export const DEFAULT_CRON_SIGNAL_WINDOW_MS=6e4;
@@ -0,0 +1 @@
1
+ import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal-session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal-bridge-handler.js";export class ACPServer{agentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch{}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,i,r){this.agentFetchFn=r,this.config=e,this.logger=t,this.writeObs=i??(()=>{}),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>this.core.completePrompt(e,t),failPrompt:(e,t,i)=>this.core.failPrompt(e,t,i),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.state.startSessionGc(e=>this.core.cleanupPrompt(e))}async start(){const e=this.config.bridge.url,t=this.config.bridge.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),await this.connectGateway()}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const i=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});i.on("connected",()=>{this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"})}),i.on("disconnected",(...e)=>{const t=String(e[0]??"unknown");this.logger.warn?.(`[acp-server] Bridge disconnected: ${t}`),this.core.initialized=!1}),i.on("message",(...e)=>{const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),i.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});try{await i.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}const r={readyState:WebSocket.OPEN,send:(e,t)=>{try{i.send(e),t?.()}catch(e){t?.(e)}},close:()=>i.close(),terminate:()=>i.close(!1),on:()=>r,once:()=>r,removeListener:()=>r,removeAllListeners:()=>r,ping:()=>{}};Object.defineProperty(r,"readyState",{get:()=>"connected"===i.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=r,this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,t=>{this.wss.emit("connection",t,e)})}async connectGateway(){const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,i){this.core.pushToolResultContent(e,t,i)}isCronCorrelatedRun(e){return this.core.isCronCorrelatedRun(e)}async stop(){this.state.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop()),this.client&&(this.client.removeAllListeners(),this.client.close()),this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close()),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
@@ -0,0 +1 @@
1
+ import*as crypto from"node:crypto";import*as fs from"node:fs";import*as path from"node:path";const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function wrapPrivateKey(e){return 32===e.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e)]):Buffer.from(e)}const ED25519_SPKI_PREFIX=Buffer.from("302a300506032b6570032100","hex");function derivePublicKey(e){const r=crypto.createPrivateKey({key:wrapPrivateKey(e),format:"der",type:"pkcs8"}),t=crypto.createPublicKey(r).export({type:"spki",format:"der"});return 44===t.length?new Uint8Array(t.subarray(12)):new Uint8Array(t)}export function loadAgentKeyPair(e){if(!fs.existsSync(e))return generateAgentKeyPair(e);const r=fs.readFileSync(e,"utf-8").trim();try{const e=JSON.parse(r);if(e.publicKey&&e.privateKey)return{publicKey:new Uint8Array(Buffer.from(e.publicKey,"base64")),privateKey:new Uint8Array(Buffer.from(e.privateKey,"base64"))}}catch{}const t=new Uint8Array(Buffer.from(r,"base64"));if(32===t.length)return{publicKey:derivePublicKey(t),privateKey:t};throw new Error(`Invalid key file format at ${e}`)}function generateAgentKeyPair(e){const r=crypto.generateKeyPairSync("ed25519",{publicKeyEncoding:{type:"spki",format:"der"},privateKeyEncoding:{type:"pkcs8",format:"der"}}),t=r.publicKey,n=r.privateKey,a=new Uint8Array(44===t.length?t.subarray(12):t),i=new Uint8Array(48===n.length?n.subarray(16):n),o=path.dirname(e);return fs.existsSync(o)||fs.mkdirSync(o,{recursive:!0}),fs.writeFileSync(e,JSON.stringify({publicKey:Buffer.from(a).toString("base64"),privateKey:Buffer.from(i).toString("base64")},null,2),{mode:384}),{publicKey:a,privateKey:i}}function signMessage(e,r){const t=crypto.createPrivateKey({key:wrapPrivateKey(r),format:"der",type:"pkcs8"});return new Uint8Array(crypto.sign(null,Buffer.from(e),t))}export function agentAuthHeaders(e,r,t){const n=Math.floor(Date.now()/1e3).toString(),a=crypto.randomUUID(),i=signMessage(`${e.toUpperCase()}|${r}|${n}|${a}`,t.privateKey);return{"X-Agent-Public-Key":Buffer.from(t.publicKey).toString("base64"),"X-Agent-Signature":Buffer.from(i).toString("base64"),"X-Agent-Timestamp":n,"X-Agent-Nonce":a}}export async function agentFetch(e,r,t){const n="string"==typeof e?new URL(e):e,a=agentAuthHeaders(t?.method?.toUpperCase()??"GET",n.pathname,r),i=new Headers(t?.headers);for(const[e,r]of Object.entries(a))i.set(e,r);return fetch(e,{...t,headers:i})}
package/dist/config.js ADDED
@@ -0,0 +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??{},u=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,a=pick([{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.BEEOS_PLATFORM_URL)}],{source:"default",value:""}),t=r.gateway??{},l=(o.gateway??{}).auth??{},n=pick([{source:"pluginConfig",value:trimStr(t.url)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),i=pick([{source:"openclawConfig",value:trimStr(l.token)},{source:"pluginConfig",value:trimStr(t.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),s=pick([{source:"pluginConfig",value:readInt(t.protocol)}],{source:"default",value:3}),c=pick([{source:"pluginConfig",value:trimStr(t.clientId)}],{source:"default",value:`beeos-claw-${process.pid}`}),v=pick([{source:"pluginConfig",value:trimStr(t.agentId)}],{source:"default",value:"main"}),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}),_=r.retry??{},E=pick([{source:"pluginConfig",value:readInt(_.baseMs)}],{source:"default",value:1e3}),S=pick([{source:"pluginConfig",value:readInt(_.maxMs)}],{source:"default",value:6e5}),I=pick([{source:"pluginConfig",value:readInt(_.maxAttempts)}],{source:"default",value:0}),M=r.log??{},T=pick([{source:"pluginConfig",value:readBool(M.enabled)},{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),O=pick([{source:"pluginConfig",value:readBool(M.verbose)}],{source:"default",value:!1}),A=pick([{source:"pluginConfig",value:trimStr(M.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),R=pick([{source:"pluginConfig",value:readInt(p.promptTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_PROMPT_TIMEOUT_MS)}],{source:"default",value:3e5}),C=pick([{source:"pluginConfig",value:readInt(r.terminalWsPort)},{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),h=trimStr(r.terminalWsAllowedOrigins)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),k=pick([{source:h?"pluginConfig":"default",value:h?h.split(",").map(e=>e.trim()).filter(Boolean):void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),L=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}),w=pick([{source:"pluginConfig",value:readInt(r.loadReplayIdleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_LOAD_REPLAY_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),b=pick([{source:"pluginConfig",value:readInt(r.cronFlushTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_FLUSH_TIMEOUT_MS)}],{source:"default",value:12e4}),P=pick([{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),B={platformUrl:a.value.replace(/\/+$/,""),bridge:{url:d.value,keyFile:m.value,service:f.value,mode:trimStr(p.mode),shell:{enabled:g.value},promptTimeoutMs:R.value,historyPendingTimeoutMs:readInt(p.historyPendingTimeoutMs)},gateway:{url:n.value,token:i.value,protocol:s.value,clientId:c.value,agentId:v.value},retry:{baseMs:Math.max(0,E.value),maxMs:Math.max(0,S.value),maxAttempts:Math.max(0,I.value)},log:{enabled:T.value,verbose:O.value,dir:A.value},promptTimeoutMs:R.value,terminalWsPort:C.value,terminalWsAllowedOrigins:k.value,streamIdleTimeoutMs:Math.max(0,L.value),streamStaleTimeoutMs:Math.max(0,y.value),loadReplayIdleTimeoutMs:Math.max(0,w.value),cronFlushTimeoutMs:Math.max(0,b.value),cronSignalWindowMs:Math.max(0,P.value)},F={platformUrl:a.source,bridge:{url:d.source,keyFile:m.source,service:f.source,shell:{enabled:g.source}},gateway:{url:n.source,token:i.source,protocol:s.source,clientId:c.source,agentId:v.source},retry:{baseMs:E.source,maxMs:S.source,maxAttempts:I.source},log:{enabled:T.source,verbose:O.source,dir:A.source},promptTimeoutMs:R.source,terminalWsPort:C.source,terminalWsAllowedOrigins:k.source,streamIdleTimeoutMs:L.source,streamStaleTimeoutMs:y.source,loadReplayIdleTimeoutMs:w.source,cronFlushTimeoutMs:b.source,cronSignalWindowMs:P.source},U=[];return B.platformUrl||U.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 ${u}`,"Or set the BEEOS_PLATFORM_URL environment variable"]}),B.gateway.token||U.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 ${u}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${u}`]}),{config:B,sources:F,validation:U,openclawConfigPath:u}}export function resolveConfig(e){const{config:r,validation:o}=resolveConfigWithMeta({pluginConfig:e}),u=o.find(e=>"error"===e.severity);if(u)throw new Error(`[beeos-claw] ${u.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{createWriteStream}from"node:fs";import{mkdir,access,readdir,constants}from"node:fs/promises";import{join}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";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(e,r){if(e instanceof Error&&"AbortError"===e.name)return{message:"download timed out",category:"timeout",retriable:!0};if(r)return 429===r||r>=500?{message:`HTTP ${r}`,category:"http_5xx",retriable:!0,httpStatus:r}:r>=400?{message:`HTTP ${r}`,category:"http_4xx",retriable:!1,httpStatus:r}:{message:`HTTP ${r}`,category:"invalid_response",retriable:!1,httpStatus:r};const t=e instanceof Error?e.message:String(e),o=e.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(e,r,t){const o=e,a=r,n=getHeader(o,"x-beeos-token")||getHeader(o,"authorization")?.replace(/^Bearer\s+/i,""),i=process.env.BRIDGE_INTERNAL_SECRET;if(i&&n!==i)return void sendJSON(a,401,{ok:!1,error:"unauthorized"});let s;try{s=await readJSONBody(o)}catch{return void sendJSON(a,400,{ok:!1,error:"invalid JSON body"})}if(!s.fileId||!s.fileName||!s.presignedUrl)return void sendJSON(a,400,{ok:!1,error:"fileId, fileName, and presignedUrl are required"});const c=s.targetDir?join(OPENCLAW_HOME,"workspace",s.targetDir):UPLOAD_DIR,d=s.fileName.replace(/[/\\:*?"<>|]/g,"_"),l=`${s.fileId}_${d}`,f=join(c,l);try{return await access(f,constants.F_OK),void sendJSON(a,200,{ok:!0,localPath:f,cached:!0})}catch{const e=await findByFileIdPrefix(c,s.fileId);if(e)return void sendJSON(a,200,{ok:!0,localPath:e,cached:!0})}try{await mkdir(c,{recursive:!0})}catch(e){return void sendJSON(a,500,{ok:!1,error:`Failed to create target directory: ${e instanceof Error?e.message:String(e)}`})}const u=new AbortController,m=setTimeout(()=>u.abort(),3e5);"object"==typeof m&&"unref"in m&&m.unref();try{const e=await fetch(s.presignedUrl,{signal:u.signal});if(!e.ok||!e.body){const r=classifyDownloadError(null,e.status);return void sendJSON(a,502,{ok:!1,error:`Download failed: ${r.message}`,errorCategory:r.category,retriable:r.retriable,httpStatus:r.httpStatus})}const r=createWriteStream(f);await pipeline(Readable.fromWeb(e.body),r)}catch(e){const r=classifyDownloadError(e);return void sendJSON(a,502,{ok:!1,error:`Download failed: ${r.message}`,errorCategory:r.category,retriable:r.retriable})}finally{clearTimeout(m)}sendJSON(a,200,{ok:!0,localPath:f,cached:!1})}function getHeader(e,r){const t=e.headers[r];return Array.isArray(t)?t[0]:t}function readJSONBody(e){return new Promise((r,t)=>{const o=[];e.on("data",e=>{o.push(Buffer.isBuffer(e)?e:Buffer.from(e))}),e.on("end",()=>{try{r(JSON.parse(Buffer.concat(o).toString("utf-8")))}catch(e){t(e)}}),e.on("error",t)})}function sendJSON(e,r,t){e.statusCode=r,e.setHeader("Content-Type","application/json"),e.end(JSON.stringify(t))}async function findByFileIdPrefix(e,r){try{const t=await readdir(e),o=`${r}_`;for(const r of t)if(r.startsWith(o)){const t=join(e,r);try{return await access(t,constants.R_OK),t}catch{continue}}}catch{}return null}
@@ -0,0 +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:r}=generateKeyPairSync("ed25519"),o=i.export({type:"spki",format:"der"}).subarray(-32),s=r.export({type:"pkcs8",format:"der"}),c=createHash("sha256").update(o).digest("hex"),a=base64urlEncode(o),h={deviceId:c,publicKeyB64url:a,privateKeyDer:s};return writeFileSync(n,JSON.stringify({deviceId:c,publicKey:a,privateKey:s.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(),r=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:r,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 r=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),s=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()},c=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(()=>{r=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):this.emit("gateway-event",e,t)}else if("hello-ok"===a)s(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===r){if(i.ok){const e=i.payload;s(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(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"],r={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});r.nonce=e,r.signature=signDeviceAuth(this.deviceIdentity,o)}const o=randomUUID(),s={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:["tool-events"],role:n,scopes:i,auth:{token:this.config.token},device:r}};return this.ws?.send(JSON.stringify(s)),o}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const i=randomUUID(),r={type:"req",id:i,method:e,params:t};return new Promise((t,o)=>{const s=setTimeout(()=>{this.pending.delete(i),o(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(i,{resolve:t,reject:o,timer:s}),this.ws.send(JSON.stringify(r))})}async agentSend(e,t,n,i){const r={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};i?.length&&(r.attachments=i);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",{})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{sessionKey:e,...t})}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 ADDED
@@ -0,0 +1 @@
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.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";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 o,r;try{o=e.runtime?.config?.loadConfig?.()}catch{}try{r=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:o})}catch(o){return void e.logger.error?.("[beeos-claw] Failed to resolve config:",o)}const a=r.config;pluginConfig=a;const i=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};i({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),i({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:r.sources,openclawConfigPath:r.openclawConfigPath}});for(const o of r.validation)i({component:"plugin",domain:"config",name:"config.validation",severity:o.severity,summary:o.message,error:{code:o.code,message:o.message,nextSteps:o.nextSteps}}),"error"===o.severity?e.logger.error?.(`[beeos-claw] ${o.message}`):e.logger.warn?.(`[beeos-claw] ${o.message}`);if(r.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.bridge.keyFile)try{agentKeys=loadAgentKeyPair(a.bridge.keyFile);const o=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${o})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${o}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);cleanupUploadToolResultCache().catch(o=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(o)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(o,r){e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} params=${JSON.stringify(r)}`);const l=await executeUpload(r,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} ok=${l.ok}`),i({component:"upload-tool",domain:"tool",name:l.ok?"upload.success":"upload.failure",severity:l.ok?"info":"warn",payload:{toolCallId:o,ok:l.ok,fileCount:l.files?.length??0,error:l.error}}),l.ok&&l.content&&acpServer&&acpServer.pushToolResultContent(o,BEEOS_UPLOAD_TOOL_NAME,l.content),(async r=>{try{await writeUploadToolResult(o,BEEOS_UPLOAD_TOOL_NAME,r),await cleanupUploadToolResultCache()}catch(r){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${o}: ${String(r)}`)}return r})(l)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,o){await handleFileStage(e,o,a)}});const l=agentKeys?(e,o)=>agentFetch(e,agentKeys,o):void 0;acpServer=new ACPServer(a,e.logger,i,l),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 n=new TerminalManager,t=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:n,logger:e.logger,writeObs:i});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){t.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){t.stop(),n.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),o){const r=mirrorOpenClawConfig(o);r.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${r.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${r.reason}`)}e.logger.info?.("[beeos-claw] Plugin registered successfully")}};export default plugin;
@@ -0,0 +1 @@
1
+ const TIMESTAMP_PREFIX_RE=/^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}(?::\d{2})?\s+GMT[+-]\d{1,2}(?::?\d{2})?]\s*/i,WORKDIR_PREFIX_RE=/^\s*\[working directory:[^\]]*]\s*/i,MESSAGE_ID_PREFIX_RE=/^\s*\[message_id:[^\]]*]\s*/i,USER_MSG_PREFIX_RE=/^\s*User Message From \w+:\s*/i,HEARTBEAT_OK_LINE="HEARTBEAT_OK",METADATA_PREFIX_PATTERNS=[TIMESTAMP_PREFIX_RE,WORKDIR_PREFIX_RE,MESSAGE_ID_PREFIX_RE,USER_MSG_PREFIX_RE],UNTRUSTED_METADATA_BLOCK_RE=/^Conversation info \(untrusted metadata\):[\s\S]*?Current time:\s*.*?$/i;function stripMetadataPrefixesFromLine(t){let e=t,r=!1,n=!0;for(;n;){n=!1;for(const t of METADATA_PREFIX_PATTERNS)t.test(e)&&(e=e.replace(t,""),r=!0,n=!0)}return{line:e,changed:r}}export function stripTransportMetadata(t){if(!(t.includes("[")||t.includes("HEARTBEAT_OK")||t.includes("User Message From")||UNTRUSTED_METADATA_BLOCK_RE.test(t.trim())))return t;if(UNTRUSTED_METADATA_BLOCK_RE.test(t.trim()))return"";const e=t.split(/\r?\n/),r=[];let n=!1;for(const t of e){const e=stripMetadataPrefixesFromLine(t);"HEARTBEAT_OK"!==e.line.trim()?e.changed&&(n=!0,!e.line.trim())||r.push(e.line):n=!0}return n?r.join("\n").replace(/^\n+/,"").replace(/\n+$/,""):t}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function sanitizeNode(t){if("string"==typeof t){const e=stripTransportMetadata(t);return{value:e,changed:e!==t}}if(Array.isArray(t)){let e=!1;const r=t.map(t=>{const r=sanitizeNode(t);return r.changed&&(e=!0),r.value});return{value:e?r:t,changed:e}}if(!isRecord(t))return{value:t,changed:!1};let e=!1,r=t;if("string"==typeof t.type&&"text"===t.type&&"string"==typeof t.text){const n=stripTransportMetadata(t.text);n!==t.text&&(r={...r,text:n},e=!0)}if(Array.isArray(t.content)){const n=sanitizeNode(t.content);n.changed&&(e||(r={...r}),r.content=n.value,e=!0)}return{value:e?r:t,changed:e}}export function sanitizePromptPayload(t){return sanitizeNode(t).value}
@@ -0,0 +1 @@
1
+ import{join}from"node:path";import{homedir}from"node:os";import{createRollingJsonlWriter,DEFAULT_MAX_BYTES}from"./utils/rolling-jsonl.js";function collectSecrets(e){const r=[];return e.gatewayToken&&r.push(e.gatewayToken),Array.from(new Set(r)).sort((e,r)=>r.length-e.length)}function redactSecrets(e,r){let t=e;for(const e of r)e.length>=8?t=t.split(e).join("(redacted)"):t===e&&(t="(redacted)");return t}const MAX_CONTENT_LEN=2e3,BASE64_DATA_URI_RE=/data:[^;]+;base64,[A-Za-z0-9+/=]{200,}/gi,LARGE_BASE64_THRESHOLD=1024,RAW_BASE64_RE=/^[A-Za-z0-9+/]{200,}={0,3}$/;function sanitizeBase64(e){let r=e.replace(BASE64_DATA_URI_RE,e=>`[base64_data_uri:${e.length}B]`);return r.length>1024&&RAW_BASE64_RE.test(r.trim())?`[base64:${r.length}B]`:r}export function redact(e,r,t=new WeakSet){if(null==e||"boolean"==typeof e||"number"==typeof e)return e;if("string"==typeof e){let t=redactSecrets(e,r);return t=sanitizeBase64(t),t.length>2e3&&(t=t.slice(0,2e3)+`... (${e.length} chars total)`),t}if(Array.isArray(e))return t.has(e)?"[Circular]":(t.add(e),e.map(e=>redact(e,r,t)));if("object"==typeof e){if(t.has(e))return"[Circular]";t.add(e);const n={};for(const[o,a]of Object.entries(e))n[o]="token"===o||"apiKey"===o||"secret"===o||"password"===o?"(redacted)":redact(a,r,t);return n}return e}const DEFAULT_OBS_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs");export function createObsEventWriter(e){const r=e.dir||DEFAULT_OBS_DIR,t=join(r,"beeos_obs_trace.jsonl"),n=e.redaction?collectSecrets(e.redaction):[],o=createRollingJsonlWriter({filePath:t,maxBytes:DEFAULT_MAX_BYTES,contextLabel:"obs",onError:r=>e.logger?.warn?.(`[obs] log write failed: ${r.message}`)});return e=>{const r={...e,ts:e.ts||(new Date).toISOString()},t=n.length>0?redact(r,n):r;o(t)}}export function createTraceWriter(e){const r=e.dir||DEFAULT_OBS_DIR,t=join(r,e.filename),n=e.redaction?collectSecrets(e.redaction):[],o=createRollingJsonlWriter({filePath:t,maxBytes:DEFAULT_MAX_BYTES,contextLabel:e.filename,onError:r=>e.logger?.warn?.(`[trace] write failed ${e.filename}: ${r.message}`)});return(e,r,t)=>{const a={ts:(new Date).toISOString(),event:e,sessionId:t,data:null!=r?n.length>0?redact(r,n):r:void 0};o(a)}}export function createMultiTraceWriters(e){const r={dir:e.dir,redaction:e.redaction,logger:e.logger};return{forward:createTraceWriter({...r,filename:"trace_forward.jsonl"}),reply:createTraceWriter({...r,filename:"trace_reply.jsonl"}),lifecycle:createTraceWriter({...r,filename:"trace_lifecycle.jsonl"})}}export function createComponentLogger(e,r){const t=r??(()=>{});return{debug(r,n){t({component:e,domain:"runtime",name:r,severity:"debug",payload:n})},info(r,n){t({component:e,domain:"runtime",name:r,severity:"info",payload:n})},warn(r,n){t({component:e,domain:"runtime",name:r,severity:"warn",payload:n})},error(r,n){t({component:e,domain:"runtime",name:r,severity:"error",payload:n})},forward(r,n,o){t({component:e,domain:"forward",name:r,severity:"debug",payload:{sessionId:o,...n&&"object"==typeof n?n:{raw:n}}})},reply(r,n,o){t({component:e,domain:"reply",name:r,severity:"debug",payload:{sessionId:o,...n&&"object"==typeof n?n:{raw:n}}})}}}
@@ -0,0 +1 @@
1
+ import{readFileSync}from"node:fs";import{fileURLToPath}from"node:url";const DEFAULT_VERSION="0.1.0";function readVersionFromJson(o){try{const e=readFileSync(fileURLToPath(o),"utf-8"),n=JSON.parse(e);if(n&&"object"==typeof n&&"string"==typeof n.version)return n.version.trim()||void 0}catch{}}export function resolvePluginVersion(){const o=[new URL("../openclaw.plugin.json",import.meta.url),new URL("../../openclaw.plugin.json",import.meta.url),new URL("../package.json",import.meta.url),new URL("../../package.json",import.meta.url)];for(const e of o){const o=readVersionFromJson(e);if(o)return o}return"0.1.0"}
@@ -0,0 +1 @@
1
+ import{TERMINAL_ERROR_CODES,TERMINAL_UPDATE_METHOD,parseTerminalOpenParams,parseTerminalInputParams,parseTerminalResizeParams,parseTerminalCloseParams,parseTerminalWaitParams}from"./terminal-rpc.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.sendError(e.id,-32601,`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 t;if(r.value.dataBase64)try{t=Buffer.from(r.value.dataBase64,"base64")}catch{return}else t=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,t),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.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");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 t=this.terminalMgr.create(r.value.command);this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.created",severity:"info",payload:{terminalId:t.id,command:t.command,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:t.id,command:t.command})}async handleTerminalWait(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");const r=parseTerminalWaitParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const t=this.terminalMgr.get(r.value.terminalId);t?this.transport.sendResult(e.id,{terminalId:r.value.terminalId,exitCode:t.exitCode,output:t.output.join(""),state:t.state}):this.transport.sendError(e.id,TERMINAL_ERROR_CODES.terminalNotFound,"Terminal not found")}async handleTerminalRelease(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");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.sendError(e.id,TERMINAL_ERROR_CODES.terminalNotFound,"Terminal not found")}async handleTerminalKill(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const t=this.terminalMgr.get(r.value.terminalId);t?"closed"!==t.state?(this.terminalMgr.kill(r.value.terminalId,r.value.signal),this.transport.sendResult(e.id,{})):this.transport.sendError(e.id,TERMINAL_ERROR_CODES.terminalClosed,"Terminal already closed"):this.transport.sendError(e.id,TERMINAL_ERROR_CODES.terminalNotFound,"Terminal not found")}async handleTerminalOpen(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");const r=parseTerminalOpenParams(e.params);if(r.ok)try{const t=this.terminalMgr.openSession({sessionId:r.value.sessionId,command:r.value.command,cols:r.value.cols,rows:r.value.rows,cwd:r.value.cwd}),a=setTimeout(()=>{this.interactiveTerminals.delete(t.terminalId),this.terminalMgr.closeSession(t.terminalId,"heartbeat_timeout")},this.HEARTBEAT_TIMEOUT_MS);"object"==typeof a&&"unref"in a&&a.unref(),this.interactiveTerminals.set(t.terminalId,{heartbeatTimer:a}),this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.opened",severity:"info",payload:{terminalId:t.terminalId,shell:t.shell,cwd:t.cwd,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:t.terminalId,command:t.shell,shell:t.shell,cwd:t.cwd,cols:t.cols,rows:t.rows,state:t.state})}catch(r){const t=r instanceof Error?r.message:String(r);t.includes("quota")?this.transport.sendError(e.id,TERMINAL_ERROR_CODES.terminalQuotaExceeded,t):this.transport.sendError(e.id,-32e3,t)}else this.sendTerminalParseError(e.id,r)}handleTerminalInput(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");const r=parseTerminalInputParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);let t;if(r.value.dataBase64)try{t=Buffer.from(r.value.dataBase64,"base64")}catch{return void this.transport.sendError(e.id,-32602,"invalid dataBase64")}else t=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,t),this.refreshTerminalHeartbeat(r.value.terminalId),this.transport.sendResult(e.id,{})}catch(r){const t=r instanceof Error?r.message:String(r);this.transport.sendError(e.id,TERMINAL_ERROR_CODES.terminalNotFound,t)}}handleTerminalResize(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");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 t=r instanceof Error?r.message:String(r);this.transport.sendError(e.id,TERMINAL_ERROR_CODES.terminalNotFound,t)}else this.sendTerminalParseError(e.id,r)}async handleTerminalClose(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendError(e.id,TERMINAL_ERROR_CODES.shellDisabled,"Shell is disabled");const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const t=this.interactiveTerminals.get(r.value.terminalId);t&&(clearTimeout(t.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.sendError(e,r.error.code,r.error.message)}destroy(){for(const e of this.interactiveTerminals.values())clearTimeout(e.heartbeatTimer);this.interactiveTerminals.clear()}}
@@ -0,0 +1 @@
1
+ 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,t){const r=e[t];if("string"==typeof r){const e=r.trim();return e.length>0?e:void 0}}function readPositiveInt(e,t){const r=e[t];if("number"==typeof r&&Number.isInteger(r)&&r>0)return r}function invalid(e,t){return{ok:!1,error:{...TERMINAL_INVALID_PARAMS,data:{field:e,reason:t}}}}function parseBase(e){return isRecord(e)?{ok:!0,value:e}:invalid("params","must be an object")}export function parseTerminalOpenParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readPositiveInt(r,"cols")??80,a=readPositiveInt(r,"rows")??24,i=readString(r,"sessionId"),s=readString(r,"command"),o=readString(r,"cwd");return{ok:!0,value:{sessionId:i,command:s,cols:n,rows:a,...o?{cwd:o}:{}}}}export function parseTerminalInputParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(r,"data"),i=readString(r,"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 t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(r,"cols");if(!a)return invalid("cols","must be a positive integer");const i=readPositiveInt(r,"rows");return i?{ok:!0,value:{terminalId:n,cols:a,rows:i}}:invalid("rows","must be a positive integer")}export function parseTerminalCloseParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(r,"signal");return{ok:!0,value:{terminalId:n,...a?{signal:a}:{}}}}export function parseTerminalWaitParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(r,"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{spawn}from"node:child_process";import{createRequire}from"node:module";import{randomUUID}from"node:crypto";import{realpathSync}from"node:fs";import{dirname,resolve}from"node:path";export const TERMINAL_TIMEOUT_REASONS={idle:"idle_timeout",maxDuration:"max_duration_timeout"};const ALLOWED_STATE_TRANSITIONS={opening:["running","closing","closed"],running:["closing","closed"],closing:["closed"],closed:[]},require=createRequire(import.meta.url);let cachedNodePty;function resolveOpenclawNodeModulesDir(){const e=process.argv[1];if(e)try{const t=realpathSync(e);if(/openclaw/i.test(t))return dirname(t)}catch{}const t=["/opt/homebrew/lib/node_modules/openclaw","/usr/local/lib/node_modules/openclaw","/usr/lib/node_modules/openclaw"];for(const e of t)try{return realpathSync(resolve(e,"package.json")),e}catch{}return null}function getNodePty(){if(void 0!==cachedNodePty)return cachedNodePty;const e=["node-pty","@lydell/node-pty"];for(const t of e)try{return cachedNodePty=require(t),cachedNodePty}catch{}const t=resolveOpenclawNodeModulesDir();if(t){const s=createRequire(resolve(t,"package.json"));for(const t of e)try{return cachedNodePty=s(t),cachedNodePty}catch{}}return cachedNodePty=null,null}function createPtyProcess(e){const t=getNodePty();if(!t)throw new Error("node-pty not available");const s={...process.env,TERM:"xterm-256color"},i=t.spawn(e.shell,[],{name:"xterm-256color",cwd:e.cwd,env:s,cols:e.cols,rows:e.rows});let o=!1;return i.onData(t=>{e.onData(Buffer.from(t,"utf8"))}),i.onExit(t=>{var s;s={code:Number.isFinite(t.exitCode)?t.exitCode:null,signal:null!=t.signal?String(t.signal):null},o||(o=!0,e.onExit(s))}),{write(e){const t=e.toString("utf8");t&&i.write(t)},resize(e,t){i.resize(e,t)},close(){try{i.kill()}catch{}}}}function createPipeProcess(e){const t=spawn(e.shell,[],{shell:!0,cwd:e.cwd,stdio:["pipe","pipe","pipe"],env:{...process.env,TERM:"xterm-256color"}});let s=!1;const i=t=>{s||(s=!0,e.onExit(t))};return t.stdout?.on("data",t=>e.onData(Buffer.isBuffer(t)?t:Buffer.from(t))),t.stderr?.on("data",t=>e.onData(Buffer.isBuffer(t)?t:Buffer.from(t))),t.once("error",e=>i({code:null,signal:null})),t.once("exit",(e,t)=>i({code:e??null,signal:t??null})),{write(e){t.stdin?.destroyed||t.stdin?.write(e)},resize(e,t){},close(){t.killed||t.kill()}}}function createDefaultProcess(e){try{return createPtyProcess(e)}catch{return createPipeProcess(e)}}const DEFAULT_MAX_CONCURRENT=10,DEFAULT_IDLE_TIMEOUT_MS=432e5,DEFAULT_MAX_DURATION_MS=432e5;export class TerminalNotFoundError extends Error{terminalId;constructor(e){super(`terminal not found: ${e}`),this.name="TerminalNotFoundError",this.terminalId=e}}export class TerminalClosedError extends Error{terminalId;state;constructor(e,t){super(`terminal is not writable in state=${t}: ${e}`),this.name="TerminalClosedError",this.terminalId=e,this.state=t}}function chainHandler(e,t){return t?e?s=>{e(s),t(s)}:t:e}export class TerminalManager{sessions=new Map;maxConcurrent;idleTimeoutMs;maxDurationMs;onOutput;onExit;onStateChange;constructor(e){this.maxConcurrent=e?.maxConcurrent??10,this.idleTimeoutMs=e?.idleTimeoutMs??432e5,this.maxDurationMs=e?.maxDurationMs??432e5}addListeners(e){this.onOutput=chainHandler(this.onOutput,e.onOutput),this.onExit=chainHandler(this.onExit,e.onExit),this.onStateChange=chainHandler(this.onStateChange,e.onStateChange)}openSession(e){if(this.sessions.size>=this.maxConcurrent&&this.evictOldest(),this.sessions.size>=this.maxConcurrent)throw new Error("terminal quota exceeded");const t=e.terminalId&&!this.sessions.has(e.terminalId)?e.terminalId:`term_${randomUUID().slice(0,12)}`,s=e.sessionId??t,i=e.command||process.env.SHELL||"/bin/bash",o=e.cwd||process.cwd(),n=Date.now(),r={terminalId:t,sessionId:s,shell:i,cwd:o,startedAt:n,lastActiveAt:n,cols:e.cols,rows:e.rows,state:"opening",nextSeq:1,output:[],process:null};this.sessions.set(t,r);try{r.process=createDefaultProcess({shell:i,cwd:o,cols:e.cols,rows:e.rows,onData:e=>this.handleProcessData(t,e),onExit:e=>this.handleProcessExit(t,e)}),this.transitionState(r,"running"),this.scheduleIdleTimeout(r),this.scheduleMaxDuration(r)}catch(e){throw this.clearTimers(r),this.transitionState(r,"closed"),this.sessions.delete(t),e}return this.snapshot(r)}writeToSession(e,t){const s=this.requireSession(e);this.assertRunning(s),s.process?.write(t),this.markActivity(s)}resizeSession(e,t,s){const i=this.requireSession(e);this.assertRunning(i),i.cols=t,i.rows=s,i.process?.resize(t,s),this.markActivity(i)}closeSession(e,t){const s=this.sessions.get(e);s&&(t&&!s.timeoutReason&&(s.timeoutReason=t),"closed"!==s.state?(this.clearTimers(s),"closing"!==s.state&&(this.transitionState(s,"closing"),s.process?.close())):this.clearTimers(s))}touchSession(e){const t=this.requireSession(e);this.assertRunning(t),this.markActivity(t)}getSession(e){const t=this.sessions.get(e);return t?this.snapshot(t):void 0}create(e){const t=this.openSession({command:e,cols:80,rows:24}),s=this.sessions.get(t.terminalId);return{id:s.terminalId,command:s.shell,state:s.state,output:s.output,exitCode:s.exitCode,seq:s.nextSeq-1}}get(e){const t=this.sessions.get(e);if(t)return{id:t.terminalId,state:t.state,output:t.output,exitCode:t.exitCode,seq:t.nextSeq-1}}write(e,t){try{return this.writeToSession(e,Buffer.from(t,"utf8")),!0}catch{return!1}}resize(e,t,s){try{return this.resizeSession(e,t,s),!0}catch{return!1}}kill(e,t="SIGTERM"){const s=this.sessions.get(e);s&&s.process&&("closing"!==s.state&&this.transitionState(s,"closing"),s.process.close())}release(e){const t=this.sessions.get(e);t&&(!t.process||"running"!==t.state&&"opening"!==t.state||this.kill(e),this.clearTimers(t),this.sessions.delete(e))}listSessions(){return[...this.sessions.values()].map(e=>this.snapshot(e))}get sessionCount(){return this.sessions.size}get maxConcurrentLimit(){return this.maxConcurrent}destroy(){for(const e of[...this.sessions.keys()])this.release(e)}snapshot(e){return{terminalId:e.terminalId,sessionId:e.sessionId,shell:e.shell,cwd:e.cwd,startedAt:e.startedAt,lastActiveAt:e.lastActiveAt,cols:e.cols,rows:e.rows,state:e.state}}requireSession(e){const t=this.sessions.get(e);if(!t)throw new TerminalNotFoundError(e);return t}assertRunning(e){if("running"!==e.state)throw new TerminalClosedError(e.terminalId,e.state)}transitionState(e,t){const s=e.state;s!==t&&ALLOWED_STATE_TRANSITIONS[s].includes(t)&&(e.state=t,this.onStateChange?.({terminalId:e.terminalId,sessionId:e.sessionId,from:s,to:t}))}handleProcessData(e,t){const s=this.sessions.get(e);if(!s||"closed"===s.state)return;this.markActivity(s);const i=s.nextSeq++;s.output.push(t.toString("utf-8")),this.onOutput?.({terminalId:s.terminalId,sessionId:s.sessionId,data:t,seq:i})}handleProcessExit(e,t){const s=this.sessions.get(e);s&&(this.clearTimers(s),s.exitCode=t.code??void 0,"closing"!==s.state&&this.transitionState(s,"closing"),this.transitionState(s,"closed"),this.onExit?.({terminalId:s.terminalId,sessionId:s.sessionId,code:t.code,signal:t.signal,...s.timeoutReason?{reason:s.timeoutReason}:{}}))}markActivity(e){e.lastActiveAt=Date.now(),"running"===e.state&&this.refreshIdleTimer(e)}evictOldest(){let e=null;for(const t of this.sessions.values()){if("closed"===t.state){e=t;break}(!e||t.lastActiveAt<e.lastActiveAt||t.lastActiveAt===e.lastActiveAt&&t.startedAt<e.startedAt)&&(e=t)}e&&this.release(e.terminalId)}refreshIdleTimer(e){e.idleTimer&&clearTimeout(e.idleTimer),e.idleTimer=setTimeout(()=>{"running"===e.state&&this.closeSession(e.terminalId,TERMINAL_TIMEOUT_REASONS.idle)},this.idleTimeoutMs),"object"==typeof e.idleTimer&&"unref"in e.idleTimer&&e.idleTimer.unref()}scheduleIdleTimeout(e){this.refreshIdleTimer(e)}scheduleMaxDuration(e){e.maxDurationTimer=setTimeout(()=>{"running"===e.state&&this.closeSession(e.terminalId,TERMINAL_TIMEOUT_REASONS.maxDuration)},this.maxDurationMs),"object"==typeof e.maxDurationTimer&&"unref"in e.maxDurationTimer&&e.maxDurationTimer.unref()}clearTimers(e){e.idleTimer&&(clearTimeout(e.idleTimer),e.idleTimer=void 0),e.maxDurationTimer&&(clearTimeout(e.maxDurationTimer),e.maxDurationTimer=void 0)}}
@@ -0,0 +1 @@
1
+ import{WebSocketServer,WebSocket}from"ws";const HEARTBEAT_TIMEOUT_MS=6e4;export function createTerminalWebSocketService(e){const{config:t,terminalMgr:n,logger:o,writeObs:r}=e,i=r??(()=>{});let a=null;const c=new Set;function s(e){return e.replace(/^https?:\/\//,"").replace(/:\d+$/,"").replace(/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i,"localhost")}function l(e,r){const a=function(e){const t=e.headers["x-forwarded-for"];return("string"==typeof t?t.split(",")[0]?.trim():void 0)||e.socket?.remoteAddress||"unknown"}(r),l=r.headers.origin;if(!function(e){if(!t.allowedOrigins||0===t.allowedOrigins.length)return!0;if(!e)return!1;const n=s(e);return t.allowedOrigins.some(e=>s(e)===n)}(l))return i({component:"terminal-ws",domain:"auth",name:"origin_rejected",severity:"warn",payload:{origin:l,clientIdentity:a}}),void e.close(1008,"Origin not allowed");const d=function(e){try{return new URL(e.url??"/","ws://localhost").searchParams.get("token")??void 0}catch{return}}(r);if(d!==t.token)return i({component:"terminal-ws",domain:"auth",name:"token_rejected",severity:"warn",payload:{clientIdentity:a}}),void e.close(1008,"Unauthorized");const f=n.create(),m={ws:e,terminalId:f.id,clientIdentity:a};c.add(m),i({component:"terminal-ws",domain:"lifecycle",name:"client.connected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}});const u=setInterval(()=>{const t=n.get(f.id);if(t){if(t.output.length>0){const n=t.output.splice(0).join("");n&&e.readyState===WebSocket.OPEN&&e.send(n)}"closed"===t.state&&(clearInterval(u),e.readyState===WebSocket.OPEN&&e.close(1e3,`exit:${t.exitCode??0}`))}else clearInterval(u)},50);let p=Date.now();const w=setInterval(()=>{Date.now()-p>6e4&&(o?.warn?.(`[terminal-ws] heartbeat timeout terminalId=${f.id}`),e.close(1001,"Heartbeat timeout"))},3e4);"object"==typeof w&&"unref"in w&&w.unref(),e.on("message",(t,o)=>{p=Date.now();const r=Buffer.isBuffer(t)?t.toString("utf-8"):"string"==typeof t?t:Buffer.from(t).toString("utf-8");if(o)n.write(f.id,r);else try{const t=JSON.parse(r),o=t.action??t.type;if("heartbeat"===o)p=Date.now(),e.send(JSON.stringify({type:"heartbeat_ack"}));else if("resize"===o){const e="number"==typeof t.cols?t.cols:80,o="number"==typeof t.rows?t.rows:24;n.resize(f.id,e,o)}else"input"===o&&"string"==typeof t.data&&n.write(f.id,t.data)}catch{n.write(f.id,r)}}),e.on("close",()=>{clearInterval(u),clearInterval(w),c.delete(m),n.release(f.id),i({component:"terminal-ws",domain:"lifecycle",name:"client.disconnected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}})})}return{start(){a=new WebSocketServer({port:t.port}),a.on("connection",l),o?.info?.(`[terminal-ws] Listening on ws://0.0.0.0:${t.port}`),i({component:"terminal-ws",domain:"lifecycle",name:"server.started",severity:"info",payload:{port:t.port}})},stop(){for(const e of c)try{e.ws.close(1001,"Server shutting down")}catch{}c.clear(),a?.close(),a=null}}}
@@ -0,0 +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}
@@ -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 o=[];for(let r=0;r<Math.min(t,e.length);r++)o.push(n());return await Promise.all(o),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.platformUrl)return errorResult("platformUrl not configured — file upload requires platform API access");if(!r)return errorResult("agent key pair not available — cannot sign upload requests");const n=`${t.platformUrl}/api/v1/agent/files`,o=a.paths,i=await validateFiles(o),s=i.filter(e=>!e.ok);if(s.length===o.length)return errorResult(`All files invalid: ${s.map(e=>`${e.path}: ${e.error}`).join("; ")}`);const l=i.filter(e=>e.ok).map(e=>e.path),c=s.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 f=u.map(e=>({uri:`beeos-file://${e.fileId}`,name:e.fileName,mimeType:e.mimeType,fileId:e.fileId})),m={ok:!0,files:f};return{...m,content:f.map(e=>({type:"resource_link",uri:e.uri,name:e.name,mimeType:e.mimeType})),output:m,result:m,details:m,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),o=await readFile(e),i=t+"/presign";try{return await uploadViaPresign(i,t,r,a,n,o)}catch{return await uploadViaMultipart(t,r,a,n,o)}}function bufferToArrayBuffer(e){const t=new ArrayBuffer(e.byteLength),r=new Uint8Array(t);for(let t=0;t<e.byteLength;t++)r[t]=e[t];return t}async function uploadViaPresign(e,t,r,a,n,o){const i=new AbortController,s=setTimeout(()=>i.abort(),12e4);"object"==typeof s&&"unref"in s&&s.unref();try{const s=await agentFetch(e,r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fileName:a,mimeType:n}),signal:i.signal});if(!s.ok){const e=await s.text().catch(()=>s.statusText);throw new Error(`presign ${s.status}: ${e}`)}const l=await s.json(),c=new Blob([bufferToArrayBuffer(o)],{type:n}),p=await fetch(l.uploadUrl,{method:"PUT",headers:{"Content-Type":n},body:c,signal:i.signal});if(!p.ok)throw new Error(`S3 PUT failed: ${p.status}`);const u=t+"/confirm",f=await agentFetch(u,r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fileId:l.fileId}),signal:i.signal});if(f.ok){const e=await f.json();return{fileId:e.fileId,downloadUrl:e.downloadUrl,fileName:e.fileName||a,mimeType:e.mimeType||n,size:e.size||o.byteLength}}return{fileId:l.fileId,downloadUrl:l.uploadUrl.split("?")[0]??l.uploadUrl,fileName:a,mimeType:n,size:o.byteLength}}finally{clearTimeout(s)}}async function uploadViaMultipart(e,t,r,a,n){const o=new FormData;o.append("file",new Blob([bufferToArrayBuffer(n)],{type:a}),r);const i=new AbortController,s=setTimeout(()=>i.abort(),12e4);let l;"object"==typeof s&&"unref"in s&&s.unref();try{l=await agentFetch(e,t,{method:"POST",body:o,signal:i.signal})}catch(e){if(e instanceof Error&&"AbortError"===e.name)throw new Error("upload timed out after 120000ms");throw e}finally{clearTimeout(s)}if(!l.ok){const e=await l.text().catch(()=>l.statusText);throw new Error(`HTTP ${l.status}: ${e}`)}return await l.json()}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"}}
@@ -0,0 +1 @@
1
+ export class MessageQueue{messages=[];waiters=[];_closed=!1;get closed(){return this._closed}push(s){if(this._closed)return;this.messages.push(s);const e=this.waiters;this.waiters=[],e.forEach(s=>s())}async read(s){return this._closed?null:s<this.messages.length?this.messages.slice(s):new Promise(e=>{this.waiters.push(()=>{this._closed?e(null):e(this.messages.slice(s))})})}close(){this._closed=!0;const s=this.waiters;this.waiters=[],s.forEach(s=>s())}}
@@ -0,0 +1 @@
1
+ import{appendFileSync,mkdirSync,renameSync,rmSync,statSync}from"node:fs";import{dirname}from"node:path";export const DEFAULT_MAX_BYTES=524288e3;export const DEFAULT_MAX_BACKUPS=1;function fileSize(t){try{return statSync(t).size}catch(t){if("ENOENT"===t?.code)return 0;throw t}}function rotate(t,r){if(r<=0)rmSync(t,{force:!0});else for(let e=r;e>=1;e--){const r=1===e?t:`${t}.${e-1}`,n=`${t}.${e}`;rmSync(n,{force:!0});try{statSync(r)}catch(t){if("ENOENT"===t?.code)continue;throw t}renameSync(r,n)}}export function createRollingJsonlWriter(t){const r=t.filePath.trim();if(!r)return()=>{};const e=t.maxBytes??524288e3,n=t.maxBackups??1;let o=!1;try{mkdirSync(dirname(r),{recursive:!0})}catch{}const c=1048576;let i=!1;return s=>{const a=JSON.stringify(s)+"\n",f=Buffer.byteLength(a,"utf8");if(f>c&&!i){i=!0;const e=t.contextLabel??r;t.onError?.(new Error(`[rolling-jsonl] Single entry exceeds 1048576 bytes (${f} bytes) in ${e}`))}try{fileSize(r)+f>e&&rotate(r,n),appendFileSync(r,a,"utf8")}catch(r){o||(o=!0,t.onError?.(r instanceof Error?r:new Error(String(r))))}}}
@@ -0,0 +1 @@
1
+ export function asTrimmedNonEmptyString(t){if("string"!=typeof t)return;const r=t.trim();return r.length>0?r:void 0}export function normalizeTransportText(t){if("string"!=typeof t)return;const r=t.trim();return r.length>0?r:void 0}export function isPlainRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}export function asRpcId(t){return"string"==typeof t||"number"==typeof t&&Number.isFinite(t)?t:void 0}
@@ -0,0 +1 @@
1
+ import{createHash}from"node:crypto";export function buildStructuredTraceFields(e){const t={};if(!e||"object"!=typeof e)return t;const s=e;t.method="string"==typeof s.method?s.method:void 0,"string"==typeof s.sessionId&&(t.sessionId=s.sessionId),"string"==typeof s.requestId&&(t.requestId=s.requestId),"string"==typeof s.terminalId&&(t.terminalId=s.terminalId);const n=s.params;if(n&&"object"==typeof n){t.sessionId||"string"!=typeof n.sessionId||(t.sessionId=n.sessionId),t.requestId||"string"!=typeof n.requestId||(t.requestId=n.requestId),t.terminalId||"string"!=typeof n.terminalId||(t.terminalId=n.terminalId);const e=n.update;e&&"object"==typeof e&&(t.sessionId||"string"!=typeof e.sessionId||(t.sessionId=e.sessionId))}const o=s.payload;return o&&"object"==typeof o&&(t.sessionId||"string"!=typeof o.sessionKey||(t.sessionId=o.sessionKey),t.requestId||"string"!=typeof o.runId||(t.requestId=o.runId)),t}export function summarizePayload(e,t=200){if(null==e)return"";const s="string"==typeof e?e:JSON.stringify(e);return s.length<=t?s:s.slice(0,t)+`... (${s.length} chars)`}export function sanitizeTerminalPayload(e,t="size_only"){if(!e||"object"!=typeof e)return e;const s=e;for(const e of["output","data","content"]){const n=s[e];if(!("string"!=typeof n||n.length<1024))switch(t){case"size_only":return{...s,[e]:`<${n.length} bytes>`};case"inline":return{...s,[e]:n.slice(0,512)+`... (${n.length} total)`};case"artifact":{const t=createHash("sha256").update(n).digest("hex").slice(0,16);return{...s,[e]:`<artifact:${t} ${n.length} bytes>`}}}}return e}
@@ -0,0 +1,116 @@
1
+ {
2
+ "id": "beeos-claw",
3
+ "name": "beeos-claw",
4
+ "description": "BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",
5
+ "version": "0.1.0",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": true,
9
+ "properties": {
10
+ "platformUrl": {
11
+ "type": "string",
12
+ "description": "Base URL of the BeeOS platform API (e.g. https://api.beeos.com)"
13
+ },
14
+ "bridge": {
15
+ "type": "object",
16
+ "description": "Bridge connection config (Phone-Home model). When url + keyFile are set, the agent connects outbound to the Bridge server instead of listening as a WS server.",
17
+ "additionalProperties": true,
18
+ "properties": {
19
+ "url": {
20
+ "type": "string",
21
+ "description": "Bridge server WebSocket URL (e.g. wss://bridge.beeos.ai). Also readable from BRIDGE_URL env var."
22
+ },
23
+ "keyFile": {
24
+ "type": "string",
25
+ "description": "Path to Ed25519 private key file for Bridge authentication. Also readable from BRIDGE_KEY_FILE env var."
26
+ },
27
+ "service": {
28
+ "type": "string",
29
+ "description": "Service type for Bridge registration (default: acp)",
30
+ "enum": ["acp", "screen", "desktop", "shell"]
31
+ },
32
+ "mode": {
33
+ "type": "string",
34
+ "description": "Bridge operational mode"
35
+ },
36
+ "shell": {
37
+ "type": "object",
38
+ "properties": {
39
+ "enabled": {
40
+ "type": "boolean",
41
+ "description": "Enable web-ssh / terminal support (default: true)"
42
+ }
43
+ }
44
+ },
45
+ "promptTimeoutMs": {
46
+ "type": "number",
47
+ "description": "Timeout for LLM prompt responses in milliseconds"
48
+ },
49
+ "historyPendingTimeoutMs": {
50
+ "type": "number",
51
+ "description": "Timeout for history replay pending state in milliseconds"
52
+ }
53
+ }
54
+ },
55
+ "gateway": {
56
+ "type": "object",
57
+ "properties": {
58
+ "url": {
59
+ "type": "string",
60
+ "description": "WebSocket URL of the OpenClaw Gateway (default: ws://127.0.0.1:18789)"
61
+ },
62
+ "token": {
63
+ "type": "string",
64
+ "description": "Gateway authentication token"
65
+ },
66
+ "protocol": {
67
+ "type": "integer",
68
+ "description": "Gateway protocol version (default: 3)"
69
+ },
70
+ "clientId": {
71
+ "type": "string",
72
+ "description": "Client ID for the Gateway connection"
73
+ },
74
+ "agentId": {
75
+ "type": "string",
76
+ "description": "Agent ID to use (default: main)"
77
+ }
78
+ }
79
+ },
80
+ "retry": {
81
+ "type": "object",
82
+ "properties": {
83
+ "baseMs": {
84
+ "type": "integer",
85
+ "description": "Base delay for exponential backoff reconnect (default: 1000)"
86
+ },
87
+ "maxMs": {
88
+ "type": "integer",
89
+ "description": "Maximum reconnect delay (default: 600000)"
90
+ },
91
+ "maxAttempts": {
92
+ "type": "integer",
93
+ "description": "Maximum reconnect attempts, 0 = unlimited (default: 0)"
94
+ }
95
+ }
96
+ },
97
+ "log": {
98
+ "type": "object",
99
+ "properties": {
100
+ "enabled": {
101
+ "type": "boolean",
102
+ "description": "Enable structured JSONL observability logs (default: true)"
103
+ },
104
+ "verbose": {
105
+ "type": "boolean",
106
+ "description": "Enable verbose debug logging (default: false)"
107
+ },
108
+ "dir": {
109
+ "type": "string",
110
+ "description": "Directory for log files (default: ~/.openclaw/beeos-logs)"
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "beeos-claw",
3
+ "version": "0.1.7",
4
+ "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
+ "license": "MIT",
6
+ "author": "BeeOS <dev@beeos.ai>",
7
+ "homepage": "https://beeos.ai",
8
+ "keywords": [
9
+ "beeos",
10
+ "openclaw",
11
+ "plugin",
12
+ "acp",
13
+ "agent",
14
+ "bridge"
15
+ ],
16
+ "type": "module",
17
+ "main": "dist/index.js",
18
+ "files": [
19
+ "dist/",
20
+ "openclaw.plugin.json",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "openclaw": {
25
+ "extensions": [
26
+ "./dist/index.js"
27
+ ]
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "scripts": {
33
+ "clean": "rm -rf dist",
34
+ "build": "npm run clean && tsc -p tsconfig.json",
35
+ "build:release": "npm run build && npm run minify",
36
+ "minify": "find dist -name '*.js' -exec terser {} --compress drop_console=true,drop_debugger=true --mangle --output {} \\; && find dist -name '*.d.ts' -delete && find dist -name '*.d.ts.map' -delete"
37
+ },
38
+ "dependencies": {
39
+ "@beeos-ai/bridge-client": "^0.1.0",
40
+ "ws": "^8.18.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "@types/ws": "^8.18.1",
45
+ "terser": "^5.39.0",
46
+ "typescript": "^5.6.3"
47
+ }
48
+ }