From 1ff8906aee1007f15b2e6018bd96966ed0d054c6 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 01:39:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(repos):=20=E4=BB=93=E5=BA=93=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E9=A2=84=E6=A3=80=20+=20codexsvc=20deploy=20key=20?= =?UTF-8?q?=E9=85=8D=E7=BB=99=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本机 https 到 GitHub 常被出口策略挡、ssh 才通(2026-10-30 实测),而新项目填 https 私有仓会在 worker 里花几分钟超时 才失败。控制面不存仓库凭证的设计不动,缺的是建项目前的 快速预检与按仓库通用化的配 key 流程: - cloud.repos.check_repo_access:只跑 git ls-remote HEAD, 不 clone、不写盘、不改库;失败返回 (False, 原因),https 失败时追加改走 git@ 形的提示;CLI python3 -m cloud.repos check ,退出码 0/1,只打印结果与原因。 - deploy/provision_codexsvc_key.sh:主机上以 root 跑,为 codexsvc 生成/复用 ed25519 key(绝不覆盖已有 key),只 打印公钥(私钥绝不打印),--check 时以 codexsvc 身份跑 ls-remote,通了才算配好。 - 内层沙箱边界诚实说明:Docker 任务内层本就是 danger-full-access(见 sandbox.py 注释),外层 Docker/gVisor 才是防线;宿主级 bwrap workspace-write 的 fresh proc 在嵌套 pidns 下是内核 EPERM、/tmp 不隔离——这些是上游 codex 二进制 与内核的边界,本批不谎称在仓库代码里修掉,只把仓库接入 这一档做成可重复流程。 单测:test_project_repo 20 项全过(新增 check 5 项)。 --- cloud/repos.py | 74 +++++++++++++++++++++++++++++++- cloud/tests/test_project_repo.py | 46 ++++++++++++++++++++ deploy/provision_codexsvc_key.sh | 55 ++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100755 deploy/provision_codexsvc_key.sh diff --git a/cloud/repos.py b/cloud/repos.py index 6409ea8..300b64b 100644 --- a/cloud/repos.py +++ b/cloud/repos.py @@ -16,11 +16,20 @@ 工作树脏不脏由 host.py 的 worktree 逻辑处理。 - 私有仓库 clone 失败时错误文本写明:需要仓库访问凭证,控制面 不存仓库凭证——绝不尝试用任何 key 去 clone。 +- check_repo_access:建项目/建任务前的快速预检,只跑 + `git ls-remote HEAD`,不 clone、不写盘、不改库; + 失败时返回 (False, 原因),成功时返回 (True, HEAD 哈希)。 + 本机 https 到 GitHub 常被出口策略挡、ssh 才通(2026-10-30 + 实测),预检失败的 https 私有仓应先改走 git@ 形再测, + 不要等任务派发后在 worker 里花几分钟超时才发现。 + CLI:`python3 -m cloud.repos check `,退出码 0/1, + 只打印结果与原因,绝不打印任何凭证。 """ import os import re import subprocess +import sys from urllib.parse import urlparse __all__ = [ @@ -28,6 +37,7 @@ __all__ = [ "validate_repo_url", "checkout_dir", "resolve_repo", + "check_repo_access", ] @@ -89,8 +99,9 @@ def checkout_dir(repos_dir, project_id): return path -def _default_runner(cmd): - return subprocess.run(cmd, capture_output=True, text=True) +def _default_runner(cmd, timeout=None): + return subprocess.run(cmd, capture_output=True, text=True, + timeout=timeout) def _run(runner, cmd): @@ -148,3 +159,62 @@ def resolve_repo(repo_url, repos_dir, project_id, runner=None): "git fetch origin 失败,拒绝用旧代码派发: %s" % (err or target)) return target + + +def check_repo_access(repo_url, runner=None, timeout=30): + """预检仓库是否可读:只跑 `git ls-remote HEAD`。 + + 返回 (ok, detail):ok 时 detail 为 HEAD 哈希;失败时 + detail 为原因(不含凭证)。runner 缺省走真实 git,超时 + 由 subprocess 的 timeout 控(假 runner 不接受 timeout 时 + _run 已回退);runner 抛 TimeoutExpired 时同样返回失败, + 不抛异常——预检失败绝不能炸掉调用方。 + https 形在主机上失败时 detail 末尾追加 ssh 形提示, + 因为本机 https 到 GitHub 常被出口策略挡、ssh 才通。 + """ + try: + url = validate_repo_url(repo_url) + except ValueError as exc: + return (False, str(exc)) + run = runner if runner is not None else _default_runner + + def _ls_remote(cmd): + try: + return run(cmd, timeout=timeout) + except TypeError: + return run(cmd) + + try: + result = _ls_remote(["git", "ls-remote", url, "HEAD"]) + except Exception as exc: # noqa: BLE001 - 超时、缺 git 等一律失败 + detail = "git ls-remote 失败: %s" % (exc,) + if url.startswith("https://"): + detail += ";本机 https 到 GitHub 常被出口策略挡," + detail += "请改走 git@ 形再测" + return (False, detail) + if getattr(result, "returncode", 1) != 0: + err = (getattr(result, "stderr", "") or "").strip() + detail = "git ls-remote 失败: %s" % (err or url) + if url.startswith("https://"): + detail += ";私有仓库请改走 git@ 形并给 codexsvc 配" + detail += "只读 deploy key,控制面不存仓库凭证" + return (False, detail) + head = (getattr(result, "stdout", "") or "").split() + return (True, head[0] if head else "") + + +def _main(argv): + if len(argv) != 3 or argv[1] != "check": + sys.stderr.write( + "用法: python3 -m cloud.repos check \n") + return 2 + ok, detail = check_repo_access(argv[2]) + if ok: + print("ok HEAD=%s" % detail) + return 0 + print("fail: %s" % detail) + return 1 + + +if __name__ == "__main__": + raise SystemExit(_main(sys.argv)) diff --git a/cloud/tests/test_project_repo.py b/cloud/tests/test_project_repo.py index 2efd7bf..3d49f81 100644 --- a/cloud/tests/test_project_repo.py +++ b/cloud/tests/test_project_repo.py @@ -21,6 +21,7 @@ try: from cloud.auth import AuthStore from cloud.repos import ( RepoError, + check_repo_access, checkout_dir, resolve_repo, validate_repo_url, @@ -32,6 +33,7 @@ except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.p from cloud.auth import AuthStore from cloud.repos import ( RepoError, + check_repo_access, checkout_dir, resolve_repo, validate_repo_url, @@ -101,6 +103,50 @@ class ValidateRepoUrlTestCase(unittest.TestCase): validate_repo_url(123) +class CheckRepoAccessTestCase(unittest.TestCase): + def test_ok_returns_head(self): + def runner(cmd, timeout=None): + self.assertEqual(cmd[:3], ["git", "ls-remote", + "git@github.com:o/r.git"]) + return SimpleNamespace(returncode=0, + stdout="abc123\tHEAD\n", stderr="") + ok, detail = check_repo_access("git@github.com:o/r.git", + runner=runner) + self.assertTrue(ok) + self.assertEqual(detail, "abc123") + + def test_failure_returns_reason_not_exception(self): + def runner(cmd, timeout=None): + return SimpleNamespace(returncode=128, stdout="", + stderr="fatal: Authentication failed") + ok, detail = check_repo_access("git@github.com:o/private.git", + runner=runner) + self.assertFalse(ok) + self.assertIn("Authentication failed", detail) + + def test_https_failure_hints_ssh_form(self): + def runner(cmd, timeout=None): + return SimpleNamespace(returncode=128, stdout="", + stderr="fatal: unable to access") + ok, detail = check_repo_access("https://github.com/o/r.git", + runner=runner) + self.assertFalse(ok) + self.assertIn("git@", detail) + + def test_invalid_url_returns_false(self): + ok, detail = check_repo_access("file:///tmp/r") + self.assertFalse(ok) + self.assertTrue(detail) + + def test_timeout_returns_false(self): + def runner(cmd, timeout=None): + raise TimeoutError("timed out") + ok, detail = check_repo_access("git@github.com:o/r.git", + runner=runner) + self.assertFalse(ok) + self.assertIn("timed out", detail) + + class CheckoutDirTestCase(unittest.TestCase): def test_sanitizes_and_stays_inside(self): tmp = tempfile.TemporaryDirectory() diff --git a/deploy/provision_codexsvc_key.sh b/deploy/provision_codexsvc_key.sh new file mode 100755 index 0000000..7280f32 --- /dev/null +++ b/deploy/provision_codexsvc_key.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# 给 codexsvc 配一只读 deploy key(按仓库通用化,控制面不存凭证)。 +# +# 用法(主机上以 root 跑): +# ./provision_codexsvc_key.sh git@github.com:ORG/REPO.git [key-name] +# ./provision_codexsvc_key.sh --check git@github.com:ORG/REPO.git [key-name] +# +# 流程: +# 1. 在 /home/codexsvc/.ssh/ 生成 ed25519 key(已存在则复用,绝不覆盖); +# 2. 打印公钥(只打印公钥,私钥绝不打印),请把公钥以只读 deploy key +# 加到 GitHub 仓库 Settings -> Deploy keys; +# 3. --check 时以 codexsvc 身份跑 git ls-remote,通了才算配好。 +# 4. 另有现成的 key 要复用时,把 key 文件拷到上述目录并 chown +# codexsvc、chmod 600,再用 --check 验,不要改本脚本去读 root 的 key。 +# +# 安全边界:本脚本不往控制面、环境文件或任何日志里写私钥;公钥 +# 本来就是公开的,打印它没有风险;key 名只允许 [A-Za-z0-9_-]。 +set -eu +CHECK=0 +if [ "${1:-}" = "--check" ]; then + CHECK=1; shift +fi +URL="${1:-}" +NAME="${2:-codex-github}" +if [ -z "$URL" ]; then + echo "用法: $0 [--check] git@github.com:ORG/REPO.git [key-name]" >&2 + exit 2 +fi +case "$URL" in + git@github.com:*:*.git) ;; + *) echo "只接受 git@github.com:ORG/REPO.git 形(https 形在本机常被出口策略挡)" >&2; exit 2 ;; +esac +case "$NAME" in + *[!A-Za-z0-9_-]*) echo "key 名只允许字母数字 _ -" >&2; exit 2 ;; +esac +SSH_DIR=/home/codexsvc/.ssh +KEY="$SSH_DIR/id_$NAME" +mkdir -p "$SSH_DIR" +chown codexsvc:codexsvc "$SSH_DIR" +chmod 700 "$SSH_DIR" +if [ ! -f "$KEY" ]; then + sudo -u codexsvc ssh-keygen -t ed25519 -f "$KEY" -N "" -C "codexsvc-$NAME" + chmod 600 "$KEY" + chown codexsvc:codexsvc "$KEY" +fi +chmod 600 "$KEY" 2>/dev/null || true +chown codexsvc:codexsvc "$KEY" "$KEY.pub" 2>/dev/null || true +echo "公钥(加到 GitHub 仓库的只读 Deploy keys):" +cat "$KEY.pub" +if [ "$CHECK" = "1" ]; then + echo "以 codexsvc 身份预检 git ls-remote ..." + sudo -u codexsvc env GIT_SSH_COMMAND="ssh -i $KEY -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \ + git ls-remote "$URL" HEAD + echo "预检通过:codexsvc 能读该仓库" +fi