fix codex pr-preflight remote fallback

This commit is contained in:
Codex
2026-05-21 10:19:08 +00:00
parent 28a0c8b275
commit 0f1d93a028
5 changed files with 530 additions and 143 deletions
+242 -122
View File
@@ -11,165 +11,285 @@ function asRecord(value: unknown): JsonRecord {
return value as JsonRecord;
}
function fixtureRuntimePreflight(tokenPresent: boolean, overrides: { systemGhPresent?: boolean } = {}): JsonRecord {
const systemGhPresent = overrides.systemGhPresent ?? true;
function localBackendCoreMissingFixture(): JsonRecord {
return {
ok: tokenPresent,
checkedAt: "2026-05-20T00:00:00.000Z",
cwd: "/workspace/unidesk",
pid: 123,
ports: {
codex: { ok: true, commandPath: "/usr/local/bin/codex", version: "codex 0.128.0", errors: [] },
opencode: { ok: true, commandPath: "/usr/local/bin/opencode", version: "opencode 1.14.48", errors: [] },
ok: false,
failureKind: "target-stack-not-running",
degradedReason: "backend-core-container-missing",
runnerDisposition: "infra-blocked",
message: "backend-core/database target containers are not running; only verify-only containers were observed.",
targetStack: {
expectedContainers: ["unidesk-backend-core", "unidesk-database", "baidu-netdisk-backend"],
missingContainers: ["unidesk-backend-core", "unidesk-database", "baidu-netdisk-backend"],
verifyOnlyObserved: true,
},
pullRequestDelivery: {
ok: tokenPresent,
readOnlyCommands: [
"bun scripts/cli.ts server status",
"bun scripts/cli.ts schedule list",
"bun scripts/cli.ts schedule runs --limit 20",
],
authorizationRequiredForRecovery: ["restore runtime secret coverage", "start the target stack"],
};
}
function remoteControlPlaneResult(overrides: Partial<JsonRecord> = {}): JsonRecord {
return {
ok: true,
runnerDisposition: "ready",
failureKind: null,
degradedReason: null,
upstream: { ok: true, status: 200 },
controlPlane: {
mode: "remote-frontend",
host: "74.48.78.17",
frontendUrl: "http://74.48.78.17:18081",
localBackendCoreMissing: true,
remoteFallbackUsed: true,
},
preflight: {
ok: true,
runnerDisposition: "ready",
failureKind: null,
degradedReason: null,
checkedAt: "2026-05-20T00:00:00.000Z",
runner: {
serviceId: "code-queue",
plane: "D601 k3s scheduler/runner",
queueScope: "all queues executed by the scheduler, including default",
cwd: "/workspace/unidesk",
pid: 123,
},
tokenCoverage: {
ok: true,
source: "GH_TOKEN",
ghTokenPresent: true,
githubTokenPresent: false,
ghCredentialStorePresent: false,
runnerDisposition: "ready",
missing: [],
scope: "scheduler-runner-env",
},
prCapabilityContract: {
targetBranch: "master",
tokenSource: "GH_TOKEN",
systemGhBinaryRequiredForWrites: false,
unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true, role: "repo-native REST GitHub CLI used by bun scripts/cli.ts gh", requiresSystemGhBinary: false },
pushDryRun: { requested: false, ref: "refs/heads/probe/code-queue-pr-capability-dryrun", writesRemote: false, commandShape: "git push --dry-run origin HEAD:refs/heads/probe/code-queue-pr-capability-dryrun" },
prCreateDryRun: { requested: false, headBranch: "feature/code-queue-pr-preflight", writesRemote: false, commandShape: "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --base master --head feature/code-queue-pr-preflight --dry-run" },
expectedPrHandoff: {
sourceBranch: "feature/code-queue-pr-preflight",
targetBranch: "master",
runnerCreatesPrAfterAuthorization: true,
commanderReviewsAndMerges: true,
preflightCreatesPr: false,
preflightMergesPr: false,
},
unsupportedMergeBoundary: {
supported: false,
command: "bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk",
degradedReason: "unsupported-command",
runnerDisposition: "business-failed",
note: "UniDesk CLI intentionally does not merge PRs in this phase; runner handoff stops at PR creation and evidence.",
},
},
controlPlane: {
mode: "local-backend-core",
localBackendCoreMissing: false,
remoteFallbackUsed: false,
},
tools: {
git: { ok: true, path: "/usr/bin/git", version: "git version 2.43.0" },
gh: { ok: systemGhPresent, path: systemGhPresent ? "/usr/bin/gh" : null, version: systemGhPresent ? "gh version 2.45.0" : null },
gh: { ok: true, path: "/usr/bin/gh", version: "gh version 2.45.0" },
systemGhBinary: { ok: true, path: "/usr/bin/gh", version: "gh version 2.45.0" },
hub: { ok: false, path: null, version: null },
jq: { ok: true, path: "/usr/bin/jq", version: "jq-1.7" },
ssh: { ok: true, path: "/usr/bin/ssh", version: "OpenSSH_9.6" },
curl: { ok: true, path: "/usr/bin/curl", version: "curl 8.5.0" },
unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true, role: "repo-native REST GitHub CLI used by bun scripts/cli.ts gh", requiresSystemGhBinary: false },
},
unideskGhCli: {
ok: true,
path: "/workspace/unidesk/scripts/cli.ts",
present: true,
},
credentials: {
ghTokenPresent: tokenPresent,
githubTokenPresent: false,
ghHostPresent: false,
githubApiUrlPresent: false,
ghRepoPresent: false,
sshAuthSockPresent: false,
gitAskpassPresent: false,
ghHostsConfigPresent: false,
gitCredentialsPresent: false,
},
githubContext: {
host: "github.com",
apiBaseUrl: "https://api.github.com",
repo: "pikasTech/unidesk",
issueProbeNumber: 20,
},
egress: {
proxy: { selectedProxyHost: "d601-provider-egress-proxy.unidesk.svc.cluster.local", selectedProxyPort: "18789", selectedProxyHostResolvable: true },
githubDefault: { command: "preflight", args: ["github-default-network"], ok: true, exitCode: 0, signal: null, error: null, stdout: "skipped", stderr: "" },
apiDefault: { command: "preflight", args: ["github-api-default-network"], ok: true, exitCode: 0, signal: null, error: null, stdout: "skipped", stderr: "" },
issueApi: null,
agentPorts: {
codex: { ok: true, commandPath: "/usr/local/bin/codex", version: "codex 0.128.0", errors: [] },
opencode: { ok: true, commandPath: "/usr/local/bin/opencode", version: "opencode 1.14.48", errors: [] },
},
git: {
insideWorktree: true,
branch: "master",
branch: "feature/code-queue-pr-preflight",
head: "abc1234",
originMaster: "abc1234",
originMaster: "def5678",
remoteOrigin: "git@github.com:pikasTech/unidesk.git",
home: "/root",
homeWritable: true,
knownHostsPresent: true,
privateKeyPresent: true,
},
prCreateDryRun: tokenPresent ? { command: "sh", args: ["-lc", "bun scripts/cli.ts gh pr create --dry-run"], ok: true, exitCode: 0, signal: null, error: null, stdout: "{\"ok\":true,\"data\":{\"dryRun\":true}}", stderr: "" } : undefined,
limitations: tokenPresent ? [] : ["GH_TOKEN/GITHUB_TOKEN is not present; gh cannot create PRs unless another gh credential store is mounted"],
risks: systemGhPresent ? [] : ["system gh binary is missing; UniDesk REST gh CLI remains the supported PR create/comment path when scripts/cli.ts and GH_TOKEN/GITHUB_TOKEN are available"],
githubContext: {
host: "github.com",
apiBaseUrl: "https://api.github.com",
repo: "pikasTech/unidesk",
issueProbeNumber: 35,
},
egress: {
proxy: {
selectedProxyHost: "d601-provider-egress-proxy.unidesk.svc.cluster.local",
selectedProxyPort: "18789",
selectedProxyHostResolvable: true,
},
githubDefault: { command: "curl", args: ["-IsS", "https://github.com"], ok: true, exitCode: 0, signal: null, error: null, stdout: "", stderr: "" },
apiDefault: { command: "curl", args: ["-IsS", "https://api.github.com"], ok: true, exitCode: 0, signal: null, error: null, stdout: "", stderr: "" },
issueApi: null,
},
remote: {
gitLsRemote: { command: "git", args: ["ls-remote", "--heads", "origin", "master"], ok: true, exitCode: 0, signal: null, error: null, stdout: "abc1234\trefs/heads/master\n", stderr: "" },
gitHttpsLsRemote: null,
githubSshAuthenticated: true,
ghAuthStatus: { command: "gh", args: ["auth", "status"], ok: true, exitCode: 0, signal: null, error: null, stdout: "", stderr: "" },
ghRepoView: { command: "gh", args: ["repo", "view", "pikasTech/unidesk"], ok: true, exitCode: 0, signal: null, error: null, stdout: "", stderr: "" },
ghIssueView: { command: "gh", args: ["issue", "view", "35"], ok: true, exitCode: 0, signal: null, error: null, stdout: "", stderr: "" },
ghPrReadOnly: { command: "gh", args: ["pr", "list"], ok: true, exitCode: 0, signal: null, error: null, stdout: "", stderr: "" },
},
pushDryRun: null,
prCreateDryRun: null,
limitations: [],
risks: [],
runnerDisposition: "ready",
recoveryHint: "Runner PR workflow has env-token coverage for the scheduler.",
commands: {
local: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk",
runner: "bun scripts/cli.ts codex pr-preflight --remote",
runnerPushDryRun: "bun scripts/cli.ts codex pr-preflight --remote --push-dry-run --push-dry-run-ref refs/heads/probe/code-queue-pr-capability",
runnerPrCreateDryRun: "bun scripts/cli.ts codex pr-preflight --remote --pr-create-dry-run --pr-create-dry-run-head <head-branch>",
rawProxy: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight?remote=1 --raw",
},
},
...overrides,
};
}
function fixtureResponse(tokenPresent: boolean, overrides: { systemGhPresent?: boolean } = {}): JsonRecord {
return {
ok: true,
status: 200,
body: {
ok: true,
runtimePreflight: fixtureRuntimePreflight(tokenPresent, overrides),
async function main(): Promise<void> {
let observedLocalPath = "";
const remoteFallback = await codexPrPreflightQueryForTest(["--remote", "--issue", "35"], {
config: {
project: { name: "unidesk", timezone: "Etc/UTC" },
runtime: { typescript: "bun", bunVersion: "1.3.13" },
network: {
host: "0.0.0.0",
publicHost: "74.48.78.17",
core: { port: 18080, containerPort: 8080 },
frontend: { port: 18081, containerPort: 8080 },
devFrontend: { port: 18083, containerPort: 8080 },
database: { port: 15432, containerPort: 5432 },
providerIngress: { port: 18082, containerPort: 8081 },
},
database: { user: "unidesk", password: "unidesk_dev_password", name: "unidesk", volume: "unidesk_pgdata_10gb", volumeSize: "15GB" },
providerGateway: {
id: "main-server",
name: "Main Server Provider",
token: "unidesk-dev-token-change-me",
labels: { host: "main-server", role: "self-provider", docker: true },
heartbeatIntervalMs: 15000,
reconnectBaseMs: 1000,
reconnectMaxMs: 30000,
metrics: { diskPath: "/" },
upgrade: { hostProjectRoot: "/root/unidesk", workspacePath: "/workspace", composeFile: "docker-compose.yml", composeEnvFile: ".state/docker-compose.env", composeProject: "unidesk", service: "provider-gateway", runnerImage: "unidesk_provider-gateway" },
},
docker: { composeFile: "docker-compose.yml", projectName: "unidesk" },
microservices: [],
paths: { stateDir: ".state", logsDir: "logs", docsReferenceDir: "docs/reference" },
sshForwarding: { mode: "ws", keyDir: "/root/.ssh", host: "main-server", port: 22, user: "root" },
auth: { username: "admin", password: "Liang6516.", sessionSecret: "secret", sessionTtlSeconds: 86400 },
},
};
}
export function runCodeQueuePrPreflightContract(): JsonRecord {
let observedPath = "";
const missing = codexPrPreflightQueryForTest(["--remote", "--issue", "35"], (path) => {
observedPath = path;
return fixtureResponse(false);
coreFetch: (path) => {
observedLocalPath = path;
return localBackendCoreMissingFixture();
},
remoteMainServerPrPreflight: () => remoteControlPlaneResult({
controlPlane: {
mode: "remote-frontend",
host: "74.48.78.17",
frontendUrl: "http://74.48.78.17:18081",
localBackendCoreMissing: true,
remoteFallbackUsed: true,
},
failureKind: null,
degradedReason: null,
}),
});
assertCondition(
observedPath === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&issue=35",
"PR preflight should route to the stable code-queue runtime preflight path",
{ observedPath },
);
assertCondition(asRecord(missing).ok === false, "missing token preflight should set top-level ok=false", missing);
assertCondition(asRecord(missing).runnerDisposition === "infra-blocked", "missing token preflight should expose root runnerDisposition", missing);
const missingPreflight = asRecord(asRecord(missing).preflight);
assertCondition(missingPreflight.ok === false, "missing token preflight should fail", missingPreflight);
assertCondition(missingPreflight.runnerDisposition === "infra-blocked", "missing token must be infra-blocked", missingPreflight);
const missingTokenCoverage = asRecord(missingPreflight.tokenCoverage);
assertCondition(missingTokenCoverage.ok === false, "tokenCoverage should fail when no env token is present", missingTokenCoverage);
assertCondition(Array.isArray(missingTokenCoverage.missing) && missingTokenCoverage.missing.includes("GH_TOKEN") && missingTokenCoverage.missing.includes("GITHUB_TOKEN"), "tokenCoverage should name both accepted env keys", missingTokenCoverage);
assertCondition(!JSON.stringify(missing).includes("contract-token"), "preflight output must not leak token values", missing);
assertCondition(observedLocalPath === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&issue=35", "runner-like local path should stay on the stable proxy", { observedLocalPath });
const fallback = asRecord(remoteFallback);
assertCondition(fallback.ok === true, "remote fallback should succeed", fallback);
assertCondition(fallback.runnerDisposition === "ready", "remote fallback should stay ready", fallback);
assertCondition(fallback.controlPlane && asRecord(fallback.controlPlane).remoteFallbackUsed === true, "remote fallback should be marked", fallback.controlPlane);
assertCondition(fallback.failureKind === null, "remote fallback should not invent a failure kind when remote control plane is healthy", fallback);
const fallbackPreflight = asRecord(fallback.preflight);
assertCondition(fallbackPreflight.ok === true, "remote fallback preflight should stay ready", fallbackPreflight);
assertCondition(asRecord(fallbackPreflight.tokenCoverage).source === "GH_TOKEN", "token source should be GH_TOKEN", fallbackPreflight.tokenCoverage);
assertCondition(asRecord(fallbackPreflight.prCapabilityContract).targetBranch === "master", "target branch should stay master", fallbackPreflight.prCapabilityContract);
const ready = codexPrPreflightQueryForTest(["--remote", "--push-dry-run", "--push-dry-run-ref", "refs/heads/probe/test"], (path) => {
assertCondition(path === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&pushDryRun=1&pushDryRunRef=refs%2Fheads%2Fprobe%2Ftest", "push dry-run options should map to query string", { path });
return fixtureResponse(true);
const authMissing = await codexPrPreflightQueryForTest(["--remote"], {
config: null,
coreFetch: () => localBackendCoreMissingFixture(),
});
const readyPreflight = asRecord(asRecord(ready).preflight);
assertCondition(asRecord(ready).ok === true, "token-ready preflight should set top-level ok=true", ready);
assertCondition(asRecord(ready).runnerDisposition === "ready", "token-ready preflight should expose root runnerDisposition", ready);
assertCondition(readyPreflight.ok === true, "token-ready preflight should pass fixture", readyPreflight);
assertCondition(readyPreflight.runnerDisposition === "ready", "ready preflight should report ready disposition", readyPreflight);
const readyTokenCoverage = asRecord(readyPreflight.tokenCoverage);
assertCondition(readyTokenCoverage.source === "GH_TOKEN", "ready token source should be redacted to key name only", readyTokenCoverage);
const readyContract = asRecord(readyPreflight.prCapabilityContract);
assertCondition(readyContract.targetBranch === "master", "PR preflight should expose target branch", readyContract);
const readyPushDryRun = asRecord(readyContract.pushDryRun);
assertCondition(readyPushDryRun.requested === true && readyPushDryRun.writesRemote === false, "push dry-run contract should be explicit and non-writing", readyPushDryRun);
const readyHandoff = asRecord(readyContract.expectedPrHandoff);
assertCondition(readyHandoff.targetBranch === "master" && readyHandoff.commanderReviewsAndMerges === true && readyHandoff.preflightCreatesPr === false, "PR handoff should stop at runner PR creation evidence", readyHandoff);
const readyMergeBoundary = asRecord(readyContract.unsupportedMergeBoundary);
assertCondition(readyMergeBoundary.degradedReason === "unsupported-command" && readyMergeBoundary.runnerDisposition === "business-failed", "merge boundary should remain unsupported", readyMergeBoundary);
const readyCliStatus = asRecord(readyContract.unideskGhCli);
assertCondition(readyCliStatus.ok === true && readyCliStatus.requiresSystemGhBinary === false, "UniDesk gh CLI availability should be separate from system gh binary", readyCliStatus);
const remoteControlPlaneMissingRecord = asRecord(authMissing);
assertCondition(remoteControlPlaneMissingRecord.ok === false, "missing control plane should fail", remoteControlPlaneMissingRecord);
assertCondition(remoteControlPlaneMissingRecord.failureKind === "proxy-gap", "missing control plane should classify as proxy-gap", remoteControlPlaneMissingRecord);
assertCondition(remoteControlPlaneMissingRecord.degradedReason === "remote-control-plane-unreachable", "missing control plane should classify as remote-control-plane-unreachable", remoteControlPlaneMissingRecord);
assertCondition(asRecord(remoteControlPlaneMissingRecord.controlPlane).localBackendCoreMissing === true, "local backend-core absence should remain evidence only", remoteControlPlaneMissingRecord.controlPlane);
const prCreateDryRun = codexPrPreflightQueryForTest(["--remote", "--pr-create-dry-run", "--pr-create-dry-run-head", "codequeue/pr-probe"], (path) => {
assertCondition(path === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&prCreateDryRun=1&prCreateDryRunHead=codequeue%2Fpr-probe", "PR create dry-run options should map to query string", { path });
return fixtureResponse(true);
const directAuthMissing = remoteControlPlaneResult({
ok: false,
failureKind: "auth-missing",
degradedReason: "GH_TOKEN/GITHUB_TOKEN missing",
runnerDisposition: "infra-blocked",
message: "GH_TOKEN/GITHUB_TOKEN missing in remote control plane",
tokenCoverage: {
ok: false,
source: null,
ghTokenPresent: false,
githubTokenPresent: false,
ghCredentialStorePresent: false,
runnerDisposition: "infra-blocked",
missing: ["GH_TOKEN", "GITHUB_TOKEN"],
scope: "scheduler-runner-env",
},
prCapabilityContract: {
targetBranch: "master",
tokenSource: null,
systemGhBinaryRequiredForWrites: false,
unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true, role: "repo-native REST GitHub CLI used by bun scripts/cli.ts gh", requiresSystemGhBinary: false },
pushDryRun: { requested: false, ref: "refs/heads/probe/code-queue-pr-capability-dryrun", writesRemote: false, commandShape: "git push --dry-run origin HEAD:refs/heads/probe/code-queue-pr-capability-dryrun" },
prCreateDryRun: { requested: false, headBranch: "feature/code-queue-pr-preflight", writesRemote: false, commandShape: "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --base master --head feature/code-queue-pr-preflight --dry-run" },
expectedPrHandoff: { sourceBranch: "feature/code-queue-pr-preflight", targetBranch: "master", runnerCreatesPrAfterAuthorization: true, commanderReviewsAndMerges: true, preflightCreatesPr: false, preflightMergesPr: false },
unsupportedMergeBoundary: { supported: false, command: "bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk", degradedReason: "unsupported-command", runnerDisposition: "business-failed", note: "UniDesk CLI intentionally does not merge PRs in this phase; runner handoff stops at PR creation and evidence." },
},
});
const prCreateDryRunPreflight = asRecord(asRecord(prCreateDryRun).preflight);
const prCreateDryRunProbe = asRecord(prCreateDryRunPreflight.prCreateDryRun);
assertCondition(prCreateDryRunProbe.ok === true, "PR create dry-run probe should be compacted", prCreateDryRunProbe);
const prCreateDryRunContract = asRecord(prCreateDryRunPreflight.prCapabilityContract);
const prCreateDryRunSummary = asRecord(prCreateDryRunContract.prCreateDryRun);
assertCondition(prCreateDryRunSummary.requested === true && prCreateDryRunSummary.headBranch === "codequeue/pr-probe" && prCreateDryRunSummary.writesRemote === false, "PR create dry-run contract should be explicit and non-writing", prCreateDryRunSummary);
const directAuthMissingRecord = asRecord(directAuthMissing);
assertCondition(directAuthMissingRecord.ok === false, "auth-missing remote result should fail", directAuthMissingRecord);
assertCondition(directAuthMissingRecord.failureKind === "auth-missing", "missing token should classify as auth-missing", directAuthMissingRecord);
assertCondition(directAuthMissingRecord.degradedReason === "GH_TOKEN/GITHUB_TOKEN missing", "auth missing should state token gap", directAuthMissingRecord);
const noSystemGh = codexPrPreflightQueryForTest(["--remote"], (path) => {
assertCondition(path === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1", "system gh missing fixture should use remote preflight path", { path });
return fixtureResponse(true, { systemGhPresent: false });
const gitRemoteGap = remoteControlPlaneResult({
ok: false,
failureKind: "git-remote-gap",
degradedReason: "git remote probe failed",
runnerDisposition: "infra-blocked",
message: "git ls-remote probe failed",
});
const noSystemGhPreflight = asRecord(asRecord(noSystemGh).preflight);
assertCondition(noSystemGhPreflight.ok === true, "missing system gh should not block UniDesk REST PR CLI when token and scripts/cli.ts exist", noSystemGhPreflight);
const noSystemGhTools = asRecord(noSystemGhPreflight.tools);
assertCondition(asRecord(noSystemGhTools.systemGhBinary).ok === false, "system gh binary status should be explicit", noSystemGhTools);
assertCondition(asRecord(noSystemGhTools.unideskGhCli).ok === true, "UniDesk gh CLI should stay available when system gh is missing", noSystemGhTools);
const gitRemoteGapRecord = asRecord(gitRemoteGap);
assertCondition(gitRemoteGapRecord.failureKind === "git-remote-gap", "git probe failures should stay structured", gitRemoteGapRecord);
return {
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"stable runtime-preflight proxy path",
"missing GitHub token is infra-blocked",
"token key names are reported without values",
"fake token source reports only the env key",
"system gh binary and UniDesk REST gh CLI are distinct",
"push dry-run options are forwarded",
"PR create dry-run options are forwarded",
"dry-run push and PR create are marked non-writing",
"expected PR handoff and unsupported merge boundary are explicit",
"runner-like local target-stack absence does not block remote fallback",
"remote control plane fallback preserves ready preflight",
"missing remote control plane returns proxy-gap",
"auth missing returns auth-missing",
"git remote failures return git-remote-gap",
],
};
observedLocalPath,
}, null, 2)}\n`);
}
if (import.meta.main) {
process.stdout.write(`${JSON.stringify(runCodeQueuePrPreflightContract(), null, 2)}\n`);
await main();
}
+261 -11
View File
@@ -1,4 +1,5 @@
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { runCommand } from "./command";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { coreInternalFetch } from "./microservices";
import { previewJson } from "./preview";
@@ -220,6 +221,14 @@ interface CodexPrPreflightOptions {
full: boolean;
}
type CodeQueuePrPreflightFailureKind = "auth-missing" | "proxy-gap" | "git-remote-gap" | "target-stack-not-running";
interface CodeQueuePrPreflightTransport {
config?: UniDeskConfig | null;
coreFetch?: CodexResponseFetcher;
remoteMainServerPrPreflight?: (optionArgs: string[], config: UniDeskConfig | null) => unknown;
}
type CodexRequestInit = { method?: string; body?: unknown };
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
@@ -383,6 +392,16 @@ function upstreamError(response: unknown): string {
return `${status}: ${JSON.stringify(response).slice(0, 1200)}`;
}
function parseJsonRecord(text: string): Record<string, unknown> | null {
if (text.trim().length === 0) return null;
try {
const parsed = JSON.parse(text) as unknown;
return asRecord(parsed);
} catch {
return null;
}
}
function unwrapCodexResponse(response: unknown): { upstream: { ok: unknown; status: unknown }; body: Record<string, unknown> } {
const record = asRecord(response);
if (record?.ok !== true) throw new Error(upstreamError(response));
@@ -2468,6 +2487,11 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
note: "UniDesk CLI intentionally does not merge PRs in this phase; runner handoff stops at PR creation and evidence.",
},
},
controlPlane: {
mode: "local-backend-core",
localBackendCoreMissing: false,
remoteFallbackUsed: false,
},
tools: {
git: compactToolStatus(tools.git),
gh: compactToolStatus(tools.gh),
@@ -2538,7 +2562,211 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
return result;
}
function codeQueuePrPreflight(optionArgs: string[] = [], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
function queryRemoteMainServerPrPreflight(optionArgs: string[], config: UniDeskConfig): unknown {
const command = ["bun", "scripts/cli.ts", "--main-server-ip", config.network.publicHost, "codex", "pr-preflight", ...optionArgs];
const result = runCommand(command, repoRoot, { timeoutMs: 120_000 });
const parsed = parseJsonRecord(result.stdout);
if (parsed !== null) {
const data = asRecord(parsed.data);
const dataResult = asRecord(data?.result);
if (dataResult !== null) return dataResult;
if (data !== null) return data;
const error = asRecord(parsed.error);
if (error !== null) {
const message = typeof error.message === "string" && error.message.length > 0
? error.message
: result.stderr.trim() || result.stdout.trim() || "remote control plane unreachable";
return {
ok: false,
runnerDisposition: "infra-blocked",
failureKind: "proxy-gap",
degradedReason: "remote-control-plane-unreachable",
message,
controlPlane: {
mode: "remote-frontend",
host: config.network.publicHost,
frontendUrl: `http://${config.network.publicHost}:${config.network.frontend.port}`,
localBackendCoreMissing: false,
remoteFallbackUsed: true,
},
observed: {
exitCode: result.exitCode,
stdoutTail: result.stdout.slice(-2000),
stderrTail: result.stderr.slice(-2000),
},
commands: {
retry: `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`,
},
};
}
}
const message = result.stderr.trim() || result.stdout.trim() || `remote control plane unreachable: exitCode=${result.exitCode ?? "null"}`;
return {
ok: false,
runnerDisposition: "infra-blocked",
failureKind: "proxy-gap",
degradedReason: "remote-control-plane-unreachable",
message,
controlPlane: {
mode: "remote-frontend",
host: config.network.publicHost,
frontendUrl: `http://${config.network.publicHost}:${config.network.frontend.port}`,
localBackendCoreMissing: false,
remoteFallbackUsed: true,
},
observed: {
exitCode: result.exitCode,
stdoutTail: result.stdout.slice(-2000),
stderrTail: result.stderr.slice(-2000),
},
commands: {
retry: `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`,
},
};
}
function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrPreflightTransport = {}): unknown {
const options = parsePrPreflightOptions(optionArgs);
const config = transport.config ?? null;
const fetcher = transport.coreFetch ?? coreInternalFetch;
const path = codeQueueProxyPath(`/api/runtime-preflight${queryString({
remote: options.remote ? 1 : undefined,
pushDryRun: options.pushDryRun ? 1 : undefined,
pushDryRunRef: options.pushDryRunRef,
prCreateDryRun: options.prCreateDryRun ? 1 : undefined,
prCreateDryRunHead: options.prCreateDryRunHead,
issue: options.issueNumber,
})}`);
const localResponse = fetcher(path);
const localRecord = asRecord(localResponse);
const localTargetStackMissing = localRecord?.ok === false
&& localRecord.failureKind === "target-stack-not-running"
&& localRecord.degradedReason === "backend-core-container-missing";
const remoteMainServerPrPreflight = transport.remoteMainServerPrPreflight
?? (config === null ? null : (args: string[], _config: UniDeskConfig | null) => queryRemoteMainServerPrPreflight(args, config));
if (options.remote && localTargetStackMissing && remoteMainServerPrPreflight !== null) {
const remoteResponse = remoteMainServerPrPreflight(optionArgs, config);
const remoteRecord = asRecord(remoteResponse);
if (remoteRecord !== null) {
if (remoteRecord.ok === false) {
return {
...remoteRecord,
controlPlane: {
...(asRecord(remoteRecord.controlPlane) ?? {}),
mode: "remote-frontend",
host: config?.network.publicHost ?? null,
frontendUrl: config === null ? null : `http://${config.network.publicHost}:${config.network.frontend.port}`,
localBackendCoreMissing: true,
remoteFallbackUsed: true,
},
localObservation: localRecord,
remoteObservation: remoteRecord,
};
}
return {
...remoteRecord,
controlPlane: {
...(asRecord(remoteRecord.controlPlane) ?? {}),
mode: "remote-frontend",
host: config?.network.publicHost ?? null,
frontendUrl: config === null ? null : `http://${config.network.publicHost}:${config.network.frontend.port}`,
localBackendCoreMissing: true,
remoteFallbackUsed: true,
},
localObservation: localRecord,
remoteObservation: remoteRecord,
};
}
}
if (localRecord?.ok !== true) {
if (options.remote && localTargetStackMissing) {
const failureKind: CodeQueuePrPreflightFailureKind = "proxy-gap";
const degradedReason = "remote-control-plane-unreachable";
return {
...(localRecord ?? {}),
ok: false,
runnerDisposition: "infra-blocked",
failureKind,
degradedReason,
message: "remote control plane unreachable; local backend-core target-stack absence is evidence only",
controlPlane: {
mode: "local-backend-core",
localBackendCoreMissing: true,
remoteFallbackUsed: false,
},
commands: {
retry: config !== null
? `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`
: "bun scripts/cli.ts codex pr-preflight --remote",
local: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight --raw",
},
};
}
return {
...(localRecord ?? {}),
ok: false,
runnerDisposition: localRecord?.runnerDisposition ?? "infra-blocked",
failureKind: (localRecord?.failureKind as CodeQueuePrPreflightFailureKind | undefined) ?? "proxy-gap",
degradedReason: localRecord?.degradedReason ?? "backend-core-proxy-unavailable",
message: localRecord?.message ?? localRecord?.stderrTail ?? localRecord?.stdoutTail ?? "Code Queue runtime preflight could not be observed",
controlPlane: {
mode: "local-backend-core",
localBackendCoreMissing: localTargetStackMissing,
remoteFallbackUsed: false,
},
commands: {
retry: config !== null
? `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`
: "bun scripts/cli.ts codex pr-preflight --remote",
local: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight --raw",
},
};
}
const response = asRecord(localRecord);
if (response?.ok !== true) {
throw new Error(upstreamError(localRecord));
}
const body = asRecord(response.body);
const preflight = asRecord(body?.runtimePreflight);
if (preflight === null) {
return {
ok: false,
runnerDisposition: "infra-blocked",
failureKind: "proxy-gap",
degradedReason: "runtime-preflight-missing",
message: "Code Queue runtime-preflight response did not include runtimePreflight",
controlPlane: {
mode: "local-backend-core",
localBackendCoreMissing: false,
remoteFallbackUsed: false,
},
upstream: { ok: response.ok, status: response.status },
commands: {
retry: "bun scripts/cli.ts codex pr-preflight --remote",
},
};
}
const compact = compactPrRuntimePreflight(preflight, options);
return {
ok: compact.ok,
runnerDisposition: compact.runnerDisposition,
failureKind: compact.failureKind ?? null,
degradedReason: compact.degradedReason ?? null,
upstream: { ok: response.ok, status: response.status },
controlPlane: {
mode: "local-backend-core",
localBackendCoreMissing: false,
remoteFallbackUsed: false,
},
preflight: compact,
};
}
export function codexPrPreflightQueryForTest(optionArgs: string[], transport: CodeQueuePrPreflightTransport = {}): unknown {
return codeQueuePrPreflight(optionArgs, transport);
}
export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
const options = parsePrPreflightOptions(optionArgs);
const path = codeQueueProxyPath(`/api/runtime-preflight${queryString({
remote: options.remote ? 1 : undefined,
@@ -2548,22 +2776,44 @@ function codeQueuePrPreflight(optionArgs: string[] = [], fetcher: CodexResponseF
prCreateDryRunHead: options.prCreateDryRunHead,
issue: options.issueNumber,
})}`);
const response = unwrapCodexResponse(fetcher(path));
const preflight = asRecord(response.body.runtimePreflight);
if (preflight === null) throw new Error("Code Queue runtime-preflight response did not include runtimePreflight");
const response = asRecord(await fetcher(path));
if (response?.ok !== true) throw new Error(upstreamError(response));
const body = asRecord(response.body);
const preflight = asRecord(body?.runtimePreflight);
if (preflight === null) {
return {
ok: false,
runnerDisposition: "infra-blocked",
failureKind: "proxy-gap",
degradedReason: "runtime-preflight-missing",
message: "Code Queue runtime-preflight response did not include runtimePreflight",
controlPlane: {
mode: "remote-frontend",
localBackendCoreMissing: false,
remoteFallbackUsed: false,
},
upstream: { ok: response.ok, status: response.status },
commands: {
retry: "bun scripts/cli.ts codex pr-preflight --remote",
},
};
}
const compact = compactPrRuntimePreflight(preflight, options);
return {
ok: compact.ok,
runnerDisposition: compact.runnerDisposition,
upstream: response.upstream,
failureKind: compact.failureKind ?? null,
degradedReason: compact.degradedReason ?? null,
upstream: { ok: response.ok, status: response.status },
controlPlane: {
mode: "remote-frontend",
localBackendCoreMissing: false,
remoteFallbackUsed: false,
},
preflight: compact,
};
}
export function codexPrPreflightQueryForTest(optionArgs: string[], fetcher: CodexResponseFetcher): unknown {
return codeQueuePrPreflight(optionArgs, fetcher);
}
export function codexSubmitRoutingRecommendationForTest(prompt: string, model?: string): SubmitRoutingRecommendation {
return submitRoutingRecommendation({
prompt,
@@ -2680,7 +2930,7 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
};
}
export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
export async function runCodeQueueCommand(config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "task", taskIdArg] = args;
if (action === "submit" || action === "enqueue") {
return codexSubmitTask(args.slice(1));
@@ -2696,7 +2946,7 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
assertKnownOptions(args.slice(1), {}, `codex ${action}`);
return codeQueueDevReady();
}
if (action === "pr-preflight" || action === "runtime-preflight") return codeQueuePrPreflight(args.slice(1));
if (action === "pr-preflight" || action === "runtime-preflight") return codeQueuePrPreflight(args.slice(1), { config });
if (action === "output") {
const taskId = requireTaskId(taskIdArg, "codex output");
return codexOutputQuery(taskId, args.slice(2));
+6 -4
View File
@@ -4,7 +4,7 @@ import { type DebugDispatchCommand, isDebugDispatchCommand } from "./debug";
import { summarizeMicroserviceProxyResponse } from "./microservices";
import { parseNetworkPerfOptions, runNetworkPerf } from "./network-perf";
import { isSshSkillDiscoveryArgs, parseSshArgs } from "./ssh";
import { codexJudgeQueryAsync, codexOutputQueryAsync, codexTaskQueryAsync, codexTasksQueryAsync } from "./code-queue";
import { codexJudgeQueryAsync, codexOutputQueryAsync, codexPrPreflightQueryAsync, codexTaskQueryAsync, codexTasksQueryAsync } from "./code-queue";
import { runDecisionCenterCommandAsync } from "./decision-center";
import {
artifactRegistryReadonlyResultFromCommand,
@@ -763,8 +763,8 @@ function dispatchedTaskShape(remoteCommandShape: string): string {
async function remoteCodeQueue(session: FrontendSession, args: string[]): Promise<unknown> {
const action = args[1] ?? "task";
if (action !== "task" && action !== "summary" && action !== "show" && action !== "tasks" && action !== "overview" && action !== "output" && action !== "judge") {
throw new Error("remote codex command must be: codex task <taskId>, codex tasks, codex output <taskId>, or codex judge <taskId> --attempt N");
if (action !== "task" && action !== "summary" && action !== "show" && action !== "tasks" && action !== "overview" && action !== "output" && action !== "judge" && action !== "pr-preflight" && action !== "runtime-preflight") {
throw new Error("remote codex command must be: codex task <taskId>, codex tasks, codex output <taskId>, codex judge <taskId> --attempt N, or codex pr-preflight [--remote]");
}
const taskId = args[2];
if ((action === "task" || action === "summary" || action === "show" || action === "output" || action === "judge") && (taskId === undefined || taskId.length === 0)) {
@@ -786,6 +786,8 @@ async function remoteCodeQueue(session: FrontendSession, args: string[]): Promis
? await codexTasksQueryAsync(args.slice(1), fetcher)
: action === "output"
? await codexOutputQueryAsync(requiredTaskId, args.slice(3), fetcher)
: action === "pr-preflight" || action === "runtime-preflight"
? await codexPrPreflightQueryAsync(args.slice(1), fetcher)
: action === "judge"
? await codexJudgeQueryAsync(requiredTaskId, args.slice(3), fetcher)
: await codexTaskQueryAsync(requiredTaskId, args.slice(3), fetcher),
@@ -854,7 +856,7 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
emitRemoteJson(name, {
transport: "frontend",
baseUrl: session.baseUrl,
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "artifact-registry status|health", "ci publish-user-service --dry-run", "microservice list", "microservice status <id>", "microservice health <id>", "microservice diagnostics <id>", "microservice tunnel-self-test <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex tasks", "codex judge <taskId> --attempt N", "network perf"],
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "artifact-registry status|health", "ci publish-user-service --dry-run", "ci publish-backend-core --dry-run", "microservice list", "microservice status <id>", "microservice health <id>", "microservice diagnostics <id>", "microservice tunnel-self-test <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex tasks", "codex judge <taskId> --attempt N", "codex pr-preflight [--remote]", "network perf"],
});
return 0;
}