Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix(discord): keep default SecretRef active with named accounts
  • Loading branch information
849261680 authored and steipete committed Jul 6, 2026
commit f63787e14597e561a2cc35f4e7e083dc1ce82c18
42 changes: 38 additions & 4 deletions extensions/discord/src/secret-config-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import {
collectNestedChannelFieldAssignments,
collectSimpleChannelFieldAssignments,
getChannelSurface,
getChannelRecord,
hasConfiguredSecretInputValue,
isBaseFieldActiveForChannelSurface,
isEnabledFlag,
isRecord,
resolveChannelAccountSurface,
type ChannelAccountSurface,
type ResolverContext,
type SecretDefaults,
type SecretTargetRegistryEntry,
Expand Down Expand Up @@ -83,16 +86,47 @@ export const secretTargetRegistryEntries: SecretTargetRegistryEntry[] = [
},
];

function withImplicitDefaultDiscordAccount(params: {
discord: Record<string, unknown>;
surface: ChannelAccountSurface;
defaults?: SecretDefaults;
}): ChannelAccountSurface {
if (
!params.surface.hasExplicitAccounts ||
!hasConfiguredSecretInputValue(params.discord.token, params.defaults) ||
params.surface.accounts.some((entry) => entry.accountId.toLowerCase() === "default")
) {
return params.surface;
}
// Discord account listing keeps a top-level token as an implicit default account.
// Match that runtime contract so named accounts do not orphan the default SecretRef.
return {
...params.surface,
accounts: [
...params.surface.accounts,
{
accountId: "default",
account: {},
enabled: params.surface.channelEnabled,
},
],
};
}

export function collectRuntimeConfigAssignments(params: {
config: { channels?: Record<string, unknown> };
defaults?: SecretDefaults;
context: ResolverContext;
}): void {
const resolved = getChannelSurface(params.config, "discord");
if (!resolved) {
const discord = getChannelRecord(params.config, "discord");
if (!discord) {
return;
}
const { channel: discord, surface } = resolved;
const surface = withImplicitDefaultDiscordAccount({
discord,
surface: resolveChannelAccountSurface(discord),
defaults: params.defaults,
});
collectSimpleChannelFieldAssignments({
channelKey: "discord",
field: "token",
Expand Down
66 changes: 66 additions & 0 deletions src/secrets/runtime-discord-surface.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/** Tests Discord secret surfaces in runtime preparation. */
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import "./runtime-discord.test-support.ts";
import {
Expand Down Expand Up @@ -71,6 +74,69 @@ describe("secrets runtime snapshot discord surface", () => {
);
});

it("resolves the implicit default token when named Discord accounts are added", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-discord-secrets-"));
try {
const secretsPath = path.join(root, "secrets.json");
await fs.writeFile(
secretsPath,
JSON.stringify({
discord: {
defaultToken: "default-account-token",
secondToken: "second-account-token",
},
}),
"utf8",
);
await fs.chmod(secretsPath, 0o600);

const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
secrets: {
providers: {
discord_file: {
source: "file",
path: secretsPath,
mode: "json",
},
},
},
channels: {
discord: {
token: {
source: "file",
provider: "discord_file",
id: "/discord/defaultToken",
},
accounts: {
second: {
enabled: true,
token: {
source: "file",
provider: "discord_file",
id: "/discord/secondToken",
},
},
},
},
},
}),
agentDirs: ["/tmp/openclaw-agent-main"],
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});

expect(snapshot.config.channels?.discord?.token).toBe("default-account-token");
expect(snapshot.config.channels?.discord?.accounts?.second?.token).toBe(
"second-account-token",
);
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
"channels.discord.token",
);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});

it("fails when non-default Discord account inherits an unresolved top-level token ref", async () => {
await expect(
prepareSecretsRuntimeSnapshot({
Expand Down