beeos-claw 0.5.25 → 0.5.26

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/dist/a2a/tools.js CHANGED
@@ -1 +1 @@
1
- import{agentFetch}from"../agent-auth.js";import{resolveAgentIdFromMentions}from"../delegation/mention-resolver.js";async function a2aFetch(t,e,a){const n=`${t.agentGatewayUrl}${e}`;return t.agentKeys?agentFetch(n,t.agentKeys,a):fetch(n,a)}export{a2aFetch};function normalizeState(t){if(!t)return"";const e=t.toLowerCase();return{task_state_submitted:"submitted",task_state_delivered:"submitted",task_state_working:"working",task_state_completed:"completed",task_state_failed:"failed",task_state_timeout:"timeout",task_state_canceled:"canceled",task_state_rejected:"rejected",task_state_input_required:"input-required",task_state_auth_required:"auth-required"}[e]??e}function unwrapTask(t){if(!t||"object"!=typeof t)return;const e=t,a=e.task;return a&&"object"==typeof a?a:e}function extractArtifactText(t){const e=t?.artifacts;if(!e)return;return e.flatMap(t=>t.parts??[]).filter(t=>"string"==typeof t.text&&(void 0===t.kind||"text"===t.kind)).map(t=>t.text).join("\n")||void 0}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function stringValue(t){return"string"==typeof t&&""!==t?t:void 0}function normalizeArtifactFilePart(t){const e=stringValue(t.kind),a=isRecord(t.file)?t.file:void 0;let n=stringValue(t.url),r=stringValue(t.filename),s=stringValue(t.mediaType);if(a&&(n=stringValue(a.uri)??stringValue(a.url)??n,r=stringValue(a.name)??stringValue(a.filename)??r,s=stringValue(a.mimeType)??stringValue(a.mediaType)??s),n&&(!e||"file"===e))return{uri:n,...r?{name:r}:{},...s?{mimeType:s}:{}}}function extractArtifactFiles(t){const e=t?.artifacts;if(!e)return;const a=[];for(const t of e){const e=t.parts;if(Array.isArray(e))for(const t of e){if(!isRecord(t))continue;const e=normalizeArtifactFilePart(t);e&&a.push(e)}}return a.length>0?a:void 0}function extractStatusText(t){const e=t?.status,a=e?.message?.parts,n=a?.[0]?.text;return"string"==typeof n?n:void 0}function taskState(t){return normalizeState(t?.status?.state)}const TERMINAL_FAILURE_STATES=new Set(["failed","timeout","canceled","rejected"]);function taskContextId(t){const e=t?.contextId;return"string"==typeof e&&""!==e?e:void 0}function buildTaskPayload(t,e,a){const n=taskState(t),r=extractArtifactText(t),s=extractArtifactFiles(t),i=extractStatusText(t),o=taskContextId(t),c={status:n,is_terminal:"completed"===n||TERMINAL_FAILURE_STATES.has(n)};return e&&(c.task_id=e),a&&(c.agent_id=a),o&&(c.context_id=o),r?c.result=r:i&&(c.error=i),s&&(c.files=s),c}export{unwrapTask,extractArtifactText,extractArtifactFiles,extractStatusText,taskState,taskContextId,normalizeState,buildTaskPayload};export class TaskHandleRegistry{seq=0;handles=new Map;taskIdToHandle=new Map;create(t,e,a,n){const r="bth_"+ ++this.seq;return this.handles.set(r,{taskId:t,agentId:e,contextId:a,sessionKey:n}),this.taskIdToHandle.set(t,r),r}resolveTaskId(t){const e=this.handles.get(t);return e?e.taskId:t}resolveAgentId(t){const e=this.handles.get(t);if(e)return e.agentId;const a=this.taskIdToHandle.get(t);return a?this.handles.get(a)?.agentId:void 0}}const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(t,e,a=defaultRegistry,n,r,s,i){t.registerTool({name:"beeos_discover_agents",description:"List agents available for A2A collaboration. When called without a query, returns all visible agents (your own agents + public agents). When called with a query, filters by name, description, or skill. Results include an `isSelf` flag marking the agent that represents the current instance (do not call yourself). Each result includes `instanceName` (human-readable device/instance label) when available.",parameters:{type:"object",properties:{query:{type:"string",description:"Optional keyword to filter agents by name, description, or skill. Omit to list all agents."}},required:[]},async execute(t,a,n){const{query:r}=a??{};try{const t=new URLSearchParams({limit:"50"});r&&t.set("query",r);const a=await a2aFetch(e,`/api/v1/a2a/discover?${t}`,{signal:n});if(!a.ok)return{content:JSON.stringify({error:`discover failed: ${a.status}`})};const s=await a.json(),o=i?.();if(o&&Array.isArray(s?.data))for(const t of s.data)t.isSelf=t.instance_id===o;return{content:JSON.stringify(s,null,2)}}catch(t){if(n?.aborted)throw t;return{content:JSON.stringify({error:t instanceof Error?t.message:String(t)})}}}}),t.registerTool({name:"beeos_call_agent",description:"Ask another agent to perform a task. This creates the task ASYNCHRONOUSLY and returns immediately with a handle ({ task_id, agent_id, context_id, status }) — it does NOT wait for the result. To fetch the agent's reply, call beeos_get_task with the returned task_id (and agent_id). Do NOT call beeos_call_agent again to poll an existing task — that creates a brand-new task.",parameters:{type:"object",properties:{agentId:{type:"string",description:"The target agent UUID (e.g. d04080d1-...) or display name. Prefer the UUID from discover results or @mention metadata when available."},message:{type:"string",description:"The task description or message to send to the agent"},taskId:{type:"string",description:"Optional existing task ID to continue a multi-turn conversation. When provided, the message is appended to the existing task instead of creating a new one."},files:{type:"array",items:{type:"string"},description:"Optional file URLs to include with the request"},contextId:{type:"string",description:"Optional context ID for multi-turn conversations with the same agent"},timeoutMs:{type:"number",description:"Server-side task timeout in milliseconds (how long the target agent may run before the task is timed out). Defaults to 600000 (10 minutes). This is NOT a client wait time — beeos_call_agent returns immediately regardless."}},required:["agentId","message"]},async execute(t,i,o){let{agentId:c,message:d,files:l,contextId:u,timeoutMs:g,taskId:f}=i;const h=n?.(t),p=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!p.test(c)){const a=r?.(t);if(a?.length){const t=resolveAgentIdFromMentions(a,c);t&&(e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${t} via mention context`),c=t)}if(!p.test(c))try{const t=await a2aFetch(e,`/api/v1/a2a/discover?query=${encodeURIComponent(c)}&limit=10`,{signal:o});if(t.ok){const a=await t.json(),n=a?.data??[],r=c.toLowerCase(),s=n.find(t=>t.name.toLowerCase()===r)??n.find(t=>t.name.toLowerCase().includes(r));if(!s)return{content:JSON.stringify({error:`Could not resolve agent name "${c}". Available agents: ${n.map(t=>`${t.name} (${t.id})`).join(", ")||"none"}`})};e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${s.id} (${s.name}) via discover`),c=s.id}}catch{}}const m=u||(h?`session:${h}`:""),k=g||6e5;try{const t={jsonrpc:"2.0",id:`claw-${Date.now()}`,method:"SendMessage",params:{message:{role:"user",parts:[{text:d}],...m?{contextId:m}:{},...f?{taskId:f}:{}},configuration:{timeoutMs:k}}},n=`/api/v1/a2a/${encodeURIComponent(c)}`,r=AbortSignal.timeout(3e4),i=o?AbortSignal.any([o,r]):r,l=await a2aFetch(e,n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),signal:i}),g=await l.text();let p;try{p=JSON.parse(g)}catch{p={}}if(!l.ok||p.error){if(401===l.status||403===l.status){const t=p.error??{};-32600===t.code&&s&&s("_beeos/auth_required",{type:"auth_required",target_agent_id:c,target_agent_name:c,title:"Authentication Required",content:`Agent **${c}** requires authentication.`})}const t=p.error?JSON.stringify(p.error):g;return{content:JSON.stringify({error:`send task failed: ${l.status} ${t}`,agent_id:c})}}const y=unwrapTask(p.result),_=y?.id;return _?(a.create(_,c,u,h),{content:JSON.stringify({status:taskState(y)||"working",task_id:_,agent_id:c,context_id:taskContextId(y)??m??void 0,message:`Task created. Call beeos_get_task with task_id "${_}" (and agent_id "${c}") to fetch status and result.`})}):{content:JSON.stringify({error:"task created but server returned no task_id",agent_id:c})}}catch(t){if(o?.aborted)throw t;return t instanceof DOMException&&"TimeoutError"===t.name?{content:JSON.stringify({error:"create task request timed out after 30000ms",agent_id:c})}:{content:JSON.stringify({error:t instanceof Error?t.message:String(t),agent_id:c})}}}}),t.registerTool({name:"beeos_get_task",description:'Fetch the current state AND data of an existing A2A task by its task_id. Use this to check on a task that beeos_call_agent created (it returns status "working" with a task_id), or to re-read a finished task. Always returns a JSON object { status, task_id, agent_id, context_id, is_terminal, result?, error? }: `result` holds the produced output (present whenever the device produced output, EVEN for timeout/canceled/failed tasks that still produced content); `error` holds the failure reason when there is no produced output; `is_terminal` indicates whether the task has reached a final state. Do NOT call beeos_call_agent again to re-query an existing task — that creates a brand-new task.',parameters:{type:"object",properties:{taskId:{type:"string",description:"The task ID returned by a previous beeos_call_agent call."},agentId:{type:"string",description:"The target agent UUID that owns the task (the same agentId passed to beeos_call_agent). Optional when the task was created earlier in this session — it is resolved automatically."}},required:["taskId"]},async execute(t,n,r){const{taskId:s,agentId:i}=n??{};if(!s)return{content:JSON.stringify({error:"taskId is required"})};const o=i||a.resolveAgentId(s);if(!o)return{content:JSON.stringify({error:`agentId is required to query task "${s}" (it was not created in this session, so the target agent is unknown). Pass the agentId you originally called.`,task_id:s})};try{const t=`/api/v1/a2a/${encodeURIComponent(o)}`,a=await a2aFetch(e,t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`claw-get-${Date.now()}`,method:"GetTask",params:{id:s}}),signal:r}),n=await a.text();let i;try{i=JSON.parse(n)}catch{i={}}if(!a.ok||i.error){const t=i.error?JSON.stringify(i.error):n;return{content:JSON.stringify({error:`get task failed: ${a.status} ${t}`,task_id:s,agent_id:o})}}const c=unwrapTask(i.result);return{content:JSON.stringify(buildTaskPayload(c,s,o))}}catch(t){if(r?.aborted)throw t;return{content:JSON.stringify({error:t instanceof Error?t.message:String(t),task_id:s,agent_id:o})}}}})}
1
+ import{agentFetch}from"../agent-auth.js";import{resolveAgentIdFromMentions}from"../delegation/mention-resolver.js";async function a2aFetch(t,e,a){const s=`${t.agentGatewayUrl}${e}`;return t.agentKeys?agentFetch(s,t.agentKeys,a):fetch(s,a)}export{a2aFetch};function normalizeState(t){if(!t)return"";const e=t.toLowerCase();return{task_state_submitted:"submitted",task_state_delivered:"submitted",task_state_working:"working",task_state_completed:"completed",task_state_failed:"failed",task_state_timeout:"timeout",task_state_canceled:"canceled",task_state_rejected:"rejected",task_state_input_required:"input-required",task_state_auth_required:"auth-required"}[e]??e}function unwrapTask(t){if(!t||"object"!=typeof t)return;const e=t,a=e.task;return a&&"object"==typeof a?a:e}function extractArtifactText(t){const e=t?.artifacts;if(!e)return;return e.flatMap(t=>t.parts??[]).filter(t=>"string"==typeof t.text&&(void 0===t.kind||"text"===t.kind)).map(t=>t.text).join("\n")||void 0}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function stringValue(t){return"string"==typeof t&&""!==t?t:void 0}function normalizeArtifactFilePart(t){const e=stringValue(t.kind),a=isRecord(t.file)?t.file:void 0;let s=stringValue(t.url),r=stringValue(t.filename),n=stringValue(t.mediaType);if(a&&(s=stringValue(a.uri)??stringValue(a.url)??s,r=stringValue(a.name)??stringValue(a.filename)??r,n=stringValue(a.mimeType)??stringValue(a.mediaType)??n),s&&(!e||"file"===e))return{uri:s,...r?{name:r}:{},...n?{mimeType:n}:{}}}function extractArtifactFiles(t){const e=t?.artifacts;if(!e)return;const a=[];for(const t of e){const e=t.parts;if(Array.isArray(e))for(const t of e){if(!isRecord(t))continue;const e=normalizeArtifactFilePart(t);e&&a.push(e)}}return a.length>0?a:void 0}function extractStatusText(t){const e=t?.status,a=e?.message?.parts,s=a?.[0]?.text;return"string"==typeof s?s:void 0}function taskState(t){return normalizeState(t?.status?.state)}const TERMINAL_FAILURE_STATES=new Set(["failed","timeout","canceled","rejected"]);function taskContextId(t){const e=t?.contextId;return"string"==typeof e&&""!==e?e:void 0}function buildTaskPayload(t,e,a){const s=taskState(t),r=extractArtifactText(t),n=extractArtifactFiles(t),i=extractStatusText(t),o=taskContextId(t),c={status:s,is_terminal:"completed"===s||TERMINAL_FAILURE_STATES.has(s)};return e&&(c.task_id=e),a&&(c.agent_id=a),o&&(c.context_id=o),r?c.result=r:i&&(c.error=i),n&&(c.files=n),c}export{unwrapTask,extractArtifactText,extractArtifactFiles,extractStatusText,taskState,taskContextId,normalizeState,buildTaskPayload};export class TaskHandleRegistry{seq=0;handles=new Map;taskIdToHandle=new Map;create(t,e,a,s){const r="bth_"+ ++this.seq;return this.handles.set(r,{taskId:t,agentId:e,contextId:a,sessionKey:s}),this.taskIdToHandle.set(t,r),r}resolveTaskId(t){const e=this.handles.get(t);return e?e.taskId:t}resolveAgentId(t){const e=this.handles.get(t);if(e)return e.agentId;const a=this.taskIdToHandle.get(t);return a?this.handles.get(a)?.agentId:void 0}}const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(t,e,a=defaultRegistry,s,r,n,i){t.registerTool({name:"beeos_discover_agents",description:"List agents available for A2A collaboration. When called without a query, returns all visible agents (your own agents + public agents). When called with a query, filters by name, description, or skill. Results include an `isSelf` flag marking the agent that represents the current instance (do not call yourself). Each result includes `instanceName` (human-readable device/instance label) when available.",parameters:{type:"object",properties:{query:{type:"string",description:"Optional keyword to filter agents by name, description, or skill. Omit to list all agents."}},required:[]},async execute(t,a,s){const{query:r}=a??{};try{const t=new URLSearchParams({limit:"50"});r&&t.set("query",r);const a=await a2aFetch(e,`/api/v1/a2a/discover?${t}`,{signal:s});if(!a.ok)return{content:JSON.stringify({error:`discover failed: ${a.status}`})};const n=await a.json(),o=i?.();if(o&&Array.isArray(n?.data))for(const t of n.data)t.isSelf=t.instance_id===o;return{content:JSON.stringify(n,null,2)}}catch(t){if(s?.aborted)throw t;return{content:JSON.stringify({error:t instanceof Error?t.message:String(t)})}}}}),t.registerTool({name:"beeos_call_agent",description:"Ask another agent to perform a task. This creates the task ASYNCHRONOUSLY and returns immediately with a handle ({ task_id, agent_id, context_id, status }) — it does NOT wait for the result. To fetch the agent's reply, call beeos_get_task with the returned task_id (and agent_id). Do NOT call beeos_call_agent again to poll an existing task — that creates a brand-new task.",parameters:{type:"object",properties:{agentId:{type:"string",description:"The target agent UUID (e.g. d04080d1-...) or display name. Prefer the UUID from discover results or @mention metadata when available."},message:{type:"string",description:"The task description or message to send to the agent"},taskId:{type:"string",description:"Optional existing task ID to continue a multi-turn conversation. When provided, the message is appended to the existing task instead of creating a new one."},files:{type:"array",items:{type:"string"},description:"Optional file URLs to include with the request"},contextId:{type:"string",description:"Optional context ID for multi-turn conversations with the same agent"},timeoutMs:{type:"number",description:"Server-side task timeout in milliseconds (how long the target agent may run before the task is timed out). Defaults to 600000 (10 minutes). This is NOT a client wait time — beeos_call_agent returns immediately regardless."}},required:["agentId","message"]},async execute(t,i,o){let{agentId:c,message:d,files:l,contextId:u,timeoutMs:g,taskId:f}=i;const h=s?.(t),p=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!p.test(c)){const a=r?.(t);if(a?.length){const t=resolveAgentIdFromMentions(a,c);t&&(e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${t} via mention context`),c=t)}if(!p.test(c))try{const t=await a2aFetch(e,`/api/v1/a2a/discover?query=${encodeURIComponent(c)}&limit=10`,{signal:o});if(t.ok){const a=await t.json(),s=a?.data??[],r=c.toLowerCase(),n=s.find(t=>t.name.toLowerCase()===r)??s.find(t=>t.name.toLowerCase().includes(r));if(!n)return{content:JSON.stringify({error:`Could not resolve agent name "${c}". Available agents: ${s.map(t=>`${t.name} (${t.id})`).join(", ")||"none"}`})};e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${n.id} (${n.name}) via discover`),c=n.id}}catch{}}const m=u||(h?`session:${h}`:""),y=g||6e5;try{const t={jsonrpc:"2.0",id:`claw-${Date.now()}`,method:"SendMessage",params:{message:{role:"user",parts:[{text:d}],...m?{contextId:m}:{},...f?{taskId:f}:{}},configuration:{timeoutMs:y}}},s=`/api/v1/a2a/${encodeURIComponent(c)}`,r=AbortSignal.timeout(3e4),i=o?AbortSignal.any([o,r]):r,l=await a2aFetch(e,s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),signal:i}),g=await l.text();let p;try{p=JSON.parse(g)}catch{p={}}if(!l.ok||p.error){if(401===l.status||403===l.status){const t=p.error??{};-32600===t.code&&n&&n("_beeos/auth_required",{type:"auth_required",target_agent_id:c,target_agent_name:c,title:"Authentication Required",content:`Agent **${c}** requires authentication.`})}const t=p.error?JSON.stringify(p.error):g;return{content:JSON.stringify({error:`send task failed: ${l.status} ${t}`,agent_id:c})}}const k=unwrapTask(p.result),_=k?.id;return _?(a.create(_,c,u,h),{content:JSON.stringify({status:taskState(k)||"working",task_id:_,agent_id:c,context_id:taskContextId(k)??m??void 0,message:`Task created. Call beeos_get_task with task_id "${_}" (and agent_id "${c}") to fetch status and result.`})}):{content:JSON.stringify({error:"task created but server returned no task_id",agent_id:c})}}catch(t){if(o?.aborted)throw t;return t instanceof DOMException&&"TimeoutError"===t.name?{content:JSON.stringify({error:"create task request timed out after 30000ms",agent_id:c})}:{content:JSON.stringify({error:t instanceof Error?t.message:String(t),agent_id:c})}}}}),t.registerTool({name:"beeos_get_task",description:'Fetch the current state AND data of an existing A2A task by its task_id. Use this to check on a task that beeos_call_agent created (it returns status "working" with a task_id), or to re-read a finished task. Always returns a JSON object { status, task_id, agent_id, context_id, is_terminal, result?, files?, error? }: `result` holds the produced output (present whenever the device produced output, EVEN for timeout/canceled/failed tasks that still produced content); `files` contains structured file artifacts as BeeOS platform resource URIs ({ uri, name, mimeType }). For image display, reference files[].uri directly in chat or Canvas; do not upload/fetch/browser/shell files[].uri for display. Only use beeos_fetch_file when local byte analysis is required. `error` holds the failure reason when there is no produced output; `is_terminal` indicates whether the task has reached a final state. Do NOT call beeos_call_agent again to re-query an existing task — that creates a brand-new task.',parameters:{type:"object",properties:{taskId:{type:"string",description:"The task ID returned by a previous beeos_call_agent call."},agentId:{type:"string",description:"The target agent UUID that owns the task (the same agentId passed to beeos_call_agent). Optional when the task was created earlier in this session — it is resolved automatically."}},required:["taskId"]},async execute(t,s,r){const{taskId:n,agentId:i}=s??{};if(!n)return{content:JSON.stringify({error:"taskId is required"})};const o=i||a.resolveAgentId(n);if(!o)return{content:JSON.stringify({error:`agentId is required to query task "${n}" (it was not created in this session, so the target agent is unknown). Pass the agentId you originally called.`,task_id:n})};try{const t=`/api/v1/a2a/${encodeURIComponent(o)}`,a=await a2aFetch(e,t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`claw-get-${Date.now()}`,method:"GetTask",params:{id:n}}),signal:r}),s=await a.text();let i;try{i=JSON.parse(s)}catch{i={}}if(!a.ok||i.error){const t=i.error?JSON.stringify(i.error):s;return{content:JSON.stringify({error:`get task failed: ${a.status} ${t}`,task_id:n,agent_id:o})}}const c=unwrapTask(i.result);return{content:JSON.stringify(buildTaskPayload(c,n,o))}}catch(t){if(r?.aborted)throw t;return{content:JSON.stringify({error:t instanceof Error?t.message:String(t),task_id:n,agent_id:o})}}}})}
@@ -1 +1 @@
1
- import{existsSync,mkdirSync,readdirSync,statSync,writeFileSync}from"node:fs";import{join,basename}from"node:path";const BEEOS_FILE_URI_PREFIX="beeos-file://",MAX_FILE_NAME_LENGTH=120,DOWNLOAD_TIMEOUT_MS=12e4,MAX_CONCURRENT_RESOLVES=3,OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),UPLOAD_DIR=join(OPENCLAW_HOME,"workspace","uploads"),RESOURCE_CACHE_DIR=join(OPENCLAW_HOME,"workspace","resource_cache");export function parseBeeosFileUri(e){if(!e.startsWith("beeos-file://"))return{};const t=e.slice(13).trim();return t?{fileId:t}:{error:"beeos-file uri requires file_id"}}export class BeeosFileResolver{agentGatewayUrl;agentFetch;writeObs;constructor(e){this.agentGatewayUrl=e.agentGatewayUrl,this.agentFetch=e.agentFetch,this.writeObs=e.writeObs}buildResolutionPlan(e){const t=[],a=new Map;for(let i=0;i<e.length;i++){const r=e[i];if("resource_link"!==r.type||!r.uri)continue;const o=parseBeeosFileUri(r.uri);o.fileId&&(t.push({blockIndex:i,uri:r.uri,fileId:o.fileId,name:r.title??r.name}),a.set(r.uri,o.fileId))}return{refs:t,fileIds:[...new Set(a.values())],fileIdByUri:a}}async resolveMetadataForPrompt(e){if(0===e.fileIds.length)return new Map;const t=new Map,a=e.fileIds.map(e=>async()=>{const a=await this.resolveOneFile(e);t.set(e,a)});await runWithConcurrency(a,3);const i=new Map;for(const[a,r]of e.fileIdByUri.entries()){const e=t.get(r);e&&i.set(a,e)}return i}async resolveOneFile(e){const t=Date.now(),a=this.findCachedFile(e);if(a)return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"cache_hit",severity:"debug",payload:{fileId:e,localPath:a.localPath}}),{status:"resolved",fileId:e,name:a.localFileName,mimeType:"application/octet-stream",localPath:a.localPath,localFileName:a.localFileName,localSizeBytes:a.localSizeBytes,localCacheHit:!0,downloadUrl:""};const i=`${this.agentGatewayUrl}/api/v1/agent/files/${encodeURIComponent(e)}/resolve`;this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"metadata_request",severity:"info",payload:{fileId:e,url:i}});const r=new AbortController,o=setTimeout(()=>r.abort(),12e4);"object"==typeof o&&"unref"in o&&o.unref();try{const a=await this.agentFetch(i,{method:"GET",signal:r.signal});if(!a.ok){const i=a.status>=500?"http_5xx":"http_4xx",r=a.status>=500||429===a.status;return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"metadata_failed",severity:"warn",payload:{fileId:e,httpStatus:a.status,code:i,durationMs:Date.now()-t}}),{status:"resolve_failed",fileId:e,code:i,message:`resolve request failed with status ${a.status}`,retriable:r,httpStatus:a.status}}let o;try{o=await a.json()}catch{return this.makeFailure(e,"invalid_response","resolve response is not valid json",!1)}const l=asString(o.fileName),s=asString(o.mimeType),n=asString(o.downloadUrl),c="number"==typeof o.size?o.size:void 0;if(!n)return this.makeFailure(e,"missing_download_url","resolve response missing downloadUrl",!1,l);this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"metadata_ok",severity:"debug",payload:{fileId:e,fileName:l,mimeType:s,size:c,durationMs:Date.now()-t}});const d=await this.ensureFileDownloaded({fileId:e,name:l||e,downloadUrl:n});return"resolve_failed"===d.status?d:{status:"resolved",fileId:e,name:l||d.localFileName,mimeType:s||"application/octet-stream",size:c,downloadUrl:n,localPath:d.localPath,localFileName:d.localFileName,localSizeBytes:d.localSizeBytes,localCacheHit:!1}}catch(t){const a=t instanceof Error&&"AbortError"===t.name,i=a?"timeout":"network_error",r=a?"resolve request timed out after 120000ms":`resolve request failed: ${t instanceof Error?t.message:String(t)}`;return this.makeFailure(e,i,r,!0)}finally{clearTimeout(o)}}findCachedFile(e){for(const t of[UPLOAD_DIR,RESOURCE_CACHE_DIR]){const a=this.findFileByPrefix(t,e);if(a)return a}}findFileByPrefix(e,t){if(!existsSync(e))return;const a=`${t}_`;let i;try{i=readdirSync(e).sort()}catch{return}for(const t of i){if(!t.startsWith(a))continue;const i=join(e,t);try{const e=statSync(i);if(e.isFile()&&e.size>0)return{localPath:i,localFileName:t,localSizeBytes:e.size}}catch{continue}}}async ensureFileDownloaded(e){const t=sanitizeFileName(e.name),a=`${e.fileId}_${t}`,i=join(RESOURCE_CACHE_DIR,a);try{mkdirSync(RESOURCE_CACHE_DIR,{recursive:!0})}catch(t){return this.makeFailure(e.fileId,"fs_error",`failed to create cache dir: ${String(t)}`,!1,e.name)}const r=this.findFileByPrefix(RESOURCE_CACHE_DIR,e.fileId);if(r)return{status:"resolved",fileId:e.fileId,name:e.name,mimeType:"application/octet-stream",downloadUrl:e.downloadUrl,localPath:r.localPath,localFileName:r.localFileName,localSizeBytes:r.localSizeBytes,localCacheHit:!0};const o=new AbortController,l=setTimeout(()=>o.abort(),12e4);"object"==typeof l&&"unref"in l&&l.unref();try{const t=await this.agentFetch(e.downloadUrl,{method:"GET",signal:o.signal});if(!t.ok){const a=t.status>=500?"http_5xx":"http_4xx";return this.makeFailure(e.fileId,a,`file download failed with status ${t.status}`,t.status>=500||429===t.status,e.name,t.status)}let r;try{r=Buffer.from(await t.arrayBuffer())}catch{return this.makeFailure(e.fileId,"network_error","failed to read download response body",!0,e.name)}if(0===r.byteLength)return this.makeFailure(e.fileId,"invalid_response","downloaded file is empty",!1,e.name);try{writeFileSync(i,r)}catch(t){return this.makeFailure(e.fileId,"fs_error",`failed to write file: ${String(t)}`,!1,e.name)}let l=r.byteLength;try{const e=statSync(i);e.isFile()&&e.size>0&&(l=e.size)}catch{}return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"download_ok",severity:"info",payload:{fileId:e.fileId,localPath:i,localSizeBytes:l}}),{status:"resolved",fileId:e.fileId,name:e.name,mimeType:"application/octet-stream",downloadUrl:e.downloadUrl,localPath:i,localFileName:a,localSizeBytes:l,localCacheHit:!1}}catch(t){const a=t instanceof Error&&"AbortError"===t.name,i=a?"timeout":"network_error",r=a?"file download timed out after 120000ms":`file download failed: ${t instanceof Error?t.message:String(t)}`;return this.makeFailure(e.fileId,i,r,!0,e.name)}finally{clearTimeout(l)}}makeFailure(e,t,a,i,r,o){return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"resolve_failed",severity:"warn",payload:{fileId:e,code:t,message:a,retriable:i,...void 0!==o?{httpStatus:o}:{}}}),{status:"resolve_failed",fileId:e,...r?{name:r}:{},code:t,message:a,retriable:i,...void 0!==o?{httpStatus:o}:{}}}}function sanitizeFileName(e){return basename(e).trim().replace(/[\u0000-\u001f\u007f]/g,"").replace(/[\\/]+/g,"_").replace(/[^\p{L}\p{N}._-]+/gu,"_").replace(/_+/g,"_").replace(/^\.+/,"").replace(/^_+|_+$/g,"").slice(0,120)||"file"}function asString(e){if("string"==typeof e&&e.trim())return e.trim()}async function runWithConcurrency(e,t){let a=0;async function i(){for(;a<e.length;){const t=a++;await e[t]()}}const r=[];for(let a=0;a<Math.min(t,e.length);a++)r.push(i());await Promise.all(r)}
1
+ import{existsSync,mkdirSync,readdirSync,statSync,writeFileSync}from"node:fs";import{join,basename}from"node:path";import{parseBeeosFileUri}from"../files/beeos-file-uri.js";const MAX_FILE_NAME_LENGTH=120,DOWNLOAD_TIMEOUT_MS=12e4,MAX_CONCURRENT_RESOLVES=3,OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),UPLOAD_DIR=join(OPENCLAW_HOME,"workspace","uploads"),RESOURCE_CACHE_DIR=join(OPENCLAW_HOME,"workspace","resource_cache");export{parseBeeosFileUri}from"../files/beeos-file-uri.js";export class BeeosFileResolver{agentGatewayUrl;agentFetch;writeObs;constructor(e){this.agentGatewayUrl=e.agentGatewayUrl,this.agentFetch=e.agentFetch,this.writeObs=e.writeObs}buildResolutionPlan(e){const t=[],a=new Map;for(let i=0;i<e.length;i++){const o=e[i];if("resource_link"!==o.type||!o.uri)continue;const r=parseBeeosFileUri(o.uri);r.fileId&&(t.push({blockIndex:i,uri:o.uri,fileId:r.fileId,name:o.title??o.name}),a.set(o.uri,r.fileId))}return{refs:t,fileIds:[...new Set(a.values())],fileIdByUri:a}}async resolveMetadataForPrompt(e){if(0===e.fileIds.length)return new Map;const t=new Map,a=e.fileIds.map(e=>async()=>{const a=await this.resolveOneFile(e);t.set(e,a)});await runWithConcurrency(a,3);const i=new Map;for(const[a,o]of e.fileIdByUri.entries()){const e=t.get(o);e&&i.set(a,e)}return i}async resolveOneFile(e){const t=Date.now(),a=this.findCachedFile(e);if(a)return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"cache_hit",severity:"debug",payload:{fileId:e,localPath:a.localPath}}),{status:"resolved",fileId:e,name:a.localFileName,mimeType:"application/octet-stream",localPath:a.localPath,localFileName:a.localFileName,localSizeBytes:a.localSizeBytes,localCacheHit:!0,downloadUrl:""};const i=`${this.agentGatewayUrl}/api/v1/agent/files/${encodeURIComponent(e)}/resolve`;this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"metadata_request",severity:"info",payload:{fileId:e,url:i}});const o=new AbortController,r=setTimeout(()=>o.abort(),12e4);"object"==typeof r&&"unref"in r&&r.unref();try{const a=await this.agentFetch(i,{method:"GET",signal:o.signal});if(!a.ok){const i=a.status>=500?"http_5xx":"http_4xx",o=a.status>=500||429===a.status;return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"metadata_failed",severity:"warn",payload:{fileId:e,httpStatus:a.status,code:i,durationMs:Date.now()-t}}),{status:"resolve_failed",fileId:e,code:i,message:`resolve request failed with status ${a.status}`,retriable:o,httpStatus:a.status}}let r;try{r=await a.json()}catch{return this.makeFailure(e,"invalid_response","resolve response is not valid json",!1)}const l=asString(r.fileName),s=asString(r.mimeType),n=asString(r.downloadUrl),c="number"==typeof r.size?r.size:void 0;if(!n)return this.makeFailure(e,"missing_download_url","resolve response missing downloadUrl",!1,l);this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"metadata_ok",severity:"debug",payload:{fileId:e,fileName:l,mimeType:s,size:c,durationMs:Date.now()-t}});const d=await this.ensureFileDownloaded({fileId:e,name:l||e,downloadUrl:n});return"resolve_failed"===d.status?d:{status:"resolved",fileId:e,name:l||d.localFileName,mimeType:s||"application/octet-stream",size:c,downloadUrl:n,localPath:d.localPath,localFileName:d.localFileName,localSizeBytes:d.localSizeBytes,localCacheHit:!1}}catch(t){const a=t instanceof Error&&"AbortError"===t.name,i=a?"timeout":"network_error",o=a?"resolve request timed out after 120000ms":`resolve request failed: ${t instanceof Error?t.message:String(t)}`;return this.makeFailure(e,i,o,!0)}finally{clearTimeout(r)}}findCachedFile(e){for(const t of[UPLOAD_DIR,RESOURCE_CACHE_DIR]){const a=this.findFileByPrefix(t,e);if(a)return a}}findFileByPrefix(e,t){if(!existsSync(e))return;const a=`${t}_`;let i;try{i=readdirSync(e).sort()}catch{return}for(const t of i){if(!t.startsWith(a))continue;const i=join(e,t);try{const e=statSync(i);if(e.isFile()&&e.size>0)return{localPath:i,localFileName:t,localSizeBytes:e.size}}catch{continue}}}async ensureFileDownloaded(e){const t=sanitizeFileName(e.name),a=`${e.fileId}_${t}`,i=join(RESOURCE_CACHE_DIR,a);try{mkdirSync(RESOURCE_CACHE_DIR,{recursive:!0})}catch(t){return this.makeFailure(e.fileId,"fs_error",`failed to create cache dir: ${String(t)}`,!1,e.name)}const o=this.findFileByPrefix(RESOURCE_CACHE_DIR,e.fileId);if(o)return{status:"resolved",fileId:e.fileId,name:e.name,mimeType:"application/octet-stream",downloadUrl:e.downloadUrl,localPath:o.localPath,localFileName:o.localFileName,localSizeBytes:o.localSizeBytes,localCacheHit:!0};const r=new AbortController,l=setTimeout(()=>r.abort(),12e4);"object"==typeof l&&"unref"in l&&l.unref();try{const t=await this.agentFetch(e.downloadUrl,{method:"GET",signal:r.signal});if(!t.ok){const a=t.status>=500?"http_5xx":"http_4xx";return this.makeFailure(e.fileId,a,`file download failed with status ${t.status}`,t.status>=500||429===t.status,e.name,t.status)}let o;try{o=Buffer.from(await t.arrayBuffer())}catch{return this.makeFailure(e.fileId,"network_error","failed to read download response body",!0,e.name)}if(0===o.byteLength)return this.makeFailure(e.fileId,"invalid_response","downloaded file is empty",!1,e.name);try{writeFileSync(i,o)}catch(t){return this.makeFailure(e.fileId,"fs_error",`failed to write file: ${String(t)}`,!1,e.name)}let l=o.byteLength;try{const e=statSync(i);e.isFile()&&e.size>0&&(l=e.size)}catch{}return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"download_ok",severity:"info",payload:{fileId:e.fileId,localPath:i,localSizeBytes:l}}),{status:"resolved",fileId:e.fileId,name:e.name,mimeType:"application/octet-stream",downloadUrl:e.downloadUrl,localPath:i,localFileName:a,localSizeBytes:l,localCacheHit:!1}}catch(t){const a=t instanceof Error&&"AbortError"===t.name,i=a?"timeout":"network_error",o=a?"file download timed out after 120000ms":`file download failed: ${t instanceof Error?t.message:String(t)}`;return this.makeFailure(e.fileId,i,o,!0,e.name)}finally{clearTimeout(l)}}makeFailure(e,t,a,i,o,r){return this.writeObs?.({component:"beeos-file-resolver",domain:"file",name:"resolve_failed",severity:"warn",payload:{fileId:e,code:t,message:a,retriable:i,...void 0!==r?{httpStatus:r}:{}}}),{status:"resolve_failed",fileId:e,...o?{name:o}:{},code:t,message:a,retriable:i,...void 0!==r?{httpStatus:r}:{}}}}function sanitizeFileName(e){return basename(e).trim().replace(/[\u0000-\u001f\u007f]/g,"").replace(/[\\/]+/g,"_").replace(/[^\p{L}\p{N}._-]+/gu,"_").replace(/_+/g,"_").replace(/^\.+/,"").replace(/^_+|_+$/g,"").slice(0,120)||"file"}function asString(e){if("string"==typeof e&&e.trim())return e.trim()}async function runWithConcurrency(e,t){let a=0;async function i(){for(;a<e.length;){const t=a++;await e[t]()}}const o=[];for(let a=0;a<Math.min(t,e.length);a++)o.push(i());await Promise.all(o)}
@@ -1 +1 @@
1
- const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render a Google A2UI v0.8 component tree to the Canvas panel. Output a flat array of ComponentInstance objects with id references. ALWAYS prefer this over the canvas tool for displaying structured UI.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","root","components"],properties:{id:{type:"string",description:"Surface ID (e.g. 'sales-report'). Reuse same id to update an existing surface."},root:{type:"string",description:"ID of the root component (must reference one of the components below)."},components:{type:"array",description:"Flat array of A2UI ComponentInstance objects. Each has a unique id and exactly one component type key.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:"Unique component ID referenced by parent via child/children."},component:{type:"object",description:"Exactly ONE key = component type (Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart). Value = that component's properties object."}}}},data:{type:"object",description:"Optional data model for two-way binding. Keys are paths (e.g. '/name'), values are defaults."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e));async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=r&&r!==t?t:void 0,p=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(p,s),i.info?.(`[canvas] Joined canvas=${p} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),p}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,isCanvasActive:o,getGatewaySessionKey:i}=e;let r=0;return async(s,l)=>{if(o){const e=i?.(s);if(!o(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,p=c.root,u="panel";let m=c.components;if("string"==typeof m)try{m=JSON.parse(m)}catch{}if(!(d&&p&&Array.isArray(m)&&m.length))return{success:!1,error:"id, root, and components (array) are required"};const h=normalizeComponents(m);if(!t.validateComponentCount(h))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const g=c.data;let y=e.getActiveSessionId?.(s);for(let t=0;!y&&t<3;t++)await delay(400),y=e.getActiveSessionId?.(s);if(!y&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}y=e.getActiveSessionId?.(s)}if(!y)return n.warn?.(`[canvas] No active session for toolCallId=${s} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(s),v=await ensureRelayConnected(e,y,s,f),x=t.has(d,v??void 0),b=Date.now(),C=t.getRelayClient();v&&C&&b-r>6e4&&C.createSnapshot(v,"Pre-agent edit").then(()=>{r=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),x?t.update(d,h,v,p,g):t.create(v??`_fallback_${Date.now()}`,d,u,h,{origin:{kind:"agent"},rootComponentId:p,dataModel:g});const S=h.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:x?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:h.length,componentTypes:S}}),n.debug?.(`[canvas] ${x?"Updated":"Created"} surface=${d} components=${h.length}`);const T={success:!0,surfaceId:d,presentation:u,isUpdate:x,activeSurfaces:v?t.getAllActive(v).map(e=>e.surfaceId):[d]};return g&&(T.dataModel=g),T}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI — Google A2UI v0.8\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.**\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n\nParameters:\n- `id`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- `root`: The ID of the top-level component (usually a Column).\n- `components`: Flat array of objects. **Each object MUST have `id` and `component`**:\n `{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}`\n The `component` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- `data` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same `id` updates existing surface; different `id` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: `{"literalString": "Hello"}`\n- Data binding: `{"path": "/fieldName"}` (reads from the `data` model)\n\nShorthand: `sv("text")` means `{"literalString": "text"}` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n`{Text: {text: sv("Title"), usageHint: "h2"}}`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}`\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n`{Icon: {name: sv("check")}}`\n\n**Video** — Embed a video.\n`{Video: {url: sv("https://youtube.com/...")}}`\n\n**AudioPlayer** — Audio playback.\n`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}`\n\n**Row** — Horizontal layout. Children referenced by ID.\n`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n`{Column: {children: {explicitList: ["child1", "child2"]}}}`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}`\n\n**Card** — Container with a single child.\n`{Card: {child: "contentColumnId"}}`\n\n**Tabs** — Tabbed container.\n`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}`\n\n**Divider** — Horizontal or vertical separator.\n`{Divider: {axis: "horizontal"}}`\n\n**Modal** — Dialog with trigger.\n`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}`\n\n**Button** — Clickable with action.\n`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}`\n\n**TextField** — Text input with data binding.\n`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}`\n\n**DateTimeInput** — Date/time picker.\n`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}`\n\n**MultipleChoice** — Selection from options.\n`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}`\n\n**Slider** — Numeric range input.\n`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}`\n\n**Chart** (BeeOS extension) — Data visualization.\n`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition Patterns\n\n**Simple text display:**\n```json\n{"id":"s1","root":"r","components":[\n {"id":"t","component":{"Text":{"text":{"literalString":"Hello World"},"usageHint":"h2"}}},\n {"id":"r","component":{"Column":{"children":{"explicitList":["t"]}}}}\n]}\n```\n\n**Dashboard with KPI cards:**\n```json\n{"id":"dashboard","root":"root","components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Dashboard"},"usageHint":"h2"}}},\n {"id":"rev-val","component":{"Text":{"text":{"literalString":"$1.2M"},"usageHint":"h2"}}},\n {"id":"rev-lbl","component":{"Text":{"text":{"literalString":"Revenue"},"usageHint":"caption"}}},\n {"id":"rev-col","component":{"Column":{"children":{"explicitList":["rev-lbl","rev-val"]},"alignment":"center"}}},\n {"id":"rev-card","component":{"Card":{"child":"rev-col"}}},\n {"id":"users-val","component":{"Text":{"text":{"literalString":"8,432"},"usageHint":"h2"}}},\n {"id":"users-lbl","component":{"Text":{"text":{"literalString":"Users"},"usageHint":"caption"}}},\n {"id":"users-col","component":{"Column":{"children":{"explicitList":["users-lbl","users-val"]},"alignment":"center"}}},\n {"id":"users-card","component":{"Card":{"child":"users-col"}}},\n {"id":"kpi-row","component":{"Row":{"children":{"explicitList":["rev-card","users-card"]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","kpi-row"]}}}}\n]}\n```\n\n**Table layout (Column + Row + Text):**\n```json\n{"id":"users-table","root":"root","components":[\n {"id":"h-name","component":{"Text":{"text":{"literalString":"Name"},"usageHint":"caption"}}},\n {"id":"h-role","component":{"Text":{"text":{"literalString":"Role"},"usageHint":"caption"}}},\n {"id":"header","component":{"Row":{"children":{"explicitList":["h-name","h-role"]}}}},\n {"id":"div","component":{"Divider":{"axis":"horizontal"}}},\n {"id":"r1-name","component":{"Text":{"text":{"literalString":"Alice"},"usageHint":"body"}}},\n {"id":"r1-role","component":{"Text":{"text":{"literalString":"Engineer"},"usageHint":"body"}}},\n {"id":"row1","component":{"Row":{"children":{"explicitList":["r1-name","r1-role"]}}}},\n {"id":"r2-name","component":{"Text":{"text":{"literalString":"Bob"},"usageHint":"body"}}},\n {"id":"r2-role","component":{"Text":{"text":{"literalString":"Designer"},"usageHint":"body"}}},\n {"id":"row2","component":{"Row":{"children":{"explicitList":["r2-name","r2-role"]}}}},\n {"id":"table-col","component":{"Column":{"children":{"explicitList":["header","div","row1","row2"]}}}},\n {"id":"root","component":{"Card":{"child":"table-col"}}}\n]}\n```\n\n**Form with data binding:**\n```json\n{"id":"settings","root":"root","data":{"/name":"","/email":"","/notify":false},"components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Settings"},"usageHint":"h3"}}},\n {"id":"f-name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"},"type":"shortText"}}},\n {"id":"f-email","component":{"TextField":{"label":{"literalString":"Email"},"text":{"path":"/email"},"type":"shortText"}}},\n {"id":"f-notify","component":{"CheckBox":{"label":{"literalString":"Email notifications"},"value":{"path":"/notify"}}}},\n {"id":"btn-text","component":{"Text":{"text":{"literalString":"Save"},"usageHint":"body"}}},\n {"id":"btn","component":{"Button":{"child":"btn-text","action":{"name":"save_settings","context":[{"key":"name","value":{"path":"/name"}},{"key":"email","value":{"path":"/email"}},{"key":"notify","value":{"path":"/notify"}}]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","f-name","f-email","f-notify","btn"]}}}}\n]}\n```\n\n**Chart:**\n```json\n{"id":"sales-chart","root":"root","components":[\n {"id":"chart","component":{"Chart":{"chartType":{"literalString":"bar"},"title":{"literalString":"Monthly Sales"},"labels":["Jan","Feb","Mar"],"datasets":[{"label":"Revenue","data":[100,200,150]}]}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["chart"]}}}}\n]}\n```\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING='[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content. Each component in the array MUST be: {"id":"x","component":{"TypeName":{props}}} — the "component" wrapper key is required. render_ui uses Google A2UI v0.8 format: flat component array with id references. Components: Text (usageHint: h1-h5/caption/body), Image, Icon, Video, AudioPlayer, Row (horizontal layout, children.explicitList), Column (vertical layout, children.explicitList), List, Card (single child), Tabs (tabItems), Divider, Modal, Button (child + action), TextField, CheckBox, MultipleChoice, Slider, DateTimeInput, Chart (BeeOS extension: chartType, labels, datasets). StringValue: {"literalString":"text"} or {"path":"/binding"}. root param = top-level component ID. Tables: Card > Column > Row > Text. Forms: TextField/CheckBox/Slider + Button with action.context paths. Chart: chartType(bar/line/pie/area/scatter/doughnut/radar/funnel/gauge/candlestick/sparkline), labels, datasets:[{label,data,color?}]. Candlestick: [{open,high,low,close}]. Gauge: value+max. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.';export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
1
+ const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render a Google A2UI v0.8 component tree to the Canvas panel. Output a flat array of ComponentInstance objects with id references. ALWAYS prefer this over the canvas tool for displaying structured UI.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","root","components"],properties:{id:{type:"string",description:"Surface ID (e.g. 'sales-report'). Reuse same id to update an existing surface."},root:{type:"string",description:"ID of the root component (must reference one of the components below)."},components:{type:"array",description:"Flat array of A2UI ComponentInstance objects. Each has a unique id and exactly one component type key.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:"Unique component ID referenced by parent via child/children."},component:{type:"object",description:"Exactly ONE key = component type (Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart). Value = that component's properties object."}}}},data:{type:"object",description:"Optional data model for two-way binding. Keys are paths (e.g. '/name'), values are defaults."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e));async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=r&&r!==t?t:void 0,p=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(p,s),i.info?.(`[canvas] Joined canvas=${p} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),p}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,isCanvasActive:o,getGatewaySessionKey:i}=e;let r=0;return async(s,l)=>{if(o){const e=i?.(s);if(!o(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,p=c.root,u="panel";let m=c.components;if("string"==typeof m)try{m=JSON.parse(m)}catch{}if(!(d&&p&&Array.isArray(m)&&m.length))return{success:!1,error:"id, root, and components (array) are required"};const h=normalizeComponents(m);if(!t.validateComponentCount(h))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const g=c.data;let y=e.getActiveSessionId?.(s);for(let t=0;!y&&t<3;t++)await delay(400),y=e.getActiveSessionId?.(s);if(!y&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}y=e.getActiveSessionId?.(s)}if(!y)return n.warn?.(`[canvas] No active session for toolCallId=${s} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(s),v=await ensureRelayConnected(e,y,s,f),x=t.has(d,v??void 0),b=Date.now(),C=t.getRelayClient();v&&C&&b-r>6e4&&C.createSnapshot(v,"Pre-agent edit").then(()=>{r=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),x?t.update(d,h,v,p,g):t.create(v??`_fallback_${Date.now()}`,d,u,h,{origin:{kind:"agent"},rootComponentId:p,dataModel:g});const S=h.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:x?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:h.length,componentTypes:S}}),n.debug?.(`[canvas] ${x?"Updated":"Created"} surface=${d} components=${h.length}`);const T={success:!0,surfaceId:d,presentation:u,isUpdate:x,activeSurfaces:v?t.getAllActive(v).map(e=>e.surfaceId):[d]};return g&&(T.dataModel=g),T}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI — Google A2UI v0.8\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.**\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n\nParameters:\n- `id`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- `root`: The ID of the top-level component (usually a Column).\n- `components`: Flat array of objects. **Each object MUST have `id` and `component`**:\n `{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}`\n The `component` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- `data` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same `id` updates existing surface; different `id` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: `{"literalString": "Hello"}`\n- Data binding: `{"path": "/fieldName"}` (reads from the `data` model)\n\nShorthand: `sv("text")` means `{"literalString": "text"}` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n`{Text: {text: sv("Title"), usageHint: "h2"}}`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}`\nurl may be an http(s), data/blob, or BeeOS file URI such as `beeos-file://<fileId>`; the renderer resolves BeeOS file URIs when the viewing surface has permission.\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n`{Icon: {name: sv("check")}}`\n\n**Video** — Embed a video.\n`{Video: {url: sv("https://youtube.com/...")}}`\n\n**AudioPlayer** — Audio playback.\n`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}`\n\n**Row** — Horizontal layout. Children referenced by ID.\n`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n`{Column: {children: {explicitList: ["child1", "child2"]}}}`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}`\n\n**Card** — Container with a single child.\n`{Card: {child: "contentColumnId"}}`\n\n**Tabs** — Tabbed container.\n`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}`\n\n**Divider** — Horizontal or vertical separator.\n`{Divider: {axis: "horizontal"}}`\n\n**Modal** — Dialog with trigger.\n`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}`\n\n**Button** — Clickable with action.\n`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}`\n\n**TextField** — Text input with data binding.\n`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}`\n\n**DateTimeInput** — Date/time picker.\n`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}`\n\n**MultipleChoice** — Selection from options.\n`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}`\n\n**Slider** — Numeric range input.\n`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}`\n\n**Chart** (BeeOS extension) — Data visualization.\n`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition Patterns\n\n**Simple text display:**\n```json\n{"id":"s1","root":"r","components":[\n {"id":"t","component":{"Text":{"text":{"literalString":"Hello World"},"usageHint":"h2"}}},\n {"id":"r","component":{"Column":{"children":{"explicitList":["t"]}}}}\n]}\n```\n\n**Dashboard with KPI cards:**\n```json\n{"id":"dashboard","root":"root","components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Dashboard"},"usageHint":"h2"}}},\n {"id":"rev-val","component":{"Text":{"text":{"literalString":"$1.2M"},"usageHint":"h2"}}},\n {"id":"rev-lbl","component":{"Text":{"text":{"literalString":"Revenue"},"usageHint":"caption"}}},\n {"id":"rev-col","component":{"Column":{"children":{"explicitList":["rev-lbl","rev-val"]},"alignment":"center"}}},\n {"id":"rev-card","component":{"Card":{"child":"rev-col"}}},\n {"id":"users-val","component":{"Text":{"text":{"literalString":"8,432"},"usageHint":"h2"}}},\n {"id":"users-lbl","component":{"Text":{"text":{"literalString":"Users"},"usageHint":"caption"}}},\n {"id":"users-col","component":{"Column":{"children":{"explicitList":["users-lbl","users-val"]},"alignment":"center"}}},\n {"id":"users-card","component":{"Card":{"child":"users-col"}}},\n {"id":"kpi-row","component":{"Row":{"children":{"explicitList":["rev-card","users-card"]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","kpi-row"]}}}}\n]}\n```\n\n**Table layout (Column + Row + Text):**\n```json\n{"id":"users-table","root":"root","components":[\n {"id":"h-name","component":{"Text":{"text":{"literalString":"Name"},"usageHint":"caption"}}},\n {"id":"h-role","component":{"Text":{"text":{"literalString":"Role"},"usageHint":"caption"}}},\n {"id":"header","component":{"Row":{"children":{"explicitList":["h-name","h-role"]}}}},\n {"id":"div","component":{"Divider":{"axis":"horizontal"}}},\n {"id":"r1-name","component":{"Text":{"text":{"literalString":"Alice"},"usageHint":"body"}}},\n {"id":"r1-role","component":{"Text":{"text":{"literalString":"Engineer"},"usageHint":"body"}}},\n {"id":"row1","component":{"Row":{"children":{"explicitList":["r1-name","r1-role"]}}}},\n {"id":"r2-name","component":{"Text":{"text":{"literalString":"Bob"},"usageHint":"body"}}},\n {"id":"r2-role","component":{"Text":{"text":{"literalString":"Designer"},"usageHint":"body"}}},\n {"id":"row2","component":{"Row":{"children":{"explicitList":["r2-name","r2-role"]}}}},\n {"id":"table-col","component":{"Column":{"children":{"explicitList":["header","div","row1","row2"]}}}},\n {"id":"root","component":{"Card":{"child":"table-col"}}}\n]}\n```\n\n**Form with data binding:**\n```json\n{"id":"settings","root":"root","data":{"/name":"","/email":"","/notify":false},"components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Settings"},"usageHint":"h3"}}},\n {"id":"f-name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"},"type":"shortText"}}},\n {"id":"f-email","component":{"TextField":{"label":{"literalString":"Email"},"text":{"path":"/email"},"type":"shortText"}}},\n {"id":"f-notify","component":{"CheckBox":{"label":{"literalString":"Email notifications"},"value":{"path":"/notify"}}}},\n {"id":"btn-text","component":{"Text":{"text":{"literalString":"Save"},"usageHint":"body"}}},\n {"id":"btn","component":{"Button":{"child":"btn-text","action":{"name":"save_settings","context":[{"key":"name","value":{"path":"/name"}},{"key":"email","value":{"path":"/email"}},{"key":"notify","value":{"path":"/notify"}}]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","f-name","f-email","f-notify","btn"]}}}}\n]}\n```\n\n**Chart:**\n```json\n{"id":"sales-chart","root":"root","components":[\n {"id":"chart","component":{"Chart":{"chartType":{"literalString":"bar"},"title":{"literalString":"Monthly Sales"},"labels":["Jan","Feb","Mar"],"datasets":[{"label":"Revenue","data":[100,200,150]}]}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["chart"]}}}}\n]}\n```\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING='[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content. Each component in the array MUST be: {"id":"x","component":{"TypeName":{props}}} — the "component" wrapper key is required. render_ui uses Google A2UI v0.8 format: flat component array with id references. Components: Text (usageHint: h1-h5/caption/body), Image, Icon, Video, AudioPlayer, Row (horizontal layout, children.explicitList), Column (vertical layout, children.explicitList), List, Card (single child), Tabs (tabItems), Divider, Modal, Button (child + action), TextField, CheckBox, MultipleChoice, Slider, DateTimeInput, Chart (BeeOS extension: chartType, labels, datasets). StringValue: {"literalString":"text"} or {"path":"/binding"}. root param = top-level component ID. Tables: Card > Column > Row > Text. Forms: TextField/CheckBox/Slider + Button with action.context paths. Chart: chartType(bar/line/pie/area/scatter/doughnut/radar/funnel/gauge/candlestick/sparkline), labels, datasets:[{label,data,color?}]. Candlestick: [{open,high,low,close}]. Gauge: value+max. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.';export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
@@ -0,0 +1 @@
1
+ export const BEEOS_FILE_URI_PREFIX="beeos-file://";export const BEEOS_FILE_RESOURCE_ROUTING_RULE="Treat beeos-file://... as a displayable BeeOS resource ID, not a URL/path; fetch only for local byte analysis.";export function isBeeosFileUri(e){return e.startsWith("beeos-file://")}export function parseBeeosFileUri(e){if(!isBeeosFileUri(e))return{};const r=e.slice(13).trim();return r?{fileId:r}:{error:"beeos-file uri requires file_id"}}
@@ -0,0 +1 @@
1
+ import{describe,expect,it}from"vitest";import{BEEOS_FILE_RESOURCE_ROUTING_RULE,BEEOS_FILE_URI_PREFIX,isBeeosFileUri,parseBeeosFileUri}from"./beeos-file-uri.js";describe("BeeOS file URI helpers",()=>{it("recognizes BeeOS file URIs",()=>{expect(isBeeosFileUri("beeos-file://abc")).toBe(!0),expect(isBeeosFileUri("https://example.com/file")).toBe(!1)}),it("extracts the raw fileId from BeeOS file URIs",()=>{expect(parseBeeosFileUri("beeos-file://abc-123")).toEqual({fileId:"abc-123"})}),it("does not treat other URLs as BeeOS file URIs",()=>{expect(parseBeeosFileUri("https://example.com/file")).toEqual({})}),it("reports missing fileId",()=>{expect(parseBeeosFileUri(BEEOS_FILE_URI_PREFIX)).toEqual({error:"beeos-file uri requires file_id"})}),it("keeps the global routing rule short",()=>{expect(BEEOS_FILE_RESOURCE_ROUTING_RULE).toContain("not a URL/path"),expect(BEEOS_FILE_RESOURCE_ROUTING_RULE.length).toBeLessThan(140)})});
@@ -1 +1 @@
1
- import{mkdir,writeFile}from"node:fs/promises";import{basename,dirname,isAbsolute,resolve as resolvePathFs}from"node:path";import{agentFetch}from"../agent-auth.js";export const BEEOS_FETCH_FILE_TOOL_NAME="beeos_fetch_file";export const BEEOS_FETCH_FILE_TOOL_DESCRIPTION="Download a file from the user's BeeOS account to the local workspace by its fileId (discover fileIds with `beeos_list_files`). Returns the local path where the file was saved so you can then read or process it. Any file type is supported.";export const BEEOS_FETCH_FILE_TOOL_SCHEMA={title:"BeeOS fetch account file",type:"object",description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,additionalProperties:!1,required:["fileId"],properties:{fileId:{type:"string",minLength:1,description:"The fileId of the account file to download."},destPath:{type:"string",description:"Optional destination path (absolute or workspace-relative). Defaults to the original file name in the current working directory."}}};const REQUEST_TIMEOUT_MS=12e4;export async function executeFetchFile(e,t,r,o){const s=parseParams(e);if(!s.ok)return errorResult(s.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — fetching account files requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign fetch requests");const a=AbortSignal.timeout(12e4),i=o?.signal?AbortSignal.any([o.signal,a]):a,n=`${t.agentGatewayUrl}/api/v1/agent/files/${encodeURIComponent(s.fileId)}/resolve`;let l;try{l=await agentFetch(n,r,{method:"GET",signal:i})}catch(e){return errorResult(`resolve request failed: ${e instanceof Error?e.message:String(e)}`)}if(!l.ok){const e=await l.text().catch(()=>l.statusText);return errorResult(`resolve ${l.status}: ${e}`)}const c=await l.json();if(!c.downloadUrl)return errorResult("resolve returned no download URL");let d;try{d=await fetch(c.downloadUrl,{method:"GET",signal:i})}catch(e){return errorResult(`download failed: ${e instanceof Error?e.message:String(e)}`)}if(!d.ok)return errorResult(`download failed: ${d.status}`);const f=Buffer.from(await d.arrayBuffer()),u=resolveDestPath(s.destPath,c.fileName||s.fileId,o?.resolvePath);try{await mkdir(dirname(u),{recursive:!0}),await writeFile(u,f)}catch(e){return errorResult(`write failed: ${e instanceof Error?e.message:String(e)}`)}const h={ok:!0,path:u,fileName:c.fileName,mimeType:c.mimeType,size:f.byteLength};return{...h,content:[{type:"text",text:`Downloaded ${c.fileName} to ${u} (${f.byteLength}B)`}],output:h,result:h,details:h,isError:!1}}function resolveDestPath(e,t,r){if(e)return isAbsolute(e)?e:r?r(e):resolvePathFs(process.cwd(),e);const o=basename(t)||"download";return r?r(o):resolvePathFs(process.cwd(),o)}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e,r=new Set(["fileId","destPath"]),o=Object.keys(t).filter(e=>!r.has(e));if(o.length>0)return{ok:!1,error:`unexpected properties: ${o.join(", ")}`};if("string"!=typeof t.fileId||""===t.fileId.trim())return{ok:!1,error:"fileId must be a non-empty string"};let s;if(void 0!==t.destPath){if("string"!=typeof t.destPath||""===t.destPath.trim())return{ok:!1,error:"destPath must be a non-empty string"};s=t.destPath.trim()}return{ok:!0,fileId:t.fileId.trim(),destPath:s}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
1
+ import{mkdir,writeFile}from"node:fs/promises";import{basename,dirname,isAbsolute,resolve as resolvePathFs}from"node:path";import{agentFetch}from"../agent-auth.js";import{isBeeosFileUri,parseBeeosFileUri}from"./beeos-file-uri.js";export const BEEOS_FETCH_FILE_TOOL_NAME="beeos_fetch_file";export const BEEOS_FETCH_FILE_TOOL_DESCRIPTION="Download a file from the user's BeeOS account to the local workspace by its raw fileId (discover fileIds with `beeos_list_files`). Use only when local byte analysis is required; for display, keep the beeos-file:// URI in chat or Canvas. Returns the local path where the file was saved so you can then read or process it. Any file type is supported.";export const BEEOS_FETCH_FILE_TOOL_SCHEMA={title:"BeeOS fetch account file",type:"object",description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,additionalProperties:!1,required:["fileId"],properties:{fileId:{type:"string",minLength:1,description:"The raw fileId of the account file to download, not a beeos-file:// URI."},destPath:{type:"string",description:"Optional destination path (absolute or workspace-relative). Defaults to the original file name in the current working directory."}}};const REQUEST_TIMEOUT_MS=12e4;export async function executeFetchFile(e,t,r,o){const s=parseParams(e);if(!s.ok)return errorResult(s.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — fetching account files requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign fetch requests");const i=AbortSignal.timeout(12e4),a=o?.signal?AbortSignal.any([o.signal,i]):i,n=`${t.agentGatewayUrl}/api/v1/agent/files/${encodeURIComponent(s.fileId)}/resolve`;let l;try{l=await agentFetch(n,r,{method:"GET",signal:a})}catch(e){return errorResult(`resolve request failed: ${e instanceof Error?e.message:String(e)}`)}if(!l.ok){const e=await l.text().catch(()=>l.statusText);return errorResult(`resolve ${l.status}: ${e}`)}const f=await l.json();if(!f.downloadUrl)return errorResult("resolve returned no download URL");let d;try{d=await fetch(f.downloadUrl,{method:"GET",signal:a})}catch(e){return errorResult(`download failed: ${e instanceof Error?e.message:String(e)}`)}if(!d.ok)return errorResult(`download failed: ${d.status}`);const c=Buffer.from(await d.arrayBuffer()),u=resolveDestPath(s.destPath,f.fileName||s.fileId,o?.resolvePath);try{await mkdir(dirname(u),{recursive:!0}),await writeFile(u,c)}catch(e){return errorResult(`write failed: ${e instanceof Error?e.message:String(e)}`)}const h={ok:!0,path:u,fileName:f.fileName,mimeType:f.mimeType,size:c.byteLength};return{...h,content:[{type:"text",text:`Downloaded ${f.fileName} to ${u} (${c.byteLength}B)`}],output:h,result:h,details:h,isError:!1}}function resolveDestPath(e,t,r){if(e)return isAbsolute(e)?e:r?r(e):resolvePathFs(process.cwd(),e);const o=basename(t)||"download";return r?r(o):resolvePathFs(process.cwd(),o)}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e,r=new Set(["fileId","destPath"]),o=Object.keys(t).filter(e=>!r.has(e));if(o.length>0)return{ok:!1,error:`unexpected properties: ${o.join(", ")}`};if("string"!=typeof t.fileId||""===t.fileId.trim())return{ok:!1,error:"fileId must be a non-empty string"};const s=t.fileId.trim();if(isBeeosFileUri(s)){const e=parseBeeosFileUri(s);return{ok:!1,error:`fileId must be a raw BeeOS fileId, not a beeos-file:// URI.${e.fileId?` Use raw fileId "${e.fileId}" only when local byte analysis is required.`:" Use a raw fileId only when local byte analysis is required."}`}}let i;if(void 0!==t.destPath){if("string"!=typeof t.destPath||""===t.destPath.trim())return{ok:!1,error:"destPath must be a non-empty string"};i=t.destPath.trim()}return{ok:!0,fileId:s,destPath:i}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
@@ -0,0 +1 @@
1
+ import{describe,expect,it}from"vitest";import{BEEOS_FETCH_FILE_TOOL_DESCRIPTION,BEEOS_FETCH_FILE_TOOL_SCHEMA,executeFetchFile}from"./fetch-tool.js";function makeConfig(e=""){return{agentGatewayUrl:e}}describe("executeFetchFile",()=>{it("rejects full beeos-file URIs and points at the raw fileId",async()=>{const e=await executeFetchFile({fileId:"beeos-file://abc"},makeConfig(),null);expect(e).toMatchObject({ok:!1,isError:!0,error:'fileId must be a raw BeeOS fileId, not a beeos-file:// URI. Use raw fileId "abc" only when local byte analysis is required.'})}),it("keeps raw fileIds on the normal fetch validation path",async()=>{const e=await executeFetchFile({fileId:"abc"},makeConfig(),null);expect(e).toMatchObject({ok:!1,error:"agentGatewayUrl not configured — fetching account files requires Agent Gateway access"})}),it("documents raw fileId usage and local byte-analysis intent",()=>{expect(BEEOS_FETCH_FILE_TOOL_DESCRIPTION).toContain("raw fileId"),expect(BEEOS_FETCH_FILE_TOOL_DESCRIPTION).toContain("local byte analysis"),expect(BEEOS_FETCH_FILE_TOOL_SCHEMA.properties.fileId.description).toContain("not a beeos-file:// URI")})});
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{hasRuntimeReplyApi}from"./runtime/reply-router.js";import{PromptRunStore}from"./runtime/prompt-run-store.js";import{createImInboundHandler}from"./im-inbound-handler.js";import{MessageServiceRecoveryScanner}from"./im-recovery-scanner.js";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{BEEOS_LIST_FILES_TOOL_NAME,BEEOS_LIST_FILES_TOOL_SCHEMA,BEEOS_LIST_FILES_TOOL_DESCRIPTION,executeListFiles}from"./files/list-tool.js";import{BEEOS_FETCH_FILE_TOOL_NAME,BEEOS_FETCH_FILE_TOOL_SCHEMA,BEEOS_FETCH_FILE_TOOL_DESCRIPTION,executeFetchFile}from"./files/fetch-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,sharedAcpServer=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async r=>{const{createPrivateKey:o,sign:t}=await import("node:crypto"),a=o({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(t(null,Buffer.from(r),a))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,r,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] unhandledRejection:",o),r({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:t,error:{code:"unhandled_rejection",message:t}})}),process.on("uncaughtException",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] uncaughtException:",o),r({component:"plugin",domain:"fatal",name:"uncaught_exception",severity:"error",summary:t,error:{code:"uncaught_exception",message:t}})}))}function initializePlugin(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const t=o.config,a=buildThirdPartyChannelSet(r);a.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...a].join(", ")}`);const n=t.log.enabled?createObsEventWriter({dir:t.log.dir,redaction:{gatewayToken:t.gateway.token},logger:e.logger}):()=>{};n({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),n({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:t,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)n({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),t.keyFile)try{agentKeys=loadAgentKeyPair(t.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&t.agentGatewayUrl&&(setUploadDeps({gatewayUrl:t.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:t,writeObs:n,thirdPartyChannels:a,openclawConfig:r}}function registerUploadTools(e,r,o){const{cfg:t,writeObs:a}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeUpload(o,t,agentKeys,n);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${s.ok}`),a({component:"upload-tool",domain:"tool",name:s.ok?"upload.success":"upload.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(s)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,r.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,r){handleStatusRequest(e,r,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}}),e.registerTool({name:BEEOS_LIST_FILES_TOOL_NAME,description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,parameters:BEEOS_LIST_FILES_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_list_files] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeListFiles(o,t,agentKeys,n);return a({component:"file-list-tool",domain:"tool",name:s.ok?"list_files.success":"list_files.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),s}}),e.registerTool({name:BEEOS_FETCH_FILE_TOOL_NAME,description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,parameters:BEEOS_FETCH_FILE_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_fetch_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeFetchFile(o,t,agentKeys,{signal:n,resolvePath:e.resolvePath});return a({component:"file-fetch-tool",domain:"tool",name:s.ok?"fetch_file.success":"fetch_file.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,path:s.path,size:s.size,error:s.error}}),s}})}function registerServices(e,r,o){const{cfg:t,writeObs:a,thirdPartyChannels:n}=r,s=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;sharedAcpServer||(sharedAcpServer=new ACPServer(t,e.logger,a,s,i,n));const l=sharedAcpServer;o.current=l;const c=new PromptRunStore;e.registerService({id:"beeos-acp",async start(){const r=Date.now(),o=_pluginRegisteredAt?r-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${o}ms, bridge.url=${t.bridge.url||"(empty)"}`),!t.bridge.url&&t.agentGatewayUrl&&s){const r=Date.now(),o=await discoverBridgeFromGW(t.agentGatewayUrl,s);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-r}ms result=${o?.url??"null"}`),o)t.bridge.url=o.url,writeDiscoveryCache({bridge:{url:o.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${o.url}`);else{const r=readDiscoveryCache();r?.bridge?.url?(t.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(t.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-r}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const{enabled:g,url:d,principalId:p,a2aEnabled:u}=t.messageBus;let f;if(t.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);f=createTokenProvider({agentGatewayUrl:t.agentGatewayUrl,identity:e})}const m=g&&(f||t.messageBus.serviceUrl),y=m?l.getMessageClient():null;if(m&&y){y.on("disconnect",r=>{e.logger.warn?.("[beeos-claw] message bus disconnected (code=%s, reason=%s) — SDK will auto-reconnect",r?.code??"?",r?.reason??"?")}),y.on("error",r=>{e.logger.warn?.("[beeos-claw] message bus error: %s",r instanceof Error?r.message:String(r))});const r=p||process.env.BEEOS_PRINCIPAL_ID||"",o={serviceUrl:t.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:d,tokenProvider:f,logger:e.logger},n=new Map;let s,i=!1;const g=()=>{if(r)return[{localAgentId:l.getLocalAgentId(),principalId:r}];const e=l.getPlatformAgentMappings().map(({localAgentId:e,platformAgent:r})=>({localAgentId:e,principalId:r.instanceId||""})).filter(e=>e.principalId);if(e.length>0)return e;const o=l.getDefaultPlatformAgent()?.instanceId||"";return o?[{localAgentId:l.getLocalAgentId(),principalId:o}]:[]},u=e=>({getLocalAgentId:()=>e.localAgentId,getDefaultPlatformAgent:()=>({instanceId:e.principalId}),ensureImInboundSession:(e,r,o)=>{l.ensureImInboundSession(e,r,o)}}),m=async(r,s)=>{if(n.has(r.principalId))return;const i=s?y:new MessageClient(o);s&&l.setMessageClient(o,r.principalId);const g=createImInboundHandler({runtime:e.runtime,pluginConfig:e.pluginConfig,client:i,localServer:u(r),promptRunStore:c,logger:e.logger,writeObs:a,principalIdOverride:r.principalId,promptTimeoutMs:t.promptTimeoutMs}),d=new MessageServiceRecoveryScanner({client:i,imInbound:g,logger:e.logger});i.on("connect",()=>{d.trigger("reconnect"),e.logger.info?.("[beeos-claw] MessageClient connected (localAgentId=%s, principalId=%s, via=%s)",r.localAgentId,r.principalId,f?"gateway":"direct")}),i.on("message",async e=>{"chat_message"===e.type?await g.onChatMessage(e):"chat_cancel"===e.type&&g.onChatCancel(e)}),n.set(r.principalId,{client:i,scanner:d,primary:s});try{await i.connect({identityId:r.principalId}),d.start(r.principalId)}catch(e){throw n.delete(r.principalId),d.stop(),s||await i.disconnect().catch(()=>{}),e}},b=async r=>{if(i)if(s)await s;else{s=(async()=>{const o=g();if(0===o.length)return void e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled");const t=new Set(o.map(e=>e.principalId));let a=!1;for(const[o,s]of n)t.has(o)||(s.primary&&(a=!0),s.scanner.stop(),await s.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected stale identity principalId=%s reason=%s",o,r));if(a&&n.size>0)for(const[o,t]of n)t.scanner.stop(),await t.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected for primary rebalance principalId=%s reason=%s",o,r);let s=[...n.values()].some(e=>e.primary);for(const e of o){const r=!s;await m(e,r),r&&(s=!0)}})();try{await s}finally{s=void 0}}};e.registerService({id:"beeos-message-bus",async start(){if(i=!0,l.onAgentMappingUpdated(()=>{b("agent-mapping-updated").catch(r=>{e.logger.error?.("[beeos-claw] Message bus mapping sync failed:",r)})}),!r)try{await l.waitForAgentMapping()}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}await b("startup")},async stop(){i=!1;const r=[...n.values()];n.clear();for(const e of r)e.scanner.stop(),await e.client.disconnect();e.logger.info?.("[beeos-claw] MessageClient disconnected")}})}else g?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",g);if(!1!==u&&t.agentGatewayUrl){const r=e=>l.getGatewaySessionKeyForToolCall(e),o=e=>l.getMentionsForToolCall(e),a=(e,r)=>{l.broadcastNotification(e,r)},n=new TaskHandleRegistry;registerA2ATools(e,{agentGatewayUrl:t.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},n,r,o,a,()=>l?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(t.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const r=sharedCanvasState;if(t.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=t.agentGatewayUrl,r=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}const a=a=>{const n=new CanvasRelayClient({canvasRelayUrl:t.canvasRelayUrl,agentKeys:agentKeys,agentId:a.id,displayName:a.displayName,avatarUrl:a.avatarUrl,tokenProvider:o,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});n.connect(),r.setRelayClient(n),r.setAgentId(a.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${a.id}, name=${a.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const r=l.getDefaultPlatformAgent();if(r?.id)return void a(r);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const r=l.getDefaultPlatformAgent();if(r?.id)try{a(r)}catch(r){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${r}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>a(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},s=new FormSchemaRegistry,i=buildCronSchemas(n,{capMs:t.cronMaxDurationCapMs});s.register(i.create),s.register(i.update);const c=new ResourceDescriptorRegistry;c.register(CRON_RESOURCE);const g=e=>async(r,o,t)=>(l.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),l.setCanvasState(r,{schemaRegistry:s,descriptorRegistry:c});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(s),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:s,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(c,e,async(e,r)=>l.gatewayRequest(e,r),p,e.logger)}const u='\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.',f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),m=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(y.size>=m&&!y.has(r.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!r?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(r.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&l.setPendingToolCallSessionKey(r.sessionKey),!f.has(e.toolName))return;const o=r?.sessionKey?y.get(r.sessionKey):void 0;return isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:o})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:r?.sessionKey?l.isCanvasActiveForGatewayKey(r.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const b=new TerminalManager,_=createTerminalWebSocketService({config:{port:t.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:t.terminalWsAllowedOrigins},terminalMgr:b,logger:e.logger,writeObs:a});if(t.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){_.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${t.terminalWsPort}`)},stop(){_.stop(),b.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const o=mirrorOpenClawConfig(r.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{if(!hasRuntimeReplyApi(e.runtime))throw e.logger.error?.("[beeos-claw] FATAL: OpenClaw runtime.channel.reply.dispatchReplyFromConfig not exposed. Upgrade OpenClaw or revert beeos-claw."),new Error("openclaw_runtime_reply_api_unavailable");const r=initializePlugin(e);if(!r)return;const o={current:null};installUnhandledRejectionObserver(e,r.writeObs,o),registerUploadTools(e,r,o),registerServices(e,r,o),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{hasRuntimeReplyApi}from"./runtime/reply-router.js";import{PromptRunStore}from"./runtime/prompt-run-store.js";import{createImInboundHandler}from"./im-inbound-handler.js";import{MessageServiceRecoveryScanner}from"./im-recovery-scanner.js";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{BEEOS_LIST_FILES_TOOL_NAME,BEEOS_LIST_FILES_TOOL_SCHEMA,BEEOS_LIST_FILES_TOOL_DESCRIPTION,executeListFiles}from"./files/list-tool.js";import{BEEOS_FETCH_FILE_TOOL_NAME,BEEOS_FETCH_FILE_TOOL_SCHEMA,BEEOS_FETCH_FILE_TOOL_DESCRIPTION,executeFetchFile}from"./files/fetch-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{BEEOS_FILE_RESOURCE_ROUTING_RULE}from"./files/beeos-file-uri.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,sharedAcpServer=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async r=>{const{createPrivateKey:o,sign:t}=await import("node:crypto"),a=o({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(t(null,Buffer.from(r),a))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,r,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] unhandledRejection:",o),r({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:t,error:{code:"unhandled_rejection",message:t}})}),process.on("uncaughtException",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] uncaughtException:",o),r({component:"plugin",domain:"fatal",name:"uncaught_exception",severity:"error",summary:t,error:{code:"uncaught_exception",message:t}})}))}function initializePlugin(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const t=o.config,a=buildThirdPartyChannelSet(r);a.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...a].join(", ")}`);const n=t.log.enabled?createObsEventWriter({dir:t.log.dir,redaction:{gatewayToken:t.gateway.token},logger:e.logger}):()=>{};n({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),n({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:t,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)n({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),t.keyFile)try{agentKeys=loadAgentKeyPair(t.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&t.agentGatewayUrl&&(setUploadDeps({gatewayUrl:t.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:t,writeObs:n,thirdPartyChannels:a,openclawConfig:r}}function registerUploadTools(e,r,o){const{cfg:t,writeObs:a}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeUpload(o,t,agentKeys,n);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${s.ok}`),a({component:"upload-tool",domain:"tool",name:s.ok?"upload.success":"upload.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(s)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,r.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,r){handleStatusRequest(e,r,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}}),e.registerTool({name:BEEOS_LIST_FILES_TOOL_NAME,description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,parameters:BEEOS_LIST_FILES_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_list_files] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeListFiles(o,t,agentKeys,n);return a({component:"file-list-tool",domain:"tool",name:s.ok?"list_files.success":"list_files.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),s}}),e.registerTool({name:BEEOS_FETCH_FILE_TOOL_NAME,description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,parameters:BEEOS_FETCH_FILE_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_fetch_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeFetchFile(o,t,agentKeys,{signal:n,resolvePath:e.resolvePath});return a({component:"file-fetch-tool",domain:"tool",name:s.ok?"fetch_file.success":"fetch_file.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,path:s.path,size:s.size,error:s.error}}),s}})}function registerServices(e,r,o){const{cfg:t,writeObs:a,thirdPartyChannels:n}=r,s=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;sharedAcpServer||(sharedAcpServer=new ACPServer(t,e.logger,a,s,i,n));const l=sharedAcpServer;o.current=l;const c=new PromptRunStore;e.registerService({id:"beeos-acp",async start(){const r=Date.now(),o=_pluginRegisteredAt?r-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${o}ms, bridge.url=${t.bridge.url||"(empty)"}`),!t.bridge.url&&t.agentGatewayUrl&&s){const r=Date.now(),o=await discoverBridgeFromGW(t.agentGatewayUrl,s);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-r}ms result=${o?.url??"null"}`),o)t.bridge.url=o.url,writeDiscoveryCache({bridge:{url:o.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${o.url}`);else{const r=readDiscoveryCache();r?.bridge?.url?(t.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(t.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-r}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const{enabled:g,url:d,principalId:p,a2aEnabled:u}=t.messageBus;let f;if(t.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);f=createTokenProvider({agentGatewayUrl:t.agentGatewayUrl,identity:e})}const m=g&&(f||t.messageBus.serviceUrl),y=m?l.getMessageClient():null;if(m&&y){y.on("disconnect",r=>{e.logger.warn?.("[beeos-claw] message bus disconnected (code=%s, reason=%s) — SDK will auto-reconnect",r?.code??"?",r?.reason??"?")}),y.on("error",r=>{e.logger.warn?.("[beeos-claw] message bus error: %s",r instanceof Error?r.message:String(r))});const r=p||process.env.BEEOS_PRINCIPAL_ID||"",o={serviceUrl:t.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:d,tokenProvider:f,logger:e.logger},n=new Map;let s,i=!1;const g=()=>{if(r)return[{localAgentId:l.getLocalAgentId(),principalId:r}];const e=l.getPlatformAgentMappings().map(({localAgentId:e,platformAgent:r})=>({localAgentId:e,principalId:r.instanceId||""})).filter(e=>e.principalId);if(e.length>0)return e;const o=l.getDefaultPlatformAgent()?.instanceId||"";return o?[{localAgentId:l.getLocalAgentId(),principalId:o}]:[]},u=e=>({getLocalAgentId:()=>e.localAgentId,getDefaultPlatformAgent:()=>({instanceId:e.principalId}),ensureImInboundSession:(e,r,o)=>{l.ensureImInboundSession(e,r,o)}}),m=async(r,s)=>{if(n.has(r.principalId))return;const i=s?y:new MessageClient(o);s&&l.setMessageClient(o,r.principalId);const g=createImInboundHandler({runtime:e.runtime,pluginConfig:e.pluginConfig,client:i,localServer:u(r),promptRunStore:c,logger:e.logger,writeObs:a,principalIdOverride:r.principalId,promptTimeoutMs:t.promptTimeoutMs}),d=new MessageServiceRecoveryScanner({client:i,imInbound:g,logger:e.logger});i.on("connect",()=>{d.trigger("reconnect"),e.logger.info?.("[beeos-claw] MessageClient connected (localAgentId=%s, principalId=%s, via=%s)",r.localAgentId,r.principalId,f?"gateway":"direct")}),i.on("message",r=>{(async()=>{"chat_message"===r.type?await g.onChatMessage(r):"chat_cancel"===r.type&&g.onChatCancel(r)})().catch(r=>{e.logger.warn?.(`[beeos-claw] message handler failed: ${r instanceof Error?r.message:String(r)}`)})}),n.set(r.principalId,{client:i,scanner:d,primary:s});try{await i.connect({identityId:r.principalId}),d.start(r.principalId)}catch(e){throw n.delete(r.principalId),d.stop(),s||await i.disconnect().catch(()=>{}),e}},_=async r=>{if(i)if(s)await s;else{s=(async()=>{const o=g();if(0===o.length)return void e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled");const t=new Set(o.map(e=>e.principalId));let a=!1;for(const[o,s]of n)t.has(o)||(s.primary&&(a=!0),s.scanner.stop(),await s.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected stale identity principalId=%s reason=%s",o,r));if(a&&n.size>0)for(const[o,t]of n)t.scanner.stop(),await t.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected for primary rebalance principalId=%s reason=%s",o,r);let s=[...n.values()].some(e=>e.primary);for(const e of o){const r=!s;await m(e,r),r&&(s=!0)}})();try{await s}finally{s=void 0}}};e.registerService({id:"beeos-message-bus",async start(){if(i=!0,l.onAgentMappingUpdated(()=>{_("agent-mapping-updated").catch(r=>{e.logger.error?.("[beeos-claw] Message bus mapping sync failed:",r)})}),!r)try{await l.waitForAgentMapping()}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}await _("startup")},async stop(){i=!1;const r=[...n.values()];n.clear();for(const e of r)e.scanner.stop(),await e.client.disconnect();e.logger.info?.("[beeos-claw] MessageClient disconnected")}})}else g?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",g);if(!1!==u&&t.agentGatewayUrl){const r=e=>l.getGatewaySessionKeyForToolCall(e),o=e=>l.getMentionsForToolCall(e),a=(e,r)=>{l.broadcastNotification(e,r)},n=new TaskHandleRegistry;registerA2ATools(e,{agentGatewayUrl:t.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},n,r,o,a,()=>l?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(t.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const r=sharedCanvasState;if(t.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=t.agentGatewayUrl,r=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}const a=a=>{const n=new CanvasRelayClient({canvasRelayUrl:t.canvasRelayUrl,agentKeys:agentKeys,agentId:a.id,displayName:a.displayName,avatarUrl:a.avatarUrl,tokenProvider:o,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});n.connect(),r.setRelayClient(n),r.setAgentId(a.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${a.id}, name=${a.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const r=l.getDefaultPlatformAgent();if(r?.id)return void a(r);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const r=l.getDefaultPlatformAgent();if(r?.id)try{a(r)}catch(r){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${r}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>a(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},s=new FormSchemaRegistry,i=buildCronSchemas(n,{capMs:t.cronMaxDurationCapMs});s.register(i.create),s.register(i.update);const c=new ResourceDescriptorRegistry;c.register(CRON_RESOURCE);const g=e=>async(r,o,t)=>(l.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),l.setCanvasState(r,{schemaRegistry:s,descriptorRegistry:c});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(s),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:s,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(c,e,async(e,r)=>l.gatewayRequest(e,r),p,e.logger)}const u=`\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\n- ${BEEOS_FILE_RESOURCE_ROUTING_RULE}\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.`,f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),m=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(y.size>=m&&!y.has(r.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!r?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(r.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&l.setPendingToolCallSessionKey(r.sessionKey),!f.has(e.toolName))return;const o=r?.sessionKey?y.get(r.sessionKey):void 0;return isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:o})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:r?.sessionKey?l.isCanvasActiveForGatewayKey(r.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const _=new TerminalManager,b=createTerminalWebSocketService({config:{port:t.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:t.terminalWsAllowedOrigins},terminalMgr:_,logger:e.logger,writeObs:a});if(t.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${t.terminalWsPort}`)},stop(){b.stop(),_.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const o=mirrorOpenClawConfig(r.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{if(!hasRuntimeReplyApi(e.runtime))throw e.logger.error?.("[beeos-claw] FATAL: OpenClaw runtime.channel.reply.dispatchReplyFromConfig not exposed. Upgrade OpenClaw or revert beeos-claw."),new Error("openclaw_runtime_reply_api_unavailable");const r=initializePlugin(e);if(!r)return;const o={current:null};installUnhandledRejectionObserver(e,r.writeObs,o),registerUploadTools(e,r,o),registerServices(e,r,o),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
@@ -1 +1 @@
1
- import{constants}from"node:fs";import{access,stat}from"node:fs/promises";import{uploadLocalFile}from"./upload-file.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,a){const n=parseParams(e);if(!n.ok)return errorResult(n.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — file upload requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign upload requests");const o=t.agentGatewayUrl,i=n.paths,s=(await validateFiles(i)).find(e=>!e.ok);if(s)return errorResult(`${s.path}: ${s.error}`);const l=[],p=i.map(e=>async()=>{try{return await uploadSingleFile(e,o,r,a)}catch(t){if(a?.aborted)throw t;return l.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: ${l.join("; ")}`);const c=u.map(e=>({uri:`beeos-file://${e.fileId}`,name:e.fileName,mimeType:e.mimeType,fileId:e.fileId})),m=u.length===i.length?`Uploaded ${u.length} file(s): ${c.map(e=>e.name).join(", ")}`:`Uploaded ${u.length}/${i.length} file(s): ${c.map(e=>e.name).join(", ")}. Failed: ${l.join("; ")}`,f={ok:!0,files:c};return{...f,content:[{type:"text",text:m},...c.map(e=>({type:"resource_link",uri:e.uri,name:e.name,mimeType:e.mimeType}))],output:f,result:f,details:f,isError:!1}}async function uploadSingleFile(e,t,r,a){const n=await uploadLocalFile(e,t,r,{signal:a,timeoutMs:12e4});return{fileId:n.fileId,downloadUrl:"",fileName:n.name,mimeType:n.mimeType,size:n.size}}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e;if(Object.keys(t).filter(e=>"paths"!==e).length>0)return{ok:!1,error:"only paths is allowed"};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}}
1
+ import{constants}from"node:fs";import{access,stat}from"node:fs/promises";import{uploadLocalFile}from"./upload-file.js";import{isBeeosFileUri}from"../files/beeos-file-uri.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. Accepts only local filesystem paths; never pass beeos-file:// URIs because they are already BeeOS platform resources. 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. Do not pass beeos-file:// URIs.",items:{type:"string",minLength:1,description:"Single local filesystem path, not a beeos-file:// URI."}}}};const MAX_PATHS=5,REQUEST_TIMEOUT_MS=12e4,MAX_CONCURRENCY=3,BEEOS_FILE_URI_UPLOAD_ERROR="beeos-file:// is already a BeeOS platform file URI, not a local path. Do not upload it; display it directly.";async function runWithConcurrency(e,t){const r=new Array(e.length);let o=0;async function a(){for(;o<e.length;){const t=o++;r[t]=await e[t]()}}const s=[];for(let r=0;r<Math.min(t,e.length);r++)s.push(a());return await Promise.all(s),r}async function validateFiles(e){return Promise.all(e.map(async e=>{try{await access(e,constants.R_OK);return(await stat(e)).isFile()?{path:e,ok:!0}:{path:e,ok:!1,error:"not a regular file"}}catch(t){const r=t.code;let o;return o="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:o}}}))}export async function executeUpload(e,t,r,o){const a=parseParams(e);if(!a.ok)return errorResult(a.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — file upload requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign upload requests");const s=t.agentGatewayUrl,i=a.paths,n=(await validateFiles(i)).find(e=>!e.ok);if(n)return errorResult(`${n.path}: ${n.error}`);const l=[],p=i.map(e=>async()=>{try{return await uploadSingleFile(e,s,r,o)}catch(t){if(o?.aborted)throw t;return l.push(`${e}: ${t instanceof Error?t.message:String(t)}`),null}}),c=(await runWithConcurrency(p,3)).filter(e=>null!==e);if(0===c.length)return errorResult(`All uploads failed: ${l.join("; ")}`);const u=c.map(e=>({uri:`beeos-file://${e.fileId}`,name:e.fileName,mimeType:e.mimeType,fileId:e.fileId})),f=c.length===i.length?`Uploaded ${c.length} file(s): ${u.map(e=>e.name).join(", ")}`:`Uploaded ${c.length}/${i.length} file(s): ${u.map(e=>e.name).join(", ")}. Failed: ${l.join("; ")}`,m={ok:!0,files:u};return{...m,content:[{type:"text",text:f},...u.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,o){const a=await uploadLocalFile(e,t,r,{signal:o,timeoutMs:12e4});return{fileId:a.fileId,downloadUrl:"",fileName:a.name,mimeType:a.mimeType,size:a.size}}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e;if(Object.keys(t).filter(e=>"paths"!==e).length>0)return{ok:!1,error:"only paths is allowed"};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 o=t.paths[e];if("string"!=typeof o||""===o.trim())return{ok:!1,error:`paths[${e}] must be a non-empty string`};const a=o.trim();if(isBeeosFileUri(a))return{ok:!1,error:BEEOS_FILE_URI_UPLOAD_ERROR};r.push(a)}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}}
@@ -0,0 +1 @@
1
+ import{describe,expect,it}from"vitest";import{BEEOS_UPLOAD_TOOL_DESCRIPTION,BEEOS_UPLOAD_TOOL_SCHEMA,executeUpload}from"./tool.js";function makeConfig(e=""){return{agentGatewayUrl:e}}describe("executeUpload",()=>{it("rejects BeeOS platform file URIs before filesystem or gateway checks",async()=>{const e=await executeUpload({paths:["beeos-file://abc"]},makeConfig(),null);expect(e).toMatchObject({ok:!1,isError:!0,error:"beeos-file:// is already a BeeOS platform file URI, not a local path. Do not upload it; display it directly."})}),it("keeps local paths on the normal upload validation path",async()=>{const e=await executeUpload({paths:["/tmp/example.png"]},makeConfig(),null);expect(e).toMatchObject({ok:!1,error:"agentGatewayUrl not configured — file upload requires Agent Gateway access"})}),it("documents that paths are local filesystem paths",()=>{expect(BEEOS_UPLOAD_TOOL_DESCRIPTION).toContain("local filesystem paths"),expect(BEEOS_UPLOAD_TOOL_DESCRIPTION).toContain("never pass beeos-file://"),expect(BEEOS_UPLOAD_TOOL_SCHEMA.properties.paths.description).toContain("Do not pass beeos-file://")})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.5.25",
3
+ "version": "0.5.26",
4
4
  "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
5
  "license": "MIT",
6
6
  "author": "BeeOS <dev@beeos.ai>",