merge(chv-muse-round12): 合入 P7 状态与记忆分离 (29fa0c8)

冲突按并集解:build.sh 保留 P2+P3+P4 检查/拷入 + P7 的
chv-memory 检查/拷入(P7 的重复 runcmd 行删掉——同一文件
写两次会盖掉 authd 启动行);buildinfo.py 加 chv-memory 一项;
production-readiness.md P7 一节顺延为 §18。P7 测试 46 项全过。
This commit is contained in:
host committed 2026-11-07 00:02:54 +00:00
commit 320cd36921
8 files changed
+1667 -1

No files matched your search

+8
View File
@@ -207,6 +207,11 @@ for p4_file in chv-cdp-broker.py chv_browser_guard.py browser-blocklist.txt; do
exit 1
fi
done
if [ ! -f "$SCRIPT_DIR/chv-memory.py" ]; then
echo "缺少 chv-memory.py(客内记忆文件脚本):$SCRIPT_DIR" >&2
exit 1
fi
if [ "$DRY_RUN" = "0" ]; then
if [ ! -e /dev/kvm ]; then
echo "CHV_GUEST_BUILD needs a KVM host: /dev/kvm 不存在" >&2
@@ -316,6 +321,9 @@ VC_ARGS=(-a "$OUT/disk.raw"
--copy-in "$SCRIPT_DIR/chv_browser_guard.py:/usr/local/bin/"
--copy-in "$SCRIPT_DIR/browser-blocklist.txt:/usr/local/share/chv/"
--run-command 'mv /usr/local/bin/chv-cdp-broker.py /usr/local/bin/chv-cdp-broker && chmod +x /usr/local/bin/chv-cdp-broker && chmod 0644 /usr/local/bin/chv_browser_guard.py /usr/local/share/chv/browser-blocklist.txt'
--copy-in "$SCRIPT_DIR/chv-memory.py:/usr/local/bin/"
--run-command 'mv /usr/local/bin/chv-memory.py /usr/local/bin/chv-memory && chmod +x /usr/local/bin/chv-memory'
--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 "$GT_INSTALL_CMD"
--run-command '/usr/local/bin/gt version > /app/guest-versions-gt.txt'
+2 -1
View File
@@ -36,7 +36,7 @@ BASE_CONTENTS = [
"python3", "python3-pip", "curl", "ca-certificates",
"xz-utils", "fonts-wqy-microhei", "fontconfig", "codex",
"codex-code-mode-host", "codex-resources/bwrap",
"/app/host.py", "gt", "chv-sync",
"/app/host.py", "gt", "chv-sync", "chv-memory",
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理。
"systemd-container", "chv-cell", "chv-broker", "chv-browser",
# 对齐 Muse P3(round-11 合入镜像):authd 守护进程 + git
@@ -48,6 +48,7 @@ BASE_CONTENTS = [
# 对齐 Muse P4:单元外 CDP broker + 浏览器安全共享模块 +
# 静态拦截名单。
"chv-cdp-broker", "chv-browser-guard", "browser-blocklist",
]
# 执行器默认内核 cmdline(config.py),构建产物须与它一起记录(B-12),
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""客内记忆文件操作(对齐 Muse P7,纯标准库)。
记忆家目录 /home/chv/memory(workspace 之外,不同步到 Gitea),
布局与记忆系统同款:MEMORY.md + memory/*.md。由 executor 经
SSH 调用(与 host.py 同级,跑在 nspawn cell 之外);绝不含凭证
值——写操作过本地守卫,命中疑似凭证模式即拒绝。
用法:
chv-memory.py list [--home DIR]
chv-memory.py read <相对路径> [--home DIR]
chv-memory.py append <相对路径> <行...> [--home DIR]
chv-memory.py forget <needle> [--home DIR]
chv-memory.py export <目标目录> [--home DIR]
"""
import argparse
import json
import os
import re
import shutil
import sys
DEFAULT_HOME = "/home/chv/memory"
_SECRET_VALUE_PATTERNS = (
re.compile(r"ghp_[A-Za-z0-9]{20,}"),
re.compile(r"gho_[A-Za-z0-9]{20,}"),
re.compile(r"github_pat_[A-Za-z0-9_]{20,}"),
re.compile(r"sk-[A-Za-z0-9]{20,}"),
re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"),
re.compile(r"AKIA[0-9A-Z]{16}"),
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
)
def _fail(message: str) -> "NoReturn":
"""打印错误到 stderr 并以 1 退出。"""
print("chv-memory: %s" % message, file=sys.stderr)
raise SystemExit(1)
def _check_no_secret(text: str) -> None:
"""文本命中凭证模式即拒(不打印命中内容)。"""
for pattern in _SECRET_VALUE_PATTERNS:
if pattern.search(text):
_fail("refused: text looks like a credential")
def _resolve(home: str, rel: str) -> str:
"""相对路径 → 家目录内绝对路径,穿越即拒。"""
base = os.path.abspath(home)
candidate = os.path.abspath(os.path.join(base, rel))
if candidate != base and not candidate.startswith(base + os.sep):
_fail("path escapes memory home: %s" % rel)
return candidate
def _ensure(home: str) -> None:
"""建家目录结构(MEMORY.md + memory/)。"""
os.makedirs(os.path.join(home, "memory"), exist_ok=True)
memory_md = os.path.join(home, "MEMORY.md")
if not os.path.exists(memory_md):
with open(memory_md, "w", encoding="utf-8") as handle:
handle.write("# MEMORY.md\n")
def cmd_list(home: str) -> dict:
"""列家目录下全部文件(相对路径,排序)。"""
_ensure(home)
found = []
for root, _dirs, names in os.walk(home):
for name in names:
found.append(os.path.relpath(os.path.join(root, name), home))
return {"files": sorted(found)}
def cmd_read(home: str, rel: str) -> dict:
"""读一个记忆文件。"""
_ensure(home)
path = _resolve(home, rel)
if not os.path.isfile(path):
_fail("memory file missing: %s" % rel)
with open(path, "r", encoding="utf-8") as handle:
return {"path": rel, "text": handle.read()}
def cmd_append(home: str, rel: str, lines: list) -> dict:
"""向一个记忆文件追加行。"""
_ensure(home)
for line in lines:
_check_no_secret(line)
path = _resolve(home, rel)
parent = os.path.dirname(path)
if parent:
os.makedirs(parent, exist_ok=True)
with open(path, "a", encoding="utf-8") as handle:
for line in lines:
handle.write(line if line.endswith("\n") else line + "\n")
return {"path": rel, "appended": len(lines)}
def cmd_forget(home: str, needle: str) -> dict:
"""删掉全部 Markdown 里含 needle 的行(大小写不敏感)。"""
_ensure(home)
removed = 0
touched = []
for root, _dirs, names in os.walk(home):
for name in names:
if not name.endswith(".md"):
continue
path = os.path.join(root, name)
with open(path, "r", encoding="utf-8") as handle:
lines = handle.readlines()
kept = [ln for ln in lines
if needle.lower() not in ln.lower()]
cut = len(lines) - len(kept)
if cut:
with open(path, "w", encoding="utf-8") as handle:
handle.writelines(kept)
removed += cut
touched.append(os.path.relpath(path, home))
return {"needle": needle, "removed": removed, "files": touched}
def cmd_export(home: str, dest: str) -> dict:
"""导出全部记忆文件到目标目录(覆盖式,不含审计日志)。"""
_ensure(home)
os.makedirs(dest, exist_ok=True)
count = 0
for root, _dirs, names in os.walk(home):
for name in names:
if name == "audit.jsonl":
continue
src = os.path.join(root, name)
rel = os.path.relpath(src, home)
target = os.path.join(dest, rel)
os.makedirs(os.path.dirname(target) or dest, exist_ok=True)
shutil.copyfile(src, target)
count += 1
return {"dest": dest, "files": count}
def build_parser() -> argparse.ArgumentParser:
"""命令行解析器。"""
parser = argparse.ArgumentParser(prog="chv-memory.py")
parser.add_argument("--home", default=DEFAULT_HOME)
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("list")
read_p = sub.add_parser("read")
read_p.add_argument("path")
append_p = sub.add_parser("append")
append_p.add_argument("path")
append_p.add_argument("lines", nargs="+")
forget_p = sub.add_parser("forget")
forget_p.add_argument("needle")
export_p = sub.add_parser("export")
export_p.add_argument("dest")
return parser
def main(argv: list | None = None) -> int:
"""入口:按子命令分发,打印 JSON 结果。"""
args = build_parser().parse_args(argv)
if args.cmd == "list":
result = cmd_list(args.home)
elif args.cmd == "read":
result = cmd_read(args.home, args.path)
elif args.cmd == "append":
result = cmd_append(args.home, args.path, args.lines)
elif args.cmd == "forget":
result = cmd_forget(args.home, args.needle)
elif args.cmd == "export":
result = cmd_export(args.home, args.dest)
else: # pragma: no cover - argparse required=True 已挡住
_fail("unknown command: %s" % args.cmd)
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())