mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(qa-lab): keep workspace skills in sandbox
This commit is contained in:
@@ -52,6 +52,7 @@ import {
|
||||
callPluginToolsMcp,
|
||||
findSkill,
|
||||
handleQaAction,
|
||||
resolveWorkspaceSkillPath,
|
||||
writeWorkspaceSkill,
|
||||
} from "./suite-runtime-agent-tools.js";
|
||||
import { createTempDirHarness } from "./temp-dir.test-helper.js";
|
||||
@@ -91,6 +92,25 @@ describe("qa suite runtime agent tools helpers", () => {
|
||||
expect(skillPath).toBe(path.join(workspaceDir, "skills", "my-skill", "SKILL.md"));
|
||||
});
|
||||
|
||||
it("rejects workspace skill names that escape the skills directory", async () => {
|
||||
const workspaceDir = await makeTempDir("qa-workspace-");
|
||||
|
||||
for (const name of ["", " spaced", "spaced ", ".", "..", "../escape", "..\\escape", "a/b"]) {
|
||||
expect(() => resolveWorkspaceSkillPath(workspaceDir, name), name).toThrow(
|
||||
`invalid QA workspace skill name: ${JSON.stringify(name)}`,
|
||||
);
|
||||
await expect(
|
||||
writeWorkspaceSkill({
|
||||
env: { gateway: { workspaceDir } } as never,
|
||||
name,
|
||||
body: "escape",
|
||||
}),
|
||||
).rejects.toThrow(`invalid QA workspace skill name: ${JSON.stringify(name)}`);
|
||||
}
|
||||
|
||||
await expect(fs.readdir(path.join(workspaceDir, "skills"))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("routes generic transport actions through the payload extractor", async () => {
|
||||
const handleAction = vi.fn(async () => ({
|
||||
content: [{ type: "text", text: "done" }],
|
||||
|
||||
@@ -26,14 +26,36 @@ function findSkill(skills: QaSkillStatusEntry[], name: string) {
|
||||
return skills.find((skill) => skill.name === name);
|
||||
}
|
||||
|
||||
function resolveWorkspaceSkillPath(workspaceDir: string, name: string) {
|
||||
const trimmed = name.trim();
|
||||
if (
|
||||
!trimmed ||
|
||||
trimmed !== name ||
|
||||
trimmed === "." ||
|
||||
trimmed === ".." ||
|
||||
trimmed.includes("\0") ||
|
||||
/[\\/]/u.test(trimmed)
|
||||
) {
|
||||
throw new Error(`invalid QA workspace skill name: ${JSON.stringify(name)}`);
|
||||
}
|
||||
|
||||
const skillsDir = path.resolve(workspaceDir, "skills");
|
||||
const skillDir = path.resolve(skillsDir, trimmed);
|
||||
const relative = path.relative(skillsDir, skillDir);
|
||||
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
throw new Error(`invalid QA workspace skill name: ${JSON.stringify(name)}`);
|
||||
}
|
||||
return path.join(skillDir, "SKILL.md");
|
||||
}
|
||||
|
||||
async function writeWorkspaceSkill(params: {
|
||||
env: Pick<QaSuiteRuntimeEnv, "gateway">;
|
||||
name: string;
|
||||
body: string;
|
||||
}) {
|
||||
const skillDir = path.join(params.env.gateway.workspaceDir, "skills", params.name);
|
||||
const skillPath = resolveWorkspaceSkillPath(params.env.gateway.workspaceDir, params.name);
|
||||
const skillDir = path.dirname(skillPath);
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
const skillPath = path.join(skillDir, "SKILL.md");
|
||||
await fs.writeFile(skillPath, `${params.body.trim()}\n`, "utf8");
|
||||
return skillPath;
|
||||
}
|
||||
@@ -113,4 +135,10 @@ async function handleQaAction(params: {
|
||||
return extractQaToolPayload(result as Parameters<typeof extractQaToolPayload>[0]);
|
||||
}
|
||||
|
||||
export { callPluginToolsMcp, findSkill, handleQaAction, writeWorkspaceSkill };
|
||||
export {
|
||||
callPluginToolsMcp,
|
||||
findSkill,
|
||||
handleQaAction,
|
||||
resolveWorkspaceSkillPath,
|
||||
writeWorkspaceSkill,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user