diff --git a/CHANGELOG.md b/CHANGELOG.md index d10a7788748..42959f47455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,6 +166,7 @@ Docs: https://docs.openclaw.ai - Agents/runtime: validate agent model allowlists against manifest model catalog metadata during reply startup, avoiding broad provider runtime catalog loading before the agent run lane starts. Thanks @shakkernerd. - Agents/runtime: keep allowlisted configured model thinking metadata available when manifest catalog rows are absent, so explicit high-reasoning levels remain valid for custom configured models. Thanks @shakkernerd. - Agents/tools: preserve plugin-declared config-only generation providers such as local Comfy workflows during reply tool pre-gating, and share manifest auth/config availability checks between the planner and final tool factories. Thanks @shakkernerd. +- Agents/tools: keep Comfy generation tools visible from legacy local workflow config and cloud API-key config when no Gateway metadata snapshot is active, using plugin-declared manifest signals instead of loading provider runtimes. Thanks @shakkernerd. - Agents/tools: route media and generation capability lookups through the Gateway plugin metadata snapshot during reply tool registration, avoiding repeated manifest registry reloads on the live reply path. Thanks @shakkernerd. - Agents/tools: let plugins declare media generation auth aliases and base-url guards in manifests, preserving OpenAI Codex OAuth image generation availability without core-owned provider special cases. Thanks @shakkernerd. - Agents/tools: reuse the auth profile store already loaded for the active run when deciding media and generation tool availability, avoiding repeated provider-auth runtime discovery during reply startup. Thanks @shakkernerd. diff --git a/extensions/comfy/openclaw.plugin.json b/extensions/comfy/openclaw.plugin.json index caf9efeb9e3..abdbaa87905 100644 --- a/extensions/comfy/openclaw.plugin.json +++ b/extensions/comfy/openclaw.plugin.json @@ -43,6 +43,37 @@ }, "requiredAny": ["workflow", "workflowPath"], "required": ["promptNodeId"] + }, + { + "rootPath": "models.providers.comfy", + "overlayPath": "image", + "mode": { + "path": "mode", + "default": "local", + "allowed": ["local"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId"] + }, + { + "rootPath": "plugins.entries.comfy.config", + "overlayPath": "image", + "mode": { + "path": "mode", + "allowed": ["cloud"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId", "apiKey"] + }, + { + "rootPath": "models.providers.comfy", + "overlayPath": "image", + "mode": { + "path": "mode", + "allowed": ["cloud"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId", "apiKey"] } ] } @@ -60,6 +91,37 @@ }, "requiredAny": ["workflow", "workflowPath"], "required": ["promptNodeId"] + }, + { + "rootPath": "models.providers.comfy", + "overlayPath": "music", + "mode": { + "path": "mode", + "default": "local", + "allowed": ["local"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId"] + }, + { + "rootPath": "plugins.entries.comfy.config", + "overlayPath": "music", + "mode": { + "path": "mode", + "allowed": ["cloud"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId", "apiKey"] + }, + { + "rootPath": "models.providers.comfy", + "overlayPath": "music", + "mode": { + "path": "mode", + "allowed": ["cloud"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId", "apiKey"] } ] } @@ -77,6 +139,37 @@ }, "requiredAny": ["workflow", "workflowPath"], "required": ["promptNodeId"] + }, + { + "rootPath": "models.providers.comfy", + "overlayPath": "video", + "mode": { + "path": "mode", + "default": "local", + "allowed": ["local"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId"] + }, + { + "rootPath": "plugins.entries.comfy.config", + "overlayPath": "video", + "mode": { + "path": "mode", + "allowed": ["cloud"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId", "apiKey"] + }, + { + "rootPath": "models.providers.comfy", + "overlayPath": "video", + "mode": { + "path": "mode", + "allowed": ["cloud"] + }, + "requiredAny": ["workflow", "workflowPath"], + "required": ["promptNodeId", "apiKey"] } ] } diff --git a/src/agents/openclaw-tools.media-factory-plan.test.ts b/src/agents/openclaw-tools.media-factory-plan.test.ts index a38c29cfb3c..70602bb5f5c 100644 --- a/src/agents/openclaw-tools.media-factory-plan.test.ts +++ b/src/agents/openclaw-tools.media-factory-plan.test.ts @@ -1,5 +1,7 @@ +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js"; import { clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot, @@ -103,6 +105,7 @@ function installSnapshot( describe("optional media tool factory planning", () => { afterEach(() => { clearCurrentPluginMetadataSnapshot(); + setBundledPluginsDirOverrideForTest(undefined); vi.unstubAllEnvs(); }); @@ -390,6 +393,69 @@ describe("optional media tool factory planning", () => { }); }); + it.each([ + { + name: "legacy local provider config", + config: { + models: { + providers: { + comfy: { + workflow: { "1": { inputs: {} } }, + promptNodeId: "1", + }, + }, + }, + } satisfies OpenClawConfig, + }, + { + name: "plugin cloud API key config", + config: { + plugins: { + entries: { + comfy: { + config: { + mode: "cloud", + apiKey: "cloud-key", + workflow: { "1": { inputs: {} } }, + promptNodeId: "1", + }, + }, + }, + }, + } satisfies OpenClawConfig, + }, + { + name: "legacy cloud API key config", + config: { + models: { + providers: { + comfy: { + mode: "cloud", + apiKey: "cloud-key", + workflow: { "1": { inputs: {} } }, + promptNodeId: "1", + }, + }, + }, + } satisfies OpenClawConfig, + }, + ])( + "registers generation tools from Comfy $name without a current metadata snapshot", + ({ config }) => { + setBundledPluginsDirOverrideForTest(path.join(process.cwd(), "extensions")); + + const toolNames = createOpenClawTools({ + config, + authProfileStore: createAuthStore(), + pluginToolAllowlist: ["image_generate", "video_generate", "music_generate"], + }).map((tool) => tool.name); + + expect(toolNames).toContain("image_generate"); + expect(toolNames).toContain("video_generate"); + expect(toolNames).toContain("music_generate"); + }, + ); + it("honors manifest-declared image provider auth alias base-url guards", () => { const config: OpenClawConfig = { models: { diff --git a/src/agents/tools/media-tool-shared.ts b/src/agents/tools/media-tool-shared.ts index a809300c4a8..ecd5a9acc80 100644 --- a/src/agents/tools/media-tool-shared.ts +++ b/src/agents/tools/media-tool-shared.ts @@ -4,7 +4,8 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { SsrFPolicy } from "../../infra/net/ssrf.js"; import { getDefaultLocalRoots } from "../../media/web-media.js"; import { readSnakeCaseParamRaw } from "../../param-key.js"; -import { resolveManifestCapabilityProviderIds } from "../../plugins/capability-provider-runtime.js"; +import { loadCapabilityManifestSnapshot } from "../../plugins/capability-provider-runtime.js"; +import { listAvailableManifestContractValues } from "../../plugins/manifest-contract-eligibility.js"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -321,12 +322,16 @@ export function hasGenerationToolAvailability(params: { }), ); } - const snapshot = getCurrentCapabilityMetadataSnapshot({ - config: params.cfg, - workspaceDir: params.workspaceDir, - }); + const snapshot = + getCurrentCapabilityMetadataSnapshot({ + config: params.cfg, + workspaceDir: params.workspaceDir, + }) ?? + loadCapabilityManifestSnapshot({ + cfg: params.cfg, + workspaceDir: params.workspaceDir, + }); if ( - snapshot && hasSnapshotCapabilityAvailability({ snapshot, key: params.providerKey, @@ -336,10 +341,10 @@ export function hasGenerationToolAvailability(params: { ) { return true; } - return resolveManifestCapabilityProviderIds({ - key: params.providerKey, - cfg: params.cfg, - workspaceDir: params.workspaceDir, + return listAvailableManifestContractValues({ + snapshot, + contract: params.providerKey, + config: params.cfg, }).some((providerId) => hasAuthForProvider({ provider: providerId, diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index abdb6e33ac1..b7fe4ef0074 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -85,7 +85,7 @@ function uniqueSorted(values: Iterable): string[] { return [...new Set(values)].toSorted((left, right) => left.localeCompare(right)); } -function loadCapabilityManifestSnapshot(params: { +export function loadCapabilityManifestSnapshot(params: { cfg?: OpenClawConfig; workspaceDir?: string; }): Pick {