fix(agents): prefer runtime snapshot for skill secrets

This commit is contained in:
Vincent Koc
2026-03-23 13:03:19 -07:00
parent 6c58277577
commit 13e81870bb
5 changed files with 120 additions and 25 deletions

View File

@@ -1,28 +1,21 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
clearRuntimeConfigSnapshot,
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../../config/config.js";
import * as skillsModule from "../skills.js";
import type { SkillSnapshot } from "../skills.js";
const hoisted = vi.hoisted(() => ({
loadWorkspaceSkillEntries: vi.fn(
(_workspaceDir: string, _options?: { config?: OpenClawConfig }) => [],
),
}));
vi.mock("../skills.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../skills.js")>();
return {
...actual,
loadWorkspaceSkillEntries: (workspaceDir: string, options?: { config?: OpenClawConfig }) =>
hoisted.loadWorkspaceSkillEntries(workspaceDir, options),
};
});
const { resolveEmbeddedRunSkillEntries } = await import("./skills-runtime.js");
describe("resolveEmbeddedRunSkillEntries", () => {
const loadWorkspaceSkillEntriesSpy = vi.spyOn(skillsModule, "loadWorkspaceSkillEntries");
beforeEach(() => {
hoisted.loadWorkspaceSkillEntries.mockReset();
hoisted.loadWorkspaceSkillEntries.mockReturnValue([]);
clearRuntimeConfigSnapshot();
loadWorkspaceSkillEntriesSpy.mockReset();
loadWorkspaceSkillEntriesSpy.mockReturnValue([]);
});
it("loads skill entries with config when no resolved snapshot skills exist", () => {
@@ -44,8 +37,47 @@ describe("resolveEmbeddedRunSkillEntries", () => {
});
expect(result.shouldLoadSkillEntries).toBe(true);
expect(hoisted.loadWorkspaceSkillEntries).toHaveBeenCalledTimes(1);
expect(hoisted.loadWorkspaceSkillEntries).toHaveBeenCalledWith("/tmp/workspace", { config });
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledTimes(1);
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledWith("/tmp/workspace", { config });
});
it("prefers the active runtime snapshot when caller config still contains SecretRefs", () => {
const sourceConfig: OpenClawConfig = {
skills: {
entries: {
diffs: {
apiKey: {
source: "file",
provider: "default",
id: "/skills/entries/diffs/apiKey",
},
},
},
},
};
const runtimeConfig: OpenClawConfig = {
skills: {
entries: {
diffs: {
apiKey: "resolved-key",
},
},
},
};
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
resolveEmbeddedRunSkillEntries({
workspaceDir: "/tmp/workspace",
config: sourceConfig,
skillsSnapshot: {
prompt: "skills prompt",
skills: [],
},
});
expect(loadWorkspaceSkillEntriesSpy).toHaveBeenCalledWith("/tmp/workspace", {
config: runtimeConfig,
});
});
it("skips skill entry loading when resolved snapshot skills are present", () => {
@@ -65,6 +97,6 @@ describe("resolveEmbeddedRunSkillEntries", () => {
shouldLoadSkillEntries: false,
skillEntries: [],
});
expect(hoisted.loadWorkspaceSkillEntries).not.toHaveBeenCalled();
expect(loadWorkspaceSkillEntriesSpy).not.toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../../config/config.js";
import { loadWorkspaceSkillEntries, type SkillEntry, type SkillSnapshot } from "../skills.js";
import { resolveSkillRuntimeConfig } from "../skills/runtime-config.js";
export function resolveEmbeddedRunSkillEntries(params: {
workspaceDir: string;
@@ -10,10 +11,11 @@ export function resolveEmbeddedRunSkillEntries(params: {
skillEntries: SkillEntry[];
} {
const shouldLoadSkillEntries = !params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
const config = resolveSkillRuntimeConfig(params.config);
return {
shouldLoadSkillEntries,
skillEntries: shouldLoadSkillEntries
? loadWorkspaceSkillEntries(params.workspaceDir, { config: params.config })
? loadWorkspaceSkillEntries(params.workspaceDir, { config })
: [],
};
}

View File

@@ -1,6 +1,11 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
clearRuntimeConfigSnapshot,
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../config/config.js";
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
import { writeSkill } from "./skills.e2e-test-helpers.js";
@@ -75,6 +80,10 @@ afterAll(async () => {
await fixtureSuite.cleanup();
});
afterEach(() => {
clearRuntimeConfigSnapshot();
});
describe("buildWorkspaceSkillCommandSpecs", () => {
it("sanitizes and de-duplicates command names", async () => {
const workspaceDir = await makeWorkspace();
@@ -370,6 +379,50 @@ describe("applySkillEnvOverrides", () => {
});
});
it("prefers the active runtime snapshot over raw SecretRef skill config", async () => {
const workspaceDir = await makeWorkspace();
await writeEnvSkill(workspaceDir);
const entries = loadWorkspaceSkillEntries(workspaceDir, resolveTestSkillDirs(workspaceDir));
const sourceConfig: OpenClawConfig = {
skills: {
entries: {
"env-skill": {
apiKey: {
source: "file",
provider: "default",
id: "/skills/entries/env-skill/apiKey",
},
},
},
},
};
const runtimeConfig: OpenClawConfig = {
skills: {
entries: {
"env-skill": {
apiKey: "resolved-key",
},
},
},
};
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
withClearedEnv(["ENV_KEY"], () => {
const restore = applySkillEnvOverrides({
skills: entries,
config: sourceConfig,
});
try {
expect(process.env.ENV_KEY).toBe("resolved-key");
} finally {
restore();
expect(process.env.ENV_KEY).toBeUndefined();
}
});
});
it("blocks unsafe env overrides but allows declared secrets", async () => {
const workspaceDir = await makeWorkspace();
const skillDir = path.join(workspaceDir, "skills", "unsafe-env-skill");

View File

@@ -5,6 +5,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
import { sanitizeEnvVars, validateEnvVarValue } from "../sandbox/sanitize-env-vars.js";
import { resolveSkillConfig } from "./config.js";
import { resolveSkillKey } from "./frontmatter.js";
import { resolveSkillRuntimeConfig } from "./runtime-config.js";
import type { SkillEntry, SkillSnapshot } from "./types.js";
const log = createSubsystemLogger("env-overrides");
@@ -211,7 +212,8 @@ function createEnvReverter(updates: EnvUpdate[]) {
}
export function applySkillEnvOverrides(params: { skills: SkillEntry[]; config?: OpenClawConfig }) {
const { skills, config } = params;
const { skills } = params;
const config = resolveSkillRuntimeConfig(params.config);
const updates: EnvUpdate[] = [];
for (const entry of skills) {
@@ -237,7 +239,8 @@ export function applySkillEnvOverridesFromSnapshot(params: {
snapshot?: SkillSnapshot;
config?: OpenClawConfig;
}) {
const { snapshot, config } = params;
const { snapshot } = params;
const config = resolveSkillRuntimeConfig(params.config);
if (!snapshot) {
return () => {};
}

View File

@@ -0,0 +1,5 @@
import { getRuntimeConfigSnapshot, type OpenClawConfig } from "../../config/config.js";
export function resolveSkillRuntimeConfig(config?: OpenClawConfig): OpenClawConfig | undefined {
return getRuntimeConfigSnapshot() ?? config;
}