beeos-claw 0.6.66-cloud.1 → 0.6.66-cloud.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +12 -0
  2. package/dist/a2a/tools.js +1 -1
  3. package/dist/acp-gateway/beeos-file-resolver.js +1 -1
  4. package/dist/acp-gateway/bridge-core.js +1 -1
  5. package/dist/acp-gateway/gateway-events.js +1 -1
  6. package/dist/acp-gateway/local-session-history.js +1 -1
  7. package/dist/acp-gateway/mcp-desired-state-controller.js +1 -1
  8. package/dist/acp-gateway/mcp-desired-state-store.js +1 -1
  9. package/dist/acp-gateway/mcp-service.js +1 -1
  10. package/dist/acp-server.js +1 -1
  11. package/dist/agent-sync.js +1 -1
  12. package/dist/approval/gate.js +1 -0
  13. package/dist/approval/journal.js +1 -0
  14. package/dist/approval/presentation.js +1 -0
  15. package/dist/approval/protocol.generated.js +1 -0
  16. package/dist/approval/turn-policy.js +1 -0
  17. package/dist/auto-reply-tokens.js +1 -1
  18. package/dist/automation/tools.js +1 -1
  19. package/dist/canvas/relay-client.js +1 -1
  20. package/dist/canvas/static-html.js +1 -0
  21. package/dist/canvas/tools.js +1 -1
  22. package/dist/canvas/ws-provider.js +1 -1
  23. package/dist/canvas-orchestrator.js +1 -1
  24. package/dist/chat-envelope.js +1 -0
  25. package/dist/connectors/managed-connector-tools.js +1 -0
  26. package/dist/files/fetch-tool.js +1 -1
  27. package/dist/files/list-tool.js +1 -1
  28. package/dist/gateway-client.js +1 -1
  29. package/dist/im-inbound-handler.js +1 -1
  30. package/dist/im-runtime-dispatch.js +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/mcp-connector-proxy.js +1 -1
  33. package/dist/media/arouter-config.js +1 -1
  34. package/dist/media/image-provider.js +1 -1
  35. package/dist/media/video-provider.js +1 -1
  36. package/dist/message-client-composition.js +1 -1
  37. package/dist/message-projection-inbound-consumer.js +1 -1
  38. package/dist/plugin-registration-plan.js +1 -1
  39. package/dist/runtime/agent-projection-production.js +1 -1
  40. package/dist/runtime/callback-queue.js +1 -1
  41. package/dist/runtime/chat-handler-port.js +1 -0
  42. package/dist/runtime/conversation-title.js +1 -1
  43. package/dist/runtime/core-execution-attempt-context.js +1 -1
  44. package/dist/runtime/deepseek-reasoning-stream.js +1 -1
  45. package/dist/runtime/im-reply-sink.js +1 -1
  46. package/dist/runtime/message-service-conversation-clear-http-authority.js +1 -1
  47. package/dist/runtime/message-service-conversation-run-boundary.js +1 -1
  48. package/dist/runtime/message-service-cron-delivery-http-authority.js +1 -1
  49. package/dist/runtime/message-service-runtime-authenticated-fetch.js +1 -1
  50. package/dist/runtime/message-service-runtime-claim-authority.js +1 -1
  51. package/dist/runtime/message-service-runtime-message-channel.js +1 -1
  52. package/dist/runtime/message-service-session-model-http-authority.js +1 -1
  53. package/dist/runtime/ms-runtime-message-channel-adapter.js +1 -1
  54. package/dist/runtime/ms-runtime-message-channel-consumer-factory.js +1 -1
  55. package/dist/runtime/ms-runtime-rpc-adapter.js +1 -1
  56. package/dist/runtime/native-cloud-chat-composition.js +1 -0
  57. package/dist/runtime/native-cloud-prompt-bridge.js +1 -0
  58. package/dist/runtime/native-input-files-http.js +1 -0
  59. package/dist/runtime/native-input-files.js +1 -0
  60. package/dist/runtime/native-output-file-attempt.js +1 -0
  61. package/dist/runtime/native-prompt-authority.js +1 -0
  62. package/dist/runtime/native-prompt-preflight.js +1 -0
  63. package/dist/runtime/native-session-new-bridge.js +1 -0
  64. package/dist/runtime/openclaw-runtime-capabilities.js +1 -1
  65. package/dist/runtime/openclaw-session-model.js +1 -0
  66. package/dist/runtime/openclaw-session-write-quiescence.js +1 -1
  67. package/dist/runtime/reply-router.js +1 -1
  68. package/dist/runtime/runtime-command-dispatcher.js +1 -1
  69. package/dist/runtime/runtime-error-part.js +1 -0
  70. package/dist/runtime/runtime-local-parity-matrix.js +1 -1
  71. package/dist/runtime/runtime-message-channel-contract.js +1 -1
  72. package/dist/runtime/runtime-operation-resource-resolver.js +1 -1
  73. package/dist/runtime/runtime-parity-manifest-digest.js +1 -1
  74. package/dist/runtime/runtime-parity-manifest.js +1 -1
  75. package/dist/runtime/runtime-production-authority.js +1 -1
  76. package/dist/runtime/runtime-service-contract-compatibility.js +1 -1
  77. package/dist/runtime/runtime-service-production-candidate.js +1 -1
  78. package/dist/runtime/runtime-session-control-service.js +1 -1
  79. package/dist/runtime/runtime-skills-service.js +1 -1
  80. package/dist/runtime/session-new-runtime-delivery-bridge.js +1 -1
  81. package/dist/runtime/session-set-model-mapping.js +1 -1
  82. package/dist/runtime/write-runtime-failure.js +1 -1
  83. package/dist/terminal/runtime-terminal-outbound-registration.js +1 -1
  84. package/dist/terminal/runtime-terminal-service.js +1 -1
  85. package/dist/terminal/terminal-lease-bound-bridge-client.js +1 -1
  86. package/dist/upload/desktop-screenshot-tool.js +1 -1
  87. package/dist/upload/finalize-hook.js +1 -1
  88. package/dist/upload/native-output-tool-hook.js +1 -0
  89. package/dist/upload/tool-artifact-enrichment.js +1 -1
  90. package/dist/upload/tool.js +1 -1
  91. package/dist/upload/upload-file.js +1 -1
  92. package/openclaw.plugin.json +7 -2
  93. package/package.json +8 -7
  94. package/runtime-service-wire-identity.v1.json +5 -5
  95. package/scripts/runtime-blackbox-worker-main.mjs +5 -2
  96. package/dist/agent-auth.js +0 -1
  97. package/dist/bridge-discovery.js +0 -1
  98. package/dist/runtime/agent-gateway-runtime-attempt-authorization.js +0 -1
  99. package/dist/runtime/message-service-runtime-invocation-authority.js +0 -1
  100. package/dist/runtime/openclaw-native-sessions.js +0 -1
  101. package/dist/runtime/openclaw-title-provider.js +0 -1
  102. package/dist/runtime/requested-model.js +0 -1
  103. package/dist/runtime/runtime-jwks-verifier.js +0 -1
  104. package/dist/runtime/runtime-registration.js +0 -1
  105. package/dist/runtime/runtime-transport-failure-policy.js +0 -1
  106. package/dist/runtime/split-runtime-authorization-verifier.js +0 -1
@@ -0,0 +1 @@
1
+ import{closeSync,existsSync,fsyncSync,mkdirSync,openSync,readFileSync,renameSync,writeFileSync}from"node:fs";import{createHash,randomUUID}from"node:crypto";import{join}from"node:path";export class FileApprovalJournal{directory;constructor(r){this.directory=r,mkdirSync(r,{recursive:!0,mode:448})}path(r){return join(this.directory,`${createHash("sha256").update(r).digest("hex")}.json`)}read(r){const e=this.path(r);if(!existsSync(e))return;const n=JSON.parse(readFileSync(e,"utf8"));if(n.request?.requestId!==r)throw new Error("approval_journal_mismatch");return n}write(r){const e=this.path(r.request.requestId),n=`${e}.${randomUUID()}.tmp`,t=openSync(n,"wx",384);try{writeFileSync(t,JSON.stringify(r)),fsyncSync(t)}finally{closeSync(t)}renameSync(n,e);const c=openSync(this.directory,"r");try{fsyncSync(c)}finally{closeSync(c)}}}
@@ -0,0 +1 @@
1
+ import{redact}from"../observability.js";const sensitive=/(?:password|passwd|secret|token|authorization|api[_-]?key|cookie|credential)/iu;export function approvalPresentation(e,t){return function e(t){return null===t||"boolean"==typeof t||"number"==typeof t?t:"string"==typeof t?t.replace(/\bBearer\s+[^\s"']+/giu,"Bearer [redacted]").replace(/\b([A-Z_]*(?:TOKEN|SECRET|PASSWORD|API_KEY)[A-Z_]*=)(?:"[^"]*"|'[^']*'|[^\s;]+)/giu,"$1[redacted]").replace(/(https?:\/\/)[^\s/:]+:[^\s/@]+@/giu,"$1[redacted]@"):Array.isArray(t)?t.map(e):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map(([t,r])=>[t,sensitive.test(t)?"[redacted]":e(r)])):"[unavailable]"}(redact(e,[...t]))}
@@ -0,0 +1 @@
1
+ export const APPROVAL_PROTOCOL="beeos.approval.v1";export const APPROVAL_SETTINGS_RUNTIME_METHOD="session/set_approval_settings";export const APPROVAL_SETTINGS_CANONICAL_COMMAND="session.setApprovalSettings";export const APPROVAL_OPERATION_TYPES=["process.exec","filesystem.mutate","network.request","tool.invoke","external.mutation","privilege.change","sensitive_data.read"];export const APPROVAL_POLICY_MODES=["deny","allowlist","ask","auto_review","full"];export const APPROVAL_ERROR_CODES=["INVALID_APPROVAL_ACTION","APPROVAL_NOT_FOUND","APPROVAL_ALREADY_RESOLVED","APPROVAL_EXPIRED","APPROVAL_REQUEST_MISMATCH","APPROVAL_ACTION_CONFLICT","APPROVAL_UNAVAILABLE","APPROVAL_SETTINGS_UNAVAILABLE","INVALID_APPROVAL_SETTINGS","REVISION_CONFLICT","IDEMPOTENCY_CONFLICT","SETTINGS_UPDATE_IN_PROGRESS","SETTINGS_PROJECTION_PENDING","SETTINGS_PROJECTION_BLOCKED","SESSION_BASELINE_NOT_FOUND","RUNTIME_METHOD_NOT_SUPPORTED","RUNTIME_EXECUTION_FAILED","GUARDRAIL_CONFLICT","UNKNOWN_OPERATION_TYPE","UNKNOWN_POLICY_MODE","RULE_SCOPE_FORBIDDEN"];function record(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)&&(Object.getPrototypeOf(e)===Object.prototype||null===Object.getPrototypeOf(e))}function text(e){return"string"==typeof e&&e.length>0&&e.length<=128&&/^[A-Za-z0-9_.:-]+$/u.test(e)}function revision(e){return Number.isSafeInteger(e)&&e>=0}function exact(e,t){return Object.keys(e).length===t.length&&t.every(t=>Object.hasOwn(e,t))}export function parseApprovalUserAction(e){if(!record(e)||e.protocol!==APPROVAL_PROTOCOL||!text(e.requestId)||!revision(e.requestRevision)||e.requestRevision<1||!text(e.sessionId)||!text(e.taskId)||!text(e.actionId))return null;const t=["protocol","kind","requestId","requestRevision","sessionId","taskId","actionId"];return"approval_decision"===e.kind&&exact(e,[...t,"decision"])&&["approve_once","deny","cancel"].includes(e.decision)||"approval_allow_always"===e.kind&&exact(e,[...t,"scope"])&&"session"===e.scope?structuredClone(e):null}export function parseApprovalSettings(e,t=!1){return!record(e)||Object.keys(e).some(e=>!["defaultMode","operationModes","reviewer"].includes(e))?null:t||exact(e,["defaultMode","operationModes","reviewer"])?Object.hasOwn(e,"defaultMode")&&!APPROVAL_POLICY_MODES.includes(e.defaultMode)||Object.hasOwn(e,"reviewer")&&!["user","auto_review"].includes(e.reviewer)?null:!Object.hasOwn(e,"operationModes")||record(e.operationModes)&&!Object.entries(e.operationModes).some(([e,t])=>!APPROVAL_OPERATION_TYPES.includes(e)||!APPROVAL_POLICY_MODES.includes(t))?structuredClone(e):null:null}
@@ -0,0 +1 @@
1
+ import{AsyncLocalStorage}from"node:async_hooks";import{approvalPresentation}from"./presentation.js";export class ApprovalTurnPolicy{gate;secrets;turns=new Map;current=new AsyncLocalStorage;constructor(e,t=[]){this.gate=e,this.secrets=t}isManaged(e){return Boolean(this.current.getStore()||e.runId&&this.turns.has(e.runId))}enter(e,t,s){if(!e||!t||this.turns.has(t))throw new Error("approval_turn_identity_conflict");const o={sessionKey:e,open:!0,permits:new Map,seenCalls:new Set,context:{...s,active:()=>o.open&&s.active()}};return this.turns.set(t,o),{run:e=>this.current.run(o,e),close:()=>{o.open=!1,this.gate.cancelTask(s.sessionId,s.taskId),o.permits.clear(),this.turns.delete(t)}}}async evaluate(e,t){const s=this.resolve(t);if(!s||!s.context.active()||!t.toolCallId||t.abortSignal?.aborted)return{block:!0,blockReason:"APPROVAL_EXECUTION_CONTEXT_UNAVAILABLE"};if(["sessions_spawn","subagents","delegate_task"].includes(e.toolName))return{block:!0,blockReason:"APPROVAL_DELEGATED_EXECUTION_NOT_SUPPORTED"};if(s.seenCalls.has(t.toolCallId))return{block:!0,blockReason:"APPROVAL_DUPLICATE_TOOL_CALL"};s.seenCalls.add(t.toolCallId);const o=approvalPresentation(e.params,this.secrets),r=await this.gate.request(s.context,e.toolName,e.params,{toolName:e.toolName,arguments:o},t.abortSignal);if(!r)return{block:!0,blockReason:"APPROVAL_OPERATION_NOT_APPROVED"};s.permits.set(t.toolCallId,r)}finalize(e,t){try{const s=this.resolve(t);if(!s||!t.toolCallId||t.abortSignal?.aborted)return{block:!0,blockReason:"APPROVAL_EXECUTION_CONTEXT_UNAVAILABLE"};const o=s.permits.get(t.toolCallId);return s.permits.delete(t.toolCallId),o&&this.gate.consume(o,s.context,e.toolName,e.params)?{params:structuredClone(e.params)}:{block:!0,blockReason:"APPROVAL_EXECUTION_BINDING_CHANGED"}}catch{return{block:!0,blockReason:"APPROVAL_EXECUTION_AUTHORITY_UNAVAILABLE"}}}resolve(e){const t=e.runId?this.turns.get(e.runId):this.current.getStore();return!t||e.sessionKey&&e.sessionKey!==t.sessionKey?void 0:t}}
@@ -1 +1 @@
1
- function escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const HEARTBEAT_TOKEN="HEARTBEAT_OK";export const SILENT_REPLY_TOKEN="NO_REPLY";const HARMONY_CHANNEL_MARKER_RE=/^\s*(?:set-thought\s+)?<[\w]*\|[^>]*>\s*$/,BOX_DRAWING_HR_ONLY_RE=/^\s*─{3,}\s*$/;export function isInternalFormattingArtifact(e){return!!e&&(HARMONY_CHANNEL_MARKER_RE.test(e)||BOX_DRAWING_HR_ONLY_RE.test(e))}function createTokenRegex(e){const t=new Map;return n=>{const i=t.get(n);if(i)return i;const s=e(escapeRegExp(n));return t.set(n,s),s}}const getSilentExactRegex=createTokenRegex(e=>new RegExp(`^\\s*${e}(?:\\s+${e})*\\s*$`,"i")),getSilentTrailingRegex=createTokenRegex(e=>new RegExp(`$(?<=((?:^|\\s+|\\*+)${e}(?:\\s+${e})*\\s*))`,"i"));function stripEdgePunctuation(e){const t=e.match(/^\p{P}+/u)?.[0].length??0,n=e.match(/$(?<=(\p{P}+))/u)?.[1]?.length??0;return e.slice(t,e.length-n)}export function isSilentReplyText(e,t="NO_REPLY"){return!!e&&(getSilentExactRegex(t).test(e)||getSilentExactRegex(t).test(stripEdgePunctuation(e.trim())))}function isSilentReplyJsonStringText(e,t="NO_REPLY"){if(!e)return!1;const n=e.trim();if(!n.startsWith('"')||!n.endsWith('"')||!n.includes(t))return!1;try{const e=JSON.parse(n);return"string"==typeof e&&e.trim()===t}catch{return!1}}export function isSilentReplyEnvelopeText(e,t="NO_REPLY"){if(!e)return!1;const n=e.trim();if(!(n&&n.startsWith("{")&&n.endsWith("}")&&n.includes(t)))return!1;try{const e=JSON.parse(n);if(!e||"object"!=typeof e||Array.isArray(e))return!1;const i=Object.keys(e);return 1===i.length&&"action"===i[0]&&"string"==typeof e.action&&e.action.trim()===t}catch{return!1}}const taggedReasoningPrefixRe=/^\s*<\s*(?:(?:antml:|mm:)?(?:think(?:ing)?|thought)|antthinking)\b[^<>]*>[\s\S]*?<\s*\/\s*(?:(?:antml:|mm:)?(?:think(?:ing)?|thought)|antthinking)\s*>\s*/i,openReasoningPrefixRe=/^\s*<\s*(?:(?:antml:|mm:)?(?:think(?:ing)?|thought)|antthinking)\b[^<>]*>/i,plainReasoningPrefixRe=/^\s*(?:think(?:ing)?|thought|analysis|reasoning)\s*:?\s*\r?\n/i;function stripLeadingReasoningBlocks(e){let t=e;for(;;){const e=t.replace(taggedReasoningPrefixRe,"");if(e===t)return t;t=e}}function stripFinalSilentToken(e,t){const n=escapeRegExp(t),i=e.replace(new RegExp(`(?:^|[\\s*.])${n}\\s*$`,"i"),"").trim();return i===e.trim()?null:i}const silentIntentTextRe=/^\s*(?:i|i'll|i\s+will|i'm|i\s+am|we|we'll|we\s+will|the\s+assistant|assistant|the\s+bot|bot|openclaw)\s+(?:(?:will\s+)?(?:stay|remain|keep|be)\s+(?:quiet|silent)(?:\s+(?:here|for\s+now|on\s+this|in\s+this\s+(?:chat|thread|channel|conversation)))?|(?:do\s+not|don't|dont|will\s+not|won't|would\s+not|should\s+not)\s+(?:reply|respond)(?:\s+(?:here|for\s+now|on\s+this|in\s+this\s+(?:chat|thread|channel|conversation)))?|(?:have|has)\s+nothing\s+(?:to|for)\s+(?:say|add|reply|respond))(?:[.!?]+)?\s*$/i;function hasSilentIntentFinalSilentToken(e,t){const n=stripFinalSilentToken(e,t);return null!==n&&(!n||silentIntentTextRe.test(n))}const substantiveAnswerCueRe=/\b(?:answer|here(?:'s|\s+is)|tell\s+them|you\s+(?:should|can|could|need|must)|please|try|use|send|service\s+is|resolved|retry|yes|no,|sure)\b/i,bareReasoningPlaceholderRe=/^\s*(?:(?:internal|private)\s+)?(?:reasoning|thinking|thoughts?|analysis)(?:\s+notes?)?\s*$/i;function hasPlainReasoningFinalSilentToken(e,t){const n=stripFinalSilentToken(e,t);if(null===n)return!1;if(!n||silentIntentTextRe.test(n))return!0;const i=n.split(/\r?\n/).map(e=>e.trim()).filter(Boolean),s=i.at(-1),r=i.slice(0,-1).join("\n");return Boolean(s&&silentIntentTextRe.test(s)&&r&&!substantiveAnswerCueRe.test(r))||bareReasoningPlaceholderRe.test(n)}function isReasoningPrefixedSilentReplyText(e,t="NO_REPLY"){if(!e)return!1;const n=e.trim();if(!n)return!1;const i=stripLeadingReasoningBlocks(n);if(i!==n)return isSilentReplyText(i,t)||hasSilentIntentFinalSilentToken(i,t);if(openReasoningPrefixRe.test(n)){const e=n.replace(openReasoningPrefixRe,"");return isSilentReplyText(e,t)||hasPlainReasoningFinalSilentToken(e,t)}if(!plainReasoningPrefixRe.test(n))return!1;const s=n.replace(plainReasoningPrefixRe,"");return isSilentReplyText(s,t)||hasPlainReasoningFinalSilentToken(s,t)}export function isSilentReplyPayloadText(e,t="NO_REPLY"){return isSilentReplyText(e,t)||isSilentReplyJsonStringText(e,t)||isSilentReplyEnvelopeText(e,t)||isReasoningPrefixedSilentReplyText(e,t)}export function stripSilentToken(e,t="NO_REPLY"){const n=getSilentTrailingRegex(t).exec(e)?.[1]?.length??0;return e.slice(0,e.length-n).trim()}const getSilentLeadingAttachedRegex=createTokenRegex(e=>new RegExp(`^\\s*(?:${e}\\s+)*${e}(?=[\\p{L}\\p{N}])`,"iu")),getSilentLeadingRegex=createTokenRegex(e=>new RegExp(`^\\s*${e}((?:\\s*${e})*)(\\s*)`,"i"));export function stripLeadingSilentToken(e,t="NO_REPLY"){return e.replace(getSilentLeadingRegex(t),"").trim()}export function startsWithSilentToken(e,t="NO_REPLY"){if(!e)return!1;if(getSilentLeadingAttachedRegex(t).test(e))return!0;const n=getSilentLeadingRegex(t).exec(e);return!(!n||!/[\r\n]/.test(n[2]??""))&&e.slice(n[0].length).trimStart().length>0}export function isSilentReplyPrefixText(e,t="NO_REPLY"){if(!e)return!1;const n=t.toUpperCase(),i=e.trimStart();if(!i||i.length>n.length)return!1;const s=i.toUpperCase();return i===s&&(!(s.length<2)&&(!!n.startsWith(s)&&(!!s.includes("_")||(s===n||(/[^A-Z_]/.test(n)?/[^A-Z_]/.test(s):"NO_REPLY"===n&&"NO"===s)))))}const HEARTBEAT_TOKEN_AT_END_RE=new RegExp(`${escapeRegExp("HEARTBEAT_OK")}[^\\w]{0,4}$`);function stripTokenAtEdges(e){let t=e.trim();if(!t)return{text:"",didStrip:!1};if(!t.includes("HEARTBEAT_OK"))return{text:t,didStrip:!1};let n=!1,i=!0;for(;i;){i=!1;const e=t.trim();if(e.startsWith("HEARTBEAT_OK"))t=e.slice(12).trimStart(),n=!0,i=!0;else if(HEARTBEAT_TOKEN_AT_END_RE.test(e)){const s=e.lastIndexOf("HEARTBEAT_OK"),r=e.slice(0,s).trimEnd();t=r?`${r}${e.slice(s+12).trimStart()}`.trimEnd():"",n=!0,i=!0}}return{text:t.replace(/\s+/g," ").trim(),didStrip:n}}function stripHeartbeatMarkup(e){return e.replace(/<[^>]*>/g," ").replace(/&nbsp;/gi," ").replace(/^[*`~_]+/,"").replace(/[*`~_]+$/,"")}export function stripHeartbeatToken(e,t={}){if(!e)return{shouldSkip:!0,text:"",didStrip:!1};const n=e.trim();if(!n)return{shouldSkip:!0,text:"",didStrip:!1};const i=t.mode??"message",s=t.maxAckChars,r="string"==typeof s?Number(s):s,o=Math.max(0,"number"==typeof r&&Number.isFinite(r)?r:300),l=stripHeartbeatMarkup(n);if(!n.includes("HEARTBEAT_OK")&&!l.includes("HEARTBEAT_OK"))return{shouldSkip:!1,text:n,didStrip:!1};const a=stripTokenAtEdges(n),p=stripTokenAtEdges(l),c=a.didStrip&&a.text?a:p;if(!c.didStrip)return{shouldSkip:!1,text:n,didStrip:!1};if(!c.text)return{shouldSkip:!0,text:"",didStrip:!0};const u=c.text.trim();return"heartbeat"===i&&u.length<=o?{shouldSkip:!0,text:"",didStrip:!0}:{shouldSkip:!1,text:u,didStrip:!0}}export function normalizeStreamingText(e,t={}){if(!e)return{skip:!0};let n=e;if(!t.isHeartbeat&&n.includes("HEARTBEAT_OK")){const e=stripHeartbeatToken(n,{mode:"message"});if(e.didStrip&&t.onHeartbeatStrip?.(),e.shouldSkip)return{skip:!0};n=e.text}return isSilentReplyText(n,"NO_REPLY")||isSilentReplyPrefixText(n,"NO_REPLY")||isSilentReplyPrefixText(n,"HEARTBEAT_OK")?{skip:!0}:(n&&startsWithSilentToken(n,"NO_REPLY")&&(n=stripLeadingSilentToken(n,"NO_REPLY")),n?{skip:!1,text:n}:{skip:!0})}
1
+ export const HEARTBEAT_TOKEN="HEARTBEAT_OK";export const SILENT_REPLY_TOKEN="NO_REPLY";function escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const silentExactRegexByToken=new Map,silentTrailingRegexByToken=new Map,silentLeadingAttachedRegexByToken=new Map,silentLeadingRegexByToken=new Map;function getSilentExactRegex(e){const t=silentExactRegexByToken.get(e);if(t)return t;const i=new RegExp(`^\\s*${escapeRegExp(e)}\\s*$`,"i");return silentExactRegexByToken.set(e,i),i}function getSilentTrailingRegex(e){const t=silentTrailingRegexByToken.get(e);if(t)return t;const i=new RegExp(`(?:^|\\s+|\\*+)${escapeRegExp(e)}\\s*$`,"i");return silentTrailingRegexByToken.set(e,i),i}function getSilentLeadingAttachedRegex(e){const t=silentLeadingAttachedRegexByToken.get(e);if(t)return t;const i=escapeRegExp(e),n=new RegExp(`^\\s*(?:${i}\\s+)*${i}(?=[\\p{L}\\p{N}])`,"iu");return silentLeadingAttachedRegexByToken.set(e,n),n}function getSilentLeadingRegex(e){const t=silentLeadingRegexByToken.get(e);if(t)return t;const i=new RegExp(`^(?:\\s*${escapeRegExp(e)})+\\s*`,"i");return silentLeadingRegexByToken.set(e,i),i}export function isSilentReplyText(e,t="NO_REPLY"){return!!e&&getSilentExactRegex(t).test(e)}export function isSilentReplyEnvelopeText(e,t="NO_REPLY"){if(!e)return!1;const i=e.trim();if(!(i&&i.startsWith("{")&&i.endsWith("}")&&i.includes(t)))return!1;try{const e=JSON.parse(i);if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e,r=Object.keys(n);return 1===r.length&&"action"===r[0]&&"string"==typeof n.action&&n.action.trim()===t}catch{return!1}}export function isSilentReplyPayloadText(e,t="NO_REPLY"){return isSilentReplyText(e,t)||isSilentReplyEnvelopeText(e,t)}export function stripSilentToken(e,t="NO_REPLY"){return e.replace(getSilentTrailingRegex(t),"").trim()}export function stripLeadingSilentToken(e,t="NO_REPLY"){return e.replace(getSilentLeadingRegex(t),"").trim()}export function startsWithSilentToken(e,t="NO_REPLY"){return!!e&&getSilentLeadingAttachedRegex(t).test(e)}export function isSilentReplyPrefixText(e,t="NO_REPLY"){if(!e)return!1;const i=e.trimStart();if(!i)return!1;if(i!==i.toUpperCase())return!1;const n=i.toUpperCase();if(!n)return!1;if(n.length<2)return!1;if(/[^A-Z_]/.test(n))return!1;const r=t.toUpperCase();return!!r.startsWith(n)&&(!!n.includes("_")||"NO_REPLY"===r&&"NO"===n)}const HEARTBEAT_TOKEN_AT_END_RE=new RegExp(`${escapeRegExp("HEARTBEAT_OK")}[^\\w]{0,4}$`);function stripTokenAtEdges(e){let t=e.trim();if(!t)return{text:"",didStrip:!1};if(!t.includes("HEARTBEAT_OK"))return{text:t,didStrip:!1};let i=!1,n=!0;for(;n;){n=!1;const e=t.trim();if(e.startsWith("HEARTBEAT_OK"))t=e.slice(12).trimStart(),i=!0,n=!0;else if(HEARTBEAT_TOKEN_AT_END_RE.test(e)){const r=e.lastIndexOf("HEARTBEAT_OK"),s=e.slice(0,r).trimEnd();t=s?`${s}${e.slice(r+12).trimStart()}`.trimEnd():"",i=!0,n=!0}}return{text:t.replace(/\s+/g," ").trim(),didStrip:i}}function stripHeartbeatMarkup(e){return e.replace(/<[^>]*>/g," ").replace(/&nbsp;/gi," ").replace(/^[*`~_]+/,"").replace(/[*`~_]+$/,"")}export function stripHeartbeatToken(e,t={}){if(!e)return{shouldSkip:!0,text:"",didStrip:!1};const i=e.trim();if(!i)return{shouldSkip:!0,text:"",didStrip:!1};const n=t.mode??"message",r=t.maxAckChars,s="string"==typeof r?Number(r):r,p=Math.max(0,"number"==typeof s&&Number.isFinite(s)?s:300),o=stripHeartbeatMarkup(i);if(!i.includes("HEARTBEAT_OK")&&!o.includes("HEARTBEAT_OK"))return{shouldSkip:!1,text:i,didStrip:!1};const a=stripTokenAtEdges(i),l=stripTokenAtEdges(o),c=a.didStrip&&a.text?a:l;if(!c.didStrip)return{shouldSkip:!1,text:i,didStrip:!1};if(!c.text)return{shouldSkip:!0,text:"",didStrip:!0};const x=c.text.trim();return"heartbeat"===n&&x.length<=p?{shouldSkip:!0,text:"",didStrip:!0}:{shouldSkip:!1,text:x,didStrip:!0}}export function normalizeStreamingText(e,t={}){if(!e)return{skip:!0};let i=e;if(!t.isHeartbeat&&i.includes("HEARTBEAT_OK")){const e=stripHeartbeatToken(i,{mode:"message"});if(e.didStrip&&t.onHeartbeatStrip?.(),e.shouldSkip)return{skip:!0};i=e.text}return isSilentReplyText(i,"NO_REPLY")||isSilentReplyPrefixText(i,"NO_REPLY")||isSilentReplyPrefixText(i,"HEARTBEAT_OK")?{skip:!0}:(i&&startsWithSilentToken(i,"NO_REPLY")&&(i=stripLeadingSilentToken(i,"NO_REPLY")),i?{skip:!1,text:i}:{skip:!0})}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{agentFetch}from"../agent-auth.js";const AUTOMATIONS_PATH="/api/v1/automations",WEBHOOK_AUTOMATIONS_PATH="/api/v1/automations/webhooks",CONNECTOR_MCP_PATH="/api/v1/agent/connectors/mcp",UUID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function asObject(e){return e&&"object"==typeof e&&!Array.isArray(e)?e:{}}function stringValue(e){return"string"==typeof e&&e.trim()?e.trim():void 0}function toolResult(e){return{content:JSON.stringify(e,null,2)}}function isUuid(e){return Boolean(e&&UUID_RE.test(e))}function resolveTarget(e){const t=e.getDefaultAgent?.(),r=stringValue(t?.instanceId),o=stringValue(t?.id);if(r&&o)return{instanceId:r,agentId:o}}function resolveConversationId(e,t){const r=stringValue(e.getCurrentConversationId?.(t));return isUuid(r)?r:void 0}async function request(e,t,r={}){const o=`${e.agentGatewayUrl.replace(/\/+$/,"")}${t}`;try{const t=e.agentKeys?await agentFetch(o,e.agentKeys,r):await fetch(o,r),n=await t.text();let a=n;try{a=n?JSON.parse(n):{}}catch{}if(!t.ok)return{ok:!1,status:t.status,error:`Automation request failed: HTTP ${t.status}`,details:a};const s=asObject(a);return{ok:!0,data:Object.hasOwn(s,"data")?s.data:a}}catch(e){return{ok:!1,status:0,error:e instanceof Error?e.message:String(e),details:void 0}}}function jsonRequestBody(e){return{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}}function buildSchedule(e){const t=stringValue(e.kind)??"every",r={kind:t,tz:stringValue(e.tz)??"UTC"};switch(t){case"every":{r.intervalSeconds="number"==typeof e.intervalSeconds?String(Math.trunc(e.intervalSeconds)):stringValue(e.intervalSeconds)??"3600";const t=stringValue(e.anchorAt);t&&(r.anchorAt=t);break}case"at":{const t=stringValue(e.at);t&&(r.at=t);break}case"cron":{const t=stringValue(e.expression);t&&(r.expression=t);break}default:return{error:"schedule.kind must be one of at, every, or cron"}}return r}function buildSession(e,t,r){const o=stringValue(asObject(e.session).mode)??"new";if("new"!==o&&"bound"!==o)return{error:"session.mode must be new or bound"};const n={mode:o},a=resolveConversationId(t,r);if("bound"===o){if(!a)return{error:"bound Automation requires the current conversation context"};n.boundConversationId=a}return n}function createInput(e,t,r){const o=asObject(e),n=stringValue(o.name),a=stringValue(o.prompt),s=resolveTarget(t);if(!n||!a||!s)return{error:"name, prompt, and a resolvable target agent are required"};const i=buildSchedule(asObject(o.schedule));if("error"in i)return i;const u=buildSession(o,t,r);if("error"in u)return u;const c={name:n,prompt:a,target:s,schedule:i,session:u},l=stringValue(o.validFrom),d=stringValue(o.validUntil);l&&(c.validFrom=l),d&&(c.validUntil=d);const p=resolveConversationId(t,r);return p&&(c.createdInConversationId=p),c}function createWebhookInput(e,t,r){const o=asObject(e),n=stringValue(o.name),a=stringValue(o.prompt),s=(stringValue(o.source)??"").toLowerCase(),i=resolveTarget(t);if(!n||!a||!i)return{error:"name, prompt, and a resolvable target agent are required"};if("github"!==s&&"gmail"!==s)return{error:"source must be github or gmail"};const u=o.events,c=Array.isArray(u)?u.filter(e=>"string"==typeof e&&Boolean(e.trim())).map(e=>e.trim()):[],l=stringValue(o.repoOwner),d=stringValue(o.repoName),p=stringValue(o.mailbox);if(!("github"!==s||0!==c.length&&l&&d))return{error:"github webhook requires events, repoOwner, and repoName"};if("gmail"===s&&!p)return{error:"gmail webhook requires mailbox"};const m=buildSession(o,t,r);if("error"in m)return m;const g={name:n,prompt:a,target:i,session:m,source:s};"github"===s?(g.events=c,g.repoOwner=l,g.repoName=d):g.mailbox=p;const y=stringValue(o.eventFilter);y&&(g.eventFilter=y);const f=stringValue(o.validFrom),b=stringValue(o.validUntil);f&&(g.validFrom=f),b&&(g.validUntil=b);const h=resolveConversationId(t,r);return h&&(g.createdInConversationId=h),g}function unwrapConnectorMcp(e){let t=e;if("string"==typeof t)try{t=JSON.parse(t)}catch{return}if(!t||"object"!=typeof t||Array.isArray(t))return;const r=t;if(null!=r.error)return;let o=r.result??r;if(!o||"object"!=typeof o||Array.isArray(o))return;const n=o;if(n.isError)return;const a=n.structuredContent;if(a&&"object"==typeof a&&!Array.isArray(a))return a;const s=n.content;if(Array.isArray(s))for(const e of s){if(!e||"object"!=typeof e||Array.isArray(e))continue;const t=e;if("text"===t.type&&"string"==typeof t.text)try{const e=JSON.parse(t.text);if(e&&"object"==typeof e&&!Array.isArray(e))return e}catch{continue}}return n}async function runPreflight(e,t,r){const o=asObject(t),n=(stringValue(o.source)??"").toLowerCase(),a=resolveConversationId(e,r),s=resolveTarget(e),i={source:n,operation:"webhook",ready:!1,platform:{reachable:!1,authorized:!1},connector:{id:n,installed:!1,credentialStatus:"unknown"},operationSupported:"github"===n||"gmail"===n,target:{resolved:Boolean(s)},session:{canBindCurrentConversation:Boolean(a)},nextAction:"retry_or_check_connection"};if("github"!==n&&"gmail"!==n)return i.blockingReason="unsupported_source",i.nextAction="retry_or_check_connection",i;if("gmail"===n&&!stringValue(o.mailbox))return i.blockingReason="mailbox_required",i.nextAction="provide_mailbox",i;if("github"===n){const e=o.events,t=Array.isArray(e)&&e.some(e=>"string"==typeof e&&e.trim());if(!stringValue(o.repoOwner)||!stringValue(o.repoName)||!t)return i.blockingReason="github_webhook_input_required",i.nextAction="provide_repository_and_events",i}const u=await request(e,`${AUTOMATIONS_PATH}?limit=1`,{method:"GET"});if(!u.ok)return i.blockingReason="automation_gateway_unavailable",i.platform={reachable:!1,authorized:!1,status:u.status,error:u.error},i.nextAction="retry_or_check_connection",i;i.platform={reachable:!0,authorized:!0};const c={jsonrpc:"2.0",id:randomUUID(),method:"tools/call",params:{name:"list_connectors",arguments:{}}},l=await request(e,CONNECTOR_MCP_PATH,{...jsonRequestBody(c),headers:{"Content-Type":"application/json",Accept:"application/json"}});if(!l.ok)return i.blockingReason="mcp_unavailable",i.connector={id:n,installed:!1,credentialStatus:"unknown",error:l.error,status:l.status},i.nextAction="retry_or_check_connection",i;const d=unwrapConnectorMcp(l.data),p=d&&Array.isArray(d.connectors)?d.connectors:void 0;let m;if(Array.isArray(p))for(const e of p){if(!e||"object"!=typeof e||Array.isArray(e))continue;const t=e;if((stringValue(t.connectorId)??stringValue(t.connector_id)??"").toLowerCase()===n){m=t;break}}if(!m)return i.blockingReason="mcp_not_connected",i.nextAction="install_connector",i;const g=(stringValue(m.credentialStatus)??stringValue(m.credential_status)??"unknown").toLowerCase();return i.connector={id:n,installed:!0,credentialStatus:g},"active"!==g?(i.blockingReason="credential_missing",i.nextAction="authorize_connector",i):s?(i.ready=!0,i.nextAction="create_automation",i.sideEffects="gmail"===n?["create_rule","ensure_shared_gmail_watch"]:["create_rule","issue_hook","register_connector_webhook"],i):(i.blockingReason="target_not_resolved",i.nextAction="sync_openclaw_agent",i)}async function runCapabilities(e,t){const r=resolveTarget(e),o=resolveConversationId(e,t),n={platform:{reachable:!1,authorized:!1},capabilities:{createSchedule:Boolean(r),createWebhook:Boolean(r),run:!0},target:{resolved:Boolean(r)},session:{currentConversationAvailable:Boolean(o),canBindCurrentConversation:Boolean(o)}},a=await request(e,`${AUTOMATIONS_PATH}?limit=1`,{method:"GET"});return a.ok?n.platform={reachable:!0,authorized:!0}:n.platform={reachable:!1,authorized:!1,status:a.status,error:a.error},n}function requestErrorResult(e){return toolResult({error:e.error,status:e.status,details:e.details})}export function registerAutomationTools(e,t){e.registerTool({name:"beeos_automation_create",description:"Create a BeeOS platform Automation owned by the current user. The target is the current Agent and is resolved automatically. Prefer session.mode=bound for a task created in this chat.",parameters:{type:"object",properties:{name:{type:"string",description:"Human-readable automation name"},prompt:{type:"string",description:"Prompt sent to the target agent when the automation fires"},schedule:{type:"object",properties:{kind:{type:"string",enum:["at","every","cron"]},tz:{type:"string"},at:{type:"string"},intervalSeconds:{type:"integer",minimum:1},anchorAt:{type:"string"},expression:{type:"string"}}},session:{type:"object",description:"Always set session.mode. Prefer bound for tasks created in the current BeeOS chat so every run returns here. Use new if the user requests a separate conversation per run or the current chat cannot bind. The plugin fills the bound conversation ID; do not supply it.",properties:{mode:{type:"string",enum:["new","bound"]}}},validFrom:{type:"string"},validUntil:{type:"string"}},required:["name","prompt"]},async execute(e,r,o){const n=createInput(r,t,e);if("error"in n)return toolResult(n);try{const e=await request(t,AUTOMATIONS_PATH,{...jsonRequestBody(n),signal:o});return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return t.logger?.warn?.(`[beeos_automation_create] ${e instanceof Error?e.message:String(e)}`),toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_create_webhook",description:"Create a BeeOS platform Webhook Automation. GitHub uses source/events/repoOwner/repoName; Gmail uses source/mailbox only (never hookId/hookUrl/repo*). Runs preflight first. Prefer session.mode=bound for a task created in this chat.",parameters:{type:"object",properties:{name:{type:"string"},prompt:{type:"string"},source:{type:"string",enum:["github","gmail"]},events:{type:"array",items:{type:"string"},minItems:1},repoOwner:{type:"string"},repoName:{type:"string"},eventFilter:{type:"string"},mailbox:{type:"string"},session:{type:"object",description:"Always set session.mode. Prefer bound for tasks created in the current BeeOS chat so every event returns here. Use new if the user requests a separate conversation per event or the current chat cannot bind. The plugin fills the bound conversation ID; do not supply it.",properties:{mode:{type:"string",enum:["new","bound"]}}},validFrom:{type:"string"},validUntil:{type:"string"}},required:["name","prompt","source"]},async execute(e,r,o){const n=createWebhookInput(r,t,e);if("error"in n)return toolResult(n);try{const a=await runPreflight(t,r,e);if(!a.ready)return toolResult({error:"automation_preflight_failed",preflight:a});const s=await request(t,WEBHOOK_AUTOMATIONS_PATH,{...jsonRequestBody(n),signal:o});return s.ok?toolResult(s.data):requestErrorResult(s)}catch(e){return t.logger?.warn?.(`[beeos_automation_create_webhook] ${e instanceof Error?e.message:String(e)}`),toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_capabilities",description:"Check whether the BeeOS Automation platform is reachable/authorized and whether the current chat can bind as an Automation session. Read-only.",parameters:{type:"object",properties:{}},async execute(e){try{return toolResult(await runCapabilities(t,e))}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_preflight",description:"Read-only readiness check for creating a BeeOS Webhook Automation. Returns nextAction such as authorize_connector, install_connector, provide_mailbox, provide_repository_and_events, sync_openclaw_agent, retry_or_check_connection, or create_automation.",parameters:{type:"object",properties:{source:{type:"string",enum:["github","gmail"]},events:{type:"array",items:{type:"string"}},repoOwner:{type:"string"},repoName:{type:"string"},mailbox:{type:"string"},session:{type:"object",properties:{mode:{type:"string",enum:["new","bound"]}}}},required:["source"]},async execute(e,r){try{return toolResult(await runPreflight(t,r,e))}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_list",description:"List BeeOS platform Automations owned by the current user.",parameters:{type:"object",properties:{status:{type:"string",enum:["enabled","paused","expired"]},limit:{type:"integer",minimum:1,maximum:100},cursor:{type:"string"}}},async execute(e,r,o){const n=asObject(r),a=new URLSearchParams;for(const e of["status","cursor"]){const t=stringValue(n[e]);t&&a.set(e,t)}"number"==typeof n.limit&&Number.isFinite(n.limit)&&a.set("limit",String(Math.trunc(n.limit)));try{const e=await request(t,`${AUTOMATIONS_PATH}?${a}`,{signal:o});return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_get",description:"Get one BeeOS platform Automation by its automation ID.",parameters:{type:"object",properties:{automationId:{type:"string"}},required:["automationId"]},async execute(e,r,o){const n=stringValue(asObject(r).automationId);if(!n)return toolResult({error:"automationId is required"});try{const e=await request(t,`${AUTOMATIONS_PATH}/${encodeURIComponent(n)}`,{signal:o});return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_run",description:"Trigger an existing BeeOS platform Automation immediately and return its Run record.",parameters:{type:"object",properties:{automationId:{type:"string"},requestKey:{type:"string",description:"Optional idempotency key; generated when omitted"},sourceRunId:{type:"string"},rerunSessionMode:{type:"string",enum:["original","new"]}},required:["automationId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId);if(!a)return toolResult({error:"automationId is required"});const s=stringValue(n.requestKey)??randomUUID(),i=stringValue(n.sourceRunId),u=stringValue(n.rerunSessionMode),c={};i&&(c.sourceRunId=i),u&&(c.rerunSessionMode=u);try{const e=await request(t,`${AUTOMATIONS_PATH}/${encodeURIComponent(a)}/runs`,{...jsonRequestBody(c),headers:{"Content-Type":"application/json","Idempotency-Key":s},signal:o});return e.ok?toolResult({requestKey:s,data:e.data}):toolResult({error:e.error,status:e.status,details:e.details,requestKey:s})}catch(e){return toolResult({error:e instanceof Error?e.message:String(e),requestKey:s})}}}),e.registerTool({name:"beeos_automation_runs",description:"List Runs for one BeeOS platform Automation.",parameters:{type:"object",properties:{automationId:{type:"string"},status:{type:"string",enum:["scheduled","dispatched","failed","skipped"]},limit:{type:"integer",minimum:1,maximum:100},cursor:{type:"string"}},required:["automationId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId);if(!a)return toolResult({error:"automationId is required"});const s=new URLSearchParams;for(const e of["status","cursor"]){const t=stringValue(n[e]);t&&s.set(e,t)}"number"==typeof n.limit&&Number.isFinite(n.limit)&&s.set("limit",String(Math.trunc(n.limit)));try{const e=await request(t,`${AUTOMATIONS_PATH}/${encodeURIComponent(a)}/runs?${s}`,{signal:o});return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_get_run",description:"Get one Run record for a BeeOS platform Automation.",parameters:{type:"object",properties:{automationId:{type:"string"},runId:{type:"string"}},required:["automationId","runId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId),s=stringValue(n.runId);if(!a||!s)return toolResult({error:"automationId and runId are required"});try{const e=await request(t,`${AUTOMATIONS_PATH}/${encodeURIComponent(a)}/runs/${encodeURIComponent(s)}`,{signal:o});return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}})}
1
+ import{randomUUID}from"node:crypto";import{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";const UUID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function asObject(e){return e&&"object"==typeof e&&!Array.isArray(e)?e:{}}function stringValue(e){return"string"==typeof e&&e.trim()?e.trim():void 0}function toolResult(e){return{content:JSON.stringify(e,null,2)}}function isUuid(e){return Boolean(e&&UUID_RE.test(e))}function resolveTarget(e){const t=e.getDefaultAgent?.(),r=stringValue(t?.instanceId),o=stringValue(t?.id);if(r&&o)return{instanceId:r,agentId:o}}function resolveConversationId(e,t){const r=stringValue(e.getCurrentConversationId?.(t));return isUuid(r)?r:void 0}async function request(e){try{const t=await e()??{},r=asObject(t);return{ok:!0,data:Object.hasOwn(r,"data")?r.data:t}}catch(e){if(e instanceof CloudHttpError){let t=e.body;try{t=e.body?JSON.parse(e.body):{}}catch{}return{ok:!1,status:e.status,error:`Automation request failed: HTTP ${e.status}`,details:t}}return{ok:!1,status:0,error:e instanceof Error?e.message:String(e),details:void 0}}}function listQuery(e){const t={};for(const r of["status","cursor"]){const o=stringValue(e[r]);o&&(t[r]=o)}return"number"==typeof e.limit&&Number.isFinite(e.limit)&&(t.limit=String(Math.trunc(e.limit))),t}function buildSchedule(e){const t=stringValue(e.kind)??"every",r={kind:t,tz:stringValue(e.tz)??"UTC"};switch(t){case"every":{r.intervalSeconds="number"==typeof e.intervalSeconds?String(Math.trunc(e.intervalSeconds)):stringValue(e.intervalSeconds)??"3600";const t=stringValue(e.anchorAt);t&&(r.anchorAt=t);break}case"at":{const t=stringValue(e.at);t&&(r.at=t);break}case"cron":{const t=stringValue(e.expression);t&&(r.expression=t);break}default:return{error:"schedule.kind must be one of at, every, or cron"}}return r}function buildSession(e,t,r){const o=stringValue(asObject(e.session).mode)??"new";if("new"!==o&&"bound"!==o)return{error:"session.mode must be new or bound"};const n={mode:o},a=resolveConversationId(t,r);if("bound"===o){if(!a)return{error:"bound Automation requires the current conversation context"};n.boundConversationId=a}return n}function createInput(e,t,r){const o=asObject(e),n=stringValue(o.name),a=stringValue(o.prompt),s=resolveTarget(t);if(!n||!a||!s)return{error:"name, prompt, and a resolvable target agent are required"};const i=buildSchedule(asObject(o.schedule));if("error"in i)return i;const u=buildSession(o,t,r);if("error"in u)return u;const c={name:n,prompt:a,target:s,schedule:i,session:u},l=stringValue(o.validFrom),d=stringValue(o.validUntil);l&&(c.validFrom=l),d&&(c.validUntil=d);const m=resolveConversationId(t,r);return m&&(c.createdInConversationId=m),c}function createWebhookInput(e,t,r){const o=asObject(e),n=stringValue(o.name),a=stringValue(o.prompt),s=(stringValue(o.source)??"").toLowerCase(),i=resolveTarget(t);if(!n||!a||!i)return{error:"name, prompt, and a resolvable target agent are required"};if("github"!==s&&"gmail"!==s)return{error:"source must be github or gmail"};const u=o.events,c=Array.isArray(u)?u.filter(e=>"string"==typeof e&&Boolean(e.trim())).map(e=>e.trim()):[],l=stringValue(o.repoOwner),d=stringValue(o.repoName),m=stringValue(o.mailbox);if(!("github"!==s||0!==c.length&&l&&d))return{error:"github webhook requires events, repoOwner, and repoName"};if("gmail"===s&&!m)return{error:"gmail webhook requires mailbox"};const p=buildSession(o,t,r);if("error"in p)return p;const g={name:n,prompt:a,target:i,session:p,source:s};"github"===s?(g.events=c,g.repoOwner=l,g.repoName=d):g.mailbox=m;const y=stringValue(o.eventFilter);y&&(g.eventFilter=y);const f=stringValue(o.validFrom),b=stringValue(o.validUntil);f&&(g.validFrom=f),b&&(g.validUntil=b);const h=resolveConversationId(t,r);return h&&(g.createdInConversationId=h),g}function unwrapConnectorMcp(e){let t=e;if("string"==typeof t)try{t=JSON.parse(t)}catch{return}if(!t||"object"!=typeof t||Array.isArray(t))return;const r=t;if(null!=r.error)return;let o=r.result??r;if(!o||"object"!=typeof o||Array.isArray(o))return;const n=o;if(n.isError)return;const a=n.structuredContent;if(a&&"object"==typeof a&&!Array.isArray(a))return a;const s=n.content;if(Array.isArray(s))for(const e of s){if(!e||"object"!=typeof e||Array.isArray(e))continue;const t=e;if("text"===t.type&&"string"==typeof t.text)try{const e=JSON.parse(t.text);if(e&&"object"==typeof e&&!Array.isArray(e))return e}catch{continue}}return n}async function runPreflight(e,t,r){const o=asObject(t),n=(stringValue(o.source)??"").toLowerCase(),a=resolveConversationId(e,r),s=resolveTarget(e),i={source:n,operation:"webhook",ready:!1,platform:{reachable:!1,authorized:!1},connector:{id:n,installed:!1,credentialStatus:"unknown"},operationSupported:"github"===n||"gmail"===n,target:{resolved:Boolean(s)},session:{canBindCurrentConversation:Boolean(a)},nextAction:"retry_or_check_connection"};if("github"!==n&&"gmail"!==n)return i.blockingReason="unsupported_source",i.nextAction="retry_or_check_connection",i;if("gmail"===n&&!stringValue(o.mailbox))return i.blockingReason="mailbox_required",i.nextAction="provide_mailbox",i;if("github"===n){const e=o.events,t=Array.isArray(e)&&e.some(e=>"string"==typeof e&&e.trim());if(!stringValue(o.repoOwner)||!stringValue(o.repoName)||!t)return i.blockingReason="github_webhook_input_required",i.nextAction="provide_repository_and_events",i}const u=await request(()=>e.gateway.automations.list({limit:1}));if(!u.ok)return i.blockingReason="automation_gateway_unavailable",i.platform={reachable:!1,authorized:!1,status:u.status,error:u.error},i.nextAction="retry_or_check_connection",i;i.platform={reachable:!0,authorized:!0};const c={jsonrpc:"2.0",id:randomUUID(),method:"tools/call",params:{name:"list_connectors",arguments:{}}},l=await request(()=>e.gateway.connectors.callJson(c));if(!l.ok)return i.blockingReason="mcp_unavailable",i.connector={id:n,installed:!1,credentialStatus:"unknown",error:l.error,status:l.status},i.nextAction="retry_or_check_connection",i;const d=unwrapConnectorMcp(l.data),m=d&&Array.isArray(d.connectors)?d.connectors:void 0;let p;if(Array.isArray(m))for(const e of m){if(!e||"object"!=typeof e||Array.isArray(e))continue;const t=e;if((stringValue(t.connectorId)??stringValue(t.connector_id)??"").toLowerCase()===n){p=t;break}}if(!p)return i.blockingReason="mcp_not_connected",i.nextAction="install_connector",i;const g=(stringValue(p.credentialStatus)??stringValue(p.credential_status)??"unknown").toLowerCase();return i.connector={id:n,installed:!0,credentialStatus:g},"active"!==g?(i.blockingReason="credential_missing",i.nextAction="authorize_connector",i):s?(i.ready=!0,i.nextAction="create_automation",i.sideEffects="gmail"===n?["create_rule","ensure_shared_gmail_watch"]:["create_rule","issue_hook","register_connector_webhook"],i):(i.blockingReason="target_not_resolved",i.nextAction="sync_openclaw_agent",i)}async function runCapabilities(e,t){const r=resolveTarget(e),o=resolveConversationId(e,t),n={platform:{reachable:!1,authorized:!1},capabilities:{createSchedule:Boolean(r),createWebhook:Boolean(r),run:!0},target:{resolved:Boolean(r)},session:{currentConversationAvailable:Boolean(o),canBindCurrentConversation:Boolean(o)}},a=await request(()=>e.gateway.automations.list({limit:1}));return a.ok?n.platform={reachable:!0,authorized:!0}:n.platform={reachable:!1,authorized:!1,status:a.status,error:a.error},n}function requestErrorResult(e){return toolResult({error:e.error,status:e.status,details:e.details})}export function registerAutomationTools(e,t){e.registerTool({name:"beeos_automation_create",description:"Create a BeeOS platform Automation owned by the current user. The target is the current Agent and is resolved automatically. Prefer session.mode=bound for a task created in this chat.",parameters:{type:"object",properties:{name:{type:"string",description:"Human-readable automation name"},prompt:{type:"string",description:"Prompt sent to the target agent when the automation fires"},schedule:{type:"object",properties:{kind:{type:"string",enum:["at","every","cron"]},tz:{type:"string"},at:{type:"string"},intervalSeconds:{type:"integer",minimum:1},anchorAt:{type:"string"},expression:{type:"string"}}},session:{type:"object",description:"Always set session.mode. Prefer bound for tasks created in the current BeeOS chat so every run returns here. Use new if the user requests a separate conversation per run or the current chat cannot bind. The plugin fills the bound conversation ID; do not supply it.",properties:{mode:{type:"string",enum:["new","bound"]}}},validFrom:{type:"string"},validUntil:{type:"string"}},required:["name","prompt"]},async execute(e,r,o){const n=createInput(r,t,e);if("error"in n)return toolResult(n);try{const e=await request(()=>t.gateway.automations.create(n,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return t.logger?.warn?.(`[beeos_automation_create] ${e instanceof Error?e.message:String(e)}`),toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_create_webhook",description:"Create a BeeOS platform Webhook Automation. GitHub uses source/events/repoOwner/repoName; Gmail uses source/mailbox only (never hookId/hookUrl/repo*). Runs preflight first. Prefer session.mode=bound for a task created in this chat.",parameters:{type:"object",properties:{name:{type:"string"},prompt:{type:"string"},source:{type:"string",enum:["github","gmail"]},events:{type:"array",items:{type:"string"},minItems:1},repoOwner:{type:"string"},repoName:{type:"string"},eventFilter:{type:"string"},mailbox:{type:"string"},session:{type:"object",description:"Always set session.mode. Prefer bound for tasks created in the current BeeOS chat so every event returns here. Use new if the user requests a separate conversation per event or the current chat cannot bind. The plugin fills the bound conversation ID; do not supply it.",properties:{mode:{type:"string",enum:["new","bound"]}}},validFrom:{type:"string"},validUntil:{type:"string"}},required:["name","prompt","source"]},async execute(e,r,o){const n=createWebhookInput(r,t,e);if("error"in n)return toolResult(n);try{const a=await runPreflight(t,r,e);if(!a.ready)return toolResult({error:"automation_preflight_failed",preflight:a});const s=await request(()=>t.gateway.automations.createWebhook(n,{signal:o}));return s.ok?toolResult(s.data):requestErrorResult(s)}catch(e){return t.logger?.warn?.(`[beeos_automation_create_webhook] ${e instanceof Error?e.message:String(e)}`),toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_capabilities",description:"Check whether the BeeOS Automation platform is reachable/authorized and whether the current chat can bind as an Automation session. Read-only.",parameters:{type:"object",properties:{}},async execute(e){try{return toolResult(await runCapabilities(t,e))}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_preflight",description:"Read-only readiness check for creating a BeeOS Webhook Automation. Returns nextAction such as authorize_connector, install_connector, provide_mailbox, provide_repository_and_events, sync_openclaw_agent, retry_or_check_connection, or create_automation.",parameters:{type:"object",properties:{source:{type:"string",enum:["github","gmail"]},events:{type:"array",items:{type:"string"}},repoOwner:{type:"string"},repoName:{type:"string"},mailbox:{type:"string"},session:{type:"object",properties:{mode:{type:"string",enum:["new","bound"]}}}},required:["source"]},async execute(e,r){try{return toolResult(await runPreflight(t,r,e))}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_list",description:"List BeeOS platform Automations owned by the current user.",parameters:{type:"object",properties:{status:{type:"string",enum:["enabled","paused","expired"]},limit:{type:"integer",minimum:1,maximum:100},cursor:{type:"string"}}},async execute(e,r,o){const n=listQuery(asObject(r));try{const e=await request(()=>t.gateway.automations.list(n,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_get",description:"Get one BeeOS platform Automation by its automation ID.",parameters:{type:"object",properties:{automationId:{type:"string"}},required:["automationId"]},async execute(e,r,o){const n=stringValue(asObject(r).automationId);if(!n)return toolResult({error:"automationId is required"});try{const e=await request(()=>t.gateway.automations.get(n,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_run",description:"Trigger an existing BeeOS platform Automation immediately and return its Run record.",parameters:{type:"object",properties:{automationId:{type:"string"},requestKey:{type:"string",description:"Optional idempotency key; generated when omitted"},sourceRunId:{type:"string"},rerunSessionMode:{type:"string",enum:["original","new"]}},required:["automationId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId);if(!a)return toolResult({error:"automationId is required"});const s=stringValue(n.requestKey)??randomUUID(),i=stringValue(n.sourceRunId),u=stringValue(n.rerunSessionMode),c={};i&&(c.sourceRunId=i),u&&(c.rerunSessionMode=u);try{const e=await request(()=>t.gateway.automations.run(a,c,s,{signal:o}));return e.ok?toolResult({requestKey:s,data:e.data}):toolResult({error:e.error,status:e.status,details:e.details,requestKey:s})}catch(e){return toolResult({error:e instanceof Error?e.message:String(e),requestKey:s})}}}),e.registerTool({name:"beeos_automation_runs",description:"List Runs for one BeeOS platform Automation.",parameters:{type:"object",properties:{automationId:{type:"string"},status:{type:"string",enum:["scheduled","dispatched","failed","skipped"]},limit:{type:"integer",minimum:1,maximum:100},cursor:{type:"string"}},required:["automationId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId);if(!a)return toolResult({error:"automationId is required"});const s=listQuery(n);try{const e=await request(()=>t.gateway.automations.runs(a,s,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_get_run",description:"Get one Run record for a BeeOS platform Automation.",parameters:{type:"object",properties:{automationId:{type:"string"},runId:{type:"string"}},required:["automationId","runId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId),s=stringValue(n.runId);if(!a||!s)return toolResult({error:"automationId and runId are required"});try{const e=await request(()=>t.gateway.automations.runDetail(a,s,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}})}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;hasConnectedOnce=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;controlHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,n={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relay_url||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`,n=agentAuthHeaders("GET",e,this.opts.agentKeys)}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),n=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const s=new WebSocket(t,{headers:n});s.binaryType="arraybuffer",s.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=s,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[],this.hasConnectedOnce?(this.opts.logger?.info?.("[canvas-relay] Reconnected — notifying upper layer"),this.opts.onReconnect?.()):(this.hasConnectedOnce=!0,this.opts.onConnected?.())}),s.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const n=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(n)}),s.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),s.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),s.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,n)=>{this.pendingJoins.set(e,{resolve:t,reject:n}),this.sendJoinCommand(e);const s=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(s),o.resolve()},reject:e=>{clearTimeout(s),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}onCanvasControl(e){this.controlHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){if(!this.connected)return Promise.reject(new Error(`WebSocket not connected (request: ${e.type})`));const t=randomUUID();return e.reqId=t,new Promise((n,s)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),s(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:n,reject:s,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const n=(new TextEncoder).encode(e),s=new Uint8Array(1+n.length+t.length);s[0]=n.length,s.set(n,1),s.set(t,1+n.length),this.ws.send(s)}handleTextMessage(e){try{const t=JSON.parse(e),n=t.type;if(n?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(n){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.opts.logger?.info?.(`[canvas-relay] Received action: canvasId=${t.canvasId} surfaceId=${t.surfaceId} requestId=${t.requestId} senderClientId=${t.senderClientId}`),this.actionHandler?this.actionHandler(t):this.opts.logger?.warn?.("[canvas-relay] Action received but no handler registered");break;case"canvas.toggle":case"canvas.dimensions":case"canvas.reference":case"canvas.clear":this.opts.logger?.info?.(`[canvas-relay] Received control frame type=${n} canvasId=${t.canvasId} sessionId=${t.sessionId??""}`),this.controlHandler?this.controlHandler(t):this.opts.logger?.warn?.(`[canvas-relay] Control frame ${n} received but no handler registered`);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${n}`)}}catch(e){this.opts.logger?.warn?.(`[canvas-relay] Failed to parse control message: ${e instanceof Error?e.message:String(e)}`)}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const n=(new TextDecoder).decode(e.slice(1,1+t)),s=e.slice(1+t),o=this.canvasHandlers.get(n);if(!o)return;const a=s[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(s):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(s)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
1
+ import{randomUUID}from"node:crypto";import WebSocket from"ws";import{canvasRelayJoinFrame,canvasRelayLeaveFrame,canvasRelayUpgradeHeaders,canvasRelayWebSocketUrl,encodeCanvasRelayBinary}from"@beeos-ai/cloud-harness-sdk";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;hasConnectedOnce=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;controlHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e=canvasRelayUpgradeHeaders(this.opts.agentKeys);let n;if(this.opts.tokenProvider){const e=await this.opts.tokenProvider();n=canvasRelayWebSocketUrl(e.relayUrl||this.opts.canvasRelayUrl,{...this.opts.agentId?{agentId:this.opts.agentId}:{},token:e.token})}else n=canvasRelayWebSocketUrl(this.opts.canvasRelayUrl,{...this.opts.agentId?{agentId:this.opts.agentId}:{}});this.opts.logger?.info?.(`[canvas-relay] Connecting to ${n}`);const s=new WebSocket(n,{headers:e});s.binaryType="arraybuffer",s.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=s,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[],this.hasConnectedOnce?(this.opts.logger?.info?.("[canvas-relay] Reconnected — notifying upper layer"),this.opts.onReconnect?.()):(this.hasConnectedOnce=!0,this.opts.onConnected?.())}),s.on("message",(e,n)=>{if(!n&&"string"!=typeof e){const n=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(n)}if("string"==typeof e)return void this.handleTextMessage(e);const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(s)}),s.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),s.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),s.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,n){if(this.canvasHandlers.set(e,n),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((n,s)=>{this.pendingJoins.set(e,{resolve:n,reject:s}),this.sendJoinCommand(e);const t=setTimeout(()=>{const n=this.pendingJoins.get(e);n&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),n.resolve())},5e3),a=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(t),a.resolve()},reject:e=>{clearTimeout(t),a.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendRawText(canvasRelayLeaveFrame(e))}sendUpdate(e,n){this.sendTagged(e,n)}sendAwareness(e,n){this.sendTagged(e,n)}async ensureCanvas(e,n){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:n?.agentId,title:n?.title,linkSessionId:n?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,n){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:n})).snapshotId??null}onAction(e){this.actionHandler=e}onCanvasControl(e){this.controlHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){if(!this.connected)return Promise.reject(new Error(`WebSocket not connected (request: ${e.type})`));const n=randomUUID();return e.reqId=n,new Promise((s,t)=>{const a=setTimeout(()=>{this.pendingRequests.delete(n),t(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(n,{resolve:s,reject:t,timer:a}),this.sendControlMessage(e)})}sendTagged(e,n){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:n});this.ws.send(encodeCanvasRelayBinary(e,n))}handleTextMessage(e){try{const n=JSON.parse(e),s=n.type;if(s?.endsWith(":result")&&n.reqId){const e=this.pendingRequests.get(n.reqId);return void(e&&(this.pendingRequests.delete(n.reqId),clearTimeout(e.timer),n.error?e.reject(new Error(n.error)):e.resolve(n)))}switch(s){case"joined":{const e=this.pendingJoins.get(n.canvasId??"");e&&(this.pendingJoins.delete(n.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${n.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${n.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${n.canvasId}: ${n.message}`);break;case"action":this.opts.logger?.info?.(`[canvas-relay] Received action: canvasId=${n.canvasId} surfaceId=${n.surfaceId} requestId=${n.requestId} senderClientId=${n.senderClientId}`),this.actionHandler?this.actionHandler(n):this.opts.logger?.warn?.("[canvas-relay] Action received but no handler registered");break;case"canvas.toggle":case"canvas.dimensions":case"canvas.reference":case"canvas.clear":this.opts.logger?.info?.(`[canvas-relay] Received control frame type=${s} canvasId=${n.canvasId} sessionId=${n.sessionId??""}`),this.controlHandler?this.controlHandler(n):this.opts.logger?.warn?.(`[canvas-relay] Control frame ${s} received but no handler registered`);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${s}`)}}catch(e){this.opts.logger?.warn?.(`[canvas-relay] Failed to parse control message: ${e instanceof Error?e.message:String(e)}`)}}handleBinaryMessage(e){if(e.length<2)return;const n=e[0];if(e.length<1+n+1)return;const s=(new TextDecoder).decode(e.slice(1,1+n)),t=e.slice(1+n),a=this.canvasHandlers.get(s);if(!a)return;const o=t[0];o===MSG_SYNC&&a.onUpdate?a.onUpdate(t):o===MSG_AWARENESS&&a.onAwareness&&a.onAwareness(t)}sendJoinCommand(e){this.sendRawText(canvasRelayJoinFrame(e))}sendRawText(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}sendControlMessage(e){this.sendRawText(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
@@ -0,0 +1 @@
1
+ import{mkdtemp,rm,writeFile}from"node:fs/promises";import{tmpdir}from"node:os";import{join}from"node:path";import{consumeNativeOutputTool}from"../runtime/native-output-file-attempt.js";import{getUploadDeps,uploadLocalFile,uploadNativeOutputFile}from"../upload/upload-file.js";export function createStaticCanvasHtmlPublisher(t){return async(e,i)=>{if(void 0===t){const t=getUploadDeps();if(!t)throw new Error("Agent Gateway upload is not configured");return publishStaticCanvasHtml(e,(e,i)=>uploadLocalFile(e,t.gateway,{fileNameOverride:i}))}const a=consumeNativeOutputTool(i);return publishStaticCanvasHtml(e,e=>uploadNativeOutputFile(e,t,a,0))}}async function publishStaticCanvasHtml(t,e){const i=await mkdtemp(join(tmpdir(),"beeos-canvas-")),a=`canvas-${t.id.replace(/[^a-zA-Z0-9_-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)||"result"}.html`,o=join(i,a);try{await writeFile(o,t.html,{encoding:"utf8",mode:384});const i=await e(o,a);return{fileId:i.fileId,uri:i.uri,mimeType:i.mimeType}}finally{await rm(i,{recursive:!0,force:!0})}}
@@ -1 +1 @@
1
- import{extractBeeosConversationIdFromSessionKey}from"../runtime/openclaw-session-key-resolution.js";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 A2UI v0.8 components to the Canvas panel. Prefer this over the built-in canvas tool.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","root","components"],properties:{id:{type:"string",description:"Surface id. Reuse to update."},root:{type:"string",description:"Root component id."},components:{type:"array",description:"Flat ComponentInstance list. Each item has id plus one component type.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:"Component id."},component:{type:"object",description:"One type key: Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart."}}}},data:{type:"object",description:"Optional binding model, path keys like /name."}}};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?extractBeeosConversationIdFromSessionKey(r):void 0)??(r&&r!==t?t:void 0),u=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(u,s),i.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),u}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,getGatewaySessionKey:o}=e;let i=0;return async(o,r)=>{const s=r,l=s.id,c=s.root,d="panel";let u=s.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!(l&&c&&Array.isArray(u)&&u.length))return{success:!1,error:"id, root, and components (array) are required"};const p=normalizeComponents(u);if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return n.warn?.(`[canvas] Rate limited surfaceId=${l}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const m=s.data;let h=e.getActiveSessionId?.(o);for(let t=0;!h&&t<3;t++)await delay(400),h=e.getActiveSessionId?.(o);if(!h&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}h=e.getActiveSessionId?.(o)}if(!h)return n.warn?.(`[canvas] No active session for toolCallId=${o} 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 y=e.getGatewaySessionKey?.(o),f=await ensureRelayConnected(e,h,o,y),g=t.has(l,f??void 0),v=Date.now(),C=t.getRelayClient();f&&C&&v-i>6e4&&C.createSnapshot(f,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),g?t.update(l,p,f,c,m):t.create(f??`_fallback_${Date.now()}`,l,d,p,{origin:{kind:"agent"},rootComponentId:c,dataModel:m});const b=p.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:g?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:b}}),n.debug?.(`[canvas] ${g?"Updated":"Created"} surface=${l} components=${p.length}`);const w={success:!0,surfaceId:l,presentation:d,isUpdate:g,activeSurfaces:f?t.getAllActive(f).map(e=>e.surfaceId):[l]};return m&&(w.dataModel=m),w}}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_PRESENTATION_GATE="## BeeOS Canvas presentation gate\n\nCanvas presentation is available in the background for this BeeOS conversation. The Canvas resource tab does not need to be open and no Web subscriber is required before a Canvas tool is called. Describe successful rendering as content created or updated on the canvas; do not claim that a user is currently viewing it.\n\nDo not read `beeos-canvas/SKILL.md` or any canvas skill file. Call `render_ui` directly. The A2UI schema is in this prompt and the `render_ui` tool.\n\nAfter any skill or data tool returns, apply this gate before writing the final reply:\n1. Count the related records, items, rows, time points, options, or repeated field groups in the completed result.\n2. If there are two or more, you MUST call `render_ui` before the final reply.\n3. Do not replace `render_ui` with a Markdown table, bullets, JSON, or a prose summary.\n4. If there is only one fact or value, or the answer is a prose explanation, reply in chat without `render_ui`.\n\nDomain skills control how data is obtained and interpreted. This gate controls how the completed result is presented and overrides a domain skill's default text-summary format. When `render_ui` succeeds, keep the chat reply to one short confirmation sentence.";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"}}`\nImage.url may be `beeos-file://<fileId>`; the renderer resolves it. Do not convert it to a signed URL.\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\n\nNest by id: Column/Row `children.explicitList`, Card `child`. Example:\n`{"id":"s1","root":"r","components":[{"id":"t","component":{"Text":{"text":{"literalString":"Hello"},"usageHint":"h2"}}},{"id":"r","component":{"Column":{"children":{"explicitList":["t"]}}}}]}`\nTables: Card > Column > Row > Text. Forms: TextField/CheckBox + Button `action.context` paths.\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
+ import{extractBeeosConversationIdFromSessionKey}from"../runtime/openclaw-session-key-resolution.js";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 CANVAS_COMPONENT_ID_GUIDANCE="Component IDs must differ from non-reference scalar values (usageHint, literalString, and action.name). Use type-qualified IDs such as text_heading, text_body, and column_root; do not use scalar values such as body, h1, or center as IDs. A2UI v0.8 resolves strings matching component IDs as references. Keep root, child, explicitList, and template.componentId references matching their target IDs. Reuse the same component IDs when updating a surface.";export const RENDER_UI_TOOL_DESCRIPTION="Publish a static Canvas result as self-contained HTML using id, title, and html. The result is stored as a BeeOS file and shown in the conversation. Legacy A2UI calls with id, root, and components remain available for interactive experiences.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"Surface id. Reuse to update."},title:{type:"string",description:"Human-readable title for a static HTML Canvas result."},html:{type:"string",description:"Complete self-contained static HTML. Maximum UTF-8 size: 5 MiB."},root:{type:"string",description:"Root component id."},components:{type:"array",description:"Flat ComponentInstance list. Each item has id plus one component type.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:CANVAS_COMPONENT_ID_GUIDANCE},component:{type:"object",description:"One type key: Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart."}}}},data:{type:"object",description:"Optional binding model, path keys like /name."}}};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)),MAX_STATIC_HTML_BYTES=5242880,FILE_ID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function renderStaticHtml(e,t,n){const a=e=>({success:!1,presentation:"static_html",error:e}),o="string"==typeof n.id?n.id.trim():"",i="string"==typeof n.title?n.title.trim():"",r=n.html;if(!o||!i||"string"!=typeof r||!r.trim())return a("id, title, and non-empty html are required");if("root"in n||"components"in n)return a("html cannot be combined with root or components");if(i.length>160)return a("Canvas title is too long (max 160 characters)");if(Buffer.byteLength(r,"utf8")>5242880)return a("Canvas HTML exceeds the 5 MiB preview limit");if(!e.publishStaticHtml)return a("Canvas file upload is unavailable");try{const n=await e.publishStaticHtml({id:o,title:i,html:r},t);return FILE_ID.test(n.fileId)&&n.uri===`beeos-file://${n.fileId}`&&"text/html"===n.mimeType?{success:!0,presentation:"static_html",surfaceId:o,artifact:{kind:"canvas",fileId:n.fileId,uri:n.uri,mimeType:"text/html",title:i}}:a("Canvas HTML upload returned an invalid file reference")}catch(e){return a(`Canvas HTML upload failed: ${e instanceof Error?e.message:String(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?extractBeeosConversationIdFromSessionKey(r):void 0)??(r&&r!==t?t:void 0),u=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(u,s),i.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),u}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,getGatewaySessionKey:o}=e;let i=0;return async(o,r)=>{const s=r;if(Object.prototype.hasOwnProperty.call(s,"html"))return renderStaticHtml(e,o,s);const l=s.id,c=s.root,d="panel";let u=s.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!(l&&c&&Array.isArray(u)&&u.length))return{success:!1,error:"id, root, and components (array) are required"};const p=normalizeComponents(u);if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return n.warn?.(`[canvas] Rate limited surfaceId=${l}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const m=s.data;let h=e.getActiveSessionId?.(o);for(let t=0;!h&&t<3;t++)await delay(400),h=e.getActiveSessionId?.(o);if(!h&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}h=e.getActiveSessionId?.(o)}if(!h)return n.warn?.(`[canvas] No active session for toolCallId=${o} 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?.(o),y=await ensureRelayConnected(e,h,o,f),g=t.has(l,y??void 0),v=Date.now(),C=t.getRelayClient();y&&C&&v-i>6e4&&C.createSnapshot(y,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),g?t.update(l,p,y,c,m):t.create(y??`_fallback_${Date.now()}`,l,d,p,{origin:{kind:"agent"},rootComponentId:c,dataModel:m});const b=p.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:g?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:b}}),n.debug?.(`[canvas] ${g?"Updated":"Created"} surface=${l} components=${p.length}`);const T={success:!0,surfaceId:l,presentation:d,isUpdate:g,activeSurfaces:y?t.getAllActive(y).map(e=>e.surfaceId):[l]};return m&&(T.dataModel=m),T}}export function createRenderUiToolExecutor(e){const t=createRenderUiHandler(e);return async(e,n)=>{const a=await t(e,n),o=[{type:"text",text:JSON.stringify(a)}];if(!0===a.success&&"static_html"===a.presentation){const e=a.artifact;o.push({type:"resource_link",uri:e.uri,name:`${e.title}.html`,mimeType:e.mimeType})}return{...a,content:o,details:a,isError:!1===a.success}}}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_PRESENTATION_GATE="## BeeOS Canvas presentation gate\n\nCanvas presentation is available in the background for this BeeOS conversation. The Canvas resource tab does not need to be open and no Web subscriber is required before a Canvas tool is called. Describe successful rendering as content created or updated on the canvas; do not claim that a user is currently viewing it.\n\nDo not read `beeos-canvas/SKILL.md` or any canvas skill file. Call `render_ui` directly. The A2UI schema is in this prompt and the `render_ui` tool.\n\nAfter any skill or data tool returns, apply this gate before writing the final reply:\n1. Honor an explicit requested format when it is supported.\n2. For one fact, an ordinary question, a prose explanation, or light structure that belongs in the conversation, reply in chat using Markdown.\n3. For a reusable text document or an explicit Markdown request, create and deliver a `.md` artifact through the normal file-delivery path.\n4. For a rich report, dashboard, substantial table, other visual structure, or two or more related records that need layout, you MUST call `render_ui` with `id`, `title`, and self-contained `html` before the final reply.\n5. For an interaction, form, or dynamic state, use `render_form` or legacy A2UI `render_ui`.\n6. Generate PNG or PDF only when the user explicitly asks for an image, PDF, download, or export. They are not the default presentation format.\n7. Do not replace a required `render_ui` result with an unlabelled file upload, Markdown table, JSON, or prose summary.\n\nDomain skills control how data is obtained and interpreted. This gate controls how the completed result is presented and overrides a domain skill's default text-summary format. When `render_ui` succeeds, keep the chat reply to one short confirmation sentence.";export const CANVAS_SYSTEM_PROMPT=`## Static Canvas HTML\n\nCall \`render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."})\` for non-interactive reports, dashboards, tables, charts, and other structured results.\n\nUse a complete UTF-8 HTML document with inline CSS and inline SVG for charts. Keep it self-contained. Scripts, forms, external stylesheets, fonts, images, and network requests do not run in the preview. The HTML preview has a 5 MiB limit. Static HTML is a BeeOS file artifact and does not require the Live Canvas panel or relay to be open. Do not call \`beeos_upload_file\` for the same result.\n\n## Live Canvas — Google A2UI v0.8\n\nUse legacy A2UI only for interactions, forms, and dynamic state. Use \`render_ui\`, not the built-in \`canvas\` tool.\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n${CANVAS_COMPONENT_ID_GUIDANCE}\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 this A2UI schema for interactive experiences. Use static HTML for non-interactive structured results and 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"}}\`\nImage.url may be \`beeos-file://<fileId>\`; the renderer resolves it. Do not convert it to a signed URL.\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\n\nNest by id: Column/Row \`children.explicitList\`, Card \`child\`. Example:\n\`{"id":"s1","root":"column_root","components":[{"id":"text_body","component":{"Text":{"text":{"literalString":"Hello"},"usageHint":"body"}}},{"id":"column_root","component":{"Column":{"children":{"explicitList":["text_body"]}}}}]}\`\nTables: Card > Column > Row > Text. Forms: TextField/CheckBox + Button \`action.context\` paths.\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. ${CANVAS_COMPONENT_ID_GUIDANCE} 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 +1 @@
1
- import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_MESSAGES=500;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(e){this.opts=e}connect(e,s){this.canvasId=e,this.onUpdate=s.onUpdate,this.onAwareness=s.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const e=`/ws/agent/${this.canvasId}`;let s=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`;this.opts.agentId&&(s+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),this.opts.logger?.info?.(`[canvas-ws] Connecting to ${s}`);const t=agentAuthHeaders("GET",e,this.opts.agentKeys),n=new WebSocket(s,{headers:t});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n;for(const e of this.pendingMessages)n.send(e);this.pendingMessages=[]}),n.on("message",e=>{const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));if(0===s.length)return;const t=s[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(s):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(s)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-ws] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}sendUpdate(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}sendAwareness(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}enqueuePending(e){this.pendingMessages.length>=500&&(this.pendingMessages.shift(),this.opts.logger?.warn?.("[canvas-ws] pendingMessages exceeded 500, dropping oldest")),this.pendingMessages.push(e)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}
1
+ import WebSocket from"ws";import{canvasRelayJoinFrame,canvasRelayUpgradeHeaders,canvasRelayWebSocketUrl,encodeCanvasRelayBinary}from"@beeos-ai/cloud-harness-sdk";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_MESSAGES=500;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(e){this.opts=e}connect(e,s){this.canvasId=e,this.onUpdate=s.onUpdate,this.onAwareness=s.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const e=canvasRelayWebSocketUrl(this.opts.canvasRelayUrl,{...this.opts.agentId?{agentId:this.opts.agentId}:{}});this.opts.logger?.info?.(`[canvas-ws] Connecting to ${e}`);const s=canvasRelayUpgradeHeaders(this.opts.agentKeys),n=new WebSocket(e,{headers:s});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n,n.send(canvasRelayJoinFrame(this.canvasId));for(const e of this.pendingMessages)n.send(encodeCanvasRelayBinary(this.canvasId,e));this.pendingMessages=[]}),n.on("message",(e,s)=>{if(!s)return;const n=decodeCanvasRelayBinary(new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)));if(!n||n.canvasId!==this.canvasId||0===n.payload.length)return;const t=n.payload[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(n.payload):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(n.payload)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-ws] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}sendUpdate(e){this.sendYjs(e)}sendAwareness(e){this.sendYjs(e)}sendYjs(e){this.canvasId&&this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(encodeCanvasRelayBinary(this.canvasId,e)):this.enqueuePending(e)}enqueuePending(e){this.pendingMessages.length>=500&&(this.pendingMessages.shift(),this.opts.logger?.warn?.("[canvas-ws] pendingMessages exceeded 500, dropping oldest")),this.pendingMessages.push(e)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}function decodeCanvasRelayBinary(e){if(e.length<2)return;const s=e[0];if(s<1||e.length<1+s)return;return{canvasId:(new TextDecoder).decode(e.subarray(1,1+s)),payload:e.subarray(1+s)}}
@@ -1 +1 @@
1
- import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";import{extractBeeosConversationIdFromSessionKey}from"./runtime/openclaw-session-key-resolution.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,n]of Object.entries(e)){if("object"==typeof n&&null!==n){const a=n;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];canvasPanelOpen=!1;deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const n=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const n=this.deps.state.sessions.get(s),a=n?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},t=s?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:n,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=i,this.schemaRegistry=t,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:n,schemaRegistry:t,formBuilder:i,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),e.onCanvasControl(e=>{this.handleRelayCanvasControl(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action+control handlers wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}handleRelayCanvasControl(e){if("canvas.toggle"===e.type){const s=!0===e.visible||!0===e.enabled;this.canvasPanelOpen=s;const n={enabled:s,canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}};if(e.sessionId?this.deps.state.resolveSessionFromParams(n):void 0)return void this.handleCanvasNotification("canvas/toggle",n);let a=0;for(const e of this.deps.state.sessionsByGatewayKey.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;for(const e of this.deps.state.sessions.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;if(s&&e.sessionId){const s=e.sessionId;for(const e of this.deps.state.sessionsByGatewayKey.values()){if(extractBeeosConversationIdFromSessionKey(e.gatewaySessionKey)===s||e.sessionId===s){this.ensureCanvasForSession(s,e.gatewaySessionKey);break}}}return void this.deps.logger.info?.(`[canvas-orch] Relay canvas.toggle enabled=${s} fan-out sessions≈${a} canvasId=${e.canvasId}`)}"canvas.dimensions"!==e.type?"canvas.reference"!==e.type?"canvas.clear"===e.type&&this.handleCanvasNotification("canvas/clear",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}}):this.handleCanvasNotification("canvas/reference",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},surfaceId:e.surfaceId??null}):this.handleCanvasNotification("canvas/dimensions",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},width:e.width,height:e.height,...void 0===e.devicePixelRatio?{}:{devicePixelRatio:e.devicePixelRatio}})}isCanvasActive(e){if(this.canvasPanelOpen)return!0;if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){if(this.canvasPanelOpen)return!0;const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,n){if(this.canvasPanelOpen){const n=this.deps.state.sessionsByGatewayKey.get(s)??this.deps.state.sessions.get(e);n&&(n.canvasActive=!0)}if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const n=this.canvasState?.resolveCanvasForSession(s);n&&(this.canvasState.clear(n),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,n=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==n.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${n.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of n)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,n){if(!this.canvasState)return e;if(n.has(s.split(":").slice(0,2).join(":"))||n.has(s))return e;try{let n=this.canvasState.resolveCanvasForSession(s);if(!n){const e=this.canvasState.getRelayClient();if(e)try{const a=extractBeeosConversationIdFromSessionKey(s);n=await e.ensureCanvas(s,{...a?{linkSessionId:a}:{}}),this.canvasState.registerSession(s,n)}catch(e){this.deps.logger.warn?.(`[canvas-orch] ensureCanvas for prompt inject failed (session may not be linked yet): ${e instanceof Error?e.message:String(e)}`)}}if(n){let s=this.canvasState.buildContextForCanvas(n);s||(await this.canvasState.lazyLoadCanvas(n),s=this.canvasState.buildContextForCanvas(n));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,n=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=n,this.canvasPanelOpen=n,n&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,n=a.agentId,t=this.canvasState?.getRelayClient();if(t){const i=this.deps.resolvePlatformAgentId(n??"")??a.platformAgentId,o=s&&s!==e?e:void 0;t.ensureCanvas(s??e,{agentId:i,linkSessionId:o}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${n}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:n}})}else if("canvas/clear"===e){const e=s,n=this.deps.state.resolveSessionFromParams(e),a=n?this.canvasState.resolveCanvasForSession(n.gatewaySessionKey):void 0,t=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${t} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,n=s.action;if("a2ui_push"===n||"surface_update"===n){const e=String(s.surfaceId??nextOclId("openclaw")),n=s.components;if(n&&Array.isArray(n)){const a=sanitizeA2UIComponents(n),t=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,t)?this.canvasState.update(e,a,t):this.canvasState.create(t??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===n||"url_open"===n){const e=String(s.url??"");if(e){const n=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),t=[{id:`${n}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,n,"inline",t,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${n}`)}ensureCanvasForSession(e,s){const n=this.canvasState?.getRelayClient();if(!n)return;const a=extractBeeosConversationIdFromSessionKey(s)??(e!==s?e:void 0);n.ensureCanvas(s,{linkSessionId:a}).then(async n=>{this.canvasState?.registerSession(s,n),await(this.canvasState?.joinCanvas(n,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${n} acpSid=${e} gwKey=${s} linkSessionId=${a??"none"}`)}).catch(n=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} linkSessionId=${a??"none"} err=${n}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);const n=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,n??void 0);try{await this.deps.sendAgentMessage(a,s.gatewaySessionKey)}catch(n){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${n instanceof Error?n.message:n}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const n=s.userAction?.name??"unknown",a=`Clicked "${n}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:n,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${n} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:n,userAction:a,requestId:t,senderClientId:i}=e,o=this.canvasState.resolveSessionForCanvas(s);if(!o)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,t,i,"no_session");const r=this.deps.state.findSessionByKey(o);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${o}`),void this.sendRelayActionError(s,t,i,"no_session");const c={surfaceId:n,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${n} request=${t}`),this.sendRelayActionResult(s,t,i,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,t,i,"processing_failed")})}sendRelayActionResult(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:n,status:a})}sendRelayActionError(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:n,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
1
+ import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";import{extractBeeosConversationIdFromSessionKey}from"./runtime/openclaw-session-key-resolution.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,n]of Object.entries(e)){if("object"==typeof n&&null!==n){const a=n;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];canvasPanelOpen=!1;deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const n=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const n=this.deps.state.sessions.get(s),a=n?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},t=s?.schemaRegistry??new FormSchemaRegistry,o=new FormSurfaceBuilder({canvasState:e,gatewayRequest:n,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=o,this.schemaRegistry=t,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:n,schemaRegistry:t,formBuilder:o,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),e.onCanvasControl(e=>{this.handleRelayCanvasControl(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action+control handlers wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}handleRelayCanvasControl(e){if("canvas.toggle"===e.type){const s=!0===e.visible||!0===e.enabled;this.canvasPanelOpen=s;const n={enabled:s,canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}};if(e.sessionId?this.deps.state.resolveSessionFromParams(n):void 0)return void this.handleCanvasNotification("canvas/toggle",n);let a=0;for(const e of this.deps.state.sessionsByGatewayKey.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;for(const e of this.deps.state.sessions.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;if(s&&e.sessionId){const s=e.sessionId;for(const e of this.deps.state.sessionsByGatewayKey.values()){if(extractBeeosConversationIdFromSessionKey(e.gatewaySessionKey)===s||e.sessionId===s){this.ensureCanvasForSession(s,e.gatewaySessionKey);break}}}return void this.deps.logger.info?.(`[canvas-orch] Relay canvas.toggle enabled=${s} fan-out sessions≈${a} canvasId=${e.canvasId}`)}"canvas.dimensions"!==e.type?"canvas.reference"!==e.type?"canvas.clear"===e.type&&this.handleCanvasNotification("canvas/clear",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}}):this.handleCanvasNotification("canvas/reference",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},surfaceId:e.surfaceId??null}):this.handleCanvasNotification("canvas/dimensions",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},width:e.width,height:e.height,...void 0===e.devicePixelRatio?{}:{devicePixelRatio:e.devicePixelRatio}})}isCanvasActive(e){if(this.canvasPanelOpen)return!0;if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){if(this.canvasPanelOpen)return!0;const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,n){if(this.canvasPanelOpen){const n=this.deps.state.sessionsByGatewayKey.get(s)??this.deps.state.sessions.get(e);n&&(n.canvasActive=!0)}if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const n=this.canvasState?.resolveCanvasForSession(s);n&&(this.canvasState.clear(n),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,n=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==n.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${n.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of n)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,n){if(!this.canvasState)return e;if(n.has(s.split(":").slice(0,2).join(":"))||n.has(s))return e;try{let n=this.canvasState.resolveCanvasForSession(s);if(!n){const e=this.canvasState.getRelayClient();if(e)try{const a=extractBeeosConversationIdFromSessionKey(s),t=this.resolveCanvasAgentId(s);n=await e.ensureCanvas(s,{...t?{agentId:t}:{},...a?{linkSessionId:a}:{}}),this.canvasState.registerSession(s,n)}catch(e){this.deps.logger.warn?.(`[canvas-orch] ensureCanvas for prompt inject failed (session may not be linked yet): ${e instanceof Error?e.message:String(e)}`)}}if(n){let s=this.canvasState.buildContextForCanvas(n);s||(await this.canvasState.lazyLoadCanvas(n),s=this.canvasState.buildContextForCanvas(n));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,n=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=n,this.canvasPanelOpen=n,n&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,n=a.agentId,t=this.canvasState?.getRelayClient();if(t){const o=this.deps.resolvePlatformAgentId(n??"")??a.platformAgentId,i=s&&s!==e?e:void 0;t.ensureCanvas(s??e,{agentId:o,linkSessionId:i}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${n}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:n}})}else if("canvas/clear"===e){const e=s,n=this.deps.state.resolveSessionFromParams(e),a=n?this.canvasState.resolveCanvasForSession(n.gatewaySessionKey):void 0,t=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${t} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,n=s.action;if("a2ui_push"===n||"surface_update"===n){const e=String(s.surfaceId??nextOclId("openclaw")),n=s.components;if(n&&Array.isArray(n)){const a=sanitizeA2UIComponents(n),t=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,t)?this.canvasState.update(e,a,t):this.canvasState.create(t??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===n||"url_open"===n){const e=String(s.url??"");if(e){const n=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),t=[{id:`${n}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,n,"inline",t,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${n}`)}resolveCanvasAgentId(e,s){const n=this.deps.state.sessionsByGatewayKey.get(e)??(s?this.deps.state.sessions.get(s):void 0);if(n)return this.deps.resolvePlatformAgentId(n.agentId??"")??n.platformAgentId}ensureCanvasForSession(e,s){const n=this.canvasState?.getRelayClient();if(!n)return;const a=extractBeeosConversationIdFromSessionKey(s)??(e!==s?e:void 0),t=this.resolveCanvasAgentId(s,e);n.ensureCanvas(s,{agentId:t,linkSessionId:a}).then(async n=>{this.canvasState?.registerSession(s,n),await(this.canvasState?.joinCanvas(n,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${n} acpSid=${e} gwKey=${s} linkSessionId=${a??"none"}`)}).catch(n=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} linkSessionId=${a??"none"} err=${n}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);const n=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,n??void 0);try{await this.deps.sendAgentMessage(a,s.gatewaySessionKey)}catch(n){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${n instanceof Error?n.message:n}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const n=s.userAction?.name??"unknown",a=`Clicked "${n}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:n,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${n} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:n,userAction:a,requestId:t,senderClientId:o}=e,i=this.canvasState.resolveSessionForCanvas(s);if(!i)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,t,o,"no_session");const r=this.deps.state.findSessionByKey(i);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${i}`),void this.sendRelayActionError(s,t,o,"no_session");const c={surfaceId:n,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${n} request=${t}`),this.sendRelayActionResult(s,t,o,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,t,o,"processing_failed")})}sendRelayActionResult(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:n,status:a})}sendRelayActionError(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:n,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
@@ -0,0 +1 @@
1
+ export const A2A_PROTOCOL_FIELDS=Object.freeze(["task_id","skills","artifacts","protocol_version","caller_protocol","correlation_id","callback_url","trace_parent","trace_state","openapi_invocation_id","mcp_request_id","agent_card"]);export function extractChatPrompt(t,e={}){const n=t.content??{},o=n.metadata??{},r=extractMessageText(n);if(!r)return null;const c=pickString(t.conversationId,n.channel_id),i=pickString(n.context_id,o.context_id),s=extractFiles(n),a=pickString(t.id)||void 0,f={message:r,files:s,sessionKey:computeSessionKey({content:n,source:e.source??("agent_request"===t.type?"agent_request":"chat_message"),localAgentId:e.localAgentId??"",contextId:i,channelId:c,generateSuffix:e.generateSuffix??defaultSuffix})};return c&&(f.channelId=c),i&&(f.contextId=i),a&&(f.messageId=a),f}function extractMessageText(t){const e=pickString(t.message);if(e)return e;const n=t.parts;if(Array.isArray(n)){const t=[];for(const e of n)if(e&&"object"==typeof e&&"text"===e.type){const n=e.text;"string"==typeof n&&t.push(n)}if(t.length>0)return t.join("\n")}return""}function extractFiles(t){const e=[],n=t.parts;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t.file_url;"string"==typeof n&&n.length>0&&e.push(n)}const o=t.files;if(Array.isArray(o))for(const t of o)if("string"==typeof t&&t.length>0)e.push(t);else if(t&&"object"==typeof t){const n=pickString(t.url,t.uri);n&&e.push(n)}const r=t.attachments;if(Array.isArray(r))for(const t of r){if(!t||"object"!=typeof t)continue;const n=t,o=pickString(n.download_url,n.url,n.uri);o&&e.push(o)}return e}function computeSessionKey(t){const e=pickString(t.content.session_key);if(e)return e;const n=pickString((t.content.metadata&&"object"==typeof t.content.metadata?t.content.metadata:{}).protocol),o=n||("agent_request"===t.source?"a2a":"invoke"),r=`agent:${t.localAgentId}:${o}`;if("chat_message"===t.source||n){if(t.contextId)return`${r}:ctx-${t.contextId}`;if(t.channelId)return`${r}:ch-${t.channelId}`}return`${r}:${t.generateSuffix()}`}function defaultSuffix(){return Math.random().toString(36).slice(2,14)}function pickString(...t){for(const e of t)if("string"==typeof e&&e.length>0)return e;return""}
@@ -0,0 +1 @@
1
+ const CONNECTOR_TOOL_NAMES=["list_connectors","search_actions","get_action_guide","execute_action"],objectSchema=(t,e)=>({type:"object",additionalProperties:!1,properties:t,...e&&e.length>0?{required:[...e]}:{}}),TOOLS={list_connectors:{description:"List the connectors enabled for this OpenClaw instance.",parameters:objectSchema({})},search_actions:{description:"Search actions exposed by enabled connectors.",parameters:objectSchema({query:{type:"string"},connectorId:{type:"string"},limit:{type:"integer"}})},get_action_guide:{description:"Get the input and output schema for one connector action.",parameters:objectSchema({actionId:{type:"string"}},["actionId"])},execute_action:{description:"Execute one connector action using the credential bound to this instance. For the Weather connector, actionId is wttr.get_weather and input is an object with location.",parameters:objectSchema({actionId:{type:"string"},input:{type:"object"}},["actionId","input"])}};function argumentsOf(t){if(null==t)return{};if("object"!=typeof t||Array.isArray(t))throw new Error("connector tool arguments must be an object");return t}function textFromContent(t){if(!Array.isArray(t))return;const e=t.flatMap(t=>{if(!t||"object"!=typeof t||!("text"in t))return[];const e=t.text;return"string"==typeof e?[e]:[]});return e.length>0?e.join("\n"):void 0}export function readableConnectorResult(t){if(!t||"object"!=typeof t)return{content:JSON.stringify(t)};const e=t;if(void 0!==e.error&&null!==e.error){const t=e.error,n=t&&"object"==typeof t&&"message"in t?String(t.message):JSON.stringify(t);throw new Error(n)}const n="result"in e?e.result:t;if(n&&"object"==typeof n){const t=n;if(!0===t.isError)throw new Error(textFromContent(t.content)||"connector tool failed");if(void 0!==t.structuredContent)return{content:JSON.stringify(t.structuredContent)};const e=textFromContent(t.content);if(void 0!==e)return{content:e}}return{content:"string"==typeof n?n:JSON.stringify(n)}}async function callConnectorTool(t,e,n,o){return readableConnectorResult(await t.connectors.callJson({jsonrpc:"2.0",id:e,method:"tools/call",params:{name:e,arguments:"list_connectors"===e?{}:argumentsOf(n)}},o?{signal:o}:void 0))}export function registerManagedConnectorTools(t,e){if(e)for(const n of CONNECTOR_TOOL_NAMES){const o=TOOLS[n];t.registerTool({name:n,description:o.description,parameters:o.parameters,execute:(t,o,r)=>callConnectorTool(e,n,o,r)})}}
@@ -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";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}}
1
+ import{mkdir,writeFile}from"node:fs/promises";import{basename,dirname,isAbsolute,resolve as resolvePathFs}from"node:path";import{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";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){const o=parseParams(e);if(!o.ok)return errorResult(o.error);if(!t)return errorResult("Agent Gateway client unavailable — fetching account files requires agentGatewayUrl and a key pair");const i=AbortSignal.timeout(12e4),s=r?.signal?AbortSignal.any([r.signal,i]):i;let a,n;try{a=await t.files.resolve(o.fileId,{signal:s})}catch(e){return errorResult(e instanceof CloudHttpError?`resolve ${e.status}: ${e.body}`:`resolve request failed: ${e instanceof Error?e.message:String(e)}`)}if(!a.downloadUrl)return errorResult("resolve returned no download URL");try{n=Buffer.from(await t.files.download(a.downloadUrl,s))}catch(e){return errorResult(e instanceof CloudHttpError?`download failed: ${e.status}`:`download failed: ${e instanceof Error?e.message:String(e)}`)}const l=resolveDestPath(o.destPath,a.fileName||o.fileId,r?.resolvePath);try{await mkdir(dirname(l),{recursive:!0}),await writeFile(l,n)}catch(e){return errorResult(`write failed: ${e instanceof Error?e.message:String(e)}`)}const d={ok:!0,path:l,fileName:a.fileName,mimeType:a.mimeType,size:n.byteLength};return{...d,content:[{type:"text",text:`Downloaded ${a.fileName} to ${l} (${n.byteLength}B)`}],output:d,result:d,details:d,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 i=t.fileId.trim();if(isBeeosFileUri(i)){const e=parseBeeosFileUri(i);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 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:i,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 +1 @@
1
- import{agentFetch}from"../agent-auth.js";export const BEEOS_LIST_FILES_TOOL_NAME="beeos_list_files";export const BEEOS_LIST_FILES_TOOL_DESCRIPTION="List files stored in the user's BeeOS account, across all their agents/instances. Use this to discover files the user (or another agent) uploaded or generated earlier, then pass a returned fileId to `beeos_fetch_file` to download it locally. Returns fileId, name, mimeType, size, source and originating instance for each file.";export const BEEOS_LIST_FILES_TOOL_SCHEMA={title:"BeeOS list account files",type:"object",description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,additionalProperties:!1,properties:{query:{type:"string",description:"Optional case-insensitive substring to match against file names."},source:{type:"string",enum:["chat_upload","agent_generated","manual"],description:"Optional filter by file origin."},limit:{type:"integer",minimum:1,maximum:200,description:"Maximum number of files to return (default 50)."}}};const DEFAULT_LIMIT=50,MAX_LIMIT=200,REQUEST_TIMEOUT_MS=3e4;export async function executeListFiles(e,t,r,i){const n=parseParams(e);if(!n.ok)return errorResult(n.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — listing account files requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign list requests");const o=AbortSignal.timeout(3e4),s=i?AbortSignal.any([i,o]):o,a=`${t.agentGatewayUrl}/api/v1/agent/files`;let u;try{u=await agentFetch(a,r,{method:"GET",signal:s})}catch(e){return errorResult(`list request failed: ${e instanceof Error?e.message:String(e)}`)}if(!u.ok){const e=await u.text().catch(()=>u.statusText);return errorResult(`list ${u.status}: ${e}`)}let l=(await u.json()).files??[];if(n.source&&(l=l.filter(e=>e.source===n.source)),n.query){const e=n.query.toLowerCase();l=l.filter(t=>t.fileName.toLowerCase().includes(e))}l=l.slice(0,n.limit);const c=0===l.length?"No matching files found in the account.":`Found ${l.length} file(s):\n`+l.map(e=>`- ${e.fileName} (${e.mimeType}, ${e.size}B, ${e.source}${e.instanceName?`, from ${e.instanceName}`:""}) → fileId=${e.fileId}`).join("\n"),f={ok:!0,files:l};return{...f,content:[{type:"text",text:c}],output:f,result:f,details:f,isError:!1}}function parseParams(e){const t=e&&"object"==typeof e?e:{},r=new Set(["query","source","limit"]),i=Object.keys(t).filter(e=>!r.has(e));if(i.length>0)return{ok:!1,error:`unexpected properties: ${i.join(", ")}`};let n,o;if(void 0!==t.query){if("string"!=typeof t.query)return{ok:!1,error:"query must be a string"};n=t.query.trim()||void 0}if(void 0!==t.source){if("string"!=typeof t.source)return{ok:!1,error:"source must be a string"};const e=["chat_upload","agent_generated","manual"];if(!e.includes(t.source))return{ok:!1,error:`source must be one of ${e.join(", ")}`};o=t.source}let s=50;if(void 0!==t.limit){if("number"!=typeof t.limit||!Number.isInteger(t.limit)||t.limit<1)return{ok:!1,error:"limit must be a positive integer"};s=Math.min(t.limit,200)}return{ok:!0,query:n,source:o,limit: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{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";export const BEEOS_LIST_FILES_TOOL_NAME="beeos_list_files";export const BEEOS_LIST_FILES_TOOL_DESCRIPTION="List files stored in the user's BeeOS account, across all their agents/instances. Use this to discover files the user (or another agent) uploaded or generated earlier, then pass a returned fileId to `beeos_fetch_file` to download it locally. Returns fileId, name, mimeType, size, source and originating instance for each file.";export const BEEOS_LIST_FILES_TOOL_SCHEMA={title:"BeeOS list account files",type:"object",description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,additionalProperties:!1,properties:{query:{type:"string",description:"Optional case-insensitive substring to match against file names."},source:{type:"string",enum:["chat_upload","agent_generated","manual"],description:"Optional filter by file origin."},limit:{type:"integer",minimum:1,maximum:200,description:"Maximum number of files to return (default 50)."}}};const DEFAULT_LIMIT=50,MAX_LIMIT=200,REQUEST_TIMEOUT_MS=3e4;export async function executeListFiles(e,t,r){const i=parseParams(e);if(!i.ok)return errorResult(i.error);if(!t)return errorResult("Agent Gateway client unavailable — listing account files requires agentGatewayUrl and a key pair");const o=AbortSignal.timeout(3e4),s=r?AbortSignal.any([r,o]):o;let n;try{n=await t.files.list({signal:s})}catch(e){return errorResult(e instanceof CloudHttpError?`list ${e.status}: ${e.body}`:`list request failed: ${e instanceof Error?e.message:String(e)}`)}let a=n.files??[];if(i.source&&(a=a.filter(e=>e.source===i.source)),i.query){const e=i.query.toLowerCase();a=a.filter(t=>t.fileName.toLowerCase().includes(e))}a=a.slice(0,i.limit);const l=0===a.length?"No matching files found in the account.":`Found ${a.length} file(s):\n`+a.map(e=>`- ${e.fileName} (${e.mimeType}, ${e.size}B, ${e.source}${e.instanceName?`, from ${e.instanceName}`:""}) → fileId=${e.fileId}`).join("\n"),u={ok:!0,files:a};return{...u,content:[{type:"text",text:l}],output:u,result:u,details:u,isError:!1}}function parseParams(e){const t=e&&"object"==typeof e?e:{},r=new Set(["query","source","limit"]),i=Object.keys(t).filter(e=>!r.has(e));if(i.length>0)return{ok:!1,error:`unexpected properties: ${i.join(", ")}`};let o,s;if(void 0!==t.query){if("string"!=typeof t.query)return{ok:!1,error:"query must be a string"};o=t.query.trim()||void 0}if(void 0!==t.source){if("string"!=typeof t.source)return{ok:!1,error:"source must be a string"};const e=["chat_upload","agent_generated","manual"];if(!e.includes(t.source))return{ok:!1,error:`source must be one of ${e.join(", ")}`};s=t.source}let n=50;if(void 0!==t.limit){if("number"!=typeof t.limit||!Number.isInteger(t.limit)||t.limit<1)return{ok:!1,error:"limit must be a positive integer"};n=Math.min(t.limit,200)}return{ok:!0,query:o,source:s,limit:n}}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 +1 @@
1
- import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,createHash}from"node:crypto";import{loadOrCreateDeviceIdentity,buildDeviceAuthPayload,signDeviceAuth,ensureDevicePaired,warmupCryptoSign}from"./device-identity.js";const MIN_PROTOCOL_VERSION=3,MAX_PROTOCOL_VERSION=4,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,CHAT_HISTORY_PAGE_TIMEOUT_MS=1e4,AUTH_FAILURE_CLOSE_CODE=4001,EL_LAG_INTERVAL_MS=200,EL_LAG_THRESHOLD_MS=500,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect";export class GatewayHandshakeError extends Error{code;retryable;retryAfterMs;constructor(e){super(e?.message||"Gateway connect rejected"),this.name="GatewayHandshakeError",this.code=e?.code,this.retryable=!0===e?.retryable||"UNAVAILABLE"===e?.code,this.retryAfterMs="number"==typeof e?.retryAfterMs?e.retryAfterMs:void 0}}export function decodeChatAbortResult(e){if("object"!=typeof e||null===e||Array.isArray(e))throw new Error("Invalid chat.abort response: expected object");const t=e;if("boolean"!=typeof t.aborted)throw new Error("Invalid chat.abort response: expected boolean aborted");if(!Array.isArray(t.runIds)||t.runIds.some(e=>"string"!=typeof e))throw new Error("Invalid chat.abort response: expected string[] runIds");return{aborted:t.aborted,runIds:[...t.runIds]}}function wsUrlToHttpBase(e){try{const t=new URL(e),n="wss:"===t.protocol?"https:":"ws:"===t.protocol?"http:":null;return n?`${n}//${t.host}`:null}catch{return null}}const MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;nextReconnectDelayHintMs=null;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch(e){}Date.now();warmupCryptoSign(this.deviceIdentity)}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const s=new WebSocket(this.config.url);this.ws=s;let r,i=null,o=!1;const c=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),s.close(),t(new Error("Gateway connection timeout"))},3e4),a=t=>{clearTimeout(c),o=!0,this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},h=e=>{const r="string"==typeof e?e:e.message;clearTimeout(c),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:r,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:r}),s.close(),t("string"==typeof e?new Error(e):e)};s.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),s.on("message",e=>{const o="string"==typeof e?e:e.toString("utf-8");if("pong"===o)return void(this.lastPongTs=Date.now());let l;this.emitMessageHash(o);try{l=JSON.parse(o)}catch{return}const d=l.method;d&&!l.id&&(this.isReconnectNotification(d)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",d,l.params));const u=l.type;if("event"===u&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===u){const e=l.event,t=l.payload;if("connect.challenge"===e){const e=Date.now();this.emit("transport",{event:"handshake.challenge_recv",durationMs:e-n});const s=t?.nonce;i=this._sendConnect(s),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n}),r?.();let o=0;const c=setInterval(()=>{o++},200),a=Date.now(),h=setInterval(()=>{const e=Date.now()-a,t=e-200*o;t>500&&this.emit("transport",{event:"eventloop_lag",lagMs:t,elapsedMs:e,probeSeq:o})},200);let l;const d=()=>{clearInterval(c),clearInterval(h),l&&clearTimeout(l),this.off("connected",d),r===d&&(r=void 0)};r=d,l=setTimeout(d,3e4),this.once("connected",d)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):"session.message"===e?this.emit("sessionMessage",t):this.emit("gateway-event",e,t)}else if("hello-ok"===u)this.emit("transport",{event:"handshake.hello_ok_recv",durationMs:Date.now()-n}),a(l);else if("hello-error"===u||"connect-error"===u)h("Authentication rejected by Gateway");else if("res"===u){const e=l.id;if(e&&e===i){if(this.emit("transport",{event:"handshake.connect_res_recv",ok:!!l.ok,durationMs:Date.now()-n}),l.ok){const e=l.payload;a(e??l)}else{const e=l.error,r=new GatewayHandshakeError(e);r.retryable?(clearTimeout(c),this.emit("transport",{event:"connect_fail",reason:r.message,retryable:!0,retryAfterMs:r.retryAfterMs,durationMs:Date.now()-n}),"number"==typeof r.retryAfterMs&&r.retryAfterMs>0&&(this.nextReconnectDelayHintMs=r.retryAfterMs),s.close(),t(r)):h(r)}return}const r=this.pending.get(e);if(r)if(this.pending.delete(e),clearTimeout(r.timer),l.ok)r.resolve(l.payload);else{const e=l.error;r.reject(new Error(e?.message||"Gateway request failed"))}else this.emit("response",l)}}),s.on("pong",()=>{this.lastPongTs=Date.now()}),s.on("close",e=>{const s=o;r?.(),this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),s||(clearTimeout(c),t(new Error(`Connection closed during handshake (code=${e})`))),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),s&&this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),s.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.stopped?clearTimeout(c):(this.connected||(clearTimeout(c),t(e)),this.emit("error",e))})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);let t;const n=this.nextReconnectDelayHintMs,s=null!==n&&n>0;s?(t=Math.min(n,e.maxMs),this.nextReconnectDelayHintMs=null):(t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs),this.reconnectAttempt++),this.emit("reconnecting",{attempt:s?this.reconnectAttempt+1:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emitReconnectFailure(e)})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emitReconnectFailure(e)}))}emitReconnectFailure(e){this.stopped||this.authFailed||this.emit("error",e instanceof Error?e:new Error(String(e)))}_sendConnect(e){const t=Date.now(),n=Date.now(),s="operator",r=["operator.admin","operator.approvals","operator.pairing"],i={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:n};if(e){const t=Date.now(),o=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:s,scopes:r,signedAtMs:n,token:this.config.token,nonce:e}),c=Date.now()-t,a=Date.now();i.nonce=e,i.signature=signDeviceAuth(this.deviceIdentity,o);const h=Date.now()-a;this.emit("transport",{event:"handshake.sign_done",buildPayloadMs:c,signMs:h})}const o=randomUUID(),c={type:"req",id:o,method:"connect",params:{minProtocol:3,maxProtocol:4,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:s,scopes:r,auth:{token:this.config.token},device:i}},a=Date.now(),h=JSON.stringify(c),l=Date.now()-a,d=Date.now();this.ws?.send(h);const u=Date.now()-d;return this.emit("transport",{event:"handshake._sendConnect_done",totalMs:Date.now()-t,serializeMs:l,sendMs:u}),o}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const s=randomUUID(),r={type:"req",id:s,method:e,params:t};return new Promise((t,i)=>{const o=setTimeout(()=>{this.pending.delete(s),i(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(s,{resolve:t,reject:i,timer:o}),this.ws.send(JSON.stringify(r))})}async agentSend(e,t,n,s,r){const i={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};s?.length&&(i.attachments=s),r&&(i.inputProvenance=r);return await this.request("agent",i,12e4)}async chatSend(e,t,n){const s={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(s.attachments=n);return await this.request("chat.send",s,12e4)}async chatAbort(e,t){const n={sessionKey:e};t&&(n.runId=t);return decodeChatAbortResult(await this.request("chat.abort",n))}sendFrame(e){if(!this.ws||!this.connected)return!1;try{return this.ws.send(JSON.stringify(e)),!0}catch{return!1}}agentCancel(e,t){const n={sessionKey:e};t?.runId&&(n.runId=t.runId);const s=`abort_${t?.runId??t?.requestId??e}`;return this.sendFrame({type:"req",id:s,method:"chat.abort",params:n})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async chatHistoryPage(e,t){const n=wsUrlToHttpBase(this.config.url);if(!n)return null;let s;try{s=new URL(`${n}/sessions/${encodeURIComponent(e)}/history`)}catch{return null}null!=t?.limit&&s.searchParams.set("limit",String(t.limit)),t?.cursor&&s.searchParams.set("cursor",t.cursor);try{const e=await fetch(s,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${this.config.token}`},signal:AbortSignal.timeout(1e4)});if(!e.ok)return null;const t=await e.json(),n=Array.isArray(t.messages)?t.messages:Array.isArray(t.items)?t.items:[];return{messages:n.filter(e=>"object"==typeof e&&null!==e),nextCursor:"string"==typeof t.nextCursor&&t.nextCursor.length>0?t.nextCursor:void 0,hasMore:!0===t.hasMore}}catch{return null}}async sessionsMessagesSubscribe(e){return this.request("sessions.messages.subscribe",{key:e})}async sessionsMessagesUnsubscribe(e){return this.request("sessions.messages.unsubscribe",{key:e})}async skillsStatus(e){return this.request("skills.status",e?{agentId:e}:{})}async skillsUpdate(e,t,n){return this.request("skills.update",{skillKey:e,enabled:t,...n?{agentId:n}:{}})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{key:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
1
+ import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,createHash}from"node:crypto";import{loadOrCreateDeviceIdentity,buildDeviceAuthPayload,signDeviceAuth,ensureDevicePaired,warmupCryptoSign}from"./device-identity.js";const MIN_PROTOCOL_VERSION=3,MAX_PROTOCOL_VERSION=4,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,CHAT_HISTORY_PAGE_TIMEOUT_MS=1e4,AUTH_FAILURE_CLOSE_CODE=4001,EL_LAG_INTERVAL_MS=200,EL_LAG_THRESHOLD_MS=500,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect";export class GatewayHandshakeError extends Error{code;retryable;retryAfterMs;constructor(e){super(e?.message||"Gateway connect rejected"),this.name="GatewayHandshakeError",this.code=e?.code,this.retryable=!0===e?.retryable||"UNAVAILABLE"===e?.code,this.retryAfterMs="number"==typeof e?.retryAfterMs?e.retryAfterMs:void 0}}export function decodeChatAbortResult(e){if("object"!=typeof e||null===e||Array.isArray(e))throw new Error("Invalid chat.abort response: expected object");const t=e;if("boolean"!=typeof t.aborted)throw new Error("Invalid chat.abort response: expected boolean aborted");if(!Array.isArray(t.runIds)||t.runIds.some(e=>"string"!=typeof e))throw new Error("Invalid chat.abort response: expected string[] runIds");return{aborted:t.aborted,runIds:[...t.runIds]}}function wsUrlToHttpBase(e){try{const t=new URL(e),n="wss:"===t.protocol?"https:":"ws:"===t.protocol?"http:":null;return n?`${n}//${t.host}`:null}catch{return null}}const MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;nextReconnectDelayHintMs=null;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch(e){}Date.now();warmupCryptoSign(this.deviceIdentity)}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const s=new WebSocket(this.config.url);this.ws=s;let r,i=null,o=!1;const c=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),s.close(),t(new Error("Gateway connection timeout"))},3e4),a=t=>{clearTimeout(c),o=!0,this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},h=e=>{const r="string"==typeof e?e:e.message;clearTimeout(c),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:r,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:r}),s.close(),t("string"==typeof e?new Error(e):e)};s.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),s.on("message",e=>{const o="string"==typeof e?e:e.toString("utf-8");if("pong"===o)return void(this.lastPongTs=Date.now());let l;this.emitMessageHash(o);try{l=JSON.parse(o)}catch{return}const d=l.method;d&&!l.id&&(this.isReconnectNotification(d)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",d,l.params));const u=l.type;if("event"===u&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===u){const e=l.event,t=l.payload;if("connect.challenge"===e){const e=Date.now();this.emit("transport",{event:"handshake.challenge_recv",durationMs:e-n});const s=t?.nonce;i=this._sendConnect(s),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n}),r?.();let o=0;const c=setInterval(()=>{o++},200),a=Date.now(),h=setInterval(()=>{const e=Date.now()-a,t=e-200*o;t>500&&this.emit("transport",{event:"eventloop_lag",lagMs:t,elapsedMs:e,probeSeq:o})},200);let l;const d=()=>{clearInterval(c),clearInterval(h),l&&clearTimeout(l),this.off("connected",d),r===d&&(r=void 0)};r=d,l=setTimeout(d,3e4),this.once("connected",d)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):"session.message"===e?this.emit("sessionMessage",t):this.emit("gateway-event",e,t)}else if("hello-ok"===u)this.emit("transport",{event:"handshake.hello_ok_recv",durationMs:Date.now()-n}),a(l);else if("hello-error"===u||"connect-error"===u)h("Authentication rejected by Gateway");else if("res"===u){const e=l.id;if(e&&e===i){if(this.emit("transport",{event:"handshake.connect_res_recv",ok:!!l.ok,durationMs:Date.now()-n}),l.ok){const e=l.payload;a(e??l)}else{const e=l.error,r=new GatewayHandshakeError(e);r.retryable?(clearTimeout(c),this.emit("transport",{event:"connect_fail",reason:r.message,retryable:!0,retryAfterMs:r.retryAfterMs,durationMs:Date.now()-n}),"number"==typeof r.retryAfterMs&&r.retryAfterMs>0&&(this.nextReconnectDelayHintMs=r.retryAfterMs),s.close(),t(r)):h(r)}return}const r=this.pending.get(e);if(r)if(this.pending.delete(e),clearTimeout(r.timer),l.ok)r.resolve(l.payload);else{const e=l.error;r.reject(new Error(e?.message||"Gateway request failed"))}else this.emit("response",l)}}),s.on("pong",()=>{this.lastPongTs=Date.now()}),s.on("close",e=>{const s=o;r?.(),this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),s||(clearTimeout(c),t(new Error(`Connection closed during handshake (code=${e})`))),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),s&&this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),s.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.stopped?clearTimeout(c):(this.connected||(clearTimeout(c),t(e)),this.emit("error",e))})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);let t;const n=this.nextReconnectDelayHintMs,s=null!==n&&n>0;s?(t=Math.min(n,e.maxMs),this.nextReconnectDelayHintMs=null):(t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs),this.reconnectAttempt++),this.emit("reconnecting",{attempt:s?this.reconnectAttempt+1:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emitReconnectFailure(e)})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emitReconnectFailure(e)}))}emitReconnectFailure(e){this.stopped||this.authFailed||this.emit("error",e instanceof Error?e:new Error(String(e)))}_sendConnect(e){const t=Date.now(),n=Date.now(),s="operator",r=["operator.admin","operator.approvals","operator.pairing"],i={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:n};if(e){const t=Date.now(),o=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:s,scopes:r,signedAtMs:n,token:this.config.token,nonce:e}),c=Date.now()-t,a=Date.now();i.nonce=e,i.signature=signDeviceAuth(this.deviceIdentity,o);const h=Date.now()-a;this.emit("transport",{event:"handshake.sign_done",buildPayloadMs:c,signMs:h})}const o=randomUUID(),c={type:"req",id:o,method:"connect",params:{minProtocol:3,maxProtocol:4,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:s,scopes:r,auth:{token:this.config.token},device:i}},a=Date.now(),h=JSON.stringify(c),l=Date.now()-a,d=Date.now();this.ws?.send(h);const u=Date.now()-d;return this.emit("transport",{event:"handshake._sendConnect_done",totalMs:Date.now()-t,serializeMs:l,sendMs:u}),o}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const s=randomUUID(),r={type:"req",id:s,method:e,params:t};return new Promise((t,i)=>{const o=setTimeout(()=>{this.pending.delete(s),i(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(s,{resolve:t,reject:i,timer:o}),this.ws.send(JSON.stringify(r))})}async agentSend(e,t,n,s,r){const i={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};s?.length&&(i.attachments=s),r&&(i.inputProvenance=r);return await this.request("agent",i,12e4)}async chatSend(e,t,n){const s={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(s.attachments=n);return await this.request("chat.send",s,12e4)}async chatAbort(e,t){const n={sessionKey:e};t&&(n.runId=t);return decodeChatAbortResult(await this.request("chat.abort",n))}sendFrame(e){if(!this.ws||!this.connected)return!1;try{return this.ws.send(JSON.stringify(e)),!0}catch{return!1}}agentCancel(e,t){const n={sessionKey:e};t?.runId&&(n.runId=t.runId);const s=`abort_${t?.runId??t?.requestId??e}`;return this.sendFrame({type:"req",id:s,method:"chat.abort",params:n})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async chatHistoryPage(e,t){const n=wsUrlToHttpBase(this.config.url);if(!n)return null;let s;try{s=new URL(`${n}/sessions/${encodeURIComponent(e)}/history`)}catch{return null}null!=t?.limit&&s.searchParams.set("limit",String(t.limit)),t?.cursor&&s.searchParams.set("cursor",t.cursor);try{const e=await fetch(s,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${this.config.token}`},signal:AbortSignal.timeout(1e4)});if(!e.ok)return null;const t=await e.json(),n=Array.isArray(t.messages)?t.messages:Array.isArray(t.items)?t.items:[];return{messages:n.filter(e=>"object"==typeof e&&null!==e),nextCursor:"string"==typeof t.nextCursor&&t.nextCursor.length>0?t.nextCursor:void 0,hasMore:!0===t.hasMore}}catch{return null}}async sessionsMessagesSubscribe(e){return this.request("sessions.messages.subscribe",{key:e})}async sessionsMessagesUnsubscribe(e){return this.request("sessions.messages.unsubscribe",{key:e})}async skillsStatus(e){return this.request("skills.status",e?{agentId:e}:{})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
@@ -1 +1 @@
1
- import{extractChatPrompt}from"@beeos-ai/message-sdk/chat-envelope";import{buildMentionInstruction,parseMentionsParam}from"./delegation/mention-resolver.js";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{decodeWake}from"./automation/wake.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>"string"==typeof e.conversationId?e.conversationId:"";export const createImInboundHandler=e=>{const{runtime:t,pluginConfig:n,client:o,localServer:a,promptRunStore:s,waitIfExclusive:i,assistantMessageCommitRouter:r,toolResultObservationRouter:c,modelCallObservationRouter:d,sessionMutationCoordinator:l,logger:m,writeObs:p,principalIdOverride:g,dispatch:u=dispatchImInboundPrompt,completeA2ATask:I,activeRunAuthority:h}=e;return{onChatMessage:async _=>{const v=decodeWake(_);if("invalid"===v.kind||"wake"===v.kind&&!e.automationTurnPolicy?.isReady())return void m.warn?.("[beeos-claw] Automation Wake rejected: invalid contract or unavailable turn policy");const y=_.content??{},f=y.metadata??void 0,b="string"==typeof f?.target_agent_id?f.target_agent_id:"",w=b?a.resolveLocalAgentId?.(b):void 0;if(!b||!w)return void m.warn?.(`[beeos-claw] chat_message target_agent_id=${b||"missing"} not resolvable to a local agent — drop`);const $=extractChatPrompt(_,{source:"chat_message",localAgentId:w});if(!$)return;if(!$.channelId||!$.messageId)return void m.warn?.(`[beeos-claw] chat_message missing channelId/messageId (sessionKey=${$.sessionKey}) — drop`);const R=g||a.getDefaultPlatformAgent()?.instanceId||"";if(!R)return void m.warn?.(`[beeos-claw] chat_message dropped (no principalId resolved, sessionKey=${$.sessionKey})`);const k="string"==typeof f?.delivery_principal?f.delivery_principal:"";if(k&&k!==R)return void m.info?.(`[beeos-claw] chat_message skipped (delivery_principal=${k}, selfPrincipalId=${R}, message_id=${$.messageId})`);const C=parseMentionsParam(y.mentions);a.ensureImInboundSession($.sessionKey,$.channelId,$.messageId,C);const M=buildMentionInstruction(C),K=M?`${M}\n${$.message}`:$.message,O=resolveOrigin(f?.protocol),P=$.messageId,A="a2a"===O&&I?(e,t)=>I(P,e,t):void 0;m.debug?.(`[beeos-claw] chat_message dispatch (sessionKey=${$.sessionKey}, channelId=${$.channelId}, origin=${O})`);const T=e=>{p({component:"plugin",domain:"im_dispatch",name:`im.dispatch.${e.type}`,severity:"dispatch_error"===e.type?"warn":"info",payload:e})};try{await u({runtime:t,titles:e.titles,pluginConfig:n,prompt:{channelId:$.channelId,inboundMessageId:$.messageId,requestId:_.requestId,selfPrincipalId:R,dispatchInput:{message:K,attachments:$.files},sessionKey:$.sessionKey},client:o,logger:m,promptRunStore:s,...i?{waitIfExclusive:i}:{},assistantMessageCommitRouter:r,toolResultObservationRouter:c,modelCallObservationRouter:d,sessionMutationCoordinator:l,abortTransport:a,origin:O,onLifecycleEvent:T,...A?{onTerminalResult:A}:{},activeRunAuthority:h,..."wake"===v.kind?{enterTurn:(t,n)=>e.automationTurnPolicy.enter(t,n,v.context)}:{}})}catch(e){m.error?.(`[beeos-claw] chat_message dispatch failed channelId=${$.channelId} in_reply_to=${$.messageId} error_code=runtime_dispatch_failed`)}},onChatCancel:async e=>{const t=e.content??{},n="string"==typeof t.target_message_id?t.target_message_id:"",o="string"==typeof t.reason?t.reason:"unknown";if(!n)return void m.warn?.(`[beeos-claw] chat_cancel dropped (no target_message_id, reason=${o})`);const a=conversationIdFromMessage(e);if(!a)return void m.warn?.(`[beeos-claw] chat_cancel dropped (no conversation identity, target_message_id=${n}, reason=${o})`);const i=`chat_cancel:${o}`,r=await s.abort(promptRunKey(a,n),i);r.ok?m.info?.(`[beeos-claw] chat_cancel accepted (target_message_id=${n}, requestId=${r.requestId}, channelId=${r.channelId}, reason=${o})`):m.info?.(`[beeos-claw] chat_cancel no-op (target_message_id=${n}, reason=${o}, why=${r.reason})`)}}};
1
+ import{extractChatPrompt}from"./chat-envelope.js";import{buildMentionInstruction,parseMentionsParam}from"./delegation/mention-resolver.js";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{decodeWake}from"./automation/wake.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>"string"==typeof e.conversationId?e.conversationId:"";export const createImInboundHandler=e=>{const{runtime:n,pluginConfig:t,client:o,localServer:a,promptRunStore:s,waitIfExclusive:r,assistantMessageCommitRouter:i,toolResultObservationRouter:d,modelCallObservationRouter:c,sessionMutationCoordinator:l,logger:p,writeObs:m,principalIdOverride:u,dispatch:g=dispatchImInboundPrompt,completeA2ATask:I,activeRunAuthority:v,applyTurnModel:h,restoreTurnModel:y}=e;return{onChatMessage:async _=>{const f=decodeWake(_);if("invalid"===f.kind||"wake"===f.kind&&!e.automationTurnPolicy?.isReady())return void p.warn?.("[beeos-claw] Automation Wake rejected: invalid contract or unavailable turn policy");const w=_.content??{},b=w.metadata??void 0,$="string"==typeof b?.target_agent_id?b.target_agent_id:"",R=$?a.resolveLocalAgentId?.($):void 0;if(!$||!R)return void p.warn?.(`[beeos-claw] chat_message target_agent_id=${$||"missing"} not resolvable to a local agent — drop`);const M=extractChatPrompt(_,{source:"chat_message",localAgentId:R});if(!M)return;if(!M.channelId||!M.messageId)return void p.warn?.(`[beeos-claw] chat_message missing channelId/messageId (sessionKey=${M.sessionKey}) — drop`);const k=u||a.getDefaultPlatformAgent()?.instanceId||"";if(!k)return void p.warn?.(`[beeos-claw] chat_message dropped (no principalId resolved, sessionKey=${M.sessionKey})`);const O="string"==typeof b?.delivery_principal?b.delivery_principal:"";if(O&&O!==k)return void p.info?.(`[beeos-claw] chat_message skipped (delivery_principal=${O}, selfPrincipalId=${k}, message_id=${M.messageId})`);const A=parseMentionsParam(w.mentions);a.ensureImInboundSession(M.sessionKey,M.channelId,M.messageId,A);const C=buildMentionInstruction(A),K=C?`${C}\n${M.message}`:M.message,T=resolveOrigin(b?.protocol),P="string"==typeof b?.modelOverrideId&&b.modelOverrideId.length>0?b.modelOverrideId:void 0,E=M.messageId,j="a2a"===T&&I?(e,n)=>I(E,e,n):void 0;p.debug?.(`[beeos-claw] chat_message dispatch (sessionKey=${M.sessionKey}, channelId=${M.channelId}, origin=${T})`);const x=e=>{m({component:"plugin",domain:"im_dispatch",name:`im.dispatch.${e.type}`,severity:"dispatch_error"===e.type?"warn":"info",payload:e})};try{await g({runtime:n,pluginConfig:t,prompt:{channelId:M.channelId,inboundMessageId:M.messageId,requestId:_.requestId,selfPrincipalId:k,dispatchInput:{message:K,attachments:M.files},sessionKey:M.sessionKey,...P?{modelOverrideId:P}:{}},...h?{applyTurnModel:h}:{},...y?{restoreTurnModel:y}:{},client:o,logger:p,promptRunStore:s,...r?{waitIfExclusive:r}:{},assistantMessageCommitRouter:i,toolResultObservationRouter:d,modelCallObservationRouter:c,sessionMutationCoordinator:l,abortTransport:a,origin:T,onLifecycleEvent:x,...j?{onTerminalResult:j}:{},activeRunAuthority:v,..."wake"===f.kind||e.approval?{enterTurn:(n,t)=>{const o="wake"===f.kind?e.automationTurnPolicy.enter(n,t,f.context):void 0,a=e.approval?.policy.enter(n,t,{sessionId:M.channelId,taskId:M.messageId,ownerIdentity:_.senderId??"",runtimeEpoch:e.approval.runtimeEpoch,platformAgentId:e.approval.platformAgentId,active:()=>e.approval.active()&&"running"===s.phase(promptRunKey(M.channelId,M.messageId))});return{run:e=>a?a.run(()=>o?o.run(e):e()):o.run(e),close:()=>{a?.close(),o?.close()}}}}:{}})}catch(e){p.error?.(`[beeos-claw] chat_message dispatch failed channelId=${M.channelId} in_reply_to=${M.messageId} error_code=runtime_dispatch_failed`)}},onChatCancel:async n=>{const t=n.content??{},o="string"==typeof t.target_message_id?t.target_message_id:"",a="string"==typeof t.reason?t.reason:"unknown";if(!o)return void p.warn?.(`[beeos-claw] chat_cancel dropped (no target_message_id, reason=${a})`);const r=conversationIdFromMessage(n);if(!r)return void p.warn?.(`[beeos-claw] chat_cancel dropped (no conversation identity, target_message_id=${o}, reason=${a})`);const i=`chat_cancel:${a}`;e.approval?.gate.cancelTask(r,o);const d=await s.abort(promptRunKey(r,o),i);d.ok?p.info?.(`[beeos-claw] chat_cancel accepted (target_message_id=${o}, requestId=${d.requestId}, channelId=${d.channelId}, reason=${a})`):p.info?.(`[beeos-claw] chat_cancel no-op (target_message_id=${o}, reason=${a}, why=${d.reason})`)},onApprovalAction:async n=>{if(!e.approval||!e.approval.active()||!n.senderId?.startsWith("user:"))return;const t=n.content;await e.approval.gate.resolve(t?.payload,n.senderId,n.conversationId)}}};