Files
openclaw/src/secrets/runtime.test.ts
Peter Steinberger 82d1a03f25 refactor(agents): move implicit-main fallback into load-time roster injection (#112678)
* refactor(agents): require explicit roster defaults

* feat(onboard): create named first roster agent

* refactor(agents): remove runtime main fallbacks

* style(agents): apply roster refactor formatting

* refactor(agents): finish roster-only runtime sweep

* fix(doctor): migrate legacy main session sqlite

* fix(doctor): harden roster session migrations

* fix(onboard): commit first agent atomically

* fix(config): support empty-roster analysis

* fix(agents): preserve legacy main state during creation

* fix(setup): materialize baseline agent roster

* fix(agents): harden legacy default transfer recovery

* fix(agents): simplify roster-only legacy compatibility

* fix(agents): preserve staged first-agent entries

* fix(config): migrate persisted implicit-main rosters

* fix(config): preserve staged empty rosters

* fix(agents): finalize roster-only upgrade paths

* fix(sessions): close legacy main migration outcomes

* fix(config): migrate legacy roster markers at load

* fix(sessions): preserve roster upgrade history

* refactor(sessions): restore lean legacy main compatibility

* fix(setup): prepare first-agent credentials before publish

* fix(config): stabilize roster snapshot migration

* refactor(sessions): shrink legacy main compatibility

* fix(agents): restore roster compatibility fidelity

* fix(sessions): preserve divergent legacy history

* refactor(agents): narrow roster-only scope

* fix(config): isolate roster migration

* test(agents): align roster-only fixtures

* fix(agents): keep main agent undeletable

* fix(agents): harden roster migration invariants

* fix(agents): close setup and audit scope gaps

* fix(cron): scope session reaper throttles by agent

* fix(agents): preserve scoped owner precedence

* fix(config): preserve authored config ownership

* fix(setup): keep default workspace and roster in sync

* fix(setup): preserve default entry workspace on bare runs

* fix(agents): adapt roster rebase to keyed entries

* fix(agents): honor both roster representations

* fix(agents): route roster reads through shared helpers

* fix(config): preserve canonical roster writes

* fix(cron): resolve dynamic default for session reaper

* fix(agents): close dynamic default migration gaps

* fix(agents): align scoped session ownership

* fix(sessions): preserve legacy main directory casing

* fix(agents): align cron and legacy auth ownership

* fix(setup): provision the committed default workspace

* fix(cron): align scoped ownership and reaping

* fix(cron): treat blank agent ids as absent

* fix(cron): retain configured session-store owners

* fix(agents): repair roster-aware CI boundaries

* fix(cron): preserve scoped ownership resolution

* fix(agents): preserve rosterless maintenance paths

* fix(agents): propagate roster ownership through runtime boundaries

* fix(agents): preserve roster ownership across runtime paths

* fix(agents): harden roster diagnostics and legacy routing

* fix(agents): remove redundant diagnostic import

* test(agents): type CLI policy fixture explicitly

* fix(config): preserve canonical roster mutation identity

* fix(doctor): read canonical agent rosters consistently

* fix(config): resolve compound roster unsets safely

* fix(config): finalize main-session reconciliation

* fix(doctor): read canonical session state safely

* fix(sessions): preserve current visibility alias

* fix(config): track roster include provenance

* test(config): type roster provenance cases

* fix(config): refine roster include ownership

* fix(agents): preserve staged roster invariants

* test(config): align fixtures with explicit roster ownership

* test(node-host): preserve optional plan typing

* fix(config): preserve authored roster projections

* test(config): keep raw roster fixtures explicit

* test(config): normalize rosters at runtime fixtures

* fix(config): protect authored roster ownership

* fix(agents): require explicit session ownership

* fix(agents): enforce scoped roster ownership

* fix(sessions): merge fixed-store agent partitions

* fix(agents): harden roster ownership boundaries

* fix(config): reject ambiguous roster projections

* fix(sessions): preserve persisted store ownership

* fix(sessions): keep collision diagnostics additive

* fix(security): scan malformed roster workspaces

* test(config): align snapshot fixtures after rebase

* test(agents): use explicit roster fixtures

* fix(config): harden roster diagnostic boundaries

* fix(sessions): isolate fixed-store agent databases

* test(agents): type malformed default markers

* refactor(sessions): extract store collision resolution

* test(system-agent): split oversized setup coverage

* style(system-agent): format split setup suite

* fix(sessions): preserve promoted store ownership

* fix(sessions): derive scoped owner before target

* fix(sessions): preserve explicit sqlite ownership

* fix(agents): restore roster compatibility across CI

* fix(agents): enforce roster-owned runtime boundaries

* fix(agents): satisfy default lookup lint

* test(sessions): split known-owner coverage

* fix(state): satisfy path identity lint

* fix(agents): preserve malformed roster safety boundaries

* fix(agents): restore roster compatibility at runtime boundaries

* fix(config): satisfy roster boundary type checks

* fix(agents): preserve roster ownership across runtime probes

Setup inference probes now execute as the configured roster owner. Malformed agent-prefixed session rows are intentionally omitted by the fail-closed visibility contract rather than normalized by tests.

* fix(agents): satisfy session list owner lint

* fix(agents): preserve roster-owned runtime boundaries

Restore shared logical rows for exact SQLite session locators while keeping their physical database owner separate. The ownership regression test now constructs an explicit sole-owner database directly instead of relying on first-touch capture, matching the intentional shared-store contract.

* fix(sessions): preserve multiply owned exact stores

* fix(sessions): restore runtime owner boundaries

Keep incognito sentinels agent-owned, fold default-agent approvals into the global snapshot, and preserve the configless legacy-main CLI policy fallback. Also repair the existing CLI watchdog test lifecycle so the compact shard observes its timeout without an unawaited assertion or async timer stall; product behavior is unchanged by that test-only fix.

* test(ci): align owner-scoped fixtures

These assertions are unchanged. The fixtures now declare the intended non-default runner, expose the session-key constant imported by production status code, and select the main approvals bucket explicitly on Windows.

* fix(agents): close final roster ownership gaps
2026-07-24 22:38:09 -07:00

937 lines
29 KiB
TypeScript

/** Tests runtime SecretRef resolution across core config and auth-profile surfaces. */
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.ts";
import { redactSensitiveText } from "../logging/redact.js";
import { resetSecretRedactionRegistryForTest } from "../logging/secret-redaction-registry.test-support.js";
import { assertSecretOwnerAvailable } from "./runtime-degraded-state.js";
import {
activateSecretsRuntimeSnapshotState,
clearSecretsRuntimeSnapshot,
} from "./runtime-state.js";
import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts";
const EMPTY_LOADABLE_PLUGIN_ORIGINS = new Map();
const BUNDLED_CODEX_PLUGIN_ORIGINS = new Map([["codex", "bundled" as const]]);
const BUNDLED_WEBHOOKS_PLUGIN_ORIGINS = new Map([["webhooks", "bundled" as const]]);
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
function explicitMainRoster() {
return { agents: { list: [{ id: "main", default: true }] } };
}
const CODEX_APP_SERVER_TOKEN_REF = {
source: "env",
provider: "default",
id: "CODEX_APP_SERVER_TOKEN",
} as const;
afterEach(() => {
resetSecretRedactionRegistryForTest();
clearSecretsRuntimeSnapshot();
});
const TTS_REF = {
source: "env",
provider: "default",
id: "ELEVENLABS_API_KEY",
} as const;
function expectWarning(
snapshot: Awaited<ReturnType<typeof prepareSecretsRuntimeSnapshot>>,
expected: { code: string; path: string },
): void {
const warning = snapshot.warnings.find(
(entry) => entry.code === expected.code && entry.path === expected.path,
);
if (!warning) {
throw new Error(`Expected warning ${expected.code} ${expected.path}`);
}
}
describe("secrets runtime snapshot", () => {
it("refreshes healthy owners while an unchanged failed owner keeps last-known-good", async () => {
const ref = (id: string) => ({ source: "env" as const, provider: "default", id });
const config = (firstId: string) =>
asConfig({
...explicitMainRoster(),
models: {
providers: {
first: {
apiKey: ref(firstId),
baseUrl: "https://first.example.invalid/v1",
models: [],
},
second: {
apiKey: ref("SECOND_KEY"),
baseUrl: "https://second.example.invalid/v1",
models: [],
},
},
},
});
const active = await prepareSecretsRuntimeSnapshot({
config: config("FIRST_KEY"),
env: { FIRST_KEY: "first-old", SECOND_KEY: "second-old" },
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
activateSecretsRuntimeSnapshotState({
snapshot: active,
refreshContext: null,
refreshHandler: null,
});
const candidate = await prepareSecretsRuntimeSnapshot({
config: config("FIRST_KEY"),
env: { SECOND_KEY: "second-new" },
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(candidate.config.models?.providers?.first?.apiKey).toBe("first-old");
expect(candidate.config.models?.providers?.second?.apiKey).toBe("second-new");
expect(candidate.degradedOwners).toMatchObject([
{ ownerKind: "provider", ownerId: "first", degradationState: "stale" },
]);
activateSecretsRuntimeSnapshotState({
snapshot: candidate,
refreshContext: null,
refreshHandler: null,
});
expect(() => assertSecretOwnerAvailable("provider", "first")).not.toThrow();
});
it("keeps last-known-good across equivalent SecretRef encodings", async () => {
const canonicalRef = {
source: "env" as const,
provider: "default",
id: "PROVIDER_KEY",
};
const config = (apiKey: typeof canonicalRef | string) =>
asConfig({
...explicitMainRoster(),
models: {
providers: {
first: {
apiKey,
baseUrl: "https://first.example.invalid/v1",
models: [],
},
},
},
});
const active = await prepareSecretsRuntimeSnapshot({
config: config(canonicalRef),
env: { PROVIDER_KEY: "last-known-good" },
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
activateSecretsRuntimeSnapshotState({
snapshot: active,
refreshContext: null,
refreshHandler: null,
});
const candidate = await prepareSecretsRuntimeSnapshot({
config: config("$PROVIDER_KEY"),
env: {},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(candidate.config.models?.providers?.first?.apiKey).toBe("last-known-good");
expect(candidate.degradedOwners).toMatchObject([
{ ownerKind: "provider", ownerId: "first", degradationState: "stale" },
]);
});
it("makes a changed unresolved owner cold while healthy siblings refresh", async () => {
const ref = (id: string) => ({ source: "env" as const, provider: "default", id });
const config = (firstId: string) =>
asConfig({
...explicitMainRoster(),
models: {
providers: {
first: {
apiKey: ref(firstId),
baseUrl: "https://first.example.invalid/v1",
models: [],
},
second: {
apiKey: ref("SECOND_KEY"),
baseUrl: "https://second.example.invalid/v1",
models: [],
},
},
},
});
const active = await prepareSecretsRuntimeSnapshot({
config: config("FIRST_KEY"),
env: { FIRST_KEY: "first-old", SECOND_KEY: "second-old" },
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
activateSecretsRuntimeSnapshotState({
snapshot: active,
refreshContext: null,
refreshHandler: null,
});
const changedRef = ref("FIRST_KEY_CHANGED");
const candidate = await prepareSecretsRuntimeSnapshot({
config: config(changedRef.id),
env: { SECOND_KEY: "second-new" },
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(candidate.config.models?.providers?.first?.apiKey).toEqual(changedRef);
expect(candidate.config.models?.providers?.second?.apiKey).toBe("second-new");
expect(candidate.degradedOwners).toMatchObject([
{ ownerKind: "provider", ownerId: "first", degradationState: "cold" },
]);
activateSecretsRuntimeSnapshotState({
snapshot: candidate,
refreshContext: null,
refreshHandler: null,
});
expect(() => assertSecretOwnerAvailable("provider", "first")).toThrow(
"configured but unavailable",
);
});
it("does not send a stale provider credential to a changed endpoint", async () => {
const apiKeyRef = {
source: "env" as const,
provider: "default",
id: "PROVIDER_KEY",
};
const config = (baseUrl: string) =>
asConfig({
...explicitMainRoster(),
models: {
providers: {
first: { apiKey: apiKeyRef, baseUrl, models: [] },
},
},
});
const active = await prepareSecretsRuntimeSnapshot({
config: config("https://old.example.invalid/v1"),
env: { PROVIDER_KEY: "last-known-good" },
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
activateSecretsRuntimeSnapshotState({
snapshot: active,
refreshContext: null,
refreshHandler: null,
});
const candidate = await prepareSecretsRuntimeSnapshot({
config: config("https://new.example.invalid/v1"),
env: {},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(candidate.config.models?.providers?.first).toMatchObject({
apiKey: apiKeyRef,
baseUrl: "https://new.example.invalid/v1",
});
expect(candidate.degradedOwners).toMatchObject([
{ ownerKind: "provider", ownerId: "first", degradationState: "cold" },
]);
});
it("isolates only the skill whose API key cannot resolve", async () => {
const missingRef = {
source: "env",
provider: "default",
id: "MISSING_SKILL_KEY",
} as const;
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
skills: {
entries: {
cold: { apiKey: missingRef },
healthy: {
apiKey: { source: "env", provider: "default", id: "HEALTHY_SKILL_KEY" },
},
},
},
}),
env: { HEALTHY_SKILL_KEY: "healthy" },
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.skills?.entries?.cold?.apiKey).toEqual(missingRef);
expect(snapshot.config.skills?.entries?.healthy?.apiKey).toBe("healthy");
expect(snapshot.degradedOwners).toMatchObject([
{
ownerKind: "capability",
ownerId: "skill:cold",
state: "unavailable",
paths: ["skills.entries.cold.apiKey"],
},
]);
expectWarning(snapshot, {
code: "SECRETS_OWNER_UNAVAILABLE",
path: "skills.entries.cold.apiKey",
});
});
it("isolates one webhooks route while resolving its sibling snapshot", async () => {
const missingRef = {
source: "env",
provider: "default",
id: "MISSING_WEBHOOK_SECRET",
} as const;
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
plugins: {
entries: {
webhooks: {
enabled: true,
config: {
routes: {
healthy: {
sessionKey: "agent:main:main",
secret: {
source: "env",
provider: "default",
id: "HEALTHY_WEBHOOK_SECRET",
},
},
cold: {
sessionKey: "agent:main:main",
secret: missingRef,
},
inlineCold: {
sessionKey: "agent:main:main",
secret: "${MISSING_INLINE_WEBHOOK_SECRET}",
},
},
},
},
},
},
}),
env: { HEALTHY_WEBHOOK_SECRET: "healthy-secret" },
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: BUNDLED_WEBHOOKS_PLUGIN_ORIGINS,
});
const routes = snapshot.config.plugins?.entries?.webhooks?.config?.routes as Record<
string,
{ secret?: unknown }
>;
expect(routes.healthy?.secret).toBe("healthy-secret");
expect(routes.cold?.secret).toEqual(missingRef);
expect(routes.inlineCold?.secret).toEqual({
source: "env",
provider: "default",
id: "MISSING_INLINE_WEBHOOK_SECRET",
});
expect(snapshot.degradedOwners).toMatchObject([
{
ownerKind: "route",
ownerId: "plugins.entries.webhooks.config.routes.cold.secret",
state: "unavailable",
paths: ["plugins.entries.webhooks.config.routes.cold.secret"],
reason: "secret reference was not found",
},
{
ownerKind: "route",
ownerId: "plugins.entries.webhooks.config.routes.inlineCold.secret",
state: "unavailable",
paths: ["plugins.entries.webhooks.config.routes.inlineCold.secret"],
reason: "secret reference was not found",
},
]);
});
it("registers every resolved value for exact redaction", async () => {
const secret = "runtime-registration-secret";
await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
talk: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
}),
env: { TALK_API_KEY: secret },
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(redactSensitiveText(`resolved ${secret}`, { mode: "off" })).toBe("resolved runtim…cret");
});
it("registers resolved TTS values for exact redaction", async () => {
const secret = "test-secret";
await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
tts: { providers: { elevenlabs: { apiKey: TTS_REF } } },
}),
env: { ELEVENLABS_API_KEY: secret },
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(redactSensitiveText(`resolved ${secret}`, { mode: "off" })).toBe("resolved ***");
});
it("resolves sandbox ssh secret refs for active ssh backends", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
agents: {
list: [{ id: "main", default: true }],
defaults: {
sandbox: {
mode: "all",
backend: "ssh",
ssh: {
target: "peter@example.com:22",
identityData: { source: "env", provider: "default", id: "SSH_IDENTITY_DATA" },
certificateData: {
source: "env",
provider: "default",
id: "SSH_CERTIFICATE_DATA",
},
knownHostsData: {
source: "env",
provider: "default",
id: "SSH_KNOWN_HOSTS_DATA",
},
},
},
},
},
}),
env: {
SSH_IDENTITY_DATA: "PRIVATE KEY",
SSH_CERTIFICATE_DATA: "SSH CERT",
SSH_KNOWN_HOSTS_DATA: "example.com ssh-ed25519 AAAATEST",
},
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
const ssh = snapshot.config.agents?.defaults?.sandbox?.ssh;
expect(ssh?.identityData).toBe("PRIVATE KEY");
expect(ssh?.certificateData).toBe("SSH CERT");
expect(ssh?.knownHostsData).toBe("example.com ssh-ed25519 AAAATEST");
});
it("keeps SSH lifecycle secrets materialized after the agent sandbox is disabled", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
agents: {
defaults: {
sandbox: {
mode: "off",
backend: "ssh",
ssh: { target: "peter@example.com:22" },
},
},
list: [
{
id: "worker",
default: true,
enabled: false,
sandbox: {
ssh: {
identityData: {
source: "env",
provider: "default",
id: "DISABLED_WORKER_SSH_IDENTITY",
},
},
},
},
],
},
}),
env: { DISABLED_WORKER_SSH_IDENTITY: "DISABLED WORKER PRIVATE KEY" },
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.agents?.list?.[0]?.sandbox?.ssh?.identityData).toBe(
"DISABLED WORKER PRIVATE KEY",
);
});
it("keeps default SSH lifecycle secrets materialized when every listed agent overrides them", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
agents: {
defaults: {
sandbox: {
mode: "off",
backend: "ssh",
ssh: {
target: "peter@example.com:22",
identityData: {
source: "env",
provider: "default",
id: "DEFAULT_SSH_IDENTITY",
},
},
},
},
entries: {
worker: {
default: true,
sandbox: {
ssh: {
identityData: {
source: "env",
provider: "default",
id: "WORKER_SSH_IDENTITY",
},
},
},
},
},
},
}),
env: {
DEFAULT_SSH_IDENTITY: "DEFAULT PRIVATE KEY",
WORKER_SSH_IDENTITY: "WORKER PRIVATE KEY",
},
includeAuthStoreRefs: false,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.agents?.defaults?.sandbox?.ssh?.identityData).toBe(
"DEFAULT PRIVATE KEY",
);
expect(snapshot.config.agents?.entries?.worker?.sandbox?.ssh?.identityData).toBe(
"WORKER PRIVATE KEY",
);
});
it("isolates only the agent whose inherited sandbox SSH SecretRef is unavailable", async () => {
const missingRef = {
source: "env",
provider: "default",
id: "MISSING_COLD_SSH_IDENTITY",
} as const;
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
agents: {
defaults: {
sandbox: {
mode: "all",
backend: "ssh",
ssh: {
target: "sandbox@example.com:22",
identityData: missingRef,
},
},
},
entries: {
cold: { default: true },
healthy: {
sandbox: {
ssh: {
identityData: {
source: "env",
provider: "default",
id: "HEALTHY_SSH_IDENTITY",
},
},
},
},
},
},
}),
env: { HEALTHY_SSH_IDENTITY: "HEALTHY PRIVATE KEY" },
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.agents?.defaults?.sandbox?.ssh?.identityData).toEqual(missingRef);
expect(snapshot.config.agents?.entries?.healthy?.sandbox?.ssh?.identityData).toBe(
"HEALTHY PRIVATE KEY",
);
expect(snapshot.degradedOwners).toMatchObject([
{
ownerKind: "capability",
ownerId: "agent-sandbox:cold",
state: "unavailable",
paths: ["agents.defaults.sandbox.ssh.identityData"],
},
]);
expectWarning(snapshot, {
code: "SECRETS_OWNER_UNAVAILABLE",
path: "agents.defaults.sandbox.ssh.identityData",
});
});
it("treats sandbox ssh secret refs as inactive when ssh backend is not selected", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
agents: {
list: [{ id: "main", default: true }],
defaults: {
sandbox: {
mode: "all",
backend: "docker",
ssh: {
identityData: { source: "env", provider: "default", id: "SSH_IDENTITY_DATA" },
},
},
},
},
}),
env: {},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.agents?.defaults?.sandbox?.ssh?.identityData).toEqual({
source: "env",
provider: "default",
id: "SSH_IDENTITY_DATA",
});
expectWarning(snapshot, {
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
path: "agents.defaults.sandbox.ssh.identityData",
});
});
it("resolves active bundled Codex app-server plugin SecretRefs", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
plugins: {
entries: {
codex: {
enabled: true,
config: {
appServer: {
transport: "websocket",
url: "wss://codex-app-server.example.internal/ws",
authToken: CODEX_APP_SERVER_TOKEN_REF,
headers: {
Authorization: "Bearer literal-token",
"x-codex-client-session-token": "${CODEX_CLIENT_SESSION_TOKEN}",
},
},
},
},
},
},
}),
env: {
CODEX_APP_SERVER_TOKEN: "resolved-app-server-token",
CODEX_CLIENT_SESSION_TOKEN: "resolved-session-token",
},
includeAuthStoreRefs: false,
loadablePluginOrigins: BUNDLED_CODEX_PLUGIN_ORIGINS,
});
expect(snapshot.config.plugins?.entries?.codex?.config).toMatchObject({
appServer: {
authToken: "resolved-app-server-token",
headers: {
Authorization: "Bearer literal-token",
"x-codex-client-session-token": "resolved-session-token",
},
},
});
});
it("fails active bundled Codex app-server plugin SecretRefs when env is missing", async () => {
await expect(
prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
plugins: {
entries: {
codex: {
enabled: true,
config: {
appServer: {
transport: "websocket",
url: "wss://codex-app-server.example.internal/ws",
authToken: CODEX_APP_SERVER_TOKEN_REF,
headers: {
"x-codex-client-session-token": "${CODEX_CLIENT_SESSION_TOKEN}",
},
},
},
},
},
},
}),
env: {
CODEX_CLIENT_SESSION_TOKEN: "resolved-session-token",
},
includeAuthStoreRefs: false,
loadablePluginOrigins: BUNDLED_CODEX_PLUGIN_ORIGINS,
}),
).rejects.toThrow('Environment variable "CODEX_APP_SERVER_TOKEN" is missing or empty.');
});
it("isolates the TTS owner when its SecretRef is missing during cold startup", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
tts: {
providers: {
elevenlabs: {
apiKey: TTS_REF,
},
},
},
}),
env: {},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.tts?.providers?.elevenlabs?.apiKey).toEqual(TTS_REF);
expectWarning(snapshot, {
code: "SECRETS_OWNER_UNAVAILABLE",
path: "tts.providers.elevenlabs.apiKey",
});
expect(snapshot.degradedOwners).toMatchObject([
{
ownerKind: "capability",
ownerId: "tts",
state: "unavailable",
paths: ["tts.providers.elevenlabs.apiKey"],
reason: "secret reference was not found",
},
]);
expect(snapshot.warnings[0]?.message).not.toContain("ELEVENLABS_API_KEY");
});
it("isolates the TTS owner when a file value is absent", async () => {
if (process.platform === "win32") {
return;
}
const root = tempDirs.make("openclaw-tts-secretref-missing-");
const secretsPath = path.join(root, "secrets.json");
await fs.writeFile(secretsPath, JSON.stringify({ providers: {} }, null, 2), "utf8");
await fs.chmod(secretsPath, 0o600);
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
secrets: {
providers: {
ttsfile: {
source: "file",
path: secretsPath,
mode: "json",
},
},
},
tts: {
providers: {
elevenlabs: {
apiKey: {
source: "file",
provider: "ttsfile",
id: "/providers/elevenlabs/apiKey",
},
},
},
},
}),
env: {},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.tts?.providers?.elevenlabs?.apiKey).toEqual({
source: "file",
provider: "ttsfile",
id: "/providers/elevenlabs/apiKey",
});
expectWarning(snapshot, {
code: "SECRETS_OWNER_UNAVAILABLE",
path: "tts.providers.elevenlabs.apiKey",
});
expect(snapshot.warnings[0]?.message).toContain("secret reference was not found");
});
it("rejects owner isolation after provider policy failures", async () => {
await expect(
prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
secrets: {
providers: {
default: {
source: "env",
allowlist: ["OTHER_API_KEY"],
},
},
},
tts: {
providers: {
elevenlabs: {
apiKey: TTS_REF,
},
},
},
}),
env: {
ELEVENLABS_API_KEY: "test-elevenlabs-api-key",
},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
}),
).rejects.toThrow("not allowlisted");
});
it("keeps invalid TTS SecretRef ids fail-closed", async () => {
await expect(
prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
tts: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "elevenlabs_api_key" },
},
},
},
}),
env: {
elevenlabs_api_key: "test-elevenlabs-api-key",
},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
}),
).rejects.toThrow("Env secret reference id must match");
});
it("keeps unconfigured SecretRef provider aliases fail-closed", async () => {
await expect(
prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
tts: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "missing", id: "ELEVENLABS_API_KEY" },
},
},
},
}),
env: {},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
}),
).rejects.toThrow('Secret provider "missing" is not configured');
});
it("isolates an unavailable model provider without applying another credential source", async () => {
const ref = { source: "env", provider: "default", id: "MISSING_PROVIDER_KEY" } as const;
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
models: {
providers: {
example: {
apiKey: ref,
baseUrl: "https://example.invalid/v1",
models: [{ id: "example-model", name: "Example" }],
},
},
},
}),
env: { EXAMPLE_API_KEY: "placeholder" },
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.models?.providers?.example?.apiKey).toEqual(ref);
expect(snapshot.degradedOwners).toMatchObject([
{
ownerKind: "provider",
ownerId: "example",
state: "unavailable",
paths: ["models.providers.example.apiKey"],
},
]);
});
it("isolates cron webhook delivery when its token cannot resolve", async () => {
const ref = { source: "env", provider: "default", id: "MISSING_WEBHOOK_TOKEN" } as const;
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
cron: { webhookToken: ref },
}),
env: {},
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true,
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
});
expect(snapshot.config.cron?.webhookToken).toEqual(ref);
expect(snapshot.degradedOwners).toMatchObject([
{
ownerKind: "capability",
ownerId: "cron-webhook",
state: "unavailable",
paths: ["cron.webhookToken"],
},
]);
});
it("fails when an active exec ref id contains traversal segments", async () => {
await expect(
prepareSecretsRuntimeSnapshot({
config: asConfig({
...explicitMainRoster(),
talk: {
apiKey: { source: "exec", provider: "vault", id: "a/../b" },
},
secrets: {
providers: {
vault: {
source: "exec",
command: process.execPath,
},
},
},
}),
env: {},
includeAuthStoreRefs: false,
agentDirs: ["/tmp/openclaw-agent-main"],
loadAuthStore: () => ({ version: 1, profiles: {} }),
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
}),
).rejects.toThrow(/must not include "\." or "\.\." path segments/i);
});
});