193 lines
8.1 KiB
Python
193 lines
8.1 KiB
Python
|
|
#!/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())
|