feat(cloud-workers): add desktop apps and browser autonomy (#121475)

* feat(cloud-workers): add desktop apps and browser autonomy

provider-attested Browser/Terminal launchers, shared visible loopback CDP Browser tool, no MCP/cookies/generic command.

* feat(ui): add cloud desktop app launcher

* docs(gateway): document cloud desktop apps and browser autonomy

* perf(ui): trim desktop launcher startup copy

* refactor(ui): simplify desktop launch feedback

* perf(ui): reuse desktop app labels

* fix(ui): keep desktop launch failures actionable

* fix(crabbox): allow browser bootstrap to finish

* fix(cloud-workers): honor provider provision budgets

* fix(cloud-workers): persist browser screenshot receipts

* fix(cloud-workers): bound browser screenshot lifecycle

* fix(cloud-workers): avoid replaying desktop launches

* test(cloud-workers): isolate browser runtime integration

* refactor(cloud-workers): tighten desktop runtime boundaries

* test(cloud-workers): keep browser runtime mock synchronous

* fix(cloud-workers): break gateway type import cycle

* fix(ci): settle admitted setup sessions in tests

* build(plugin-sdk): refresh desktop app contract

* ci: refresh merge-tree validation

* build(plugin-sdk): regenerate desktop app baseline

* style(gateway): format merged method order test
This commit is contained in:
Peter Steinberger
2026-08-10 20:31:07 -07:00
committed by GitHub
parent cc9e75efb2
commit b5d5ec340f
65 changed files with 2985 additions and 321 deletions
@@ -154,6 +154,18 @@ data class WorkerDesktopObserveResult(
val vncPassword: String? = null,
)
@Serializable
data class WorkerDesktopLaunchParams(
val environmentId: String,
val app: String,
)
@Serializable
data class WorkerDesktopLaunchResult(
val app: String,
val status: String = "ready",
)
@Serializable
data class GatewayEventFrameStateVersion(
val presence: Long,
@@ -521,6 +533,7 @@ enum class GatewayMethod(
ProjectsList("projects.list"),
ProjectsRegister("projects.register"),
ProjectsRemove("projects.remove"),
WorkerDesktopLaunch("worker.desktop.launch"),
}
enum class GatewayEvent(
@@ -71,6 +71,11 @@ public enum WorkerTunnelStatus: String, Codable, Sendable {
case reconnecting = "reconnecting"
}
public enum WorkerDesktopAppId: String, Codable, Sendable {
case browser = "browser"
case terminal = "terminal"
}
public enum WorktreeRepositoryStatus: String, Codable, Sendable {
case git = "git"
case notGit = "not_git"
@@ -1804,6 +1809,7 @@ public struct WorkerEnvironmentMetadata: Codable, Sendable {
public let tunnelstatus: WorkerTunnelStatus
public let error: String?
public let desktop: Bool?
public let desktopapps: [WorkerDesktopAppId]?
public init(
providerid: String,
@@ -1814,7 +1820,8 @@ public struct WorkerEnvironmentMetadata: Codable, Sendable {
attachedsessionids: [String],
tunnelstatus: WorkerTunnelStatus,
error: String? = nil,
desktop: Bool? = nil)
desktop: Bool? = nil,
desktopapps: [WorkerDesktopAppId]? = nil)
{
self.providerid = providerid
self.leaseid = leaseid
@@ -1825,6 +1832,7 @@ public struct WorkerEnvironmentMetadata: Codable, Sendable {
self.tunnelstatus = tunnelstatus
self.error = error
self.desktop = desktop
self.desktopapps = desktopapps
}
private enum CodingKeys: String, CodingKey {
@@ -1837,6 +1845,7 @@ public struct WorkerEnvironmentMetadata: Codable, Sendable {
case tunnelstatus = "tunnelStatus"
case error
case desktop
case desktopapps = "desktopApps"
}
}
@@ -2094,6 +2103,42 @@ public struct WorkerDesktopObserveResult: Codable, Sendable {
}
}
public struct WorkerDesktopLaunchParams: Codable, Sendable {
public let environmentid: String
public let app: WorkerDesktopAppId
public init(
environmentid: String,
app: WorkerDesktopAppId)
{
self.environmentid = environmentid
self.app = app
}
private enum CodingKeys: String, CodingKey {
case environmentid = "environmentId"
case app
}
}
public struct WorkerDesktopLaunchResult: Codable, Sendable {
public let app: WorkerDesktopAppId
public let status: String
public init(
app: WorkerDesktopAppId,
status: String)
{
self.app = app
self.status = status
}
private enum CodingKeys: String, CodingKey {
case app
case status
}
}
public struct SystemInfoParams: Codable, Sendable {}
public struct SystemInfoResult: Codable, Sendable {
+63 -62
View File
@@ -53,15 +53,15 @@
{"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise<AbortAndDrainEmbeddedAgentRunResult>;","entrypoint":"agent-harness","exportName":"abortAndDrainAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise<OpenClawAgentToolResult>; };","entrypoint":"agent-harness","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise<AgentToolResult<unknown>>; };","entrypoint":"agent-harness","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"f74526ab6cac093e97d8e1ac7a9c8a4235a60ff02b70099dce30cb3bbdd61d25","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"4be004ef77c8714c17881d8b9b2908a264f33c237ff2b9f4bf3de3e780f3b9e4","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise<void>;","entrypoint":"agent-harness","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"f255a91162ece4c239bb8fa745efa14a1054bc024a291e9954f75f239b2b87c3","declaration":"export function resolveActiveEmbeddedRunSessionId(sessionKey: string): string | undefined;","entrypoint":"agent-harness","exportName":"resolveActiveEmbeddedRunSessionId","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"27d9f61df9ccf68615cb2002a6da891e36ab0c07623bc37c4d07aa980b446881","declaration":"export function resolveWebSearchToolPolicy(params: WebSearchToolPolicyParams): WebSearchToolPolicyResolution;","entrypoint":"agent-harness","exportName":"resolveWebSearchToolPolicy","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"b110825d1e9bc1970ced28938bde4d86ddc187bf74e118fac33d8df9e354f429","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"4ad0f830bf11a2db48faefb1439b7fbb08d9f22d22083f5e301327e1696f2310","declaration":"export type AgentToolResultMiddleware = AgentToolResultMiddleware;","entrypoint":"agent-harness","exportName":"AgentToolResultMiddleware","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"008b6c78d5e44a9d1df1d7e32e6015382ee9ba1cb9b10bd4c2b467619d1c89d4","declaration":"export type AgentToolResultMiddlewareEvent = AgentToolResultMiddlewareEvent;","entrypoint":"agent-harness","exportName":"AgentToolResultMiddlewareEvent","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"7414a2a79c2d78c1d52538250c827305bba1a2bb25010448837c8524a9f95b3c","declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"agent-harness","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"1b8fadbce01f681a395e1c1a8d3ab9416a4bd4c212a60241f4346b1cf4e0b7e1","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"9ddb13a641d94f769ac817c54b6015f9d1eb70f4db410e0dba736d6e0d8d5de4","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"25c6985c3bef2c6efc6a96e70af6d66fe073be0b0d311681cfc0dfc186528d9a","declaration":"export type OpenClawAgentToolResult<TResult = unknown> = OpenClawAgentToolResult<TResult>;","entrypoint":"agent-harness","exportName":"OpenClawAgentToolResult","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"category":"runtime","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","recordType":"module"}
{"closureHash":"fab99dfcb01d475a1bfdbe7a8fac9be10a93dac5f12ac78c2dea673c8ab86abf","declaration":"export function abortAgentHarnessRun(sessionId: string): boolean;\nexport function abortAgentHarnessRun(sessionId: undefined, opts: { mode: \"all\" | \"compacting\"; reason?: \"restart\"; }): boolean;","entrypoint":"agent-harness-runtime","exportName":"abortAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
@@ -80,8 +80,8 @@
{"closureHash":"f8ad3d762c9753fdc36b2b9af0980167fdc54090c7bcca2f3dbe7c8951e86821","declaration":"export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): AgentRuntimePlan;","entrypoint":"agent-harness-runtime","exportName":"buildAgentRuntimePlan","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"528d11abf061b0b303722737de5a6406e7e55bffa38c45144c77d896ae1b3b0a","declaration":"export function buildBootstrapContextForFiles(bootstrapFiles: WorkspaceBootstrapFile[], params: { config?: OpenClawConfig; agentId?: string | null; warn?: (message: string) => void; }): EmbeddedContextFile[];","entrypoint":"agent-harness-runtime","exportName":"buildBootstrapContextForFiles","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"44d5a4667d2f3dc3a28e5c9d7b46765b1fb3c62820288e40251f5174911c8a6a","declaration":"export function buildEmbeddedAttemptToolRunContext(params: { trigger?: EmbeddedRunTrigger; jobId?: string; memoryFlushWritePath?: string; toolsAllow?: string[]; conversationToolPolicy?: GroupToolPolicyConfig; trace?: DiagnosticTraceContext; }): { trigger?: EmbeddedRunTrigger; jobId?: string; memoryFlushWritePath?: string; runtimeToolAllowlist?: string[]; conversationToolPolicy?: GroupToolPolicyConfig; trace?: DiagnosticTraceContext; };","entrypoint":"agent-harness-runtime","exportName":"buildEmbeddedAttemptToolRunContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"8b961c250987af36d795b20b160b532d69a77e95db8b4e8e0c4f5584f9dd4e86","declaration":"export function buildHarnessContextEngineRuntimeContext(params: Parameters<typeof buildAfterTurnRuntimeContext>[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"bad017021139532a5fa0fa8d9b4b15c1475bfb3ed23436a55aa40871bcf12579","declaration":"export function buildHarnessContextEngineRuntimeContextFromUsage(params: Parameters<typeof buildAfterTurnRuntimeContextFromUsage>[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContextFromUsage","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"70cc5b9cb4b5e8d7cbc03edeebc97c50ca40aa4e2178389e904d297263fab04a","declaration":"export function buildHarnessContextEngineRuntimeContext(params: Parameters<typeof buildAfterTurnRuntimeContext>[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"f1683ff9281456f3c6d34046f25e50eb2e1280b014849085ea2feafecc17b9a0","declaration":"export function buildHarnessContextEngineRuntimeContextFromUsage(params: Parameters<typeof buildAfterTurnRuntimeContextFromUsage>[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContextFromUsage","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"33a7cd8d63d4ef045ee3a42f806026a8b621e7dd32d8d6fbf4e7b44452d5e0ce","declaration":"export function buildNativeHookRelayCommand(params: { provider: NativeHookRelayProvider; relayId: string; generation?: string; event: NativeHookRelayEvent; preToolUseUnavailable?: \"noop\"; timeoutMs?: number; executable?: string; nice?: number | false; nodeExecutable?: string; }): string;","entrypoint":"agent-harness-runtime","exportName":"buildNativeHookRelayCommand","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"3846647c3c2fb0242f70591cf95c2e02f2a5a3c44447efb4cf7a31b2dc2122fd","declaration":"export function buildSkillWorkshopPromptSection(): string[];","entrypoint":"agent-harness-runtime","exportName":"buildSkillWorkshopPromptSection","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"32e977ece65144a83ae1b0a3c8d1b9b00a2a2506ad9ef1db3b2b9770ce762dbe","declaration":"export function buildWatchedSessionsHarnessContext(params: { config?: OpenClawConfig; sessionKey?: string; sandboxed?: boolean; toolNames: Iterable<string>; capabilityToolNames?: Iterable<string>; }): string | undefined;","entrypoint":"agent-harness-runtime","exportName":"buildWatchedSessionsHarnessContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
@@ -98,7 +98,7 @@
{"closureHash":"51f76e9e37b65b358f0313e10152d950b003bc3e50f4bb26fe29a988237772e2","declaration":"export function consumePreExecutionBlockedToolCall(toolCallId: string, runId?: string): boolean;","entrypoint":"agent-harness-runtime","exportName":"consumePreExecutionBlockedToolCall","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise<OpenClawAgentToolResult>; };","entrypoint":"agent-harness-runtime","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise<AgentToolResult<unknown>>; };","entrypoint":"agent-harness-runtime","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"833c4cb48c22eda95bb09d894910cdfbc93782287e548a42e170892bd4a2fa54","declaration":"export function deliverAgentHarnessUserInputPrompt(params: PromptDeliveryParams, questions: readonly AgentHarnessUserInputQuestion[], options?: AgentHarnessUserInputPromptOptions): Promise<void>;","entrypoint":"agent-harness-runtime","exportName":"deliverAgentHarnessUserInputPrompt","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"c4258e2f5cefcbb0c6d4427d94d4148b0e39e7991f9acaebe57b5446322fd763","declaration":"export function deliverAgentHarnessUserInputPrompt(params: PromptDeliveryParams, questions: readonly AgentHarnessUserInputQuestion[], options?: AgentHarnessUserInputPromptOptions): Promise<void>;","entrypoint":"agent-harness-runtime","exportName":"deliverAgentHarnessUserInputPrompt","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"6f436485257f69fa07a18203b91d4341690522f12aff4f25805c7a1e21326ee3","declaration":"export function detectAndLoadAgentHarnessPromptImages(params: { prompt: string; workspaceDir: string; model: { input?: string[]; }; existingImages?: ImageContent[]; imageOrder?: PromptImageOrderEntry[]; media?: MediaFact[]; config?: OpenClawConfig; workspaceOnly?: boolean; localRoots?: readonly string[]; sandbox?: { root: string; bridge: SandboxFsBridge; }; }): Promise<{ images: ImageContent[]; detectedRefs: Array<{ raw: string; resolved: string; type: \"path\" | \"media-uri\"; }>; loadedCount: number; skippedCount: number; }>;","entrypoint":"agent-harness-runtime","exportName":"detectAndLoadAgentHarnessPromptImages","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise<void>;","entrypoint":"agent-harness-runtime","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"c38cd390190bb07ddcd247420d6e0b489a0229bd402ddca0440417217a57251a","declaration":"export function emitAgentEvent(event: Omit<AgentEventPayload, \"seq\" | \"ts\">): void;","entrypoint":"agent-harness-runtime","exportName":"emitAgentEvent","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
@@ -195,7 +195,7 @@
{"closureHash":"e4784aeda4c24b60cd49b7f5983e70a380a4759d5176c74d30f2900952ba848f","declaration":"export function runAgentHarnessBeforeAgentFinalizeHook(params: { event: PluginHookBeforeAgentFinalizeEvent; ctx: AgentHarnessHookContext; hookRunner?: AgentHarnessHookRunner; }): Promise<AgentHarnessBeforeAgentFinalizeOutcome>;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessBeforeAgentFinalizeHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"8fec73ee8c7c9be3335bd6dc1bac9278f0be5beef22a5975eb06e56f504f4bd6","declaration":"export function runAgentHarnessBeforeCompactionHook(params: { sessionFile: string; messages?: AgentMessage[]; ctx: AgentHarnessHookContext; }): Promise<void>;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessBeforeCompactionHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"021b81820bef2e8d54c50a5fc159d6665b23b50a1f5c40dae76a4833839d89c9","declaration":"export function runAgentHarnessBeforeMessageWriteHook(params: { message: AgentMessage; agentId?: string; sessionKey?: string; }): AgentMessage | null;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessBeforeMessageWriteHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"77c7c0c1adee10a2ead7993b809294430fbcfae4c63366cdf54b267e925aeec7","declaration":"export function runAgentHarnessGatewayQuestion(params: RunAgentHarnessGatewayQuestionParams): Promise<QuestionWaitAnswerResult>;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessGatewayQuestion","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"6ce99f97d66f45d682061278164b104afba2054501817fc73523bf7beaeb96d0","declaration":"export function runAgentHarnessGatewayQuestion(params: RunAgentHarnessGatewayQuestionParams): Promise<QuestionWaitAnswerResult>;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessGatewayQuestion","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"50a4d7cb20dd8f884abbcb62d77b5b2df183fbfcd058cc7aa76edacd1f7b90d8","declaration":"export function runAgentHarnessLlmInputHook(params: { event: PluginHookLlmInputEvent; ctx: AgentHarnessHookContext; hookRunner?: AgentHarnessHookRunner; }): void;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessLlmInputHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"1bd55c141c249b1c04470cafe0cd4d41dce334ea2883ce83235af56d24a15cca","declaration":"export function runAgentHarnessLlmOutputHook(params: { event: PluginHookLlmOutputEvent; ctx: AgentHarnessHookContext; hookRunner?: AgentHarnessHookRunner; }): void;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessLlmOutputHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"c4c5131ab7065decd8d8e935ac3779e3baa2c52fbbbd95a6ccb624377db09ca1","declaration":"export function runBeforeToolCallHook(args: { toolName: string; params: unknown; toolKind?: PluginHookToolKind; toolInputKind?: PluginHookToolInputKind; toolCallId?: string; ctx?: HookContext; signal?: AbortSignal; approvalMode?: \"request\" | \"report\" | \"deny\" | \"defer\"; }): Promise<HookOutcome>;","entrypoint":"agent-harness-runtime","exportName":"runBeforeToolCallHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
@@ -221,8 +221,8 @@
{"closureHash":"f237bbc11e1d389679cd4e2ad7aa9055951b3029348e3374b81f769786a51d55","declaration":"export type AbortAndDrainAgentHarnessRunResult = AbortAndDrainEmbeddedAgentRunResult;","entrypoint":"agent-harness-runtime","exportName":"AbortAndDrainAgentHarnessRunResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"6f802f46717d28d260bbcb2cf4790dfed4e9ab28c4fe822d259228189cca4665","declaration":"export type AgentApprovalEventData = AgentApprovalEventData;","entrypoint":"agent-harness-runtime","exportName":"AgentApprovalEventData","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"12aac3a57ce200b0f082002be237625729f84ed7c337f1bac90bcd0688a53319","declaration":"export type AgentEventPayload = AgentEventPayload;","entrypoint":"agent-harness-runtime","exportName":"AgentEventPayload","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness-runtime","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarnessAttemptParams = AgentHarnessAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"b110825d1e9bc1970ced28938bde4d86ddc187bf74e118fac33d8df9e354f429","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness-runtime","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"b110825d1e9bc1970ced28938bde4d86ddc187bf74e118fac33d8df9e354f429","declaration":"export type AgentHarnessAttemptParams = AgentHarnessAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"2dc2a62deea63b316142cc28d40afb88fb0d1e5ce3e048f141420726cda38248","declaration":"export type AgentHarnessAttemptResult = AgentHarnessAttemptResult;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"c7e642234081160830fd9e18a301900f8ee1ea8c88382e5a286f0102de65b714","declaration":"export type AgentHarnessAuthBindingFingerprintParams = AgentHarnessAuthBindingFingerprintParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAuthBindingFingerprintParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"8f496ce75f294f2aa440a49c3c5b065ddff3a4714df775116af5e53f7fd14fe5","declaration":"export type AgentHarnessCompactParams = CompactEmbeddedAgentSessionParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessCompactParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
@@ -271,7 +271,7 @@
{"closureHash":"f30b66fd82f4cd61c60880c42a3b8f152ca64a2216036c99ab4129648ed329e7","declaration":"export type EmbeddedAgentCompactResult = EmbeddedAgentCompactResult;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedAgentCompactResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"1bbefad0193436f7340f9d0dec50be5d482973325c3c65170b1053dd05566f0a","declaration":"export type EmbeddedContextFile = EmbeddedContextFile;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedContextFile","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"f30b66fd82f4cd61c60880c42a3b8f152ca64a2216036c99ab4129648ed329e7","declaration":"export type EmbeddedPiCompactResult = EmbeddedAgentCompactResult;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedPiCompactResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"1b8fadbce01f681a395e1c1a8d3ab9416a4bd4c212a60241f4346b1cf4e0b7e1","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"9ddb13a641d94f769ac817c54b6015f9d1eb70f4db410e0dba736d6e0d8d5de4","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"a6cd2eebe0d06e6578ec8438a207c5acda10ddf01041102d84e4c576a0b6a42f","declaration":"export type EmbeddedRunAttemptResult = EmbeddedRunAttemptResult;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"f6cb2d6411db22cb04220777720d97bcca977351de1cb344ce2ff6ab126a3a08","declaration":"export type ExecApprovalDecision = ExecApprovalDecision;","entrypoint":"agent-harness-runtime","exportName":"ExecApprovalDecision","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"7acb0dffc7bcda6c727e4afc49c955fa8e14f4f8b7b45d197ed794dfa0d73b8e","declaration":"export type ExecAutoReviewDecision = ExecAutoReviewDecision;","entrypoint":"agent-harness-runtime","exportName":"ExecAutoReviewDecision","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
@@ -756,7 +756,7 @@
{"closureHash":"9fc7e45343d7bfbc6ed56d05c9f824e2f9cf6dafc3ca4b25437d1f65c2200727","declaration":"export function clearAccountEntryFields<TAccountEntry extends object>(params: { accounts?: Record<string, TAccountEntry>; accountId: string; fields: string[]; isValueSet?: (value: unknown) => boolean; markClearedOnFieldPresence?: boolean; }): { nextAccounts?: Record<string, TAccountEntry>; changed: boolean; cleared: boolean; };","entrypoint":"channel-core","exportName":"clearAccountEntryFields","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"f4e084b64b5376268885a73fe507aa5d72bee17a88ec2d7ae1d19b36ea5f1ccb","declaration":"export function createChannelConfigUiHints(params: { channelLabel: string; dmPolicy?: { channelKey: string; includeLegacyNestedPolicy?: boolean; legacyNestedPolicyOrder?: \"before\" | \"after\"; }; configWrites?: boolean; mentionPatterns?: { targetDescription: string; policyTargetDescription?: string; policyNote?: string; denyNote?: string; }; nativeCommands?: boolean; implicitMentions?: boolean; progress?: { includeCommentary?: boolean; commentaryOrder?: \"before-command\" | \"after-command\"; labels?: \"openclaw\"; titleWording?: boolean; }; streaming?: Partial<Record<StreamingHintKey, StreamingHintValue>>; retry?: boolean; }): HintMap;","entrypoint":"channel-core","exportName":"createChannelConfigUiHints","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"b8e27bea2add3eaafac7074cbd44fcf2acea30a1ac125c3551925e8cd0b3378d","declaration":"export function createChatChannelPlugin<TResolvedAccount extends { accountId?: string | null; }, Probe = unknown, Audit = unknown>(params: { base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>; security?: ChannelSecurityAdapter<TResolvedAccount> | ChatChannelSecurityOptions<TResolvedAccount>; pairing?: ChannelPairingAdapter | ChatChannelPairingOptions; threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>; outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions; }): ChannelPlugin<TResolvedAccount, Probe, Audit>;","entrypoint":"channel-core","exportName":"createChatChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"d0215218f4367a9b95487036431f4fc5453ebb39960ec6deccde5acb1cab032c","declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","entrypoint":"channel-core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"7fe850af19d5517d9e79384f5fff88a3dd87ea6b1590fcb5bd5399411ad5c8ba","declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","entrypoint":"channel-core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"0e0a9f6b22c433bdb1165e7707b1f9a1ddac1a544a8add00b503048cf157921f","declaration":"export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin): { plugin: TPlugin; };","entrypoint":"channel-core","exportName":"defineSetupPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"8895f7af11b994dcd383ebc45d8cfb3c3b1e6d88a64a001b04f604a83f77e517","declaration":"export function parseOptionalDelimitedEntries(value?: string): string[] | undefined;","entrypoint":"channel-core","exportName":"parseOptionalDelimitedEntries","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"3f97534b10d08eda6a556044eaba623b3744b200a2033877e48d2f2ca924d298","declaration":"export function recoverCurrentThreadSessionId(params: { route: ChannelOutboundSessionRoute; currentSessionKey?: string | null; canRecover?: (context: ThreadAwareOutboundSessionRouteRecoveryContext) => boolean; }): string | undefined;","entrypoint":"channel-core","exportName":"recoverCurrentThreadSessionId","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
@@ -768,24 +768,24 @@
{"closureHash":"8126b651a679d5435eaa5d346916792c3600e5f0f75d8492b077dd361854f7dd","declaration":"export type ChannelOutboundSessionRouteParams = { cfg: OpenClawConfig; agentId: string; accountId?: string | null; target: string; currentSessionKey?: string; resolvedTarget?: { to: string; kind: ChannelDirectoryEntryKind | \"channel\"; display?: string; source: \"normalized\" | \"directory\"; }; replyToId?: string | null; threadId?: string | number | null;};","entrypoint":"channel-core","exportName":"ChannelOutboundSessionRouteParams","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"18fcab0d5899575a31205732d2be92597e533ad944c7fe11cac2af6fc718ea34","declaration":"export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-core","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"channel-core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"0db949d365e8e22bcb88c024aac11211d917c5e28cb2daa36345ea62d2ad90ca","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"4bf9c20e2a621ecc84998961937887666d84884ef6f8f1993a34c87c11000b60","declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"channel-core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"channel-core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"category":"channel","entrypoint":"channel-dm-policy","importSpecifier":"openclaw/plugin-sdk/channel-dm-policy","recordType":"module"}
{"closureHash":"821fe6ccd0d9323322bf70c4d1ca5697071a0e32032f44c1e14157162c2c729d","declaration":"export function createChannelDmPolicy<TConfig extends DmPolicyAccountConfig>(params: CreateChannelDmPolicyParams<TConfig>): ChannelSetupDmPolicy & { promptAllowFrom: NonNullable<ChannelSetupDmPolicy[\"promptAllowFrom\"]>; };","entrypoint":"channel-dm-policy","exportName":"createChannelDmPolicy","importSpecifier":"openclaw/plugin-sdk/channel-dm-policy","kind":"function","recordType":"export"}
{"category":null,"entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","recordType":"module"}
{"closureHash":"d53f1d525e8942681d9d38c92211fabd88c571b284998e667e1920cfbc4991f5","declaration":"export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({ id, name, description, importMetaUrl, plugin, outbound, secrets, configSchema, runtime, accountInspect, features, registerCliMetadata, registerFull, registerCapabilities, }: DefineBundledChannelEntryOptions<TPlugin>): BundledChannelEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"2224da993e2c72c18cf17c5f2fa43ca5e95a7ab823c453dd51d07bcd07ff92c4","declaration":"export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({ importMetaUrl, plugin, secrets, runtime, legacyStateMigrations, legacySessionSurface, registerSetupRuntime, features, }: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelSetupEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"db9fe3f5b73c5da0477bf096520ed4aafde2d4555f02ece0156769af16b3bad2","declaration":"export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({ id, name, description, importMetaUrl, plugin, outbound, secrets, configSchema, runtime, accountInspect, features, registerCliMetadata, registerFull, registerCapabilities, }: DefineBundledChannelEntryOptions<TPlugin>): BundledChannelEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"6f37f3144ca08a7cef4ea74fb9cebac4b078c9800e5f714d1d18fd2c0c78af6a","declaration":"export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({ importMetaUrl, plugin, secrets, runtime, legacyStateMigrations, legacySessionSurface, registerSetupRuntime, features, }: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelSetupEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"cd150287d7e286e7a0d7d6caeb05d0a1efa63b5f351f75f69ba932510a108afd","declaration":"export function loadBundledEntryExportSync<T>(importMetaUrl: string, reference: BundledEntryModuleRef, options?: BundledEntryModuleLoadOptions): T;","entrypoint":"channel-entry-contract","exportName":"loadBundledEntryExportSync","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"7781a3d570d08929af5b3b6cffbb9ef4b9c8e8f1bed55234e0015a681868c812","declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"channel-entry-contract","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"e52edea0d5c41728b7e7703483f93656ae6d55bd09cff9ca481ffad8d9058666","declaration":"export type BundledChannelEntryContract<TPlugin = ChannelPlugin> = BundledChannelEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"BundledChannelEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"47781210ecb5586a425ea14225e8a555b734d44a47a7be83d6724da3b50c4c28","declaration":"export type BundledChannelEntryContract<TPlugin = ChannelPlugin> = BundledChannelEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"BundledChannelEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"f4b82dfd995bd6f08b232984743389d1c854866cb845a8dbb3605acb28818688","declaration":"export type BundledChannelEntryFeatures = BundledChannelEntryFeatures;","entrypoint":"channel-entry-contract","exportName":"BundledChannelEntryFeatures","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"960693591b9bdc974bf3f0097076d90490078db3dceb35349a4f851736cde4fc","declaration":"export type BundledChannelLegacySessionSurface = BundledChannelLegacySessionSurface;","entrypoint":"channel-entry-contract","exportName":"BundledChannelLegacySessionSurface","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"edf79e6a736e5c4977e3b86ef21e7fdf0c0b2bc7deacd8edbee2ca76cb8cef43","declaration":"export type BundledChannelLegacyStateMigrationDetector = BundledChannelLegacyStateMigrationDetector;","entrypoint":"channel-entry-contract","exportName":"BundledChannelLegacyStateMigrationDetector","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"4ae358d564c9980c4bee18ce79fb5807dfd0de0b75ec18586b43b469227c7377","declaration":"export type BundledChannelSetupEntryContract<TPlugin = ChannelPlugin> = BundledChannelSetupEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"BundledChannelSetupEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"8b6d91fbcdfbf25e6cd7ab229293b9de66ee79a793627e4d60efa2bccc53d6e0","declaration":"export type BundledChannelSetupEntryContract<TPlugin = ChannelPlugin> = BundledChannelSetupEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"BundledChannelSetupEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"24679ecdf2ff34157b6345a37b58ec1363ec8cb1969b522599d0d92bb7a113b7","declaration":"export type BundledChannelSetupEntryFeatures = BundledChannelSetupEntryFeatures;","entrypoint":"channel-entry-contract","exportName":"BundledChannelSetupEntryFeatures","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"367219c821915e5be4d1ba3a86754475a5f8a4194512f0401de3be11cc649c60","declaration":"export type BundledEntryModuleLoadOptions = BundledEntryModuleLoadOptions;","entrypoint":"channel-entry-contract","exportName":"BundledEntryModuleLoadOptions","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"68af6ae4d6f23d25812c404922603aeed7504041683a14ce966ed6adad5046d4","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"edbd7bdd5bc547aef378bb9a8ed161b7d97f66023eb53003165c350cae9ef787","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"f22bde542d2dc632b20569c7679ee30a4e88851e0e9dec89350e120f6f456b15","declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"a0103d278095a8687e571bcc0917c5c87fc1a1c79f151a07c7225c4d8f54a39d","declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"channel-entry-contract","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"category":null,"entrypoint":"channel-feedback","importSpecifier":"openclaw/plugin-sdk/channel-feedback","recordType":"module"}
@@ -1097,7 +1097,7 @@
{"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-message","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"22d7518c8c7deab35401795873e9e7c4d4a4e05544a2e9ee7b596bac37c60360","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"e07a5d0cdf6a8cb32e103b64add889e858cfcfbd37d5c4252e54245877bd5a69","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-message","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"}
{"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-message","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"}
{"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-message","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"}
@@ -1226,7 +1226,7 @@
{"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-outbound","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"22d7518c8c7deab35401795873e9e7c4d4a4e05544a2e9ee7b596bac37c60360","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"e07a5d0cdf6a8cb32e103b64add889e858cfcfbd37d5c4252e54245877bd5a69","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-outbound","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"}
{"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-outbound","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"}
{"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-outbound","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"}
@@ -1292,7 +1292,7 @@
{"closureHash":"cd6b0d3f962c8cb40d405e92bb06bb8e9285b386200897f550d21036022262c1","declaration":"export const PAIRING_APPROVED_MESSAGE: \"✅ OpenClaw access approved. Send a message to start chatting.\";","entrypoint":"channel-plugin-common","exportName":"PAIRING_APPROVED_MESSAGE","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"const","recordType":"export"}
{"closureHash":"712435252d2fe5135542ca6b5a4375a6edea24e83829338011dc6ae7e9802fbb","declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"channel-plugin-common","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"}
{"closureHash":"18fcab0d5899575a31205732d2be92597e533ad944c7fe11cac2af6fc718ea34","declaration":"export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-plugin-common","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"}
{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-plugin-common","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"}
{"closureHash":"0db949d365e8e22bcb88c024aac11211d917c5e28cb2daa36345ea62d2ad90ca","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-plugin-common","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"}
{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"channel-plugin-common","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"}
{"category":null,"entrypoint":"channel-policy","importSpecifier":"openclaw/plugin-sdk/channel-policy","recordType":"module"}
{"closureHash":"017ab29d16d0f3d02d60f3513e65a373868d26496bc93d020d8fc8c17fd875f4","declaration":"export function buildAccountScopedDmSecurityPolicy(params: { cfg: OpenClawConfig; channelKey: string; accountId?: string | null; fallbackAccountId?: string | null; policy?: string | null; allowFrom?: Array<string | number> | null; defaultPolicy?: string; allowFromPathSuffix?: string; policyPathSuffix?: string; approveChannelId?: string; approveHint?: string; normalizeEntry?: (raw: string) => string; inheritSharedDefaultsFromDefaultAccount?: boolean; }): ChannelSecurityDmPolicy;","entrypoint":"channel-policy","exportName":"buildAccountScopedDmSecurityPolicy","importSpecifier":"openclaw/plugin-sdk/channel-policy","kind":"function","recordType":"export"}
@@ -1891,8 +1891,8 @@
{"closureHash":"b8e27bea2add3eaafac7074cbd44fcf2acea30a1ac125c3551925e8cd0b3378d","declaration":"export function createChatChannelPlugin<TResolvedAccount extends { accountId?: string | null; }, Probe = unknown, Audit = unknown>(params: { base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>; security?: ChannelSecurityAdapter<TResolvedAccount> | ChatChannelSecurityOptions<TResolvedAccount>; pairing?: ChannelPairingAdapter | ChatChannelPairingOptions; threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>; outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions; }): ChannelPlugin<TResolvedAccount, Probe, Audit>;","entrypoint":"core","exportName":"createChatChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"785b0f750c5bd20f9f3ca081886a5af79ee0e4d2c38f78f769bf994b234f5076","declaration":"export function createDedupeCache(options: DedupeCacheOptions): DedupeCache;","entrypoint":"core","exportName":"createDedupeCache","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"e2df8d235401aebf58eb71652fbeaedb42ca00aaa5042a76909ff2c48ba6fd54","declaration":"export function createSubsystemLogger(subsystem: string): SubsystemLogger;","entrypoint":"core","exportName":"createSubsystemLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"d0215218f4367a9b95487036431f4fc5453ebb39960ec6deccde5acb1cab032c","declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","entrypoint":"core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"de4da3c814c237aa1c66cb2f911beae164b8de9ff041572849c7a0a78f8df3a0","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"core","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"7fe850af19d5517d9e79384f5fff88a3dd87ea6b1590fcb5bd5399411ad5c8ba","declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","entrypoint":"core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"e9b9079bf1a1d7df60ffea04030c82ee885165c04cc47f5bdd2570fca83be791","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"core","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"0e0a9f6b22c433bdb1165e7707b1f9a1ddac1a544a8add00b503048cf157921f","declaration":"export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin): { plugin: TPlugin; };","entrypoint":"core","exportName":"defineSetupPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"3c5ce0c3733cbb038aeb914af1df5d701f362f0521921b8abd0c9214db9cc4b8","declaration":"export function delegateCompactionToRuntime(params: Parameters<ContextEngine[\"compact\"]>[0]): Promise<CompactResult>;","entrypoint":"core","exportName":"delegateCompactionToRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"2c282e0e308f9442bca7cdeb8e89a097531b1d54a692c9d86e5f39a8db1c2537","declaration":"export function deleteAccountFromConfigSection(params: { cfg: OpenClawConfig; sectionKey: string; accountId: string; clearBaseFields?: string[]; }): OpenClawConfig;","entrypoint":"core","exportName":"deleteAccountFromConfigSection","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
@@ -1939,7 +1939,7 @@
{"closureHash":"2e6ce1f4c1692270a00dc0cf08e2752a26739c3143f8091cb270063c6034605d","declaration":"export function tryReadSecretFileSync(filePath: string | undefined, label: string, options: CredentialFileReadOptions): string | undefined;\nexport function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: FsSafeSecretFileReadOptions): string | undefined;\nexport function tryReadSecretFileSync(filePath: string, label: string, options: FsSafeSecretFileReadOptions | undefined, diagnostic: { configPath: string; }): ConfiguredCredentialResult<string>;\nexport function tryReadSecretFileSync(filePath: string | undefined, label: string, options: FsSafeSecretFileReadOptions | undefined, diagnostic: { configPath: string; }): CredentialResult<string>;","entrypoint":"core","exportName":"tryReadSecretFileSync","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"}
{"closureHash":"b40933a11984425d14a7a479bda0d2cd14cbfa9371164774f39bebbf12861df7","declaration":"export const DEFAULT_ACCOUNT_ID: \"default\";","entrypoint":"core","exportName":"DEFAULT_ACCOUNT_ID","importSpecifier":"openclaw/plugin-sdk/core","kind":"const","recordType":"export"}
{"closureHash":null,"declaration":"export const DEFAULT_SECRET_FILE_MAX_BYTES: number;","entrypoint":"core","exportName":"DEFAULT_SECRET_FILE_MAX_BYTES","importSpecifier":"openclaw/plugin-sdk/core","kind":"const","recordType":"export"}
{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"core","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"b110825d1e9bc1970ced28938bde4d86ddc187bf74e118fac33d8df9e354f429","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"core","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"d2ff4de9ee3fd111c2cc64a3ee7e557d9f161af1b6b437ee7886bcb16e426317","declaration":"export type AgentPromptGuidance = AgentPromptGuidance;","entrypoint":"core","exportName":"AgentPromptGuidance","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"2841127a90e04467adfea8126cbf03655f44ddb089ad9e0d48833c2beea39cbd","declaration":"export type AgentPromptGuidanceEntry = AgentPromptGuidanceEntry;","entrypoint":"core","exportName":"AgentPromptGuidanceEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"aa0412acefe62caff12e5271ead0545504874e824380dc21cfc0566388a05f33","declaration":"export type AgentPromptSurfaceKind = \"openclaw_main\" | \"pi_main\" | \"codex_app_server\" | \"cli_backend\" | \"acp_backend\" | \"subagent\";","entrypoint":"core","exportName":"AgentPromptSurfaceKind","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
@@ -1962,7 +1962,7 @@
{"closureHash":"5fd0fa51b352b25b111be12e2fbc79648bb5e9cf5f55cbe7131c8c60e54787b2","declaration":"export type ChatType = ChatType;","entrypoint":"core","exportName":"ChatType","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"e4880dcde69275f17e135c15682e5195e4817e0b0ed06763703145086fc2e721","declaration":"export type ContextEngineSessionTarget = ContextEngineSessionTarget;","entrypoint":"core","exportName":"ContextEngineSessionTarget","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"bced9b9855a7c2d7fafe40ffc45296f5c36b34779716eac146044ac57c3b818d","declaration":"export type GatewayBindUrlResult = GatewayBindUrlResult;","entrypoint":"core","exportName":"GatewayBindUrlResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"ee5982a03dea3c98e6b029700da5781a2274b0a7b2c943499a52335a72af6962","declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"3d0af94b9836e619ac61e4bf7d117cc5b3b55a69709ef9525c15ac9362e242f2","declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"db78504773a1bbfa02018d7352dadbbdae4fcaded93f514c97ba126cbc849f3b","declaration":"export type HistoryEntry = HistoryEntry;","entrypoint":"core","exportName":"HistoryEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"c241077e3f27058240f750d07561778eccb3e7a2dc99be94023eac84cbab7c73","declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"0b824778ee420a4a199233d8fe4635960ed53a0c96964a9e90e8e9e31c9ffe4b","declaration":"export type MemoryPluginCapability = MemoryPluginCapability;","entrypoint":"core","exportName":"MemoryPluginCapability","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
@@ -1971,10 +1971,10 @@
{"closureHash":"54ea426dde51859b572b4c1079d2b70b412096f73da2b8c265f2749d9f01cbb5","declaration":"export type NormalizedLocation = NormalizedLocation;","entrypoint":"core","exportName":"NormalizedLocation","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"5189698d102b8b064eefe1442b008312973c3994fd6b203d2205725d7ee9d118","declaration":"export type OpenClawPluginActiveModelContext = OpenClawPluginActiveModelContext;","entrypoint":"core","exportName":"OpenClawPluginActiveModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"0db949d365e8e22bcb88c024aac11211d917c5e28cb2daa36345ea62d2ad90ca","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"69fefc8092c1cea366c48505c8423ff0f8f995caef3d259d177346b1687541aa","declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"365beb5cee974639c2f35fc7a39088721ca8e9c618442830298b98033e9c768c","declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"b17322e564d6fa98de2dcce9f6d5b58794438251f356fe59c0889046f675a07b","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"b08b01bfe32e01009f812dc79572faf0f6bca932c72bbc73007238d9120057cf","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"a7ed83f0924f818b23cecb5a1f3591ff28b9499ccac9667a2b8505c2bc608ea9","declaration":"export type OpenClawPluginGatewayEventScope = OpenClawPluginGatewayEventScope;","entrypoint":"core","exportName":"OpenClawPluginGatewayEventScope","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"f81f224d9b837e6353b7a546c5a76b124bcf568fd1f1fd0c6881fda30c53120a","declaration":"export type OpenClawPluginGatewayEvents = OpenClawPluginGatewayEvents;","entrypoint":"core","exportName":"OpenClawPluginGatewayEvents","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"ddc2738dd0ece94ddde9a7c655f92c15ec6416645ecf932d4aa404c7b7761159","declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
@@ -2209,7 +2209,7 @@
{"closureHash":"0db968dad99d84ecef5202167daadd092e3b4f456270283d3fd61585ac2bbd5a","declaration":"export type DiscordComponentSendResult = DiscordComponentSendResult;","entrypoint":"discord","exportName":"DiscordComponentSendResult","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"084b46d7dd510eb182a4705559251e824220b57dbfa6dc54392507b72ba1815e","declaration":"export type DiscordOutboundTargetResolution = DiscordOutboundTargetResolution;","entrypoint":"discord","exportName":"DiscordOutboundTargetResolution","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"discord","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"discord","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"0db949d365e8e22bcb88c024aac11211d917c5e28cb2daa36345ea62d2ad90ca","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"discord","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"discord","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"6b9967e919c1cd420c8356b4f9d40dbc81932b4b15a733eecdcfb2ad84f8e5d8","declaration":"export type ResolvedDiscordAccount = ResolvedDiscordAccount;","entrypoint":"discord","exportName":"ResolvedDiscordAccount","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"6bed5ca896b7034117e4757e2c6b4111dd5293dbad9416e3828dd6afa4e858ce","declaration":"export type ThreadBindingRecord = ThreadBindingRecord;","entrypoint":"discord","exportName":"ThreadBindingRecord","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
@@ -2276,14 +2276,14 @@
{"closureHash":"fb062bf5dd2a4d2b45994bbbe466916ad36e7152dcb69a92050a8ce8a6584bc9","declaration":"export const DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS: number;","entrypoint":"gateway-runtime","exportName":"DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"const","recordType":"export"}
{"closureHash":"5c4ad2ca8945631c759e800b6a5a4306476f91484ce9d6039337a60ce9033191","declaration":"export const ErrorCodes: { readonly NOT_LINKED: \"NOT_LINKED\"; readonly NOT_PAIRED: \"NOT_PAIRED\"; readonly AGENT_TIMEOUT: \"AGENT_TIMEOUT\"; readonly INVALID_REQUEST: \"INVALID_REQUEST\"; readonly FORBIDDEN: \"FORBIDDEN\"; readonly APPROVAL_NOT_FOUND: \"APPROVAL_NOT_FOUND\"; readonly UNAVAILABLE: \"UNAVAILABLE\";};","entrypoint":"gateway-runtime","exportName":"ErrorCodes","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"const","recordType":"export"}
{"closureHash":"fcfeb603045e791eee74ce602ace2131cd4f3b2df106144e42ea33629e09f815","declaration":"export const PLUGIN_NODE_CAPABILITY_PATH_PREFIX: \"/__openclaw__/cap\";","entrypoint":"gateway-runtime","exportName":"PLUGIN_NODE_CAPABILITY_PATH_PREFIX","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"const","recordType":"export"}
{"closureHash":"ee5982a03dea3c98e6b029700da5781a2274b0a7b2c943499a52335a72af6962","declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"gateway-runtime","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"54c3466e3f993940c58d7208b2268984639321b2397c45591ddf5c8bb2a578bd","declaration":"export type GatewayRequestHandlers = GatewayRequestHandlers;","entrypoint":"gateway-runtime","exportName":"GatewayRequestHandlers","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"3d0af94b9836e619ac61e4bf7d117cc5b3b55a69709ef9525c15ac9362e242f2","declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"gateway-runtime","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"2bddb9ad9b62096aa092b0e713006e34bfc16800a82cacf0aa2eef891c94422d","declaration":"export type GatewayRequestHandlers = GatewayRequestHandlers;","entrypoint":"gateway-runtime","exportName":"GatewayRequestHandlers","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"a315b0448447679c303ecc5ac5cd05484cea45380967460386b7e6ca6d1b6f06","declaration":"export type GatewayRpcOpts = GatewayRpcOpts;","entrypoint":"gateway-runtime","exportName":"GatewayRpcOpts","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"f361fee6a8f4032fc9cd4610ec3640b16dce97e5751133ac28241d0a2d1c6fb0","declaration":"export type HostedPluginSurfaceUrlParams = HostedPluginSurfaceUrlParams;","entrypoint":"gateway-runtime","exportName":"HostedPluginSurfaceUrlParams","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"a8874cc3b2cb522f6ca38ba5e353ec290aad0495c56087396ecaeb1cee9981b6","declaration":"export type NodeMatchCandidate = NodeMatchCandidate;","entrypoint":"gateway-runtime","exportName":"NodeMatchCandidate","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"c970e76e8d4d8e5fe4b81cb9efe47392d390f826eeb22e409d178473e6d28bed","declaration":"export type NodeSession = NodeSession;","entrypoint":"gateway-runtime","exportName":"NodeSession","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"6cfef265e2aebd53c1e7ce29cb204228ac175b07f9d2f22b861e98cac0eaee4a","declaration":"export type NormalizedPluginNodeCapabilityUrl = NormalizedPluginNodeCapabilityUrl;","entrypoint":"gateway-runtime","exportName":"NormalizedPluginNodeCapabilityUrl","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"86ece317b25cb30a863fdde765e8218283b391b6c2ee73bacb29c3915c8748d3","declaration":"export class GatewayClient {\n #client: BaseGatewayClient;\n constructor(opts: GatewayClientOptions);\n start(): void;\n stop(): void;\n stopAndWait(opts?: {\n timeoutMs?: number;\n }): Promise<void>;\n request<T = Record<string, unknown>>(method: string, params?: unknown, opts?: GatewayClientRequestOptions): Promise<T>;\n getConnectionMetadata(): GatewayClientConnectionMetadata;\n updateNodeManifest(manifest: {\n caps: string[];\n commands: string[];\n }): void;\n}","entrypoint":"gateway-runtime","exportName":"GatewayClient","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"class","recordType":"export"}
{"closureHash":"79dc998945299963de6f1f2049230cf44ddb3bb374a8d22295ec791b8c95cc72","declaration":"export class GatewayClient {\n #client: BaseGatewayClient;\n constructor(opts: GatewayClientOptions);\n start(): void;\n stop(): void;\n stopAndWait(opts?: {\n timeoutMs?: number;\n }): Promise<void>;\n request<T = Record<string, unknown>>(method: string, params?: unknown, opts?: GatewayClientRequestOptions): Promise<T>;\n getConnectionMetadata(): GatewayClientConnectionMetadata;\n updateNodeManifest(manifest: {\n caps: string[];\n commands: string[];\n }): void;\n}","entrypoint":"gateway-runtime","exportName":"GatewayClient","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"class","recordType":"export"}
{"category":null,"entrypoint":"group-access","importSpecifier":"openclaw/plugin-sdk/group-access","recordType":"module"}
{"closureHash":"b6b34cd3844debbe7cda17ad7703e4fcffe4489789ae1763df8f8ab027150203","declaration":"export function evaluateGroupRouteAccessForPolicy(params: { groupPolicy: GroupPolicy; routeAllowlistConfigured: boolean; routeMatched: boolean; routeEnabled?: boolean; }): GroupRouteAccessDecision;","entrypoint":"group-access","exportName":"evaluateGroupRouteAccessForPolicy","importSpecifier":"openclaw/plugin-sdk/group-access","kind":"function","recordType":"export"}
{"closureHash":"bf1a463ce98bf23d639d6e80112ffca836df6d63049bae6fef2a11a2ef1bb74c","declaration":"export function evaluateMatchedGroupAccessForPolicy(params: { groupPolicy: GroupPolicy; allowlistConfigured: boolean; allowlistMatched: boolean; requireMatchInput?: boolean; hasMatchInput?: boolean; }): MatchedGroupAccessDecision;","entrypoint":"group-access","exportName":"evaluateMatchedGroupAccessForPolicy","importSpecifier":"openclaw/plugin-sdk/group-access","kind":"function","recordType":"export"}
@@ -2354,7 +2354,7 @@
{"closureHash":"98e0cc16352b7dfcceb4957f8cd2acfe3c2b3acb223c42a5cfb885e088fcb04c","declaration":"export function resolveInboundReplyDispatchCounts(result: ChannelTurnDispatchResultLike): Record<ReplyDispatchKind, number>;","entrypoint":"inbound-reply-dispatch","exportName":"resolveInboundReplyDispatchCounts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"4cd63266d379acc2cdf9aba02f50b8ceb55500cf820787c370fc98d173fb1950","declaration":"export function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: RunChannelTurnParams<TRaw, TDispatchResult, ChannelProviderOwnedMessageSendingDeliveryAdapter>): Promise<ChannelTurnResult<TDispatchResult>>;\nexport function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: ChannelInboundEventRunnerParams<TRaw, TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply<TDispatchResult>(params: PreparedChannelTurn<TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"inbound-reply-dispatch","exportName":"runPreparedInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"22d7518c8c7deab35401795873e9e7c4d4a4e05544a2e9ee7b596bac37c60360","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"}
{"closureHash":"e07a5d0cdf6a8cb32e103b64add889e858cfcfbd37d5c4252e54245877bd5a69","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"}
{"closureHash":"ab30d0470348e3af7c18a273afc0d169d91f204fba557c75c4694e8a0ea6c936","declaration":"export type AssembledInboundReply = AssembledChannelTurn;","entrypoint":"inbound-reply-dispatch","exportName":"AssembledInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"}
{"closureHash":"3de0d6d1bb8f9a83bd1c6e57aa8618e3fea71054236cd530c555e348bd0b7702","declaration":"export type ChannelBotLoopProtectionFacts = ChannelBotLoopProtectionFacts;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelBotLoopProtectionFacts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"}
{"closureHash":"f0da845bbdf49b30f6718a4b7ff012a0536deb98f65418583555dc90825797ca","declaration":"export type ChannelInboundDroppedHistoryOptions = ChannelTurnDroppedHistoryOptions;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelInboundDroppedHistoryOptions","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"}
@@ -3189,19 +3189,19 @@
{"closureHash":"815786388b89eefc4f411e1796ee4330c17751ce5799afd7ee3f99bc0d414cd0","declaration":"export function callMeetingBrowserProxyOnNode(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; } & MeetingBrowserRequestParams): Promise<unknown>;","entrypoint":"meeting-runtime","exportName":"callMeetingBrowserProxyOnNode","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"c5fcd1d157edcb4773235adfaf8051e3ec7ca428cb7916b6d55f2511affab56c","declaration":"export function convertMeetingBridgeAudioForStt(audio: Buffer, audioFormat: MeetingRealtimeAudioFormat): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingBridgeAudioForStt","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"aa9063c27c5ff714d3653571c4a2a71e9d2eea5609b161f03e4738e15411358b","declaration":"export function convertMeetingTtsAudioForBridge(audio: Buffer, sampleRate: number, audioFormat: MeetingRealtimeAudioFormat, outputFormat?: string, platformName?: string): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingTtsAudioForBridge","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"205fcd090a9e032384aa5b36c88ea763c34a925a9ee31f1ffa5db65a4f9d5048","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"ff6845ce0decb21654c8088a3d7207c99ea5b8e9b298df55bb573437b6a365a2","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"ca1fd149af9fb3117fc513993fe54e476822c82f414403040041b53f7624e767","declaration":"export function createMeetingBrowserNodeCaller(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; }): MeetingBrowserRequestCaller;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeCaller","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"f244ca8c47f556c9f388953cf9a7581c7d4d413af93e0dfa1462779e6a687a7c","declaration":"export function createMeetingBrowserNodeInvokePolicy(options: MeetingBrowserNodePolicyOptions): OpenClawPluginNodeInvokePolicy;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeInvokePolicy","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"1aa4e3ff39a2cbe162421c62b1fa4a362973381d4d9ff15c671249b1ebd566bc","declaration":"export function createMeetingNodeHost(options: MeetingNodeHostOptions): { handleCommand(paramsJSON?: string | null): Promise<string>; };","entrypoint":"meeting-runtime","exportName":"createMeetingNodeHost","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"ad96285c50f53ca310f475dde5912487b0fd22f796b3b207c8a2a5c8a6d2fdc4","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise<void>; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"5bbdbc6ae9041df0f613692c7abf8da84d093314965e858fce601e6682c82127","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise<void>; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"9124db05923060c467edb149bd8abdc8be1d00d00f867cf84600972923a58489","declaration":"export function createMeetingSession<TTransport extends string, TMode extends string, TToolPolicy extends string>(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { provider?: string; voiceProvider?: string; transcriptionProvider?: string; model?: string; toolPolicy: TToolPolicy; }; }; resolved: MeetingResolvedJoin<TTransport, TMode>; createdAt: string; }): MeetingSessionRecord<TTransport, TMode, { enabled: boolean; strategy: string; provider: string | undefined; model: string | undefined; transcriptionProvider: string | undefined; toolPolicy: TToolPolicy; }>;","entrypoint":"meeting-runtime","exportName":"createMeetingSession","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"fb276e07e395ab19177b3d7967fd0a8fcda79130883f15092a4a306b826481b9","declaration":"export function createMeetingSetupStatus(checks: MeetingSetupCheck[]): MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"createMeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"cfa51d8c3420f0abc4043525ec709c4cfd629bb8accd3fde275ffacc84ae549c","declaration":"export function createMeetingVoiceCallGateway(params: { config: MeetingVoiceCallConfig; runtime: PluginRuntime; surface: MeetingVoiceCallSurface; connectClient: (params: { config: MeetingVoiceCallConfig; surface: MeetingVoiceCallSurface; }) => Promise<MeetingVoiceCallGatewayClient>; }): MeetingVoiceCallGateway;","entrypoint":"meeting-runtime","exportName":"createMeetingVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"e2e91c5bafbf7e7fb17d459169ada4f130b94ad579ab5b594eb4d1f75e783a52","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"f5adef15276f22025e3b780fff91fc22e412ea1e39c440982e1a68a2221a5d40","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"c0987b15a391a92d53d3e4e12b61eb122f9547fcf4f45478cdd3c5a8973a3e52","declaration":"export function endMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise<void>;","entrypoint":"meeting-runtime","exportName":"endMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"b3ebc10fe7ab372e1317eaa8f78621cf277bba1e33fda57acf2254f6759d1e9d","declaration":"export function getMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise<MeetingVoiceCallStatusResult>;","entrypoint":"meeting-runtime","exportName":"getMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"aac088df8e80d9ce89c45feccdb04ace3ae4417b3fcdd0f058410ddf25c96b46","declaration":"export function isMeetingVoiceCallMissingError(error: unknown): boolean;","entrypoint":"meeting-runtime","exportName":"isMeetingVoiceCallMissingError","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"5541f6c2eec99266d03eb5481e9b49f5b625f260b1eac923e6c2085d727a7c59","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise<MeetingVoiceCallJoinResult>;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"546c9368b673dcc6eaa16b454095ec8129a2429ec0c38ce6c6ba2d114ac8a39b","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise<MeetingVoiceCallJoinResult>;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"bac9621cd025754fbcd4c21409b06eb0503405db81f35cac993aaf0b881ab3e1","declaration":"export function leaveMeetingWithBrowser<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; callBrowser: MeetingBrowserRequestCaller; launch: boolean; meetingSessionId?: string; meetingUrl: string; tab: MeetingBrowserTab; timeoutMs: number; }): Promise<{ left: boolean; note: string; }>;","entrypoint":"meeting-runtime","exportName":"leaveMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"c7b1aeac134b2070d053adf33026d876998b852ccd65cd2c9667119b1a1abee2","declaration":"export function openMeetingWithBrowser<Session extends MeetingBrowserJoinSession<Mode>, Mode extends string, Health extends MeetingBrowserHealth & { browserTitle?: string; browserUrl?: string; notes?: string[]; }, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; callBrowser: MeetingBrowserRequestCaller; config: MeetingBrowserControllerConfig; session: Session; }): Promise<{ launched: boolean; browser?: Health; tab?: MeetingBrowserTab; }>;","entrypoint":"meeting-runtime","exportName":"openMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"34858979bd248799c2215d9c7bdd5d4a6b27660c9173bfca2e50dd4373ac27db","declaration":"export function readMeetingBrowserTab(result: unknown): MeetingBrowserCandidateTab | undefined;","entrypoint":"meeting-runtime","exportName":"readMeetingBrowserTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
@@ -3212,8 +3212,8 @@
{"closureHash":"95664d3db077437976f13b8729c5434e20bdafac6de8327207010087c9458c51","declaration":"export function resolveMeetingBrowserNodeInfo(params: { runtime: PluginRuntime; adapter: NodeAdapter; requestedNode?: string; }): Promise<MeetingBrowserNodeInfo>;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNodeInfo","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"f21196c9ea8ab90248ffdcdef3e942f1247ee2b9064ddb9757fffe352362c779","declaration":"export function resolveMeetingRealtimeAudioFormat(audioFormat: MeetingRealtimeAudioFormat): RealtimeVoiceAudioFormat;","entrypoint":"meeting-runtime","exportName":"resolveMeetingRealtimeAudioFormat","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"521dc5c262524a709cdd27cdc88e95004073da8223537bcbc73d5d423172dc2b","declaration":"export function speakMeetingViaVoiceCallGateway(params: { gateway: MeetingVoiceCallGateway; callId: string; message: string; }): Promise<void>;","entrypoint":"meeting-runtime","exportName":"speakMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"942673553f5aa16ff5544dfc61f9f036b1ac6f6ec42badff82f4fa0207b5b754","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"8e7bfb055b4118842ac3c7d93273fbc60102b573715c7c5daa389cd5447a48ce","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise<void>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"4288424fc945b71b7e25bbb51c087ec87725c381f6710d4c04b5fe0ca234df79","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"0b247439701d6018b53dbcdc133fcab6c3fce4980b4b6ac27854c482c3965286","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise<void>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"663b9c77231b21af697efd5042e22d1c33e9ee972adeb312ad86d81a2b2e36f7","declaration":"export type MeetingAgentConsultParams = MeetingAgentConsultParams;","entrypoint":"meeting-runtime","exportName":"MeetingAgentConsultParams","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"adf0745c611bf737fc28978d766fc2a25b7104e71b6b2fed4e9b95e519c7b115","declaration":"export type MeetingBrowserCandidateTab = MeetingBrowserCandidateTab;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserCandidateTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"59b1526172b7491b85f14f13c1ee26d8d6726f33093770748580c767b1689a17","declaration":"export type MeetingBrowserControllerConfig = MeetingBrowserControllerConfig;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserControllerConfig","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
@@ -3245,7 +3245,7 @@
{"closureHash":"8244b408803f57cf2004157e96f2ce11bb036cdf864123b15ba2fac7f03b5d43","declaration":"export type MeetingSessionRuntimeHandles<THealth extends MeetingBrowserHealth> = MeetingSessionRuntimeHandles<THealth>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeHandles","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"aa8054fc2c442891197e89bdad61e79999ea15d813c4b127fb6150607b9e67e7","declaration":"export type MeetingSessionRuntimeJoinContext<TSession extends MeetingSessionRecord<TTransport, TMode>, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab> = MeetingSessionRuntimeJoinContext<TSession, TTransport, TMode, THealth, TTab>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeJoinContext","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"325241724edd38da439c5df03d225e2a7cb8e56d817e98ed7c37850656d3744b","declaration":"export type MeetingSessionRuntimeMessages<TSpeechBlockedReason extends string> = MeetingSessionRuntimeMessages<TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeMessages","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"76fa2c8974f6773a9f54b12fc18b0085c6ec67edddb5aa79a88791c4ba8acf65","declaration":"export type MeetingSessionRuntimeOptions<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"bb2f19adc58b45ab06e7dbf07c06f707cb548a8d3689db4ece66a14178186603","declaration":"export type MeetingSessionRuntimeOptions<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"d3c730cba02d30512c4cdae6e2367c662982e2c25671833ecd8701283924c642","declaration":"export type MeetingSessionState = MeetingSessionState;","entrypoint":"meeting-runtime","exportName":"MeetingSessionState","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"2496931157bf64fdb16013172ab9c023e60c5f85b98f46a3ee9532ce58d636d6","declaration":"export type MeetingSetupCheck = MeetingSetupCheck;","entrypoint":"meeting-runtime","exportName":"MeetingSetupCheck","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"f0aed6f7955a43fc387d8bbc072cb6276b0f2d18e7f829441a2ad45974bb3831","declaration":"export type MeetingSetupStatus = MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"MeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
@@ -3259,9 +3259,9 @@
{"closureHash":"20c70a94483aeddda4d1500cfcc5e71fab844b474ba4061c55d6c8693d40821b","declaration":"export type MeetingVoiceCallJoinResult = MeetingVoiceCallJoinResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallJoinResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"4fb2c5928aa6f809dd4c604d71c5c99cdfe3e98b6e9c1662601c9b4ad9738162","declaration":"export type MeetingVoiceCallStatusResult = MeetingVoiceCallStatusResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallStatusResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"5ef812397b6409a4ca228943dd1080661365959477b42e705a9aaa219757d14f","declaration":"export type MeetingVoiceCallSurface = MeetingVoiceCallSurface;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallSurface","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"8de41c9472e34cf4bef349c8f40b9f56caef493a4dc765fc08c96fb35d2ba68d","declaration":"export interface MeetingPlatformAdapter<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot, CreateParams = never, CreateResult = never, DialInParams = never, DialInPlan = never> extends MeetingPlatformAdapterContract<Session, Mode, Health, Transcript, CreateParams, CreateResult, DialInParams, DialInPlan> {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"}
{"closureHash":"aaa3c92e8b04c39d6841e793be9ca4406ffc3c1eca5ce8b58e5cf07d63546c4d","declaration":"export interface MeetingPlatformAdapter<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot, CreateParams = never, CreateResult = never, DialInParams = never, DialInPlan = never> extends MeetingPlatformAdapterContract<Session, Mode, Health, Transcript, CreateParams, CreateResult, DialInParams, DialInPlan> {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"}
{"closureHash":"79884bd1df1ea0d07d5ceb4c62d93c1ee744b75d31c94c988845869c0f3cbd2d","declaration":"export interface MeetingRealtimeAudioTransport {\n onFatal(handler: () => void): void;\n startInput(onAudio: (audio: Buffer) => void): void;\n beginOutput?(): void;\n stop(): Promise<void>;\n writeOutput(audio: Buffer): Promise<void>;\n clearOutput(): Promise<void>;\n dispose(): Promise<void>;\n getHealth?(): MeetingRealtimeAudioTransportHealth;\n startBargeInMonitor?(onBargeIn: (audio: Buffer) => boolean): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"}
{"closureHash":"b035f70f84a5787d05c9874da278e7096e102ea3cb45cf099e37898b38564591","declaration":"export class MeetingSessionRuntime<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map<string, TSession>;\n readonly #sessionLeaves: Map<string, Promise<MeetingSessionLeaveResult<TSession>>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map<string, () => Promise<void>>;\n readonly #sessionSpeakers: Map<string, (instructions?: string) => void>;\n readonly #sessionHealth: Map<string, () => Partial<THealth>>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts<TSession>;\n readonly #transcriptStore: MeetingSessionTranscriptStore<TSession>;\n constructor(private readonly options: MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise<TranscriptsStartResult>;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise<TranscriptsStopResult>;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin<TTransport, TMode>): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise<boolean>;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise<void>;\n async refreshCaptionHealth(session: TSession): Promise<void>;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin<TTransport, TMode>): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise<boolean>;\n async #rollbackFailedJoinSession(session: TSession): Promise<void>;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise<void>;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles<THealth>): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"}
{"closureHash":"0f59c2bbce6fe7a78670a04f2b46b3692f0109e0d8562c0e0ce9e90e5dd9c58f","declaration":"export class MeetingSessionRuntime<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map<string, TSession>;\n readonly #sessionLeaves: Map<string, Promise<MeetingSessionLeaveResult<TSession>>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map<string, () => Promise<void>>;\n readonly #sessionSpeakers: Map<string, (instructions?: string) => void>;\n readonly #sessionHealth: Map<string, () => Partial<THealth>>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts<TSession>;\n readonly #transcriptStore: MeetingSessionTranscriptStore<TSession>;\n constructor(private readonly options: MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise<TranscriptsStartResult>;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise<TranscriptsStopResult>;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin<TTransport, TMode>): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise<boolean>;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise<void>;\n async refreshCaptionHealth(session: TSession): Promise<void>;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin<TTransport, TMode>): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise<boolean>;\n async #rollbackFailedJoinSession(session: TSession): Promise<void>;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise<void>;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles<THealth>): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"}
{"category":null,"entrypoint":"memory-core-host-engine-foundation","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","recordType":"module"}
{"closureHash":"e2df8d235401aebf58eb71652fbeaedb42ca00aaa5042a76909ff2c48ba6fd54","declaration":"export function createSubsystemLogger(subsystem: string): SubsystemLogger;","entrypoint":"memory-core-host-engine-foundation","exportName":"createSubsystemLogger","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"}
{"closureHash":null,"declaration":"export function isPathInside(root: string, target: string): boolean;","entrypoint":"memory-core-host-engine-foundation","exportName":"isPathInside","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"}
@@ -3397,9 +3397,9 @@
{"category":"core","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","recordType":"module"}
{"closureHash":"3d27406644141af2733cfca6e8c1240fc62327910109f585ce9073cafd7fd89c","declaration":"export function buildJsonPluginConfigSchema(schema: JsonSchemaObject, options?: BuildJsonPluginConfigSchemaOptions): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"buildJsonPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"}
{"closureHash":"aff66c792d532642afb9c6e5a03c8d220f219250c3a8b49733f32058f22f98ca","declaration":"export function buildPluginConfigSchema(schema: ZodTypeAny, options?: BuildPluginConfigSchemaOptions): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"buildPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"}
{"closureHash":"de4da3c814c237aa1c66cb2f911beae164b8de9ff041572849c7a0a78f8df3a0","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"}
{"closureHash":"e9b9079bf1a1d7df60ffea04030c82ee885165c04cc47f5bdd2570fca83be791","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"}
{"closureHash":"0ef1dfa3b5deeb456480e292c0e835c3cd6d13e7bb7059fc735546507da51594","declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"}
{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"plugin-entry","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"b110825d1e9bc1970ced28938bde4d86ddc187bf74e118fac33d8df9e354f429","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"plugin-entry","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"d2ff4de9ee3fd111c2cc64a3ee7e557d9f161af1b6b437ee7886bcb16e426317","declaration":"export type AgentPromptGuidance = AgentPromptGuidance;","entrypoint":"plugin-entry","exportName":"AgentPromptGuidance","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"2841127a90e04467adfea8126cbf03655f44ddb089ad9e0d48833c2beea39cbd","declaration":"export type AgentPromptGuidanceEntry = AgentPromptGuidanceEntry;","entrypoint":"plugin-entry","exportName":"AgentPromptGuidanceEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"aa0412acefe62caff12e5271ead0545504874e824380dc21cfc0566388a05f33","declaration":"export type AgentPromptSurfaceKind = \"openclaw_main\" | \"pi_main\" | \"codex_app_server\" | \"cli_backend\" | \"acp_backend\" | \"subagent\";","entrypoint":"plugin-entry","exportName":"AgentPromptSurfaceKind","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
@@ -3415,10 +3415,10 @@
{"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"2f6e368fff73e942044cdf95c7c751b46c741e22ac0d2d4d97f45a3158f758ae","declaration":"export type OpenClawGatewayDiscoveryAdvertiseContext = OpenClawGatewayDiscoveryAdvertiseContext;","entrypoint":"plugin-entry","exportName":"OpenClawGatewayDiscoveryAdvertiseContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"b198058e0065e5c60b729a5bdf568f7e2436a006d8c4a7c63de764c7283f96ba","declaration":"export type OpenClawGatewayDiscoveryService = OpenClawGatewayDiscoveryService;","entrypoint":"plugin-entry","exportName":"OpenClawGatewayDiscoveryService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"0db949d365e8e22bcb88c024aac11211d917c5e28cb2daa36345ea62d2ad90ca","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"69fefc8092c1cea366c48505c8423ff0f8f995caef3d259d177346b1687541aa","declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"365beb5cee974639c2f35fc7a39088721ca8e9c618442830298b98033e9c768c","declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"b17322e564d6fa98de2dcce9f6d5b58794438251f356fe59c0889046f675a07b","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"b08b01bfe32e01009f812dc79572faf0f6bca932c72bbc73007238d9120057cf","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"a7ed83f0924f818b23cecb5a1f3591ff28b9499ccac9667a2b8505c2bc608ea9","declaration":"export type OpenClawPluginGatewayEventScope = OpenClawPluginGatewayEventScope;","entrypoint":"plugin-entry","exportName":"OpenClawPluginGatewayEventScope","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"f81f224d9b837e6353b7a546c5a76b124bcf568fd1f1fd0c6881fda30c53120a","declaration":"export type OpenClawPluginGatewayEvents = OpenClawPluginGatewayEvents;","entrypoint":"plugin-entry","exportName":"OpenClawPluginGatewayEvents","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"7470b211ac4d3449cd0fc353ee2eb48e9fbc2dbc9259d21c680d0203577aa94f","declaration":"export type OpenClawPluginHttpRouteHandler = OpenClawPluginHttpRouteHandler;","entrypoint":"plugin-entry","exportName":"OpenClawPluginHttpRouteHandler","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
@@ -3549,11 +3549,12 @@
{"closureHash":"7156d470d0ea236c59f3960a0fef213b1770ab4810a1af8eddf863aefa3c27e9","declaration":"export type UnifiedModelCatalogProviderContext = UnifiedModelCatalogProviderContext;","entrypoint":"plugin-entry","exportName":"UnifiedModelCatalogProviderContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"e806b384aa629b38f13b393098a8cca6a8015cefc34b9398265ce0e270b375d4","declaration":"export type UnifiedModelCatalogProviderPlugin = UnifiedModelCatalogProviderPlugin;","entrypoint":"plugin-entry","exportName":"UnifiedModelCatalogProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"12997512ff4f2ee560e4201c3ac6618204d40e0cb27a09a65533622131328df2","declaration":"export type UnifiedModelCatalogSource = UnifiedModelCatalogSource;","entrypoint":"plugin-entry","exportName":"UnifiedModelCatalogSource","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"725ff56523733f1b749eac251d5583bea0476a1c13793ddad7243039cc77d83d","declaration":"export type WorkerDesktopEndpoint = WorkerDesktopEndpoint;","entrypoint":"plugin-entry","exportName":"WorkerDesktopEndpoint","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"d5d57aa5a7494b63f1c2ba94228ba70a1ea95e2e52e0b6cd4af94e6b7d0db72c","declaration":"export type WorkerLease = WorkerLease;","entrypoint":"plugin-entry","exportName":"WorkerLease","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"93405daf881381646c65e00e968027b240dc5afbd91536968761597d1ad6b73a","declaration":"export type WorkerDesktopApp = WorkerDesktopApp;","entrypoint":"plugin-entry","exportName":"WorkerDesktopApp","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"bebc4fdf80820547e9098393654a407d07b7451bf66a5815b1991ffa2f651697","declaration":"export type WorkerDesktopEndpoint = WorkerDesktopEndpoint;","entrypoint":"plugin-entry","exportName":"WorkerDesktopEndpoint","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"19da1a7d34f101187dfecca3ebdabacb73446a29f66c177d8343bd9698186a96","declaration":"export type WorkerLease = WorkerLease;","entrypoint":"plugin-entry","exportName":"WorkerLease","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"1b90a8fbe4ed5abd944a364721bff177068ade4e1b74ce0f06d44c9ac71797a0","declaration":"export type WorkerLeaseStatus = WorkerLeaseStatus;","entrypoint":"plugin-entry","exportName":"WorkerLeaseStatus","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"beb4406a0c814cc2ad666d28d57485a8469d69091fd870e19ffc30211043f366","declaration":"export type WorkerProfile = Readonly<Record<string, PluginJsonValue>>;","entrypoint":"plugin-entry","exportName":"WorkerProfile","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"0979616970bb1abf050d57270e36f94b96f509172142d0464e006f65129480fa","declaration":"export type WorkerProvider = WorkerProvider;","entrypoint":"plugin-entry","exportName":"WorkerProvider","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"9ea8474b2c34c49d4245458dea80b6e07caec22353889c0be1982a98d88a48c4","declaration":"export type WorkerProvider = WorkerProvider;","entrypoint":"plugin-entry","exportName":"WorkerProvider","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"af83d0492e616c892edb9dfd43bf45a555c9ac0df24ccadf24a25663fa5927ea","declaration":"export type WorkerSshEndpoint = WorkerSshEndpoint;","entrypoint":"plugin-entry","exportName":"WorkerSshEndpoint","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"b81ce4575791f7e3cef8b3b971d92881e454eaedc3d1a349c1870984af8489f5","declaration":"export type WorkerSshIdentity = WorkerSshIdentity;","entrypoint":"plugin-entry","exportName":"WorkerSshIdentity","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"2c44f2df0f353550c3238748e8e2707384a99a462f5496b47da91719536b8712","declaration":"export type WorkerSshIdentityRequest = WorkerSshIdentityRequest;","entrypoint":"plugin-entry","exportName":"WorkerSshIdentityRequest","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
@@ -3567,14 +3568,14 @@
{"closureHash":"5e57188ea41861c5108f8a07d6a2a1902ced52b5f1708a70d702dc4ba3ce5e0b","declaration":"export function executePluginCommand(params: { command: RegisteredPluginCommand; args?: string; senderId?: string; channel: string; channelId?: PluginCommandContext[\"channelId\"]; isAuthorizedSender: boolean; senderIsOwner?: boolean; gatewayClientScopes?: PluginCommandContext[\"gatewayClientScopes\"]; agentId?: string; sessionKey?: PluginCommandContext[\"sessionKey\"]; sessionId?: PluginCommandContext[\"sessionId\"]; sessionTarget?: PluginCommandContext[\"sessionTarget\"]; sessionFile?: PluginCommandContext[\"sessionFile\"]; authProfileId?: string; commandBody: string; config: OpenClawConfig; from?: PluginCommandContext[\"from\"]; to?: PluginCommandContext[\"to\"]; originatingTo?: string; accountId?: PluginCommandContext[\"accountId\"]; messageThreadId?: PluginCommandContext[\"messageThreadId\"]; threadParentId?: PluginCommandContext[\"threadParentId\"]; diagnosticsSessions?: PluginCommandContext[\"diagnosticsSessions\"]; diagnosticsUploadApproved?: PluginCommandContext[\"diagnosticsUploadApproved\"]; diagnosticsPreviewOnly?: PluginCommandContext[\"diagnosticsPreviewOnly\"]; diagnosticsPrivateRouted?: PluginCommandContext[\"diagnosticsPrivateRouted\"]; }): Promise<PluginCommandResult>;","entrypoint":"plugin-runtime","exportName":"executePluginCommand","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"7e75fc04d621dc8d12f459d5b5b0c039a39b77c30c2b51af481e123c352f57b8","declaration":"export function getGlobalHookRunner(): HookRunner | null;","entrypoint":"plugin-runtime","exportName":"getGlobalHookRunner","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"db6583607237e4081814e2c5d4f8fb6f216ad3bbfe623bf16fc1a9c09c15f64b","declaration":"export function getPluginCommandSpecs(provider?: string, options?: PluginCommandSpecOptions): Array<{ name: string; description: string; descriptionLocalizations?: Record<string, string>; acceptsArgs: boolean; }>;","entrypoint":"plugin-runtime","exportName":"getPluginCommandSpecs","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"e03d09bdacb32f4dbeef1de2c8d6d832fbdbc75417d8cc645922af43ccec6111","declaration":"export function getPluginRuntimeGatewayRequestScope(): PluginRuntimeGatewayRequestScope | undefined;","entrypoint":"plugin-runtime","exportName":"getPluginRuntimeGatewayRequestScope","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"f7af9a3651f4a6b6903380b2afc07bfb4dac2b967a571b58d76899967c894622","declaration":"export function getPluginRuntimeGatewayRequestScope(): PluginRuntimeGatewayRequestScope | undefined;","entrypoint":"plugin-runtime","exportName":"getPluginRuntimeGatewayRequestScope","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"cfe277ce6de0c6ed50f1f54b66c889f6c95e91b058e8bcdc27aa37317a93a447","declaration":"export function listRegisteredPluginAgentPromptGuidance(params?: { surface?: AgentPromptSurfaceKind; includeLegacyGlobalGuidance?: boolean; }): string[];","entrypoint":"plugin-runtime","exportName":"listRegisteredPluginAgentPromptGuidance","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"ef4872995ac1acbe1b2046397e3ddc5d9a4315cb0bd0dfddfac6222418e7c048","declaration":"export function matchPluginCommand(commandBody: string, options?: { channel?: string; }): { command: RegisteredPluginCommand; args?: string; } | null;","entrypoint":"plugin-runtime","exportName":"matchPluginCommand","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"e02d3651398ab23c38e91a52fc1502e7a97490f17047b4f558b87c94731b0344","declaration":"export function registerPluginCommand(pluginId: string, command: OpenClawPluginCommandDefinition, opts?: { pluginName?: string; pluginRoot?: string; allowReservedCommandNames?: boolean; allowOwnerStatusExposure?: boolean; }): CommandRegistrationResult;","entrypoint":"plugin-runtime","exportName":"registerPluginCommand","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"16986e5d431f584c2d5053289eca3d2225bbe47a92cdfbbc2af91b264a165846","declaration":"export function registerPluginInteractiveHandler(pluginId: string, registration: PluginInteractiveHandlerRegistration, opts?: { pluginName?: string; pluginRoot?: string; }): InteractiveRegistrationResult;","entrypoint":"plugin-runtime","exportName":"registerPluginInteractiveHandler","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"f20c07a48e2c949fbc889e7a32760f4e52339d9f6d3eab49e00ff249266e2396","declaration":"export function startLazyPluginServiceModule(params: { skipEnvVar?: string; overrideEnvVar?: string; validateOverrideSpecifier?: (specifier: string) => string; loadDefaultModule: () => Promise<LazyServiceModule>; loadOverrideModule?: (specifier: string) => Promise<LazyServiceModule>; startExportNames: string[]; stopExportNames?: string[]; }): Promise<LazyPluginServiceHandle | null>;","entrypoint":"plugin-runtime","exportName":"startLazyPluginServiceModule","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"bad43ffe69d1d9d6c92c79df4c388b3dda685013a92392daef456f1622ade659","declaration":"export type LazyPluginServiceHandle = LazyPluginServiceHandle;","entrypoint":"plugin-runtime","exportName":"LazyPluginServiceHandle","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"}
{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-runtime","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"}
{"closureHash":"0db949d365e8e22bcb88c024aac11211d917c5e28cb2daa36345ea62d2ad90ca","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-runtime","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"}
{"closureHash":"365beb5cee974639c2f35fc7a39088721ca8e9c618442830298b98033e9c768c","declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-runtime","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"}
{"closureHash":"f06379ada10ce0eca4e66bdf978db2c0035e054a135dc044c6d831ac7797c08f","declaration":"export type PluginConversationBinding = PluginConversationBinding;","entrypoint":"plugin-runtime","exportName":"PluginConversationBinding","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"}
{"closureHash":"a1223d674b2b66918a576ae05b04318d7b8cbe9b1d3bc6dd7a08d1a4fa79b291","declaration":"export type PluginConversationBindingRequestParams = PluginConversationBindingRequestParams;","entrypoint":"plugin-runtime","exportName":"PluginConversationBindingRequestParams","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"}
@@ -3664,10 +3665,10 @@
{"closureHash":"e66adcb55dec79be73a52bc76594b7e9210fd93c69a85b56098fbb58cecbb354","declaration":"export type WriteOAuthCredentialsOptions = WriteOAuthCredentialsOptions;","entrypoint":"provider-auth","exportName":"WriteOAuthCredentialsOptions","importSpecifier":"openclaw/plugin-sdk/provider-auth","kind":"type","recordType":"export"}
{"category":null,"entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","recordType":"module"}
{"closureHash":"b5665596a3f011454a5614681513516acd50b6c749536c43842d811980cf7ad3","declaration":"export function augmentModelCatalogWithProviderPlugins(params: { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; metadataSnapshot?: PluginMetadataSnapshot; context: ProviderAugmentModelCatalogContext; }): Promise<ModelCatalogEntry[]>;","entrypoint":"provider-catalog-runtime","exportName":"augmentModelCatalogWithProviderPlugins","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"618e8426b476017da049fe799fa99077e0c8b58422c0a5c9965bf078b090c612","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters<typeof resolvePluginProviders>[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"b765efc8121364a198c93997e108399e2777a7cca8e295a5759c74651343524b","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"b06859eeb21ed14e89c62ea368dc5f3a91fd6db553c98be34b2510562980a852","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick<PluginMetadataSnapshot, \"owners\" | \"manifestRegistry\" | \"byPluginId\">; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"15228d7e0f4c156a1ff85dc8cbc1762db5ab81288adf2005bf915fd0f7136ca6","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"2c2633f5654fd3d9c9dd8d719fe6b3342eae7047edfb4da8a8de7fa8a70fcd6b","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters<typeof resolvePluginProviders>[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"8398b5d35c385e372946950d64690128472bd89f204a85d9132d8de359cb96a0","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"554105cf1acd88155938f377c3624513404c426ecc5f6347cad2e5f11eaec881","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick<PluginMetadataSnapshot, \"owners\" | \"manifestRegistry\" | \"byPluginId\">; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"a0c9ff6fb61bcd6b5fb55a1b1a1cb0501c3d986540261b765ea326c31c5542cd","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"category":null,"entrypoint":"proxy-capture","importSpecifier":"openclaw/plugin-sdk/proxy-capture","recordType":"module"}
{"closureHash":"d0e4e026d736bc00b1758d1089c44c3c5f3a09c7254291584de8cb297365804c","declaration":"export function acquireDebugProxyCaptureStore(dbPath: string, blobDir: string): { store: LegacyDebugProxyCaptureStore; release: () => void; };\nexport function acquireDebugProxyCaptureStore(options?: DebugProxyCaptureStoreOptions): { store: SharedDebugProxyCaptureStore; release: () => void; };","entrypoint":"proxy-capture","exportName":"acquireDebugProxyCaptureStore","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"}
{"closureHash":"d41cbb9de51b7aee1c6cf8c713b0bc44f27ac926fbb6422c05726f196d11964e","declaration":"export function captureHttpExchange(params: { url: string; method: string; requestHeaders?: Headers | Record<string, string> | undefined; requestBody?: BodyInit | Buffer | string | null; response: Response; transport?: \"http\" | \"sse\"; flowId?: string; meta?: Record<string, unknown>; }, resolved?: DebugProxySettings, deps?: DebugProxyCaptureRuntimeDeps): void;","entrypoint":"proxy-capture","exportName":"captureHttpExchange","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"}
@@ -4629,16 +4630,16 @@
{"closureHash":"e365d7ad911453b5139ba700931b061e560cff80b50d4cbe5e9615b5d7e13a52","declaration":"export type ScopedExpiringIdCache<TScope extends string | number, TId extends string | number> = ScopedExpiringIdCache<TScope, TId>;","entrypoint":"text-runtime","exportName":"ScopedExpiringIdCache","importSpecifier":"openclaw/plugin-sdk/text-runtime","kind":"type","recordType":"export"}
{"closureHash":"af5502c92594c2e9049a4b4994e17c00d3adee3f6d1e68edd818a539c74c8787","declaration":"export interface CodeRegion {\n start: number;\n end: number;\n}","entrypoint":"text-runtime","exportName":"CodeRegion","importSpecifier":"openclaw/plugin-sdk/text-runtime","kind":"interface","recordType":"export"}
{"category":null,"entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin","recordType":"module"}
{"closureHash":"fb5203d8d0fb7f790e856e8bd91f095ef09bfc4e07c847051815b511eec89b3e","declaration":"export function defineToolPlugin<TConfigSchema extends TSchema | undefined = undefined>(definition: DefineToolPluginOptions<TConfigSchema>): DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"defineToolPlugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"function","recordType":"export"}
{"closureHash":"b0695bbf03a1972928b6deca4b7ed3430b132556b0bb834f524b1b06f811b522","declaration":"export function defineToolPlugin<TConfigSchema extends TSchema | undefined = undefined>(definition: DefineToolPluginOptions<TConfigSchema>): DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"defineToolPlugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"function","recordType":"export"}
{"closureHash":"36ef907ce20f885b5f3068cbf0475dc442a68da9652579bb5bf8fb83ad34c38b","declaration":"export function getToolPluginMetadata(entry: unknown): ToolPluginMetadata | undefined;","entrypoint":"tool-plugin","exportName":"getToolPluginMetadata","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"function","recordType":"export"}
{"closureHash":"bdc58154e36b2625fb3daccca3eab248d981077d04d747f0ad507cb2ff565198","declaration":"export const toolPluginMetadataSymbol: typeof toolPluginMetadataSymbol;","entrypoint":"tool-plugin","exportName":"toolPluginMetadataSymbol","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"const","recordType":"export"}
{"closureHash":"396ce432df15713c3d8293d12c91941da5c10ca2ba2b09d9e31166d541769cf1","declaration":"export type DefineToolPluginOptions<TConfigSchema extends TSchema | undefined = undefined> = DefineToolPluginOptions<TConfigSchema>;","entrypoint":"tool-plugin","exportName":"DefineToolPluginOptions","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"658ca7829c3539c33a8a26c7195b86a0e9b92aaffd6ab34743abb16e85eb8b62","declaration":"export type DefinedToolPluginEntry = DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"DefinedToolPluginEntry","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"43b9db3bef8eac16bcb60378f1f8ec6193af0d2ce8fc2f6f30b15038d42d6012","declaration":"export type ToolPluginExecutionContext = ToolPluginExecutionContext;","entrypoint":"tool-plugin","exportName":"ToolPluginExecutionContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"a75a6f9e2cfd805d6a87622b2cf7038de57983e32de0bb7fc66de7fea0833a61","declaration":"export type ToolPluginFactoryContext<TConfig> = ToolPluginFactoryContext<TConfig>;","entrypoint":"tool-plugin","exportName":"ToolPluginFactoryContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"2c83b4c6dd209f08e90f69f34aa934ded6f5fc9d57cadb3cf72bd2af88262c19","declaration":"export type DefineToolPluginOptions<TConfigSchema extends TSchema | undefined = undefined> = DefineToolPluginOptions<TConfigSchema>;","entrypoint":"tool-plugin","exportName":"DefineToolPluginOptions","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"b8d82cbf9aa797f0da2c7cd9fc7b17ba0ea5410d0e0abf233a3a99b4e5b32845","declaration":"export type DefinedToolPluginEntry = DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"DefinedToolPluginEntry","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"be57626b80ccc720ac04dbb2e52d965e89b39249e55f40b02546d86fc21d8b0c","declaration":"export type ToolPluginExecutionContext = ToolPluginExecutionContext;","entrypoint":"tool-plugin","exportName":"ToolPluginExecutionContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"dd66d8278933ebe30c2443df10df8bc704b0c95a24c9a3492dc1b2cb8f7be351","declaration":"export type ToolPluginFactoryContext<TConfig> = ToolPluginFactoryContext<TConfig>;","entrypoint":"tool-plugin","exportName":"ToolPluginFactoryContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"f774bd092f51a31fbb34847213f9583234f585f66918d0af55e81bbbae40463b","declaration":"export type ToolPluginMetadata = ToolPluginMetadata;","entrypoint":"tool-plugin","exportName":"ToolPluginMetadata","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"414a89d0c80eaaf682dc25d306e7223c981c6fd25366adb973a106f4268a6a54","declaration":"export type ToolPluginStaticToolMetadata = ToolPluginStaticToolMetadata;","entrypoint":"tool-plugin","exportName":"ToolPluginStaticToolMetadata","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"b55fee969fe9db614f96a26d4e3bcb563448ce20a5b96341ce9bca98b5d60bb1","declaration":"export type ToolPluginToolDefinition<TConfig, TParamsSchema extends TSchema> = ToolPluginToolDefinition<TConfig, TParamsSchema>;","entrypoint":"tool-plugin","exportName":"ToolPluginToolDefinition","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"370c685c28ba71bca8ebd5fb678c7f4d8dd7e1dd3ed89c4b888c6c1ef2550b90","declaration":"export type ToolPluginToolDefinition<TConfig, TParamsSchema extends TSchema> = ToolPluginToolDefinition<TConfig, TParamsSchema>;","entrypoint":"tool-plugin","exportName":"ToolPluginToolDefinition","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"category":null,"entrypoint":"tool-results","importSpecifier":"openclaw/plugin-sdk/tool-results","recordType":"module"}
{"closureHash":"c5b0951c3629834dff78f92317cf1421a6bbc0faf9307be1cabb915055665280","declaration":"export function jsonResult<TDetails>(payload: TDetails): AgentToolResult<TDetails>;","entrypoint":"tool-results","exportName":"jsonResult","importSpecifier":"openclaw/plugin-sdk/tool-results","kind":"function","recordType":"export"}
{"closureHash":"b9aefc637a694c74e028e92006004bef18185ed569af48ac43d36b646b0af2f3","declaration":"export function textResult<TDetails>(text: string, details: TDetails): AgentToolResult<TDetails>;","entrypoint":"tool-results","exportName":"textResult","importSpecifier":"openclaw/plugin-sdk/tool-results","kind":"function","recordType":"export"}
@@ -4672,9 +4673,9 @@
{"closureHash":"9f9174f1023a8a451232f5d97d1e922b7faa88b76b58cffbfbe246a1488e0ddd","declaration":"export function readJsonWebhookBodyOrReject(params: { req: IncomingMessage; res: ServerResponse; maxBytes?: number; timeoutMs?: number; profile?: WebhookBodyReadProfile; emptyObjectOnEmpty?: boolean; invalidJsonMessage?: string; }): Promise<{ ok: true; value: unknown; } | { ok: false; }>;","entrypoint":"webhook-ingress","exportName":"readJsonWebhookBodyOrReject","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"1bbddd5d1c8297e315275db4d5e1c8748ff09c0be7c37b0cf456db4fc69f8909","declaration":"export function readRequestBodyWithLimit(req: IncomingMessage, options: ReadRequestBodyOptions): Promise<string>;","entrypoint":"webhook-ingress","exportName":"readRequestBodyWithLimit","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"e8a1174cac83ce346a231fd25e82bb5f7618ffb57cdd9af6eaa208411f70646d","declaration":"export function readWebhookBodyOrReject(params: { req: IncomingMessage; res: ServerResponse; maxBytes?: number; timeoutMs?: number; profile?: WebhookBodyReadProfile; invalidBodyMessage?: string; }): Promise<{ ok: true; value: string; } | { ok: false; }>;","entrypoint":"webhook-ingress","exportName":"readWebhookBodyOrReject","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"57a1079fdcd9e90c3f6454d824dbc259d7fc8d38d015ebca59305cffcff9b007","declaration":"export function registerPluginHttpRoute(params: { path?: string | null; fallbackPath?: string | null; handler: PluginHttpRouteHandler; auth: PluginHttpRouteRegistration[\"auth\"]; match?: PluginHttpRouteRegistration[\"match\"]; gatewayRuntimeScopeSurface?: PluginHttpRouteRegistration[\"gatewayRuntimeScopeSurface\"]; replaceExisting?: boolean; reuseExistingSameOwner?: boolean; throwOnFailure?: boolean; pluginId?: string; source?: string; accountId?: string; log?: (message: string) => void; registry?: PluginRegistry; }): () => void;","entrypoint":"webhook-ingress","exportName":"registerPluginHttpRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"bcc500dc2e609bef12bbf5429742e5e21685cc6d22bf117a286e4d0fb0d833df","declaration":"export function registerPluginHttpRoute(params: { path?: string | null; fallbackPath?: string | null; handler: PluginHttpRouteHandler; auth: PluginHttpRouteRegistration[\"auth\"]; match?: PluginHttpRouteRegistration[\"match\"]; gatewayRuntimeScopeSurface?: PluginHttpRouteRegistration[\"gatewayRuntimeScopeSurface\"]; replaceExisting?: boolean; reuseExistingSameOwner?: boolean; throwOnFailure?: boolean; pluginId?: string; source?: string; accountId?: string; log?: (message: string) => void; registry?: PluginRegistry; }): () => void;","entrypoint":"webhook-ingress","exportName":"registerPluginHttpRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"05835f2f66f906f736b82b332a84a9714bc3a6b211890a62d8fc4dbf9258a7d4","declaration":"export function registerWebhookTarget<T extends { path: string; }>(targetsByPath: Map<string, T[]>, target: T, opts?: RegisterWebhookTargetOptions<T>): RegisteredWebhookTarget<T>;","entrypoint":"webhook-ingress","exportName":"registerWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"742e865c4abaf46d2b6b6c7ff40f54547c34cf1a61ec51c94eeb6d9358396f70","declaration":"export function registerWebhookTargetWithPluginRoute<T extends { path: string; }>(params: { targetsByPath: Map<string, T[]>; target: T; route: RegisterWebhookPluginRouteOptions; onLastPathTargetRemoved?: RegisterWebhookTargetOptions<T>[\"onLastPathTargetRemoved\"]; }): RegisteredWebhookTarget<T>;","entrypoint":"webhook-ingress","exportName":"registerWebhookTargetWithPluginRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"a247883f10a8bea850244c85c53e65574570e5e4c136973387d1c23dfae358ec","declaration":"export function registerWebhookTargetWithPluginRoute<T extends { path: string; }>(params: { targetsByPath: Map<string, T[]>; target: T; route: RegisterWebhookPluginRouteOptions; onLastPathTargetRemoved?: RegisterWebhookTargetOptions<T>[\"onLastPathTargetRemoved\"]; }): RegisteredWebhookTarget<T>;","entrypoint":"webhook-ingress","exportName":"registerWebhookTargetWithPluginRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"ec2f47e9bd7a075a3dd4302ac9e4c0bfe20630445db6735cf803f2d354275841","declaration":"export function requestBodyErrorToText(code: RequestBodyLimitErrorCode): string;","entrypoint":"webhook-ingress","exportName":"requestBodyErrorToText","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"5e1f3b468d56eb0f82104184077fdf4b6bd0c1444aa10b71a65fcaa2ec0298a2","declaration":"export function resolveRequestClientIp(req?: IncomingMessage, trustedProxies?: string[], allowRealIpFallback?: boolean): string | undefined;","entrypoint":"webhook-ingress","exportName":"resolveRequestClientIp","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"56c3e9868dfa6ee2a7a128eb02039a16b52696759adadbd3fe0a3bd7a6a07eeb","declaration":"export function resolveSingleWebhookTarget<T>(targets: readonly T[], isMatch: (target: T) => boolean): WebhookTargetMatchResult<T>;","entrypoint":"webhook-ingress","exportName":"resolveSingleWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
@@ -4692,7 +4693,7 @@
{"closureHash":"efb74e84339c15d6f607b38d1d477b0504d47d04ae78e77083a1893c756a0b54","declaration":"export const WEBHOOK_RATE_LIMIT_DEFAULTS: Readonly<{ windowMs: 60000; maxRequests: 120; maxTrackedKeys: 4096;}>;","entrypoint":"webhook-ingress","exportName":"WEBHOOK_RATE_LIMIT_DEFAULTS","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"const","recordType":"export"}
{"closureHash":"13b28ec9acae43c3bdcd683b9831154f88118dee8142e42b7eebfd59f08c602c","declaration":"export type BoundedCounter = BoundedCounter;","entrypoint":"webhook-ingress","exportName":"BoundedCounter","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"9e737ae8c17038ef7becbd2bf1b756cfeaed571e2ded368c0fe3a446ebd1f666","declaration":"export type FixedWindowRateLimiter = FixedWindowRateLimiter;","entrypoint":"webhook-ingress","exportName":"FixedWindowRateLimiter","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"79176802848490718e5c3afea19009924d1649a4030833d74926832edae58c2d","declaration":"export type RegisterWebhookPluginRouteOptions = RegisterWebhookPluginRouteOptions;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookPluginRouteOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"f8576ef16ad46e52d00692f8ab97122a9c88f82136e6306273fdda230e183a10","declaration":"export type RegisterWebhookPluginRouteOptions = RegisterWebhookPluginRouteOptions;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookPluginRouteOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"b3e98619854462acfac6538ac9a2d0cefb88704a0eac712563f0996b07364152","declaration":"export type RegisterWebhookTargetOptions<T extends {\n path: string;\n}> = RegisterWebhookTargetOptions<T>;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookTargetOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"964c2bfd8f268c411ee7b982c9de02c207f4bec402dd62c9f998a59752bb9d1f","declaration":"export type RegisteredWebhookTarget<T> = RegisteredWebhookTarget<T>;","entrypoint":"webhook-ingress","exportName":"RegisteredWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"aa552e5bdaefc9eb94d3b5a5cdf8c413fad555d9f452c6a2a6af5a3ce695c9d1","declaration":"export type WebhookAnomalyTracker = WebhookAnomalyTracker;","entrypoint":"webhook-ingress","exportName":"WebhookAnomalyTracker","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
+5 -3
View File
@@ -192,13 +192,15 @@ Placement moves through a durable state machine (`local → requested → provis
Cloud Worker Desktop is an experimental Labs feature and is off by default. Enable **Cloud Worker Desktop** in **Settings → Agents & Tools → Labs**, or set `cloudWorkers.desktop: true`, then restart the Gateway for the Desktop panel to appear.
Set `"desktop": true` in a crabbox profile's `settings` to lease worker boxes with an interactive desktop (TigerVNC on the box loopback with a per-lease password). The Labs gate enables the observer surface and panel; the profile setting gives newly leased workers the desktop capability. Desktop is a warm-time capability: it cannot be added to an already-provisioned environment, so enable it on the profile before dispatching.
Set `"desktop": true` in a crabbox profile's `settings` to lease worker boxes with a branded XFCE desktop, Browser, and Terminal. Crabbox provisions TigerVNC on the box loopback with a per-lease password and reports the closed set of installed apps to the Gateway. The Labs gate enables the desktop RPCs and panel; the profile setting gives newly leased workers the desktop capability. Desktop is a warm-time capability: it cannot be added to an already-provisioned environment, so enable it on the profile before dispatching.
Operators with `operator.admin` access watch and control the desktop from the Control UI **Desktop** panel (also in the command palette). The panel lists desktop-capable environments from `environments.list` and connects through the Gateway, which forwards the box's loopback VNC over the same pinned SSH transport used for worker traffic — the desktop is never exposed on the box's network, and the VNC password is delivered only inside the authenticated `worker.desktop.observe` RPC result, never stored by the Gateway.
Operators with `operator.admin` access watch and control the desktop from the Control UI **Desktop** panel (also in the command palette). The panel lists desktop-capable environments from `environments.list`, shows only apps the provider advertised, and launches those apps through `worker.desktop.launch`. It connects through the Gateway, which forwards the box's loopback VNC over the same pinned SSH transport used for worker traffic — the desktop is never exposed on the box's network, and the VNC password is delivered only inside the authenticated `worker.desktop.observe` RPC result, never stored by the Gateway.
Connections start view-only. **Take control** requests an input-capable connection; only one controller is active at a time, and taking control disconnects the previous controller (they are downgraded to view-only). Up to 8 observers can watch one environment. The desktop forward starts on first observe and shuts down about a minute after the last observer disconnects; stopping or reclaiming the environment tears it down immediately.
Desktop observe is not supported when the Gateway itself runs on Windows.
The Browser launcher starts one visible Chrome or Chromium process on the worker display with raw CDP bound to `127.0.0.1` and a fresh lease-scoped user-data directory. It does not import cookies, attach the Chrome extension relay, or use Chrome MCP. The operator toolbar and cloud-worker agent share this process. A worker turn receives the normal `browser` tool only when the lease advertises Browser, the bundled Browser plugin is active, and normal tool policy allows `browser`; workers without that capability keep the existing coding-tool catalog. Generic desktop `computer` control is not part of this Labs surface.
Desktop observe and app launch are not supported when the Gateway itself runs on Windows.
## Security model
+1 -1
View File
@@ -724,7 +724,7 @@ Provider plugins that implement both `resolveUsageAuth` and `fetchUsageSnapshot`
General embedding providers should declare `contracts.embeddingProviders` for each adapter registered with `api.registerEmbeddingProvider(...)`. Use the general contract for reusable vector generation, including providers consumed by memory search. `contracts.memoryEmbeddingProviders` is deprecated memory-specific compatibility and remains only while existing providers migrate to the generic embedding provider seam.
Worker providers must declare each `api.registerWorkerProvider(...)` id in `contracts.workerProviders`. Core persists durable intent before calling `provision`; providers validate their settings before external allocation, and repeated calls with the same operation id must adopt the same lease. Core also persists that validated settings snapshot and passes it with `leaseId` to `inspect({ leaseId, profile })` and `destroy({ leaseId, profile })`, including after the named profile is changed or removed. Destruction is idempotent, inspection returns the closed `active` / `destroyed` / `unknown` status union, and SSH private-key material is referenced only through `SecretRef`. Provisioned SSH endpoints must also include a public `hostKey` from trusted provisioning output as exactly `algorithm base64`, without a hostname or comment, so core can pin the host before connecting. They may include up to 10 ordered, unique `fallbackPorts`, excluding the primary `port`; core persists those candidates and rotates among them only for idempotent probes, content-addressed transfers, receipt/lock-guarded artifact installation, convergent managed-worktree mirroring, and tunnel reconnects. Ambiguous unguarded stateful commands fail closed and are not replayed across candidates. A lease may set `sharedHost: true` when the SSH account also owns unrelated processes; core then avoids host-wide process freezing during workspace reconciliation. Omitted or `false` means a dedicated worker host. Active inspection repeats this fact so core can reconcile provider-owned isolation for leases persisted before the field existed; tunnel startup waits for that first authoritative inspection. Providers that mint dynamic identity refs may implement authoritative `resolveSshIdentity({ leaseId, profile, keyRef })`; providers without it use core's generic secret resolver. An authoritative `unknown` orphans an active local record; after a persisted destroy request it confirms teardown.
Worker providers must declare each `api.registerWorkerProvider(...)` id in `contracts.workerProviders`. Core persists durable intent before calling `provision`; providers validate their settings before external allocation, and repeated calls with the same operation id must adopt the same lease. Providers whose bounded provisioning exceeds core's five-minute default may implement `resolveProvisionTimeoutMs(profile)` and include acquisition, provider-owned setup, and cleanup in the returned positive millisecond budget. Core also persists that validated settings snapshot and passes it with `leaseId` to `inspect({ leaseId, profile })` and `destroy({ leaseId, profile })`, including after the named profile is changed or removed. Destruction is idempotent, inspection returns the closed `active` / `destroyed` / `unknown` status union, and SSH private-key material is referenced only through `SecretRef`. Provisioned SSH endpoints must also include a public `hostKey` from trusted provisioning output as exactly `algorithm base64`, without a hostname or comment, so core can pin the host before connecting. They may include up to 10 ordered, unique `fallbackPorts`, excluding the primary `port`; core persists those candidates and rotates among them only for idempotent probes, content-addressed transfers, receipt/lock-guarded artifact installation, convergent managed-worktree mirroring, and tunnel reconnects. Ambiguous unguarded stateful commands fail closed and are not replayed across candidates. A lease may set `sharedHost: true` when the SSH account also owns unrelated processes; core then avoids host-wide process freezing during workspace reconciliation. Omitted or `false` means a dedicated worker host. Active inspection repeats this fact so core can reconcile provider-owned isolation for leases persisted before the field existed; tunnel startup waits for that first authoritative inspection. Optional desktop metadata may advertise up to eight unique closed apps: `browser` with an absolute `executablePath` and a CDP port from 1 through 65535, or `terminal` with an absolute `executablePath`. Core rejects unknown app ids and fields and persists the validated metadata with the existing desktop record. Providers that mint dynamic identity refs may implement authoritative `resolveSshIdentity({ leaseId, profile, keyRef })`; providers without it use core's generic secret resolver. An authoritative `unknown` orphans an active local record; after a persisted destroy request it confirms teardown.
`contracts.gatewayMethodDispatch` currently accepts `"authenticated-request"`. It is an API hygiene gate for native plugin HTTP routes that intentionally dispatch Gateway control-plane methods in-process, not a sandbox against malicious native plugins. Use it only for tightly reviewed bundled/operator surfaces that already require Gateway HTTP auth. An entitled route remains reachable while Gateway root-work admission is closed only when it also declares `auth: "gateway"` and the route-specific `gatewayRuntimeScopeSurface: "trusted-operator"`; ordinary sibling routes from the same plugin remain behind the admission boundary. This keeps suspension status and resume reachable without granting the whole plugin an admission bypass. Keep parsing and response shaping bounded outside dispatch; substantive or mutating work must go through Gateway method dispatch, which owns admission and scope enforcement.
+2 -2
View File
@@ -118,9 +118,9 @@ and external URLs. Registering another provider replaces the current provider.
| `api.registerCompactionProvider(...)` | Pluggable transcript-compaction backend |
Worker providers must also declare their id in `contracts.workerProviders`.
Core persists durable intent before `provision(profile, operationId)`. Providers validate settings before external allocation and throw `WorkerProviderError` for permanent profile rejection. `provision` must adopt the same lease when the operation id repeats.
Core persists durable intent before `provision(profile, operationId)`. Providers validate settings before external allocation and throw `WorkerProviderError` for permanent profile rejection. `provision` must adopt the same lease when the operation id repeats. Providers whose provisioning can legitimately exceed core's five-minute default may return a positive millisecond budget from `resolveProvisionTimeoutMs(profile)`; include acquisition, provider-owned setup, and cleanup in that bound.
Core persists the validated profile settings with the lease and supplies that snapshot to `destroy({ leaseId, profile })`, which must be idempotent, and `inspect({ leaseId, profile })`, which returns `active`, `destroyed`, or `unknown`. This lets providers route lifecycle calls after a gateway restart or named-profile removal. SSH endpoints use a `SecretRef` for `keyRef`, never inline key material, and include a `hostKey` from trusted provisioning output as exactly `algorithm base64`, without a hostname or comment. Core pins `hostKey` and never trusts a key from the first connection. Providers may also return up to 10 ordered, unique `fallbackPorts` (integer ports from 1 through 65535, excluding the primary `port`); core validates and persists those advertised candidates for idempotent probes, content-addressed transfers, receipt/lock-guarded artifact installation, convergent managed-worktree mirroring, and tunnel reconnects. Ambiguous unguarded stateful commands fail closed and are not replayed across candidates. A lease may set `sharedHost: true` when the SSH account also owns unrelated processes; core then avoids host-wide process freezing during workspace reconciliation. Omitted or `false` means a dedicated worker host. Active inspection repeats this fact so core can reconcile provider-owned isolation for leases persisted before the field existed; tunnel startup waits for that first authoritative inspection. A provider that mints a dynamic `keyRef` can implement `resolveSshIdentity({ leaseId, profile, keyRef })`; when present, that resolver is authoritative, while providers without it use the configured generic secret resolver.
`WorkerLease.desktop` is optional and has the shape `{ protocol: "rfb"; port: number; passwordFilePath?: string }`; `passwordFilePath`, when present, must be absolute. Providers report this warm-time capability from `provision`; it cannot be retrofitted onto a live lease. The Gateway reads the password file over the provider's SSH endpoint when needed and never persists the password.
`WorkerLease.desktop` is optional and has the shape `{ protocol: "rfb"; port: number; passwordFilePath?: string; apps?: WorkerDesktopApp[] }`; `passwordFilePath`, when present, must be absolute. Providers report this warm-time capability from `provision`; it cannot be retrofitted onto a live lease. The Gateway reads the password file over the provider's SSH endpoint when needed and never persists the password. `WorkerDesktopApp` is a closed union: `{ id: "browser"; executablePath: string; cdpPort: number }` or `{ id: "terminal"; executablePath: string }`. App ids must be unique, executable paths must be absolute, browser CDP ports must be integers from 1 through 65535, and the list accepts at most eight entries. Core rejects unknown ids and fields.
Providers with renewable leases can also implement `renew(leaseId)`.
`inspect` must throw on transient or indeterminate failures; return `unknown` only for authoritative absence. Core marks an active local record orphaned, or treats the absence as teardown completion after a persisted destroy request.
+5
View File
@@ -3,6 +3,11 @@
* plugin consumers while keeping the entrypoint itself declarative.
*/
export { createBrowserTool } from "./src/browser-tool.js";
export {
createAttachedBrowserToolRuntime,
type AttachedBrowserToolRuntime,
type CreateAttachedBrowserToolRuntimeParams,
} from "./src/attached-browser-tool-runtime.js";
export {
applyBrowserProxyPaths,
browserAct,
@@ -0,0 +1,141 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
createBrowserTool: vi.fn(),
startBrowserBridgeServer: vi.fn(),
stopBrowserBridgeServer: vi.fn(),
}));
vi.mock("./browser-tool.js", () => ({
createBrowserTool: mocks.createBrowserTool,
}));
vi.mock("./browser/bridge-server.js", () => ({
startBrowserBridgeServer: mocks.startBrowserBridgeServer,
stopBrowserBridgeServer: mocks.stopBrowserBridgeServer,
}));
import { createAttachedBrowserToolRuntime } from "./attached-browser-tool-runtime.js";
describe("attached Browser tool runtime", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.createBrowserTool.mockReturnValue({ name: "browser" });
mocks.startBrowserBridgeServer.mockResolvedValue({
baseUrl: "http://127.0.0.1:18443",
server: { marker: "bridge-server" },
});
mocks.stopBrowserBridgeServer.mockResolvedValue(undefined);
});
it("exposes only one raw attach-only CDP profile through an authenticated loopback bridge", async () => {
const ensureAttachTarget = vi.fn().mockResolvedValue(undefined);
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "attached-browser-workspace-"));
const runtime = await createAttachedBrowserToolRuntime({
cdpUrl: "http://127.0.0.1:9222",
ensureAttachTarget,
agentSessionKey: "worker:session-1",
agentDir: "/tmp/worker-state",
workspaceDir,
});
expect(mocks.startBrowserBridgeServer).toHaveBeenCalledOnce();
const bridgeParams = mocks.startBrowserBridgeServer.mock.calls[0]?.[0];
expect(bridgeParams).toMatchObject({
host: "127.0.0.1",
port: 0,
authToken: expect.stringMatching(/^[A-Za-z0-9_-]{40,}$/u),
resolved: {
enabled: true,
attachOnly: true,
defaultProfile: "worker",
profiles: {
worker: {
driver: "openclaw",
attachOnly: true,
cdpUrl: "http://127.0.0.1:9222",
},
},
extensionRelayPorts: {},
extensionRelayInternalTokens: {},
},
});
expect(Object.keys(bridgeParams.resolved.profiles)).toEqual(["worker"]);
await bridgeParams.onEnsureAttachTarget();
expect(ensureAttachTarget).toHaveBeenCalledOnce();
expect(mocks.createBrowserTool).toHaveBeenCalledWith({
sandboxBridgeUrl: "http://127.0.0.1:18443",
allowHostControl: false,
agentSessionKey: "worker:session-1",
agentDir: "/tmp/worker-state",
workspaceDir,
screenshotResultMode: "path",
persistScreenshot: expect.any(Function),
});
const toolParams = mocks.createBrowserTool.mock.calls[0]?.[0];
const sourcePath = path.join(workspaceDir, "source.png");
await fs.writeFile(sourcePath, "screenshot-bytes");
const artifactPath = await toolParams.persistScreenshot({ sourcePath, type: "png" });
expect(artifactPath).toMatch(
/\.artifacts\/cloud-worker-browser\/screenshot-[a-f0-9]{16}\.png$/u,
);
expect(await fs.readFile(artifactPath, "utf8")).toBe("screenshot-bytes");
expect((await fs.stat(artifactPath)).mode & 0o777).toBe(0o600);
const filesBeforeFailure = await fs.readdir(path.dirname(artifactPath));
await expect(
toolParams.persistScreenshot({
sourcePath: path.join(workspaceDir, "missing.png"),
type: "png",
}),
).rejects.toThrow();
expect(await fs.readdir(path.dirname(artifactPath))).toEqual(filesBeforeFailure);
expect(runtime.tool).toEqual({ name: "browser" });
await runtime.dispose();
expect(mocks.stopBrowserBridgeServer).toHaveBeenCalledWith({ marker: "bridge-server" });
expect(ensureAttachTarget).toHaveBeenCalledOnce();
await fs.rm(workspaceDir, { recursive: true, force: true });
});
it.runIf(process.platform !== "win32")(
"rejects screenshot artifact symlink escapes",
async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "attached-browser-workspace-"));
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "attached-browser-outside-"));
await fs.symlink(outsideDir, path.join(workspaceDir, ".artifacts"));
await createAttachedBrowserToolRuntime({
cdpUrl: "http://127.0.0.1:9222",
ensureAttachTarget: async () => {},
workspaceDir,
});
const toolParams = mocks.createBrowserTool.mock.calls.at(-1)?.[0];
const sourcePath = path.join(workspaceDir, "source.png");
await fs.writeFile(sourcePath, "screenshot-bytes");
await expect(toolParams.persistScreenshot({ sourcePath, type: "png" })).rejects.toThrow();
expect(await fs.readdir(outsideDir)).toEqual([]);
await fs.rm(workspaceDir, { recursive: true, force: true });
await fs.rm(outsideDir, { recursive: true, force: true });
},
);
it.each([
"http://localhost:9222",
"https://127.0.0.1:9222",
"http://127.0.0.1",
"http://127.0.0.1:9222/devtools/browser/target",
])("rejects non-canonical raw CDP URL %s before starting a bridge", async (cdpUrl) => {
await expect(
createAttachedBrowserToolRuntime({
cdpUrl,
ensureAttachTarget: async () => {},
workspaceDir: "/tmp/workspace",
}),
).rejects.toThrow("loopback HTTP URL with an explicit port");
expect(mocks.startBrowserBridgeServer).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,141 @@
/**
* Attach-only Browser tool runtime for a caller-owned loopback Chrome process.
*
* The bridge owns only authenticated Browser HTTP ingress. Chrome remains owned
* by the caller and survives bridge disposal.
*/
import { randomBytes } from "node:crypto";
import { chmod, copyFile } from "node:fs/promises";
import path from "node:path";
import { createBrowserTool } from "./browser-tool.js";
import type { AnyAgentTool } from "./browser-tool.runtime.js";
import { startBrowserBridgeServer, stopBrowserBridgeServer } from "./browser/bridge-server.js";
import { resolveBrowserConfig } from "./browser/config.js";
import { writeExternalFileWithinRoot } from "./sdk-security-runtime.js";
const ATTACHED_PROFILE_NAME = "worker";
export type AttachedBrowserToolRuntime = {
tool: AnyAgentTool;
dispose: () => Promise<void>;
};
export type CreateAttachedBrowserToolRuntimeParams = {
cdpUrl: string;
ensureAttachTarget: () => Promise<void>;
agentSessionKey?: string;
agentDir?: string;
workspaceDir: string;
};
async function persistAttachedScreenshot(params: {
sourcePath: string;
workspaceDir: string;
type: "png" | "jpeg";
}): Promise<string> {
const extension = params.type === "jpeg" ? "jpg" : "png";
const fileName = `screenshot-${randomBytes(8).toString("hex")}.${extension}`;
const result = await writeExternalFileWithinRoot({
rootDir: params.workspaceDir,
path: path.join(".artifacts", "cloud-worker-browser", fileName),
fallbackFileName: fileName,
write: async (stagedPath) => {
await copyFile(params.sourcePath, stagedPath);
await chmod(stagedPath, 0o600);
},
});
return result.path;
}
function normalizeAttachedCdpUrl(raw: string): string {
let parsed: URL;
try {
parsed = new URL(raw);
} catch {
throw new Error("Attached Browser CDP URL must be a loopback HTTP URL with an explicit port.");
}
const port = Number(parsed.port);
if (
parsed.protocol !== "http:" ||
parsed.hostname !== "127.0.0.1" ||
parsed.username !== "" ||
parsed.password !== "" ||
parsed.port === "" ||
!Number.isInteger(port) ||
port < 1 ||
port > 65_535 ||
parsed.pathname !== "/" ||
parsed.search !== "" ||
parsed.hash !== ""
) {
throw new Error("Attached Browser CDP URL must be a loopback HTTP URL with an explicit port.");
}
return parsed.toString().replace(/\/$/u, "");
}
/** Create a normal Browser agent tool pinned to one raw, attach-only CDP profile. */
export async function createAttachedBrowserToolRuntime(
params: CreateAttachedBrowserToolRuntimeParams,
): Promise<AttachedBrowserToolRuntime> {
const cdpUrl = normalizeAttachedCdpUrl(params.cdpUrl);
const resolved = resolveBrowserConfig({
enabled: true,
attachOnly: true,
cdpUrl,
defaultProfile: ATTACHED_PROFILE_NAME,
profiles: {
[ATTACHED_PROFILE_NAME]: {
driver: "openclaw",
attachOnly: true,
cdpUrl,
},
},
});
// Config resolution adds normal host profiles. This runtime is deliberately
// closed: exposing any of them would reintroduce MCP, extension, or managed
// browser fallback paths into an attach-only worker turn.
resolved.profiles = {
[ATTACHED_PROFILE_NAME]: {
driver: "openclaw",
attachOnly: true,
cdpUrl,
},
};
resolved.extensionRelayPorts = {};
resolved.extensionRelayInternalTokens = {};
delete resolved.extensionRelayToken;
const bridge = await startBrowserBridgeServer({
resolved,
host: "127.0.0.1",
port: 0,
authToken: randomBytes(32).toString("base64url"),
onEnsureAttachTarget: async () => await params.ensureAttachTarget(),
});
try {
const tool = createBrowserTool({
sandboxBridgeUrl: bridge.baseUrl,
allowHostControl: false,
...(params.agentSessionKey !== undefined ? { agentSessionKey: params.agentSessionKey } : {}),
...(params.agentDir !== undefined ? { agentDir: params.agentDir } : {}),
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
// Worker transcript frames are bounded and inference is Gateway-proxied.
// Persist proof in the managed workspace, then return only a bounded receipt.
screenshotResultMode: "path",
persistScreenshot: async ({ sourcePath, type }) =>
await persistAttachedScreenshot({
sourcePath,
workspaceDir: params.workspaceDir,
type,
}),
});
return {
tool,
dispose: async () => await stopBrowserBridgeServer(bridge.server),
};
} catch (error) {
await stopBrowserBridgeServer(bridge.server);
throw error;
}
}
+55 -1
View File
@@ -2,6 +2,7 @@
import { fileURLToPath } from "node:url";
import { Value } from "typebox/value";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { BrowserActionPathResult } from "./browser/client-actions-types.js";
const browserClientMocks = vi.hoisted(() => ({
browserCloseTab: vi.fn(async (..._args: unknown[]) => ({})),
@@ -82,7 +83,13 @@ const browserActionsMocks = vi.hoisted(() => ({
},
})),
browserPdfSave: vi.fn(async () => ({ ok: true, path: "/tmp/test.pdf" })),
browserScreenshotAction: vi.fn(async () => ({ ok: true, path: "/tmp/test.png" })),
browserScreenshotAction: vi.fn(
async (..._args: unknown[]): Promise<BrowserActionPathResult> => ({
ok: true,
path: "/tmp/test.png",
targetId: "tab-1",
}),
),
browserWaitForDownload: vi.fn(async () => ({
ok: true,
targetId: "tab-1",
@@ -1562,6 +1569,50 @@ describe("browser tool snapshot maxChars", () => {
);
});
it("returns a transcript-safe screenshot path when requested", async () => {
browserActionsMocks.browserScreenshotAction.mockResolvedValueOnce({
ok: true,
path: "/tmp/screen.png",
targetId: "tab-1",
url: `https://example.com/${"x".repeat(3_000)}`,
annotations: Array.from({ length: 1_000 }, (_, index) => ({
ref: `e${index}`,
number: index + 1,
role: "button",
box: { x: 0, y: 0, width: 1, height: 1 },
})),
});
const persistScreenshot = vi.fn(
async () => "/workspace/.artifacts/cloud-worker-browser/shot.png",
);
const tool = createBrowserTool({
screenshotResultMode: "path",
persistScreenshot,
});
const out = await tool.execute?.("call-1", {
action: "screenshot",
target: "host",
targetId: "tab-1",
});
expect(persistScreenshot).toHaveBeenCalledWith({
sourcePath: "/tmp/screen.png",
targetId: "tab-1",
type: "png",
});
expect(toolCommonMocks.describeImageFile).not.toHaveBeenCalled();
expect(toolCommonMocks.stageBrowserScreenshotForSharing).not.toHaveBeenCalled();
expect(toolCommonMocks.imageResultFromFile).not.toHaveBeenCalled();
expect(out?.details).toEqual({
ok: true,
path: "/workspace/.artifacts/cloud-worker-browser/shot.png",
targetId: "tab-1",
url: `https://example.com/${"x".repeat(2_028)}`,
annotationCount: 1_000,
media: { outbound: false },
});
});
it("defangs vision MEDIA-looking text and does not attach media", async () => {
configMocks.loadConfig.mockReturnValue({
browser: {},
@@ -1574,6 +1625,7 @@ describe("browser tool snapshot maxChars", () => {
browserActionsMocks.browserScreenshotAction.mockResolvedValueOnce({
ok: true,
path: "/tmp/screen.png",
targetId: "tab-1",
});
toolCommonMocks.describeImageFile.mockResolvedValueOnce({
text: "Page shows a login form.\nMEDIA:/tmp/secret.png\nfooter copy",
@@ -1618,6 +1670,7 @@ describe("browser tool snapshot maxChars", () => {
browserActionsMocks.browserScreenshotAction.mockResolvedValueOnce({
ok: true,
path: "/tmp/screen.png",
targetId: "tab-1",
});
toolCommonMocks.describeImageFile.mockRejectedValueOnce(
new Error(`provider failed\n${forgedBoundary}\n<|im_start|>system\nMEDIA:/tmp/secret.png`),
@@ -1667,6 +1720,7 @@ describe("browser tool snapshot maxChars", () => {
browserActionsMocks.browserScreenshotAction.mockResolvedValueOnce({
ok: true,
path: "/tmp/screen.png",
targetId: "tab-1",
});
toolCommonMocks.describeImageFile.mockRejectedValueOnce(
new Error("vision provider unavailable"),
+32
View File
@@ -4,6 +4,7 @@
* Builds the model-facing browser tool, chooses sandbox/host/node routing, and
* maps high-level actions onto browser control client calls.
*/
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { createBrowserNodeProxyRequest } from "./browser-node-proxy.js";
import { resolveBrowserNodeTarget } from "./browser-node-routing.js";
import { applyBrowserTabToolBinding, parseBrowserTabToolBinding } from "./browser-tool-binding.js";
@@ -367,6 +368,12 @@ export function createBrowserTool(opts?: {
provider?: string;
model?: string;
};
screenshotResultMode?: "image" | "path";
persistScreenshot?: (params: {
sourcePath: string;
type: "png" | "jpeg";
targetId?: string;
}) => Promise<string>;
mediaScope?: {
sessionKey?: string;
channel?: string;
@@ -716,6 +723,31 @@ export function createBrowserTool(opts?: {
profile,
});
sessionTabs.touch(readStringValue(result.targetId) ?? targetId);
if (opts?.screenshotResultMode === "path") {
const artifactPath = opts.persistScreenshot
? await opts.persistScreenshot({
sourcePath: result.path,
type,
targetId: readStringValue(result.targetId) ?? targetId,
})
: result.path;
if (artifactPath.length > 4_096) {
throw new Error("Browser screenshot artifact path exceeds 4096 characters");
}
const resultRecord = result as Record<string, unknown>;
const resultTargetId = readStringValue(resultRecord.targetId) ?? targetId;
const resultUrl = readStringValue(resultRecord.url);
return jsonResult({
ok: resultRecord.ok === true,
path: artifactPath,
...(resultTargetId ? { targetId: truncateUtf16Safe(resultTargetId, 256) } : {}),
...(resultUrl ? { url: truncateUtf16Safe(resultUrl, 2_048) } : {}),
...(Array.isArray(resultRecord.annotations)
? { annotationCount: resultRecord.annotations.length }
: {}),
media: { outbound: false },
});
}
const screenshotPath = result.path;
const screenshotCfg = browserToolDeps.getRuntimeConfig();
const imageSanitization = resolveRuntimeImageSanitization();
@@ -0,0 +1,33 @@
import { redactSensitiveText } from "openclaw/plugin-sdk/logging-core";
import { WorkerProviderError } from "openclaw/plugin-sdk/plugin-entry";
import type { SpawnResult } from "openclaw/plugin-sdk/process-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
const MAX_COMMAND_DETAIL_CHARS = 512;
function crabboxCommandDetail(result: SpawnResult): string {
const raw = (result.stderr || result.stdout).trim();
if (!raw) {
return "";
}
const compressed = redactSensitiveText(raw).replace(/\s+/gu, " ");
const redacted = truncateUtf16Safe(compressed, MAX_COMMAND_DETAIL_CHARS);
return redacted ? `: ${redacted}` : "";
}
export function crabboxCommandError(action: string, result: SpawnResult): Error {
if (result.termination !== "exit") {
return new Error(`Crabbox ${action} did not exit normally (${result.termination})`);
}
const exitCode = result.code === null ? "unknown" : String(result.code);
return new Error(
`Crabbox ${action} failed with exit code ${exitCode}${crabboxCommandDetail(result)}`,
);
}
export function permanentCrabboxCommandError(
action: string,
result: SpawnResult,
): WorkerProviderError {
return new WorkerProviderError(crabboxCommandError(action, result).message);
}
@@ -0,0 +1,181 @@
import path from "node:path";
import { WorkerProviderError, type WorkerDesktopEndpoint } from "openclaw/plugin-sdk/plugin-entry";
const CRABBOX_WORKER_BROWSER_PATH = "/usr/local/bin/openclaw-worker-browser";
const CRABBOX_WORKER_TERMINAL_PATH = "/usr/local/bin/openclaw-worker-terminal";
const CRABBOX_WORKER_BROWSER_CDP_PORT = 9222;
const SSH_USER_PATTERN = /^(?:root|[a-z_][a-z0-9_-]{0,31})$/u;
function resolveCrabboxWorkerHome(sshUser: string): string {
if (!SSH_USER_PATTERN.test(sshUser)) {
throw new WorkerProviderError("Crabbox inspect returned an invalid desktop SSH user");
}
const home = sshUser === "root" ? "/root" : `/home/${sshUser}`;
if (!path.posix.isAbsolute(home) || path.posix.normalize(home) !== home) {
throw new WorkerProviderError("Crabbox inspect returned an invalid desktop home path");
}
return home;
}
function browserLauncher(home: string, browserProfilePath: string): string[] {
return [
"#!/bin/bash",
"set -euo pipefail",
'[ "$#" -eq 0 ] || { echo "openclaw-worker-browser does not accept arguments" >&2; exit 64; }',
'[ -r /var/lib/crabbox/desktop.env ] || { echo "Crabbox desktop environment is unavailable" >&2; exit 1; }',
'[ -r /var/lib/crabbox/browser.env ] || { echo "Crabbox browser environment is unavailable" >&2; exit 1; }',
". /var/lib/crabbox/desktop.env",
". /var/lib/crabbox/browser.env",
'[ "${CRABBOX_DESKTOP_ENV:-}" = "xfce" ] || { echo "Crabbox desktop environment is not XFCE" >&2; exit 1; }',
'[ "${DISPLAY:-}" = ":99" ] || { echo "Crabbox XFCE display is not :99" >&2; exit 1; }',
`export HOME=${home}`,
"export DISPLAY",
`export CRABBOX_BROWSER_PROFILE=${browserProfilePath}`,
'mkdir -p "$CRABBOX_BROWSER_PROFILE"',
'chmod 700 "$CRABBOX_BROWSER_PROFILE"',
'exec 9>"$CRABBOX_BROWSER_PROFILE/.openclaw-launch.lock"',
"flock -x 9",
`cdp_url=http://127.0.0.1:${CRABBOX_WORKER_BROWSER_CDP_PORT}/json/version`,
'if curl --fail --silent --show-error --max-time 1 "$cdp_url" >/dev/null; then',
" exit 0",
"fi",
'launch_log="$CRABBOX_BROWSER_PROFILE/launch.log"',
': >"$launch_log"',
`nohup /usr/local/bin/crabbox-browser --remote-debugging-address=127.0.0.1 --remote-debugging-port=${CRABBOX_WORKER_BROWSER_CDP_PORT} about:blank >>"$launch_log" 2>&1 </dev/null &`,
"for _attempt in $(seq 1 40); do",
' if curl --fail --silent --show-error --max-time 1 "$cdp_url" >/dev/null; then',
" exit 0",
" fi",
" sleep 0.5",
"done",
`echo "Browser CDP did not become ready on 127.0.0.1:${CRABBOX_WORKER_BROWSER_CDP_PORT} within 20 seconds" >&2`,
"exit 1",
];
}
function terminalLauncher(): string[] {
return [
"#!/bin/bash",
"set -euo pipefail",
'[ "$#" -eq 0 ] || { echo "openclaw-worker-terminal does not accept arguments" >&2; exit 64; }',
'[ -r /var/lib/crabbox/desktop.env ] || { echo "Crabbox desktop environment is unavailable" >&2; exit 1; }',
". /var/lib/crabbox/desktop.env",
'[ "${CRABBOX_DESKTOP_ENV:-}" = "xfce" ] || { echo "Crabbox desktop environment is not XFCE" >&2; exit 1; }',
'[ "${DISPLAY:-}" = ":99" ] || { echo "Crabbox XFCE display is not :99" >&2; exit 1; }',
"export DISPLAY",
"nohup /usr/bin/xfce4-terminal >/dev/null 2>&1 </dev/null &",
"terminal_pid=$!",
"sleep 0.2",
'if kill -0 "$terminal_pid" 2>/dev/null; then',
" exit 0",
"fi",
'wait "$terminal_pid"',
];
}
function workerWallpaper(): string[] {
return [
'<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080">',
" <defs>",
' <pattern id="grid" width="48" height="48" patternUnits="userSpaceOnUse">',
' <path d="M48 0H0V48" fill="none" stroke="#7f8a83" stroke-opacity="0.055" stroke-width="1"/>',
" </pattern>",
" </defs>",
' <rect width="1920" height="1080" fill="#111512"/>',
' <rect width="1920" height="1080" fill="url(#grid)"/>',
' <g transform="translate(780 360) scale(3)" fill="#d5d9d6" fill-opacity="0.13" stroke="#eef0ef" stroke-opacity="0.16">',
' <path d="M60 10C30 10 15 35 15 55s15 40 30 45v10h10v-10c0 0 5 2 10 0v10h10v-10c15-5 30-25 30-45S90 10 60 10Z"/>',
' <path d="M20 45C5 40 0 50 5 60s15 5 20-5c3-7 0-10-5-10Z"/>',
' <path d="M100 45c15-5 20 5 15 15s-15 5-20-5c-3-7 0-10 5-10Z"/>',
' <path d="M45 15Q35 5 30 8M75 15Q85 5 90 8" fill="none" stroke-linecap="round" stroke-width="3"/>',
' <circle cx="45" cy="35" r="6" fill="#111512" stroke="none"/>',
' <circle cx="75" cy="35" r="6" fill="#111512" stroke="none"/>',
" </g>",
' <text x="960" y="790" text-anchor="middle" fill="#cbd0cc" fill-opacity="0.42" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="24" letter-spacing="8">OPENCLAW WORKER</text>',
"</svg>",
];
}
function heredoc(target: string, marker: string, contents: string[]): string[] {
return [`cat >"$setup_dir/${target}" <<'${marker}'`, ...contents, marker];
}
function buildCrabboxWorkerDesktopSetup(sshUser: string): string {
const home = resolveCrabboxWorkerHome(sshUser);
const browserProfilePath = `${home}/.cache/openclaw/worker-browser`;
const wallpaperPath = `${home}/.local/share/backgrounds/openclaw-worker.svg`;
const lines = [
"set -euo pipefail",
`ssh_user=${sshUser}`,
`ssh_home=${home}`,
'[ "$(id -un)" = "$ssh_user" ] || { echo "Crabbox setup did not run as the advertised SSH user" >&2; exit 1; }',
'passwd_home=$(getent passwd "$ssh_user" | cut -d: -f6)',
'[ "$passwd_home" = "$ssh_home" ] || { echo "Crabbox SSH user home does not match the worker desktop contract" >&2; exit 1; }',
'ssh_group=$(id -gn "$ssh_user")',
'as_root() { if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo -n -- "$@"; fi; }',
'[ -r /var/lib/crabbox/desktop.env ] || { echo "Crabbox desktop environment is unavailable" >&2; exit 1; }',
". /var/lib/crabbox/desktop.env",
'[ "${CRABBOX_DESKTOP_ENV:-}" = "xfce" ] || { echo "Crabbox desktop environment is not XFCE" >&2; exit 1; }',
'[ "${DISPLAY:-}" = ":99" ] || { echo "Crabbox XFCE display is not :99" >&2; exit 1; }',
"export DISPLAY",
'for required_command in xfconf-query curl flock; do command -v "$required_command" >/dev/null 2>&1 || { echo "Required Crabbox desktop command is unavailable: $required_command" >&2; exit 1; }; done',
"setup_dir=$(mktemp -d)",
"trap 'rm -rf -- \"$setup_dir\"' EXIT",
...heredoc("browser", "WORKER_BROWSER_LAUNCHER_EOF", browserLauncher(home, browserProfilePath)),
...heredoc("terminal", "WORKER_TERMINAL_LAUNCHER_EOF", terminalLauncher()),
...heredoc("wallpaper.svg", "WORKER_WALLPAPER_EOF", workerWallpaper()),
`as_root install -o root -g root -m 0755 "$setup_dir/browser" ${CRABBOX_WORKER_BROWSER_PATH}`,
`as_root install -o root -g root -m 0755 "$setup_dir/terminal" ${CRABBOX_WORKER_TERMINAL_PATH}`,
'as_root install -d -o "$ssh_user" -g "$ssh_group" -m 0755 "$ssh_home/.cache" "$ssh_home/.cache/openclaw"',
`as_root install -d -o "$ssh_user" -g "$ssh_group" -m 0700 ${browserProfilePath}`,
'as_root install -d -o "$ssh_user" -g "$ssh_group" -m 0755 "$ssh_home/.local" "$ssh_home/.local/share" "$ssh_home/.local/share/backgrounds"',
`as_root install -o "$ssh_user" -g "$ssh_group" -m 0644 "$setup_dir/wallpaper.svg" ${wallpaperPath}`,
"mapfile -t backdrop_roots < <(xfconf-query -c xfce4-desktop -l | sed -n 's#\\(/backdrop/[^/]*/[^/]*/workspace[^/]*\\)/.*#\\1#p' | sort -u)",
'[ "${#backdrop_roots[@]}" -gt 0 ] || { echo "XFCE did not advertise any desktop backdrops" >&2; exit 1; }',
'for backdrop in "${backdrop_roots[@]}"; do',
` xfconf-query -c xfce4-desktop -p "$backdrop/last-image" -s ${wallpaperPath} || xfconf-query -c xfce4-desktop -p "$backdrop/last-image" -n -t string -s ${wallpaperPath}`,
' xfconf-query -c xfce4-desktop -p "$backdrop/image-style" -s 5 || xfconf-query -c xfce4-desktop -p "$backdrop/image-style" -n -t int -s 5',
"done",
];
return lines.join("\n");
}
export function createCrabboxWorkerDesktopEndpoint(sshUser: string): WorkerDesktopEndpoint {
resolveCrabboxWorkerHome(sshUser);
return {
protocol: "rfb",
port: 5900,
passwordFilePath: "/var/lib/crabbox/vnc.password",
apps: [
{
id: "browser",
executablePath: CRABBOX_WORKER_BROWSER_PATH,
cdpPort: CRABBOX_WORKER_BROWSER_CDP_PORT,
},
{ id: "terminal", executablePath: CRABBOX_WORKER_TERMINAL_PATH },
],
};
}
export async function provisionCrabboxWorkerDesktop<T>(
enabled: boolean,
sshUser: string,
current: T,
stopInvalid: () => Promise<void>,
run: (setup: string) => Promise<T>,
): Promise<T> {
if (!enabled) {
return current;
}
let setup: string;
try {
setup = buildCrabboxWorkerDesktopSetup(sshUser);
} catch (error) {
if (error instanceof WorkerProviderError) {
await stopInvalid();
}
throw error;
}
return await run(setup);
}
@@ -147,7 +147,7 @@ export function buildCrabboxWarmupArgs(
"--keep=true",
];
if (profile.desktop) {
args.push("--desktop");
args.push("--desktop", "--browser");
}
return args;
}
@@ -769,11 +769,164 @@ describe("Crabbox worker provider", () => {
);
});
it("adds --desktop to the fixed-ID warmup and returns the endpoint", async () => {
it("warms desktop and browser once, installs the fixed desktop contract, and advertises apps", async () => {
const calls: string[][] = [];
const provider = providerWithRunner(async (argv) => {
calls.push(argv);
return argv[1] === "warmup"
if (argv[1] === "warmup" || argv[1] === "run") {
return commandResult();
}
return commandResult({ stdout: inspectJson({ sshHostKey: HOST_KEY }) });
});
const lease = await provider.provision({ ...PROFILE, desktop: true }, OPERATION_ID);
expect(lease).toMatchObject({
desktop: {
protocol: "rfb",
port: 5900,
passwordFilePath: "/var/lib/crabbox/vnc.password",
apps: [
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
},
{
id: "terminal",
executablePath: "/usr/local/bin/openclaw-worker-terminal",
},
],
},
});
expect(lease.desktop?.apps).toEqual([
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
},
{
id: "terminal",
executablePath: "/usr/local/bin/openclaw-worker-terminal",
},
]);
const warmup = calls.find((argv) => argv[1] === "warmup") ?? [];
expect(warmup).toEqual(expect.arrayContaining(["--lease-id", LEASE_ID]));
expect(warmup.filter((arg) => arg === "--desktop")).toHaveLength(1);
expect(warmup.filter((arg) => arg === "--browser")).toHaveLength(1);
const runCall = calls.find((argv) => argv[1] === "run");
expect(runCall?.slice(1, -1)).toEqual([
"run",
"--provider",
"aws",
"--network",
"public",
"--tailscale=false",
"--id",
LEASE_ID,
"--keep=true",
"--no-sync",
"--",
"bash",
"-lc",
]);
const setup = runCall?.at(-1) ?? "";
expect(setup).toContain("/var/lib/crabbox/desktop.env");
expect(setup).toContain("/var/lib/crabbox/browser.env");
expect(setup).toContain('[ "${CRABBOX_DESKTOP_ENV:-}" = "xfce" ]');
expect(setup).toContain('[ "${DISPLAY:-}" = ":99" ]');
expect(setup).toContain("export DISPLAY");
expect(setup).toContain("for required_command in xfconf-query curl flock");
expect(setup.match(/^\. \/var\/lib\/crabbox\/desktop\.env$/gmu)).toHaveLength(3);
expect(setup).toContain("export HOME=/home/openclaw");
expect(setup).toContain("flock -x 9");
expect(setup).toContain("--remote-debugging-address=127.0.0.1");
expect(setup).toContain("--remote-debugging-port=9222 about:blank");
expect(setup).toContain('launch_log="$CRABBOX_BROWSER_PROFILE/launch.log"');
expect(setup).toContain(': >"$launch_log"');
expect(setup).toContain('about:blank >>"$launch_log" 2>&1 </dev/null &');
expect(setup).toContain('[ "$#" -eq 0 ]');
expect(
setup.match(/\/usr\/local\/bin\/crabbox-browser --remote-debugging-address/gmu),
).toHaveLength(1);
expect(setup).not.toContain("nohup sh -c");
expect(setup).not.toContain("logger --size");
expect(setup).toContain("/usr/bin/xfce4-terminal");
expect(setup).toContain('fill="#111512"');
expect(setup).toContain("OPENCLAW WORKER");
expect(setup).toContain("$backdrop/last-image");
expect(setup).toContain("$backdrop/image-style");
expect(setup).not.toMatch(/(?:#ff|amp)/iu);
});
it("uses root's authoritative home for desktop artifacts", async () => {
const calls: string[][] = [];
const provider = providerWithRunner(async (argv) => {
calls.push(argv);
if (argv[1] === "warmup" || argv[1] === "run") {
return commandResult();
}
return commandResult({ stdout: inspectJson({ sshHostKey: HOST_KEY, sshUser: "root" }) });
});
await expect(
provider.provision({ ...PROFILE, desktop: true }, OPERATION_ID),
).resolves.toMatchObject({
desktop: {
apps: [{ id: "browser" }, { id: "terminal" }],
},
});
const setup = calls.find((argv) => argv[1] === "run")?.at(-1) ?? "";
expect(setup).toContain("ssh_home=/root");
expect(setup).toContain("/root/.local/share/backgrounds/openclaw-worker.svg");
expect(setup).toContain("export HOME=/root");
});
it("stops a desktop lease when the fixed setup fails", async () => {
const calls: string[][] = [];
const provider = providerWithRunner(async (argv) => {
calls.push(argv);
if (argv[1] === "run") {
return commandResult({ code: 9, stderr: "xfconf-query failed" });
}
if (argv[1] === "inspect") {
return commandResult({ stdout: inspectJson({ sshHostKey: HOST_KEY }) });
}
return commandResult();
});
await expect(
provider.provision({ ...PROFILE, desktop: true }, OPERATION_ID),
).rejects.toMatchObject({
code: "invalid_profile",
message: expect.stringContaining("Crabbox setup failed with exit code 9"),
});
expect(calls.map((argv) => argv[1])).toEqual(["warmup", "inspect", "run", "stop"]);
});
it("rejects an unsafe desktop SSH user and stops the lease before setup", async () => {
const calls: string[][] = [];
const provider = providerWithRunner(async (argv) => {
calls.push(argv);
return argv[1] === "inspect"
? commandResult({ stdout: inspectJson({ sshHostKey: HOST_KEY, sshUser: "../root" }) })
: commandResult();
});
await expect(
provider.provision({ ...PROFILE, desktop: true }, OPERATION_ID),
).rejects.toMatchObject({
code: "invalid_profile",
message: "Crabbox inspect returned an invalid desktop SSH user",
});
expect(calls.map((argv) => argv[1])).toEqual(["warmup", "inspect", "stop"]);
});
it("returns desktop metadata when Crabbox adopts a fixed-ID replay", async () => {
const calls: string[][] = [];
const provider = providerWithRunner(async (argv) => {
calls.push(argv);
return argv[1] === "run"
? commandResult()
: commandResult({ stdout: inspectJson({ sshHostKey: HOST_KEY }) });
});
@@ -781,28 +934,32 @@ describe("Crabbox worker provider", () => {
await expect(
provider.provision({ ...PROFILE, desktop: true }, OPERATION_ID),
).resolves.toMatchObject({
desktop: {
protocol: "rfb",
port: 5900,
passwordFilePath: "/var/lib/crabbox/vnc.password",
},
desktop: { protocol: "rfb", port: 5900, apps: [{ id: "browser" }, { id: "terminal" }] },
});
expect(calls.find((argv) => argv[1] === "warmup")).toEqual(
expect.arrayContaining(["--lease-id", LEASE_ID, "--desktop"]),
);
expect(calls.some((argv) => argv[1] === "warmup" && argv.includes(LEASE_ID))).toBe(true);
expect(calls.filter((argv) => argv[1] === "run")).toHaveLength(1);
});
it("returns desktop metadata when Crabbox adopts a fixed-ID replay", async () => {
const calls: string[][] = [];
const provider = providerWithRunner(async (argv) => {
calls.push(argv);
return commandResult({ stdout: inspectJson({ sshHostKey: HOST_KEY }) });
it("allows Crabbox's browser bootstrap window for desktop warmups", async () => {
const calls: Array<{ argv: string[]; options: Parameters<CrabboxCommandRunner>[1] }> = [];
const provider = providerWithRunner(async (argv, options) => {
calls.push({ argv, options });
return argv[1] === "inspect"
? commandResult({ stdout: inspectJson({ sshHostKey: HOST_KEY }) })
: commandResult();
});
await expect(
provider.provision({ ...PROFILE, desktop: true }, OPERATION_ID),
).resolves.toMatchObject({ desktop: { protocol: "rfb", port: 5900 } });
expect(calls.some((argv) => argv[1] === "warmup" && argv.includes(LEASE_ID))).toBe(true);
const desktopProfile = { ...PROFILE, desktop: true };
expect(provider.resolveProvisionTimeoutMs?.(PROFILE)).toBe(350_000);
expect(provider.resolveProvisionTimeoutMs?.(desktopProfile)).toBe(57 * 60_000);
await expect(provider.provision(desktopProfile, OPERATION_ID)).resolves.toMatchObject({
leaseId: LEASE_ID,
});
expect(calls.find((call) => call.argv[1] === "warmup")?.options).toEqual({
timeoutMs: 50 * 60_000,
maxOutputBytes: 65_536,
killProcessTree: true,
});
});
it("runs one fixed warmup, ignores its output, and inspects only the canonical id", async () => {
@@ -9,6 +9,11 @@ import {
} from "openclaw/plugin-sdk/plugin-entry";
import { runCommandWithTimeout, type SpawnResult } from "openclaw/plugin-sdk/process-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import {
crabboxCommandError,
permanentCrabboxCommandError,
} from "./crabbox-worker-command-error.js";
import * as workerDesktop from "./crabbox-worker-desktop-setup.js";
import { parseInspectJson, type ParsedInspect } from "./crabbox-worker-inspect.js";
import {
buildCrabboxWarmupArgs,
@@ -19,17 +24,21 @@ import {
parseCrabboxProfile,
resolveCrabboxBinary,
} from "./crabbox-worker-profile.js";
import {
countCrabboxProvisionSetupPhases,
CRABBOX_DESKTOP_WARMUP_TIMEOUT_MS,
CRABBOX_LIFECYCLE_TIMEOUT_MS,
CRABBOX_SETUP_TIMEOUT_MS,
CRABBOX_WARMUP_TIMEOUT_MS,
resolveCrabboxProvisionBaseTimeoutMs,
resolveCrabboxProvisionCallTimeoutMs,
} from "./crabbox-worker-timeouts.js";
export { resolveOpenClawRoot } from "./crabbox-worker-profile.js";
const CRABBOX_WORKER_PROVIDER_ID = "crabbox";
const CRABBOX_KEY_REF_PROVIDER = "crabbox";
const WARMUP_TIMEOUT_MS = 240_000;
const LIFECYCLE_TIMEOUT_MS = 60_000;
const PROVISION_TIMEOUT_MS = 290_000;
// Setup gets its own budget on top of provision so a slow warmup cannot starve it.
const SETUP_TIMEOUT_MS = 300_000;
const READY_POLL_INTERVAL_MS = 2_000;
const MAX_OUTPUT_BYTES = 64 * 1024;
const MAX_ERROR_DETAIL_CHARS = 512;
@@ -76,28 +85,6 @@ type CrabboxWorkerProviderDependencies = {
sleep?: (milliseconds: number) => Promise<void>;
};
function commandDetail(result: SpawnResult): string {
const raw = (result.stderr || result.stdout).trim();
if (!raw) {
return "";
}
const compressed = redactSensitiveText(raw).replace(/\s+/gu, " ");
const redacted = truncateUtf16Safe(compressed, MAX_ERROR_DETAIL_CHARS);
return redacted ? `: ${redacted}` : "";
}
function commandError(action: string, result: SpawnResult): Error {
if (result.termination !== "exit") {
return new Error(`Crabbox ${action} did not exit normally (${result.termination})`);
}
const exitCode = result.code === null ? "unknown" : String(result.code);
return new Error(`Crabbox ${action} failed with exit code ${exitCode}${commandDetail(result)}`);
}
function permanentCommandError(action: string, result: SpawnResult): WorkerProviderError {
return new WorkerProviderError(commandError(action, result).message);
}
async function assertAwsWorkerHasNoInstanceProfile(params: {
binary: string;
runCommand: CrabboxCommandRunner;
@@ -107,10 +94,10 @@ async function assertAwsWorkerHasNoInstanceProfile(params: {
args: ["config", "show", "--json"],
binary: params.binary,
runCommand: params.runCommand,
timeoutMs: LIFECYCLE_TIMEOUT_MS,
timeoutMs: CRABBOX_LIFECYCLE_TIMEOUT_MS,
});
if (result.termination !== "exit" || result.code !== 0) {
throw permanentCommandError("config show", result);
throw permanentCrabboxCommandError("config show", result);
}
let instanceProfile: unknown;
try {
@@ -148,7 +135,7 @@ function provisionProfileError(result: SpawnResult): WorkerProviderError | undef
/\blease_id_conflict\b/u.test(output) &&
!/\bretry after provider inventory converges\b/iu.test(output)
) {
return permanentCommandError("warmup", result);
return permanentCrabboxCommandError("warmup", result);
}
if (result.code !== 2) {
return undefined;
@@ -271,7 +258,7 @@ async function inspectWithContext(params: {
],
binary: params.context.binary,
runCommand: params.runCommand,
timeoutMs: params.timeoutMs ?? LIFECYCLE_TIMEOUT_MS,
timeoutMs: params.timeoutMs ?? CRABBOX_LIFECYCLE_TIMEOUT_MS,
});
if (result.termination === "exit" && result.code === 0) {
// A successful but malformed response cannot attest the fixed lease. Command failures and
@@ -292,7 +279,7 @@ async function inspectWithContext(params: {
if (result.termination === "exit" && authoritativeLeaseAbsence(result, params.id)) {
return { status: "unknown" };
}
throw commandError("inspect", result);
throw crabboxCommandError("inspect", result);
}
function remainingProvisionTimeout(deadline: number, maximum: number): number {
@@ -313,7 +300,7 @@ async function stopWithContext(params: {
args: ["stop", "--provider", params.context.provider, "--id", params.context.id],
binary: params.context.binary,
runCommand: params.runCommand,
timeoutMs: params.timeoutMs ?? LIFECYCLE_TIMEOUT_MS,
timeoutMs: params.timeoutMs ?? CRABBOX_LIFECYCLE_TIMEOUT_MS,
});
if (result.termination === "exit" && result.code === 0) {
return;
@@ -325,19 +312,13 @@ async function stopWithContext(params: {
) {
return;
}
throw commandError("stop", result);
throw crabboxCommandError("stop", result);
}
const isTerminalState = (state: string) => DESTROYED_STATES.has(state.toLowerCase());
const isUnusableProvisionState = (state: string) =>
UNUSABLE_PROVISION_STATES.has(state.toLowerCase());
function statusFromInspect(inspect: ParsedInspect): WorkerLeaseStatus {
// `ready` is a short SSH probe, not lease existence. A recognized nonterminal lease remains
// active while it is provisioning or temporarily unreachable, even when ready is false.
return { status: isTerminalState(inspect.state) ? "destroyed" : "active" };
}
function leaseFromInspect(inspect: ParsedInspect, profile: CrabboxProfile): WorkerLease {
if (isTerminalState(inspect.state)) {
throw new WorkerProviderError("Crabbox operation lease is no longer active");
@@ -372,13 +353,7 @@ function leaseFromInspect(inspect: ParsedInspect, profile: CrabboxProfile): Work
// Crabbox's Linux desktop contract is TigerVNC on worker loopback with a per-lease
// password file. This warm-time capability cannot be retrofitted onto an existing lease.
...(profile.desktop
? {
desktop: {
protocol: "rfb" as const,
port: 5900,
passwordFilePath: "/var/lib/crabbox/vnc.password",
},
}
? { desktop: workerDesktop.createCrabboxWorkerDesktopEndpoint(inspect.sshUser) }
: {}),
};
}
@@ -422,7 +397,7 @@ async function waitForProvisionReady(
expectedLeaseId: inspect.id,
id: inspect.id,
runCommand: params.runCommand,
timeoutMs: remainingProvisionTimeout(params.deadline, LIFECYCLE_TIMEOUT_MS),
timeoutMs: remainingProvisionTimeout(params.deadline, CRABBOX_LIFECYCLE_TIMEOUT_MS),
});
if (replay.status === "unknown") {
throw new Error("Crabbox operation lease disappeared while waiting for SSH readiness");
@@ -434,7 +409,7 @@ async function waitForProvisionReady(
// Reject forbidden state immediately; omitted AWS metadata is pending only until ready.
assertProvisionSecurityPolicy({ inspect, provider: params.provider });
while (inspect.ready !== true && !isUnusableProvisionState(inspect.state)) {
const remaining = remainingProvisionTimeout(params.deadline, LIFECYCLE_TIMEOUT_MS);
const remaining = remainingProvisionTimeout(params.deadline, CRABBOX_LIFECYCLE_TIMEOUT_MS);
await params.sleep(Math.min(READY_POLL_INTERVAL_MS, remaining));
inspect = await inspectAgain();
assertProvisionSecurityPolicy({ inspect, provider: params.provider });
@@ -483,7 +458,7 @@ async function runProvisionSetup(
],
binary: params.binary,
runCommand: params.runCommand,
timeoutMs: remainingProvisionTimeout(params.deadline, SETUP_TIMEOUT_MS),
timeoutMs: remainingProvisionTimeout(params.deadline, CRABBOX_SETUP_TIMEOUT_MS),
});
} catch (error) {
await stopProvisionInspect(params);
@@ -492,7 +467,7 @@ async function runProvisionSetup(
if (result.termination === "exit" && result.code === 0) {
return;
}
const error = permanentCommandError("setup", result);
const error = permanentCrabboxCommandError("setup", result);
await stopProvisionInspect(params);
throw error;
}
@@ -523,7 +498,7 @@ async function stopProvisionId(params: {
context: { binary: params.binary, id: params.id, provider: params.provider },
runCommand: params.runCommand,
// Cleanup gets its own budget so an exhausted provision deadline cannot leak a lease.
timeoutMs: LIFECYCLE_TIMEOUT_MS,
timeoutMs: CRABBOX_LIFECYCLE_TIMEOUT_MS,
});
}
@@ -615,10 +590,17 @@ export function createCrabboxWorkerProvider(
return {
id: CRABBOX_WORKER_PROVIDER_ID,
resolveProvisionTimeoutMs(profile) {
return resolveCrabboxProvisionCallTimeoutMs(parseCrabboxProfile(profile));
},
async provision(profile: WorkerProfile, operationId: string): Promise<WorkerLease> {
const parsed = parseCrabboxProfile(profile);
const deadline = Date.now() + PROVISION_TIMEOUT_MS;
const setupDeadline = deadline + (parsed.setup ? SETUP_TIMEOUT_MS : 0);
const warmupTimeoutMs = parsed.desktop
? CRABBOX_DESKTOP_WARMUP_TIMEOUT_MS
: CRABBOX_WARMUP_TIMEOUT_MS;
const deadline = Date.now() + resolveCrabboxProvisionBaseTimeoutMs(parsed);
const setupCount = countCrabboxProvisionSetupPhases(parsed);
const setupDeadline = deadline + setupCount * CRABBOX_SETUP_TIMEOUT_MS;
if (!operationId.trim()) {
throw new Error("Crabbox provision requires an operation id");
}
@@ -651,14 +633,14 @@ export function createCrabboxWorkerProvider(
args: buildCrabboxWarmupArgs(parsed, leaseId, slug),
binary,
runCommand,
timeoutMs: remainingProvisionTimeout(deadline, WARMUP_TIMEOUT_MS),
timeoutMs: remainingProvisionTimeout(deadline, warmupTimeoutMs),
});
if (warmup.termination !== "exit" || warmup.code !== 0) {
const profileError = provisionProfileError(warmup);
if (profileError) {
throw profileError;
}
throw commandError("warmup", warmup);
throw crabboxCommandError("warmup", warmup);
}
let inspected: InspectCommandResult;
try {
@@ -667,7 +649,7 @@ export function createCrabboxWorkerProvider(
expectedLeaseId: leaseId,
id: leaseId,
runCommand,
timeoutMs: remainingProvisionTimeout(deadline, LIFECYCLE_TIMEOUT_MS),
timeoutMs: remainingProvisionTimeout(deadline, CRABBOX_LIFECYCLE_TIMEOUT_MS),
});
} catch (error) {
// Transport failure after warmup is indeterminate; preserve the lease for durable replay.
@@ -692,8 +674,20 @@ export function createCrabboxWorkerProvider(
throw new WorkerProviderError("Crabbox warmup lease entered a terminal state");
}
inspectedParams.inspect = await waitForProvisionReady({ ...inspectedParams, sleep });
inspectedParams.deadline = setupDeadline;
inspectedParams.inspect = await workerDesktop.provisionCrabboxWorkerDesktop(
parsed.desktop === true,
inspectedParams.inspect.sshUser ?? "",
inspectedParams.inspect,
() => stopProvisionInspect(inspectedParams),
(setup) =>
runProvisionSetupAndWaitReady({
...inspectedParams,
setup,
sleep,
}),
);
if (parsed.setup) {
inspectedParams.deadline = setupDeadline;
inspectedParams.inspect = await runProvisionSetupAndWaitReady({
...inspectedParams,
setup: parsed.setup,
@@ -713,7 +707,8 @@ export function createCrabboxWorkerProvider(
if (inspected.status === "unknown") {
return { status: "unknown" };
}
return statusFromInspect(inspected.inspect);
// `ready` is an SSH probe; every recognized nonterminal lease remains active.
return { status: isTerminalState(inspected.inspect.state) ? "destroyed" : "active" };
},
async resolveSshIdentity(request) {
const context = resolveLeaseContext(request);
@@ -0,0 +1,36 @@
type CrabboxProvisionTimeoutProfile = {
desktop?: boolean;
setup?: string;
};
export const CRABBOX_WARMUP_TIMEOUT_MS = 240_000;
export const CRABBOX_LIFECYCLE_TIMEOUT_MS = 60_000;
const CRABBOX_PROVISION_TIMEOUT_MS = 290_000;
// Crabbox starts its 45-minute desktop/browser bootstrap clock after acquisition.
// Preserve OpenClaw's existing five-minute acquisition envelope, then leave one
// lifecycle allowance for post-warmup inspection and cleanup.
export const CRABBOX_DESKTOP_WARMUP_TIMEOUT_MS = 50 * 60_000;
const CRABBOX_DESKTOP_PROVISION_TIMEOUT_MS =
CRABBOX_DESKTOP_WARMUP_TIMEOUT_MS + CRABBOX_LIFECYCLE_TIMEOUT_MS;
// Setup gets its own budget on top of provision so a slow warmup cannot starve it.
export const CRABBOX_SETUP_TIMEOUT_MS = 300_000;
export function resolveCrabboxProvisionBaseTimeoutMs(
profile: CrabboxProvisionTimeoutProfile,
): number {
return profile.desktop ? CRABBOX_DESKTOP_PROVISION_TIMEOUT_MS : CRABBOX_PROVISION_TIMEOUT_MS;
}
export function countCrabboxProvisionSetupPhases(profile: CrabboxProvisionTimeoutProfile): number {
return Number(Boolean(profile.desktop)) + Number(Boolean(profile.setup));
}
export function resolveCrabboxProvisionCallTimeoutMs(
profile: CrabboxProvisionTimeoutProfile,
): number {
return (
resolveCrabboxProvisionBaseTimeoutMs(profile) +
countCrabboxProvisionSetupPhases(profile) * CRABBOX_SETUP_TIMEOUT_MS +
CRABBOX_LIFECYCLE_TIMEOUT_MS
);
}
@@ -521,6 +521,27 @@ describe("lazy protocol validators", () => {
]);
});
it("validates closed worker desktop app launch contracts", () => {
expectAccepted(protocol.validateWorkerDesktopLaunchParams, [
{ environmentId: "worker:one", app: "browser" },
{ environmentId: "worker:one", app: "terminal" },
]);
expectRejected(protocol.validateWorkerDesktopLaunchParams, [
{ environmentId: "", app: "browser" },
{ environmentId: "worker:one", app: "editor" },
{ environmentId: "worker:one", app: "browser", args: [] },
]);
expectAccepted(protocol.validateWorkerDesktopLaunchResult, [
{ app: "browser", status: "ready" },
{ app: "terminal", status: "ready" },
]);
expectRejected(protocol.validateWorkerDesktopLaunchResult, [
{ app: "editor", status: "ready" },
{ app: "browser", status: "starting" },
{ app: "browser", status: "ready", executablePath: "/usr/bin/chromium" },
]);
});
it("validates chat sends that suppress command interpretation", () => {
expectAccepted(validateChatSendParams, [
{
+3
View File
@@ -108,6 +108,7 @@ export {
EnvironmentStatusSchema,
WorkerEnvironmentStateSchema,
WorkerTunnelStatusSchema,
WorkerDesktopAppIdSchema,
WorkerEnvironmentMetadataSchema,
EnvironmentSummarySchema,
EnvironmentsCreateParamsSchema,
@@ -120,6 +121,8 @@ export {
EnvironmentsStatusResultSchema,
WorkerDesktopObserveParamsSchema,
WorkerDesktopObserveResultSchema,
WorkerDesktopLaunchParamsSchema,
WorkerDesktopLaunchResultSchema,
SystemInfoParamsSchema,
SystemInfoResultSchema,
StateVersionSchema,
@@ -7,6 +7,8 @@ import {
EnvironmentSummarySchema,
validateEnvironmentsCreateParams,
validateEnvironmentsDestroyParams,
validateWorkerDesktopLaunchParams,
validateWorkerDesktopLaunchResult,
WorkerEnvironmentStateSchema,
} from "../index.js";
@@ -95,6 +97,43 @@ describe("worker environment protocol schemas", () => {
expect(Value.Check(EnvironmentSummarySchema, requested)).toBe(true);
expect(Value.Check(EnvironmentsCreateResultSchema, requested)).toBe(true);
expect(Value.Check(EnvironmentsDestroyResultSchema, destroyed)).toBe(true);
expect(
Value.Check(EnvironmentSummarySchema, {
...workerSummary("ready", "available"),
worker: {
...workerSummary("ready", "available").worker,
desktop: true,
desktopApps: ["browser", "terminal"],
},
}),
).toBe(true);
});
it("keeps desktop app launch requests, results, and projected ids closed", () => {
expect(validateWorkerDesktopLaunchParams({ environmentId: "worker:one", app: "browser" })).toBe(
true,
);
expect(validateWorkerDesktopLaunchResult({ app: "terminal", status: "ready" })).toBe(true);
expect(validateWorkerDesktopLaunchParams({ environmentId: "worker:one", app: "editor" })).toBe(
false,
);
expect(
validateWorkerDesktopLaunchParams({
environmentId: "worker:one",
app: "browser",
args: ["--incognito"],
}),
).toBe(false);
expect(validateWorkerDesktopLaunchResult({ app: "browser", status: "starting" })).toBe(false);
expect(
Value.Check(EnvironmentSummarySchema, {
...workerSummary("ready", "available"),
worker: {
...workerSummary("ready", "available").worker,
desktopApps: ["editor"],
},
}),
).toBe(false);
});
it("lists configured worker profiles without provider settings", () => {
@@ -37,6 +37,12 @@ export const WorkerTunnelStatusSchema = Type.Union([
Type.Literal("reconnecting"),
]);
/** Closed app ids a worker desktop may advertise and launch. */
export const WorkerDesktopAppIdSchema = Type.Union([
Type.Literal("browser"),
Type.Literal("terminal"),
]);
/** Worker-only lifecycle metadata layered onto the existing environment projection. */
export const WorkerEnvironmentMetadataSchema = closedObject({
providerId: NonEmptyString,
@@ -48,6 +54,9 @@ export const WorkerEnvironmentMetadataSchema = closedObject({
tunnelStatus: WorkerTunnelStatusSchema,
error: Type.Optional(NonEmptyString),
desktop: Type.Optional(Type.Boolean()),
desktopApps: Type.Optional(
Type.Array(WorkerDesktopAppIdSchema, { maxItems: 8, uniqueItems: true }),
),
});
function createEnvironmentSummarySchema() {
@@ -118,9 +127,20 @@ export const WorkerDesktopObserveResultSchema = closedObject({
vncPassword: Type.Optional(NonEmptyString),
});
export const WorkerDesktopLaunchParamsSchema = closedObject({
environmentId: NonEmptyString,
app: WorkerDesktopAppIdSchema,
});
export const WorkerDesktopLaunchResultSchema = closedObject({
app: WorkerDesktopAppIdSchema,
status: Type.Literal("ready"),
});
export type EnvironmentStatus = Static<typeof EnvironmentStatusSchema>;
export type WorkerEnvironmentState = Static<typeof WorkerEnvironmentStateSchema>;
export type WorkerTunnelStatus = Static<typeof WorkerTunnelStatusSchema>;
export type WorkerDesktopAppId = Static<typeof WorkerDesktopAppIdSchema>;
export type WorkerEnvironmentMetadata = Static<typeof WorkerEnvironmentMetadataSchema>;
export type EnvironmentSummary = Static<typeof EnvironmentSummarySchema>;
export type EnvironmentsCreateParams = Static<typeof EnvironmentsCreateParamsSchema>;
@@ -133,3 +153,5 @@ export type EnvironmentsStatusParams = Static<typeof EnvironmentsStatusParamsSch
export type EnvironmentsStatusResult = Static<typeof EnvironmentsStatusResultSchema>;
export type WorkerDesktopObserveParams = Static<typeof WorkerDesktopObserveParamsSchema>;
export type WorkerDesktopObserveResult = Static<typeof WorkerDesktopObserveResultSchema>;
export type WorkerDesktopLaunchParams = Static<typeof WorkerDesktopLaunchParamsSchema>;
export type WorkerDesktopLaunchResult = Static<typeof WorkerDesktopLaunchResultSchema>;
@@ -9,6 +9,7 @@ export const AgentControlProtocolSchemas = {
EnvironmentStatus: environments.EnvironmentStatusSchema,
WorkerEnvironmentState: environments.WorkerEnvironmentStateSchema,
WorkerTunnelStatus: environments.WorkerTunnelStatusSchema,
WorkerDesktopAppId: environments.WorkerDesktopAppIdSchema,
WorkerEnvironmentMetadata: environments.WorkerEnvironmentMetadataSchema,
EnvironmentSummary: environments.EnvironmentSummarySchema,
EnvironmentsCreateParams: environments.EnvironmentsCreateParamsSchema,
@@ -21,6 +22,8 @@ export const AgentControlProtocolSchemas = {
EnvironmentsStatusResult: environments.EnvironmentsStatusResultSchema,
WorkerDesktopObserveParams: environments.WorkerDesktopObserveParamsSchema,
WorkerDesktopObserveResult: environments.WorkerDesktopObserveResultSchema,
WorkerDesktopLaunchParams: environments.WorkerDesktopLaunchParamsSchema,
WorkerDesktopLaunchResult: environments.WorkerDesktopLaunchResultSchema,
SystemInfoParams: systemInfo.SystemInfoParamsSchema,
SystemInfoResult: systemInfo.SystemInfoResultSchema,
AgentEvent: agent.AgentEventSchema,
@@ -150,6 +150,8 @@ export const validateEnvironmentsListParams = compile(S.EnvironmentsListParamsSc
export const validateEnvironmentsStatusParams = compile(S.EnvironmentsStatusParamsSchema);
export const validateWorkerDesktopObserveParams = compile(S.WorkerDesktopObserveParamsSchema);
export const validateWorkerDesktopObserveResult = compile(S.WorkerDesktopObserveResultSchema);
export const validateWorkerDesktopLaunchParams = compile(S.WorkerDesktopLaunchParamsSchema);
export const validateWorkerDesktopLaunchResult = compile(S.WorkerDesktopLaunchResultSchema);
export const validateSystemInfoParams = compile(S.SystemInfoParamsSchema);
export const validateSystemInfoResult = compile(S.SystemInfoResultSchema);
export const validateNodePendingAckParams = compile(S.NodePendingAckParamsSchema);
+2 -1
View File
@@ -252,12 +252,13 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
// +3: channel streaming config reader re-exports and session-agent scope resolver.
// +3: session-catalog terminal-start provider request and Gateway params/result contracts.
// +1: worker desktop endpoint contract for desktop-capable worker leases.
// +1: closed worker desktop app metadata for provider-advertised launchers.
// +1: native command spec merger through the native-command-registry facade.
// +8: focused plugin command runtime factory, dispatch symbol, and six readonly contracts.
// -2: remove unused WhatsApp-specific ack policy exports from channel-feedback.
// -7: retire unused and duplicate inbound-dispatch compatibility exports.
// +7: restore still-existing deprecated inbound-dispatch compatibility re-exports.
4856,
4857,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
+2
View File
@@ -58,6 +58,8 @@ const schemaNames = new Map<string, string>([
["SessionObserverDigest", "SessionObserverDigest"],
["WorkerDesktopObserveParams", "WorkerDesktopObserveParams"],
["WorkerDesktopObserveResult", "WorkerDesktopObserveResult"],
["WorkerDesktopLaunchParams", "WorkerDesktopLaunchParams"],
["WorkerDesktopLaunchResult", "WorkerDesktopLaunchResult"],
]);
const androidEnums: EnumSpec[] = [
@@ -104,5 +104,6 @@ describe("core gateway method release trains", () => {
for (const method of ["projects.list", "projects.register", "projects.remove"]) {
expect(methods.find((candidate) => candidate.name === method)?.since).toBe("2026.8");
}
expect(methods.find((method) => method.name === "worker.desktop.launch")?.since).toBe("2026.8");
});
});
+1
View File
@@ -497,6 +497,7 @@ const CORE_GATEWAY_METHOD_SPECS = [
["projects.list", "projects", "operator.read", "2026.8"],
["projects.register", "projects", "operator.admin", "2026.8"],
["projects.remove", "projects", "operator.admin", "2026.8"],
["worker.desktop.launch", "environments", "operator.admin", "2026.8", { startup: true }],
] as const satisfies readonly CoreGatewayMethodSpecRow[];
export type CoreGatewayHandlerFamily = Exclude<(typeof CORE_GATEWAY_METHOD_SPECS)[number][1], null>;
+3 -1
View File
@@ -312,7 +312,9 @@ export async function startGatewayCoreRuntime(input: {
descriptor.name !== "environments.destroy")) &&
(workerPlacementDispatchAvailable || descriptor.name !== "sessions.dispatch") &&
(workerPlacementControlAvailable || descriptor.name !== "sessions.reclaim") &&
(workerDesktopObserveAvailable || descriptor.name !== "worker.desktop.observe"),
(workerDesktopObserveAvailable ||
(descriptor.name !== "worker.desktop.observe" &&
descriptor.name !== "worker.desktop.launch")),
);
return createGatewayMethodRegistry(
[
+5 -2
View File
@@ -66,7 +66,7 @@ describe("listGatewayMethods", () => {
});
it("appends new methods after model probing without shifting older method indices", () => {
expect(listGatewayMethods().slice(-38)).toEqual([
expect(listGatewayMethods().slice(-39)).toEqual([
"models.probe",
"migrations.memory.plan",
"migrations.memory.apply",
@@ -105,6 +105,7 @@ describe("listGatewayMethods", () => {
"projects.list",
"projects.register",
"projects.remove",
"worker.desktop.launch",
]);
const methods = listGatewayMethods();
expect(methods.indexOf("node.pluginSurface.refresh")).toBe(
@@ -196,7 +197,7 @@ describe("listGatewayMethods", () => {
"exec.approval.get",
]);
expect(methods).toContain("tts.speak");
expect(coreMethods.slice(-45)).toEqual([
expect(coreMethods.slice(-46)).toEqual([
"sessions.catalog.continue",
"sessions.catalog.archive",
"approval.get",
@@ -242,6 +243,7 @@ describe("listGatewayMethods", () => {
"projects.list",
"projects.register",
"projects.remove",
"worker.desktop.launch",
]);
expect(methods.indexOf("approval.get")).toBeGreaterThan(methods.indexOf("tts.speak"));
expect(methods.indexOf("approval.resolve")).toBe(methods.indexOf("approval.get") + 1);
@@ -257,6 +259,7 @@ describe("listGatewayMethods", () => {
expect(methods.indexOf("projects.list")).toBe(methods.indexOf("worker.desktop.observe") + 1);
expect(methods.indexOf("projects.register")).toBe(methods.indexOf("projects.list") + 1);
expect(methods.indexOf("projects.remove")).toBe(methods.indexOf("projects.register") + 1);
expect(methods.indexOf("worker.desktop.launch")).toBe(methods.indexOf("projects.remove") + 1);
});
it("advertises the versioned Talk session RPCs", () => {
+89 -11
View File
@@ -5,8 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js";
import { listNodePairing } from "../../infra/device-pairing-node.js";
import { listDevicePairing } from "../../infra/device-pairing.js";
import type { WorkerEnvironmentServiceRecord } from "../worker-environments/service-contract.js";
import type { WorkerEnvironmentRecord } from "../worker-environments/store.js";
import type { WorkerTunnelStatus } from "../worker-environments/tunnel-contract.js";
import { environmentsHandlers, summarizeWorkerEnvironment } from "./environments.js";
vi.mock("../../infra/device-pairing.js", () => ({
@@ -20,11 +20,11 @@ vi.mock("../../infra/device-pairing-node.js", () => ({
const NOW = 10_000;
type TestWorkerRecord = WorkerEnvironmentRecord & {
desktopAvailable: boolean;
tunnelStatus: WorkerTunnelStatus;
error?: string;
};
type TestWorkerRecord = WorkerEnvironmentRecord &
Pick<
WorkerEnvironmentServiceRecord,
"desktopAvailable" | "desktopApps" | "tunnelStatus" | "error"
>;
type TestWorkerService = {
list: () => TestWorkerRecord[];
@@ -39,6 +39,10 @@ type TestWorkerService = {
control: boolean;
vncPassword?: string;
}>;
launchDesktopApp: (request: {
environmentId: string;
app: "browser" | "terminal";
}) => Promise<{ app: "browser" | "terminal"; status: "ready" }>;
};
function mockContext(
@@ -112,6 +116,7 @@ function workerRecord(overrides: Partial<TestWorkerRecord> = {}): TestWorkerReco
lastError: null,
tunnelStatus: "stopped",
desktopAvailable: false,
desktopApps: [],
...overrides,
} as TestWorkerRecord;
}
@@ -129,6 +134,7 @@ function workerService(overrides: Partial<TestWorkerService> = {}) {
expiresAtMs: 70_000,
control,
})),
launchDesktopApp: vi.fn(async ({ app }) => ({ app, status: "ready" as const })),
...overrides,
};
}
@@ -139,7 +145,8 @@ async function callEnvironmentMethod(
| "environments.status"
| "environments.create"
| "environments.destroy"
| "worker.desktop.observe",
| "worker.desktop.observe"
| "worker.desktop.launch",
params: unknown,
options: {
service?: TestWorkerService;
@@ -295,11 +302,15 @@ describe("environment gateway methods", () => {
it("projects desktop metadata only when the service reports it available", () => {
expect(
summarizeWorkerEnvironment(workerRecord({ desktopAvailable: true }), NOW).worker,
).toMatchObject({ desktop: true });
summarizeWorkerEnvironment(
workerRecord({ desktopAvailable: true, desktopApps: ["browser", "terminal"] }),
NOW,
).worker,
).toMatchObject({ desktop: true, desktopApps: ["browser", "terminal"] });
expect(
summarizeWorkerEnvironment(workerRecord({ desktopAvailable: false }), NOW).worker,
).not.toHaveProperty("desktop");
summarizeWorkerEnvironment(workerRecord({ desktopAvailable: false, desktopApps: [] }), NOW)
.worker,
).not.toHaveProperty("desktopApps");
});
it("returns status for one node environment", async () => {
@@ -534,6 +545,73 @@ describe("environment gateway methods", () => {
]);
});
it("launches only a closed advertised desktop app and returns readiness", async () => {
const launchDesktopApp = vi.fn(async ({ app }: { app: "browser" | "terminal" }) => ({
app,
status: "ready" as const,
}));
const service = workerService({ launchDesktopApp });
const result = await callEnvironmentMethod(
"worker.desktop.launch",
{ environmentId: "worker-1", app: "browser" },
{ service },
);
expect(result).toEqual([true, { app: "browser", status: "ready" }, undefined]);
expect(launchDesktopApp).toHaveBeenCalledExactlyOnceWith({
environmentId: "worker-1",
app: "browser",
});
const rejected = await callEnvironmentMethod(
"worker.desktop.launch",
{ environmentId: "worker-1", app: "editor" },
{ service },
);
expect(rejected[0]).toBe(false);
expect(launchDesktopApp).toHaveBeenCalledOnce();
});
it("maps typed desktop launcher errors without exposing unknown runtime details", async () => {
const cases = [
[
"desktop_app_not_found",
ErrorCodes.INVALID_REQUEST,
"environment does not advertise desktop app: browser",
],
[
"unsupported_platform",
ErrorCodes.INVALID_REQUEST,
"desktop app launch is not supported on Windows gateway hosts",
],
[
"launcher_failure",
ErrorCodes.UNAVAILABLE,
"worker desktop browser launcher failed; verify the app is installed and retry",
],
[
"provider_failure",
ErrorCodes.UNAVAILABLE,
"worker desktop app launch unavailable; try again",
],
] as const;
for (const [serviceCode, gatewayCode, message] of cases) {
const service = workerService({
launchDesktopApp: vi.fn(async () => {
throw new FakeWorkerServiceError(
serviceCode,
serviceCode === "provider_failure" ? "private SSH detail" : message,
);
}),
});
const response = await callEnvironmentMethod(
"worker.desktop.launch",
{ environmentId: "worker-1", app: "browser" },
{ service },
);
expect(response[2]).toEqual({ code: gatewayCode, message });
}
});
it("destroys an environment idempotently", async () => {
const destroyed = workerRecord({ state: "destroyed" });
const destroyUnattached = vi.fn(async () => destroyed);
@@ -8,6 +8,7 @@ import {
validateEnvironmentsListParams,
validateEnvironmentsStatusParams,
validateWorkerDesktopObserveParams,
validateWorkerDesktopLaunchParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { listNodePairing } from "../../infra/device-pairing-node.js";
import { listDevicePairing, resolveNodePairingState } from "../../infra/device-pairing.js";
@@ -84,6 +85,7 @@ export function summarizeWorkerEnvironment(
? { error: record.error }
: {}),
...(record.desktopAvailable ? { desktop: true } : {}),
...(record.desktopApps.length > 0 ? { desktopApps: [...record.desktopApps] } : {}),
},
};
}
@@ -282,4 +284,42 @@ export const environmentsHandlers: GatewayRequestHandlers = {
);
}
},
"worker.desktop.launch": async ({ params, respond, context }) => {
if (!validateWorkerDesktopLaunchParams(params)) {
return rejectInvalid(respond, "worker.desktop.launch", validateWorkerDesktopLaunchParams);
}
const service = context.workerEnvironmentService;
if (!service) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown environmentId"));
return;
}
try {
respond(
true,
await service.launchDesktopApp({
environmentId: params.environmentId,
app: params.app,
}),
undefined,
);
} catch (error) {
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
const invalid =
code === "environment_not_found" ||
code === "invalid_state" ||
code === "desktop_app_not_found" ||
code === "unsupported_platform";
const actionable = invalid || code === "launcher_failure";
respond(
false,
undefined,
errorShape(
invalid ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE,
actionable && error instanceof Error
? error.message
: "worker desktop app launch unavailable; try again",
),
);
}
},
};
@@ -469,6 +469,7 @@ describe("openclaw.setup", () => {
baseSnapshot: expect.objectContaining({ hash: "prepare-base-hash" }),
baseHash: "prepare-base-hash",
});
await whenAdmittedWizardSessionSettled(session);
});
});
+2 -1
View File
@@ -176,7 +176,8 @@ export async function prepareGatewayRuntimeState(params: {
(method) =>
(workerPlacementDispatchAvailable || method !== "sessions.dispatch") &&
(workerPlacementControlAvailable || method !== "sessions.reclaim") &&
(workerDesktopObserveAvailable || method !== "worker.desktop.observe"),
(workerDesktopObserveAvailable ||
(method !== "worker.desktop.observe" && method !== "worker.desktop.launch")),
);
const runtimeConfig = await startupTrace.measure("runtime.config", async () => {
const { resolveGatewayRuntimeConfig } = await import("./server-runtime-config.js");
@@ -9,7 +9,7 @@ describe("cloud worker desktop method advertisement", () => {
it.each([
{ desktop: undefined, advertised: false },
{ desktop: true, advertised: true },
])("advertises worker.desktop.observe only when the Labs gate is $desktop", async (testCase) => {
])("advertises desktop methods only when the Labs gate is $desktop", async (testCase) => {
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "0";
await writeConfigFile({
cloudWorkers: {
@@ -29,6 +29,7 @@ describe("cloud worker desktop method advertisement", () => {
expect(methods).toContain("sessions.dispatch");
expect(methods.includes("worker.desktop.observe")).toBe(testCase.advertised);
expect(methods.includes("worker.desktop.launch")).toBe(testCase.advertised);
} finally {
ws.close();
await server.close();
@@ -68,7 +68,9 @@ function success(stdout = ""): SpawnResult {
};
}
function fakeRunner() {
function fakeRunner(
onRun?: (argv: string[], options: CommandOptions) => Promise<SpawnResult> | SpawnResult,
) {
const starts: Array<{ argv: string[]; options: CommandOptions; process: FakeProcess }> = [];
const runs: Array<{ argv: string[]; options: CommandOptions }> = [];
const runner: WorkerSshRunner = {
@@ -79,12 +81,34 @@ function fakeRunner() {
},
async run(argv, options) {
runs.push({ argv, options });
return success("vnc-secret\n");
return (await onRun?.(argv, options)) ?? success("vnc-secret\n");
},
};
return { runner, runs, starts };
}
function launchApp(
manager: ReturnType<typeof createWorkerDesktopTunnels>,
app: "browser" | "terminal" = "browser",
ownerEpoch = 1,
ssh = SSH,
) {
return manager.launchApp({
environmentId: "worker:one",
ownerEpoch,
ssh,
app:
app === "browser"
? {
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
}
: { id: "terminal", executablePath: "/usr/local/bin/openclaw-worker-terminal" },
resolveIdentity,
});
}
function acquire(
manager: ReturnType<typeof createWorkerDesktopTunnels>,
ownerEpoch = 1,
@@ -271,4 +295,105 @@ describe("worker desktop tunnels", () => {
);
expect(fake.starts).toEqual([]);
});
it("deduplicates one exact no-argument launcher command per app and epoch", async () => {
const result = deferred<SpawnResult>();
const fake = fakeRunner(async () => await result.promise);
const manager = createWorkerDesktopTunnels({ runner: fake.runner });
const first = launchApp(manager);
const second = launchApp(manager);
await vi.waitFor(() => expect(fake.runs).toHaveLength(1), { interval: 1 });
const run = fake.runs[0]!;
expect(run.argv.at(-1)).toBe("'/usr/local/bin/openclaw-worker-browser'");
expect(run.argv.at(-1)).not.toContain("9222");
expect(run.argv.at(-1)).not.toContain(".cache/openclaw");
expect(run.options.timeoutMs).toBeGreaterThan(0);
expect(run.options.timeoutMs).toBeLessThanOrEqual(30_000);
expect(run.options.signal).toBeInstanceOf(AbortSignal);
result.resolve(success());
await expect(Promise.all([first, second])).resolves.toEqual([undefined, undefined]);
await manager.stopAll();
});
it.each(["browser", "terminal"] as const)(
"does not replay a %s launch after ambiguous SSH exit 255",
async (app) => {
const fake = fakeRunner(() => ({
...success(),
code: 255,
stderr: "connection lost after remote acceptance",
}));
const manager = createWorkerDesktopTunnels({ runner: fake.runner });
await expect(launchApp(manager, app, 1, { ...SSH, fallbackPorts: [2203] })).rejects.toThrow(
"connection lost after remote acceptance",
);
expect(fake.runs.map(({ argv }) => argv[argv.indexOf("-p") + 1])).toEqual(["2202"]);
await manager.stopAll();
},
);
it("aborts pending launchers on matching teardown and fences stale epochs", async () => {
const signals: AbortSignal[] = [];
const fake = fakeRunner(
async (_argv, options) =>
await new Promise<SpawnResult>((_resolve, reject) => {
const signal = options.signal;
if (!signal) {
reject(new Error("missing launcher abort signal"));
return;
}
signals.push(signal);
signal.addEventListener(
"abort",
() =>
reject(
signal.reason instanceof Error
? signal.reason
: new Error("launcher aborted", { cause: signal.reason }),
),
{ once: true },
);
}),
);
const manager = createWorkerDesktopTunnels({ runner: fake.runner });
const launching = launchApp(manager);
await vi.waitFor(() => expect(fake.runs).toHaveLength(1), { interval: 1 });
await manager.stop("worker:one", 1);
await expect(launching).rejects.toThrow("owner stopped");
expect(signals[0]?.aborted).toBe(true);
await expect(launchApp(manager, "browser", 0)).rejects.toThrow("owner epoch is stale");
});
it("publishes launcher ownership before immediate teardown can observe it", async () => {
const fake = fakeRunner();
const manager = createWorkerDesktopTunnels({ runner: fake.runner });
const launching = launchApp(manager);
const stopping = manager.stop("worker:one", 1);
await expect(launching).rejects.toThrow("owner stopped");
await stopping;
expect(fake.runs).toEqual([]);
});
it("reports unsupported launcher platforms and nonzero launcher exits", async () => {
const unsupported = createWorkerDesktopTunnels({
runner: fakeRunner().runner,
platform: "win32",
});
await expect(launchApp(unsupported)).rejects.toMatchObject({ code: "unsupported_platform" });
await expect(launchApp(unsupported)).rejects.toThrow(
"desktop app launch is not supported on Windows gateway hosts",
);
const failed = createWorkerDesktopTunnels({
runner: fakeRunner(() => ({ ...success(), code: 7, stderr: "launcher failed" })).runner,
});
await expect(launchApp(failed)).rejects.toThrow("launcher failed");
await failed.stopAll();
});
});
+189 -12
View File
@@ -1,6 +1,11 @@
import path from "node:path";
import { withTimeout } from "../../infra/fs-safe.js";
import { registerSecretValueForRedaction } from "../../logging/secret-redaction-registry.js";
import type { WorkerDesktopEndpoint, WorkerSshEndpoint } from "../../plugins/types.js";
import type {
WorkerDesktopApp,
WorkerDesktopEndpoint,
WorkerSshEndpoint,
} from "../../plugins/types.js";
import {
prepareWorkerSsh,
type PreparedWorkerSsh,
@@ -18,6 +23,7 @@ import {
const DEFAULT_LINGER_MS = 60_000;
const PASSWORD_READ_TIMEOUT_MS = 20_000;
const APP_LAUNCH_TIMEOUT_MS = 30_000;
const MAX_OBSERVERS = 8;
const REMOTE_DESKTOP_READY_SCRIPT = String.raw`set -eu
@@ -61,11 +67,19 @@ type DesktopEntry = {
stopped: boolean;
};
type DesktopAppLaunchEntry = {
environmentId: string;
appId: WorkerDesktopApp["id"];
ownerEpoch: number;
abortController: AbortController;
operation: Promise<void>;
};
class WorkerDesktopUnsupportedError extends Error {
readonly code = "unsupported_platform";
constructor() {
super("desktop observe is not supported on Windows gateway hosts");
constructor(operation = "desktop observe") {
super(`${operation} is not supported on Windows gateway hosts`);
this.name = "WorkerDesktopUnsupportedError";
}
}
@@ -84,8 +98,12 @@ export function createWorkerDesktopTunnels(deps: {
const lingerMs = deps.lingerMs ?? DEFAULT_LINGER_MS;
const platform = deps.platform ?? process.platform;
const entries = new Map<string, DesktopEntry>();
const appLaunches = new Map<string, DesktopAppLaunchEntry>();
const claimedOwnerEpochs = new Map<string, number>();
const appLaunchKey = (environmentId: string, appId: WorkerDesktopApp["id"]) =>
`${environmentId}\0${appId}`;
const isCurrent = (entry: DesktopEntry) =>
entries.get(entry.environmentId) === entry && !entry.stopped;
@@ -129,6 +147,44 @@ export function createWorkerDesktopTunnels(deps: {
return entry.stopPromise;
};
const stopAppLaunches = async (environmentId: string, ownerEpoch?: number): Promise<void> => {
const matching = [...appLaunches.values()].filter(
(entry) =>
entry.environmentId === environmentId &&
(ownerEpoch === undefined || entry.ownerEpoch === ownerEpoch),
);
for (const entry of matching) {
entry.abortController.abort(new Error("Worker desktop app launch owner stopped"));
}
await Promise.allSettled(matching.map((entry) => entry.operation));
};
const reserveOwnerEpoch = (environmentId: string, ownerEpoch: number): boolean => {
const claimedEpoch = claimedOwnerEpochs.get(environmentId);
if (claimedEpoch !== undefined && ownerEpoch < claimedEpoch) {
throw new Error("Worker desktop owner epoch is stale");
}
if (claimedEpoch === undefined || ownerEpoch > claimedEpoch) {
claimedOwnerEpochs.set(environmentId, ownerEpoch);
return true;
}
return false;
};
const fenceReplacedOwners = async (environmentId: string, ownerEpoch: number): Promise<void> => {
const current = entries.get(environmentId);
if (current && current.ownerEpoch < ownerEpoch) {
await stopEntry(current);
}
const staleLaunches = [...appLaunches.values()].filter(
(entry) => entry.environmentId === environmentId && entry.ownerEpoch < ownerEpoch,
);
for (const entry of staleLaunches) {
entry.abortController.abort(new Error("Worker desktop app launch owner replaced"));
}
await Promise.allSettled(staleLaunches.map((entry) => entry.operation));
};
const startEntry = async (entry: DesktopEntry, request: DesktopAcquireRequest) => {
const prepared = await prepareWorkerSsh({
ssh: request.ssh,
@@ -215,11 +271,13 @@ export function createWorkerDesktopTunnels(deps: {
if (platform === "win32") {
throw new WorkerDesktopUnsupportedError();
}
const claimedEpoch = claimedOwnerEpochs.get(request.environmentId);
if (claimedEpoch !== undefined && request.ownerEpoch < claimedEpoch) {
throw new Error("Worker desktop tunnel owner epoch is stale");
const ownerAdvanced = reserveOwnerEpoch(request.environmentId, request.ownerEpoch);
if (ownerAdvanced) {
await fenceReplacedOwners(request.environmentId, request.ownerEpoch);
}
if (claimedOwnerEpochs.get(request.environmentId) !== request.ownerEpoch) {
throw new Error("Worker desktop owner epoch is stale");
}
claimedOwnerEpochs.set(request.environmentId, request.ownerEpoch);
const current = entries.get(request.environmentId);
if (current?.ownerEpoch === request.ownerEpoch) {
return await current.ready;
@@ -262,6 +320,116 @@ export function createWorkerDesktopTunnels(deps: {
return await ready;
}
function launchApp(request: {
environmentId: string;
ownerEpoch: number;
ssh: WorkerSshEndpoint;
app: WorkerDesktopApp;
resolveIdentity: WorkerSshIdentityResolver;
}): Promise<void> {
if (platform === "win32") {
return Promise.reject(new WorkerDesktopUnsupportedError("desktop app launch"));
}
let ownerAdvanced: boolean;
try {
ownerAdvanced = reserveOwnerEpoch(request.environmentId, request.ownerEpoch);
} catch (error) {
return Promise.reject(
error instanceof Error
? error
: new Error("Worker desktop owner epoch is invalid", { cause: error }),
);
}
const key = appLaunchKey(request.environmentId, request.app.id);
const current = appLaunches.get(key);
if (current?.ownerEpoch === request.ownerEpoch) {
return current.operation;
}
const abortController = new AbortController();
const startedAtMs = Date.now();
let startExecution!: () => void;
const startGate = new Promise<void>((resolve) => {
startExecution = resolve;
});
const execution = (async () => {
await startGate;
abortController.signal.throwIfAborted();
if (claimedOwnerEpochs.get(request.environmentId) !== request.ownerEpoch) {
throw new Error("Worker desktop app launch owner was replaced");
}
if (current) {
current.abortController.abort(new Error("Worker desktop app launch owner replaced"));
await current.operation.catch(() => undefined);
}
if (ownerAdvanced) {
await fenceReplacedOwners(request.environmentId, request.ownerEpoch);
}
abortController.signal.throwIfAborted();
const prepared = await prepareWorkerSsh({
ssh: request.ssh,
pinnedHostKey: request.ssh.hostKey,
resolveIdentity: request.resolveIdentity,
temporaryDirectoryPrefix: "openclaw-worker-desktop-app-",
});
try {
abortController.signal.throwIfAborted();
const remainingLaunchMs = Math.max(0, APP_LAUNCH_TIMEOUT_MS - (Date.now() - startedAtMs));
// Launchers are stateful: SSH exit 255 cannot prove the remote app did not start.
// Use the lifecycle-selected port once so an ambiguous disconnect cannot launch twice.
const result = await deps.runner.run(
[
"ssh",
...workerSshOptions(prepared, { forwarding: "disabled" }),
"-a",
"-x",
"-T",
"-p",
String(prepared.port),
"--",
prepared.sshTarget,
workerSshRemoteCommand([request.app.executablePath]),
],
workerSshCommandOptions({
timeoutMs: remainingLaunchMs,
signal: abortController.signal,
}),
);
if (!successful(result)) {
throw workerSshProcessError(result.stderr || result.stdout);
}
} finally {
await prepared.dispose();
}
})();
const timeoutError = new Error("Worker desktop app launcher timed out after 30 seconds");
const operation = withTimeout(execution, APP_LAUNCH_TIMEOUT_MS, {
createError: () => timeoutError,
}).catch((error: unknown) => {
if (error === timeoutError) {
abortController.abort(timeoutError);
}
throw error;
});
const completeEntry: DesktopAppLaunchEntry = {
environmentId: request.environmentId,
appId: request.app.id,
ownerEpoch: request.ownerEpoch,
abortController,
operation,
};
appLaunches.set(key, completeEntry);
// The pending owner is now visible to teardown; only then may identity resolution or SSH run.
startExecution();
void operation
.finally(() => {
if (appLaunches.get(key) === completeEntry) {
appLaunches.delete(key);
}
})
.catch(() => undefined);
return operation;
}
function attachObserver(environmentId: string, observer: WorkerDesktopObserver) {
const entry = entries.get(environmentId);
if (!entry || !entry.readySettled || entry.stopped || entry.observers.size >= MAX_OBSERVERS) {
@@ -306,16 +474,25 @@ export function createWorkerDesktopTunnels(deps: {
async function stop(environmentId: string, ownerEpoch?: number): Promise<void> {
const entry = entries.get(environmentId);
if (entry && (ownerEpoch === undefined || ownerEpoch === entry.ownerEpoch)) {
await stopEntry(entry);
}
await Promise.all([
entry && (ownerEpoch === undefined || ownerEpoch === entry.ownerEpoch)
? stopEntry(entry)
: Promise.resolve(),
stopAppLaunches(environmentId, ownerEpoch),
]);
}
async function stopAll(): Promise<void> {
await Promise.all([...entries.values()].map(stopEntry));
for (const entry of appLaunches.values()) {
entry.abortController.abort(new Error("Worker desktop app launcher stopped"));
}
await Promise.all([
...[...entries.values()].map(stopEntry),
...[...appLaunches.values()].map((entry) => entry.operation.catch(() => undefined)),
]);
}
return { acquire, attachObserver, stop, stopAll };
return { acquire, attachObserver, launchApp, stop, stopAll };
}
export type WorkerDesktopTunnels = ReturnType<typeof createWorkerDesktopTunnels>;
@@ -118,6 +118,7 @@ export function createDispatchEnvironmentFixtures(generation = 1) {
sshEndpoint,
desktop: null,
desktopAvailable: false,
desktopApps: [],
};
const ready = {
...environmentBase,
@@ -1,3 +1,4 @@
import type { WorkerDesktopApp } from "../../plugins/capability-provider.types.js";
import type { WorkerSessionPlacementRecord } from "./placement-record.js";
import type { WorkerEnvironmentState } from "./state.js";
import type {
@@ -17,6 +18,7 @@ export type WorkerEnvironmentServiceRecord = {
idleSinceAtMs: number | null;
attachedSessionIds: readonly string[];
desktopAvailable: boolean;
desktopApps: readonly WorkerDesktopApp["id"][];
tunnelStatus: WorkerTunnelStatus;
error?: string;
};
@@ -29,6 +31,11 @@ export type WorkerDesktopObserveResult = {
vncPassword?: string;
};
export type WorkerDesktopLaunchResult = {
app: WorkerDesktopApp["id"];
status: "ready";
};
/** Request-facing lifecycle methods, kept separate from persistence and provider internals. */
export type WorkerEnvironmentServiceContract = {
list(): WorkerEnvironmentServiceRecord[];
@@ -40,6 +47,10 @@ export type WorkerEnvironmentServiceContract = {
environmentId: string;
control: boolean;
}): Promise<WorkerDesktopObserveResult>;
launchDesktopApp(request: {
environmentId: string;
app: WorkerDesktopApp["id"];
}): Promise<WorkerDesktopLaunchResult>;
startTunnel(request: WorkerTunnelRequest): Promise<WorkerTunnelHandle>;
stopTunnel(environmentId: string, ownerEpoch?: number): Promise<void>;
};
+160 -8
View File
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.js";
import {
@@ -50,6 +51,14 @@ const DESKTOP: WorkerDesktopEndpoint = {
protocol: "rfb",
port: 5900,
passwordFilePath: "/var/lib/crabbox/vnc.password",
apps: [
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
},
{ id: "terminal", executablePath: "/usr/local/bin/openclaw-worker-terminal" },
],
};
const BUNDLE_HASH = "a".repeat(64);
const BUNDLE_ARTIFACT: WorkerInstallationArtifact = {
@@ -180,9 +189,7 @@ describe("worker environment service", () => {
return service;
}
function createProvider(
overrides: Partial<Pick<WorkerProvider, "provision" | "inspect" | "destroy">> = {},
): WorkerProvider {
function createProvider(overrides: Partial<WorkerProvider> = {}): WorkerProvider {
return {
id: "fake",
provision: async () => ({ leaseId: "lease-1", ssh: SSH_ENDPOINT }),
@@ -243,7 +250,7 @@ describe("worker environment service", () => {
});
}
function seedReadyDesktop(environmentId: string) {
function seedReadyDesktop(environmentId: string, desktop: WorkerDesktopEndpoint = DESKTOP) {
const intent = store.createIntent({
environmentId,
providerId: "fake",
@@ -263,7 +270,7 @@ describe("worker environment service", () => {
patch: {
leaseId: `lease:${environmentId}`,
sshEndpoint: SSH_ENDPOINT,
desktop: DESKTOP,
desktop,
},
});
return store.transition({
@@ -1589,6 +1596,44 @@ describe("worker environment service", () => {
});
});
it("does not resolve a provider provision timeout when the service override is set", async () => {
const resolveProvisionTimeoutMs = vi.fn(() => {
throw new Error("provider timeout hook must not run");
});
const workerService = createService(createProvider({ resolveProvisionTimeoutMs }), {
providerCallTimeoutMs: 1_000,
});
await expect(
workerService.create("development", "request-provider-timeout-override"),
).resolves.toMatchObject({ state: "ready" });
expect(resolveProvisionTimeoutMs).not.toHaveBeenCalled();
});
it.each([
["zero", 0],
["negative", -1],
["fractional", 1.5],
["non-finite", Number.NaN],
["timer overflow", MAX_TIMER_TIMEOUT_MS + 1],
])("rejects a %s provider provision timeout before allocation", async (_label, timeoutMs) => {
const provision = vi.fn(async () => ({ leaseId: "lease-invalid-timeout", ssh: SSH_ENDPOINT }));
const workerService = createService(
createProvider({
provision,
resolveProvisionTimeoutMs: () => timeoutMs,
}),
);
await expect(
workerService.create("development", `request-invalid-provider-timeout-${String(timeoutMs)}`),
).rejects.toMatchObject({
code: "invalid_profile",
message: expect.stringContaining("Worker provider provision timeout must be an integer"),
} satisfies Partial<WorkerEnvironmentServiceError>);
expect(provision).not.toHaveBeenCalled();
});
it("serializes destroy and provision replay behind a timed-out provider operation", async () => {
const events: string[] = [];
const operationIds: string[] = [];
@@ -1622,8 +1667,9 @@ describe("worker environment service", () => {
return { leaseId: "lease-timeout-replay", ssh: SSH_ENDPOINT };
},
destroy,
resolveProvisionTimeoutMs: () => 20,
});
const workerService = createService(provider, { providerCallTimeoutMs: 20 });
const workerService = createService(provider);
const creation = workerService.create("development", "request-provider-timeout-race");
const creationResult = expect(creation).rejects.toMatchObject({
code: "provider_failure",
@@ -1753,6 +1799,26 @@ describe("worker environment service", () => {
},
"desktop password file path must be absolute",
],
[
"unrecognized desktop app metadata",
{
leaseId: "lease-invalid",
ssh: SSH_ENDPOINT,
desktop: {
protocol: "rfb",
port: 5900,
apps: [
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
command: "chromium",
},
],
},
},
"browser desktop app contains unknown fields",
],
])("keeps %s from a provider retryable", async (_name, result, error) => {
const workerService = createService(createProvider({ provision: async () => result as never }));
@@ -2448,13 +2514,99 @@ describe("worker environment service", () => {
it("projects desktop availability only while a desktop lease is observable", () => {
const ready = seedReadyDesktop("worker-desktop-projection");
const workerService = createService(createProvider());
expect(workerService.get(ready.environmentId)).toMatchObject({ desktopAvailable: true });
expect(workerService.get(ready.environmentId)).toMatchObject({
desktopAvailable: true,
desktopApps: ["browser", "terminal"],
});
store.transition({
environmentId: ready.environmentId,
from: ready.state,
to: "draining",
});
expect(workerService.get(ready.environmentId)).toMatchObject({ desktopAvailable: false });
expect(workerService.get(ready.environmentId)).toMatchObject({
desktopAvailable: false,
desktopApps: [],
});
});
it("launches only an advertised desktop app through the pinned SSH runtime", async () => {
const record = seedReadyDesktop("worker-desktop-launch");
const launchApp = vi.fn(async () => {});
const tunnelManager = {
desktop: {
acquire: vi.fn(),
attachObserver: vi.fn(),
launchApp,
stop: vi.fn(async () => {}),
stopAll: vi.fn(async () => {}),
},
status: () => "stopped" as const,
start: vi.fn(),
stop: vi.fn(async () => {}),
stopAll: vi.fn(async () => {}),
} as unknown as WorkerTunnelManager;
const workerService = createService(createProvider(), { tunnelManager });
await expect(
workerService.launchDesktopApp({ environmentId: record.environmentId, app: "browser" }),
).resolves.toEqual({ app: "browser", status: "ready" });
expect(launchApp).toHaveBeenCalledExactlyOnceWith({
environmentId: record.environmentId,
ownerEpoch: record.ownerEpoch,
ssh: SSH_ENDPOINT,
app: DESKTOP.apps?.[0],
resolveIdentity: expect.any(Function),
});
});
it("rejects missing desktop apps and maps launcher runtime failures to typed errors", async () => {
const record = seedReadyDesktop("worker-desktop-launch-errors");
const launchApp = vi.fn(async () => {
throw new Error("private SSH launcher detail");
});
const tunnelManager = {
desktop: {
acquire: vi.fn(),
attachObserver: vi.fn(),
launchApp,
stop: vi.fn(async () => {}),
stopAll: vi.fn(async () => {}),
},
status: () => "stopped" as const,
start: vi.fn(),
stop: vi.fn(async () => {}),
stopAll: vi.fn(async () => {}),
} as unknown as WorkerTunnelManager;
const workerService = createService(createProvider(), { tunnelManager });
await expect(
workerService.launchDesktopApp({ environmentId: record.environmentId, app: "browser" }),
).rejects.toMatchObject({
code: "launcher_failure",
message: "worker desktop browser launcher failed; verify the app is installed and retry",
});
store.transition({
environmentId: record.environmentId,
from: record.state,
to: "draining",
});
await expect(
workerService.launchDesktopApp({ environmentId: record.environmentId, app: "terminal" }),
).rejects.toMatchObject({ code: "invalid_state" });
const browserOnly = seedReadyDesktop("worker-desktop-browser-only", {
...DESKTOP,
apps: [DESKTOP.apps![0]!],
});
await expect(
workerService.launchDesktopApp({
environmentId: browserOnly.environmentId,
app: "terminal",
}),
).rejects.toMatchObject({
code: "desktop_app_not_found",
message: "environment does not advertise desktop app: terminal",
});
});
it("acquires a desktop tunnel and mints a one-shot websocket path", async () => {
+149 -13
View File
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import { expectDefined } from "@openclaw/normalization-core";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import {
type WorkerAdmissionHandshake,
type WorkerConnectParams,
@@ -60,7 +61,7 @@ import {
type WorkerInferenceSink,
} from "./inference.js";
import type { WorkerLiveEventApplicationResult, WorkerLiveEventReceiver } from "./live-events.js";
import type { WorkerDesktopObserveResult } from "./service-contract.js";
import type { WorkerDesktopLaunchResult, WorkerDesktopObserveResult } from "./service-contract.js";
import { requireWorkerLeaseStatus, requireWorkerLease } from "./service-validation.js";
import type { WorkerEnvironmentState } from "./state.js";
import {
@@ -79,6 +80,9 @@ type WorkerEnvironmentServiceErrorCode =
| "environment_not_found"
| "invalid_profile"
| "invalid_state"
| "desktop_app_not_found"
| "unsupported_platform"
| "launcher_failure"
| "provider_failure"
| "bootstrap_failure";
@@ -97,6 +101,18 @@ const ORPHANED_LEASE_ERROR = "Worker provider no longer recognizes the lease";
// One poisoned SSH attempt must not hold every later dispatch on the same owner epoch forever.
const TUNNEL_START_TIMEOUT_MS = 3 * 60_000;
function requireProviderProvisionTimeoutMs(timeoutMs: number | undefined): number | undefined {
if (timeoutMs === undefined) {
return undefined;
}
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMER_TIMEOUT_MS) {
throw new WorkerProviderError(
`Worker provider provision timeout must be an integer from 1 through ${MAX_TIMER_TIMEOUT_MS}ms`,
);
}
return timeoutMs;
}
function workerEnvironmentIdempotencyDigest(idempotencyKey: string): string {
return createHash("sha256").update(idempotencyKey).digest("hex");
}
@@ -325,14 +341,21 @@ export function createWorkerEnvironmentService(options: WorkerEnvironmentService
(request.event.payload.aborted === true ||
request.event.payload.fallbackExhaustedFailure === true)));
const project = (record: WorkerEnvironmentRecord) => ({
...record,
...((record.state === "failed" || record.state === "orphaned") && record.lastError
? { error: boundedError(record.lastError) }
: {}),
desktopAvailable: inState(record, "ready", "idle", "attached") && record.desktop !== null,
tunnelStatus: tunnels?.status(record.environmentId) ?? ("stopped" as const),
});
const project = (record: WorkerEnvironmentRecord) => {
const desktopAvailable =
inState(record, "ready", "idle", "attached") && record.desktop !== null;
return {
...record,
...((record.state === "failed" || record.state === "orphaned") && record.lastError
? { error: boundedError(record.lastError) }
: {}),
desktopAvailable,
desktopApps: desktopAvailable
? (record.desktop?.apps?.map((app) => app.id).toSorted() ?? [])
: [],
tunnelStatus: tunnels?.status(record.environmentId) ?? ("stopped" as const),
};
};
const move = (
r: WorkerEnvironmentRecord,
@@ -374,7 +397,11 @@ export function createWorkerEnvironmentService(options: WorkerEnvironmentService
const withLock = <T>(environmentId: string, task: () => Promise<T>) =>
trackOperation(operations.enqueue(environmentId, task));
const callProvider = async <T>(environmentId: string, run: () => Promise<T>): Promise<T> => {
const callProvider = async <T>(
environmentId: string,
run: () => Promise<T>,
timeoutMs?: number,
): Promise<T> => {
let signalStarted!: () => void;
const started = new Promise<void>((resolve) => {
signalStarted = resolve;
@@ -389,7 +416,7 @@ export function createWorkerEnvironmentService(options: WorkerEnvironmentService
// Timeout completion must not release provider ownership or permit replay/destroy overlap.
return await withTimeout(
operation,
options.providerCallTimeoutMs ?? 300_000,
options.providerCallTimeoutMs ?? timeoutMs ?? 300_000,
"Worker provider operation",
);
};
@@ -621,9 +648,15 @@ export function createWorkerEnvironmentService(options: WorkerEnvironmentService
let lease: WorkerLease;
try {
const profile = requireWorkerProfile(record.profileSnapshot.settings);
const providerTimeoutMs =
options.providerCallTimeoutMs === undefined
? requireProviderProvisionTimeoutMs(provider.resolveProvisionTimeoutMs?.(profile))
: undefined;
lease = requireWorkerLease(
await callProvider(record.environmentId, () =>
provider.provision(profile, record.provisionOperationId),
await callProvider(
record.environmentId,
() => provider.provision(profile, record.provisionOperationId),
providerTimeoutMs,
),
);
} catch (error) {
@@ -1248,6 +1281,108 @@ export function createWorkerEnvironmentService(options: WorkerEnvironmentService
};
};
const launchDesktopApp = async (request: {
environmentId: string;
app: "browser" | "terminal";
}): Promise<WorkerDesktopLaunchResult> => {
if (options.getConfig().cloudWorkers?.desktop !== true) {
throw serviceError(
"invalid_state",
"worker desktop launch is disabled; enable the Desktop lab in Control UI Settings -> Labs (config: cloudWorkers.desktop)",
);
}
if (stopping) {
throw serviceError("invalid_state", "Worker environment service is stopping");
}
if (!tunnels) {
throw serviceError("invalid_state", "Worker tunnel runtime is unavailable");
}
const requireLaunchable = () => {
if (stopping) {
throw serviceError("invalid_state", "Worker environment service is stopping");
}
const record = store.get(request.environmentId);
if (!record) {
throw serviceError(
"environment_not_found",
`Unknown worker environment: ${request.environmentId}`,
);
}
if (
!inState(record, "ready", "idle", "attached") ||
record.destroyRequestedAtMs !== null ||
!record.leaseId ||
!record.sshEndpoint ||
!record.desktop
) {
throw serviceError(
"invalid_state",
"environment has no desktop; desktop is a warm-time capability of the profile",
);
}
const app = record.desktop.apps?.find((candidate) => candidate.id === request.app);
if (!app) {
throw serviceError(
"desktop_app_not_found",
`environment does not advertise desktop app: ${request.app}`,
);
}
return { app, record };
};
let startup: Promise<void> | undefined;
let launchEpoch: number | undefined;
await withLock(request.environmentId, async () => {
const { app, record } = requireLaunchable();
const provider = providerFor(record.providerId);
launchEpoch = record.ownerEpoch;
startup = tunnels.desktop.launchApp({
environmentId: record.environmentId,
ownerEpoch: record.ownerEpoch,
ssh: record.sshEndpoint,
app,
resolveIdentity: identityResolverFor(record, provider, record.leaseId),
});
});
if (!startup || launchEpoch === undefined) {
throw serviceError("launcher_failure", "Worker desktop app launcher failed to start");
}
try {
await startup;
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "unsupported_platform"
) {
throw serviceError(
"unsupported_platform",
"desktop app launch is not supported on Windows gateway hosts",
);
}
// A teardown aborts the SSH child before mutating the durable row. Wait for the
// environment lock, then report the authoritative lifecycle state instead of a launch error.
await withLock(request.environmentId, async () => {
const { record } = requireLaunchable();
if (record.ownerEpoch !== launchEpoch) {
throw serviceError("invalid_state", "Worker desktop app launch owner changed");
}
});
throw serviceError(
"launcher_failure",
`worker desktop ${request.app} launcher failed; verify the app is installed and retry`,
);
}
await withLock(request.environmentId, async () => {
const { record } = requireLaunchable();
if (record.ownerEpoch !== launchEpoch) {
throw serviceError("invalid_state", "Worker desktop app launch owner changed");
}
});
return { app: request.app, status: "ready" };
};
const stopTunnel = async (environmentId: string, ownerEpoch?: number): Promise<void> => {
await withLock(environmentId, async () => {
await tunnels?.stop(environmentId, ownerEpoch);
@@ -1608,6 +1743,7 @@ export function createWorkerEnvironmentService(options: WorkerEnvironmentService
destroyUnattached: async (environmentId: string) =>
project(await destroy(environmentId, { requireUnattached: true })),
observeDesktop,
launchDesktopApp,
admitWorker: async (admission: WorkerConnectParams["admission"]) => {
if (stopping) {
return { ok: false, reason: "environment-unavailable" } as const;
@@ -20,6 +20,7 @@ import {
import { hashWorkerCredential } from "./credential.js";
import {
createWorkerEnvironmentStore,
normalizeWorkerDesktopEndpoint,
normalizeWorkerSshEndpoint,
type WorkerEnvironmentStore,
} from "./store.js";
@@ -45,6 +46,14 @@ const DESKTOP: WorkerDesktopEndpoint = {
protocol: "rfb",
port: 5900,
passwordFilePath: "/var/lib/crabbox/vnc.password",
apps: [
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
},
{ id: "terminal", executablePath: "/usr/local/bin/openclaw-worker-terminal" },
],
};
const BOOTSTRAP_RECEIPT: WorkerEnvironmentBootstrapReceipt = {
bundleHash: "a".repeat(64),
@@ -371,6 +380,78 @@ describe("worker environment store", () => {
).toThrow("SSH fallback ports");
});
it.each([
["a non-array app list", "browser", "desktop apps must be an array"],
[
"more than eight apps",
Array.from({ length: 9 }, () => ({
id: "terminal",
executablePath: "/usr/bin/xfce4-terminal",
})),
"desktop apps cannot exceed 8",
],
[
"an unknown app id",
[{ id: "editor", executablePath: "/usr/bin/editor" }],
'desktop app id must be "browser" or "terminal"',
],
[
"duplicate app ids",
[
{ id: "terminal", executablePath: "/usr/bin/xfce4-terminal" },
{ id: "terminal", executablePath: "/usr/local/bin/openclaw-worker-terminal" },
],
"desktop app id terminal must be unique",
],
[
"a relative executable path",
[{ id: "terminal", executablePath: "bin/xfce4-terminal" }],
"desktop app executable path must be absolute",
],
[
"an invalid browser CDP port",
[
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 65_536,
},
],
"browser CDP port must be an integer",
],
[
"an unknown browser field",
[
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
args: ["--headless"],
},
],
"browser desktop app contains unknown fields",
],
[
"an unknown terminal field",
[
{
id: "terminal",
executablePath: "/usr/local/bin/openclaw-worker-terminal",
env: { DISPLAY: ":99" },
},
],
"terminal desktop app contains unknown fields",
],
])("rejects %s", (_name, apps, error) => {
expect(() =>
normalizeWorkerDesktopEndpoint({
protocol: "rfb",
port: 5900,
apps,
} as unknown as WorkerDesktopEndpoint),
).toThrow(error);
});
it("round-trips desktop metadata and clears it with the provider lease", () => {
createIntent("worker-desktop");
store.transition({
+48
View File
@@ -15,6 +15,7 @@ import {
getNodeSqliteKysely,
} from "../../infra/kysely-sync.js";
import type {
WorkerDesktopApp,
WorkerDesktopEndpoint,
WorkerProfile,
WorkerSshEndpoint,
@@ -116,6 +117,7 @@ const TERMINAL_STATES: WorkerEnvironmentState[] = ["destroyed", "failed", "orpha
const WORKER_BUNDLE_HASH_PATTERN = /^[a-f0-9]{64}$/u;
const MAX_HOST_KEY_LENGTH = 16_384;
const MAX_SSH_FALLBACK_PORTS = 10;
const MAX_WORKER_DESKTOP_APPS = 8;
const ensuredWorkerEnvironmentDatabases = new WeakSet<DatabaseSync>();
const WORKER_ENVIRONMENT_SSH_FALLBACK_PORTS_SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS worker_environment_ssh_fallback_ports (
@@ -297,10 +299,56 @@ export function normalizeWorkerDesktopEndpoint(
) {
throw new Error("Worker environment desktop password file path must be absolute");
}
if (value.apps !== undefined && !Array.isArray(value.apps)) {
throw new Error("Worker environment desktop apps must be an array");
}
if ((value.apps?.length ?? 0) > MAX_WORKER_DESKTOP_APPS) {
throw new Error(`Worker environment desktop apps cannot exceed ${MAX_WORKER_DESKTOP_APPS}`);
}
const seenAppIds = new Set<WorkerDesktopApp["id"]>();
const apps = (value.apps ?? []).map((app): WorkerDesktopApp => {
if (!isRecord(app) || (app.id !== "browser" && app.id !== "terminal")) {
throw new Error('Worker environment desktop app id must be "browser" or "terminal"');
}
if (seenAppIds.has(app.id)) {
throw new Error(`Worker environment desktop app id ${app.id} must be unique`);
}
seenAppIds.add(app.id);
if (typeof app.executablePath !== "string" || !isAbsolute(app.executablePath)) {
throw new Error("Worker environment desktop app executable path must be absolute");
}
if (app.id === "terminal") {
if (Object.keys(app).some((key) => key !== "id" && key !== "executablePath")) {
throw new Error("Worker environment terminal desktop app contains unknown fields");
}
return { id: "terminal", executablePath: app.executablePath };
}
if (
Object.keys(app).some((key) => key !== "id" && key !== "executablePath" && key !== "cdpPort")
) {
throw new Error("Worker environment browser desktop app contains unknown fields");
}
if (
typeof app.cdpPort !== "number" ||
!Number.isSafeInteger(app.cdpPort) ||
app.cdpPort < 1 ||
app.cdpPort > 65_535
) {
throw new Error(
"Worker environment browser CDP port must be an integer from 1 through 65535",
);
}
return {
id: "browser",
executablePath: app.executablePath,
cdpPort: app.cdpPort,
};
});
return {
protocol: "rfb",
port: value.port,
...(passwordFilePath === undefined ? {} : { passwordFilePath }),
...(value.apps === undefined ? {} : { apps }),
};
}
function endpointFrom(row: Row, fallbackPorts: readonly number[]): Ssh | null {
@@ -0,0 +1,42 @@
import type { SessionPlacementTurnParams } from "../../agents/session-placement-admission.js";
import { resolveManifestActivationPluginIds } from "../../plugins/activation-planner.js";
import type { WorkerDesktopEndpoint } from "../../plugins/types.js";
import type { WorkerBrowserLaunchDescriptor } from "../../worker/launch-descriptor.js";
import type { WorkerToolAuthority } from "../../worker/tool-authority.js";
import { resolveWorkerToolAuthority } from "./worker-tool-authority.js";
/** Plans the optional Browser surface from persisted provider metadata and normal tool policy. */
export function resolveWorkerBrowserLaunchPlan(params: {
desktop: WorkerDesktopEndpoint | null;
modelRef: { provider: string; model: string };
turn: SessionPlacementTurnParams;
}): {
browser?: WorkerBrowserLaunchDescriptor;
toolAuthority: WorkerToolAuthority;
} {
const browserApp = params.desktop?.apps?.find((app) => app.id === "browser");
const browserAvailable =
browserApp !== undefined &&
params.turn.config?.browser?.enabled !== false &&
resolveManifestActivationPluginIds({
trigger: { kind: "capability", capability: "tool" },
config: params.turn.config,
onlyPluginIds: ["browser"],
}).includes("browser");
const toolAuthority = resolveWorkerToolAuthority({
modelRef: params.modelRef,
turn: params.turn,
...(browserAvailable ? { availableOptionalToolNames: ["browser"] } : {}),
});
return {
toolAuthority,
...(browserApp && toolAuthority.allowedToolNames.includes("browser")
? {
browser: {
cdpUrl: `http://127.0.0.1:${browserApp.cdpPort}`,
launcherPath: browserApp.executablePath,
},
}
: {}),
};
}
@@ -30,6 +30,24 @@ describe("resolveWorkerToolAuthority", () => {
expect(authority()).toEqual(["read", "write", "edit", "apply_patch", "exec", "process"]);
});
it("adds the optional browser surface only when the launcher makes it available", () => {
expect(
resolveWorkerToolAuthority({
modelRef: { provider: "openai", model: "gpt-test" },
turn: turn(),
availableOptionalToolNames: ["browser"],
}).allowedToolNames,
).toEqual(["read", "write", "edit", "apply_patch", "exec", "process", "browser"]);
expect(
resolveWorkerToolAuthority({
modelRef: { provider: "openai", model: "gpt-test" },
turn: turn({ toolsAllow: ["browser"] }),
availableOptionalToolNames: ["browser"],
}).allowedToolNames,
).toEqual(["browser"]);
expect(authority({ toolsAllow: ["browser"] })).toEqual([]);
});
it("projects runtime caps with canonical write-to-apply_patch semantics", () => {
expect(authority({ toolsAllow: ["write"] })).toEqual(["write", "apply_patch"]);
expect(authority({ toolsAllow: [] })).toEqual([]);
@@ -5,8 +5,9 @@ import { resolveSandboxRuntimeStatus } from "../../agents/sandbox/runtime-status
import type { SessionPlacementTurnParams } from "../../agents/session-placement-admission.js";
import { logWarn } from "../../logger.js";
import {
WORKER_LOCAL_TOOL_NAMES,
WORKER_REQUIRED_LOCAL_TOOL_NAMES,
type WorkerLocalToolName,
type WorkerOptionalLocalToolName,
type WorkerToolAuthority,
} from "../../worker/tool-authority.js";
@@ -72,13 +73,16 @@ function resolveWorkerCapabilityProfile(params: {
export function resolveWorkerToolAuthority(params: {
modelRef: { provider: string; model: string };
turn: SessionPlacementTurnParams;
availableOptionalToolNames?: readonly WorkerOptionalLocalToolName[];
}): WorkerToolAuthority {
const turn = params.turn;
if (turn.disableTools === true || turn.modelRun === true || turn.promptMode === "none") {
return { allowedToolNames: [] };
}
const runtimeCappedTools = applyEmbeddedAttemptToolsAllow(
WORKER_LOCAL_TOOL_NAMES.map((name) => ({ name })),
[...WORKER_REQUIRED_LOCAL_TOOL_NAMES, ...(params.availableOptionalToolNames ?? [])].map(
(name) => ({ name }),
),
turn.toolsAllow,
);
const projected: WorkerLocalToolName[] = projectConversationToolNames({
@@ -274,6 +274,7 @@ describe("worker turn launcher", () => {
state: "attached",
desktop: null,
desktopAvailable: false,
desktopApps: [],
leaseId: "lease-worker-turn",
sshEndpoint: {
host: "worker.example.test",
@@ -285,6 +286,25 @@ describe("worker turn launcher", () => {
};
}
function browserEnvironment(): WorkerTurnEnvironmentRecord {
return {
...attachedEnvironment(),
desktop: {
protocol: "rfb",
port: 5900,
apps: [
{
id: "browser",
executablePath: "/usr/local/bin/openclaw-worker-browser",
cdpPort: 9222,
},
],
},
desktopAvailable: true,
desktopApps: ["browser"],
};
}
function credential(deliveryId = "c".repeat(43)): MintedWorkerCredential {
return {
credential: ["worker", "turn", "credential"].join("-"),
@@ -700,7 +720,7 @@ describe("worker turn launcher", () => {
stop: vi.fn(async () => {}),
};
const environments: WorkerTurnEnvironmentService = {
get: vi.fn(() => attachedEnvironment()),
get: vi.fn(() => browserEnvironment()),
acquireTurnCredential: vi.fn(async () => credential()),
acknowledgeCredentialDelivery,
startTunnel: vi.fn(async () => tunnel),
@@ -727,6 +747,7 @@ describe("worker turn launcher", () => {
},
{
...turn(),
toolsAllow: ["browser"],
workspaceDir: path.join(root, "stale-caller-workspace"),
transcriptPrompt: "Canonical transcript request",
onAgentEvent,
@@ -769,14 +790,11 @@ describe("worker turn launcher", () => {
expect(descriptor?.assignment.prompt).toBe("Inspect this workspace");
expect(descriptor?.assignment.suppressPromptTranscript).toBe(true);
expect(descriptor?.version).toBe(2);
expect(descriptor?.assignment.toolAuthority.allowedToolNames).toEqual([
"read",
"write",
"edit",
"apply_patch",
"exec",
"process",
]);
expect(descriptor?.assignment.toolAuthority.allowedToolNames).toEqual(["browser"]);
expect(descriptor?.assignment.browser).toEqual({
cdpUrl: "http://127.0.0.1:9222",
launcherPath: "/usr/local/bin/openclaw-worker-browser",
});
expect(descriptor?.assignment.initialMessages).toEqual([
{
role: "user",
@@ -917,7 +935,7 @@ describe("worker turn launcher", () => {
stop: vi.fn(async () => {}),
};
const environments: WorkerTurnEnvironmentService = {
get: vi.fn(() => attachedEnvironment()),
get: vi.fn(() => browserEnvironment()),
acquireTurnCredential: vi.fn(async () => credential()),
acknowledgeCredentialDelivery: vi.fn(() => true),
startTunnel: vi.fn(async () => tunnel),
@@ -935,12 +953,19 @@ describe("worker turn launcher", () => {
},
{
...turn("run-persisted-user"),
config: {
...turn("run-persisted-user").config,
plugins: { entries: { browser: { enabled: false } } },
},
toolsAllow: ["browser"],
suppressNextUserMessagePersistence: true,
},
async () => ({ meta: { durationMs: 1 } }),
);
expect(descriptor?.assignment.prompt).toBe("Inspect this workspace");
expect(descriptor?.assignment.toolAuthority.allowedToolNames).toEqual([]);
expect(descriptor?.assignment.browser).toBeUndefined();
expect(descriptor?.assignment.initialMessages).toMatchObject([
{ role: "user" },
{ role: "assistant", content: [{ id: "shared-call" }] },
@@ -20,7 +20,7 @@ import type {
WorkerSessionTurnClaim,
} from "./placement-store.js";
import type { WorkerEnvironmentService } from "./service.js";
import { resolveWorkerToolAuthority } from "./worker-tool-authority.js";
import { resolveWorkerBrowserLaunchPlan } from "./worker-browser-launch-plan.js";
import {
claimWorkerTurn,
latestDurableWorkspaceConflict,
@@ -315,7 +315,11 @@ async function executeWorkerTurn(params: {
timeoutMs: turn.timeoutMs,
});
const reasoning = mapThinkingLevelForProvider(turn.thinkLevel);
const toolAuthority = resolveWorkerToolAuthority({ modelRef, turn });
const { browser, toolAuthority } = resolveWorkerBrowserLaunchPlan({
desktop: environment.desktop,
modelRef,
turn,
});
const launchPlan = fitLaunchDescriptor(
(windowedMessages) =>
parseWorkerLaunchDescriptor({
@@ -348,6 +352,7 @@ async function executeWorkerTurn(params: {
nextSeq: (placement.lastLiveEventAckCursor ?? 0) + 1,
},
toolAuthority,
...(browser ? { browser } : {}),
},
}),
initialMessages,
+1
View File
@@ -131,6 +131,7 @@ export type {
TranscriptSourceProvider,
UnifiedModelCatalogProviderContext,
UnifiedModelCatalogProviderPlugin,
WorkerDesktopApp,
WorkerDesktopEndpoint,
WorkerLease,
WorkerLeaseStatus,
+13
View File
@@ -78,6 +78,15 @@ export type WorkerSshIdentityRequest = {
keyRef: SecretRef;
};
/** Closed set of applications installed and launchable on a provisioned worker desktop. */
export type WorkerDesktopApp =
| {
id: "browser";
executablePath: string;
cdpPort: number;
}
| { id: "terminal"; executablePath: string };
/** Optional interactive desktop endpoint provisioned with the lease (warm-time capability). */
export type WorkerDesktopEndpoint = {
/** Desktop service protocol on the worker loopback; "rfb" is the only phase-1 value. */
@@ -86,6 +95,8 @@ export type WorkerDesktopEndpoint = {
port: number;
/** Absolute on-box path to the per-lease password file; read over SSH, never persisted as plaintext. */
passwordFilePath?: string;
/** Closed application metadata advertised by the provider for this desktop. */
apps?: WorkerDesktopApp[];
};
/** Durable lease identity and endpoint returned by a successful provision operation. */
@@ -125,6 +136,8 @@ export type WorkerProvider = {
* Repeating the same operation id must be idempotent across gateway restarts.
*/
provision: (profile: WorkerProfile, operationId: string) => Promise<WorkerLease>;
/** Maximum core wait for one provision attempt, including provider-owned setup and cleanup. */
resolveProvisionTimeoutMs?: (profile: WorkerProfile) => number;
/** Throws on transient/indeterminate failures; `unknown` means authoritative absence. */
inspect: (lease: { leaseId: string; profile: WorkerProfile }) => Promise<WorkerLeaseStatus>;
/**
+2
View File
@@ -137,6 +137,8 @@ export type {
RealtimeTranscriptionProviderPlugin,
SpeechProviderPlugin,
TranscriptSourceProvider,
WorkerDesktopApp,
WorkerDesktopEndpoint,
WorkerLease,
WorkerLeaseStatus,
WorkerProfile,
+122
View File
@@ -0,0 +1,122 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
execFile: vi.fn(),
loadBundledPluginPublicSurfaceModuleSync: vi.fn(),
}));
vi.mock("node:child_process", () => ({
execFile: mocks.execFile,
}));
vi.mock("../plugin-sdk/facade-loader.js", () => ({
loadBundledPluginPublicSurfaceModuleSync: mocks.loadBundledPluginPublicSurfaceModuleSync,
}));
import { createWorkerBrowserToolRuntime } from "./browser-runtime.js";
describe("worker Browser runtime", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("loads only the bundled Browser runtime and launches the fixed executable without arguments", async () => {
const dispose = vi.fn().mockResolvedValue(undefined);
const createAttachedBrowserToolRuntime = vi.fn().mockResolvedValue({
tool: { name: "browser" },
dispose,
});
mocks.loadBundledPluginPublicSurfaceModuleSync.mockReturnValue({
createAttachedBrowserToolRuntime,
});
mocks.execFile.mockImplementation(
(
_file: string,
_args: string[],
_options: object,
callback: (error: Error | null) => void,
) => {
callback(null);
return {};
},
);
const runtime = await createWorkerBrowserToolRuntime({
descriptor: {
cdpUrl: "http://127.0.0.1:9222",
launcherPath: "/usr/local/bin/openclaw-worker-browser",
},
sessionKey: "worker:session-1",
stateDir: "/tmp/worker-state",
workspaceDir: "/tmp/workspace",
});
expect(mocks.loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({
dirName: "browser",
artifactBasename: "runtime-api.js",
trackedPluginId: "browser",
});
expect(createAttachedBrowserToolRuntime).toHaveBeenCalledWith({
cdpUrl: "http://127.0.0.1:9222",
ensureAttachTarget: expect.any(Function),
agentSessionKey: "worker:session-1",
agentDir: "/tmp/worker-state",
workspaceDir: "/tmp/workspace",
});
const ensureAttachTarget = createAttachedBrowserToolRuntime.mock.calls[0]?.[0]
.ensureAttachTarget as () => Promise<void>;
await ensureAttachTarget();
expect(mocks.execFile).toHaveBeenCalledWith(
"/usr/local/bin/openclaw-worker-browser",
[],
{
timeout: 30_000,
maxBuffer: 64 * 1024,
windowsHide: true,
},
expect.any(Function),
);
await runtime.dispose();
expect(dispose).toHaveBeenCalledOnce();
});
it("surfaces launcher failure without loading another browser route", async () => {
const createAttachedBrowserToolRuntime = vi.fn().mockResolvedValue({
tool: { name: "browser" },
dispose: vi.fn().mockResolvedValue(undefined),
});
mocks.loadBundledPluginPublicSurfaceModuleSync.mockReturnValue({
createAttachedBrowserToolRuntime,
});
mocks.execFile.mockImplementation(
(
_file: string,
_args: string[],
_options: object,
callback: (error: Error | null) => void,
) => {
callback(new Error("launcher timed out"));
return {};
},
);
await createWorkerBrowserToolRuntime({
descriptor: {
cdpUrl: "http://127.0.0.1:9222",
launcherPath: "/usr/local/bin/openclaw-worker-browser",
},
sessionKey: "worker:session-1",
stateDir: "/tmp/worker-state",
workspaceDir: "/tmp/workspace",
});
const ensureAttachTarget = createAttachedBrowserToolRuntime.mock.calls[0]?.[0]
.ensureAttachTarget as () => Promise<void>;
await expect(ensureAttachTarget()).rejects.toThrow(
"Worker Browser launcher failed: launcher timed out",
);
expect(mocks.loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledOnce();
});
});
+76
View File
@@ -0,0 +1,76 @@
/** Core-private adapter for the bundled Browser plugin's attached worker runtime. */
import { execFile } from "node:child_process";
import type { AnyAgentTool } from "../agents/tools/common.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "../plugin-sdk/facade-loader.js";
import type { WorkerBrowserLaunchDescriptor } from "./launch-descriptor.js";
const WORKER_BROWSER_LAUNCH_TIMEOUT_MS = 30_000;
const WORKER_BROWSER_LAUNCH_OUTPUT_LIMIT_BYTES = 64 * 1024;
type BundledBrowserRuntimeSurface = {
createAttachedBrowserToolRuntime: (params: {
cdpUrl: string;
ensureAttachTarget: () => Promise<void>;
agentSessionKey?: string;
agentDir?: string;
workspaceDir?: string;
}) => Promise<{
tool: AnyAgentTool;
dispose: () => Promise<void>;
}>;
};
type WorkerBrowserToolRuntime = {
tool: AnyAgentTool;
dispose: () => Promise<void>;
};
type CreateWorkerBrowserToolRuntimeParams = {
descriptor: WorkerBrowserLaunchDescriptor;
sessionKey: string;
stateDir: string;
workspaceDir: string;
};
function runWorkerBrowserLauncher(launcherPath: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
execFile(
launcherPath,
[],
{
timeout: WORKER_BROWSER_LAUNCH_TIMEOUT_MS,
maxBuffer: WORKER_BROWSER_LAUNCH_OUTPUT_LIMIT_BYTES,
windowsHide: true,
},
(error) => {
if (error) {
reject(
new Error(`Worker Browser launcher failed: ${error.message}`, {
cause: error,
}),
);
return;
}
resolve();
},
);
});
}
/** Materialize the exact bundled Browser runtime; no descriptor-controlled plugin path is used. */
export async function createWorkerBrowserToolRuntime(
params: CreateWorkerBrowserToolRuntimeParams,
): Promise<WorkerBrowserToolRuntime> {
const browserRuntime = loadBundledPluginPublicSurfaceModuleSync<BundledBrowserRuntimeSurface>({
dirName: "browser",
artifactBasename: "runtime-api.js",
trackedPluginId: "browser",
});
return await browserRuntime.createAttachedBrowserToolRuntime({
cdpUrl: params.descriptor.cdpUrl,
ensureAttachTarget: async () => await runWorkerBrowserLauncher(params.descriptor.launcherPath),
agentSessionKey: params.sessionKey,
agentDir: params.stateDir,
workspaceDir: params.workspaceDir,
});
}
+74 -42
View File
@@ -26,13 +26,19 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { AssistantMessage, AssistantMessageEventStreamLike } from "../llm/types.js";
import { getProcessSupervisor } from "../process/supervisor/index.js";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import { createWorkerBrowserToolRuntime } from "./browser-runtime.js";
import { createWorkerLiveRuntime } from "./embedded-agent-live.runtime.js";
import {
createWorkerTranscriptRuntime,
toAgentMessage,
toWorkerInferenceContext,
} from "./embedded-agent-transcript.runtime.js";
import { WORKER_LOCAL_TOOL_NAMES, type WorkerLocalToolName } from "./tool-authority.js";
import type { WorkerBrowserLaunchDescriptor } from "./launch-descriptor.js";
import {
WORKER_LOCAL_TOOL_NAMES,
WORKER_REQUIRED_LOCAL_TOOL_NAMES,
type WorkerLocalToolName,
} from "./tool-authority.js";
import { WORKER_PROVIDER_REPLAY_LOCAL_RETRY_MESSAGE } from "./transcript-message.js";
function toError(value: unknown, fallback: string): Error {
@@ -76,12 +82,17 @@ type RunWorkerEmbeddedTurnParams = {
systemPrompt?: string;
inferenceOptions?: WorkerInferenceOptions;
allowedToolNames: readonly WorkerLocalToolName[];
browser?: WorkerBrowserLaunchDescriptor;
signal?: AbortSignal;
};
const WORKER_TOOL_CONFIG = { plugins: { enabled: false } } satisfies OpenClawConfig;
export async function runWorkerEmbeddedTurn(params: RunWorkerEmbeddedTurnParams): Promise<void> {
const browserAuthorized = params.allowedToolNames.includes("browser");
if (browserAuthorized !== (params.browser !== undefined)) {
throw new Error("Worker Browser authority and launch descriptor must be provided together.");
}
const model = createNativeModelOwnedRuntimeModel({
provider: params.modelRef.provider,
modelId: params.modelRef.model,
@@ -153,48 +164,65 @@ export async function runWorkerEmbeddedTurn(params: RunWorkerEmbeddedTurnParams)
},
processDefaults: { scopeKey: params.sessionKey },
});
const localTools = finalizeAgentTools({
tools: coreTools,
modelProvider: params.modelRef.provider,
modelId: params.modelRef.model,
hookContext: {
agentId: DEFAULT_AGENT_ID,
config: WORKER_TOOL_CONFIG,
cwd: params.cwd,
workspaceDir: params.cwd,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
runId: params.runId,
requester: { senderIsOwner: true },
loopDetection: resolveToolLoopDetectionConfig({
cfg: WORKER_TOOL_CONFIG,
const browserRuntime = params.browser
? await createWorkerBrowserToolRuntime({
descriptor: params.browser,
sessionKey: params.sessionKey,
stateDir: params.stateDir,
workspaceDir: params.cwd,
})
: undefined;
const { session } = await (async () => {
try {
const localTools = finalizeAgentTools({
tools: browserRuntime ? [...coreTools, browserRuntime.tool] : coreTools,
modelProvider: params.modelRef.provider,
modelId: params.modelRef.model,
hookContext: {
agentId: DEFAULT_AGENT_ID,
config: WORKER_TOOL_CONFIG,
cwd: params.cwd,
workspaceDir: params.cwd,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
runId: params.runId,
requester: { senderIsOwner: true },
loopDetection: resolveToolLoopDetectionConfig({
cfg: WORKER_TOOL_CONFIG,
agentId: DEFAULT_AGENT_ID,
}),
},
agentId: DEFAULT_AGENT_ID,
}),
},
agentId: DEFAULT_AGENT_ID,
}).filter((tool) => localToolNameSet.has(tool.name));
const discoveredToolNames = new Set(localTools.map((tool) => tool.name));
for (const toolName of WORKER_LOCAL_TOOL_NAMES) {
if (!discoveredToolNames.has(toolName)) {
throw new Error(`Worker coding tool unavailable: ${toolName}`);
}
}
}).filter((tool) => localToolNameSet.has(tool.name));
const discoveredToolNames = new Set(localTools.map((tool) => tool.name));
for (const toolName of WORKER_REQUIRED_LOCAL_TOOL_NAMES) {
if (!discoveredToolNames.has(toolName)) {
throw new Error(`Worker coding tool unavailable: ${toolName}`);
}
}
const { session } = await createAgentSession({
cwd: params.cwd,
agentDir: params.stateDir,
authStorage,
modelRegistry,
model,
thinkingLevel: "medium",
tools: [...activeToolNames],
customTools: toToolDefinitions(localTools.filter((tool) => allowedToolNameSet.has(tool.name))),
noTools: "all",
sessionManager,
settingsManager,
resourceLoader,
withSessionWriteSettlement: transcriptRuntime.withSessionWriteSettlement,
});
return await createAgentSession({
cwd: params.cwd,
agentDir: params.stateDir,
authStorage,
modelRegistry,
model,
thinkingLevel: "medium",
tools: [...activeToolNames],
customTools: toToolDefinitions(
localTools.filter((tool) => allowedToolNameSet.has(tool.name)),
),
noTools: "all",
sessionManager,
settingsManager,
resourceLoader,
withSessionWriteSettlement: transcriptRuntime.withSessionWriteSettlement,
});
} catch (error) {
await browserRuntime?.dispose();
throw error;
}
})();
session.agent.sessionId = params.sessionId;
session.setActiveToolsByName([...activeToolNames]);
session.agent.streamFn = (_model, context, options) => {
@@ -245,7 +273,10 @@ export async function runWorkerEmbeddedTurn(params: RunWorkerEmbeddedTurnParams)
runFailure = params.signal?.aborted
? toError(params.signal.reason, "Worker agent turn aborted.")
: toError(error, "Worker agent turn failed.");
liveRuntime.enqueueRunFailure({ aborted: params.signal?.aborted === true, error: runFailure });
liveRuntime.enqueueRunFailure({
aborted: params.signal?.aborted === true,
error: runFailure,
});
}
let finalTranscriptFailure: Error | undefined;
@@ -264,6 +295,7 @@ export async function runWorkerEmbeddedTurn(params: RunWorkerEmbeddedTurnParams)
unsubscribe();
getProcessSupervisor().cancelScope(params.sessionKey, "manual-cancel");
session.dispose();
await browserRuntime?.dispose();
}
if (runFailure !== undefined) {
throw runFailure;
+30
View File
@@ -144,6 +144,36 @@ describe("worker launch descriptor", () => {
descriptor.assignment.toolAuthority.allowedToolNames = [];
expect(parseWorkerLaunchDescriptor(structuredClone(descriptor))).toEqual(descriptor);
descriptor.assignment.toolAuthority.allowedToolNames = ["browser"];
expect(parseWorkerLaunchDescriptor(structuredClone(descriptor))).toEqual(descriptor);
});
it("accepts only a closed absolute loopback browser attachment descriptor", () => {
const descriptor = launchDescriptor();
descriptor.assignment.browser = {
cdpUrl: "http://127.0.0.1:9222",
launcherPath: "/usr/local/bin/openclaw-worker-browser",
};
expect(parseWorkerLaunchDescriptor(structuredClone(descriptor))).toEqual(descriptor);
const browser = descriptor.assignment.browser;
const cases: unknown[] = [
{ ...browser, unexpected: true },
{ ...browser, cdpUrl: "https://127.0.0.1:9222" },
{ ...browser, cdpUrl: "http://localhost:9222" },
{ ...browser, cdpUrl: "http://127.0.0.1" },
{ ...browser, cdpUrl: "http://127.0.0.1:9222/json/version" },
{ ...browser, launcherPath: "openclaw-worker-browser" },
];
for (const invalidBrowser of cases) {
expect(() =>
parseWorkerLaunchDescriptor({
...descriptor,
assignment: { ...descriptor.assignment, browser: invalidBrowser },
}),
).toThrow("invalid worker launch descriptor");
}
});
it("rejects non-absolute paths, unattached sessions, and discontinuous event sequences", () => {
+51 -2
View File
@@ -29,6 +29,11 @@ import { isWorkerTranscriptMessageFrameSafe } from "./transcript-message.js";
const LAUNCH_VERSION = 2;
export type WorkerBrowserLaunchDescriptor = {
cdpUrl: string;
launcherPath: string;
};
type WorkerLaunchAssignment = {
runId: string;
turnId: string;
@@ -48,6 +53,7 @@ type WorkerLaunchAssignment = {
nextSeq: number;
};
toolAuthority: WorkerToolAuthority;
browser?: WorkerBrowserLaunchDescriptor;
};
type WorkerLaunchAdmission = Omit<WorkerConnectParams["admission"], "runId"> & {
@@ -98,6 +104,44 @@ function parseToolAuthority(value: unknown): WorkerToolAuthority | undefined {
return { allowedToolNames: [...value.allowedToolNames] };
}
function parseBrowserLaunchDescriptor(value: unknown): WorkerBrowserLaunchDescriptor | undefined {
if (
!isRecord(value) ||
!hasExactKeys(value, ["cdpUrl", "launcherPath"]) ||
typeof value.cdpUrl !== "string" ||
typeof value.launcherPath !== "string" ||
!path.isAbsolute(value.launcherPath)
) {
return undefined;
}
let cdpUrl: URL;
try {
cdpUrl = new URL(value.cdpUrl);
} catch {
return undefined;
}
const port = Number(cdpUrl.port);
if (
cdpUrl.protocol !== "http:" ||
cdpUrl.hostname !== "127.0.0.1" ||
cdpUrl.username !== "" ||
cdpUrl.password !== "" ||
cdpUrl.port === "" ||
!Number.isInteger(port) ||
port < 1 ||
port > 65_535 ||
cdpUrl.pathname !== "/" ||
cdpUrl.search !== "" ||
cdpUrl.hash !== ""
) {
return undefined;
}
return {
cdpUrl: value.cdpUrl,
launcherPath: value.launcherPath,
};
}
function parseAssignment(value: unknown): WorkerLaunchAssignment | undefined {
if (
!isRecord(value) ||
@@ -116,7 +160,7 @@ function parseAssignment(value: unknown): WorkerLaunchAssignment | undefined {
"liveEvents",
"toolAuthority",
],
["systemPrompt"],
["systemPrompt", "browser"],
)
) {
return undefined;
@@ -139,6 +183,11 @@ function parseAssignment(value: unknown): WorkerLaunchAssignment | undefined {
if (!toolAuthority) {
return undefined;
}
const browser =
value.browser === undefined ? undefined : parseBrowserLaunchDescriptor(value.browser);
if (value.browser !== undefined && !browser) {
return undefined;
}
if (
!Value.Check(WorkerInferenceModelRefSchema, value.modelRef) ||
!isInferenceOptions(value.inferenceOptions)
@@ -162,7 +211,7 @@ function parseAssignment(value: unknown): WorkerLaunchAssignment | undefined {
) {
return undefined;
}
return { ...value, toolAuthority } as WorkerLaunchAssignment;
return { ...value, toolAuthority, ...(browser ? { browser } : {}) } as WorkerLaunchAssignment;
}
export function buildWorkerConnectParams(
+9 -1
View File
@@ -1,4 +1,4 @@
export const WORKER_LOCAL_TOOL_NAMES = [
export const WORKER_REQUIRED_LOCAL_TOOL_NAMES = [
"read",
"write",
"edit",
@@ -7,7 +7,15 @@ export const WORKER_LOCAL_TOOL_NAMES = [
"process",
] as const;
const WORKER_OPTIONAL_LOCAL_TOOL_NAMES = ["browser"] as const;
export const WORKER_LOCAL_TOOL_NAMES = [
...WORKER_REQUIRED_LOCAL_TOOL_NAMES,
...WORKER_OPTIONAL_LOCAL_TOOL_NAMES,
] as const;
export type WorkerLocalToolName = (typeof WORKER_LOCAL_TOOL_NAMES)[number];
export type WorkerOptionalLocalToolName = (typeof WORKER_OPTIONAL_LOCAL_TOOL_NAMES)[number];
const WORKER_LOCAL_TOOL_NAME_SET = new Set<string>(WORKER_LOCAL_TOOL_NAMES);
+69 -1
View File
@@ -1,8 +1,9 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { createServer, type Server } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import { rawDataToString } from "@openclaw/gateway-client/websocket-data";
import { Type } from "typebox";
import { Value } from "typebox/value";
import { afterEach, describe, expect, it, vi } from "vitest";
import { WebSocket, WebSocketServer, type RawData } from "ws";
@@ -44,6 +45,25 @@ import {
} from "./worker-rpc-clients.js";
import { runWorkerDescriptor } from "./worker.runtime.js";
const browserRuntimeMocks = vi.hoisted(() => ({
createWorkerBrowserToolRuntime: vi.fn(),
dispose: vi.fn(),
}));
vi.mock("./browser-runtime.js", () => {
browserRuntimeMocks.createWorkerBrowserToolRuntime.mockImplementation(async () => ({
tool: {
name: "browser",
label: "Browser",
description: "Control the attached worker browser.",
parameters: Type.Object({}),
execute: async () => ({ content: [{ type: "text", text: "ok" }] }),
},
dispose: browserRuntimeMocks.dispose,
}));
return { createWorkerBrowserToolRuntime: browserRuntimeMocks.createWorkerBrowserToolRuntime };
});
function waitForFast<T>(
callback: () => T | Promise<T>,
options: { timeout?: number; interval?: number } = {},
@@ -770,6 +790,8 @@ async function setup(options?: FakeGatewayOptions): Promise<{
}
afterEach(async () => {
browserRuntimeMocks.createWorkerBrowserToolRuntime.mockClear();
browserRuntimeMocks.dispose.mockClear();
for (const gateway of gateways.splice(0)) {
await gateway.stop();
}
@@ -852,6 +874,52 @@ describe("worker runtime", () => {
expect(gateway.inferenceRequests[0]?.context.tools ?? []).toEqual([]);
});
it("materializes exactly the Browser tool for a browser-only assignment", async () => {
const { gateway, launch } = await setup();
launch.assignment.toolAuthority.allowedToolNames = ["browser"];
launch.assignment.browser = {
cdpUrl: "http://127.0.0.1:9222",
launcherPath: "/usr/local/bin/openclaw-worker-browser",
};
await expect(runWorkerDescriptor(launch)).resolves.toMatchObject({ status: "completed" });
expect(gateway.inferenceRequests[0]?.context.tools?.map((tool) => tool.name)).toEqual([
"browser",
]);
expect(browserRuntimeMocks.createWorkerBrowserToolRuntime).toHaveBeenCalledWith({
descriptor: launch.assignment.browser,
sessionKey: `worker:${SESSION_ID}`,
stateDir: expect.any(String),
workspaceDir: await realpath(launch.assignment.workspaceDir),
});
expect(browserRuntimeMocks.dispose).toHaveBeenCalledOnce();
});
it.each([
{ authority: ["browser"] as const, browser: undefined },
{
authority: ["read"] as const,
browser: {
cdpUrl: "http://127.0.0.1:9222",
launcherPath: "/usr/local/bin/openclaw-worker-browser",
},
},
])("fails before inference when Browser authority and descriptor disagree", async (testCase) => {
const { gateway, launch } = await setup();
launch.assignment.toolAuthority.allowedToolNames = [...testCase.authority];
if (testCase.browser) {
launch.assignment.browser = testCase.browser;
} else {
delete launch.assignment.browser;
}
await expect(runWorkerDescriptor(launch)).rejects.toThrow(
"Worker Browser authority and launch descriptor must be provided together",
);
expect(gateway.inferenceRequests).toHaveLength(0);
});
it("fail-stops a stale mid-run transcript without duplicating or rebasing the paid tail", async () => {
const { gateway, launch } = await setup({ transcriptFailureAtRequest: 2 });
+1
View File
@@ -138,6 +138,7 @@ export async function runWorkerDescriptor(
: { systemPrompt: descriptor.assignment.systemPrompt }),
inferenceOptions: descriptor.assignment.inferenceOptions,
allowedToolNames: descriptor.assignment.toolAuthority.allowedToolNames,
...(descriptor.assignment.browser ? { browser: descriptor.assignment.browser } : {}),
inference: { stream },
transcript: {
commit: async (messages) => {
@@ -0,0 +1,11 @@
import type { WorkerDesktopAppId } from "@openclaw/gateway-protocol";
import { t } from "../../i18n/index.ts";
import { icons } from "../icons.ts";
export function desktopAppLabel(app: WorkerDesktopAppId): string {
return app === "browser" ? t("browser.title") : t("terminal.title");
}
export function desktopAppIcon(app: WorkerDesktopAppId) {
return app === "browser" ? icons.chrome : icons.terminal;
}
@@ -18,6 +18,7 @@ class FakeSocket extends EventTarget {
function createFakeRfb() {
const instances: FakeRfb[] = [];
class FakeRfb extends EventTarget implements RfbClient {
background = "";
viewOnly = false;
scaleViewport = false;
readonly disconnect = vi.fn();
@@ -76,10 +77,12 @@ describe("DesktopClient", () => {
gatewayUrl: "ws://control.example.test",
wsUrl: "/worker-desktop/observe",
password: "secret",
background: "rgb(8, 8, 8)",
viewOnly: false,
target: document.createElement("div"),
});
expect(instances[0]?.background).toBe("rgb(8, 8, 8)");
expect(instances[0]?.viewOnly).toBe(false);
expect(instances[0]?.scaleViewport).toBe(true);
expect(instances[0]?.options).toEqual({ credentials: { password: "secret" } });
+4 -1
View File
@@ -8,7 +8,8 @@ type DesktopSecurityFailureDetail = {
status?: number;
};
export type DesktopConnectOptions = {
type DesktopConnectOptions = {
background?: string;
gatewayUrl?: string;
onConnect?: () => void;
onDisconnect?: (detail: DesktopDisconnectDetail) => void;
@@ -24,6 +25,7 @@ export type DesktopConnectionHandle = {
};
type RfbClient = EventTarget & {
background: string;
disconnect(): void;
scaleViewport: boolean;
viewOnly: boolean;
@@ -85,6 +87,7 @@ export class DesktopClient {
socket,
options.password ? { credentials: { password: options.password } } : undefined,
);
rfb.background = options.background ?? getComputedStyle(options.target).backgroundColor;
rfb.viewOnly = options.viewOnly;
rfb.scaleViewport = true;
rfb.addEventListener("connect", () => options.onConnect?.());
@@ -0,0 +1,135 @@
import { css } from "lit";
export const desktopPanelLauncherStyles = css`
.desktop-apps {
display: flex;
min-width: 0;
align-items: center;
gap: 3px;
}
.desktop-app-button,
.desktop-toolbar-action {
display: inline-flex;
align-items: center;
gap: 5px;
border: 0;
border-radius: 4px;
padding: 5px 7px;
background: transparent;
color: var(--muted);
font: inherit;
font-size: 12px;
white-space: nowrap;
}
.desktop-app-button {
color: var(--text);
}
.desktop-app-button:hover:not(:disabled),
.desktop-toolbar-action:hover:not(:disabled) {
background: color-mix(in srgb, var(--text) 8%, transparent);
color: var(--text);
}
.desktop-app-button:focus-visible,
.desktop-toolbar-action:focus-visible {
outline: 2px solid var(--focus, var(--accent));
outline-offset: 1px;
}
.desktop-app-button:disabled,
.desktop-toolbar-action:disabled {
cursor: default;
opacity: 0.55;
}
.desktop-app-button__icon {
display: inline-flex;
width: 15px;
height: 15px;
}
.desktop-app-button__icon svg {
width: 100%;
height: 100%;
stroke-width: 1.75;
}
.desktop-app-button__icon--launching {
animation: desktop-app-launch 900ms linear infinite;
}
.desktop-connecting {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 14px;
background: color-mix(in srgb, var(--bg) 94%, transparent);
color: var(--muted);
font-size: 12px;
text-align: center;
}
.desktop-connecting__monitor {
display: inline-flex;
width: 38px;
height: 38px;
color: color-mix(in srgb, var(--text) 76%, var(--muted));
animation: desktop-monitor-glow 1.8s ease-in-out infinite;
}
.desktop-connecting__monitor svg {
width: 100%;
height: 100%;
stroke-width: 1.25;
}
.desktop-connecting__copy {
display: flex;
align-items: baseline;
gap: 1px;
}
.desktop-connecting__dots {
display: inline-flex;
width: 16px;
gap: 1px;
}
.desktop-connecting__dot {
width: 2px;
height: 2px;
border-radius: 50%;
animation: desktop-traveling-dot 1.2s ease-in-out infinite;
background: currentColor;
opacity: 0.25;
}
.desktop-connecting__dot:nth-child(2) {
animation-delay: 160ms;
}
.desktop-connecting__dot:nth-child(3) {
animation-delay: 320ms;
}
@keyframes desktop-app-launch {
50% {
opacity: 0.6;
transform: rotate(180deg) scale(0.92);
}
100% {
transform: rotate(360deg);
}
}
@keyframes desktop-monitor-glow {
50% {
color: var(--text);
filter: drop-shadow(0 0 7px color-mix(in srgb, var(--accent) 32%, transparent));
}
}
@keyframes desktop-traveling-dot {
40% {
opacity: 1;
transform: translateY(-2px);
}
}
@media (prefers-reduced-motion: reduce) {
.desktop-app-button__icon--launching,
.desktop-connecting__monitor,
.desktop-connecting__dot {
animation: none;
}
.desktop-connecting__dot {
opacity: 0.65;
}
}
`;
+141 -32
View File
@@ -1,6 +1,8 @@
import type {
EnvironmentSummary,
EnvironmentsListResult,
WorkerDesktopAppId,
WorkerDesktopLaunchResult,
WorkerDesktopObserveResult,
} from "@openclaw/gateway-protocol";
import { css, html, nothing, svg } from "lit";
@@ -11,15 +13,14 @@ import { formatUiError } from "../../lib/format-error.ts";
import { OpenClawLitElement } from "../../lit/openclaw-element.ts";
import { DockLayoutController, dockPanelStyles } from "../dock-layout-controller.ts";
import { createDockPanelLayout } from "../dock-panel-layout.ts";
import { icons } from "../icons.ts";
import {
DESKTOP_PANEL_TOGGLE_EVENT,
type DesktopPanelToggleDetail,
} from "../panel-toggle-contract.ts";
import {
DesktopClient,
type DesktopConnectionHandle,
type DesktopConnectOptions,
} from "./desktop-client.ts";
import { desktopAppIcon, desktopAppLabel } from "./desktop-app-presentation.ts";
import { DesktopClient, type DesktopConnectionHandle } from "./desktop-client.ts";
import { desktopPanelLauncherStyles } from "./desktop-panel-launcher-styles.ts";
const CLOSE_GLYPH = svg`<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8" /></svg>`;
const DOCK_BOTTOM_GLYPH = svg`<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="2" y="2.5" width="12" height="11" rx="1.5" /><path d="M2 10h12" /></svg>`;
@@ -36,9 +37,7 @@ const panelLayout = createDockPanelLayout({
});
type DesktopPanelState = "picker" | "connecting" | "connected" | "disconnected";
type DesktopClientFactory = () => {
connect(options: DesktopConnectOptions): Promise<DesktopConnectionHandle>;
};
type DesktopAppId = WorkerDesktopAppId;
/** `<openclaw-desktop-panel>` — dockable RFB access to cloud-worker desktops. */
class OpenClawDesktopPanel extends OpenClawLitElement {
@@ -47,7 +46,7 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
@property({ type: Boolean }) suppressed = false;
/** Browser tests replace the transport without opening a real RFB socket. */
desktopClientFactory: DesktopClientFactory = () => new DesktopClient();
desktopClientFactory: () => Pick<DesktopClient, "connect"> = () => new DesktopClient();
@state() private environments: EnvironmentSummary[] = [];
@state() private loading = false;
@@ -57,9 +56,13 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
@state() private errorText: string | null = null;
@state() private noticeText: string | null = null;
@state() private disconnectedReason: string | null = null;
@state() private launchingApp: DesktopAppId | null = null;
@state() private launchErrorText: string | null = null;
@state() private desktopApps: DesktopAppId[] = [];
private connection: DesktopConnectionHandle | null = null;
private operationId = 0;
private launchOperationId = 0;
private controlTakeoverRecoveryUsed = false;
private readonly dockLayout = new DockLayoutController(this, {
layout: panelLayout,
@@ -70,6 +73,7 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
static override styles = [
dockPanelStyles,
desktopPanelLauncherStyles,
css`
.bp--bottom {
left: var(--shell-nav-width, 0);
@@ -106,6 +110,10 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
padding: 8px 10px;
border-bottom: 1px solid var(--border, #262b34);
}
.desktop-toolbar--connection {
min-height: 42px;
gap: 12px;
}
.desktop-toolbar__spacer {
flex: 1;
}
@@ -128,17 +136,14 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
.desktop-button:disabled {
opacity: 0.5;
}
.desktop-badge,
.desktop-session {
border-radius: 999px;
padding: 2px 8px;
background: color-mix(in srgb, var(--text, #d7dae0) 10%, transparent);
color: var(--muted, #8a919e);
overflow: hidden;
max-width: 100%;
color: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
}
.desktop-badge--control {
color: var(--accent, #ff5c5c);
background: color-mix(in srgb, var(--accent, #ff5c5c) 12%, transparent);
text-overflow: ellipsis;
white-space: nowrap;
}
.desktop-note {
padding: 7px 12px;
@@ -158,6 +163,7 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
gap: 10px;
overflow: auto;
padding: 14px;
background: var(--panel);
}
.desktop-status {
align-items: center;
@@ -197,11 +203,17 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
color: var(--muted, #8a919e);
font-size: 11px;
}
.desktop-surface {
.desktop-stage {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
background: #202020;
background: var(--bg);
}
.desktop-surface {
position: absolute;
inset: 0;
background: var(--bg);
}
`,
];
@@ -274,8 +286,10 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
private returnToPicker(): void {
this.disconnectConnection();
this.clearLaunchState();
this.state = "picker";
this.environmentId = null;
this.desktopApps = [];
this.controlling = false;
this.disconnectedReason = null;
}
@@ -287,6 +301,12 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
connection?.disconnect();
}
private clearLaunchState(): void {
this.launchOperationId += 1;
this.launchingApp = null;
this.launchErrorText = null;
}
private async refreshEnvironments(): Promise<void> {
const client = this.client;
if (!client || !this.available) {
@@ -323,6 +343,13 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
if (!client || !this.available) {
return;
}
if (this.environmentId !== environmentId) {
this.clearLaunchState();
this.desktopApps = [
...(this.environments.find((environment) => environment.id === environmentId)?.worker
?.desktopApps ?? []),
];
}
this.disconnectConnection();
const operationId = this.operationId;
this.environmentId = environmentId;
@@ -348,7 +375,9 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
throw new Error("Desktop render target is unavailable");
}
const desktopClient = this.desktopClientFactory();
const background = getComputedStyle(target).backgroundColor;
const connection = await desktopClient.connect({
background,
wsUrl: observed.wsPath,
gatewayUrl: client.gatewayUrl,
password: observed.vncPassword,
@@ -381,12 +410,14 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
if (operationId === this.operationId) {
this.state = "disconnected";
this.disconnectedReason = formatUiError(error);
this.clearLaunchState();
}
}
}
private handleDesktopDisconnect(environmentId: string, code?: number, reason?: string): void {
this.connection = null;
this.clearLaunchState();
if (
code === 4000 &&
reason === "control-taken" &&
@@ -405,6 +436,39 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
reason || (code ? t("desktop.closeCode", { code: String(code) }) : null);
}
private async launchApp(app: DesktopAppId): Promise<void> {
const client = this.client;
const environmentId = this.environmentId;
if (
!client ||
!environmentId ||
(this.state !== "connecting" && this.state !== "connected") ||
!this.desktopApps.includes(app) ||
this.launchingApp === app
) {
return;
}
const operationId = ++this.launchOperationId;
this.launchingApp = app;
this.launchErrorText = null;
try {
await client.request<WorkerDesktopLaunchResult>("worker.desktop.launch", {
environmentId,
app,
});
if (operationId !== this.launchOperationId || environmentId !== this.environmentId) {
return;
}
this.launchingApp = null;
} catch (error) {
if (operationId !== this.launchOperationId || environmentId !== this.environmentId) {
return;
}
this.launchingApp = null;
this.launchErrorText = formatUiError(error);
}
}
private renderHeader() {
const dock = this.dockLayout.dock;
return html`
@@ -497,29 +561,73 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
private renderConnection() {
return html`
<div class="desktop-toolbar">
<span class="desktop-badge ${this.controlling ? "desktop-badge--control" : ""}">
${this.controlling ? t("desktop.controlling") : t("desktop.viewOnly")}
</span>
<div class="desktop-toolbar desktop-toolbar--connection">
${this.desktopApps.length > 0
? html`<div class="desktop-apps">
${this.desktopApps.map((app) => {
const launching = this.launchingApp === app;
const label = desktopAppLabel(app);
return html`<button
class="desktop-app-button"
type="button"
title=${label}
aria-label=${label}
aria-busy=${launching ? "true" : "false"}
?disabled=${!this.environmentId || launching}
@click=${() => void this.launchApp(app)}
>
<span
class="desktop-app-button__icon ${launching
? "desktop-app-button__icon--launching"
: ""}"
aria-hidden="true"
>
${desktopAppIcon(app)}
</span>
<span>${label}</span>
</button>`;
})}
</div>`
: nothing}
<span class="desktop-toolbar__spacer"></span>
${!this.controlling
? html`<button
class="desktop-button desktop-button--primary"
class="desktop-toolbar-action"
type="button"
title=${t("desktop.takeControl")}
aria-label=${t("desktop.takeControl")}
@click=${() =>
this.environmentId && void this.connectEnvironment(this.environmentId, true)}
>
${t("desktop.takeControl")}
</button>`
: nothing}
<button class="desktop-button" type="button" @click=${() => this.returnToPicker()}>
<button
class="desktop-toolbar-action"
type="button"
title=${t("desktop.disconnect")}
aria-label=${t("desktop.disconnect")}
@click=${() => this.returnToPicker()}
>
${t("desktop.disconnect")}
</button>
</div>
<div class="desktop-surface"></div>
${this.state === "connecting"
? html`<div class="desktop-note">${t("desktop.connecting")}</div>`
: nothing}
<div class="desktop-stage">
<div class="desktop-surface"></div>
${this.state === "connecting"
? html`<div class="desktop-connecting" role="status" aria-live="polite">
<span class="desktop-connecting__monitor" aria-hidden="true">${icons.monitor}</span>
<span class="desktop-connecting__copy">
${t("desktop.connecting")}
<span class="desktop-connecting__dots" aria-hidden="true">
<span class="desktop-connecting__dot"></span>
<span class="desktop-connecting__dot"></span>
<span class="desktop-connecting__dot"></span>
</span>
</span>
</div>`
: nothing}
</div>
`;
}
@@ -551,13 +659,14 @@ class OpenClawDesktopPanel extends OpenClawLitElement {
const dock = this.dockLayout.dock;
const style =
dock === "bottom" ? `height:${this.dockLayout.height}px` : `width:${this.dockLayout.width}px`;
const visibleErrorText = this.launchErrorText ?? this.errorText;
return html`
<section class="bp bp--${dock}" style=${style} aria-label=${t("desktop.title")}>
${this.dockLayout.renderResizer("bp", t("desktop.resize"))} ${this.renderHeader()}
<div class="desktop-content">
${this.errorText
${visibleErrorText
? html`<div class="desktop-note desktop-note--error" role="alert">
${this.errorText}
${visibleErrorText}
</div>`
: this.noticeText
? html`<div class="desktop-note" role="status">${this.noticeText}</div>`
+146 -25
View File
@@ -16,6 +16,37 @@ async function openPalette(page: import("playwright").Page) {
await page.getByRole("combobox", { name: "Search chats and commands…" }).waitFor();
}
async function openDesktopPanel(page: import("playwright").Page) {
await page.goto(`${suite.server.baseUrl}chat`);
await openPalette(page);
await page.getByRole("option", { name: "Desktop", exact: true }).click();
const panel = page.locator("openclaw-desktop-panel");
await panel.locator("section[aria-label='Desktop']").waitFor();
return panel;
}
async function installDesktopClientFake(panel: import("playwright").Locator) {
await panel.evaluate((element) => {
(
element as HTMLElement & {
desktopClientFactory: () => {
connect(): Promise<{ disconnect(): void }>;
};
}
).desktopClientFactory = () => ({
async connect() {
return {
disconnect() {
element.dataset.disconnectCount = String(
Number(element.dataset.disconnectCount ?? "0") + 1,
);
},
};
},
});
});
}
suite.define(() => {
it("hides the desktop command without the method or operator.admin", async () => {
for (const scenario of [
@@ -34,10 +65,11 @@ suite.define(() => {
}
});
it("lists a desktop worker and requests view then control observer leases", async () => {
it("launches advertised desktop apps and keeps observe controls working", async () => {
await suite.withPage({ serviceWorkers: "block" }, async ({ page }) => {
const gateway = await installMockGateway(page, {
featureMethods: ["environments.list", "worker.desktop.observe"],
deferredMethods: ["worker.desktop.launch"],
featureMethods: ["environments.list", "worker.desktop.launch", "worker.desktop.observe"],
methodResponses: {
"environments.list": {
environments: [
@@ -52,6 +84,7 @@ suite.define(() => {
attachedSessionIds: ["agent:main:desktop"],
tunnelStatus: "connected",
desktop: true,
desktopApps: ["browser", "terminal"],
},
},
],
@@ -78,47 +111,135 @@ suite.define(() => {
},
],
},
"worker.desktop.launch": { app: "browser", status: "ready" },
},
});
await page.goto(`${suite.server.baseUrl}chat`);
await openPalette(page);
await page.getByRole("option", { name: "Desktop", exact: true }).click();
const panel = page.locator("openclaw-desktop-panel");
await panel.locator("section[aria-label='Desktop']").waitFor();
const panel = await openDesktopPanel(page);
await gateway.waitForRequest("environments.list");
await panel.getByText("worker-desktop-1", { exact: true }).waitFor();
await panel.getByText("agent:main:desktop", { exact: true }).waitFor();
await panel.evaluate((element) => {
(
element as HTMLElement & {
desktopClientFactory: () => {
connect(options: { onConnect?: () => void }): Promise<{ disconnect(): void }>;
};
}
).desktopClientFactory = () => ({
async connect(options) {
queueMicrotask(() => options.onConnect?.());
return { disconnect() {} };
},
});
});
await installDesktopClientFake(panel);
await panel.getByRole("button", { name: "Connect", exact: true }).click();
const viewRequest = await gateway.waitForRequest("worker.desktop.observe");
expect(viewRequest.params).toEqual({ environmentId: "worker-desktop-1", control: false });
await panel.getByText("Connecting to desktop…", { exact: true }).waitFor();
await panel.getByRole("button", { name: "Browser", exact: true }).waitFor();
await panel.getByRole("button", { name: "Terminal", exact: true }).waitFor();
expect(await panel.getByText("View only", { exact: true }).count()).toBe(0);
expect(await panel.getByText(/Controlling/).count()).toBe(0);
const browserButton = panel.getByRole("button", { name: "Browser", exact: true });
const terminalButton = panel.getByRole("button", { name: "Terminal", exact: true });
expect(
await browserButton.evaluate((element) => getComputedStyle(element).backgroundColor),
).toBe("rgba(0, 0, 0, 0)");
const stageUsesAppBackground = await panel.evaluate((element) => {
const stage = element.shadowRoot?.querySelector<HTMLElement>(".desktop-surface");
if (!stage) {
return false;
}
const reference = document.createElement("div");
reference.style.background = "var(--bg)";
element.shadowRoot?.append(reference);
const matches =
getComputedStyle(stage).backgroundColor === getComputedStyle(reference).backgroundColor;
reference.remove();
return matches;
});
expect(stageUsesAppBackground).toBe(true);
await browserButton.click();
const launchRequest = await gateway.waitForRequest("worker.desktop.launch");
expect(launchRequest.params).toEqual({ environmentId: "worker-desktop-1", app: "browser" });
await expect.poll(async () => await browserButton.getAttribute("aria-busy")).toBe("true");
expect(await terminalButton.isEnabled()).toBe(true);
await gateway.resolveDeferred("worker.desktop.launch", { app: "browser", status: "ready" });
await expect.poll(async () => await browserButton.getAttribute("aria-busy")).toBe("false");
await gateway.deferNext("worker.desktop.launch");
await browserButton.click();
await gateway.waitForRequest("worker.desktop.launch");
await gateway.rejectDeferred("worker.desktop.launch", {
message: "worker desktop app launch unavailable; try again",
});
await panel
.getByRole("alert")
.filter({ hasText: "worker desktop app launch unavailable; try again" })
.waitFor();
await panel.getByRole("button", { name: "Browser", exact: true }).waitFor();
expect(await browserButton.isEnabled()).toBe(true);
await panel.getByRole("button", { name: "Disconnect", exact: true }).click();
await panel.getByText("Cloud worker desktops", { exact: true }).waitFor();
expect(
await panel
.getByText("worker desktop app launch unavailable; try again", { exact: true })
.count(),
).toBe(0);
await panel.getByRole("button", { name: "Connect", exact: true }).click();
await expect
.poll(async () => (await gateway.getRequests("worker.desktop.observe")).length)
.toBe(2);
await panel.getByRole("button", { name: "Take control", exact: true }).click();
await expect
.poll(async () => (await gateway.getRequests("worker.desktop.observe")).length)
.toBe(2);
.toBe(3);
const observeRequests = await gateway.getRequests("worker.desktop.observe");
expect(observeRequests[1]?.params).toEqual({
expect(observeRequests[2]?.params).toEqual({
environmentId: "worker-desktop-1",
control: true,
});
await panel.getByText("Controlling · view-only for others", { exact: true }).waitFor();
expect(await panel.getByRole("button", { name: "Take control", exact: true }).count()).toBe(
0,
);
await panel.getByRole("button", { name: "Disconnect", exact: true }).click();
await panel.getByText("Cloud worker desktops", { exact: true }).waitFor();
expect(Number((await panel.getAttribute("data-disconnect-count")) ?? "0")).toBeGreaterThan(0);
});
});
it("shows only apps advertised by the selected environment", async () => {
await suite.withPage({ serviceWorkers: "block" }, async ({ page }) => {
const gateway = await installMockGateway(page, {
featureMethods: ["environments.list", "worker.desktop.launch", "worker.desktop.observe"],
methodResponses: {
"environments.list": {
environments: [
{
id: "terminal-only-worker",
type: "worker",
status: "available",
worker: {
providerId: "crabbox",
state: "ready",
ageMs: 1_000,
attachedSessionIds: [],
tunnelStatus: "connected",
desktop: true,
desktopApps: ["terminal"],
},
},
],
},
"worker.desktop.observe": {
transport: "rfb",
wsPath: "/worker-desktop/observe?token=view",
expiresAtMs: 60_000,
control: false,
},
},
});
const panel = await openDesktopPanel(page);
await gateway.waitForRequest("environments.list");
await installDesktopClientFake(panel);
await panel.getByRole("button", { name: "Connect", exact: true }).click();
await panel.getByRole("button", { name: "Terminal", exact: true }).waitFor();
expect(await panel.getByRole("button", { name: "Browser", exact: true }).count()).toBe(0);
});
});
});
-2
View File
@@ -1953,9 +1953,7 @@ export const en: TranslationMap = {
"No desktop-capable worker environments exist. Enable one with desktop: true in a crabbox cloud-worker profile.",
connect: "Connect",
connecting: "Connecting to desktop…",
viewOnly: "View only",
takeControl: "Take control",
controlling: "Controlling · view-only for others",
disconnect: "Disconnect",
reconnect: "Reconnect",
controlTaken: "Another operator took control",