fix: cover comfy manifest availability contracts

This commit is contained in:
Shakker
2026-05-01 21:33:07 +01:00
parent 6b0356257a
commit 1de7362679
5 changed files with 176 additions and 11 deletions

View File

@@ -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.

View File

@@ -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"]
}
]
}

View File

@@ -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: {

View File

@@ -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,

View File

@@ -85,7 +85,7 @@ function uniqueSorted(values: Iterable<string>): string[] {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
function loadCapabilityManifestSnapshot(params: {
export function loadCapabilityManifestSnapshot(params: {
cfg?: OpenClawConfig;
workspaceDir?: string;
}): Pick<PluginMetadataSnapshot, "index" | "plugins"> {