diff --git a/AGENTS.md b/AGENTS.md index f96c4292..bb9dc041 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # UniDesk Agent Index -UniDesk 是一个以主 server 为统一入口的分布式工作平台。本文件只做自动加载的顶级索引;长期规则、详细流程和判定标准必须放到 skill 或 `docs/reference/*.md`。 +UniDesk 以主 server 为统一入口。本文件只做自动加载的顶级索引;长期规则、流程和判定标准必须放到 skill 或 `docs/reference/*.md`。 ## P0: 文件体积与脚本分流 @@ -11,9 +11,9 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台。本文 - P0: 发现固定主/目标 worktree 落后 remote 时,必须立刻先 `git stash push -u` 保存脏改(如有,含 untracked),再 `git pull --ff-only` 快进到最新 remote,然后 `git stash apply` 并按语义合并;主工作区恢复出的并行改动必须先直接提交,再继续后续任务;禁止用 reset、drop 或覆盖式 checkout 丢弃并行改动。 -## P0: backend-core 运行面冻结 +## P0: NC01 k8s server 固定运行面 -- P0: backend-core 运行面恢复 healthy 后,除非用户明确要求,不得再触碰 backend-core 运行面;禁止主动执行 `server rebuild backend-core`、`server restart backend-core`、手工重建/重启/替换 `unidesk-backend-core` 容器或等价生产变更。 +- P0: NC01 用 k8s 部署 UniDesk server/backend-core 是固定原则;恢复、验证和 `trans` 必须以 `config/unidesk-host-k8s.yaml` 与 k8s `unidesk` namespace 为准,禁止把 Docker Compose/`unidesk-backend-core` 容器当 NC01 运行面;healthy 后非明确要求不得重建/重启/替换。 ## P0: AGENTS.md 体积与分流 diff --git a/config/platform-infra/host-proxy.yaml b/config/platform-infra/host-proxy.yaml index 605f1641..52c3031b 100644 --- a/config/platform-infra/host-proxy.yaml +++ b/config/platform-infra/host-proxy.yaml @@ -26,6 +26,23 @@ server: host: 127.0.0.1 port: 18792 sources: + pk01-nc01-vpn-server-hysteria: + sourceType: vpn-server-hysteria-existing + serverRef: /root/vpn-server + benchmarkRef: local-vpn-server + implementationRef: /root/vpn-server + sourceConfigRef: /root/vpn-server/output/client.yaml + expectedServer: 152-53-229-148.nip.io:443 + client: + mode: external-existing-hysteria + configPath: /etc/host-proxy/hysteria-client.yaml + serviceName: host-proxy-hysteria.service + httpListenHost: 127.0.0.1 + httpListenPort: 10809 + socks5ListenHost: 127.0.0.1 + socks5ListenPort: 10808 + proxyUrl: http://127.0.0.1:10809 + externalProbeUrl: https://www.gstatic.com/generate_204 nc01-vpn-server-shadowsocks: extends: templates.sources.singBoxHostClient benchmarkRef: local-vpn-server @@ -33,6 +50,24 @@ sources: extends: templates.sources.singBoxHostClient benchmarkRef: "pikasTech/unidesk#1110" targets: + PK01: + extends: templates.targets.hostProxyClient + route: PK01 + sourceRef: sources.pk01-nc01-vpn-server-hysteria + env: + httpProxy: http://127.0.0.1:10809 + httpsProxy: http://127.0.0.1:10809 + allProxy: http://127.0.0.1:10809 + noProxyRefs: + common: noProxy.common + extras: noProxy.nodeExtras.PK01 + trans: + hostProxyEnv: + enabled: false + apply: + reloadSystemd: false + restartDocker: false + restartK3s: false NC01: extends: templates.targets.hostProxyClient route: NC01 @@ -133,8 +168,11 @@ noProxy: - hyueapi.com - .hyueapi.com nodeExtras: + PK01: + - 152.53.229.148 + - 152-53-229-148.nip.io NC01: - 152.53.229.148 JD01: - 74.48.78.17 - - 82.156.23.220 \ No newline at end of file + - 82.156.23.220 diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index 6a993690..6dabeee9 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -94,15 +94,16 @@ manualAccounts: kind: proxy provider: target-egress-proxy description: Bind a protected manual account to the selected target's YAML-declared egress proxy. - pk01-local-egress-proxy: + pk01-host-proxy: enabled: true kind: proxy provider: fixed-http-proxy - description: Bind a protected manual account to PK01 local provider-gateway egress. + description: Bind protected/manual PK01 Sub2API accounts to the PK01 host proxy HTTP endpoint. + hostProxyRef: config/platform-infra/host-proxy.yaml#targets.PK01 fixedProxy: protocol: http host: 127.0.0.1 - port: 18789 + port: 10809 unified-pool-group: enabled: true kind: group @@ -113,7 +114,7 @@ manualAccounts: reason: Manually configured in Sub2API as protected; keep outside pool auto-management and sentinel control. proxyBinding: enabled: true - source: pk01-local-egress-proxy + source: pk01-host-proxy proxyName: platform-infra-sub2api-pk01-local-egress-proxy groupBinding: enabled: true diff --git a/docs/reference/pk01.md b/docs/reference/pk01.md index f2eba6db..b128ee6b 100644 --- a/docs/reference/pk01.md +++ b/docs/reference/pk01.md @@ -14,6 +14,33 @@ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' SH ``` +When the PK01 provider-gateway channel is offline, operators may use the ignored break-glass SSH source `.env/PK01_ssh.txt`. The file format is two lines: line 1 is the SSH target such as `ubuntu@`, line 2 is the password. Do not print the second line, commit it, copy it into issue comments, or pass it through commands that echo typed input. Prefer an ephemeral `SSH_ASKPASS` wrapper so OpenSSH can handle `keyboard-interactive` or password prompts without exposing the value: + +```bash +tmp=$(mktemp -d /tmp/pk01-askpass.XXXXXX) +chmod 700 "$tmp" +cat >"$tmp/askpass.sh" <<'SH' +#!/bin/sh +printf '%s\n' "$PK01_SSH_PASS" +SH +chmod 700 "$tmp/askpass.sh" + +PK01_SSH_PASS="$(sed -n '2p' .env/PK01_ssh.txt)" \ +SSH_ASKPASS="$tmp/askpass.sh" \ +SSH_ASKPASS_REQUIRE=force \ +DISPLAY=none \ +setsid -w ssh \ + -o StrictHostKeyChecking=accept-new \ + -o ConnectTimeout=8 \ + -o PreferredAuthentications=keyboard-interactive,password,publickey \ + "$(sed -n '1p' .env/PK01_ssh.txt)" \ + 'hostname; date -Is' + +rm -rf "$tmp" +``` + +This fallback is for bounded host diagnostics or node-local recovery only. After restoring `unidesk-provider-gateway-pk01`, return to `trans PK01 ...` and `platform-infra ... --target PK01` as the operating truth. + Before closing an operation, verify both the provider channel and host workload state: ```bash @@ -36,7 +63,9 @@ PK01 currently uses a direct Docker provider-gateway deployment rather than a fu | Env file | `/home/ubuntu/.unidesk/state/provider-pk01/provider.env` | Contains provider token and must not be printed, copied into docs, or committed. | | Host SSH key | `/home/ubuntu/.unidesk/host-ssh-pk01/id_ed25519` | Mounted read-only at `/run/host-ssh`; public key is authorized for `ubuntu`. | | Logs | `/home/ubuntu/.unidesk/logs/provider-pk01` | Node-local runtime logs, not a Git source of truth. | -| Egress proxy | `127.0.0.1:18789` | Loopback only; never expose as a public endpoint. | +| Host SSH target | `root@host.docker.internal:22` | `trans PK01 ...` is expected to land as root for host/systemd operations. | +| Provider-gateway egress proxy | `127.0.0.1:18789` | Provider maintenance tunnel only; do not bind Sub2API upstream accounts to this endpoint. | +| Host proxy HTTP endpoint | `127.0.0.1:10809` | PK01 host proxy endpoint for Sub2API account proxy binding, declared by `config/platform-infra/host-proxy.yaml#targets.PK01`. | Long-term provider-gateway upgrades should converge to the standard `provider.upgrade mode=schedule` flow described in `docs/reference/provider-gateway.md`. If PK01 is still on the direct Docker bootstrap path, do not rebuild the gateway synchronously through the gateway's own `trans PK01` session. Use a detached node-local job or first move PK01 to the standard attach/upgrade bundle. @@ -68,7 +97,9 @@ If one public service fails while other services still work, restore the missing In this mode, public ports `80` and `443` belong to Caddy. The existing `pikanode` container must be bound to a loopback HTTP port and used only as the apex PikaPython/PikaNode upstream. Public platform routes must be read from their owning YAML. For the current Sub2API PK01 host-Docker target, `api.pikapython.com` uses `publicExposure.mode=pk01-local` and API traffic follows `client -> PK01 Caddy -> 127.0.0.1: -> PK01 host-Docker Sub2API`. For k3s external-active targets, the route may instead be `client -> PK01 Caddy -> PK01 frps remote port -> target frpc -> target Sub2API`. Neither path may pass through pikanode or a master-server reverse proxy. -Caddy binary installation is also YAML-controlled. If `publicExposure.pk01.caddyDownloadProxyUrl` is set, PK01 Caddy downloads must use that proxy URL; the PK01 loopback provider egress proxy is the preferred source. A slow or failing Caddy download should first be treated as missing proxy use, not as a reason to keep retrying a naked GitHub release download. +Caddy binary installation is also YAML-controlled. If `publicExposure.pk01.caddyDownloadProxyUrl` is set, PK01 Caddy downloads must use that proxy URL. Sub2API account-level proxy binding must use the PK01 host proxy HTTP endpoint, not the provider-gateway egress proxy. + +PK01's account-level host proxy is a status-only Hysteria client connected to the NC01-local `../vpn-server` deployment. The durable declaration is `config/platform-infra/host-proxy.yaml#targets.PK01`, and protected/manual Sub2API account bindings should reference it through `config/platform-infra/sub2api-codex-pool.yaml` `manualAccounts.bindingSources.*.hostProxyRef`. Do not replace it with `127.0.0.1:18789`; verify it with `bun scripts/cli.ts platform-infra egress-proxy host status --target PK01` and then validate the Sub2API account/user request path. The public certificate depends on DNS. The `api.pikapython.com` record must resolve to the YAML-declared PK01 public address before Caddy can complete ACME issuance. If the DNS record is absent or stale, local probes such as PK01 loopback app health checks, loopback Caddy `--resolve` checks, or target-specific FRP remote-port checks can prove parts of the data path, but final `https://api.pikapython.com` validation remains blocked until DNS is corrected. diff --git a/scripts/src/platform-infra-host-proxy.ts b/scripts/src/platform-infra-host-proxy.ts index 8303e01e..a5108507 100644 --- a/scripts/src/platform-infra-host-proxy.ts +++ b/scripts/src/platform-infra-host-proxy.ts @@ -43,7 +43,7 @@ interface HostProxyServer { interface HostProxySource { id: string; - sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks"; + sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks" | "vpn-server-hysteria-existing"; serverRef: string; benchmarkRef: string; implementationRef: string; @@ -51,14 +51,17 @@ interface HostProxySource { sourceRef: string; sourceKey: string; sourceFingerprint: string; - masterShadowsocks: MasterShadowsocksSourceSpec; + masterShadowsocks: MasterShadowsocksSourceSpec | null; client: HostProxyClient; proxyUrl: string; externalProbeUrl: string; + expectedServer: string | null; fingerprint: string; } -interface HostProxyClient { +type HostProxyClient = HostProxyManagedClient | HostProxyExistingHysteriaClient; + +interface HostProxyManagedClient { mode: "trans-static-binary"; upstreamUrl: string; version: string; @@ -79,6 +82,16 @@ interface HostProxyClient { healthUrl: string; } +interface HostProxyExistingHysteriaClient { + mode: "external-existing-hysteria"; + configPath: string; + serviceName: string; + httpListenHost: string; + httpListenPort: number; + socks5ListenHost: string; + socks5ListenPort: number; +} + interface HostProxyPodAccess { enabled: boolean; listenHost: string; @@ -136,14 +149,11 @@ export async function runPlatformInfraHostProxyCommand(_config: UniDeskConfig, a if (options.action === "plan") return renderPlan(plan(config.server, target)); if (options.action === "status") return renderStatus(status(config.server, target)); if (!options.confirm || options.dryRun) { + const dryRunPlan = plan(config.server, target); return renderPlan({ - ...plan(config.server, target), + ...dryRunPlan, mode: "dry-run", mutation: false, - next: { - apply: `bun scripts/cli.ts platform-infra egress-proxy host apply --target ${target.id} --confirm`, - status: `bun scripts/cli.ts platform-infra egress-proxy host status --target ${target.id}`, - }, }); } return renderApply(apply(config.server, target)); @@ -233,13 +243,14 @@ function parseServer(raw: Record, label: string): HostProxyServ } function parseSource(id: string, raw: Record): HostProxySource { - const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks"] as const); + const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks", "vpn-server-hysteria-existing"] as const); + if (sourceType === "vpn-server-hysteria-existing") return parseExistingHysteriaSource(id, raw, sourceType); const sourceConfigRef = stringField(raw, "sourceConfigRef", `${configPath}.sources.${id}`); const resolved = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.sources.${id}.sourceConfigRef`); if (resolved.sourceType !== "master-shadowsocks" || resolved.masterShadowsocks === null) { throw new Error(`${configPath}.sources.${id}.sourceConfigRef must reference a master-shadowsocks source`); } - const client = clientSpec(record(raw.client, `${configPath}.sources.${id}.client`), `${configPath}.sources.${id}.client`); + const client = managedClientSpec(record(raw.client, `${configPath}.sources.${id}.client`), `${configPath}.sources.${id}.client`); const proxyUrl = urlField(raw, "proxyUrl", `${configPath}.sources.${id}`); const expectedProxyUrl = `http://${client.listenHost}:${client.listenPort}`; if (proxyUrl !== expectedProxyUrl) throw new Error(`${configPath}.sources.${id}.proxyUrl must be ${expectedProxyUrl}`); @@ -257,6 +268,7 @@ function parseSource(id: string, raw: Record): HostProxySource client, proxyUrl, externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`), + expectedServer: null, fingerprint: "", }; return { @@ -274,7 +286,46 @@ function parseSource(id: string, raw: Record): HostProxySource }; } -function clientSpec(raw: Record, label: string): HostProxyClient { +function parseExistingHysteriaSource(id: string, raw: Record, sourceType: HostProxySource["sourceType"]): HostProxySource { + const client = existingHysteriaClientSpec(record(raw.client, `${configPath}.sources.${id}.client`), `${configPath}.sources.${id}.client`); + const proxyUrl = urlField(raw, "proxyUrl", `${configPath}.sources.${id}`); + const expectedProxyUrl = `http://${client.httpListenHost}:${client.httpListenPort}`; + if (proxyUrl !== expectedProxyUrl) throw new Error(`${configPath}.sources.${id}.proxyUrl must be ${expectedProxyUrl}`); + const sourceConfigRef = stringField(raw, "sourceConfigRef", `${configPath}.sources.${id}`); + const expectedServer = listenAddressField(raw, "expectedServer", `${configPath}.sources.${id}`); + const source = { + id, + sourceType, + serverRef: stringField(raw, "serverRef", `${configPath}.sources.${id}`), + benchmarkRef: stringField(raw, "benchmarkRef", `${configPath}.sources.${id}`), + implementationRef: stringField(raw, "implementationRef", `${configPath}.sources.${id}`), + sourceConfigRef, + sourceRef: sourceConfigRef, + sourceKey: "", + sourceFingerprint: fingerprint({ sourceConfigRef, expectedServer }), + masterShadowsocks: null, + client, + proxyUrl, + externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`), + expectedServer, + fingerprint: "", + }; + return { + ...source, + fingerprint: fingerprint({ + sourceType, + benchmarkRef: source.benchmarkRef, + implementationRef: source.implementationRef, + sourceConfigRef, + expectedServer, + client, + proxyUrl, + externalProbeUrl: source.externalProbeUrl, + }), + }; +} + +function managedClientSpec(raw: Record, label: string): HostProxyManagedClient { const mode = enumField(raw, "mode", label, ["trans-static-binary"] as const); const podAccess = raw.podAccess === undefined ? null : podAccessSpec(record(raw.podAccess, `${label}.podAccess`), `${label}.podAccess`); const client = { @@ -302,6 +353,22 @@ function clientSpec(raw: Record, label: string): HostProxyClien return client; } +function existingHysteriaClientSpec(raw: Record, label: string): HostProxyExistingHysteriaClient { + const mode = enumField(raw, "mode", label, ["external-existing-hysteria"] as const); + const client = { + mode, + configPath: absolutePathField(raw, "configPath", label), + serviceName: stringField(raw, "serviceName", label), + httpListenHost: hostField(raw, "httpListenHost", label), + httpListenPort: portField(raw, "httpListenPort", label), + socks5ListenHost: hostField(raw, "socks5ListenHost", label), + socks5ListenPort: portField(raw, "socks5ListenPort", label), + }; + if (client.httpListenHost !== "127.0.0.1") throw new Error(`${label}.httpListenHost must stay 127.0.0.1 for host-local HTTP proxy`); + if (client.socks5ListenHost !== "127.0.0.1") throw new Error(`${label}.socks5ListenHost must stay 127.0.0.1 for host-local SOCKS5 proxy`); + return client; +} + function podAccessSpec(raw: Record, label: string): HostProxyPodAccess { const enabled = booleanField(raw, "enabled", label); const listenHost = hostField(raw, "listenHost", label); @@ -394,6 +461,7 @@ function resolveTarget(config: HostProxyConfig, targetId: string): HostProxyTarg function plan(server: HostProxyServer, target: HostProxyTarget): Record { const client = target.source.client; + const applyEnabled = client.mode === "trans-static-binary"; return { ok: true, command: "platform-infra egress-proxy host plan", @@ -407,14 +475,17 @@ function plan(server: HostProxyServer, target: HostProxyTarget): Record { + if (target.source.client.mode !== "trans-static-binary") { + throw new Error(`${configPath}.targets.${target.id} uses ${target.source.client.mode}; host apply is disabled for existing external clients. Use status to verify the declared runtime.`); + } const material = proxySecretMaterial(server, target.source); const serverApply = sourceUsesManagedServer(target.source) ? applyMasterProxyServer(server, material) : skippedMasterProxyServer(target.source, "apply"); const artifact = prepareClientArtifact(target.source.client); @@ -495,13 +566,14 @@ function skippedMasterProxyServer(source: HostProxySource, mode: "apply" | "stat existingHealthy: null, inspect: null, container: "external", - listen: `${source.masterShadowsocks.serverHost}:${source.masterShadowsocks.serverPort}`, + listen: source.masterShadowsocks === null ? source.expectedServer ?? source.proxyUrl : `${source.masterShadowsocks.serverHost}:${source.masterShadowsocks.serverPort}`, health: { ok: true, mode: "external", host: "", port: 0 }, valuesPrinted: false, }; } function proxySecretMaterial(server: HostProxyServer, source: HostProxySource): HostProxySecretMaterial { + if (source.masterShadowsocks === null) throw new Error(`${configPath}.sources.${source.id} does not render managed shadowsocks secret material`); const envSource = readEnvSourceFile({ root: rootPath(".state", "secrets"), sourceRef: source.sourceRef, @@ -531,6 +603,9 @@ function renderMasterShadowsocksServerConfig(server: HostProxyServer, password: } function renderSingBoxClientConfig(source: HostProxySource, password: string): string { + if (source.masterShadowsocks === null || source.client.mode !== "trans-static-binary") { + throw new Error(`${configPath}.sources.${source.id} cannot render sing-box client config`); + } const client = source.client; const inbounds = [ { type: "mixed", tag: "mixed-host-local", listen: client.listenHost, listen_port: client.listenPort }, @@ -741,6 +816,7 @@ ${remoteStatusProbeShell(target)} function remoteStatusProbeShell(target: HostProxyTarget): string { const client = target.source.client; + if (client.mode === "external-existing-hysteria") return remoteExistingHysteriaStatusProbeShell(target, client); const podAccess = client.podAccess; const podAccessShell = podAccess?.enabled ? ` @@ -811,6 +887,101 @@ PY `; } +function remoteExistingHysteriaStatusProbeShell(target: HostProxyTarget, client: HostProxyExistingHysteriaClient): string { + return ` +set -eu +config_present=missing +[ -f ${shQuote(client.configPath)} ] && config_present=present +service_active="$(systemctl is-active ${shQuote(client.serviceName)} 2>/dev/null || true)" +http_rc=0 +http_metrics="$(curl -sS -o /dev/null -w '%{http_code} %{time_connect} %{time_pretransfer} %{time_starttransfer} %{time_total}' --max-time 20 -x ${shQuote(target.env.httpProxy)} ${shQuote(target.source.externalProbeUrl)} 2>/tmp/unidesk-host-proxy-hysteria-http.err)" || http_rc=$? +socks_rc=0 +socks_metrics="$(curl -sS -o /dev/null -w '%{http_code} %{time_connect} %{time_pretransfer} %{time_starttransfer} %{time_total}' --max-time 20 --socks5-hostname ${shQuote(`${client.socks5ListenHost}:${client.socks5ListenPort}`)} ${shQuote(target.source.externalProbeUrl)} 2>/tmp/unidesk-host-proxy-hysteria-socks.err)" || socks_rc=$? +python3 - "$config_present" "$service_active" "$http_rc" "$http_metrics" "$socks_rc" "$socks_metrics" <<'PY' +import json +import re +import sys +from pathlib import Path + +config_path = "${client.configPath}" +expected_server = "${target.source.expectedServer ?? ""}" +expected_http = "${client.httpListenHost}:${client.httpListenPort}" +expected_socks = "${client.socks5ListenHost}:${client.socks5ListenPort}" + +def parse_metrics(raw): + parts = str(raw or "").split() + if len(parts) != 5: + return {"status": "", "timeConnect": "", "timePretransfer": "", "timeStarttransfer": "", "timeTotal": ""} + return { + "status": parts[0], + "timeConnect": parts[1], + "timePretransfer": parts[2], + "timeStarttransfer": parts[3], + "timeTotal": parts[4], + } + +def section_listen(text, section): + match = re.search(r"(?ms)^" + re.escape(section) + r":\\s*\\n(?:[ \\t]+.*\\n)*?[ \\t]+listen:\\s*([^\\s#]+)", text) + return match.group(1).strip() if match else "" + +text = Path(config_path).read_text(encoding="utf-8") if Path(config_path).exists() else "" +server_match = re.search(r"(?m)^server:\\s*([^\\s#]+)", text) +server = server_match.group(1).strip() if server_match else "" +server_host_port = re.sub(r"^[^@]*@", "", server) +http_listen = section_listen(text, "http") +socks_listen = section_listen(text, "socks5") +http = parse_metrics(sys.argv[4]) +socks = parse_metrics(sys.argv[6]) +payload = { + "ok": ( + sys.argv[1] == "present" + and sys.argv[2] == "active" + and server_host_port == expected_server + and http_listen == expected_http + and socks_listen == expected_socks + and int(sys.argv[3]) == 0 + and http["status"] == "204" + and int(sys.argv[5]) == 0 + and socks["status"] == "204" + ), + "target": "${target.id}", + "route": "${target.route}", + "sourceRef": "${target.sourceRef}", + "sourceFingerprint": "${target.source.fingerprint}", + "client": { + "mode": "${client.mode}", + "service": "${client.serviceName}", + "serviceActive": sys.argv[2], + "configPath": config_path, + "configPresent": sys.argv[1], + "configMatches": server_host_port == expected_server and http_listen == expected_http and socks_listen == expected_socks, + "server": server_host_port, + "expectedServer": expected_server, + "httpListen": http_listen, + "socks5Listen": socks_listen, + "listen": expected_http, + "podAccess": {"enabled": False, "listen": "", "proxyUrl": ""}, + }, + "files": { + "envFile": "status-only", + "profile": "status-only", + "apt": "status-only", + "dockerSystemdDropIn": "status-only", + "k3sSystemdDropIn": "status-only", + }, + "noProxy": {"hyueapi": True, "wildcardHyueapi": True}, + "health": {"exitCode": int(sys.argv[3]), "url": "${target.source.externalProbeUrl}", "mode": "http-proxy"}, + "externalProbe": {"exitCode": int(sys.argv[3]), **http, "url": "${target.source.externalProbeUrl}", "proxyUrl": "${target.env.httpProxy}"}, + "socks5Probe": {"exitCode": int(sys.argv[5]), **socks, "url": "${target.source.externalProbeUrl}", "proxyUrl": "socks5h://${client.socks5ListenHost}:${client.socks5ListenPort}"}, + "podAccessProbe": {"enabled": False, "exitCode": 0, "status": "skipped", "url": "${target.source.externalProbeUrl}"}, + "valuesPrinted": False, +} +print(json.dumps(payload, ensure_ascii=False, indent=2)) +sys.exit(0 if payload["ok"] else 1) +PY +`; +} + function renderClientUnit(client: HostProxyClient): string { return [ "[Unit]", @@ -900,15 +1071,7 @@ function targetSummary(target: HostProxyTarget): Record { proxyUrl: target.source.proxyUrl, benchmarkRef: target.source.benchmarkRef, implementationRef: target.source.implementationRef, - client: { - mode: client.mode, - version: client.version, - installPath: client.installPath, - configPath: client.configPath, - unitPath: client.unitPath, - serviceName: client.serviceName, - listen: `${client.listenHost}:${client.listenPort}`, - }, + client: clientSummary(client), trans: { hostProxyEnv: transHostProxyEnvSummary(target.id), }, @@ -916,6 +1079,19 @@ function targetSummary(target: HostProxyTarget): Record { } function artifactSummary(client: HostProxyClient): Record { + if (client.mode === "external-existing-hysteria") { + return { + mode: client.mode, + version: "existing", + upstreamUrl: "external", + archiveCachePath: "external", + binaryCachePath: "external", + archiveSha256: "external", + archiveInstallPath: "external", + binarySha256: "external", + installPath: client.configPath, + }; + } return { mode: client.mode, version: client.version, @@ -929,6 +1105,30 @@ function artifactSummary(client: HostProxyClient): Record { }; } +function clientSummary(client: HostProxyClient): Record { + if (client.mode === "external-existing-hysteria") { + return { + mode: client.mode, + version: "existing", + installPath: "external", + configPath: client.configPath, + unitPath: "external", + serviceName: client.serviceName, + listen: `${client.httpListenHost}:${client.httpListenPort}`, + socks5Listen: `${client.socks5ListenHost}:${client.socks5ListenPort}`, + }; + } + return { + mode: client.mode, + version: client.version, + installPath: client.installPath, + configPath: client.configPath, + unitPath: client.unitPath, + serviceName: client.serviceName, + listen: `${client.listenHost}:${client.listenPort}`, + }; +} + function masterProxyTcpHealth(server: HostProxyServer): Record { const result = runCommand(["bash", "-lc", `timeout 5 bash -lc ${shQuote(``); + } + return text; +} + export function validateManualBindingSourceId(value: string, key: string): void { if (!/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported source id format`); } diff --git a/scripts/src/platform-infra-sub2api-codex/public-exposure.ts b/scripts/src/platform-infra-sub2api-codex/public-exposure.ts index 377021a1..22257031 100644 --- a/scripts/src/platform-infra-sub2api-codex/public-exposure.ts +++ b/scripts/src/platform-infra-sub2api-codex/public-exposure.ts @@ -169,6 +169,7 @@ export function manualBindingSourcePlan(source: CodexPoolManualBindingSource): R kind: source.kind, provider: source.provider, description: source.description, + hostProxyRef: source.hostProxyRef, fixedProxy: source.fixedProxy, }; } diff --git a/scripts/src/platform-infra-sub2api-codex/types.ts b/scripts/src/platform-infra-sub2api-codex/types.ts index f62e868e..9f401e03 100644 --- a/scripts/src/platform-infra-sub2api-codex/types.ts +++ b/scripts/src/platform-infra-sub2api-codex/types.ts @@ -209,6 +209,7 @@ export interface CodexPoolManualBindingSource { kind: CodexPoolManualBindingKind; provider: CodexPoolManualBindingProvider; description: string | null; + hostProxyRef: string | null; fixedProxy: CodexPoolManualFixedProxyConfig | null; }