fix: confine sandbox skill sync destinations

This commit is contained in:
Peter Steinberger
2026-02-13 01:24:46 +01:00
parent 113ebfd6a2
commit 3eb6a31b6f
3 changed files with 128 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ import type {
} from "./types.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
import { resolveSandboxPath } from "../sandbox-paths.js";
import { resolveBundledSkillsDir } from "./bundled-dir.js";
import { shouldIncludeSkill } from "./config.js";
import {
@@ -301,6 +302,45 @@ export function loadWorkspaceSkillEntries(
return loadSkillEntries(workspaceDir, opts);
}
function resolveUniqueSyncedSkillDirName(base: string, used: Set<string>): string {
if (!used.has(base)) {
used.add(base);
return base;
}
for (let index = 2; index < 10_000; index += 1) {
const candidate = `${base}-${index}`;
if (!used.has(candidate)) {
used.add(candidate);
return candidate;
}
}
let fallbackIndex = 10_000;
let fallback = `${base}-${fallbackIndex}`;
while (used.has(fallback)) {
fallbackIndex += 1;
fallback = `${base}-${fallbackIndex}`;
}
used.add(fallback);
return fallback;
}
function resolveSyncedSkillDestinationPath(params: {
targetSkillsDir: string;
entry: SkillEntry;
usedDirNames: Set<string>;
}): string | null {
const sourceDirName = path.basename(params.entry.skill.baseDir).trim();
if (!sourceDirName || sourceDirName === "." || sourceDirName === "..") {
return null;
}
const uniqueDirName = resolveUniqueSyncedSkillDirName(sourceDirName, params.usedDirNames);
return resolveSandboxPath({
filePath: uniqueDirName,
cwd: params.targetSkillsDir,
root: params.targetSkillsDir,
}).resolved;
}
export async function syncSkillsToWorkspace(params: {
sourceWorkspaceDir: string;
targetWorkspaceDir: string;
@@ -326,8 +366,28 @@ export async function syncSkillsToWorkspace(params: {
await fsp.rm(targetSkillsDir, { recursive: true, force: true });
await fsp.mkdir(targetSkillsDir, { recursive: true });
const usedDirNames = new Set<string>();
for (const entry of entries) {
const dest = path.join(targetSkillsDir, entry.skill.name);
let dest: string | null = null;
try {
dest = resolveSyncedSkillDestinationPath({
targetSkillsDir,
entry,
usedDirNames,
});
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
console.warn(
`[skills] Failed to resolve safe destination for ${entry.skill.name}: ${message}`,
);
continue;
}
if (!dest) {
console.warn(
`[skills] Failed to resolve safe destination for ${entry.skill.name}: invalid source directory name`,
);
continue;
}
try {
await fsp.cp(entry.skill.baseDir, dest, {
recursive: true,