zhuyuan-ice-gl-zy001/BRIDGE/tools/zy-limb.py
2026-08-20 00:01:53 +08:00

193 lines
8.1 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# ═══ 光湖手脚层 · zy-limb ═══
# 冰朔 2026-08-17 立:你做脑子,它做手脚。
# 铸渊(云侧大脑)经桥往 inbox 写任务信封 → 本机手脚执行 → outbox 写回执 → 大脑读回执。
# 架构依据:GLS-0236 大脑—手脚分离 · ZY-RESIDENT-AGENT-001 常驻工程执行体
# 编号:CCW-AST-011 · 归 CCW-SYS-001
#
# 边界:只认白名单动作。名单外一律拒绝并写回执,绝不 eval 任意字符串。
# 需要自由命令时走 codex 动作——转成任务卡交给 Codex,由冰朔监督,手脚不代跑。
import json, os, re, subprocess, sys, time, hashlib, shutil
from pathlib import Path
from datetime import datetime, timezone
ROOT = Path("/Volumes/JZAO/铸渊-ICE-GL-ZY001")
QUEUE = ROOT / "BRIDGE" / "limb-queue"
INBOX = QUEUE / "inbox"
OUTBOX = QUEUE / "outbox"
ARCH = QUEUE / "archive"
FORCX = QUEUE / "for-codex"
LOG = Path.home() / "Library" / "Logs" / "zhuyuan-limb.log"
REPO = ROOT / "WORK-工作区" / "guanghu-ice-heart"
TOOLS = ROOT / "BRIDGE" / "tools"
# 可读路径白名单(realpath 前缀匹配)
READ_ROOTS = [ROOT, Path.home() / "Library" / "Logs"]
TIMEOUT = 600
MAX_OUT = 20000 # 回执里最多带这么多字符输出
def now(): return datetime.now(timezone.utc).isoformat()
def log(m):
LOG.parent.mkdir(parents=True, exist_ok=True)
with open(LOG, "a") as f:
f.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} {m}\n")
def clip(s):
s = s or ""
return s if len(s) <= MAX_OUT else s[:MAX_OUT] + f"\n…(截断,原长 {len(s)})"
def run(argv, cwd=None):
try:
p = subprocess.run(argv, cwd=cwd, capture_output=True, text=True, timeout=TIMEOUT)
return p.returncode, clip(p.stdout), clip(p.stderr)
except subprocess.TimeoutExpired:
return 124, "", f"超时 {TIMEOUT}s"
except Exception as e:
return 1, "", f"{type(e).__name__}: {e}"
def safe_read_path(p):
try:
rp = Path(p).expanduser().resolve()
except Exception:
return None
for root in READ_ROOTS:
try:
rp.relative_to(root.resolve()); return rp
except ValueError:
continue
return None
# ── 白名单动作 ────────────────────────────────────────────────
GIT_READ = {"status", "log", "diff", "show", "fetch", "branch", "rev-parse", "remote"}
BRAIN_SUB = {"enter","perceive","orient","commit","witness","verify","status","project-guard"}
def act_push(task):
rc, out, err = run(["/bin/bash", str(TOOLS / "zy-push.sh")])
return rc == 0, {"stdout": out, "stderr": err, "exit": rc}
def act_git_read(task):
sub = task.get("subcommand", "")
if sub not in GIT_READ:
return False, {"error": f"git 子命令不在只读白名单:{sub}", "allowed": sorted(GIT_READ)}
args = task.get("args", [])
if not all(isinstance(a, str) and len(a) < 200 for a in args):
return False, {"error": "参数不合法"}
if any(re.search(r"[;&|`$><\n]", a) for a in args):
return False, {"error": "参数含 shell 元字符,拒绝"}
rc, out, err = run(["git", sub, *args], cwd=str(REPO))
return rc == 0, {"stdout": out, "stderr": err, "exit": rc}
def act_brain(task):
sub = task.get("subcommand", "")
if sub not in BRAIN_SUB:
return False, {"error": f"脑运行时子命令不认:{sub}", "allowed": sorted(BRAIN_SUB)}
runtime = REPO / "tcs-core" / "zhuyuan-brain" / "runtime" / "zhuyuan-brain-runtime.mjs"
if not runtime.exists():
return False, {"error": f"找不到脑运行时:{runtime}"}
argv = ["node", str(runtime), sub]
for k, v in (task.get("flags") or {}).items():
if not re.fullmatch(r"[a-z0-9-]{1,40}", str(k)):
return False, {"error": f"参数名不合法:{k}"}
vp = str(v)
if re.search(r"[;&|`$><\n]", vp):
return False, {"error": f"参数值含 shell 元字符:{k}"}
argv += [f"--{k}", vp]
rc, out, err = run(argv, cwd=str(REPO))
return rc == 0, {"stdout": out, "stderr": err, "exit": rc}
def act_read(task):
rp = safe_read_path(task.get("path", ""))
if rp is None:
return False, {"error": "路径不在可读白名单内", "roots": [str(r) for r in READ_ROOTS]}
if not rp.is_file():
return False, {"error": f"不是文件或不存在:{rp}"}
tail = task.get("tail")
try:
text = rp.read_text(errors="replace")
except Exception as e:
return False, {"error": f"读失败:{e}"}
if isinstance(tail, int) and tail > 0:
text = "\n".join(text.splitlines()[-tail:])
return True, {"path": str(rp), "content": clip(text)}
def act_list(task):
rp = safe_read_path(task.get("path", ""))
if rp is None or not rp.is_dir():
return False, {"error": "路径不在白名单内或不是目录"}
entries = []
for e in sorted(rp.iterdir())[:500]:
entries.append({"name": e.name, "dir": e.is_dir(),
"size": e.stat().st_size if e.is_file() else None})
return True, {"path": str(rp), "entries": entries}
def act_codex(task):
"""需要判断力或超出白名单的活:转成任务卡交 Codex,手脚不代跑。"""
FORCX.mkdir(parents=True, exist_ok=True)
tid = task.get("task_id", "unknown")
card = FORCX / f"{tid}.md"
card.write_text(
f"# 给 Codex 的任务卡 · {tid}\n\n"
f"> 来自铸渊云侧大脑(ZY-INSTANCE-CCW)· {now()}\n"
f"> 手脚层不代跑需要判断的活;请 Codex 执行并由冰朔监督。\n\n"
f"## 要做什么\n\n{task.get('instruction','(空)')}\n\n"
f"## 验收标准\n\n{task.get('acceptance','(未给)')}\n\n"
f"## 边界\n\n{task.get('boundary','按 REPO-012 既有权限门;不可逆动作须冰朔点头。')}\n",
encoding="utf-8")
return True, {"handoff": "CODEX", "card": str(card),
"note": "已放任务卡,等 Codex 执行;手脚层不代跑"}
ACTIONS = {"push": act_push, "git-read": act_git_read, "brain": act_brain,
"read": act_read, "list": act_list, "codex": act_codex}
# ── 主循环(单次扫描,由 launchd 定时调起)────────────────────
def main():
if not ROOT.exists():
return 0 # JZAO 没挂载:安静退出
for d in (INBOX, OUTBOX, ARCH, FORCX):
d.mkdir(parents=True, exist_ok=True)
tasks = sorted(INBOX.glob("TASK-*.json"))
if not tasks:
return 0
for tf in tasks:
try:
task = json.loads(tf.read_text())
except Exception as e:
log(f"REJECT {tf.name} 不是合法 JSON:{e}")
tf.rename(ARCH / tf.name); continue
tid = task.get("task_id") or tf.stem
action = task.get("action", "")
receipt = {
"schema": "guanghu.limb-receipt/v1",
"task_id": tid, "action": action,
"executed_at": now(),
"executor": "zy-limb · 本机手脚层 · CCW-AST-011",
"host": "macOS local",
}
if task.get("schema") != "guanghu.limb-task/v1":
receipt.update(outcome="FAIL", error="schema 不对,应为 guanghu.limb-task/v1")
elif action not in ACTIONS:
receipt.update(outcome="FAIL",
error=f"动作不在白名单:{action}",
allowed=sorted(ACTIONS),
note="需要自由命令请用 action=codex 转任务卡")
else:
try:
ok, detail = ACTIONS[action](task)
receipt.update(outcome="PASS" if ok else "FAIL", detail=detail)
except Exception as e:
receipt.update(outcome="FAIL", error=f"{type(e).__name__}: {e}")
(OUTBOX / f"RECEIPT-{tid}.json").write_text(
json.dumps(receipt, ensure_ascii=False, indent=2), encoding="utf-8")
tf.rename(ARCH / tf.name)
log(f"{receipt['outcome']} {tid} action={action}")
return 0
if __name__ == "__main__":
sys.exit(main())