fix: resolve code queue runner skills path

Resolve Code Queue runner skills through approved source fallback while preserving hostPath contract diagnostics.
This commit is contained in:
Lyon
2026-05-23 22:50:31 +08:00
committed by GitHub
parent 12fdc9e238
commit 026a718a24
11 changed files with 308 additions and 76 deletions
@@ -1,4 +1,6 @@
import { readFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { collectSkillAvailability, collectSkillSyncPreflight } from "../src/components/microservices/code-queue/src/skill-availability";
import { codexPrPreflightQueryForTest } from "./src/code-queue";
import { summarizeMicroserviceObservation } from "./src/microservices";
@@ -18,6 +20,14 @@ function countOccurrences(haystack: string, needle: string): number {
return haystack.split(needle).length - 1;
}
function createSkillSet(root: string, skills: string[]): void {
for (const skill of skills) {
const dir = join(root, skill);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "SKILL.md"), `---\nname: ${skill}\n---\n# ${skill}\n`);
}
}
const productionManifest = readFileSync("src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml", "utf8");
const devManifest = readFileSync("src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml", "utf8");
const runtimePreflight = readFileSync("src/components/microservices/code-queue/src/runtime-preflight.ts", "utf8");
@@ -58,6 +68,8 @@ const available = collectSkillAvailability({
});
assertCondition(available.source === "/home/ubuntu/.agents/skills", "skill report must expose source");
assertCondition(available.target === "/home/ubuntu/.agents/skills", "skill report must expose target");
assertCondition(typeof available.resolvedPath === "string" && available.resolvedPath.length > 0, "skill report must expose resolved path", available);
assertCondition(asRecord(available.resolution, "available.resolution").passesToRunnerEnv === true, "skill report must expose runner env path resolution", available.resolution);
assertCondition(Array.isArray(available.requiredSkills) && available.requiredSkills.includes("docs-spec"), "skill report must expose requiredSkills");
assertCondition(Array.isArray(available.missingSkills), "skill report must expose missingSkills");
assertCondition(available.valuesPrinted === false, "skill report must declare valuesPrinted=false");
@@ -65,15 +77,74 @@ assertCondition(asRecord(available.pathSpelling, "pathSpelling").forbiddenPathMu
assertCondition(!JSON.stringify(available).includes(forbiddenPathLiteral), "skill report must not propagate misspelled path literal");
assertCondition(!JSON.stringify(available).includes("GH_TOKEN"), "skill report must not include secret environment names unrelated to skills");
const tmpRoot = mkdtempSync(join(tmpdir(), "unidesk-codequeue-skills-"));
const fixtureSource = join(tmpRoot, "source");
const fixtureMissingTarget = join(tmpRoot, "target-missing");
const fixtureSymlinkTarget = join(tmpRoot, "target-symlink");
const fixtureMissingSource = join(tmpRoot, "source-missing");
mkdirSync(fixtureSource, { recursive: true });
createSkillSet(fixtureSource, ["docs-spec", "cli-spec"]);
symlinkSync(fixtureSource, fixtureSymlinkTarget, "dir");
const sourceFallback = collectSkillAvailability({
source: fixtureSource,
target: fixtureMissingTarget,
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(sourceFallback.ok === true, "source exists target missing should keep runner usable", sourceFallback);
assertCondition(sourceFallback.runnerUsable === true, "source fallback should mark runner usable", sourceFallback);
assertCondition(sourceFallback.contractOk === false, "source fallback should still mark target projection contract degraded", sourceFallback);
assertCondition(sourceFallback.degraded === true, "source fallback should remain degraded for host rollout", sourceFallback);
assertCondition(sourceFallback.blocker === "skills-target-missing", "source fallback should preserve target missing degraded reason", sourceFallback);
assertCondition(sourceFallback.degradedReason === "skills-target-missing", "source fallback should expose bounded degraded reason", sourceFallback);
assertCondition(sourceFallback.resolvedPath === fixtureSource, "source fallback should resolve to source path", sourceFallback);
assertCondition(sourceFallback.resolvedPathSource === "source-fallback", "source fallback should expose resolved path source", sourceFallback);
assertCondition(sourceFallback.skillCount === 2 && sourceFallback.sourceSkillCount === 2 && sourceFallback.targetSkillCount === 0, "source fallback should expose bounded counts", sourceFallback);
assertCondition(asRecord(sourceFallback.resolution, "sourceFallback.resolution").runnerEnvValue === fixtureSource, "source fallback should pass resolved path to runner env", sourceFallback.resolution);
assertCondition(asRecord(sourceFallback.resolution, "sourceFallback.resolution").hostRolloutRequired === true, "source fallback should require host rollout repair", sourceFallback.resolution);
const symlinkOk = collectSkillAvailability({
source: fixtureSource,
target: fixtureSymlinkTarget,
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(symlinkOk.ok === true && symlinkOk.contractOk === true, "target symlink to source should satisfy runner and contract", symlinkOk);
assertCondition(symlinkOk.resolvedPath === fixtureSymlinkTarget, "target symlink should keep target as runner path", symlinkOk);
assertCondition(symlinkOk.resolvedPathSource === "target-symlink", "target symlink should expose target-symlink source", symlinkOk);
assertCondition(symlinkOk.targetSymlink === true, "target symlink should be reported", symlinkOk);
assertCondition(asRecord(symlinkOk.resolution, "symlinkOk.resolution").hostRolloutRequired === false, "target symlink should not require rollout repair", symlinkOk.resolution);
const missingBoth = collectSkillAvailability({
source: fixtureMissingSource,
target: fixtureMissingTarget,
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(missingBoth.ok === false && missingBoth.runnerUsable === false, "missing source and target should fail runner availability", missingBoth);
assertCondition(missingBoth.blocker === "skills-source-and-target-missing", "missing both should expose dedicated blocker", missingBoth);
assertCondition(missingBoth.resolvedPathSource === "missing", "missing both should expose missing resolution", missingBoth);
const redactionProbe = collectSkillAvailability({
source: fixtureSource,
target: fixtureMissingTarget,
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(!JSON.stringify(redactionProbe).includes("ghp_"), "skill report must not include token-like values", redactionProbe);
assertCondition(!JSON.stringify(redactionProbe).includes("github_pat_"), "skill report must not include GitHub PAT-like values", redactionProbe);
rmSync(tmpRoot, { recursive: true, force: true });
const missing = collectSkillAvailability({
source: "/home/ubuntu/.agents/skills",
target: "/path/that/does/not/exist/for-code-queue-skills-test",
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(missing.ok === false, "missing target should fail");
assertCondition(missing.ok === true, "approved source should keep missing-target runner usable");
assertCondition(missing.runnerUsable === true, "missing target with approved source should expose runner usable");
assertCondition(missing.contractOk === false, "missing target with approved source should expose hostPath contract degraded");
assertCondition(missing.degraded === true, "missing target should be degraded");
assertCondition(missing.blocker === "skills-target-missing", "missing target should expose blocker", missing);
assertCondition(missing.missingSkills.includes("docs-spec") && missing.missingSkills.includes("cli-spec"), "missing target should list required missing skills", missing);
assertCondition(missing.targetMissingSkills.includes("docs-spec") && missing.targetMissingSkills.includes("cli-spec"), "missing target should list target missing skills", missing);
assertCondition(missing.resolvedPath === "/home/ubuntu/.agents/skills", "missing target should resolve to approved source", missing);
assertCondition(missing.resolvedPathSource === "source-fallback", "missing target should expose source fallback", missing);
assertCondition(missing.valuesPrinted === false, "missing report must also declare valuesPrinted=false");
const typoTarget = collectSkillAvailability({
@@ -128,8 +199,9 @@ assertCondition(runtimePreflight.includes("skills: SkillAvailabilityReport"), "r
assertCondition(runtimePreflight.includes("skillsSync: SkillSyncPreflightReport"), "runtime preflight type must include skills sync report");
assertCondition(runtimePreflight.includes("collectSkillAvailability"), "runtime preflight must collect skills availability");
assertCondition(runtimePreflight.includes("collectSkillSyncPreflight"), "runtime preflight must collect skills sync preflight");
assertCondition(runtimePreflight.includes("skills.ok && skillsSync.ok && ports.codex.ok"), "runtime preflight ok must depend on skills and skillsSync");
assertCondition(indexSource.includes("const skillsReady = skills.ok === true"), "dev-ready must gate on structured skills ok");
assertCondition(runtimePreflight.includes("skills.runnerUsable && ports.codex.ok"), "runtime preflight ok must depend on runner usable skills without blocking on host rollout contract drift");
assertCondition(indexSource.includes("skills.runnerUsable === true"), "dev-ready must gate on structured runner usable skills");
assertCondition(indexSource.includes("resolvedRunnerSkillsPath"), "runtime must pass resolved skills path to code agents");
assertCondition(indexSource.includes("collectSkillsSyncPreflight"), "runtime index must expose skills sync preflight");
assertCondition(indexSource.includes("/api/skills-sync"), "runtime must expose a dry-run skills sync endpoint");
assertCondition(indexSource.includes("pass dryRun=1"), "skills sync endpoint must reject non-dry-run calls");
@@ -199,8 +271,8 @@ const skillsPreflightTransport = {
}),
};
const defaultPreflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], skillsPreflightTransport), "default preflight summary");
assertCondition(defaultPreflightSummary.failureKind === "runner-skills-blocker", "default preflight should classify missing skills as runner-skills-blocker", defaultPreflightSummary);
assertCondition(defaultPreflightSummary.degradedReason === "unapproved-target", "default preflight degraded reason should use the skills sync blocker first", defaultPreflightSummary);
assertCondition(defaultPreflightSummary.failureKind !== "runner-skills-blocker", "source fallback should not classify as runner skills blocker", defaultPreflightSummary);
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").hostRolloutRequired === true, "default preflight should expose host rollout blocker separately", defaultPreflightSummary);
assertCondition(defaultPreflightSummary.preflight === undefined, "default PR preflight should omit detailed preflight internals", defaultPreflightSummary);
assertCondition(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").fullDetailOmitted === true, "default PR preflight should disclose full detail omission", defaultPreflightSummary.disclosure);
assertCondition(String(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").expandWith ?? "").includes("--full"), "default PR preflight should point to --full expansion", defaultPreflightSummary.disclosure);
@@ -209,9 +281,11 @@ const preflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote", "--f
const preflight = asRecord(preflightSummary.preflight, "preflight");
const preflightSkills = asRecord(preflight.skills, "preflight.skills");
const preflightSkillsSync = asRecord(preflight.skillsSync, "preflight.skillsSync");
assertCondition(preflightSummary.failureKind === "runner-skills-blocker", "full preflight should classify missing skills as runner-skills-blocker", preflightSummary);
assertCondition(preflightSummary.degradedReason === "unapproved-target", "full preflight degraded reason should use the skills sync blocker first", preflightSummary);
assertCondition(preflightSummary.failureKind !== "runner-skills-blocker", "full preflight should keep source fallback out of runner blocker classification", preflightSummary);
assertCondition(asRecord(preflightSummary.skillsContract, "preflightSummary.skillsContract").degradedReason === "skills-target-missing", "full preflight should expose target missing as contract degraded reason", preflightSummary);
assertCondition(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "full preflight must show skills target", preflightSkills);
assertCondition(preflightSkills.resolvedPath === "/home/ubuntu/.agents/skills", "full preflight must show resolved source fallback path", preflightSkills);
assertCondition(preflightSkills.resolvedPathSource === "source-fallback", "full preflight must show source fallback resolution", preflightSkills);
assertCondition(preflightSkillsSync.dryRun === true && preflightSkillsSync.mutation === false, "full preflight must show non-mutating skills sync dry-run", preflightSkillsSync);
assertCondition(asRecord(preflightSkillsSync.counts, "preflight.skillsSync.counts").missingTargetSkills === 2, "full preflight must show missing target count", preflightSkillsSync);
assertCondition(asRecord(preflightSkillsSync.plannedActions, "preflight.skillsSync.plannedActions").copy === false, "full preflight must show no copy action", preflightSkillsSync);
+26 -4
View File
@@ -6078,7 +6078,12 @@ function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
if (record === null) return null;
return {
ok: record.ok ?? false,
runnerUsable: record.runnerUsable ?? record.ok ?? false,
contractOk: record.contractOk ?? record.ok ?? false,
path: record.path ?? null,
resolvedPath: record.resolvedPath ?? record.path ?? null,
resolvedPathSource: record.resolvedPathSource ?? null,
resolution: record.resolution ?? null,
source: record.source ?? null,
target: record.target ?? null,
mountPoint: record.mountPoint ?? null,
@@ -6086,8 +6091,13 @@ function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
available: record.available ?? false,
degraded: record.degraded ?? true,
blocker: record.blocker ?? null,
degradedReason: record.degradedReason ?? record.blocker ?? null,
readonly: record.readonly ?? false,
skillCount: record.skillCount ?? 0,
sourceSkillCount: record.sourceSkillCount ?? null,
targetSkillCount: record.targetSkillCount ?? null,
sourceMissingSkills: Array.isArray(record.sourceMissingSkills) ? record.sourceMissingSkills.map(String) : [],
targetMissingSkills: Array.isArray(record.targetMissingSkills) ? record.targetMissingSkills.map(String) : [],
requiredSkills: Array.isArray(record.requiredSkills) ? record.requiredSkills : [],
missingSkills: Array.isArray(record.missingSkills) ? record.missingSkills : [],
valuesPrinted: record.valuesPrinted ?? false,
@@ -6115,6 +6125,8 @@ function compactSkillPathReport(value: unknown): Record<string, unknown> | null
writable: record.writable ?? false,
readonly: record.readonly ?? false,
mountPoint: record.mountPoint ?? null,
symlink: record.symlink ?? false,
realPath: record.realPath ?? null,
skillCount: record.skillCount ?? 0,
requiredSkills: Array.isArray(record.requiredSkills) ? record.requiredSkills.map(String) : [],
missingSkills: Array.isArray(record.missingSkills) ? record.missingSkills.map(String) : [],
@@ -6729,6 +6741,7 @@ function compactPrPreflightCommanderView(record: Record<string, unknown>, option
capabilitySource: authBrokerCapability?.source ?? tokenCoverage?.credentialSource ?? null,
},
},
skillsContract: record.skillsContract ?? undefined,
activeRunnerPrCapability: {
scope: activeRunnerDevContainer.scope ?? "current-cli-process",
tokenCandidatePresent: activeRunnerTokenCandidatePresent,
@@ -6902,11 +6915,11 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
const risks = Array.isArray(pull.risks) ? pull.risks.map(String) : [];
const ok = preflight.ok === true && tokenCoverage.ok === true;
const githubTransient = githubTransientEvidence(pull);
const skillsBlocked = skills !== null && skills.ok !== true;
const skillsSyncBlocked = skillsSync !== null && skillsSync.ok !== true;
const skillsBlocked = skills !== null && skills.runnerUsable !== true && skills.ok !== true;
const skillsContractDegraded = skills !== null && skills.contractOk !== true;
const failureKind = !tokenCoverage.ok
? "auth-missing"
: skillsBlocked || skillsSyncBlocked
: skillsBlocked
? "runner-skills-blocker"
: githubTransient !== null
? "github-transient"
@@ -6918,7 +6931,7 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
const degradedReason = failureKind === "auth-missing"
? "auth-broker-needed"
: failureKind === "runner-skills-blocker"
? typeof skillsSync?.blocker === "string" ? skillsSync.blocker : typeof skills?.blocker === "string" ? skills.blocker : "runner-skills-degraded"
? typeof skills?.degradedReason === "string" ? skills.degradedReason : typeof skills?.blocker === "string" ? skills.blocker : typeof skillsSync?.blocker === "string" ? skillsSync.blocker : "runner-skills-degraded"
: failureKind === "github-transient"
? "github-dns-api-transient"
: failureKind === "git-remote-gap"
@@ -6948,6 +6961,13 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
},
skills,
skillsSync,
skillsContract: {
ok: !skillsContractDegraded,
degraded: skillsContractDegraded,
hostRolloutRequired: asRecord(skills?.resolution)?.hostRolloutRequired === true,
degradedReason: skills?.degradedReason ?? skills?.blocker ?? skillsSync?.blocker ?? null,
note: skillsContractDegraded ? "runner skills are usable through the resolved path, but the read-only target projection still needs host rollout repair" : null,
},
tokenCoverage,
authBroker: authBrokerNeededStatus(tokenCoverage, authBrokerRuntime, systemGhBinary, unideskGhCli),
authScopeSummary,
@@ -7319,6 +7339,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP
scopeBoundary: compact.scopeBoundary,
activeRunnerDevContainer: compact.activeRunnerDevContainer,
recommendedActions: compact.recommendedActions,
skillsContract: compact.skillsContract,
upstream: { ok: response.ok, status: response.status },
controlPlane: {
mode: "local-backend-core",
@@ -7378,6 +7399,7 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher:
scopeBoundary: compact.scopeBoundary,
activeRunnerDevContainer: compact.activeRunnerDevContainer,
recommendedActions: compact.recommendedActions,
skillsContract: compact.skillsContract,
upstream: { ok: response.ok, status: response.status },
controlPlane: {
mode: "remote-frontend",
+14
View File
@@ -626,7 +626,12 @@ function compactSkillAvailability(value: unknown): Record<string, unknown> | nul
if (skills === null) return null;
return {
ok: skills.ok ?? false,
runnerUsable: skills.runnerUsable ?? skills.ok ?? false,
contractOk: skills.contractOk ?? skills.ok ?? false,
path: skills.path ?? null,
resolvedPath: skills.resolvedPath ?? skills.path ?? null,
resolvedPathSource: skills.resolvedPathSource ?? null,
resolution: skills.resolution ?? null,
source: skills.source ?? null,
target: skills.target ?? null,
mountPoint: skills.mountPoint ?? null,
@@ -634,8 +639,13 @@ function compactSkillAvailability(value: unknown): Record<string, unknown> | nul
available: skills.available ?? false,
degraded: skills.degraded ?? true,
blocker: skills.blocker ?? null,
degradedReason: skills.degradedReason ?? skills.blocker ?? null,
readonly: skills.readonly ?? false,
skillCount: skills.skillCount ?? 0,
sourceSkillCount: skills.sourceSkillCount ?? null,
targetSkillCount: skills.targetSkillCount ?? null,
sourceMissingSkills: Array.isArray(skills.sourceMissingSkills) ? skills.sourceMissingSkills.map(String) : [],
targetMissingSkills: Array.isArray(skills.targetMissingSkills) ? skills.targetMissingSkills.map(String) : [],
requiredSkills: Array.isArray(skills.requiredSkills) ? skills.requiredSkills.map(String) : [],
missingSkills: Array.isArray(skills.missingSkills) ? skills.missingSkills.map(String) : [],
valuesPrinted: skills.valuesPrinted ?? false,
@@ -663,6 +673,8 @@ function compactSkillSync(value: unknown): Record<string, unknown> | null {
"writable",
"readonly",
"mountPoint",
"symlink",
"realPath",
"skillCount",
"requiredSkills",
"missingSkills",
@@ -677,6 +689,8 @@ function compactSkillSync(value: unknown): Record<string, unknown> | null {
"writable",
"readonly",
"mountPoint",
"symlink",
"realPath",
"skillCount",
"requiredSkills",
"missingSkills",