feat: add code queue PR preflight

This commit is contained in:
Codex
2026-05-20 08:19:36 +00:00
parent d76ccc111f
commit a6e21ec201
5 changed files with 551 additions and 4 deletions
+17
View File
@@ -41,6 +41,23 @@ D601 原生 k3s 的人工诊断必须显式使用 host kubeconfig`KUBECONFIG=
`bun scripts/cli.ts microservice diagnostics code-queue` 必须作为 Code Queue 的长期诊断入口。它必须显式报告 `d601-provider-egress-proxy``d601-tcp-egress-gateway` 的 Deployment available、Endpoint non-empty、scheduler 到 PostgreSQL route、以及 stale active/retry_wait reconcile;任何一项失败都应让诊断结果 degraded/failing,而不是只显示 scheduler HTTP healthy。
## Pull Request Delivery
Code Queue worker 可以在任务明确要求审查型交付时创建 Pull Request。PR 交付不是默认出口;默认集成仍遵循项目当前 master-only 规则,直到具体任务或指挥官要求改为 PR。PR 型任务必须从最新目标线创建短生命周期分支,报告源分支、目标分支、PR URL、关联 issue、验证证据和未完成风险;分支命名应使用 `probe/``code-queue/` 或其他明确任务前缀,禁止把隐藏分支当成长期交付状态。
Code Queue runtime 提供 `/api/runtime-preflight` 作为 PR 能力探测入口。默认请求只检查本地工具、凭证可见性、Git worktree、HOME、known_hosts、agent port 和 proxy DNS`/api/runtime-preflight?remote=1` 会增加 GitHub 网络、issue API、SSH/HTTPS `git ls-remote`、GitHub SSH、`gh auth status``gh repo view``gh issue view` 和只读 `gh pr list` 探测;`/api/runtime-preflight?remote=1&pushDryRun=1&pushDryRunRef=refs/heads/probe/<name>` 会额外执行 `git push --dry-run`,验证远端写权限但不创建分支。`issue=<number>` 可覆盖默认 issue #20 探针。探测输出只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在,不得输出 token 内容。
PR 创建依赖以下最小运行时能力:
- runtime image 必须包含 `git``gh``jq``ca-certificates``curl``openssh-client`D601 provider dev container 准备脚本也必须补齐这些工具。
- GitHub 凭证只能通过运行时 secret、环境变量或已有 SSH identity 注入。优先使用 `GH_TOKEN`,兼容 `GITHUB_TOKEN`;不得把 token 写入 Git remote、任务 prompt、日志、镜像或仓库文件。
- `CODE_QUEUE_REMOTE_CODEX_ENV_KEYS` 默认允许把 `GH_TOKEN`/`GITHUB_TOKEN` 以及 `GH_HOST``GITHUB_API_URL``GH_REPO` 从 scheduler 传给 provider dev container,供隔离执行环境内的 `gh repo view``gh issue view``gh pr list``gh pr create` 使用。
- DEV 的持久注入路径复用 `unidesk-dev-runtime-secrets`:把 `GH_TOKEN``GITHUB_TOKEN` 写入该 Secret 后,通过受控 `deploy apply --env dev --service code-queue` 或等价 dev-only rollout 重启 Code Queue scheduler/read/write。不要在 PROD Code Queue 上直接 patch Secret 或 rollout。
安全验证顺序固定为先只读、再 dry-run、最后才创建真实 PR。优先执行 `/api/runtime-preflight``/api/runtime-preflight?remote=1` 和带 `pushDryRun=1` 的 probe ref;只有工具、token、网络和 push dry-run 都满足且任务明确允许时,才创建 draft PR 或普通 PR。若创建真实 probe PR,最终报告必须记录 URL 并说明保留或关闭状态。
preflight failure 必须定位到具体缺失项。已知必须区分的状态包括:`gh`/`hub` 缺失、`GH_TOKEN`/`GITHUB_TOKEN`/`GH_HOST`/`GITHUB_API_URL`/`GH_REPO` 未注入、`/root/.config/gh/hosts.yml``/root/.git-credentials` 缺失、SSH Git 可读但 GitHub issue/API 匿名访问返回 404、匿名 HTTPS Git 不可用、以及 `HTTP_PROXY`/`HTTPS_PROXY` 指向的 egress proxy 在 runner 内不可解析。发现这类缺口时,worker 应报告 preflight error 和阻塞项,不得强行创建 PR。
## Boundaries
Code Queue 由 D601 k3s/k8s 控制面代管,不再通过 `server rebuild``codex deploy`、维护通道直连 D601 或手工 `docker compose up` 作为正式部署路径。Code Queue 部署必须在自身正在执行任务时仍可运行;服务重启后由 restart-recovery 恢复任务状态,不能等待当前 Code Queue task 退出后再部署。
@@ -4,7 +4,7 @@ FROM ${CODE_QUEUE_BASE_IMAGE}
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
ENV UNIDESK_SKILLS_PATH=/root/.agents/skills
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1 && test -x "$PLAYWRIGHT_BROWSERS_PATH/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell") \
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v gh >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1 && test -x "$PLAYWRIGHT_BROWSERS_PATH/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell") \
|| (apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
@@ -16,6 +16,7 @@ RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 &&
file \
g++ \
gcc \
gh \
git \
gzip \
iproute2 \
@@ -132,6 +132,7 @@ import {
readOaTraceStatsForTasks,
readOaTraceStepsForTask,
} from "./oa-events";
import { collectRuntimePreflight, runtimePreflightJson } from "./runtime-preflight";
import { configureSelfTests, runJudgeInfraSelfTest, runQueueClaimMoveSelfTest, runQueueOrderingSelfTest, runReferenceInjectionSelfTest, runTracePortSelfTest, runTraceSummaryContractSelfTest } from "./self-tests";
import {
codexToolLifecycleStartedBeforeIn,
@@ -384,7 +385,7 @@ function readConfig(): RuntimeConfig {
mainProviderId,
remoteDefaultWorkdir,
executionProviderIds,
remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL"]),
remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "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"),
codexHome: envString("CODE_QUEUE_CODEX_HOME", "/var/lib/unidesk/code-queue/codex-home"),
opencodeXdgDir: envString("CODE_QUEUE_OPENCODE_XDG_DIR", resolve(dataDir, "opencode-xdg")),
@@ -2501,6 +2502,7 @@ function collectDevReady(): JsonValue {
"pip3",
"docker",
"docker-compose",
"gh",
"jq",
"ssh",
"rsync",
@@ -2529,6 +2531,7 @@ function collectDevReady(): JsonValue {
const sshSharedReady = existsSync("/root/.ssh") && sshKeyProbe.ok && sshKeyProbe.output.trim().length > 0;
const skills = collectSkillsStatus() as Record<string, JsonValue>;
const skillsReady = skills.available === true && skills.readonly === true && skills.cliSpecAvailable === true;
const runtimePreflight = runtimePreflightJson(collectRuntimePreflight({ includeRemote: false, includePushDryRun: false }));
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady;
const value: JsonValue = {
ok,
@@ -2562,6 +2565,7 @@ function collectDevReady(): JsonValue {
ready: sshSharedReady,
},
skills,
runtimePreflight,
};
devReadyCache = { checkedAtMs: now, value };
return value;
@@ -5273,6 +5277,17 @@ 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/runtime-preflight" && req.method === "GET") {
return jsonResponse({
ok: true,
runtimePreflight: runtimePreflightJson(collectRuntimePreflight({
includeRemote: url.searchParams.get("remote") === "1",
includePushDryRun: url.searchParams.get("pushDryRun") === "1",
pushDryRunRef: url.searchParams.get("pushDryRunRef") ?? undefined,
issueNumber: Number(url.searchParams.get("issue") ?? url.searchParams.get("issueNumber") ?? NaN),
})),
});
}
if (url.pathname === "/api/dev-containers" && req.method === "GET") {
const plan = buildDevContainerPlan(normalizeProviderId(config.devContainerDefaultProviderId) ?? "D601", {});
return jsonResponse({
@@ -976,11 +976,19 @@ OPENCODE_XDG_DIR=${shellQuote(plan.remoteOpencodeXdgDir)}
export PATH=/tmp/unidesk-tools:$PATH
if command -v apt-get >/dev/null 2>&1; then
missing=""
for c in git rg curl python3 pip3 jq rsync patch make gcc g++ tar gzip unzip; do command -v "$c" >/dev/null 2>&1 || missing="$missing $c"; done
for c in git gh rg curl python3 pip3 jq rsync patch make gcc g++ tar gzip unzip; do command -v "$c" >/dev/null 2>&1 || missing="$missing $c"; done
if [ -n "$missing" ]; then
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends git ripgrep curl python3 python3-pip jq rsync patch make gcc g++ tar gzip unzip ca-certificates
packages="git ripgrep curl python3 python3-pip jq rsync patch make gcc g++ tar gzip unzip ca-certificates"
if echo "$missing" | grep -qw gh; then
if apt-cache show gh >/dev/null 2>&1; then
packages="$packages gh"
else
echo "warning: apt package gh is unavailable in this container base image" >&2
fi
fi
apt-get install -y --no-install-recommends $packages
rm -rf /var/lib/apt/lists/*
fi
fi
@@ -0,0 +1,506 @@
// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007fblob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。
import { spawnSync } from "node:child_process";
import type { JsonValue } from "./types";
export type RuntimePreflightAgentPort = "codex" | "opencode";
export interface RuntimePreflightOptions {
includeRemote?: boolean;
includePushDryRun?: boolean;
pushDryRunRef?: string;
issueNumber?: number;
}
export interface RuntimeCommandProbe {
command: string;
args: string[];
ok: boolean;
exitCode: number | null;
signal: string | null;
error: string | null;
stdout: string;
stderr: string;
}
export interface RuntimePreflightPortStatus {
agentPort: RuntimePreflightAgentPort;
ok: boolean;
path: string;
commandPath: string | null;
version: string | null;
appServerDryRun?: RuntimeCommandProbe;
probes: {
commandV: RuntimeCommandProbe;
version: RuntimeCommandProbe;
};
errors: string[];
checkedAt: string;
}
export interface RuntimePreflightReport {
ok: boolean;
checkedAt: string;
cwd: string;
pid: number;
node: {
platform: NodeJS.Platform;
arch: string;
version: string;
};
path: string;
ports: Record<RuntimePreflightAgentPort, RuntimePreflightPortStatus>;
pullRequestDelivery: PullRequestDeliveryPreflight;
}
export interface PullRequestDeliveryPreflight {
ok: boolean;
checkedAt: string;
tools: Record<string, RuntimeToolStatus>;
credentials: {
ghTokenPresent: boolean;
githubTokenPresent: boolean;
ghHostPresent: boolean;
githubApiUrlPresent: boolean;
ghRepoPresent: boolean;
sshAuthSockPresent: boolean;
gitAskpassPresent: boolean;
ghHostsConfigPresent: boolean;
gitCredentialsPresent: boolean;
};
githubContext: {
host: string;
apiBaseUrl: string;
repo: string | null;
issueProbeNumber: number;
};
egress: {
proxy: {
httpProxy: string | null;
httpsProxy: string | null;
allProxy: string | null;
noProxy: string | null;
selectedProxyHost: string | null;
selectedProxyPort: string | null;
selectedProxyHostProbe: RuntimeCommandProbe | null;
selectedProxyHostResolvable: boolean | null;
};
githubDefault: RuntimeCommandProbe;
githubDirect: RuntimeCommandProbe;
apiDefault: RuntimeCommandProbe;
apiDirect: RuntimeCommandProbe;
issueApi: RuntimeCommandProbe | null;
};
git: {
insideWorktree: boolean;
branch: string | null;
head: string | null;
originMaster: string | null;
remoteOrigin: string | null;
userName: string | null;
userEmail: string | null;
home: string | null;
homeWritable: boolean;
knownHostsPresent: boolean;
privateKeyPresent: boolean;
};
remote?: {
gitLsRemote: RuntimeCommandProbe;
gitHttpsLsRemote: RuntimeCommandProbe | null;
githubSshAuth: RuntimeCommandProbe;
githubSshAuthenticated: boolean;
ghAuthStatus: RuntimeCommandProbe | null;
ghRepoView: RuntimeCommandProbe | null;
ghIssueView: RuntimeCommandProbe | null;
ghPrReadOnly: RuntimeCommandProbe | null;
};
pushDryRun?: RuntimeCommandProbe;
limitations: string[];
risks: string[];
}
export interface RuntimeToolStatus {
name: string;
ok: boolean;
path: string | null;
version: string | null;
probe: RuntimeCommandProbe;
}
function redactSensitive(value: string): string {
return value
.replace(/gh[pousr]_[A-Za-z0-9_]{20,}/gu, "***")
.replace(/github_pat_[A-Za-z0-9_]+/gu, "***")
.replace(/(https?:\/\/)([^/\s:@]+):([^/\s@]+)@/giu, "$1***:***@")
.replace(/(https?:\/\/)([^/\s@]+)@/giu, "$1***@")
.replace(/([?&](?:access_token|token)=)[^&\s]+/giu, "$1***");
}
function compactOutput(value: string, maxChars = 4000): string {
const text = redactSensitive(value).replace(/\u001b\[[0-9;]*m/gu, "").trim();
return text.length <= maxChars ? text : `${text.slice(0, maxChars)}...`;
}
function commandProbe(command: string, args: string[], options: { timeoutMs?: number } = {}): RuntimeCommandProbe {
const run = spawnSync(command, args, {
encoding: "utf8",
timeout: options.timeoutMs ?? 5000,
maxBuffer: 256 * 1024,
env: process.env,
shell: false,
});
const error = run.error instanceof Error ? run.error.message : null;
const timedOut = run.error instanceof Error && run.error.message.includes("ETIMEDOUT");
return {
command,
args,
ok: error === null && !timedOut && run.status === 0,
exitCode: run.status,
signal: run.signal,
error,
stdout: compactOutput(typeof run.stdout === "string" ? run.stdout : ""),
stderr: compactOutput(typeof run.stderr === "string" ? run.stderr : ""),
};
}
function shellProbe(script: string, timeoutMs = 5000): RuntimeCommandProbe {
return commandProbe("sh", ["-lc", script], { timeoutMs });
}
function skippedProbe(name: string): RuntimeCommandProbe {
return {
command: "preflight",
args: [name],
ok: true,
exitCode: 0,
signal: null,
error: null,
stdout: "skipped; pass remote=1 to run this probe",
stderr: "",
};
}
function firstLine(value: string): string | null {
const line = value.split(/\r?\n/u).map((item) => item.trim()).find((item) => item.length > 0);
return line ?? null;
}
function errorSummary(probe: RuntimeCommandProbe): string | null {
if (probe.ok) return null;
const detail = firstLine([probe.error ?? "", probe.stderr, probe.stdout].filter(Boolean).join("\n"));
return `${probe.command} ${probe.args.join(" ")} failed${detail === null ? "" : `: ${detail}`}`;
}
function commandPath(command: string): string | null {
const probe = shellProbe(`command -v ${shellQuote(command)}`, 2000);
return probe.ok ? firstLine(probe.stdout) : null;
}
function shellQuote(value: string): string {
return `'${value.replace(/'/gu, "'\\''")}'`;
}
function toolStatus(name: string, versionArgs: string[] = ["--version"]): RuntimeToolStatus {
const path = commandPath(name);
const probe = path === null ? shellProbe(`command -v ${shellQuote(name)}`, 2000) : commandProbe(name, versionArgs, { timeoutMs: 5000 });
return {
name,
ok: path !== null,
path,
version: path === null ? null : firstLine(probe.stdout) ?? firstLine(probe.stderr),
probe,
};
}
function gitValue(args: string[]): string | null {
const probe = commandProbe("git", args, { timeoutMs: 5000 });
return probe.ok ? firstLine(probe.stdout) : null;
}
function homeWritable(): boolean {
const home = process.env.HOME;
if (home === undefined || home.trim().length === 0) return false;
return shellProbe(`test -w ${shellQuote(home)}`, 2000).ok;
}
function boolEnv(name: string): boolean {
const value = process.env[name];
return value !== undefined && value.trim().length > 0;
}
function fileExistsProbe(path: string): boolean {
return shellProbe(`test -s ${shellQuote(path)}`, 2000).ok;
}
function parseGitHubRepo(remote: string | null): string | null {
const envRepo = process.env.GH_REPO?.trim();
if (envRepo !== undefined && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(envRepo)) return envRepo;
if (remote === null) return null;
const sshMatch = remote.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/u);
if (sshMatch !== null) return sshMatch[1] ?? null;
const httpsMatch = remote.match(/^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?(?:\/)?$/u);
if (httpsMatch !== null) return httpsMatch[1] ?? null;
return null;
}
function envUrl(name: string): URL | null {
const raw = process.env[name]?.trim();
if (raw === undefined || raw.length === 0) return null;
try {
return new URL(raw);
} catch {
return null;
}
}
function proxySummary(): PullRequestDeliveryPreflight["egress"]["proxy"] {
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy || null;
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy || null;
const allProxy = process.env.ALL_PROXY || process.env.all_proxy || null;
const noProxy = process.env.NO_PROXY || process.env.no_proxy || null;
const selected = envUrl("HTTPS_PROXY") ?? envUrl("https_proxy") ?? envUrl("HTTP_PROXY") ?? envUrl("http_proxy") ?? envUrl("ALL_PROXY") ?? envUrl("all_proxy");
const selectedProxyHost = selected?.hostname ?? null;
const selectedProxyPort = selected === null ? null : selected.port || (selected.protocol === "https:" ? "443" : "80");
const selectedProxyHostProbe = selectedProxyHost === null ? null : commandProbe("getent", ["ahosts", selectedProxyHost], { timeoutMs: 5000 });
return {
httpProxy: httpProxy === null ? null : compactOutput(httpProxy, 1000),
httpsProxy: httpsProxy === null ? null : compactOutput(httpsProxy, 1000),
allProxy: allProxy === null ? null : compactOutput(allProxy, 1000),
noProxy: noProxy === null ? null : compactOutput(noProxy, 1000),
selectedProxyHost,
selectedProxyPort,
selectedProxyHostProbe,
selectedProxyHostResolvable: selectedProxyHostProbe === null ? null : selectedProxyHostProbe.ok,
};
}
function authCurlProbe(url: string): RuntimeCommandProbe {
const script = [
"set -eu",
"status_file=/tmp/unidesk-github-api-probe.status",
"body_file=/tmp/unidesk-github-api-probe.json",
"rm -f \"$status_file\" \"$body_file\"",
"if [ -n \"${GH_TOKEN:-}\" ]; then",
` curl -sS -o "$body_file" -w 'http_status=%{http_code}\\n' -H "Authorization: Bearer \${GH_TOKEN}" ${shellQuote(url)} > "$status_file"`,
"elif [ -n \"${GITHUB_TOKEN:-}\" ]; then",
` curl -sS -o "$body_file" -w 'http_status=%{http_code}\\n' -H "Authorization: Bearer \${GITHUB_TOKEN}" ${shellQuote(url)} > "$status_file"`,
"else",
` curl -sS -o "$body_file" -w 'http_status=%{http_code}\\n' ${shellQuote(url)} > "$status_file"`,
"fi",
"cat \"$status_file\"",
"status=$(sed -n 's/^http_status=//p' \"$status_file\" | tail -1)",
"rm -f /tmp/unidesk-github-api-probe.json",
"rm -f \"$status_file\"",
"case \"$status\" in 2*) exit 0 ;; *) exit 1 ;; esac",
].join("\n");
return shellProbe(script, 15_000);
}
function issueApiUrl(apiBaseUrl: string, repo: string | null, issueNumber: number): string | null {
if (repo === null) return null;
return `${apiBaseUrl.replace(/\/+$/u, "")}/repos/${repo}/issues/${issueNumber}`;
}
function ghRepoArgs(repo: string | null): string[] {
return repo === null ? ["repo", "view", "--json", "nameWithOwner,visibility,isPrivate"] : ["repo", "view", repo, "--json", "nameWithOwner,visibility,isPrivate"];
}
function httpsGitUrl(repo: string | null, host: string): string | null {
return repo === null ? null : `https://${host}/${repo}.git`;
}
function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, checkedAt: string): PullRequestDeliveryPreflight {
const tools = {
git: toolStatus("git", ["--version"]),
gh: toolStatus("gh", ["--version"]),
hub: toolStatus("hub", ["version"]),
jq: toolStatus("jq", ["--version"]),
ssh: toolStatus("ssh", ["-V"]),
curl: toolStatus("curl", ["--version"]),
};
const insideProbe = commandProbe("git", ["rev-parse", "--is-inside-work-tree"], { timeoutMs: 5000 });
const insideWorktree = insideProbe.ok && firstLine(insideProbe.stdout) === "true";
const home = process.env.HOME ?? null;
const remoteOrigin = gitValue(["remote", "get-url", "origin"]);
const githubHost = process.env.GH_HOST?.trim() || "github.com";
const githubApiBaseUrl = (process.env.GITHUB_API_URL?.trim() || "https://api.github.com").replace(/\/+$/u, "");
const githubRepo = parseGitHubRepo(remoteOrigin);
const issueProbeNumberRaw = Number(options.issueNumber ?? process.env.CODE_QUEUE_PR_PREFLIGHT_ISSUE_NUMBER ?? 20);
const issueProbeNumber = Number.isInteger(issueProbeNumberRaw) && issueProbeNumberRaw > 0 ? issueProbeNumberRaw : 20;
const privateKeyProbe = shellProbe("test -d \"$HOME/.ssh\" && find \"$HOME/.ssh\" -maxdepth 1 -type f \\( -name 'id_*' ! -name '*.pub' -o -name '*.pem' \\) -print -quit", 2000);
const knownHostsProbe = shellProbe("test -s \"$HOME/.ssh/known_hosts\"", 2000);
const gitInfo = {
insideWorktree,
branch: gitValue(["branch", "--show-current"]),
head: gitValue(["rev-parse", "--short", "HEAD"]),
originMaster: gitValue(["rev-parse", "--short", "origin/master"]),
remoteOrigin: remoteOrigin === null ? null : compactOutput(remoteOrigin, 800),
userName: gitValue(["config", "--get", "user.name"]),
userEmail: gitValue(["config", "--get", "user.email"]),
home,
homeWritable: homeWritable(),
knownHostsPresent: knownHostsProbe.ok,
privateKeyPresent: privateKeyProbe.ok && privateKeyProbe.stdout.trim().length > 0,
};
const credentials = {
ghTokenPresent: boolEnv("GH_TOKEN"),
githubTokenPresent: boolEnv("GITHUB_TOKEN"),
ghHostPresent: boolEnv("GH_HOST"),
githubApiUrlPresent: boolEnv("GITHUB_API_URL"),
ghRepoPresent: boolEnv("GH_REPO"),
sshAuthSockPresent: boolEnv("SSH_AUTH_SOCK"),
gitAskpassPresent: boolEnv("GIT_ASKPASS"),
ghHostsConfigPresent: fileExistsProbe(`${home ?? "/root"}/.config/gh/hosts.yml`),
gitCredentialsPresent: fileExistsProbe(`${home ?? "/root"}/.git-credentials`),
};
const egressProxy = proxySummary();
const issueUrl = issueApiUrl(githubApiBaseUrl, githubRepo, issueProbeNumber);
const egress = {
proxy: egressProxy,
githubDefault: options.includeRemote === true ? commandProbe("curl", ["-IsS", "--max-time", "10", `https://${githubHost}`], { timeoutMs: 15_000 }) : skippedProbe("github-default-network"),
githubDirect: options.includeRemote === true ? commandProbe("curl", ["--noproxy", "*", "-IsS", "--max-time", "10", `https://${githubHost}`], { timeoutMs: 15_000 }) : skippedProbe("github-direct-network"),
apiDefault: options.includeRemote === true ? commandProbe("curl", ["-IsS", "--max-time", "10", githubApiBaseUrl], { timeoutMs: 15_000 }) : skippedProbe("github-api-default-network"),
apiDirect: options.includeRemote === true ? commandProbe("curl", ["--noproxy", "*", "-IsS", "--max-time", "10", githubApiBaseUrl], { timeoutMs: 15_000 }) : skippedProbe("github-api-direct-network"),
issueApi: options.includeRemote === true && issueUrl !== null ? authCurlProbe(issueUrl) : null,
};
const githubSshAuthProbe = options.includeRemote === true
? commandProbe("ssh", ["-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", `git@${githubHost}`], { timeoutMs: 20_000 })
: null;
const githubSshAuthenticated = githubSshAuthProbe !== null
&& (githubSshAuthProbe.ok || /successfully authenticated/iu.test(`${githubSshAuthProbe.stdout}\n${githubSshAuthProbe.stderr}`));
const ghRepoView = options.includeRemote === true && tools.gh.ok
? commandProbe("gh", ghRepoArgs(githubRepo), { timeoutMs: 15_000 })
: null;
const ghIssueView = options.includeRemote === true && tools.gh.ok && githubRepo !== null
? commandProbe("gh", ["issue", "view", String(issueProbeNumber), "--repo", githubRepo, "--json", "number,title,state"], { timeoutMs: 15_000 })
: null;
const ghPrReadOnly = options.includeRemote === true && tools.gh.ok && githubRepo !== null
? commandProbe("gh", ["pr", "list", "--repo", githubRepo, "--limit", "1", "--json", "number,title,state,headRefName,baseRefName"], { timeoutMs: 15_000 })
: null;
const gitHttpsUrl = httpsGitUrl(githubRepo, githubHost);
const remote = options.includeRemote === true ? {
gitLsRemote: commandProbe("git", ["ls-remote", "--heads", "origin", "master"], { timeoutMs: 20_000 }),
gitHttpsLsRemote: gitHttpsUrl === null ? null : commandProbe("git", ["-c", "credential.helper=", "ls-remote", "--heads", gitHttpsUrl, "master"], { timeoutMs: 20_000 }),
githubSshAuth: githubSshAuthProbe ?? shellProbe("exit 1", 1000),
githubSshAuthenticated,
ghAuthStatus: tools.gh.ok ? commandProbe("gh", ["auth", "status", "-h", githubHost], { timeoutMs: 10_000 }) : null,
ghRepoView,
ghIssueView,
ghPrReadOnly,
} : undefined;
const pushDryRunRef = options.pushDryRunRef?.trim() || "refs/heads/probe/code-queue-pr-capability-dryrun";
const pushDryRun = options.includePushDryRun === true
? commandProbe("git", ["push", "--dry-run", "origin", `HEAD:${pushDryRunRef}`], { timeoutMs: 30_000 })
: undefined;
const limitations: string[] = [];
const risks: string[] = [];
if (!tools.git.ok) limitations.push("missing git CLI");
if (!tools.jq.ok) limitations.push("missing jq CLI");
if (!tools.gh.ok && !tools.hub.ok) limitations.push("missing PR CLI: install gh or hub");
if (!credentials.ghTokenPresent && !credentials.githubTokenPresent) limitations.push("GH_TOKEN/GITHUB_TOKEN is not present; gh cannot create PRs unless another gh credential store is mounted");
if (!credentials.ghHostsConfigPresent && !credentials.gitCredentialsPresent && !credentials.ghTokenPresent && !credentials.githubTokenPresent) limitations.push("no gh hosts config, git credentials file, GH_TOKEN, or GITHUB_TOKEN is visible for GitHub API/issue/PR access");
if (!gitInfo.insideWorktree) limitations.push("current directory is not a Git worktree");
if (gitInfo.remoteOrigin === null) limitations.push("git remote origin is not configured");
if (githubRepo === null) limitations.push("cannot infer GitHub owner/repo from GH_REPO or git remote origin");
if (!gitInfo.homeWritable) limitations.push("HOME is not writable");
if (!gitInfo.knownHostsPresent) risks.push("GitHub known_hosts is not pre-seeded; first SSH use may need accept-new behavior");
if (!gitInfo.privateKeyPresent && !credentials.sshAuthSockPresent && !credentials.ghTokenPresent && !credentials.githubTokenPresent) limitations.push("no SSH key, SSH agent, or GitHub token credential is visible");
if (egress.proxy.selectedProxyHost !== null && egress.proxy.selectedProxyHostResolvable === false) limitations.push(`configured GitHub egress proxy host is not resolvable: ${egress.proxy.selectedProxyHost}`);
if (options.includeRemote === true && !egress.githubDefault.ok) limitations.push("GitHub HTTPS probe failed with the default environment/proxy");
if (options.includeRemote === true && !egress.apiDefault.ok) limitations.push("GitHub API probe failed with the default environment/proxy");
if (options.includeRemote === true && egress.issueApi !== null && !egress.issueApi.ok) limitations.push(`GitHub issue API probe for #${issueProbeNumber} failed; private repo access likely lacks token or egress`);
if (remote !== undefined) {
if (!remote.gitLsRemote.ok) limitations.push("git ls-remote origin master failed");
if (remote.gitHttpsLsRemote !== null && !remote.gitHttpsLsRemote.ok) limitations.push("anonymous HTTPS git ls-remote failed; HTTPS Git access likely needs token credentials");
if (!remote.githubSshAuthenticated) limitations.push("GitHub SSH auth probe did not authenticate");
if (remote.ghAuthStatus !== null && !remote.ghAuthStatus.ok) limitations.push("gh auth status failed");
if (tools.gh.ok && remote.ghRepoView !== null && !remote.ghRepoView.ok) limitations.push("gh repo view failed");
if (tools.gh.ok && remote.ghIssueView !== null && !remote.ghIssueView.ok) limitations.push(`gh issue view #${issueProbeNumber} failed`);
if (tools.gh.ok && remote.ghPrReadOnly !== null && !remote.ghPrReadOnly.ok) limitations.push("gh pr read-only probe failed");
}
if (pushDryRun !== undefined && !pushDryRun.ok) limitations.push("git push --dry-run failed for probe branch");
const prCliReady = tools.gh.ok || tools.hub.ok;
const tokenOrGhStore = credentials.ghTokenPresent || credentials.githubTokenPresent || credentials.ghHostsConfigPresent || (remote?.ghAuthStatus?.ok === true);
const remoteReady = remote === undefined || (egress.githubDefault.ok && egress.apiDefault.ok && remote.gitLsRemote.ok && remote.githubSshAuthenticated);
const pushReady = pushDryRun === undefined || pushDryRun.ok;
return {
ok: tools.git.ok && tools.jq.ok && prCliReady && tokenOrGhStore && gitInfo.insideWorktree && gitInfo.remoteOrigin !== null && gitInfo.homeWritable && remoteReady && pushReady,
checkedAt,
tools,
credentials,
githubContext: {
host: githubHost,
apiBaseUrl: githubApiBaseUrl,
repo: githubRepo,
issueProbeNumber,
},
egress,
git: gitInfo,
remote,
pushDryRun,
limitations,
risks,
};
}
function codexStatus(path: string, checkedAt: string): RuntimePreflightPortStatus {
const commandV = shellProbe("command -v codex");
const version = commandProbe("codex", ["--version"]);
const appServerDryRun = commandProbe("codex", ["app-server", "--help"]);
const errors = [commandV, version, appServerDryRun].map(errorSummary).filter((value): value is string => value !== null);
return {
agentPort: "codex",
ok: errors.length === 0,
path,
commandPath: firstLine(commandV.stdout),
version: firstLine(version.stdout) ?? firstLine(version.stderr),
appServerDryRun,
probes: { commandV, version },
errors,
checkedAt,
};
}
function opencodeStatus(path: string, checkedAt: string): RuntimePreflightPortStatus {
const commandV = shellProbe("command -v opencode");
const version = commandProbe("opencode", ["--version"]);
const errors = [commandV, version].map(errorSummary).filter((value): value is string => value !== null);
return {
agentPort: "opencode",
ok: errors.length === 0,
path,
commandPath: firstLine(commandV.stdout),
version: firstLine(version.stdout) ?? firstLine(version.stderr),
probes: { commandV, version },
errors,
checkedAt,
};
}
export function collectRuntimePreflight(options: RuntimePreflightOptions = {}): RuntimePreflightReport {
const checkedAt = new Date().toISOString();
const path = process.env.PATH ?? "";
const ports = {
codex: codexStatus(path, checkedAt),
opencode: opencodeStatus(path, checkedAt),
};
const pullRequestDelivery = collectPullRequestDeliveryPreflight(options, checkedAt);
return {
ok: ports.codex.ok && ports.opencode.ok && pullRequestDelivery.ok,
checkedAt,
cwd: process.cwd(),
pid: process.pid,
node: {
platform: process.platform,
arch: process.arch,
version: process.version,
},
path,
ports,
pullRequestDelivery,
};
}
export function runtimePreflightJson(report: RuntimePreflightReport): JsonValue {
return report as unknown as JsonValue;
}