收紧平台与编辑器编译范围并分离构建校验

分离 Codex 构建端元数据校验并保留原有测试
限定 Linux 沙箱与进程清理入口,删除无调用的平台占位实现
按实际 feature 收紧编辑器注册并删除冗余宿主包装
保留 Runner 备用端口算法的 Linux 与测试编译入口
同步警告清单、插件契约及定向验证边界
This commit is contained in:
2026-09-23 17:10:39 +00:00
parent b39cd555ae
commit c62a911745
12 changed files with 143 additions and 241 deletions
@@ -1,5 +1,7 @@
#[path = "build_support/codex_bundle.rs"]
mod codex_bundle;
#[path = "build_support/codex_package_metadata.rs"]
mod codex_package_metadata;
#[path = "build_support/frontend_dist_guard.rs"]
mod frontend_dist_guard;
#[path = "build_support/godot_bundle.rs"]
@@ -64,7 +66,7 @@ fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) {
.parent()
.and_then(|apps_dir| apps_dir.parent())
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
let package = layout.npm_package;
let package = format!("codex-{}", layout.platform);
let source_candidates = [app_root, repo_root]
.into_iter()
.flat_map(|root| {
@@ -99,7 +101,7 @@ fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) {
&fs::read(source.join("codex-package.json")).expect("读取 Codex 原生包元数据失败"),
)
.expect("Codex 原生包元数据无效");
codex_bundle::validate_package_metadata(&metadata, target, layout)
codex_package_metadata::validate_package_metadata(&metadata, target, layout)
.unwrap_or_else(|error| panic!("{error}"));
let target_dir = manifest_dir.join("resources/codex").join(layout.directory);
let notice = target_dir.join("NOTICE.md");
@@ -7,7 +7,6 @@ pub const SCHEMA: &str = "genarrative-codex-sidecar.v2";
#[derive(Clone, Copy, Debug)]
pub struct Layout {
pub platform: &'static str,
pub npm_package: &'static str,
pub directory: &'static str,
pub executable: &'static str,
pub files: &'static [&'static str],
@@ -33,7 +32,6 @@ pub fn for_target(target: &str) -> Option<Layout> {
match target {
"x86_64-pc-windows-msvc" => Some(Layout {
platform: "win32-x64",
npm_package: "codex-win32-x64",
directory: "win-x64",
executable: "bin/codex.exe",
files: WINDOWS_FILES,
@@ -44,11 +42,6 @@ pub fn for_target(target: &str) -> Option<Layout> {
} else {
"darwin-x64"
},
npm_package: if target.starts_with("aarch64") {
"codex-darwin-arm64"
} else {
"codex-darwin-x64"
},
directory: if target.starts_with("aarch64") {
"mac-native/darwin-arm64"
} else {
@@ -61,24 +54,6 @@ pub fn for_target(target: &str) -> Option<Layout> {
}
}
pub fn validate_package_metadata(
metadata: &serde_json::Value,
target: &str,
layout: Layout,
) -> Result<(), String> {
if metadata["layoutVersion"] == 1
&& metadata["version"] == VERSION
&& metadata["target"] == target
&& metadata["entrypoint"] == layout.executable
&& metadata["resourcesDir"] == "codex-resources"
&& metadata["pathDir"] == "codex-path"
{
Ok(())
} else {
Err(format!("Codex 原生包版本、布局或架构不匹配目标 {target}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -87,13 +62,11 @@ mod tests {
fn platform_layouts_are_explicit_and_preserve_upstream_components() {
let mac = for_target("aarch64-apple-darwin").unwrap();
assert_eq!(mac.platform, "darwin-arm64");
assert_eq!(mac.npm_package, "codex-darwin-arm64");
assert!(mac.files.contains(&"codex-resources/zsh/bin/zsh"));
assert!(mac.files.contains(&"bin/codex-code-mode-host"));
assert!(!mac.files.iter().any(|file| file.ends_with(".exe")));
let intel = for_target("x86_64-apple-darwin").unwrap();
assert_eq!(intel.platform, "darwin-x64");
assert_eq!(intel.npm_package, "codex-darwin-x64");
assert_eq!(mac.directory, "mac-native/darwin-arm64");
assert_eq!(intel.directory, "mac-native/darwin-x64");
assert_ne!(mac.directory, intel.directory);
@@ -107,34 +80,4 @@ mod tests {
assert!(for_target("aarch64-pc-windows-msvc").is_none());
assert!(for_target("x86_64-unknown-linux-gnu").is_none());
}
#[test]
fn metadata_rejects_version_architecture_and_layout_drift() {
let target = "aarch64-apple-darwin";
let layout = for_target(target).unwrap();
let valid = serde_json::json!({
"layoutVersion": 1,
"version": VERSION,
"target": target,
"entrypoint": "bin/codex",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
});
assert!(validate_package_metadata(&valid, target, layout).is_ok());
for (key, value) in [
("layoutVersion", serde_json::json!(2)),
("version", serde_json::json!("0.0.0")),
("target", serde_json::json!("x86_64-apple-darwin")),
("entrypoint", serde_json::json!("bin/codex.exe")),
("resourcesDir", serde_json::json!("../private")),
("pathDir", serde_json::json!(null)),
] {
let mut invalid = valid.clone();
invalid[key] = value;
assert!(
validate_package_metadata(&invalid, target, layout).is_err(),
"{key}"
);
}
}
}
@@ -0,0 +1,57 @@
//! 随包阶段的原生包元数据校验,不进入运行时生产模块。
use super::codex_bundle::{Layout, VERSION};
pub fn validate_package_metadata(
metadata: &serde_json::Value,
target: &str,
layout: Layout,
) -> Result<(), String> {
if metadata["layoutVersion"] == 1
&& metadata["version"] == VERSION
&& metadata["target"] == target
&& metadata["entrypoint"] == layout.executable
&& metadata["resourcesDir"] == "codex-resources"
&& metadata["pathDir"] == "codex-path"
{
Ok(())
} else {
Err(format!("Codex 原生包版本、布局或架构不匹配目标 {target}"))
}
}
#[cfg(test)]
mod tests {
use super::super::codex_bundle::for_target;
use super::*;
#[test]
fn metadata_rejects_version_architecture_and_layout_drift() {
let target = "aarch64-apple-darwin";
let layout = for_target(target).unwrap();
let valid = serde_json::json!({
"layoutVersion": 1,
"version": VERSION,
"target": target,
"entrypoint": "bin/codex",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
});
assert!(validate_package_metadata(&valid, target, layout).is_ok());
for (key, value) in [
("layoutVersion", serde_json::json!(2)),
("version", serde_json::json!("0.0.0")),
("target", serde_json::json!("x86_64-apple-darwin")),
("entrypoint", serde_json::json!("bin/codex.exe")),
("resourcesDir", serde_json::json!("../private")),
("pathDir", serde_json::json!(null)),
] {
let mut invalid = valid.clone();
invalid[key] = value;
assert!(
validate_package_metadata(&invalid, target, layout).is_err(),
"{key}"
);
}
}
}
@@ -8,6 +8,11 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
#[path = "../../build_support/codex_bundle.rs"]
pub(crate) mod codex_bundle;
// 复用构建端校验的既有单测,生产运行时只编译共享布局。
#[cfg(test)]
#[path = "../../build_support/codex_package_metadata.rs"]
mod codex_package_metadata;
const GAME_CREATOR_CODEX_CLI_EXECUTABLE: &str = "codex";
const GAME_CREATOR_CODEX_CLI_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
const GAME_CREATOR_CODEX_CLI_STDOUT_MAX_BYTES: usize = 4 * 1024 * 1024;
@@ -1968,60 +1968,14 @@ fn request_unix_project_command_process_group_termination(
Err(format!("请求终止受控进程组失败:{error}"))
}
async fn request_project_command_process_group_termination(
process_id: u32,
) -> Result<&'static str, String> {
#[cfg(unix)]
{
return request_unix_project_command_process_group_termination(process_id);
}
#[cfg(windows)]
{
let system_root = std::env::var_os("SystemRoot")
.ok_or_else(|| "请求终止受控进程组失败:缺少 SystemRoot".to_string())?;
let taskkill = fs::canonicalize(PathBuf::from(&system_root).join("System32/taskkill.exe"))
.map_err(|error| format!("请求终止受控进程组失败:定位 taskkill.exe 失败:{error}"))?;
if !taskkill.is_absolute() || !taskkill.is_file() {
return Err("请求终止受控进程组失败:taskkill.exe 不是绝对普通文件".to_string());
}
let mut command = tokio::process::Command::new(taskkill);
command
.args(["/PID", &process_id.to_string(), "/T", "/F"])
.env_clear()
.env("SystemRoot", &system_root)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
crate::configure_windows_background_tokio_command(&mut command, false);
let status = command
.status()
.await
.map_err(|error| format!("请求终止受控进程组失败:启动 taskkill.exe 失败:{error}"))?;
if !status.success() {
return Err(format!(
"请求终止受控进程组失败:taskkill.exe 退出码 {}",
status
.code()
.map(|code| code.to_string())
.unwrap_or_else(|| "none".to_string())
));
}
return Ok("已请求终止受控进程组");
}
#[cfg(not(any(unix, windows)))]
{
let _ = process_id;
Err("请求终止受控进程组失败:当前平台不支持受控进程组终止".to_string())
}
}
#[cfg(target_os = "linux")]
async fn terminate_project_command_process_group(
child: &mut tokio::process::Child,
) -> Result<String, String> {
let process_id = child
.id()
.ok_or_else(|| "请求终止受控进程组失败:子进程缺少 pid".to_string())?;
let group_result = request_project_command_process_group_termination(process_id).await;
let group_result = request_unix_project_command_process_group_termination(process_id);
let child_kill_error = child.start_kill().err();
let wait_result = child.wait().await;
if let Err(error) = &group_result {
@@ -1,7 +1,10 @@
#[cfg(target_os = "linux")]
use std::ffi::OsStr;
#[cfg(target_os = "linux")]
use std::ffi::OsString;
#[cfg(target_os = "linux")]
use std::fmt;
#[cfg(target_os = "linux")]
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
@@ -48,6 +51,7 @@ impl CommandSandboxMetadata {
}
}
#[cfg(target_os = "linux")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct CommandSandboxLaunch {
pub(crate) executable: PathBuf,
@@ -66,12 +70,14 @@ pub(crate) struct StagedCommandSandboxLaunch {
pub(crate) gate: LaunchGate,
}
#[cfg(target_os = "linux")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct CommandSandboxError {
message: String,
metadata: CommandSandboxMetadata,
}
#[cfg(target_os = "linux")]
impl CommandSandboxError {
fn new(message: impl Into<String>, metadata: CommandSandboxMetadata) -> Self {
Self {
@@ -79,19 +85,16 @@ impl CommandSandboxError {
metadata,
}
}
#[cfg(not(target_os = "linux"))]
pub(crate) fn metadata(&self) -> &CommandSandboxMetadata {
&self.metadata
}
}
#[cfg(target_os = "linux")]
impl fmt::Display for CommandSandboxError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
#[cfg(target_os = "linux")]
impl std::error::Error for CommandSandboxError {}
pub(crate) fn command_sandbox_platform_metadata() -> CommandSandboxMetadata {
@@ -107,6 +110,7 @@ pub(crate) fn command_sandbox_platform_metadata() -> CommandSandboxMetadata {
/// Builds a fail-closed launcher for a direct executable plus structured argv.
/// It never falls back to launching the original command on the host.
#[cfg(target_os = "linux")]
pub(crate) fn prepare_command_sandbox_launch(
root: &Path,
executable: &Path,
@@ -114,19 +118,7 @@ pub(crate) fn prepare_command_sandbox_launch(
cwd: &Path,
environment: &[(OsString, OsString)],
) -> Result<CommandSandboxLaunch, CommandSandboxError> {
#[cfg(target_os = "linux")]
{
prepare_linux_command_sandbox_launch(root, executable, arguments, cwd, environment)
}
#[cfg(not(target_os = "linux"))]
{
let _ = (root, executable, arguments, cwd, environment);
let metadata = CommandSandboxMetadata::unsupported_legacy();
Err(CommandSandboxError::new(
"当前平台没有可用的 OS-enforced workspace sandbox;拒绝宿主直通执行",
metadata,
))
}
prepare_linux_command_sandbox_launch(root, executable, arguments, cwd, environment)
}
#[cfg(target_os = "linux")]
@@ -1446,23 +1438,3 @@ print("SANDBOX_OK")
#[cfg(target_os = "linux")]
use linux::prepare_linux_command_sandbox_launch;
#[cfg(all(test, not(target_os = "linux")))]
mod unsupported_tests {
use super::*;
#[test]
fn unsupported_platform_returns_legacy_metadata_without_launcher() {
let error = prepare_command_sandbox_launch(
Path::new("."),
Path::new("tool"),
&[],
Path::new("."),
&[],
)
.expect_err("unsupported platform must fail closed");
assert_eq!(error.metadata().backend, "legacy-host-restricted");
assert_eq!(error.metadata().mode, "fixed-command");
assert_eq!(error.metadata().network, "proxy-only");
}
}
@@ -5,4 +5,6 @@
//! plugin package they belong to under the `plugins/` workspace. This module
//! only re-exports the shared contract so the host stays editor-agnostic.
pub(crate) use editor_adapter_api::{EditorAdapter, EditorConnectionInfo};
pub(crate) use editor_adapter_api::EditorAdapter;
#[cfg(test)]
pub(crate) use editor_adapter_api::EditorConnectionInfo;
@@ -11,16 +11,45 @@ use std::path::PathBuf;
use tauri::Manager;
use crate::plugin_host::PluginHost;
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
use editor_adapter_api::{EditorAdapter, EditorConnectionInfo};
use serde_json::{json, Value};
#[cfg(any(
test,
all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
)
))]
use serde_json::json;
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
use serde_json::Value;
use std::path::Path;
mod execution;
pub(crate) use execution::*;
/// GUI 只转发已有 Runner RPC;每个引擎的连接和回执均归同一个 owner。
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
struct RunnerManagedEditorAdapter(ManagedEditor);
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
impl EditorAdapter for RunnerManagedEditorAdapter {
fn id(&self) -> &'static str {
self.0.adapter()
@@ -21,7 +21,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri::{Manager, State};
use crate::editor_adapter::{EditorAdapter, EditorConnectionInfo};
use crate::editor_adapter::EditorAdapter;
type EditorRegistry = Arc<Mutex<BTreeMap<String, Box<dyn EditorAdapter>>>>;
type ProjectContext = Arc<Mutex<Option<PathBuf>>>;
@@ -1873,6 +1873,15 @@ impl PluginHost {
Ok(())
}
#[cfg(any(
test,
all(windows, feature = "cocos-editor-execute"),
all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
)
))]
pub(crate) fn register_editor_adapter(
&self,
adapter: Box<dyn EditorAdapter>,
@@ -1891,98 +1900,6 @@ impl PluginHost {
editors.insert(adapter.id().to_string(), adapter);
Ok(())
}
pub(crate) fn detect_editor(
&self,
adapter: String,
project_path: String,
) -> Result<EditorConnectionInfo, String> {
let state = self
.state
.lock()
.map_err(|_| "插件宿主锁已损坏".to_string())?;
let editors = state
.editors
.try_lock()
.map_err(|_| "编辑器注册表锁已损坏".to_string())?;
editors
.get(&adapter)
.ok_or_else(|| format!("未知编辑器适配器:{adapter}"))?
.detect(Path::new(project_path.trim()))
}
pub(crate) fn connect_editor(
&self,
adapter: String,
pid: u32,
project_path: String,
version: String,
) -> Result<EditorConnectionInfo, String> {
let state = self
.state
.lock()
.map_err(|_| "插件宿主锁已损坏".to_string())?;
let mut editors = state
.editors
.try_lock()
.map_err(|_| "编辑器注册表锁已损坏".to_string())?;
editors
.get_mut(&adapter)
.ok_or_else(|| format!("未知编辑器适配器:{adapter}"))?
.connect(pid, Path::new(project_path.trim()), version.trim())
}
pub(crate) fn disconnect_editor(&self, adapter: String) -> Result<(), String> {
let state = self
.state
.lock()
.map_err(|_| "插件宿主锁已损坏".to_string())?;
let mut editors = state
.editors
.try_lock()
.map_err(|_| "编辑器注册表锁已损坏".to_string())?;
if adapter == "godot-editor" {
if !editors.contains_key(&adapter) {
return Err(format!("未知编辑器适配器:{adapter}"));
}
let project = state
.active_project
.lock()
.map_err(|_| "项目上下文锁已损坏".to_string())?
.clone();
drop(editors);
drop(state);
return crate::editor_adapters::disconnect_managed_editor_project(
crate::editor_adapters::ManagedEditor::Godot,
project.as_deref(),
);
}
editors
.get_mut(&adapter)
.ok_or_else(|| format!("未知编辑器适配器:{adapter}"))?
.disconnect();
Ok(())
}
pub(crate) fn translate_editor_rpc(
&self,
adapter: String,
method: String,
params: Value,
) -> Result<Value, String> {
let state = self
.state
.lock()
.map_err(|_| "插件宿主锁已损坏".to_string())?;
let editors = state
.editors
.try_lock()
.map_err(|_| "编辑器注册表锁已损坏".to_string())?;
editors
.get(&adapter)
.ok_or_else(|| format!("未知编辑器适配器:{adapter}"))?
.translate_rpc(&method, params)
}
}
#[tauri::command]
@@ -2078,6 +1995,7 @@ pub(crate) async fn set_agc_plugin_project_path(
#[cfg(test)]
mod tests {
use super::*;
use crate::editor_adapter::EditorConnectionInfo;
#[test]
fn writer_receipt_loss_after_json_write_is_persistently_uncertain() {
@@ -22,6 +22,7 @@ pub(super) fn refresh_external_agent_runner_heartbeat(
write_external_agent_runner_endpoint_atomic(&state.endpoint_path, &endpoint)
}
#[cfg(any(target_os = "linux", test))]
pub(super) fn bind_external_agent_runner_listener_with<T>(
mut fallback_ports: impl FnMut() -> Vec<u16>,
mut bind: impl FnMut(u16) -> io::Result<T>,