fix(code-queue): reject runner skills typo path

This commit is contained in:
Codex
2026-05-23 09:05:23 +00:00
parent a0fb63c098
commit 023509d10a
5 changed files with 167 additions and 8 deletions
@@ -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: {