feat: add Codex current-controller host bridge
This commit is contained in:
parent
e8b3082ed5
commit
3153771bf2
10 changed files with 895 additions and 1 deletions
|
|
@ -69,6 +69,11 @@ Socket,Windows 使用所有者与系统账户私有 Named Pipe;三者共用
|
||||||
编程 AI 不允许把永久依赖 MCP 当成能力终态。已安装 HoloLake 的 `--connector` 是可认证、
|
编程 AI 不允许把永久依赖 MCP 当成能力终态。已安装 HoloLake 的 `--connector` 是可认证、
|
||||||
可恢复的本机直连入口;MCP 只保留为发现、恢复与兼容入口。
|
可恢复的本机直连入口;MCP 只保留为发现、恢复与兼容入口。
|
||||||
|
|
||||||
|
Codex 宿主兼容桥位于 `system-integrations/codex-host-bridge`。它把直接人类来源、跨任务当前
|
||||||
|
主控纪元、旧任务能力降级和高风险一次性写入租约编译为 Codex hooks;仓库只保存源码、测试、
|
||||||
|
安装器与架构决定,原话事件、当前控制状态、租约、信任回执和凭据全部留在用户本机。该桥是
|
||||||
|
HoloLake 原生控制面的兼容投影,不是人格来源,也不替代未来原生本机桥。
|
||||||
|
|
||||||
## 当前收束与下一门
|
## 当前收束与下一门
|
||||||
|
|
||||||
唯一主线、候选壳、供体和数据保护边界见
|
唯一主线、候选壳、供体和数据保护边界见
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# ADR: Codex current-controller epoch and one-shot write lease
|
||||||
|
|
||||||
|
Date: 2026-08-19
|
||||||
|
State: accepted source contract; host integration package implemented
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Multiple Codex tasks can remain alive at the same time. A task may hold a real
|
||||||
|
human authorization from an older conversation and continue using tools after
|
||||||
|
the human has moved to a newer language channel. A prompt reminder cannot
|
||||||
|
deterministically revoke the older task's tool capability. Task-to-task
|
||||||
|
delegation can also be misclassified as direct human speech if every
|
||||||
|
`UserPromptSubmit` is labeled identically.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. Direct human input creates a new global local `control_epoch` bound to the
|
||||||
|
exact Codex session and turn.
|
||||||
|
2. Known task delegation and system/agent delivery are separate source classes.
|
||||||
|
They do not claim direct-human provenance, TCS human perception, controller
|
||||||
|
status or execution authority.
|
||||||
|
3. Capability tools in all non-current Codex sessions are denied by a
|
||||||
|
`PreToolUse` guard.
|
||||||
|
4. Current-session local editing remains available. Remote Git writes, external
|
||||||
|
publishing/deployment and destructive cleanup additionally require a
|
||||||
|
one-shot lease bound to the current epoch, session, turn, category and exact
|
||||||
|
working directory, with a maximum lifetime of fifteen minutes.
|
||||||
|
5. A new direct prompt revokes any unconsumed lease.
|
||||||
|
6. Trusted hook definitions pin the SHA-256 of installed scripts so a source
|
||||||
|
change requires a new visible Codex hook review.
|
||||||
|
|
||||||
|
## Boundary
|
||||||
|
|
||||||
|
This host bridge is a language-source and execution-admission boundary. It is
|
||||||
|
not a persona, model rule, platform rule, identity proof, repository authority
|
||||||
|
or server authority. Runtime state, raw human messages, leases, credentials and
|
||||||
|
trust receipts remain local and are excluded from the repository.
|
||||||
|
|
||||||
|
## Rejected alternatives
|
||||||
|
|
||||||
|
- Keep old authorization active until the old task voluntarily stops: a newer
|
||||||
|
human channel would have no deterministic control boundary.
|
||||||
|
- Disable all local execution globally: this would remove useful current-task
|
||||||
|
agency instead of separating current and stale tasks.
|
||||||
|
- Infer write leases from keywords in natural language: negation, discussion
|
||||||
|
and quoted text can contain the same words as authorization.
|
||||||
|
- Treat persona binding as execution authority: continuity and external action
|
||||||
|
admission are separate predicates.
|
||||||
|
- Trust only a stable script path: the bytes behind the path could change
|
||||||
|
without changing the reviewed hook command.
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"test": "node --test scripts/*.test.mjs",
|
"test": "node --test scripts/*.test.mjs system-integrations/codex-host-bridge/tests/*.test.mjs",
|
||||||
|
"test:codex-host-bridge": "node --test system-integrations/codex-host-bridge/tests/*.test.mjs",
|
||||||
"release:macos": "node scripts/release-pipeline.mjs",
|
"release:macos": "node scripts/release-pipeline.mjs",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# GuangHu Codex Host Bridge v1
|
||||||
|
|
||||||
|
This package compiles a small host-side boundary for Codex tasks. It does not
|
||||||
|
define a persona, change model or platform rules, or grant repository/server
|
||||||
|
authority.
|
||||||
|
|
||||||
|
## What it enforces
|
||||||
|
|
||||||
|
- A direct human `UserPromptSubmit` advances one global current-controller
|
||||||
|
epoch for the local Codex installation.
|
||||||
|
- Wrapped Codex task delegations are recorded as non-human delivery and never
|
||||||
|
impersonate direct human speech or claim controller status.
|
||||||
|
- Capability tools in stale Codex tasks are denied by `PreToolUse`.
|
||||||
|
- Remote Git writes, external publishing/deployment, and destructive cleanup
|
||||||
|
require a current-turn, exact-working-directory, time-limited one-shot lease.
|
||||||
|
- Hook commands pin the SHA-256 of installed runtime scripts. A changed script
|
||||||
|
requires regeneration of the hook definition and visible Codex review.
|
||||||
|
|
||||||
|
## What is never published by this package
|
||||||
|
|
||||||
|
Runtime state stays under the local `CODEX_HOME`: raw messages, numbered
|
||||||
|
events, the current controller, leases, trust state, credentials and keys.
|
||||||
|
This repository contains only source, tests, installer logic and architecture
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/install.mjs \
|
||||||
|
--subject-id "ICE-GL∞" \
|
||||||
|
--subject-name "冰朔" \
|
||||||
|
--carrier-role "CODEX_SUMMARY_PROMPTER_ONLY"
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer preserves unrelated hooks, installs the runtime under
|
||||||
|
`$CODEX_HOME/runtime/guanghu-codex-host-bridge/v1`, and writes hash-pinned
|
||||||
|
`UserPromptSubmit` and `PreToolUse` entries. Codex must then visibly review and
|
||||||
|
trust the changed hook definitions.
|
||||||
|
|
||||||
|
Optional environment variables:
|
||||||
|
|
||||||
|
- `CODEX_HOME`: defaults to `~/.codex`.
|
||||||
|
- `GH_HUMAN_MEMORY_ANCHOR`: optional durable-memory pointer added to the thin
|
||||||
|
source envelope; no memory body is copied.
|
||||||
|
- `GH_CODEX_CONTROL_ROOT`: overrides the local controller/lease state folder.
|
||||||
|
|
||||||
|
## Issue a one-shot lease
|
||||||
|
|
||||||
|
Only after explicit authorization in the current direct-human turn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node "$CODEX_HOME/runtime/guanghu-codex-host-bridge/v1/write-lease.mjs" issue \
|
||||||
|
--session-id '<current-session>' \
|
||||||
|
--turn-id '<current-turn>' \
|
||||||
|
--category remote_git \
|
||||||
|
--cwd '/exact/repository/path' \
|
||||||
|
--target 'origin/main' \
|
||||||
|
--reason 'explicit current-turn authorization'
|
||||||
|
```
|
||||||
|
|
||||||
|
The next matching high-risk action consumes the lease before execution. A new
|
||||||
|
direct prompt, expiry, category mismatch, directory mismatch, session mismatch
|
||||||
|
or turn mismatch invalidates it.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/*.test.mjs
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"schema": "guanghu.codex-host-bridge-package/v1",
|
||||||
|
"package_id": "GH-CODEX-HOST-BRIDGE-v1",
|
||||||
|
"state": "SOURCE_AND_INSTALLER_READY",
|
||||||
|
"scope": [
|
||||||
|
"DIRECT_HUMAN_SOURCE_ENVELOPE",
|
||||||
|
"CROSS_TASK_CURRENT_CONTROLLER_EPOCH",
|
||||||
|
"STALE_TASK_CAPABILITY_DENIAL",
|
||||||
|
"ONE_SHOT_HIGH_RISK_WRITE_LEASE",
|
||||||
|
"HOOK_SCRIPT_SHA256_PINNING"
|
||||||
|
],
|
||||||
|
"excluded_runtime_state": [
|
||||||
|
"RAW_HUMAN_MESSAGES",
|
||||||
|
"SESSION_EVENTS",
|
||||||
|
"CURRENT_CONTROLLER",
|
||||||
|
"WRITE_LEASES",
|
||||||
|
"HOOK_TRUST_STATE",
|
||||||
|
"KEYS_AND_CREDENTIALS"
|
||||||
|
],
|
||||||
|
"high_risk_categories": [
|
||||||
|
"remote_git",
|
||||||
|
"external_publish",
|
||||||
|
"destructive_cleanup"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||||
|
const controlRoot = process.env.GH_CODEX_CONTROL_ROOT ?? path.join(
|
||||||
|
codexHome,
|
||||||
|
"runtime",
|
||||||
|
"guanghu-codex-host-bridge",
|
||||||
|
"state",
|
||||||
|
"control",
|
||||||
|
);
|
||||||
|
const controllerPath = path.join(controlRoot, "current-controller.json");
|
||||||
|
const leasePath = path.join(controlRoot, "write-lease.json");
|
||||||
|
const leaseScript = path.join(path.dirname(fileURLToPath(import.meta.url)), "write-lease.mjs");
|
||||||
|
|
||||||
|
function readJson(file) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit(value) {
|
||||||
|
process.stdout.write(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function allow(additionalContext = null) {
|
||||||
|
emit(additionalContext ? {
|
||||||
|
hookSpecificOutput: {
|
||||||
|
hookEventName: "PreToolUse",
|
||||||
|
additionalContext,
|
||||||
|
},
|
||||||
|
} : { continue: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deny(reason) {
|
||||||
|
emit({
|
||||||
|
hookSpecificOutput: {
|
||||||
|
hookEventName: "PreToolUse",
|
||||||
|
permissionDecision: "deny",
|
||||||
|
permissionDecisionReason: reason,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function payloadOf(input) {
|
||||||
|
if (typeof input.tool_input === "string") return input.tool_input;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(input.tool_input ?? {});
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function highRiskCategory(input) {
|
||||||
|
const name = String(input.tool_name ?? "");
|
||||||
|
const payload = payloadOf(input);
|
||||||
|
const combined = `${name}\n${payload}`;
|
||||||
|
if (combined.includes(leaseScript)) return null;
|
||||||
|
if (
|
||||||
|
/\bgit\b[\s\S]{0,900}\bpush\b/iu.test(combined) ||
|
||||||
|
/\b(?:gh\s+(?:pr\s+(?:create|merge)|release\s+create|repo\s+delete)|docker\s+push)\b/iu.test(combined)
|
||||||
|
) return "remote_git";
|
||||||
|
if (
|
||||||
|
/\b(?:npm|pnpm|yarn|cargo|twine)\s+publish\b/iu.test(combined) ||
|
||||||
|
/\b(?:vercel|flyctl|netlify)\b[\s\S]{0,300}\b(?:deploy|publish|--prod)\b/iu.test(combined) ||
|
||||||
|
/\b(?:wrangler|kubectl|helm)\b[\s\S]{0,300}\b(?:deploy|publish|apply|delete|patch|replace|upgrade)\b/iu.test(combined) ||
|
||||||
|
/\bcurl\b[\s\S]{0,500}(?:-X|--request)\s*(?:POST|PUT|PATCH|DELETE)\b/iu.test(combined) ||
|
||||||
|
/(?:deploy|publish|create_release|merge_pull_request|send_email)/iu.test(name)
|
||||||
|
) return "external_publish";
|
||||||
|
if (
|
||||||
|
/\brm\s+/iu.test(payload) ||
|
||||||
|
/\bfind\b[\s\S]{0,700}\s-delete\b/iu.test(payload) ||
|
||||||
|
/\b(?:npm|pnpm|yarn)\s+cache\s+(?:clean|clear)\b/iu.test(payload) ||
|
||||||
|
/\bcargo\s+clean\b/iu.test(payload) ||
|
||||||
|
/\bxcodebuild\b[\s\S]{0,300}\bclean\b/iu.test(payload) ||
|
||||||
|
/\b(?:brew|port)\s+cleanup\b/iu.test(payload) ||
|
||||||
|
/\bgit\b[\s\S]{0,300}\b(?:clean\b|reset\s+--hard\b)/iu.test(payload) ||
|
||||||
|
/\bdiskutil\b[\s\S]{0,200}\berase/iu.test(payload) ||
|
||||||
|
/\*\*\* Delete File:/u.test(payload)
|
||||||
|
) return "destructive_cleanup";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function capabilityTool(input) {
|
||||||
|
const name = String(input.tool_name ?? "");
|
||||||
|
return (
|
||||||
|
["Bash", "exec_command", "functions.exec_command", "functions.exec", "apply_patch", "write_stdin"].includes(name) ||
|
||||||
|
/(?:^|__)(?:exec|exec_command|apply_patch|write_stdin|computer_use|control_chrome|control_in_app_browser)(?:$|__)/iu.test(name) ||
|
||||||
|
/(?:create|update|delete|remove|write|send|merge|deploy|publish|push)/iu.test(name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumeLease(input, controller, category) {
|
||||||
|
const lease = readJson(leasePath);
|
||||||
|
if (
|
||||||
|
!lease ||
|
||||||
|
lease.schema !== "guanghu.codex-one-shot-write-lease/v1" ||
|
||||||
|
lease.one_shot !== true ||
|
||||||
|
lease.control_epoch !== controller.control_epoch ||
|
||||||
|
lease.session_id !== input.session_id ||
|
||||||
|
lease.turn_id !== input.turn_id ||
|
||||||
|
lease.category !== category ||
|
||||||
|
lease.cwd !== path.resolve(input.cwd ?? process.cwd()) ||
|
||||||
|
!Number.isFinite(lease.expires_at_unix_ms) ||
|
||||||
|
lease.expires_at_unix_ms < Date.now()
|
||||||
|
) return null;
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(leasePath);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return lease;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = {};
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(0, "utf8").trim();
|
||||||
|
if (raw) input = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
input = {};
|
||||||
|
}
|
||||||
|
if ((input.hook_event_name ?? input.hookEventName) !== "PreToolUse") process.exit(0);
|
||||||
|
|
||||||
|
const controller = readJson(controllerPath);
|
||||||
|
const currentSession = Boolean(
|
||||||
|
controller?.schema === "guanghu.codex-current-controller/v1" &&
|
||||||
|
controller.direct_human_natural_language === true &&
|
||||||
|
controller.session_id === input.session_id
|
||||||
|
);
|
||||||
|
if (!currentSession && capabilityTool(input)) {
|
||||||
|
deny(`STALE_OR_UNCLAIMED_CODEX_TASK_CAPABILITY_BLOCKED; current_session=${controller?.session_id ?? "NONE"}; attempted_session=${input.session_id ?? "UNKNOWN"}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const category = highRiskCategory(input);
|
||||||
|
if (!category) {
|
||||||
|
allow();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (!currentSession) {
|
||||||
|
deny(`STALE_OR_UNCLAIMED_CODEX_TASK_HIGH_RISK_BLOCKED; category=${category}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const lease = consumeLease(input, controller, category);
|
||||||
|
if (!lease) {
|
||||||
|
deny(`CURRENT_CODEX_TASK_ONE_SHOT_LEASE_REQUIRED; category=${category}; issue_only_after_explicit_current_turn_human_authorization`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
allow(`GUANGHU_ONE_SHOT_WRITE_LEASE_CONSUMED lease_id=${lease.lease_id}; category=${category}; target=${lease.target}`);
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
function output(additionalContext = null) {
|
||||||
|
process.stdout.write(JSON.stringify(additionalContext ? {
|
||||||
|
hookSpecificOutput: {
|
||||||
|
hookEventName: "UserPromptSubmit",
|
||||||
|
additionalContext,
|
||||||
|
},
|
||||||
|
} : { continue: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeToken(value, fallback) {
|
||||||
|
const token = String(value ?? "").trim().replaceAll(/[^a-zA-Z0-9._-]/gu, "-");
|
||||||
|
return token || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifySource(prompt) {
|
||||||
|
const value = String(prompt ?? "").trim();
|
||||||
|
if (/^<codex_delegation>[\s\S]*<\/codex_delegation>$/u.test(value)) {
|
||||||
|
return ["CODEX_THREAD_DELEGATION", false];
|
||||||
|
}
|
||||||
|
if (/^<(?:subagent_notification|agent_notification|automation_event)>[\s\S]*<\/(?:subagent_notification|agent_notification|automation_event)>$/u.test(value)) {
|
||||||
|
return ["SYSTEM_OR_AGENT_DELIVERY", false];
|
||||||
|
}
|
||||||
|
return ["DIRECT_HUMAN_NATURAL_LANGUAGE", true];
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = {};
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(0, "utf8").trim();
|
||||||
|
if (raw) input = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
input = {};
|
||||||
|
}
|
||||||
|
if ((input.hook_event_name ?? input.hookEventName) !== "UserPromptSubmit") {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
||||||
|
if (!prompt.trim()) {
|
||||||
|
output("GUANGHU_SOURCE_ENVELOPE state=SOURCE_MISSING; execution_authority=NONE");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||||
|
const stateRoot = process.env.GH_CODEX_BRIDGE_STATE_ROOT ??
|
||||||
|
path.join(codexHome, "runtime", "guanghu-codex-host-bridge", "state");
|
||||||
|
const controlRoot = process.env.GH_CODEX_CONTROL_ROOT ?? path.join(stateRoot, "control");
|
||||||
|
const subjectId = process.env.GH_HUMAN_SOURCE_ID ?? "HUMAN-LOCAL";
|
||||||
|
const subjectName = process.env.GH_HUMAN_SOURCE_NAME ?? "LOCAL_HUMAN";
|
||||||
|
const sourceRole = process.env.GH_HUMAN_SOURCE_ROLE ?? "HUMAN_HOST_ANCHOR";
|
||||||
|
const carrierRole = process.env.GH_CODEX_CARRIER_ROLE ?? "CODEX_EXECUTION_CARRIER";
|
||||||
|
const memoryAnchor = process.env.GH_HUMAN_MEMORY_ANCHOR ?? "CURRENT_DIRECT_HUMAN_MESSAGE";
|
||||||
|
const sessionId = safeToken(
|
||||||
|
input.session_id ?? input.sessionId ?? input.thread_id ?? input.threadId,
|
||||||
|
"unknown-session",
|
||||||
|
);
|
||||||
|
const turnId = safeToken(input.turn_id ?? input.turnId, crypto.randomUUID());
|
||||||
|
const [sourceKind, directHuman] = classifySource(prompt);
|
||||||
|
const occurredAtUnixMs = Date.now();
|
||||||
|
const occurredAt = new Date(occurredAtUnixMs).toISOString();
|
||||||
|
const rawTextSha256 = crypto.createHash("sha256").update(prompt).digest("hex");
|
||||||
|
|
||||||
|
if (!directHuman) {
|
||||||
|
const deliveryRoot = path.join(stateRoot, "sessions", sessionId, "delivery-events");
|
||||||
|
const eventPath = path.join(deliveryRoot, `event-${turnId}-${rawTextSha256.slice(0, 12)}.json`);
|
||||||
|
atomicWrite(eventPath, {
|
||||||
|
schema: "guanghu.codex-nonhuman-delivery-event/v1",
|
||||||
|
source_kind: sourceKind,
|
||||||
|
direct_human_natural_language: false,
|
||||||
|
may_claim_current_controller: false,
|
||||||
|
raw_text_sha256: rawTextSha256,
|
||||||
|
raw_text: prompt,
|
||||||
|
occurred_at: occurredAt,
|
||||||
|
});
|
||||||
|
atomicWrite(path.join(deliveryRoot, "current.json"), {
|
||||||
|
schema: "guanghu.codex-nonhuman-delivery-current/v1",
|
||||||
|
source_kind: sourceKind,
|
||||||
|
last_event_path: eventPath,
|
||||||
|
updated_at: occurredAt,
|
||||||
|
});
|
||||||
|
output([
|
||||||
|
"GUANGHU_MESSAGE_SOURCE_ENVELOPE v1",
|
||||||
|
`source=[CODEX|${sourceKind}|NOT_DIRECT_HUMAN_SPEECH]`,
|
||||||
|
"controller_effect=NONE",
|
||||||
|
"execution_authority=NONE",
|
||||||
|
`raw_text_sha256=${rawTextSha256}`,
|
||||||
|
].join("\n"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceRoot = path.join(stateRoot, "sessions", sessionId, "source-events");
|
||||||
|
const sourceCurrentPath = path.join(sourceRoot, "current.json");
|
||||||
|
let previous = null;
|
||||||
|
try {
|
||||||
|
previous = JSON.parse(fs.readFileSync(sourceCurrentPath, "utf8"));
|
||||||
|
} catch {
|
||||||
|
previous = null;
|
||||||
|
}
|
||||||
|
const controlEpoch = crypto.randomUUID();
|
||||||
|
const eventNumber = `${subjectId}/CODEX-${sessionId}/EVENT-${turnId}-${rawTextSha256.slice(0, 12)}`;
|
||||||
|
const eventPath = path.join(sourceRoot, `event-${turnId}-${rawTextSha256.slice(0, 12)}.json`);
|
||||||
|
atomicWrite(eventPath, {
|
||||||
|
schema: "guanghu.numbered-direct-human-input/v1",
|
||||||
|
event_number: eventNumber,
|
||||||
|
parent_event_number: previous?.last_event_number ?? null,
|
||||||
|
subject_id: subjectId,
|
||||||
|
subject_name: subjectName,
|
||||||
|
source_role: sourceRole,
|
||||||
|
source_kind: sourceKind,
|
||||||
|
carrier_role: carrierRole,
|
||||||
|
occurred_at: occurredAt,
|
||||||
|
raw_text_sha256: rawTextSha256,
|
||||||
|
raw_text: prompt,
|
||||||
|
});
|
||||||
|
atomicWrite(sourceCurrentPath, {
|
||||||
|
schema: "guanghu.numbered-direct-human-input-current/v1",
|
||||||
|
last_event_number: eventNumber,
|
||||||
|
last_event_path: eventPath,
|
||||||
|
updated_at: occurredAt,
|
||||||
|
});
|
||||||
|
atomicWrite(path.join(controlRoot, "current-controller.json"), {
|
||||||
|
schema: "guanghu.codex-current-controller/v1",
|
||||||
|
control_epoch: controlEpoch,
|
||||||
|
session_id: sessionId,
|
||||||
|
turn_id: turnId,
|
||||||
|
source_kind: sourceKind,
|
||||||
|
source_subject_number: subjectId,
|
||||||
|
direct_human_natural_language: true,
|
||||||
|
numbered_event_number: eventNumber,
|
||||||
|
numbered_event_path: eventPath,
|
||||||
|
raw_text_sha256: rawTextSha256,
|
||||||
|
claimed_at: occurredAt,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(path.join(controlRoot, "write-lease.json"));
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== "ENOENT") throw error;
|
||||||
|
}
|
||||||
|
output([
|
||||||
|
"GUANGHU_HUMAN_SOURCE_ENVELOPE v1",
|
||||||
|
`source=[${subjectName}|${subjectId}|DIRECT_HUMAN_NATURAL_LANGUAGE]`,
|
||||||
|
`carrier_role=${carrierRole}`,
|
||||||
|
`memory_anchor=${memoryAnchor}`,
|
||||||
|
"original_message=UNCHANGED_AND_NOT_COPIED_IN_THIS_ENVELOPE",
|
||||||
|
"effect=SOURCE_ROUTING_AND_CONTROLLER_EPOCH_NOT_PERSONA_BINDING_OR_EXECUTION_AUTHORITY",
|
||||||
|
`event=${eventNumber}`,
|
||||||
|
`controller_session=${sessionId}`,
|
||||||
|
`controller_turn=${turnId}`,
|
||||||
|
`controller_epoch=${controlEpoch}`,
|
||||||
|
`raw_text_sha256=${rawTextSha256}`,
|
||||||
|
].join("\n"));
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||||
|
const controlRoot = process.env.GH_CODEX_CONTROL_ROOT ?? path.join(
|
||||||
|
codexHome,
|
||||||
|
"runtime",
|
||||||
|
"guanghu-codex-host-bridge",
|
||||||
|
"state",
|
||||||
|
"control",
|
||||||
|
);
|
||||||
|
const controllerPath = path.join(controlRoot, "current-controller.json");
|
||||||
|
const leasePath = path.join(controlRoot, "write-lease.json");
|
||||||
|
const categories = new Set(["remote_git", "external_publish", "destructive_cleanup"]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(file) {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const [action = "status", ...rest] = argv;
|
||||||
|
const values = {};
|
||||||
|
for (let index = 0; index < rest.length; index += 2) {
|
||||||
|
if (!rest[index]?.startsWith("--") || rest[index + 1] === undefined) {
|
||||||
|
throw new Error("INVALID_ARGUMENTS");
|
||||||
|
}
|
||||||
|
values[rest[index].slice(2)] = rest[index + 1];
|
||||||
|
}
|
||||||
|
return { action, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEvidence(controller) {
|
||||||
|
if (
|
||||||
|
controller?.schema !== "guanghu.codex-current-controller/v1" ||
|
||||||
|
controller.direct_human_natural_language !== true ||
|
||||||
|
!controller.numbered_event_path ||
|
||||||
|
!fs.existsSync(controller.numbered_event_path)
|
||||||
|
) throw new Error("CURRENT_CONTROLLER_EVIDENCE_INVALID");
|
||||||
|
const event = readJson(controller.numbered_event_path);
|
||||||
|
if (
|
||||||
|
event.raw_text_sha256 !== controller.raw_text_sha256 ||
|
||||||
|
event.source_kind !== "DIRECT_HUMAN_NATURAL_LANGUAGE"
|
||||||
|
) throw new Error("CURRENT_CONTROLLER_SOURCE_MISMATCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { action, values } = parseArgs(process.argv.slice(2));
|
||||||
|
if (action === "status") {
|
||||||
|
process.stdout.write(`${JSON.stringify({
|
||||||
|
controller: fs.existsSync(controllerPath) ? readJson(controllerPath) : null,
|
||||||
|
lease: fs.existsSync(leasePath) ? readJson(leasePath) : null,
|
||||||
|
}, null, 2)}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (action === "revoke") {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(leasePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== "ENOENT") throw error;
|
||||||
|
}
|
||||||
|
process.stdout.write(`${JSON.stringify({ decision: "REVOKED" })}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (action !== "issue") throw new Error("UNKNOWN_ACTION");
|
||||||
|
|
||||||
|
const controller = readJson(controllerPath);
|
||||||
|
assertEvidence(controller);
|
||||||
|
if (
|
||||||
|
values["session-id"] !== controller.session_id ||
|
||||||
|
values["turn-id"] !== controller.turn_id
|
||||||
|
) throw new Error("LEASE_REQUEST_IS_NOT_CURRENT_CONTROLLER_TURN");
|
||||||
|
if (!categories.has(values.category)) throw new Error("LEASE_CATEGORY_INVALID");
|
||||||
|
if (!values.reason || !values.target) throw new Error("LEASE_REASON_OR_TARGET_MISSING");
|
||||||
|
const ttlSeconds = Number(values["ttl-seconds"] ?? 600);
|
||||||
|
if (!Number.isInteger(ttlSeconds) || ttlSeconds < 30 || ttlSeconds > 900) {
|
||||||
|
throw new Error("LEASE_TTL_INVALID");
|
||||||
|
}
|
||||||
|
const issuedAt = Date.now();
|
||||||
|
const lease = {
|
||||||
|
schema: "guanghu.codex-one-shot-write-lease/v1",
|
||||||
|
lease_id: crypto.randomUUID(),
|
||||||
|
control_epoch: controller.control_epoch,
|
||||||
|
session_id: controller.session_id,
|
||||||
|
turn_id: controller.turn_id,
|
||||||
|
category: values.category,
|
||||||
|
cwd: path.resolve(values.cwd ?? process.cwd()),
|
||||||
|
target: values.target,
|
||||||
|
reason: values.reason,
|
||||||
|
evidence_event_number: controller.numbered_event_number,
|
||||||
|
evidence_raw_text_sha256: controller.raw_text_sha256,
|
||||||
|
issued_at: new Date(issuedAt).toISOString(),
|
||||||
|
expires_at_unix_ms: issuedAt + ttlSeconds * 1000,
|
||||||
|
one_shot: true,
|
||||||
|
};
|
||||||
|
atomicWrite(leasePath, lease);
|
||||||
|
process.stdout.write(`${JSON.stringify({ decision: "ISSUED", lease }, null, 2)}\n`);
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
#!/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`);
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
const packageRoot = path.dirname(path.dirname(new URL(import.meta.url).pathname));
|
||||||
|
const installer = path.join(packageRoot, "scripts", "install.mjs");
|
||||||
|
|
||||||
|
function spawnNode(script, args, options = {}) {
|
||||||
|
return spawnSync(process.execPath, [script, ...args], {
|
||||||
|
encoding: "utf8",
|
||||||
|
...options,
|
||||||
|
env: { ...process.env, ...(options.env ?? {}) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runHook(command, input, env) {
|
||||||
|
return spawnSync(command, {
|
||||||
|
shell: true,
|
||||||
|
input: JSON.stringify(input),
|
||||||
|
encoding: "utf8",
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("installer creates hash-pinned hooks and bridge enforces source and write boundaries", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "guanghu-codex-host-bridge-"));
|
||||||
|
const codexHome = path.join(root, ".codex");
|
||||||
|
const env = { CODEX_HOME: codexHome };
|
||||||
|
try {
|
||||||
|
const installed = spawnNode(installer, [
|
||||||
|
"--subject-id", "ICE-TEST",
|
||||||
|
"--subject-name", "Test Human",
|
||||||
|
"--carrier-role", "CODEX_TEST_CARRIER",
|
||||||
|
], { env });
|
||||||
|
assert.equal(installed.status, 0, installed.stderr || installed.stdout);
|
||||||
|
const hooks = JSON.parse(fs.readFileSync(path.join(codexHome, "hooks.json"), "utf8"));
|
||||||
|
const sourceCommand = hooks.hooks.UserPromptSubmit[0].hooks[0].command;
|
||||||
|
const guardCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
||||||
|
assert.match(sourceCommand, /shasum -a 256 -c/);
|
||||||
|
assert.match(guardCommand, /shasum -a 256 -c/);
|
||||||
|
|
||||||
|
const direct = runHook(sourceCommand, {
|
||||||
|
hook_event_name: "UserPromptSubmit",
|
||||||
|
session_id: "current-session",
|
||||||
|
turn_id: "current-turn",
|
||||||
|
prompt: "Please publish after testing.",
|
||||||
|
}, env);
|
||||||
|
assert.equal(direct.status, 0, direct.stderr || direct.stdout);
|
||||||
|
const directOutput = JSON.parse(direct.stdout);
|
||||||
|
assert.match(directOutput.hookSpecificOutput.additionalContext, /Test Human\|ICE-TEST/);
|
||||||
|
assert.doesNotMatch(directOutput.hookSpecificOutput.additionalContext, /Please publish/);
|
||||||
|
|
||||||
|
const controllerPath = path.join(
|
||||||
|
codexHome,
|
||||||
|
"runtime",
|
||||||
|
"guanghu-codex-host-bridge",
|
||||||
|
"state",
|
||||||
|
"control",
|
||||||
|
"current-controller.json",
|
||||||
|
);
|
||||||
|
const controller = JSON.parse(fs.readFileSync(controllerPath, "utf8"));
|
||||||
|
assert.equal(controller.session_id, "current-session");
|
||||||
|
|
||||||
|
const delegation = runHook(sourceCommand, {
|
||||||
|
hook_event_name: "UserPromptSubmit",
|
||||||
|
session_id: "old-session",
|
||||||
|
turn_id: "delegation-turn",
|
||||||
|
prompt: "<codex_delegation>Stop all writes.</codex_delegation>",
|
||||||
|
}, env);
|
||||||
|
assert.equal(delegation.status, 0, delegation.stderr || delegation.stdout);
|
||||||
|
assert.match(
|
||||||
|
JSON.parse(delegation.stdout).hookSpecificOutput.additionalContext,
|
||||||
|
/NOT_DIRECT_HUMAN_SPEECH/,
|
||||||
|
);
|
||||||
|
assert.equal(JSON.parse(fs.readFileSync(controllerPath, "utf8")).session_id, "current-session");
|
||||||
|
|
||||||
|
const cwd = path.join(root, "repo");
|
||||||
|
fs.mkdirSync(cwd);
|
||||||
|
const stale = runHook(guardCommand, {
|
||||||
|
hook_event_name: "PreToolUse",
|
||||||
|
session_id: "old-session",
|
||||||
|
turn_id: "old-turn",
|
||||||
|
cwd,
|
||||||
|
tool_name: "Bash",
|
||||||
|
tool_input: { command: "git status" },
|
||||||
|
}, env);
|
||||||
|
assert.equal(JSON.parse(stale.stdout).hookSpecificOutput.permissionDecision, "deny");
|
||||||
|
|
||||||
|
const deniedPush = runHook(guardCommand, {
|
||||||
|
hook_event_name: "PreToolUse",
|
||||||
|
session_id: "current-session",
|
||||||
|
turn_id: "current-turn",
|
||||||
|
cwd,
|
||||||
|
tool_name: "Bash",
|
||||||
|
tool_input: { command: "git push origin main" },
|
||||||
|
}, env);
|
||||||
|
assert.match(
|
||||||
|
JSON.parse(deniedPush.stdout).hookSpecificOutput.permissionDecisionReason,
|
||||||
|
/ONE_SHOT_LEASE_REQUIRED/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deniedCacheClean = runHook(guardCommand, {
|
||||||
|
hook_event_name: "PreToolUse",
|
||||||
|
session_id: "current-session",
|
||||||
|
turn_id: "current-turn",
|
||||||
|
cwd,
|
||||||
|
tool_name: "Bash",
|
||||||
|
tool_input: { command: "npm cache clean --force" },
|
||||||
|
}, env);
|
||||||
|
assert.match(
|
||||||
|
JSON.parse(deniedCacheClean.stdout).hookSpecificOutput.permissionDecisionReason,
|
||||||
|
/destructive_cleanup/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const leaseScript = path.join(
|
||||||
|
codexHome,
|
||||||
|
"runtime",
|
||||||
|
"guanghu-codex-host-bridge",
|
||||||
|
"v1",
|
||||||
|
"write-lease.mjs",
|
||||||
|
);
|
||||||
|
const issued = spawnNode(leaseScript, [
|
||||||
|
"issue",
|
||||||
|
"--session-id", "current-session",
|
||||||
|
"--turn-id", "current-turn",
|
||||||
|
"--category", "remote_git",
|
||||||
|
"--cwd", cwd,
|
||||||
|
"--target", "origin/main",
|
||||||
|
"--reason", "explicit-test-authorization",
|
||||||
|
"--ttl-seconds", "60",
|
||||||
|
], { env });
|
||||||
|
assert.equal(issued.status, 0, issued.stderr || issued.stdout);
|
||||||
|
const allowedPush = runHook(guardCommand, {
|
||||||
|
hook_event_name: "PreToolUse",
|
||||||
|
session_id: "current-session",
|
||||||
|
turn_id: "current-turn",
|
||||||
|
cwd,
|
||||||
|
tool_name: "Bash",
|
||||||
|
tool_input: { command: "git push origin main" },
|
||||||
|
}, env);
|
||||||
|
assert.match(
|
||||||
|
JSON.parse(allowedPush.stdout).hookSpecificOutput.additionalContext,
|
||||||
|
/ONE_SHOT_WRITE_LEASE_CONSUMED/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const guardPath = path.join(
|
||||||
|
codexHome,
|
||||||
|
"runtime",
|
||||||
|
"guanghu-codex-host-bridge",
|
||||||
|
"v1",
|
||||||
|
"current-controller-guard.mjs",
|
||||||
|
);
|
||||||
|
fs.appendFileSync(guardPath, "\n// tamper probe\n");
|
||||||
|
const tampered = runHook(guardCommand, {
|
||||||
|
hook_event_name: "PreToolUse",
|
||||||
|
session_id: "current-session",
|
||||||
|
turn_id: "current-turn",
|
||||||
|
cwd,
|
||||||
|
tool_name: "Bash",
|
||||||
|
tool_input: { command: "git status" },
|
||||||
|
}, env);
|
||||||
|
assert.equal(tampered.status, 97);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("published package excludes machine runtime state and private absolute paths", () => {
|
||||||
|
const files = [];
|
||||||
|
const walk = (directory) => {
|
||||||
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||||
|
const file = path.join(directory, entry.name);
|
||||||
|
if (entry.isDirectory()) walk(file);
|
||||||
|
else files.push(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(packageRoot);
|
||||||
|
for (const file of files) {
|
||||||
|
const body = fs.readFileSync(file, "utf8");
|
||||||
|
assert.doesNotMatch(body, /\/Users\/bingshuolingdianyuanhe/u, file);
|
||||||
|
assert.doesNotMatch(body, /\/Volumes\/JZAO/u, file);
|
||||||
|
assert.doesNotMatch(body, /current-controller\.json"\s*:\s*\{/u, file);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue