diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 94023a35..35852e22 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -150,7 +150,7 @@ bun scripts/cli.ts ssh D601 glob --root /home/ubuntu/pikapython --pattern '**/*- `--main-server-ip` 是一个全局前缀,必须放在需要透传的命令同一次调用中,例如 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`。默认传输是公网 frontend:本地 CLI 读取本仓库 `config.json` 中的 frontend 登录账号密码,登录 `http://:/` 获取 HttpOnly session cookie,然后通过 frontend 的 `/api/*` 同源代理访问 backend-core 内网 API;因此计算节点只需要能访问公网 frontend,不需要主 server SSH key,也不需要打开 backend-core REST API 或 PostgreSQL 端口。 -默认 frontend 传输支持 `debug health`、`debug dispatch`、`debug task`、`artifact-registry status|health`、`ci publish-user-service --dry-run`、`microservice list/status/health/diagnostics/tunnel-self-test/proxy`、`decision upload/list/show/health`、`decision requirement list/upsert`、`decision diary import/list/history/months/show/edit/upsert`、`codex task `、`codex tasks`、`codex output `、`codex judge --attempt N` 和 `ssh `。运行中纠偏 `codex steer` 属于 active run write control,应在主 server 本机 CLI 或显式 SSH 传输上执行,避免公网 frontend 透传限制 stdin/body 审计语义。其中 `ssh` 的 remote frontend 传输使用 `host.ssh` dispatch 执行有界远端命令,适合 `ssh D601 hostname` 和 `ssh D601 skills` 这类自测;交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。frontend 远程透传不会流式转发本地 stdin,因此 `ssh py < script.py`、`ssh apply-patch < patch.diff` 这类 stdin-backed helper 必须在主 server 本机运行,或显式切换到 `--main-server-transport ssh`。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--main-server-key ` 或 `--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts `。 +默认 frontend 传输支持 `debug health`、`debug dispatch`、`debug task`、`artifact-registry status|health`、`ci publish-user-service --dry-run`、`microservice list/status/health/diagnostics/tunnel-self-test/proxy`、`decision upload/list/show/health`、`decision requirement list/upsert`、`decision diary import/list/history/months/show/edit/upsert`、`codex task `、`codex tasks`、`codex output `、`codex judge --attempt N` 和 `ssh `。`microservice status/health/diagnostics` 经 frontend 远程传输时也复用本地 CLI 的默认 compact summary,`microservice health code-queue` 只有显式 `--raw` 或 `--full` 才返回完整健康 body。运行中纠偏 `codex steer` 属于 active run write control,应在主 server 本机 CLI 或显式 SSH 传输上执行,避免公网 frontend 透传限制 stdin/body 审计语义。其中 `ssh` 的 remote frontend 传输使用 `host.ssh` dispatch 执行有界远端命令,适合 `ssh D601 hostname` 和 `ssh D601 skills` 这类自测;交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。frontend 远程透传不会流式转发本地 stdin,因此 `ssh py < script.py`、`ssh apply-patch < patch.diff` 这类 stdin-backed helper 必须在主 server 本机运行,或显式切换到 `--main-server-transport ssh`。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--main-server-key ` 或 `--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts `。 计算节点可以用该入口测试自身的远程升级闭环,而不需要在计算节点公开 core REST API 或 database。标准顺序是:先运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health` 确认主 server 看到当前 Provider 在线,且该 Provider labels 中 `unideskCapabilities` 包含 `host.ssh`、`hostSshConfigured=true`、`hostSshKeyPresent=true`;再运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch provider.upgrade --mode schedule --wait-ms 15000` 触发真实 `provider.upgrade`;随后再次运行 `debug health` 确认节点重新上线;最后运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch host.ssh --wait-ms 15000` 和 `bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh hostname` 验证 SSH 透传能力。provider-gateway 新部署或升级后没有完成这组 remote CLI 自测,不能视为交付完成。 diff --git a/scripts/microservice-health-output-contract-test.ts b/scripts/microservice-health-output-contract-test.ts index e42f20cf..d54dbde0 100644 --- a/scripts/microservice-health-output-contract-test.ts +++ b/scripts/microservice-health-output-contract-test.ts @@ -1,4 +1,5 @@ import { summarizeMicroserviceHealthResponse } from "./src/microservices"; +import { summarizeRemoteMicroserviceResponse } from "./src/remote"; type JsonRecord = Record; @@ -102,6 +103,8 @@ const fixture = largeCodeQueueHealth(); const compact = asRecord(summarizeMicroserviceHealthResponse(fixture, ["health", "code-queue"], "code-queue")); const raw = summarizeMicroserviceHealthResponse(fixture, ["health", "code-queue", "--raw"], "code-queue"); const full = summarizeMicroserviceHealthResponse(fixture, ["health", "code-queue", "--full"], "code-queue"); +const remoteCompact = asRecord(summarizeRemoteMicroserviceResponse("health", "code-queue", fixture, ["microservice", "health", "code-queue"])); +const remoteRaw = summarizeRemoteMicroserviceResponse("health", "code-queue", fixture, ["microservice", "health", "code-queue", "--raw"]); const unhealthyWrapper = asRecord(summarizeMicroserviceHealthResponse({ ok: false, status: 503, @@ -137,6 +140,8 @@ assertCondition(liveness.effectiveLiveness === "live" && liveness.splitBrainLive assertCondition(String(liveness.interpretation).includes("continue supervision"), "compact liveness includes commander interpretation", liveness); assertCondition(typeof outputPolicy.rawCommand === "string" && String(outputPolicy.rawCommand).includes("--raw"), "compact output exposes raw drill-down command", outputPolicy); assertCondition(raw === fixture && full === fixture, "--raw/--full must preserve full health access", { rawSame: raw === fixture, fullSame: full === fixture }); +assertCondition(!("body" in remoteCompact) && asRecord(remoteCompact.queue).runningCount === 5, "remote frontend health must reuse compact code-queue summary by default", remoteCompact); +assertCondition(remoteRaw === fixture, "remote frontend health --raw must preserve full diagnostics", { remoteRawSame: remoteRaw === fixture }); assertCondition(unhealthyWrapper.ok === false && unhealthyWrapper.reason === "health body ok=false", "unhealthy wrapper keeps probe failure", unhealthyWrapper); assertCondition(unhealthyQueue.runningCount === 5 && unhealthyLiveness.effectiveLiveness === "live", "unhealthy wrapper still surfaces upstream queue liveness", { unhealthyQueue, unhealthyLiveness }); @@ -146,6 +151,7 @@ console.log(JSON.stringify({ "default code-queue health output is compact and omits raw body/queue dumps", "critical ok/status/service/running/heartbeat/liveness fields remain visible", "unhealthy backend health wrapper still surfaces upstream code-queue liveness", + "remote frontend microservice health uses the same compact/raw policy", "--raw and --full return the original health response for diagnostics", ], compactChars: compactBody.length, diff --git a/scripts/src/remote.ts b/scripts/src/remote.ts index 9116efa4..bd8dbee0 100644 --- a/scripts/src/remote.ts +++ b/scripts/src/remote.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; import { type UniDeskConfig } from "./config"; import { type DebugDispatchCommand, isDebugDispatchCommand } from "./debug"; -import { summarizeMicroserviceProxyResponse } from "./microservices"; +import { summarizeMicroserviceHealthResponse, summarizeMicroserviceObservation, summarizeMicroserviceProxyResponse } from "./microservices"; import { parseNetworkPerfOptions, runNetworkPerf } from "./network-perf"; import { isSshSkillDiscoveryArgs, parseSshArgs } from "./ssh"; import { codexJudgeQueryAsync, codexOutputQueryAsync, codexPrPreflightQueryAsync, codexTaskQueryAsync, codexTasksQueryAsync } from "./code-queue"; @@ -618,6 +618,13 @@ async function remoteDebugTask(session: FrontendSession, args: string[]): Promis return { transport: "frontend", tasksResponse, taskId, task: task ?? null }; } +export function summarizeRemoteMicroserviceResponse(action: string, id: string, response: unknown, args: string[]): unknown { + const optionArgs = args.slice(3); + if (action === "health") return summarizeMicroserviceHealthResponse(response, optionArgs, id); + if (action === "status" || action === "diagnostics") return summarizeMicroserviceObservation(action, id, response, optionArgs); + return response; +} + async function remoteMicroservice(session: FrontendSession, args: string[]): Promise { const action = args[1] ?? "list"; const id = args[2]; @@ -626,9 +633,10 @@ async function remoteMicroservice(session: FrontendSession, args: string[]): Pro return { transport: "frontend", response: await frontendJson(session, "/api/microservices", undefined, 12_000) }; } if ((action === "status" || action === "health" || action === "diagnostics" || action === "tunnel-self-test") && id !== undefined) { + const response = await frontendJson(session, `/api/microservices/${encodeURIComponent(id)}/${action}`, undefined, 18_000); return { transport: "frontend", - response: await frontendJson(session, `/api/microservices/${encodeURIComponent(id)}/${action}`, undefined, 18_000), + response: summarizeRemoteMicroserviceResponse(action, id, response, args), }; } if (action === "proxy" && id !== undefined && path !== undefined && path.startsWith("/")) {