Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
179a86d655 | ||
|
|
581e850eb7 | ||
|
|
04287f6053 | ||
|
|
c1d00b3620 | ||
|
|
4a7ca25334 | ||
|
|
f093558c0d | ||
|
|
9f593d62ef | ||
|
|
2d12179b9a | ||
|
|
b41fb67026 | ||
|
|
e02828c590 | ||
|
|
d324d87529 |
No files matched your search
+67
-13
@@ -14,7 +14,10 @@ socket 与 seed 文件)。它只做三件事:
|
|||||||
GITEA_TOKEN 持在内存里(cloud.chv.authd.CredentialVault),
|
GITEA_TOKEN 持在内存里(cloud.chv.authd.CredentialVault),
|
||||||
除此之外不落盘、不进日志;
|
除此之外不落盘、不进日志;
|
||||||
2. 在 Unix socket /run/chv-authd/authd.sock 上接受 git credential
|
2. 在 Unix socket /run/chv-authd/authd.sock 上接受 git credential
|
||||||
helper 的请求(SO_PEERCRED 只放行 root 与 chv 用户);
|
helper 的请求(SO_PEERCRED 只放行 root 与 chv 用户;socket
|
||||||
|
用 group 权限——目录 0750、socket 0660、组为第一个非 root
|
||||||
|
放行 uid 的主组,round-11 真机修复层 3,探针用的 0666
|
||||||
|
不上生产);
|
||||||
3. 每次放凭证前先过 Sentinel 评估(cloud.chv.sentinel,主机/
|
3. 每次放凭证前先过 Sentinel 评估(cloud.chv.sentinel,主机/
|
||||||
方法/路径/解析后 IP/SSRF),再按次释放(单次、300 秒 TTL),
|
方法/路径/解析后 IP/SSRF),再按次释放(单次、300 秒 TTL),
|
||||||
全程审计(审计里无值)。
|
全程审计(审计里无值)。
|
||||||
@@ -51,6 +54,46 @@ SEED_KEYS = frozenset({
|
|||||||
DEFAULT_ALLOWED_UIDS = (0, 1000)
|
DEFAULT_ALLOWED_UIDS = (0, 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def _socket_group_gid(allowed_uids, socket_group=None):
|
||||||
|
"""socket 共享组的 gid:socket_group 显式给时优先(int 即 gid,
|
||||||
|
str 即组名);否则取第一个非 root 放行 uid 的主组。解析不出
|
||||||
|
(uid 无 passwd 条目、组名不存在)即 None——调用方回落
|
||||||
|
root-only 权限(目录 0700、socket 0600),绝不把 socket 放给
|
||||||
|
全员可读(round-11 真机修复层 3:探针用 0666,生产用 group
|
||||||
|
权限——chv 在组内,目录 0750 + socket 0660 + chgrp)。"""
|
||||||
|
import grp
|
||||||
|
import pwd
|
||||||
|
if socket_group is not None:
|
||||||
|
if isinstance(socket_group, int):
|
||||||
|
return socket_group
|
||||||
|
try:
|
||||||
|
return grp.getgrnam(socket_group).gr_gid
|
||||||
|
except (KeyError, OverflowError):
|
||||||
|
return None
|
||||||
|
for uid in sorted(allowed_uids or ()):
|
||||||
|
if uid == 0:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return pwd.getpwuid(uid).pw_gid
|
||||||
|
except (KeyError, OverflowError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_group_perms(path, gid, mode):
|
||||||
|
"""目录/socket 的组权限:先 chgrp(失败只放过,不中断启动),
|
||||||
|
再 chmod。gid 为 None 时只 chmod(root-only 模式)。"""
|
||||||
|
if gid is not None:
|
||||||
|
try:
|
||||||
|
os.chown(path, -1, gid)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.chmod(path, mode)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _load_seed(path):
|
def _load_seed(path):
|
||||||
"""读 seed 文件(KEY=VALUE,每行一项)→ 白名单后的 dict。
|
"""读 seed 文件(KEY=VALUE,每行一项)→ 白名单后的 dict。
|
||||||
文件不存在即 FileNotFoundError(调用方 fail-fast,绝不带着
|
文件不存在即 FileNotFoundError(调用方 fail-fast,绝不带着
|
||||||
@@ -87,13 +130,18 @@ def _operation_for_path(path):
|
|||||||
推断不到的一律拒绝):
|
推断不到的一律拒绝):
|
||||||
- .../git-receive-pack → push(写)
|
- .../git-receive-pack → push(写)
|
||||||
- .../git-upload-pack → pull(读)
|
- .../git-upload-pack → pull(读)
|
||||||
- .../info/refs → pull(读,clone 也走这条)"""
|
- .../info/refs → pull(读,clone 也走这条)
|
||||||
|
- 裸 .../ → pull(读;round-11 真机修复层 4:git 智能
|
||||||
|
HTTP 的发现请求只发裸 .git 路径,裸路径一律当 pull——
|
||||||
|
只读默认;push 只走显式的 git-receive-pack)"""
|
||||||
if path.endswith(".git/git-receive-pack"):
|
if path.endswith(".git/git-receive-pack"):
|
||||||
return "POST", "push"
|
return "POST", "push"
|
||||||
if path.endswith(".git/git-upload-pack"):
|
if path.endswith(".git/git-upload-pack"):
|
||||||
return "POST", "pull"
|
return "POST", "pull"
|
||||||
if path.endswith(".git/info/refs"):
|
if path.endswith(".git/info/refs"):
|
||||||
return "GET", "pull"
|
return "GET", "pull"
|
||||||
|
if path.endswith(".git"):
|
||||||
|
return "GET", "pull"
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@@ -227,7 +275,8 @@ class AuthdServer(socketserver.ThreadingUnixStreamServer):
|
|||||||
allow_reuse_address = False
|
allow_reuse_address = False
|
||||||
|
|
||||||
def __init__(self, socket_path, vault, policy, gitea_user,
|
def __init__(self, socket_path, vault, policy, gitea_user,
|
||||||
allowed_uids, log_path, resolve=None):
|
allowed_uids, log_path, resolve=None,
|
||||||
|
socket_group=None):
|
||||||
self.vault = vault
|
self.vault = vault
|
||||||
self.policy = policy
|
self.policy = policy
|
||||||
self.gitea_user = gitea_user
|
self.gitea_user = gitea_user
|
||||||
@@ -236,20 +285,24 @@ class AuthdServer(socketserver.ThreadingUnixStreamServer):
|
|||||||
# 解析器可注入(测试用假解析器;不给时 Sentinel 用真实
|
# 解析器可注入(测试用假解析器;不给时 Sentinel 用真实
|
||||||
# getaddrinfo)。与执行器的 sentinel_resolver 同款口径。
|
# getaddrinfo)。与执行器的 sentinel_resolver 同款口径。
|
||||||
self.resolve = resolve
|
self.resolve = resolve
|
||||||
|
# round-11 真机修复(层 3):socket 用 group 权限。
|
||||||
|
# gid 取 socket_group 显式值或第一个非 root 放行 uid 的
|
||||||
|
# 主组;解析不出即 None,回落 root-only(0700/0600)。
|
||||||
|
self.socket_gid = _socket_group_gid(allowed_uids, socket_group)
|
||||||
directory = os.path.dirname(socket_path)
|
directory = os.path.dirname(socket_path)
|
||||||
if directory:
|
if directory:
|
||||||
os.makedirs(directory, exist_ok=True)
|
os.makedirs(directory, exist_ok=True)
|
||||||
try:
|
if self.socket_gid is not None:
|
||||||
os.chmod(directory, 0o700)
|
_set_group_perms(directory, self.socket_gid, 0o750)
|
||||||
except OSError:
|
else:
|
||||||
pass
|
_set_group_perms(directory, None, 0o700)
|
||||||
if os.path.exists(socket_path):
|
if os.path.exists(socket_path):
|
||||||
os.unlink(socket_path)
|
os.unlink(socket_path)
|
||||||
super().__init__(socket_path, AuthdHandler)
|
super().__init__(socket_path, AuthdHandler)
|
||||||
try:
|
if self.socket_gid is not None:
|
||||||
os.chmod(socket_path, 0o600)
|
_set_group_perms(socket_path, self.socket_gid, 0o660)
|
||||||
except OSError:
|
else:
|
||||||
pass
|
_set_group_perms(socket_path, None, 0o600)
|
||||||
|
|
||||||
def audit(self, record):
|
def audit(self, record):
|
||||||
_audit(self.log_path, record)
|
_audit(self.log_path, record)
|
||||||
@@ -260,7 +313,8 @@ def build_server(seed_path=DEFAULT_SEED_PATH,
|
|||||||
log_path=DEFAULT_LOG_PATH,
|
log_path=DEFAULT_LOG_PATH,
|
||||||
lib_dir=DEFAULT_LIB_DIR,
|
lib_dir=DEFAULT_LIB_DIR,
|
||||||
allowed_uids=None,
|
allowed_uids=None,
|
||||||
resolve=None):
|
resolve=None,
|
||||||
|
socket_group=None):
|
||||||
"""建服务(不启动):读 seed → 入库 → 建策略 → 建 socket 服务。
|
"""建服务(不启动):读 seed → 入库 → 建策略 → 建 socket 服务。
|
||||||
|
|
||||||
seed 缺必填键(GITEA_URL/GITEA_USER/GITEA_TOKEN/SYNC_REPO)
|
seed 缺必填键(GITEA_URL/GITEA_USER/GITEA_TOKEN/SYNC_REPO)
|
||||||
@@ -295,7 +349,7 @@ def build_server(seed_path=DEFAULT_SEED_PATH,
|
|||||||
if raw.strip() else list(DEFAULT_ALLOWED_UIDS)
|
if raw.strip() else list(DEFAULT_ALLOWED_UIDS)
|
||||||
server = AuthdServer(socket_path, vault, policy,
|
server = AuthdServer(socket_path, vault, policy,
|
||||||
seed["GITEA_USER"], uids, log_path,
|
seed["GITEA_USER"], uids, log_path,
|
||||||
resolve=resolve)
|
resolve=resolve, socket_group=socket_group)
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""chv-authd 叶子树的 stub cloud/chv/__init__.py(round-11 真机修复,层 1)。
|
||||||
|
|
||||||
|
真实的 cloud/chv/__init__.py import executor/orphans(进而要整套
|
||||||
|
cloud 包),拷进 authd 的最小叶子树(/usr/local/lib/chv-authd)
|
||||||
|
会让 chv-authd 在 import 时 ModuleNotFoundError 而 crash loop:
|
||||||
|
先是 cloud.chv.errors 缺失,补上 errors.py 后又会在 executor
|
||||||
|
上失败。探针 fixprobe3 的形状:叶子树里只放 errors.py + 仅导出
|
||||||
|
ChvFailure 的 stub __init__.py——authd 只需要这个名字。
|
||||||
|
build.sh 拷本文件作叶子树的 cloud/chv/__init__.py,不拷真实的
|
||||||
|
包 __init__。
|
||||||
|
"""
|
||||||
|
from cloud.chv.errors import ChvFailure
|
||||||
|
|
||||||
|
__all__ = ["ChvFailure"]
|
||||||
+29
-13
@@ -19,9 +19,10 @@
|
|||||||
#
|
#
|
||||||
# Gitea 双向同步(用户工作区口径,2026-11-06 追加):镜像里加
|
# Gitea 双向同步(用户工作区口径,2026-11-06 追加):镜像里加
|
||||||
# git + gt 1.0.0(官方 install.sh + SHA256 校验,跟 170/172
|
# git + gt 1.0.0(官方 install.sh + SHA256 校验,跟 170/172
|
||||||
# 同款),配好 git user 与 credential helper 占位,不含真实
|
# 同款),配好 git user,不含真实 token,不设 credential helper
|
||||||
# token;客机启动时 /usr/local/bin/chv-sync boot 按 seed 盘
|
#(round-11 真机修复层 5:store helper 已去掉,authd helper 是
|
||||||
# /etc/chv/guest-env(0600)把用户选定的仓 clone 到
|
# 唯一凭证 helper,且自身绝不落盘);客机启动时 chv-sync boot
|
||||||
|
# 按 seed 盘 /etc/chv/guest-env(0600)把用户选定的仓 clone 到
|
||||||
# ~/workspace,后续 pull/push 由用户显式同步,分叉/冲突时
|
# ~/workspace,后续 pull/push 由用户显式同步,分叉/冲突时
|
||||||
# 失败关闭(不 --force、不自动 rebase)。token 绝不进镜像、
|
# 失败关闭(不 --force、不自动 rebase)。token 绝不进镜像、
|
||||||
# 代码、日志。
|
# 代码、日志。
|
||||||
@@ -188,14 +189,18 @@ done
|
|||||||
#(test_chv_residual.BuildScriptMinimalAppTest 钉住),缺失时只
|
#(test_chv_residual.BuildScriptMinimalAppTest 钉住),缺失时只
|
||||||
# 警告、不失败——缺叶子模块的镜像里 chv-authd 起不来,chv-sync
|
# 警告、不失败——缺叶子模块的镜像里 chv-authd 起不来,chv-sync
|
||||||
# 按 P3 差距表 §3.5 回落旧落盘路径并警告,绝不静默假装有 authd。
|
# 按 P3 差距表 §3.5 回落旧落盘路径并警告,绝不静默假装有 authd。
|
||||||
for p3_file in authd.py git-credential-chv-authd chv-authd.service; do
|
# round-11 真机修复(层 1):叶子树还须 errors.py + stub
|
||||||
|
# __init__(authd_init_stub.py)——真实的 cloud/chv/__init__.py
|
||||||
|
# import executor/orphans,拷进最小叶子树会让 chv-authd
|
||||||
|
# ModuleNotFoundError 而 crash loop(探针 fixprobe3 的形状)。
|
||||||
|
for p3_file in authd.py git-credential-chv-authd chv-authd.service authd_init_stub.py; do
|
||||||
if [ ! -f "$SCRIPT_DIR/$p3_file" ]; then
|
if [ ! -f "$SCRIPT_DIR/$p3_file" ]; then
|
||||||
echo "缺少 $p3_file(Muse P3 客机文件):$SCRIPT_DIR" >&2
|
echo "缺少 $p3_file(Muse P3 客机文件):$SCRIPT_DIR" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
P3_LIBS_PRESENT=0
|
P3_LIBS_PRESENT=0
|
||||||
if [ -f "$REPO_DIR/cloud/chv/authd.py" ] && [ -f "$REPO_DIR/cloud/chv/sentinel.py" ] && [ -f "$REPO_DIR/cloud/__init__.py" ] && [ -f "$REPO_DIR/cloud/chv/__init__.py" ]; then
|
if [ -f "$REPO_DIR/cloud/chv/authd.py" ] && [ -f "$REPO_DIR/cloud/chv/sentinel.py" ] && [ -f "$REPO_DIR/cloud/chv/errors.py" ] && [ -f "$REPO_DIR/cloud/__init__.py" ] && [ -f "$REPO_DIR/cloud/chv/__init__.py" ]; then
|
||||||
P3_LIBS_PRESENT=1
|
P3_LIBS_PRESENT=1
|
||||||
else
|
else
|
||||||
echo "警告:Muse P3 云端叶子模块不在仓库里(cloud/chv/authd.py 与 sentinel.py),镜像暂不含 authd 叶子模块" >&2
|
echo "警告:Muse P3 云端叶子模块不在仓库里(cloud/chv/authd.py 与 sentinel.py),镜像暂不含 authd 叶子模块" >&2
|
||||||
@@ -305,33 +310,44 @@ VC_ARGS=(-a "$OUT/disk.raw"
|
|||||||
--run-command 'mv /usr/local/bin/authd.py /usr/local/bin/chv-authd && chmod +x /usr/local/bin/chv-authd /usr/local/bin/git-credential-chv-authd'
|
--run-command 'mv /usr/local/bin/authd.py /usr/local/bin/chv-authd && chmod +x /usr/local/bin/chv-authd /usr/local/bin/git-credential-chv-authd'
|
||||||
--run-command 'systemctl enable chv-authd || true'
|
--run-command 'systemctl enable chv-authd || true'
|
||||||
--run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - systemctl start chv-authd || true\n - test -f /etc/chv/guest-env && chown chv:chv /etc/chv/guest-env || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg'
|
--run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - systemctl start chv-authd || true\n - test -f /etc/chv/guest-env && chown chv:chv /etc/chv/guest-env || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg'
|
||||||
--run-command 'git config --system user.name chv && git config --system user.email chv@localhost && git config --system credential.helper store && git config --system init.defaultBranch main'
|
--run-command 'git config --system user.name chv && git config --system user.email chv@localhost && git config --system init.defaultBranch main'
|
||||||
--run-command "$GT_INSTALL_CMD"
|
--run-command "$GT_INSTALL_CMD"
|
||||||
--run-command '/usr/local/bin/gt version > /app/guest-versions-gt.txt'
|
--run-command '/usr/local/bin/gt version > /app/guest-versions-gt.txt'
|
||||||
--run-command 'printf "ssh_deletekeys: false\nbootcmd:\n - \"[ -f /etc/ssh/ssh_host_ed25519_key ] || ssh-keygen -A\"\n" > /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg; systemctl enable ssh || true'
|
--run-command 'printf "ssh_deletekeys: false\nbootcmd:\n - \"[ -f /etc/ssh/ssh_host_ed25519_key ] || ssh-keygen -A\"\n" > /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg; systemctl enable ssh || true'
|
||||||
--run-command 'printf "PasswordAuthentication no\nPermitRootLogin prohibit-password\nChallengeResponseAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\n" > /etc/ssh/sshd_config.d/99-chv.conf && mkdir -p /run/sshd && ssh-keygen -A && sshd -t && grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config.d/99-chv.conf && grep -q "^PermitRootLogin prohibit-password" /etc/ssh/sshd_config.d/99-chv.conf && rm -f /etc/ssh/ssh_host_* && test -z "$(ls /etc/ssh/ssh_host_* 2>/dev/null)"'
|
--run-command 'printf "PasswordAuthentication no\nPermitRootLogin prohibit-password\nChallengeResponseAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\n" > /etc/ssh/sshd_config.d/99-chv.conf && mkdir -p /run/sshd && ssh-keygen -A && sshd -t && grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config.d/99-chv.conf && grep -q "^PermitRootLogin prohibit-password" /etc/ssh/sshd_config.d/99-chv.conf && rm -f /etc/ssh/ssh_host_* && test -z "$(ls /etc/ssh/ssh_host_* 2>/dev/null)"'
|
||||||
--run-command 'dpkg-query -W -f "\${Package} \${Version}\n" > /app/guest-versions-dpkg.txt; pip3 freeze > /app/guest-versions-pip.txt || true'
|
--run-command 'dpkg-query -W -f "\${Package} \${Version}\n" > /app/guest-versions-dpkg.txt; pip3 freeze > /app/guest-versions-pip.txt || true'
|
||||||
--run-command 'useradd --system --home-dir /var/lib/chv-browser --shell /usr/sbin/nologin chv-browser && mkdir -p /var/lib/chv-browser /run/chv/shots && chown chv-browser:chv-browser /var/lib/chv-browser /run/chv/shots && chmod 0755 /run/chv /run/chv/shots'
|
--run-command 'useradd --system --home-dir /var/lib/chv-browser --shell /usr/sbin/nologin chv-browser && mkdir -p /var/lib/chv-browser /run/chv/shots /run/chv-browser && chown chv-browser:chv-browser /var/lib/chv-browser /run/chv/shots /run/chv-browser && chmod 0755 /run/chv /run/chv/shots /run/chv-browser'
|
||||||
--run-command 'printf "[Unit]\nDescription=CHV cell broker (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 /usr/local/bin/chv-broker\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-broker.service'
|
--run-command 'printf "[Unit]\nDescription=CHV cell broker (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 /usr/local/bin/chv-broker\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-broker.service'
|
||||||
--run-command 'printf "[Unit]\nDescription=CHV browser sub-agent (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nUser=chv-browser\nGroup=chv-browser\nExecStart=/usr/bin/python3 /usr/local/bin/chv-browser\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-browser.service'
|
--run-command 'printf "[Unit]\nDescription=CHV browser sub-agent (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nUser=chv-browser\nGroup=chv-browser\nExecStart=/usr/bin/python3 /usr/local/bin/chv-browser\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-browser.service'
|
||||||
--run-command 'printf "d /run/chv 0755 root root -\nd /run/chv/shots 0755 chv-browser chv-browser -\n" > /etc/tmpfiles.d/chv.conf && systemctl enable chv-broker chv-browser || true')
|
--run-command 'printf "d /run/chv 0755 root root -\nd /run/chv/shots 0755 chv-browser chv-browser -\nd /run/chv-browser 0755 chv-browser chv-browser -\n" > /etc/tmpfiles.d/chv.conf && systemctl enable chv-broker chv-browser || true')
|
||||||
# P3 交接(Muse P3 的 authd/sentinel 叶子模块):文件在仓库里存在时
|
# P3 交接(Muse P3 的 authd/sentinel 叶子模块):文件在仓库里存在时
|
||||||
# 拷入镜像(本分支已有,合入后自动生效);不存在时只警告、不失败——
|
# 拷入镜像(本分支已有,合入后自动生效);不存在时只警告、不失败——
|
||||||
# M4 既定 build.sh 不硬要求 cloud/,见上。缺叶子模块时 chv-authd
|
# M4 既定 build.sh 不硬要求 cloud/,见上。缺叶子模块时 chv-authd
|
||||||
# 守护进程起不来,chv-sync 按差距表 §3.5 回落旧路径并警告。
|
# 守护进程起不来,chv-sync 按差距表 §3.5 回落旧路径并警告。
|
||||||
|
# round-11 真机修复(层 1):叶子树是 errors.py + stub __init__
|
||||||
|
#(authd_init_stub.py),不拷真实的 cloud/chv/__init__.py(它
|
||||||
|
# import executor/orphans,会让 authd 在最小叶子树里
|
||||||
|
# ModuleNotFoundError 而 crash loop);cloud/__init__.py 本身只
|
||||||
|
# 有 docstring,照常拷。
|
||||||
if [ "$P3_LIBS_PRESENT" = "1" ]; then
|
if [ "$P3_LIBS_PRESENT" = "1" ]; then
|
||||||
VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/authd.py:/usr/local/lib/chv-authd/cloud/chv/"
|
VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/authd.py:/usr/local/lib/chv-authd/cloud/chv/"
|
||||||
--copy-in "$REPO_DIR/cloud/chv/sentinel.py:/usr/local/lib/chv-authd/cloud/chv/"
|
--copy-in "$REPO_DIR/cloud/chv/sentinel.py:/usr/local/lib/chv-authd/cloud/chv/"
|
||||||
|
--copy-in "$REPO_DIR/cloud/chv/errors.py:/usr/local/lib/chv-authd/cloud/chv/"
|
||||||
--copy-in "$REPO_DIR/cloud/__init__.py:/usr/local/lib/chv-authd/cloud/"
|
--copy-in "$REPO_DIR/cloud/__init__.py:/usr/local/lib/chv-authd/cloud/"
|
||||||
--copy-in "$REPO_DIR/cloud/chv/__init__.py:/usr/local/lib/chv-authd/cloud/chv/")
|
--copy-in "$SCRIPT_DIR/authd_init_stub.py:/usr/local/lib/chv-authd/cloud/chv/"
|
||||||
|
--run-command 'mv /usr/local/lib/chv-authd/cloud/chv/authd_init_stub.py /usr/local/lib/chv-authd/cloud/chv/__init__.py')
|
||||||
fi
|
fi
|
||||||
# P1 交接(Muse P1 的 vsock_notify.py):文件在仓库里存在时拷入
|
# P1 交接(Muse P1 的 vsock_notify.py):文件在仓库里存在时拷入
|
||||||
# 镜像(P1 分支才有,本分支尚无);不存在时只警告、不失败——
|
# 镜像并建 vsock-notify.service(自动启动,round-11 真机修复:
|
||||||
# 合并后自动生效,P2 不改该脚本内容。
|
# 此前只拷文件、不建服务,vsock 通道从未自动启动;服务形状按
|
||||||
|
# vsock_notify.py 的 docstring:ExecStart=/usr/local/bin/
|
||||||
|
# vsock-notify %H,Restart=no,After/WantedBy=multi-user.target;
|
||||||
|
# printf 格式串里 %H 须写 %%H);不存在时只警告、不失败。
|
||||||
VSOCK_NOTIFY_PRESENT=0
|
VSOCK_NOTIFY_PRESENT=0
|
||||||
if [ -f "$REPO_DIR/cloud/chv/guest/vsock_notify.py" ]; then
|
if [ -f "$REPO_DIR/cloud/chv/guest/vsock_notify.py" ]; then
|
||||||
VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/guest/vsock_notify.py:/usr/local/bin/"
|
VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/guest/vsock_notify.py:/usr/local/bin/"
|
||||||
--run-command 'mv /usr/local/bin/vsock_notify.py /usr/local/bin/vsock-notify && chmod +x /usr/local/bin/vsock-notify')
|
--run-command 'mv /usr/local/bin/vsock_notify.py /usr/local/bin/vsock-notify && chmod +x /usr/local/bin/vsock-notify'
|
||||||
|
--run-command 'printf "[Unit]\nDescription=CHV vsock notify (Muse P1)\nAfter=multi-user.target\n\n[Service]\nType=simple\nExecStart=/usr/local/bin/vsock-notify %%H\nRestart=no\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/vsock-notify.service && systemctl enable vsock-notify || true')
|
||||||
VSOCK_NOTIFY_PRESENT=1
|
VSOCK_NOTIFY_PRESENT=1
|
||||||
else
|
else
|
||||||
echo "警告:cloud/chv/guest/vsock_notify.py 不在仓库里(P1 未合并),镜像暂不含 vsock notify" >&2
|
echo "警告:cloud/chv/guest/vsock_notify.py 不在仓库里(P1 未合并),镜像暂不含 vsock notify" >&2
|
||||||
@@ -395,7 +411,7 @@ else
|
|||||||
echo "警告:CHROMIUM_HEADLESS_SHELL_REV 未设,Chromium 版本只记录、不钉死(V-9 验收须设该变量重建)" >&2
|
echo "警告:CHROMIUM_HEADLESS_SHELL_REV 未设,Chromium 版本只记录、不钉死(V-9 验收须设该变量重建)" >&2
|
||||||
fi
|
fi
|
||||||
EXTRA_CONTENTS=""
|
EXTRA_CONTENTS=""
|
||||||
[ "$P3_LIBS_PRESENT" = "1" ] && EXTRA_CONTENTS="cloud/chv/authd.py,cloud/chv/sentinel.py"
|
[ "$P3_LIBS_PRESENT" = "1" ] && EXTRA_CONTENTS="cloud/chv/authd.py,cloud/chv/sentinel.py,cloud/chv/errors.py,cloud/chv/__init__.py"
|
||||||
[ "$VSOCK_NOTIFY_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}vsock_notify"
|
[ "$VSOCK_NOTIFY_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}vsock_notify"
|
||||||
python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-1}" "${CHROMIUM_HEADLESS_SHELL_REV:-}" "$EXTRA_CONTENTS"
|
python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-1}" "${CHROMIUM_HEADLESS_SHELL_REV:-}" "$EXTRA_CONTENTS"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -30,6 +30,20 @@ DECLARED_SOURCES = {
|
|||||||
"chv-cell": "repo_commit",
|
"chv-cell": "repo_commit",
|
||||||
"chv-broker": "repo_commit",
|
"chv-broker": "repo_commit",
|
||||||
"chv-browser": "repo_commit",
|
"chv-browser": "repo_commit",
|
||||||
|
# 对齐 Muse P3 + P1 vsock:authd 三件套、authd/sentinel
|
||||||
|
# 叶子模块与 vsock 通知脚本,均是仓库内脚本,版本随
|
||||||
|
# repo_commit。缺了它们 check 会报 contents 未覆盖(717efc4
|
||||||
|
# 真机实测:6 项 unresolved,verify 与 check 双双失败)。
|
||||||
|
# round-11 真机修复(层 1):叶子树新增 errors.py 与 stub
|
||||||
|
# __init__.py(authd_init_stub.py),同款 repo_commit 登记。
|
||||||
|
"chv-authd": "repo_commit",
|
||||||
|
"git-credential-chv-authd": "repo_commit",
|
||||||
|
"chv-authd.service": "repo_commit",
|
||||||
|
"cloud/chv/authd.py": "repo_commit",
|
||||||
|
"cloud/chv/sentinel.py": "repo_commit",
|
||||||
|
"cloud/chv/errors.py": "repo_commit",
|
||||||
|
"cloud/chv/__init__.py": "repo_commit",
|
||||||
|
"vsock_notify": "repo_commit",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
BROKER_SOCK = "/run/chv/broker.sock"
|
BROKER_SOCK = "/run/chv/broker.sock"
|
||||||
BROWSER_SOCK = "/run/chv/browser.sock"
|
# round-11 真机修复(缺陷 4):browser.sock 从 /run/chv 移到
|
||||||
|
# chv-browser 自有目录——/run/chv 是 0755 root root,
|
||||||
|
# chv-browser 在 bind 时 EACCES 而无限 crash loop(restart 21);
|
||||||
|
# 放进组可写的 /run/chv 会给 chv-browser 对 broker.sock/cell.pid
|
||||||
|
# 的 unlink 能力,所以用独立目录(0755 chv-browser 所有)。
|
||||||
|
BROWSER_SOCK = "/run/chv-browser/browser.sock"
|
||||||
CELL_PID_FILE = "/run/chv/cell.pid"
|
CELL_PID_FILE = "/run/chv/cell.pid"
|
||||||
UID_BASE = 1000
|
UID_BASE = 1000
|
||||||
UID_COUNT = 65536
|
UID_COUNT = 65536
|
||||||
|
|||||||
+53
-46
@@ -2,9 +2,9 @@
|
|||||||
"""chv-browser:浏览器子代理(客机侧、单元外独立进程)。
|
"""chv-browser:浏览器子代理(客机侧、单元外独立进程)。
|
||||||
|
|
||||||
对齐 Muse P2(见 docs/chv-muse-p2-gap.md):浏览器任务由专门子代理
|
对齐 Muse P2(见 docs/chv-muse-p2-gap.md):浏览器任务由专门子代理
|
||||||
完成,与 codex 单元分离。子代理只向单元返回页面无障碍树快照
|
完成,与 codex 单元分离。子代理只向单元返回页面无障碍快照
|
||||||
(Playwright accessibility.snapshot),不见原始 DOM,不能在页面
|
(Playwright Page.aria_snapshot() 的 ARIA 快照字符串),不见原始
|
||||||
上下文跑 JS,不暴露 evaluate/content,不开 DevTools。
|
DOM,不能在页面上下文跑 JS,不暴露 evaluate/content,不开 DevTools。
|
||||||
|
|
||||||
操作(Unix socket /run/chv/browser.sock,JSON 行):
|
操作(Unix socket /run/chv/browser.sock,JSON 行):
|
||||||
open {url} -> 导航并返回 {title, url, a11y, truncated}
|
open {url} -> 导航并返回 {title, url, a11y, truncated}
|
||||||
@@ -31,7 +31,10 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
BROWSER_SOCK = "/run/chv/browser.sock"
|
# round-11 真机修复(缺陷 4):自有目录 /run/chv-browser,
|
||||||
|
# 0755 chv-browser 所有——/run/chv 是 0755 root root,
|
||||||
|
# 在此 bind 会 EACCES 而无限 crash loop(见 chv-broker.py)。
|
||||||
|
BROWSER_SOCK = "/run/chv-browser/browser.sock"
|
||||||
SHOTS_DIR = "/run/chv/shots"
|
SHOTS_DIR = "/run/chv/shots"
|
||||||
PLAYWRIGHT_BROWSERS_PATH = "/usr/local/share/ms-playwright"
|
PLAYWRIGHT_BROWSERS_PATH = "/usr/local/share/ms-playwright"
|
||||||
MAX_REQUEST_BYTES = 65536
|
MAX_REQUEST_BYTES = 65536
|
||||||
@@ -61,47 +64,40 @@ def validate_url(url):
|
|||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
def truncate_a11y(node, max_nodes=MAX_A11Y_NODES,
|
def truncate_aria(text, max_nodes=MAX_A11Y_NODES,
|
||||||
max_chars=MAX_A11Y_CHARS):
|
max_chars=MAX_A11Y_CHARS):
|
||||||
"""截断无障碍树:节点数超上限时剪掉多余子树。返回 (tree, truncated)。
|
"""截断 ARIA 快照字符串:节点行(行首 "- ")超上限时剪掉多余
|
||||||
纯函数,可单测。节点计数按 dict 节点数(含 children 展开)。"""
|
行,字符超上限时截断。返回 (text, truncated)。纯函数,可单测。
|
||||||
state = {"count": 0, "truncated": False}
|
round-11 复测纠正(第二轮):Playwright 根本没有
|
||||||
|
page.accessibility(那是 Puppeteer 的 API),原生快照是
|
||||||
def walk(n):
|
Page.aria_snapshot() 返回的字符串;旧的 dict 版 truncate_a11y
|
||||||
if not isinstance(n, dict):
|
随之废止。a11y 字段仍叫 a11y,值改为 ARIA 快照字符串。"""
|
||||||
return n
|
text = text or ""
|
||||||
state["count"] += 1
|
lines = text.splitlines()
|
||||||
if state["count"] > max_nodes:
|
kept = []
|
||||||
state["truncated"] = True
|
nodes = 0
|
||||||
return None
|
truncated = False
|
||||||
out = {}
|
for line in lines:
|
||||||
for key, value in n.items():
|
if line.lstrip().startswith("- "):
|
||||||
if key == "children" and isinstance(value, list):
|
nodes += 1
|
||||||
children = []
|
if nodes > max_nodes:
|
||||||
for child in value:
|
truncated = True
|
||||||
walked = walk(child)
|
break
|
||||||
if walked is None:
|
kept.append(line)
|
||||||
state["truncated"] = True
|
out = "\n".join(kept)
|
||||||
break
|
if len(out) > max_chars:
|
||||||
children.append(walked)
|
out = out[:max_chars]
|
||||||
out[key] = children
|
truncated = True
|
||||||
else:
|
return out, truncated
|
||||||
out[key] = value
|
|
||||||
return out
|
|
||||||
|
|
||||||
tree = walk(node)
|
|
||||||
text = json.dumps(tree, ensure_ascii=False)
|
|
||||||
if len(text) > max_chars:
|
|
||||||
state["truncated"] = True
|
|
||||||
# 字符超限时只留顶层角色/名字/URL 信息,不灌整棵树。
|
|
||||||
tree = {"role": (node or {}).get("role"),
|
|
||||||
"name": (node or {}).get("name"),
|
|
||||||
"truncated_chars": True}
|
|
||||||
return tree, state["truncated"]
|
|
||||||
|
|
||||||
|
|
||||||
def _load_playwright():
|
def _load_playwright():
|
||||||
"""懒加载 playwright;缺失时报明错(不静默假装有浏览器)。"""
|
"""懒加载 playwright;缺失时报明错(不静默假装有浏览器)。
|
||||||
|
返回调用后的上下文管理器(sync_playwright()),不是函数本身——
|
||||||
|
round-11 复测回归:曾返回裸 sync_playwright 函数,
|
||||||
|
_ensure_browser 对它调 __enter__ 即 AttributeError,open
|
||||||
|
永远失败;单测的假工厂直接返回带 __enter__ 的对象,盖不住
|
||||||
|
这个形状(见 test_load_playwright_returns_called_cm)。"""
|
||||||
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH",
|
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH",
|
||||||
PLAYWRIGHT_BROWSERS_PATH)
|
PLAYWRIGHT_BROWSERS_PATH)
|
||||||
try:
|
try:
|
||||||
@@ -110,7 +106,7 @@ def _load_playwright():
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"chv-browser: playwright 未装(%s),浏览器子代理不可用"
|
"chv-browser: playwright 未装(%s),浏览器子代理不可用"
|
||||||
% e)
|
% e)
|
||||||
return sync_playwright
|
return sync_playwright()
|
||||||
|
|
||||||
|
|
||||||
class BrowserAgent:
|
class BrowserAgent:
|
||||||
@@ -158,12 +154,16 @@ class BrowserAgent:
|
|||||||
try:
|
try:
|
||||||
title = self._page.title()
|
title = self._page.title()
|
||||||
url = self._page.url
|
url = self._page.url
|
||||||
tree = self._page.accessibility.snapshot()
|
# round-11 复测纠正(第二轮):Playwright 没有
|
||||||
|
# page.accessibility(Puppeteer 的 API),原生快照是
|
||||||
|
# Page.aria_snapshot()(字符串);a11y 字段仍叫 a11y,
|
||||||
|
# 值改为 ARIA 快照字符串(截断到 200 节点行/64KB)。
|
||||||
|
aria = self._page.aria_snapshot()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"ok": False, "error": "取快照失败:%s" % e}
|
return {"ok": False, "error": "取快照失败:%s" % e}
|
||||||
tree, truncated = truncate_a11y(tree or {})
|
aria, truncated = truncate_aria(aria or "")
|
||||||
return {"ok": True, "title": title, "url": url,
|
return {"ok": True, "title": title, "url": url,
|
||||||
"a11y": tree, "truncated": truncated}
|
"a11y": aria, "truncated": truncated}
|
||||||
|
|
||||||
def screenshot(self):
|
def screenshot(self):
|
||||||
if self._page is None:
|
if self._page is None:
|
||||||
@@ -205,10 +205,17 @@ FORBIDDEN_OPS = ("evaluate", "content", "devtools", "js")
|
|||||||
|
|
||||||
|
|
||||||
def handle_request(agent, req):
|
def handle_request(agent, req):
|
||||||
"""处理一个请求 dict,返回响应 dict(纯逻辑,可单测)。"""
|
"""处理一个请求 dict,返回响应 dict(纯逻辑,可单测)。
|
||||||
|
op "browser" 是 broker 原样转发的形状(round-11 真机修复):
|
||||||
|
内层 op 在 action 里;无 action 但带 url 时默认 open(驱动
|
||||||
|
只发 url);action 同样受 FORBIDDEN_OPS 约束。"""
|
||||||
if not isinstance(req, dict):
|
if not isinstance(req, dict):
|
||||||
return {"ok": False, "error": "请求须是 JSON 对象"}
|
return {"ok": False, "error": "请求须是 JSON 对象"}
|
||||||
op = req.get("op")
|
op = req.get("op")
|
||||||
|
if op == "browser":
|
||||||
|
op = req.get("action")
|
||||||
|
if op is None and req.get("url"):
|
||||||
|
op = "open"
|
||||||
if op in FORBIDDEN_OPS:
|
if op in FORBIDDEN_OPS:
|
||||||
return {"ok": False,
|
return {"ok": False,
|
||||||
"error": "操作 %r 不暴露(无 JS/无原始 DOM)" % op}
|
"error": "操作 %r 不暴露(无 JS/无原始 DOM)" % op}
|
||||||
|
|||||||
+24
-6
@@ -11,7 +11,9 @@ systemd),单元即沙箱——单元内 codex 用 danger-full-access,不
|
|||||||
--private-users=1000:65536 —— 单元内 uid 0 → 客机侧 chv(uid 1000)。
|
--private-users=1000:65536 —— 单元内 uid 0 → 客机侧 chv(uid 1000)。
|
||||||
效果:workspace / ~/.codex(chv 所有)单元内仍可写;host-root 0600
|
效果:workspace / ~/.codex(chv 所有)单元内仍可写;host-root 0600
|
||||||
文件(如 /etc/chv/guest-env)在单元内显示为 overflow uid、不可读;
|
文件(如 /etc/chv/guest-env)在单元内显示为 overflow uid、不可读;
|
||||||
再加 --inaccessible 遮蔽作纵深防御。
|
再加 --inaccessible 遮蔽作纵深防御(只遮 /usr 下路径:--volatile=yes
|
||||||
|
下容器根是基本空的 tmpfs,/etc/chv 等在容器根里根本不存在,按构造
|
||||||
|
即不可达,列出反而让 nspawn 解析失败而起不来,见 INACCESSIBLE_BASE)。
|
||||||
|
|
||||||
网络:--network-veth。宿主侧 ve-chv-cell(10.200.0.1/24),单元侧
|
网络:--network-veth。宿主侧 ve-chv-cell(10.200.0.1/24),单元侧
|
||||||
host0(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站
|
host0(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站
|
||||||
@@ -79,11 +81,15 @@ DROP_CAPABILITIES = [
|
|||||||
SYSCALL_FILTER = ("@system-service ~io_uring_setup "
|
SYSCALL_FILTER = ("@system-service ~io_uring_setup "
|
||||||
"~io_uring_enter ~io_uring_register")
|
"~io_uring_enter ~io_uring_register")
|
||||||
|
|
||||||
# 单元内遮掉的路径:凭证与浏览器(单元内跑不起自己的浏览器,
|
# 单元内遮掉的路径:浏览器(单元内跑不起自己的浏览器,
|
||||||
# 只能走 broker 的 chv-browser)。
|
# 只能走 broker 的 chv-browser)。只列 /usr 下的路径——
|
||||||
|
# round-11 复测纠正(第二轮):--volatile=yes 下容器根是
|
||||||
|
# 基本空的 tmpfs(只挂进只读 /usr),nspawn 按容器根解析
|
||||||
|
# --inaccessible 路径,不存在的路径即启动失败;/etc/chv 与
|
||||||
|
# /home/chv/.git-credentials 在容器根里根本不存在(无 /home、
|
||||||
|
# 无 /etc/chv),凭证文件按构造就不可达,不再列出遮蔽。
|
||||||
|
# 旧的两项若列回,cell 会报 Failed to resolve 而起不来。
|
||||||
INACCESSIBLE_BASE = [
|
INACCESSIBLE_BASE = [
|
||||||
"/etc/chv",
|
|
||||||
"/home/chv/.git-credentials",
|
|
||||||
"/usr/local/share/ms-playwright",
|
"/usr/local/share/ms-playwright",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -131,9 +137,21 @@ def build_nspawn_argv(command, network="veth",
|
|||||||
"systemd-nspawn", "--quiet",
|
"systemd-nspawn", "--quiet",
|
||||||
"--machine=%s" % MACHINE,
|
"--machine=%s" % MACHINE,
|
||||||
"--directory=/",
|
"--directory=/",
|
||||||
|
# round-11 真机修复:nspawn 255 拒裸 --directory=/(须
|
||||||
|
# --ephemeral 或 --volatile),也拒裸 --no-new-privileges
|
||||||
|
#(把它当作带必填参数的选项,吞掉下一个参数后报错)。
|
||||||
|
# 2026-11-07 复测纠正:首轮修用的 --ephemeral 在客内
|
||||||
|
# ext4 根盘(2.4G、已用 1.8G、可用 593M)上做整盘快照
|
||||||
|
# 拷贝,空间不够即 ENOSPC,全部 cell 调用失败。改
|
||||||
|
# --volatile=yes:根为基本空的 tmpfs、/usr 只读挂进,
|
||||||
|
# 单元内除 bind 出去的目录外一律可写、用完即弃——与
|
||||||
|
# --ephemeral 的设计意图一致,但不拷盘、不占空间。
|
||||||
|
# --volatile=state 不行:整树只读、仅 /var 为 tmpfs,
|
||||||
|
# codex 写 $HOME/.cache 等非 bind 路径会失败。
|
||||||
|
"--volatile=yes",
|
||||||
"--as-pid2",
|
"--as-pid2",
|
||||||
"--private-users=%d:%d" % (UID_BASE, UID_COUNT),
|
"--private-users=%d:%d" % (UID_BASE, UID_COUNT),
|
||||||
"--no-new-privileges",
|
"--no-new-privileges=yes",
|
||||||
"--system-call-filter=%s" % SYSCALL_FILTER,
|
"--system-call-filter=%s" % SYSCALL_FILTER,
|
||||||
]
|
]
|
||||||
for cap in DROP_CAPABILITIES:
|
for cap in DROP_CAPABILITIES:
|
||||||
|
|||||||
+16
-19
@@ -14,11 +14,12 @@
|
|||||||
# 只给客内 authd 守护进程读)。
|
# 只给客内 authd 守护进程读)。
|
||||||
# boot(root,cloud-init runcmd 调)读 guest-env,authd 在时配
|
# boot(root,cloud-init runcmd 调)读 guest-env,authd 在时配
|
||||||
# git credential helper 走 authd 按次取凭证(只进 git 子进程
|
# git credential helper 走 authd 按次取凭证(只进 git 子进程
|
||||||
# 内存,不落盘);authd 不在时(旧镜像)才回落到把 token 写进
|
# 内存,不落盘);authd 不在时直接失败(round-11 真机修复层 5:
|
||||||
# /home/chv/.git-credentials(0600,chv 所有,git credential
|
# 旧镜像的落盘回落已废止——它在真机测试里把 authd 释放的长效
|
||||||
# store 标准机制)并警告。pull/push/status 以 chv 身份跑,用
|
# token 写进了客内 ~/.git-credentials;authd helper 是唯一凭证
|
||||||
# helper 或已落盘的 credential,不再读 guest-env;token 不会
|
# helper,且自身绝不落盘)。pull/push/status 以 chv 身份跑,用
|
||||||
# 出现在 git remote URL 或镜像里。
|
# helper,不再读 guest-env;token 不会出现在 git remote URL 或
|
||||||
|
# 镜像里。
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# chv-sync boot # 启动时 clone(已 clone 则静默退出,不自动 pull)
|
# chv-sync boot # 启动时 clone(已 clone 则静默退出,不自动 pull)
|
||||||
@@ -65,19 +66,16 @@ cmd_boot() {
|
|||||||
: "${GITEA_USER:?guest-env 缺 GITEA_USER}"
|
: "${GITEA_USER:?guest-env 缺 GITEA_USER}"
|
||||||
: "${SYNC_REPO:?guest-env 缺 SYNC_REPO}"
|
: "${SYNC_REPO:?guest-env 缺 SYNC_REPO}"
|
||||||
[ "$(id -u)" = "0" ] || die "boot 须以 root 运行(cloud-init runcmd)"
|
[ "$(id -u)" = "0" ] || die "boot 须以 root 运行(cloud-init runcmd)"
|
||||||
local host="${GITEA_URL#https://}"
|
|
||||||
host="${host#http://}"
|
|
||||||
host="${host%%/*}"
|
|
||||||
local name="${SYNC_REPO##*/}"
|
local name="${SYNC_REPO##*/}"
|
||||||
local dir="$WORKSPACE/$name"
|
local dir="$WORKSPACE/$name"
|
||||||
mkdir -p "$WORKSPACE"
|
mkdir -p "$WORKSPACE"
|
||||||
chown "$SYNC_USER:$SYNC_USER" "$WORKSPACE"
|
chown "$SYNC_USER:$SYNC_USER" "$WORKSPACE"
|
||||||
# feat-chv-muse-p3:authd 路径——socket 存在时凭证走 helper,
|
# feat-chv-muse-p3:authd 路径——socket 存在时凭证走 helper,
|
||||||
# 不再把真 token 写进 .git-credentials 落盘;helper 按次向
|
# 不再把真 token 写进 .git-credentials 落盘;helper 按次向
|
||||||
# authd 取凭证,只进 git 子进程内存。socket 不存在(旧镜像)
|
# authd 取凭证,只进 git 子进程内存。socket 不存在时直接失败
|
||||||
# 时走旧路径 + 警告(过渡兼容,旧镜像退役后改为 fail-closed)。
|
#(round-11 真机修复层 5:落盘回落已废止,见下)。
|
||||||
# 注意:authd 路径下 GITEA_TOKEN 不在 guest-env 里(它只进
|
# 注意:authd 路径下 GITEA_TOKEN 不在 guest-env 里(它只进
|
||||||
# authd-seed 给守护进程),所以这里不要求它;旧路径才要求。
|
# authd-seed 给守护进程),所以这里不要求它。
|
||||||
local authd_socket="${CHV_AUTHD_SOCKET:-/run/chv-authd/authd.sock}"
|
local authd_socket="${CHV_AUTHD_SOCKET:-/run/chv-authd/authd.sock}"
|
||||||
local helper="/usr/local/bin/git-credential-chv-authd"
|
local helper="/usr/local/bin/git-credential-chv-authd"
|
||||||
if [ -S "$authd_socket" ] && [ -x "$helper" ]; then
|
if [ -S "$authd_socket" ] && [ -x "$helper" ]; then
|
||||||
@@ -88,14 +86,13 @@ cmd_boot() {
|
|||||||
runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git config --global credential.useHttpPath true
|
runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git config --global credential.useHttpPath true
|
||||||
info "凭证走 chv-authd(不落盘)"
|
info "凭证走 chv-authd(不落盘)"
|
||||||
else
|
else
|
||||||
: "${GITEA_TOKEN:?guest-env 缺 GITEA_TOKEN(旧路径)}"
|
# round-11 真机修复(层 5):authd 不在时直接失败,不再
|
||||||
echo "chv-sync: 警告:chv-authd 未运行,凭证走旧落盘路径(须用含 authd 的新镜像)" >&2
|
# 把真 token 写进 ~/.git-credentials 落盘——旧路径在
|
||||||
# token 只落盘进用户家目录的 credential store(0600),
|
# round-11 真机测试里把 authd 释放的长效 token 写进了
|
||||||
# 不进 git remote URL、不进镜像、不进日志(此处不打印 token)。
|
# 客内磁盘(/home/chv/.git-credentials,0600)。authd
|
||||||
local creds="$USER_HOME/.git-credentials"
|
# helper 是唯一凭证 helper,且自身绝不落盘;旧镜像退役
|
||||||
printf 'https://%s:%s@%s\n' "$GITEA_USER" "$GITEA_TOKEN" "$host" > "$creds"
|
# 后过渡兼容即废止,不再留落盘逃生口。
|
||||||
chmod 0600 "$creds"
|
die "chv-authd 未运行(socket 或 helper 缺失),拒绝同步:凭证绝不落盘,请用含 authd 的镜像"
|
||||||
chown "$SYNC_USER:$SYNC_USER" "$creds"
|
|
||||||
fi
|
fi
|
||||||
if [ -d "$dir/.git" ]; then
|
if [ -d "$dir/.git" ]; then
|
||||||
info "已 clone:$dir(不自动 pull,改动由用户显式同步)"
|
info "已 clone:$dir(不自动 pull,改动由用户显式同步)"
|
||||||
|
|||||||
@@ -51,6 +51,21 @@ DECLARED_SOURCES = {
|
|||||||
"chv-cell": "repo_commit",
|
"chv-cell": "repo_commit",
|
||||||
"chv-broker": "repo_commit",
|
"chv-broker": "repo_commit",
|
||||||
"chv-browser": "repo_commit",
|
"chv-browser": "repo_commit",
|
||||||
|
# 对齐 Muse P3 + P1 vsock:authd 三件套、authd/sentinel
|
||||||
|
# 叶子模块与 vsock 通知脚本,均是仓库内脚本,版本随
|
||||||
|
# repo_commit。缺了它们 generate 会记 6 项 unresolved,
|
||||||
|
# verify 与 check 双双失败(717efc4 真机实测)。
|
||||||
|
# round-11 真机修复(层 1):叶子树新增 errors.py 与 stub
|
||||||
|
# __init__.py(authd_init_stub.py),同款 repo_commit 登记,
|
||||||
|
# 缺了它们新镜像的 lockfile 同样会 unresolved。
|
||||||
|
"chv-authd": "repo_commit",
|
||||||
|
"git-credential-chv-authd": "repo_commit",
|
||||||
|
"chv-authd.service": "repo_commit",
|
||||||
|
"cloud/chv/authd.py": "repo_commit",
|
||||||
|
"cloud/chv/sentinel.py": "repo_commit",
|
||||||
|
"cloud/chv/errors.py": "repo_commit",
|
||||||
|
"cloud/chv/__init__.py": "repo_commit",
|
||||||
|
"vsock_notify": "repo_commit",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -250,8 +250,9 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
3-4294967295 内,0/1/2 为保留 CID)
|
3-4294967295 内,0/1/2 为保留 CID)
|
||||||
- CHV_CELL_ENABLED:nspawn 单元开关(默认 1 开;只认
|
- CHV_CELL_ENABLED:nspawn 单元开关(默认 1 开;只认
|
||||||
0/1,非法值 ValueError)。开时客内 host.py 改走
|
0/1,非法值 ValueError)。开时客内 host.py 改走
|
||||||
chv-cell run(P2 差距表 §4);关时走原命令,给
|
sudo chv-cell run(P2 差距表 §4;round-11 真机修复:
|
||||||
lite10 及更早无单元的旧镜像留逃生口
|
chv-cell 须 root,执行器以 chv 身份调,须 sudo);
|
||||||
|
关时走原命令,给 lite10 及更早无单元的旧镜像留逃生口
|
||||||
|
|
||||||
前置校验(preflight,执行入口必调,缺一即 fail-fast):
|
前置校验(preflight,执行入口必调,缺一即 fail-fast):
|
||||||
收集全部缺失项一次报出具体缺哪一项,绝不笼统报错:
|
收集全部缺失项一次报出具体缺哪一项,绝不笼统报错:
|
||||||
@@ -1462,11 +1463,15 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
放在该路径(与 Docker 的 /app:ro 同款约定,只是载体
|
放在该路径(与 Docker 的 /app:ro 同款约定,只是载体
|
||||||
从容器换成 VM 镜像)。
|
从容器换成 VM 镜像)。
|
||||||
round-11 集成补洞(P2 差距表 §4):单元开时(默认)
|
round-11 集成补洞(P2 差距表 §4):单元开时(默认)
|
||||||
命令改为 chv-cell run -- python3 /app/host.py --state
|
命令改为 sudo chv-cell run -- python3 /app/host.py --state
|
||||||
/home/chv/.codex/host-state.json --codex-home
|
/home/chv/.codex/host-state.json --codex-home
|
||||||
/home/chv/.codex … --sandbox danger-full-access [子命令
|
/home/chv/.codex … --sandbox danger-full-access [子命令
|
||||||
...]。--state/--codex-home 须显式给(见模块顶 _CELL_*
|
...]。sudo 不可省:chv-cell 须 root(nspawn + 配网),
|
||||||
注释);--sandbox 放全局参数最后(argparse 后胜出),
|
执行器经 SSH 以 chv 身份调,chv 的 NOPASSWD sudo 来自
|
||||||
|
seed users 块(见 test_chv_gitea_sync.py 的 users 夹具;
|
||||||
|
探针 p2.log 的手动调用也是 sudo chv-cell run)。
|
||||||
|
--state/--codex-home 须显式给(见模块顶 _CELL_* 注释);
|
||||||
|
--sandbox 放全局参数最后(argparse 后胜出),
|
||||||
确保单元内恒为 danger-full-access:单元即沙箱,调用
|
确保单元内恒为 danger-full-access:单元即沙箱,调用
|
||||||
方传的 read-only(plan 任务)与 approval=full 自带的
|
方传的 read-only(plan 任务)与 approval=full 自带的
|
||||||
值都不得把它盖回 bwrap。单元关时
|
值都不得把它盖回 bwrap。单元关时
|
||||||
@@ -1476,7 +1481,7 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
cell = self._cell_enabled()
|
cell = self._cell_enabled()
|
||||||
cmd = ["python3", self.guest_host_py]
|
cmd = ["python3", self.guest_host_py]
|
||||||
if cell:
|
if cell:
|
||||||
cmd = ["chv-cell", "run", "--"] + cmd
|
cmd = ["sudo", "chv-cell", "run", "--"] + cmd
|
||||||
cmd += ["--state", _CELL_STATE_PATH,
|
cmd += ["--state", _CELL_STATE_PATH,
|
||||||
"--codex-home", _CELL_CODEX_HOME]
|
"--codex-home", _CELL_CODEX_HOME]
|
||||||
cmd += _host_command_options(
|
cmd += _host_command_options(
|
||||||
@@ -2598,7 +2603,7 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
ValueError(_to_switch 的 CHV_BALLOON/CHV_VSOCK 同款
|
ValueError(_to_switch 的 CHV_BALLOON/CHV_VSOCK 同款
|
||||||
口径,绝不静默 fallback)。关时 _guest_host_command
|
口径,绝不静默 fallback)。关时 _guest_host_command
|
||||||
走原命令,给 lite10 及更早无单元的旧镜像留逃生口;
|
走原命令,给 lite10 及更早无单元的旧镜像留逃生口;
|
||||||
开时走 chv-cell run -- python3 /app/host.py …。读
|
开时走 sudo chv-cell run -- python3 /app/host.py …。读
|
||||||
self._env(与 _resident_enabled 同款惰性读法),不
|
self._env(与 _resident_enabled 同款惰性读法),不
|
||||||
经 config:开关只影响命令构造,不影响建池/起 VM。"""
|
经 config:开关只影响命令构造,不影响建池/起 VM。"""
|
||||||
flag = _to_switch(
|
flag = _to_switch(
|
||||||
|
|||||||
+18
-6
@@ -193,10 +193,16 @@ def build_seed_user_data(private_key_text: str, public_key_text: str) -> str:
|
|||||||
|
|
||||||
def _authd_env_entry(authd_env: dict) -> list:
|
def _authd_env_entry(authd_env: dict) -> list:
|
||||||
"""authd-seed 的 write_files 条目行(0600 root,值经
|
"""authd-seed 的 write_files 条目行(0600 root,值经
|
||||||
shlex.quote 引用):只装 AUTHD_SECRET_KEYS 命中的密钥,
|
shlex.quote 引用):装 AUTHD_SECRET_KEYS 命中的密钥
|
||||||
与 guest-env 条目并列写进同一 write_files 列表(绝不另写
|
(本期只有 GITEA_TOKEN),外加与 guest-env 同源抄进来的
|
||||||
一个 write_files: 键,见 _guest_env_entry 的 YAML 重复键
|
三个非密钥同步变量(GITEA_URL/GITEA_USER/SYNC_REPO——
|
||||||
禁令)。"""
|
round-11 真机修复层 2:authd 的 build_server 要求 seed
|
||||||
|
里同时有这三键,只 GITEA_TOKEN 会 ValueError;探针
|
||||||
|
fixprobe3 即从 guest-env 补这三键)。三键只在调用方
|
||||||
|
guest-env 里有才抄,没有不补、不编默认值;与 guest-env
|
||||||
|
条目并列写进同一 write_files 列表(绝不另写一个
|
||||||
|
write_files: 键,见 _guest_env_entry 的 YAML 重复键禁令)。
|
||||||
|
"""
|
||||||
lines = [
|
lines = [
|
||||||
" - path: %s" % AUTHD_SEED_PATH,
|
" - path: %s" % AUTHD_SEED_PATH,
|
||||||
" permissions: '0600'",
|
" permissions: '0600'",
|
||||||
@@ -296,12 +302,18 @@ def _write_seed_meta(seed_dir: str, vm_name: str) -> str:
|
|||||||
def _seed_env_entries(merged_env: dict,
|
def _seed_env_entries(merged_env: dict,
|
||||||
authd_env: dict | None) -> list:
|
authd_env: dict | None) -> list:
|
||||||
"""guest-env 与 authd-seed 两路的 write_files 条目行
|
"""guest-env 与 authd-seed 两路的 write_files 条目行
|
||||||
(皆空即空列表,不写条目)。"""
|
(皆空即空列表,不写条目)。authd-seed 除了密钥还抄
|
||||||
|
guest-env 里的 GITEA_URL/GITEA_USER/SYNC_REPO(层 2,
|
||||||
|
见 _authd_env_entry)——build_server 要求四键齐全。"""
|
||||||
extra_lines = []
|
extra_lines = []
|
||||||
if merged_env:
|
if merged_env:
|
||||||
extra_lines += _guest_env_entry(merged_env)
|
extra_lines += _guest_env_entry(merged_env)
|
||||||
if authd_env:
|
if authd_env:
|
||||||
extra_lines += _authd_env_entry(authd_env)
|
seed_env = dict(authd_env)
|
||||||
|
for key in ("GITEA_URL", "GITEA_USER", "SYNC_REPO"):
|
||||||
|
if key in merged_env:
|
||||||
|
seed_env[key] = merged_env[key]
|
||||||
|
extra_lines += _authd_env_entry(seed_env)
|
||||||
return extra_lines
|
return extra_lines
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,13 +66,16 @@ _BLOCKED_NETWORKS = tuple(
|
|||||||
|
|
||||||
# 每连接器的 L7 路径策略:(方法, 匹配方式, 模式)。
|
# 每连接器的 L7 路径策略:(方法, 匹配方式, 模式)。
|
||||||
# gitea:API 只放 /api/v1/;git 智能 HTTP 只放三条(clone/pull/push
|
# gitea:API 只放 /api/v1/;git 智能 HTTP 只放三条(clone/pull/push
|
||||||
# 的实际路径,见 git http-protocol)。其它路径(Web UI、登录页、
|
# 的实际路径,见 git http-protocol)加裸 .git(round-11 真机修复
|
||||||
|
# 层 4:git 智能 HTTP 的发现请求只发裸 .git 路径,裸路径只放 GET、
|
||||||
|
# 一律当 pull;POST 裸 .git 仍拒绝)。其它路径(Web UI、登录页、
|
||||||
# 任意管理接口)一律不在本连接器的能力内。
|
# 任意管理接口)一律不在本连接器的能力内。
|
||||||
_CONNECTOR_PATH_RULES = {
|
_CONNECTOR_PATH_RULES = {
|
||||||
"gitea": (
|
"gitea": (
|
||||||
("GET", "prefix", "/api/v1/"),
|
("GET", "prefix", "/api/v1/"),
|
||||||
("POST", "prefix", "/api/v1/"),
|
("POST", "prefix", "/api/v1/"),
|
||||||
("GET", "suffix", ".git/info/refs"),
|
("GET", "suffix", ".git/info/refs"),
|
||||||
|
("GET", "suffix", ".git"),
|
||||||
("POST", "suffix", ".git/git-upload-pack"),
|
("POST", "suffix", ".git/git-upload-pack"),
|
||||||
("POST", "suffix", ".git/git-receive-pack"),
|
("POST", "suffix", ".git/git-receive-pack"),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -732,6 +732,31 @@ class ChvSyncTest(unittest.TestCase):
|
|||||||
r = self._sync("boot", env_extra={"CHV_GUEST_ENV": env_file})
|
r = self._sync("boot", env_extra={"CHV_GUEST_ENV": env_file})
|
||||||
self.assertNotEqual(r.returncode, 0)
|
self.assertNotEqual(r.returncode, 0)
|
||||||
|
|
||||||
|
def test_boot_without_authd_fails_closed_no_credentials_file(self):
|
||||||
|
# round-11 真机修复(层 5):authd 不在时 boot 直接失败,
|
||||||
|
# 绝不把 token 写进 ~/.git-credentials(旧落盘回落已废止;
|
||||||
|
# 即使 guest-env 里有 token 也不写)。
|
||||||
|
if os.getuid() != 0:
|
||||||
|
self.skipTest("boot 须以 root 运行")
|
||||||
|
env_file = os.path.join(self.dir, "guest-env")
|
||||||
|
with open(env_file, "w") as fh:
|
||||||
|
fh.write("GITEA_URL=https://gitea.tose.top\n"
|
||||||
|
"GITEA_USER=admin\n"
|
||||||
|
"SYNC_REPO=admin/chv-sync-test\n"
|
||||||
|
"GITEA_TOKEN=fake-token-for-test\n")
|
||||||
|
home = os.path.join(self.dir, "home")
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
r = self._sync("boot", env_extra={
|
||||||
|
"CHV_GUEST_ENV": env_file,
|
||||||
|
"CHV_USER_HOME": home,
|
||||||
|
"CHV_USER": "root",
|
||||||
|
"CHV_AUTHD_SOCKET": os.path.join(self.dir, "no-authd.sock"),
|
||||||
|
})
|
||||||
|
self.assertNotEqual(r.returncode, 0)
|
||||||
|
self.assertIn("chv-authd 未运行", r.stderr)
|
||||||
|
self.assertFalse(
|
||||||
|
os.path.exists(os.path.join(home, ".git-credentials")))
|
||||||
|
|
||||||
|
|
||||||
# -- (6) 中途死亡 → interrupted → 断点续跑闭环 ---------------------------
|
# -- (6) 中途死亡 → interrupted → 断点续跑闭环 ---------------------------
|
||||||
class MidDeathLoopTest(ChvGapCase):
|
class MidDeathLoopTest(ChvGapCase):
|
||||||
|
|||||||
+198
-80
@@ -43,7 +43,15 @@ class CellArgvTest(unittest.TestCase):
|
|||||||
self.assertIn("--machine=chv-cell", argv)
|
self.assertIn("--machine=chv-cell", argv)
|
||||||
self.assertIn("--directory=/", argv)
|
self.assertIn("--directory=/", argv)
|
||||||
self.assertIn("--as-pid2", argv)
|
self.assertIn("--as-pid2", argv)
|
||||||
self.assertIn("--no-new-privileges", argv)
|
# round-11 真机修复:nspawn 255 拒裸 --no-new-privileges
|
||||||
|
#(当作带必填参数的选项)与裸 --directory=/(须
|
||||||
|
# --volatile/--ephemeral)。复测纠正:--ephemeral 在客内
|
||||||
|
# ext4 根盘上做整盘快照拷贝,2.4G 盘只剩 593M 即 ENOSPC,
|
||||||
|
# 改 --volatile=yes(tmpfs 根 + 只读 /usr,不拷盘)。
|
||||||
|
self.assertIn("--no-new-privileges=yes", argv)
|
||||||
|
self.assertNotIn("--no-new-privileges", argv)
|
||||||
|
self.assertIn("--volatile=yes", argv)
|
||||||
|
self.assertNotIn("--ephemeral", argv)
|
||||||
|
|
||||||
def test_muse_named_capabilities_dropped(self):
|
def test_muse_named_capabilities_dropped(self):
|
||||||
argv = CELL.build_nspawn_argv(["true"])
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
@@ -70,10 +78,17 @@ class CellArgvTest(unittest.TestCase):
|
|||||||
playwright_package_dir="/usr/local/lib/python3.12/dist-packages/playwright")
|
playwright_package_dir="/usr/local/lib/python3.12/dist-packages/playwright")
|
||||||
hidden = [a.split("=", 1)[1] for a in argv
|
hidden = [a.split("=", 1)[1] for a in argv
|
||||||
if a.startswith("--inaccessible=")]
|
if a.startswith("--inaccessible=")]
|
||||||
for path in ("/etc/chv", "/home/chv/.git-credentials",
|
# /usr 下的浏览器路径仍遮蔽(/usr 只读挂进单元,遮蔽有效)。
|
||||||
"/usr/local/share/ms-playwright",
|
for path in ("/usr/local/share/ms-playwright",
|
||||||
"/usr/local/lib/python3.12/dist-packages/playwright"):
|
"/usr/local/lib/python3.12/dist-packages/playwright"):
|
||||||
self.assertIn(path, hidden)
|
self.assertIn(path, hidden)
|
||||||
|
# round-11 复测纠正(第二轮):/etc/chv 与
|
||||||
|
# /home/chv/.git-credentials 不再列出——--volatile=yes 下
|
||||||
|
# 容器根是基本空的 tmpfs,这两处在容器根里根本不存在,
|
||||||
|
# 凭证文件按构造即不可达;列出反而让 nspawn 解析失败
|
||||||
|
#(Failed to resolve)而起不来。
|
||||||
|
for path in ("/etc/chv", "/home/chv/.git-credentials"):
|
||||||
|
self.assertNotIn(path, hidden)
|
||||||
|
|
||||||
def test_writable_binds_and_tmpfs(self):
|
def test_writable_binds_and_tmpfs(self):
|
||||||
argv = CELL.build_nspawn_argv(["true"])
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
@@ -340,26 +355,27 @@ class BrowserTest(unittest.TestCase):
|
|||||||
ok, _ = BROWSER.validate_url(bad)
|
ok, _ = BROWSER.validate_url(bad)
|
||||||
self.assertFalse(ok, bad)
|
self.assertFalse(ok, bad)
|
||||||
|
|
||||||
def test_truncate_a11y_small_tree_untouched(self):
|
def test_truncate_aria_small_snapshot_untouched(self):
|
||||||
tree = {"role": "WebArea", "name": "Example",
|
text = ("- generic [ref=e1]:\n"
|
||||||
"children": [{"role": "heading", "name": "Hi"}]}
|
" - heading \"Example Domain\" [level=1] [ref=e2]")
|
||||||
out, truncated = BROWSER.truncate_a11y(tree)
|
out, truncated = BROWSER.truncate_aria(text)
|
||||||
self.assertFalse(truncated)
|
self.assertFalse(truncated)
|
||||||
self.assertEqual(out, tree)
|
self.assertEqual(out, text)
|
||||||
|
|
||||||
def test_truncate_a11y_node_cap(self):
|
def test_truncate_aria_node_cap(self):
|
||||||
tree = {"role": "WebArea",
|
text = "\n".join("- link \"l%d\" [ref=e%d]" % (i, i)
|
||||||
"children": [{"role": "link", "name": "l%d" % i}
|
for i in range(300))
|
||||||
for i in range(300)]}
|
out, truncated = BROWSER.truncate_aria(text)
|
||||||
out, truncated = BROWSER.truncate_a11y(tree)
|
|
||||||
self.assertTrue(truncated)
|
self.assertTrue(truncated)
|
||||||
self.assertLessEqual(len(out["children"]), 200)
|
nodes = [ln for ln in out.splitlines()
|
||||||
|
if ln.lstrip().startswith("- ")]
|
||||||
|
self.assertLessEqual(len(nodes), 200)
|
||||||
|
|
||||||
def test_truncate_a11y_char_cap(self):
|
def test_truncate_aria_char_cap(self):
|
||||||
tree = {"role": "WebArea", "name": "x" * 70000, "children": []}
|
text = "- paragraph \"" + "x" * 70000 + "\" [ref=e1]"
|
||||||
out, truncated = BROWSER.truncate_a11y(tree)
|
out, truncated = BROWSER.truncate_aria(text)
|
||||||
self.assertTrue(truncated)
|
self.assertTrue(truncated)
|
||||||
self.assertTrue(out.get("truncated_chars"))
|
self.assertLessEqual(len(out), BROWSER.MAX_A11Y_CHARS)
|
||||||
|
|
||||||
def test_forbidden_ops_never_execute(self):
|
def test_forbidden_ops_never_execute(self):
|
||||||
agent = mock.Mock()
|
agent = mock.Mock()
|
||||||
@@ -397,7 +413,11 @@ class BrowserTest(unittest.TestCase):
|
|||||||
self.assertTrue(resp["ok"])
|
self.assertTrue(resp["ok"])
|
||||||
self.assertEqual(resp["title"], "Example Domain")
|
self.assertEqual(resp["title"], "Example Domain")
|
||||||
self.assertEqual(resp["url"], "https://example.com/")
|
self.assertEqual(resp["url"], "https://example.com/")
|
||||||
self.assertEqual(resp["a11y"]["role"], "WebArea")
|
# round-11 复测纠正(第二轮):a11y 是 ARIA 快照字符串
|
||||||
|
#(Playwright Page.aria_snapshot()),不是 dict 树。
|
||||||
|
self.assertIsInstance(resp["a11y"], str)
|
||||||
|
self.assertIn("Example Domain", resp["a11y"])
|
||||||
|
self.assertFalse(resp["truncated"])
|
||||||
snap = agent.snapshot()
|
snap = agent.snapshot()
|
||||||
self.assertTrue(snap["ok"])
|
self.assertTrue(snap["ok"])
|
||||||
shot = agent.screenshot()
|
shot = agent.screenshot()
|
||||||
@@ -406,6 +426,65 @@ class BrowserTest(unittest.TestCase):
|
|||||||
self.assertTrue(os.path.exists(shot["path"]))
|
self.assertTrue(os.path.exists(shot["path"]))
|
||||||
self.assertTrue(agent.close()["ok"])
|
self.assertTrue(agent.close()["ok"])
|
||||||
|
|
||||||
|
def test_load_playwright_returns_called_cm(self):
|
||||||
|
# round-11 复测回归:真实 _load_playwright 曾返回裸
|
||||||
|
# sync_playwright 函数(未调用),_ensure_browser 对它调
|
||||||
|
# __enter__ 即 AttributeError,open 永远失败;单测的假工厂
|
||||||
|
# 直接返回带 __enter__ 的对象,盖不住这个形状。现
|
||||||
|
# _load_playwright 必须返回调用后的上下文管理器。
|
||||||
|
import types
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class FakeCM:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
return False
|
||||||
|
|
||||||
|
cm = FakeCM()
|
||||||
|
|
||||||
|
def sync_playwright():
|
||||||
|
calls.append("called")
|
||||||
|
return cm
|
||||||
|
|
||||||
|
fake_api = types.ModuleType("playwright.sync_api")
|
||||||
|
fake_api.sync_playwright = sync_playwright
|
||||||
|
fake_pkg = types.ModuleType("playwright")
|
||||||
|
fake_pkg.sync_api = fake_api
|
||||||
|
sys.modules["playwright"] = fake_pkg
|
||||||
|
sys.modules["playwright.sync_api"] = fake_api
|
||||||
|
self.addCleanup(sys.modules.pop, "playwright.sync_api", None)
|
||||||
|
self.addCleanup(sys.modules.pop, "playwright", None)
|
||||||
|
self.assertIs(BROWSER._load_playwright(), cm)
|
||||||
|
self.assertEqual(calls, ["called"])
|
||||||
|
|
||||||
|
def test_browser_op_dispatches_on_action(self):
|
||||||
|
# round-11 复测回归:broker 原样转发 {"op": "browser", …},
|
||||||
|
# chv-browser 白名单却不认 "browser"。内层 op 在 action 里;
|
||||||
|
# 无 action 但带 url 时默认 open(驱动只发 url)。
|
||||||
|
agent = mock.Mock()
|
||||||
|
agent.open.return_value = {"ok": True}
|
||||||
|
resp = BROWSER.handle_request(
|
||||||
|
agent, {"op": "browser", "action": "open",
|
||||||
|
"url": "https://example.com"})
|
||||||
|
self.assertTrue(resp["ok"])
|
||||||
|
agent.open.assert_called_once_with("https://example.com")
|
||||||
|
agent.reset_mock()
|
||||||
|
resp = BROWSER.handle_request(
|
||||||
|
agent, {"op": "browser", "url": "https://example.com"})
|
||||||
|
self.assertTrue(resp["ok"])
|
||||||
|
agent.open.assert_called_once_with("https://example.com")
|
||||||
|
# 内层 forbidden 同样拒绝。
|
||||||
|
agent.reset_mock()
|
||||||
|
resp = BROWSER.handle_request(
|
||||||
|
agent, {"op": "browser", "action": "evaluate"})
|
||||||
|
self.assertFalse(resp["ok"])
|
||||||
|
agent.open.assert_not_called()
|
||||||
|
# 无 action 也无 url:无法推断内层 op。
|
||||||
|
resp = BROWSER.handle_request(agent, {"op": "browser"})
|
||||||
|
self.assertFalse(resp["ok"])
|
||||||
|
|
||||||
|
|
||||||
class FakePage:
|
class FakePage:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -421,13 +500,13 @@ class FakePage:
|
|||||||
def title(self):
|
def title(self):
|
||||||
return "Example Domain"
|
return "Example Domain"
|
||||||
|
|
||||||
@property
|
# 真实 Playwright 1.64.1 的快照入口是 Page.aria_snapshot()
|
||||||
def accessibility(self):
|
#(返回字符串);旧 fake 的 accessibility.snapshot() 是
|
||||||
return self
|
# Puppeteer 的形状,真实库里不存在,round-11 第二轮复测
|
||||||
|
# 已证实会报 'Page' object has no attribute 'accessibility'。
|
||||||
def snapshot(self):
|
def aria_snapshot(self):
|
||||||
return {"role": "WebArea", "name": "Example Domain",
|
return ("- generic [ref=e1]:\n"
|
||||||
"children": [{"role": "heading", "name": "Example"}]}
|
" - heading \"Example Domain\" [level=1] [ref=e2]")
|
||||||
|
|
||||||
def screenshot(self, path=None):
|
def screenshot(self, path=None):
|
||||||
with open(path, "wb") as fh:
|
with open(path, "wb") as fh:
|
||||||
@@ -557,71 +636,110 @@ class BuildInfoTest(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class BuildDryRunTest(unittest.TestCase):
|
class BuildDryRunTest(unittest.TestCase):
|
||||||
|
def _fixture(self, tmp, with_vsock=False):
|
||||||
|
# 隔离的临时 git 仓:工作树干净、HEAD 与 REPO_COMMIT
|
||||||
|
# 一致、假 codex 二进制报对版本。with_vsock 时一并拷入
|
||||||
|
# vsock_notify.py(round-11 真机修复:服务须随文件建)。
|
||||||
|
repo = os.path.join(tmp, "repo")
|
||||||
|
guest = os.path.join(repo, "chv-guest")
|
||||||
|
bindir = os.path.join(tmp, "bin")
|
||||||
|
os.makedirs(guest)
|
||||||
|
os.makedirs(os.path.join(bindir, "codex-resources"))
|
||||||
|
for name in ("build.sh", "buildinfo.py", "check_lockfile.py",
|
||||||
|
"generate_lockfile.py", "chv-sync.sh",
|
||||||
|
"chv-cell.py", "chv-broker.py", "chv-browser.py",
|
||||||
|
"authd.py", "git-credential-chv-authd",
|
||||||
|
"chv-authd.service", "authd_init_stub.py"):
|
||||||
|
shutil.copy2(ROOT / "chv-guest" / name,
|
||||||
|
os.path.join(guest, name))
|
||||||
|
shutil.copy2(ROOT / "host.py", os.path.join(repo, "host.py"))
|
||||||
|
# round-11:build.sh 要求 P3 云端叶子模块在仓里(fail-fast),
|
||||||
|
# 夹具须一并拷入,否则干跑在 P3 预检就失败。
|
||||||
|
for rel in ("cloud/chv/authd.py", "cloud/chv/sentinel.py",
|
||||||
|
"cloud/chv/errors.py",
|
||||||
|
"cloud/__init__.py", "cloud/chv/__init__.py"):
|
||||||
|
dest = os.path.join(repo, rel)
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
shutil.copy2(ROOT / rel, dest)
|
||||||
|
if with_vsock:
|
||||||
|
rel = "cloud/chv/guest/vsock_notify.py"
|
||||||
|
dest = os.path.join(repo, rel)
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
shutil.copy2(ROOT / rel, dest)
|
||||||
|
codex = os.path.join(bindir, "codex")
|
||||||
|
with open(codex, "w") as fh:
|
||||||
|
fh.write("#!/bin/sh\necho 'codex-cli 0.164.0-alpha.9'\n")
|
||||||
|
os.chmod(codex, 0o755)
|
||||||
|
host_bin = os.path.join(bindir, "codex-code-mode-host")
|
||||||
|
with open(host_bin, "w") as fh:
|
||||||
|
fh.write("#!/bin/sh\nexit 0\n")
|
||||||
|
os.chmod(host_bin, 0o755)
|
||||||
|
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||||
|
subprocess.run(["git", "add", "-A"], cwd=repo, check=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "-c", "user.email=t@t", "-c", "user.name=t",
|
||||||
|
"commit", "-qm", "p2 dry-run fixture"],
|
||||||
|
cwd=repo, check=True)
|
||||||
|
head = subprocess.run(
|
||||||
|
["git", "rev-parse", "--short", "HEAD"], cwd=repo,
|
||||||
|
capture_output=True, text=True, check=True).stdout.strip()
|
||||||
|
env = dict(os.environ)
|
||||||
|
env.update({
|
||||||
|
"CODEX_BIN_DIR": bindir,
|
||||||
|
"UBUNTU_VERSION": "24.04",
|
||||||
|
"CODEX_VERSION": "0.164.0-alpha.9",
|
||||||
|
"REPO_COMMIT": head,
|
||||||
|
"UBUNTU_IMAGE_SHA256": "a" * 64,
|
||||||
|
})
|
||||||
|
return repo, guest, env
|
||||||
|
|
||||||
|
def _dry_run(self, repo, guest, env):
|
||||||
|
proc = subprocess.run(
|
||||||
|
["bash", os.path.join(guest, "build.sh"),
|
||||||
|
"--dry-run", "out"],
|
||||||
|
cwd=repo, capture_output=True, text=True, env=env)
|
||||||
|
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||||
|
return proc.stdout + proc.stderr
|
||||||
|
|
||||||
def test_dry_run_contains_p2_steps(self):
|
def test_dry_run_contains_p2_steps(self):
|
||||||
# 在隔离的临时 git 仓里跑 build.sh --dry-run:工作树干净、
|
# 在隔离的临时 git 仓里跑 build.sh --dry-run。
|
||||||
# HEAD 与 REPO_COMMIT 一致、假 codex 二进制报对版本。
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
repo = os.path.join(tmp, "repo")
|
repo, guest, env = self._fixture(tmp)
|
||||||
guest = os.path.join(repo, "chv-guest")
|
out = self._dry_run(repo, guest, env)
|
||||||
bindir = os.path.join(tmp, "bin")
|
|
||||||
os.makedirs(guest)
|
|
||||||
os.makedirs(os.path.join(bindir, "codex-resources"))
|
|
||||||
for name in ("build.sh", "buildinfo.py", "check_lockfile.py",
|
|
||||||
"generate_lockfile.py", "chv-sync.sh",
|
|
||||||
"chv-cell.py", "chv-broker.py", "chv-browser.py",
|
|
||||||
"authd.py", "git-credential-chv-authd",
|
|
||||||
"chv-authd.service"):
|
|
||||||
shutil.copy2(ROOT / "chv-guest" / name,
|
|
||||||
os.path.join(guest, name))
|
|
||||||
shutil.copy2(ROOT / "host.py", os.path.join(repo, "host.py"))
|
|
||||||
# round-11:build.sh 要求 P3 云端叶子模块在仓里(fail-fast),
|
|
||||||
# 夹具须一并拷入,否则干跑在 P3 预检就失败。
|
|
||||||
for rel in ("cloud/chv/authd.py", "cloud/chv/sentinel.py",
|
|
||||||
"cloud/__init__.py", "cloud/chv/__init__.py"):
|
|
||||||
dest = os.path.join(repo, rel)
|
|
||||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
||||||
shutil.copy2(ROOT / rel, dest)
|
|
||||||
codex = os.path.join(bindir, "codex")
|
|
||||||
with open(codex, "w") as fh:
|
|
||||||
fh.write("#!/bin/sh\necho 'codex-cli 0.164.0-alpha.9'\n")
|
|
||||||
os.chmod(codex, 0o755)
|
|
||||||
host_bin = os.path.join(bindir, "codex-code-mode-host")
|
|
||||||
with open(host_bin, "w") as fh:
|
|
||||||
fh.write("#!/bin/sh\nexit 0\n")
|
|
||||||
os.chmod(host_bin, 0o755)
|
|
||||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
||||||
subprocess.run(["git", "add", "-A"], cwd=repo, check=True)
|
|
||||||
subprocess.run(
|
|
||||||
["git", "-c", "user.email=t@t", "-c", "user.name=t",
|
|
||||||
"commit", "-qm", "p2 dry-run fixture"],
|
|
||||||
cwd=repo, check=True)
|
|
||||||
head = subprocess.run(
|
|
||||||
["git", "rev-parse", "--short", "HEAD"], cwd=repo,
|
|
||||||
capture_output=True, text=True, check=True).stdout.strip()
|
|
||||||
env = dict(os.environ)
|
|
||||||
env.update({
|
|
||||||
"CODEX_BIN_DIR": bindir,
|
|
||||||
"UBUNTU_VERSION": "24.04",
|
|
||||||
"CODEX_VERSION": "0.164.0-alpha.9",
|
|
||||||
"REPO_COMMIT": head,
|
|
||||||
"UBUNTU_IMAGE_SHA256": "a" * 64,
|
|
||||||
})
|
|
||||||
proc = subprocess.run(
|
|
||||||
["bash", os.path.join(guest, "build.sh"),
|
|
||||||
"--dry-run", "out"],
|
|
||||||
cwd=repo, capture_output=True, text=True, env=env)
|
|
||||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
||||||
out = proc.stdout + proc.stderr
|
|
||||||
for needle in ("systemd-container", "chv-cell", "chv-broker",
|
for needle in ("systemd-container", "chv-cell", "chv-broker",
|
||||||
"chv-browser", "chv-browser.service",
|
"chv-browser", "chv-browser.service",
|
||||||
"chv-broker.service", "useradd --system",
|
"chv-broker.service", "useradd --system",
|
||||||
"systemctl enable chv-broker chv-browser",
|
"systemctl enable chv-broker chv-browser",
|
||||||
"chv-authd", "git-credential-chv-authd",
|
"chv-authd", "git-credential-chv-authd",
|
||||||
"systemctl enable chv-authd",
|
"systemctl enable chv-authd",
|
||||||
"systemctl start chv-authd"):
|
"systemctl start chv-authd",
|
||||||
|
# round-11 真机修复(缺陷 4):browser.sock
|
||||||
|
# 移进 chv-browser 自有目录,build.sh 须
|
||||||
|
# 建目录 + tmpfiles 登记。
|
||||||
|
"/run/chv-browser",
|
||||||
|
# round-11 真机修复(层 1):叶子树含
|
||||||
|
# errors.py + stub __init__,不拷真实的
|
||||||
|
# cloud/chv/__init__.py。
|
||||||
|
"errors.py", "authd_init_stub.py"):
|
||||||
self.assertIn(needle, out, needle)
|
self.assertIn(needle, out, needle)
|
||||||
# P1 文件不在本分支:干跑只警告、不失败。
|
# P1 文件不在本分支:干跑只警告、不失败。
|
||||||
self.assertIn("vsock_notify.py 不在仓库里", out)
|
self.assertIn("vsock_notify.py 不在仓库里", out)
|
||||||
|
# round-11 真机修复(层 5):镜像不再设 store helper
|
||||||
|
#(authd helper 是唯一凭证 helper),git user 仍配。
|
||||||
|
self.assertNotIn("credential.helper store", out)
|
||||||
|
self.assertIn("git config --system user.name chv", out)
|
||||||
|
|
||||||
|
def test_dry_run_with_vsock_creates_service(self):
|
||||||
|
# round-11 真机修复(vsock 自动启动):vsock_notify.py
|
||||||
|
# 在场时建 vsock-notify.service 并 enable,不再只警告。
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
repo, guest, env = self._fixture(tmp, with_vsock=True)
|
||||||
|
out = self._dry_run(repo, guest, env)
|
||||||
|
self.assertNotIn("vsock_notify.py 不在仓库里", out)
|
||||||
|
for needle in ("vsock-notify.service",
|
||||||
|
"systemctl enable vsock-notify",
|
||||||
|
"ExecStart=/usr/local/bin/vsock-notify"):
|
||||||
|
self.assertIn(needle, out, needle)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -271,14 +271,18 @@ class SentinelTest(unittest.TestCase):
|
|||||||
(self._req(port=80), "port_not_allowed"),
|
(self._req(port=80), "port_not_allowed"),
|
||||||
(self._req(host="bad.example"), "host_denied"),
|
(self._req(host="bad.example"), "host_denied"),
|
||||||
(self._req(path="/login"), "path_not_allowed"),
|
(self._req(path="/login"), "path_not_allowed"),
|
||||||
|
# round-11 真机修复(层 4):裸 .git 只放 GET,POST 仍拒。
|
||||||
|
(self._req(method="POST", path="/admin/r.git"),
|
||||||
|
"path_not_allowed"),
|
||||||
]
|
]
|
||||||
for req, reason in cases:
|
for req, reason in cases:
|
||||||
dec = policy.evaluate(req, resolve=_public_resolver)
|
dec = policy.evaluate(req, resolve=_public_resolver)
|
||||||
self.assertEqual(dec.verdict, "deny", reason)
|
self.assertEqual(dec.verdict, "deny", reason)
|
||||||
self.assertEqual(dec.reasons, [reason])
|
self.assertEqual(dec.reasons, [reason])
|
||||||
# git 智能 HTTP 三条路径放行。
|
# git 智能 HTTP 三条路径放行,裸 .git 只放 GET(层 4)。
|
||||||
for method, path in (
|
for method, path in (
|
||||||
("GET", "/admin/r.git/info/refs"),
|
("GET", "/admin/r.git/info/refs"),
|
||||||
|
("GET", "/admin/r.git"),
|
||||||
("POST", "/admin/r.git/git-upload-pack"),
|
("POST", "/admin/r.git/git-upload-pack"),
|
||||||
("POST", "/admin/r.git/git-receive-pack")):
|
("POST", "/admin/r.git/git-receive-pack")):
|
||||||
dec = policy.evaluate(
|
dec = policy.evaluate(
|
||||||
@@ -367,6 +371,49 @@ class SeedSplitTest(unittest.TestCase):
|
|||||||
self.assertNotIn("GITEA_TOKEN",
|
self.assertNotIn("GITEA_TOKEN",
|
||||||
entries["/etc/chv/guest-env"]["content"])
|
entries["/etc/chv/guest-env"]["content"])
|
||||||
|
|
||||||
|
def test_write_seed_authd_entry_carries_sync_keys(self):
|
||||||
|
# round-11 真机修复(层 2):authd-seed 须同时有
|
||||||
|
# GITEA_URL/GITEA_USER/SYNC_REPO,build_server 才会放行
|
||||||
|
#(只 GITEA_TOKEN 时 ValueError,探针 fixprobe3 即从
|
||||||
|
# guest-env 补这三键);三键只在 guest-env 里有才抄。
|
||||||
|
import yaml
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
seed_dir = os.path.join(tmp, "seed")
|
||||||
|
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
|
||||||
|
guest_env={
|
||||||
|
"GITEA_URL": "https://gitea.tose.top",
|
||||||
|
"GITEA_USER": "admin",
|
||||||
|
"SYNC_REPO": "admin/chv-sync-test",
|
||||||
|
"SYNC_BRANCH": "main"},
|
||||||
|
authd_env={"GITEA_TOKEN": TOKEN})
|
||||||
|
with open(os.path.join(seed_dir, "user-data")) as fh:
|
||||||
|
parsed = yaml.safe_load(fh.read())
|
||||||
|
entries = {e["path"]: e for e in parsed["write_files"]}
|
||||||
|
content = entries[AUTHD_SEED_PATH]["content"]
|
||||||
|
for key in ("GITEA_URL=https://gitea.tose.top",
|
||||||
|
"GITEA_USER=admin",
|
||||||
|
"SYNC_REPO=admin/chv-sync-test",
|
||||||
|
"GITEA_TOKEN="):
|
||||||
|
self.assertIn(key, content)
|
||||||
|
# 非同步键不进 authd-seed。
|
||||||
|
self.assertNotIn("SYNC_BRANCH", content)
|
||||||
|
|
||||||
|
def test_write_seed_authd_entry_without_sync_keys(self):
|
||||||
|
# guest-env 里没有三键时 authd-seed 只装密钥,不编默认值。
|
||||||
|
import yaml
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
seed_dir = os.path.join(tmp, "seed")
|
||||||
|
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
|
||||||
|
authd_env={"GITEA_TOKEN": TOKEN})
|
||||||
|
with open(os.path.join(seed_dir, "user-data")) as fh:
|
||||||
|
parsed = yaml.safe_load(fh.read())
|
||||||
|
entries = {e["path"]: e for e in parsed["write_files"]}
|
||||||
|
content = entries[AUTHD_SEED_PATH]["content"]
|
||||||
|
self.assertIn("GITEA_TOKEN=", content)
|
||||||
|
self.assertNotIn("GITEA_URL", content)
|
||||||
|
self.assertNotIn("GITEA_USER", content)
|
||||||
|
self.assertNotIn("SYNC_REPO", content)
|
||||||
|
|
||||||
|
|
||||||
# -- (4) wake 预审 -----------------------------------------------------------
|
# -- (4) wake 预审 -----------------------------------------------------------
|
||||||
class _FakeResult:
|
class _FakeResult:
|
||||||
@@ -555,6 +602,28 @@ class GuestAuthdTest(unittest.TestCase):
|
|||||||
self.assertEqual(resp["password"], TOKEN)
|
self.assertEqual(resp["password"], TOKEN)
|
||||||
self.assertGreater(resp["expires_at"], 0)
|
self.assertGreater(resp["expires_at"], 0)
|
||||||
|
|
||||||
|
def test_bare_git_path_is_pull(self):
|
||||||
|
# round-11 真机修复(层 4):裸 .git 路径一律当 pull。
|
||||||
|
resp = self._ask({"connector": "gitea",
|
||||||
|
"host": "gitea.tose.top",
|
||||||
|
"path": "/admin/chv-sync-test.git"})
|
||||||
|
self.assertTrue(resp["ok"])
|
||||||
|
self.assertEqual(resp["username"], "admin")
|
||||||
|
self.assertEqual(resp["password"], TOKEN)
|
||||||
|
|
||||||
|
def test_socket_uses_group_perms(self):
|
||||||
|
# round-11 真机修复(层 3):socket 用 group 权限(目录
|
||||||
|
# 0750、socket 0660),不用探针的 0666;放行 uid 无
|
||||||
|
# passwd 条目时才回落 root-only(见 SocketGroupTest)。
|
||||||
|
import stat
|
||||||
|
mod = _load_guest_authd()
|
||||||
|
if mod._socket_group_gid([os.getuid()]) is None:
|
||||||
|
self.skipTest("放行 uid 无 passwd 条目,组权限不可解析")
|
||||||
|
self.assertEqual(
|
||||||
|
stat.S_IMODE(os.stat(self.socket_path).st_mode), 0o660)
|
||||||
|
self.assertEqual(
|
||||||
|
stat.S_IMODE(os.stat(self.dir).st_mode), 0o750)
|
||||||
|
|
||||||
def test_second_request_gets_fresh_release(self):
|
def test_second_request_gets_fresh_release(self):
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
resp = self._ask({"connector": "gitea",
|
resp = self._ask({"connector": "gitea",
|
||||||
@@ -612,6 +681,60 @@ class GuestAuthdTest(unittest.TestCase):
|
|||||||
resolve=_public_resolver)
|
resolve=_public_resolver)
|
||||||
|
|
||||||
|
|
||||||
|
class SocketGroupTest(unittest.TestCase):
|
||||||
|
"""round-11 真机修复(层 3):socket 共享组 gid 的派生与回落。"""
|
||||||
|
|
||||||
|
def test_explicit_group_and_derivation(self):
|
||||||
|
mod = _load_guest_authd()
|
||||||
|
# 显式 int 即 gid;显式组名解析;不存在的组名 → None。
|
||||||
|
self.assertEqual(mod._socket_group_gid([0, 1000], 1234), 1234)
|
||||||
|
self.assertEqual(mod._socket_group_gid([0, 1000], "root"), 0)
|
||||||
|
self.assertIsNone(
|
||||||
|
mod._socket_group_gid([0, 1000], "no-such-group-xyz"))
|
||||||
|
# 只剩 root → 无组可派,回落 root-only。
|
||||||
|
self.assertIsNone(mod._socket_group_gid([0]))
|
||||||
|
self.assertIsNone(mod._socket_group_gid([]))
|
||||||
|
# 派生:第一个非 root 放行 uid 的主组(用 passwd 里第一个
|
||||||
|
# 非 root 条目,不依赖当前测试进程的 uid——本机测试以 root
|
||||||
|
# 跑,os.getuid() 本身就是 0,派生不到组)。
|
||||||
|
import pwd
|
||||||
|
others = [p for p in pwd.getpwall() if p.pw_uid != 0]
|
||||||
|
if not others:
|
||||||
|
self.skipTest("passwd 里无非 root 用户")
|
||||||
|
first = sorted(others, key=lambda p: p.pw_uid)[0]
|
||||||
|
self.assertEqual(
|
||||||
|
mod._socket_group_gid([0, first.pw_uid]), first.pw_gid)
|
||||||
|
|
||||||
|
def test_server_applies_group_perms_with_explicit_group(self):
|
||||||
|
# 显式给组时服务器落地 0750/0660(本机以 root 跑测试,
|
||||||
|
# allowed_uids 只有 root,派生不到组,须显式给才能覆到
|
||||||
|
# AuthdServer 的落盘路径)。
|
||||||
|
import stat
|
||||||
|
mod = _load_guest_authd()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
seed_path = os.path.join(tmp, "authd-seed")
|
||||||
|
with open(seed_path, "w") as fh:
|
||||||
|
fh.write("GITEA_URL=https://gitea.tose.top\n"
|
||||||
|
"GITEA_USER=admin\n"
|
||||||
|
"GITEA_TOKEN=" + TOKEN + "\n"
|
||||||
|
"SYNC_REPO=admin/chv-sync-test\n")
|
||||||
|
socket_path = os.path.join(tmp, "authd.sock")
|
||||||
|
log_path = os.path.join(tmp, "authd.log")
|
||||||
|
server = mod.build_server(
|
||||||
|
seed_path=seed_path, socket_path=socket_path,
|
||||||
|
log_path=log_path, lib_dir=str(REPO_ROOT),
|
||||||
|
allowed_uids=[os.getuid()], resolve=_public_resolver,
|
||||||
|
socket_group=os.getgid())
|
||||||
|
try:
|
||||||
|
self.assertEqual(server.socket_gid, os.getgid())
|
||||||
|
self.assertEqual(
|
||||||
|
stat.S_IMODE(os.stat(socket_path).st_mode), 0o660)
|
||||||
|
self.assertEqual(
|
||||||
|
stat.S_IMODE(os.stat(tmp).st_mode), 0o750)
|
||||||
|
finally:
|
||||||
|
server.server_close()
|
||||||
|
|
||||||
|
|
||||||
class HelperE2ETest(unittest.TestCase):
|
class HelperE2ETest(unittest.TestCase):
|
||||||
"""helper + 真 git credential fill(假 authd socket 在后台)。"""
|
"""helper + 真 git credential fill(假 authd socket 在后台)。"""
|
||||||
|
|
||||||
@@ -656,6 +779,19 @@ class HelperE2ETest(unittest.TestCase):
|
|||||||
self.assertEqual(proc.returncode, 1)
|
self.assertEqual(proc.returncode, 1)
|
||||||
self.assertIn("path_not_allowed", proc.stderr)
|
self.assertIn("path_not_allowed", proc.stderr)
|
||||||
|
|
||||||
|
def test_helper_returns_credential_for_bare_git_path(self):
|
||||||
|
# round-11 真机修复(层 4):裸 .git 路径一律当 pull,
|
||||||
|
# helper 须照常返回凭证(git 智能 HTTP 发现请求只发裸路径)。
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, str(self.HELPER)],
|
||||||
|
input="protocol=https\nhost=gitea.tose.top\n"
|
||||||
|
"path=/admin/chv-sync-test.git\n\n",
|
||||||
|
capture_output=True, text=True, env=self._helper_env(),
|
||||||
|
check=False)
|
||||||
|
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||||
|
self.assertIn("username=admin", proc.stdout)
|
||||||
|
self.assertIn("password=" + TOKEN, proc.stdout)
|
||||||
|
|
||||||
def test_git_credential_fill_end_to_end(self):
|
def test_git_credential_fill_end_to_end(self):
|
||||||
env = self._helper_env()
|
env = self._helper_env()
|
||||||
env["GIT_CONFIG_NOSYSTEM"] = "1"
|
env["GIT_CONFIG_NOSYSTEM"] = "1"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""round-11 集成补洞:执行器接 nspawn 单元(P2 差距表 §4)。
|
"""round-11 集成补洞:执行器接 nspawn 单元(P2 差距表 §4)。
|
||||||
|
|
||||||
只测 _guest_host_command 的命令构造与 CHV_CELL_ENABLED 开关:
|
只测 _guest_host_command 的命令构造与 CHV_CELL_ENABLED 开关:
|
||||||
单元开(默认)时 host.py 调用改为 chv-cell run -- python3
|
单元开(默认)时 host.py 调用改为 sudo chv-cell run -- python3
|
||||||
/app/host.py --state … --codex-home … --sandbox
|
/app/host.py --state … --codex-home … --sandbox
|
||||||
danger-full-access …;单元关时走原命令;非法开关值即
|
danger-full-access …(sudo 不可省:chv-cell 须 root,执行器以
|
||||||
|
chv 身份调;round-11 真机修复);单元关时走原命令;非法开关值即
|
||||||
ValueError。只用 mock,不启动真实 VM、不连真实网络。
|
ValueError。只用 mock,不启动真实 VM、不连真实网络。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class CellCommandTest(CellCase):
|
|||||||
ex = self.make_exec()
|
ex = self.make_exec()
|
||||||
cmd = ex._guest_host_command("status", "--task", "t1")
|
cmd = ex._guest_host_command("status", "--task", "t1")
|
||||||
self.assertEqual(cmd, [
|
self.assertEqual(cmd, [
|
||||||
"chv-cell", "run", "--",
|
"sudo", "chv-cell", "run", "--",
|
||||||
"python3", "/app/host.py",
|
"python3", "/app/host.py",
|
||||||
"--state", "/home/chv/.codex/host-state.json",
|
"--state", "/home/chv/.codex/host-state.json",
|
||||||
"--codex-home", "/home/chv/.codex",
|
"--codex-home", "/home/chv/.codex",
|
||||||
@@ -93,7 +94,7 @@ class CellCommandTest(CellCase):
|
|||||||
def test_cell_on_explicit_1(self):
|
def test_cell_on_explicit_1(self):
|
||||||
ex = self.make_exec(env_extra={"CHV_CELL_ENABLED": "1"})
|
ex = self.make_exec(env_extra={"CHV_CELL_ENABLED": "1"})
|
||||||
cmd = ex._guest_host_command("status", "--task", "t1")
|
cmd = ex._guest_host_command("status", "--task", "t1")
|
||||||
self.assertEqual(cmd[:3], ["chv-cell", "run", "--"])
|
self.assertEqual(cmd[:4], ["sudo", "chv-cell", "run", "--"])
|
||||||
self.assertIn("--sandbox", cmd)
|
self.assertIn("--sandbox", cmd)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
cmd[cmd.index("--sandbox") + 1], "danger-full-access")
|
cmd[cmd.index("--sandbox") + 1], "danger-full-access")
|
||||||
@@ -120,7 +121,7 @@ class CellCommandTest(CellCase):
|
|||||||
model="m1", effort="low", approval="ask",
|
model="m1", effort="low", approval="ask",
|
||||||
model_provider="sub2api", sandbox="read-only")
|
model_provider="sub2api", sandbox="read-only")
|
||||||
self.assertEqual(cmd, [
|
self.assertEqual(cmd, [
|
||||||
"chv-cell", "run", "--",
|
"sudo", "chv-cell", "run", "--",
|
||||||
"python3", "/app/host.py",
|
"python3", "/app/host.py",
|
||||||
"--state", "/home/chv/.codex/host-state.json",
|
"--state", "/home/chv/.codex/host-state.json",
|
||||||
"--codex-home", "/home/chv/.codex",
|
"--codex-home", "/home/chv/.codex",
|
||||||
@@ -164,12 +165,14 @@ class CellCommandTest(CellCase):
|
|||||||
|
|
||||||
def test_cell_wiring_reaches_ssh_command(self):
|
def test_cell_wiring_reaches_ssh_command(self):
|
||||||
# 接线落到 SSH 层:build_ssh_command 的远端命令里
|
# 接线落到 SSH 层:build_ssh_command 的远端命令里
|
||||||
# chv-cell 前缀在、python3 在其后。
|
# sudo chv-cell 前缀在、python3 在其后。
|
||||||
ex = self.make_exec()
|
ex = self.make_exec()
|
||||||
inner = ex._guest_host_command("status", "--task", "t1")
|
inner = ex._guest_host_command("status", "--task", "t1")
|
||||||
cmd = ex.build_ssh_command({"id": "t1"}, inner)
|
cmd = ex.build_ssh_command({"id": "t1"}, inner)
|
||||||
|
self.assertIn("sudo", cmd)
|
||||||
self.assertIn("chv-cell", cmd)
|
self.assertIn("chv-cell", cmd)
|
||||||
self.assertIn("danger-full-access", cmd)
|
self.assertIn("danger-full-access", cmd)
|
||||||
|
self.assertLess(cmd.index("sudo"), cmd.index("chv-cell"))
|
||||||
self.assertLess(cmd.index("chv-cell"), cmd.index("python3"))
|
self.assertLess(cmd.index("chv-cell"), cmd.index("python3"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -418,7 +418,8 @@ class LockfileTest(unittest.TestCase):
|
|||||||
"openssh-client 1:9.9", "cloud-init 9.9",
|
"openssh-client 1:9.9", "cloud-init 9.9",
|
||||||
"python3 9.9", "python3-pip 9.9", "curl 9.9",
|
"python3 9.9", "python3-pip 9.9", "curl 9.9",
|
||||||
"ca-certificates 9.9", "xz-utils 9.9",
|
"ca-certificates 9.9", "xz-utils 9.9",
|
||||||
"fonts-wqy-microhei 9.9", "fontconfig 9.9"]
|
"fonts-wqy-microhei 9.9", "fontconfig 9.9",
|
||||||
|
"systemd-container 9.9"]
|
||||||
info = {
|
info = {
|
||||||
"tag": "chv-guest-24.04-0.164.0-alpha.9-abc1234",
|
"tag": "chv-guest-24.04-0.164.0-alpha.9-abc1234",
|
||||||
"ubuntu": "24.04",
|
"ubuntu": "24.04",
|
||||||
@@ -432,6 +433,19 @@ class LockfileTest(unittest.TestCase):
|
|||||||
"fonts-wqy-microhei", "fontconfig", "codex",
|
"fonts-wqy-microhei", "fontconfig", "codex",
|
||||||
"codex-code-mode-host", "codex-resources/bwrap",
|
"codex-code-mode-host", "codex-resources/bwrap",
|
||||||
"/app/host.py", "gt", "chv-sync",
|
"/app/host.py", "gt", "chv-sync",
|
||||||
|
# 与 717efc4 真实 BUILDINFO.contents 对齐:
|
||||||
|
# P2 三件套 + P3 authd 三件套 + 叶子模块 +
|
||||||
|
# vsock_notify(缺 DECLARED_SOURCES 登记时
|
||||||
|
# generate 记 unresolved、verify/check 失败,
|
||||||
|
# 合成清单须覆盖才拦得住回归)。round-11
|
||||||
|
# 真机修复(层 1)后叶子模块还含 errors.py
|
||||||
|
# 与 stub __init__.py。
|
||||||
|
"systemd-container", "chv-cell", "chv-broker",
|
||||||
|
"chv-browser", "chv-authd",
|
||||||
|
"git-credential-chv-authd", "chv-authd.service",
|
||||||
|
"cloud/chv/authd.py", "cloud/chv/sentinel.py",
|
||||||
|
"cloud/chv/errors.py", "cloud/chv/__init__.py",
|
||||||
|
"vsock_notify",
|
||||||
"playwright", "chromium-headless-shell"],
|
"playwright", "chromium-headless-shell"],
|
||||||
"versions": {"dpkg": dpkg,
|
"versions": {"dpkg": dpkg,
|
||||||
"pip": ["playwright==9.9.9"],
|
"pip": ["playwright==9.9.9"],
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ CDP 由单元外 broker 管理。
|
|||||||
| 单元内 codex 沙箱 | `host.py` 默认 `sandbox="workspace-write"`(bwrap) | 单元本身就是边界 | 单元内 codex 用 `danger-full-access`:bwrap 的 `--proc /proc` 挂载在嵌套命名空间里是已知走不通的形状(见 AGENTS.md 内层 proc 定位),单元内再套 bwrap 只会假隔离。执行器侧改 `--sandbox` 透传是 P1 的文件,本轨只定义接口(见 §4),不改 `executor.py` |
|
| 单元内 codex 沙箱 | `host.py` 默认 `sandbox="workspace-write"`(bwrap) | 单元本身就是边界 | 单元内 codex 用 `danger-full-access`:bwrap 的 `--proc /proc` 挂载在嵌套命名空间里是已知走不通的形状(见 AGENTS.md 内层 proc 定位),单元内再套 bwrap 只会假隔离。执行器侧改 `--sandbox` 透传是 P1 的文件,本轨只定义接口(见 §4),不改 `executor.py` |
|
||||||
| 单元网络 | 无(与客机共享网络命名空间) | 单元自有虚拟网络接口 | `--network-veth`:宿主侧 `ve-chv-cell`(10.200.0.1/24)、单元侧 `host0`(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站(MASQUERADE 走客机默认路由的上行口,动态发现,不写死口名)。单元内配网经 nsenter 进单元 init 的 netns 做(`chv-cell.py` 内实现)。veth 建不起来即大声失败,不静默回落共享网络;调试逃生口 `CHV_CELL_NETWORK=shared` 须显式设置、打日志,绝不默认 |
|
| 单元网络 | 无(与客机共享网络命名空间) | 单元自有虚拟网络接口 | `--network-veth`:宿主侧 `ve-chv-cell`(10.200.0.1/24)、单元侧 `host0`(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站(MASQUERADE 走客机默认路由的上行口,动态发现,不写死口名)。单元内配网经 nsenter 进单元 init 的 netns 做(`chv-cell.py` 内实现)。veth 建不起来即大声失败,不静默回落共享网络;调试逃生口 `CHV_CELL_NETWORK=shared` 须显式设置、打日志,绝不默认 |
|
||||||
| 单元内外 IPC | 无(SSH 直达,host key 钉死) | Unix socket + SO_PEERCRED + 对端 ACL 的最小权限 IPC | `chv-broker`(客机侧、单元外):Unix socket `/run/chv/broker.sock`(bind 进单元)。SO_PEERCRED 校验:对端 uid 须在单元映射范围 1000–66535,且对端 pid 的 pid ns 须与 `/run/chv/cell.pid` 记的单元 init 同 ns(cell.pid 不存在时只做 uid 检查,并打日志)。操作白名单:`ping`、`sync pull|push|status`(仓目录只允许 `~/workspace` 下,`chv-sync` 以 `chv` 身份跑,不进 shell)、`browser …`(转发给浏览器子代理)。其余一律拒绝 |
|
| 单元内外 IPC | 无(SSH 直达,host key 钉死) | Unix socket + SO_PEERCRED + 对端 ACL 的最小权限 IPC | `chv-broker`(客机侧、单元外):Unix socket `/run/chv/broker.sock`(bind 进单元)。SO_PEERCRED 校验:对端 uid 须在单元映射范围 1000–66535,且对端 pid 的 pid ns 须与 `/run/chv/cell.pid` 记的单元 init 同 ns(cell.pid 不存在时只做 uid 检查,并打日志)。操作白名单:`ping`、`sync pull|push|status`(仓目录只允许 `~/workspace` 下,`chv-sync` 以 `chv` 身份跑,不进 shell)、`browser …`(转发给浏览器子代理)。其余一律拒绝 |
|
||||||
| 浏览器 | Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS | 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 | `chv-browser`(客机侧、单元外独立进程,用户 `chv-browser`):Unix socket `/run/chv/browser.sock`,操作只有 `open|snapshot|screenshot|close`。`open`/`snapshot` 返回标题、URL 与 Playwright `accessibility.snapshot()` 的无障碍树(截断到 200 节点/64KB);`screenshot` 落盘到 `/run/chv/shots/`(bind 进单元,单元只读路径取图)。**不暴露** `evaluate`、`content`(原始 DOM)、DevTools。单元内 playwright 包与浏览器二进制用 `--inaccessible` 遮掉,单元内跑不起自己的浏览器,只能走 broker |
|
| 浏览器 | Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS | 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 | `chv-browser`(客机侧、单元外独立进程,用户 `chv-browser`):Unix socket `/run/chv-browser/browser.sock`,操作只有 `open|snapshot|screenshot|close`。`open`/`snapshot` 返回标题、URL 与 Playwright `Page.aria_snapshot()` 的 ARIA 快照字符串(截断到 200 节点行/64KB);`screenshot` 落盘到 `/run/chv/shots/`(bind 进单元,单元只读路径取图)。**不暴露** `evaluate`、`content`(原始 DOM)、DevTools。单元内 playwright 包与浏览器二进制用 `--inaccessible` 遮掉,单元内跑不起自己的浏览器,只能走 broker |
|
||||||
| 凭证 | `/etc/chv/guest-env`(0600 root)+ `~/.git-credentials`(0600 chv),codex 在客机内直接可读后者 | 凭证由 authd 保管,代理只见替身令牌 | 本轨只做“单元看不见”:`/etc/chv` 与 `~/.git-credentials` 都 `--inaccessible`,同步走 broker 的 `sync` 操作(broker 在单元外跑 `chv-sync`,token 不进单元)。替身化本身是 P3,不在本轨冒充已做 |
|
| 凭证 | `/etc/chv/guest-env`(0600 root)+ `~/.git-credentials`(0600 chv),codex 在客机内直接可读后者 | 凭证由 authd 保管,代理只见替身令牌 | 本轨只做“单元看不见”:`/etc/chv` 与 `~/.git-credentials` 都 `--inaccessible`,同步走 broker 的 `sync` 操作(broker 在单元外跑 `chv-sync`,token 不进单元)。替身化本身是 P3,不在本轨冒充已做 |
|
||||||
| 客机启动同步 | `chv-sync boot` 由 cloud-init runcmd 以 root 跑,clone 到 `~/workspace` | — | 不动:boot clone 仍在单元外做;单元内只经 broker 做 pull/push/status |
|
| 客机启动同步 | `chv-sync boot` 由 cloud-init runcmd 以 root 跑,clone 到 `~/workspace` | — | 不动:boot clone 仍在单元外做;单元内只经 broker 做 pull/push/status |
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -106,10 +106,16 @@ authd 保管、代理只见替身令牌;所有连接器动作与网络出口
|
|||||||
凭证前评估),不是网络层强制。客内任意进程的直连仍由 Tinyproxy
|
凭证前评估),不是网络层强制。客内任意进程的直连仍由 Tinyproxy
|
||||||
白名单 + 宿主防火墙(第三棒)兜底;eBPF cgroup 拦截 + LSM 污点传播
|
白名单 + 宿主防火墙(第三棒)兜底;eBPF cgroup 拦截 + LSM 污点传播
|
||||||
是后续工作,不得写成“出口已全部经 Sentinel”。
|
是后续工作,不得写成“出口已全部经 Sentinel”。
|
||||||
5. **过渡兼容留了旧路径**:`chv-sync boot` 在 authd socket 不存在时仍走
|
5. **过渡兼容已关闭(round-11 真机修复,层 5)**:此前 `chv-sync
|
||||||
旧的 `.git-credentials` 落盘(只多一句 stderr 警告),为的是 lite9/
|
boot` 在 authd socket 不存在时仍走旧的 `.git-credentials` 落盘
|
||||||
lite10 旧镜像不被一次改断。旧镜像退役后应删掉该回落,改为无 socket
|
(只多一句 stderr 警告),为的是 lite9/lite10 旧镜像不被一次
|
||||||
即 fail-closed;删除前不得声称“旧路径已关闭”。
|
改断。round-11 真机测试证实该回落把 authd 释放的长效 token 写
|
||||||
|
进了客内磁盘,修复轮已改为无 socket 即 fail-closed(`die`),
|
||||||
|
同时镜像 `/etc/gitconfig` 的 `credential.helper store` 也已删
|
||||||
|
除——authd helper 是唯一凭证 helper,且自身绝不落盘。旧镜像
|
||||||
|
(lite10 及更早)上 boot 现在会直接失败,须用含 authd 的新
|
||||||
|
镜像;真机复测须验证跑完一次完整 Gitea 链后客内
|
||||||
|
`~/.git-credentials` 不存在。
|
||||||
6. **无真机验证**:本轮全部是代码 + 本地单测(含真 git helper 端到端,
|
6. **无真机验证**:本轮全部是代码 + 本地单测(含真 git helper 端到端,
|
||||||
但 authd 守护进程本身未在真 VM 里跑过)。真机待验证清单:authd 在
|
但 authd 守护进程本身未在真 VM 里跑过)。真机待验证清单:authd 在
|
||||||
lite11 镜像里启动时序(须在 `chv-sync boot` 之前就绪)、SO_PEERCRED
|
lite11 镜像里启动时序(须在 `chv-sync boot` 之前就绪)、SO_PEERCRED
|
||||||
|
|||||||
Reference in new issue
Block a user