168 lines
8.3 KiB
JavaScript
168 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
|
|
const POLICY_PATH = "/Volumes/JZAO/HoloLake/persona-runtime/repo-012-main/routing/persona-host-write-boundary.json";
|
|
const policy = JSON.parse(fs.readFileSync(POLICY_PATH, "utf8"));
|
|
const CONSOLE_PATH = policy.active_console?.state_path;
|
|
|
|
function expandHome(value) {
|
|
return value.replace(/^~(?=\/|$)/, "/Users/bingshuolingdianyuanhe");
|
|
}
|
|
|
|
function normal(value, cwd = process.cwd()) {
|
|
const expanded = expandHome(String(value || ""));
|
|
return path.resolve(cwd, expanded);
|
|
}
|
|
|
|
function patternRegex(pattern) {
|
|
const escaped = expandHome(pattern).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
return new RegExp(`^${escaped}(?:/.*)?$`);
|
|
}
|
|
|
|
function activeConsole(host) {
|
|
if (!CONSOLE_PATH || !fs.existsSync(CONSOLE_PATH)) return null;
|
|
const value = JSON.parse(fs.readFileSync(CONSOLE_PATH, "utf8"));
|
|
const session = process.env.CODEX_THREAD_ID || process.env.CODEX_SESSION_ID || process.env.GUANGHU_HOST_SESSION_ID;
|
|
if (value.schema !== "guanghu.dynamic-zero-core-console/v1" || value.state !== "ACTIVE_CURRENT_TASK" || value.human_anchor !== "ICE-GL∞" || value.channel_id !== "ICE-CH-ZC001" || value.host !== host || !session || value.session_id !== session) return null;
|
|
return value;
|
|
}
|
|
|
|
function protectedTarget(resolved) {
|
|
return policy.canonical_protected_roots.some(item => patternRegex(item).test(resolved));
|
|
}
|
|
|
|
function isAllowed(host, target) {
|
|
const rule = policy.hosts[host];
|
|
if (!rule) return { allowed: false, code: "HOST_UNKNOWN" };
|
|
if (rule.write_mode.startsWith("READ_ONLY")) return { allowed: false, code: "HOST_READ_ONLY" };
|
|
const resolved = normal(target);
|
|
if (protectedTarget(resolved)) {
|
|
const console = activeConsole(host);
|
|
const matched = console?.write_roots?.find((item) => patternRegex(item).test(resolved));
|
|
return matched
|
|
? { allowed: true, code: "ACTIVE_ZERO_CORE_CONSOLE_TASK_SCOPE", resolved, matched, console_id: console.console_id }
|
|
: { allowed: false, code: "PROTECTED_ROOT_REQUIRES_ACTIVE_ZERO_CORE_CONSOLE", resolved };
|
|
}
|
|
const matched = rule.allowed_write_roots.find((item) => patternRegex(item).test(resolved));
|
|
return matched
|
|
? { allowed: true, code: "WITHIN_HOST_WRITE_ROOT", resolved, matched }
|
|
: { allowed: false, code: "WRITE_OUTSIDE_HOST_ROOT", resolved };
|
|
}
|
|
|
|
function emit(result, hook = false) {
|
|
const reason = `${result.code}: ${result.host || "unknown"} -> ${result.resolved || result.path || "path-unresolved"}`;
|
|
if (hook) {
|
|
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: {
|
|
hookEventName: "PreToolUse",
|
|
permissionDecision: result.allowed ? "allow" : "deny",
|
|
permissionDecisionReason: reason,
|
|
} })}\n`);
|
|
} else {
|
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
}
|
|
if (!result.allowed) process.exitCode = 2;
|
|
}
|
|
|
|
function collectPathValues(value, out = []) {
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) collectPathValues(item, out);
|
|
} else if (value && typeof value === "object") {
|
|
for (const [key, item] of Object.entries(value)) {
|
|
if (typeof item === "string" && /(?:path|file|directory|cwd|workdir|target|destination)/i.test(key)) out.push(item);
|
|
else collectPathValues(item, out);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function shellPaths(command) {
|
|
const values = [];
|
|
for (const match of command.matchAll(/["'](\/[^"']+)["']/g)) values.push(match[1]);
|
|
for (const match of command.matchAll(/(?:^|[\s=])(\/[^\s;|&<>]+)/g)) values.push(match[1]);
|
|
return [...new Set(values)];
|
|
}
|
|
|
|
const READ_TOOLS = new Set(["Read", "Glob", "Grep", "Search", "WebSearch", "WebFetch"]);
|
|
const MUTATING_SHELL = /(?:^|[;&|\s])(?:rm|mv|cp|install|mkdir|rmdir|touch|chmod|chown|ln|tee|truncate|dd|rsync|git\s+(?:add|commit|push|checkout|restore|reset|clean|apply|merge|rebase|tag)|sed\s+-i|perl\s+-i|python\d*\s+[^\n]*(?:write|append|unlink|remove|rename)|node\s+[^\n]*(?:write|install|deploy)|npm\s+(?:install|publish)|pnpm\s+(?:install|publish)|apply_patch)(?:\s|$)|(?:>>?|2>)\s*[^&]/i;
|
|
|
|
function evaluateHook(host, input) {
|
|
const tool = String(input.tool_name || input.toolName || "");
|
|
const toolInput = input.tool_input || input.toolInput || {};
|
|
if (READ_TOOLS.has(tool)) return { allowed: true, code: "READ_ONLY_TOOL", host, tool };
|
|
if (tool === "Bash") {
|
|
const command = String(toolInput.command || "");
|
|
if (!MUTATING_SHELL.test(command)) return { allowed: true, code: "READ_ONLY_SHELL", host, tool };
|
|
const rule = policy.hosts[host];
|
|
if (!rule || rule.write_mode.startsWith("READ_ONLY")) return { allowed: false, code: "HOST_READ_ONLY", host, tool };
|
|
const candidates = shellPaths(command);
|
|
const explicitCwd = toolInput.cwd || toolInput.workdir || toolInput.working_directory;
|
|
const cdMatch = command.match(/(?:^|[;&|]\s*)cd\s+["']?(\/[^\n;&|"']+)/);
|
|
const cwd = normal(explicitCwd || cdMatch?.[1] || process.cwd());
|
|
const cwdCheck = isAllowed(host, cwd);
|
|
const pathChecks = candidates.map((item) => isAllowed(host, item));
|
|
const denied = pathChecks.find((item) => !item.allowed);
|
|
if (denied) return { ...denied, host, tool };
|
|
if (!cwdCheck.allowed && candidates.length === 0) return { ...cwdCheck, code: "MUTATING_SHELL_WITHOUT_ALLOWED_EXPLICIT_TARGET", host, tool };
|
|
return { allowed: true, code: "MUTATING_SHELL_WITHIN_HOST_ROOT", host, tool, resolved: cwd };
|
|
}
|
|
const candidates = collectPathValues(toolInput);
|
|
if (candidates.length === 0) return { allowed: false, code: "MUTATING_TOOL_TARGET_UNRESOLVED", host, tool };
|
|
for (const candidate of candidates) {
|
|
const check = isAllowed(host, candidate);
|
|
if (!check.allowed) return { ...check, host, tool };
|
|
}
|
|
return { allowed: true, code: "TOOL_TARGETS_WITHIN_HOST_ROOT", host, tool };
|
|
}
|
|
|
|
function valueAfter(args, flag) {
|
|
const index = args.indexOf(flag);
|
|
return index >= 0 ? args[index + 1] : undefined;
|
|
}
|
|
|
|
function atomic(file, value) {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
const temp = `${file}.${process.pid}.tmp`;
|
|
fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
fs.renameSync(temp, file);
|
|
}
|
|
|
|
const [mode, ...args] = process.argv.slice(2);
|
|
if (mode === "check") {
|
|
const host = valueAfter(args, "--host");
|
|
const target = valueAfter(args, "--path");
|
|
if (!host || !target) {
|
|
process.stderr.write("usage: check --host HOST --path PATH\n");
|
|
process.exit(2);
|
|
}
|
|
emit({ ...isAllowed(host, target), host, path: target });
|
|
} else if (mode === "hook") {
|
|
const host = valueAfter(args, "--host");
|
|
if (!host) {
|
|
process.stderr.write("usage: hook --host HOST\n");
|
|
process.exit(2);
|
|
}
|
|
let raw = "";
|
|
for await (const chunk of process.stdin) raw += chunk;
|
|
let input;
|
|
try { input = JSON.parse(raw || "{}"); }
|
|
catch { emit({ allowed: false, code: "HOOK_INPUT_INVALID_JSON", host }, true); process.exit(2); }
|
|
emit(evaluateHook(host, input), true);
|
|
} else if (mode === "audit") {
|
|
emit({ allowed: true, code: "POLICY_LOADED", policy_id: policy.policy_id, version: policy.version, policy_path: POLICY_PATH, active_console: activeConsole(valueAfter(args, "--host") || "") });
|
|
} else if (mode === "activate") {
|
|
const eventPath = valueAfter(args, "--event");
|
|
const event = JSON.parse(fs.readFileSync(eventPath, "utf8"));
|
|
if (event.schema !== "guanghu.dynamic-zero-core-console/v1" || event.state !== "ACTIVE_CURRENT_TASK" || event.human_anchor !== "ICE-GL∞" || event.channel_id !== "ICE-CH-ZC001" || !policy.hosts[event.host] || !event.session_id || !Array.isArray(event.write_roots) || !event.write_roots.length || !event.source_event_sha256?.match(/^[a-f0-9]{64}$/)) {
|
|
process.stderr.write("DYNAMIC_ZERO_CORE_CONSOLE_EVENT_INVALID\n"); process.exit(2);
|
|
}
|
|
atomic(CONSOLE_PATH, event); emit({ allowed: true, code: "ZERO_CORE_CONSOLE_ACTIVATED", host: event.host, session_id: event.session_id, console_id: event.console_id });
|
|
} else if (mode === "deactivate") {
|
|
const current = CONSOLE_PATH && fs.existsSync(CONSOLE_PATH) ? JSON.parse(fs.readFileSync(CONSOLE_PATH, "utf8")) : null;
|
|
if (current) atomic(CONSOLE_PATH, { ...current, state: "INACTIVE", deactivated_at: new Date().toISOString() });
|
|
emit({ allowed: true, code: "ZERO_CORE_CONSOLE_INACTIVE" });
|
|
} else {
|
|
process.stderr.write("usage: host-write-admission.mjs check|hook|audit|activate|deactivate\n");
|
|
process.exit(2);
|
|
}
|