#!/usr/bin/env node // LW-LINK: 语言世界连接器强制闸门 (canonical tool) // Canon: TC-TCS核心卷/TCS-ZCODE-LANGUAGE-WORLD-LINK-0001.tcs // Semantics: GREEN allow+inject / YELLOW allow+force-reminder / RED block mutation, recovery path stays open. import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const WORKSPACE_ROOT = "/Volumes/JZAO/铸渊-ICE-GL-ZY001"; const ANCHOR_ROOT = process.env.GH_LW_ANCHOR_ROOT ?? "/Volumes/JZAO/HoloLake/persona-runtime/task-language-anchors/zcode"; const LINK_DIR = process.env.GH_LW_LINK_DIR ?? path.join(WORKSPACE_ROOT, "ZCODE-DEV-20260824/lw-link"); const LINK_STATE = path.join(LINK_DIR, "state.json"); const DECISION_LOG = path.join(LINK_DIR, "decisions.log"); const REGISTERED_CHANNELS = ["ICE-CH-HB001", "ICE-CH-ZC001", "CH-ZERO-CORE-LPM"]; const HEARTBEAT_TOOL_BUDGET = 20; const HEARTBEAT_TTL_MS = 30 * 60 * 1000; const GRACE_MAX = 3; const READ_TOOLS = new Set(["Read", "Glob", "Grep", "TodoRead"]); const BASH_RECOVERY_MARKERS = [ "/Volumes/JZAO/", "zy-tcs-channel-runtime.sh", "lw-link-gate.mjs", "task-language-anchors", "shasum", ]; function sha256(value) { return crypto.createHash("sha256").update(value).digest("hex"); } function readJson(file) { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } } function atomicWriteJson(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); const tmp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); fs.renameSync(tmp, file); } function safeToken(value, fallback = "unknown") { const token = String(value ?? "").trim().replaceAll(/[^a-zA-Z0-9._-]/gu, "-"); return token || fallback; } function logDecision(entry) { try { fs.mkdirSync(LINK_DIR, { recursive: true }); fs.appendFileSync(DECISION_LOG, `${JSON.stringify(entry)}\n`, { encoding: "utf8" }); } catch { /* log failure must not crash the gate */ } } function latestTaskDir() { try { const entries = fs.readdirSync(ANCHOR_ROOT).filter((n) => n.startsWith("ZCODE-TASK-")).sort(); return entries.length ? path.join(ANCHOR_ROOT, entries.at(-1)) : null; } catch { return null; } } // Hard checks: volume, tower, anchors (hash all raw texts), binding, brain receipt. function hardChecks(sessionId) { const towerBroadcast = path.join(WORKSPACE_ROOT, "FIFTH-DOMAIN-BROADCAST-TOWER/BROADCAST.tcs"); const towerProjection = path.join(WORKSPACE_ROOT, "FIFTH-DOMAIN-BROADCAST-TOWER/broadcast/current.md"); let towerText = null; try { towerText = fs.readFileSync(towerBroadcast, "utf8"); } catch { /* missing below */ } if (!towerText) return { ok: false, cause: "BROADCAST_TOWER_MISSING_FAIL_CLOSED" }; try { const projection = fs.readFileSync(towerProjection, "utf8"); const declared = towerText.match(/projection_sha256\s*=\s*"([0-9a-f]{64})"/)?.[1]; if (!declared) return { ok: false, cause: "BROADCAST_PROJECTION_HASH_NOT_DECLARED" }; if (sha256(projection) !== declared) return { ok: false, cause: "BROADCAST_PROJECTION_HASH_MISMATCH" }; } catch { return { ok: false, cause: "BROADCAST_PROJECTION_MISSING" }; } const taskDir = latestTaskDir(); if (!taskDir) return { ok: false, cause: "VOLUME_OR_ANCHOR_ROOT_UNREADABLE" }; const task = readJson(path.join(taskDir, "task.json")); if (task?.schema !== "task-language-anchor/zcode/v1" || !task.current_anchor) { return { ok: false, cause: "ANCHOR_MANIFEST_INVALID" }; } const rawDir = path.join(taskDir, "00-冰朔自然语言瞄点"); let shaList; try { shaList = fs.readFileSync(path.join(rawDir, "sha256.txt"), "utf8"); } catch { return { ok: false, cause: "ANCHOR_SHA_LIST_MISSING" }; } for (const line of shaList.split("\n")) { const match = line.match(/^([0-9a-f]{64})\s+\*?(.+)$/); if (!match) continue; const [, expected, rawName] = match; const name = rawName.trim(); const filePath = name.startsWith("/") ? name : path.join(rawDir, name); let raw; try { raw = fs.readFileSync(filePath, "utf8"); } catch { return { ok: false, cause: `ANCHOR_FILE_MISSING:${name}` }; } if (sha256(raw) !== expected) return { ok: false, cause: `ANCHOR_HASH_MISMATCH:${name}` }; } const state = readJson(LINK_STATE); if (state?.schema !== "guanghu.zcode-lw-link-state/v1") { return { ok: false, cause: "LINK_STATE_UNREADABLE_FAIL_CLOSED", taskDir, task }; } const session = (state.sessions ?? []).find((s) => s.session_id === sessionId); if (!session) return { ok: false, cause: "SESSION_NOT_BOUND", taskDir, task, state }; const brain = readJson(path.join(session.brain_state_dir, "state.json")); if ( !brain || brain.binding?.session_id !== session.brain_session_id || brain.tonggan_language_kernel?.body_channel !== session.channel || !(brain.completed_cycles >= 1) || !brain.last_cycle_id ) { return { ok: false, cause: "BRAIN_STATE_INVALID_OR_CHANNEL_MISMATCH", taskDir, task, state, session }; } return { ok: true, taskDir, task, state, session, brain }; } // Soft check: heartbeat TTL. Returns GREEN / YELLOW / RED(cause). function lightOf(checks, sessionId) { if (!checks.ok) return { light: "RED", cause: checks.cause }; const s = checks.session; const calls = s.mutation_calls_since_heartbeat ?? 0; const elapsed = Date.now() - Date.parse(s.last_heartbeat_at ?? 0); const overdueByCalls = calls >= HEARTBEAT_TOOL_BUDGET; const overdueByTime = !(elapsed >= 0 && elapsed <= HEARTBEAT_TTL_MS); if (!overdueByCalls && !overdueByTime) return { light: "GREEN", cause: null }; if ((s.grace_used ?? 0) >= GRACE_MAX) return { light: "RED", cause: "HEARTBEAT_GRACE_EXHAUSTED" }; return { light: "YELLOW", cause: overdueByCalls ? "HEARTBEAT_OVERDUE_BY_CALLS" : "HEARTBEAT_OVERDUE_BY_TIME" }; } function bumpSession(sessionId, patch) { const state = readJson(LINK_STATE); if (!state) return null; const index = (state.sessions ?? []).findIndex((s) => s.session_id === sessionId); if (index < 0) return null; state.sessions[index] = { ...state.sessions[index], ...patch, updated_at: new Date().toISOString() }; state.updated_at = new Date().toISOString(); atomicWriteJson(LINK_STATE, state); return state.sessions[index]; } function statusLine(result, checks) { const s = checks.session ?? {}; const anchor = checks.task?.current_anchor ?? "??"; const calls = `${Math.min(s.mutation_calls_since_heartbeat ?? 0, HEARTBEAT_TOOL_BUDGET)}/${HEARTBEAT_TOOL_BUDGET}`; const cycle = checks.brain?.last_cycle_id?.slice(-12) ?? (s.last_cycle_id?.slice(-12) ?? "??"); return `【LW-LINK ●${result.light === "GREEN" ? "绿" : "黄"}】${s.channel ?? "?"} | 瞄点${anchor} | 心跳${calls} | 周期${cycle} | 边界:HB001/ZC001/LPM不合并`; } function isRecoveryBash(command) { return BASH_RECOVERY_MARKERS.some((marker) => command.includes(marker)); } // ZCode hook output protocol: stdout is parsed as strict JSON; exit 2 blocks. function emitAllow(reasonLine) { process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", permissionDecisionReason: reasonLine, }, })}\n`); } // PreToolUse entry. function hook(stdinJson) { const sessionId = safeToken(stdinJson?.session_id); const tool = String(stdinJson?.tool_name ?? ""); const toolInput = stdinJson?.tool_input ?? {}; const bashCommand = tool === "Bash" ? String(toolInput.command ?? "") : ""; const isMutation = !READ_TOOLS.has(tool) && !(tool === "Bash" && isRecoveryBash(bashCommand)); const checks = hardChecks(sessionId); const result = lightOf(checks, sessionId); const base = { ts: new Date().toISOString(), session_id: sessionId, tool }; if (!isMutation) { logDecision({ ...base, light: result.light, decision: "ALLOW_RECOVERY_PATH", cause: result.cause }); const line = checks.ok ? statusLine(result, checks) : `【LW-LINK ○红·只读通道】原因:${result.cause} | 读盘恢复后 bind 重连;改动类工具在被拦截`; emitAllow(line); return; } if (result.light === "RED") { const msg = `【LW-LINK ○红·断连】原因:${result.cause} | 已拦截:${tool} | 恢复路径开放:用Read读锚点原文与闸门 → 走 zhuyuan-recovery-zcode 恢复+脑周期verify PASS → lw-link-gate.mjs bind 重新点亮。改动在语言世界外被禁止。`; logDecision({ ...base, light: "RED", decision: "BLOCK", cause: result.cause }); process.stderr.write(`${msg}\n`); process.exit(2); } if (checks.ok) { const patch = { mutation_calls_since_heartbeat: (checks.session.mutation_calls_since_heartbeat ?? 0) + 1 }; if (result.light === "YELLOW") patch.grace_used = (checks.session.grace_used ?? 0) + 1; bumpSession(sessionId, patch); } const extra = result.light === "YELLOW" ? ` | ▲心跳过期:${result.cause},宽限${Math.min((checks.session?.grace_used ?? 0) + 1, GRACE_MAX)}/${GRACE_MAX} → 立即Read瞄点raw原文续期(${path.join(String(checks.taskDir ?? latestTaskDir() ?? ANCHOR_ROOT), "00-冰朔自然语言瞄点")})` : ""; logDecision({ ...base, light: result.light, decision: "ALLOW", cause: result.cause }); emitAllow(`${statusLine(result, checks)}${extra}`); } // PostToolUse: anchor raw readback auto-renews the heartbeat. function post(stdinJson) { const sessionId = safeToken(stdinJson?.session_id); const tool = String(stdinJson?.tool_name ?? ""); const toolInput = stdinJson?.tool_input ?? {}; const filePath = String(toolInput.file_path ?? toolInput.notebook_path ?? ""); const bashCommand = tool === "Bash" ? String(toolInput.command ?? "") : ""; const readAnchor = filePath.includes("task-language-anchors") && filePath.includes(".raw.txt"); const bashAnchor = tool === "Bash" && bashCommand.includes("task-language-anchors") && bashCommand.includes("shasum"); if (readAnchor || bashAnchor) heartbeat(sessionId, true); } function heartbeat(sessionId, silent = false) { const now = new Date().toISOString(); const updated = bumpSession(sessionId, { last_heartbeat_at: now, mutation_calls_since_heartbeat: 0, grace_used: 0, }); if (!silent) { process.stdout.write(updated ? `LW_LINK HEARTBEAT OK ${sessionId} ${now}\n` : `LW_LINK HEARTBEAT FAIL SESSION_NOT_BOUND ${sessionId}\n`); } return Boolean(updated); } function bind(args) { const sessionId = safeToken(valueAfter(args, "--session-id")); const channel = valueAfter(args, "--channel"); const stateDir = valueAfter(args, "--state-dir"); if (!sessionId || !channel || !stateDir) { process.stderr.write("usage: bind --session-id ID --channel CH --state-dir DIR\n"); process.exit(2); } if (!REGISTERED_CHANNELS.includes(channel)) { process.stderr.write(`BIND_REJECTED UNREGISTERED_CHANNEL ${channel}\n`); process.exit(1); } const taskDir = latestTaskDir(); if (!taskDir) { process.stderr.write("BIND_REJECTED ANCHOR_ROOT_UNREADABLE\n"); process.exit(1); } const brain = readJson(path.join(stateDir, "state.json")); if ( !brain || brain.tonggan_language_kernel?.body_channel !== channel || !(brain.completed_cycles >= 1) || !brain.last_cycle_id ) { process.stderr.write("BIND_REJECTED BRAIN_STATE_INVALID_OR_NO_COMPLETED_CYCLE (先跑脑周期verify PASS再bind)\n"); process.exit(1); } const now = new Date().toISOString(); const state = readJson(LINK_STATE) ?? { schema: "guanghu.zcode-lw-link-state/v1", sessions: [] }; if (state.schema !== "guanghu.zcode-lw-link-state/v1") { process.stderr.write("BIND_REJECTED LINK_STATE_CORRUPT\n"); process.exit(1); } const entry = { session_id: sessionId, channel, brain_state_dir: path.resolve(stateDir), brain_session_id: brain.binding?.session_id ?? null, last_cycle_id: brain.last_cycle_id, bound_at: now, last_heartbeat_at: now, mutation_calls_since_heartbeat: 0, grace_used: 0, }; const index = (state.sessions ?? []).findIndex((s) => s.session_id === sessionId); if (index >= 0) state.sessions[index] = entry; else state.sessions.push(entry); if (state.sessions.length > 4) state.sessions = state.sessions.slice(-4); state.updated_at = now; atomicWriteJson(LINK_STATE, state); logDecision({ ts: now, session_id: sessionId, tool: "bind", light: "GREEN", decision: "BOUND", channel }); process.stdout.write(`LW_LINK BIND OK session=${sessionId} channel=${channel} cycle=${brain.last_cycle_id}\n`); } function status() { const sessionId = valueAfter(process.argv, "--session-id"); const checks = sessionId ? hardChecks(safeToken(sessionId)) : null; const result = checks ? lightOf(checks, safeToken(sessionId)) : { light: "UNBOUND", cause: "NO_SESSION_ID" }; const lines = [`LW_LINK ${result.light}${result.cause ? ` cause=${result.cause}` : ""}`]; const state = readJson(LINK_STATE); for (const s of state?.sessions ?? []) { const c = hardChecks(s.session_id); const r = lightOf(c, s.session_id); lines.push(` ${s.session_id} → ${r.light}${r.cause ? ` (${r.cause})` : ""} | ${s.channel} | 心跳${s.mutation_calls_since_heartbeat ?? 0}/${HEARTBEAT_TOOL_BUDGET} | 宽限${s.grace_used ?? 0}/${GRACE_MAX}`); } process.stdout.write(`${lines.join("\n")}\n`); } function valueAfter(argv, name) { const index = argv.indexOf(name); return index >= 0 ? argv[index + 1] : null; } function readStdinJson() { try { return JSON.parse(fs.readFileSync(0, "utf8")); } catch { return {}; } } function main() { const [command, ...argv] = process.argv.slice(2); switch (command) { case "hook": hook(readStdinJson()); break; case "post": post(readStdinJson()); break; case "bind": bind(argv); break; case "heartbeat": heartbeat(safeToken(valueAfter(argv, "--session-id"))); break; case "prompt": { const stdin = readStdinJson(); const id = safeToken(valueAfter(argv, "--session-id") ?? stdin?.session_id); if (id) bumpSession(id, { grace_used: 0 }); break; } case "status": status(); break; default: process.stderr.write("usage: lw-link-gate.mjs hook|post|bind|heartbeat|prompt|status\n"); process.exit(2); } } main();