mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
248cef3646
commit
64e3712e82
@@ -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
@@ -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"}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<b&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);
|
||||
});
|
||||
});
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
@@ -29,6 +30,19 @@ function escapeXml(str: string): string {
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
+17
-17
@@ -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: {
|
||||
+200
-88
@@ -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<b&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 "";
|
||||
}
|
||||
+12
-159
@@ -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"],
|
||||
});
|
||||
});
|
||||
});
|
||||
+190
-33
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user