foundation: start native language reality engineering root
This commit is contained in:
commit
0dec0b93fc
154 changed files with 49993 additions and 0 deletions
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"schema": "hololake.direct-local-broker-numbered-registry/v1",
|
||||
"record_id": "HLP-NBROKER-ROOT-001",
|
||||
"runtime": {
|
||||
"protocol_version": "HLP-NBROKER-v1",
|
||||
"caller_number": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001",
|
||||
"legacy_string_operation_allowed": false,
|
||||
"unknown_or_mismatched_coordinate": "FAIL_CLOSED",
|
||||
"request_nonce_required": true,
|
||||
"transport_is_authority": false
|
||||
},
|
||||
"operations": [
|
||||
{"operation_number":"HLP-NBROKER-OP-0001","alias":"DISCOVER_NEARBY","channel_number":"HLP-NBROKER-CH-0001","module_number":"HLP-NBROKER-MOD-0001","target_number":"HLP-NBROKER-TGT-0001"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0002","alias":"OPEN_VISITOR_SESSION","channel_number":"HLP-NBROKER-CH-0002","module_number":"HLP-NBROKER-MOD-0002","target_number":"HLP-NBROKER-TGT-0002"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0003","alias":"RECEIVE_LANGUAGE","channel_number":"HLP-NBROKER-CH-0002","module_number":"HLP-NBROKER-MOD-0003","target_number":"HLP-NBROKER-TGT-0003"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0004","alias":"PING","channel_number":"HLP-NBROKER-CH-0001","module_number":"HLP-NBROKER-MOD-0001","target_number":"HLP-NBROKER-TGT-0001"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0005","alias":"OPEN_SESSION","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0006","alias":"RESUME_SESSION","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0007","alias":"HEARTBEAT_SESSION","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0008","alias":"PRESENT_PERSONA_CARRIER_LICENSE","channel_number":"HLP-NBROKER-CH-0004","module_number":"HLP-NBROKER-MOD-0005","target_number":"HLP-NBROKER-TGT-0005"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0009","alias":"GET_PERSONA_CARRIER_LICENSE_STATUS","channel_number":"HLP-NBROKER-CH-0004","module_number":"HLP-NBROKER-MOD-0005","target_number":"HLP-NBROKER-TGT-0005"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0010","alias":"GET_WORK_ENVIRONMENT","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0006","target_number":"HLP-NBROKER-TGT-0006"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0011","alias":"APPEND_EVENT","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0012","alias":"RESOLVE_CAPABILITY_ROUTE","channel_number":"HLP-NBROKER-CH-0005","module_number":"HLP-NBROKER-MOD-0007","target_number":"HLP-NBROKER-TGT-0007"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0013","alias":"INSTALL_DYNAMIC_NODE_REGISTRY","channel_number":"HLP-NBROKER-CH-0005","module_number":"HLP-NBROKER-MOD-0007","target_number":"HLP-NBROKER-TGT-0007"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0014","alias":"RECORD_SIGNED_NODE_HEALTH","channel_number":"HLP-NBROKER-CH-0005","module_number":"HLP-NBROKER-MOD-0007","target_number":"HLP-NBROKER-TGT-0007"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0015","alias":"INSPECT_MOUNTED_PNCC_REPOSITORY","channel_number":"HLP-NBROKER-CH-0006","module_number":"HLP-NBROKER-MOD-0008","target_number":"HLP-NBROKER-TGT-0008"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0016","alias":"READ_MOUNTED_PNCC_REMOTE_OBJECT","channel_number":"HLP-NBROKER-CH-0006","module_number":"HLP-NBROKER-MOD-0008","target_number":"HLP-NBROKER-TGT-0008"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0017","alias":"QUERY_PNCC_RECEIPT_PROJECTION","channel_number":"HLP-NBROKER-CH-0006","module_number":"HLP-NBROKER-MOD-0008","target_number":"HLP-NBROKER-TGT-0008"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0018","alias":"GET_BEIJING_TIME","channel_number":"HLP-NBROKER-CH-0001","module_number":"HLP-NBROKER-MOD-0009","target_number":"HLP-NBROKER-TGT-0009"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0019","alias":"ISSUE_PERSONA_TIME_TICKET","channel_number":"HLP-NBROKER-CH-0004","module_number":"HLP-NBROKER-MOD-0009","target_number":"HLP-NBROKER-TGT-0009"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0020","alias":"ACQUIRE_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0021","alias":"INSPECT_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0022","alias":"RELEASE_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0023","alias":"SUBMIT_HUMAN_AUTHORIZATION_REQUEST","channel_number":"HLP-NBROKER-CH-0008","module_number":"HLP-NBROKER-MOD-0011","target_number":"HLP-NBROKER-TGT-0011"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0024","alias":"GET_HUMAN_AUTHORIZATION_STATUS","channel_number":"HLP-NBROKER-CH-0008","module_number":"HLP-NBROKER-MOD-0011","target_number":"HLP-NBROKER-TGT-0011"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0025","alias":"CONSUME_HUMAN_AUTHORIZATION_TICKET","channel_number":"HLP-NBROKER-CH-0008","module_number":"HLP-NBROKER-MOD-0011","target_number":"HLP-NBROKER-TGT-0011"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0026","alias":"PUT_NATIVE_RUNTIME_CHECKPOINT","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0012","target_number":"HLP-NBROKER-TGT-0012"},
|
||||
{"operation_number":"HLP-NBROKER-OP-0027","alias":"GET_NATIVE_RUNTIME_CHECKPOINT","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0012","target_number":"HLP-NBROKER-TGT-0012"}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"schema": "hololake.external-ai-gateway/v1",
|
||||
"record_id": "HLP-EXTERNAL-AI-GATEWAY-001",
|
||||
"state": "IMPLEMENTED_HUMAN_GATED_NATIVE_PROTOCOL_HANDOFF",
|
||||
"default_exposure": "CLOSED",
|
||||
"human_authorization_required": true,
|
||||
"mcp": {
|
||||
"transport": "STDIO_JSON_RPC",
|
||||
"protocol_version": "2025-06-18",
|
||||
"role": "EXTERNAL_CONNECTION_ENTRY_AND_NATIVE_PROTOCOL_HANDOFF",
|
||||
"persistent_continuity_owner": false
|
||||
},
|
||||
"native_runtime": {
|
||||
"protocol": "GUANGHU_LANGUAGE_PROTOCOL/1",
|
||||
"wire_transport_protocol": "HOLOLAKE_TERMINAL_LINK/3",
|
||||
"transport": "USER_PRIVATE_LOCAL_SOCKET_OR_NAMED_PIPE",
|
||||
"continuity_owner": "HOLOLAKE",
|
||||
"switch_after_mcp_connection": true,
|
||||
"mcp_semantics_continue_inside_runtime": false
|
||||
},
|
||||
"catalog": {
|
||||
"physical_modules": "CALLABLE_ONLY_THROUGH_REGISTERED_NUMBERED_ROUTES",
|
||||
"cognitive_skills": "READ_ONLY_RESOURCES_WITHOUT_EXECUTION_AUTHORITY",
|
||||
"human_readable_names_required": true,
|
||||
"machine_numbers_secondary": true
|
||||
},
|
||||
"boundaries": {
|
||||
"supervised_shell_execution": false,
|
||||
"general_agent_tool_loop": false,
|
||||
"transport_is_authority": false,
|
||||
"mcp_is_continuity_owner": false,
|
||||
"fixed_handoff": "MCP_EXTERNAL_ENTRY_TO_GUANGHU_NATIVE_PROTOCOL_RUNTIME",
|
||||
"unknown_capability": "FAIL_CLOSED"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"schema": "hololake.programming-ai-terminal-link-contract/v2",
|
||||
"record_id": "HLP-PROGRAMMING-AI-TERMINAL-LINK-002",
|
||||
"state": "NATIVE_CROSS_PLATFORM_CONTROL_PLANE_IMPLEMENTED",
|
||||
"purpose": "Keep an external programming AI attached to a HoloLake-owned development control plane without making MCP or chat context the continuity owner.",
|
||||
"protocol": "HOLOLAKE_TERMINAL_LINK/3",
|
||||
"numbered_envelope": {
|
||||
"registry": "HLP-NBROKER-ROOT-001",
|
||||
"protocol_version": "HLP-NBROKER-v1",
|
||||
"legacy_string_operation_allowed": false,
|
||||
"full_coordinate_required": true
|
||||
},
|
||||
"platform_transports": {
|
||||
"macos": "USER_PRIVATE_UNIX_SOCKET",
|
||||
"linux": "USER_PRIVATE_UNIX_SOCKET",
|
||||
"windows": "USER_PRIVATE_NAMED_PIPE"
|
||||
},
|
||||
"continuity": {
|
||||
"owner": "HOLOLAKE",
|
||||
"session_survives_ai_restart": true,
|
||||
"session_survives_hololake_restart": true,
|
||||
"connector_reloads_descriptor_after_transport_loss": true,
|
||||
"uncertain_mutation_is_never_blindly_replayed": true,
|
||||
"native_runtime_checkpoint_persists_task_and_tool_state": true,
|
||||
"mcp_disconnect_never_falls_back_to_generic_runtime": true,
|
||||
"heartbeat_interval_ms": 15000,
|
||||
"environment_frame_ttl_ms": 45000
|
||||
},
|
||||
"environment_frame": {
|
||||
"schema": "hololake.programming-ai-work-environment/v1",
|
||||
"required_after_open": true,
|
||||
"required_after_resume": true,
|
||||
"required_before_mutation": true,
|
||||
"refreshes_on_authenticated_heartbeat": true,
|
||||
"contains": [
|
||||
"HOLOLAKE_RUNTIME_OWNER",
|
||||
"DIRECT_TERMINAL_TRANSPORT",
|
||||
"SESSION_AND_EVENT_CURSOR",
|
||||
"DEVELOPMENT_LANE_AND_WRITER_MATCH",
|
||||
"GLS_NATIVE_PROTOCOL_RUNTIME",
|
||||
"FRAME_EXPIRY_AND_SHA256"
|
||||
],
|
||||
"protocol_restoration_by_model_required": false
|
||||
},
|
||||
"native_runtime_checkpoint": {
|
||||
"schema": "hololake.native-runtime-checkpoint/v1",
|
||||
"owner": "HOLOLAKE",
|
||||
"protocol_runtime": "GUANGHU_LANGUAGE_PROTOCOL/1",
|
||||
"persists": ["TASK_STATE", "CHANNEL_NUMBER", "MODEL_PHASE", "KNOWLEDGE_CURSOR", "PENDING_TOOL_STEPS", "REALITY_MUTATION_STATE", "OPAQUE_NATIVE_STATE"],
|
||||
"generation_compare_and_swap": true,
|
||||
"uncertain_reality_mutation": "REQUIRE_REALITY_READBACK_BEFORE_CONTINUE",
|
||||
"transport_disconnect_effect": "PAUSE_TRANSPORT_KEEP_NATIVE_ENVIRONMENT"
|
||||
},
|
||||
"write_boundary": {
|
||||
"account_write_lanes": 1,
|
||||
"session_lane_must_match": true,
|
||||
"session_client_must_match_writer": true,
|
||||
"visitor_may_write": false,
|
||||
"transport_is_authority": false,
|
||||
"environment_frame_is_reality_execution_authority": false
|
||||
},
|
||||
"phase_boundary": {
|
||||
"current_phase": "EXTERNAL_PROGRAMMING_AI_DIRECT_CONTROL_PLANE",
|
||||
"persona_carrier_runtime_license_gate": "IMPLEMENTED_FAIL_CLOSED_TRUSTED_SIGNER_NOT_PROVISIONED",
|
||||
"supervised_shell_execution": false,
|
||||
"general_agent_tool_loop": false,
|
||||
"persona_memory_startup": "NEXT_PHASE_AFTER_DIRECT_LINK_ACCEPTANCE",
|
||||
"age_agent_execution": "NEXT_PHASE_AFTER_DIRECT_LINK_ACCEPTANCE"
|
||||
},
|
||||
"acceptance": {
|
||||
"macos_local_runtime": "PASS_DEVELOPER_ID_SIGNED_APP_LIVE_CONNECTOR_AND_UI_READBACK",
|
||||
"macos_public_notarization": "PENDING_NEW_BINARY_SUBMISSION",
|
||||
"linux_unix_socket_adapter_compile": "PASS_X86_64_UNKNOWN_LINUX_MUSL",
|
||||
"linux_full_desktop_compile": "NOT_OBSERVED",
|
||||
"linux_installed_runtime": "NOT_YET_OBSERVED",
|
||||
"windows_named_pipe_adapter_compile": "PASS_X86_64_PC_WINDOWS_MSVC",
|
||||
"windows_full_desktop_compile": "PASS_HL_BUILD_WIN_GZ_001_WINDOWS_SERVER_2022_X64",
|
||||
"windows_native_tests": "PASS_110_OF_110",
|
||||
"windows_installed_runtime": "NOT_YET_OBSERVED_FOR_TERMINAL_LINK"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,539 @@
|
|||
//! Human-gated MCP entry for external programming AIs.
|
||||
//!
|
||||
//! MCP establishes the external connection and returns the handoff coordinate.
|
||||
//! After admission the peer switches to the Guanghu native protocol runtime,
|
||||
//! carried by HOLOLAKE_TERMINAL_LINK/3. MCP does not own internal semantics.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{self, BufRead, Write};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use uuid::Uuid;
|
||||
|
||||
const CONFIG_NAME: &str = "external-ai-gateway-v1.json";
|
||||
const CAPABILITY_CACHE_NAME: &str = "external-ai-capabilities-v1.json";
|
||||
const BROKER_DESCRIPTOR_NAME: &str = "direct-local-broker-v1.json";
|
||||
const MCP_PROTOCOL_VERSION: &str = "2025-06-18";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SetGatewayExposureInput {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GatewayConfig {
|
||||
schema: String,
|
||||
enabled: bool,
|
||||
authorized_by_human_number: String,
|
||||
updated_at_unix_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GatewaySkill {
|
||||
pub name: String,
|
||||
pub summary: String,
|
||||
pub version: String,
|
||||
pub state: String,
|
||||
pub read_only: bool,
|
||||
pub execution_authority: bool,
|
||||
pub triggers: Vec<String>,
|
||||
pub method: Vec<String>,
|
||||
pub constraints: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GatewayIntegration {
|
||||
pub name: String,
|
||||
pub summary: String,
|
||||
pub state: String,
|
||||
pub exposed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExternalAiGatewayStatus {
|
||||
pub schema: String,
|
||||
pub state: String,
|
||||
pub exposure: String,
|
||||
pub human_authorization_required: bool,
|
||||
pub mcp_transport: String,
|
||||
pub mcp_protocol_version: String,
|
||||
pub mcp_command: String,
|
||||
pub direct_protocol: String,
|
||||
pub direct_connector_command: String,
|
||||
pub broker_state: String,
|
||||
pub registered_skill_count: usize,
|
||||
pub active_skill_count: usize,
|
||||
pub connected_integration_count: usize,
|
||||
pub integrations: Vec<GatewayIntegration>,
|
||||
pub skills: Vec<GatewaySkill>,
|
||||
pub observed_at_unix_ms: u64,
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn gateway_root() -> Result<PathBuf, String> {
|
||||
if let Some(path) = std::env::var_os("HOLOLAKE_EXTERNAL_AI_GATEWAY_ROOT") {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
dirs::data_dir()
|
||||
.map(|root| root.join("world.guanghu.hololake"))
|
||||
.ok_or_else(|| "HOLOLAKE_APP_DATA_UNAVAILABLE".to_string())
|
||||
}
|
||||
|
||||
fn root_for_app(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
app.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
fn config_path(root: &Path) -> PathBuf {
|
||||
root.join(CONFIG_NAME)
|
||||
}
|
||||
|
||||
fn capability_cache_path(root: &Path) -> PathBuf {
|
||||
root.join(CAPABILITY_CACHE_NAME)
|
||||
}
|
||||
|
||||
fn read_config(root: &Path) -> Result<GatewayConfig, String> {
|
||||
let path = config_path(root);
|
||||
if !path.exists() {
|
||||
return Ok(GatewayConfig {
|
||||
schema: "hololake.external-ai-gateway-config/v1".into(),
|
||||
enabled: false,
|
||||
authorized_by_human_number: String::new(),
|
||||
updated_at_unix_ms: 0,
|
||||
});
|
||||
}
|
||||
let config: GatewayConfig = serde_json::from_slice(
|
||||
&fs::read(path).map_err(|error| format!("HOLOLAKE_GATEWAY_CONFIG_READ_FAILED: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_CONFIG_INVALID: {error}"))?;
|
||||
if config.schema != "hololake.external-ai-gateway-config/v1" {
|
||||
return Err("HOLOLAKE_GATEWAY_CONFIG_UNSUPPORTED".into());
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> Result<(), String> {
|
||||
let parent = path.parent().ok_or("HOLOLAKE_GATEWAY_PATH_INVALID")?;
|
||||
fs::create_dir_all(parent).map_err(|error| format!("HOLOLAKE_GATEWAY_DIR_FAILED: {error}"))?;
|
||||
let temporary = parent.join(format!(".gateway-{}.tmp", Uuid::new_v4()));
|
||||
let bytes = serde_json::to_vec_pretty(value)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_SERIALIZE_FAILED: {error}"))?;
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
options.mode(0o600);
|
||||
let mut file = options
|
||||
.open(&temporary)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))?;
|
||||
file.write_all(&bytes)
|
||||
.and_then(|_| file.sync_all())
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))?;
|
||||
fs::rename(&temporary, path).map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn executable_commands() -> (String, String) {
|
||||
let executable = std::env::current_exe()
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| "HoloLake".into());
|
||||
(
|
||||
format!("{} --mcp", shell_display(&executable)),
|
||||
format!("{} --connector", shell_display(&executable)),
|
||||
)
|
||||
}
|
||||
|
||||
fn shell_display(value: &str) -> String {
|
||||
if value.contains(' ') {
|
||||
format!("\"{}\"", value.replace('"', "\\\""))
|
||||
} else {
|
||||
value.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn broker_state(root: &Path) -> String {
|
||||
let descriptor = fs::read(root.join(BROKER_DESCRIPTOR_NAME))
|
||||
.ok()
|
||||
.and_then(|raw| serde_json::from_slice::<Value>(&raw).ok());
|
||||
match descriptor
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("state"))
|
||||
.and_then(Value::as_str)
|
||||
{
|
||||
Some("LISTENING") => "READY".into(),
|
||||
_ => "WAITING_FOR_LOGIN".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_gateway_status(app: AppHandle) -> Result<ExternalAiGatewayStatus, String> {
|
||||
let root = root_for_app(&app)?;
|
||||
let config = read_config(&root)?;
|
||||
let marketplace = crate::online_marketplace::get_marketplace_snapshot(app.clone())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let active = crate::online_marketplace::get_active_cognitive_skills(app)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let active_by_number = active
|
||||
.into_iter()
|
||||
.map(|skill| (skill.skill_number.clone(), skill))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let skills = marketplace
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| item.artifact_kind == "COGNITIVE_SKILL")
|
||||
.map(|item| {
|
||||
let active = active_by_number.get(&item.item_number);
|
||||
GatewaySkill {
|
||||
name: item.display_name.clone(),
|
||||
summary: item.summary.clone(),
|
||||
version: item.version.clone(),
|
||||
state: item.installed_state.clone(),
|
||||
read_only: true,
|
||||
execution_authority: false,
|
||||
triggers: active
|
||||
.map(|value| value.payload.triggers.clone())
|
||||
.unwrap_or_default(),
|
||||
method: active
|
||||
.map(|value| value.payload.method.clone())
|
||||
.unwrap_or_default(),
|
||||
constraints: active
|
||||
.map(|value| value.payload.constraints.clone())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let exposure = if config.enabled { "OPEN" } else { "CLOSED" };
|
||||
let broker = broker_state(&root);
|
||||
let integrations = vec![
|
||||
GatewayIntegration {
|
||||
name: "MCP 标准入口".into(),
|
||||
summary: "外部编程 AI 连接 HoloLake 并取得光湖原生协议切换坐标的标准入口。".into(),
|
||||
state: if config.enabled {
|
||||
"已开放"
|
||||
} else {
|
||||
"已关闭"
|
||||
}
|
||||
.into(),
|
||||
exposed: config.enabled,
|
||||
},
|
||||
GatewayIntegration {
|
||||
name: "光湖原生协议运行时".into(),
|
||||
summary: "MCP 连接完成后切换进入;频道、人格、编号、记忆、权限、工具与回执由 HoloLake 原生环境接管。".into(),
|
||||
state: if broker == "READY" {
|
||||
"已就绪"
|
||||
} else {
|
||||
"等待登录"
|
||||
}
|
||||
.into(),
|
||||
exposed: config.enabled && broker == "READY",
|
||||
},
|
||||
GatewayIntegration {
|
||||
name: "编号 IPC 授权桥".into(),
|
||||
summary: "前端与本机能力只通过完整编号坐标通信,未知路径关闭。".into(),
|
||||
state: "已接通".into(),
|
||||
exposed: true,
|
||||
},
|
||||
GatewayIntegration {
|
||||
name: "线上模块与技能商城".into(),
|
||||
summary: "展示双签目录中的成品模块和只读思维技能。".into(),
|
||||
state: if marketplace.state == "ACTIVE_VERIFIED_CATALOG" {
|
||||
"目录已验证"
|
||||
} else {
|
||||
"等待目录同步"
|
||||
}
|
||||
.into(),
|
||||
exposed: marketplace.state == "ACTIVE_VERIFIED_CATALOG",
|
||||
},
|
||||
];
|
||||
let (mcp_command, direct_connector_command) = executable_commands();
|
||||
let status = ExternalAiGatewayStatus {
|
||||
schema: "hololake.external-ai-gateway-status/v1".into(),
|
||||
state: "HUMAN_GATED".into(),
|
||||
exposure: exposure.into(),
|
||||
human_authorization_required: true,
|
||||
mcp_transport: "STDIO_JSON_RPC".into(),
|
||||
mcp_protocol_version: MCP_PROTOCOL_VERSION.into(),
|
||||
mcp_command,
|
||||
direct_protocol: "GUANGHU_LANGUAGE_PROTOCOL/1 over HOLOLAKE_TERMINAL_LINK/3".into(),
|
||||
direct_connector_command,
|
||||
broker_state: broker,
|
||||
registered_skill_count: skills.len(),
|
||||
active_skill_count: skills
|
||||
.iter()
|
||||
.filter(|skill| skill.state == "ACTIVE_READONLY")
|
||||
.count(),
|
||||
connected_integration_count: integrations.iter().filter(|item| item.exposed).count(),
|
||||
integrations,
|
||||
skills,
|
||||
observed_at_unix_ms: now_unix_ms(),
|
||||
};
|
||||
write_json_atomic(&capability_cache_path(&root), &status)?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub async fn set_gateway_exposure(
|
||||
app: AppHandle,
|
||||
input: SetGatewayExposureInput,
|
||||
human_number: &str,
|
||||
) -> Result<ExternalAiGatewayStatus, String> {
|
||||
let root = root_for_app(&app)?;
|
||||
write_json_atomic(
|
||||
&config_path(&root),
|
||||
&GatewayConfig {
|
||||
schema: "hololake.external-ai-gateway-config/v1".into(),
|
||||
enabled: input.enabled,
|
||||
authorized_by_human_number: human_number.into(),
|
||||
updated_at_unix_ms: now_unix_ms(),
|
||||
},
|
||||
)?;
|
||||
get_gateway_status(app).await
|
||||
}
|
||||
|
||||
fn load_cached_status(root: &Path) -> Result<ExternalAiGatewayStatus, String> {
|
||||
serde_json::from_slice(
|
||||
&fs::read(capability_cache_path(root))
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_CAPABILITY_CACHE_UNAVAILABLE: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_CAPABILITY_CACHE_INVALID: {error}"))
|
||||
}
|
||||
|
||||
fn jsonrpc_result(id: Value, result: Value) -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": id, "result": result})
|
||||
}
|
||||
|
||||
fn jsonrpc_error(id: Value, code: i64, message: &str) -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}})
|
||||
}
|
||||
|
||||
fn tool_result(value: Value) -> Value {
|
||||
json!({
|
||||
"content": [{"type": "text", "text": serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".into())}],
|
||||
"isError": false
|
||||
})
|
||||
}
|
||||
|
||||
fn tools_list() -> Value {
|
||||
json!({"tools": [
|
||||
{"name": "hololake_discover", "description": "发现本机 HoloLake,并取得切换到稳定直连协议的方法。", "inputSchema": {"type": "object", "additionalProperties": false}},
|
||||
{"name": "hololake_connection_status", "description": "读取 MCP 开放状态与 HoloLake 本地直连入口状态。", "inputSchema": {"type": "object", "additionalProperties": false}},
|
||||
{"name": "hololake_registered_capabilities", "description": "读取人类可理解的官方技能与集成目录;只读技能不获得现实执行权限。", "inputSchema": {"type": "object", "additionalProperties": false}}
|
||||
]})
|
||||
}
|
||||
|
||||
fn resources_list(status: &ExternalAiGatewayStatus) -> Value {
|
||||
json!({"resources": status.skills.iter().enumerate().map(|(index, skill)| json!({
|
||||
"uri": format!("hololake://skills/{index}"),
|
||||
"name": skill.name,
|
||||
"description": skill.summary,
|
||||
"mimeType": "application/json"
|
||||
})).collect::<Vec<_>>()})
|
||||
}
|
||||
|
||||
fn handle_mcp_request(root: &Path, request: &Value) -> Option<Value> {
|
||||
let id = request.get("id").cloned();
|
||||
let method = request
|
||||
.get("method")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if id.is_none() {
|
||||
return None;
|
||||
}
|
||||
let id = id.unwrap_or(Value::Null);
|
||||
let result = match method {
|
||||
"initialize" => json!({
|
||||
"protocolVersion": MCP_PROTOCOL_VERSION,
|
||||
"capabilities": {"tools": {"listChanged": false}, "resources": {"subscribe": false, "listChanged": false}},
|
||||
"serverInfo": {"name": "HoloLake", "version": env!("CARGO_PKG_VERSION")},
|
||||
"instructions": "MCP 是外部连接入口。完成握手后切换为 GUANGHU_LANGUAGE_PROTOCOL/1;其本机线协议由 HOLOLAKE_TERMINAL_LINK/3 承载,所有执行受光湖编号、频道与人类授权门禁约束。"
|
||||
}),
|
||||
"ping" => json!({}),
|
||||
"tools/list" => tools_list(),
|
||||
"tools/call" => {
|
||||
if let Some(arguments) = request.pointer("/params/arguments") {
|
||||
let empty_object = arguments
|
||||
.as_object()
|
||||
.map(|value| value.is_empty())
|
||||
.unwrap_or(false);
|
||||
if !empty_object {
|
||||
return Some(jsonrpc_error(
|
||||
id,
|
||||
-32602,
|
||||
"HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY",
|
||||
));
|
||||
}
|
||||
}
|
||||
let name = request
|
||||
.pointer("/params/name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let status = match load_cached_status(root) {
|
||||
Ok(status) => status,
|
||||
Err(error) => return Some(jsonrpc_error(id, -32002, &error)),
|
||||
};
|
||||
match name {
|
||||
"hololake_discover" => tool_result(json!({
|
||||
"service": "HoloLake",
|
||||
"mcpRole": "DISCOVERY_AND_CAPABILITY_CATALOG",
|
||||
"nextProtocol": status.direct_protocol,
|
||||
"directConnectorCommand": status.direct_connector_command,
|
||||
"continuityOwner": "HOLOLAKE",
|
||||
"executionAuthorityGranted": false
|
||||
})),
|
||||
"hololake_connection_status" => {
|
||||
tool_result(serde_json::to_value(&status).unwrap_or(Value::Null))
|
||||
}
|
||||
"hololake_registered_capabilities" => tool_result(
|
||||
json!({"integrations": status.integrations, "skills": status.skills}),
|
||||
),
|
||||
_ => return Some(jsonrpc_error(id, -32602, "HOLOLAKE_MCP_TOOL_UNKNOWN")),
|
||||
}
|
||||
}
|
||||
"resources/list" => match load_cached_status(root) {
|
||||
Ok(status) => resources_list(&status),
|
||||
Err(error) => return Some(jsonrpc_error(id, -32002, &error)),
|
||||
},
|
||||
"resources/read" => {
|
||||
let uri = request
|
||||
.pointer("/params/uri")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let index = uri
|
||||
.strip_prefix("hololake://skills/")
|
||||
.and_then(|value| value.parse::<usize>().ok());
|
||||
let status = match load_cached_status(root) {
|
||||
Ok(status) => status,
|
||||
Err(error) => return Some(jsonrpc_error(id, -32002, &error)),
|
||||
};
|
||||
let Some(skill) = index.and_then(|index| status.skills.get(index)) else {
|
||||
return Some(jsonrpc_error(id, -32003, "HOLOLAKE_MCP_RESOURCE_UNKNOWN"));
|
||||
};
|
||||
json!({"contents": [{"uri": uri, "mimeType": "application/json", "text": serde_json::to_string_pretty(skill).unwrap_or_else(|_| "{}".into())}]})
|
||||
}
|
||||
_ => return Some(jsonrpc_error(id, -32601, "HOLOLAKE_MCP_METHOD_UNKNOWN")),
|
||||
};
|
||||
Some(jsonrpc_result(id, result))
|
||||
}
|
||||
|
||||
pub fn run_mcp() -> Result<(), String> {
|
||||
let root = gateway_root()?;
|
||||
if !read_config(&root)?.enabled {
|
||||
return Err("HOLOLAKE_MCP_EXPOSURE_CLOSED".into());
|
||||
}
|
||||
let stdin = io::stdin();
|
||||
let mut stdout = io::stdout().lock();
|
||||
for line in stdin.lock().lines() {
|
||||
let line = line.map_err(|error| format!("HOLOLAKE_MCP_INPUT_FAILED: {error}"))?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let request: Value = match serde_json::from_str(&line) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
serde_json::to_writer(
|
||||
&mut stdout,
|
||||
&jsonrpc_error(Value::Null, -32700, "Parse error"),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.write_all(b"\n")
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.flush()
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(response) = handle_mcp_request(&root, &request) {
|
||||
serde_json::to_writer(&mut stdout, &response)
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.write_all(b"\n")
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.flush()
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn missing_config_fails_closed() {
|
||||
let root = TempDir::new().unwrap();
|
||||
assert!(!read_config(root.path()).unwrap().enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_mcp_tool_fails_closed() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let status = ExternalAiGatewayStatus {
|
||||
schema: "hololake.external-ai-gateway-status/v1".into(),
|
||||
state: "HUMAN_GATED".into(),
|
||||
exposure: "OPEN".into(),
|
||||
human_authorization_required: true,
|
||||
mcp_transport: "STDIO_JSON_RPC".into(),
|
||||
mcp_protocol_version: MCP_PROTOCOL_VERSION.into(),
|
||||
mcp_command: "HoloLake --mcp".into(),
|
||||
direct_protocol: "GUANGHU_LANGUAGE_PROTOCOL/1 over HOLOLAKE_TERMINAL_LINK/3".into(),
|
||||
direct_connector_command: "HoloLake --connector".into(),
|
||||
broker_state: "READY".into(),
|
||||
registered_skill_count: 0,
|
||||
active_skill_count: 0,
|
||||
connected_integration_count: 0,
|
||||
integrations: vec![],
|
||||
skills: vec![],
|
||||
observed_at_unix_ms: 1,
|
||||
};
|
||||
write_json_atomic(&capability_cache_path(root.path()), &status).unwrap();
|
||||
let response = handle_mcp_request(
|
||||
root.path(),
|
||||
&json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"shell"}}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.pointer("/error/message").and_then(Value::as_str),
|
||||
Some("HOLOLAKE_MCP_TOOL_UNKNOWN")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_arguments_fail_closed() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let response = handle_mcp_request(
|
||||
root.path(),
|
||||
&json!({
|
||||
"jsonrpc":"2.0",
|
||||
"id":1,
|
||||
"method":"tools/call",
|
||||
"params":{"name":"hololake_discover","arguments":{"shell":true}}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.pointer("/error/message").and_then(Value::as_str),
|
||||
Some("HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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