diff --git a/CHANGELOG.md b/CHANGELOG.md index df8a091e61d..ae545c7c722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Changes - Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys. +- Secrets/external channel contracts: also look in `/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss. - Models/auth: add `openclaw models auth list [--provider ] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc. - Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar. - Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev. diff --git a/src/secrets/channel-contract-api.external.test.ts b/src/secrets/channel-contract-api.external.test.ts index 47a4b9a16ad..8fff506a62c 100644 --- a/src/secrets/channel-contract-api.external.test.ts +++ b/src/secrets/channel-contract-api.external.test.ts @@ -98,6 +98,66 @@ describe("external channel secret contract api", () => { expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function"); }); + it("loads dist/ secret-contract-api sidecars for compiled npm-published external channel plugins", () => { + const rootDir = makeTrackedTempDir("openclaw-channel-secret-contract-dist", tempDirs); + fs.mkdirSync(path.join(rootDir, "dist"), { recursive: true }); + fs.writeFileSync( + path.join(rootDir, "dist", "secret-contract-api.cjs"), + ` +module.exports = { + secretTargetRegistryEntries: [ + { + id: "channels.discord.token", + targetType: "channels.discord.token", + configFile: "openclaw.json", + pathPattern: "channels.discord.token", + secretShape: "secret_input", + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true + } + ], + collectRuntimeConfigAssignments(params) { + params.context.assignments.push({ + path: "channels.discord.token", + ref: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" }, + expected: "string", + apply() {} + }); + } +}; +`, + "utf8", + ); + const record = { + id: "discord", + origin: "global", + channels: ["discord"], + channelConfigs: {}, + rootDir, + }; + loadPluginMetadataSnapshotMock.mockReturnValue({ + plugins: [record], + }); + + const api = loadChannelSecretContractApi({ + channelId: "discord", + config: { channels: { discord: {} } }, + env: {}, + loadablePluginOrigins: new Map([["discord", "global"]]), + }); + + expect(api?.secretTargetRegistryEntries).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "channels.discord.token", + }), + ]), + ); + expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function"); + }); + it("skips external channel records outside the loadable plugin origin set", () => { const record = writeExternalChannelPlugin({ pluginId: "discord", channelId: "discord" }); loadPluginMetadataSnapshotMock.mockReturnValue({ diff --git a/src/secrets/channel-contract-api.ts b/src/secrets/channel-contract-api.ts index 9f97ff59e78..908b2d48e5b 100644 --- a/src/secrets/channel-contract-api.ts +++ b/src/secrets/channel-contract-api.ts @@ -87,16 +87,21 @@ function orderedContractApiExtensions(): readonly string[] { } function resolvePluginContractApiPath(rootDir: string): string | null { - for (const extension of orderedContractApiExtensions()) { - const candidate = path.join(rootDir, `secret-contract-api${extension}`); - if (fs.existsSync(candidate)) { - return candidate; - } - } - for (const extension of orderedContractApiExtensions()) { - const candidate = path.join(rootDir, `contract-api${extension}`); - if (fs.existsSync(candidate)) { - return candidate; + // Compiled npm-published plugins place their public artifacts under /dist/ + // (per package.json `openclaw.runtimeExtensions`), while flat-layout plugins keep + // them at /. Search both, preferring dist/ when running from built openclaw + // artifacts and rootDir/ when running from source. + const searchDirs = RUNNING_FROM_BUILT_ARTIFACT + ? [path.join(rootDir, "dist"), rootDir] + : [rootDir, path.join(rootDir, "dist")]; + for (const basename of ["secret-contract-api", "contract-api"]) { + for (const dir of searchDirs) { + for (const extension of orderedContractApiExtensions()) { + const candidate = path.join(dir, `${basename}${extension}`); + if (fs.existsSync(candidate)) { + return candidate; + } + } } } return null;