fix(code-queue): add runner skills sync contract

This commit is contained in:
Codex
2026-05-23 04:39:44 +00:00
parent 9418cc4f12
commit 1358803c9a
8 changed files with 709 additions and 29 deletions
@@ -134,7 +134,7 @@ import {
readOaTraceStepsForTask,
} from "./oa-events";
import { collectRuntimePreflight, runtimePreflightJson } from "./runtime-preflight";
import { collectSkillAvailability, skillAvailabilityJson } from "./skill-availability";
import { collectSkillAvailability, collectSkillSyncPreflight, skillAvailabilityJson, skillSyncPreflightJson } from "./skill-availability";
import { configureSelfTests, runJudgeInfraSelfTest, runQueueClaimMoveSelfTest, runQueueOrderingSelfTest, runReferenceInjectionSelfTest, runTracePortSelfTest, runTraceSummaryContractSelfTest } from "./self-tests";
import {
codexToolLifecycleStartedBeforeIn,
@@ -2425,6 +2425,10 @@ function collectSkillsStatus(): JsonValue {
return skillAvailabilityJson(collectSkillAvailability({ target: config.skillsPath }));
}
function collectSkillsSyncPreflight(): JsonValue {
return skillSyncPreflightJson(collectSkillSyncPreflight({ target: config.skillsPath }));
}
function collectDevReady(): JsonValue {
const now = Date.now();
if (devReadyCache !== null && now - devReadyCache.checkedAtMs < 30_000) return devReadyCache.value;
@@ -2471,9 +2475,10 @@ function collectDevReady(): JsonValue {
const githubKnownHostProbe = runProbe("ssh-keygen", ["-F", "github.com", "-f", "/root/.ssh/known_hosts"]);
const sshSharedReady = existsSync("/root/.ssh") && sshKeyProbe.ok && sshKeyProbe.output.trim().length > 0;
const skills = collectSkillsStatus() as Record<string, JsonValue>;
const skillsSync = collectSkillsSyncPreflight() as Record<string, JsonValue>;
const skillsReady = skills.ok === true;
const runtimePreflight = runtimePreflightJson(collectRuntimePreflight({ includeRemote: false, includePushDryRun: false }));
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady;
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady && skillsSync.ok === true;
const value: JsonValue = {
ok,
missingTools,
@@ -2506,6 +2511,7 @@ function collectDevReady(): JsonValue {
ready: sshSharedReady,
},
skills,
skillsSync,
runtimePreflight,
};
devReadyCache = { checkedAtMs: now, value };
@@ -5195,6 +5201,7 @@ async function route(req: Request): Promise<Response> {
databaseLastError,
modelProviderConfig: codeModelProviderSourceContract(config),
skills: collectSkillsStatus(),
skillsSync: collectSkillsSyncPreflight(),
startedAt: serviceStartedAt,
}, 503);
return jsonResponse({
@@ -5213,6 +5220,7 @@ async function route(req: Request): Promise<Response> {
modelProviderConfig: codeModelProviderSourceContract(config),
executionDiagnostics: executionDiagnosticsForTasks(state.tasks),
skills: collectSkillsStatus(),
skillsSync: collectSkillsSyncPreflight(),
egressProxy: await providerGatewayEgressProxyStatus(),
oaEventPublisher: oaEventPublisherStatus(),
startedAt: serviceStartedAt,
@@ -5221,6 +5229,16 @@ async function route(req: Request): Promise<Response> {
if (url.pathname === "/logs") return jsonResponse({ ok: true, logs: recentLogs.slice(-parseLimit(url)) });
if (url.pathname === "/api/events" && req.method === "GET") return jsonResponse({ ok: false, error: "Code Queue private SSE was removed; subscribe to oa-event-flow /api/events/stream with service:code-queue tags." }, 410);
if (url.pathname === "/api/dev-ready" && req.method === "GET") return jsonResponse({ ok: true, devReady: collectDevReady() });
if (url.pathname === "/api/skills-sync" && req.method === "GET") {
const dryRun = url.searchParams.get("dryRun") === "1" || url.searchParams.get("dry-run") === "1";
if (!dryRun) return jsonResponse({
ok: false,
error: "skills sync is dry-run only from the runtime API; pass dryRun=1",
mutation: false,
valuesPrinted: false,
}, 400);
return jsonResponse({ ok: true, skillsSync: collectSkillsSyncPreflight() });
}
if (url.pathname === "/api/runtime-preflight" && req.method === "GET") {
return jsonResponse({
ok: true,
@@ -2,7 +2,7 @@
import { spawnSync } from "node:child_process";
import type { JsonValue } from "./types";
import { collectSkillAvailability, type SkillAvailabilityReport } from "./skill-availability";
import { collectSkillAvailability, collectSkillSyncPreflight, type SkillAvailabilityReport, type SkillSyncPreflightReport } from "./skill-availability";
export type RuntimePreflightAgentPort = "codex" | "opencode";
@@ -53,6 +53,7 @@ export interface RuntimePreflightReport {
};
path: string;
skills: SkillAvailabilityReport;
skillsSync: SkillSyncPreflightReport;
ports: Record<RuntimePreflightAgentPort, RuntimePreflightPortStatus>;
pullRequestDelivery: PullRequestDeliveryPreflight;
}
@@ -648,13 +649,14 @@ export function collectRuntimePreflight(options: RuntimePreflightOptions = {}):
const checkedAt = new Date().toISOString();
const path = process.env.PATH ?? "";
const skills = collectSkillAvailability({ target: process.env.UNIDESK_SKILLS_PATH || "/root/.agents/skills" });
const skillsSync = collectSkillSyncPreflight({ target: process.env.UNIDESK_SKILLS_PATH || "/root/.agents/skills" });
const ports = {
codex: codexStatus(path, checkedAt),
opencode: opencodeStatus(path, checkedAt),
};
const pullRequestDelivery = collectPullRequestDeliveryPreflight(options, checkedAt);
return {
ok: skills.ok && ports.codex.ok && ports.opencode.ok && pullRequestDelivery.ok,
ok: skills.ok && skillsSync.ok && ports.codex.ok && ports.opencode.ok && pullRequestDelivery.ok,
checkedAt,
cwd: process.cwd(),
pid: process.pid,
@@ -665,6 +667,7 @@ export function collectRuntimePreflight(options: RuntimePreflightOptions = {}):
},
path,
skills,
skillsSync,
ports,
pullRequestDelivery,
};
@@ -1,4 +1,4 @@
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { resolve } from "node:path";
import { spawnSync } from "node:child_process";
import type { JsonValue } from "./types";
@@ -38,9 +38,95 @@ export interface SkillAvailabilityReport {
valuesPrinted: false;
}
const defaultRequiredSkills = ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"];
const defaultSource = "/home/ubuntu/.agents/skills";
const expectedTarget = "/root/.agents/skills";
export interface SkillSyncPathReport {
path: string;
approved: boolean;
exists: boolean;
directory: boolean;
readable: boolean;
writable: boolean;
readonly: boolean;
mountPoint: string | null;
skillCount: number;
requiredSkills: string[];
missingSkills: string[];
skills: Array<{ name: string; present: boolean; skillMdPresent: boolean; path: string }>;
error: string | null;
}
export interface SkillSyncPreflightOptions {
source?: string;
target?: string;
requiredSkills?: string[];
}
export interface SkillSyncPreflightReport {
ok: boolean;
degraded: boolean;
blocker:
| "unapproved-source"
| "unapproved-target"
| "skills-source-missing"
| "skills-source-not-directory"
| "skills-source-permission-failed"
| "skills-target-missing"
| "skills-target-not-directory"
| "skills-target-permission-failed"
| "skills-target-not-readonly"
| "required-source-skills-missing"
| "required-target-skills-missing"
| "forbidden-skills-path-present"
| null;
checkedAt: string;
mode: "dry-run";
dryRun: true;
mutation: false;
syncMode: "hostPath-read-only-projection";
source: SkillSyncPathReport;
target: SkillSyncPathReport;
expected: {
source: string;
target: string;
env: "UNIDESK_SKILLS_PATH";
envValue: string;
mount: string;
requiredSkills: string[];
};
counts: {
sourceSkills: number;
targetSkills: number;
requiredSkills: number;
missingSourceSkills: number;
missingTargetSkills: number;
};
missing: {
sourceSkills: string[];
targetSkills: string[];
};
permissionFailures: Array<{ path: string; operation: string; error: string }>;
pathSpelling: SkillAvailabilityReport["pathSpelling"];
plannedActions: {
copy: false;
writesSource: false;
writesTarget: false;
restartRequired: false;
readsSecrets: false;
copyFromArbitraryPath: false;
};
instructions: string[];
commands: {
dryRun: string;
full: string;
health: string;
runtimePreflight: string;
contractTest: string;
};
valuesPrinted: false;
}
export const defaultRequiredSkills = ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"];
export const defaultSource = "/home/ubuntu/.agents/skills";
export const expectedTarget = "/root/.agents/skills";
const forbiddenSkillsDirName = [".ag", "nets"].join("");
const forbiddenTargets = [`/root/${forbiddenSkillsDirName}/skills`, `/home/ubuntu/${forbiddenSkillsDirName}/skills`];
const skillDirectoryAliases: Record<string, string[]> = {
@@ -103,6 +189,90 @@ function skillStatus(target: string, requiredSkills: string[]): SkillAvailabilit
});
}
function accessProbe(path: string, mode: number, operation: string): { ok: boolean; failure: { path: string; operation: string; error: string } | null } {
try {
accessSync(path, mode);
return { ok: true, failure: null };
} catch (error) {
return {
ok: false,
failure: {
path,
operation,
error: error instanceof Error ? error.message : String(error),
},
};
}
}
function emptySkillStatuses(target: string, requiredSkills: string[]): SkillAvailabilityReport["skills"] {
return requiredSkills.map((name) => ({
name,
present: false,
skillMdPresent: false,
path: resolve(target, name),
}));
}
function collectSyncPathReport(path: string, approved: boolean, requiredSkills: string[]): { report: SkillSyncPathReport; permissionFailures: SkillSyncPreflightReport["permissionFailures"] } {
const mountInfo = mountInfoForPath(path);
const exists = existsSync(path);
const permissionFailures: SkillSyncPreflightReport["permissionFailures"] = [];
let directory = false;
let readable = false;
let writable = false;
let skillCount = 0;
let skills = emptySkillStatuses(path, requiredSkills);
let error: string | null = null;
if (exists) {
try {
const stat = statSync(path);
directory = stat.isDirectory();
} catch (probeError) {
error = probeError instanceof Error ? probeError.message : String(probeError);
}
}
if (exists && directory) {
const read = accessProbe(path, constants.R_OK | constants.X_OK, "read-directory");
readable = read.ok;
if (read.failure !== null) permissionFailures.push(read.failure);
const write = accessProbe(path, constants.W_OK, "write-check");
writable = write.ok;
if (readable) {
try {
const entries = readdirSync(path, { withFileTypes: true });
skillCount = entries.filter((entry) => entry.isDirectory()).length;
skills = skillStatus(path, requiredSkills);
} catch (probeError) {
error = probeError instanceof Error ? probeError.message : String(probeError);
permissionFailures.push({ path, operation: "list-skills", error });
}
}
}
const missingSkills = skills.filter((skill) => !skill.present || !skill.skillMdPresent).map((skill) => skill.name);
return {
report: {
path,
approved,
exists,
directory,
readable,
writable,
readonly: exists && directory && (mountInfo.readonly === true || !writable),
mountPoint: mountInfo.mountPoint,
skillCount,
requiredSkills,
missingSkills,
skills,
error,
},
permissionFailures,
};
}
export function collectSkillAvailability(options: SkillAvailabilityOptions): SkillAvailabilityReport {
const target = options.target;
const source = options.source ?? defaultSource;
@@ -183,6 +353,107 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
};
}
export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {}): SkillSyncPreflightReport {
const sourcePath = options.source ?? defaultSource;
const targetPath = options.target ?? expectedTarget;
const requiredSkills = options.requiredSkills ?? defaultRequiredSkills;
const checkedAt = new Date().toISOString();
const source = collectSyncPathReport(sourcePath, sourcePath === defaultSource, requiredSkills);
const target = collectSyncPathReport(targetPath, targetPath === expectedTarget, requiredSkills);
const forbiddenPathExists = forbiddenTargets.some((path) => existsSync(path));
const permissionFailures = [...source.permissionFailures, ...target.permissionFailures];
const blocker = !source.report.approved
? "unapproved-source"
: !target.report.approved
? "unapproved-target"
: !source.report.exists
? "skills-source-missing"
: !source.report.directory
? "skills-source-not-directory"
: !source.report.readable
? "skills-source-permission-failed"
: !target.report.exists
? "skills-target-missing"
: !target.report.directory
? "skills-target-not-directory"
: !target.report.readable
? "skills-target-permission-failed"
: !target.report.readonly
? "skills-target-not-readonly"
: source.report.missingSkills.length > 0
? "required-source-skills-missing"
: target.report.missingSkills.length > 0
? "required-target-skills-missing"
: forbiddenPathExists
? "forbidden-skills-path-present"
: null;
const ok = blocker === null;
return {
ok,
degraded: !ok,
blocker,
checkedAt,
mode: "dry-run",
dryRun: true,
mutation: false,
syncMode: "hostPath-read-only-projection",
source: source.report,
target: target.report,
expected: {
source: defaultSource,
target: expectedTarget,
env: "UNIDESK_SKILLS_PATH",
envValue: expectedTarget,
mount: `${defaultSource} mounted read-only to ${expectedTarget}`,
requiredSkills,
},
counts: {
sourceSkills: source.report.skillCount,
targetSkills: target.report.skillCount,
requiredSkills: requiredSkills.length,
missingSourceSkills: source.report.missingSkills.length,
missingTargetSkills: target.report.missingSkills.length,
},
missing: {
sourceSkills: source.report.missingSkills,
targetSkills: target.report.missingSkills,
},
permissionFailures,
pathSpelling: {
expectedTarget,
forbiddenPathChecked: true,
forbiddenPathExists,
forbiddenPathMustNotBeUsed: true,
},
plannedActions: {
copy: false,
writesSource: false,
writesTarget: false,
restartRequired: false,
readsSecrets: false,
copyFromArbitraryPath: false,
},
instructions: [
`Use ${defaultSource} as the only approved runner skills source and ${expectedTarget} as the only approved Code Queue container target.`,
"Keep the lifecycle as a read-only hostPath projection; this dry-run does not copy files, restart services, roll pods, or read secrets.",
"Update the approved source through the controlled skills installation path, then let production and dev Code Queue pods read it through the manifest mount.",
"Use the full command only when per-skill status or permission failure detail is needed.",
],
commands: {
dryRun: "bun scripts/cli.ts codex skills-sync --dry-run",
full: "bun scripts/cli.ts codex skills-sync --dry-run --full",
health: "bun scripts/cli.ts microservice health code-queue",
runtimePreflight: "bun scripts/cli.ts codex pr-preflight --remote",
contractTest: "bun scripts/code-queue-runner-skills-contract-test.ts",
},
valuesPrinted: false,
};
}
export function skillAvailabilityJson(report: SkillAvailabilityReport): JsonValue {
return report as unknown as JsonValue;
}
export function skillSyncPreflightJson(report: SkillSyncPreflightReport): JsonValue {
return report as unknown as JsonValue;
}