mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
fix(secretrefs): resolve external channel contracts in dist/ sidecars (#77421)
* fix(secretrefs): resolve external channel contracts in dist/ sidecars Externalized channel plugins published to npm (e.g. @openclaw/discord since 2026.5.2) keep their compiled secret-contract-api artifact under <rootDir>/dist/, per the package.json `openclaw.runtimeExtensions` convention. The runtime contract loader added in #76449 only searched the rootDir, so npm-installed plugins silently dropped their channel SecretRef contracts: the runtime snapshot left `channels.<id>.token` as an unresolved SecretRef, the plugin's `isConfigured` check then returned false, and the gateway recorded `error: not configured` without firing the usual channel startup logs. Look in `<rootDir>/dist/` as well as `<rootDir>/`, preferring dist when running from a built openclaw artifact and rootDir when running from source. The new `loads dist/ secret-contract-api sidecars …` test in channel-contract-api.external.test.ts mirrors the real npm-package layout and fails without this change. Refs #76371. Fixes #77416. * docs: credit changelog contributor --------- Co-authored-by: Magpie <magpie@local> Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
@@ -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 `<rootDir>/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 <id>] [--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.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 <rootDir>/dist/
|
||||
// (per package.json `openclaw.runtimeExtensions`), while flat-layout plugins keep
|
||||
// them at <rootDir>/. 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;
|
||||
|
||||
Reference in New Issue
Block a user