130 lines
4.5 KiB
JavaScript
130 lines
4.5 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
import crypto from "node:crypto";
|
||
|
|
import fs from "node:fs";
|
||
|
|
import os from "node:os";
|
||
|
|
import path from "node:path";
|
||
|
|
import { fileURLToPath } from "node:url";
|
||
|
|
|
||
|
|
function parseArgs(argv) {
|
||
|
|
const values = {};
|
||
|
|
for (let index = 0; index < argv.length; index += 2) {
|
||
|
|
if (!argv[index]?.startsWith("--") || argv[index + 1] === undefined) {
|
||
|
|
throw new Error("INVALID_ARGUMENTS");
|
||
|
|
}
|
||
|
|
values[argv[index].slice(2)] = argv[index + 1];
|
||
|
|
}
|
||
|
|
return values;
|
||
|
|
}
|
||
|
|
|
||
|
|
function sha256(file) {
|
||
|
|
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||
|
|
}
|
||
|
|
|
||
|
|
function shellQuote(value) {
|
||
|
|
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function pinnedCommand(file, hash, environment = {}) {
|
||
|
|
const assignments = Object.entries(environment)
|
||
|
|
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
||
|
|
.join(" ");
|
||
|
|
const body = [
|
||
|
|
`printf \"%s %s\\n\" \"${hash}\" ${shellQuote(file)}`,
|
||
|
|
"/usr/bin/shasum -a 256 -c - >/dev/null",
|
||
|
|
"exit 97",
|
||
|
|
`exec /usr/local/bin/node ${shellQuote(file)}`,
|
||
|
|
];
|
||
|
|
return `${assignments ? `/usr/bin/env ${assignments} ` : ""}/bin/sh -c ${shellQuote(`${body[0]} | ${body[1]} || ${body[2]}; ${body[3]}`)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function atomicWrite(file, value) {
|
||
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||
|
|
const temporary = `${file}.${process.pid}.tmp`;
|
||
|
|
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
||
|
|
encoding: "utf8",
|
||
|
|
mode: 0o600,
|
||
|
|
});
|
||
|
|
fs.renameSync(temporary, file);
|
||
|
|
}
|
||
|
|
|
||
|
|
const args = parseArgs(process.argv.slice(2));
|
||
|
|
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||
|
|
const hooksPath = path.join(codexHome, "hooks.json");
|
||
|
|
const installRoot = path.join(codexHome, "runtime", "guanghu-codex-host-bridge", "v1");
|
||
|
|
const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||
|
|
const runtimeSource = path.join(packageRoot, "runtime");
|
||
|
|
const runtimeFiles = [
|
||
|
|
"source-controller-gate.mjs",
|
||
|
|
"current-controller-guard.mjs",
|
||
|
|
"write-lease.mjs",
|
||
|
|
];
|
||
|
|
fs.mkdirSync(installRoot, { recursive: true });
|
||
|
|
for (const file of runtimeFiles) {
|
||
|
|
fs.copyFileSync(path.join(runtimeSource, file), path.join(installRoot, file));
|
||
|
|
fs.chmodSync(path.join(installRoot, file), 0o700);
|
||
|
|
}
|
||
|
|
|
||
|
|
let config = { hooks: {} };
|
||
|
|
if (fs.existsSync(hooksPath)) config = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
|
||
|
|
config.hooks ??= {};
|
||
|
|
const environment = {
|
||
|
|
GH_HUMAN_SOURCE_ID: args["subject-id"] ?? "HUMAN-LOCAL",
|
||
|
|
GH_HUMAN_SOURCE_NAME: args["subject-name"] ?? "LOCAL_HUMAN",
|
||
|
|
GH_HUMAN_SOURCE_ROLE: args["subject-role"] ?? "HUMAN_HOST_ANCHOR",
|
||
|
|
GH_CODEX_CARRIER_ROLE: args["carrier-role"] ?? "CODEX_EXECUTION_CARRIER",
|
||
|
|
GH_HUMAN_MEMORY_ANCHOR: process.env.GH_HUMAN_MEMORY_ANCHOR ?? "CURRENT_DIRECT_HUMAN_MESSAGE",
|
||
|
|
};
|
||
|
|
const definitions = {
|
||
|
|
UserPromptSubmit: {
|
||
|
|
matcher: ".*",
|
||
|
|
hooks: [{
|
||
|
|
type: "command",
|
||
|
|
command: pinnedCommand(
|
||
|
|
path.join(installRoot, "source-controller-gate.mjs"),
|
||
|
|
sha256(path.join(installRoot, "source-controller-gate.mjs")),
|
||
|
|
environment,
|
||
|
|
),
|
||
|
|
timeout: 10,
|
||
|
|
additionalContextLimit: 1536,
|
||
|
|
statusMessage: "GuangHu: attach direct-human source envelope and advance controller epoch",
|
||
|
|
}],
|
||
|
|
},
|
||
|
|
PreToolUse: {
|
||
|
|
matcher: ".*",
|
||
|
|
hooks: [{
|
||
|
|
type: "command",
|
||
|
|
command: pinnedCommand(
|
||
|
|
path.join(installRoot, "current-controller-guard.mjs"),
|
||
|
|
sha256(path.join(installRoot, "current-controller-guard.mjs")),
|
||
|
|
),
|
||
|
|
timeout: 5,
|
||
|
|
additionalContextLimit: 1024,
|
||
|
|
statusMessage: "GuangHu: verify current controller epoch and one-shot write lease",
|
||
|
|
}],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
for (const [event, definition] of Object.entries(definitions)) {
|
||
|
|
const existing = Array.isArray(config.hooks[event]) ? config.hooks[event] : [];
|
||
|
|
config.hooks[event] = [
|
||
|
|
...existing.filter((entry) => !JSON.stringify(entry).includes("guanghu-codex-host-bridge")),
|
||
|
|
definition,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
atomicWrite(hooksPath, config);
|
||
|
|
atomicWrite(path.join(installRoot, "installation-receipt.json"), {
|
||
|
|
schema: "guanghu.codex-host-bridge-installation/v1",
|
||
|
|
package_id: "GH-CODEX-HOST-BRIDGE-v1",
|
||
|
|
installed_at: new Date().toISOString(),
|
||
|
|
codex_home: codexHome,
|
||
|
|
installed_files: runtimeFiles.map((file) => ({
|
||
|
|
path: path.join(installRoot, file),
|
||
|
|
sha256: sha256(path.join(installRoot, file)),
|
||
|
|
})),
|
||
|
|
runtime_state_in_repository: false,
|
||
|
|
trust_state: "REQUIRES_VISIBLE_CODEX_REVIEW",
|
||
|
|
});
|
||
|
|
process.stdout.write(`${JSON.stringify({
|
||
|
|
decision: "INSTALLED_REVIEW_REQUIRED",
|
||
|
|
hooks_path: hooksPath,
|
||
|
|
install_root: installRoot,
|
||
|
|
}, null, 2)}\n`);
|