- 新测试 cloud/tests/test_chv_muse_p3.py 30 项:替身过期、真 token 泄露面、SSRF 挡截、deny/ask 路径、authd-seed 拆分、wake 预审、 客内 authd 守护进程、git credential helper 端到端(真 git fill) - 8 个旧测试按新 authd 行为改写(未删测试、未放宽断言): GITEA_TOKEN 改走 authd-seed,build_ssh_command 不再 source 进 codex 环境;wake 测试补 Sentinel 白名单 + 假解析器 - 7 处超 50 行函数拆到 50 行内(不改行为);补 authd/sentinel 公共 API 注解,mypy 在改动文件上干净 - 真问题修复:git 默认不把 path 发给 helper,chv-sync boot 加 credential.useHttpPath=true,否则 authd 路径每次都拒绝 - 全量 cloud 套件 1897 项 OK(skipped=8) 真机待验证(未做,等第三棒让出 WSL 测试机):见 docs/chv-muse-p3-gap.md §3.6 清单
147 lines
6.0 KiB
Python
147 lines
6.0 KiB
Python
"""Gitea 同步变量供给(CHV guest-env 集成,叶子模块)。
|
||
|
||
背景:客内 chv-sync boot 在 cloud-init 时运行、SSH 之前,
|
||
只能读 seed 投递的 /etc/chv/guest-env,看不到 SSH env 前缀。
|
||
因此同步所需的 6 个变量必须随 seed 进 guest-env,不能只靠
|
||
extra_env 的 SSH 前缀;其中 GITEA_TOKEN 是密钥,只进 seed,
|
||
绝不进 argv/日志/镜像。
|
||
|
||
变量与来源(优先级低→高,后者覆盖前者):
|
||
1. 宿主配置环境变量(主来源,生产口径):CHV_GITEA_URL、
|
||
CHV_GITEA_USER、CHV_GITEA_TOKEN、CHV_SYNC_REPO、
|
||
CHV_SYNC_BRANCH、CHV_GITEA_EMAIL。由部署该 worker/执行器
|
||
的运维在宿主环境里设置,与 CHV_SSH_KEY、CHV_EGRESS_PROXY
|
||
同款口径;CHV 是单租户单 VM 模型,一台宿主对应一个
|
||
Gitea 身份。密钥来源绝不硬编码进代码。
|
||
2. 任务级覆盖(内存态):任务 dict 的 `gitea_sync` 子 dict,
|
||
键即 6 个变量名。仅在调用方以内存态构造任务 dict 时生效
|
||
(直接调 wake_vm、测试、手工驱动);tasks 表目前没有
|
||
JSON/元数据列,DB 往返会丢掉它——这是已知边界,不做
|
||
静默声称,需要每任务不同仓库时再做任务表迁移。
|
||
3. 显式 extra_env:调用方直接传给 wake_vm/dispatch 的
|
||
extra_env 里的同名键,优先级最高。
|
||
|
||
缺失语义(fail-closed,不跳过同步):
|
||
- 6 个键一个都没有 → 本次启动不同步(chv-sync boot 见无
|
||
guest-env 静默成功,VM 照常用)。
|
||
- 任一键出现、但必填 4 键(GITEA_URL、GITEA_USER、
|
||
GITEA_TOKEN、SYNC_REPO)缺失 → 校验失败,wake 在起 VMM
|
||
之前直接拒绝;半份同步配置绝不能让 VM 带着“我以为同步
|
||
了”的状态启动。SYNC_BRANCH、GITEA_EMAIL 可选。
|
||
- 校验错误只含缺失的键名,绝不含任何变量值。
|
||
|
||
本模块是纯函数,不读文件、不跑 VM、不含任何密钥值。
|
||
"""
|
||
|
||
# 同步变量全集(guest-env 里应有的键)。
|
||
GITEA_SYNC_KEYS = frozenset({
|
||
"GITEA_URL", "GITEA_USER", "GITEA_TOKEN",
|
||
"SYNC_REPO", "SYNC_BRANCH", "GITEA_EMAIL",
|
||
})
|
||
|
||
# 其中密钥:只进 seed,绝不进 argv(见 ssh_transport 的
|
||
# _CHV_SECRET_ENV_NAMES)。
|
||
GITEA_SYNC_SECRET_KEYS = frozenset({"GITEA_TOKEN"})
|
||
|
||
# 其中 authd 专用密钥(feat-chv-muse-p3):只进客内 authd 的
|
||
# 独立 seed 文件 /etc/chv/authd-seed(见 hostkeys.AUTHD_SEED_PATH),
|
||
# 不进 guest-env、不进 codex 进程环境。本期只有 GITEA_TOKEN。
|
||
AUTHD_SECRET_KEYS = frozenset({"GITEA_TOKEN"})
|
||
|
||
|
||
def split_authd_env(secrets: dict | None) -> tuple[dict, dict]:
|
||
"""把密钥集拆成(走 authd 通道的子集,仍走 guest-env 的子集)。
|
||
|
||
authd 子集 = AUTHD_SECRET_KEYS 命中的键(本期只有 GITEA_TOKEN);
|
||
其余密钥(如 SUB2API_API_KEY)仍走 guest-env(见差距表 §3.3,
|
||
网关 key 的 privsep 改造是后续工作)。"""
|
||
authd, rest = {}, {}
|
||
for key, value in dict(secrets or {}).items():
|
||
if key in AUTHD_SECRET_KEYS:
|
||
authd[key] = value
|
||
else:
|
||
rest[key] = value
|
||
return authd, rest
|
||
|
||
# 其中非密钥、但 boot 时就要用、也必须进 guest-env 的键
|
||
#(SSH env 前缀到不了 cloud-init 时运行的 chv-sync boot)。
|
||
GITEA_SYNC_PLAIN_KEYS = GITEA_SYNC_KEYS - GITEA_SYNC_SECRET_KEYS
|
||
|
||
# 必填键:缺一即 fail-closed(见模块头缺失语义)。
|
||
GITEA_SYNC_REQUIRED_KEYS = frozenset({
|
||
"GITEA_URL", "GITEA_USER", "GITEA_TOKEN", "SYNC_REPO",
|
||
})
|
||
|
||
# 宿主配置环境变量名 → guest-env 键名。
|
||
CHV_GITEA_ENV_MAP = {
|
||
"CHV_GITEA_URL": "GITEA_URL",
|
||
"CHV_GITEA_USER": "GITEA_USER",
|
||
"CHV_GITEA_TOKEN": "GITEA_TOKEN",
|
||
"CHV_SYNC_REPO": "SYNC_REPO",
|
||
"CHV_SYNC_BRANCH": "SYNC_BRANCH",
|
||
"CHV_GITEA_EMAIL": "GITEA_EMAIL",
|
||
}
|
||
|
||
|
||
def _clean(mapping: dict | None) -> dict:
|
||
"""只留白名单键:值须为非空字符串(strip 后),其它
|
||
类型/空值/未知键一律丢掉——调用方拼错的键名绝不静默
|
||
进 seed 变成客内无人读的脏变量。"""
|
||
cleaned = {}
|
||
for key, value in dict(mapping or {}).items():
|
||
if key not in GITEA_SYNC_KEYS:
|
||
continue
|
||
if not isinstance(value, str):
|
||
continue
|
||
value = value.strip()
|
||
if not value:
|
||
continue
|
||
cleaned[key] = value
|
||
return cleaned
|
||
|
||
|
||
def config_gitea_sync_env(env: dict | None) -> dict:
|
||
"""宿主配置环境变量(CHV_GITEA_*)→ guest-env 键名。"""
|
||
mapped = {}
|
||
for env_name, key in CHV_GITEA_ENV_MAP.items():
|
||
value = (env or {}).get(env_name)
|
||
if value is not None:
|
||
mapped[key] = value
|
||
return _clean(mapped)
|
||
|
||
|
||
def task_gitea_sync_env(task: dict | None) -> dict:
|
||
"""任务 dict 的 `gitea_sync` 子 dict → 白名单后的键值。
|
||
不是 dict(或缺失)即空——绝不把任务其它字段误读成
|
||
同步变量。"""
|
||
if not isinstance(task, dict):
|
||
return {}
|
||
return _clean(task.get("gitea_sync"))
|
||
|
||
|
||
def extract_gitea_sync_env(task: dict | None = None,
|
||
extra_env: dict | None = None,
|
||
config_sync: dict | None = None) -> dict:
|
||
"""三源合并(优先级:config_sync < 任务 gitea_sync <
|
||
extra_env 同名键)。返回白名单后的 dict,可能为空
|
||
(空 = 本次启动不同步)。"""
|
||
merged = {}
|
||
merged.update(_clean(config_sync))
|
||
merged.update(task_gitea_sync_env(task))
|
||
merged.update(_clean(extra_env))
|
||
return merged
|
||
|
||
|
||
def validate_gitea_sync_env(env: dict | None) -> None:
|
||
"""校验同步变量完整性:空即不同步(通过);非空但必填
|
||
键缺失即 ValueError(只含缺失键名,不含值)。调用方
|
||
(执行器 wake_vm)在起 VMM 前调本函数做 fail-closed。"""
|
||
env = dict(env or {})
|
||
if not env:
|
||
return
|
||
missing = sorted(GITEA_SYNC_REQUIRED_KEYS - set(env))
|
||
if missing:
|
||
raise ValueError(
|
||
"Gitea 同步变量不完整,缺失:%s(同步已请求但必填"
|
||
"键不全,拒绝启动)" % ",".join(missing))
|