feat(chv): 对齐 Muse P6 privsep worker + 确定性安全检查层

- cloud/chv/privsep.py(新):Gitea worker 注册表(凭证白名单只含
  GITEA_TOKEN)+ 类型化请求/响应(request_id 绑定、64KB 上限)+
  Unix socket 客户端(超时/崩溃 -> 类型化失败 WorkerRefusal)+
  客户端白名单校验
- cloud/chv/safety.py(新):UntrustedData 来源标记 +
  InjectionScanner 确定性规则(block/flag/allow,中英)+
  redact_secrets + AuditLogger(只记指纹,不记值)+
  audit_inference_request/response
- chv-guest/chv-gitea-worker.py + .service(新):systemd 沙箱化
  worker(SO_PEERCRED 只放 root,仓限 ~/workspace,git 只配 authd
  helper,绝不持 token;输出先扫描再返回)
- chv-guest/chv-sync-worker-client.py(新):agent 侧薄 CLI
- chv-guest/chv-sync.sh:pull/push/status 有 socket 时走 worker,
  否则旧路径 + stderr 警告(过渡兼容)
- cloud/chv/executor.py:_run_guest 前后加推理边界审计钩子
  (best-effort)
- docs/chv-muse-p6-gap.md(新):差距表 + 覆盖边界 + 真机待验证清单
- docs/chv-production-readiness.md:§15
- cloud/tests/test_chv_muse_p6.py(新):43 项

测试:新测试 43/43;全量 cloud 套件 1910 OK(skipped=9);mypy
在新增文件上干净(剩余 4 处为基线旧错 executor.py:1396/1401、
marker_tools.py:142,未动)。
真机验证:未做,排队在 round-11 之后,清单见
docs/chv-muse-p6-gap.md §4。
This commit is contained in:
host committed 2026-11-06 22:31:54 +00:00
1 parent 06c3e172fe
commit 81e4eaf4db
10 files changed
+2071

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""chv-sync-worker-client:Gitea worker 薄客户端(feat-chv-muse-p6)。
agent 侧(broker / chv-sync)只剩薄 CLI:解析参数、发类型化 JSON
请求到 worker socket、原样回显 worker 的 stdout/stderr、按 worker
的 returncode 退出。本进程不跑 git、不碰凭证、不做业务逻辑。
用法:chv-sync-worker-client <pull|push|status|ping> [仓目录]
仓目录省略时由 worker 侧取唯一仓(与 chv-sync 同款语义,由 worker
的 repo=None 处理——本客户端只透传,不自己解析)。
只用标准库。
"""
import json
import os
import socket
import sys
import time
DEFAULT_SOCKET_PATH = "/run/chv-gitea/worker.sock"
def _exchange(sock_path, request):
"""连 worker、发请求、收一帧回包。
返回 (resp_dict, error_str),成功时 error_str 为 None。"""
try:
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.settimeout(310)
conn.connect(sock_path)
except OSError as exc:
return None, "worker 连不上:%s" % exc
try:
conn.sendall((json.dumps(request) + "\n").encode("utf-8"))
data = b""
while len(data) < 65536:
chunk = conn.recv(4096)
if not chunk:
break
data += chunk
if data.endswith(b"\n"):
break
except OSError as exc:
return None, "worker 通信失败:%s" % exc
finally:
conn.close()
try:
resp = json.loads(data.decode("utf-8"))
except (ValueError, UnicodeDecodeError):
return None, "worker 回包不是 JSON"
if not isinstance(resp, dict) or "ok" not in resp:
return None, "worker 回包格式不对"
return resp, None
def main(argv=None):
"""发一次类型化请求并回显。返回 worker 的 returncode;
通信失败即 exit 2(调用方按失败处理,不静默成功)。"""
args = list(argv if argv is not None else sys.argv[1:])
if not args or args[0] not in ("pull", "push", "status", "ping"):
print("用法:chv-sync-worker-client "
"pull|push|status|ping [仓目录]", file=sys.stderr)
return 2
op = args[0]
repo = args[1] if len(args) > 1 and args[1] else None
sock_path = os.environ.get("CHV_GITEA_WORKER_SOCK",
DEFAULT_SOCKET_PATH)
request = {"op": op, "connector": "gitea", "repo": repo,
"request_id": "cli-%d" % int(time.time() * 1000)}
resp, err = _exchange(sock_path, request)
if err is not None:
print("chv-sync-worker-client: %s" % err, file=sys.stderr)
return 2
if resp.get("stdout"):
sys.stdout.write(resp["stdout"])
if resp.get("stderr"):
sys.stderr.write(resp["stderr"])
if resp.get("error"):
print("chv-sync-worker-client: %s" % resp["error"],
file=sys.stderr)
if resp.get("safety", {}).get("verdict") == "flag":
print("chv-sync-worker-client: 输出已标记复核:%s"
% resp["safety"], file=sys.stderr)
return int(resp.get("returncode") or (0 if resp["ok"] else 1))
if __name__ == "__main__":
raise SystemExit(main())