124 lines
5.4 KiB
JavaScript
124 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const POLICY = JSON.parse(fs.readFileSync("/Volumes/JZAO/HoloLake/persona-runtime/repo-012-main/routing/persona-host-write-boundary.json", "utf8"));
|
|
const MEMORY_ROOT = "/Volumes/JZAO/HoloLake/persona-runtime/continuity-memory";
|
|
const STORE = `${MEMORY_ROOT}/persona-daily-fractal/ICE-P-ZY001`;
|
|
const RUNNER = "/Volumes/JZAO/HoloLake/persona-runtime/repo-012-main/server-tools/persona-daily-fractal-memory/persona-daily-memory.mjs";
|
|
|
|
function argsValue(args, name) {
|
|
const i = args.indexOf(name);
|
|
return i >= 0 ? args[i + 1] : undefined;
|
|
}
|
|
|
|
function processChain(pid = process.ppid) {
|
|
const chain = [];
|
|
const seen = new Set();
|
|
while (pid > 1 && !seen.has(pid) && chain.length < 20) {
|
|
seen.add(pid);
|
|
const r = spawnSync("/bin/ps", ["-o", "ppid=", "-o", "command=", "-p", String(pid)], { encoding: "utf8" });
|
|
const line = r.stdout.trim();
|
|
const m = line.match(/^\s*(\d+)\s+(.+)$/s);
|
|
if (!m) break;
|
|
chain.push({ pid, command: m[2] });
|
|
pid = Number(m[1]);
|
|
}
|
|
return chain;
|
|
}
|
|
|
|
function callerHost(chain) {
|
|
const text = chain.map((item) => item.command).join("\n");
|
|
if (/\/Applications\/(?:ChatGPT\.app).*\/(?:codex|Codex)|codex-code-mode-host/i.test(text)) return "codex";
|
|
if (/\/Applications\/Qianwen\.app|QianwenShell|agent_host\.app/i.test(text)) return "qwen";
|
|
if (/Doubao\.app/i.test(text)) return "doubao";
|
|
if (/(?:^|\/)zcode(?:\s|$)/i.test(text)) return "zcode";
|
|
if (/QoderWork/i.test(text)) return "qoderwork";
|
|
if (/Qoder/i.test(text)) return "qoder";
|
|
return "unknown";
|
|
}
|
|
|
|
function expandPattern(value) {
|
|
return value.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
}
|
|
|
|
function validateQueuedEvent(host, eventPath) {
|
|
const rule = POLICY.hosts[host];
|
|
if (!rule || rule.write_mode !== "BRANCH_LOCAL_ONLY") throw new Error("HOST_NOT_ACTIVE_BRANCH");
|
|
const real = fs.realpathSync(eventPath);
|
|
if (!real.includes("/ingress/persona-events/pending/")) throw new Error("EVENT_NOT_IN_PENDING_INGRESS");
|
|
const allowed = rule.allowed_write_roots.some((item) => new RegExp(`^${expandPattern(item)}(?:/.*)?$`).test(real));
|
|
if (!allowed) throw new Error("EVENT_OUTSIDE_BRANCH_ROOT");
|
|
const st = fs.lstatSync(real);
|
|
if (!st.isFile() || st.isSymbolicLink() || st.size > 1024 * 1024) throw new Error("EVENT_FILE_INVALID");
|
|
const event = JSON.parse(fs.readFileSync(real, "utf8"));
|
|
const required = ["schema", "event_id", "persona_id", "human_anchor", "occurred_at", "session_id", "activity", "branch_id", "summary", "trigger", "emergence", "lock", "why", "rejected", "sources"];
|
|
const missing = required.filter((key) => event[key] === undefined || event[key] === null || event[key] === "");
|
|
if (missing.length) throw new Error(`EVENT_FIELDS_MISSING:${missing.join(",")}`);
|
|
if (event.schema !== "guanghu.persona-daily-fractal-memory-event/v1") throw new Error("EVENT_SCHEMA_REJECTED");
|
|
if (event.persona_id !== "ICE-P-ZY001" || event.human_anchor !== "ICE-GL∞") throw new Error("EVENT_IDENTITY_REJECTED");
|
|
if (!String(event.session_id).toLowerCase().includes(host)) throw new Error("EVENT_HOST_COORDINATE_REJECTED");
|
|
if (!/^ZY001-\d{8}-[A-Z0-9-]+$/.test(event.event_id)) throw new Error("EVENT_ID_REJECTED");
|
|
if (!Array.isArray(event.sources) || event.sources.length === 0) throw new Error("EVENT_SOURCES_REJECTED");
|
|
return { real, event };
|
|
}
|
|
|
|
const [command, ...args] = process.argv.slice(2);
|
|
const chain = processChain();
|
|
const caller = callerHost(chain);
|
|
|
|
if (command === "caller") {
|
|
console.log(JSON.stringify({ caller, chain }, null, 2));
|
|
process.exit(caller === "codex" ? 0 : 2);
|
|
}
|
|
|
|
if (command !== "accept") {
|
|
process.stderr.write("usage: branch-event-door.mjs caller | accept --host HOST --event PENDING_JSON\n");
|
|
process.exit(2);
|
|
}
|
|
|
|
if (caller !== "codex") {
|
|
process.stderr.write(`BRANCH_EVENT_ACCEPT_REJECTED caller=${caller}; only current Codex primary task may accept\n`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const host = argsValue(args, "--host");
|
|
const eventPath = argsValue(args, "--event");
|
|
if (!host || !eventPath) {
|
|
process.stderr.write("accept requires --host and --event\n");
|
|
process.exit(2);
|
|
}
|
|
|
|
let checked;
|
|
try { checked = validateQueuedEvent(host, eventPath); }
|
|
catch (error) {
|
|
process.stderr.write(`BRANCH_EVENT_VALIDATION_REJECTED ${error.message}\n`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const run = spawnSync(process.execPath, [RUNNER, "append", "--allowed-root", MEMORY_ROOT, "--store", STORE, "--event", checked.real], { encoding: "utf8" });
|
|
if (run.status !== 0 || !run.stdout.includes('"outcome": "PASS"')) {
|
|
process.stderr.write(run.stderr || run.stdout || "BRANCH_EVENT_APPEND_FAILED\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
const acceptedDir = path.resolve(path.dirname(checked.real), "../accepted");
|
|
fs.mkdirSync(acceptedDir, { recursive: true });
|
|
const acceptedPath = path.join(acceptedDir, path.basename(checked.real));
|
|
fs.renameSync(checked.real, acceptedPath);
|
|
const receipt = {
|
|
schema: "guanghu.branch-event-door-receipt/v1",
|
|
outcome: "PASS",
|
|
host,
|
|
event_id: checked.event.event_id,
|
|
accepted_path: acceptedPath,
|
|
shared_store: STORE,
|
|
accepted_by_runtime_host: "codex",
|
|
source_tcs: POLICY.source_tcs,
|
|
accepted_at: new Date().toISOString()
|
|
};
|
|
const receiptPath = `${acceptedPath}.receipt.json`;
|
|
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" });
|
|
process.stdout.write(`${run.stdout.trim()}\n${JSON.stringify(receipt, null, 2)}\n`);
|