fix(secrets): align SecretRef inspect/strict behavior across preload/runtime paths (#66818)

* Config: add inspect/strict SecretRef string resolver

* CLI: pass resolved/source config snapshots to plugin preload

* Slack: keep HTTP route registration config-only

* Providers: normalize SecretRef handling for auth and web tools

* Secrets: add Exa web search target to registry and docs

* Telegram: resolve env SecretRef tokens at runtime

* Agents: resolve custom provider env SecretRef ids

* Providers: fail closed on blocked SecretRef fallback

* Telegram: enforce env SecretRef policy for runtime token refs

* Status/Providers/Telegram: tighten SecretRef preload and fallback handling

* Providers: enforce env SecretRef policy checks in fallback auth paths

* fix: add SecretRef lifecycle changelog entry (#66818) (thanks @joshavant)
This commit is contained in:
Josh Avant
2026-04-14 17:59:28 -05:00
committed by GitHub
parent 4491bdad76
commit 1769fb2aa1
28 changed files with 1497 additions and 70 deletions

View File

@@ -229,7 +229,8 @@ describe("resolveTelegramToken", () => {
expectNoTokenForUnknownAccount(createUnknownAccountConfig());
});
it("throws when botToken is an unresolved SecretRef object", () => {
it("resolves env-backed SecretRefs from process.env", () => {
vi.stubEnv("TELEGRAM_BOT_TOKEN", "secretref-env-token");
const cfg = {
channels: {
telegram: {
@@ -238,6 +239,148 @@ describe("resolveTelegramToken", () => {
},
} as unknown as OpenClawConfig;
expect(resolveTelegramToken(cfg)).toEqual({
token: "secretref-env-token",
source: "config",
});
});
it("does not fall back to TELEGRAM_BOT_TOKEN when an explicit env SecretRef is configured but unavailable", () => {
vi.stubEnv("TELEGRAM_BOT_TOKEN", "fallback-env-token");
vi.stubEnv("TELEGRAM_REF_TOKEN", "");
const cfg = {
channels: {
telegram: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_REF_TOKEN" },
},
},
} as unknown as OpenClawConfig;
expect(resolveTelegramToken(cfg)).toEqual({
token: "",
source: "none",
});
});
it("does not fall through when account-level env SecretRef is configured but unavailable", () => {
vi.stubEnv("TELEGRAM_BOT_TOKEN", "fallback-env-token");
vi.stubEnv("TELEGRAM_ACCOUNT_REF_TOKEN", "");
const cfg = {
channels: {
telegram: {
botToken: "channel-token",
accounts: {
default: {
botToken: {
source: "env",
provider: "default",
id: "TELEGRAM_ACCOUNT_REF_TOKEN",
},
},
},
},
},
} as unknown as OpenClawConfig;
expect(resolveTelegramToken(cfg)).toEqual({
token: "",
source: "none",
});
});
it("does not bypass env provider allowlists for env-backed SecretRefs", () => {
vi.stubEnv("TELEGRAM_BOT_TOKEN", "secretref-env-token");
const cfg = {
secrets: {
providers: {
"telegram-env": {
source: "env",
allowlist: ["OTHER_TELEGRAM_BOT_TOKEN"],
},
},
},
channels: {
telegram: {
botToken: { source: "env", provider: "telegram-env", id: "TELEGRAM_BOT_TOKEN" },
},
},
} as unknown as OpenClawConfig;
expect(() => resolveTelegramToken(cfg)).toThrow(
/not allowlisted in secrets\.providers\.telegram-env\.allowlist/i,
);
});
it("throws when an env SecretRef points at a provider configured with another source", () => {
const cfg = {
secrets: {
providers: {
"telegram-env": {
source: "file",
path: "/tmp/secrets.json",
},
},
},
channels: {
telegram: {
botToken: { source: "env", provider: "telegram-env", id: "TELEGRAM_BOT_TOKEN" },
},
},
} as unknown as OpenClawConfig;
expect(() => resolveTelegramToken(cfg)).toThrow(
/Secret provider "telegram-env" has source "file" but ref requests "env"/i,
);
});
it("throws when an env SecretRef provider is not configured and not the default env alias", () => {
const cfg = {
channels: {
telegram: {
botToken: { source: "env", provider: "ops-env", id: "TELEGRAM_BOT_TOKEN" },
},
},
} as unknown as OpenClawConfig;
expect(() => resolveTelegramToken(cfg)).toThrow(
/Secret provider "ops-env" is not configured \(ref: env:ops-env:TELEGRAM_BOT_TOKEN\)/i,
);
});
it("accepts env SecretRefs that use the configured default env provider alias", () => {
vi.stubEnv("TELEGRAM_RUNTIME_TOKEN", "secretref-env-token");
const cfg = {
secrets: {
defaults: {
env: "telegram-runtime",
},
},
channels: {
telegram: {
botToken: {
source: "env",
provider: "telegram-runtime",
id: "TELEGRAM_RUNTIME_TOKEN",
},
},
},
} as unknown as OpenClawConfig;
expect(resolveTelegramToken(cfg)).toEqual({
token: "secretref-env-token",
source: "config",
});
});
it("keeps strict runtime behavior for unresolved non-env SecretRefs", () => {
const cfg = {
channels: {
telegram: {
botToken: { source: "file", provider: "vault", id: "/telegram/bot-token" },
},
},
} as unknown as OpenClawConfig;
expect(() => resolveTelegramToken(cfg)).toThrow(
/channels\.telegram\.botToken: unresolved SecretRef/i,
);

View File

@@ -3,8 +3,12 @@ import type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-runtime";
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import {
normalizeSecretInputString,
resolveSecretInputString,
} from "openclaw/plugin-sdk/secret-input";
export type TelegramTokenSource = "env" | "tokenFile" | "config" | "none";
@@ -12,6 +16,83 @@ export type TelegramTokenResolution = BaseTokenResolution & {
source: TelegramTokenSource;
};
type RuntimeTokenValueResolution =
| { status: "available"; value: string }
| { status: "configured_unavailable" }
| { status: "missing" };
function resolveEnvSecretRefValue(params: {
cfg?: Pick<OpenClawConfig, "secrets">;
provider: string;
id: string;
env?: NodeJS.ProcessEnv;
}): string | undefined {
const providerConfig = params.cfg?.secrets?.providers?.[params.provider];
if (providerConfig) {
if (providerConfig.source !== "env") {
throw new Error(
`Secret provider "${params.provider}" has source "${providerConfig.source}" but ref requests "env".`,
);
}
if (providerConfig.allowlist && !providerConfig.allowlist.includes(params.id)) {
throw new Error(
`Environment variable "${params.id}" is not allowlisted in secrets.providers.${params.provider}.allowlist.`,
);
}
} else if (
params.provider !== resolveDefaultSecretProviderAlias({ secrets: params.cfg?.secrets }, "env")
) {
throw new Error(
`Secret provider "${params.provider}" is not configured (ref: env:${params.provider}:${params.id}).`,
);
}
return normalizeSecretInputString((params.env ?? process.env)[params.id]);
}
function resolveRuntimeTokenValue(params: {
cfg?: Pick<OpenClawConfig, "secrets">;
value: unknown;
path: string;
}): RuntimeTokenValueResolution {
const resolved = resolveSecretInputString({
value: params.value,
path: params.path,
defaults: params.cfg?.secrets?.defaults,
mode: "inspect",
});
if (resolved.status === "available") {
return {
status: "available",
value: resolved.value,
};
}
if (resolved.status === "missing") {
return { status: "missing" };
}
if (resolved.ref.source === "env") {
const envValue = resolveEnvSecretRefValue({
cfg: params.cfg,
provider: resolved.ref.provider,
id: resolved.ref.id,
});
if (envValue) {
return {
status: "available",
value: envValue,
};
}
return { status: "configured_unavailable" };
}
// Runtime resolution stays strict for non-env SecretRefs.
resolveSecretInputString({
value: params.value,
path: params.path,
defaults: params.cfg?.secrets?.defaults,
mode: "strict",
});
return { status: "configured_unavailable" };
}
type ResolveTelegramTokenOpts = {
envToken?: string | null;
accountId?: string | null;
@@ -79,12 +160,16 @@ export function resolveTelegramToken(
return { token: "", source: "none" };
}
const accountToken = normalizeResolvedSecretInputString({
const accountToken = resolveRuntimeTokenValue({
cfg,
value: accountCfg?.botToken,
path: `channels.telegram.accounts.${accountId}.botToken`,
});
if (accountToken) {
return { token: accountToken, source: "config" };
if (accountToken.status === "available") {
return { token: accountToken.value, source: "config" };
}
if (accountToken.status === "configured_unavailable") {
return { token: "", source: "none" };
}
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
@@ -100,12 +185,16 @@ export function resolveTelegramToken(
return { token: "", source: "none" };
}
const configToken = normalizeResolvedSecretInputString({
const configToken = resolveRuntimeTokenValue({
cfg,
value: telegramCfg?.botToken,
path: "channels.telegram.botToken",
});
if (configToken) {
return { token: configToken, source: "config" };
if (configToken.status === "available") {
return { token: configToken.value, source: "config" };
}
if (configToken.status === "configured_unavailable") {
return { token: "", source: "none" };
}
const envToken = allowEnv ? (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim() : "";