beeos-claw 0.5.22 → 0.5.24
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 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 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),n=extractArtifactText(t),r=extractStatusText(t),i=taskContextId(t),o={status:s,is_terminal:"completed"===s||TERMINAL_FAILURE_STATES.has(s)};return e&&(o.task_id=e),a&&(o.agent_id=a),i&&(o.context_id=i),n?o.result=n:r&&(o.error=r),o}export{unwrapTask,extractArtifactText,extractStatusText,taskState,taskContextId,normalizeState,buildTaskPayload};export class TaskHandleRegistry{seq=0;handles=new Map;taskIdToHandle=new Map;create(t,e,a,s){const n="bth_"+ ++this.seq;return this.handles.set(n,{taskId:t,agentId:e,contextId:a,sessionKey:s}),this.taskIdToHandle.set(t,n),n}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,n,r,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:n}=a??{};try{const t=new URLSearchParams({limit:"50"});n&&t.set("query",n);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 r=await a.json(),o=i?.();if(o&&Array.isArray(r?.data))for(const t of r.data)t.isSelf=t.instance_id===o;return{content:JSON.stringify(r,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:d,message:c,files:l,contextId:u,timeoutMs:g,taskId:h}=i;const f=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(d)){const a=n?.(t);if(a?.length){const t=resolveAgentIdFromMentions(a,d);t&&(e.logger.info?.(`beeos_call_agent: resolved "${d}" → ${t} via mention context`),d=t)}if(!p.test(d))try{const t=await a2aFetch(e,`/api/v1/a2a/discover?query=${encodeURIComponent(d)}&limit=10`,{signal:o});if(t.ok){const a=await t.json(),s=a?.data??[],n=d.toLowerCase(),r=s.find(t=>t.name.toLowerCase()===n)??s.find(t=>t.name.toLowerCase().includes(n));if(!r)return{content:JSON.stringify({error:`Could not resolve agent name "${d}". Available agents: ${s.map(t=>`${t.name} (${t.id})`).join(", ")||"none"}`})};e.logger.info?.(`beeos_call_agent: resolved "${d}" → ${r.id} (${r.name}) via discover`),d=r.id}}catch{}}const k=u||(f?`session:${f}`:""),m=g||6e5;try{const t={jsonrpc:"2.0",id:`claw-${Date.now()}`,method:"SendMessage",params:{message:{role:"user",parts:[{text:c}],...k?{contextId:k}:{},...h?{taskId:h}:{}},configuration:{timeoutMs:m}}},s=`/api/v1/a2a/${encodeURIComponent(d)}`,n=AbortSignal.timeout(3e4),i=o?AbortSignal.any([o,n]):n,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&&r&&r("_beeos/auth_required",{type:"auth_required",target_agent_id:d,target_agent_name:d,title:"Authentication Required",content:`Agent **${d}** requires authentication.`})}const t=p.error?JSON.stringify(p.error):g;return{content:JSON.stringify({error:`send task failed: ${l.status} ${t}`,agent_id:d})}}const _=unwrapTask(p.result),y=_?.id;return y?(a.create(y,d,u,f),{content:JSON.stringify({status:taskState(_)||"working",task_id:y,agent_id:d,context_id:taskContextId(_)??k??void 0,message:`Task created. Call beeos_get_task with task_id "${y}" (and agent_id "${d}") to fetch status and result.`})}):{content:JSON.stringify({error:"task created but server returned no task_id",agent_id:d})}}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:d})}:{content:JSON.stringify({error:t instanceof Error?t.message:String(t),agent_id:d})}}}}),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,s,n){const{taskId:r,agentId:i}=s??{};if(!r)return{content:JSON.stringify({error:"taskId is required"})};const o=i||a.resolveAgentId(r);if(!o)return{content:JSON.stringify({error:`agentId is required to query task "${r}" (it was not created in this session, so the target agent is unknown). Pass the agentId you originally called.`,task_id:r})};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:r}}),signal:n}),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:r,agent_id:o})}}const d=unwrapTask(i.result);return{content:JSON.stringify(buildTaskPayload(d,r,o))}}catch(t){if(n?.aborted)throw t;return{content:JSON.stringify({error:t instanceof Error?t.message:String(t),task_id:r,agent_id:o})}}}})}
|
|
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})}}}})}
|
package/dist/a2a/tools.test.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,it,expect,vi,afterEach}from"vitest";import{registerA2ATools,TaskHandleRegistry,unwrapTask,extractArtifactText,extractStatusText,taskState,buildTaskPayload}from"./tools.js";const AGENT_ID="d04080d1-1111-2222-3333-444455556666";function makeCfg(){return{agentGatewayUrl:"https://gw.test",agentKeys:null,logger:{info:vi.fn(),warn:vi.fn(),error:vi.fn(),debug:vi.fn()}}}function registerTools(t,e=new TaskHandleRegistry){const s=new Map;return registerA2ATools({registerTool:t=>s.set(t.name,t)},t,e),s}function jsonResponse(t,e=!0,s=200){const a=JSON.stringify(t);return{ok:e,status:s,text:async()=>a,json:async()=>JSON.parse(a)}}describe("unwrapTask",()=>{it("unwraps the v1.0 SendMessage { task } envelope",()=>{const t=unwrapTask({task:{id:"t1",status:{state:"TASK_STATE_WORKING"}}});expect(t?.id).toBe("t1")}),it("returns the bare task for v0.3 / GetTask results",()=>{const t=unwrapTask({id:"t2",status:{state:"working"}});expect(t?.id).toBe("t2")}),it("returns undefined for non-object input",()=>{expect(unwrapTask(void 0)).toBeUndefined(),expect(unwrapTask("nope")).toBeUndefined()})}),describe("extractArtifactText",()=>{it("reads v1.0 text parts ({ text } without kind)",()=>{expect(extractArtifactText({artifacts:[{parts:[{text:"hello"},{text:"world"}]}]})).toBe("hello\nworld")}),it("reads v0.3 text parts ({ kind: 'text', text })",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"text",text:"hi"}]}]})).toBe("hi")}),it("skips non-text parts (e.g. file/data)",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"file",url:"u"},{text:"kept"}]}]})).toBe("kept")}),it("returns undefined when there are no artifacts",()=>{expect(extractArtifactText({status:{state:"working"}})).toBeUndefined(),expect(extractArtifactText(void 0)).toBeUndefined()})}),describe("extractStatusText",()=>{it("reads the agent status message text",()=>{expect(extractStatusText({status:{message:{parts:[{text:"boom"}]}}})).toBe("boom")}),it("returns undefined when no status message",()=>{expect(extractStatusText({status:{}})).toBeUndefined()})}),describe("taskState",()=>{it("normalizes v1.0 SCREAMING_SNAKE states",()=>{expect(taskState({status:{state:"TASK_STATE_COMPLETED"}})).toBe("completed"),expect(taskState({status:{state:"TASK_STATE_WORKING"}})).toBe("working")}),it("passes through v0.3 lowercase states",()=>{expect(taskState({status:{state:"failed"}})).toBe("failed")})}),describe("buildTaskPayload",()=>{it("surfaces produced content with the terminal state (canceled with artifact)",()=>{expect(buildTaskPayload({contextId:"ctx-1",status:{state:"TASK_STATE_CANCELED"},artifacts:[{parts:[{text:"partial work"}]}]},"t1","agent-1")).toEqual({status:"canceled",task_id:"t1",agent_id:"agent-1",context_id:"ctx-1",is_terminal:!0,result:"partial work"})}),it("reports status-message text as error when there is no artifact",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_FAILED",message:{parts:[{text:"boom"}]}}},"t2","agent-2")).toEqual({status:"failed",task_id:"t2",agent_id:"agent-2",is_terminal:!0,error:"boom"})}),it("marks a working task as non-terminal with neither result nor error",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_WORKING"}},"t3","agent-3")).toEqual({status:"working",task_id:"t3",agent_id:"agent-3",is_terminal:!1})}),it("includes result for a completed task",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"the answer"}]}]},"t4","agent-4")).toEqual({status:"completed",task_id:"t4",agent_id:"agent-4",is_terminal:!0,result:"the answer"})})}),describe("beeos_call_agent async create",()=>{afterEach(()=>{vi.unstubAllGlobals()}),it("creates the task and returns the handle (task_id, agent_id, context_id) without polling or task data",async()=>{const t=[],e=vi.fn(async(e,s)=>{const a=s?.body?JSON.parse(s.body):{};if(t.push({url:e,method:s?.method,rpcMethod:a.method}),"SendMessage"===a.method)return jsonResponse({jsonrpc:"2.0",id:a.id,result:{task:{id:"task-async-1",contextId:"c1",status:{state:"TASK_STATE_WORKING"}}}});throw new Error(`unexpected request method: ${a.method}`)});vi.stubGlobal("fetch",e);const s=registerTools(makeCfg()).get("beeos_call_agent"),a=await s.execute("tc-1",{agentId:AGENT_ID,message:"do it"}),o=JSON.parse(a.content);expect(o.status).toBe("working"),expect(o.task_id).toBe("task-async-1"),expect(o.agent_id).toBe(AGENT_ID),expect(o.context_id).toBe("c1"),expect(o.result).toBeUndefined(),expect(t).toHaveLength(1),expect(t[0].rpcMethod).toBe("SendMessage"),expect(t[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`)}),it("registers the task so beeos_get_task can resolve the agent id",async()=>{const t=vi.fn(async(t,e)=>{const s=e?.body?JSON.parse(e.body):{};return"SendMessage"===s.method?jsonResponse({jsonrpc:"2.0",id:s.id,result:{task:{id:"task-reg-1",status:{state:"TASK_STATE_WORKING"}}}}):jsonResponse({jsonrpc:"2.0",id:s.id,result:{id:"task-reg-1",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"done"}]}]}})});vi.stubGlobal("fetch",t);const e=new TaskHandleRegistry,s=registerTools(makeCfg(),e);await s.get("beeos_call_agent").execute("tc",{agentId:AGENT_ID,message:"do it"});const a=await s.get("beeos_get_task").execute("tc",{taskId:"task-reg-1"}),o=JSON.parse(a.content);expect(o.status).toBe("completed"),expect(o.result).toBe("done"),expect(o.agent_id).toBe(AGENT_ID)}),it("returns an error handle (with agent_id, no task_id) when create fails",async()=>{const t=vi.fn(async(t,e)=>jsonResponse({jsonrpc:"2.0",id:(e?.body?JSON.parse(e.body):{}).id,error:{code:-32e3,message:"boom"}},!1,500));vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_call_agent"),s=await e.execute("tc",{agentId:AGENT_ID,message:"do it"}),a=JSON.parse(s.content);expect(a.error).toContain("send task failed"),expect(a.agent_id).toBe(AGENT_ID),expect(a.task_id).toBeUndefined()})}),describe("beeos_get_task",()=>{function t(t){const e=[],s=vi.fn(async(s,a)=>{const o=a?.body?JSON.parse(a.body):{};if(e.push({url:s,method:a?.method,rpcMethod:o.method,params:o.params}),"GetTask"!==o.method)throw new Error(`unexpected method: ${o.method}`);return jsonResponse({jsonrpc:"2.0",id:o.id,result:t})});return vi.stubGlobal("fetch",s),e}afterEach(()=>{vi.unstubAllGlobals()}),it("returns the data of a completed task and queries the right endpoint",async()=>{const e=t({id:"task-1",contextId:"ctx-1",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"the answer"}]}]}),s=registerTools(makeCfg()).get("beeos_get_task"),a=await s.execute("tc",{taskId:"task-1",agentId:AGENT_ID}),o=JSON.parse(a.content);expect(o.status).toBe("completed"),expect(o.task_id).toBe("task-1"),expect(o.agent_id).toBe(AGENT_ID),expect(o.context_id).toBe("ctx-1"),expect(o.is_terminal).toBe(!0),expect(o.result).toBe("the answer"),expect(e).toHaveLength(1),expect(e[0].method).toBe("POST"),expect(e[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`),expect(e[0].params.id).toBe("task-1")}),it("surfaces partial output AND the terminal state for a canceled task that still produced an artifact",async()=>{t({id:"task-cx",status:{state:"TASK_STATE_CANCELED"},artifacts:[{parts:[{text:"partial plan"}]}]});const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"task-cx",agentId:AGENT_ID}),a=JSON.parse(s.content);expect(a.status).toBe("canceled"),expect(a.task_id).toBe("task-cx"),expect(a.result).toBe("partial plan")}),it("returns an error for a terminal task with no produced content",async()=>{t({id:"task-to",status:{state:"TASK_STATE_FAILED",message:{parts:[{text:"boom"}]}}});const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"task-to",agentId:AGENT_ID}),a=JSON.parse(s.content);expect(a.error).toBe("boom"),expect(a.task_id).toBe("task-to")}),it("reports a still-running task as working",async()=>{t({id:"task-w",status:{state:"TASK_STATE_WORKING"}});const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"task-w",agentId:AGENT_ID}),a=JSON.parse(s.content);expect(a.status).toBe("working"),expect(a.task_id).toBe("task-w")}),it("resolves agentId from the session registry when omitted",async()=>{const e=new TaskHandleRegistry;e.create("task-reg",AGENT_ID);const s=t({id:"task-reg",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"ok"}]}]}),a=registerTools(makeCfg(),e).get("beeos_get_task"),o=await a.execute("tc",{taskId:"task-reg"}),r=JSON.parse(o.content);expect(r.status).toBe("completed"),expect(r.result).toBe("ok"),expect(r.agent_id).toBe(AGENT_ID),expect(s[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`)}),it("errors when agentId is neither passed nor resolvable",async()=>{const t=vi.fn();vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"unknown-task"}),a=JSON.parse(s.content);expect(a.error).toContain("agentId is required"),expect(t).not.toHaveBeenCalled()})});
|
|
1
|
+
import{describe,it,expect,vi,afterEach}from"vitest";import{registerA2ATools,TaskHandleRegistry,unwrapTask,extractArtifactText,extractArtifactFiles,extractStatusText,taskState,buildTaskPayload}from"./tools.js";const AGENT_ID="d04080d1-1111-2222-3333-444455556666";function makeCfg(){return{agentGatewayUrl:"https://gw.test",agentKeys:null,logger:{info:vi.fn(),warn:vi.fn(),error:vi.fn(),debug:vi.fn()}}}function registerTools(t,e=new TaskHandleRegistry){const a=new Map;return registerA2ATools({registerTool:t=>a.set(t.name,t)},t,e),a}function jsonResponse(t,e=!0,a=200){const s=JSON.stringify(t);return{ok:e,status:a,text:async()=>s,json:async()=>JSON.parse(s)}}describe("unwrapTask",()=>{it("unwraps the v1.0 SendMessage { task } envelope",()=>{const t=unwrapTask({task:{id:"t1",status:{state:"TASK_STATE_WORKING"}}});expect(t?.id).toBe("t1")}),it("returns the bare task for v0.3 / GetTask results",()=>{const t=unwrapTask({id:"t2",status:{state:"working"}});expect(t?.id).toBe("t2")}),it("returns undefined for non-object input",()=>{expect(unwrapTask(void 0)).toBeUndefined(),expect(unwrapTask("nope")).toBeUndefined()})}),describe("extractArtifactText",()=>{it("reads v1.0 text parts ({ text } without kind)",()=>{expect(extractArtifactText({artifacts:[{parts:[{text:"hello"},{text:"world"}]}]})).toBe("hello\nworld")}),it("reads v0.3 text parts ({ kind: 'text', text })",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"text",text:"hi"}]}]})).toBe("hi")}),it("skips non-text parts (e.g. file/data)",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"file",url:"u"},{text:"kept"}]}]})).toBe("kept")}),it("returns undefined when there are no artifacts",()=>{expect(extractArtifactText({status:{state:"working"}})).toBeUndefined(),expect(extractArtifactText(void 0)).toBeUndefined()})}),describe("extractArtifactFiles",()=>{it("reads v1.0 flat file parts",()=>{expect(extractArtifactFiles({artifacts:[{parts:[{url:"beeos-file://file-1",filename:"screenshot.jpeg",mediaType:"image/jpeg"}]}]})).toEqual([{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}])}),it("reads v0.3 nested file parts",()=>{expect(extractArtifactFiles({artifacts:[{parts:[{kind:"file",file:{uri:"beeos-file://file-2",name:"capture.png",mimeType:"image/png"}}]}]})).toEqual([{uri:"beeos-file://file-2",name:"capture.png",mimeType:"image/png"}])}),it("does not infer files from text-only beeos-file URI artifacts",()=>{expect(extractArtifactFiles({artifacts:[{parts:[{text:"beeos-file://file-3"}]}]})).toBeUndefined()})}),describe("extractStatusText",()=>{it("reads the agent status message text",()=>{expect(extractStatusText({status:{message:{parts:[{text:"boom"}]}}})).toBe("boom")}),it("returns undefined when no status message",()=>{expect(extractStatusText({status:{}})).toBeUndefined()})}),describe("taskState",()=>{it("normalizes v1.0 SCREAMING_SNAKE states",()=>{expect(taskState({status:{state:"TASK_STATE_COMPLETED"}})).toBe("completed"),expect(taskState({status:{state:"TASK_STATE_WORKING"}})).toBe("working")}),it("passes through v0.3 lowercase states",()=>{expect(taskState({status:{state:"failed"}})).toBe("failed")})}),describe("buildTaskPayload",()=>{it("surfaces produced content with the terminal state (canceled with artifact)",()=>{expect(buildTaskPayload({contextId:"ctx-1",status:{state:"TASK_STATE_CANCELED"},artifacts:[{parts:[{text:"partial work"}]}]},"t1","agent-1")).toEqual({status:"canceled",task_id:"t1",agent_id:"agent-1",context_id:"ctx-1",is_terminal:!0,result:"partial work"})}),it("reports status-message text as error when there is no artifact",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_FAILED",message:{parts:[{text:"boom"}]}}},"t2","agent-2")).toEqual({status:"failed",task_id:"t2",agent_id:"agent-2",is_terminal:!0,error:"boom"})}),it("marks a working task as non-terminal with neither result nor error",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_WORKING"}},"t3","agent-3")).toEqual({status:"working",task_id:"t3",agent_id:"agent-3",is_terminal:!1})}),it("includes result for a completed task",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"the answer"}]}]},"t4","agent-4")).toEqual({status:"completed",task_id:"t4",agent_id:"agent-4",is_terminal:!0,result:"the answer"})}),it("returns result text and structured files together",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"beeos-file://file-1"},{url:"beeos-file://file-1",filename:"screenshot.jpeg",mediaType:"image/jpeg"}]}]},"t5","agent-5")).toEqual({status:"completed",task_id:"t5",agent_id:"agent-5",is_terminal:!0,result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})}),it("does not turn text-only beeos-file URI results into files",()=>{expect(buildTaskPayload({status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"beeos-file://file-only-text"}]}]},"t6","agent-6")).toEqual({status:"completed",task_id:"t6",agent_id:"agent-6",is_terminal:!0,result:"beeos-file://file-only-text"})})}),describe("beeos_call_agent async create",()=>{afterEach(()=>{vi.unstubAllGlobals()}),it("creates the task and returns the handle (task_id, agent_id, context_id) without polling or task data",async()=>{const t=[],e=vi.fn(async(e,a)=>{const s=a?.body?JSON.parse(a.body):{};if(t.push({url:e,method:a?.method,rpcMethod:s.method}),"SendMessage"===s.method)return jsonResponse({jsonrpc:"2.0",id:s.id,result:{task:{id:"task-async-1",contextId:"c1",status:{state:"TASK_STATE_WORKING"}}}});throw new Error(`unexpected request method: ${s.method}`)});vi.stubGlobal("fetch",e);const a=registerTools(makeCfg()).get("beeos_call_agent"),s=await a.execute("tc-1",{agentId:AGENT_ID,message:"do it"}),r=JSON.parse(s.content);expect(r.status).toBe("working"),expect(r.task_id).toBe("task-async-1"),expect(r.agent_id).toBe(AGENT_ID),expect(r.context_id).toBe("c1"),expect(r.result).toBeUndefined(),expect(t).toHaveLength(1),expect(t[0].rpcMethod).toBe("SendMessage"),expect(t[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`)}),it("registers the task so beeos_get_task can resolve the agent id",async()=>{const t=vi.fn(async(t,e)=>{const a=e?.body?JSON.parse(e.body):{};return"SendMessage"===a.method?jsonResponse({jsonrpc:"2.0",id:a.id,result:{task:{id:"task-reg-1",status:{state:"TASK_STATE_WORKING"}}}}):jsonResponse({jsonrpc:"2.0",id:a.id,result:{id:"task-reg-1",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"done"}]}]}})});vi.stubGlobal("fetch",t);const e=new TaskHandleRegistry,a=registerTools(makeCfg(),e);await a.get("beeos_call_agent").execute("tc",{agentId:AGENT_ID,message:"do it"});const s=await a.get("beeos_get_task").execute("tc",{taskId:"task-reg-1"}),r=JSON.parse(s.content);expect(r.status).toBe("completed"),expect(r.result).toBe("done"),expect(r.agent_id).toBe(AGENT_ID)}),it("returns an error handle (with agent_id, no task_id) when create fails",async()=>{const t=vi.fn(async(t,e)=>jsonResponse({jsonrpc:"2.0",id:(e?.body?JSON.parse(e.body):{}).id,error:{code:-32e3,message:"boom"}},!1,500));vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_call_agent"),a=await e.execute("tc",{agentId:AGENT_ID,message:"do it"}),s=JSON.parse(a.content);expect(s.error).toContain("send task failed"),expect(s.agent_id).toBe(AGENT_ID),expect(s.task_id).toBeUndefined()})}),describe("beeos_get_task",()=>{function t(t){const e=[],a=vi.fn(async(a,s)=>{const r=s?.body?JSON.parse(s.body):{};if(e.push({url:a,method:s?.method,rpcMethod:r.method,params:r.params}),"GetTask"!==r.method)throw new Error(`unexpected method: ${r.method}`);return jsonResponse({jsonrpc:"2.0",id:r.id,result:t})});return vi.stubGlobal("fetch",a),e}afterEach(()=>{vi.unstubAllGlobals()}),it("returns the data of a completed task and queries the right endpoint",async()=>{const e=t({id:"task-1",contextId:"ctx-1",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"the answer"}]}]}),a=registerTools(makeCfg()).get("beeos_get_task"),s=await a.execute("tc",{taskId:"task-1",agentId:AGENT_ID}),r=JSON.parse(s.content);expect(r.status).toBe("completed"),expect(r.task_id).toBe("task-1"),expect(r.agent_id).toBe(AGENT_ID),expect(r.context_id).toBe("ctx-1"),expect(r.is_terminal).toBe(!0),expect(r.result).toBe("the answer"),expect(e).toHaveLength(1),expect(e[0].method).toBe("POST"),expect(e[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`),expect(e[0].params.id).toBe("task-1")}),it("surfaces partial output AND the terminal state for a canceled task that still produced an artifact",async()=>{t({id:"task-cx",status:{state:"TASK_STATE_CANCELED"},artifacts:[{parts:[{text:"partial plan"}]}]});const e=registerTools(makeCfg()).get("beeos_get_task"),a=await e.execute("tc",{taskId:"task-cx",agentId:AGENT_ID}),s=JSON.parse(a.content);expect(s.status).toBe("canceled"),expect(s.task_id).toBe("task-cx"),expect(s.result).toBe("partial plan")}),it("returns an error for a terminal task with no produced content",async()=>{t({id:"task-to",status:{state:"TASK_STATE_FAILED",message:{parts:[{text:"boom"}]}}});const e=registerTools(makeCfg()).get("beeos_get_task"),a=await e.execute("tc",{taskId:"task-to",agentId:AGENT_ID}),s=JSON.parse(a.content);expect(s.error).toBe("boom"),expect(s.task_id).toBe("task-to")}),it("reports a still-running task as working",async()=>{t({id:"task-w",status:{state:"TASK_STATE_WORKING"}});const e=registerTools(makeCfg()).get("beeos_get_task"),a=await e.execute("tc",{taskId:"task-w",agentId:AGENT_ID}),s=JSON.parse(a.content);expect(s.status).toBe("working"),expect(s.task_id).toBe("task-w")}),it("resolves agentId from the session registry when omitted",async()=>{const e=new TaskHandleRegistry;e.create("task-reg",AGENT_ID);const a=t({id:"task-reg",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"ok"}]}]}),s=registerTools(makeCfg(),e).get("beeos_get_task"),r=await s.execute("tc",{taskId:"task-reg"}),o=JSON.parse(r.content);expect(o.status).toBe("completed"),expect(o.result).toBe("ok"),expect(o.agent_id).toBe(AGENT_ID),expect(a[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`)}),it("errors when agentId is neither passed nor resolvable",async()=>{const t=vi.fn();vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_get_task"),a=await e.execute("tc",{taskId:"unknown-task"}),s=JSON.parse(a.content);expect(s.error).toContain("agentId is required"),expect(t).not.toHaveBeenCalled()})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import AdmZip from"adm-zip";export const MAX_ZIP_BYTES=
|
|
1
|
+
import AdmZip from"adm-zip";export const MAX_ZIP_BYTES=268435456;export const MAX_DECOMPRESSED_BYTES=536870912;export const MAX_FILE_BYTES=268435456;export const MAX_ENTRIES=5e4;const MANIFEST_NAME="manifest.json",IDENTITY_NAME="IDENTITY.md",SKILLS_DIR="skills/",SLUG_PATTERN=/^[a-z0-9][a-z0-9-]{1,63}$/,VERSION_PATTERN=/^[0-9]+(\.[0-9]+){0,2}([.-][0-9A-Za-z.-]+)?$/;export class TemplatePackageError extends Error{constructor(e){super(e),this.name="TemplatePackageError"}}function normalizeEntryName(e){return e.replace(/\\/g,"/")}function isIgnored(e){if("__MACOSX"===e||e.startsWith("__MACOSX/"))return!0;return".DS_Store"===(e.replace(/\/$/,"").split("/").pop()??"")}function isSymlink(e){return 40960===((e.header.attr??0)>>>16&61440)}function baseName(e){return e.replace(/\/$/,"").split("/").pop()??""}function hasUnsafePath(e){return!!e.startsWith("/")||(e.length>=2&&":"===e[1]||e.split("/").some(e=>".."===e))}function findManifestIndex(e){let t=-1,r=0;for(let n=0;n<e.length;n++){const a=normalizeEntryName(e[n].entryName);if(""===a||isIgnored(a)||baseName(a)!==MANIFEST_NAME)continue;const o=(a.match(/\//g)??[]).length;(-1===t||o<r)&&(t=n,r=o)}return t}function detectFlattenPrefix(e,t){let r="";for(;;){let n="",a=!0,o=!1;for(let i=0;i<e.length;i++){if(i===t)continue;const s=normalizeEntryName(e[i].entryName);if(""===s||isIgnored(s)||!s.startsWith(r))continue;const l=s.slice(r.length);if(""===l)continue;o=!0;const c=l.indexOf("/");if(c<0)return r;const f=l.slice(0,c);if(a)n=f,a=!1;else if(f!==n)return r}if(!o||""===n||n+"/"=="skills/")return r;r+=n+"/"}}function cleanRelPath(e){if(e.startsWith("/")||/^[a-zA-Z]:/.test(e))throw new TemplatePackageError(`path traversal detected: ${e}`);const t=[];for(const r of e.split("/"))if(""!==r&&"."!==r){if(".."===r)throw new TemplatePackageError(`path traversal detected: ${e}`);t.push(r)}return t.join("/")}function stripPrefixSafe(e,t){let r=e;if(""!==t){if(!e.startsWith(t))throw new TemplatePackageError(`entry escapes package root: ${e}`);r=e.slice(t.length)}if(""===r)return"";const n=r.endsWith("/"),a=cleanRelPath(r);return""===a?"":n?a+"/":a}function parseManifest(e){let t;try{t=JSON.parse(e.toString("utf8"))}catch(e){throw new TemplatePackageError(`invalid manifest.json: ${e instanceof Error?e.message:String(e)}`)}if("object"!=typeof t||null===t)throw new TemplatePackageError("invalid manifest.json: not an object");const r=t;if(1!==r.schema_version)throw new TemplatePackageError(`unsupported manifest schema_version: ${String(r.schema_version)} (expected 1)`);const n=String(r.slug??"").trim().toLowerCase();if(""!==n&&!SLUG_PATTERN.test(n))throw new TemplatePackageError(`manifest slug "${n}" must match ${SLUG_PATTERN}`);const a=String(r.display_name??"").trim();if(""===a)throw new TemplatePackageError("manifest display_name is required");const o=String(r.version??"").trim();if(""!==o&&!VERSION_PATTERN.test(o))throw new TemplatePackageError(`manifest version "${o}" is not a valid version`);const i=t;return i.slug=n,i.display_name=a,i.version=o,i}export function parseAndValidateTemplatePackage(e){if(0===e.length)throw new TemplatePackageError("package is empty");if(e.length>268435456)throw new TemplatePackageError(`package too large: ${e.length} bytes (max 268435456)`);let t;try{t=new AdmZip(e)}catch(e){throw new TemplatePackageError(`not a valid zip archive: ${e instanceof Error?e.message:String(e)}`)}const r=t.getEntries();if(r.length>5e4)throw new TemplatePackageError(`too many entries: ${r.length} (max 50000)`);for(const e of r){const t=normalizeEntryName(e.entryName);if(""!==t&&!isIgnored(t)&&hasUnsafePath(t))throw new TemplatePackageError(`path traversal detected: ${e.entryName}`)}const n=findManifestIndex(r),a=detectFlattenPrefix(r,n),o=new Map;let i=null,s=0;const l=e=>{if(s+=e,s>536870912)throw new TemplatePackageError("package decompresses to more than 536870912 bytes")};if(n>=0){const e=r[n];if((e.header.size??0)>268435456)throw new TemplatePackageError(`file too large: ${MANIFEST_NAME} (max 268435456)`);if(i=e.getData(),i.length>268435456)throw new TemplatePackageError(`file too large: ${MANIFEST_NAME} (max 268435456)`);l(i.length)}for(let e=0;e<r.length;e++){if(e===n)continue;const t=r[e],i=normalizeEntryName(t.entryName);if(""===i||isIgnored(i))continue;if(i.endsWith("/"))continue;if(isSymlink(t))throw new TemplatePackageError(`symlinks are not allowed: ${t.entryName}`);const s=stripPrefixSafe(i,a);if(""===s||s.endsWith("/"))continue;const c=t.header.size??0;if(c>268435456)throw new TemplatePackageError(`file too large: ${s} is ${c} bytes (max 268435456)`);if("IDENTITY.md"===s)continue;const f=t.getData();if(f.length>268435456)throw new TemplatePackageError(`file too large: ${s} is ${f.length} bytes (max 268435456)`);l(f.length),o.set(s,f)}return{manifest:null!==i?parseManifest(i):null,workspaceFiles:o,flattenPrefix:a}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,it,expect}from"vitest";import AdmZip from"adm-zip";import{parseAndValidateTemplatePackage,TemplatePackageError}from"./agent-template-package.js";const validManifest=JSON.stringify({schema_version:1,slug:"demo-expert",display_name:"Demo Expert",version:"1.0.0"});function buildZip(e){const
|
|
1
|
+
import{describe,it,expect}from"vitest";import AdmZip from"adm-zip";import{parseAndValidateTemplatePackage,TemplatePackageError}from"./agent-template-package.js";const validManifest=JSON.stringify({schema_version:1,slug:"demo-expert",display_name:"Demo Expert",version:"1.0.0"});function buildZip(e){const t=new AdmZip;for(const[a,s]of Object.entries(e))t.addFile(a,Buffer.from(s,"utf8"));return t.toBuffer()}describe("parseAndValidateTemplatePackage",()=>{it("parses a BeeOS package and flattens the workspace/ subdir",()=>{const e=buildZip({"manifest.json":validManifest,"workspace/SOUL.md":"you are a demo","workspace/skills/foo/SKILL.md":"skill foo"}),t=parseAndValidateTemplatePackage(e);expect(t.manifest?.slug).toBe("demo-expert"),expect(t.flattenPrefix).toBe("workspace/"),expect(t.workspaceFiles.get("SOUL.md")?.toString()).toBe("you are a demo"),expect(t.workspaceFiles.get("skills/foo/SKILL.md")?.toString()).toBe("skill foo"),expect(t.workspaceFiles.has("manifest.json")).toBe(!1)}),it("peels both an outer wrapper and the workspace/ subdir",()=>{const e=buildZip({"expert-pkg/manifest.json":validManifest,"expert-pkg/workspace/SOUL.md":"soul"}),t=parseAndValidateTemplatePackage(e);expect(t.flattenPrefix).toBe("expert-pkg/workspace/"),expect(t.workspaceFiles.get("SOUL.md")?.toString()).toBe("soul")}),it("ignores __MACOSX and .DS_Store noise",()=>{const e=buildZip({"manifest.json":validManifest,"workspace/SOUL.md":"soul","__MACOSX/workspace/._SOUL.md":"junk","workspace/.DS_Store":"junk"}),t=parseAndValidateTemplatePackage(e);expect(t.workspaceFiles.has("SOUL.md")).toBe(!0),expect(t.workspaceFiles.size).toBe(1)}),it("never writes IDENTITY.md",()=>{const e=buildZip({"manifest.json":validManifest,"workspace/IDENTITY.md":"name: hacked","workspace/SOUL.md":"soul"}),t=parseAndValidateTemplatePackage(e);expect(t.workspaceFiles.has("IDENTITY.md")).toBe(!1),expect(t.workspaceFiles.has("SOUL.md")).toBe(!0)}),it("carries arbitrary non-bootstrap files verbatim",()=>{const e=buildZip({"SOUL.md":"soul","NOTES.md":"notes","data/config.json":"{}","secrets.env":"TOKEN=1"}),t=parseAndValidateTemplatePackage(e);expect(t.workspaceFiles.get("NOTES.md")?.toString()).toBe("notes"),expect(t.workspaceFiles.get("data/config.json")?.toString()).toBe("{}"),expect(t.workspaceFiles.get("secrets.env")?.toString()).toBe("TOKEN=1")}),it("accepts a QClaw native layout and carries unknown files",()=>{const e=buildZip({"IDENTITY.md":"- Name: Car Agent\n- Vibe: helpful","SOUL.md":"you are a car agent","AGENTS.md":"agents","skills/car-mgr/SKILL.md":"skill","skills/car-mgr/scripts/main.py":"code","skills/car-mgr/_meta.json":'{"name":"car-mgr"}',"memory/notes.jsonl":"{}"}),t=parseAndValidateTemplatePackage(e);expect(t.manifest).toBeNull(),expect(t.flattenPrefix).toBe(""),expect(t.workspaceFiles.get("SOUL.md")?.toString()).toBe("you are a car agent"),expect(t.workspaceFiles.get("AGENTS.md")?.toString()).toBe("agents"),expect(t.workspaceFiles.get("skills/car-mgr/SKILL.md")?.toString()).toBe("skill"),expect(t.workspaceFiles.has("skills/car-mgr/_meta.json")).toBe(!0),expect(t.workspaceFiles.has("memory/notes.jsonl")).toBe(!0),expect(t.workspaceFiles.has("IDENTITY.md")).toBe(!1)}),it("accepts a wrapped QClaw export (single top-level dir, no manifest)",()=>{const e=buildZip({"car-mgr/SOUL.md":"soul","car-mgr/IDENTITY.md":"- Name: Car\n","car-mgr/skills/car-mgr/SKILL.md":"skill"}),t=parseAndValidateTemplatePackage(e);expect(t.flattenPrefix).toBe("car-mgr/"),expect(t.manifest).toBeNull(),expect(t.workspaceFiles.get("SOUL.md")?.toString()).toBe("soul"),expect(t.workspaceFiles.has("IDENTITY.md")).toBe(!1)}),it("skips explicit directory entries (real zip tooling)",()=>{const e=new AdmZip;e.addFile("pkg/",Buffer.alloc(0)),e.addFile("pkg/manifest.json",Buffer.from(validManifest,"utf8")),e.addFile("pkg/workspace/",Buffer.alloc(0)),e.addFile("pkg/workspace/SOUL.md",Buffer.from("soul","utf8")),e.addFile("pkg/workspace/skills/",Buffer.alloc(0)),e.addFile("pkg/workspace/skills/x/",Buffer.alloc(0)),e.addFile("pkg/workspace/skills/x/SKILL.md",Buffer.from("skill","utf8"));const t=parseAndValidateTemplatePackage(e.toBuffer());expect(t.flattenPrefix).toBe("pkg/workspace/"),expect(t.manifest?.slug).toBe("demo-expert"),expect(t.workspaceFiles.get("SOUL.md")?.toString()).toBe("soul"),expect(t.workspaceFiles.get("skills/x/SKILL.md")?.toString()).toBe("skill"),expect(t.workspaceFiles.has("pkg/")).toBe(!1)}),it("recognises an OpenClaw workspace-<profile> wrapper",()=>{const e=buildZip({"workspace-acme/SOUL.md":"soul","workspace-acme/skills/foo/SKILL.md":"skill"}),t=parseAndValidateTemplatePackage(e);expect(t.flattenPrefix).toBe("workspace-acme/"),expect(t.workspaceFiles.get("SOUL.md")?.toString()).toBe("soul"),expect(t.workspaceFiles.get("skills/foo/SKILL.md")?.toString()).toBe("skill")}),it("keeps a skills-only package's skills/ directory intact",()=>{const e=buildZip({"skills/foo/SKILL.md":"skill"}),t=parseAndValidateTemplatePackage(e);expect(t.flattenPrefix).toBe(""),expect(t.workspaceFiles.get("skills/foo/SKILL.md")?.toString()).toBe("skill")}),it("never writes outside the workspace on traversal input",()=>{const e=buildZip({"manifest.json":validManifest,"workspace/SOUL.md":"soul","workspace/../../etc/passwd":"root"});let t;try{t=parseAndValidateTemplatePackage(e)}catch(e){return void expect(e).toBeInstanceOf(TemplatePackageError)}for(const e of t.workspaceFiles.keys())expect(e.startsWith("..")).toBe(!1),expect(e.startsWith("/")).toBe(!1),expect(e.includes("/../")).toBe(!1)}),it("rejects an unsupported schema_version",()=>{const e=buildZip({"manifest.json":JSON.stringify({schema_version:2,slug:"x",display_name:"X",version:"1.0.0"}),"workspace/SOUL.md":"soul"});expect(()=>parseAndValidateTemplatePackage(e)).toThrow(/schema_version/)}),it("rejects an invalid slug in manifest",()=>{const e=buildZip({"manifest.json":JSON.stringify({schema_version:1,slug:"Bad Slug!",display_name:"X",version:"1.0.0"}),"workspace/SOUL.md":"soul"});expect(()=>parseAndValidateTemplatePackage(e)).toThrow(/slug/)}),it("rejects a non-zip buffer",()=>{expect(()=>parseAndValidateTemplatePackage(Buffer.from("not a zip"))).toThrow(TemplatePackageError)}),it("rejects an empty buffer",()=>{expect(()=>parseAndValidateTemplatePackage(Buffer.alloc(0))).toThrow(/empty/)})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.24",
|
|
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>",
|