mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(secrets): preserve auth profile key refs during provider scrub (#77489)
* fix(secrets): preserve auth profile key refs during provider scrub * Add changelog for secrets apply fix * Seed auth profile ref for scrub regression * fix(secrets): guard auth profile ref scrub --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/performance: pass the resolved workspace through BTW, compaction, embedded-run model generation, and PDF model setup so explicit agent-dir model refreshes can reuse the current workspace-scoped plugin metadata snapshot instead of falling back to cold plugin metadata scans. (#77519, #77532)
|
||||
- Plugins/performance: let unscoped model catalog and manifest-contract readers reuse the current workspace-compatible plugin metadata snapshot, avoiding repeated cold plugin metadata scans on hot control-plane paths while preserving env/config/workspace compatibility checks. (#77519, #77532)
|
||||
- Config/plugin auto-enable: prefer the claiming plugin manifest id over a built-in channel alias when auto-allowlisting a configured channel, so WeCom/Yuanbao-style aliases resolve to the installed plugin id. Thanks @Beandon13.
|
||||
- Secrets/apply: preserve auth-profile `keyRef` and `tokenRef` fields when scrubbing provider-target secrets, so the canonical SecretRef metadata survives `secrets apply` without keeping plaintext values. Thanks @Beandon13.
|
||||
- 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.
|
||||
|
||||
@@ -110,7 +110,8 @@ async function seedDefaultApplyFixture(fixture: ApplyFixture): Promise<void> {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-openai-plaintext", // pragma: allowlist secret
|
||||
key: "sk-ope...text", // pragma: allowlist secret
|
||||
keyRef: OPENAI_API_KEY_ENV_REF,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -290,7 +291,11 @@ describe("secrets apply", () => {
|
||||
profiles: { "openai:default": { key?: string; keyRef?: unknown } };
|
||||
};
|
||||
expect(nextAuthStore.profiles["openai:default"].key).toBeUndefined();
|
||||
expect(nextAuthStore.profiles["openai:default"].keyRef).toBeUndefined();
|
||||
expect(nextAuthStore.profiles["openai:default"].keyRef).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
});
|
||||
|
||||
const nextAuthJson = JSON.parse(await fs.readFile(fixture.authJsonPath, "utf8")) as Record<
|
||||
string,
|
||||
@@ -303,6 +308,58 @@ describe("secrets apply", () => {
|
||||
expect(nextEnv).toContain("UNRELATED=value");
|
||||
});
|
||||
|
||||
it("preserves auth-profile tokenRef during provider scrub", async () => {
|
||||
await writeJsonFile(fixture.authStorePath, {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:bot": {
|
||||
type: "token",
|
||||
provider: "openai",
|
||||
token: "sk-token-plaintext", // pragma: allowlist secret
|
||||
tokenRef: OPENAI_API_KEY_ENV_REF,
|
||||
},
|
||||
},
|
||||
});
|
||||
const plan = createPlan({
|
||||
targets: [createOpenAiProviderTarget()],
|
||||
options: createOneWayScrubOptions(),
|
||||
});
|
||||
|
||||
await runSecretsApply({ plan, env: fixture.env, write: true });
|
||||
|
||||
const nextAuthStore = JSON.parse(await fs.readFile(fixture.authStorePath, "utf8")) as {
|
||||
profiles: { "openai:bot": { token?: string; tokenRef?: unknown } };
|
||||
};
|
||||
expect(nextAuthStore.profiles["openai:bot"].token).toBeUndefined();
|
||||
expect(nextAuthStore.profiles["openai:bot"].tokenRef).toEqual(OPENAI_API_KEY_ENV_REF);
|
||||
});
|
||||
|
||||
it("scrubs malformed auth-profile ref residue during provider scrub", async () => {
|
||||
await writeJsonFile(fixture.authStorePath, {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-openai-plaintext", // pragma: allowlist secret
|
||||
keyRef: "secretref-managed", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
});
|
||||
const plan = createPlan({
|
||||
targets: [createOpenAiProviderTarget()],
|
||||
options: createOneWayScrubOptions(),
|
||||
});
|
||||
|
||||
await runSecretsApply({ plan, env: fixture.env, write: true });
|
||||
|
||||
const nextAuthStore = JSON.parse(await fs.readFile(fixture.authStorePath, "utf8")) as {
|
||||
profiles: { "openai:default": { key?: string; keyRef?: unknown } };
|
||||
};
|
||||
expect(nextAuthStore.profiles["openai:default"].key).toBeUndefined();
|
||||
expect(nextAuthStore.profiles["openai:default"].keyRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips exec SecretRef checks during dry-run unless explicitly allowed", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type OpenClawConfig,
|
||||
} from "../config/config.js";
|
||||
import type { ConfigWriteOptions } from "../config/io.js";
|
||||
import type { SecretProviderConfig } from "../config/types.secrets.js";
|
||||
import { coerceSecretRef, type SecretProviderConfig } from "../config/types.secrets.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { resolveConfigDir, resolveUserPath } from "../utils.js";
|
||||
import { iterateAuthProfileCredentials } from "./auth-profiles-scan.js";
|
||||
@@ -411,7 +411,10 @@ function scrubAuthStoresForProviderTargets(params: {
|
||||
delete profile.profile[profile.valueField];
|
||||
mutated = true;
|
||||
}
|
||||
if (profile.refField in profile.profile) {
|
||||
if (
|
||||
profile.refField in profile.profile &&
|
||||
coerceSecretRef(profile.refValue, params.nextConfig.secrets?.defaults) === null
|
||||
) {
|
||||
delete profile.profile[profile.refField];
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user