guanghu-ice-heart/zhuyuan-persona-system/zcode-host/language-world-skeleton-20260825/BRIDGE-tools/lw-link-gate.mjs
ICE-P-ZY001-zcode-host f2a8cdf5c1 feat(zcode-host): language-world-skeleton-20260825 (ZHUYUAN-LANGUAGE-WORLD-SKELETON-001)
58 files: TCS canons+GIR+projections (LW-LINK/BROADCAST/NOTARY/SHENBI/TRANSLATION-INDEX/TCS-ONLY-MANDATE + teaching&promotion events), 7 executors, neural-chain v5 (28 neurons), executor translation index, 8 acceptance receipts, numbered push manifest ZY-LWS-001 mapping every file back to JZAO canonical paths+sha256.
Authorized by ICE-GL∞ direct language 2026-08-25. Route: LOCAL_GIT_OSXKEYCHAIN+FETCH+REBASE+HEAD:main+NO_FORCE.
2026-08-25 20:25:09 +08:00

302 lines
14 KiB
JavaScript

#!/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();