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

@@ -30,7 +30,10 @@ function stubChannelPlugin(params: {
id: "discord" | "slack" | "telegram";
label: string;
resolveAccount: (cfg: OpenClawConfig, accountId: string | null | undefined) => unknown;
inspectAccount?: (cfg: OpenClawConfig, accountId: string | null | undefined) => unknown;
listAccountIds?: (cfg: OpenClawConfig) => string[];
isConfigured?: (account: unknown, cfg: OpenClawConfig) => boolean;
isEnabled?: (account: unknown, cfg: OpenClawConfig) => boolean;
}): ChannelPlugin {
return {
id: params.id,
@@ -54,9 +57,10 @@ function stubChannelPlugin(params: {
);
return enabled ? ["default"] : [];
}),
inspectAccount: params.inspectAccount,
resolveAccount: (cfg, accountId) => params.resolveAccount(cfg, accountId),
isEnabled: () => true,
isConfigured: () => true,
isEnabled: (account, cfg) => params.isEnabled?.(account, cfg) ?? true,
isConfigured: (account, cfg) => params.isConfigured?.(account, cfg) ?? true,
},
};
}
@@ -1837,6 +1841,247 @@ description: test skill
});
});
it("keeps channel security findings when SecretRef credentials are configured but unavailable", async () => {
await withChannelSecurityStateDir(async () => {
const sourceConfig: OpenClawConfig = {
channels: {
discord: {
enabled: true,
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
groupPolicy: "allowlist",
guilds: {
"123": {
channels: {
general: { allow: true },
},
},
},
},
},
};
const resolvedConfig: OpenClawConfig = {
channels: {
discord: {
enabled: true,
groupPolicy: "allowlist",
guilds: {
"123": {
channels: {
general: { allow: true },
},
},
},
},
},
};
const inspectableDiscordPlugin = stubChannelPlugin({
id: "discord",
label: "Discord",
inspectAccount: (cfg) => {
const channel = cfg.channels?.discord ?? {};
const token = channel.token;
return {
accountId: "default",
enabled: true,
configured:
Boolean(token) &&
typeof token === "object" &&
!Array.isArray(token) &&
"source" in token,
token: "",
tokenSource:
Boolean(token) &&
typeof token === "object" &&
!Array.isArray(token) &&
"source" in token
? "config"
: "none",
tokenStatus:
Boolean(token) &&
typeof token === "object" &&
!Array.isArray(token) &&
"source" in token
? "configured_unavailable"
: "missing",
config: channel,
};
},
resolveAccount: (cfg) => ({ config: cfg.channels?.discord ?? {} }),
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
});
const res = await runSecurityAudit({
config: resolvedConfig,
sourceConfig,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [inspectableDiscordPlugin],
});
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "channels.discord.commands.native.no_allowlists",
severity: "warn",
}),
]),
);
});
});
it("keeps Slack HTTP slash-command findings when resolved inspection only exposes signingSecret status", async () => {
await withChannelSecurityStateDir(async () => {
const sourceConfig: OpenClawConfig = {
channels: {
slack: {
enabled: true,
mode: "http",
groupPolicy: "open",
slashCommand: { enabled: true },
},
},
};
const resolvedConfig: OpenClawConfig = {
channels: {
slack: {
enabled: true,
mode: "http",
groupPolicy: "open",
slashCommand: { enabled: true },
},
},
};
const inspectableSlackPlugin = stubChannelPlugin({
id: "slack",
label: "Slack",
inspectAccount: (cfg) => {
const channel = cfg.channels?.slack ?? {};
if (cfg === sourceConfig) {
return {
accountId: "default",
enabled: false,
configured: true,
mode: "http",
botTokenSource: "config",
botTokenStatus: "configured_unavailable",
signingSecretSource: "config",
signingSecretStatus: "configured_unavailable",
config: channel,
};
}
return {
accountId: "default",
enabled: true,
configured: true,
mode: "http",
botTokenSource: "config",
botTokenStatus: "available",
signingSecretSource: "config",
signingSecretStatus: "available",
config: channel,
};
},
resolveAccount: (cfg) => ({ config: cfg.channels?.slack ?? {} }),
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
});
const res = await runSecurityAudit({
config: resolvedConfig,
sourceConfig,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [inspectableSlackPlugin],
});
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "channels.slack.commands.slash.no_allowlists",
severity: "warn",
}),
]),
);
});
});
it("keeps source-configured Slack HTTP findings when resolved inspection is unconfigured", async () => {
await withChannelSecurityStateDir(async () => {
const sourceConfig: OpenClawConfig = {
channels: {
slack: {
enabled: true,
mode: "http",
groupPolicy: "open",
slashCommand: { enabled: true },
},
},
};
const resolvedConfig: OpenClawConfig = {
channels: {
slack: {
enabled: true,
mode: "http",
groupPolicy: "open",
slashCommand: { enabled: true },
},
},
};
const inspectableSlackPlugin = stubChannelPlugin({
id: "slack",
label: "Slack",
inspectAccount: (cfg) => {
const channel = cfg.channels?.slack ?? {};
if (cfg === sourceConfig) {
return {
accountId: "default",
enabled: true,
configured: true,
mode: "http",
botTokenSource: "config",
botTokenStatus: "configured_unavailable",
signingSecretSource: "config",
signingSecretStatus: "configured_unavailable",
config: channel,
};
}
return {
accountId: "default",
enabled: true,
configured: false,
mode: "http",
botTokenSource: "config",
botTokenStatus: "available",
signingSecretSource: "config",
signingSecretStatus: "missing",
config: channel,
};
},
resolveAccount: (cfg) => ({ config: cfg.channels?.slack ?? {} }),
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
});
const res = await runSecurityAudit({
config: resolvedConfig,
sourceConfig,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [inspectableSlackPlugin],
});
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "channels.slack.commands.slash.no_allowlists",
severity: "warn",
}),
]),
);
});
});
it("does not flag Discord slash commands when dm.allowFrom includes a Discord snowflake id", async () => {
await withChannelSecurityStateDir(async () => {
const cfg: OpenClawConfig = {