beeos-claw 0.6.66-cloud.2 → 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.
- package/README.md +12 -13
- package/dist/a2a/tools.js +1 -1
- package/dist/acp-gateway/beeos-file-resolver.js +1 -1
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-gateway/gateway-events.js +1 -1
- package/dist/acp-gateway/local-session-history.js +1 -1
- package/dist/acp-gateway/mcp-desired-state-controller.js +1 -1
- package/dist/acp-gateway/mcp-desired-state-store.js +1 -1
- package/dist/acp-gateway/mcp-service.js +1 -1
- package/dist/acp-server.js +1 -1
- package/dist/agent-sync.js +1 -1
- package/dist/approval/gate.js +1 -0
- package/dist/approval/journal.js +1 -0
- package/dist/approval/presentation.js +1 -0
- package/dist/approval/protocol.generated.js +1 -0
- package/dist/approval/turn-policy.js +1 -0
- package/dist/auto-reply-tokens.js +1 -1
- package/dist/automation/tools.js +1 -1
- package/dist/canvas/relay-client.js +1 -1
- package/dist/canvas/static-html.js +1 -0
- package/dist/canvas/tools.js +1 -1
- package/dist/canvas/ws-provider.js +1 -1
- package/dist/canvas-orchestrator.js +1 -1
- package/dist/chat-envelope.js +1 -0
- package/dist/connectors/managed-connector-tools.js +1 -0
- package/dist/files/fetch-tool.js +1 -1
- package/dist/files/list-tool.js +1 -1
- package/dist/gateway-client.js +1 -1
- package/dist/im-inbound-handler.js +1 -1
- package/dist/im-runtime-dispatch.js +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-connector-proxy.js +1 -1
- package/dist/media/arouter-config.js +1 -1
- package/dist/media/image-provider.js +1 -1
- package/dist/media/video-provider.js +1 -1
- package/dist/message-client-composition.js +1 -1
- package/dist/message-projection-inbound-consumer.js +1 -1
- package/dist/plugin-registration-plan.js +1 -1
- package/dist/runtime/agent-projection-production.js +1 -1
- package/dist/runtime/callback-queue.js +1 -1
- package/dist/runtime/chat-handler-port.js +1 -0
- package/dist/runtime/conversation-title.js +1 -1
- package/dist/runtime/core-execution-attempt-context.js +1 -1
- package/dist/runtime/deepseek-reasoning-stream.js +1 -1
- package/dist/runtime/im-reply-sink.js +1 -1
- package/dist/runtime/message-service-conversation-clear-http-authority.js +1 -1
- package/dist/runtime/message-service-conversation-run-boundary.js +1 -1
- package/dist/runtime/message-service-cron-delivery-http-authority.js +1 -1
- package/dist/runtime/message-service-runtime-authenticated-fetch.js +1 -1
- package/dist/runtime/message-service-runtime-claim-authority.js +1 -1
- package/dist/runtime/message-service-runtime-message-channel.js +1 -1
- package/dist/runtime/message-service-session-model-http-authority.js +1 -1
- package/dist/runtime/ms-runtime-message-channel-adapter.js +1 -1
- package/dist/runtime/ms-runtime-message-channel-consumer-factory.js +1 -1
- package/dist/runtime/ms-runtime-rpc-adapter.js +1 -1
- package/dist/runtime/native-cloud-chat-composition.js +1 -0
- package/dist/runtime/native-cloud-prompt-bridge.js +1 -0
- package/dist/runtime/native-input-files-http.js +1 -0
- package/dist/runtime/native-input-files.js +1 -0
- package/dist/runtime/native-output-file-attempt.js +1 -0
- package/dist/runtime/native-prompt-authority.js +1 -0
- package/dist/runtime/native-prompt-preflight.js +1 -0
- package/dist/runtime/native-session-new-bridge.js +1 -0
- package/dist/runtime/openclaw-runtime-capabilities.js +1 -1
- package/dist/runtime/openclaw-session-model.js +1 -0
- package/dist/runtime/openclaw-session-write-quiescence.js +1 -1
- package/dist/runtime/reply-router.js +1 -1
- package/dist/runtime/runtime-command-dispatcher.js +1 -1
- package/dist/runtime/runtime-command-journal.js +1 -1
- package/dist/runtime/runtime-error-part.js +1 -0
- package/dist/runtime/runtime-local-parity-matrix.js +1 -1
- package/dist/runtime/runtime-message-channel-contract.js +1 -1
- package/dist/runtime/runtime-operation-resource-resolver.js +1 -1
- package/dist/runtime/runtime-parity-manifest-digest.js +1 -1
- package/dist/runtime/runtime-parity-manifest.js +1 -1
- package/dist/runtime/runtime-production-authority.js +1 -1
- package/dist/runtime/runtime-service-contract-compatibility.js +1 -1
- package/dist/runtime/runtime-service-production-candidate.js +1 -1
- package/dist/runtime/runtime-session-control-service.js +1 -1
- package/dist/runtime/runtime-skills-service.js +1 -1
- package/dist/runtime/session-new-runtime-delivery-bridge.js +1 -1
- package/dist/runtime/session-set-model-mapping.js +1 -1
- package/dist/runtime/write-runtime-failure.js +1 -1
- package/dist/terminal/terminal-lease-bound-bridge-client.js +1 -1
- package/dist/upload/desktop-screenshot-tool.js +1 -1
- package/dist/upload/finalize-hook.js +1 -1
- package/dist/upload/native-output-tool-hook.js +1 -0
- package/dist/upload/tool-artifact-enrichment.js +1 -1
- package/dist/upload/tool.js +1 -1
- package/dist/upload/upload-file.js +1 -1
- package/openclaw.plugin.json +7 -2
- package/package.json +11 -11
- package/runtime-service-wire-identity.v1.json +12 -12
- package/scripts/runtime-blackbox-worker-main.mjs +5 -2
- package/dist/agent-auth.js +0 -1
- package/dist/bridge-discovery.js +0 -1
- package/dist/contracts/question/codec.js +0 -1
- package/dist/runtime/agent-gateway-runtime-attempt-authorization.js +0 -1
- package/dist/runtime/message-service-runtime-invocation-authority.js +0 -1
- package/dist/runtime/message-service-session-reasoning-http-authority.js +0 -1
- package/dist/runtime/openclaw-native-sessions.js +0 -1
- package/dist/runtime/openclaw-question-adapter.js +0 -1
- package/dist/runtime/openclaw-title-provider.js +0 -1
- package/dist/runtime/question-wire.js +0 -1
- package/dist/runtime/requested-model.js +0 -1
- package/dist/runtime/runtime-jwks-verifier.js +0 -1
- package/dist/runtime/runtime-reasoning.js +0 -1
- package/dist/runtime/runtime-registration.js +0 -1
- package/dist/runtime/runtime-transport-failure-policy.js +0 -1
- package/dist/runtime/split-runtime-authorization-verifier.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{classifyAgentRuntimeFailure}from"../runtime/agent-runtime-failure-classifier.js";import{stripTransportMetadata,normalizeTransportText,previewSanitizedText}from"../message-sanitization/transport-metadata-sanitizer.js";import{classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{toMessageServiceCompletedStopReason}from"../runtime/message-service-stop-reason.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";import{observeLifecyclePromise}from"../runtime/lifecycle-promise-observer.js";import{runOutputTerminalFromFailure}from"../runtime/run-output-router.js";import{fromAcpRunOutputUpdate}from"./acp-run-output-mapper.js";import{openRunOutputRoute}from"./run-output-route.js";export function lifecycleEndFailureMessage(t){if(!t)return;const e=!0===t.isError||!0===t.is_error,s=t.error,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof t.message||"string"==typeof t.reason;if(e||o){if(!e){const e="string"==typeof t.stopReason?t.stopReason:"";if("error"!==e&&"timeout"!==e&&"aborted"!==e)return}return("string"==typeof t.message?t.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof t.reason?t.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error"}}function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export function recordToolCallTurnForUpdate(t,e,s){s&&"tool_call"===e.sessionUpdate&&e.toolCallId&&(t.toolCallTurns??=new Map,t.toolCallTurns.set(e.toolCallId,{turnId:s,..."string"==typeof e.name&&e.name?{toolName:e.name}:{}}))}export class AcpGatewayEvents{state;transport;config;logger;writeObs;runOutputRouter;adoptBackgroundMutation;abortGatewayRun;agentFetchFn;getCronConversationDelivery;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.runOutputRouter=t.runOutputRouter,this.adoptBackgroundMutation=t.adoptBackgroundMutation,this.abortGatewayRun=t.abortGatewayRun,this.agentFetchFn=t.agentFetch,this.getCronConversationDelivery=t.getCronConversationDelivery,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}observeLifecycle(t,e,s){observeLifecyclePromise(t,this.logger,{component:"gateway-events",action:e,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.observeLifecycle(this.terminalRunOutput(t,this.failedRunOutput(classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${e}ms`,source:"live_gateway",runtimeKind:"openclaw"}))),"stream_stale_timeout",t))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);if(!s)return!1;s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),recordToolCallTurnForUpdate(t,e,s);const n=fromAcpRunOutputUpdate(e);return!!n&&(this.runOutputRouter.project(s,n),!0)}trackRunOutputWork(t,e){const s=resolveTurnId(t);s&&this.runOutputRouter.track(s,e)}beginRunOutputUpload(t,e,s){if(s!==BEEOS_UPLOAD_TOOL_NAME)return;const n=resolveTurnId(t);n&&this.runOutputRouter.beginUpload(n,e)}terminalRunOutput(t,e){const s=resolveTurnId(t);return s?this.runOutputRouter.terminal(s,e):Promise.reject(new Error(`run output terminal has no run id: ${t.sessionId}`))}failedRunOutput(t){return runOutputTerminalFromFailure(t,Errors.AgentError.rpcCode)}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.beginRunOutputUpload(e,t,s),this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i),this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const u={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,u),e.assistantMq?.push(u)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(!e.promptDone)if("final"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t.stopReason)}),"chat_final",e);else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"chat_cancelled",e);else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(n)),"interrupted"===n.kind?"chat_interrupt":"chat_failure",e)}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0,this.scheduleStructuredAssistantMedia(e,t.data);const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);this.beginRunOutputUpload(e,o,a),n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);this.beginRunOutputUpload(e,t,a),n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.trackRunOutputWork(e,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)}));else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,source:"live_gateway",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(a)),"interrupted"===a.kind?"agent_interrupt":o,e)};if("end"===n){const t=lifecycleEndFailureMessage(s);if(t)o(t,"agent_failure");else{const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t)}),"agent_final",e)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"agent_cancelled",e);else if("error"===n){const t=s?.error,e=t&&"object"==typeof t?t:void 0;o(("string"==typeof s?.message?s.message:void 0)??("string"==typeof t?t:void 0)??("string"==typeof e?.message?e.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(e?JSON.stringify(e):void 0)??"gateway lifecycle error","agent_failure")}}}scheduleStructuredAssistantMedia(t,e){const s=resolveTurnId(t);if(!s||!this.runOutputRouter.routesTo(s,"message_service"))return;const n=isRecord(e)?e:void 0,o=[...new Set([..."string"==typeof n?.mediaUrl?[n.mediaUrl]:[],...Array.isArray(n?.mediaUrls)?n.mediaUrls.filter(t=>"string"==typeof t):[]].map(t=>t.trim()).filter(Boolean))];if(0===o.length)return;const a=resolveToolArtifactLinks({toolCallId:`final:${s}`,toolName:"final_reply_media",payloadMediaUrls:o}).then(e=>{for(const s of e){const e={sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}},timestamp:(new Date).toISOString()};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}).catch(t=>{this.logger.warn?.(`[gateway-events] structured assistant media enrichment failed runId=${s}: ${t instanceof Error?t.message:String(t)}`)});this.trackRunOutputWork(t,a)}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,u=a??r,d=n?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:u,toolName:i,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}),n=resolveTurnId(t);try{await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:n,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}finally{n&&e.toolName===BEEOS_UPLOAD_TOOL_NAME&&this.runOutputRouter.completeUpload(n,e.toolCallId)}}attachGatewayRun(t,e){this.logger.debug?.(`[gateway-events] Attaching Gateway run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`);const s=this.adoptBackgroundMutation(t,e);if(!s.ok)return this.observeLifecycle(this.abortGatewayRun(t.gatewaySessionKey,e),"background_run_conflict_abort",t),!1;t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const n=this.state.runIdToCronJobId.get(e);n&&(t.cronJobId=n),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.emittedToolResultKeys=new Set;const o=["acp"];return"owner"===s.ownership&&t.channelId?.trim()&&!t.cronJobId&&o.push("message_service"),openRunOutputRoute(this.runOutputRouter,t,e,o,{awaitPersistenceReconciliation:o.includes("message_service")}),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),!0}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),"started"===e.action&&this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const e="string"==typeof t.jobId&&t.jobId.trim()?t.jobId.trim():void 0,s="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!e||void 0===s)return void this.logger.warn?.("[gateway-events] cron start missing stable execution identity; skipping notification");const n="string"==typeof t.runId&&t.runId.trim()?t.runId.trim():`cron:${e}:${s}`,o={...t,jobId:e,runAtMs:s,runId:n},a="string"==typeof t.sessionKey?t.sessionKey:void 0;a&&null==o.sessionKey&&(o.sessionKey=a);const r=a?this.state.findSessionByKey(a)?.sessionId:void 0;if(r&&(o.sessionId=r),("string"!=typeof o.name||!o.name)&&e&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),s=t?.jobs;if(Array.isArray(s))for(const t of s)if(isRecord(t)&&t.id===e&&"string"==typeof t.name&&t.name){o.name=t.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}const i=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:o})})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const u=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:u}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getCronConversationDelivery?.();if(!e)return;const s="string"==typeof t.jobId?t.jobId:void 0;if(!s)return;const n=t.summary,o="string"==typeof t.runId&&t.runId?t.runId:void 0,a="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!o&&void 0===a)return void this.logger.warn?.(`[gateway-events] cron result missing stable execution identity; skipping conversation delivery job=${s}`);let r=this.cronChannelCache?.get(s);if(!r&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===s){r=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(r)try{const i=await e.deliver({conversationId:r,jobId:s,...o?{runId:o}:{},...void 0!==a?{runAtMs:a}:{},..."string"==typeof t.sessionKey&&t.sessionKey?{sessionKey:t.sessionKey}:{},body:n,..."string"==typeof t.status&&t.status?{status:t.status}:{}});this.logger.info?.(`[gateway-events] delivered cron result to channel=${r} job=${s} message=${i.messageId} disposition=${i.disposition}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${r} job=${s} err=${e}`),void this.cronChannelCache?.delete(s);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewSanitizedText(t.cumulative),s=void 0!==t.delta?previewSanitizedText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
|
|
1
|
+
import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{classifyAgentRuntimeFailure}from"../runtime/agent-runtime-failure-classifier.js";import{stripTransportMetadata,normalizeTransportText,previewSanitizedText}from"../message-sanitization/transport-metadata-sanitizer.js";import{classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{toMessageServiceCompletedStopReason}from"../runtime/message-service-stop-reason.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";import{observeLifecyclePromise}from"../runtime/lifecycle-promise-observer.js";import{runOutputTerminalFromFailure}from"../runtime/run-output-router.js";import{fromAcpRunOutputUpdate}from"./acp-run-output-mapper.js";import{openRunOutputRoute}from"./run-output-route.js";export function lifecycleEndFailureMessage(t){if(!t)return;const e=!0===t.isError||!0===t.is_error,s=t.error,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof t.message||"string"==typeof t.reason;if(e||o){if(!e){const e="string"==typeof t.stopReason?t.stopReason:"";if("error"!==e&&"timeout"!==e&&"aborted"!==e)return}return("string"==typeof t.message?t.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof t.reason?t.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error"}}function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export function recordToolCallTurnForUpdate(t,e,s){s&&"tool_call"===e.sessionUpdate&&e.toolCallId&&(t.toolCallTurns??=new Map,t.toolCallTurns.set(e.toolCallId,{turnId:s,..."string"==typeof e.name&&e.name?{toolName:e.name}:{}}))}export class AcpGatewayEvents{state;transport;config;logger;writeObs;runOutputRouter;adoptBackgroundMutation;abortGatewayRun;cloud;getCronConversationDelivery;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.runOutputRouter=t.runOutputRouter,this.adoptBackgroundMutation=t.adoptBackgroundMutation,this.abortGatewayRun=t.abortGatewayRun,this.cloud=t.cloud,this.getCronConversationDelivery=t.getCronConversationDelivery,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}observeLifecycle(t,e,s){observeLifecyclePromise(t,this.logger,{component:"gateway-events",action:e,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.observeLifecycle(this.terminalRunOutput(t,this.failedRunOutput(classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${e}ms`,source:"live_gateway",runtimeKind:"openclaw"}))),"stream_stale_timeout",t))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);if(!s)return!1;s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),recordToolCallTurnForUpdate(t,e,s);const n=fromAcpRunOutputUpdate(e);return!!n&&(this.runOutputRouter.project(s,n),!0)}trackRunOutputWork(t,e){const s=resolveTurnId(t);s&&this.runOutputRouter.track(s,e)}beginRunOutputUpload(t,e,s){if(s!==BEEOS_UPLOAD_TOOL_NAME)return;const n=resolveTurnId(t);n&&this.runOutputRouter.beginUpload(n,e)}terminalRunOutput(t,e){const s=resolveTurnId(t);return s?this.runOutputRouter.terminal(s,e):Promise.reject(new Error(`run output terminal has no run id: ${t.sessionId}`))}failedRunOutput(t){return runOutputTerminalFromFailure(t,Errors.AgentError.rpcCode)}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.beginRunOutputUpload(e,t,s),this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i),this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const u={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,u),e.assistantMq?.push(u)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(!e.promptDone)if("final"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t.stopReason)}),"chat_final",e);else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"chat_cancelled",e);else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(n)),"interrupted"===n.kind?"chat_interrupt":"chat_failure",e)}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0,this.scheduleStructuredAssistantMedia(e,t.data);const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);this.beginRunOutputUpload(e,o,a),n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);this.beginRunOutputUpload(e,t,a),n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.trackRunOutputWork(e,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)}));else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,source:"live_gateway",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(a)),"interrupted"===a.kind?"agent_interrupt":o,e)};if("end"===n){const t=lifecycleEndFailureMessage(s);if(t)o(t,"agent_failure");else{const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t)}),"agent_final",e)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"agent_cancelled",e);else if("error"===n){const t=s?.error,e=t&&"object"==typeof t?t:void 0;o(("string"==typeof s?.message?s.message:void 0)??("string"==typeof t?t:void 0)??("string"==typeof e?.message?e.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(e?JSON.stringify(e):void 0)??"gateway lifecycle error","agent_failure")}}}scheduleStructuredAssistantMedia(t,e){const s=resolveTurnId(t);if(!s||!this.runOutputRouter.routesTo(s,"message_service"))return;const n=isRecord(e)?e:void 0,o=[...new Set([..."string"==typeof n?.mediaUrl?[n.mediaUrl]:[],...Array.isArray(n?.mediaUrls)?n.mediaUrls.filter(t=>"string"==typeof t):[]].map(t=>t.trim()).filter(Boolean))];if(0===o.length)return;const a=resolveToolArtifactLinks({toolCallId:`final:${s}`,toolName:"final_reply_media",payloadMediaUrls:o}).then(e=>{for(const s of e){const e={sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}},timestamp:(new Date).toISOString()};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}).catch(t=>{this.logger.warn?.(`[gateway-events] structured assistant media enrichment failed runId=${s}: ${t instanceof Error?t.message:String(t)}`)});this.trackRunOutputWork(t,a)}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,u=a??r,d=n?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:u,toolName:i,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}),n=resolveTurnId(t);try{await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:n,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}finally{n&&e.toolName===BEEOS_UPLOAD_TOOL_NAME&&this.runOutputRouter.completeUpload(n,e.toolCallId)}}attachGatewayRun(t,e){this.logger.debug?.(`[gateway-events] Attaching Gateway run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`);const s=this.adoptBackgroundMutation(t,e);if(!s.ok)return this.observeLifecycle(this.abortGatewayRun(t.gatewaySessionKey,e),"background_run_conflict_abort",t),!1;t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const n=this.state.runIdToCronJobId.get(e);n&&(t.cronJobId=n),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.emittedToolResultKeys=new Set;const o=["acp"];return"owner"===s.ownership&&t.channelId?.trim()&&!t.cronJobId&&o.push("message_service"),openRunOutputRoute(this.runOutputRouter,t,e,o,{awaitPersistenceReconciliation:o.includes("message_service")}),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),!0}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),"started"===e.action&&this.cloud&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.cloud)return;const e="string"==typeof t.jobId&&t.jobId.trim()?t.jobId.trim():void 0,s="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!e||void 0===s)return void this.logger.warn?.("[gateway-events] cron start missing stable execution identity; skipping notification");const n="string"==typeof t.runId&&t.runId.trim()?t.runId.trim():`cron:${e}:${s}`,o={...t,jobId:e,runAtMs:s,runId:n},a="string"==typeof t.sessionKey?t.sessionKey:void 0;a&&null==o.sessionKey&&(o.sessionKey=a);const r=a?this.state.findSessionByKey(a)?.sessionId:void 0;if(r&&(o.sessionId=r),("string"!=typeof o.name||!o.name)&&e&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),s=t?.jobs;if(Array.isArray(s))for(const t of s)if(isRecord(t)&&t.id===e&&"string"==typeof t.name&&t.name){o.name=t.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}try{await this.cloud.events.publish({type:"cron",data:o})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const u=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:u}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getCronConversationDelivery?.();if(!e)return;const s="string"==typeof t.jobId?t.jobId:void 0;if(!s)return;const n=t.summary,o="string"==typeof t.runId&&t.runId?t.runId:void 0,a="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!o&&void 0===a)return void this.logger.warn?.(`[gateway-events] cron result missing stable execution identity; skipping conversation delivery job=${s}`);let r=this.cronChannelCache?.get(s);if(!r&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===s){r=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(r)try{const i=await e.deliver({conversationId:r,jobId:s,...o?{runId:o}:{},...void 0!==a?{runAtMs:a}:{},..."string"==typeof t.sessionKey&&t.sessionKey?{sessionKey:t.sessionKey}:{},body:n,..."string"==typeof t.status&&t.status?{status:t.status}:{}});this.logger.info?.(`[gateway-events] delivered cron result to channel=${r} job=${s} message=${i.messageId} disposition=${i.disposition}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${r} job=${s} err=${e}`),void this.cronChannelCache?.delete(s);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewSanitizedText(t.cumulative),s=void 0!==t.delta?previewSanitizedText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{closeSync,existsSync,openSync,readFileSync,readSync,statSync}from"node:fs";import path from"node:path";import{resolveOpenClawStateDir}from"../runtime/openclaw-state-dir.js";
|
|
1
|
+
import{closeSync,existsSync,openSync,readFileSync,readSync,statSync}from"node:fs";import path from"node:path";import{resolveOpenClawStateDir}from"../runtime/openclaw-state-dir.js";export const LOCAL_HISTORY_LIMIT=200;export const LOCAL_HISTORY_TAIL_WINDOW_LINES=6400;export const LOCAL_HISTORY_TAIL_CHUNK_BYTES=65536;export const LOCAL_HISTORY_TAIL_MAX_BYTES=1048576;export function findLocalHistoryEntryByMessageId(t,e){for(let s=t.length-1;s>=0;s--){const i=t[s];if(!i)continue;const o=i.__openclaw;if(isRecord(o)&&o.id===e)return i;if(i.id===e)return i}}export class LocalSessionHistory{logger;stateDirOverride;agentsDirOverride;constructor(t){this.logger=t.logger,this.stateDirOverride=t.stateDir,this.agentsDirOverride=t.agentsDir}readEntries(t){const{agentId:e,sessionKey:s,sessionId:i}=t;if(!e||!s)return[];const o=this.resolveSessionsDir(e),r=this.resolveSessionFile(o,s,i);if(!r)return[];const n=this.readTailLines(r);if(0===n.length)return[];const c=[];for(const t of n){let e;try{e=JSON.parse(t)}catch{continue}if(!e||"object"!=typeof e)continue;const s=e;if("message"!==s.type)continue;const i=s.message;if(!i||"object"!=typeof i)continue;const o=i,r="string"==typeof o.role?o.role:void 0;if(!r)continue;const n="string"==typeof s.id?s.id:void 0,a=n?{id:n}:void 0,l=o.timestamp,f=s.timestamp,p="number"==typeof l||"string"==typeof l?l:"string"==typeof f?f:void 0,y={...o,role:r,...void 0!==p?{timestamp:p}:{},...a?{__openclaw:a}:{}};if(c.push(y),c.length>=200)break}return c.reverse()}resolveStateDir(){return this.stateDirOverride??resolveOpenClawStateDir()}resolveSessionsDir(t){return this.agentsDirOverride?path.join(this.agentsDirOverride,t,"sessions"):path.join(this.resolveStateDir(),"agents",t,"sessions")}resolveSessionFile(t,e,s){const i=path.join(t,"sessions.json");let o;if(existsSync(i))try{const t=readFileSync(i,"utf-8"),e=JSON.parse(t);e&&"object"==typeof e&&(o=e)}catch(t){this.logger.warn?.(`[local-session-history] failed to read sessions.json at ${i}: ${String(t)}`)}const r=s?[e,s]:[e];let n,c=s;for(const t of r){const e=o?.[t];if(!e||"object"!=typeof e)continue;const s=e,i="string"==typeof s.sessionFile?s.sessionFile.trim():"",r="string"==typeof s.sessionId?s.sessionId.trim():"";if(i&&(n=i),r&&!c&&(c=r),n)break}if(n){const e=path.isAbsolute(n)?n:path.join(t,n);return existsSync(e)?e:void 0}if(!c)return;const a=path.join(t,`${c}.jsonl`);return existsSync(a)?a:void 0}readTailLines(t){let e,s;try{e=statSync(t).size}catch(e){return this.logger.warn?.(`[local-session-history] failed to stat ${t}: ${String(e)}`),[]}if(e<=0)return[];try{s=openSync(t,"r");let i=e,o=1048576,r="";const n=[];for(;i>0&&o>0&&n.length<6400;){const t=Math.min(65536,i,o),e=i-t,c=Buffer.allocUnsafe(t),a=readSync(s,c,0,t,e);if(a<=0)break;i=e,o-=a;const l=(c.toString("utf-8",0,a)+r).split("\n");r=l.shift()??"";for(let t=l.length-1;t>=0;t--){const e=l[t]?.trim();if(e&&(n.push(e),n.length>=6400))break}}const c=r.trim();return c&&n.length<6400&&n.push(c),n}catch(e){return this.logger.warn?.(`[local-session-history] failed to tail ${t}: ${String(e)}`),[]}finally{if(void 0!==s)try{closeSync(s)}catch{}}}}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{RUNTIME_MESSAGE_TYPES}from"../runtime/runtime-message-channel-contract.js";export class McpCredentialRevokedError extends Error{credentialRef;constructor(e){super("MCP credential is revoked"),this.credentialRef=e}}export class McpDesiredStateController{store;projection;credentials;references;releases;receiptHistory;logger;tail=Promise.resolve();unsubscribe;started=!1;revoked=new Map;activeCredentialRevisions=new Map;projections=new Map;credentialServers=new Map;liveReleaseKeys=new Map;desiredRevision="0";authorityEpoch;authorityGeneration=0;constructor(e,t,i,r,s,n,a){this.store=e,this.projection=t,this.credentials=i,this.references=r,this.releases=s,this.receiptHistory=n,this.logger=a,this.authorityEpoch=i?.currentAuthority()?.runtimeEpoch??null}setRuntimeReceiptHistoryPort(e){this.receiptHistory=e}async start(e){await this.exclusive(async()=>{this.started||(this.started=!0,this.authorityEpoch=this.credentials?.currentAuthority()?.runtimeEpoch??null,this.unsubscribe=this.credentials?.subscribe(e=>this.acceptCredentialEvent(e)));const t=await this.store.load();await this.reconcileAll(e,t),await this.collectLocalGarbage(void 0,!0)})}async reconnect(e){await this.exclusive(async()=>{await this.reconcileAll(e)})}async list(){return await this.exclusive(async()=>{const e=await this.store.load();this.rememberDesired(e);const t=new Map(this.projection.list().map(e=>[e.name,e]));return{revision:e.revision,servers:e.servers.map(i=>{const r=t.get(i.name);return Object.freeze({...i,...void 0!==r?.connected?{connected:r.connected}:{},tools:r?.tools??Object.freeze([]),projection:this.projections.get(i.name)??pending(e.revision)})})}})}async set(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.set(e);return e.credentialRef&&this.references?.owns(e.credentialRef)&&await this.references.commit(e.credentialRef).catch(()=>{}),this.rememberDesired(r),this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,pending(r.revision)),await this.recordDesiredStateCommit(r,e.name,t),await this.collectLocalGarbage(i),r}))}async unset(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.unset(e);return this.rememberDesired(r),this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.projection.unset(e),this.projections.set(e,pending(r.revision)),await this.recordDesiredStateCommit(r,e,t),await this.collectLocalGarbage(i),r}))}async isCredentialReferenced(e){return await this.exclusive(async()=>(await this.store.load()).servers.some(t=>t.credentialRef===e))}async removeLocalCredentialIfUnreferenced(e){await this.exclusive(async()=>{this.references?.owns(e)&&await this.store.transaction(async t=>{(await t.load()).servers.some(t=>t.credentialRef===e)||(await t.assertOwned(),await this.references.remove(e))})})}async project(e,t,i,r){return await this.exclusive(async()=>{const s=await this.store.load();this.rememberDesired(s);const n=s.servers.find(t=>t.name===e);if(t?.aborted)return pending(s.revision);if(i&&!matchesProjectionAttempt(e,n,i))return blocked(s.revision);const a=i?await this.findAttemptActive(s,n,i):void 0;if((i||this.releases)&&!a){this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e);const t=pending(s.revision);return this.projections.set(e,t),t}if(i&&!r&&n?.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==i.runtimeEpoch)return this.projections.set(e,blocked(s.revision)),blocked(s.revision);if(!n){this.activeCredentialRevisions.delete(e),await this.projection.unset(e);const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,void 0,i,a)}const o=a?releaseKey(a):void 0;if(o&&this.liveReleaseKeys.get(e)===o&&this.projection.list().some(t=>t.name===e)){const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,n,i,a)}const c=await this.reconcileServer(n,s,t,a,i,r);return"synced"===c.state&&o&&this.liveReleaseKeys.set(e,o),withOperationReceipt(c,s,n,i,a)})}async stop(){await this.exclusive(async()=>{this.started=!1,this.unsubscribe?.(),this.unsubscribe=void 0,this.activeCredentialRevisions.clear(),this.revoked.clear(),this.projections.clear(),this.credentialServers.clear(),this.liveReleaseKeys.clear(),this.authorityEpoch=null,await this.projection.close()})}nudgeNativeProjection(){this.liveReleaseKeys.clear(),this.reconnect().catch(e=>this.logger?.warn?.(`[mcp-desired] native projection nudge failed: ${e instanceof Error?e.message:e}`))}acceptCredentialEvent(e){if("authority"===e.type){this.authorityEpoch=null;const t=++this.authorityGeneration;for(const e of this.credentialServers.keys())this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),this.projections.set(e,pending(this.desiredRevision));return void this.exclusive(()=>this.onAuthorityEvent(e.runtimeEpoch,t))}this.exclusive(()=>this.onCredentialEvent(e))}async onAuthorityEvent(e,t){const i=await this.store.load();if(this.rememberDesired(i),t!==this.authorityGeneration||null===e)return;const r=this.credentials?.currentAuthority();r&&r.runtimeEpoch===e&&(this.authorityEpoch=e,await this.reconcileAll(void 0,i))}async onCredentialEvent(e){const t=BigInt(e.credentialRevision),i=this.revoked.get(e.credentialRef)??-1n,r=await this.store.load();this.rememberDesired(r);const s=r.servers.filter(t=>t.credentialRef===e.credentialRef);if("revoked"!==e.type){if(!(t<=i)){this.revoked.delete(e.credentialRef);for(const e of s){if(t>(this.activeCredentialRevisions.get(e.name)??-1n)){if(this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.projection.unset(e.name),!this.releases){await this.reconcileServer(e,r);continue}this.projections.set(e.name,blocked(r.revision))}}}}else{if(t<i)return;this.revoked.set(e.credentialRef,t);for(const e of s)this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,blocked(r.revision)),await this.projection.unset(e.name)}}async reconcileAll(e,t){const i=t??await this.store.load();if(this.rememberDesired(i),!this.releases){const t=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())t.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers)await this.reconcileServer(t,i,e);return}const r=await this.releases.listMcpActiveDesiredStates(),s=await this.releases.listMcpLiveReleases(),n=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())n.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers){let n=r.find(e=>releaseMatchesDesired(e,i,t));if(!n){const e=s.find(e=>releaseMatchesDesired(e,i,t)),r=void 0===e?"pending":await this.recoverPendingServiceRelease(e,i,t);if("string"==typeof r){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),t.credentialRef||await this.projection.unset(t.name),this.projections.set(t.name,"pending"===r?pending(i.revision):blocked(i.revision));continue}n=r}if(t.credentialRef){const e=await this.projection.has(t.name),r=e?synced(i.revision):pending(i.revision);this.projections.set(t.name,r),e&&this.liveReleaseKeys.set(t.name,releaseKey(n));continue}const a=releaseKey(n);if(this.liveReleaseKeys.get(t.name)===a&&this.projection.list().some(e=>e.name===t.name)){this.projections.set(t.name,synced(i.revision));continue}"synced"===(await this.reconcileServer(t,i,e,n)).state&&this.liveReleaseKeys.set(t.name,releaseKey(n))}}async recoverPendingServiceRelease(e,t,i){if("service"!==e.source)return"blocked";if(!this.releases||!releaseMatchesDesired(e,t,i))return"blocked";let r;try{r=await this.releases.get(e.operationId)}catch{return"pending"}if(!r||!releaseMatchesJournalRecord(e,r))return"blocked";const s=await this.readReceiptHistory(e.operationId);if("unavailable"===s.status)return"pending";if("not_found"===s.status||"expired"===s.status)return"blocked";if(!serviceHistoryMatchesRelease(e,r,s.snapshot))return"blocked";try{const r=(await this.releases.promoteMcpDesiredStateActive({operationId:e.operationId,source:"service",effectRecordHash:e.effectRecordHash,terminalMessageId:s.snapshot.final.message_id})).mcpActiveDesiredState;return r&&releaseMatchesDesired(r,t,i)?r:"blocked"}catch{return"pending"}}async readReceiptHistory(e){const t=this.receiptHistory;if(!t)return{status:"unavailable"};const i=async()=>{try{return await t.readOperationHistory({operationId:e})}catch{return{status:"unavailable"}}},r=await i();return"unavailable"!==r.status?r:(await Promise.resolve(),await i())}async reconcileServer(e,t,i,r,s,n){try{await this.installServer(e,t,i,r,s,n);const a=synced(t.revision);return this.projections.set(e.name,a),a}catch(i){if(i instanceof McpCredentialRevokedError){this.revoked.set(e.credentialRef,this.revoked.get(e.credentialRef)??0n),this.activeCredentialRevisions.delete(e.name),await this.projection.unset(e.name);const i=blocked(t.revision);return this.projections.set(e.name,i),i}this.logger?.warn?.(`[mcp-desired] projection failed name=${e.name}: ${i instanceof Error?i.message:i}`);const r=pending(t.revision);return this.projections.set(e.name,r),r}}async installServer(e,t,i,r,s,n){if(i?.aborted)throw new Error("MCP projection attempt is no longer authoritative");if(n){if(!s||n.runtimeEpoch!==s.runtimeEpoch||r&&!releaseAuthorizesServer(r,e,t,n.runtimeEpoch,s))throw new Error("MCP operation-scoped activation is no longer authoritative");const a={generation:this.authorityGeneration,isCurrent:()=>!i?.aborted&&n.runtimeEpoch===s.runtimeEpoch},o=await this.projection.set(e.name,n.config,n.credential,a);if(!a.isCurrent())throw new Error("MCP operation-scoped activation lost authority during OpenClaw installation");return o}if(r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("MCP live release proof no longer authorizes this server");if(!e.credentialRef){if(this.activeCredentialRevisions.delete(e.name),!e.config)throw new Error("MCP desired state has no persisted public config for this server");const t=await this.projection.set(e.name,e.config);if(i?.aborted)throw new Error("MCP projection attempt lost authority during OpenClaw installation");return t}if(!this.credentials)throw new Error("runtime-scoped MCP credential resolver is unavailable");if(this.revoked.has(e.credentialRef))throw new McpCredentialRevokedError(e.credentialRef);const a=this.credentials.currentAuthority();if(!a||null===this.authorityEpoch||a.runtimeEpoch!==this.authorityEpoch)throw new Error("runtime-scoped MCP credential authority is pending");const o=this.authorityGeneration,c=this.authorityEpoch,d={generation:o,isCurrent:()=>o===this.authorityGeneration&&this.authorityEpoch===c&&this.credentials?.currentAuthority()?.runtimeEpoch===c};if(!d.isCurrent())throw new Error("runtime-scoped MCP credential authority changed");const h=await this.credentials.resolve({credentialRef:e.credentialRef,serverName:e.name,...i?{signal:i}:{}});if(i?.aborted||h.runtimeEpoch!==c||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during resolution");const l=BigInt(h.credentialRevision),u=this.revoked.get(e.credentialRef);if(void 0!==u&&l<=u)throw new McpCredentialRevokedError(e.credentialRef);if(!e.config)throw new Error("MCP desired state has no persisted public config for this server");const p=await this.projection.set(e.name,e.config,h.credential,d);if(i?.aborted||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during OpenClaw installation");return this.activeCredentialRevisions.set(e.name,l),p}rememberDesired(e){this.desiredRevision=e.revision,this.credentialServers.clear();for(const t of e.servers)t.credentialRef&&this.credentialServers.set(t.name,t.credentialRef)}async recordDesiredStateCommit(e,t,i){if(!i)return;if(!this.releases)throw new Error("MCP desired state requires the shared command journal release authority");const r=e.servers.find(e=>e.name===t);await this.releases.recordMcpDesiredStateCommit({schemaVersion:1,...i,desiredStateRevision:e.revision,desiredStateHash:e.sha256,server:r?{name:r.name,definitionRef:r.definitionRef,version:r.version,definitionSha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{}}:{name:t,removed:!0}})}async findAttemptActive(e,t,i){const r=(await(this.releases?.listMcpActiveDesiredStates())??[]).find(e=>e.operationId===i.operationId&&e.method===i.method&&e.source===i.source);if(r&&releaseMatchesAttempt(r,e,t,i))return r}async collectLocalGarbage(e,t=!1){if(!this.references)return;if(!e)return void await this.store.transaction(async e=>this.collectLocalGarbage(e,t));const i=await this.references.listOwned(),r=t?await(this.references.listPendingOwned?.())??[]:[];for(const t of[...new Set([...i,...r])]){const i=(await e.load()).servers.some(e=>e.credentialRef===t);await e.assertOwned(),i?r.includes(t)&&await this.references.commit(t).catch(()=>{}):await this.references.remove(t).catch(()=>{})}}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function pending(e){return Object.freeze({revision:e,state:"pending"})}function synced(e){return Object.freeze({revision:e,state:"synced"})}function blocked(e){return Object.freeze({revision:e,state:"blocked"})}function matchesProjectionAttempt(e,t,i){return i.expected.name===e&&(i.expected.absent?void 0===t:void 0!==t&&t.name===i.expected.name&&t.definitionRef===i.expected.definitionRef&&t.version===i.expected.version&&t.sha256===i.expected.sha256&&t.credentialRef===i.expected.credentialRef)}function withOperationReceipt(e,t,i,r,s){if(!r||!s||"synced"!==e.state)return e;const n=Object.freeze({schemaVersion:1,operationId:r.operationId,method:r.method,runtimeEpoch:r.runtimeEpoch,desiredStateRevision:t.revision,desiredStateHash:t.sha256,server:Object.freeze(i?{name:i.name,definitionRef:i.definitionRef,definitionSha256:i.sha256}:{name:r.expected.name,removed:!0})});return Object.freeze({...e,mcpReceipt:n})}function releaseMatchesDesired(e,t,i){return"mcp.set"===e.method&&e.desiredStateRevision===t.revision&&e.desiredStateHash===t.sha256&&e.server.name===i.name&&e.server.definitionRef===i.definitionRef&&e.server.version===i.version&&e.server.definitionSha256===i.sha256&&e.server.credentialRef===i.credentialRef}function releaseMatchesJournalRecord(e,t){const i=t.mcpDesiredStateCommit,r=t.mcpLiveRelease;return t.operationId===e.operationId&&t.command===e.method&&t.paramsHash===e.paramsHash&&t.runtimeEpoch===e.runtimeEpoch&&("effect_committed"===t.phase||"projection_committed"===t.phase)&&void 0!==t.messageBinding&&void 0!==i&&void 0!==r&&"service"===i.source&&sameMcpRelease(r,e)&&r.effectRecordHash===e.effectRecordHash&&sameMcpRelease(i,e)}function serviceHistoryMatchesRelease(e,t,i){const r=t.messageBinding;if(!r||!e.effectRecordHash)return!1;const s=i.request,n=s.payload,a=i.effectCommitted,o=a?.payload;if(i.operationId!==e.operationId||i.requestMessageId!==r.requestMessageId||i.requestHash!==r.requestHash||s.type!==RUNTIME_MESSAGE_TYPES.request||s.channel_id!==e.operationId||s.message_id!==r.requestMessageId||n.operationId!==e.operationId||n.requestMessageId!==r.requestMessageId||n.requestHash!==r.requestHash||n.paramsHash!==e.paramsHash||("mcp.set"===e.method?"mcp/set"!==n.method:"mcp/unset"!==n.method)||n.command.name!==e.method||n.dispatchEpoch!==e.runtimeEpoch||!a||!o||a.type!==RUNTIME_MESSAGE_TYPES.effectCommitted||a.channel_id!==e.operationId||a.in_reply_to!==r.requestMessageId||a.publisher_id!==n.instanceId||o.operationId!==e.operationId||o.requestMessageId!==r.requestMessageId||o.requestHash!==r.requestHash||o.dispatchEpoch!==e.runtimeEpoch||o.method!==n.method||o.effectRecordHash!==e.effectRecordHash||o.effectRecord.operationId!==e.operationId||o.effectRecord.method!==n.method)return!1;if(i.cancelRequested)return!1;if(!i.terminal)return!1;const c=i.final;return"final_message"===i.terminalSource&&"succeeded"===i.status&&void 0!==c&&c.type===RUNTIME_MESSAGE_TYPES.final&&c.channel_id===e.operationId&&c.in_reply_to===r.requestMessageId&&c.publisher_id===n.instanceId&&c.payload.operationId===e.operationId&&c.payload.requestMessageId===r.requestMessageId&&c.payload.requestHash===r.requestHash&&c.payload.dispatchEpoch===e.runtimeEpoch&&c.payload.method===n.method&&"succeeded"===c.payload.outcome&&c.payload.effectRecordHash===e.effectRecordHash}function sameMcpRelease(e,t){return e.source===t.source&&e.operationId===t.operationId&&e.method===t.method&&e.paramsHash===t.paramsHash&&e.runtimeEpoch===t.runtimeEpoch&&e.desiredStateRevision===t.desiredStateRevision&&e.desiredStateHash===t.desiredStateHash&&e.server.name===t.server.name&&e.server.definitionRef===t.server.definitionRef&&e.server.version===t.server.version&&e.server.definitionSha256===t.server.definitionSha256&&e.server.credentialRef===t.server.credentialRef&&e.server.removed===t.server.removed}function releaseMatchesAttempt(e,t,i,r){return!(e.operationId!==r.operationId||e.method!==r.method||e.source!==r.source||e.paramsHash!==r.paramsHash||e.runtimeEpoch!==r.runtimeEpoch||e.desiredStateRevision!==t.revision||e.desiredStateHash!==t.sha256||"service"===r.source&&!e.effectRecordHash||"acp_local"===r.source&&void 0!==e.effectRecordHash)&&(r.expected.absent?void 0===i&&e.server.name===r.expected.name&&!0===e.server.removed:void 0!==i&&releaseMatchesDesired(e,t,i)&&i.definitionRef===r.expected.definitionRef&&i.version===r.expected.version&&i.sha256===r.expected.sha256&&i.credentialRef===r.expected.credentialRef)}function releaseAuthorizesServer(e,t,i,r,s){return"mcp.set"===e.method&&(!t.credentialRef||e.runtimeEpoch===r)&&releaseMatchesDesired(e,i,t)&&(!s||releaseMatchesAttempt(e,i,t,s))}function releaseKey(e){return[e.source,e.operationId,e.paramsHash,e.runtimeEpoch,e.desiredStateRevision,e.desiredStateHash,e.effectRecordHash??"local"].join("\0")}
|
|
1
|
+
import{isExpiredMcpServer}from"./mcp-desired-state-store.js";import{RUNTIME_MESSAGE_TYPES}from"../runtime/runtime-message-channel-contract.js";const MAX_EXPIRY_DELAY_MS=2147483647;export class McpCredentialRevokedError extends Error{credentialRef;constructor(e){super("MCP credential is revoked"),this.credentialRef=e}}export class McpDesiredStateController{store;projection;credentials;references;releases;receiptHistory;logger;tail=Promise.resolve();unsubscribe;started=!1;revoked=new Map;activeCredentialRevisions=new Map;projections=new Map;credentialServers=new Map;liveReleaseKeys=new Map;expiryTimers=new Map;desiredRevision="0";authorityEpoch;authorityGeneration=0;constructor(e,t,i,r,s,n,a){this.store=e,this.projection=t,this.credentials=i,this.references=r,this.releases=s,this.receiptHistory=n,this.logger=a,this.authorityEpoch=i?.currentAuthority()?.runtimeEpoch??null}setRuntimeReceiptHistoryPort(e){this.receiptHistory=e}async start(e){await this.exclusive(async()=>{this.started||(this.started=!0,this.authorityEpoch=this.credentials?.currentAuthority()?.runtimeEpoch??null,this.unsubscribe=this.credentials?.subscribe(e=>this.acceptCredentialEvent(e)));const t=await this.store.load();await this.reconcileAll(e,t),await this.collectLocalGarbage(void 0,!0)})}async reconnect(e){await this.exclusive(async()=>{await this.reconcileAll(e)})}async list(){return await this.exclusive(async()=>{const e=await this.store.load();this.rememberDesired(e);const t=new Map(this.projection.list().map(e=>[e.name,e]));return{revision:e.revision,servers:e.servers.map(i=>{const r=t.get(i.name);return Object.freeze({...i,...void 0!==r?.connected?{connected:r.connected}:{},tools:r?.tools??Object.freeze([]),projection:this.projections.get(i.name)??pending(e.revision)})})}})}async set(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.set(e);return e.credentialRef&&this.references?.owns(e.credentialRef)&&await this.references.commit(e.credentialRef).catch(()=>{}),this.rememberDesired(r),this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,pending(r.revision)),await this.recordDesiredStateCommit(r,e.name,t),await this.collectLocalGarbage(i),r}))}async unset(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.unset(e);return this.rememberDesired(r),this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.projection.unset(e),this.projections.set(e,pending(r.revision)),await this.recordDesiredStateCommit(r,e,t),await this.collectLocalGarbage(i),r}))}async isCredentialReferenced(e){return await this.exclusive(async()=>(await this.store.load()).servers.some(t=>t.credentialRef===e))}async removeLocalCredentialIfUnreferenced(e){await this.exclusive(async()=>{this.references?.owns(e)&&await this.store.transaction(async t=>{(await t.load()).servers.some(t=>t.credentialRef===e)||(await t.assertOwned(),await this.references.remove(e))})})}async project(e,t,i,r){return await this.exclusive(async()=>{const s=await this.store.load();this.rememberDesired(s);const n=s.servers.find(t=>t.name===e);if(t?.aborted)return pending(s.revision);if(i&&!matchesProjectionAttempt(e,n,i))return blocked(s.revision);const a=i?await this.findAttemptActive(s,n,i):void 0;if((i||this.releases)&&!a){this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e);const t=pending(s.revision);return this.projections.set(e,t),t}if(i&&!r&&n?.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==i.runtimeEpoch)return this.projections.set(e,blocked(s.revision)),blocked(s.revision);if(!n){this.activeCredentialRevisions.delete(e),await this.projection.unset(e);const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,void 0,i,a)}const o=a?releaseKey(a):void 0;if(o&&this.liveReleaseKeys.get(e)===o&&this.projection.list().some(t=>t.name===e)){const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,n,i,a)}const c=await this.reconcileServer(n,s,t,a,i,r);return"synced"===c.state&&o&&this.liveReleaseKeys.set(e,o),withOperationReceipt(c,s,n,i,a)})}async stop(){await this.exclusive(async()=>{this.started=!1,this.unsubscribe?.(),this.unsubscribe=void 0,this.activeCredentialRevisions.clear(),this.revoked.clear(),this.projections.clear(),this.credentialServers.clear(),this.liveReleaseKeys.clear();for(const e of this.expiryTimers.values())clearTimeout(e.timer);this.expiryTimers.clear(),this.authorityEpoch=null,await this.projection.close()})}nudgeNativeProjection(){this.liveReleaseKeys.clear(),this.reconnect().catch(e=>this.logger?.warn?.(`[mcp-desired] native projection nudge failed: ${e instanceof Error?e.message:e}`))}acceptCredentialEvent(e){if("authority"===e.type){this.authorityEpoch=null;const t=++this.authorityGeneration;for(const e of this.credentialServers.keys())this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),this.projections.set(e,pending(this.desiredRevision));return void this.exclusive(()=>this.onAuthorityEvent(e.runtimeEpoch,t))}this.exclusive(()=>this.onCredentialEvent(e))}async onAuthorityEvent(e,t){const i=await this.store.load();if(this.rememberDesired(i),t!==this.authorityGeneration||null===e)return;const r=this.credentials?.currentAuthority();r&&r.runtimeEpoch===e&&(this.authorityEpoch=e,await this.reconcileAll(void 0,i))}async onCredentialEvent(e){const t=BigInt(e.credentialRevision),i=this.revoked.get(e.credentialRef)??-1n,r=await this.store.load();this.rememberDesired(r);const s=r.servers.filter(t=>t.credentialRef===e.credentialRef);if("revoked"!==e.type){if(!(t<=i)){this.revoked.delete(e.credentialRef);for(const e of s){if(t>(this.activeCredentialRevisions.get(e.name)??-1n)){if(this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.projection.unset(e.name),!this.releases){await this.reconcileServer(e,r);continue}this.projections.set(e.name,blocked(r.revision))}}}}else{if(t<i)return;this.revoked.set(e.credentialRef,t);for(const e of s)this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,blocked(r.revision)),await this.projection.unset(e.name)}}async reconcileAll(e,t){const i=t??await this.store.load();if(this.rememberDesired(i),!this.releases){const t=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())t.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers)await this.reconcileServer(t,i,e);return}const r=await this.releases.listMcpActiveDesiredStates(),s=await this.releases.listMcpLiveReleases(),n=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())n.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers){let n=r.find(e=>releaseMatchesDesired(e,i,t));if(!n){const e=s.find(e=>releaseMatchesDesired(e,i,t)),r=void 0===e?"pending":await this.recoverPendingServiceRelease(e,i,t);if("string"==typeof r){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),t.credentialRef||await this.projection.unset(t.name),this.projections.set(t.name,"pending"===r?pending(i.revision):blocked(i.revision));continue}n=r}if(t.credentialRef){const e=await this.projection.has(t.name),r=e?synced(i.revision):pending(i.revision);this.projections.set(t.name,r),e&&this.liveReleaseKeys.set(t.name,releaseKey(n));continue}const a=releaseKey(n);if(this.liveReleaseKeys.get(t.name)===a&&this.projection.list().some(e=>e.name===t.name)){this.projections.set(t.name,synced(i.revision));continue}"synced"===(await this.reconcileServer(t,i,e,n)).state&&this.liveReleaseKeys.set(t.name,releaseKey(n))}}async recoverPendingServiceRelease(e,t,i){if("service"!==e.source)return"blocked";if(!this.releases||!releaseMatchesDesired(e,t,i))return"blocked";let r;try{r=await this.releases.get(e.operationId)}catch{return"pending"}if(!r||!releaseMatchesJournalRecord(e,r))return"blocked";const s=await this.readReceiptHistory(e.operationId);if("unavailable"===s.status)return"pending";if("not_found"===s.status||"expired"===s.status)return"blocked";if(!serviceHistoryMatchesRelease(e,r,s.snapshot))return"blocked";try{const r=(await this.releases.promoteMcpDesiredStateActive({operationId:e.operationId,source:"service",effectRecordHash:e.effectRecordHash,terminalMessageId:s.snapshot.final.message_id})).mcpActiveDesiredState;return r&&releaseMatchesDesired(r,t,i)?r:"blocked"}catch{return"pending"}}async readReceiptHistory(e){const t=this.receiptHistory;if(!t)return{status:"unavailable"};const i=async()=>{try{return await t.readOperationHistory({operationId:e})}catch{return{status:"unavailable"}}},r=await i();return"unavailable"!==r.status?r:(await Promise.resolve(),await i())}async reconcileServer(e,t,i,r,s,n){if(isExpiredMcpServer(e)){await this.projection.unset(e.name),this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.dropExpired(e.name);const i=synced(t.revision);return this.projections.set(e.name,i),i}try{await this.installServer(e,t,i,r,s,n);const a=synced(t.revision);return this.projections.set(e.name,a),a}catch(i){if(i instanceof McpCredentialRevokedError){this.revoked.set(e.credentialRef,this.revoked.get(e.credentialRef)??0n),this.activeCredentialRevisions.delete(e.name),await this.projection.unset(e.name);const i=blocked(t.revision);return this.projections.set(e.name,i),i}this.logger?.warn?.(`[mcp-desired] projection failed name=${e.name}: ${i instanceof Error?i.message:i}`);const r=pending(t.revision);return this.projections.set(e.name,r),r}}async installServer(e,t,i,r,s,n){if(i?.aborted)throw new Error("MCP projection attempt is no longer authoritative");if(n){if(!s||n.runtimeEpoch!==s.runtimeEpoch||r&&!releaseAuthorizesServer(r,e,t,n.runtimeEpoch,s))throw new Error("MCP operation-scoped activation is no longer authoritative");const a={generation:this.authorityGeneration,isCurrent:()=>!i?.aborted&&n.runtimeEpoch===s.runtimeEpoch},o=await this.projection.set(e.name,n.config,n.credential,a);if(!a.isCurrent())throw new Error("MCP operation-scoped activation lost authority during OpenClaw installation");return o}if(r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("MCP live release proof no longer authorizes this server");if(!e.credentialRef){if(this.activeCredentialRevisions.delete(e.name),!e.config)throw new Error("MCP desired state has no persisted public config for this server");const t=await this.projection.set(e.name,e.config);if(i?.aborted)throw new Error("MCP projection attempt lost authority during OpenClaw installation");return t}if(!this.credentials)throw new Error("runtime-scoped MCP credential resolver is unavailable");if(this.revoked.has(e.credentialRef))throw new McpCredentialRevokedError(e.credentialRef);const a=this.credentials.currentAuthority();if(!a||null===this.authorityEpoch||a.runtimeEpoch!==this.authorityEpoch)throw new Error("runtime-scoped MCP credential authority is pending");const o=this.authorityGeneration,c=this.authorityEpoch,d={generation:o,isCurrent:()=>o===this.authorityGeneration&&this.authorityEpoch===c&&this.credentials?.currentAuthority()?.runtimeEpoch===c};if(!d.isCurrent())throw new Error("runtime-scoped MCP credential authority changed");const h=await this.credentials.resolve({credentialRef:e.credentialRef,serverName:e.name,...i?{signal:i}:{}});if(i?.aborted||h.runtimeEpoch!==c||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during resolution");const l=BigInt(h.credentialRevision),p=this.revoked.get(e.credentialRef);if(void 0!==p&&l<=p)throw new McpCredentialRevokedError(e.credentialRef);if(!e.config)throw new Error("MCP desired state has no persisted public config for this server");const u=await this.projection.set(e.name,e.config,h.credential,d);if(i?.aborted||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during OpenClaw installation");return this.activeCredentialRevisions.set(e.name,l),u}rememberDesired(e){this.desiredRevision=e.revision,this.credentialServers.clear();for(const t of e.servers)t.credentialRef&&this.credentialServers.set(t.name,t.credentialRef);this.scheduleExpiries(e)}scheduleExpiries(e){const t=new Map(e.servers.filter(e=>e.expiresAt).map(e=>[e.name,e.expiresAt]));for(const[e,i]of[...this.expiryTimers])t.get(e)!==i.expiresAt&&(clearTimeout(i.timer),this.expiryTimers.delete(e));for(const[e,i]of t){if(this.expiryTimers.has(e))continue;const t=Math.min(Math.max(Date.parse(i)-Date.now(),0),2147483647),r=setTimeout(()=>{this.dropExpired(e)},t);r.unref?.(),this.expiryTimers.set(e,{expiresAt:i,timer:r})}}async dropExpired(e){this.expiryTimers.delete(e);try{await this.exclusive(async()=>{await this.store.transaction(async t=>{const i=await t.load(),r=i.servers.find(t=>t.name===e);if(!r||!isExpiredMcpServer(r))return void this.rememberDesired(i);const s=await t.unset(e);this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.projection.unset(e),this.projections.set(e,synced(s.revision)),this.rememberDesired(s),await this.collectLocalGarbage(t),this.logger?.info?.(`[mcp-desired] credential expired, server dropped name=${e}`)})})}catch(t){this.logger?.error?.(`[mcp-desired] expiry drop failed name=${e}: ${t instanceof Error?t.message:t}`)}}async recordDesiredStateCommit(e,t,i){if(!i)return;if(!this.releases)throw new Error("MCP desired state requires the shared command journal release authority");const r=e.servers.find(e=>e.name===t);await this.releases.recordMcpDesiredStateCommit({schemaVersion:1,...i,desiredStateRevision:e.revision,desiredStateHash:e.sha256,server:r?{name:r.name,definitionRef:r.definitionRef,version:r.version,definitionSha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{}}:{name:t,removed:!0}})}async findAttemptActive(e,t,i){const r=(await(this.releases?.listMcpActiveDesiredStates())??[]).find(e=>e.operationId===i.operationId&&e.method===i.method&&e.source===i.source);if(r&&releaseMatchesAttempt(r,e,t,i))return r}async collectLocalGarbage(e,t=!1){if(!this.references)return;if(!e)return void await this.store.transaction(async e=>this.collectLocalGarbage(e,t));const i=await this.references.listOwned(),r=t?await(this.references.listPendingOwned?.())??[]:[];for(const t of[...new Set([...i,...r])]){const i=(await e.load()).servers.some(e=>e.credentialRef===t);await e.assertOwned(),i?r.includes(t)&&await this.references.commit(t).catch(()=>{}):await this.references.remove(t).catch(()=>{})}}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function pending(e){return Object.freeze({revision:e,state:"pending"})}function synced(e){return Object.freeze({revision:e,state:"synced"})}function blocked(e){return Object.freeze({revision:e,state:"blocked"})}function matchesProjectionAttempt(e,t,i){return i.expected.name===e&&(i.expected.absent?void 0===t:void 0!==t&&t.name===i.expected.name&&t.definitionRef===i.expected.definitionRef&&t.version===i.expected.version&&t.sha256===i.expected.sha256&&t.credentialRef===i.expected.credentialRef)}function withOperationReceipt(e,t,i,r,s){if(!r||!s||"synced"!==e.state)return e;const n=Object.freeze({schemaVersion:1,operationId:r.operationId,method:r.method,runtimeEpoch:r.runtimeEpoch,desiredStateRevision:t.revision,desiredStateHash:t.sha256,server:Object.freeze(i?{name:i.name,definitionRef:i.definitionRef,definitionSha256:i.sha256}:{name:r.expected.name,removed:!0})});return Object.freeze({...e,mcpReceipt:n})}function releaseMatchesDesired(e,t,i){return"mcp.set"===e.method&&e.desiredStateRevision===t.revision&&e.desiredStateHash===t.sha256&&e.server.name===i.name&&e.server.definitionRef===i.definitionRef&&e.server.version===i.version&&e.server.definitionSha256===i.sha256&&e.server.credentialRef===i.credentialRef}function releaseMatchesJournalRecord(e,t){const i=t.mcpDesiredStateCommit,r=t.mcpLiveRelease;return t.operationId===e.operationId&&t.command===e.method&&t.paramsHash===e.paramsHash&&t.runtimeEpoch===e.runtimeEpoch&&("effect_committed"===t.phase||"projection_committed"===t.phase)&&void 0!==t.messageBinding&&void 0!==i&&void 0!==r&&"service"===i.source&&sameMcpRelease(r,e)&&r.effectRecordHash===e.effectRecordHash&&sameMcpRelease(i,e)}function serviceHistoryMatchesRelease(e,t,i){const r=t.messageBinding;if(!r||!e.effectRecordHash)return!1;const s=i.request,n=s.payload,a=i.effectCommitted,o=a?.payload;if(i.operationId!==e.operationId||i.requestMessageId!==r.requestMessageId||i.requestHash!==r.requestHash||s.type!==RUNTIME_MESSAGE_TYPES.request||s.channel_id!==e.operationId||s.message_id!==r.requestMessageId||n.operationId!==e.operationId||n.requestMessageId!==r.requestMessageId||n.requestHash!==r.requestHash||n.paramsHash!==e.paramsHash||("mcp.set"===e.method?"mcp/set"!==n.method:"mcp/unset"!==n.method)||n.command.name!==e.method||n.dispatchEpoch!==e.runtimeEpoch||!a||!o||a.type!==RUNTIME_MESSAGE_TYPES.effectCommitted||a.channel_id!==e.operationId||a.in_reply_to!==r.requestMessageId||a.publisher_id!==n.instanceId||o.operationId!==e.operationId||o.requestMessageId!==r.requestMessageId||o.requestHash!==r.requestHash||o.dispatchEpoch!==e.runtimeEpoch||o.method!==n.method||o.effectRecordHash!==e.effectRecordHash||o.effectRecord.operationId!==e.operationId||o.effectRecord.method!==n.method)return!1;if(i.cancelRequested)return!1;if(!i.terminal)return!1;const c=i.final;return"final_message"===i.terminalSource&&"succeeded"===i.status&&void 0!==c&&c.type===RUNTIME_MESSAGE_TYPES.final&&c.channel_id===e.operationId&&c.in_reply_to===r.requestMessageId&&c.publisher_id===n.instanceId&&c.payload.operationId===e.operationId&&c.payload.requestMessageId===r.requestMessageId&&c.payload.requestHash===r.requestHash&&c.payload.dispatchEpoch===e.runtimeEpoch&&c.payload.method===n.method&&"succeeded"===c.payload.outcome&&c.payload.effectRecordHash===e.effectRecordHash}function sameMcpRelease(e,t){return e.source===t.source&&e.operationId===t.operationId&&e.method===t.method&&e.paramsHash===t.paramsHash&&e.runtimeEpoch===t.runtimeEpoch&&e.desiredStateRevision===t.desiredStateRevision&&e.desiredStateHash===t.desiredStateHash&&e.server.name===t.server.name&&e.server.definitionRef===t.server.definitionRef&&e.server.version===t.server.version&&e.server.definitionSha256===t.server.definitionSha256&&e.server.credentialRef===t.server.credentialRef&&e.server.removed===t.server.removed}function releaseMatchesAttempt(e,t,i,r){return!(e.operationId!==r.operationId||e.method!==r.method||e.source!==r.source||e.paramsHash!==r.paramsHash||e.runtimeEpoch!==r.runtimeEpoch||e.desiredStateRevision!==t.revision||e.desiredStateHash!==t.sha256||"service"===r.source&&!e.effectRecordHash||"acp_local"===r.source&&void 0!==e.effectRecordHash)&&(r.expected.absent?void 0===i&&e.server.name===r.expected.name&&!0===e.server.removed:void 0!==i&&releaseMatchesDesired(e,t,i)&&i.definitionRef===r.expected.definitionRef&&i.version===r.expected.version&&i.sha256===r.expected.sha256&&i.credentialRef===r.expected.credentialRef)}function releaseAuthorizesServer(e,t,i,r,s){return"mcp.set"===e.method&&(!t.credentialRef||e.runtimeEpoch===r)&&releaseMatchesDesired(e,i,t)&&(!s||releaseMatchesAttempt(e,i,t,s))}function releaseKey(e){return[e.source,e.operationId,e.paramsHash,e.runtimeEpoch,e.desiredStateRevision,e.desiredStateHash,e.effectRecordHash??"local"].join("\0")}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash,randomUUID}from"node:crypto";import{chmod,mkdir,open,readFile,rename,unlink}from"node:fs/promises";import{dirname}from"node:path";import{assertPublicMcpValues,requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";import{withOwnedDirectoryLock}from"../runtime/owned-directory-lock.js";import{MonotonicJournalSegments}from"../runtime/monotonic-journal-segments.js";const EMPTY_PAYLOAD={schemaVersion:1,revision:"0",servers:[]};export class McpDesiredStateStore{path;options;walPath;lockPath;tail=Promise.resolve();segments;constructor(e,t={}){this.path=e,this.options=t,this.walPath=`${e}.wal`,this.lockPath=`${e}.lock`,this.segments=new MonotonicJournalSegments(e,{beforeWatermark:t.beforeAuthorityWatermark})}async load(){return await this.transaction(e=>e.load())}async set(e){return await this.transaction(t=>t.set(e))}async unset(e){return await this.transaction(t=>t.unset(e))}async transaction(e){return await this.exclusive(()=>withOwnedDirectoryLock(this.lockPath,async t=>{const i=await this.segments.authorityFloor();let r=await this.loadAndRecover();const n=async()=>(await t.assertOwned(),r=await this.loadAndRecover(),r),a={load:n,set:async e=>{const a=normalizeServer(e);r=await n();const s=new Map(r.servers.map(e=>[e.name,e]));return s.set(a.name,a),r=await this.commit(r,[...s.values()].sort((e,t)=>e.name.localeCompare(t.name)),t,i),r},unset:async e=>(requireName(e),r=await n(),r.servers.some(t=>t.name===e)?(r=await this.commit(r,r.servers.filter(t=>t.name!==e),t,i),r):r),assertOwned:()=>t.assertOwned()};return await e(a)},{...this.options.lockOptions,epochFloor:()=>this.segments.authorityFloor()}))}async loadAndRecover(){await mkdir(dirname(this.path),{recursive:!0,mode:448});const[e,t]=await Promise.all([readState(this.path),readState(this.walPath)]);if("corrupt"===t.kind)throw new Error("MCP desired-state WAL is corrupt");if("corrupt"===e.kind&&"valid"!==t.kind)throw new Error("MCP desired state is corrupt");let i;if("valid"===t.kind)if("valid"===e.kind&&BigInt(e.state.revision)>BigInt(t.state.revision))await unlinkIfPresent(this.walPath),await syncDirectory(dirname(this.path)),i=e.state;else{if("valid"===e.kind&&BigInt(t.state.revision)>BigInt(e.state.revision)+1n)throw new Error("MCP desired-state WAL revision is discontinuous");"valid"===e.kind&&e.state.sha256===t.state.sha256||(await atomicWrite(this.path,encode(t.state),this.temp("recover")),await syncDirectory(dirname(this.path))),await unlinkIfPresent(this.walPath),await syncDirectory(dirname(this.path)),i=t.state}else i="valid"===e.kind?e.state:state(EMPTY_PAYLOAD);const r=new Map([["snapshot",{value:i,logicalRevision:i.revision,publisherEpoch:"0"}]]);return(await this.segments.reduce(r)).get("snapshot")?.value??i}async commit(e,t,i,r){const n=state({schemaVersion:1,revision:`${BigInt(e.revision)+1n}`,servers:t}),a=encode(n);return await i.assertOwned(),await this.segments.publish(i.epoch,[{key:"snapshot",logicalRevision:n.revision,value:n}],r),await atomicWrite(this.walPath,a,this.temp("wal")),await syncDirectory(dirname(this.path)),await(this.options.fault?.("wal-synced")),await atomicWrite(this.path,a,this.temp("state")),await syncDirectory(dirname(this.path)),await(this.options.fault?.("state-renamed")),await unlinkIfPresent(this.walPath),await syncDirectory(dirname(this.path)),await(this.options.fault?.("wal-cleared")),await this.segments.prune(),n}temp(e){return`${this.path}.${e}.${process.pid}.${randomUUID()}.tmp`}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function state(e){const t={schemaVersion:1,revision:e.revision,servers:Object.freeze(e.servers.map(normalizeServer).sort((e,t)=>e.name.localeCompare(t.name)))};return Object.freeze({...t,sha256:digest(t)})}function normalizeServer(e){requireName(e.name);for(const[t,i]of[["definitionRef",e.definitionRef],["version",e.version],["sha256",e.sha256]])if("string"!=typeof i||!i||i.length>512)throw new Error(`Invalid MCP ${t}`);if(!/^[a-f0-9]{64}$/i.test(e.sha256))throw new Error("Invalid MCP sha256");if(void 0!==e.credentialRef&&(!e.credentialRef||e.credentialRef.length>512))throw new Error("Invalid MCP credentialRef");assertPublicMcpValues([e.definitionRef,e.version,e.credentialRef],"MCP durable reference");const t=void 0===e.config?void 0:normalizeMcpPublicServerConfig(e.config);return Object.freeze({name:e.name,definitionRef:e.definitionRef,version:e.version,sha256:e.sha256.toLowerCase(),...e.credentialRef?{credentialRef:e.credentialRef}:{},...t?{config:t}:{}})}export function normalizeMcpPublicServerConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("Invalid MCP public config");const t=new Set(["transport","url","connectionTimeoutMs","command","args","cwd"]);if(Object.keys(e).some(e=>!t.has(e)))throw new Error("MCP desired state accepts public config only");if(!["sse","streamable-http","stdio"].includes(e.transport))throw new Error("Invalid MCP transport");if(void 0!==e.connectionTimeoutMs&&(!Number.isSafeInteger(e.connectionTimeoutMs)||e.connectionTimeoutMs<=0||e.connectionTimeoutMs>12e4))throw new Error("Invalid MCP connection timeout");if("stdio"===e.transport){if("string"!=typeof e.command||!e.command||void 0!==e.url)throw new Error("Invalid MCP stdio config");if(void 0!==e.args&&(!Array.isArray(e.args)||e.args.some(e=>"string"!=typeof e)))throw new Error("Invalid MCP stdio args");return assertPublicMcpValues([e.command,...e.args??[],e.cwd],"MCP stdio public definition contains secret-like argv"),Object.freeze({transport:e.transport,command:e.command,...e.args?{args:Object.freeze([...e.args])}:{},...e.cwd?{cwd:e.cwd}:{},...e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}})}if("string"!=typeof e.url||void 0!==e.command||void 0!==e.args||void 0!==e.cwd)throw new Error("Invalid MCP HTTP config");let i;try{i=new URL(e.url)}catch{throw new Error("Invalid MCP HTTP config")}if("https:"!==i.protocol||i.username||i.password||i.search||i.hash)throw new Error("MCP HTTP definition requires a credential-free HTTPS URL");let r=i.pathname;try{r=decodeURIComponent(r)}catch{}return assertPublicMcpValues([i.toString(),r],"MCP HTTP public definition"),Object.freeze({transport:e.transport,url:i.toString(),...e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}})}function digest(e){return createHash("sha256").update(canonical(e)).digest("hex")}function canonical(e){return null===e||"object"!=typeof e?JSON.stringify(e):Array.isArray(e)?`[${e.map(canonical).join(",")}]`:`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${canonical(t)}`).join(",")}}`}function encode(e){return`${canonical(e)}\n`}async function readState(e){let t;try{t=await readFile(e,"utf8")}catch(e){return"ENOENT"===e.code?{kind:"missing"}:{kind:"corrupt"}}try{const e=JSON.parse(t);if("revision,schemaVersion,servers,sha256"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||"string"!=typeof e.revision||!/^(0|[1-9][0-9]*)$/.test(e.revision)||!Array.isArray(e.servers)||"string"!=typeof e.sha256)return{kind:"corrupt"};const i=state({schemaVersion:1,revision:e.revision,servers:e.servers});return i.sha256===e.sha256?{kind:"valid",state:i}:{kind:"corrupt"}}catch{return{kind:"corrupt"}}}async function atomicWrite(e,t,i){const r=await open(i,"wx",384);try{await r.writeFile(t,"utf8"),await r.sync()}finally{await r.close()}await chmod(i,384),await rename(i,e),await chmod(e,384)}async function syncDirectory(e){const t=await open(e,"r");try{await t.sync()}finally{await t.close()}}async function unlinkIfPresent(e){try{await unlink(e)}catch(e){if("ENOENT"!==e.code)throw e}}const requireName=e=>{requireSecretFreeMcpServerName(e)};
|
|
1
|
+
import{createHash,randomUUID}from"node:crypto";import{chmod,mkdir,open,readFile,rename,unlink}from"node:fs/promises";import{dirname}from"node:path";import{assertPublicMcpValues,requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";import{withOwnedDirectoryLock}from"../runtime/owned-directory-lock.js";import{MonotonicJournalSegments}from"../runtime/monotonic-journal-segments.js";const EMPTY_PAYLOAD={schemaVersion:1,revision:"0",servers:[]};export class McpDesiredStateStore{path;options;walPath;lockPath;tail=Promise.resolve();segments;constructor(e,t={}){this.path=e,this.options=t,this.walPath=`${e}.wal`,this.lockPath=`${e}.lock`,this.segments=new MonotonicJournalSegments(e,{beforeWatermark:t.beforeAuthorityWatermark})}async load(){return await this.transaction(e=>e.load())}async set(e){return await this.transaction(t=>t.set(e))}async unset(e){return await this.transaction(t=>t.unset(e))}async transaction(e){return await this.exclusive(()=>withOwnedDirectoryLock(this.lockPath,async t=>{const i=await this.segments.authorityFloor();let r=await this.loadAndRecover();const n=async()=>(await t.assertOwned(),r=await this.loadAndRecover(),r),a={load:n,set:async e=>{const a=normalizeServer(e);r=await n();const s=new Map(r.servers.map(e=>[e.name,e]));return s.set(a.name,a),r=await this.commit(r,[...s.values()].sort((e,t)=>e.name.localeCompare(t.name)),t,i),r},unset:async e=>(requireName(e),r=await n(),r.servers.some(t=>t.name===e)?(r=await this.commit(r,r.servers.filter(t=>t.name!==e),t,i),r):r),assertOwned:()=>t.assertOwned()};return await e(a)},{...this.options.lockOptions,epochFloor:()=>this.segments.authorityFloor()}))}async loadAndRecover(){await mkdir(dirname(this.path),{recursive:!0,mode:448});const[e,t]=await Promise.all([readState(this.path),readState(this.walPath)]);if("corrupt"===t.kind)throw new Error("MCP desired-state WAL is corrupt");if("corrupt"===e.kind&&"valid"!==t.kind)throw new Error("MCP desired state is corrupt");let i;if("valid"===t.kind)if("valid"===e.kind&&BigInt(e.state.revision)>BigInt(t.state.revision))await unlinkIfPresent(this.walPath),await syncDirectory(dirname(this.path)),i=e.state;else{if("valid"===e.kind&&BigInt(t.state.revision)>BigInt(e.state.revision)+1n)throw new Error("MCP desired-state WAL revision is discontinuous");"valid"===e.kind&&e.state.sha256===t.state.sha256||(await atomicWrite(this.path,encode(t.state),this.temp("recover")),await syncDirectory(dirname(this.path))),await unlinkIfPresent(this.walPath),await syncDirectory(dirname(this.path)),i=t.state}else i="valid"===e.kind?e.state:state(EMPTY_PAYLOAD);const r=new Map([["snapshot",{value:i,logicalRevision:i.revision,publisherEpoch:"0"}]]);return(await this.segments.reduce(r)).get("snapshot")?.value??i}async commit(e,t,i,r){const n=state({schemaVersion:1,revision:`${BigInt(e.revision)+1n}`,servers:t}),a=encode(n);return await i.assertOwned(),await this.segments.publish(i.epoch,[{key:"snapshot",logicalRevision:n.revision,value:n}],r),await atomicWrite(this.walPath,a,this.temp("wal")),await syncDirectory(dirname(this.path)),await(this.options.fault?.("wal-synced")),await atomicWrite(this.path,a,this.temp("state")),await syncDirectory(dirname(this.path)),await(this.options.fault?.("state-renamed")),await unlinkIfPresent(this.walPath),await syncDirectory(dirname(this.path)),await(this.options.fault?.("wal-cleared")),await this.segments.prune(),n}temp(e){return`${this.path}.${e}.${process.pid}.${randomUUID()}.tmp`}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function state(e){const t={schemaVersion:1,revision:e.revision,servers:Object.freeze(e.servers.map(normalizeServer).sort((e,t)=>e.name.localeCompare(t.name)))};return Object.freeze({...t,sha256:digest(t)})}function normalizeServer(e){requireName(e.name);for(const[t,i]of[["definitionRef",e.definitionRef],["version",e.version],["sha256",e.sha256]])if("string"!=typeof i||!i||i.length>512)throw new Error(`Invalid MCP ${t}`);if(!/^[a-f0-9]{64}$/i.test(e.sha256))throw new Error("Invalid MCP sha256");if(void 0!==e.credentialRef&&(!e.credentialRef||e.credentialRef.length>512))throw new Error("Invalid MCP credentialRef");assertPublicMcpValues([e.definitionRef,e.version,e.credentialRef],"MCP durable reference");const t=void 0===e.config?void 0:normalizeMcpPublicServerConfig(e.config),i=normalizeExpiry(e.expiresAt);return Object.freeze({name:e.name,definitionRef:e.definitionRef,version:e.version,sha256:e.sha256.toLowerCase(),...e.credentialRef?{credentialRef:e.credentialRef}:{},...i?{expiresAt:i}:{},...t?{config:t}:{}})}function normalizeExpiry(e){if(void 0===e)return;const t=parseRfc3339Instant(e);if(!t)throw new Error("Invalid MCP expiresAt");return t}const RFC3339_INSTANT=/^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;export function parseRfc3339Instant(e){if("string"!=typeof e||!RFC3339_INSTANT.test(e))return;const t=Date.parse(e);return Number.isNaN(t)?void 0:new Date(t).toISOString()}export function isExpiredMcpServer(e,t=Date.now()){return void 0!==e.expiresAt&&Date.parse(e.expiresAt)<=t}export function normalizeMcpPublicServerConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("Invalid MCP public config");const t=new Set(["transport","url","connectionTimeoutMs","command","args","cwd"]);if(Object.keys(e).some(e=>!t.has(e)))throw new Error("MCP desired state accepts public config only");if(!["sse","streamable-http","stdio"].includes(e.transport))throw new Error("Invalid MCP transport");if(void 0!==e.connectionTimeoutMs&&(!Number.isSafeInteger(e.connectionTimeoutMs)||e.connectionTimeoutMs<=0||e.connectionTimeoutMs>12e4))throw new Error("Invalid MCP connection timeout");if("stdio"===e.transport){if("string"!=typeof e.command||!e.command||void 0!==e.url)throw new Error("Invalid MCP stdio config");if(void 0!==e.args&&(!Array.isArray(e.args)||e.args.some(e=>"string"!=typeof e)))throw new Error("Invalid MCP stdio args");return assertPublicMcpValues([e.command,...e.args??[],e.cwd],"MCP stdio public definition contains secret-like argv"),Object.freeze({transport:e.transport,command:e.command,...e.args?{args:Object.freeze([...e.args])}:{},...e.cwd?{cwd:e.cwd}:{},...e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}})}if("string"!=typeof e.url||void 0!==e.command||void 0!==e.args||void 0!==e.cwd)throw new Error("Invalid MCP HTTP config");let i;try{i=new URL(e.url)}catch{throw new Error("Invalid MCP HTTP config")}if("https:"!==i.protocol||i.username||i.password||i.search||i.hash)throw new Error("MCP HTTP definition requires a credential-free HTTPS URL");let r=i.pathname;try{r=decodeURIComponent(r)}catch{}return assertPublicMcpValues([i.toString(),r],"MCP HTTP public definition"),Object.freeze({transport:e.transport,url:i.toString(),...e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}})}function digest(e){return createHash("sha256").update(canonical(e)).digest("hex")}function canonical(e){return null===e||"object"!=typeof e?JSON.stringify(e):Array.isArray(e)?`[${e.map(canonical).join(",")}]`:`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${canonical(t)}`).join(",")}}`}function encode(e){return`${canonical(e)}\n`}async function readState(e){let t;try{t=await readFile(e,"utf8")}catch(e){return"ENOENT"===e.code?{kind:"missing"}:{kind:"corrupt"}}try{const e=JSON.parse(t);if("revision,schemaVersion,servers,sha256"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||"string"!=typeof e.revision||!/^(0|[1-9][0-9]*)$/.test(e.revision)||!Array.isArray(e.servers)||"string"!=typeof e.sha256)return{kind:"corrupt"};const i=state({schemaVersion:1,revision:e.revision,servers:e.servers});return i.sha256===e.sha256?{kind:"valid",state:i}:{kind:"corrupt"}}catch{return{kind:"corrupt"}}}async function atomicWrite(e,t,i){const r=await open(i,"wx",384);try{await r.writeFile(t,"utf8"),await r.sync()}finally{await r.close()}await chmod(i,384),await rename(i,e),await chmod(e,384)}async function syncDirectory(e){const t=await open(e,"r");try{await t.sync()}finally{await t.close()}}async function unlinkIfPresent(e){try{await unlink(e)}catch(e){if("ENOENT"!==e.code)throw e}}const requireName=e=>{requireSecretFreeMcpServerName(e)};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{listMcpServers,setMcpServer,unsetMcpServer,OpenClawCliError}from"../openclaw-cli.js";import{Errors}from"../errors/index.js";import{requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";import{prepareManagedMcpPackage,requirePreparedManagedMcpConfig}from"../runtime/managed-mcp-package-installer.js";const RESTART_DEBOUNCE_MS=750;export class McpService{transport;logger;state;exit;restartTimer;list;set;unset;resolveDefinition;desiredState;preparePackage;requirePreparedPackage;desiredStateStarted=!1;constructor(e){this.transport=e.transport,this.logger=e.logger,this.state=e.state,this.exit=e.exit??(e=>process.exit(e)),this.list=e.list??listMcpServers,this.set=e.set??setMcpServer,this.unset=e.unset??unsetMcpServer,this.resolveDefinition=e.resolveDefinition,this.desiredState=e.desiredState,this.preparePackage=e.preparePackage??prepareManagedMcpPackage,this.requirePreparedPackage=e.requirePreparedPackage??requirePreparedManagedMcpConfig}setRuntimeReceiptHistoryPort(e){this.desiredState?.setRuntimeReceiptHistoryPort(e)}async execute(e,r,t){const i=e;if("mcp.list"===i.name){if(this.desiredState){await this.ensureDesiredState();const e=await this.desiredState.list();if(e.servers.some(e=>void 0===e.config))throw new Error("Service MCP config is available only within its active operation");return{servers:e.servers.map(e=>({name:e.name,definitionRef:e.definitionRef,transport:e.config.transport,...e.config.url?{url:e.config.url}:{},...e.config.command?{command:e.config.command}:{},...e.config.args?{args:e.config.args}:{},...e.config.cwd?{cwd:e.config.cwd}:{},...void 0!==e.connected?{connected:e.connected}:{},...e.config.connectionTimeoutMs?{connectionTimeoutMs:e.config.connectionTimeoutMs}:{}}))}}const e=await this.list();return{servers:Object.entries(e).map(([e,r])=>sanitizeMcpConfig(e,r))}}if(this.hasInFlightPrompts())throw new Error("PROMPT_IN_FLIGHT");if("service"===t.origin&&"mcp.prepare"!==i.name&&!this.desiredState)throw new Error("Service MCP mutations require the desired-state controller");if("mcp.prepare"===i.name){const e=i;requireSecretFreeMcpServerName(e.params.name);const r=t.resources.resolveMcpDefinition?.bind(t.resources)??("acp"===t.origin?this.resolveDefinition:void 0);if(!r)throw new Error("operation-scoped MCP definition resolver is not configured");const s=await this.preparePackage(await r(e.params.definition),{signal:t.abortSignal});return{ready:!0,cached:s.cached,source:s.source,...s.packageVersion?{packageVersion:s.packageVersion}:{}}}if("mcp.set"===i.name){const e=i;requireSecretFreeMcpServerName(e.params.name);const r=t.resources.resolveMcpDefinition?.bind(t.resources)??("acp"===t.origin?this.resolveDefinition:void 0);if(!r)throw new Error("operation-scoped MCP definition resolver is not configured");const s=await this.requirePreparedPackage(await r(e.params.definition));return this.desiredState?(await this.ensureDesiredState(),await this.desiredState.set(desiredServer(e.params.name,e.params.definition,publicConfig(s)),commitBinding(e,t)),{applied:!0,restartRequired:!1}):(await this.set(e.params.name,s),{applied:!0,restartRequired:!0})}const s=i.params.name;return requireSecretFreeMcpServerName(s),this.desiredState?(await this.ensureDesiredState(),await this.desiredState.unset(s,commitBinding(i,t)),{applied:!0,restartRequired:!1}):(await this.unset(s),{applied:!0,restartRequired:!0})}afterCommit(e){this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name||this.scheduleRestart()}async projectCommitted(e,r,t){if(!this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name)return;await this.ensureDesiredState();const i=e.params?.name;if("string"!=typeof i||!i)throw new Error("committed MCP projection lacks a server name");const s=t?"mcp.set"===e.name?{operationId:e.context.operationId,method:"mcp.set",source:"service"===t.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,definitionRef:e.params.definition.definitionRef,version:e.params.definition.version,sha256:e.params.definition.sha256,...e.params.definition.credentialRef?{credentialRef:e.params.definition.credentialRef}:{}}}:{operationId:e.context.operationId,method:"mcp.unset",source:"service"===t.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,absent:!0}}:void 0;let a;if("service"===t?.origin&&"mcp.set"===e.name){const r=e.params.definition,i=t.resources.resolveMcpDefinition?.bind(t.resources);if(!i)throw new Error("operation-scoped MCP definition resolver is not configured");a={config:publicConfig(await this.requirePreparedPackage(await i(r))),runtimeEpoch:t.runtimeEpoch,...t.resources.resolveMcpCredential?{credential:await t.resources.resolveMcpCredential(r)}:{}}}return a?await this.desiredState.project(i,t?.abortSignal,s,a):await this.desiredState.project(i,t?.abortSignal,s)}async removeCredentialIfUnreferenced(e){return!!this.desiredState&&(await this.desiredState.removeLocalCredentialIfUnreferenced(e),!0)}async handleList(e){try{if(this.desiredState){await this.ensureDesiredState();const r=await this.desiredState.list();return void this.transport.sendResult(e.id,{revision:r.revision,servers:r.servers})}const r=await this.list();this.transport.sendResult(e.id,{servers:r})}catch(r){this.logger.warn?.(`[mcp-service] list failed: ${r instanceof Error?r.message:r}`),this.sendMappedError(e,r,!0)}}async handleSet(e){const r=e.params,t=r?.name,i=r?.server;if(t&&i&&"object"==typeof i){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(t),this.desiredState){const r=asDesiredServer(t,i);return await this.ensureDesiredState(),await this.desiredState.set(r),await this.desiredState.project(t),this.logger.info?.(`[mcp-service] desired state applied name=${t}`),void this.transport.sendResult(e.id,{ok:!0,restarting:!1})}await this.set(t,i),this.logger.info?.(`[mcp-service] set applied name=${t}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(r){this.logger.error?.(`[mcp-service] set failed name=${t}: ${r instanceof Error?r.message:r}`),this.sendMappedError(e,r,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name' or 'server' config"})}async handleUnset(e){const r=e.params,t=r?.name;if(t){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(t),this.desiredState)return await this.ensureDesiredState(),await this.desiredState.unset(t),await this.desiredState.project(t),void this.transport.sendResult(e.id,{ok:!0,restarting:!1});await this.unset(t),this.logger.info?.(`[mcp-service] unset applied name=${t}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(r){this.logger.error?.(`[mcp-service] unset failed name=${t}: ${r instanceof Error?r.message:r}`),this.sendMappedError(e,r,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name'"})}hasInFlightPrompts(){for(const e of this.state.sessions.values())if(void 0!==e.pendingPromptId||void 0!==e.currentRunId)return!0;return!1}rejectIfInFlight(e){return!!this.hasInFlightPrompts()&&(this.transport.sendError(e.id,Errors.InvalidParams.rpcCode,"Agent is busy",{code:Errors.InvalidParams.code,reason:"PROMPT_IN_FLIGHT",message:"Agent is processing a message; please retry after it completes."}),!0)}scheduleRestart(){this.restartTimer&&clearTimeout(this.restartTimer),this.restartTimer=setTimeout(()=>{this.logger.info?.("[mcp-service] mcp change applied — exiting for container restart"),this.exit(0)},750)}async ensureDesiredState(){if(!this.desiredStateStarted){if(!this.desiredState)throw new Error("MCP desired-state controller is unavailable");await this.desiredState.start(),this.desiredStateStarted=!0}}async start(){this.desiredState&&(this.desiredStateStarted?await this.desiredState.reconnect():await this.ensureDesiredState())}nudgeNativeProjection(){this.desiredState?.nudgeNativeProjection()}async stop(){this.desiredState&&this.desiredStateStarted&&(this.desiredStateStarted=!1,await this.desiredState.stop())}sendMappedError(e,r,t){const i=cliErrToAcpError(r,t);this.transport.sendError(e.id,i.code,i.message,i.data)}}function desiredServer(e,r,t){return{name:e,definitionRef:r.definitionRef,version:r.version,sha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{},config:t}}function commitBinding(e,r){return{source:"service"===r.origin?"service":"acp_local",operationId:e.context.operationId,method:e.name,paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch}}function publicConfig(e){if(void 0!==e.headers||void 0!==e.env)throw new Error("resolved MCP secrets must not enter desired state");return{transport:e.transport??(e.command?"stdio":"streamable-http"),..."string"==typeof e.url?{url:e.url}:{},..."string"==typeof e.command?{command:e.command}:{},...Array.isArray(e.args)?{args:e.args}:{},..."string"==typeof e.cwd?{cwd:e.cwd}:{},..."number"==typeof e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}}}function asDesiredServer(e,r){const t=r;if("string"!=typeof t.definitionRef||"string"!=typeof t.version||"string"!=typeof t.sha256)throw new Error("ACP mcp/set requires a verified definitionRef, version and sha256");if(void 0!==t.headers||void 0!==t.env)throw new Error("ACP mcp/set accepts credentialRef, not resolved headers or env");return desiredServer(e,{definitionRef:t.definitionRef,version:t.version,sha256:t.sha256,..."string"==typeof t.credentialRef?{credentialRef:t.credentialRef}:{},..."number"==typeof t.connectionTimeoutMs?{connectionTimeoutMs:t.connectionTimeoutMs}:{}},publicConfig(r))}function sanitizeMcpConfig(e,r){const t=r.transport??(r.command?"stdio":"streamable-http");return{name:e,definitionRef:`runtime-mcp:${encodeURIComponent(e)}`,transport:t,..."number"==typeof r.connectionTimeoutMs?{connectionTimeoutMs:r.connectionTimeoutMs}:{},..."string"==typeof r.url?{url:r.url}:{},..."string"==typeof r.command?{command:r.command}:{},...Array.isArray(r.args)?{args:r.args}:{},..."string"==typeof r.cwd?{cwd:r.cwd}:{}}}export function cliErrToAcpError(e,r){if(e instanceof OpenClawCliError){const t=e.stderr??"";return null===e.exitCode||r?{code:Errors.InternalError.rpcCode,message:e.message,data:{code:Errors.InternalError.code}}:/No MCP server named/i.test(t)?{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code,reason:"NOT_FOUND"}}:{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code}}}return{code:Errors.InternalError.rpcCode,message:e instanceof Error?e.message:String(e),data:{code:Errors.InternalError.code}}}
|
|
1
|
+
import{listMcpServers,setMcpServer,unsetMcpServer,OpenClawCliError}from"../openclaw-cli.js";import{Errors}from"../errors/index.js";import{requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";import{prepareManagedMcpPackage,requirePreparedManagedMcpConfig}from"../runtime/managed-mcp-package-installer.js";const RESTART_DEBOUNCE_MS=750;export class McpService{transport;logger;state;exit;restartTimer;list;set;unset;resolveDefinition;desiredState;preparePackage;requirePreparedPackage;desiredStateStarted=!1;constructor(e){this.transport=e.transport,this.logger=e.logger,this.state=e.state,this.exit=e.exit??(e=>process.exit(e)),this.list=e.list??listMcpServers,this.set=e.set??setMcpServer,this.unset=e.unset??unsetMcpServer,this.resolveDefinition=e.resolveDefinition,this.desiredState=e.desiredState,this.preparePackage=e.preparePackage??prepareManagedMcpPackage,this.requirePreparedPackage=e.requirePreparedPackage??requirePreparedManagedMcpConfig}setRuntimeReceiptHistoryPort(e){this.desiredState?.setRuntimeReceiptHistoryPort(e)}async execute(e,r,t){const i=e;if("mcp.list"===i.name){if(this.desiredState){await this.ensureDesiredState();const e=await this.desiredState.list();if(e.servers.some(e=>void 0===e.config))throw new Error("Service MCP config is available only within its active operation");return{servers:e.servers.map(e=>({name:e.name,definitionRef:e.definitionRef,transport:e.config.transport,...e.config.url?{url:e.config.url}:{},...e.config.command?{command:e.config.command}:{},...e.config.args?{args:e.config.args}:{},...e.config.cwd?{cwd:e.config.cwd}:{},...void 0!==e.connected?{connected:e.connected}:{},...e.config.connectionTimeoutMs?{connectionTimeoutMs:e.config.connectionTimeoutMs}:{}}))}}const e=await this.list();return{servers:Object.entries(e).map(([e,r])=>sanitizeMcpConfig(e,r))}}if(this.hasInFlightPrompts())throw new Error("PROMPT_IN_FLIGHT");if("service"===t.origin&&"mcp.prepare"!==i.name&&!this.desiredState)throw new Error("Service MCP mutations require the desired-state controller");if("mcp.prepare"===i.name){const e=i;requireSecretFreeMcpServerName(e.params.name);const r=t.resources.resolveMcpDefinition?.bind(t.resources)??("acp"===t.origin?this.resolveDefinition:void 0);if(!r)throw new Error("operation-scoped MCP definition resolver is not configured");const s=await this.preparePackage(await r(e.params.definition),{signal:t.abortSignal});return{ready:!0,cached:s.cached,source:s.source,...s.packageVersion?{packageVersion:s.packageVersion}:{}}}if("mcp.set"===i.name){const e=i;requireSecretFreeMcpServerName(e.params.name);const r=t.resources.resolveMcpDefinition?.bind(t.resources)??("acp"===t.origin?this.resolveDefinition:void 0);if(!r)throw new Error("operation-scoped MCP definition resolver is not configured");const s=await this.requirePreparedPackage(await r(e.params.definition));if(this.desiredState){await this.ensureDesiredState();const r="service"===t.origin?await(t.resources.resolveMcpCredential?.bind(t.resources)?.(e.params.definition)):void 0;if(r?.expiresAt&&Date.parse(r.expiresAt)<=Date.now())throw new Error("MCP credential is already expired");return await this.desiredState.set(desiredServer(e.params.name,e.params.definition,publicConfig(s),r?.expiresAt),commitBinding(e,t)),{applied:!0,restartRequired:!1}}return await this.set(e.params.name,s),{applied:!0,restartRequired:!0}}const s=i.params.name;return requireSecretFreeMcpServerName(s),this.desiredState?(await this.ensureDesiredState(),await this.desiredState.unset(s,commitBinding(i,t)),{applied:!0,restartRequired:!1}):(await this.unset(s),{applied:!0,restartRequired:!0})}afterCommit(e){this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name||this.scheduleRestart()}async projectCommitted(e,r,t){if(!this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name)return;await this.ensureDesiredState();const i=e.params?.name;if("string"!=typeof i||!i)throw new Error("committed MCP projection lacks a server name");const s=t?"mcp.set"===e.name?{operationId:e.context.operationId,method:"mcp.set",source:"service"===t.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,definitionRef:e.params.definition.definitionRef,version:e.params.definition.version,sha256:e.params.definition.sha256,...e.params.definition.credentialRef?{credentialRef:e.params.definition.credentialRef}:{}}}:{operationId:e.context.operationId,method:"mcp.unset",source:"service"===t.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,absent:!0}}:void 0;let a;if("service"===t?.origin&&"mcp.set"===e.name){const r=e.params.definition,i=t.resources.resolveMcpDefinition?.bind(t.resources);if(!i)throw new Error("operation-scoped MCP definition resolver is not configured");a={config:publicConfig(await this.requirePreparedPackage(await i(r))),runtimeEpoch:t.runtimeEpoch,...t.resources.resolveMcpCredential?{credential:await t.resources.resolveMcpCredential(r)}:{}}}return a?await this.desiredState.project(i,t?.abortSignal,s,a):await this.desiredState.project(i,t?.abortSignal,s)}async removeCredentialIfUnreferenced(e){return!!this.desiredState&&(await this.desiredState.removeLocalCredentialIfUnreferenced(e),!0)}async handleList(e){try{if(this.desiredState){await this.ensureDesiredState();const r=await this.desiredState.list();return void this.transport.sendResult(e.id,{revision:r.revision,servers:r.servers})}const r=await this.list();this.transport.sendResult(e.id,{servers:r})}catch(r){this.logger.warn?.(`[mcp-service] list failed: ${r instanceof Error?r.message:r}`),this.sendMappedError(e,r,!0)}}async handleSet(e){const r=e.params,t=r?.name,i=r?.server;if(t&&i&&"object"==typeof i){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(t),this.desiredState){const r=asDesiredServer(t,i);return await this.ensureDesiredState(),await this.desiredState.set(r),await this.desiredState.project(t),this.logger.info?.(`[mcp-service] desired state applied name=${t}`),void this.transport.sendResult(e.id,{ok:!0,restarting:!1})}await this.set(t,i),this.logger.info?.(`[mcp-service] set applied name=${t}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(r){this.logger.error?.(`[mcp-service] set failed name=${t}: ${r instanceof Error?r.message:r}`),this.sendMappedError(e,r,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name' or 'server' config"})}async handleUnset(e){const r=e.params,t=r?.name;if(t){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(t),this.desiredState)return await this.ensureDesiredState(),await this.desiredState.unset(t),await this.desiredState.project(t),void this.transport.sendResult(e.id,{ok:!0,restarting:!1});await this.unset(t),this.logger.info?.(`[mcp-service] unset applied name=${t}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(r){this.logger.error?.(`[mcp-service] unset failed name=${t}: ${r instanceof Error?r.message:r}`),this.sendMappedError(e,r,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name'"})}hasInFlightPrompts(){for(const e of this.state.sessions.values())if(void 0!==e.pendingPromptId||void 0!==e.currentRunId)return!0;return!1}rejectIfInFlight(e){return!!this.hasInFlightPrompts()&&(this.transport.sendError(e.id,Errors.InvalidParams.rpcCode,"Agent is busy",{code:Errors.InvalidParams.code,reason:"PROMPT_IN_FLIGHT",message:"Agent is processing a message; please retry after it completes."}),!0)}scheduleRestart(){this.restartTimer&&clearTimeout(this.restartTimer),this.restartTimer=setTimeout(()=>{this.logger.info?.("[mcp-service] mcp change applied — exiting for container restart"),this.exit(0)},750)}async ensureDesiredState(){if(!this.desiredStateStarted){if(!this.desiredState)throw new Error("MCP desired-state controller is unavailable");await this.desiredState.start(),this.desiredStateStarted=!0}}async start(){this.desiredState&&(this.desiredStateStarted?await this.desiredState.reconnect():await this.ensureDesiredState())}nudgeNativeProjection(){this.desiredState?.nudgeNativeProjection()}async stop(){this.desiredState&&this.desiredStateStarted&&(this.desiredStateStarted=!1,await this.desiredState.stop())}sendMappedError(e,r,t){const i=cliErrToAcpError(r,t);this.transport.sendError(e.id,i.code,i.message,i.data)}}function desiredServer(e,r,t,i){return{name:e,definitionRef:r.definitionRef,version:r.version,sha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{},...i?{expiresAt:i}:{},config:t}}function commitBinding(e,r){return{source:"service"===r.origin?"service":"acp_local",operationId:e.context.operationId,method:e.name,paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch}}function publicConfig(e){if(void 0!==e.headers||void 0!==e.env)throw new Error("resolved MCP secrets must not enter desired state");return{transport:e.transport??(e.command?"stdio":"streamable-http"),..."string"==typeof e.url?{url:e.url}:{},..."string"==typeof e.command?{command:e.command}:{},...Array.isArray(e.args)?{args:e.args}:{},..."string"==typeof e.cwd?{cwd:e.cwd}:{},..."number"==typeof e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}}}function asDesiredServer(e,r){const t=r;if("string"!=typeof t.definitionRef||"string"!=typeof t.version||"string"!=typeof t.sha256)throw new Error("ACP mcp/set requires a verified definitionRef, version and sha256");if(void 0!==t.headers||void 0!==t.env)throw new Error("ACP mcp/set accepts credentialRef, not resolved headers or env");return desiredServer(e,{definitionRef:t.definitionRef,version:t.version,sha256:t.sha256,..."string"==typeof t.credentialRef?{credentialRef:t.credentialRef}:{},..."number"==typeof t.connectionTimeoutMs?{connectionTimeoutMs:t.connectionTimeoutMs}:{}},publicConfig(r))}function sanitizeMcpConfig(e,r){const t=r.transport??(r.command?"stdio":"streamable-http");return{name:e,definitionRef:`runtime-mcp:${encodeURIComponent(e)}`,transport:t,..."number"==typeof r.connectionTimeoutMs?{connectionTimeoutMs:r.connectionTimeoutMs}:{},..."string"==typeof r.url?{url:r.url}:{},..."string"==typeof r.command?{command:r.command}:{},...Array.isArray(r.args)?{args:r.args}:{},..."string"==typeof r.cwd?{cwd:r.cwd}:{}}}export function cliErrToAcpError(e,r){if(e instanceof OpenClawCliError){const t=e.stderr??"";return null===e.exitCode||r?{code:Errors.InternalError.rpcCode,message:e.message,data:{code:Errors.InternalError.code}}:/No MCP server named/i.test(t)?{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code,reason:"NOT_FOUND"}}:{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code}}}return{code:Errors.InternalError.rpcCode,message:e instanceof Error?e.message:String(e),data:{code:Errors.InternalError.code}}}
|
package/dist/acp-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{join}from"node:path";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{AcpRunOutputChannel}from"./acp-gateway/acp-run-output-channel.js";import{openRunOutputRoute}from"./acp-gateway/run-output-route.js";import{createAcpRunOutputRouter}from"./runtime/run-output-composition.js";import{AgentMappingRegistry}from"./runtime/agent-mapping-registry.js";import{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;runtimeAuthority;mcpProjection;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;markAgentProjectionDirty;templateOriginReporter;agentMappings;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;runOutputRouter;cronConversationDelivery;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;runtimeServiceFactoryFenced=!1;legacyFencedSyncFailureCount=0;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).catch(e=>this.logger.warn?.(`[acp-server] Deferred agentSend failed: ${e instanceof Error?e.message:String(e)}`)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const n=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,s;i?(r=this.state.findSessionByKey(i),s=i,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${r.sessionId}`))):(s=`agent:${n}:ephemeral:${randomUUID()}`,r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const a=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let o=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(a);o=!0;const c=await i.agentSend(s,e,n,t);return c?.runId&&r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(a,c.runId),this.core.observeActiveRun(r),r.assistantStreamStartedAt=Date.now(),r.writeGateToken=a,this.state.promptsByRunId.set(c.runId,r),openRunOutputRoute(this.runOutputRouter,r,c.runId,["acp"])):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setCronConversationDelivery(e){this.cronConversationDelivery=e}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}nudgeMcpNativeProjection(){this.core.nudgeMcpNativeProjection()}ensureImInboundSession(e,t,i,n){const r=this.core.ensureSessionForKey(e);r.channelId=t,r.inboundMessageId=i,r.activeMentions=n.length>0?n:void 0}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,i,n,r,s,a,o,c,l,h,g,d,u={}){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpProjection=h,this.mcpCredentialReferences=g,this.boundAgentFetchFn=d,this.config=e,this.logger=t,this.writeObs=n??(()=>{}),this.thirdPartyChannels=a??new Set,this.sessionMutationCoordinator=i,this.gateway=u.gateway??null,this.agentProjection=u.agentProjection,this.markAgentProjectionDirty=u.markAgentProjectionDirty,this.templateOriginReporter=u.templateOriginReporter,this.agentMappings=u.agentMappings??new AgentMappingRegistry,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities=u.agentCapabilities??{canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",!0===this.config.terminalServiceEnabled),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const m=(u.createRunOutputRouter??createAcpRunOutputRouter)({acp:new AcpRunOutputChannel({state:this.state,transport:this.transport}),lifecycle:{terminal:(e,t)=>this.core.commitRunOutputTerminal(e,t)}});this.runOutputRouter=m;const p=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,declareModel:u.declareModel,titles:u.titles,instanceId:this.instanceId,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:p,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.markAgentProjectionDirty,getAgentProjection:this.agentProjection?()=>this.agentProjection:void 0,templateOriginReporter:this.templateOriginReporter,...u.runExclusive?{runExclusive:u.runExclusive}:{},mcpProjection:this.mcpProjection,mcpCredentialReferences:this.mcpCredentialReferences,runOutputRouter:m,agentMappings:this.agentMappings}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,sendAgentMessage:(e,t)=>this.agentSend(e,void 0,t),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,runOutputRouter:m,adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},agentFetch:this.agentFetchFn,getCronConversationDelivery:()=>this.cronConversationDelivery??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=o??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)}),this.gateway&&this.bindGateway(this.gateway)}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}async rebindRuntimeDelivery(){if("started"!==this.lifecyclePhase)return;const e=this.activeGeneration,t=e?.factory??this.runtimeRpcConsumerFactory,i=e?.authority??this.runtimeAuthority;if(!e||!t||!i)return;if(!i.authorityLease)return;const n=i.currentConsumerBinding();if(!n)return void this.logger.warn?.("[acp-server] runtime Service delivery rebind skipped — no consumer binding");const r=e.runtimeConsumer;let s;r&&(e.runtimeConsumer=void 0,this.runtimeRpcConsumer===r&&(this.runtimeRpcConsumer=void 0),this.core.setRuntimeReceiptHistoryPort(void 0),await this.closeConsumerOnce(r).catch(()=>{}));try{s=await t.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:n,getBinding:()=>i.currentConsumerBinding()??n,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal})}catch(e){this.logger.error?.(`[acp-server] runtime Service delivery rebind failed: ${e instanceof Error?e.stack??e.message:String(e)}`);try{i.setRuntimeConsumerLive(!1)}catch{}return}if(this.activeGeneration!==e||"started"!==this.lifecyclePhase||e.abortController.signal.aborted)await this.closeConsumerOnce(s).catch(()=>{});else{this.core.setRuntimeReceiptHistoryPort(s.receiptHistory),e.runtimeConsumer=s,this.runtimeRpcConsumer=s,s.start();try{i.setRuntimeConsumerLive(!0)}catch{}this.runtimeServiceFactoryFenced=!1,this.logger.info?.("[acp-server] runtime Service delivery rebound after MessageClient connect")}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,n=e.factory;let r=!1;if(i&&n){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=n.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getBinding:()=>i.currentConsumerBinding()??t.value,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal}),a=await this.waitForGeneration(s,e);if("completed"!==a.status)return void("cancelled"===a.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const o=a.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(o,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(o.receiptHistory),e.runtimeConsumer=o,this.runtimeRpcConsumer=o,o.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),r=!0,this.runtimeServiceFactoryFenced=!1}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&(this.runtimeServiceFactoryFenced=!0,this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced"));if(this.core.startMcp().catch(e=>{this.logger.warn?.(`[acp-server] MCP desired-state reconcile failed: ${e instanceof Error?e.message:String(e)}`)}),!this.shouldContinueStart(e))return;if(i&&r){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const a=this.config.bridge.url,o=this.config.keyFile;if(a&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(this.shouldContinueStart(e)){if(!0===this.config.terminalServiceEnabled&&a&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:a,authority:i,keys:i.agentKeys}),n=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void n.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=n,this.terminalOutbound=n;try{const t=await this.waitForGeneration(n.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(this.shouldContinueStart(e)){if(a&&o&&BridgeClientCtor){Date.now();await this.startBridgeMode(a,o,e)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-s}ms`)}}}shouldContinueStart(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&"starting"===this.lifecyclePhase}isGenerationActive(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)}setTerminalServiceReady(e,t){if(this.activeGeneration!==e)return;const i=!0===this.config.terminalServiceEnabled&&t;this.terminalServiceReady!==i&&(this.terminalServiceReady=i,this.runtimeAgentCapabilities.terminal=i,this.markAgentProjectionDirty?this.markAgentProjectionDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const n=t.abortController.signal;return n.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,r)=>{let s,a=!1;const o=e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>o({status:"cancelled"});n.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>o({status:"timed_out"}),i),s.unref?.()),e.then(e=>o({status:"completed",value:e}),e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),r(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,n),n}async startBridgeMode(e,t,i){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const n=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(n,!1).catch(()=>{});i.bridge=n,this.bridgeInstance=n;const r=()=>this.bridgeInstance===n;n.on("connected",()=>{r()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),n.on("disconnected",(...e)=>{if(!r())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),a=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${a})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async s=>{if(this.isGenerationActive(i)&&r()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(n,!1),!this.isGenerationActive(i)||this.bridgeInstance!==n)return;i.bridge===n&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),n.on("message",(...e)=>{if(!r())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),n.on("error",(...e)=>{if(!r())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{n.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(n).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(n,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===n.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const a=Date.now();try{const e=await this.waitForGeneration(n.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-a}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-a}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,n),n}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return;if(!this.runtimeServiceFactoryFenced)return;0===e&&this.logger.error?.("[acp-server] runtime Service factory fenced with no authority lease — attempting one-shot legacy agent-sync bootstrap to recover Agent Identity")}const t=!!this.agentProjection;if(t||(this.legacyFencedSyncFailureCount=0),this.agentSyncInFlight)return;const i=[1e3,2e3,5e3,1e4],n=3e5,r=()=>{const r=e+1;if(t&&(this.legacyFencedSyncFailureCount+=1,this.legacyFencedSyncFailureCount>=4)){const e=Math.round(5);return this.logger.error?.(`[acp-server] legacy agent-sync bootstrap fallback has failed ${this.legacyFencedSyncFailureCount} consecutive times while the runtime Service factory is fenced — this fallback path is confirmed incompatible with a backend that requires the runtime-projection agent-sync schema and cannot self-heal; restart this instance once Message Service / the runtime Service factory recover. Backing off to a ${e}m retry interval to stop busy-looping a request that cannot succeed.`),void(this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},n))}const s=i[Math.min(e,i.length-1)],a=Math.round(s/1e3),o=`[acp-server] agent sync retry #${r} in ${a}s`;r>0&&r%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${r} attempts — retrying every ${a}s`):r<=i.length?this.logger.info?.(o):this.logger.debug?.(o),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},s)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:this.terminalServiceReady,fileSystem:!0,cron:!0,streaming:!0},deliveryMode:this.config.messageBus.enabled?"push":void 0,boundAgentFetch:this.boundAgentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.legacyFencedSyncFailureCount=0,await this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync + session hydration took ${Date.now()-s}ms)`),this.agentProjection&&this.runtimeServiceFactoryFenced&&this.logger.error?.("[acp-server] agent bootstrap sync recovered Agent Identity while the runtime Service factory was fenced — restart this instance to complete registration")):r()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),r()}finally{this.agentSyncInFlight=!1}}dispatchGatewayEvent(e,t,i){try{i()}catch(i){const n=i instanceof Error?i.message:String(i),r=n.startsWith("run output route unavailable:");this.logger.error?.(`[acp-server] Gateway ${e} handler failed${t?` runId=${t}`:""}: ${n}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:r?"run_output.route_missing":"gateway_event.handler_failed",severity:"error",summary:n,payload:{event:e,...t?{runId:t}:{}}})}}bindGateway(e){e.on("chat",e=>{this.dispatchGatewayEvent("chat",e.runId,()=>this.events.handleChatEvent(e))}),e.on("agent",e=>{this.dispatchGatewayEvent("agent",e.runId,()=>this.events.handleAgentEvent(e))}),e.on("cron",e=>{this.dispatchGatewayEvent("cron",void 0,()=>this.events.handleCronEvent(e))}),e.on("sessionMessage",e=>{this.dispatchGatewayEvent("session.message",void 0,()=>this.core.handleSessionMessage(e))}),e.on("response",e=>{this.dispatchGatewayEvent("response",void 0,()=>this.core.handleGatewayResponse(e))});const t=e;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t)}:null}),this.config.openclawCanvasCompat&&(e.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),e.on("connected",()=>{this.core.beginGatewaySessionPreload(),this.logger.info?.("[acp-server] connection transport=openclaw_gateway_ws state=connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.core.prepareWorkspaceCatalog(),this.tryAgentSync()}),e.on("disconnected",()=>{this.core.failGatewaySessionPreload(),this.logger.warn?.("[acp-server] connection transport=openclaw_gateway_ws state=disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),e.on("reconnecting",e=>{this.logger.info?.(`[acp-server] connection transport=openclaw_gateway_ws state=reconnecting attempt=${e.attempt} delay_ms=${Math.round(e.delayMs)}`)}),e.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),e.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),e.on("transport",e=>{const t="connect_fail"===e.event&&!0===e.retryable,i=`[acp-server] connection transport=openclaw_gateway_ws event=${e.event} duration_ms=${e.durationMs??"?"}`;t?this.logger.debug?.(i):this.logger.info?.(i),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:t?"debug":"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),e.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),e.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")})}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",t=>{if(this.client!==e)return;const n="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(n);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,n)=>this.core.dispatchLegacyTerminalCapability(e,t,i,n)});return t.setupListeners(),t}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.agentMappings.waitForSynchronization()}onceAgentMappingReady(e){this.agentMappings.onceReady(e)}onAgentMappingUpdated(e){const t=this.agentMappings.snapshot();t&&t.size>0&&e(new Map(t)),this.agentMappings.subscribe(t=>e(new Map(t)))}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}resolveLocalAgentIdForPlatformAgent(e){return this.core.resolveLocalAgentId(e)}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.agentMappings.snapshot();if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.agentMappings.snapshot();return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:n})=>{try{const r=`${t}/skill.json`,s=await n(r,"utf-8"),a=JSON.parse(s);if(a.formSchemas?.length){for(const e of a.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,n;const r=new Promise((e,t)=>{i=e,n=t});t.stopPromise=r,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){n(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),r}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let n,r=!1;const s=new Promise(e=>{n=setTimeout(()=>{r=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),n.unref?.()}),a=async(n,r)=>{let a;const o=new Promise(e=>{a=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:a},()=>r(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),o.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${n}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:n,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},o=[];this.setTerminalServiceReady(e,!1),o.push(a("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),o.push(a("session-state",()=>this.state.destroy())),o.push(a("canvas-orchestrator",()=>this.canvasOrch.destroy())),o.push(a("mcp-receipt-history",()=>this.core.setRuntimeReceiptHistoryPort(void 0)));const c=e.bridge,l=e.bridgeSocket,h=e.terminalOutbound,g=e.runtimeConsumer,d=this.client&&this.client!==l?this.client:null,u=this.wss;if(e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.bridgeInstance===c&&(this.bridgeInstance=null),this.client!==d&&this.client!==l||(this.client=null),this.terminalOutbound===h&&(this.terminalOutbound=null),this.runtimeRpcConsumer===g&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,d&&o.push(a("incoming-client",()=>{d.removeAllListeners(),d.close()})),c&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(c)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),h&&o.push(a("terminal-outbound",e=>h.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),g&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(g,e))),u&&o.push(a("websocket-server",()=>{u.removeAllListeners(),u.close()})),await Promise.all(o),await a("runtime-authority-reset",()=>e.authority?.setRuntimeConsumerLive(!1)),await a("terminal-handler",()=>this.terminalHandler.destroy()),await a("terminal-manager",()=>this.terminalMgr.destroy()),n&&clearTimeout(n),r||i.abort(),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"}),t.length>0)throw t[0]}}
|
|
1
|
+
import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{join}from"node:path";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{AcpRunOutputChannel}from"./acp-gateway/acp-run-output-channel.js";import{openRunOutputRoute}from"./acp-gateway/run-output-route.js";import{createAcpRunOutputRouter}from"./runtime/run-output-composition.js";import{AgentMappingRegistry}from"./runtime/agent-mapping-registry.js";import{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{writeDiscoveryCache}from"./discovery-cache.js";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{cloud;instanceId;runtimeAuthority;mcpProjection;mcpCredentialReferences;wss=null;gateway;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;markAgentProjectionDirty;templateOriginReporter;agentMappings;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;runOutputRouter;cronConversationDelivery;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;runtimeServiceFactoryFenced=!1;legacyFencedSyncFailureCount=0;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).catch(e=>this.logger.warn?.(`[acp-server] Deferred agentSend failed: ${e instanceof Error?e.message:String(e)}`)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const r=this.config.gateway.agentId||this.core.getDefaultAgentId();let n,s;i?(n=this.state.findSessionByKey(i),s=i,n?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${n.sessionId}`):(n=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${n.sessionId}`))):(s=`agent:${r}:ephemeral:${randomUUID()}`,n=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const o=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let a=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(n&&!n.promptDone&&(void 0!==n.pendingPromptId||void 0!==n.currentRunId||void 0!==n.gatewayRequestId))return(n.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",n.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(o);a=!0;const c=await i.agentSend(s,e,r,t);return c?.runId&&n?(this.state.resetForNewRun(n,n.sessionId,this.transport),n.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(o,c.runId),this.core.observeActiveRun(n),n.assistantStreamStartedAt=Date.now(),n.writeGateToken=o,this.state.promptsByRunId.set(c.runId,n),openRunOutputRoute(this.runOutputRouter,n,c.runId,["acp"])):this.sessionMutationCoordinator.releaseBeforeRuntime(o),c}catch(e){throw a?this.sessionMutationCoordinator.requireRecovery(o,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(o),e}}setCronConversationDelivery(e){this.cronConversationDelivery=e}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}nudgeMcpNativeProjection(){this.core.nudgeMcpNativeProjection()}resolveNativePromptSessionKey(e,t){return this.core.resolveNativePromptSessionKey(e,t)}prepareNativePromptModel(e,t,i){return this.core.prepareNativePromptModel(e,t,i)}applyImTurnModel(e,t){return this.core.applyImTurnModel(e,t)}restoreImTurnModel(e){return this.core.restoreImTurnModel(e)}ensureImInboundSession(e,t,i,r){const n=this.core.ensureSessionForKey(e);n.channelId=t,n.inboundMessageId=i,n.activeMentions=r.length>0?r:void 0}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,i,r,n,s,o,a,c,l,h,g,d={}){this.cloud=n,this.instanceId=s,this.runtimeAuthority=c,this.mcpProjection=h,this.mcpCredentialReferences=g,this.config=e,this.logger=t,this.writeObs=r??(()=>{}),this.thirdPartyChannels=o??new Set,this.sessionMutationCoordinator=i,this.gateway=d.gateway??null,this.agentProjection=d.agentProjection,this.markAgentProjectionDirty=d.markAgentProjectionDirty,this.templateOriginReporter=d.templateOriginReporter,this.agentMappings=d.agentMappings??new AgentMappingRegistry,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities=d.agentCapabilities??{canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",!0===this.config.terminalServiceEnabled),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const u=(d.createRunOutputRouter??createAcpRunOutputRouter)({acp:new AcpRunOutputChannel({state:this.state,transport:this.transport}),lifecycle:{terminal:(e,t)=>this.core.commitRunOutputTerminal(e,t)}});this.runOutputRouter=u;const m=this.cloud?new BeeosFileResolver({cloud:this.cloud,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:m,cloud:this.cloud,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.markAgentProjectionDirty,getAgentProjection:this.agentProjection?()=>this.agentProjection:void 0,templateOriginReporter:this.templateOriginReporter,...d.runExclusive?{runExclusive:d.runExclusive}:{},mcpProjection:this.mcpProjection,mcpCredentialReferences:this.mcpCredentialReferences,runOutputRouter:u,agentMappings:this.agentMappings}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,sendAgentMessage:(e,t)=>this.agentSend(e,void 0,t),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,runOutputRouter:u,adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},cloud:this.cloud,getCronConversationDelivery:()=>this.cronConversationDelivery??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=a??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)}),this.gateway&&this.bindGateway(this.gateway)}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}async rebindRuntimeDelivery(){if("started"!==this.lifecyclePhase)return;const e=this.activeGeneration,t=e?.factory??this.runtimeRpcConsumerFactory,i=e?.authority??this.runtimeAuthority;if(!e||!t||!i)return;if(!i.authorityLease)return;const r=i.currentConsumerBinding();if(!r)return void this.logger.warn?.("[acp-server] runtime Service delivery rebind skipped — no consumer binding");const n=e.runtimeConsumer;let s;n&&(e.runtimeConsumer=void 0,this.runtimeRpcConsumer===n&&(this.runtimeRpcConsumer=void 0),this.core.setRuntimeReceiptHistoryPort(void 0),await this.closeConsumerOnce(n).catch(()=>{}));try{s=await t.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:r,getBinding:()=>i.currentConsumerBinding()??r,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal})}catch(e){this.logger.error?.(`[acp-server] runtime Service delivery rebind failed: ${e instanceof Error?e.stack??e.message:String(e)}`);try{i.setRuntimeConsumerLive(!1)}catch{}return}if(this.activeGeneration!==e||"started"!==this.lifecyclePhase||e.abortController.signal.aborted)await this.closeConsumerOnce(s).catch(()=>{});else{this.core.setRuntimeReceiptHistoryPort(s.receiptHistory),e.runtimeConsumer=s,this.runtimeRpcConsumer=s,s.start();try{i.setRuntimeConsumerLive(!0)}catch{}this.runtimeServiceFactoryFenced=!1,this.logger.info?.("[acp-server] runtime Service delivery rebound after MessageClient connect")}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,r=e.factory;let n=!1;if(i&&r){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=r.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getBinding:()=>i.currentConsumerBinding()??t.value,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal}),o=await this.waitForGeneration(s,e);if("completed"!==o.status)return void("cancelled"===o.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const a=o.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(a,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(a.receiptHistory),e.runtimeConsumer=a,this.runtimeRpcConsumer=a,a.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),n=!0,this.runtimeServiceFactoryFenced=!1}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&(this.runtimeServiceFactoryFenced=!0,this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced"));if(this.core.startMcp().catch(e=>{this.logger.warn?.(`[acp-server] MCP desired-state reconcile failed: ${e instanceof Error?e.message:String(e)}`)}),!this.shouldContinueStart(e))return;if(i&&n){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const o=this.config.bridge.url,a=this.config.keyFile;if(o&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(!this.shouldContinueStart(e))return;if(!0===this.config.terminalServiceEnabled&&o&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:o,authority:i,keys:i.agentKeys}),r=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void r.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=r,this.terminalOutbound=r;try{const t=await this.waitForGeneration(r.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return;const c="terminal"!==this.config.bridge.mode;if(c&&o&&a&&BridgeClientCtor){Date.now();await this.startBridgeMode(o,a,e)}else c?(this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone()):(this.logger.info?.("[acp-server] terminal-only bridge mode; ACP phone-home not started"),this.core.markAgentSyncDone());this.logger.info?.(`[acp-server] start() total ${Date.now()-s}ms`)}shouldContinueStart(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&"starting"===this.lifecyclePhase}isGenerationActive(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)}setTerminalServiceReady(e,t){if(this.activeGeneration!==e)return;const i=!0===this.config.terminalServiceEnabled&&t;this.terminalServiceReady!==i&&(this.terminalServiceReady=i,this.runtimeAgentCapabilities.terminal=i,this.markAgentProjectionDirty?this.markAgentProjectionDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const r=t.abortController.signal;return r.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,n)=>{let s,o=!1;const a=e=>{o||(o=!0,r.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>a({status:"cancelled"});r.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>a({status:"timed_out"}),i),s.unref?.()),e.then(e=>a({status:"completed",value:e}),e=>{o||(o=!0,r.removeEventListener("abort",c),s&&clearTimeout(s),n(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const r=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,r),r}async startBridgeMode(e,t,i){if("terminal"===this.config.bridge.mode)return void this.logger.info?.("[acp-server] terminal-only bridge mode; ACP phone-home suppressed");if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const r=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(r,!1).catch(()=>{});i.bridge=r,this.bridgeInstance=r;const n=()=>this.bridgeInstance===r;r.on("connected",()=>{n()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),r.on("disconnected",(...e)=>{if(!n())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),o=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${o})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.cloud){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;this.cloud.bridge.config().then(async s=>{if(this.isGenerationActive(i)&&n()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(r,!1),!this.isGenerationActive(i)||this.bridgeInstance!==r)return;i.bridge===r&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),r.on("message",(...e)=>{if(!n())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),r.on("error",(...e)=>{if(!n())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{r.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(r).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(r,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===r.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const o=Date.now();try{const e=await this.waitForGeneration(r.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-o}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-o}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const r=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,r),r}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.cloud)return void this.core.markAgentSyncDone();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return;if(!this.runtimeServiceFactoryFenced)return;0===e&&this.logger.error?.("[acp-server] runtime Service factory fenced with no authority lease — attempting one-shot legacy agent-sync bootstrap to recover Agent Identity")}const t=!!this.agentProjection;if(t||(this.legacyFencedSyncFailureCount=0),this.agentSyncInFlight)return;const i=[1e3,2e3,5e3,1e4],r=3e5,n=()=>{const n=e+1;if(t&&(this.legacyFencedSyncFailureCount+=1,this.legacyFencedSyncFailureCount>=4)){const e=Math.round(5);return this.logger.error?.(`[acp-server] legacy agent-sync bootstrap fallback has failed ${this.legacyFencedSyncFailureCount} consecutive times while the runtime Service factory is fenced — this fallback path is confirmed incompatible with a backend that requires the runtime-projection agent-sync schema and cannot self-heal; restart this instance once Message Service / the runtime Service factory recover. Backing off to a ${e}m retry interval to stop busy-looping a request that cannot succeed.`),void(this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(n)},r))}const s=i[Math.min(e,i.length-1)],o=Math.round(s/1e3),a=`[acp-server] agent sync retry #${n} in ${o}s`;n>0&&n%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${n} attempts — retrying every ${o}s`):n<=i.length?this.logger.info?.(a):this.logger.debug?.(a),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(n)},s)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,cloud:this.cloud,capabilities:{canvas:this.config.canvasEnabled,terminal:this.terminalServiceReady,fileSystem:!0,cron:!0,streaming:!0},deliveryMode:this.config.messageBus.enabled?"push":void 0,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.legacyFencedSyncFailureCount=0,await this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync + session hydration took ${Date.now()-s}ms)`),this.agentProjection&&this.runtimeServiceFactoryFenced&&this.logger.error?.("[acp-server] agent bootstrap sync recovered Agent Identity while the runtime Service factory was fenced — restart this instance to complete registration")):n()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),n()}finally{this.agentSyncInFlight=!1}}dispatchGatewayEvent(e,t,i){try{i()}catch(i){const r=i instanceof Error?i.message:String(i),n=r.startsWith("run output route unavailable:");this.logger.error?.(`[acp-server] Gateway ${e} handler failed${t?` runId=${t}`:""}: ${r}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:n?"run_output.route_missing":"gateway_event.handler_failed",severity:"error",summary:r,payload:{event:e,...t?{runId:t}:{}}})}}bindGateway(e){e.on("chat",e=>{this.dispatchGatewayEvent("chat",e.runId,()=>this.events.handleChatEvent(e))}),e.on("agent",e=>{this.dispatchGatewayEvent("agent",e.runId,()=>this.events.handleAgentEvent(e))}),e.on("cron",e=>{this.dispatchGatewayEvent("cron",void 0,()=>this.events.handleCronEvent(e))}),e.on("sessionMessage",e=>{this.dispatchGatewayEvent("session.message",void 0,()=>this.core.handleSessionMessage(e))}),e.on("response",e=>{this.dispatchGatewayEvent("response",void 0,()=>this.core.handleGatewayResponse(e))});const t=e;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t)}:null}),this.config.openclawCanvasCompat&&(e.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),e.on("connected",()=>{this.core.beginGatewaySessionPreload(),this.logger.info?.("[acp-server] connection transport=openclaw_gateway_ws state=connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.core.prepareWorkspaceCatalog(),this.tryAgentSync()}),e.on("disconnected",()=>{this.core.failGatewaySessionPreload(),this.logger.warn?.("[acp-server] connection transport=openclaw_gateway_ws state=disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),e.on("reconnecting",e=>{this.logger.info?.(`[acp-server] connection transport=openclaw_gateway_ws state=reconnecting attempt=${e.attempt} delay_ms=${Math.round(e.delayMs)}`)}),e.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),e.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),e.on("transport",e=>{const t="connect_fail"===e.event&&!0===e.retryable,i=`[acp-server] connection transport=openclaw_gateway_ws event=${e.event} duration_ms=${e.durationMs??"?"}`;t?this.logger.debug?.(i):this.logger.info?.(i),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:t?"debug":"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),e.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),e.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")})}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",t=>{if(this.client!==e)return;const r="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(r);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,r)=>this.core.dispatchLegacyTerminalCapability(e,t,i,r)});return t.setupListeners(),t}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);if(e)return e}const r=e.split("_");if(r.length>=3&&"tc"===r[0]){const e=r[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.agentMappings.waitForSynchronization()}onceAgentMappingReady(e){this.agentMappings.onceReady(e)}onAgentMappingUpdated(e){const t=this.agentMappings.snapshot();t&&t.size>0&&e(new Map(t)),this.agentMappings.subscribe(t=>e(new Map(t)))}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}resolveLocalAgentIdForPlatformAgent(e){return this.core.resolveLocalAgentId(e)}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.agentMappings.snapshot();if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.agentMappings.snapshot();return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:r})=>{try{const n=`${t}/skill.json`,s=await r(n,"utf-8"),o=JSON.parse(s);if(o.formSchemas?.length){for(const e of o.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${o.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,r;const n=new Promise((e,t)=>{i=e,r=t});t.stopPromise=n,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){r(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),n}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let r,n=!1;const s=new Promise(e=>{r=setTimeout(()=>{n=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),r.unref?.()}),o=async(r,n)=>{let o;const a=new Promise(e=>{o=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:o},()=>n(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),a.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${r}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:r,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},a=[];this.setTerminalServiceReady(e,!1),a.push(o("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),a.push(o("session-state",()=>this.state.destroy())),a.push(o("canvas-orchestrator",()=>this.canvasOrch.destroy())),a.push(o("mcp-receipt-history",()=>this.core.setRuntimeReceiptHistoryPort(void 0)));const c=e.bridge,l=e.bridgeSocket,h=e.terminalOutbound,g=e.runtimeConsumer,d=this.client&&this.client!==l?this.client:null,u=this.wss;if(e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.bridgeInstance===c&&(this.bridgeInstance=null),this.client!==d&&this.client!==l||(this.client=null),this.terminalOutbound===h&&(this.terminalOutbound=null),this.runtimeRpcConsumer===g&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,d&&a.push(o("incoming-client",()=>{d.removeAllListeners(),d.close()})),c&&a.push(o("bridge",async()=>{try{await this.closeBridgeOnce(c)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),h&&a.push(o("terminal-outbound",e=>h.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,a.push(o("runtime-authority",()=>e.authority?.stop()))),g&&a.push(o("runtime-consumer",e=>this.closeConsumerOnce(g,e))),u&&a.push(o("websocket-server",()=>{u.removeAllListeners(),u.close()})),await Promise.all(a),await o("runtime-authority-reset",()=>e.authority?.setRuntimeConsumerLive(!1)),await o("terminal-handler",()=>this.terminalHandler.destroy()),await o("terminal-manager",()=>this.terminalMgr.destroy()),r&&clearTimeout(r),n||i.abort(),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"}),t.length>0)throw t[0]}}
|
package/dist/agent-sync.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const UUID_AGENT_ID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;export function resolveSyncAgentName(t){const e=t.id.trim(),
|
|
1
|
+
import{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";const UUID_AGENT_ID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;export function resolveSyncAgentName(t){const e=t.id.trim(),a=t.name?.trim()??"";return UUID_AGENT_ID_RE.test(e)&&a?a:e||a}export function isAgentSyncWaitForGateway(t){return!(!t||t instanceof Map||"wait_gateway"!==t.status)}export async function syncAgentsToPlatform(t){const{gateway:e,cloud:a,capabilities:n,deliveryMode:s,logger:i}=t;if(!a)return i?.debug?.("[agent-sync] no Cloud gateway client — skipping sync (no key pair or agentGatewayUrl)"),null;try{const t=await e.request("agents.list",{}),r=t?.agents??[];if(0===r.length)return i?.debug?.("[agent-sync] no agents from Gateway — skipping sync"),null;const o=r.map(t=>({agent:t,syncName:resolveSyncAgentName(t)})).filter(t=>t.syncName),c=o.some(t=>!UUID_AGENT_ID_RE.test(t.syncName)),l=[];for(const{agent:t,syncName:a}of o){if(c&&UUID_AGENT_ID_RE.test(a)){i?.debug?.(`[agent-sync] skipping platform-id shadow agent ${a}`);continue}let r=[];try{const a=await e.request("skills.status",{agentId:t.id});r=(a?.skills??[]).filter(t=>t.eligible&&!t.disabled).map(t=>({name:t.name,description:t.description,...t.emoji?{emoji:t.emoji}:{},...t.skillKey?{skillKey:t.skillKey}:{}}))}catch{i?.debug?.(`[agent-sync] skills.status failed for agent ${t.id} — continuing without skills`)}const o={};if(n)for(const[t,e]of Object.entries(n))e&&(o[t]=!0);const d=t.identity?.name||t.name||a;l.push({name:a,displayName:d,description:t.name||t.identity?.name||a,version:"0.1.0",capabilities:o,skills:r,avatarUrl:t.identity?.avatarUrl||"",homepage:"",...s?{deliveryMode:s}:{}})}let d;try{d=await a.agents.sync({agents:l})}catch(t){if(!(t instanceof CloudHttpError))throw t;return i?.warn?.(`[agent-sync] sync failed: HTTP ${t.status} — ${t.body.slice(0,200)}`),null}i?.info?.(`[agent-sync] synced ${l.length} agent(s) — created=${d.created??0} updated=${d.updated??0} deactivated=${d.deactivated??0}`);const y=new Map,g=Array.isArray(d.data)?d.data:[];for(const t of g)t&&"string"==typeof t.name&&"string"==typeof t.id&&y.set(t.name,{id:t.id,instanceId:t.instanceId??"",name:t.name,displayName:t.displayName??t.name,description:t.description??"",version:t.version??"",avatarUrl:t.avatarUrl??"",status:t.status??"active"});return y.size>0?y:null}catch(t){const e=t instanceof Error?t.message:String(t);return i?.warn?.(`[agent-sync] sync error: ${e}`),"Not connected to Gateway"===e?{status:"wait_gateway",reason:e}:null}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash,randomUUID}from"node:crypto";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";import{APPROVAL_PROTOCOL,parseApprovalUserAction}from"./protocol.generated.js";export function approvalOperationHash(e,t){const s=canonicalizeJcs({toolName:e,params:t});if(s.length>1048576)throw new Error("approval_operation_too_large");return createHash("sha256").update(s).digest("hex")}export class ApprovalGate{journal;publish;timeoutMs;now;pending=new Map;tasks=new Set;issued=new Map;constructor(e,t,s=3e5,r=Date.now){if(this.journal=e,this.publish=t,this.timeoutMs=s,this.now=r,!Number.isFinite(s)||s<=0||s>3e5)throw new Error("invalid_approval_timeout")}async request(e,t,s,r,o){if(!e.active()||!e.ownerIdentity.startsWith("user:")||e.ownerIdentity.length<=5||!e.sessionId||!e.taskId||!/^[1-9]\d*$/u.test(e.runtimeEpoch)||o?.aborted)return;const i=`${e.sessionId}\0${e.taskId}`;if(this.tasks.has(i))return;const n=`apr_${randomUUID()}`,a={protocol:APPROVAL_PROTOCOL,kind:"approval_request",requestId:n,requestRevision:1,sessionId:e.sessionId,taskId:e.taskId,trigger:{type:"policy_required",reasonCode:"SESSION_HUMAN_APPROVAL_REQUIRED"},operation:{type:"tool.invoke",summary:`Run ${t}`,parameters:r},availableActions:["approve_once","deny","cancel"],createdAt:new Date(this.now()).toISOString(),expiresAt:new Date(this.now()+this.timeoutMs).toISOString()},d={request:a,ownerIdentity:e.ownerIdentity,runtimeEpoch:e.runtimeEpoch,platformAgentId:e.platformAgentId,operationHash:approvalOperationHash(t,s)};this.journal.write(d);const c=this.issued.get(i)??new Set;let l;c.add(n),this.issued.set(i,c);const u=new Promise(e=>{l=e}),p={record:d,context:e,release:l};this.pending.set(n,p),this.tasks.add(i);const h=()=>{this.settleSystem(p,"task_cancelled","TASK_CANCELLED").catch(()=>l(!1))};o?.addEventListener("abort",h,{once:!0});const I=setTimeout(()=>{this.settleSystem(p,"expired","APPROVAL_EXPIRED").catch(()=>l(!1)),l(!1)},this.timeoutMs);try{await Promise.race([this.publish("agent.input_required",`approval_request:${n}`,e.sessionId,a),u.then(e=>{if(!e)throw new Error("approval_wait_closed")})]),e.active()&&!o?.aborted||h();return await u&&e.active()&&!o?.aborted&&this.now()<Date.parse(a.expiresAt)?n:void 0}catch{return void this.settleSystem(p,"withdrawn","APPROVAL_DELIVERY_FAILED").catch(()=>{})}finally{clearTimeout(I),o?.removeEventListener("abort",h),this.pending.delete(n),this.tasks.delete(i)}}async resolve(e,t,s){const r=parseApprovalUserAction(e);if(!r||"approval_decision"!==r.kind)throw new Error("INVALID_APPROVAL_ACTION");const o=this.journal.read(r.requestId);if(!o)throw new Error("APPROVAL_NOT_FOUND");const i=o.request;if(o.ownerIdentity!==t||i.sessionId!==s||r.sessionId!==s||r.taskId!==i.taskId||r.requestRevision!==i.requestRevision)throw new Error("APPROVAL_REQUEST_MISMATCH");if(o.result){if(o.result.acceptedActionId===r.actionId&&canonicalizeJcs(o.acceptedAction)!==canonicalizeJcs(r))throw new Error("APPROVAL_ACTION_CONFLICT");return await this.publishResult(o),structuredClone(o.result)}const n=this.pending.get(r.requestId);if(!n||!n.context.active())throw new Error("APPROVAL_UNAVAILABLE");if(this.now()>=Date.parse(i.expiresAt))throw await this.settleSystem(n,"expired","APPROVAL_EXPIRED"),new Error("APPROVAL_EXPIRED");const a="approve_once"===r.decision?"approved":"deny"===r.decision?"denied":"cancelled";return o.acceptedAction=r,o.result={protocol:APPROVAL_PROTOCOL,kind:"approval_resolved",requestId:i.requestId,sessionId:s,taskId:i.taskId,resolution:{status:a,decision:r.decision,responseAction:r.decision,source:"user",reasonCode:`USER_${r.decision.toUpperCase()}`},acceptedActionId:r.actionId,resolvedAt:new Date(this.now()).toISOString()},this.journal.write(o),n.record=o,await this.publishResult(o),structuredClone(o.result)}consume(e,t,s,r){const o=this.journal.read(e);return!(!o||o.consumed||o.invalidated||"approved"!==o.result?.resolution.status||!t.active()||o.request.sessionId!==t.sessionId||o.request.taskId!==t.taskId||o.ownerIdentity!==t.ownerIdentity||o.runtimeEpoch!==t.runtimeEpoch||o.platformAgentId!==t.platformAgentId||this.now()>=Date.parse(o.request.expiresAt)||o.operationHash!==approvalOperationHash(s,r))&&(o.consumed=!0,this.journal.write(o),!0)}cancelTask(e,t){const s=`${e}\0${t}`;for(const e of this.issued.get(s)??[]){const t=this.journal.read(e);t&&!t.consumed&&(t.invalidated=!0,this.journal.write(t))}this.issued.delete(s);for(const s of this.pending.values())s.record.request.sessionId===e&&s.record.request.taskId===t&&this.settleSystem(s,"task_cancelled","TASK_CANCELLED").catch(()=>s.release(!1))}async settleSystem(e,t,s){if(e.record.result)return void e.release(!1);const r=e.record.request;e.record.result={protocol:APPROVAL_PROTOCOL,kind:"approval_resolved",requestId:r.requestId,sessionId:r.sessionId,taskId:r.taskId,resolution:{status:t,source:"system",reasonCode:s},resolvedAt:new Date(this.now()).toISOString()},this.journal.write(e.record),e.release(!1);try{await this.publishResult(e.record)}finally{e.release(!1)}}async publishResult(e){let t;try{await Promise.race([this.publish("approval.resolved",`approval_resolved:${e.request.requestId}`,e.request.sessionId,e.result),new Promise((e,s)=>{t=setTimeout(()=>s(new Error("approval_publication_timeout")),Math.min(this.timeoutMs,3e4))})])}finally{t&&clearTimeout(t)}this.pending.get(e.request.requestId)?.release("approved"===e.result.resolution.status)}}
|