fix(code-queue): reject runner skills typo path
This commit is contained in:
@@ -30,6 +30,8 @@ export interface SkillAvailabilityReport {
|
||||
expectedTarget: string;
|
||||
forbiddenPathChecked: true;
|
||||
forbiddenPathExists: boolean;
|
||||
forbiddenPathConfigured: boolean;
|
||||
forbiddenPathRoles: Array<"source" | "target">;
|
||||
forbiddenPathMustNotBeUsed: true;
|
||||
};
|
||||
expectedMount: string;
|
||||
@@ -75,6 +77,7 @@ export interface SkillSyncPreflightReport {
|
||||
| "skills-target-not-readonly"
|
||||
| "required-source-skills-missing"
|
||||
| "required-target-skills-missing"
|
||||
| "forbidden-skills-path-configured"
|
||||
| "forbidden-skills-path-present"
|
||||
| null;
|
||||
checkedAt: string;
|
||||
@@ -128,6 +131,7 @@ export const defaultRequiredSkills = ["docs-spec", "cli-spec", "frontend-design"
|
||||
export const defaultSource = "/home/ubuntu/.agents/skills";
|
||||
export const expectedTarget = "/root/.agents/skills";
|
||||
const forbiddenSkillsDirName = [".ag", "nets"].join("");
|
||||
const forbiddenRelativeSkillsPath = `${forbiddenSkillsDirName}/skills`;
|
||||
const forbiddenTargets = [`/root/${forbiddenSkillsDirName}/skills`, `/home/ubuntu/${forbiddenSkillsDirName}/skills`];
|
||||
const skillDirectoryAliases: Record<string, string[]> = {
|
||||
"playwright-cli": ["playwright-cli", "playwright"],
|
||||
@@ -151,6 +155,18 @@ function decodeMountInfoPath(value: string): string {
|
||||
return value.replace(/\\([0-7]{3})/gu, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
|
||||
}
|
||||
|
||||
function normalizedPathText(path: string): string {
|
||||
return path.replace(/\\/gu, "/").replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function isForbiddenSkillsPath(path: string): boolean {
|
||||
const normalized = normalizedPathText(path);
|
||||
return normalized === forbiddenRelativeSkillsPath
|
||||
|| normalized === `~/${forbiddenRelativeSkillsPath}`
|
||||
|| normalized.endsWith(`/${forbiddenRelativeSkillsPath}`)
|
||||
|| forbiddenTargets.includes(resolve(path));
|
||||
}
|
||||
|
||||
function mountInfoForPath(path: string): { mountPoint: string | null; readonly: boolean | null } {
|
||||
try {
|
||||
const target = resolve(path);
|
||||
@@ -280,6 +296,9 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
|
||||
const checkedAt = new Date().toISOString();
|
||||
const mountInfo = mountInfoForPath(target);
|
||||
const exists = existsSync(target);
|
||||
const forbiddenSourceConfigured = isForbiddenSkillsPath(source);
|
||||
const forbiddenTargetConfigured = isForbiddenSkillsPath(target);
|
||||
const forbiddenPathConfigured = forbiddenSourceConfigured || forbiddenTargetConfigured;
|
||||
const forbiddenPathExists = forbiddenTargets.some((path) => existsSync(path));
|
||||
let directory = false;
|
||||
let skillCount = 0;
|
||||
@@ -311,7 +330,9 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
|
||||
|
||||
const missingSkills = skills.filter((skill) => !skill.present || !skill.skillMdPresent).map((skill) => skill.name);
|
||||
const available = exists && directory;
|
||||
const blocker = !available
|
||||
const blocker = forbiddenPathConfigured
|
||||
? "forbidden-skills-path-configured"
|
||||
: !available
|
||||
? "skills-target-missing"
|
||||
: !readonly
|
||||
? "skills-target-not-readonly"
|
||||
@@ -342,12 +363,17 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
|
||||
expectedTarget,
|
||||
forbiddenPathChecked: true,
|
||||
forbiddenPathExists,
|
||||
forbiddenPathConfigured,
|
||||
forbiddenPathRoles: [
|
||||
...(forbiddenSourceConfigured ? ["source" as const] : []),
|
||||
...(forbiddenTargetConfigured ? ["target" as const] : []),
|
||||
],
|
||||
forbiddenPathMustNotBeUsed: true,
|
||||
},
|
||||
expectedMount: `${source} mounted read-only to ${target}`,
|
||||
expectedMount: `${defaultSource} mounted read-only to ${expectedTarget}`,
|
||||
repairHint: ok
|
||||
? null
|
||||
: `Mount ${source} read-only at ${target}, set UNIDESK_SKILLS_PATH=${target}, and remove any forbidden skills path spelling.`,
|
||||
: `Mount ${defaultSource} read-only at ${expectedTarget}, set UNIDESK_SKILLS_PATH=${expectedTarget}, and remove any forbidden skills path spelling.`,
|
||||
error,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -360,9 +386,14 @@ export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {
|
||||
const checkedAt = new Date().toISOString();
|
||||
const source = collectSyncPathReport(sourcePath, sourcePath === defaultSource, requiredSkills);
|
||||
const target = collectSyncPathReport(targetPath, targetPath === expectedTarget, requiredSkills);
|
||||
const forbiddenSourceConfigured = isForbiddenSkillsPath(sourcePath);
|
||||
const forbiddenTargetConfigured = isForbiddenSkillsPath(targetPath);
|
||||
const forbiddenPathConfigured = forbiddenSourceConfigured || forbiddenTargetConfigured;
|
||||
const forbiddenPathExists = forbiddenTargets.some((path) => existsSync(path));
|
||||
const permissionFailures = [...source.permissionFailures, ...target.permissionFailures];
|
||||
const blocker = !source.report.approved
|
||||
const blocker = forbiddenPathConfigured
|
||||
? "forbidden-skills-path-configured"
|
||||
: !source.report.approved
|
||||
? "unapproved-source"
|
||||
: !target.report.approved
|
||||
? "unapproved-target"
|
||||
@@ -423,6 +454,11 @@ export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {
|
||||
expectedTarget,
|
||||
forbiddenPathChecked: true,
|
||||
forbiddenPathExists,
|
||||
forbiddenPathConfigured,
|
||||
forbiddenPathRoles: [
|
||||
...(forbiddenSourceConfigured ? ["source" as const] : []),
|
||||
...(forbiddenTargetConfigured ? ["target" as const] : []),
|
||||
],
|
||||
forbiddenPathMustNotBeUsed: true,
|
||||
},
|
||||
plannedActions: {
|
||||
|
||||
Reference in New Issue
Block a user