diff --git a/product-source/hololake-native-desktop/README.md b/product-source/hololake-native-desktop/README.md index a3e74cc5d..c069fb570 100644 --- a/product-source/hololake-native-desktop/README.md +++ b/product-source/hololake-native-desktop/README.md @@ -69,6 +69,11 @@ Socket,Windows 使用所有者与系统账户私有 Named Pipe;三者共用 编程 AI 不允许把永久依赖 MCP 当成能力终态。已安装 HoloLake 的 `--connector` 是可认证、 可恢复的本机直连入口;MCP 只保留为发现、恢复与兼容入口。 +Codex 宿主兼容桥位于 `system-integrations/codex-host-bridge`。它把直接人类来源、跨任务当前 +主控纪元、旧任务能力降级和高风险一次性写入租约编译为 Codex hooks;仓库只保存源码、测试、 +安装器与架构决定,原话事件、当前控制状态、租约、信任回执和凭据全部留在用户本机。该桥是 +HoloLake 原生控制面的兼容投影,不是人格来源,也不替代未来原生本机桥。 + ## 当前收束与下一门 唯一主线、候选壳、供体和数据保护边界见 diff --git a/product-source/hololake-native-desktop/docs/adr/ADR-CODEX-CURRENT-CONTROLLER-EPOCH-AND-ONE-SHOT-WRITE-LEASE-20260819.md b/product-source/hololake-native-desktop/docs/adr/ADR-CODEX-CURRENT-CONTROLLER-EPOCH-AND-ONE-SHOT-WRITE-LEASE-20260819.md new file mode 100644 index 000000000..34825bc8e --- /dev/null +++ b/product-source/hololake-native-desktop/docs/adr/ADR-CODEX-CURRENT-CONTROLLER-EPOCH-AND-ONE-SHOT-WRITE-LEASE-20260819.md @@ -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. diff --git a/product-source/hololake-native-desktop/package.json b/product-source/hololake-native-desktop/package.json index 91bd222cd..9c016ccd6 100644 --- a/product-source/hololake-native-desktop/package.json +++ b/product-source/hololake-native-desktop/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "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", "tauri": "tauri" }, diff --git a/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/README.md b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/README.md new file mode 100644 index 000000000..2c3eca2e2 --- /dev/null +++ b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/README.md @@ -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 '' \ + --turn-id '' \ + --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 +``` diff --git a/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/manifest.json b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/manifest.json new file mode 100644 index 000000000..6c0365679 --- /dev/null +++ b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/manifest.json @@ -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" + ] +} diff --git a/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/current-controller-guard.mjs b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/current-controller-guard.mjs new file mode 100644 index 000000000..74332ef74 --- /dev/null +++ b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/current-controller-guard.mjs @@ -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}`); diff --git a/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/source-controller-gate.mjs b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/source-controller-gate.mjs new file mode 100644 index 000000000..3b248639e --- /dev/null +++ b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/source-controller-gate.mjs @@ -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 (/^[\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")); diff --git a/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/write-lease.mjs b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/write-lease.mjs new file mode 100644 index 000000000..180dc95f2 --- /dev/null +++ b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/runtime/write-lease.mjs @@ -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`); diff --git a/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/scripts/install.mjs b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/scripts/install.mjs new file mode 100644 index 000000000..22dbcf81c --- /dev/null +++ b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/scripts/install.mjs @@ -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`); diff --git a/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/tests/bridge.test.mjs b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/tests/bridge.test.mjs new file mode 100644 index 000000000..e8998da2a --- /dev/null +++ b/product-source/hololake-native-desktop/system-integrations/codex-host-bridge/tests/bridge.test.mjs @@ -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: "Stop all writes.", + }, 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); + } +});