refactor(skills): split workspace loading into concept modules (#121916)

* refactor(skills): split workspace loading into concept modules

* refactor(skills): delete dead seams and duplicate helpers

* chore(lint): ratchet max-lines baseline after workspace split

* chore(plugin-sdk): refresh API baseline

* refactor(skills): internalize discovery result type

* test(tooling): drop stale unit-fast fixtures
This commit is contained in:
Peter Steinberger
2026-08-11 01:28:20 -07:00
committed by GitHub
parent 248cef3646
commit 64e3712e82
56 changed files with 4445 additions and 4398 deletions
-2
View File
@@ -886,8 +886,6 @@ src/shared/json-schema-defaults.ts
src/shared/text/assistant-visible-text.ts
src/skills/lifecycle/clawhub.test.ts
src/skills/lifecycle/install.ts
src/skills/loading/workspace-load.test.ts
src/skills/loading/workspace.ts
src/skills/security/scanner.ts
src/skills/workshop/service.test.ts
src/snapshot/local-repository.test.ts
+71 -71
View File
@@ -53,17 +53,17 @@
{"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":"d0cc2d1a92a55bfe73ad51bb32078cf4e8911a2a5a2f1c2f3190ba044922e56b","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"bf2a01a087d508c066401f5df6cc34065b95a11718244ce38f0525140bf11524","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":"d759a25775cb15d3f1804e39e98518f83b67b29ba3ab2fd84d4bf16106d9fdb9","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"bd666466495cef203461f4484eb439faa6a4fd10756c44a64a45ddef3b65961c","declaration":"export type AgentHarnessV2 = AgentHarnessV2;","entrypoint":"agent-harness","exportName":"AgentHarnessV2","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"bfca309d41b8bed7b9b967ea4c643d6044cbd3dead3c87c6f05ea2d01859c4e9","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"d813ac33976a047720de5f7b9612a0c703f516d78615e61a9880de0dff51e191","declaration":"export type AgentHarnessV2 = AgentHarnessV2;","entrypoint":"agent-harness","exportName":"AgentHarnessV2","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":"7cce5f7355df47a66ed208855889b16b2c40782a75575e8e1a8dce43517b801f","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"b56f77aa308700cc9ea82f50957a860852d0c83c006b5020d97f0d9b2d6c9d6a","declaration":"export type EmbeddedRunAttemptParamsV2 = EmbeddedRunAttemptParamsV2;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParamsV2","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"9257c34a998d535fee6b043e0931dc84d395f7373876dc919c71b4f2d9d71e09","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"}
{"closureHash":"a3434c206c28a79c697b64019add9861228634c250df3a17c0a43caa203e9ef9","declaration":"export type EmbeddedRunAttemptParamsV2 = EmbeddedRunAttemptParamsV2;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParamsV2","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"}
@@ -82,8 +82,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":"273bc789e32d09355911a553f30e7fd13f0ac7fc7d08f76d6c6ea96b8ab4e1d3","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":"9cd796a6dc567e647776b5d9af435e8c179796142948b64b088eb9e91c42ba66","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":"3af35ed22836860ff7eb66ec82b8f2fa3758fb5ed48bd909ea375657a195b15a","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":"626d86d7fae06a3415dd20afa8f907e8c71541a043821dadf8758942b8e873b4","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"}
@@ -100,7 +100,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":"1368f69c7a2b975961e4bec1a8709522669fed0df0eca2d669159c9e65eb1cf3","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":"a82ff9b3a834c5c4bf16dfd0b211827dd8395e72079e9aa1549bacbe9d199a61","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"}
@@ -162,7 +162,7 @@
{"closureHash":"40154c3b7b2a7179df33f4a3fbef5e56078be2759d5aab29f39eb0f3c92fc631","declaration":"export function projectRuntimeToolInputSchema(schema: unknown, path?: string): RuntimeToolInputSchemaProjection;","entrypoint":"agent-harness-runtime","exportName":"projectRuntimeToolInputSchema","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"e42e3dc63e8545bb9a6056336fbd6c1dc8f356dce4f0f1c6d9765254c974390e","declaration":"export function projectSettledTurnFinalizationAttemptResult(result: AgentHarnessAttemptResult): AgentHarnessSettledTurnFinalizationResult;","entrypoint":"agent-harness-runtime","exportName":"projectSettledTurnFinalizationAttemptResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"28fce991efc7c1c2e4d24a5380acdeca72aea42a3e3b14d053ec352a84ab5211","declaration":"export function queueAgentHarnessMessage(sessionId: string, text: string, options?: EmbeddedAgentQueueMessageOptions): boolean;","entrypoint":"agent-harness-runtime","exportName":"queueAgentHarnessMessage","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"a5942178bfa7de60f6b738d58d5f511a7baa1abf25119454180b81daec365833","declaration":"export function registerNativeHookRelay(params: RegisterNativeHookRelayParams): ActiveNativeHookRelayRegistrationHandle;","entrypoint":"agent-harness-runtime","exportName":"registerNativeHookRelay","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"a415720a81deec67cd76000e5f813229d14f883ed9cef6eec4a1ff637ede552c","declaration":"export function registerNativeHookRelay(params: RegisterNativeHookRelayParams): ActiveNativeHookRelayRegistrationHandle;","entrypoint":"agent-harness-runtime","exportName":"registerNativeHookRelay","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"b7cc6c35506dc6955e0359aa5c53c15a4d0db23639da8c1717f8fefda953c119","declaration":"export function requestDeferredPluginToolApproval(params: { deferredApproval: DeferredPluginToolApproval; signal?: AbortSignal; }): Promise<HookOutcome>;","entrypoint":"agent-harness-runtime","exportName":"requestDeferredPluginToolApproval","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"a2306cba08cb05ff8430c8362c0cc3c821706da5a4e65a06849ad0d0fb007ae3","declaration":"export function resetAgentEventsForTest(options?: { preserveListeners?: boolean; }): void;","entrypoint":"agent-harness-runtime","exportName":"resetAgentEventsForTest","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
{"closureHash":"f255a91162ece4c239bb8fa745efa14a1054bc024a291e9954f75f239b2b87c3","declaration":"export function resolveActiveEmbeddedRunSessionId(sessionKey: string): string | undefined;","entrypoint":"agent-harness-runtime","exportName":"resolveActiveEmbeddedRunSessionId","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"}
@@ -197,7 +197,7 @@
{"closureHash":"da8b724c5858691b8a1c71c63294a397631c031270db92fdff9ef4493efa2492","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":"234d68d071e378bce7a3780dd308a39e6f611b722d86203c3366951420d9f1b1","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":"45bbc96bc997e28c7af02ae4c96c2d7e8a7fc6b316495eda845d27156c463b4f","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":"89aa32c12a44b446777bc4cfbb106a048dd9cbc668260525798a895a433256f4","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":"ba63f9cdd34249fba984af87a039494b75b3f93c0c70b925e884f3f98935b984","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"}
@@ -219,13 +219,13 @@
{"closureHash":"0701a22057edc3b733da7e05948db94aa98b337b224a5718efdd85212a8602bf","declaration":"export const TRANSCRIPT_CREDENTIAL_SAFETY_PROMPT: string;","entrypoint":"agent-harness-runtime","exportName":"TRANSCRIPT_CREDENTIAL_SAFETY_PROMPT","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"const","recordType":"export"}
{"closureHash":"b5845c99df6c2a834a16678c320e0111bad96a3a7ac245d379aa5e6d2555d29b","declaration":"export const agentHarnessAttemptTerminal: { merge: (current: AgentRunAttemptTerminal, incoming: AgentRunAttemptTerminal) => AgentRunAttemptTerminal; normalize: (input: { aborted?: boolean; externalAbort?: boolean; idleTimedOut?: boolean; promptError?: unknown; promptErrorSource?: AgentRunAttemptFailureSource | null; timedOut?: boolean; timedOutByRunBudget?: boolean; timedOutDuringCompaction?: boolean; timedOutDuringToolExecution?: boolean; }) => AgentRunAttemptTerminal; project: (terminal: AgentRunAttemptTerminal) => { aborted: boolean; cleanupYieldAborted: boolean; externalAbort: boolean; failed: boolean; idleTimedOut: boolean; interrupted: boolean; promptError: unknown; promptErrorSource: AgentRunAttemptFailureSource | null; timedOut: boolean; timedOutByRunBudget: boolean; timedOutDuringCompaction: boolean; timedOutDuringToolExecution: boolean; }; setFailure: (terminal: AgentRunAttemptTerminal, failure: { source: AgentRunAttemptFailureSource; error: unknown; } | null) => AgentRunAttemptTerminal;};","entrypoint":"agent-harness-runtime","exportName":"agentHarnessAttemptTerminal","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"const","recordType":"export"}
{"closureHash":"22bcf962aa76c76074c4ed4ad7d6558edbc3588e239263e09af74c1000bbe012","declaration":"export const embeddedAgentLog: SubsystemLogger;","entrypoint":"agent-harness-runtime","exportName":"embeddedAgentLog","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"const","recordType":"export"}
{"closureHash":"c0f6b29e20cd3f18f1f123bc1e5d458f391013e2812541fd58698155bd72fe96","declaration":"export const nativeHookRelayTesting: { readonly clearNativeHookRelaysForTests: () => void; readonly getNativeHookRelayInvocationsForTests: () => NativeHookRelayInvocation[]; readonly getNativeHookRelayRegistrationForTests: (relayId: string) => NativeHookRelayRegistration | undefined; readonly getNativeHookRelayBridgeDirForTests: () => string; readonly getNativeHookRelayBridgeRegistryPathForTests: (relayId: string) => string; readonly getNativeHookRelayBridgeRecordForTests: (relayId: string) => Record<string, unknown> | undefined; readonly isNativeHookRelayBridgeLookupRetryableForTests: (error: unknown, elapsedMs?: number) => boolean; readonly formatPermissionApprovalDescriptionForTests: (request: NativeHookRelayPermissionApprovalRequest) => string; readonly permissionRequestContentFingerprintForTests: (request: NativeHookRelayPermissionApprovalRequest) => string; readonly permissionRequestToolInputKeyFingerprintForTests: (toolInput: Record<string, unknown>) => string; readonly setNativeHookRelayPermissionApprovalRequesterForTests: (requester: NativeHookRelayPermissionApprovalRequester) => void; readonly setNativeHookRelayDeferredToolApprovalRequesterForTests: (requester: NativeHookRelayDeferredToolApprovalRequester) => void;};","entrypoint":"agent-harness-runtime","exportName":"nativeHookRelayTesting","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"const","recordType":"export"}
{"closureHash":"ca8e36c9027d9d5cdee1398e5ede7f0e4b0ffadffaca98dc88ff11ff522b064e","declaration":"export const nativeHookRelayTesting: { readonly clearNativeHookRelaysForTests: () => void; readonly getNativeHookRelayInvocationsForTests: () => NativeHookRelayInvocation[]; readonly getNativeHookRelayRegistrationForTests: (relayId: string) => NativeHookRelayRegistration | undefined; readonly getNativeHookRelayBridgeDirForTests: () => string; readonly getNativeHookRelayBridgeRegistryPathForTests: (relayId: string) => string; readonly getNativeHookRelayBridgeRecordForTests: (relayId: string) => Record<string, unknown> | undefined; readonly isNativeHookRelayBridgeLookupRetryableForTests: (error: unknown, elapsedMs?: number) => boolean; readonly formatPermissionApprovalDescriptionForTests: (request: NativeHookRelayPermissionApprovalRequest) => string; readonly permissionRequestContentFingerprintForTests: (request: NativeHookRelayPermissionApprovalRequest) => string; readonly permissionRequestToolInputKeyFingerprintForTests: (toolInput: Record<string, unknown>) => string; readonly setNativeHookRelayPermissionApprovalRequesterForTests: (requester: NativeHookRelayPermissionApprovalRequester) => void; readonly setNativeHookRelayDeferredToolApprovalRequesterForTests: (requester: NativeHookRelayDeferredToolApprovalRequester) => void;};","entrypoint":"agent-harness-runtime","exportName":"nativeHookRelayTesting","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"const","recordType":"export"}
{"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":"d759a25775cb15d3f1804e39e98518f83b67b29ba3ab2fd84d4bf16106d9fdb9","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness-runtime","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"d759a25775cb15d3f1804e39e98518f83b67b29ba3ab2fd84d4bf16106d9fdb9","declaration":"export type AgentHarnessAttemptParams = AgentHarnessAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"acb796810881f352798f8026ebebc9ba1ce3faf3286785e4b9a964367ebd24d9","declaration":"export type AgentHarnessAttemptParamsV2 = AgentHarnessAttemptParamsV2;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParamsV2","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"bfca309d41b8bed7b9b967ea4c643d6044cbd3dead3c87c6f05ea2d01859c4e9","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness-runtime","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"bfca309d41b8bed7b9b967ea4c643d6044cbd3dead3c87c6f05ea2d01859c4e9","declaration":"export type AgentHarnessAttemptParams = AgentHarnessAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"9d6ae26d110c34587071b268243778e6dc8d6d490550c1d810a437b31657889e","declaration":"export type AgentHarnessAttemptParamsV2 = AgentHarnessAttemptParamsV2;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParamsV2","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"}
@@ -239,8 +239,8 @@
{"closureHash":"95741b872a194687696eba4a96d6e887ec6e81922695fa0282cb2f0a46ff9f97","declaration":"export type AgentHarnessSessionForkParams = AgentHarnessSessionForkParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSessionForkParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"f3c46fef2a6c3393b0b6ad09915abcd6f57bb58ddddde1d60ebd113c7a1c5fa6","declaration":"export type AgentHarnessSessionForkResult = AgentHarnessSessionForkResult;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSessionForkResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"4b5a59730309e446f59a8ec1c554c3193aca1959916594cc657347098bd67a35","declaration":"export type AgentHarnessSettledTurnFinalizationResult = AgentHarnessSettledTurnFinalizationResult;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSettledTurnFinalizationResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"e2c320165e7f072dd5d053d82be3fa5db3b3648112d39a3b40e393d92b07033a","declaration":"export type AgentHarnessSideQuestionParams = AgentHarnessSideQuestionParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSideQuestionParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"1311982296ef9a93957c4559b85e87fc6f3e414476b5c77418df522d2312f795","declaration":"export type AgentHarnessSideQuestionParamsV2 = AgentHarnessSideQuestionParamsV2;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSideQuestionParamsV2","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"2ac87a91220b9b712e35d4da89186417bb0daf1306ec159b417a35a7d54bd200","declaration":"export type AgentHarnessSideQuestionParams = AgentHarnessSideQuestionParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSideQuestionParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"39674e80bbf1656c7fad97fb9e2289be09603df97747e1f5026a0a69b4b1dc3d","declaration":"export type AgentHarnessSideQuestionParamsV2 = AgentHarnessSideQuestionParamsV2;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSideQuestionParamsV2","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"e84173055503c40dd0ef1cb67c6d333e9257fcf995252b27c1d58f5c904e8de9","declaration":"export type AgentHarnessSideQuestionResult = AgentHarnessSideQuestionResult;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSideQuestionResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"50214d08dac98e3c1031ac7bc733b85cb2247d2eef3b6b2045758ae403af7647","declaration":"export type AgentHarnessSupport = AgentHarnessSupport;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSupport","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"50ce26ea22bc225e6064d2e5f37356b6f4ce4a57f8b9fa42baa4c421da48aad5","declaration":"export type AgentHarnessSupportContext = AgentHarnessSupportContext;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessSupportContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
@@ -250,7 +250,7 @@
{"closureHash":"20a8a7c9d5ebf425c30d04eea3856c6c889a210f1fa53c99299df24882afbf92","declaration":"export type AgentHarnessUserInputOption = AgentHarnessUserInputOption;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessUserInputOption","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"6224f51b4189f10a642ad7a8f50a902ec9c6a6a4f8dac0c0de5f7bc0271a27e5","declaration":"export type AgentHarnessUserInputPromptOptions = AgentHarnessUserInputPromptOptions;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessUserInputPromptOptions","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"d8f1d0d118e1536d1f442d11aeae5334a1ed7949a6c61824f547e12931278e1d","declaration":"export type AgentHarnessUserInputQuestion = AgentHarnessUserInputQuestion;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessUserInputQuestion","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"bd666466495cef203461f4484eb439faa6a4fd10756c44a64a45ddef3b65961c","declaration":"export type AgentHarnessV2 = AgentHarnessV2;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessV2","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"d813ac33976a047720de5f7b9612a0c703f516d78615e61a9880de0dff51e191","declaration":"export type AgentHarnessV2 = AgentHarnessV2;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessV2","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"6f2af683e6a31cde5a473c77e4f03b8f0ec0351a43ea6c0af979d5b394a33f95","declaration":"export type AgentMessage = AgentMessage;","entrypoint":"agent-harness-runtime","exportName":"AgentMessage","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"4ad0f830bf11a2db48faefb1439b7fbb08d9f22d22083f5e301327e1696f2310","declaration":"export type AgentToolResultMiddleware = AgentToolResultMiddleware;","entrypoint":"agent-harness-runtime","exportName":"AgentToolResultMiddleware","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"185abd2870d4df65e9f94623061a30167d17ef1279a395201fe429398bf23500","declaration":"export type AgentToolResultMiddlewareContext = AgentToolResultMiddlewareContext;","entrypoint":"agent-harness-runtime","exportName":"AgentToolResultMiddlewareContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
@@ -276,8 +276,8 @@
{"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":"7cce5f7355df47a66ed208855889b16b2c40782a75575e8e1a8dce43517b801f","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"b56f77aa308700cc9ea82f50957a860852d0c83c006b5020d97f0d9b2d6c9d6a","declaration":"export type EmbeddedRunAttemptParamsV2 = EmbeddedRunAttemptParamsV2;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParamsV2","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"9257c34a998d535fee6b043e0931dc84d395f7373876dc919c71b4f2d9d71e09","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"a3434c206c28a79c697b64019add9861228634c250df3a17c0a43caa203e9ef9","declaration":"export type EmbeddedRunAttemptParamsV2 = EmbeddedRunAttemptParamsV2;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParamsV2","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"}
@@ -293,7 +293,7 @@
{"closureHash":"d8a720bf250da8daf4dc6a52087aa8728ec33b650b3ad3906fcb40e184de66a0","declaration":"export type NativeHookRelayEvent = \"pre_tool_use\" | \"post_tool_use\" | \"permission_request\" | \"before_agent_finalize\";","entrypoint":"agent-harness-runtime","exportName":"NativeHookRelayEvent","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"cb72ca18e056e52e779d0bf5b67606e1dc160f225225b55937304541105027f4","declaration":"export type NativeHookRelayProcessResponse = NativeHookRelayProcessResponse;","entrypoint":"agent-harness-runtime","exportName":"NativeHookRelayProcessResponse","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"6ee390ae4784b987e22255c16884fb4171033bbfaefaef655e3dd2a2513814a5","declaration":"export type NativeHookRelayProvider = \"codex\";","entrypoint":"agent-harness-runtime","exportName":"NativeHookRelayProvider","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"389e3f59bfd6a95d6edeeb1afc2dbd51e6b26828fda9bbcdef480d7314962976","declaration":"export type NativeHookRelayRegistrationHandle = NativeHookRelayRegistrationHandle;","entrypoint":"agent-harness-runtime","exportName":"NativeHookRelayRegistrationHandle","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"5d17b841c07853cedf3b9c6025e4a4bba812cb911ba5d7188f0e10783a7d268f","declaration":"export type NativeHookRelayRegistrationHandle = NativeHookRelayRegistrationHandle;","entrypoint":"agent-harness-runtime","exportName":"NativeHookRelayRegistrationHandle","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"90fea3155b5d32c22da90460fdaea94e4fd6895d1ca18613d5036ba9835153f9","declaration":"export type NodeListNode = NodeListNode;","entrypoint":"agent-harness-runtime","exportName":"NodeListNode","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"cefe1325c866f0cf7edbec5dcca2fe420237eedc0da172e54094e365894d205d","declaration":"export type NormalizedUsage = NormalizedUsage;","entrypoint":"agent-harness-runtime","exportName":"NormalizedUsage","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
{"closureHash":"25c6985c3bef2c6efc6a96e70af6d66fe073be0b0d311681cfc0dfc186528d9a","declaration":"export type OpenClawAgentToolResult<TResult = unknown> = OpenClawAgentToolResult<TResult>;","entrypoint":"agent-harness-runtime","exportName":"OpenClawAgentToolResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"}
@@ -315,7 +315,7 @@
{"closureHash":"666634000ee5bf317dabcbd70524833961fad58edc2b3cd66fe5fa0172e89602","declaration":"export function getAgentScopedMediaLocalRoots(cfg: OpenClawConfig, agentId?: string): readonly string[];","entrypoint":"agent-media-payload","exportName":"getAgentScopedMediaLocalRoots","importSpecifier":"openclaw/plugin-sdk/agent-media-payload","kind":"function","recordType":"export"}
{"closureHash":"160c685c3f4e7ed80fb24d3d76655a182116efe6a7b985bb3bc57b89fe436d2a","declaration":"export type AgentMediaPayload = AgentMediaPayload;","entrypoint":"agent-media-payload","exportName":"AgentMediaPayload","importSpecifier":"openclaw/plugin-sdk/agent-media-payload","kind":"type","recordType":"export"}
{"category":"runtime","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime","recordType":"module"}
{"closureHash":"d676259fd3157772725733326c64420d7b5d750225551ac6501664954281135a","declaration":"export function agentCommandFromIngress(opts: AgentCommandIngressOpts, runtime?: RuntimeEnv, deps?: CliDeps): Promise<{ payloads: ReturnType<typeof projectOutboundPayloadPlanForJson>; meta: EmbeddedAgentRunMeta; didSendViaMessagingTool?: boolean; messagingToolSentTexts?: string[]; messagingToolSentMediaUrls?: string[]; messagingToolSentTargets?: MessagingToolSend[]; deliverySucceeded?: boolean; deliveryStatus?: { requested: true; attempted: boolean; status: \"sent\" | \"suppressed\" | \"partial_failed\" | \"failed\"; succeeded: true | false | \"partial\"; error?: true; errorMessage?: string; reason?: string; resultCount?: number; sentBeforeError?: true; payloadOutcomes?: SerializedDurableMessagePayloadOutcome[]; }; }>;","entrypoint":"agent-runtime","exportName":"agentCommandFromIngress","importSpecifier":"openclaw/plugin-sdk/agent-runtime","kind":"function","recordType":"export"}
{"closureHash":"2e52ff09ecfb43b22fdd7d03446baab43c24fd7e864a1e6e567591c1d0dd829f","declaration":"export function agentCommandFromIngress(opts: AgentCommandIngressOpts, runtime?: RuntimeEnv, deps?: CliDeps): Promise<{ payloads: ReturnType<typeof projectOutboundPayloadPlanForJson>; meta: EmbeddedAgentRunMeta; didSendViaMessagingTool?: boolean; messagingToolSentTexts?: string[]; messagingToolSentMediaUrls?: string[]; messagingToolSentTargets?: MessagingToolSend[]; deliverySucceeded?: boolean; deliveryStatus?: { requested: true; attempted: boolean; status: \"sent\" | \"suppressed\" | \"partial_failed\" | \"failed\"; succeeded: true | false | \"partial\"; error?: true; errorMessage?: string; reason?: string; resultCount?: number; sentBeforeError?: true; payloadOutcomes?: SerializedDurableMessagePayloadOutcome[]; }; }>;","entrypoint":"agent-runtime","exportName":"agentCommandFromIngress","importSpecifier":"openclaw/plugin-sdk/agent-runtime","kind":"function","recordType":"export"}
{"closureHash":"cb95256473d5489ef80ec3e1d93c6a612b2cc733c26ab535f945f8797995b5cb","declaration":"export function buildConfiguredModelCatalog(params: { cfg: OpenClawConfig; workspaceDir?: string; manifestPlugins?: ModelManifestPlugins; }): ModelCatalogEntry[];","entrypoint":"agent-runtime","exportName":"buildConfiguredModelCatalog","importSpecifier":"openclaw/plugin-sdk/agent-runtime","kind":"function","recordType":"export"}
{"closureHash":"4a71b73b60d846e9d99fa3881cb72eb7eaaebe7b3386b6bcbaa6cfa46eb3a073","declaration":"export function buildModelAliasIndex(params: BuildModelAliasIndexParams): ModelAliasIndex;","entrypoint":"agent-runtime","exportName":"buildModelAliasIndex","importSpecifier":"openclaw/plugin-sdk/agent-runtime","kind":"function","recordType":"export"}
{"closureHash":"ae0f930ec291a0345c4e99e247711465a5d29a8bbdd63968900fa69f382b3a78","declaration":"export function clearExpiredCooldowns(store: AuthProfileStore, now?: number): boolean;","entrypoint":"agent-runtime","exportName":"clearExpiredCooldowns","importSpecifier":"openclaw/plugin-sdk/agent-runtime","kind":"function","recordType":"export"}
@@ -762,7 +762,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":"9e93bcdfc6299e65ef2a3d267047a9e8c856f0b2f8e9b79d570fb2a4cd238c74","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":"ad9a5a09235e9a426da9b310cd466830ad103dda2e5ce4891ef4131fdf50deda","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":"ac583dafb57787174ec7563fabd453a375a7a1e1155a33f42323f68d9cea9c5d","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"}
@@ -774,24 +774,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":"d02e5c7a995847180696d2a05d8ea092b364ef32b36753e4091fc97ffdaf44ab","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":"2ddf03be75f0d46c8a7e1d7a3d3e70122001b190dcb361132cef2f1d23d8ddec","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"a7692c42bc8dd93b27887836381fa24095aa6776f3210b5155cf6e404dc17b38","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"d9d24565b06f891a687f49672129beac9d38ccb817526ed1a6579efcb5825eb0","declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"channel-core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"}
{"closureHash":"b94637421f524b48f7e497e449491475a2cc35f3d6dc43bdf5274b401b9424b0","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":"5885bf21e94629126fa96a1a953d2c913eeeaac3fc603e9d23846e0700b53835","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":"237c32c30b7f6979b8e955acfd6520db3ac2d98a3c46539c3b083d5d1af48247","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":"eb415824d16db17a50d7c90bd990684048255e5bf9ddc3099c86f624d6165cbd","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":"b06dd84646003c9e493e47c7a7249bc5f7ed63cec17586110c0274b7dc501b79","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":"4b7913e3376b2de7370a5970d8330e81f65574e173a313aec21d5dc5428007dd","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":"2fdb74bb36a7fd0b531c47e3fed55be12d9093046ed6d2a09ea98b6777c8fa88","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":"882a849a3b180ad11b9622cd5d38b932012672f8a7b7b67c229874bb1862ac48","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":"edf3dd5d9961a117ee2da538eb8741695dc57aeb0871bd9258a1bdaad5291be2","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":"d092260da21786feb5266e642269dc90807ac6a33131fffa66641d95b7544145","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"64ea70860eab8f956b0edf1624f2994eda772010247baf37139fb0e0d006e9c8","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"2a50f49bf524a67b6554ad9e3afc32487403828d09f966191930f7bb701032a1","declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"627f955848f6970b3fce5e72f1ccccc136636d67355114ce38aedd886be38db8","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"}
@@ -862,7 +862,7 @@
{"closureHash":"6b537112d5d84ed07c0b9fd24787eabc2f893f7bfdce1ed5915f88574a7e101c","declaration":"export function normalizeMentionText(text: string): string;","entrypoint":"channel-inbound","exportName":"normalizeMentionText","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"92900098c3cc5b73fba7622a3aa5a1a8c48462ff7ec0cfae8ee0a3aa6f8277f0","declaration":"export function normalizeOutboundLocation(value: unknown, label?: string): OutboundLocation | undefined;","entrypoint":"channel-inbound","exportName":"normalizeOutboundLocation","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"65f5f2725c1124096e437bcf5894983eee1023f7033be886f1a506044b77c58e","declaration":"export function recordChannelBotPairLoopAndCheckSuppression(params: ChannelBotLoopProtectionFacts): PairLoopGuardResult;","entrypoint":"channel-inbound","exportName":"recordChannelBotPairLoopAndCheckSuppression","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"b8271620147b917162fda83897092229d5691a4e418288a79912ae41775e83da","declaration":"export function recordChannelFeedbackEvent(params: { cfg: OpenClawConfig; agentId: string; sessionKey: string; event: Parameters<typeof appendTranscriptEvent>[1]; }): Promise<boolean>;","entrypoint":"channel-inbound","exportName":"recordChannelFeedbackEvent","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"d4c2f90ba1b19748480106cea43d965d7c3505b4c6325ee87deb7bd5e8885abe","declaration":"export function recordChannelFeedbackEvent(params: { cfg: OpenClawConfig; agentId: string; sessionKey: string; event: Parameters<typeof appendTranscriptEvent>[1]; }): Promise<boolean>;","entrypoint":"channel-inbound","exportName":"recordChannelFeedbackEvent","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"33b4280a69de6a4a87acf915c6d694b9ee41fc4f0262a1bb9836c46d53fe337c","declaration":"export function recordDroppedChannelInboundHistory(params: { input: NormalizedTurnInput; preflight: PreflightFacts; admission?: ChannelTurnAdmission; }): Promise<void>;","entrypoint":"channel-inbound","exportName":"recordDroppedChannelInboundHistory","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"33b4280a69de6a4a87acf915c6d694b9ee41fc4f0262a1bb9836c46d53fe337c","declaration":"export function recordDroppedChannelTurnHistory(params: { input: NormalizedTurnInput; preflight: PreflightFacts; admission?: ChannelTurnAdmission; }): Promise<void>;","entrypoint":"channel-inbound","exportName":"recordDroppedChannelTurnHistory","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"54b920fd80ac659a03000356b64e34d4a44ec22ae39a77accdd2ac97dc5a094e","declaration":"export function resolveChannelInboundRouteEnvelope(params: ResolveAgentRouteInput): { route: ResolvedAgentRoute; buildEnvelope: (input: ChannelInboundEnvelopeInput) => string; };","entrypoint":"channel-inbound","exportName":"resolveChannelInboundRouteEnvelope","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
@@ -1034,7 +1034,7 @@
{"closureHash":"acd64a0d6ce6dbd9db22f9e4a4497cef2110a9e962f240c6f5e90cef31505459","declaration":"export function createFinalizableDraftStreamControlsForState<T = string>(params: { throttleMs: number; state: FinalizableDraftStreamState; sendOrEditStreamMessage: (value: T) => Promise<void | boolean>; emptyValue?: T; isEmpty?: (value: T) => boolean; }): { loop: DraftStreamLoop<T> & { takePending: () => T; }; update: (value: T) => void; stop: () => Promise<void>; seal: () => Promise<void>; discardPending: () => Promise<void>; stopForClear: () => Promise<void>; };","entrypoint":"channel-message","exportName":"createFinalizableDraftStreamControlsForState","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"f2bb1868ec1cf7d97e6b8eb0a9a8a37122ec53c9cd4e2bbfc8448c1555f3d382","declaration":"export function createMessageReceiptFromOutboundResults(params: { results: readonly MessageReceiptInputResult[]; kind?: MessageReceiptPartKind; threadId?: string; replyToId?: string; sentAt?: number; }): MessageReceipt;","entrypoint":"channel-message","exportName":"createMessageReceiptFromOutboundResults","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"3c150fb027a4e4a1685b572d26aa1f70b3838641bd7629434c4265bfda549d0a","declaration":"export function createMessageReceiveContext<TMessage>(params: { id: string; channel: string; accountId?: string; message: TMessage; ackPolicy?: MessageAckPolicy; receivedAt?: number; signal?: AbortSignal; onAck?: () => Promise<void> | void; onNack?: (error: unknown) => Promise<void> | void; }): MessageReceiveContext<TMessage>;","entrypoint":"channel-message","exportName":"createMessageReceiveContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"93906d2c7105382007072ea3e79787732a62828361034e59306d3cd0f5b05a25","declaration":"export function createOutboundPayloadPlan(payloads: readonly ReplyPayload[], context?: OutboundPayloadPlanContext): OutboundPayloadPlan[];","entrypoint":"channel-message","exportName":"createOutboundPayloadPlan","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"541c4ed521f12c5e2ef9ea702c67923b559c5fdd109450c193239c9247a4f77b","declaration":"export function createOutboundPayloadPlan(payloads: readonly ReplyPayload[], context?: OutboundPayloadPlanContext): OutboundPayloadPlan[];","entrypoint":"channel-message","exportName":"createOutboundPayloadPlan","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"7abc004852a9162c3f4241d6c80b9962c9732dd03d2d38a56e4080a4e92758a1","declaration":"export function createPreviewMessageReceipt(params: { id: unknown; threadId?: string; replyToId?: string; sentAt?: number; raw?: unknown; }): MessageReceipt;","entrypoint":"channel-message","exportName":"createPreviewMessageReceipt","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"449a8d46d726b3de6fdf7aabf60939d37565ef5701ef58dd91b817040712ab87","declaration":"export function createReplyPrefixContext(params: { cfg: OpenClawConfig; agentId: string; channel?: string; accountId?: string; }): ReplyPrefixContextBundle;","entrypoint":"channel-message","exportName":"createReplyPrefixContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"acb4b6a4562c6a6ac30594fcd9431a9aabb031135fff7faa41a74067139ab572","declaration":"export function createReplyPrefixOptions(params: { cfg: OpenClawConfig; agentId: string; channel?: string; accountId?: string; }): ReplyPrefixOptions;","entrypoint":"channel-message","exportName":"createReplyPrefixOptions","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
@@ -1063,7 +1063,7 @@
{"closureHash":"844a3251dcba84a9735c855d7b6c930570cc486f3a1c5fc49b541572d52bb7d7","declaration":"export function normalizeAgentPlanSteps(value: unknown): AgentPlanStep[] | undefined;","entrypoint":"channel-message","exportName":"normalizeAgentPlanSteps","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"b19d38e15e2054e826fd132198c606165fc369fb5a96f4e6bbbf7b89bee35ac2","declaration":"export function normalizeChannelProgressDraftLineIdentity(line: string | ChannelProgressDraftLine | undefined): string;","entrypoint":"channel-message","exportName":"normalizeChannelProgressDraftLineIdentity","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"ba2389600eeea10867e3891e411bd84e8fd07dcd410249c29ca92ba59ef27bad","declaration":"export function processPidFromOwnerId(ownerId: string): number;","entrypoint":"channel-message","exportName":"processPidFromOwnerId","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"70bfbeeee4a0a2290cbf9620933c2bf7d00fbcaf3c20958c2064f5e2ce5a0bb8","declaration":"export function projectOutboundPayloadPlanForDelivery(plan: readonly OutboundPayloadPlan[]): ReplyPayload[];","entrypoint":"channel-message","exportName":"projectOutboundPayloadPlanForDelivery","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"ec60c4ceadf16fb4e54b875058278a3f5a1e7a5b7954b95bd621998ae05a2978","declaration":"export function projectOutboundPayloadPlanForDelivery(plan: readonly OutboundPayloadPlan[]): ReplyPayload[];","entrypoint":"channel-message","exportName":"projectOutboundPayloadPlanForDelivery","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"bc16377d8f9c3467da5115ff2d6b3ef0423980794a0f5af287b1f1e8ee9533ef","declaration":"export function recordOutboundMessageIdentity(identity: OutboundMessageIdentity): void;","entrypoint":"channel-message","exportName":"recordOutboundMessageIdentity","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"3090faaaca1e2c970a0eadbb7792c531a053746df6df83a082d8564a8313b401","declaration":"export function resolveAgentOutboundIdentity(cfg: OpenClawConfig, agentId: string): OutboundIdentity | undefined;","entrypoint":"channel-message","exportName":"resolveAgentOutboundIdentity","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
{"closureHash":"ba763bf7ff1dee852fb9b58f5995f9df744028d47ca29f687b215ba316c66723","declaration":"export function resolveChannelDraftStreamingChunking(cfg: OpenClawConfig | undefined, channelId: ChannelId, accountId: string | null | undefined, opts: { fallbackLimit: number; }): ChannelDraftStreamingChunking;","entrypoint":"channel-message","exportName":"resolveChannelDraftStreamingChunking","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"function","recordType":"export"}
@@ -1103,7 +1103,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":"64792a0c33912731a154f8e8e1b3f598b3396d2e0b93ed35fb4cc67a150bf9e2","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":"2e9cda856e1cfdb150d56efbdcb1225469b0fed11c67b3d80b1baa744a7f0cf3","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"}
@@ -1166,7 +1166,7 @@
{"closureHash":"acd64a0d6ce6dbd9db22f9e4a4497cef2110a9e962f240c6f5e90cef31505459","declaration":"export function createFinalizableDraftStreamControlsForState<T = string>(params: { throttleMs: number; state: FinalizableDraftStreamState; sendOrEditStreamMessage: (value: T) => Promise<void | boolean>; emptyValue?: T; isEmpty?: (value: T) => boolean; }): { loop: DraftStreamLoop<T> & { takePending: () => T; }; update: (value: T) => void; stop: () => Promise<void>; seal: () => Promise<void>; discardPending: () => Promise<void>; stopForClear: () => Promise<void>; };","entrypoint":"channel-outbound","exportName":"createFinalizableDraftStreamControlsForState","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"f2bb1868ec1cf7d97e6b8eb0a9a8a37122ec53c9cd4e2bbfc8448c1555f3d382","declaration":"export function createMessageReceiptFromOutboundResults(params: { results: readonly MessageReceiptInputResult[]; kind?: MessageReceiptPartKind; threadId?: string; replyToId?: string; sentAt?: number; }): MessageReceipt;","entrypoint":"channel-outbound","exportName":"createMessageReceiptFromOutboundResults","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"3c150fb027a4e4a1685b572d26aa1f70b3838641bd7629434c4265bfda549d0a","declaration":"export function createMessageReceiveContext<TMessage>(params: { id: string; channel: string; accountId?: string; message: TMessage; ackPolicy?: MessageAckPolicy; receivedAt?: number; signal?: AbortSignal; onAck?: () => Promise<void> | void; onNack?: (error: unknown) => Promise<void> | void; }): MessageReceiveContext<TMessage>;","entrypoint":"channel-outbound","exportName":"createMessageReceiveContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"93906d2c7105382007072ea3e79787732a62828361034e59306d3cd0f5b05a25","declaration":"export function createOutboundPayloadPlan(payloads: readonly ReplyPayload[], context?: OutboundPayloadPlanContext): OutboundPayloadPlan[];","entrypoint":"channel-outbound","exportName":"createOutboundPayloadPlan","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"541c4ed521f12c5e2ef9ea702c67923b559c5fdd109450c193239c9247a4f77b","declaration":"export function createOutboundPayloadPlan(payloads: readonly ReplyPayload[], context?: OutboundPayloadPlanContext): OutboundPayloadPlan[];","entrypoint":"channel-outbound","exportName":"createOutboundPayloadPlan","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"7abc004852a9162c3f4241d6c80b9962c9732dd03d2d38a56e4080a4e92758a1","declaration":"export function createPreviewMessageReceipt(params: { id: unknown; threadId?: string; replyToId?: string; sentAt?: number; raw?: unknown; }): MessageReceipt;","entrypoint":"channel-outbound","exportName":"createPreviewMessageReceipt","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"449a8d46d726b3de6fdf7aabf60939d37565ef5701ef58dd91b817040712ab87","declaration":"export function createReplyPrefixContext(params: { cfg: OpenClawConfig; agentId: string; channel?: string; accountId?: string; }): ReplyPrefixContextBundle;","entrypoint":"channel-outbound","exportName":"createReplyPrefixContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"acb4b6a4562c6a6ac30594fcd9431a9aabb031135fff7faa41a74067139ab572","declaration":"export function createReplyPrefixOptions(params: { cfg: OpenClawConfig; agentId: string; channel?: string; accountId?: string; }): ReplyPrefixOptions;","entrypoint":"channel-outbound","exportName":"createReplyPrefixOptions","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
@@ -1193,7 +1193,7 @@
{"closureHash":"844a3251dcba84a9735c855d7b6c930570cc486f3a1c5fc49b541572d52bb7d7","declaration":"export function normalizeAgentPlanSteps(value: unknown): AgentPlanStep[] | undefined;","entrypoint":"channel-outbound","exportName":"normalizeAgentPlanSteps","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"b19d38e15e2054e826fd132198c606165fc369fb5a96f4e6bbbf7b89bee35ac2","declaration":"export function normalizeChannelProgressDraftLineIdentity(line: string | ChannelProgressDraftLine | undefined): string;","entrypoint":"channel-outbound","exportName":"normalizeChannelProgressDraftLineIdentity","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"ba2389600eeea10867e3891e411bd84e8fd07dcd410249c29ca92ba59ef27bad","declaration":"export function processPidFromOwnerId(ownerId: string): number;","entrypoint":"channel-outbound","exportName":"processPidFromOwnerId","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"70bfbeeee4a0a2290cbf9620933c2bf7d00fbcaf3c20958c2064f5e2ce5a0bb8","declaration":"export function projectOutboundPayloadPlanForDelivery(plan: readonly OutboundPayloadPlan[]): ReplyPayload[];","entrypoint":"channel-outbound","exportName":"projectOutboundPayloadPlanForDelivery","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"ec60c4ceadf16fb4e54b875058278a3f5a1e7a5b7954b95bd621998ae05a2978","declaration":"export function projectOutboundPayloadPlanForDelivery(plan: readonly OutboundPayloadPlan[]): ReplyPayload[];","entrypoint":"channel-outbound","exportName":"projectOutboundPayloadPlanForDelivery","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"bc16377d8f9c3467da5115ff2d6b3ef0423980794a0f5af287b1f1e8ee9533ef","declaration":"export function recordOutboundMessageIdentity(identity: OutboundMessageIdentity): void;","entrypoint":"channel-outbound","exportName":"recordOutboundMessageIdentity","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"3090faaaca1e2c970a0eadbb7792c531a053746df6df83a082d8564a8313b401","declaration":"export function resolveAgentOutboundIdentity(cfg: OpenClawConfig, agentId: string): OutboundIdentity | undefined;","entrypoint":"channel-outbound","exportName":"resolveAgentOutboundIdentity","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
{"closureHash":"ba763bf7ff1dee852fb9b58f5995f9df744028d47ca29f687b215ba316c66723","declaration":"export function resolveChannelDraftStreamingChunking(cfg: OpenClawConfig | undefined, channelId: ChannelId, accountId: string | null | undefined, opts: { fallbackLimit: number; }): ChannelDraftStreamingChunking;","entrypoint":"channel-outbound","exportName":"resolveChannelDraftStreamingChunking","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"function","recordType":"export"}
@@ -1232,7 +1232,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":"64792a0c33912731a154f8e8e1b3f598b3396d2e0b93ed35fb4cc67a150bf9e2","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":"2e9cda856e1cfdb150d56efbdcb1225469b0fed11c67b3d80b1baa744a7f0cf3","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"}
@@ -1298,7 +1298,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":"d02e5c7a995847180696d2a05d8ea092b364ef32b36753e4091fc97ffdaf44ab","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":"2ddf03be75f0d46c8a7e1d7a3d3e70122001b190dcb361132cef2f1d23d8ddec","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-plugin-common","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"}
{"closureHash":"a7692c42bc8dd93b27887836381fa24095aa6776f3210b5155cf6e404dc17b38","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-plugin-common","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"}
{"closureHash":"b94637421f524b48f7e497e449491475a2cc35f3d6dc43bdf5274b401b9424b0","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"}
@@ -1897,8 +1897,8 @@
{"closureHash":"9e93bcdfc6299e65ef2a3d267047a9e8c856f0b2f8e9b79d570fb2a4cd238c74","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":"ad9a5a09235e9a426da9b310cd466830ad103dda2e5ce4891ef4131fdf50deda","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":"fd766c74118002291fa28dcd19a9677cdf9f615fe09994862f95b989dd36479a","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":"ac583dafb57787174ec7563fabd453a375a7a1e1155a33f42323f68d9cea9c5d","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":"ed949ec84859c0ebca72e1a10d757b1063cf05a83898c18ffe0e62eb5a72f80c","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"}
@@ -1945,7 +1945,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":"d759a25775cb15d3f1804e39e98518f83b67b29ba3ab2fd84d4bf16106d9fdb9","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"core","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"bfca309d41b8bed7b9b967ea4c643d6044cbd3dead3c87c6f05ea2d01859c4e9","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"}
@@ -1977,10 +1977,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":"2ddf03be75f0d46c8a7e1d7a3d3e70122001b190dcb361132cef2f1d23d8ddec","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"a7692c42bc8dd93b27887836381fa24095aa6776f3210b5155cf6e404dc17b38","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"647007b989ba3d08c520ec68873f65f22f231b0274daf10f5d219074fd2fcba6","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":"ccae64ce42b52f34fc0f2adffe6202985c2c8464e3e2df19f43ae1eb0005e808","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"}
{"closureHash":"09482263ed4b15cc73a7d59079368b3c646b6482764650d08827d946d85895e7","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"}
@@ -2215,7 +2215,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":"2ddf03be75f0d46c8a7e1d7a3d3e70122001b190dcb361132cef2f1d23d8ddec","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"discord","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"a7692c42bc8dd93b27887836381fa24095aa6776f3210b5155cf6e404dc17b38","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"discord","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"}
{"closureHash":"b94637421f524b48f7e497e449491475a2cc35f3d6dc43bdf5274b401b9424b0","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"}
@@ -2360,7 +2360,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":"871d3ff30c14af1a605fdb47c50923a910a7886f7cc886ba1cd9c2eff1ea8334","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":"64792a0c33912731a154f8e8e1b3f598b3396d2e0b93ed35fb4cc67a150bf9e2","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":"2e9cda856e1cfdb150d56efbdcb1225469b0fed11c67b3d80b1baa744a7f0cf3","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":"10d47ee68a65fa2df62012bf98988026560dd0bc30d2be56783ed4132864a149","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"}
@@ -3195,19 +3195,19 @@
{"closureHash":"66f0964725c2b9640e8ab8cfb803a421a9e58a2238ea4fbf7ffa97d0c4d33df0","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":"c7e8cad42fa48c1be4ae1b1f7f87339c849cacf81a53ad53df76e188f4dd4629","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":"21edc4c0432bbcf579b3e35e390dbb217f09649c1fea5aadff78773f306da866","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":"eaf5cff3027412d66a1e6488a71c1fb21ae1efb16d5c7415afed7370a1641dbf","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":"2ce663d0c867849f055335c2e964429a31516c653e0628173149d4f991067808","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":"3c09a8c46d56d7eb2489dcdad4386365ce7d464f41d6826fe682d43e8c0d8472","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":"abfbbc6b5eca3947bed22ea782cf5ad582b6a364f554d8851c92c2d3cbc3c6f7","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":"364dbe49fce073de6aff2b666e2e80b9d188f04458f179f0569506702b2e5075","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":"ed383aeec67155486ebb84de62db0c0ba2832f4c70fcccf47e7ed8de92d98ecb","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":"c571be67deff656fc2e53aecb61d02bd58b468d4b548860b37b61eafa18f8d6b","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":"e80aa4d0d11d08e96fc7b42afbf3ee38aca718a342b3d12c09f47d55d3bbdd43","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"}
@@ -3218,8 +3218,8 @@
{"closureHash":"120055d3915ab89e6960d454ba75088f5d000e2abe1715901b791f86ab0f2ba5","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":"c64d9bc7f2e478a5affc01a910f49882aef707b077d8ebc0da889985cfaf9595","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":"20dff0ae02d5d945273bedcb54a853efed1f43cf1f03754a52e0eb3e584c7b88","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":"63f2ba1711d2c7892ecb4d133ed5f2d4acca4d72680e8efe0c16ef721d5b6673","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":"21744f5ed9b90d14f80ef303087d5bcec0b6e83ca6248ac57d7bbdff1d6897f3","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"}
@@ -3251,7 +3251,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":"8d3f6abdbf39603c6f28a69b9bb8f0b214d978dc10dd83ca3f6a692f565a7cb0","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":"672ace60a1e69878afa3cf2382a07f478128158f9229a678fcc1864565df6f96","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"}
@@ -3265,9 +3265,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":"376e9dae283ed91fb2a5e5769c33f09bed27db7a0d42afe7d305bb1f6d58167e","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":"38cc3441d19861bc1bc1de8eedf0649465eb0f38512de81f39658ef590c6e212","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":"2bf79ddb49656aafb6210e91b35334500af758fbe90bc6da026a8660fd3ed5b4","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":"0ffca719e5af0cc3a4165180bd9bcb416056635b07683191a26aa27aa9168add","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"}
@@ -3403,9 +3403,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":"fd766c74118002291fa28dcd19a9677cdf9f615fe09994862f95b989dd36479a","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":"ed949ec84859c0ebca72e1a10d757b1063cf05a83898c18ffe0e62eb5a72f80c","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":"d759a25775cb15d3f1804e39e98518f83b67b29ba3ab2fd84d4bf16106d9fdb9","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"plugin-entry","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"bfca309d41b8bed7b9b967ea4c643d6044cbd3dead3c87c6f05ea2d01859c4e9","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"}
@@ -3421,10 +3421,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":"2ddf03be75f0d46c8a7e1d7a3d3e70122001b190dcb361132cef2f1d23d8ddec","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"a7692c42bc8dd93b27887836381fa24095aa6776f3210b5155cf6e404dc17b38","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"647007b989ba3d08c520ec68873f65f22f231b0274daf10f5d219074fd2fcba6","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":"ccae64ce42b52f34fc0f2adffe6202985c2c8464e3e2df19f43ae1eb0005e808","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"}
{"closureHash":"09482263ed4b15cc73a7d59079368b3c646b6482764650d08827d946d85895e7","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"}
@@ -3574,14 +3574,14 @@
{"closureHash":"983131a9d5c2971ecd7f2fb04f9d664d6854535d1abe2099baf2292014c30b23","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":"dfc8fc733f281bfbe0cc270964f0f657f17c2b97f21bc1b316aee6e9de8ff6ba","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":"8240e99e361a089d6dacdaecb1d367e31bdce8be4d3b137a5b0065904bac11c6","declaration":"export function getPluginRuntimeGatewayRequestScope(): PluginRuntimeGatewayRequestScope | undefined;","entrypoint":"plugin-runtime","exportName":"getPluginRuntimeGatewayRequestScope","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"}
{"closureHash":"5773d2030bc8b30003ebf283763f478f27e4cd9b0f2205e9c2b1f3a872bd8487","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":"c53214cb523257e979580c6683f378f2f8e85c154e18c8e30ad84ae0eba28c28","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":"94e8ab88f5f42afc3ba7300ea0da6364722360be9083d89aade693b82c3e162f","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":"2ddf03be75f0d46c8a7e1d7a3d3e70122001b190dcb361132cef2f1d23d8ddec","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-runtime","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"}
{"closureHash":"a7692c42bc8dd93b27887836381fa24095aa6776f3210b5155cf6e404dc17b38","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"}
@@ -3671,10 +3671,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":"720c33e7d2fb1f65d8fd6c7949e9498fda0a1b4eb0ccf6a7635b2dce58d68d05","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":"228e8e3d4bd122168da916b923639accece60b5706df52fb437fff2b2a46f18f","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":"b066e2fa15256bc32e1fa098c5302922f0db5ccbca4a59c3331860e822c779fc","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":"b7e37ed1adcc928a9a65520f5c288051f01e873a99bcb559488de03869bb6cf7","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":"d9a3d65ab4200e4f9857b0459e8cfaf898bfebde8a94a6c8d82bf9f3ce81eb64","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":"b03d523d4b368c8f4fca0caf78165bbfbfaa8be4dec5390201e12aeb66134001","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":"15f34788b0e40c2febc8b538ad49f3b297ffcac589e32065d9664794d70a1e20","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":"fb56eedf17994958a2c0cf52398cf79191fdcfbeeceba87ae3412466de7b4ef5","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"}
@@ -4639,16 +4639,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":"296a332a70c3af6ed244dc40f665226c3fec4c68ee1e6f836ce62ff9504cbabc","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":"f034787dce706f74e5ad08ed9a85af4cd5a6f3eeda279af7b6fb31d4861a6dde","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":"6c3a1859eedd030e8f47fd172946ab37de66367752388c459eac4c3261e929e2","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":"dbb9e65a5f0c657542d4f2ee7a9ecc287476375dc2100984c162c2c9c00447db","declaration":"export type DefinedToolPluginEntry = DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"DefinedToolPluginEntry","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"724409d65bf50cf7dcf9c9031c780f03824af1a7e82c7b7024844c57260f5c34","declaration":"export type ToolPluginExecutionContext = ToolPluginExecutionContext;","entrypoint":"tool-plugin","exportName":"ToolPluginExecutionContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"53cee5e8cd018ab1ae19f13012dcaed6e1837f7bd01098e63ceae66ce61237fa","declaration":"export type ToolPluginFactoryContext<TConfig> = ToolPluginFactoryContext<TConfig>;","entrypoint":"tool-plugin","exportName":"ToolPluginFactoryContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"ab56234c79293b6b206c7975789e8f4cfb6fb481fb227c5464051a0b682db828","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":"a3043ea977a394b53d157073bc5f373ad28f61997fc1752bb01265be62bc9b01","declaration":"export type DefinedToolPluginEntry = DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"DefinedToolPluginEntry","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"7b0e2a7c96dd726816480b62b466f34df5efba467d77da173bf9b08704311225","declaration":"export type ToolPluginExecutionContext = ToolPluginExecutionContext;","entrypoint":"tool-plugin","exportName":"ToolPluginExecutionContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"1814b014e6e25490835292c58c786b9d7945b0f3179a9698029f55f53df75448","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":"5cb770b85f7a568f9563c0d6d746ab8f10bb551e61c5ee07b28683ec50a28d00","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":"f851db4de9ce7588165a8fa0d3cf5a572596bc27135639cd24004e147fe925fe","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"}
@@ -4682,9 +4682,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":"137a9d7b92bee027864e6b3ed9dab727b82cec93e88d72099ea796f3cba761cf","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":"32467eac87696d9fe01b7f3fb4586c0e10ae4cd8131c353faa0f9663ff46d313","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":"081f16d31a0ae6a890fa372cde3100a3544fe95d584bbaef196009b56a60d275","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":"c36279d85866fd9e2672d3acc910ac71b73886fe118dfe64d976090e7552c0de","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"}
@@ -4702,7 +4702,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":"9357b6c5e08176f9f4416d77f22dfce793961da76bf7b88ea1b3a756be25d924","declaration":"export type RegisterWebhookPluginRouteOptions = RegisterWebhookPluginRouteOptions;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookPluginRouteOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"49675c44ab8b6607d9a56e0e2b9458ca15394f86798ada8e0e9b90051708ad92","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"}
+3 -3
View File
@@ -45,7 +45,7 @@ import {
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js";
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
import { resolveSkillsPrompt } from "../../skills/loading/workspace-skill-prompt.js";
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
import { resolveUserPath } from "../../utils.js";
import { normalizeMessageChannel } from "../../utils/message-channel.js";
@@ -259,7 +259,7 @@ async function resolveCliSkillsPrompt(params: {
workspaceDir: params.workspaceDir,
});
if (!sandboxWorkspace) {
return resolveSkillsPromptForRun({
return resolveSkillsPrompt({
skillsSnapshot: params.skillsSnapshot,
workspaceDir: params.workspaceDir,
config: params.config,
@@ -305,7 +305,7 @@ async function resolveCliSkillsPrompt(params: {
skillsWorkspaceDir,
skillsPromptWorkspaceDir,
});
return resolveSkillsPromptForRun({
return resolveSkillsPrompt({
skillsSnapshot: skillsSnapshotForRun,
entries: promptSkillEntries,
workspaceDir: skillsPromptWorkspaceDir,
+2 -2
View File
@@ -3,7 +3,7 @@
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Skill } from "../skills/loading/skill-contract.js";
import { resolveSkillsPromptForRun } from "../skills/loading/workspace.js";
import { resolveSkillsPrompt } from "../skills/loading/workspace-skill-prompt.js";
import { createFixtureSkillEntry } from "../skills/test-support/test-helpers.js";
import { createOpenClawReadTool } from "./agent-tools.read.js";
import { resolveCodeModeSkills } from "./code-mode-skills.js";
@@ -48,7 +48,7 @@ describe("Code Mode skills and read tools", () => {
it("keeps Code Mode skill parsing aligned with the production prompt renderer", () => {
const entries = [createFixtureSkillEntry("alpha"), createFixtureSkillEntry("beta")];
const skillsPrompt = resolveSkillsPromptForRun({
const skillsPrompt = resolveSkillsPrompt({
entries,
workspaceDir: "/workspace",
});
+2 -2
View File
@@ -44,7 +44,7 @@ import {
type UserTurnInput,
type UserTurnTranscriptRecorder,
} from "../../sessions/user-turn-transcript.js";
import { buildWorkspaceSkillSnapshot } from "../../skills/loading/workspace.js";
import type { SkillSnapshot } from "../../skills/types.js";
import {
getGeneratedMediaTaskIdsForSessionKey,
hasNewGeneratedMediaTaskForSessionKey,
@@ -535,7 +535,7 @@ export function runAgentAttempt(params: {
runContext: ReturnType<typeof resolveAgentRunContext>;
spawnedBy: string | undefined;
messageChannel: ReturnType<typeof resolveMessageChannel>;
skillsSnapshot: ReturnType<typeof buildWorkspaceSkillSnapshot> | undefined;
skillsSnapshot: SkillSnapshot | undefined;
resolvedVerboseLevel: VerboseLevel | undefined;
agentDir: string;
onAgentEvent: (evt: {
@@ -964,9 +964,12 @@ export async function loadCompactHooksHarness(): Promise<{
applySkillEnvOverridesFromSnapshot: vi.fn(() => () => {}),
}));
vi.doMock("../../skills/loading/workspace.js", () => ({
loadWorkspaceSkillEntries: vi.fn(() => []),
resolveSkillsPromptForRun: vi.fn(() => undefined),
vi.doMock("../../skills/loading/workspace-skill-loader.js", () => ({
loadWorkspaceSkills: vi.fn(() => []),
}));
vi.doMock("../../skills/loading/workspace-skill-prompt.js", () => ({
resolveSkillsPrompt: vi.fn(() => undefined),
}));
vi.doMock("../agent-scope.js", () => ({
@@ -16,7 +16,7 @@ import { extractModelCompat } from "../../plugins/provider-model-compat.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { transformProviderSystemPrompt } from "../../plugins/provider-runtime.js";
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
import { resolveSkillsPrompt } from "../../skills/loading/workspace-skill-prompt.js";
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
import {
applySkillEnvOverrides,
@@ -170,7 +170,7 @@ export async function buildPreparedCompactionRuntime(prepared: DirectCompactionP
skillsWorkspaceDir: effectiveSkillsWorkspace,
skillsPromptWorkspaceDir: effectiveSkillsPromptWorkspace,
});
const skillsPrompt = resolveSkillsPromptForRun({
const skillsPrompt = resolveSkillsPrompt({
skillsSnapshot: skillsSnapshotForRun,
entries: promptSkillEntries,
config: params.config,
@@ -28,7 +28,7 @@ import {
resolveProviderRuntimePluginHandle,
type ProviderRuntimePluginHandle,
} from "../../../plugins/provider-hook-runtime.js";
import { resolveSkillsPromptForRun } from "../../../skills/loading/workspace.js";
import { resolveSkillsPrompt } from "../../../skills/loading/workspace-skill-prompt.js";
import { resolveEmbeddedRunSkillEntries } from "../../../skills/runtime/embedded-run-entries.js";
import {
applySkillEnvOverrides,
@@ -524,7 +524,7 @@ export function prepareEmbeddedAttemptSkills(params: {
skillsWorkspaceDir,
skillsPromptWorkspaceDir,
});
const skillsPrompt = resolveSkillsPromptForRun({
const skillsPrompt = resolveSkillsPrompt({
skillsSnapshot,
entries: promptSkillEntries,
config: params.attempt.config,
@@ -491,8 +491,8 @@ vi.mock("../../../skills/runtime/env-overrides.js", () => ({
applySkillEnvOverridesFromSnapshot: () => () => {},
}));
vi.mock("../../../skills/loading/workspace.js", () => ({
resolveSkillsPromptForRun: (...args: unknown[]) => hoisted.resolveSkillsPromptForRunMock(...args),
vi.mock("../../../skills/loading/workspace-skill-prompt.js", () => ({
resolveSkillsPrompt: (...args: unknown[]) => hoisted.resolveSkillsPromptForRunMock(...args),
}));
vi.mock("../../../skills/runtime/embedded-run-entries.js", () => ({
@@ -18,8 +18,8 @@ vi.mock("../../../skills/runtime/embedded-run-entries.js", () => ({
})),
}));
vi.mock("../../../skills/loading/workspace.js", () => ({
resolveSkillsPromptForRun: vi.fn(() => "skills prompt"),
vi.mock("../../../skills/loading/workspace-skill-prompt.js", () => ({
resolveSkillsPrompt: vi.fn(() => "skills prompt"),
}));
vi.mock("../sandbox-skills.js", () => ({
@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createSyntheticSourceInfo } from "../../skills/loading/skill-contract.js";
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
import { resolveSkillsPrompt } from "../../skills/loading/workspace-skill-prompt.js";
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
import type { SkillSnapshot } from "../../skills/types.js";
import {
@@ -177,7 +177,7 @@ describe("resolveSandboxSkillRuntimeInputs", () => {
skillsWorkspaceDir,
skillsPromptWorkspaceDir,
});
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
skillsSnapshot: skillsSnapshotForRun,
entries: promptSkillEntries,
workspaceDir: skillsPromptWorkspaceDir,
@@ -228,7 +228,7 @@ describe("resolveSandboxSkillRuntimeInputs", () => {
eligibility: skillsEligibility,
workspaceOnly: true,
});
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
entries: shouldLoadSkillEntries ? skillEntries : undefined,
workspaceDir: root,
eligibility: skillsEligibility,
@@ -34,8 +34,8 @@ vi.mock("../process/exec.js", async (importOriginal) => ({
spawnCommand: spawnDockerProcess,
}));
vi.mock("../skills/loading/workspace.js", () => ({
syncSkillsToWorkspace: vi.fn(async () => undefined),
vi.mock("../skills/loading/workspace-skill-sync.runtime.js", () => ({
syncWorkspaceSkills: vi.fn(async () => undefined),
}));
let resolveSandboxContext: typeof import("./sandbox/context.js").resolveSandboxContext;
@@ -60,8 +60,8 @@ vi.mock("../skills/runtime/remote.js", () => ({
getRemoteSkillEligibility: vi.fn(() => ({ note: "test-remote" })),
}));
vi.mock("../skills/loading/workspace.js", () => ({
syncSkillsToWorkspace: syncSkillsToWorkspaceMock,
vi.mock("../skills/loading/workspace-skill-sync.runtime.js", () => ({
syncWorkspaceSkills: syncSkillsToWorkspaceMock,
}));
let sandboxFixtureRoot = "";
+13 -10
View File
@@ -14,6 +14,7 @@ import {
resolveBrowserConfig,
} from "../../plugin-sdk/browser-profiles.js";
import { defaultRuntime } from "../../runtime.js";
import { createLazyRuntimeNamedExport } from "../../shared/lazy-runtime.js";
import type { SkillEligibilityContext, SkillSnapshot, SkillUsagePath } from "../../skills/types.js";
import type { ExecPolicyOverrides } from "../exec-defaults.js";
import { getSandboxBackendWorkdirResolver, requireSandboxBackendFactory } from "./backend.js";
@@ -29,6 +30,11 @@ import { resolveSandboxWorkspaceLayoutPaths } from "./shared.js";
import type { SandboxContext, SandboxWorkspaceInfo } from "./types.js";
import { ensureSandboxWorkspace } from "./workspace.js";
const loadSyncWorkspaceSkills = createLazyRuntimeNamedExport(
() => import("../../skills/loading/workspace-skill-sync.runtime.js"),
"syncWorkspaceSkills",
);
async function syncSandboxSkillsToWorkspace(params: {
sourceWorkspaceDir: string;
targetWorkspaceDir: string;
@@ -39,15 +45,12 @@ async function syncSandboxSkillsToWorkspace(params: {
skillsSnapshot?: SkillSnapshot;
}): Promise<{ eligibility?: SkillEligibilityContext; skillUsagePaths?: SkillUsagePath[] }> {
try {
const [
{ syncSkillsToWorkspace },
{ getRemoteSkillEligibility },
{ resolveNodeExecEligibility },
] = await Promise.all([
import("../../skills/loading/workspace.js"),
import("../../skills/runtime/remote.js"),
import("../exec-defaults.js"),
]);
const [syncWorkspaceSkills, { getRemoteSkillEligibility }, { resolveNodeExecEligibility }] =
await Promise.all([
loadSyncWorkspaceSkills(),
import("../../skills/runtime/remote.js"),
import("../exec-defaults.js"),
]);
const nodeSkills = resolveNodeExecEligibility({
cfg: params.config,
sessionKey: params.rawSessionKey,
@@ -60,7 +63,7 @@ async function syncSandboxSkillsToWorkspace(params: {
advertiseExecNode: nodeSkills.canExec,
}),
};
const skillUsagePaths = await syncSkillsToWorkspace({
const skillUsagePaths = await syncWorkspaceSkills({
sourceWorkspaceDir: params.sourceWorkspaceDir,
targetWorkspaceDir: params.targetWorkspaceDir,
config: params.config,
@@ -21,7 +21,7 @@ import { buildConfiguredAgentSystemPrompt } from "../../agents/system-prompt-con
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
import type { WorkspaceBootstrapFile } from "../../agents/workspace.js";
import { listRegisteredPluginAgentPromptGuidance } from "../../plugins/command-registry-state.js";
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
import { resolveSkillsPrompt } from "../../skills/loading/workspace-skill-prompt.js";
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
import { getRemoteSkillEligibility } from "../../skills/runtime/remote.js";
import { resolveReusableWorkspaceSkillSnapshot } from "../../skills/runtime/session-snapshot.js";
@@ -127,7 +127,7 @@ async function resolveCommandSkillsPrompt(params: {
skillsWorkspaceDir,
skillsPromptWorkspaceDir,
});
return resolveSkillsPromptForRun({
return resolveSkillsPrompt({
skillsSnapshot: skillsSnapshotForRun,
entries: promptSkillEntries,
config: params.config,
+2 -2
View File
@@ -50,8 +50,8 @@ vi.mock("../../skills/runtime/remote.js", () => ({
getRemoteSkillEligibility: getRemoteSkillEligibilityMock,
}));
vi.mock("../../skills/loading/workspace.js", () => ({
buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock,
vi.mock("../../skills/loading/workspace-skill-prompt.js", () => ({
buildSkillSnapshot: buildWorkspaceSkillSnapshotMock,
}));
vi.mock("../../skills/runtime/refresh.js", () => ({
+6 -3
View File
@@ -297,9 +297,12 @@ vi.mock("../agents/workspace.js", () => ({
ensureAgentWorkspace: vi.fn(async ({ dir }: { dir: string }) => ({ dir })),
}));
vi.mock("../skills/loading/workspace.js", () => ({
buildWorkspaceSkillSnapshot: vi.fn(() => undefined),
loadWorkspaceSkillEntries: vi.fn(() => []),
vi.mock("../skills/loading/workspace-skill-prompt.js", () => ({
buildSkillSnapshot: vi.fn(() => undefined),
}));
vi.mock("../skills/loading/workspace-skill-loader.js", () => ({
loadWorkspaceSkills: vi.fn(() => []),
}));
vi.mock("../skills/runtime/remote.js", () => ({
+2 -2
View File
@@ -42,7 +42,7 @@ import {
} from "../../skills/lifecycle/clawhub.js";
import { installSkill } from "../../skills/lifecycle/install.js";
import { installUploadedSkillArchive } from "../../skills/lifecycle/upload-install.js";
import { loadWorkspaceSkillEntries } from "../../skills/loading/workspace.js";
import { loadWorkspaceSkills } from "../../skills/loading/workspace-skill-loader.js";
import { getRemoteSkillEligibility } from "../../skills/runtime/remote.js";
import {
collectClawHubVerdictTargets,
@@ -272,7 +272,7 @@ export const skillsHandlers: GatewayRequestHandlers = {
const workspaceDirs = listAgentWorkspaceDirs(cfg);
const bins = new Set<string>();
for (const workspaceDir of workspaceDirs) {
const entries = loadWorkspaceSkillEntries(workspaceDir, { config: cfg });
const entries = loadWorkspaceSkills(workspaceDir, { config: cfg });
for (const bin of collectSkillBins(entries)) {
bins.add(bin);
}
+2 -2
View File
@@ -11,8 +11,8 @@ import {
collectStateDeepFilesystemFindings,
} from "./audit-extra.async.js";
vi.mock("../skills/loading/workspace.js", () => ({
loadWorkspaceSkillEntries: (workspaceDir: string) => {
vi.mock("../skills/loading/workspace-skill-loader.js", () => ({
loadWorkspaceSkills: (workspaceDir: string) => {
const sep = workspaceDir.includes("\\") ? "\\" : "/";
const baseDir = `${workspaceDir}${sep}skills${sep}evil-skill`;
return [
+2 -4
View File
@@ -24,6 +24,7 @@ import { readRegularFile, statRegularFile } from "../infra/fs-safe.js";
import { LEGACY_IMPLICIT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
import { getOrCreatePromise } from "../shared/lazy-promise.js";
import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "../shared/lazy-runtime.js";
import { loadWorkspaceSkills } from "../skills/loading/workspace-skill-loader.js";
import type { SkillScanFinding } from "../skills/security/scanner.js";
import { listInstalledPluginDirs } from "./installed-plugin-dirs.js";
import { extensionUsesSkippedScannerPath, isPathInside } from "./scan-paths.js";
@@ -48,7 +49,6 @@ type ExecDockerRawFn = (
const DEFAULT_SANDBOX_BROWSER_DOCKER_PROBE_TIMEOUT_MS = 5000;
type CodeSafetySummaryCache = Map<string, Promise<unknown>>;
const loadSkillsModule = createLazyRuntimeModule(() => import("../skills/loading/workspace.js"));
const loadConfigModule = createLazyRuntimeModule(() => import("../config/config.js"));
@@ -919,10 +919,8 @@ export async function collectInstalledSkillsCodeSafetyFindings(params: {
workspaceDirs.add(workspaceDir);
}
}
const { loadWorkspaceSkillEntries } = await loadSkillsModule();
for (const workspaceDir of workspaceDirs) {
const entries = loadWorkspaceSkillEntries(workspaceDir, {
const entries = loadWorkspaceSkills(workspaceDir, {
config: params.cfg,
includeArchived: true,
});
+3 -3
View File
@@ -26,9 +26,9 @@ vi.mock("../../plugins/bundle-commands.js", () => ({
loadEnabledClaudeBundleCommands: () => bundleCommandState.entries,
}));
vi.mock("../loading/workspace.js", () => ({
filterWorkspaceSkillEntriesWithOptions: (entries: SkillEntry[]) => entries,
loadVisibleWorkspaceSkillEntries: () => [],
vi.mock("../loading/workspace-skill-loader.js", () => ({
filterWorkspaceSkills: (entries: SkillEntry[]) => entries,
loadVisibleSkills: () => [],
}));
beforeEach(() => {
+3 -6
View File
@@ -9,10 +9,7 @@ import { createDedupeCache } from "../../infra/dedupe.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { loadEnabledClaudeBundleCommands } from "../../plugins/bundle-commands.js";
import { resolveSkillTelemetrySource } from "../loading/source.js";
import {
filterWorkspaceSkillEntriesWithOptions,
loadVisibleWorkspaceSkillEntries,
} from "../loading/workspace.js";
import { filterWorkspaceSkills, loadVisibleSkills } from "../loading/workspace-skill-loader.js";
import type { SkillEligibilityContext, SkillCommandSpec, SkillEntry } from "../types.js";
import { resolveEffectiveAgentSkillFilter } from "./agent-filter.js";
import { filterUserInvocableSkillEntries, isSkillPromptVisible } from "./skill-index.js";
@@ -89,12 +86,12 @@ export function buildWorkspaceSkillCommandSpecs(
const effectiveSkillFilter =
opts?.skillFilter ?? resolveEffectiveAgentSkillFilter(opts?.config, opts?.agentId);
const eligible = opts?.entries
? filterWorkspaceSkillEntriesWithOptions(opts.entries, {
? filterWorkspaceSkills(opts.entries, {
config: opts?.config,
skillFilter: effectiveSkillFilter,
eligibility: opts?.eligibility,
})
: loadVisibleWorkspaceSkillEntries(workspaceDir, {
: loadVisibleSkills(workspaceDir, {
config: opts?.config,
managedSkillsDir: opts?.managedSkillsDir,
bundledSkillsDir: opts?.bundledSkillsDir,
@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { withEnv, withEnvAsync } from "../../test-utils/env.js";
import { loadWorkspaceSkillEntries } from "../loading/workspace.js";
import { loadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js";
import type { SkillEntry } from "../types.js";
@@ -194,7 +194,7 @@ describe("buildWorkspaceSkillStatus", () => {
it("requires explicit enablement before exposing bundled coding-agent", async () => {
const workspaceDir = await createTempWorkspaceDir();
const bundledSkillsDir = path.resolve("skills");
const entries = loadWorkspaceSkillEntries(workspaceDir, {
const entries = loadWorkspaceSkills(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir,
config: {
+2 -2
View File
@@ -22,7 +22,7 @@ import {
resolveSkillConfig,
resolveSkillsInstallPreferences,
} from "../loading/config.js";
import { loadWorkspaceSkillEntries } from "../loading/workspace.js";
import { loadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
import { mergeRemoteNodeSkillEntries } from "../runtime/remote-skills.js";
import type {
SkillEntry,
@@ -361,7 +361,7 @@ export function buildWorkspaceSkillStatus(
// the loader must stay unfiltered; node-hosted skills merge in separately.
const skillEntries = mergeRemoteNodeSkillEntries(
opts?.entries ??
loadWorkspaceSkillEntries(workspaceDir, {
loadWorkspaceSkills(workspaceDir, {
config: opts?.config,
managedSkillsDir,
bundledSkillsDir: bundledContext.dir,
@@ -8,7 +8,7 @@ import { hasBinaryMock, runCommandWithTimeoutMock } from "../test-support/instal
import type { SkillEntry, SkillInstallSpec } from "../types.js";
const skillsMocks = vi.hoisted(() => ({
loadWorkspaceSkillEntries: vi.fn(),
loadWorkspaceSkills: vi.fn(),
}));
vi.mock("../../process/exec.js", () => ({
@@ -19,8 +19,8 @@ vi.mock("../../plugins/install-security-scan.js", () => ({
evaluateSkillInstallPolicy: vi.fn(async () => undefined),
}));
vi.mock("../loading/workspace.js", () => ({
loadWorkspaceSkillEntries: skillsMocks.loadWorkspaceSkillEntries,
vi.mock("../loading/workspace-skill-loader.js", () => ({
loadWorkspaceSkills: skillsMocks.loadWorkspaceSkills,
}));
let installSkill: typeof import("./install.js").installSkill;
@@ -132,7 +132,7 @@ describe("skills-install fallback edge cases", () => {
beforeAll(async () => {
workspaceDir = await suiteTempDirs.setup();
skillsMocks.loadWorkspaceSkillEntries.mockReturnValue([
skillsMocks.loadWorkspaceSkills.mockReturnValue([
makeSkillEntry(workspaceDir, "go-tool-single", {
kind: "go",
module: "example.com/tool@latest",
@@ -282,7 +282,7 @@ describe("skills-install fallback edge cases", () => {
});
mockAvailableBinaries([]);
try {
skillsMocks.loadWorkspaceSkillEntries.mockReturnValueOnce([
skillsMocks.loadWorkspaceSkills.mockReturnValueOnce([
makeSkillEntry(workspaceDir, "brew-tool-container", {
kind: "brew",
formula: "openai-whisper",
+2 -2
View File
@@ -1,10 +1,10 @@
import type { resolveSkillsInstallPreferences } from "../loading/config.js";
import type { loadWorkspaceSkillEntries } from "../loading/workspace.js";
import type { loadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
import "./install.js";
type SkillsInstallDeps = {
hasBinary(bin: string): boolean;
loadWorkspaceSkillEntries: typeof loadWorkspaceSkillEntries;
loadWorkspaceSkills: typeof loadWorkspaceSkills;
resolveNodeInstallStateDir(): string;
resolveBrewExecutable(): string | undefined;
isContainerEnvironment(): boolean;
+1 -1
View File
@@ -115,7 +115,7 @@ describe("installSkill before_install hooks", () => {
resetGlobalHookRunner();
runCommandWithTimeoutMock.mockClear();
skillsInstallTesting.setDepsForTest({
loadWorkspaceSkillEntries: loadTestWorkspaceSkillEntries,
loadWorkspaceSkills: loadTestWorkspaceSkillEntries,
resolveNodeInstallStateDir: () => {
const stateDir = process.env.OPENCLAW_STATE_DIR;
if (!stateDir) {
+4 -4
View File
@@ -17,7 +17,7 @@ import {
resolveSkillsInstallPreferences as defaultResolveSkillsInstallPreferences,
} from "../loading/config.js";
import { resolveSkillSource } from "../loading/source.js";
import { loadWorkspaceSkillEntries as defaultLoadWorkspaceSkillEntries } from "../loading/workspace.js";
import { loadWorkspaceSkills as defaultLoadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
import type { SkillEntry, SkillInstallSpec, SkillsInstallPreferences } from "../types.js";
import { installDownloadSpec } from "./install-download.js";
import { formatInstallFailureMessage } from "./install-output.js";
@@ -34,7 +34,7 @@ export type { SkillInstallSkipReason } from "./install-types.js";
type SkillsInstallDeps = {
hasBinary: (bin: string) => boolean;
loadWorkspaceSkillEntries: typeof defaultLoadWorkspaceSkillEntries;
loadWorkspaceSkills: typeof defaultLoadWorkspaceSkills;
resolveNodeInstallStateDir: () => string;
resolveBrewExecutable: () => string | undefined;
isContainerEnvironment: () => boolean;
@@ -43,7 +43,7 @@ type SkillsInstallDeps = {
const defaultSkillsInstallDeps: SkillsInstallDeps = {
hasBinary: defaultHasBinary,
loadWorkspaceSkillEntries: defaultLoadWorkspaceSkillEntries,
loadWorkspaceSkills: defaultLoadWorkspaceSkills,
resolveNodeInstallStateDir: resolveDefaultNodeInstallStateDir,
resolveBrewExecutable: defaultResolveBrewExecutable,
isContainerEnvironment: defaultIsContainerEnvironment,
@@ -680,7 +680,7 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
const timeoutMs = Math.min(Math.max(params.timeoutMs ?? 300_000, 1_000), 900_000);
const workspaceDir = resolveUserPath(params.workspaceDir);
const deps = getSkillsInstallDeps();
const entries = deps.loadWorkspaceSkillEntries(workspaceDir);
const entries = deps.loadWorkspaceSkills(workspaceDir);
const entry = entries.find((item) => item.skill.name === params.skillName);
if (!entry) {
return {
+3 -3
View File
@@ -9,7 +9,7 @@ import {
type SkillsHomeEnvSnapshot,
} from "../test-support/home-env.test-support.js";
import { writeSkill } from "../test-support/test-helpers.js";
import { buildWorkspaceSkillsPrompt } from "./workspace.js";
import { buildSkillSnapshot } from "./workspace-skill-prompt.js";
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
@@ -18,10 +18,10 @@ vi.mock("./plugin-skills.js", () => ({
const tempDirs = createTempDirTracker();
function buildSkillsPrompt(workspaceDir: string, managedDir: string, bundledDir: string): string {
return buildWorkspaceSkillsPrompt(workspaceDir, {
return buildSkillSnapshot(workspaceDir, {
managedSkillsDir: managedDir,
bundledSkillsDir: bundledDir,
});
}).prompt;
}
async function createWorkspaceSkillDirs() {
@@ -1,200 +0,0 @@
// Compact skill path tests cover short path formatting for skill prompt payloads.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withEnv } from "../../test-utils/env.js";
import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js";
import { testing as workspaceSkillsTesting, buildWorkspaceSkillsPrompt } from "./workspace.js";
describe("compactSkillPaths", () => {
function buildPromptForFixtureSkill(params: {
workspaceRoot: string;
skillDir: string;
name: string;
description: string;
}) {
return buildWorkspaceSkillsPrompt(params.workspaceRoot, {
entries: [
{
skill: createCanonicalFixtureSkill({
name: params.name,
description: params.description,
filePath: path.join(params.skillDir, "SKILL.md"),
baseDir: params.skillDir,
source: "test",
}),
frontmatter: {},
metadata: undefined,
invocation: { disableModelInvocation: false, userInvocable: true },
exposure: {
includeInRuntimeRegistry: true,
includeInAvailableSkillsPrompt: true,
userInvocable: true,
},
},
],
});
}
it("replaces home directory prefix with ~ in skill locations", () => {
const home = os.homedir();
const skillDir = path.join(home, ".openclaw-test-skills", "test-skill");
const prompt = buildPromptForFixtureSkill({
workspaceRoot: home,
skillDir,
name: "test-skill",
description: "A test skill for path compaction",
});
expect(prompt).not.toContain(home + path.sep);
expect(prompt).toContain("~/");
expect(prompt).toContain("test-skill");
expect(prompt).toContain("A test skill for path compaction");
});
it("does not compact explicit state-root managed skill paths to OS-home tilde paths", () => {
const root = path.parse(os.homedir()).root;
const osHome = path.join(root, "data");
const stateDir = path.join(osHome, ".openclaw");
const skillDir = path.join(stateDir, "skills", "world-cup-soccer-openclaw-skill");
const skillFile = path.join(skillDir, "SKILL.md");
const prompt = withEnv(
{
HOME: osHome,
OPENCLAW_HOME: osHome,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
},
() =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(root, "workspace"),
skillDir,
name: "world-cup-soccer-openclaw-skill",
description: "World Cup standings lookup",
}),
);
expect(prompt).toContain(`<location>${skillFile}</location>`);
expect(prompt).not.toContain("~/.openclaw/skills/world-cup-soccer-openclaw-skill/SKILL.md");
});
it("does not compact explicit state-root plugin skill paths to OS-home tilde paths", () => {
const root = path.parse(os.homedir()).root;
const osHome = path.join(root, "data");
const stateDir = path.join(osHome, ".openclaw");
const skillDir = path.join(stateDir, "plugin-skills", "calendar-plugin-skill");
const skillFile = path.join(skillDir, "SKILL.md");
const prompt = withEnv(
{
HOME: osHome,
OPENCLAW_HOME: osHome,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
},
() =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(root, "workspace"),
skillDir,
name: "calendar-plugin-skill",
description: "Calendar plugin skill",
}),
);
expect(prompt).toContain(`<location>${skillFile}</location>`);
expect(prompt).not.toContain("~/.openclaw/plugin-skills/calendar-plugin-skill/SKILL.md");
});
it("compacts managed skill paths when OS-home tilde reaches the same path", () => {
const home = os.homedir();
const stateDir = path.join(home, ".openclaw");
const skillDir = path.join(stateDir, "skills", "home-managed-skill");
const prompt = withEnv(
{
HOME: home,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_HOME: undefined,
},
() =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(home, "workspace"),
skillDir,
name: "home-managed-skill",
description: "Home managed skill",
}),
);
expect(prompt).toContain("<location>~/.openclaw/skills/home-managed-skill/SKILL.md</location>");
expect(prompt).not.toContain(`<location>${path.join(skillDir, "SKILL.md")}</location>`);
});
it("normalizes compacted Windows skill locations to forward slashes", () => {
const home = "C:\\Users\\alice";
const skillPath = path.win32.join(home, ".openclaw-test-skills", "win-skill", "SKILL.md");
const compactedPath = workspaceSkillsTesting.compactHomePath(skillPath, [home]);
expect(compactedPath).toBe("~/.openclaw-test-skills/win-skill/SKILL.md");
});
it("preserves POSIX literal backslashes after home compaction", () => {
const home = os.homedir();
const skillDir = path.join(home, ".openclaw-test-skills\\literal-skill");
const prompt = buildPromptForFixtureSkill({
workspaceRoot: home,
skillDir,
name: "literal-skill",
description: "POSIX literal backslash skill",
});
const locationMatch = prompt.match(/<location>([^<]+)<\/location>/);
if (!locationMatch) {
throw new Error("expected prompt location tag");
}
expect(locationMatch[1]).toContain("~/");
expect(locationMatch[1]).toContain("\\literal-skill");
});
it("preserves paths outside home directory", () => {
const outsideHome = path.join(path.parse(os.homedir()).root, "openclaw-external-skills");
const skillDir = path.join(outsideHome, "skills", "ext-skill");
const prompt = buildPromptForFixtureSkill({
workspaceRoot: outsideHome,
skillDir,
name: "ext-skill",
description: "External skill",
});
expect(prompt).toMatch(/<location>[^<]+SKILL\.md<\/location>/);
expect(prompt).toContain(path.join(skillDir, "SKILL.md"));
});
it("loads skills when the shared state database is unavailable", () => {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-skill-load-")));
const blockedParent = path.join(root, "state-blocker");
fs.writeFileSync(blockedParent, "not a directory", "utf8");
const skillDir = path.join(root, "workspace", "skills", "available-skill");
try {
const prompt = withEnv({ OPENCLAW_STATE_DIR: path.join(blockedParent, "state") }, () =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(root, "workspace"),
skillDir,
name: "available-skill",
description: "Available despite missing lifecycle state",
}),
);
expect(prompt).toContain("available-skill");
expect(prompt).toContain("Available despite missing lifecycle state");
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
});
@@ -1,194 +0,0 @@
// Plugin sandbox sync tests cover syncing plugin-provided skills into sandbox state.
import fs from "node:fs";
import fsPromises from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import { buildWorkspaceSkillsPrompt, syncSkillsToWorkspace } from "./workspace.js";
// Mock resolvePluginSkillDirs to return our test plugin skill directories
const mockResolvePluginSkillDirs = vi.hoisted(() => vi.fn(() => [] as string[]));
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: mockResolvePluginSkillDirs,
}));
let fixtureRoot = "";
let fixtureCount = 0;
async function createCaseDir(prefix: string): Promise<string> {
const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`);
await fsPromises.mkdir(dir, { recursive: true });
return dir;
}
async function pathExists(filePath: string): Promise<boolean> {
try {
await fsPromises.access(filePath);
return true;
} catch {
return false;
}
}
beforeAll(async () => {
fixtureRoot = await fsPromises.realpath(
await fsPromises.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-skills-sync-")),
);
});
afterAll(async () => {
await fsPromises.rm(fixtureRoot, { recursive: true, force: true });
});
describe("syncSkillsToWorkspace for plugin skills", () => {
it("syncs plugin skills from symlinked directories to sandbox workspace", async () => {
const sourceWorkspace = await createCaseDir("source");
const targetWorkspace = await createCaseDir("target");
const realPluginSkillDir = await createCaseDir("real-plugin-skill");
await writeSkill({
dir: realPluginSkillDir,
name: "wiki-maintainer",
description: "Wiki maintenance skill for sandboxed agents",
});
const pluginSkillsDir = path.join(sourceWorkspace, ".openclaw", "plugin-skills");
await fsPromises.mkdir(pluginSkillsDir, { recursive: true });
const symlinkPath = path.join(pluginSkillsDir, "wiki-maintainer");
fs.symlinkSync(
realPluginSkillDir,
symlinkPath,
process.platform === "win32" ? "junction" : "dir",
);
mockResolvePluginSkillDirs.mockReturnValueOnce([realPluginSkillDir]);
const skillUsagePaths = await syncSkillsToWorkspace({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
pluginSkillsDir,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
managedSkillsDir: path.join(sourceWorkspace, ".managed"),
});
const syncedSkillDir = path.join(targetWorkspace, "skills", "wiki-maintainer");
const syncedSkillMd = path.join(syncedSkillDir, "SKILL.md");
const syncedStat = await fsPromises.lstat(syncedSkillDir);
const prompt = buildWorkspaceSkillsPrompt(targetWorkspace, {
bundledSkillsDir: path.join(targetWorkspace, ".bundled"),
managedSkillsDir: path.join(targetWorkspace, ".managed"),
}).replaceAll("\\", "/");
expect(await pathExists(syncedSkillMd)).toBe(true);
expect(syncedStat.isSymbolicLink()).toBe(false);
expect(prompt).toContain("Wiki maintenance skill for sandboxed agents");
expect(prompt).toContain("skills/wiki-maintainer/SKILL.md");
expect(prompt).not.toContain(realPluginSkillDir.replaceAll("\\", "/"));
expect(prompt).not.toContain(pluginSkillsDir.replaceAll("\\", "/"));
expect(prompt).not.toContain(symlinkPath.replaceAll("\\", "/"));
expect(skillUsagePaths).toEqual([
{
readPath: syncedSkillMd,
skillFile: path.join(realPluginSkillDir, "SKILL.md"),
skillName: "wiki-maintainer",
skillSource: "workspace",
},
]);
});
it("syncs multiple plugin skills directories to sandbox workspace", async () => {
const sourceWorkspace = await createCaseDir("source-multi");
const targetWorkspace = await createCaseDir("target-multi");
// Create multiple real plugin skill directories
const realSkillA = await createCaseDir("skill-a");
await writeSkill({
dir: realSkillA,
name: "browser-automation",
description: "Browser automation skill",
});
const realSkillB = await createCaseDir("skill-b");
await writeSkill({
dir: realSkillB,
name: "obsidian-vault",
description: "Obsidian vault maintenance skill",
});
// Create plugin-skills directory with symlinks
const pluginSkillsDir = path.join(sourceWorkspace, ".openclaw", "plugin-skills");
await fsPromises.mkdir(pluginSkillsDir, { recursive: true });
fs.symlinkSync(
realSkillA,
path.join(pluginSkillsDir, "browser-automation"),
process.platform === "win32" ? "junction" : "dir",
);
fs.symlinkSync(
realSkillB,
path.join(pluginSkillsDir, "obsidian-vault"),
process.platform === "win32" ? "junction" : "dir",
);
mockResolvePluginSkillDirs.mockReturnValueOnce([realSkillA, realSkillB]);
await syncSkillsToWorkspace({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
pluginSkillsDir,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
managedSkillsDir: path.join(sourceWorkspace, ".managed"),
});
// Both skills should be synced
expect(
await pathExists(path.join(targetWorkspace, "skills", "browser-automation", "SKILL.md")),
).toBe(true);
expect(
await pathExists(path.join(targetWorkspace, "skills", "obsidian-vault", "SKILL.md")),
).toBe(true);
});
it("does not sync plugin skills that escape allowed root", async () => {
const sourceWorkspace = await createCaseDir("source-escape");
const targetWorkspace = await createCaseDir("target-escape");
// Create a skill outside any allowed root
const outsideRoot = await createCaseDir("outside-root");
const escapedSkillDir = path.join(outsideRoot, "escaped-skill");
await writeSkill({
dir: escapedSkillDir,
name: "escaped-skill",
description: "Should not be synced",
});
// Create plugin-skills with symlink to escaped skill
const pluginSkillsDir = path.join(sourceWorkspace, ".openclaw", "plugin-skills");
await fsPromises.mkdir(pluginSkillsDir, { recursive: true });
fs.symlinkSync(
escapedSkillDir,
path.join(pluginSkillsDir, "escaped-skill"),
process.platform === "win32" ? "junction" : "dir",
);
// Mock returns an allowed root that doesn't include the escaped skill
const allowedRoot = await createCaseDir("allowed-root");
mockResolvePluginSkillDirs.mockReturnValueOnce([allowedRoot]);
await syncSkillsToWorkspace({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
pluginSkillsDir,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
managedSkillsDir: path.join(sourceWorkspace, ".managed"),
});
// Escaped skill should NOT be synced
expect(
await pathExists(path.join(targetWorkspace, "skills", "escaped-skill", "SKILL.md")),
).toBe(false);
});
});
+96
View File
@@ -0,0 +1,96 @@
// Skill contract tests cover full and compact catalog serialization.
import { formatSkillsForPrompt as upstreamFormatSkillsForPrompt } from "openclaw/plugin-sdk/agent-sessions";
import { describe, expect, it } from "vitest";
import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js";
import { formatSkillsForPrompt, type Skill } from "./skill-contract.js";
import { formatSkillsCompactForPrompt as formatSkillsCompact } from "./skill-contract.js";
function makeSkill(name: string, desc = "A skill", filePath = `/skills/${name}/SKILL.md`): Skill {
return createCanonicalFixtureSkill({
name,
description: desc,
filePath,
baseDir: `/skills/${name}`,
source: "workspace",
});
}
describe("formatSkillsCompact", () => {
it("keeps the full-format XML output aligned with the upstream formatter for visible skills", () => {
const skills = [
{ ...makeSkill("weather", "Get weather <data> & forecasts"), promptVersion: "sha256:abc123" },
makeSkill("notes", "Summarize notes", "/tmp/notes/SKILL.md"),
];
expect(formatSkillsForPrompt(skills)).toBe(upstreamFormatSkillsForPrompt(skills));
});
it("renders all passed skills in the full formatter without reapplying visibility policy", () => {
const hidden: Skill = { ...makeSkill("hidden"), disableModelInvocation: true };
const out = formatSkillsForPrompt([makeSkill("visible"), hidden]);
expect(out).toContain("visible");
expect(out).toContain("hidden");
});
it("returns empty string for no skills", () => {
expect(formatSkillsCompact([])).toBe("");
});
it("keeps compact descriptions with name, location, and version", () => {
const out = formatSkillsCompact([
{ ...makeSkill("weather", "Get weather data"), promptVersion: "sha256:abc123" },
]);
expect(out).toContain("<name>weather</name>");
expect(out).toContain("<description>Get weather data</description>");
expect(out).toContain("<location>/skills/weather/SKILL.md</location>");
expect(out).toContain("<version>sha256:abc123</version>");
});
it("omits descriptions when their compact budget is zero", () => {
const out = formatSkillsCompact([makeSkill("weather", "Get weather data")], {
descriptionMaxChars: 0,
});
expect(out).toContain("<name>weather</name>");
expect(out).not.toContain("<description>");
});
it("preserves location notes when compact descriptions are omitted", () => {
const out = formatSkillsCompact(
[
{
...makeSkill("remote", "Remote skill"),
locationNote: "Load with exec host=node node=node-1.",
},
],
{ descriptionMaxChars: 0 },
);
expect(out).toContain("<location_note>Load with exec host=node node=node-1.</location_note>");
});
it("truncates descriptions without splitting emoji surrogate pairs", () => {
const out = formatSkillsCompact([makeSkill("emoji", `${"A".repeat(16)}😀 trailing`)], {
descriptionMaxChars: 20,
});
expect(out).toContain(`<description>${"A".repeat(16)}...</description>`);
expect(out).not.toMatch(/[\uD800-\uDFFF]/u);
});
it("renders all passed skills without reapplying visibility policy", () => {
const hidden: Skill = { ...makeSkill("hidden"), disableModelInvocation: true };
const out = formatSkillsCompact([makeSkill("visible"), hidden]);
expect(out).toContain("visible");
expect(out).toContain("hidden");
});
it("escapes XML special characters", () => {
const out = formatSkillsCompact([makeSkill("a<b&c")]);
expect(out).toContain("a&lt;b&amp;c");
});
it("is significantly smaller than full format", () => {
const skills = Array.from({ length: 50 }, (_, i) => makeSkill(`skill-${i}`, "A".repeat(800)));
const compact = formatSkillsCompact(skills);
expect(compact.length).toBeLessThan(formatSkillsForPrompt(skills).length / 2);
});
});
+64 -6
View File
@@ -1,3 +1,4 @@
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
// Skill contract types describe loaded skill metadata, sources, and prompt surfaces.
import type { SourceInfo } from "../../agents/sessions/source-info.js";
@@ -20,7 +21,7 @@ export interface Skill {
export { createSyntheticSourceInfo } from "../../agents/sessions/source-info.js";
function escapeXml(str: string): string {
export function escapeSkillXml(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
@@ -29,6 +30,19 @@ function escapeXml(str: string): string {
.replace(/'/g, "&apos;");
}
const COMPACT_DESCRIPTION_MAX_CHARS = 220;
function truncateSkillDescription(description: string, maxChars: number): string {
const normalized = description.replace(/\s+/g, " ").trim();
if (normalized.length <= maxChars) {
return normalized;
}
if (maxChars <= 3) {
return truncateUtf16Safe(normalized, maxChars);
}
return `${truncateUtf16Safe(normalized, maxChars - 3).trimEnd()}...`;
}
/**
* Keep this formatter's XML layout byte-for-byte aligned with the upstream
* Agent Skills formatter so we can avoid importing the full session runtime
@@ -49,14 +63,58 @@ export function formatSkillsForPrompt(skills: Skill[]): string {
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeXml(skill.name)}</name>`);
lines.push(` <description>${escapeXml(skill.description)}</description>`);
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
lines.push(` <name>${escapeSkillXml(skill.name)}</name>`);
lines.push(` <description>${escapeSkillXml(skill.description)}</description>`);
lines.push(` <location>${escapeSkillXml(skill.filePath)}</location>`);
if (skill.locationNote) {
lines.push(` <location_note>${escapeXml(skill.locationNote)}</location_note>`);
lines.push(` <location_note>${escapeSkillXml(skill.locationNote)}</location_note>`);
}
if (skill.promptVersion) {
lines.push(` <version>${escapeXml(skill.promptVersion)}</version>`);
lines.push(` <version>${escapeSkillXml(skill.promptVersion)}</version>`);
}
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}
/** Compact prompt catalog with descriptions bounded independently from identities. */
export function formatSkillsCompactForPrompt(
skills: Skill[],
opts?: { descriptionMaxChars?: number },
): string {
if (skills.length === 0) {
return "";
}
const descriptionMaxChars = Math.max(
0,
Math.floor(opts?.descriptionMaxChars ?? COMPACT_DESCRIPTION_MAX_CHARS),
);
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
descriptionMaxChars > 0
? "Use the read tool to load a skill's file when the task matches its name or description."
: "Use the read tool to load a skill's file when the task matches its name.",
"If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.",
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
"",
"<available_skills>",
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeSkillXml(skill.name)}</name>`);
if (descriptionMaxChars > 0) {
const description = truncateSkillDescription(skill.description, descriptionMaxChars);
if (description) {
lines.push(` <description>${escapeSkillXml(description)}</description>`);
}
}
lines.push(` <location>${escapeSkillXml(skill.filePath)}</location>`);
if (skill.locationNote) {
lines.push(` <location_note>${escapeSkillXml(skill.locationNote)}</location_note>`);
}
if (skill.promptVersion) {
lines.push(` <version>${escapeSkillXml(skill.promptVersion)}</version>`);
}
lines.push(" </skill>");
}
@@ -0,0 +1,376 @@
// Skill path containment tests cover root escapes, allowed symlinks, and diagnostics.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { resetLogger, setLoggerOverride } from "../../logging/logger.js";
import { loggingState } from "../../logging/state.js";
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import {
restoreMockSkillsHomeEnv,
setMockSkillsHomeEnv,
type SkillsHomeEnvSnapshot,
} from "../test-support/home-env.test-support.js";
import { readSkillFrontmatterSafe } from "./local-loader.js";
import { loadWorkspaceSkills } from "./workspace-skill-loader.js";
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
}));
let fakeHome = "";
let envSnapshot: SkillsHomeEnvSnapshot;
let tempRoot = "";
let workspaceCaseIndex = 0;
async function createTempWorkspaceDir() {
const workspaceDir = path.join(tempRoot, `workspace-${++workspaceCaseIndex}`);
await fs.mkdir(workspaceDir, { recursive: true });
return workspaceDir;
}
function captureWarningLogger() {
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
const warn = vi.fn();
loggingState.rawConsole = {
log: vi.fn(),
info: vi.fn(),
warn,
error: vi.fn(),
};
return warn;
}
function firstWarningLine(warn: ReturnType<typeof vi.fn>): string {
const [line] = warn.mock.calls[0] ?? [];
return String(line);
}
function loadTestWorkspaceSkills(
workspaceDir: string,
opts?: Parameters<typeof loadWorkspaceSkills>[1],
) {
return loadWorkspaceSkills(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir: "",
pluginSkillsDir: path.join(workspaceDir, ".plugin-skills"),
...opts,
});
}
async function createEscapedBundledSkillFixture(params?: {
workspaceDir?: string;
outsideDir?: string;
}) {
const workspaceDir = params?.workspaceDir ?? (await createTempWorkspaceDir());
const outsideDir = params?.outsideDir ?? (await createTempWorkspaceDir());
const bundledDir = path.join(workspaceDir, ".bundled");
const escapedSkillDir = path.join(outsideDir, "outside-bundled-skill");
await writeSkill({
dir: escapedSkillDir,
name: "outside-bundled-skill",
description: "Outside bundled",
});
await fs.mkdir(bundledDir, { recursive: true });
const requestedPath = path.join(bundledDir, "escaped-bundled-skill");
await fs.symlink(escapedSkillDir, requestedPath, "dir");
return { workspaceDir, outsideDir, bundledDir, escapedSkillDir, requestedPath };
}
beforeAll(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skills-containment-"));
fakeHome = path.join(tempRoot, "home");
await fs.mkdir(fakeHome, { recursive: true });
envSnapshot = setMockSkillsHomeEnv(fakeHome);
});
afterEach(() => {
setLoggerOverride(null);
loggingState.rawConsole = null;
resetLogger();
});
afterAll(async () => {
await restoreMockSkillsHomeEnv(envSnapshot, async () => {
await fs.rm(tempRoot, { recursive: true, force: true });
});
});
describe("skill path containment", () => {
it.runIf(process.platform !== "win32")(
"skips workspace skill paths that resolve outside the workspace root",
async () => {
const workspaceDir = await createTempWorkspaceDir();
const outsideDir = await createTempWorkspaceDir();
const escapedSkillDir = path.join(outsideDir, "outside-skill");
await writeSkill({
dir: escapedSkillDir,
name: "outside-skill",
description: "Outside",
});
await fs.mkdir(path.join(workspaceDir, "skills"), { recursive: true });
const requestedPath = path.join(workspaceDir, "skills", "escaped-skill");
await fs.symlink(escapedSkillDir, requestedPath, "dir");
const fileLinkSkillDir = path.join(workspaceDir, "skills", "escaped-file");
await fs.mkdir(fileLinkSkillDir, { recursive: true });
await fs.symlink(path.join(outsideDir, "SKILL.md"), path.join(fileLinkSkillDir, "SKILL.md"));
const targetDir = path.join(workspaceDir, "safe-target");
await writeSkill({
dir: targetDir,
name: "symlink-target",
description: "Target skill",
});
const symlinkedSkillDir = path.join(workspaceDir, "skills", "symlinked");
await fs.mkdir(symlinkedSkillDir, { recursive: true });
await fs.symlink(path.join(targetDir, "SKILL.md"), path.join(symlinkedSkillDir, "SKILL.md"));
const warn = captureWarningLogger();
const entries = loadTestWorkspaceSkills(workspaceDir);
expect(entries.map((entry) => entry.skill.name)).not.toContain("outside-skill");
expect(entries.map((entry) => entry.skill.name)).not.toContain("outside-file-skill");
expect(entries.map((entry) => entry.skill.name)).not.toContain("symlink-target");
const warningLine = firstWarningLine(warn);
expect(warningLine).toContain("Skipping escaped skill path outside its configured root:");
expect(warningLine).toContain("reason=symlink-escape");
expect(warningLine).toContain("source=openclaw-workspace");
expect(warningLine).toContain(`root=${path.join(workspaceDir, "skills")}`);
expect(warningLine).toContain(`requested=${requestedPath}`);
expect(warningLine).toContain("resolved=");
},
);
it.runIf(process.platform !== "win32")(
"allows configured skill symlink targets outside their source root",
async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillName = `manager-${++workspaceCaseIndex}`;
const targetRoot = path.join(tempRoot, `${skillName}-skills`);
const targetSkillDir = path.join(targetRoot, skillName);
await writeSkill({
dir: targetSkillDir,
name: skillName,
description: "Manager skill",
});
const workspaceSkillsDir = path.join(workspaceDir, "skills");
await fs.mkdir(workspaceSkillsDir, { recursive: true });
const symlinkPath = path.join(workspaceSkillsDir, skillName);
await fs.symlink(targetSkillDir, symlinkPath, "dir");
const warn = captureWarningLogger();
try {
const entries = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: {
allowSymlinkTargets: [targetRoot],
},
},
},
});
expect(entries.map((entry) => entry.skill.name)).toContain(skillName);
expect(warn).not.toHaveBeenCalled();
} finally {
await fs.unlink(symlinkPath).catch(() => undefined);
}
},
);
it.runIf(process.platform !== "win32")(
"loads managed skill directory symlinks outside the managed root",
async () => {
const workspaceDir = await createTempWorkspaceDir();
const managedDir = path.join(workspaceDir, ".managed");
const skillName = `managed-${++workspaceCaseIndex}`;
const targetSkillDir = path.join(tempRoot, `${skillName}-target`, skillName);
await writeSkill({
dir: targetSkillDir,
name: skillName,
description: "Managed symlink target",
});
await fs.mkdir(managedDir, { recursive: true });
const symlinkPath = path.join(managedDir, skillName);
await fs.symlink(targetSkillDir, symlinkPath, "dir");
const warn = captureWarningLogger();
try {
const entries = loadTestWorkspaceSkills(workspaceDir, {
managedSkillsDir: managedDir,
});
expect(entries.map((entry) => entry.skill.name)).toContain(skillName);
expect(warn).not.toHaveBeenCalled();
} finally {
await fs.unlink(symlinkPath).catch(() => undefined);
}
},
);
it.runIf(process.platform !== "win32")(
"keeps SKILL.md containment for managed symlinked skill directories",
async () => {
const workspaceDir = await createTempWorkspaceDir();
const managedDir = path.join(workspaceDir, ".managed");
const skillName = `managed-file-link-${++workspaceCaseIndex}`;
const targetSkillDir = path.join(tempRoot, `${skillName}-target`, skillName);
const outsideDir = path.join(tempRoot, `${skillName}-outside`);
await fs.mkdir(targetSkillDir, { recursive: true });
await fs.mkdir(outsideDir, { recursive: true });
await writeSkill({
dir: outsideDir,
name: skillName,
description: "Escaped metadata",
});
await fs.symlink(path.join(outsideDir, "SKILL.md"), path.join(targetSkillDir, "SKILL.md"));
await fs.mkdir(managedDir, { recursive: true });
const symlinkPath = path.join(managedDir, skillName);
await fs.symlink(targetSkillDir, symlinkPath, "dir");
const warn = captureWarningLogger();
try {
const entries = loadTestWorkspaceSkills(workspaceDir, {
managedSkillsDir: managedDir,
});
expect(entries.map((entry) => entry.skill.name)).not.toContain(skillName);
const warningLine = firstWarningLine(warn);
expect(warningLine).toContain("Skipping escaped skill path outside its configured root:");
expect(warningLine).toContain("source=openclaw-managed");
expect(warningLine).toContain("reason=symlink-escape");
} finally {
await fs.unlink(symlinkPath).catch(() => undefined);
}
},
);
it.runIf(process.platform !== "win32")(
"calls out bundled symlink escapes with compact home-relative paths",
async () => {
const { workspaceDir, bundledDir, requestedPath } = await createEscapedBundledSkillFixture();
const warn = captureWarningLogger();
const entries = loadTestWorkspaceSkills(workspaceDir, {
bundledSkillsDir: bundledDir,
});
expect(entries.map((entry) => entry.skill.name)).not.toContain("outside-bundled-skill");
const warningLine = firstWarningLine(warn);
expect(warningLine).toContain("Skipping escaped skill path outside its configured root:");
expect(warningLine).toContain("source=openclaw-bundled");
expect(warningLine).toContain("reason=bundled-symlink-escape");
expect(warningLine).toContain("hint=likely-stray-local-symlink-or-checkout-mutation");
expect(warningLine).toContain(`requested=${requestedPath}`);
expect(warningLine).toContain("resolved=");
},
);
it.runIf(process.platform !== "win32")(
"uses compact home-relative paths in escaped skill console warnings",
async () => {
const { workspaceDir, bundledDir } = await createEscapedBundledSkillFixture({
workspaceDir: path.join(fakeHome, "workspace"),
outsideDir: path.join(fakeHome, "outside"),
});
const warn = captureWarningLogger();
loadTestWorkspaceSkills(workspaceDir, {
bundledSkillsDir: bundledDir,
});
const warningLine = firstWarningLine(warn);
expect(warningLine).toContain("root=~/workspace/.bundled");
expect(warningLine).toContain("requested=~/workspace/.bundled/escaped-bundled-skill");
expect(warningLine).toContain("resolved=~/outside/outside-bundled-skill");
},
);
it.runIf(process.platform !== "win32")(
"reads skill frontmatter when the allowed root is the filesystem root",
async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillDir = path.join(workspaceDir, "skills", "root-allowed");
await writeSkill({
dir: skillDir,
name: "root-allowed",
description: "Readable from filesystem root",
});
const frontmatter = readSkillFrontmatterSafe({
rootDir: path.parse(skillDir).root,
filePath: path.join(skillDir, "SKILL.md"),
});
expect(frontmatter?.name).toBe("root-allowed");
expect(frontmatter?.description).toBe("Readable from filesystem root");
},
);
it.runIf(process.platform !== "win32")(
"does not follow outside symlink dirs during repo-root detection",
async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
const outsideDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(outsideDir, "linked"),
name: "outside-linked-skill",
description: "Outside linked skill",
});
await fs.mkdir(path.join(repoDir, "examples"), { recursive: true });
await fs.symlink(outsideDir, path.join(repoDir, "examples", "linked"), "dir");
await writeSkill({
dir: path.join(repoDir, "skills", "group", "valid"),
name: "repo-nested-skill",
description: "Valid nested repo skill",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("repo-nested-skill");
expect(names).not.toContain("outside-linked-skill");
},
);
it.runIf(process.platform !== "win32")(
"keeps configured roots with possible symlink skills outside nested skills",
async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
const targetRoot = path.join(tempRoot, `linked-root-${workspaceCaseIndex++}`);
const targetSkillDir = path.join(targetRoot, "linked-skill");
await writeSkill({
dir: targetSkillDir,
name: "linked-skill",
description: "Allowed linked skill",
});
await fs.mkdir(path.join(repoDir, "group"), { recursive: true });
await fs.symlink(targetSkillDir, path.join(repoDir, "group", "linked-skill"), "dir");
await writeSkill({
dir: path.join(repoDir, "skills", "group", "valid"),
name: "repo-nested-skill",
description: "Valid nested repo skill",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: {
allowSymlinkTargets: [targetRoot],
extraDirs: [repoDir],
},
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("linked-skill");
expect(names).toContain("repo-nested-skill");
},
);
});
+127
View File
@@ -0,0 +1,127 @@
// Skill path helpers keep prompt and diagnostic paths compact without changing their meaning.
import os from "node:os";
import path from "node:path";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { resolveOsHomeDir } from "../../infra/home-dir.js";
import { isPathInside } from "../../infra/path-guards.js";
import { resolveConfigDir } from "../../utils.js";
import type { Skill } from "./skill-contract.js";
import { tryRealpath } from "./symlink-targets.js";
/** Resolve the effective user home used by skill discovery. */
export function resolveSkillsUserHomeDir(): string | undefined {
return resolveOsHomeDir(process.env, os.homedir);
}
function resolveNativeUserHomeDir(): string | undefined {
try {
return path.resolve(os.homedir());
} catch {
return undefined;
}
}
function resolveCompactHomePrefixes(): string[] {
const homes = [resolveSkillsUserHomeDir(), resolveNativeUserHomeDir()].filter(
(home): home is string => Boolean(home),
);
const resolvedHomes = homes.map((home) => path.resolve(home));
const realHomes = resolvedHomes
.map((home) => tryRealpath(home))
.filter((home): home is string => Boolean(home));
return uniqueStrings([...resolvedHomes, ...realHomes]).toSorted((a, b) => b.length - a.length);
}
/** Compact prompt-facing skill paths while preserving managed paths that `~` cannot reach. */
export function compactPromptSkills(skills: Skill[]): Skill[] {
const homes = resolveCompactHomePrefixes();
if (homes.length === 0) {
return skills;
}
const preservedRoots = resolvePreservedPromptSkillPathRoots();
const tildeRoots = resolvePromptTildeRoots();
return skills.map((skill) => ({
...skill,
filePath: shouldPreservePromptSkillPath(skill.filePath, preservedRoots, tildeRoots)
? skill.filePath
: compactHomePath(skill.filePath, homes),
}));
}
function resolvePreservedPromptSkillPathRoots(): string[] {
const configDir = resolveConfigDir();
const promptSkillDirs = [
path.resolve(configDir, "skills"),
path.resolve(configDir, "plugin-skills"),
];
const realPromptSkillDirs = promptSkillDirs
.map((dir) => tryRealpath(dir))
.filter((dir): dir is string => Boolean(dir));
return uniqueStrings([...promptSkillDirs, ...realPromptSkillDirs]);
}
function resolvePromptTildeRoots(): string[] {
const nativeHome = resolveNativeUserHomeDir();
if (!nativeHome) {
return [];
}
const resolvedNativeHome = path.resolve(nativeHome);
if (isContainerStateHomeWherePromptTildeEscapes(resolvedNativeHome)) {
return [];
}
const realNativeHome = tryRealpath(resolvedNativeHome);
return uniqueStrings([resolvedNativeHome, ...(realNativeHome ? [realNativeHome] : [])]);
}
function isContainerStateHomeWherePromptTildeEscapes(home: string): boolean {
const configDir = path.resolve(resolveConfigDir());
return (
home === "/data" &&
(configDir === "/data/.openclaw" || isPathInside("/data/.openclaw", configDir))
);
}
function shouldPreservePromptSkillPath(
filePath: string,
roots: readonly string[],
tildeRoots: readonly string[],
): boolean {
const resolvedFilePath = path.resolve(filePath);
const isManagedPromptSkillPath = roots.some(
(root) => resolvedFilePath === root || isPathInside(root, resolvedFilePath),
);
if (!isManagedPromptSkillPath) {
return false;
}
return !tildeRoots.some(
(root) => resolvedFilePath === root || isPathInside(root, resolvedFilePath),
);
}
function compactHomePath(filePath: string, homes: readonly string[]): string {
for (const home of homes) {
for (const prefix of compactHomePrefixesForHome(home)) {
if (filePath.startsWith(prefix)) {
return "~/" + normalizeCompactedSkillPath(filePath.slice(prefix.length), prefix);
}
}
}
return filePath;
}
function compactHomePrefixesForHome(home: string): string[] {
const prefixes = [home.endsWith(path.sep) ? home : home + path.sep];
if (home.includes("\\") && !home.endsWith("\\")) {
prefixes.push(home + "\\");
}
return prefixes;
}
function normalizeCompactedSkillPath(filePath: string, matchedHomePrefix: string): string {
return matchedHomePrefix.includes("\\") ? filePath.replace(/\\/g, "/") : filePath;
}
/** Compact a skill path for console diagnostics. */
export function compactSkillPath(filePath: string): string {
return compactHomePath(filePath, resolveCompactHomePrefixes());
}
@@ -0,0 +1,744 @@
// Skill root discovery tests cover bounded recursive scanning and nested repo-style roots.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { resetLogger, setLoggerOverride } from "../../logging/logger.js";
import { loggingState } from "../../logging/state.js";
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import { loadWorkspaceSkills } from "./workspace-skill-loader.js";
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
}));
let tempRoot = "";
let workspaceCaseIndex = 0;
async function createTempWorkspaceDir() {
const workspaceDir = path.join(tempRoot, `workspace-${++workspaceCaseIndex}`);
await fs.mkdir(workspaceDir, { recursive: true });
return workspaceDir;
}
function captureWarningLogger() {
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
const warn = vi.fn();
loggingState.rawConsole = {
log: vi.fn(),
info: vi.fn(),
warn,
error: vi.fn(),
};
return warn;
}
function collectMatching<T>(items: readonly T[], predicate: (item: T) => boolean): T[] {
const matches: T[] = [];
for (const item of items) {
if (predicate(item)) {
matches.push(item);
}
}
return matches;
}
function loadTestWorkspaceSkills(
workspaceDir: string,
opts?: Parameters<typeof loadWorkspaceSkills>[1],
) {
return loadWorkspaceSkills(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir: "",
pluginSkillsDir: path.join(workspaceDir, ".plugin-skills"),
...opts,
});
}
beforeAll(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skills-discovery-"));
});
afterEach(() => {
setLoggerOverride(null);
loggingState.rawConsole = null;
resetLogger();
});
afterAll(async () => {
await fs.rm(tempRoot, { recursive: true, force: true });
});
describe("discoverSkillCandidates", () => {
it("discovers SKILL.md two levels deep under a grouping subfolder", async () => {
const workspaceDir = await createTempWorkspaceDir();
// Grouped layout: skills/group/skill/SKILL.md (no SKILL.md at skills/group/).
await writeSkill({
dir: path.join(workspaceDir, "skills", "group", "nested-skill"),
name: "nested-skill",
description: "Nested under a group folder",
});
const entries = loadTestWorkspaceSkills(workspaceDir);
const names = entries.map((entry) => entry.skill.name);
expect(names).toContain("nested-skill");
});
it("keeps loading direct skills (skills/skill/SKILL.md) unchanged", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "direct-skill"),
name: "direct-skill",
description: "Direct skill at first level",
});
// Sibling group with a deeper skill.
await writeSkill({
dir: path.join(workspaceDir, "skills", "group", "grouped-skill"),
name: "grouped-skill",
description: "Skill nested under a group",
});
const names = loadTestWorkspaceSkills(workspaceDir).map((entry) => entry.skill.name);
expect(names).toContain("direct-skill");
expect(names).toContain("grouped-skill");
});
it("does not count invalid grouped candidates against the loaded skill cap", async () => {
const workspaceDir = await createTempWorkspaceDir();
for (const nestedName of ["a", "b"]) {
const invalidDir = path.join(workspaceDir, "skills", "00-group", nestedName);
await fs.mkdir(invalidDir, { recursive: true });
await fs.writeFile(
path.join(invalidDir, "SKILL.md"),
`---\nname: ${nestedName}\n---\n\n# Invalid\n`,
"utf-8",
);
}
await writeSkill({
dir: path.join(workspaceDir, "skills", "01-valid"),
name: "valid-skill",
description: "Valid sibling after invalid grouped candidates",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
limits: {
maxCandidatesPerRoot: 10,
maxSkillsLoadedPerSource: 1,
},
},
},
}).map((entry) => entry.skill.name);
expect(names).toEqual(["valid-skill"]);
});
it("loads earlier grouped skills before later direct siblings hit the source cap", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "00-group", "grouped"),
name: "grouped-skill",
description: "Grouped skill before direct siblings",
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "01-direct"),
name: "direct-skill",
description: "Direct sibling after grouped skill",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
limits: {
maxCandidatesPerRoot: 10,
maxSkillsLoadedPerSource: 1,
},
},
},
}).map((entry) => entry.skill.name);
expect(names).toEqual(["grouped-skill"]);
});
it("keeps later grouped siblings discoverable when an earlier group is noisy", async () => {
const workspaceDir = await createTempWorkspaceDir();
async function createNoisyTree(dir: string, depth: number): Promise<void> {
if (depth === 0) {
return;
}
for (const name of ["00-a", "01-b"]) {
const childDir = path.join(dir, name);
await fs.mkdir(childDir, { recursive: true });
await createNoisyTree(childDir, depth - 1);
}
}
await createNoisyTree(path.join(workspaceDir, "skills", "00-noisy"), 6);
await writeSkill({
dir: path.join(workspaceDir, "skills", "01-later", "later-skill"),
name: "later-skill",
description: "Grouped sibling after a noisy tree",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
limits: {
maxCandidatesPerRoot: 2,
maxSkillsLoadedPerSource: 10,
},
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("later-skill");
});
it("discovers deeply nested SKILL.md files within the Codex-compatible depth", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "a", "b", "c"),
name: "deep-skill",
description: "Discovered through grouped folders",
});
const names = loadTestWorkspaceSkills(workspaceDir).map((entry) => entry.skill.name);
expect(names).toContain("deep-skill");
});
it("discovers deeply nested skills in configured roots named skills", async () => {
const workspaceDir = await createTempWorkspaceDir();
const parentDir = await createTempWorkspaceDir();
const skillsDir = path.join(parentDir, "skills");
await writeSkill({
dir: path.join(skillsDir, "d0", "d1", "d2", "d3", "d4", "d5"),
name: "configured-deep-skill",
description: "Depth 6 from configured skills root",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [skillsDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("configured-deep-skill");
});
it("uses the nested skills folder as the depth root for repo-style extra dirs", async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(repoDir, "skills", "d0", "d1", "d2", "d3", "d4", "d5"),
name: "repo-depth-skill",
description: "Depth 6 from nested skills root",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("repo-depth-skill");
});
it("ignores invalid outside candidates when resolving repo-style extra dirs", async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
await fs.mkdir(path.join(repoDir, "examples", "bad"), { recursive: true });
await fs.writeFile(path.join(repoDir, "examples", "bad", "SKILL.md"), "---\nname: bad\n---\n");
await writeSkill({
dir: path.join(repoDir, "skills", "group", "valid"),
name: "repo-nested-skill",
description: "Valid nested repo skill",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("repo-nested-skill");
expect(names).not.toContain("bad");
});
it("ignores invalid root SKILL.md files when resolving repo-style extra dirs", async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
await fs.writeFile(path.join(repoDir, "SKILL.md"), "---\nname: bad\n---\n");
await writeSkill({
dir: path.join(repoDir, "examples", "valid"),
name: "outside-valid-skill",
description: "Valid outside repo skill",
});
await writeSkill({
dir: path.join(repoDir, "skills", "group", "valid"),
name: "repo-nested-skill",
description: "Valid nested repo skill",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("repo-nested-skill");
expect(names).not.toContain("outside-valid-skill");
expect(names).not.toContain("bad");
});
it("treats invalid outside SKILL.md files as terminal during repo-root detection", async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
await fs.mkdir(path.join(repoDir, "examples", "bad", "child"), { recursive: true });
await fs.writeFile(path.join(repoDir, "examples", "bad", "SKILL.md"), "---\nname: bad\n---\n");
await writeSkill({
dir: path.join(repoDir, "examples", "bad", "child"),
name: "outside-child",
description: "Valid child hidden behind invalid terminal parent",
});
await writeSkill({
dir: path.join(repoDir, "skills", "group", "valid"),
name: "repo-nested-skill",
description: "Valid nested repo skill",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("repo-nested-skill");
expect(names).not.toContain("outside-child");
});
it("keeps a configured direct skill root even when it has nested skill fixtures", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillDir = await createTempWorkspaceDir();
await writeSkill({
dir: skillDir,
name: "direct-root",
description: "Configured direct skill root",
});
await writeSkill({
dir: path.join(skillDir, "skills", "examples", "fixture"),
name: "fixture-skill",
description: "Nested fixture skill should not replace the root",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [skillDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("direct-root");
expect(names).not.toContain("fixture-skill");
});
it("does not re-root extra dirs from ignored nested skill files", async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(repoDir, "valid"),
name: "valid-root-skill",
description: "Direct child skill under configured root",
});
await writeSkill({
dir: path.join(repoDir, "skills", "node_modules", "pkg"),
name: "ignored-package-skill",
description: "Ignored nested dependency fixture",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("valid-root-skill");
expect(names).not.toContain("ignored-package-skill");
});
it("keeps direct child skills when a configured root also has a skills child", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillRootDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(skillRootDir, "valid"),
name: "valid-root-skill",
description: "Direct child skill under configured root",
});
await writeSkill({
dir: path.join(skillRootDir, "skills", "examples", "fixture"),
name: "fixture-skill",
description: "Nested fixture should not replace the configured root",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [skillRootDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("valid-root-skill");
expect(names).toContain("fixture-skill");
});
it("keeps nested skills when top-level candidate cap is filled by direct skills", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillRootDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(skillRootDir, "00-valid"),
name: "valid-root-skill",
description: "Direct child skill under configured root",
});
await writeSkill({
dir: path.join(skillRootDir, "skills", "examples", "fixture"),
name: "fixture-skill",
description: "Nested fixture should still be scanned",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [skillRootDir] },
limits: {
maxCandidatesPerRoot: 1,
maxSkillsLoadedPerSource: 10,
},
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("valid-root-skill");
expect(names).toContain("fixture-skill");
});
it("keeps nested skills depth when a configured root also has direct skills", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillRootDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(skillRootDir, "valid"),
name: "valid-root-skill",
description: "Direct child skill under configured root",
});
await writeSkill({
dir: path.join(skillRootDir, "skills", "d0", "d1", "d2", "d3", "d4", "d5"),
name: "deep-nested-skill",
description: "Depth 6 from nested skills root",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [skillRootDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("valid-root-skill");
expect(names).toContain("deep-nested-skill");
});
it("keeps configured root grouping outside skills within watcher depth", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillRootDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(skillRootDir, "group", "within-depth"),
name: "within-depth",
description: "Depth 2 from configured root",
});
await writeSkill({
dir: path.join(skillRootDir, "group", "d1", "too-deep"),
name: "too-deep",
description: "Depth 3 from configured root",
});
await writeSkill({
dir: path.join(skillRootDir, "skills", "d0", "d1", "d2", "d3", "d4", "d5"),
name: "deep-nested-skill",
description: "Depth 6 from nested skills root",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [skillRootDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("within-depth");
expect(names).toContain("deep-nested-skill");
expect(names).not.toContain("too-deep");
});
it("keeps grouped child skills when a configured root also has a skills child", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillRootDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(skillRootDir, "group", "valid"),
name: "valid-grouped-skill",
description: "Grouped child skill under configured root",
});
await writeSkill({
dir: path.join(skillRootDir, "skills", "examples", "fixture"),
name: "fixture-skill",
description: "Nested fixture should not replace the configured root",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [skillRootDir] },
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("valid-grouped-skill");
expect(names).toContain("fixture-skill");
});
it("does not descend beyond the bounded grouped skill depth", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "d0", "d1", "d2", "d3", "d4", "d5"),
name: "within-depth",
description: "Depth 6 loads",
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "e0", "e1", "e2", "e3", "e4", "e5", "e6"),
name: "too-deep",
description: "Depth 7 does not load",
});
const names = loadTestWorkspaceSkills(workspaceDir).map((entry) => entry.skill.name);
expect(names).toContain("within-depth");
expect(names).not.toContain("too-deep");
});
it("does not fall through to child skills when an immediate SKILL.md is invalid", async () => {
const workspaceDir = await createTempWorkspaceDir();
const parentDir = path.join(workspaceDir, "skills", "group", "parent");
await fs.mkdir(parentDir, { recursive: true });
await fs.writeFile(path.join(parentDir, "SKILL.md"), "---\nname: parent\n---\n", "utf-8");
await writeSkill({
dir: path.join(parentDir, "child"),
name: "too-deep",
description: "Should not be discovered through invalid parent fallback",
});
const names = loadTestWorkspaceSkills(workspaceDir).map((entry) => entry.skill.name);
expect(names).not.toContain("too-deep");
});
it("treats an immediate SKILL.md as terminal and does not descend", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "group"),
name: "group",
description: "Direct skill at the group level",
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "group", "inner"),
name: "inner",
description: "Should be ignored when parent is itself a skill",
});
const names = loadTestWorkspaceSkills(workspaceDir).map((entry) => entry.skill.name);
expect(names).toContain("group");
expect(names).not.toContain("inner");
});
it("warns and caps discovery in large grouping subfolders", async () => {
const workspaceDir = await createTempWorkspaceDir();
for (let i = 0; i < 3; i += 1) {
const name = `nested-skill-${i}`;
await writeSkill({
dir: path.join(workspaceDir, "skills", "group", name),
name,
description: `Nested skill ${i}`,
});
}
const warn = captureWarningLogger();
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
limits: {
maxCandidatesPerRoot: 2,
maxSkillsLoadedPerSource: 10,
},
},
},
}).map((entry) => entry.skill.name);
expect(
names.reduce((count, name) => count + (name.startsWith("nested-skill-") ? 1 : 0), 0),
).toBe(2);
expect(
warn.mock.calls
.map(([line]) => String(line))
.some((line) =>
line.includes("Nested skills directory has many entries, truncating discovery."),
),
).toBe(true);
});
it("does not spend nested candidate budget on ignored raw entries", async () => {
const workspaceDir = await createTempWorkspaceDir();
const groupDir = path.join(workspaceDir, "skills", "group");
await fs.mkdir(groupDir, { recursive: true });
for (let i = 0; i < 50; i += 1) {
await fs.writeFile(path.join(groupDir, `ignored-${String(i).padStart(2, "0")}.txt`), "");
}
for (const name of ["valid-a", "valid-b", "valid-c"]) {
await writeSkill({
dir: path.join(groupDir, name),
name,
description: `${name} nested under a group`,
});
}
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
limits: {
maxCandidatesPerRoot: 2,
maxSkillsLoadedPerSource: 10,
},
},
},
}).map((entry) => entry.skill.name);
expect(collectMatching(names, (name) => name.startsWith("valid-"))).toEqual([
"valid-a",
"valid-b",
]);
});
it("limits discovery for nested repo-style skills roots (dir/skills/*)", async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
for (let i = 0; i < 8; i += 1) {
const name = `repo-skill-${String(i).padStart(2, "0")}`;
await writeSkill({
dir: path.join(repoDir, "skills", name),
name,
description: `Desc ${i}`,
});
}
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
limits: {
maxCandidatesPerRoot: 5,
maxSkillsLoadedPerSource: 5,
},
},
},
}).map((entry) => entry.skill.name);
expect(names).toStrictEqual([
"repo-skill-00",
"repo-skill-01",
"repo-skill-02",
"repo-skill-03",
"repo-skill-04",
]);
});
it("skips skills whose SKILL.md exceeds maxSkillFileBytes", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "small-skill"),
name: "small-skill",
description: "Small",
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "big-skill"),
name: "big-skill",
description: "Big",
body: "x".repeat(5_000),
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: { skills: { limits: { maxSkillFileBytes: 1000 } } },
}).map((entry) => entry.skill.name);
expect(names).toContain("small-skill");
expect(names).not.toContain("big-skill");
});
it("detects nested skills roots beyond the first 25 entries", async () => {
const workspaceDir = await createTempWorkspaceDir();
const repoDir = await createTempWorkspaceDir();
for (let i = 0; i < 30; i += 1) {
await fs.mkdir(path.join(repoDir, "skills", `entry-${String(i).padStart(2, "0")}`), {
recursive: true,
});
}
await writeSkill({
dir: path.join(repoDir, "skills", "entry-29"),
name: "late-skill",
description: "Nested skill discovered late",
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [repoDir] },
limits: {
maxCandidatesPerRoot: 30,
maxSkillsLoadedPerSource: 30,
},
},
},
}).map((entry) => entry.skill.name);
expect(names).toContain("late-skill");
});
it("enforces maxSkillFileBytes for root-level SKILL.md", async () => {
const workspaceDir = await createTempWorkspaceDir();
const rootSkillDir = await createTempWorkspaceDir();
await writeSkill({
dir: rootSkillDir,
name: "root-big-skill",
description: "Big",
body: "x".repeat(5_000),
});
const names = loadTestWorkspaceSkills(workspaceDir, {
config: {
skills: {
load: { extraDirs: [rootSkillDir] },
limits: { maxSkillFileBytes: 1000 },
},
},
}).map((entry) => entry.skill.name);
expect(names).not.toContain("root-big-skill");
});
});
+712
View File
@@ -0,0 +1,712 @@
// Skill root discovery validates bounded filesystem candidates before loading skill records.
import fs from "node:fs";
import path from "node:path";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { walkDirectorySync } from "../../infra/fs-safe.js";
import { isPathInside } from "../../infra/path-guards.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { readSkillFrontmatterSafe } from "./local-loader.js";
import { compactSkillPath } from "./skill-paths.js";
import { findContainingAllowedSkillSymlinkTarget, tryRealpath } from "./symlink-targets.js";
const skillsLogger = createSubsystemLogger("skills");
const DEFAULT_MAX_CANDIDATES_PER_ROOT = 300;
const DEFAULT_MAX_SKILLS_LOADED_PER_SOURCE = 200;
const DEFAULT_MAX_SKILL_FILE_BYTES = 256_000;
const DEFAULT_MIN_RAW_ENTRIES_PER_DIRECTORY_SCAN = 1_000;
const DEFAULT_MAX_RAW_ENTRIES_PER_DIRECTORY_SCAN = 10_000;
// Match Codex's bounded recursive skills discovery without letting broad
// workspace roots turn into unbounded filesystem walks.
const MAX_GROUPED_SKILL_SCAN_DEPTH = 6;
const MAX_CONFIGURED_ROOT_GROUPED_SKILL_SCAN_DEPTH = 2;
export type ResolvedSkillDiscoveryLimits = {
maxCandidatesPerRoot: number;
maxSkillsLoadedPerSource: number;
maxSkillFileBytes: number;
};
export type CandidateSkillDir = {
skillDir: string;
skillDirRealPath: string;
name: string;
skillMdRealPath: string;
};
type DiscoveredSkillCandidates = {
candidates: CandidateSkillDir[];
rootIsSkill: boolean;
};
type ChildDirectoryScan = {
dirs: string[];
scannedEntryCount: number;
truncated: boolean;
};
type SkillDiscoveryBudget = {
remainingDirectoryScans: number;
remainingRawEntries: number;
truncated: boolean;
};
export function resolveSkillDiscoveryLimits(config?: OpenClawConfig): ResolvedSkillDiscoveryLimits {
const limits = config?.skills?.limits;
return {
maxCandidatesPerRoot: limits?.maxCandidatesPerRoot ?? DEFAULT_MAX_CANDIDATES_PER_ROOT,
maxSkillsLoadedPerSource:
limits?.maxSkillsLoadedPerSource ?? DEFAULT_MAX_SKILLS_LOADED_PER_SOURCE,
maxSkillFileBytes: limits?.maxSkillFileBytes ?? DEFAULT_MAX_SKILL_FILE_BYTES,
};
}
function listChildDirectories(
dir: string,
opts?: {
followSymlinks?: boolean;
maxCandidateDirs?: number;
maxRawEntriesToScan?: number;
},
): ChildDirectoryScan {
const maxRawEntriesToScan =
opts?.maxRawEntriesToScan === undefined
? resolveRawEntryScanLimit(opts?.maxCandidateDirs)
: Math.max(0, opts.maxRawEntriesToScan);
const scan = walkDirectorySync(dir, {
maxDepth: 1,
maxEntries: maxRawEntriesToScan,
symlinks: opts?.followSymlinks === false ? "skip" : "follow",
include: (entry) =>
entry.kind === "directory" && !entry.name.startsWith(".") && entry.name !== "node_modules",
});
if (scan.scannedEntryCount === 0 && scan.entries.length === 0) {
return { dirs: [], scannedEntryCount: 0, truncated: false };
}
return {
dirs: scan.entries.map((entry) => entry.name),
scannedEntryCount: scan.scannedEntryCount,
truncated: scan.truncated,
};
}
function resolveRawEntryScanLimit(maxCandidateDirs: number | undefined): number {
if (maxCandidateDirs === undefined) {
return Number.POSITIVE_INFINITY;
}
const normalized = Math.max(0, maxCandidateDirs);
if (normalized === 0) {
return 0;
}
return Math.min(
DEFAULT_MAX_RAW_ENTRIES_PER_DIRECTORY_SCAN,
Math.max(DEFAULT_MIN_RAW_ENTRIES_PER_DIRECTORY_SCAN, normalized * 10),
);
}
function createSkillDiscoveryBudget(maxCandidateDirs: number): SkillDiscoveryBudget {
const normalized = Math.max(0, maxCandidateDirs);
return {
remainingDirectoryScans: normalized * MAX_GROUPED_SKILL_SCAN_DEPTH,
remainingRawEntries: resolveRawEntryScanLimit(normalized) * (normalized + 1),
truncated: false,
};
}
function listBudgetedChildDirectories(
dir: string,
budget: SkillDiscoveryBudget,
opts: { followSymlinks?: boolean; maxCandidateDirs: number },
): ChildDirectoryScan {
if (budget.remainingDirectoryScans <= 0 || budget.remainingRawEntries <= 0) {
budget.truncated = true;
return { dirs: [], scannedEntryCount: 0, truncated: false };
}
budget.remainingDirectoryScans -= 1;
const maxRawEntriesToScan = Math.min(
resolveRawEntryScanLimit(opts.maxCandidateDirs),
budget.remainingRawEntries,
);
const scan = listChildDirectories(dir, {
followSymlinks: opts.followSymlinks,
maxCandidateDirs: opts.maxCandidateDirs,
maxRawEntriesToScan,
});
budget.remainingRawEntries = Math.max(0, budget.remainingRawEntries - scan.scannedEntryCount);
budget.truncated ||= scan.truncated;
return scan;
}
function containsDiscoverableSkill(
dir: string,
opts: {
maxCandidateDirs: number;
maxSkillFileBytes?: number;
skipTopLevelDirName?: string;
},
): boolean {
const discoveryBudget = createSkillDiscoveryBudget(opts.maxCandidateDirs);
const queue: Array<{ dir: string; depth: number }> = [{ dir, depth: 0 }];
for (const candidate of queue) {
if (!candidate) {
continue;
}
if (candidate.depth > 0 && fs.existsSync(path.join(candidate.dir, "SKILL.md"))) {
if (hasLoadableSkillFrontmatter(dir, candidate.dir, opts.maxSkillFileBytes)) {
return true;
}
continue;
}
if (candidate.depth >= MAX_GROUPED_SKILL_SCAN_DEPTH) {
continue;
}
if (
hasCandidateSymlinkChild(
candidate.dir,
candidate.depth === 0 ? opts.skipTopLevelDirName : undefined,
resolveRawEntryScanLimit(opts.maxCandidateDirs),
)
) {
return true;
}
const childDirs = listBudgetedChildDirectories(candidate.dir, discoveryBudget, {
followSymlinks: false,
maxCandidateDirs: opts.maxCandidateDirs,
}).dirs;
for (const childDir of childDirs.toSorted().slice(0, opts.maxCandidateDirs)) {
if (candidate.depth === 0 && childDir === opts.skipTopLevelDirName) {
continue;
}
queue.push({ dir: path.join(candidate.dir, childDir), depth: candidate.depth + 1 });
}
}
return false;
}
function hasCandidateSymlinkChild(
dir: string,
skipName: string | undefined,
maxEntriesToScan: number,
): boolean {
const maxEntries = Math.max(0, maxEntriesToScan);
if (maxEntries === 0) {
return false;
}
let handle: fs.Dir | undefined;
try {
handle = fs.opendirSync(dir);
for (let scanned = 0; scanned < maxEntries; scanned += 1) {
const entry = handle.readSync();
if (!entry) {
break;
}
if (entry.name === skipName || entry.name.startsWith(".") || entry.name === "node_modules") {
continue;
}
if (entry.isSymbolicLink()) {
return true;
}
}
} catch {
return false;
} finally {
handle?.closeSync();
}
return false;
}
function hasLoadableSkillFrontmatter(
rootDir: string,
skillDir: string,
maxSkillFileBytes?: number,
): boolean {
const frontmatter = readSkillFrontmatterSafe({
rootDir,
filePath: path.join(skillDir, "SKILL.md"),
maxBytes: maxSkillFileBytes ?? DEFAULT_MAX_SKILL_FILE_BYTES,
});
const fallbackName = path.basename(skillDir).trim();
const name = frontmatter?.name?.trim() || fallbackName;
return Boolean(name) && Boolean(frontmatter?.description?.trim());
}
function isSymlinkPath(filePath: string): boolean {
try {
return fs.lstatSync(filePath).isSymbolicLink();
} catch {
return false;
}
}
function buildEscapedSkillPathReason(params: { source: string; candidatePath: string }): {
reason: string;
consoleHint: string;
} {
const candidateIsSymlink = isSymlinkPath(params.candidatePath);
if (params.source === "openclaw-bundled" && candidateIsSymlink) {
return {
reason: "bundled-symlink-escape",
consoleHint:
"reason=bundled-symlink-escape hint=likely-stray-local-symlink-or-checkout-mutation",
};
}
if (candidateIsSymlink) {
return { reason: "symlink-escape", consoleHint: "reason=symlink-escape" };
}
if (params.source === "openclaw-bundled") {
return {
reason: "bundled-root-escape",
consoleHint:
"reason=bundled-root-escape hint=likely-stray-local-symlink-or-checkout-mutation",
};
}
return { reason: "path-escape", consoleHint: "reason=path-escape" };
}
function warnEscapedSkillPath(params: {
source: string;
rootDir: string;
rootRealPath: string;
candidatePath: string;
candidateRealPath: string;
}) {
const compactRootDir = compactSkillPath(params.rootDir);
const compactRootRealPath = compactSkillPath(params.rootRealPath);
const compactCandidatePath = compactSkillPath(params.candidatePath);
const compactCandidateRealPath = compactSkillPath(params.candidateRealPath);
const rootResolved =
path.resolve(params.rootDir) === params.rootRealPath
? ""
: ` rootResolved=${compactRootRealPath}`;
const escapeReason = buildEscapedSkillPathReason({
source: params.source,
candidatePath: params.candidatePath,
});
skillsLogger.warn("Skipping escaped skill path outside its configured root.", {
source: params.source,
rootDir: params.rootDir,
rootRealPath: params.rootRealPath,
path: params.candidatePath,
realPath: params.candidateRealPath,
reason: escapeReason.reason,
consoleMessage:
`Skipping escaped skill path outside its configured root: ` +
`source=${params.source} root=${compactRootDir}${rootResolved} ` +
`${escapeReason.consoleHint} requested=${compactCandidatePath} ` +
`resolved=${compactCandidateRealPath}`,
});
}
function resolveContainedSkillPath(params: {
source: string;
rootDir: string;
rootRealPath: string;
candidatePath: string;
allowedSymlinkTargetRealPaths?: readonly string[];
}): string | null {
const candidateRealPath = tryRealpath(params.candidatePath);
if (!candidateRealPath) {
return null;
}
if (
isPathInside(params.rootRealPath, candidateRealPath) ||
findContainingAllowedSkillSymlinkTarget(
params.allowedSymlinkTargetRealPaths ?? [],
candidateRealPath,
) !== null
) {
return candidateRealPath;
}
warnEscapedSkillPath({
source: params.source,
rootDir: params.rootDir,
rootRealPath: params.rootRealPath,
candidatePath: path.resolve(params.candidatePath),
candidateRealPath,
});
return null;
}
function resolveNestedSkillsRoot(
dir: string,
opts?: { maxEntriesToScan?: number; maxSkillFileBytes?: number },
): { baseDir: string; note?: string } {
if (hasLoadableSkillFrontmatter(dir, dir, opts?.maxSkillFileBytes)) {
return { baseDir: dir };
}
const rootSkillMdExists = fs.existsSync(path.join(dir, "SKILL.md"));
const nested = path.join(dir, "skills");
try {
if (!fs.existsSync(nested) || !fs.statSync(nested).isDirectory()) {
return { baseDir: dir };
}
} catch {
return { baseDir: dir };
}
const scanLimit = Math.max(0, opts?.maxEntriesToScan ?? 100);
if (
!rootSkillMdExists &&
containsDiscoverableSkill(dir, {
maxCandidateDirs: scanLimit,
maxSkillFileBytes: opts?.maxSkillFileBytes,
skipTopLevelDirName: "skills",
})
) {
return { baseDir: dir };
}
const discoveryBudget = createSkillDiscoveryBudget(scanLimit);
const queue: Array<{ dir: string; depth: number }> = [{ dir: nested, depth: 0 }];
for (const candidate of queue) {
if (!candidate) {
continue;
}
if (hasLoadableSkillFrontmatter(nested, candidate.dir, opts?.maxSkillFileBytes)) {
return { baseDir: nested, note: `Detected nested skills root at ${nested}` };
}
if (candidate.depth >= MAX_GROUPED_SKILL_SCAN_DEPTH) {
continue;
}
const childDirs = listBudgetedChildDirectories(candidate.dir, discoveryBudget, {
followSymlinks: false,
maxCandidateDirs: scanLimit,
}).dirs;
for (const childDir of childDirs.toSorted().slice(0, scanLimit)) {
queue.push({ dir: path.join(candidate.dir, childDir), depth: candidate.depth + 1 });
}
}
return { baseDir: dir };
}
function shouldEnforceConfiguredSkillRootContainment(source: string): boolean {
return source !== "openclaw-managed" && source !== "agents-skills-personal";
}
function shouldUseConfiguredSymlinkTargets(source: string): boolean {
return (
source === "openclaw-workspace" ||
source === "openclaw-extra" ||
source === "agents-skills-project"
);
}
function resolveSkillRootCandidatePath(params: {
source: string;
rootDir: string;
rootRealPath: string;
candidatePath: string;
allowedSymlinkTargetRealPaths: readonly string[];
}): string | null {
if (!shouldEnforceConfiguredSkillRootContainment(params.source)) {
return tryRealpath(params.candidatePath);
}
return resolveContainedSkillPath({
source: params.source,
rootDir: params.rootDir,
rootRealPath: params.rootRealPath,
candidatePath: params.candidatePath,
allowedSymlinkTargetRealPaths: shouldUseConfiguredSymlinkTargets(params.source)
? params.allowedSymlinkTargetRealPaths
: [],
});
}
export function canonicalSkillDirForSource(
source: string,
skillDirRealPath: string,
): string | undefined {
return shouldEnforceConfiguredSkillRootContainment(source) ? undefined : skillDirRealPath;
}
function resolveSkillFilePath(params: {
source: string;
skillDir: string;
skillDirRealPath: string;
candidatePath: string;
}): string | null {
return resolveContainedSkillPath({
source: params.source,
rootDir: params.skillDir,
rootRealPath: params.skillDirRealPath,
candidatePath: params.candidatePath,
});
}
/** Discover validated skill directory candidates below one configured source root. */
export function discoverSkillCandidates(params: {
dir: string;
source: string;
limits: ResolvedSkillDiscoveryLimits;
allowedSymlinkTargetRealPaths: readonly string[];
}): DiscoveredSkillCandidates {
const rootDir = path.resolve(params.dir);
if (!fs.existsSync(rootDir)) {
return { candidates: [], rootIsSkill: false };
}
const rootRealPath = tryRealpath(rootDir) ?? rootDir;
const resolved = resolveNestedSkillsRoot(params.dir, {
maxEntriesToScan: params.limits.maxCandidatesPerRoot,
maxSkillFileBytes: params.limits.maxSkillFileBytes,
});
const baseDir = resolved.baseDir;
const baseDirRealPath = resolveSkillRootCandidatePath({
source: params.source,
rootDir,
rootRealPath,
candidatePath: baseDir,
allowedSymlinkTargetRealPaths: params.allowedSymlinkTargetRealPaths,
});
if (!baseDirRealPath) {
return { candidates: [], rootIsSkill: false };
}
const rootSkillMd = path.join(baseDir, "SKILL.md");
if (fs.existsSync(rootSkillMd)) {
const rootSkillRealPath = resolveSkillFilePath({
source: params.source,
skillDir: baseDir,
skillDirRealPath: baseDirRealPath,
candidatePath: rootSkillMd,
});
return {
candidates: rootSkillRealPath
? [
{
skillDir: baseDir,
skillDirRealPath: baseDirRealPath,
name: path.basename(baseDir),
skillMdRealPath: rootSkillRealPath,
},
]
: [],
rootIsSkill: true,
};
}
const maxCandidatesPerRoot = Math.max(0, params.limits.maxCandidatesPerRoot);
const maxSkillsLoadedPerSource = Math.max(0, params.limits.maxSkillsLoadedPerSource);
const nestedSkillsRootPath = path.resolve(baseDir, "skills");
const baseDirIsNestedSkillsRoot = path.resolve(baseDir) === path.resolve(rootDir, "skills");
const baseDirLooksLikeSkillsRoot = path.basename(baseDir) === "skills";
const discoveryBudget = createSkillDiscoveryBudget(maxCandidatesPerRoot);
const childDirScan = listBudgetedChildDirectories(baseDir, discoveryBudget, {
maxCandidateDirs: maxCandidatesPerRoot,
});
const childDirs = childDirScan.dirs;
const sortedChildDirs = childDirs.toSorted();
const limitedChildren =
maxSkillsLoadedPerSource === 0 ? [] : sortedChildDirs.slice(0, maxCandidatesPerRoot);
if (
maxSkillsLoadedPerSource > 0 &&
sortedChildDirs.includes("skills") &&
!limitedChildren.includes("skills")
) {
limitedChildren.push("skills");
}
if (childDirScan.truncated) {
skillsLogger.warn("Skills root looks suspiciously large, truncating discovery.", {
dir: params.dir,
baseDir,
childDirCount: childDirs.length,
scannedEntryCount: childDirScan.scannedEntryCount,
maxEntriesToScan: resolveRawEntryScanLimit(maxCandidatesPerRoot),
maxCandidatesPerRoot: params.limits.maxCandidatesPerRoot,
maxSkillsLoadedPerSource: params.limits.maxSkillsLoadedPerSource,
});
} else if (childDirs.length > maxCandidatesPerRoot) {
skillsLogger.warn("Skills root has many entries, truncating discovery.", {
dir: params.dir,
baseDir,
childDirCount: childDirs.length,
maxCandidatesPerRoot: params.limits.maxCandidatesPerRoot,
maxSkillsLoadedPerSource: params.limits.maxSkillsLoadedPerSource,
});
}
const skillCandidates: CandidateSkillDir[] = [];
const scanQueue: Array<{ skillDir: string; name: string; depth: number }> = limitedChildren.map(
(name) => ({
skillDir: path.join(baseDir, name),
name,
depth: name === "skills" && !fs.existsSync(path.join(baseDir, name, "SKILL.md")) ? 0 : 1,
}),
);
for (const candidate of scanQueue) {
if (!candidate) {
continue;
}
const skillDirRealPath = resolveSkillRootCandidatePath({
source: params.source,
rootDir,
rootRealPath: baseDirRealPath,
candidatePath: candidate.skillDir,
allowedSymlinkTargetRealPaths: params.allowedSymlinkTargetRealPaths,
});
if (!skillDirRealPath) {
continue;
}
const skillMd = path.join(candidate.skillDir, "SKILL.md");
if (fs.existsSync(skillMd)) {
const skillMdRealPath = resolveSkillFilePath({
source: params.source,
skillDir: candidate.skillDir,
skillDirRealPath,
candidatePath: skillMd,
});
if (skillMdRealPath) {
skillCandidates.push({
skillDir: candidate.skillDir,
skillDirRealPath,
name: candidate.name,
skillMdRealPath,
});
}
continue;
}
const candidatePath = path.resolve(candidate.skillDir);
const maxGroupedDepth =
params.source === "openclaw-extra" &&
!baseDirIsNestedSkillsRoot &&
!baseDirLooksLikeSkillsRoot &&
candidatePath !== nestedSkillsRootPath &&
!isPathInside(nestedSkillsRootPath, candidatePath)
? MAX_CONFIGURED_ROOT_GROUPED_SKILL_SCAN_DEPTH
: MAX_GROUPED_SKILL_SCAN_DEPTH;
if (candidate.depth >= maxGroupedDepth) {
continue;
}
const nestedChildScan = listBudgetedChildDirectories(candidate.skillDir, discoveryBudget, {
maxCandidateDirs: maxCandidatesPerRoot,
});
const nestedChildren = nestedChildScan.dirs;
if (nestedChildScan.truncated) {
skillsLogger.warn("Nested skills directory looks suspiciously large, truncating discovery.", {
dir: params.dir,
baseDir,
nestedDir: candidate.skillDir,
nestedChildDirCount: nestedChildren.length,
scannedEntryCount: nestedChildScan.scannedEntryCount,
maxEntriesToScan: resolveRawEntryScanLimit(maxCandidatesPerRoot),
maxCandidatesPerRoot: params.limits.maxCandidatesPerRoot,
maxSkillsLoadedPerSource: params.limits.maxSkillsLoadedPerSource,
maxGroupedSkillScanDepth: MAX_GROUPED_SKILL_SCAN_DEPTH,
});
} else if (nestedChildren.length > maxCandidatesPerRoot) {
skillsLogger.warn("Nested skills directory has many entries, truncating discovery.", {
dir: params.dir,
baseDir,
nestedDir: candidate.skillDir,
nestedChildDirCount: nestedChildren.length,
maxCandidatesPerRoot: params.limits.maxCandidatesPerRoot,
maxSkillsLoadedPerSource: params.limits.maxSkillsLoadedPerSource,
maxGroupedSkillScanDepth: MAX_GROUPED_SKILL_SCAN_DEPTH,
});
}
for (const nestedName of nestedChildren.toSorted().slice(0, maxCandidatesPerRoot)) {
scanQueue.push({
skillDir: path.join(candidate.skillDir, nestedName),
name: `${candidate.name}/${nestedName}`,
depth: candidate.depth + 1,
});
}
}
if (discoveryBudget.truncated) {
skillsLogger.warn("Skills root hit recursive discovery budget, truncating discovery.", {
dir: params.dir,
baseDir,
maxCandidatesPerRoot: params.limits.maxCandidatesPerRoot,
maxSkillsLoadedPerSource: params.limits.maxSkillsLoadedPerSource,
maxGroupedSkillScanDepth: MAX_GROUPED_SKILL_SCAN_DEPTH,
});
}
return {
candidates: skillCandidates.toSorted((a, b) => a.name.localeCompare(b.name)),
rootIsSkill: false,
};
}
function resolvePluginSkillRootRealPaths(pluginSkillDirs: readonly string[]): string[] {
return uniqueStrings(
pluginSkillDirs.map((dir) => tryRealpath(dir)).filter((dir): dir is string => Boolean(dir)),
);
}
/** Discover validated generated plugin-skill symlink candidates. */
export function discoverPluginSkills(params: {
pluginSkillsDir: string;
pluginSkillDirs: readonly string[];
source: string;
limits: ResolvedSkillDiscoveryLimits;
}): CandidateSkillDir[] {
const allowedRootRealPaths = resolvePluginSkillRootRealPaths(params.pluginSkillDirs);
if (allowedRootRealPaths.length === 0) {
return [];
}
const rootDir = path.resolve(params.pluginSkillsDir);
if (!fs.existsSync(rootDir)) {
return [];
}
const rootRealPath = tryRealpath(rootDir) ?? rootDir;
const maxCandidatesPerRoot = Math.max(0, params.limits.maxCandidatesPerRoot);
const maxSkillsLoadedPerSource = Math.max(0, params.limits.maxSkillsLoadedPerSource);
const childDirScan = listChildDirectories(rootDir, {
maxCandidateDirs: maxCandidatesPerRoot,
});
const childDirs =
maxSkillsLoadedPerSource === 0
? []
: childDirScan.dirs.toSorted().slice(0, maxCandidatesPerRoot);
const candidates: CandidateSkillDir[] = [];
for (const name of childDirs) {
const skillDir = path.join(rootDir, name);
if (!isSymlinkPath(skillDir)) {
continue;
}
const skillDirRealPath = tryRealpath(skillDir);
if (
!skillDirRealPath ||
findContainingAllowedSkillSymlinkTarget(allowedRootRealPaths, skillDirRealPath) === null
) {
if (skillDirRealPath) {
warnEscapedSkillPath({
source: params.source,
rootDir,
rootRealPath,
candidatePath: path.resolve(skillDir),
candidateRealPath: skillDirRealPath,
});
}
continue;
}
const skillMd = path.join(skillDir, "SKILL.md");
let skillMdStat: fs.Stats;
try {
skillMdStat = fs.lstatSync(skillMd);
} catch {
continue;
}
if (!skillMdStat.isFile() || skillMdStat.isSymbolicLink()) {
continue;
}
const skillMdRealPath = tryRealpath(skillMd);
if (!skillMdRealPath || !isPathInside(skillDirRealPath, skillMdRealPath)) {
continue;
}
candidates.push({ skillDir, skillDirRealPath, name, skillMdRealPath });
}
return candidates;
}
+5 -1
View File
@@ -26,7 +26,7 @@ import {
} from "../test-support/home-env.test-support.js";
import type { SkillEntry, SkillSnapshot } from "../types.js";
import { shouldIncludeSkill } from "./config.js";
import { buildWorkspaceSkillsPrompt } from "./workspace.js";
import { buildSkillSnapshot } from "./workspace-skill-prompt.js";
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
@@ -43,6 +43,10 @@ const resolveTestSkillDirs = (workspaceDir: string) => ({
});
const makeWorkspace = async () => await fixtureSuite.createCaseDir("workspace");
const buildWorkspaceSkillsPrompt = (
workspaceDir: string,
opts?: Parameters<typeof buildSkillSnapshot>[1],
): string => buildSkillSnapshot(workspaceDir, opts).prompt;
const apiKeyField = ["api", "Key"].join("");
function withWorkspaceHome<T>(workspaceDir: string, cb: () => T): T {
@@ -5,7 +5,12 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import { buildWorkspaceSkillsPrompt } from "./workspace.js";
import { buildSkillSnapshot } from "./workspace-skill-prompt.js";
const buildWorkspaceSkillsPrompt = (
workspaceDir: string,
opts?: Parameters<typeof buildSkillSnapshot>[1],
): string => buildSkillSnapshot(workspaceDir, opts).prompt;
describe("buildWorkspaceSkillsPrompt", () => {
it("applies bundled allowlist without affecting workspace skills", async () => {
File diff suppressed because it is too large Load Diff
@@ -6,7 +6,12 @@ import { createFixtureSuite } from "../../test-utils/fixture-suite.js";
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import type { OpenClawSkillMetadata, SkillEntry } from "../types.js";
import { createSyntheticSourceInfo } from "./skill-contract.js";
import { buildWorkspaceSkillsPrompt } from "./workspace.js";
import { buildSkillSnapshot } from "./workspace-skill-prompt.js";
const buildWorkspaceSkillsPrompt = (
workspaceDir: string,
opts?: Parameters<typeof buildSkillSnapshot>[1],
): string => buildSkillSnapshot(workspaceDir, opts).prompt;
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
@@ -0,0 +1,527 @@
// Workspace skill loader tests cover source merging, metadata, filtering, and precedence.
import fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { resetLogger, setLoggerOverride } from "../../logging/logger.js";
import { loggingState } from "../../logging/state.js";
import { setCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
import { clearCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-state.js";
import { resolveInstalledPluginIndexPolicyHash } from "../../plugins/installed-plugin-index-policy.js";
import type {
PluginManifestRecord,
PluginManifestRegistry,
} from "../../plugins/manifest-registry.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
import { writeSkill, writeWorkspaceSkills } from "../test-support/e2e-test-helpers.js";
import {
restoreMockSkillsHomeEnv,
setMockSkillsHomeEnv,
type SkillsHomeEnvSnapshot,
} from "../test-support/home-env.test-support.js";
import { writePluginWithSkill } from "../test-support/skill-plugin-fixtures.test-support.js";
import { loadWorkspaceSkills } from "./workspace-skill-loader.js";
vi.mock("../../plugins/manifest-registry.js", async () => {
const fsLocal = await import("node:fs");
const pathLocal = await import("node:path");
return {
loadPluginManifestRegistry: (params: { workspaceDir?: string }) => {
const extensionsRoot = pathLocal.join(params.workspaceDir ?? "", ".openclaw", "extensions");
const plugins = [];
for (const id of ["open-prose", "browser"]) {
const rootDir = pathLocal.join(extensionsRoot, id);
const manifestPath = pathLocal.join(rootDir, "openclaw.plugin.json");
if (!fsLocal.existsSync(manifestPath)) {
continue;
}
const manifest = JSON.parse(fsLocal.readFileSync(manifestPath, "utf8")) as {
enabledByDefault?: boolean;
skills?: string[];
};
plugins.push({
id,
origin: id === "browser" ? "bundled" : "workspace",
enabledByDefault: manifest.enabledByDefault,
providers: [],
legacyPluginIds: [],
kind: [],
skills: manifest.skills ?? ["./skills"],
rootDir,
});
}
return { plugins, diagnostics: [] };
},
};
});
let fakeHome = "";
let envSnapshot: SkillsHomeEnvSnapshot;
let tempRoot = "";
let workspaceCaseIndex = 0;
function createWorkspacePluginRegistry(workspaceDir: string): PluginManifestRegistry {
const extensionsRoot = path.join(workspaceDir, ".openclaw", "extensions");
const plugins: PluginManifestRecord[] = [];
for (const id of ["open-prose", "browser"]) {
const rootDir = path.join(extensionsRoot, id);
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
if (!fsSync.existsSync(manifestPath)) {
continue;
}
const manifest = JSON.parse(fsSync.readFileSync(manifestPath, "utf8")) as {
id?: string;
enabledByDefault?: boolean;
skills?: string[];
configSchema?: Record<string, unknown>;
};
plugins.push({
id: manifest.id ?? id,
origin: id === "browser" ? "bundled" : "workspace",
enabledByDefault: manifest.enabledByDefault,
channels: [],
providers: [],
cliBackends: [],
legacyPluginIds: [],
kind: [],
skills: manifest.skills ?? ["./skills"],
hooks: [],
rootDir,
source: rootDir,
manifestPath,
configSchema: manifest.configSchema,
});
}
return { plugins, diagnostics: [] };
}
function createWorkspacePluginMetadataSnapshot(params: {
workspaceDir: string;
config?: OpenClawConfig;
manifestRegistry: PluginManifestRegistry;
}): PluginMetadataSnapshot {
const policyHash = resolveInstalledPluginIndexPolicyHash(params.config);
const ownerMaps = {
channels: new Map(),
channelConfigs: new Map(),
providers: new Map(),
modelCatalogProviders: new Map(),
cliBackends: new Map(),
setupProviders: new Map(),
commandAliases: new Map(),
contracts: new Map(),
};
return {
policyHash,
workspaceDir: params.workspaceDir,
index: {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash,
generatedAtMs: 1,
installRecords: {},
plugins: [],
diagnostics: [],
},
registryDiagnostics: [],
manifestRegistry: params.manifestRegistry,
plugins: params.manifestRegistry.plugins,
diagnostics: params.manifestRegistry.diagnostics,
byPluginId: new Map(params.manifestRegistry.plugins.map((plugin) => [plugin.id, plugin])),
normalizePluginId: (pluginId) => pluginId,
owners: ownerMaps,
metrics: {
registrySnapshotMs: 0,
manifestRegistryMs: 0,
ownerMapsMs: 0,
totalMs: 0,
indexPluginCount: 0,
manifestPluginCount: params.manifestRegistry.plugins.length,
},
};
}
function setWorkspacePluginMetadataSnapshot(workspaceDir: string, config?: OpenClawConfig): void {
const manifestRegistry = createWorkspacePluginRegistry(workspaceDir);
setCurrentPluginMetadataSnapshot(
createWorkspacePluginMetadataSnapshot({
workspaceDir,
manifestRegistry,
...(config === undefined ? {} : { config }),
}),
{
workspaceDir,
...(config === undefined ? {} : { config }),
},
);
}
async function expectMissingPath(pathToCheck: string) {
let thrown: unknown;
try {
await fs.lstat(pathToCheck);
} catch (error) {
thrown = error;
}
expect((thrown as NodeJS.ErrnoException | undefined)?.code).toBe("ENOENT");
}
async function createTempWorkspaceDir() {
const workspaceDir = path.join(tempRoot, `workspace-${++workspaceCaseIndex}`);
await fs.mkdir(workspaceDir, { recursive: true });
return workspaceDir;
}
function captureWarningLogger() {
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
const warn = vi.fn();
loggingState.rawConsole = {
log: vi.fn(),
info: vi.fn(),
warn,
error: vi.fn(),
};
return warn;
}
function loadTestWorkspaceSkills(
workspaceDir: string,
opts?: Parameters<typeof loadWorkspaceSkills>[1],
) {
setWorkspacePluginMetadataSnapshot(workspaceDir, opts?.config);
return loadWorkspaceSkills(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir: "",
pluginSkillsDir: path.join(workspaceDir, ".plugin-skills"),
...opts,
});
}
beforeAll(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skills-workspace-"));
fakeHome = path.join(tempRoot, "home");
await fs.mkdir(fakeHome, { recursive: true });
envSnapshot = setMockSkillsHomeEnv(fakeHome);
});
afterEach(async () => {
clearCurrentPluginMetadataSnapshot();
setLoggerOverride(null);
loggingState.rawConsole = null;
resetLogger();
});
afterAll(async () => {
await restoreMockSkillsHomeEnv(envSnapshot, async () => {
if (tempRoot) {
await fs.rm(tempRoot, { recursive: true, force: true });
}
});
});
async function setupWorkspaceWithProsePlugin() {
const workspaceDir = await createTempWorkspaceDir();
const managedDir = path.join(workspaceDir, ".managed");
const pluginRoot = path.join(workspaceDir, ".openclaw", "extensions", "open-prose");
await writePluginWithSkill({
pluginRoot,
pluginId: "open-prose",
skillId: "prose",
skillDescription: "test",
});
return { workspaceDir, managedDir };
}
describe("loadWorkspaceSkills", () => {
it("filters plugin-shipped skills through plugin config", async () => {
const { workspaceDir, managedDir } = await setupWorkspaceWithProsePlugin();
const enabledEntries = loadTestWorkspaceSkills(workspaceDir, {
config: {
plugins: {
entries: { "open-prose": { enabled: true } },
},
},
managedSkillsDir: managedDir,
});
expect(enabledEntries.map((entry) => entry.skill.name)).toContain("prose");
const blockedEntries = loadTestWorkspaceSkills(workspaceDir, {
config: {
plugins: {
allow: ["something-else"],
},
},
managedSkillsDir: managedDir,
});
expect(blockedEntries.map((entry) => entry.skill.name)).not.toContain("prose");
});
it("loads the browser plugin automation skill when the bundled plugin is enabled", async () => {
const workspaceDir = await createTempWorkspaceDir();
const managedDir = path.join(workspaceDir, ".managed");
const pluginRoot = path.join(workspaceDir, ".openclaw", "extensions", "browser");
await writePluginWithSkill({
pluginRoot,
pluginId: "browser",
skillId: "browser-automation",
skillDescription: "Browser automation",
});
await fs.writeFile(
path.join(pluginRoot, "openclaw.plugin.json"),
JSON.stringify(
{
id: "browser",
enabledByDefault: true,
skills: ["./skills"],
configSchema: { type: "object", additionalProperties: false, properties: {} },
},
null,
2,
),
"utf8",
);
const enabledEntries = loadTestWorkspaceSkills(workspaceDir, {
config: {},
managedSkillsDir: managedDir,
});
const browserEntry = enabledEntries.find((entry) => entry.skill.name === "browser-automation");
const browserSkillDir = path.join(pluginRoot, "skills", "browser-automation");
expect(browserEntry?.skill.baseDir).toBe(
path.join(workspaceDir, ".plugin-skills", "browser-automation"),
);
expect(browserEntry?.skill.filePath).toBe(
path.join(workspaceDir, ".plugin-skills", "browser-automation", "SKILL.md"),
);
await expect(
fs.readlink(path.join(workspaceDir, ".plugin-skills", "browser-automation")),
).resolves.toBe(browserSkillDir);
const blockedEntries = loadTestWorkspaceSkills(workspaceDir, {
config: {
plugins: {
entries: { browser: { enabled: false } },
},
},
managedSkillsDir: managedDir,
});
expect(blockedEntries.map((entry) => entry.skill.name)).not.toContain("browser-automation");
await expectMissingPath(path.join(workspaceDir, ".plugin-skills", "browser-automation"));
});
it("loads frontmatter edge cases in one workspace", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillDir = path.join(workspaceDir, "skills", "fallback-name");
await fs.mkdir(skillDir, { recursive: true });
await fs.writeFile(
path.join(skillDir, "SKILL.md"),
["---", "description: Skill without explicit name", "---", "", "# Fallback"].join("\n"),
"utf8",
);
await writeSkill({
dir: path.join(workspaceDir, "skills", "hidden-skill"),
name: "hidden-skill",
description: "Hidden prompt entry",
frontmatterExtra: "disable-model-invocation: true",
});
const bomSkillDir = path.join(workspaceDir, "skills", "bom-skill");
await fs.mkdir(bomSkillDir, { recursive: true });
await fs.writeFile(
path.join(bomSkillDir, "SKILL.md"),
"\uFEFF---\nname: bom-skill\ndescription: BOM-prefixed skill\n---\n\n# BOM skill\n",
"utf8",
);
const entries = loadTestWorkspaceSkills(workspaceDir);
expect(entries.map((entry) => entry.skill.name)).toContain("fallback-name");
expect(entries.map((entry) => entry.skill.name)).toContain("bom-skill");
const hiddenEntry = entries.find((entry) => entry.skill.name === "hidden-skill");
expect(hiddenEntry?.invocation?.disableModelInvocation).toBe(true);
expect(hiddenEntry?.exposure?.includeInAvailableSkillsPrompt).toBe(false);
});
it("loads workspace metadata with JSON5-style trailing commas", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillDir = path.join(workspaceDir, "skills", "json5-metadata");
await fs.mkdir(skillDir, { recursive: true });
await fs.writeFile(
path.join(skillDir, "SKILL.md"),
`---
name: json5-metadata
description: JSON5-style metadata
metadata:
{
"openclaw":
{
"requires":
{
"env": ["EXAMPLE_VAR"],
},
},
}
---
`,
"utf8",
);
const entry = loadTestWorkspaceSkills(workspaceDir).find(
(candidate) => candidate.skill.name === "json5-metadata",
);
expect(entry?.metadata?.requires?.env).toEqual(["EXAMPLE_VAR"]);
});
it("warns for malformed files and keeps loading sibling workspace skills", async () => {
const workspaceDir = await createTempWorkspaceDir();
const brokenDir = path.join(workspaceDir, "skills", "broken");
const brokenFile = path.join(brokenDir, "SKILL.md");
const unterminatedDir = path.join(workspaceDir, "skills", "unterminated");
const unterminatedFile = path.join(unterminatedDir, "SKILL.md");
await fs.mkdir(brokenDir, { recursive: true });
await fs.mkdir(unterminatedDir, { recursive: true });
await fs.writeFile(
brokenFile,
`---
name: [broken
description: Broken skill
---
`,
"utf8",
);
await fs.writeFile(
unterminatedFile,
"---\nname: unterminated\ndescription: Missing closing delimiter\n",
"utf8",
);
await writeSkill({
dir: path.join(workspaceDir, "skills", "valid"),
name: "valid",
description: "Valid sibling",
});
const warn = captureWarningLogger();
const entries = loadTestWorkspaceSkills(workspaceDir);
const warningText = warn.mock.calls.flat().map(String).join("\n");
expect(entries.map((entry) => entry.skill.name)).toContain("valid");
expect(entries.map((entry) => entry.skill.name)).not.toContain("broken");
expect(warningText).toContain(brokenFile);
expect(warningText).toContain("invalid frontmatter: BAD_INDENT");
expect(entries.map((entry) => entry.skill.name)).not.toContain("unterminated");
expect(warningText).toContain(unterminatedFile);
expect(warningText).toContain(
"invalid frontmatter: UNTERMINATED_FRONTMATTER: missing closing --- delimiter",
);
});
it("applies agent skill filters and replacement semantics", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeWorkspaceSkills(workspaceDir, [
{ name: "github", description: "GitHub" },
{ name: "weather", description: "Weather" },
{ name: "docs-search", description: "Docs" },
]);
const defaultEntries = loadTestWorkspaceSkills(workspaceDir, {
config: {
agents: {
defaults: {
skills: ["github"],
},
list: [{ id: "writer" }],
},
},
agentId: "writer",
});
expect(defaultEntries.map((entry) => entry.skill.name)).toEqual(["github"]);
const replacementEntries = loadTestWorkspaceSkills(workspaceDir, {
config: {
agents: {
defaults: {
skills: ["github"],
},
list: [{ id: "writer", skills: ["docs-search"] }],
},
},
agentId: "writer",
});
expect(replacementEntries.map((entry) => entry.skill.name)).toEqual(["docs-search"]);
});
it("keeps remote-eligible skills when agent filtering is active", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "remote-only"),
name: "remote-only",
description: "Needs a remote bin",
metadata: '{"openclaw":{"requires":{"anyBins":["missingbin","sandboxbin"]}}}',
});
const entries = loadTestWorkspaceSkills(workspaceDir, {
config: {
agents: {
defaults: {
skills: ["remote-only"],
},
list: [{ id: "writer" }],
},
},
agentId: "writer",
eligibility: {
remote: {
platforms: ["linux"],
hasBin: () => false,
hasAnyBin: (bins: string[]) => bins.includes("sandboxbin"),
note: "sandbox",
},
},
});
expect(entries.map((entry) => entry.skill.name)).toEqual(["remote-only"]);
});
it("filters remote-ineligible skills when no agent skill filter is active", async () => {
const workspaceDir = await createTempWorkspaceDir();
await writeSkill({
dir: path.join(workspaceDir, "skills", "local-only"),
name: "local-only",
description: "Always available",
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "remote-only"),
name: "remote-only",
description: "Needs a remote bin",
metadata: '{"openclaw":{"requires":{"anyBins":["missingbin","sandboxbin"]}}}',
});
const entries = loadTestWorkspaceSkills(workspaceDir, {
eligibility: {
remote: {
platforms: ["linux"],
hasBin: () => false,
hasAnyBin: () => false,
note: "sandbox",
},
},
});
expect(entries.map((entry) => entry.skill.name)).toEqual(["local-only"]);
});
});
@@ -0,0 +1,560 @@
// Workspace skill loading turns validated discovery candidates into source-aware skill entries.
import fs from "node:fs";
import path from "node:path";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
import { canonicalizePath } from "../../agents/utils/paths.js";
import { isDefaultStateDir } from "../../config/paths.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { isPathInside } from "../../infra/path-guards.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
import {
isSessionSkillEnabled,
resolveEffectiveAgentSkillFilter,
} from "../discovery/agent-filter.js";
import { normalizeSkillFilter } from "../discovery/filter.js";
import { mergeRemoteNodeSkillEntries } from "../runtime/remote-skills.js";
import type {
OpenClawSkillMetadata,
ParsedSkillFrontmatter,
SkillEligibilityContext,
SkillEntry,
} from "../types.js";
import { getArchivedSkillFiles } from "../workshop/curator.js";
import { resolveBundledSkillsDir } from "./bundled-dir.js";
import { resolveBundledAllowlist, shouldIncludeSkill } from "./config.js";
import {
resolveOpenClawMetadata,
resolveSkillInvocationPolicy,
resolveSkillKey,
} from "./frontmatter.js";
import {
loadSkillsFromDirSafe,
readSkillFrontmatterSafe,
type LocalSkillLoadDiagnostic,
} from "./local-loader.js";
import { resolvePluginSkillDirs } from "./plugin-skills.js";
import type { Skill } from "./skill-contract.js";
import { compactSkillPath, resolveSkillsUserHomeDir } from "./skill-paths.js";
import {
canonicalSkillDirForSource,
discoverPluginSkills,
discoverSkillCandidates,
resolveSkillDiscoveryLimits,
type CandidateSkillDir,
type ResolvedSkillDiscoveryLimits,
} from "./skill-root-discovery.js";
import { resolveAllowedSkillSymlinkTargetRealPaths, tryRealpath } from "./symlink-targets.js";
const skillsLogger = createSubsystemLogger("skills");
const SKILL_SOURCE_ORIGIN_RELATIVE_PATH = path.join(".openclaw", "source-origin.json");
const MAX_SKILL_SOURCE_ORIGIN_BYTES = 16 * 1024;
type LoadedSkillRecord = {
skill: Skill;
frontmatter?: ParsedSkillFrontmatter;
syncSourceDir?: string;
syncDirName?: string;
};
function warnInvalidSkillFrontmatter(source: string, diagnostic: LocalSkillLoadDiagnostic): void {
skillsLogger.warn("Skipping skill with invalid frontmatter.", {
source,
filePath: diagnostic.path,
error: diagnostic.message,
consoleMessage:
`Skipping skill with invalid frontmatter: ` +
`file=${compactSkillPath(diagnostic.path)} error=${diagnostic.message}`,
});
}
function filterSkillEntries(
entries: SkillEntry[],
config?: OpenClawConfig,
skillFilter?: string[],
skillOverrides?: Readonly<Record<string, boolean>>,
eligibility?: SkillEligibilityContext,
): SkillEntry[] {
const bundledAllowlist = resolveBundledAllowlist(config);
let filtered = entries.filter((entry) =>
shouldIncludeSkill({ entry, config, bundledAllowlist, eligibility }),
);
if (skillFilter !== undefined || skillOverrides !== undefined) {
const normalized = normalizeSkillFilter(skillFilter) ?? [];
const label = normalized.length > 0 ? normalized.join(", ") : "(none)";
skillsLogger.debug(`Applying skill filter: ${label}`);
const resolvedFilter = skillFilter === undefined ? undefined : normalized;
filtered = filtered.filter((entry) =>
isSessionSkillEnabled(
entry.skill.name,
resolvedFilter,
skillOverrides,
resolveSkillKey(entry.skill, entry),
),
);
skillsLogger.debug(
`After skill filter: ${filtered.map((entry) => entry.skill.name).join(", ") || "(none)"}`,
);
}
return filtered;
}
function loadContainedSkillRecords(params: {
skillDir: string;
source: string;
maxSkillFileBytes: number;
canonicalSkillDir?: string;
}): LoadedSkillRecord[] {
const expectedBaseDir = path.resolve(params.skillDir);
const loaded = loadSkillsFromDirSafe({
dir: params.skillDir,
source: params.source,
maxBytes: params.maxSkillFileBytes,
onDiagnostic: (diagnostic) => warnInvalidSkillFrontmatter(params.source, diagnostic),
});
const records = loaded.skills
.map((skill) => ({
skill,
frontmatter: loaded.frontmatterByFilePath.get(skill.filePath),
}))
.filter((record) => path.resolve(record.skill.baseDir) === expectedBaseDir);
const canonicalSkillDir = params.canonicalSkillDir;
return canonicalSkillDir
? records.map((record) => canonicalizeLoadedSkillRecord(record, canonicalSkillDir))
: records;
}
function readSourceInstallSkillKey(skillDir: string): string | undefined {
try {
const sourceOriginPath = path.join(skillDir, SKILL_SOURCE_ORIGIN_RELATIVE_PATH);
const stat = fs.lstatSync(sourceOriginPath);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_SKILL_SOURCE_ORIGIN_BYTES) {
return undefined;
}
const skillDirRealPath = tryRealpath(skillDir);
const sourceOriginRealPath = tryRealpath(sourceOriginPath);
if (
!skillDirRealPath ||
!sourceOriginRealPath ||
!isPathInside(skillDirRealPath, sourceOriginRealPath)
) {
return undefined;
}
const raw = fs.readFileSync(sourceOriginPath, "utf8");
const parsed = JSON.parse(raw) as { slug?: unknown };
return normalizeOptionalString(parsed.slug);
} catch {
return undefined;
}
}
function resolveSkillEntryMetadata(params: {
frontmatter: ParsedSkillFrontmatter;
skillDir: string;
}): OpenClawSkillMetadata | undefined {
const metadata = resolveOpenClawMetadata(params.frontmatter);
if (metadata?.skillKey) {
return metadata;
}
const sourceInstallSkillKey = readSourceInstallSkillKey(params.skillDir);
if (!sourceInstallSkillKey) {
return metadata;
}
return { ...metadata, skillKey: sourceInstallSkillKey };
}
function canonicalizeLoadedSkillRecord(
record: LoadedSkillRecord,
canonicalSkillDir: string,
): LoadedSkillRecord {
const originalBaseDir = path.resolve(record.skill.baseDir);
const canonicalBaseDir = path.resolve(canonicalSkillDir);
if (originalBaseDir === canonicalBaseDir) {
return record;
}
const filePath = path.join(
canonicalBaseDir,
path.relative(originalBaseDir, record.skill.filePath),
);
return {
...record,
syncSourceDir: canonicalBaseDir,
syncDirName: path.basename(originalBaseDir),
skill: {
...record.skill,
filePath,
baseDir: canonicalBaseDir,
sourceInfo: record.skill.sourceInfo
? { ...record.skill.sourceInfo, path: filePath, baseDir: canonicalBaseDir }
: record.skill.sourceInfo,
},
};
}
function setSyncSourceForPluginSkill(
record: LoadedSkillRecord,
syncSourceDir: string,
): LoadedSkillRecord {
return {
...record,
syncSourceDir,
syncDirName: path.basename(record.skill.baseDir),
};
}
function isCandidateOversized(
candidate: CandidateSkillDir,
limits: ResolvedSkillDiscoveryLimits,
rootIsSkill: boolean,
): boolean {
try {
const size = fs.statSync(candidate.skillMdRealPath).size;
if (size <= limits.maxSkillFileBytes) {
return false;
}
skillsLogger.warn(
rootIsSkill
? "Skipping skills root due to oversized SKILL.md."
: "Skipping skill due to oversized SKILL.md.",
rootIsSkill
? {
dir: candidate.skillDir,
filePath: path.join(candidate.skillDir, "SKILL.md"),
size,
maxSkillFileBytes: limits.maxSkillFileBytes,
}
: {
skill: candidate.name,
filePath: path.join(candidate.skillDir, "SKILL.md"),
size,
maxSkillFileBytes: limits.maxSkillFileBytes,
},
);
return true;
} catch {
return true;
}
}
function loadDiscoveredSkillRecords(params: {
dir: string;
source: string;
limits: ResolvedSkillDiscoveryLimits;
allowedSymlinkTargetRealPaths: readonly string[];
}): LoadedSkillRecord[] {
const discovered = discoverSkillCandidates(params);
const loadedSkills: LoadedSkillRecord[] = [];
const maxSkillsLoadedPerSource = Math.max(0, params.limits.maxSkillsLoadedPerSource);
for (const candidate of discovered.candidates) {
if (!discovered.rootIsSkill && loadedSkills.length >= maxSkillsLoadedPerSource) {
break;
}
if (isCandidateOversized(candidate, params.limits, discovered.rootIsSkill)) {
continue;
}
loadedSkills.push(
...loadContainedSkillRecords({
skillDir: candidate.skillDir,
source: params.source,
maxSkillFileBytes: params.limits.maxSkillFileBytes,
canonicalSkillDir: canonicalSkillDirForSource(params.source, candidate.skillDirRealPath),
}),
);
}
if (loadedSkills.length > maxSkillsLoadedPerSource && !discovered.rootIsSkill) {
return loadedSkills
.toSorted((a, b) => a.skill.name.localeCompare(b.skill.name, "en"))
.slice(0, maxSkillsLoadedPerSource);
}
return loadedSkills;
}
function loadGeneratedPluginSkillRecords(params: {
pluginSkillsDir: string;
pluginSkillDirs: readonly string[];
source: string;
limits: ResolvedSkillDiscoveryLimits;
}): LoadedSkillRecord[] {
const candidates = discoverPluginSkills(params);
const maxSkillsLoadedPerSource = Math.max(0, params.limits.maxSkillsLoadedPerSource);
const loadedSkills: LoadedSkillRecord[] = [];
for (const candidate of candidates) {
if (isCandidateOversized(candidate, params.limits, false)) {
continue;
}
const loadedRecords = loadContainedSkillRecords({
skillDir: candidate.skillDir,
source: params.source,
maxSkillFileBytes: params.limits.maxSkillFileBytes,
});
loadedSkills.push(
...loadedRecords.map((record) =>
setSyncSourceForPluginSkill(record, candidate.skillDirRealPath),
),
);
if (loadedSkills.length >= maxSkillsLoadedPerSource) {
break;
}
}
if (loadedSkills.length > maxSkillsLoadedPerSource) {
return loadedSkills
.toSorted((a, b) => a.skill.name.localeCompare(b.skill.name, "en"))
.slice(0, maxSkillsLoadedPerSource);
}
return loadedSkills;
}
function loadSkillEntries(
workspaceDir: string,
opts?: {
config?: OpenClawConfig;
agentId?: string;
managedSkillsDir?: string;
bundledSkillsDir?: string;
pluginSkillsDir?: string;
workspaceOnly?: boolean;
includeArchived?: boolean;
},
): SkillEntry[] {
const limits = resolveSkillDiscoveryLimits(opts?.config);
const allowedSymlinkTargetRealPaths = resolveAllowedSkillSymlinkTargetRealPaths(opts?.config);
const loadSkills = (params: { dir: string; source: string }): LoadedSkillRecord[] =>
loadDiscoveredSkillRecords({ ...params, limits, allowedSymlinkTargetRealPaths });
const workspaceOnly = opts?.workspaceOnly === true;
const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
const workspaceSkillsDir = path.resolve(workspaceDir, "skills");
const bundledSkillsDir = workspaceOnly
? undefined
: (opts?.bundledSkillsDir ?? resolveBundledSkillsDir());
const pluginSkillsDir = opts?.pluginSkillsDir ?? path.join(CONFIG_DIR, "plugin-skills");
const extraDirsRaw = workspaceOnly ? [] : (opts?.config?.skills?.load?.extraDirs ?? []);
const extraDirs = normalizeTrimmedStringList(extraDirsRaw);
const pluginSkillDirs = workspaceOnly
? []
: resolvePluginSkillDirs({ workspaceDir, config: opts?.config, pluginSkillsDir });
const mergedExtraDirs = [...extraDirs, ...pluginSkillDirs];
const bundledSkills = bundledSkillsDir
? loadSkills({ dir: bundledSkillsDir, source: "openclaw-bundled" })
: [];
const extraSkills = [
...mergedExtraDirs.flatMap((dir) =>
loadSkills({ dir: resolveUserPath(dir), source: "openclaw-extra" }),
),
...loadGeneratedPluginSkillRecords({
pluginSkillsDir,
pluginSkillDirs,
source: "openclaw-extra",
limits,
}),
];
const managedSkills = workspaceOnly
? []
: loadSkills({ dir: managedSkillsDir, source: "openclaw-managed" });
const osHomeDir = resolveSkillsUserHomeDir();
const personalAgentsSkillsDir = osHomeDir
? path.resolve(osHomeDir, ".agents", "skills")
: path.resolve(".agents", "skills");
const personalAgentsSkills =
workspaceOnly || !isDefaultStateDir()
? []
: loadSkills({ dir: personalAgentsSkillsDir, source: "agents-skills-personal" });
const projectAgentsSkillsDir = path.resolve(workspaceDir, ".agents", "skills");
const projectAgentsSkills = workspaceOnly
? []
: loadSkills({ dir: projectAgentsSkillsDir, source: "agents-skills-project" });
const workspaceSkills = loadSkills({ dir: workspaceSkillsDir, source: "openclaw-workspace" });
const merged = new Map<string, LoadedSkillRecord>();
const archivedSkillFiles = opts?.includeArchived ? null : getArchivedSkillFiles();
const mergeRecord = (record: LoadedSkillRecord) => {
if (archivedSkillFiles?.has(canonicalizePath(record.skill.filePath))) {
return;
}
merged.set(record.skill.name, record);
};
for (const record of extraSkills) {
mergeRecord(record);
}
for (const record of bundledSkills) {
mergeRecord(record);
}
for (const record of managedSkills) {
mergeRecord(record);
}
for (const record of personalAgentsSkills) {
mergeRecord(record);
}
for (const record of projectAgentsSkills) {
mergeRecord(record);
}
for (const record of workspaceSkills) {
mergeRecord(record);
}
return Array.from(merged.values())
.toSorted((a, b) => a.skill.name.localeCompare(b.skill.name, "en"))
.map((record) => {
const skill = record.skill;
const frontmatter =
record.frontmatter ??
readSkillFrontmatterSafe({
rootDir: skill.baseDir,
filePath: skill.filePath,
maxBytes: limits.maxSkillFileBytes,
}) ??
({} as ParsedSkillFrontmatter);
const invocation = resolveSkillInvocationPolicy(frontmatter);
const entry: SkillEntry = {
skill,
frontmatter,
metadata: resolveSkillEntryMetadata({ frontmatter, skillDir: skill.baseDir }),
invocation,
exposure: {
includeInRuntimeRegistry: true,
includeInAvailableSkillsPrompt: !invocation.disableModelInvocation,
userInvocable: invocation.userInvocable ?? true,
},
};
if (record.syncSourceDir !== undefined) {
entry.syncSourceDir = record.syncSourceDir;
}
if (record.syncDirName !== undefined) {
entry.syncDirName = record.syncDirName;
}
return entry;
});
}
function filterArchivedSkillEntries(entries: SkillEntry[]): SkillEntry[] {
const archivedSkillFiles = getArchivedSkillFiles();
return entries.filter((entry) => !archivedSkillFiles.has(canonicalizePath(entry.skill.filePath)));
}
function resolveEffectiveWorkspaceSkillFilter(opts?: {
config?: OpenClawConfig;
agentId?: string;
skillFilter?: string[];
}): string[] | undefined {
if (opts?.skillFilter !== undefined) {
return normalizeSkillFilter(opts.skillFilter);
}
if (!opts?.config || !opts.agentId) {
return undefined;
}
return resolveEffectiveAgentSkillFilter(opts.config, opts.agentId);
}
export function resolveWorkspaceSkillPromptEntries(
workspaceDir: string,
opts?: {
config?: OpenClawConfig;
managedSkillsDir?: string;
bundledSkillsDir?: string;
entries?: SkillEntry[];
agentId?: string;
skillFilter?: string[];
skillOverrides?: Record<string, boolean>;
eligibility?: SkillEligibilityContext;
},
): { eligible: SkillEntry[]; skillFilter: string[] | undefined } {
const skillFilter = resolveEffectiveWorkspaceSkillFilter(opts);
const skillEntries = opts?.entries
? filterArchivedSkillEntries(opts.entries)
: mergeRemoteNodeSkillEntries(loadSkillEntries(workspaceDir, opts), {
canExec: opts?.eligibility?.nodeSkills?.canExec,
node: opts?.eligibility?.nodeSkills?.node,
});
return {
eligible: filterSkillEntries(
skillEntries,
opts?.config,
skillFilter,
opts?.skillOverrides,
opts?.eligibility,
),
skillFilter,
};
}
export function loadWorkspaceSkills(
workspaceDir: string,
opts?: {
config?: OpenClawConfig;
managedSkillsDir?: string;
bundledSkillsDir?: string;
pluginSkillsDir?: string;
skillFilter?: string[];
skillOverrides?: Record<string, boolean>;
agentId?: string;
eligibility?: SkillEligibilityContext;
workspaceOnly?: boolean;
includeArchived?: boolean;
},
): SkillEntry[] {
const entries = mergeRemoteNodeSkillEntries(loadSkillEntries(workspaceDir, opts), {
canExec: opts?.eligibility?.nodeSkills?.canExec,
node: opts?.eligibility?.nodeSkills?.node,
});
const effectiveSkillFilter = resolveEffectiveWorkspaceSkillFilter(opts);
if (
effectiveSkillFilter === undefined &&
opts?.skillOverrides === undefined &&
opts?.eligibility === undefined
) {
return entries;
}
return filterSkillEntries(
entries,
opts?.config,
effectiveSkillFilter,
opts?.skillOverrides,
opts?.eligibility,
);
}
export function loadVisibleSkills(
workspaceDir: string,
opts?: {
config?: OpenClawConfig;
managedSkillsDir?: string;
bundledSkillsDir?: string;
skillFilter?: string[];
skillOverrides?: Record<string, boolean>;
agentId?: string;
eligibility?: SkillEligibilityContext;
},
): SkillEntry[] {
const entries = mergeRemoteNodeSkillEntries(loadSkillEntries(workspaceDir, opts), {
canExec: opts?.eligibility?.nodeSkills?.canExec,
node: opts?.eligibility?.nodeSkills?.node,
});
const effectiveSkillFilter = resolveEffectiveWorkspaceSkillFilter(opts);
return filterSkillEntries(
entries,
opts?.config,
effectiveSkillFilter,
opts?.skillOverrides,
opts?.eligibility,
);
}
export function filterWorkspaceSkills(
entries: SkillEntry[],
opts?: {
config?: OpenClawConfig;
skillFilter?: string[];
skillOverrides?: Record<string, boolean>;
eligibility?: SkillEligibilityContext;
},
): SkillEntry[] {
return filterSkillEntries(
entries,
opts?.config,
opts?.skillFilter,
opts?.skillOverrides,
opts?.eligibility,
);
}
@@ -1,4 +1,4 @@
// Prompt resolution tests cover skill prompt lookup and active skill selection.
// Workspace skill prompt resolution tests cover snapshot reuse and degraded-secret filtering.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -7,15 +7,15 @@ import { setActiveDegradedSecretOwners } from "../../secrets/runtime-degraded-st
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js";
import { WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION, type SkillEntry } from "../types.js";
import { buildWorkspaceSkillSnapshot, resolveSkillsPromptForRun } from "./workspace.js";
import { buildSkillSnapshot, resolveSkillsPrompt } from "./workspace-skill-prompt.js";
afterEach(() => {
setActiveDegradedSecretOwners([]);
});
describe("resolveSkillsPromptForRun", () => {
describe("resolveSkillsPrompt", () => {
it("prefers snapshot prompt when available", () => {
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
skillsSnapshot: { prompt: "SNAPSHOT", skills: [] },
workspaceDir: "/tmp/openclaw",
});
@@ -32,7 +32,7 @@ describe("resolveSkillsPromptForRun", () => {
}),
frontmatter: {},
};
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
entries: [entry],
workspaceDir: "/tmp/openclaw",
});
@@ -53,7 +53,7 @@ describe("resolveSkillsPromptForRun", () => {
};
expect(
resolveSkillsPromptForRun({
resolveSkillsPrompt({
skillsSnapshot: { prompt: "", skills: [] },
entries: [entry],
workspaceDir: "/tmp/openclaw",
@@ -74,7 +74,7 @@ describe("resolveSkillsPromptForRun", () => {
]);
expect(
resolveSkillsPromptForRun({
resolveSkillsPrompt({
skillsSnapshot: {
prompt:
"LEAKED COLD INSTRUCTIONS\n<available_skills>\n <skill>\n <name>cold-skill</name>\n </skill>\n</available_skills>",
@@ -98,7 +98,7 @@ describe("resolveSkillsPromptForRun", () => {
},
]);
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
skillsSnapshot: {
prompt: "LEGACY SKILL PROMPT",
skills: [{ name: "cold-skill" }, { name: "healthy-skill" }],
@@ -131,7 +131,7 @@ describe("resolveSkillsPromptForRun", () => {
}),
frontmatter: {},
};
const snapshot = buildWorkspaceSkillSnapshot("/tmp/openclaw", {
const snapshot = buildSkillSnapshot("/tmp/openclaw", {
entries: [cold, healthy],
});
setActiveDegradedSecretOwners([
@@ -145,7 +145,7 @@ describe("resolveSkillsPromptForRun", () => {
},
]);
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
skillsSnapshot: snapshot,
entries: [cold, healthy],
workspaceDir: "/tmp/openclaw",
@@ -167,7 +167,7 @@ describe("resolveSkillsPromptForRun", () => {
frontmatter: {},
});
const capturedEntries = [createEntry("cold-skill"), createEntry("healthy-skill")];
const snapshot = buildWorkspaceSkillSnapshot("/tmp/openclaw", {
const snapshot = buildSkillSnapshot("/tmp/openclaw", {
entries: capturedEntries,
});
setActiveDegradedSecretOwners([
@@ -181,7 +181,7 @@ describe("resolveSkillsPromptForRun", () => {
},
]);
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
skillsSnapshot: snapshot,
entries: [...capturedEntries, createEntry("new-skill")],
workspaceDir: "/tmp/openclaw",
@@ -204,7 +204,7 @@ describe("resolveSkillsPromptForRun", () => {
name: "healthy-skill",
description: "Captured healthy",
});
const snapshot = buildWorkspaceSkillSnapshot(workspaceDir);
const snapshot = buildSkillSnapshot(workspaceDir);
await writeSkill({
dir: path.join(workspaceDir, "skills", "healthy-skill"),
name: "healthy-skill",
@@ -227,7 +227,7 @@ describe("resolveSkillsPromptForRun", () => {
]);
try {
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
skillsSnapshot: snapshot,
workspaceDir,
});
@@ -255,7 +255,7 @@ describe("resolveSkillsPromptForRun", () => {
frontmatter: {},
};
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
entries: [hidden],
workspaceDir: "/tmp/openclaw",
});
@@ -285,7 +285,7 @@ describe("resolveSkillsPromptForRun", () => {
frontmatter: {},
};
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
entries: [visible, hidden],
config: {
agents: {
@@ -325,7 +325,7 @@ describe("resolveSkillsPromptForRun", () => {
frontmatter: {},
};
const prompt = resolveSkillsPromptForRun({
const prompt = resolveSkillsPrompt({
entries: [inheritedEntry, explicitEntry],
config: {
agents: {
@@ -1,8 +1,10 @@
// Compact format tests cover compact skill prompt serialization.
// Workspace skill prompt tests cover catalog budgets, ordering, and compact paths.
import fsSync from "node:fs";
import os from "node:os";
import { formatSkillsForPrompt as upstreamFormatSkillsForPrompt } from "openclaw/plugin-sdk/agent-sessions";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { withEnv } from "../../test-utils/env.js";
import {
restoreMockSkillsHomeEnv,
setMockSkillsHomeEnv,
@@ -10,12 +12,17 @@ import {
} from "../test-support/home-env.test-support.js";
import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js";
import type { SkillEntry } from "../types.js";
import { formatSkillsForPrompt, type Skill } from "./skill-contract.js";
import {
formatSkillsCompact,
buildWorkspaceSkillsPrompt,
buildWorkspaceSkillSnapshot,
} from "./workspace.js";
formatSkillsCompactForPrompt as formatSkillsCompact,
formatSkillsForPrompt,
type Skill,
} from "./skill-contract.js";
import { buildSkillSnapshot } from "./workspace-skill-prompt.js";
const buildWorkspaceSkillsPrompt = (
workspaceDir: string,
opts?: Parameters<typeof buildSkillSnapshot>[1],
): string => buildSkillSnapshot(workspaceDir, opts).prompt;
function makeSkill(name: string, desc = "A skill", filePath = `/skills/${name}/SKILL.md`): Skill {
return createCanonicalFixtureSkill({
@@ -69,86 +76,6 @@ const COMPACT_OMITTED_NOTICE =
const COMPACT_SHORTENED_NOTICE =
"⚠️ Skills catalog using compact format (descriptions shortened). Run `openclaw skills check` to audit.";
describe("formatSkillsCompact", () => {
it("keeps the full-format XML output aligned with the upstream formatter for visible skills", () => {
const skills = [
{ ...makeSkill("weather", "Get weather <data> & forecasts"), promptVersion: "sha256:abc123" },
makeSkill("notes", "Summarize notes", "/tmp/notes/SKILL.md"),
];
expect(formatSkillsForPrompt(skills)).toBe(upstreamFormatSkillsForPrompt(skills));
});
it("renders all passed skills in the full formatter without reapplying visibility policy", () => {
const hidden: Skill = { ...makeSkill("hidden"), disableModelInvocation: true };
const out = formatSkillsForPrompt([makeSkill("visible"), hidden]);
expect(out).toContain("visible");
expect(out).toContain("hidden");
});
it("returns empty string for no skills", () => {
expect(formatSkillsCompact([])).toBe("");
});
it("keeps compact descriptions with name, location, and version", () => {
const out = formatSkillsCompact([
{ ...makeSkill("weather", "Get weather data"), promptVersion: "sha256:abc123" },
]);
expect(out).toContain("<name>weather</name>");
expect(out).toContain("<description>Get weather data</description>");
expect(out).toContain("<location>/skills/weather/SKILL.md</location>");
expect(out).toContain("<version>sha256:abc123</version>");
});
it("omits descriptions when their compact budget is zero", () => {
const out = formatSkillsCompact([makeSkill("weather", "Get weather data")], {
descriptionMaxChars: 0,
});
expect(out).toContain("<name>weather</name>");
expect(out).not.toContain("<description>");
});
it("preserves location notes when compact descriptions are omitted", () => {
const out = formatSkillsCompact(
[
{
...makeSkill("remote", "Remote skill"),
locationNote: "Load with exec host=node node=node-1.",
},
],
{ descriptionMaxChars: 0 },
);
expect(out).toContain("<location_note>Load with exec host=node node=node-1.</location_note>");
});
it("truncates descriptions without splitting emoji surrogate pairs", () => {
const out = formatSkillsCompact([makeSkill("emoji", `${"A".repeat(16)}😀 trailing`)], {
descriptionMaxChars: 20,
});
expect(out).toContain(`<description>${"A".repeat(16)}...</description>`);
expect(out).not.toMatch(/[\uD800-\uDFFF]/u);
});
it("renders all passed skills without reapplying visibility policy", () => {
const hidden: Skill = { ...makeSkill("hidden"), disableModelInvocation: true };
const out = formatSkillsCompact([makeSkill("visible"), hidden]);
expect(out).toContain("visible");
expect(out).toContain("hidden");
});
it("escapes XML special characters", () => {
const out = formatSkillsCompact([makeSkill("a<b&c")]);
expect(out).toContain("a&lt;b&amp;c");
});
it("is significantly smaller than full format", () => {
const skills = Array.from({ length: 50 }, (_, i) => makeSkill(`skill-${i}`, "A".repeat(800)));
const compact = formatSkillsCompact(skills);
expect(compact.length).toBeLessThan(formatSkillsForPrompt(skills).length / 2);
});
});
describe("applySkillsPromptLimits (via buildWorkspaceSkillsPrompt)", () => {
let envSnapshot: SkillsHomeEnvSnapshot;
@@ -394,7 +321,7 @@ describe("applySkillsPromptLimits (via buildWorkspaceSkillsPrompt)", () => {
const skills = Array.from({ length: 5 }, (_, i) =>
makeSkill(`skill-${i}`, "A skill", `${home}/.openclaw/workspace/skills/skill-${i}/SKILL.md`),
);
const snapshot = buildWorkspaceSkillSnapshot("/fake", {
const snapshot = buildSkillSnapshot("/fake", {
entries: skills.map(makeEntry),
});
// Prompt should use compacted paths
@@ -407,3 +334,188 @@ describe("applySkillsPromptLimits (via buildWorkspaceSkillsPrompt)", () => {
}
});
});
describe("compactSkillPaths", () => {
function buildPromptForFixtureSkill(params: {
workspaceRoot: string;
skillDir: string;
name: string;
description: string;
}) {
return buildWorkspaceSkillsPrompt(params.workspaceRoot, {
entries: [
{
skill: createCanonicalFixtureSkill({
name: params.name,
description: params.description,
filePath: path.join(params.skillDir, "SKILL.md"),
baseDir: params.skillDir,
source: "test",
}),
frontmatter: {},
metadata: undefined,
invocation: { disableModelInvocation: false, userInvocable: true },
exposure: {
includeInRuntimeRegistry: true,
includeInAvailableSkillsPrompt: true,
userInvocable: true,
},
},
],
});
}
it("replaces home directory prefix with ~ in skill locations", () => {
const home = os.homedir();
const skillDir = path.join(home, ".openclaw-test-skills", "test-skill");
const prompt = buildPromptForFixtureSkill({
workspaceRoot: home,
skillDir,
name: "test-skill",
description: "A test skill for path compaction",
});
expect(prompt).not.toContain(home + path.sep);
expect(prompt).toContain("~/");
expect(prompt).toContain("test-skill");
expect(prompt).toContain("A test skill for path compaction");
});
it("does not compact explicit state-root managed skill paths to OS-home tilde paths", () => {
const root = path.parse(os.homedir()).root;
const osHome = path.join(root, "data");
const stateDir = path.join(osHome, ".openclaw");
const skillDir = path.join(stateDir, "skills", "world-cup-soccer-openclaw-skill");
const skillFile = path.join(skillDir, "SKILL.md");
const prompt = withEnv(
{
HOME: osHome,
OPENCLAW_HOME: osHome,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
},
() =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(root, "workspace"),
skillDir,
name: "world-cup-soccer-openclaw-skill",
description: "World Cup standings lookup",
}),
);
expect(prompt).toContain(`<location>${skillFile}</location>`);
expect(prompt).not.toContain("~/.openclaw/skills/world-cup-soccer-openclaw-skill/SKILL.md");
});
it("does not compact explicit state-root plugin skill paths to OS-home tilde paths", () => {
const root = path.parse(os.homedir()).root;
const osHome = path.join(root, "data");
const stateDir = path.join(osHome, ".openclaw");
const skillDir = path.join(stateDir, "plugin-skills", "calendar-plugin-skill");
const skillFile = path.join(skillDir, "SKILL.md");
const prompt = withEnv(
{
HOME: osHome,
OPENCLAW_HOME: osHome,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
},
() =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(root, "workspace"),
skillDir,
name: "calendar-plugin-skill",
description: "Calendar plugin skill",
}),
);
expect(prompt).toContain(`<location>${skillFile}</location>`);
expect(prompt).not.toContain("~/.openclaw/plugin-skills/calendar-plugin-skill/SKILL.md");
});
it("compacts managed skill paths when OS-home tilde reaches the same path", () => {
const home = os.homedir();
const stateDir = path.join(home, ".openclaw");
const skillDir = path.join(stateDir, "skills", "home-managed-skill");
const prompt = withEnv(
{
HOME: home,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_HOME: undefined,
},
() =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(home, "workspace"),
skillDir,
name: "home-managed-skill",
description: "Home managed skill",
}),
);
expect(prompt).toContain("<location>~/.openclaw/skills/home-managed-skill/SKILL.md</location>");
expect(prompt).not.toContain(`<location>${path.join(skillDir, "SKILL.md")}</location>`);
});
it("preserves POSIX literal backslashes after home compaction", () => {
const home = os.homedir();
const skillDir = path.join(home, ".openclaw-test-skills\\literal-skill");
const prompt = buildPromptForFixtureSkill({
workspaceRoot: home,
skillDir,
name: "literal-skill",
description: "POSIX literal backslash skill",
});
const locationMatch = prompt.match(/<location>([^<]+)<\/location>/);
if (!locationMatch) {
throw new Error("expected prompt location tag");
}
expect(locationMatch[1]).toContain("~/");
expect(locationMatch[1]).toContain("\\literal-skill");
});
it("preserves paths outside home directory", () => {
const outsideHome = path.join(path.parse(os.homedir()).root, "openclaw-external-skills");
const skillDir = path.join(outsideHome, "skills", "ext-skill");
const prompt = buildPromptForFixtureSkill({
workspaceRoot: outsideHome,
skillDir,
name: "ext-skill",
description: "External skill",
});
expect(prompt).toMatch(/<location>[^<]+SKILL\.md<\/location>/);
expect(prompt).toContain(path.join(skillDir, "SKILL.md"));
});
it("loads skills when the shared state database is unavailable", () => {
const root = fsSync.realpathSync(
fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-skill-load-")),
);
const blockedParent = path.join(root, "state-blocker");
fsSync.writeFileSync(blockedParent, "not a directory", "utf8");
const skillDir = path.join(root, "workspace", "skills", "available-skill");
try {
const prompt = withEnv({ OPENCLAW_STATE_DIR: path.join(blockedParent, "state") }, () =>
buildPromptForFixtureSkill({
workspaceRoot: path.join(root, "workspace"),
skillDir,
name: "available-skill",
description: "Available despite missing lifecycle state",
}),
);
expect(prompt).toContain("available-skill");
expect(prompt).toContain("Available despite missing lifecycle state");
} finally {
fsSync.rmSync(root, { recursive: true, force: true });
}
});
});
@@ -0,0 +1,345 @@
// Workspace skill prompt helpers render bounded catalogs and reusable snapshots.
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { resolveEffectiveAgentSkillsLimits } from "../discovery/agent-filter.js";
import { filterPromptVisibleSkillEntries } from "../discovery/skill-index.js";
import type { SkillEligibilityContext, SkillEntry, SkillSnapshot } from "../types.js";
import { WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION } from "../types.js";
import { hasUnavailableSkillSecretOwners, isSkillSecretOwnerUnavailable } from "./config.js";
import { resolveSkillKey } from "./frontmatter.js";
import {
escapeSkillXml,
formatSkillsCompactForPrompt,
formatSkillsForPrompt,
type Skill,
} from "./skill-contract.js";
import { compactPromptSkills } from "./skill-paths.js";
import { resolveWorkspaceSkillPromptEntries } from "./workspace-skill-loader.js";
const COMPACT_DESCRIPTION_MAX_CHARS = 220;
const COMPACT_DESCRIPTION_MIN_CHARS = 4;
const DEFAULT_MAX_SKILLS_IN_PROMPT = 150;
const DEFAULT_MAX_SKILLS_PROMPT_CHARS = 18_000;
type ResolvedSkillsPromptLimits = {
maxSkillsInPrompt: number;
maxSkillsPromptChars: number;
};
function resolveSkillsPromptLimits(
config?: OpenClawConfig,
agentId?: string,
): ResolvedSkillsPromptLimits {
const limits = config?.skills?.limits;
const agentLimits = resolveEffectiveAgentSkillsLimits(config, agentId);
return {
maxSkillsInPrompt: limits?.maxSkillsInPrompt ?? DEFAULT_MAX_SKILLS_IN_PROMPT,
maxSkillsPromptChars:
agentLimits?.maxSkillsPromptChars ??
limits?.maxSkillsPromptChars ??
DEFAULT_MAX_SKILLS_PROMPT_CHARS,
};
}
type SkillsPromptFormat = { kind: "full" } | { kind: "compact"; descriptionMaxChars: number };
function buildSkillsLimitNote(params: {
truncated: boolean;
format: SkillsPromptFormat;
included: number;
total: number;
}): string {
if (params.truncated) {
const compactDetails =
params.format.kind === "compact"
? ` (compact format, ${params.format.descriptionMaxChars > 0 ? "descriptions shortened" : "descriptions omitted"})`
: "";
return `⚠️ Skills truncated: included ${params.included} of ${params.total}${compactDetails}. Run \`openclaw skills check\` to audit.`;
}
if (params.format.kind === "compact") {
const compactDetails =
params.format.descriptionMaxChars > 0 ? "descriptions shortened" : "descriptions omitted";
return `⚠️ Skills catalog using compact format (${compactDetails}). Run \`openclaw skills check\` to audit.`;
}
return "";
}
function buildRenderedSkillsPrompt(params: {
remoteNote?: string;
skills: Skill[];
total: number;
format: SkillsPromptFormat;
includeLimitNote?: boolean;
}): string {
// resolveCodeModeSkills in src/agents/code-mode-skills.ts parses this exact format; update both together.
// The production-renderer parity test in src/agents/code-mode.test.ts enforces this coupling.
const truncated = params.skills.length < params.total;
const limitNote =
params.includeLimitNote === false
? ""
: buildSkillsLimitNote({
truncated,
format: params.format,
included: params.skills.length,
total: params.total,
});
const catalog =
params.format.kind === "compact"
? formatSkillsCompactForPrompt(params.skills, {
descriptionMaxChars: params.format.descriptionMaxChars,
})
: formatSkillsForPrompt(params.skills);
return [params.remoteNote, limitNote, catalog].filter(Boolean).join("\n");
}
function applySkillsPromptLimits(params: {
skills: Skill[];
config?: OpenClawConfig;
agentId?: string;
remoteNote?: string;
}): string {
const limits = resolveSkillsPromptLimits(params.config, params.agentId);
const total = params.skills.length;
const byCount = params.skills.slice(0, Math.max(0, limits.maxSkillsInPrompt));
let skillsForPrompt = byCount;
const renderWithinLimit = (
skills: Skill[],
format: SkillsPromptFormat,
includeLimitNote = true,
): string | undefined => {
const remoteNotes = params.remoteNote ? [params.remoteNote, undefined] : [undefined];
for (const remoteNote of remoteNotes) {
const prompt = buildRenderedSkillsPrompt({
remoteNote,
skills,
total,
format,
includeLimitNote,
});
if (prompt.length <= limits.maxSkillsPromptChars) {
return prompt;
}
}
return undefined;
};
const fitsFull = (skills: Skill[], includeLimitNote = true): boolean =>
renderWithinLimit(skills, { kind: "full" }, includeLimitNote) !== undefined;
const fitsCompact = (
skills: Skill[],
descriptionMaxChars: number,
includeLimitNote = true,
): boolean =>
renderWithinLimit(skills, { kind: "compact", descriptionMaxChars }, includeLimitNote) !==
undefined;
if (!fitsFull(skillsForPrompt)) {
if (!fitsCompact(skillsForPrompt, 0)) {
let lo = 0;
let hi = skillsForPrompt.length;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (fitsCompact(skillsForPrompt.slice(0, mid), 0)) {
lo = mid;
} else {
hi = mid - 1;
}
}
skillsForPrompt = skillsForPrompt.slice(0, lo);
}
if (skillsForPrompt.length === 0 && byCount.length > 0) {
const fullWithoutNotice = renderWithinLimit(byCount, { kind: "full" }, false);
if (fullWithoutNotice !== undefined) {
return fullWithoutNotice;
}
let lo = 0;
let hi = byCount.length;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (fitsCompact(byCount.slice(0, mid), 0, false)) {
lo = mid;
} else {
hi = mid - 1;
}
}
if (lo > 0) {
skillsForPrompt = byCount.slice(0, lo);
}
}
const includeLimitNote = fitsCompact(skillsForPrompt, 0);
let descriptionMaxChars = 0;
if (
skillsForPrompt.length > 0 &&
fitsCompact(skillsForPrompt, COMPACT_DESCRIPTION_MIN_CHARS, includeLimitNote)
) {
let lo = COMPACT_DESCRIPTION_MIN_CHARS;
let hi = COMPACT_DESCRIPTION_MAX_CHARS;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (fitsCompact(skillsForPrompt, mid, includeLimitNote)) {
lo = mid;
} else {
hi = mid - 1;
}
}
descriptionMaxChars = lo;
}
return (
renderWithinLimit(
skillsForPrompt,
{ kind: "compact", descriptionMaxChars },
includeLimitNote,
) ?? ""
);
}
return renderWithinLimit(skillsForPrompt, { kind: "full" }) ?? "";
}
type WorkspaceSkillBuildOptions = {
config?: OpenClawConfig;
managedSkillsDir?: string;
bundledSkillsDir?: string;
entries?: SkillEntry[];
agentId?: string;
skillFilter?: string[];
skillOverrides?: Record<string, boolean>;
eligibility?: SkillEligibilityContext;
};
function resolveWorkspaceSkillPromptState(
workspaceDir: string,
opts?: WorkspaceSkillBuildOptions,
): { eligible: SkillEntry[]; prompt: string; resolvedSkills: Skill[]; skillFilter?: string[] } {
const { eligible, skillFilter } = resolveWorkspaceSkillPromptEntries(workspaceDir, opts);
const promptEntries = filterPromptVisibleSkillEntries(eligible);
const remoteNote = opts?.eligibility?.remote?.note?.trim();
const resolvedSkills = promptEntries.map((entry) => entry.skill);
const promptSkills = compactPromptSkills(resolvedSkills).toSorted((a, b) =>
a.name.localeCompare(b.name, "en"),
);
const prompt = applySkillsPromptLimits({
skills: promptSkills,
config: opts?.config,
agentId: opts?.agentId,
remoteNote,
});
return { eligible, prompt, resolvedSkills, skillFilter };
}
export function buildSkillSnapshot(
workspaceDir: string,
opts?: WorkspaceSkillBuildOptions & { snapshotVersion?: number },
): SkillSnapshot {
const { eligible, prompt, resolvedSkills, skillFilter } = resolveWorkspaceSkillPromptState(
workspaceDir,
opts,
);
return {
prompt,
skills: eligible.map((entry) => ({
name: entry.skill.name,
skillKey: resolveSkillKey(entry.skill, entry),
primaryEnv: entry.metadata?.primaryEnv,
requiredEnv: entry.metadata?.requires?.env?.slice(),
})),
...(skillFilter === undefined ? {} : { skillFilter }),
...(opts?.skillOverrides ? { skillOverrides: opts.skillOverrides } : {}),
...(opts?.eligibility?.nodeSkills
? { nodeSkillsEligibility: opts.eligibility.nodeSkills }
: {}),
resolvedSkills,
version: opts?.snapshotVersion,
promptFormatVersion: WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION,
};
}
export function resolveSkillsPrompt(params: {
skillsSnapshot?: SkillSnapshot;
entries?: SkillEntry[];
config?: OpenClawConfig;
workspaceDir: string;
agentId?: string;
eligibility?: SkillEligibilityContext;
}): string {
const snapshotPrompt = params.skillsSnapshot?.prompt?.trim();
if (params.skillsSnapshot && !snapshotPrompt) {
return "";
}
const snapshotHasLegacySkillIdentity = params.skillsSnapshot?.skills.some(
(skill) => !skill.skillKey,
);
if (snapshotPrompt) {
const snapshotHasUnavailableSkill =
params.skillsSnapshot?.skills.some((skill) =>
isSkillSecretOwnerUnavailable(skill.skillKey ?? skill.name),
) ||
(snapshotHasLegacySkillIdentity && hasUnavailableSkillSecretOwners());
if (
snapshotHasUnavailableSkill &&
params.skillsSnapshot?.promptFormatVersion !== WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION
) {
return "";
}
if (snapshotHasLegacySkillIdentity && hasUnavailableSkillSecretOwners()) {
return "";
}
const unavailableNames = new Set(
params.skillsSnapshot?.skills
.filter(
(skill) => skill.skillKey !== undefined && isSkillSecretOwnerUnavailable(skill.skillKey),
)
.map((skill) => escapeSkillXml(skill.name)),
);
if (unavailableNames.size === 0) {
return snapshotPrompt;
}
const catalogOpen = "<available_skills>";
const catalogClose = "</available_skills>";
const catalogStart = snapshotPrompt.indexOf(catalogOpen);
const catalogEnd = snapshotPrompt.indexOf(catalogClose, catalogStart + catalogOpen.length);
if (
catalogStart < 0 ||
catalogEnd < 0 ||
snapshotPrompt.includes(catalogOpen, catalogStart + catalogOpen.length) ||
snapshotPrompt.includes(catalogClose, catalogEnd + catalogClose.length)
) {
return "";
}
const bodyStart = catalogStart + catalogOpen.length;
const catalogBody = snapshotPrompt.slice(bodyStart, catalogEnd);
const blockPattern = /\n[ ]{2}<skill>\n[\s\S]*?\n[ ]{2}<\/skill>/g;
let cursor = 0;
let filteredBody = "";
for (const match of catalogBody.matchAll(blockPattern)) {
const gap = catalogBody.slice(cursor, match.index);
const block = match[0];
const name = /^[ ]{4}<name>(.*)<\/name>$/m.exec(block)?.[1];
if (gap.trim() || !name) {
return "";
}
filteredBody += gap;
if (!unavailableNames.has(name)) {
filteredBody += block;
}
cursor = (match.index ?? 0) + block.length;
}
const tail = catalogBody.slice(cursor);
if (tail.trim()) {
return "";
}
return `${snapshotPrompt.slice(0, bodyStart)}${filteredBody}${tail}${snapshotPrompt.slice(catalogEnd)}`.trim();
}
if (params.entries && params.entries.length > 0) {
const prompt = buildSkillSnapshot(params.workspaceDir, {
entries: params.entries,
config: params.config,
agentId: params.agentId,
eligibility: params.eligibility,
}).prompt;
return prompt.trim() ? prompt : "";
}
return "";
}
@@ -11,7 +11,7 @@ import {
setMockSkillsHomeEnv,
type SkillsHomeEnvSnapshot,
} from "../test-support/home-env.test-support.js";
import { buildWorkspaceSkillSnapshot, buildWorkspaceSkillsPrompt } from "./workspace.js";
import { buildSkillSnapshot } from "./workspace-skill-prompt.js";
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
@@ -19,7 +19,6 @@ vi.mock("./plugin-skills.js", () => ({
const fixtureSuite = createFixtureSuite("openclaw-skills-snapshot-suite-");
let truncationWorkspaceTemplateDir = "";
let nestedRepoTemplateDir = "";
let tempHome: TempHomeEnv | null = null;
let skillsHomeEnv: SkillsHomeEnvSnapshot | null = null;
@@ -38,16 +37,6 @@ beforeAll(async () => {
description: "x".repeat(800),
});
}
nestedRepoTemplateDir = await fixtureSuite.createCaseDir("template-skills-repo");
for (let i = 0; i < 8; i += 1) {
const name = `repo-skill-${String(i).padStart(2, "0")}`;
await writeSkill({
dir: path.join(nestedRepoTemplateDir, "skills", name),
name,
description: `Desc ${i}`,
});
}
});
afterAll(async () => {
@@ -66,12 +55,9 @@ function withWorkspaceHome<T>(workspaceDir: string, cb: () => T): T {
return withPathResolutionEnv(workspaceDir, { PATH: "" }, () => cb());
}
function buildSnapshot(
workspaceDir: string,
options?: Parameters<typeof buildWorkspaceSkillSnapshot>[1],
) {
function buildSnapshot(workspaceDir: string, options?: Parameters<typeof buildSkillSnapshot>[1]) {
return withWorkspaceHome(workspaceDir, () =>
buildWorkspaceSkillSnapshot(workspaceDir, {
buildSkillSnapshot(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
...options,
@@ -86,7 +72,7 @@ async function cloneTemplateDir(templateDir: string, prefix: string): Promise<st
}
function expectSnapshotNamesAndPrompt(
snapshot: ReturnType<typeof buildWorkspaceSkillSnapshot>,
snapshot: ReturnType<typeof buildSkillSnapshot>,
params: { contains?: string[]; omits?: string[] },
) {
for (const name of params.contains ?? []) {
@@ -99,7 +85,7 @@ function expectSnapshotNamesAndPrompt(
}
}
describe("buildWorkspaceSkillSnapshot", () => {
describe("buildSkillSnapshot", () => {
it("returns an empty snapshot when skills dirs are missing", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
@@ -125,7 +111,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
await fs.mkdir(path.join(home, ".agents"), { recursive: true });
await fs.symlink(compatibilitySkillsDir, path.join(home, ".agents", "skills"), "dir");
const buildHomeSnapshot = () =>
buildWorkspaceSkillSnapshot(workspaceDir, {
buildSkillSnapshot(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
});
@@ -172,7 +158,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
expect(snapshot.skills.map((skill) => skill.name)).toContain("visible-skill");
});
it("keeps prompt output aligned with buildWorkspaceSkillsPrompt", async () => {
it("keeps prompt output stable across equivalent snapshot builds", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
await writeSkill({
dir: path.join(workspaceDir, "skills", "visible"),
@@ -207,11 +193,10 @@ describe("buildWorkspaceSkillSnapshot", () => {
},
};
const snapshot = withWorkspaceHome(workspaceDir, () =>
buildWorkspaceSkillSnapshot(workspaceDir, opts),
);
const prompt = withWorkspaceHome(workspaceDir, () =>
buildWorkspaceSkillsPrompt(workspaceDir, opts),
const snapshot = withWorkspaceHome(workspaceDir, () => buildSkillSnapshot(workspaceDir, opts));
const prompt = withWorkspaceHome(
workspaceDir,
() => buildSkillSnapshot(workspaceDir, opts).prompt,
);
expect(snapshot.prompt).toBe(prompt);
@@ -248,7 +233,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
const workspaceDir = await cloneTemplateDir(truncationWorkspaceTemplateDir, "workspace");
const snapshot = withWorkspaceHome(workspaceDir, () =>
buildWorkspaceSkillSnapshot(workspaceDir, {
buildSkillSnapshot(workspaceDir, {
config: {
skills: {
limits: {
@@ -292,136 +277,4 @@ describe("buildWorkspaceSkillSnapshot", () => {
]);
expect(snapshot.skillFilter).toEqual(["docs-search", "github"]);
});
it("limits discovery for nested repo-style skills roots (dir/skills/*)", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const repoDir = await cloneTemplateDir(nestedRepoTemplateDir, "skills-repo");
const snapshot = withWorkspaceHome(workspaceDir, () =>
buildWorkspaceSkillSnapshot(workspaceDir, {
config: {
skills: {
load: {
extraDirs: [repoDir],
},
limits: {
maxCandidatesPerRoot: 5,
maxSkillsLoadedPerSource: 5,
},
},
},
managedSkillsDir: path.join(workspaceDir, ".managed"),
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
}),
);
const skillNames = snapshot.skills.map((skill) => skill.name);
expect(skillNames).toStrictEqual([
"repo-skill-00",
"repo-skill-01",
"repo-skill-02",
"repo-skill-03",
"repo-skill-04",
]);
for (const name of skillNames) {
expect(snapshot.prompt).toContain(name);
}
});
it("skips skills whose SKILL.md exceeds maxSkillFileBytes", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
await writeSkill({
dir: path.join(workspaceDir, "skills", "small-skill"),
name: "small-skill",
description: "Small",
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "big-skill"),
name: "big-skill",
description: "Big",
body: "x".repeat(5_000),
});
const snapshot = buildSnapshot(workspaceDir, {
config: {
skills: {
limits: {
maxSkillFileBytes: 1000,
},
},
},
});
expectSnapshotNamesAndPrompt(snapshot, {
contains: ["small-skill"],
omits: ["big-skill"],
});
});
it("detects nested skills roots beyond the first 25 entries", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const repoDir = await fixtureSuite.createCaseDir("skills-repo");
// Create 30 nested dirs, but only the last one is an actual skill.
for (let i = 0; i < 30; i += 1) {
await fs.mkdir(path.join(repoDir, "skills", `entry-${String(i).padStart(2, "0")}`), {
recursive: true,
});
}
await writeSkill({
dir: path.join(repoDir, "skills", "entry-29"),
name: "late-skill",
description: "Nested skill discovered late",
});
const snapshot = buildSnapshot(workspaceDir, {
config: {
skills: {
load: {
extraDirs: [repoDir],
},
limits: {
maxCandidatesPerRoot: 30,
maxSkillsLoadedPerSource: 30,
},
},
},
});
expectSnapshotNamesAndPrompt(snapshot, {
contains: ["late-skill"],
});
});
it("enforces maxSkillFileBytes for root-level SKILL.md", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const rootSkillDir = await fixtureSuite.createCaseDir("root-skill");
await writeSkill({
dir: rootSkillDir,
name: "root-big-skill",
description: "Big",
body: "x".repeat(5_000),
});
const snapshot = buildSnapshot(workspaceDir, {
config: {
skills: {
load: {
extraDirs: [rootSkillDir],
},
limits: {
maxSkillFileBytes: 1000,
},
},
},
});
expectSnapshotNamesAndPrompt(snapshot, {
omits: ["root-big-skill"],
});
});
});
@@ -1,4 +1,4 @@
// Workspace sync tests cover skill synchronization between workspace and runtime state.
// Workspace skill sync runtime tests cover sandbox synchronization and plugin-provided skills.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -6,14 +6,13 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { withEnv, withEnvAsync } from "../../test-utils/env.js";
import { bumpSkillsSnapshotVersion, getSkillsSnapshotVersion } from "../runtime/refresh-state.js";
import { writeSkill } from "../test-support/e2e-test-helpers.js";
import {
buildWorkspaceSkillSnapshot,
buildWorkspaceSkillsPrompt,
syncSkillsToWorkspace,
} from "./workspace.js";
import { buildSkillSnapshot } from "./workspace-skill-prompt.js";
import { syncWorkspaceSkills } from "./workspace-skill-sync.runtime.js";
const mockResolvePluginSkillDirs = vi.hoisted(() => vi.fn(() => [] as string[]));
vi.mock("./plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
resolvePluginSkillDirs: mockResolvePluginSkillDirs,
}));
async function pathExists(filePath: string): Promise<boolean> {
@@ -36,7 +35,7 @@ async function createCaseDir(prefix: string): Promise<string> {
}
async function syncSourceSkillsToTarget(sourceWorkspace: string, targetWorkspace: string) {
await syncSkillsToWorkspace({
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
@@ -44,6 +43,13 @@ async function syncSourceSkillsToTarget(sourceWorkspace: string, targetWorkspace
});
}
function buildWorkspaceSkillsPrompt(
workspaceDir: string,
opts?: Parameters<typeof buildSkillSnapshot>[1],
): string {
return buildSkillSnapshot(workspaceDir, opts).prompt;
}
async function expectSyncedSkillConfinement(params: {
sourceWorkspace: string;
targetWorkspace: string;
@@ -91,7 +97,7 @@ afterAll(async () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
describe("buildWorkspaceSkillsPrompt", () => {
describe("syncWorkspaceSkills", () => {
const buildPrompt = (
workspaceDir: string,
opts?: Parameters<typeof buildWorkspaceSkillsPrompt>[1],
@@ -126,7 +132,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
"export {}",
);
const skillUsagePaths = await syncSkillsToWorkspace({
const skillUsagePaths = await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
config: { skills: { load: { extraDirs: [extraDir] } } },
@@ -177,7 +183,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
description: "Prompt-hidden skill",
frontmatterExtra: "disable-model-invocation: true",
});
const skillsSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const skillsSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
snapshotVersion: getSkillsSnapshotVersion(sourceWorkspace),
@@ -192,10 +198,10 @@ describe("buildWorkspaceSkillsPrompt", () => {
skillsSnapshot,
};
const first = await syncSkillsToWorkspace(params);
const first = await syncWorkspaceSkills(params);
await fs.rm(path.join(sourceWorkspace, "skills"), { recursive: true, force: true });
const copy = vi.spyOn(fs, "cp");
const second = await syncSkillsToWorkspace(params);
const second = await syncWorkspaceSkills(params);
const copyCount = copy.mock.calls.length;
copy.mockRestore();
@@ -217,7 +223,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
description: `${name} skill`,
});
}
const skillsSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const skillsSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
snapshotVersion: getSkillsSnapshotVersion(sourceWorkspace),
@@ -229,7 +235,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
managedSkillsDir,
skillsSnapshot,
};
await syncSkillsToWorkspace(syncParams);
await syncWorkspaceSkills(syncParams);
const targetSkillsDir = path.join(targetWorkspace, "skills");
const manifestPath = path.join(targetSkillsDir, ".openclaw-sync.json");
@@ -246,7 +252,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
);
const copy = vi.spyOn(fs, "cp");
const usagePaths = await syncSkillsToWorkspace(syncParams);
const usagePaths = await syncWorkspaceSkills(syncParams);
const copyCount = copy.mock.calls.length;
copy.mockRestore();
@@ -273,13 +279,13 @@ describe("buildWorkspaceSkillsPrompt", () => {
});
}
const snapshotVersion = getSkillsSnapshotVersion(sourceWorkspace);
const firstSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const firstSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
skillFilter: ["alpha", "beta"],
snapshotVersion,
});
await syncSkillsToWorkspace({
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
bundledSkillsDir,
@@ -290,14 +296,14 @@ describe("buildWorkspaceSkillsPrompt", () => {
const preservedMarker = path.join(targetWorkspace, "skills", "alpha", "preserved.txt");
await fs.writeFile(preservedMarker, "preserved");
const secondSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const secondSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
skillFilter: ["alpha", "gamma"],
snapshotVersion,
});
const copy = vi.spyOn(fs, "cp");
await syncSkillsToWorkspace({
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
bundledSkillsDir,
@@ -323,12 +329,12 @@ describe("buildWorkspaceSkillsPrompt", () => {
await writeSkill({ dir: sourceSkillDir, name: "alpha", description: "Alpha skill" });
await fs.writeFile(path.join(sourceSkillDir, "asset.txt"), "before");
await fs.writeFile(path.join(sourceSkillDir, "removed.txt"), "stale");
const firstSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const firstSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
snapshotVersion: getSkillsSnapshotVersion(sourceWorkspace),
});
await syncSkillsToWorkspace({
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
bundledSkillsDir,
@@ -339,12 +345,12 @@ describe("buildWorkspaceSkillsPrompt", () => {
await fs.writeFile(path.join(sourceSkillDir, "asset.txt"), "after");
await fs.rm(path.join(sourceSkillDir, "removed.txt"));
const nextVersion = bumpSkillsSnapshotVersion({ workspaceDir: sourceWorkspace });
const secondSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const secondSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
snapshotVersion: nextVersion,
});
await syncSkillsToWorkspace({
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
bundledSkillsDir,
@@ -368,7 +374,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
const sourceSkillDir = path.join(sourceWorkspace, "skills", "alpha");
await writeSkill({ dir: sourceSkillDir, name: "alpha", description: "Alpha skill" });
await fs.writeFile(path.join(sourceSkillDir, "asset.txt"), "before");
const firstSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const firstSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
snapshotVersion: getSkillsSnapshotVersion(sourceWorkspace),
@@ -380,22 +386,22 @@ describe("buildWorkspaceSkillsPrompt", () => {
managedSkillsDir,
skillsSnapshot: firstSnapshot,
};
await syncSkillsToWorkspace(syncParams);
await syncWorkspaceSkills(syncParams);
await fs.writeFile(path.join(sourceSkillDir, "asset.txt"), "after");
const nextVersion = bumpSkillsSnapshotVersion({ workspaceDir: sourceWorkspace });
const secondSnapshot = buildWorkspaceSkillSnapshot(sourceWorkspace, {
const secondSnapshot = buildSkillSnapshot(sourceWorkspace, {
bundledSkillsDir,
managedSkillsDir,
snapshotVersion: nextVersion,
});
const copy = vi.spyOn(fs, "cp").mockRejectedValueOnce(new Error("injected copy failure"));
await syncSkillsToWorkspace({ ...syncParams, skillsSnapshot: secondSnapshot });
await syncWorkspaceSkills({ ...syncParams, skillsSnapshot: secondSnapshot });
copy.mockRestore();
const manifestPath = path.join(targetWorkspace, "skills", ".openclaw-sync.json");
expect(await pathExists(manifestPath)).toBe(false);
await syncSkillsToWorkspace({ ...syncParams, skillsSnapshot: secondSnapshot });
await syncWorkspaceSkills({ ...syncParams, skillsSnapshot: secondSnapshot });
expect(await pathExists(manifestPath)).toBe(true);
expect(
await fs.readFile(path.join(targetWorkspace, "skills", "alpha", "asset.txt"), "utf8"),
@@ -404,7 +410,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
const interruptedTemp = path.join(targetWorkspace, "skills", ".openclaw-sync.interrupted.tmp");
await fs.rm(manifestPath);
await fs.writeFile(interruptedTemp, "partial");
await syncSkillsToWorkspace({ ...syncParams, skillsSnapshot: secondSnapshot });
await syncWorkspaceSkills({ ...syncParams, skillsSnapshot: secondSnapshot });
expect(await pathExists(interruptedTemp)).toBe(false);
expect(await pathExists(manifestPath)).toBe(true);
});
@@ -442,7 +448,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
description: "Dot variant",
});
await syncSkillsToWorkspace({
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
agentId: "alpha",
@@ -613,7 +619,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
metadata: '{"openclaw":{"requires":{"anyBins":["missingbin","sandboxbin"]}}}',
});
await syncSkillsToWorkspace({
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
agentId: "alpha",
@@ -661,7 +667,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
);
await withEnvAsync({ HOME: sourceWorkspace }, () =>
syncSkillsToWorkspace({
syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
@@ -683,3 +689,154 @@ describe("buildWorkspaceSkillsPrompt", () => {
).toContain("Managed symlink target");
});
});
describe("syncWorkspaceSkills for plugin skills", () => {
it("syncs plugin skills from symlinked directories to sandbox workspace", async () => {
const sourceWorkspace = await createCaseDir("source");
const targetWorkspace = await createCaseDir("target");
const realPluginSkillDir = await createCaseDir("real-plugin-skill");
await writeSkill({
dir: realPluginSkillDir,
name: "wiki-maintainer",
description: "Wiki maintenance skill for sandboxed agents",
});
const pluginSkillsDir = path.join(sourceWorkspace, ".openclaw", "plugin-skills");
await fs.mkdir(pluginSkillsDir, { recursive: true });
const symlinkPath = path.join(pluginSkillsDir, "wiki-maintainer");
await fs.symlink(
realPluginSkillDir,
symlinkPath,
process.platform === "win32" ? "junction" : "dir",
);
mockResolvePluginSkillDirs.mockReturnValueOnce([realPluginSkillDir]);
const skillUsagePaths = await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
pluginSkillsDir,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
managedSkillsDir: path.join(sourceWorkspace, ".managed"),
});
const syncedSkillDir = path.join(targetWorkspace, "skills", "wiki-maintainer");
const syncedSkillMd = path.join(syncedSkillDir, "SKILL.md");
const syncedStat = await fs.lstat(syncedSkillDir);
const prompt = buildWorkspaceSkillsPrompt(targetWorkspace, {
bundledSkillsDir: path.join(targetWorkspace, ".bundled"),
managedSkillsDir: path.join(targetWorkspace, ".managed"),
}).replaceAll("\\", "/");
expect(await pathExists(syncedSkillMd)).toBe(true);
expect(syncedStat.isSymbolicLink()).toBe(false);
expect(prompt).toContain("Wiki maintenance skill for sandboxed agents");
expect(prompt).toContain("skills/wiki-maintainer/SKILL.md");
expect(prompt).not.toContain(realPluginSkillDir.replaceAll("\\", "/"));
expect(prompt).not.toContain(pluginSkillsDir.replaceAll("\\", "/"));
expect(prompt).not.toContain(symlinkPath.replaceAll("\\", "/"));
expect(skillUsagePaths).toEqual([
{
readPath: syncedSkillMd,
skillFile: path.join(realPluginSkillDir, "SKILL.md"),
skillName: "wiki-maintainer",
skillSource: "workspace",
},
]);
});
it("syncs multiple plugin skills directories to sandbox workspace", async () => {
const sourceWorkspace = await createCaseDir("source-multi");
const targetWorkspace = await createCaseDir("target-multi");
// Create multiple real plugin skill directories
const realSkillA = await createCaseDir("skill-a");
await writeSkill({
dir: realSkillA,
name: "browser-automation",
description: "Browser automation skill",
});
const realSkillB = await createCaseDir("skill-b");
await writeSkill({
dir: realSkillB,
name: "obsidian-vault",
description: "Obsidian vault maintenance skill",
});
// Create plugin-skills directory with symlinks
const pluginSkillsDir = path.join(sourceWorkspace, ".openclaw", "plugin-skills");
await fs.mkdir(pluginSkillsDir, { recursive: true });
await fs.symlink(
realSkillA,
path.join(pluginSkillsDir, "browser-automation"),
process.platform === "win32" ? "junction" : "dir",
);
await fs.symlink(
realSkillB,
path.join(pluginSkillsDir, "obsidian-vault"),
process.platform === "win32" ? "junction" : "dir",
);
mockResolvePluginSkillDirs.mockReturnValueOnce([realSkillA, realSkillB]);
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
pluginSkillsDir,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
managedSkillsDir: path.join(sourceWorkspace, ".managed"),
});
// Both skills should be synced
expect(
await pathExists(path.join(targetWorkspace, "skills", "browser-automation", "SKILL.md")),
).toBe(true);
expect(
await pathExists(path.join(targetWorkspace, "skills", "obsidian-vault", "SKILL.md")),
).toBe(true);
});
it("does not sync plugin skills that escape allowed root", async () => {
const sourceWorkspace = await createCaseDir("source-escape");
const targetWorkspace = await createCaseDir("target-escape");
// Create a skill outside any allowed root
const outsideRoot = await createCaseDir("outside-root");
const escapedSkillDir = path.join(outsideRoot, "escaped-skill");
await writeSkill({
dir: escapedSkillDir,
name: "escaped-skill",
description: "Should not be synced",
});
// Create plugin-skills with symlink to escaped skill
const pluginSkillsDir = path.join(sourceWorkspace, ".openclaw", "plugin-skills");
await fs.mkdir(pluginSkillsDir, { recursive: true });
await fs.symlink(
escapedSkillDir,
path.join(pluginSkillsDir, "escaped-skill"),
process.platform === "win32" ? "junction" : "dir",
);
// Mock returns an allowed root that doesn't include the escaped skill
const allowedRoot = await createCaseDir("allowed-root");
mockResolvePluginSkillDirs.mockReturnValueOnce([allowedRoot]);
await syncWorkspaceSkills({
sourceWorkspaceDir: sourceWorkspace,
targetWorkspaceDir: targetWorkspace,
pluginSkillsDir,
bundledSkillsDir: path.join(sourceWorkspace, ".bundled"),
managedSkillsDir: path.join(sourceWorkspace, ".managed"),
});
// Escaped skill should NOT be synced
expect(
await pathExists(path.join(targetWorkspace, "skills", "escaped-skill", "SKILL.md")),
).toBe(false);
});
});
@@ -0,0 +1,278 @@
// Sandbox workspace skill synchronization is deferred behind the sandbox runtime boundary.
import fs from "node:fs";
import path from "node:path";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { resolveSandboxPath } from "../../agents/sandbox-paths.js";
import { canonicalizePath } from "../../agents/utils/paths.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { tryReadJson, writeJson } from "../../infra/json-files.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { resolveUserPath } from "../../utils.js";
import { getSkillsSnapshotVersion } from "../runtime/refresh-state.js";
import type {
SkillEligibilityContext,
SkillEntry,
SkillSnapshot,
SkillUsagePath,
} from "../types.js";
import { resolveSkillKey } from "./frontmatter.js";
import { serializeByKey } from "./serialize.js";
import { resolveSkillTelemetrySource } from "./source.js";
import { loadWorkspaceSkills } from "./workspace-skill-loader.js";
const fsp = fs.promises;
const skillsLogger = createSubsystemLogger("skills");
function resolveUniqueSyncedSkillDirName(base: string, used: Set<string>): string {
if (!used.has(base)) {
used.add(base);
return base;
}
for (let index = 2; ; index += 1) {
const candidate = `${base}-${index}`;
if (!used.has(candidate)) {
used.add(candidate);
return candidate;
}
}
}
const SYNCED_SKILLS_MANIFEST_NAME = ".openclaw-sync.json";
type SyncedSkillsManifest = {
entryKeys: string[];
skillsVersion: number;
};
const syncedSkillsUsageCache = new Map<
string,
{
destinations: Map<string, string>;
manifestKey: string;
skillUsagePaths: SkillUsagePath[];
}
>();
function resolveSyncedSkillIdentity(skillKey: string, skillName: string): string {
return JSON.stringify([skillKey, skillName]);
}
function parseSyncedSkillsManifest(value: unknown): SyncedSkillsManifest | null {
if (
!isRecord(value) ||
typeof value.skillsVersion !== "number" ||
!Number.isFinite(value.skillsVersion) ||
!Array.isArray(value.entryKeys) ||
!value.entryKeys.every((entry) => typeof entry === "string")
) {
return null;
}
return { entryKeys: value.entryKeys, skillsVersion: value.skillsVersion } as SyncedSkillsManifest;
}
function resolveSyncedSkillDestinationPath(params: {
targetSkillsDir: string;
entry: SkillEntry;
usedDirNames: Set<string>;
}): string | null {
const sourceDirName = (
params.entry.syncDirName ?? path.basename(params.entry.skill.baseDir)
).trim();
if (!sourceDirName || sourceDirName === "." || sourceDirName === "..") {
return null;
}
const uniqueDirName = resolveUniqueSyncedSkillDirName(sourceDirName, params.usedDirNames);
return resolveSandboxPath({
filePath: uniqueDirName,
cwd: params.targetSkillsDir,
root: params.targetSkillsDir,
}).resolved;
}
async function ensureSyncedSkillsDirectory(targetSkillsDir: string): Promise<void> {
let stats: fs.Stats;
try {
stats = await fsp.lstat(targetSkillsDir);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
await fsp.mkdir(targetSkillsDir, { recursive: true });
return;
}
if (!stats.isDirectory() || stats.isSymbolicLink()) {
await fsp.rm(targetSkillsDir, { recursive: true, force: true });
await fsp.mkdir(targetSkillsDir, { recursive: true });
}
}
export async function syncWorkspaceSkills(params: {
sourceWorkspaceDir: string;
targetWorkspaceDir: string;
config?: OpenClawConfig;
skillFilter?: string[];
agentId?: string;
eligibility?: SkillEligibilityContext;
managedSkillsDir?: string;
bundledSkillsDir?: string;
pluginSkillsDir?: string;
skillsSnapshot?: SkillSnapshot;
}): Promise<SkillUsagePath[]> {
const sourceDir = resolveUserPath(params.sourceWorkspaceDir);
const targetDir = resolveUserPath(params.targetWorkspaceDir);
if (sourceDir === targetDir) {
return [];
}
return await serializeByKey(`syncSkills:${targetDir}`, async () => {
const targetSkillsDir = path.join(targetDir, "skills");
const manifestPath = path.join(targetSkillsDir, SYNCED_SKILLS_MANIFEST_NAME);
const skillsVersion = getSkillsSnapshotVersion(sourceDir);
const skillsSnapshot = params.skillsSnapshot;
await ensureSyncedSkillsDirectory(targetSkillsDir);
const manifest = parseSyncedSkillsManifest(await tryReadJson<unknown>(manifestPath));
const expectedManifestKey =
skillsSnapshot?.version === skillsVersion
? JSON.stringify([
skillsVersion,
skillsSnapshot.skills
.map((skill) => resolveSyncedSkillIdentity(skill.skillKey ?? skill.name, skill.name))
.toSorted(),
])
: undefined;
const cachedUsage = syncedSkillsUsageCache.get(targetSkillsDir);
const manifestKey = manifest
? JSON.stringify([manifest.skillsVersion, manifest.entryKeys])
: undefined;
if (
expectedManifestKey &&
manifestKey === expectedManifestKey &&
cachedUsage?.manifestKey === manifestKey
) {
return cachedUsage.skillUsagePaths.map((entry) => ({ ...entry }));
}
const entries = loadWorkspaceSkills(sourceDir, {
config: params.config,
skillFilter: params.skillFilter,
agentId: params.agentId,
eligibility: params.eligibility,
managedSkillsDir: params.managedSkillsDir,
bundledSkillsDir: params.bundledSkillsDir,
pluginSkillsDir: params.pluginSkillsDir,
});
const usedDirNames = new Set<string>();
const plans: Array<{ destinationPath?: string; entry: SkillEntry; identity: string }> = [];
for (const entry of entries) {
const identity = resolveSyncedSkillIdentity(
resolveSkillKey(entry.skill, entry),
entry.skill.name,
);
if (entry.skill.filePath.startsWith("node://")) {
plans.push({ entry, identity });
continue;
}
let destinationPath: string | null;
try {
destinationPath = resolveSyncedSkillDestinationPath({
targetSkillsDir,
entry,
usedDirNames,
});
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
skillsLogger.warn(`Failed to resolve safe destination for ${entry.skill.name}: ${message}`);
continue;
}
if (!destinationPath) {
skillsLogger.warn(
`Failed to resolve safe destination for ${entry.skill.name}: invalid source directory name`,
);
continue;
}
plans.push({ destinationPath, entry, identity });
}
await fsp.rm(manifestPath, { force: true });
const previousUsage =
manifest?.skillsVersion === skillsVersion && cachedUsage?.manifestKey === manifestKey
? cachedUsage
: undefined;
syncedSkillsUsageCache.delete(targetSkillsDir);
const preservedDestinations = new Set(
plans.flatMap((plan) => {
const destination = plan.destinationPath ? path.basename(plan.destinationPath) : null;
return previousUsage?.destinations.get(plan.identity) === destination
? destination
? [destination]
: []
: [];
}),
);
for (const child of await fsp.readdir(targetSkillsDir)) {
if (!preservedDestinations.has(child)) {
await fsp.rm(path.join(targetSkillsDir, child), { recursive: true, force: true });
}
}
const skillUsagePaths: SkillUsagePath[] = [];
let copyFailed = false;
for (const plan of plans) {
const { destinationPath, entry } = plan;
if (!destinationPath) {
continue;
}
if (!preservedDestinations.has(path.basename(destinationPath))) {
try {
const syncSourceDir = entry.syncSourceDir ?? entry.skill.baseDir;
await fsp.cp(syncSourceDir, destinationPath, {
recursive: true,
force: true,
filter: (src) => {
const name = path.basename(src);
return !(name === ".git" || name === "node_modules");
},
});
} catch (error) {
copyFailed = true;
const message = error instanceof Error ? error.message : JSON.stringify(error);
skillsLogger.warn(`Failed to copy ${entry.skill.name} to sandbox: ${message}`);
continue;
}
}
skillUsagePaths.push({
readPath: path.join(
destinationPath,
path.relative(entry.skill.baseDir, entry.skill.filePath),
),
skillFile: canonicalizePath(entry.skill.filePath),
skillName: entry.skill.name,
skillSource: resolveSkillTelemetrySource(entry.skill),
});
}
if (!copyFailed) {
const nextManifest: SyncedSkillsManifest = {
entryKeys: plans.map((plan) => plan.identity).toSorted(),
skillsVersion,
};
await writeJson(manifestPath, nextManifest, { trailingNewline: true });
syncedSkillsUsageCache.set(targetSkillsDir, {
destinations: new Map(
plans.flatMap((plan) =>
plan.destinationPath
? [[plan.identity, path.basename(plan.destinationPath)] as const]
: [],
),
),
manifestKey: JSON.stringify([skillsVersion, nextManifest.entryKeys]),
skillUsagePaths,
});
pruneMapToMaxSize(syncedSkillsUsageCache, 100);
}
return skillUsagePaths;
});
}
File diff suppressed because it is too large Load Diff
+15 -18
View File
@@ -5,17 +5,17 @@ import {
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../../config/config.js";
import * as skillsWorkspaceModule from "../loading/workspace.js";
import * as skillsLoaderModule from "../loading/workspace-skill-loader.js";
import type { SkillSnapshot } from "../types.js";
import { resolveEmbeddedRunSkillEntries } from "./embedded-run-entries.js";
describe("resolveEmbeddedRunSkillEntries", () => {
const loadWorkspaceSkillEntriesSpy = vi.spyOn(skillsWorkspaceModule, "loadWorkspaceSkillEntries");
const loadWorkspaceSkillsSpy = vi.spyOn(skillsLoaderModule, "loadWorkspaceSkills");
beforeEach(() => {
clearRuntimeConfigSnapshot();
loadWorkspaceSkillEntriesSpy.mockReset();
loadWorkspaceSkillEntriesSpy.mockReturnValue([]);
loadWorkspaceSkillsSpy.mockReset();
loadWorkspaceSkillsSpy.mockReturnValue([]);
});
it("loads skill entries with config when no resolved snapshot skills exist", () => {
@@ -37,8 +37,8 @@ describe("resolveEmbeddedRunSkillEntries", () => {
});
expect(result.shouldLoadSkillEntries).toBe(true);
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledTimes(1);
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledWith("/tmp/workspace", { config });
expect(loadWorkspaceSkillsSpy).toHaveBeenCalledTimes(1);
expect(loadWorkspaceSkillsSpy).toHaveBeenCalledWith("/tmp/workspace", { config });
});
it("threads agentId through live skill loading", () => {
@@ -52,7 +52,7 @@ describe("resolveEmbeddedRunSkillEntries", () => {
},
});
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledWith("/tmp/workspace", {
expect(loadWorkspaceSkillsSpy).toHaveBeenCalledWith("/tmp/workspace", {
config: {},
agentId: "writer",
});
@@ -79,14 +79,11 @@ describe("resolveEmbeddedRunSkillEntries", () => {
workspaceOnly: true,
});
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledWith(
"/tmp/workspace/.openclaw/sandbox-skills",
{
config: {},
eligibility,
workspaceOnly: true,
},
);
expect(loadWorkspaceSkillsSpy).toHaveBeenCalledWith("/tmp/workspace/.openclaw/sandbox-skills", {
config: {},
eligibility,
workspaceOnly: true,
});
});
it("prefers the active runtime snapshot when caller config still contains SecretRefs", () => {
@@ -123,7 +120,7 @@ describe("resolveEmbeddedRunSkillEntries", () => {
},
});
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledWith("/tmp/workspace", {
expect(loadWorkspaceSkillsSpy).toHaveBeenCalledWith("/tmp/workspace", {
config: runtimeConfig,
});
});
@@ -163,7 +160,7 @@ describe("resolveEmbeddedRunSkillEntries", () => {
},
});
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledWith("/tmp/workspace", {
expect(loadWorkspaceSkillsSpy).toHaveBeenCalledWith("/tmp/workspace", {
config: callerConfig,
});
});
@@ -185,6 +182,6 @@ describe("resolveEmbeddedRunSkillEntries", () => {
shouldLoadSkillEntries: false,
skillEntries: [],
});
expect(loadWorkspaceSkillEntriesSpy).not.toHaveBeenCalled();
expect(loadWorkspaceSkillsSpy).not.toHaveBeenCalled();
});
});
+2 -2
View File
@@ -1,7 +1,7 @@
// Embedded run entry helpers serialize runtime skill metadata for agent run records.
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { resolveSkillRuntimeConfig } from "../loading/runtime-config.js";
import { loadWorkspaceSkillEntries } from "../loading/workspace.js";
import { loadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
import type { SkillEligibilityContext, SkillEntry, SkillSnapshot } from "../types.js";
/** Resolves skill entries embedded into a run payload into runtime-visible entries. */
@@ -21,7 +21,7 @@ export function resolveEmbeddedRunSkillEntries(params: {
return {
shouldLoadSkillEntries,
skillEntries: shouldLoadSkillEntries
? loadWorkspaceSkillEntries(params.workspaceDir, {
? loadWorkspaceSkills(params.workspaceDir, {
config,
agentId: params.agentId,
...(params.eligibility ? { eligibility: params.eligibility } : {}),
+4 -3
View File
@@ -4,7 +4,8 @@ import { resetLogger, setLoggerOverride } from "../../logging/logger.js";
import { loggingState } from "../../logging/state.js";
import { buildWorkspaceSkillCommandSpecs } from "../discovery/command-specs.js";
import { buildWorkspaceSkillStatus } from "../discovery/status.js";
import { buildWorkspaceSkillSnapshot, loadWorkspaceSkillEntries } from "../loading/workspace.js";
import { loadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
import { buildSkillSnapshot } from "../loading/workspace-skill-prompt.js";
import type { SkillEntry } from "../types.js";
import { getSkillsSnapshotVersion } from "./refresh-state.js";
import {
@@ -78,11 +79,11 @@ describe("node-hosted skill snapshots", () => {
],
});
const entries = loadWorkspaceSkillEntries("/workspace", {
const entries = loadWorkspaceSkills("/workspace", {
workspaceOnly: true,
eligibility: { nodeSkills: { canExec: true } },
});
const snapshot = buildWorkspaceSkillSnapshot("/workspace", { entries });
const snapshot = buildSkillSnapshot("/workspace", { entries });
expect(snapshot.skills.map((skill) => skill.name)).toEqual(["release-helper"]);
expect(snapshot.prompt).toContain("Build Mac (node-1)");
expect(snapshot.prompt).toContain(
+2 -2
View File
@@ -5,7 +5,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { NodeRegistry, NodeSession } from "../../gateway/node-registry.js";
import { listNodePairing, updatePairedNodeBins } from "../../infra/device-pairing-node.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { loadWorkspaceSkillEntries } from "../loading/workspace.js";
import { loadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
import type { SkillEligibilityContext } from "../types.js";
import { bumpSkillsSnapshotVersion } from "./refresh-state.js";
import {
@@ -492,7 +492,7 @@ async function refreshRemoteNodeBinsUncoalesced(params: {
const workspaceDirs = listAgentWorkspaceDirs(params.cfg);
const requiredBins = new Set<string>();
for (const workspaceDir of workspaceDirs) {
const entries = loadWorkspaceSkillEntries(workspaceDir, { config: params.cfg });
const entries = loadWorkspaceSkills(workspaceDir, { config: params.cfg });
for (const bin of collectRequiredBins(entries, "darwin")) {
requiredBins.add(bin);
}
+2 -2
View File
@@ -34,8 +34,8 @@ const {
),
}));
vi.mock("../loading/workspace.js", () => ({
buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock,
vi.mock("../loading/workspace-skill-prompt.js", () => ({
buildSkillSnapshot: buildWorkspaceSkillSnapshotMock,
}));
vi.mock("./refresh.js", () => ({
+2 -2
View File
@@ -3,7 +3,7 @@ import { stableStringify } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { matchesSkillFilter } from "../discovery/filter.js";
import { buildWorkspaceSkillSnapshot } from "../loading/workspace.js";
import { buildSkillSnapshot } from "../loading/workspace-skill-prompt.js";
import { WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION } from "../types.js";
import type { SkillEligibilityContext, SkillSnapshot } from "../types.js";
import { getSkillsSnapshotVersion, shouldRefreshSnapshotForVersion } from "./refresh-state.js";
@@ -68,7 +68,7 @@ export function resolveReusableWorkspaceSkillSnapshot(
!matchesSkillFilter(params.existingSnapshot?.skillFilter, params.skillFilter) ||
skillOverridesChanged;
const buildSnapshot = () => {
return buildWorkspaceSkillSnapshot(params.workspaceDir, {
return buildSkillSnapshot(params.workspaceDir, {
config: params.config,
agentId: params.agentId,
skillFilter: params.skillFilter,