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:
@@ -236,9 +236,9 @@ bun scripts/code-queue-pr-preflight-example.ts --repo pikasTech/unidesk --base m
|
||||
|
||||
### Runner Skill 可用性
|
||||
|
||||
D601 Code Queue runner 的长期 skills source of truth 是宿主 `/home/ubuntu/.agents/skills`,生产和 dev Code Queue Pod 都必须只读挂载到容器内 `/root/.agents/skills`,并设置 `UNIDESK_SKILLS_PATH=/root/.agents/skills`。不要使用或传播任何拼错的 skills 路径;如果配置的 source 或 target 命中拼写错误路径,health/preflight 必须直接返回 `blocker=forbidden-skills-path-configured`,并在 `pathSpelling.forbiddenPathConfigured=true` 与 `pathSpelling.forbiddenPathRoles` 中标明是 source、target 或两者,而不是降级成普通 missing path。
|
||||
D601 Code Queue runner 的长期 skills source of truth 是宿主 `/home/ubuntu/.agents/skills`,生产和 dev Code Queue Pod 都必须只读挂载到容器内 `/root/.agents/skills`,并设置 `UNIDESK_SKILLS_PATH=/root/.agents/skills`。如果 target 暂缺但 approved source 存在且必需 skill 可读,runtime 必须把 `UNIDESK_SKILLS_PATH` 解析为可用 source 传给 runner,并在 health/preflight 中标记 `runnerUsable=true`、`contractOk=false`、`resolvedPathSource=source-fallback`、`degradedReason=skills-target-missing` 与 `resolution.hostRolloutRequired=true`;这表示任务执行能力可用,但 hostPath 投影仍需 host rollout 修复。不要使用或传播任何拼错的 skills 路径;如果配置的 source 或 target 命中拼写错误路径,health/preflight 必须直接返回 `blocker=forbidden-skills-path-configured`,并在 `pathSpelling.forbiddenPathConfigured=true` 与 `pathSpelling.forbiddenPathRoles` 中标明是 source、target 或两者,而不是降级成普通 missing path。
|
||||
|
||||
执行面 `/health`、`/api/dev-ready` 和 `/api/runtime-preflight` 必须输出同一份只读 skill availability report。稳定字段包括 `source`、`target`、`requiredSkills`、`missingSkills`、`degraded`、`blocker`、`pathSpelling` 和 `valuesPrinted=false`。`requiredSkills` 至少覆盖 `docs-spec`、`cli-spec`、`frontend-design` 与 `playwright-cli`;如果目标目录缺失、不是只读挂载、必需 skill 缺失、拼写错误路径被配置或拼写错误路径已存在,报告必须显示 `ok=false` 和结构化 `blocker`,不能把 runner 能力缺口伪装成业务任务失败。
|
||||
执行面 `/health`、`/api/dev-ready` 和 `/api/runtime-preflight` 必须输出同一份只读 skill availability report。稳定字段包括 `source`、`target`、`resolvedPath`、`resolvedPathSource`、`resolution`、`requiredSkills`、`missingSkills`、`sourceSkillCount`、`targetSkillCount`、`degraded`、`degradedReason`、`blocker`、`pathSpelling` 和 `valuesPrinted=false`。`requiredSkills` 至少覆盖 `docs-spec`、`cli-spec`、`frontend-design` 与 `playwright-cli`;如果 source 与 target 都缺失、必需 skill 均不可用、拼写错误路径被配置或拼写错误路径已存在,报告必须显示 `ok=false` 和结构化 `blocker`,不能把 runner 能力缺口伪装成业务任务失败。target symlink 到 approved source 属于可接受的兼容修复,必须显示 `resolvedPathSource=target-symlink` 和有界 skill count。
|
||||
|
||||
执行面还必须提供 dry-run skills sync/preflight 合同:稳定入口是 `GET /api/skills-sync?dryRun=1`,CLI 入口是 `bun scripts/cli.ts codex skills-sync --dry-run [--full]`。该合同只描述受控 hostPath 生命周期,不复制文件、不从任意路径静默加载、不重启服务、不 rollout Pod、不读取 Secret。默认输出保持紧凑,必须报告 source、target、expected env/mount、required skill 列表、source/target skill counts、missing source/target skills、permission failure count、plannedActions 和修复指令;逐 skill 细节、完整 permission failure 和原始报告只能通过 `--full` 显式展开。非 dry-run 请求必须失败。
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { extractRecord, extractString, terminalStatus, textInput, withCodeAgentG
|
||||
import { classifyRunnerError, runnerErrorClassificationJson } from "../runner-error-classifier";
|
||||
|
||||
export interface CodexPortContext {
|
||||
config: Pick<RuntimeConfig, "approvalPolicy" | "codexHome" | "defaultWorkdir" | "sandbox" | "sourceCodexConfig" | "turnNoActivityTimeoutMs">;
|
||||
config: Pick<RuntimeConfig, "approvalPolicy" | "codexHome" | "defaultWorkdir" | "resolvedRunnerSkillsPath" | "sandbox" | "sourceCodexConfig" | "turnNoActivityTimeoutMs">;
|
||||
activeRuns: Map<string, ActiveRun>;
|
||||
appendOutput: (task: QueueTask, channel: "system" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error", text: string, method?: string, itemId?: string, append?: boolean) => unknown;
|
||||
addEvent: (task: QueueTask, event: CodexEventSummary) => void;
|
||||
@@ -97,6 +97,7 @@ function codexAppServerEnv(task: QueueTask): NodeJS.ProcessEnv {
|
||||
...process.env,
|
||||
CODEX_HOME: ctx().config.codexHome,
|
||||
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "unidesk_code_queue",
|
||||
UNIDESK_SKILLS_PATH: ctx().config.resolvedRunnerSkillsPath(),
|
||||
};
|
||||
if (!shouldRunCodexDirect()) return withCodeAgentGitConfigEnv(env);
|
||||
for (const key of codexProxyEnvKeys) delete env[key];
|
||||
|
||||
@@ -10,7 +10,7 @@ import { codeAgentGitConfigEntries, deepseekChatModel, extractRecord, minimaxM27
|
||||
import { classifyRunnerError, runnerErrorClassificationJson } from "../runner-error-classifier";
|
||||
|
||||
export interface OpenCodePortContext {
|
||||
config: Pick<RuntimeConfig, "deepseekApiBase" | "deepseekApiKey" | "deepseekModel" | "defaultWorkdir" | "minimaxApiBase" | "minimaxApiKey" | "minimaxModel" | "turnNoActivityTimeoutMs">;
|
||||
config: Pick<RuntimeConfig, "deepseekApiBase" | "deepseekApiKey" | "deepseekModel" | "defaultWorkdir" | "minimaxApiBase" | "minimaxApiKey" | "minimaxModel" | "resolvedRunnerSkillsPath" | "turnNoActivityTimeoutMs">;
|
||||
activeRuns: Map<string, ActiveRun>;
|
||||
addEvent: (task: QueueTask, event: CodexEventSummary) => void;
|
||||
appendOutput: (task: QueueTask, channel: "system" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error", text: string, method?: string, itemId?: string, append?: boolean) => unknown;
|
||||
@@ -140,6 +140,7 @@ function openCodeEnv(task: QueueTask): NodeJS.ProcessEnv {
|
||||
MINIMAX_API_BASE: ctx().config.minimaxApiBase,
|
||||
MINIMAX_MODEL: ctx().config.minimaxModel,
|
||||
OPENCODE_CONFIG_CONTENT: openCodeConfigContent(),
|
||||
UNIDESK_SKILLS_PATH: ctx().config.resolvedRunnerSkillsPath(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -402,6 +402,7 @@ function readConfig(): RuntimeConfig {
|
||||
executionProviderIds,
|
||||
remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "DEEPSEEK_API_KEY", "DEEPSEEK_API_BASE", "DEEPSEEK_MODEL", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST", "GITHUB_API_URL", "GH_REPO"]),
|
||||
skillsPath: envString("UNIDESK_SKILLS_PATH", "/root/.agents/skills"),
|
||||
resolvedRunnerSkillsPath,
|
||||
codexHome: envString("CODE_QUEUE_CODEX_HOME", "/var/lib/unidesk/code-queue/codex-home"),
|
||||
opencodeXdgDir: envString("CODE_QUEUE_OPENCODE_XDG_DIR", resolve(dataDir, "opencode-xdg")),
|
||||
sourceCodexConfig: envString("CODE_QUEUE_SOURCE_CODEX_CONFIG", "/root/.codex/config.toml"),
|
||||
@@ -2464,14 +2465,22 @@ function runProbe(command: string, args: string[], timeout = 3_000): { ok: boole
|
||||
return { ok: result.status === 0, output };
|
||||
}
|
||||
|
||||
function currentSkillAvailability() {
|
||||
return collectSkillAvailability({ target: config.skillsPath });
|
||||
}
|
||||
|
||||
function collectSkillsStatus(): JsonValue {
|
||||
return skillAvailabilityJson(collectSkillAvailability({ target: config.skillsPath }));
|
||||
return skillAvailabilityJson(currentSkillAvailability());
|
||||
}
|
||||
|
||||
function collectSkillsSyncPreflight(): JsonValue {
|
||||
return skillSyncPreflightJson(collectSkillSyncPreflight({ target: config.skillsPath }));
|
||||
}
|
||||
|
||||
function resolvedRunnerSkillsPath(): string {
|
||||
return currentSkillAvailability().resolution.runnerEnvValue;
|
||||
}
|
||||
|
||||
function collectDevReady(): JsonValue {
|
||||
const now = Date.now();
|
||||
if (devReadyCache !== null && now - devReadyCache.checkedAtMs < 30_000) return devReadyCache.value;
|
||||
@@ -2519,9 +2528,9 @@ function collectDevReady(): JsonValue {
|
||||
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 skillsReady = skills.ok === true || skills.runnerUsable === true;
|
||||
const runtimePreflight = runtimePreflightJson(collectRuntimePreflight({ includeRemote: false, includePushDryRun: false }));
|
||||
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady && skillsSync.ok === true;
|
||||
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady;
|
||||
const value: JsonValue = {
|
||||
ok,
|
||||
missingTools,
|
||||
|
||||
@@ -8,7 +8,7 @@ export const codeQueueEnvironmentHintTitle = "# Code Queue 运行环境提示";
|
||||
export const codeQueueEnvironmentHint = [
|
||||
codeQueueEnvironmentHintTitle,
|
||||
"如果当前 Code Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/code-queue/Dockerfile`,让后续任务重建镜像后可直接使用。",
|
||||
"任务可通过 `UNIDESK_SKILLS_PATH`(默认 `/root/.agents/skills`)读取注入的宿主 skills;若需要确认注入状态,查询 Code Queue `/api/dev-ready` 的 `devReady.skills`,缺失时报告该字段的修复建议,不要读取或输出宿主 token/auth 配置。",
|
||||
"任务可通过 `UNIDESK_SKILLS_PATH` 读取注入的宿主 skills;默认目标是 `/root/.agents/skills`,如果该挂载暂缺但 approved source `/home/ubuntu/.agents/skills` 可读,runner 会把 `UNIDESK_SKILLS_PATH` 解析到可用 source 并在 `/api/dev-ready` 的 `devReady.skills.resolution` 中报告需要 host rollout 的合同退化,不要读取或输出宿主 token/auth 配置。",
|
||||
].join("\n");
|
||||
|
||||
export function stripAutoReferenceHint(prompt: string): string {
|
||||
|
||||
@@ -656,7 +656,7 @@ export function collectRuntimePreflight(options: RuntimePreflightOptions = {}):
|
||||
};
|
||||
const pullRequestDelivery = collectPullRequestDeliveryPreflight(options, checkedAt);
|
||||
return {
|
||||
ok: skills.ok && skillsSync.ok && ports.codex.ok && ports.opencode.ok && pullRequestDelivery.ok,
|
||||
ok: skills.runnerUsable && ports.codex.ok && ports.opencode.ok && pullRequestDelivery.ok,
|
||||
checkedAt,
|
||||
cwd: process.cwd(),
|
||||
pid: process.pid,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { accessSync, constants, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import type { JsonValue } from "./types";
|
||||
@@ -11,18 +11,53 @@ export interface SkillAvailabilityOptions {
|
||||
|
||||
export interface SkillAvailabilityReport {
|
||||
ok: boolean;
|
||||
runnerUsable: boolean;
|
||||
contractOk: boolean;
|
||||
available: boolean;
|
||||
degraded: boolean;
|
||||
blocker: string | null;
|
||||
degradedReason: string | null;
|
||||
checkedAt: string;
|
||||
source: string;
|
||||
target: string;
|
||||
path: string;
|
||||
resolvedPath: string;
|
||||
resolvedPathSource: "target" | "target-symlink" | "source-fallback" | "missing";
|
||||
resolution: {
|
||||
env: "UNIDESK_SKILLS_PATH";
|
||||
configuredTarget: string;
|
||||
configuredSource: string;
|
||||
resolvedPath: string;
|
||||
resolvedPathSource: "target" | "target-symlink" | "source-fallback" | "missing";
|
||||
runnerEnvValue: string;
|
||||
passesToRunnerEnv: true;
|
||||
targetBlocker: string | null;
|
||||
degradedReason: string | null;
|
||||
hostRolloutRequired: boolean;
|
||||
};
|
||||
mountPoint: string | null;
|
||||
exists: boolean;
|
||||
directory: boolean;
|
||||
readable: boolean;
|
||||
readonly: boolean;
|
||||
skillCount: number;
|
||||
targetExists: boolean;
|
||||
targetDirectory: boolean;
|
||||
targetReadable: boolean;
|
||||
targetReadonly: boolean;
|
||||
targetSkillCount: number;
|
||||
targetMissingSkills: string[];
|
||||
targetSymlink: boolean;
|
||||
targetRealPath: string | null;
|
||||
sourceExists: boolean;
|
||||
sourceDirectory: boolean;
|
||||
sourceReadable: boolean;
|
||||
sourceReadonly: boolean;
|
||||
sourceSkillCount: number;
|
||||
sourceMissingSkills: string[];
|
||||
sourceMountPoint: string | null;
|
||||
sourceSymlink: boolean;
|
||||
sourceRealPath: string | null;
|
||||
requiredSkills: string[];
|
||||
missingSkills: string[];
|
||||
skills: Array<{ name: string; present: boolean; skillMdPresent: boolean; path: string }>;
|
||||
@@ -49,6 +84,8 @@ export interface SkillSyncPathReport {
|
||||
writable: boolean;
|
||||
readonly: boolean;
|
||||
mountPoint: string | null;
|
||||
symlink: boolean;
|
||||
realPath: string | null;
|
||||
skillCount: number;
|
||||
requiredSkills: string[];
|
||||
missingSkills: string[];
|
||||
@@ -159,6 +196,10 @@ function normalizedPathText(path: string): string {
|
||||
return path.replace(/\\/gu, "/").replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function normalizedResolvedPath(path: string | null): string | null {
|
||||
return path === null ? null : normalizedPathText(path);
|
||||
}
|
||||
|
||||
function isForbiddenSkillsPath(path: string): boolean {
|
||||
const normalized = normalizedPathText(path);
|
||||
return normalized === forbiddenRelativeSkillsPath
|
||||
@@ -235,6 +276,8 @@ function collectSyncPathReport(path: string, approved: boolean, requiredSkills:
|
||||
const exists = existsSync(path);
|
||||
const permissionFailures: SkillSyncPreflightReport["permissionFailures"] = [];
|
||||
let directory = false;
|
||||
let symlink = false;
|
||||
let realPath: string | null = null;
|
||||
let readable = false;
|
||||
let writable = false;
|
||||
let skillCount = 0;
|
||||
@@ -243,6 +286,13 @@ function collectSyncPathReport(path: string, approved: boolean, requiredSkills:
|
||||
|
||||
if (exists) {
|
||||
try {
|
||||
const lstat = lstatSync(path);
|
||||
symlink = lstat.isSymbolicLink();
|
||||
try {
|
||||
realPath = realpathSync(path);
|
||||
} catch {
|
||||
realPath = null;
|
||||
}
|
||||
const stat = statSync(path);
|
||||
directory = stat.isDirectory();
|
||||
} catch (probeError) {
|
||||
@@ -279,6 +329,8 @@ function collectSyncPathReport(path: string, approved: boolean, requiredSkills:
|
||||
writable,
|
||||
readonly: exists && directory && (mountInfo.readonly === true || !writable),
|
||||
mountPoint: mountInfo.mountPoint,
|
||||
symlink,
|
||||
realPath,
|
||||
skillCount,
|
||||
requiredSkills,
|
||||
missingSkills,
|
||||
@@ -289,73 +341,129 @@ function collectSyncPathReport(path: string, approved: boolean, requiredSkills:
|
||||
};
|
||||
}
|
||||
|
||||
function targetBlockerFor(report: SkillSyncPathReport): string | null {
|
||||
if (!report.exists) return "skills-target-missing";
|
||||
if (!report.directory) return "skills-target-not-directory";
|
||||
if (!report.readable) return "skills-target-permission-failed";
|
||||
if (!report.readonly) return "skills-target-not-readonly";
|
||||
if (report.missingSkills.length > 0) return "required-target-skills-missing";
|
||||
return null;
|
||||
}
|
||||
|
||||
function sourceBlockerFor(report: SkillSyncPathReport): string | null {
|
||||
if (!report.exists) return "skills-source-missing";
|
||||
if (!report.directory) return "skills-source-not-directory";
|
||||
if (!report.readable) return "skills-source-permission-failed";
|
||||
if (report.missingSkills.length > 0) return "required-source-skills-missing";
|
||||
return null;
|
||||
}
|
||||
|
||||
function reportHasRequiredSkills(report: SkillSyncPathReport): boolean {
|
||||
return report.exists && report.directory && report.readable && report.missingSkills.length === 0;
|
||||
}
|
||||
|
||||
function sameResolvedPath(left: string | null, right: string | null): boolean {
|
||||
const normalizedLeft = normalizedResolvedPath(left);
|
||||
const normalizedRight = normalizedResolvedPath(right);
|
||||
return normalizedLeft !== null && normalizedRight !== null && normalizedLeft === normalizedRight;
|
||||
}
|
||||
|
||||
export function collectSkillAvailability(options: SkillAvailabilityOptions): SkillAvailabilityReport {
|
||||
const target = options.target;
|
||||
const source = options.source ?? defaultSource;
|
||||
const requiredSkills = options.requiredSkills ?? defaultRequiredSkills;
|
||||
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;
|
||||
let readonly = mountInfo.readonly === true;
|
||||
let error: string | null = null;
|
||||
let skills: SkillAvailabilityReport["skills"] = requiredSkills.map((name) => ({
|
||||
name,
|
||||
present: false,
|
||||
skillMdPresent: false,
|
||||
path: resolve(target, name),
|
||||
}));
|
||||
|
||||
if (exists) {
|
||||
try {
|
||||
const stat = statSync(target);
|
||||
directory = stat.isDirectory();
|
||||
if (directory) {
|
||||
const entries = readdirSync(target, { withFileTypes: true });
|
||||
skillCount = entries.filter((entry) => entry.isDirectory()).length;
|
||||
skills = skillStatus(target, requiredSkills);
|
||||
if (mountInfo.readonly === null) {
|
||||
readonly = commandOk("sh", ["-lc", `test ! -w ${shellQuote(target)}`]);
|
||||
}
|
||||
}
|
||||
} catch (probeError) {
|
||||
error = probeError instanceof Error ? probeError.message : String(probeError);
|
||||
}
|
||||
}
|
||||
|
||||
const missingSkills = skills.filter((skill) => !skill.present || !skill.skillMdPresent).map((skill) => skill.name);
|
||||
const available = exists && directory;
|
||||
const sourceProbe = collectSyncPathReport(source, source === defaultSource, requiredSkills).report;
|
||||
const targetProbe = collectSyncPathReport(target, target === expectedTarget || normalizedPathText(target) === normalizedPathText(source), requiredSkills).report;
|
||||
const targetSymlinkToSource = targetProbe.symlink && sameResolvedPath(targetProbe.realPath, sourceProbe.realPath);
|
||||
const targetHasRequiredSkills = reportHasRequiredSkills(targetProbe);
|
||||
const sourceHasRequiredSkills = reportHasRequiredSkills(sourceProbe);
|
||||
const targetReadonlyOk = targetProbe.readonly || targetSymlinkToSource || normalizedPathText(target) === normalizedPathText(source);
|
||||
const targetReady = targetHasRequiredSkills && targetReadonlyOk;
|
||||
const targetBlocker = targetHasRequiredSkills && targetSymlinkToSource ? null : targetBlockerFor(targetProbe);
|
||||
const sourceBlocker = sourceBlockerFor(sourceProbe);
|
||||
const missingBoth = !targetProbe.exists && !sourceProbe.exists;
|
||||
const resolutionSource: SkillAvailabilityReport["resolvedPathSource"] = forbiddenPathConfigured || missingBoth
|
||||
? "missing"
|
||||
: targetReady
|
||||
? targetSymlinkToSource ? "target-symlink" : "target"
|
||||
: sourceHasRequiredSkills
|
||||
? "source-fallback"
|
||||
: "missing";
|
||||
const resolvedPath = resolutionSource === "target" || resolutionSource === "target-symlink"
|
||||
? target
|
||||
: resolutionSource === "source-fallback"
|
||||
? source
|
||||
: target;
|
||||
const selectedReport = resolutionSource === "source-fallback" ? sourceProbe : targetProbe;
|
||||
const runnerUsable = !forbiddenPathConfigured && resolutionSource !== "missing";
|
||||
const contractOk = runnerUsable && resolutionSource !== "source-fallback" && targetReady && !forbiddenPathExists;
|
||||
const blocker = forbiddenPathConfigured
|
||||
? "forbidden-skills-path-configured"
|
||||
: !available
|
||||
? "skills-target-missing"
|
||||
: !readonly
|
||||
? "skills-target-not-readonly"
|
||||
: missingSkills.length > 0
|
||||
? "required-skills-missing"
|
||||
: forbiddenPathExists
|
||||
? "forbidden-skills-path-present"
|
||||
: null;
|
||||
const ok = blocker === null;
|
||||
: missingBoth
|
||||
? "skills-source-and-target-missing"
|
||||
: !runnerUsable
|
||||
? targetBlocker ?? sourceBlocker ?? "required-skills-missing"
|
||||
: contractOk
|
||||
? null
|
||||
: targetBlocker ?? (forbiddenPathExists ? "forbidden-skills-path-present" : null);
|
||||
const degradedReason = contractOk ? null : blocker;
|
||||
const ok = runnerUsable;
|
||||
const missingSkills = selectedReport.missingSkills;
|
||||
const skills = resolutionSource === "source-fallback" ? sourceProbe.skills : targetProbe.skills;
|
||||
return {
|
||||
ok,
|
||||
available,
|
||||
degraded: !ok,
|
||||
runnerUsable,
|
||||
contractOk,
|
||||
available: runnerUsable,
|
||||
degraded: !contractOk,
|
||||
blocker,
|
||||
degradedReason,
|
||||
checkedAt,
|
||||
source,
|
||||
target,
|
||||
path: target,
|
||||
mountPoint: mountInfo.mountPoint,
|
||||
exists,
|
||||
directory,
|
||||
readonly,
|
||||
skillCount,
|
||||
path: resolvedPath,
|
||||
resolvedPath,
|
||||
resolvedPathSource: resolutionSource,
|
||||
resolution: {
|
||||
env: "UNIDESK_SKILLS_PATH",
|
||||
configuredTarget: target,
|
||||
configuredSource: source,
|
||||
resolvedPath,
|
||||
resolvedPathSource: resolutionSource,
|
||||
runnerEnvValue: resolvedPath,
|
||||
passesToRunnerEnv: true,
|
||||
targetBlocker,
|
||||
degradedReason,
|
||||
hostRolloutRequired: runnerUsable && !contractOk,
|
||||
},
|
||||
mountPoint: selectedReport.mountPoint,
|
||||
exists: targetProbe.exists,
|
||||
directory: targetProbe.directory,
|
||||
readable: targetProbe.readable,
|
||||
readonly: targetProbe.readonly,
|
||||
skillCount: selectedReport.skillCount,
|
||||
targetExists: targetProbe.exists,
|
||||
targetDirectory: targetProbe.directory,
|
||||
targetReadable: targetProbe.readable,
|
||||
targetReadonly: targetProbe.readonly,
|
||||
targetSkillCount: targetProbe.skillCount,
|
||||
targetMissingSkills: targetProbe.missingSkills,
|
||||
targetSymlink: targetProbe.symlink,
|
||||
targetRealPath: targetProbe.realPath,
|
||||
sourceExists: sourceProbe.exists,
|
||||
sourceDirectory: sourceProbe.directory,
|
||||
sourceReadable: sourceProbe.readable,
|
||||
sourceReadonly: sourceProbe.readonly,
|
||||
sourceSkillCount: sourceProbe.skillCount,
|
||||
sourceMissingSkills: sourceProbe.missingSkills,
|
||||
sourceMountPoint: sourceProbe.mountPoint,
|
||||
sourceSymlink: sourceProbe.symlink,
|
||||
sourceRealPath: sourceProbe.realPath,
|
||||
requiredSkills,
|
||||
missingSkills,
|
||||
skills,
|
||||
@@ -371,10 +479,12 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
|
||||
forbiddenPathMustNotBeUsed: true,
|
||||
},
|
||||
expectedMount: `${defaultSource} mounted read-only to ${expectedTarget}`,
|
||||
repairHint: ok
|
||||
repairHint: contractOk
|
||||
? null
|
||||
: `Mount ${defaultSource} read-only at ${expectedTarget}, set UNIDESK_SKILLS_PATH=${expectedTarget}, and remove any forbidden skills path spelling.`,
|
||||
error,
|
||||
: runnerUsable
|
||||
? `Runner can use ${resolvedPath}; restore the read-only ${defaultSource} -> ${expectedTarget} projection so host rollout no longer reports ${degradedReason ?? "skills contract degraded"}.`
|
||||
: `Mount ${defaultSource} read-only at ${expectedTarget}, set UNIDESK_SKILLS_PATH=${expectedTarget}, and remove any forbidden skills path spelling.`,
|
||||
error: selectedReport.error,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ export interface RuntimeConfig {
|
||||
executionProviderIds: string[];
|
||||
remoteCodexEnvKeys: string[];
|
||||
skillsPath: string;
|
||||
resolvedRunnerSkillsPath: () => string;
|
||||
codexHome: string;
|
||||
opencodeXdgDir: string;
|
||||
sourceCodexConfig: string;
|
||||
|
||||
Reference in New Issue
Block a user