CLI: make read-only SecretRef status flows degrade safely (#37023)

* CLI: add read-only SecretRef inspection

* CLI: fix read-only SecretRef status regressions

* CLI: preserve read-only SecretRef status fallbacks

* Docs: document read-only channel inspection hook

* CLI: preserve audit coverage for read-only SecretRefs

* CLI: fix read-only status account selection

* CLI: fix targeted gateway fallback analysis

* CLI: fix Slack HTTP read-only inspection

* CLI: align audit credential status checks

* CLI: restore Telegram read-only fallback semantics
This commit is contained in:
Josh Avant
2026-03-05 23:07:13 -06:00
committed by GitHub
parent 8d4a2f2c59
commit 0e4245063f
58 changed files with 3422 additions and 215 deletions

View File

@@ -0,0 +1,141 @@
import type { OpenClawConfig } from "../config/config.js";
import type { DiscordAccountConfig } from "../config/types.discord.js";
import { hasConfiguredSecretInput, normalizeSecretInputString } from "../config/types.secrets.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import { resolveDefaultDiscordAccountId } from "./accounts.js";
export type DiscordCredentialStatus = "available" | "configured_unavailable" | "missing";
export type InspectedDiscordAccount = {
accountId: string;
enabled: boolean;
name?: string;
token: string;
tokenSource: "env" | "config" | "none";
tokenStatus: DiscordCredentialStatus;
configured: boolean;
config: DiscordAccountConfig;
};
function resolveDiscordAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): DiscordAccountConfig | undefined {
return resolveAccountEntry(cfg.channels?.discord?.accounts, accountId);
}
function mergeDiscordAccountConfig(cfg: OpenClawConfig, accountId: string): DiscordAccountConfig {
const { accounts: _ignored, ...base } = (cfg.channels?.discord ?? {}) as DiscordAccountConfig & {
accounts?: unknown;
};
const account = resolveDiscordAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
}
function inspectDiscordTokenValue(value: unknown): {
token: string;
tokenSource: "config";
tokenStatus: Exclude<DiscordCredentialStatus, "missing">;
} | null {
const normalized = normalizeSecretInputString(value);
if (normalized) {
return {
token: normalized.replace(/^Bot\s+/i, ""),
tokenSource: "config",
tokenStatus: "available",
};
}
if (hasConfiguredSecretInput(value)) {
return {
token: "",
tokenSource: "config",
tokenStatus: "configured_unavailable",
};
}
return null;
}
export function inspectDiscordAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
envToken?: string | null;
}): InspectedDiscordAccount {
const accountId = normalizeAccountId(
params.accountId ?? resolveDefaultDiscordAccountId(params.cfg),
);
const merged = mergeDiscordAccountConfig(params.cfg, accountId);
const enabled = params.cfg.channels?.discord?.enabled !== false && merged.enabled !== false;
const accountConfig = resolveDiscordAccountConfig(params.cfg, accountId);
const hasAccountToken = Boolean(
accountConfig &&
Object.prototype.hasOwnProperty.call(accountConfig as Record<string, unknown>, "token"),
);
const accountToken = inspectDiscordTokenValue(accountConfig?.token);
if (accountToken) {
return {
accountId,
enabled,
name: merged.name?.trim() || undefined,
token: accountToken.token,
tokenSource: accountToken.tokenSource,
tokenStatus: accountToken.tokenStatus,
configured: true,
config: merged,
};
}
if (hasAccountToken) {
return {
accountId,
enabled,
name: merged.name?.trim() || undefined,
token: "",
tokenSource: "none",
tokenStatus: "missing",
configured: false,
config: merged,
};
}
const channelToken = inspectDiscordTokenValue(params.cfg.channels?.discord?.token);
if (channelToken) {
return {
accountId,
enabled,
name: merged.name?.trim() || undefined,
token: channelToken.token,
tokenSource: channelToken.tokenSource,
tokenStatus: channelToken.tokenStatus,
configured: true,
config: merged,
};
}
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
const envToken = allowEnv
? normalizeSecretInputString(params.envToken ?? process.env.DISCORD_BOT_TOKEN)
: undefined;
if (envToken) {
return {
accountId,
enabled,
name: merged.name?.trim() || undefined,
token: envToken.replace(/^Bot\s+/i, ""),
tokenSource: "env",
tokenStatus: "available",
configured: true,
config: merged,
};
}
return {
accountId,
enabled,
name: merged.name?.trim() || undefined,
token: "",
tokenSource: "none",
tokenStatus: "missing",
configured: false,
config: merged,
};
}

View File

@@ -104,4 +104,33 @@ describe("discord audit", () => {
expect(collected.channelIds).toEqual([]);
expect(collected.unresolvedChannels).toBe(0);
});
it("collects audit channel ids without resolving SecretRef-backed Discord tokens", async () => {
const { collectDiscordAuditChannelIds } = await import("./audit.js");
const cfg = {
channels: {
discord: {
enabled: true,
token: {
source: "env",
provider: "default",
id: "DISCORD_BOT_TOKEN",
},
guilds: {
"123": {
channels: {
"111": { allow: true },
general: { allow: true },
},
},
},
},
},
} as unknown as import("../config/config.js").OpenClawConfig;
const collected = collectDiscordAuditChannelIds({ cfg, accountId: "default" });
expect(collected.channelIds).toEqual(["111"]);
expect(collected.unresolvedChannels).toBe(1);
});
});

View File

@@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../config/config.js";
import type { DiscordGuildChannelConfig, DiscordGuildEntry } from "../config/types.js";
import { isRecord } from "../utils.js";
import { resolveDiscordAccount } from "./accounts.js";
import { inspectDiscordAccount } from "./account-inspect.js";
import { fetchChannelPermissionsDiscord } from "./send.js";
export type DiscordChannelPermissionsAuditEntry = {
@@ -74,7 +74,7 @@ export function collectDiscordAuditChannelIds(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) {
const account = resolveDiscordAccount({
const account = inspectDiscordAccount({
cfg: params.cfg,
accountId: params.accountId,
});

View File

@@ -1,6 +1,6 @@
import { Container } from "@buape/carbon";
import type { OpenClawConfig } from "../config/config.js";
import { resolveDiscordAccount } from "./accounts.js";
import { inspectDiscordAccount } from "./account-inspect.js";
const DEFAULT_DISCORD_ACCENT_COLOR = "#5865F2";
@@ -24,7 +24,7 @@ export function normalizeDiscordAccentColor(raw?: string | null): string | null
}
export function resolveDiscordAccentColor(params: ResolveDiscordAccentColorParams): string {
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
const account = inspectDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
const configured = normalizeDiscordAccentColor(account.config.ui?.components?.accentColor);
return configured ?? DEFAULT_DISCORD_ACCENT_COLOR;
}