Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/main.rs
T
suzmii 80b8238d0a
Project CI / Repository checks (push) Successful in 2m26s
Project CI / Frontend tests (push) Successful in 4m30s
Project CI / Backend tests (push) Successful in 8m12s
Project CI / Native shell tests (push) Successful in 17m46s
修复 AGC Windows ACL 提权与文件访问边界 (#211)
### 背景

Windows 下 AGC 读取配置目录、项目文件和本地资源时,可能先于 ACL 校验阶段就因继承权限、错误 owner 或拒绝访问而失败,表现为 `拒绝访问。 (os error 5)`。

此前自定义 `--config-dir` 场景还存在 scope 无法传递到提权子进程的问题;native picker 路径也缺少进程内来源证明,递归索引可能跟随 junction/reparse point,新建文件在权限加固失败时可能留下残留文件。

### 本次变更

1. 修复自定义 `--config-dir` 的 `managed / user-selected` scope 传递与提权票据校验。
2. 增加 native picker 文件/目录路径的短期 provenance 授权;未经过 picker 授权的任意 IPC 绝对路径不能触发 `user-selected` 自动提权。
3. 项目文件列表和索引递归改用 `symlink_metadata()`,拒绝 symlink、junction 和 Windows reparse point。
4. 统一在 metadata/read/open 前完成 ACL 准备,对 ACL 导致的 metadata 失败执行一次受控修复和重试。
5. 新建 conversation、导出包、Agent DB、资源、字体等文件在 harden 失败时关闭句柄并清理刚创建的文件。
6. 修复 UAC 提权授权票据以 `share_mode(0)` 独占打开后与 `GetNamedSecurityInfoW/SetNamedSecurityInfoW` 产生共享冲突的问题。
7. 同步 AGC Windows ACL 技术方案与安全回归测试。

### 安全行为

- UAC 取消返回 `1223`,不会被当作成功。
- 提权子进程重新校验目标路径、scope、当前用户 SID、nonce、有效期和票据一次性消费。
- symlink、junction、reparse point 和非普通文件对象保持失败关闭。
- 自定义配置目录可以位于用户 profile 外部,但仍受 owner、DACL 和路径类型校验。

### 验证结果

已执行并通过:

```
cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml
cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml private_path_elevation_policy_tests -- --nocapture
# 10 passed

cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml local_project -- --nocapture
# 20 passed

cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml checkpoint::security_tests -- --nocapture
# 10 passed

cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml private_file_write_tests -- --nocapture
# 2 passed

npm run check:encoding
# 5554 files passed

git diff --check
# passed
```

另外确认:

- `origin/master` 是当前分支祖先。
- 交接文档已删除,且原本不在 Git 跟踪列表中。
- 全量 `cargo test` 未作为通过依据;此前存在与 ACL 无关的失败项。
- `cargo fmt --check` 仍受仓库已有格式漂移影响,本次未执行全仓库格式化。
- 真实 Windows UAC 点击“Yes”的手工回归仍需在目标机器上确认。

### 手工验收

使用:

```powershell
& "C:\Users\dongy\workspace\Genarrative\.worktrees\agc-windows-acl-fix\apps\ai-game-creator-shell\src-tauri\target\debug\genarrative-ai-game-creator-shell.exe" `
  --config-dir "C:\Users\dongy\AppData\Local\Temp\agc-acl-live-20260830\deny-config-2"
```

验收标准:

1. 选择“否”:进程返回 `exit code 1223`。
2. 重新启动并选择“Yes”:不再返回 `exit code 1`,ACL 修复完成后继续启动。
3. 再次启动同一配置目录:不应重复弹出修复。
4. 检查目录 owner 和 DACL,确认归当前用户所有且为私有权限。

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/211
Co-authored-by: 董羽秦 <suzmii@qq.com>
Co-committed-by: 董羽秦 <suzmii@qq.com>
2026-08-31 14:37:34 +08:00

2481 lines
82 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![cfg_attr(all(not(dev), target_os = "windows"), windows_subsystem = "windows")]
use std::collections::BTreeMap;
use std::fs;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{mpsc, Arc, Mutex, OnceLock};
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use platform_agent::{
build_game_creation_seed_task_graph, plan_game_creation_agent_pass,
route_game_creation_repair_issues,
};
use platform_llm::{
LlmApiKind, LlmClient, LlmConfig, LlmMessage, LlmMessageContentPart, LlmProvider,
LlmRunRequest, DEFAULT_RETRY_BACKOFF_MS,
};
use reqwest::header;
use serde::{Deserialize, Serialize};
use shared_contracts::game_creation_app::{
new_game_creation_app_manifest, new_game_creation_app_seed_tasks,
validate_game_iteration_versions, GameCreationAgentArtifactTrace,
GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace,
GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace,
GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource,
GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor,
GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState,
GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus,
GameIterationVersion, GameIterationVersionCreatedReason, GameIterationVersionResourceBinding,
ProjectResourceCanvasLayout, ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition,
UpdateProjectResourceCanvasLayoutResult, UpdateProjectResourceCanvasLayoutStatus,
GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_COMMANDS,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS, GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION,
};
use tauri::{Emitter, Manager};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
// 调试落盘模块(保存 LLM 原始输出 / 失败输入,排查截断、空返回等)放在 debug_drafts.rs。
// 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。
mod agent;
mod agent_native_tools;
mod assets;
mod browser;
mod cli;
mod collaboration;
mod command_exec;
mod command_output;
mod command_sandbox;
mod command_sandbox_trampoline;
mod commands;
mod config;
mod context_compaction;
mod context_menu;
#[cfg(all(debug_assertions, not(test)))]
mod debug;
mod delegation;
mod git_inspect;
mod goal;
mod image_inspect;
mod isolated_agent;
mod patchset;
mod platform_session;
mod preview;
mod process_session;
mod process_session_bridge;
mod project;
mod provider_handoff;
mod provider_retry;
mod repository_context;
mod resource_inspect;
mod resource_preview_scheduler;
mod runner;
mod swarm_cli;
mod tool_plan_handoff;
mod user_input;
mod windows;
use agent::*;
use agent_native_tools::*;
use assets::*;
use browser::*;
use cli::*;
use collaboration::*;
use command_exec::*;
use command_output::*;
use command_sandbox::*;
use commands::*;
use config::*;
use context_compaction::*;
use delegation::*;
use git_inspect::*;
use goal::*;
use image_inspect::*;
use isolated_agent::*;
use patchset::*;
use platform_session::*;
use preview::*;
use process_session::*;
use project::*;
use repository_context::*;
use resource_inspect::*;
use resource_preview_scheduler::*;
use runner::*;
use swarm_cli::*;
use user_input::*;
use windows::*;
#[tauri::command]
async fn suggest_ui_design_semantic(
project_path: String,
state: ui_editor::state::State,
) -> Result<Vec<ui_editor::commands::UIDesignSuggestionTreeNode>, String> {
ui_editor::commands::suggest_ui_design_semantic_impl(project_path, state).await
}
#[tauri::command]
async fn recognize_ui(
project_path: String,
state: ui_editor::state::State,
) -> Result<ui_editor::commands::RecognitionDTO, String> {
ui_editor::commands::recognize_ui_impl(project_path, state).await
}
#[tauri::command]
async fn merge_ui(state: ui_editor::state::State) -> Result<ui_editor::commands::MergeDTO, String> {
ui_editor::commands::merge_ui_impl(state).await
}
#[tauri::command]
async fn bind_components(
project_path: String,
state: ui_editor::state::State,
sprite_ids: Vec<String>,
) -> Result<ui_editor::commands::BindingDTO, String> {
ui_editor::commands::bind_components_impl(project_path, state, sprite_ids).await
}
#[tauri::command]
fn load_ui_design_state(
input: ui_editor::persistence::LoadUiDesignStateInput,
) -> Result<ui_editor::persistence::UiDesignStateSnapshot, String> {
let root = Path::new(input.project_path.trim());
enforce_project_permission_policy(root, "asset.list")?;
ui_editor::persistence::load_ui_design_state_at(input)
}
#[tauri::command]
fn save_ui_design_state(
input: ui_editor::persistence::SaveUiDesignStateInput,
) -> Result<ui_editor::persistence::SaveUiDesignStateResult, String> {
let root = Path::new(input.project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
ui_editor::persistence::save_ui_design_state_at(input)
}
#[tauri::command]
fn ensure_ui_design_resource_for_prototype(
input: ui_editor::resource_bridge::EnsureUiDesignResourceForPrototypeInput,
) -> Result<ui_editor::resource_bridge::EnsureUiDesignResourceForPrototypeResult, String> {
ui_editor::resource_bridge::ensure_ui_design_resource_for_prototype(input)
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct InitLocalProjectResult {
project_path: String,
manifest_path: String,
manifest: GameCreationAppManifest,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectDirectoryStatus {
project_path: String,
exists: bool,
is_directory: bool,
is_game_creator_project: bool,
is_godot_project: bool,
godot_project_root: Option<String>,
project_name: Option<String>,
modified_at: Option<u64>,
manifest_error: Option<String>,
recent_run_status: Option<String>,
recent_run_stop_reason: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalPreviewResult {
url: String,
port: u16,
root: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalPreviewStatus {
status: String,
url: Option<String>,
port: Option<u16>,
root: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalGameProjectRevisionStatus {
revision: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GenerateLocalGameDraftResult {
project_path: String,
game_index_path: String,
design_path: String,
short_memory_path: String,
long_memory_path: String,
manifest: GameCreationAppManifest,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorChatAgentReply {
reply_text: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorRoleAgentChatStreamEvent {
project_path: String,
agent_id: String,
run_id: String,
status: String,
delta_text: String,
accumulated_text: String,
finish_reason: Option<String>,
session_id: Option<String>,
runtime_status: Option<String>,
runtime_phase: Option<String>,
runtime_summary: Option<String>,
runtime_state: Option<AgentRuntimeState>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeState {
#[serde(default)]
schema_version: String,
#[serde(default)]
agent_id: String,
#[serde(default)]
task_id: String,
#[serde(default)]
session_id: String,
#[serde(default)]
run_id: String,
#[serde(default)]
source: String,
#[serde(default = "default_agent_runtime_run_profile")]
run_profile: String,
#[serde(default)]
run_profile_binding_fingerprint: String,
#[serde(default)]
parent_agent_id: Option<String>,
#[serde(default)]
parent_run_id: Option<String>,
#[serde(default)]
delegation_id: Option<String>,
#[serde(default)]
status: String,
#[serde(default)]
phase: String,
#[serde(default)]
current_task: String,
#[serde(default)]
current_goal: String,
#[serde(default)]
goal_id: Option<String>,
#[serde(default)]
goal_revision: u64,
#[serde(default)]
goal_status: Option<String>,
#[serde(default)]
goal_outcome: Option<String>,
#[serde(default)]
goal_constraints: Vec<String>,
#[serde(default)]
goal_verification: Vec<String>,
#[serde(default)]
current_action: String,
#[serde(default)]
waiting_on: String,
#[serde(default)]
next_step: String,
#[serde(default)]
loop_iteration: u32,
/// Consecutive strict Fast-GDD submit rejections for this exact planning
/// child run. It is Runtime-owned durable state so a restart cannot turn
/// an invalid-provider-output loop back into an unbounded retry.
#[serde(default)]
plan_submit_gdd_rejection_count: u32,
/// Consecutive rounds where this run produced no action and no structured
/// plan step advance. Runtime-owned durable state so a runner restart
/// cannot launder an explanation-only planning loop back into an unbounded
/// Provider spend.
#[serde(default)]
plan_update_idle_rounds: u32,
/// Consecutive final replies this run had refused by a completion blocker.
/// Runtime-owned durable state for the same reason as the two counters
/// above: a blocker the model cannot satisfy is a livelock, and a runner
/// restart must not launder it back into unbounded Provider spend.
#[serde(default)]
stale_finalization_rounds: u32,
#[serde(default)]
max_loop_iterations: u32,
#[serde(default)]
tool_action_budget: u32,
#[serde(default)]
plan_revision: u64,
#[serde(default)]
plan_explanation: String,
#[serde(default)]
plan: Vec<String>,
#[serde(default)]
plan_steps: Vec<AgentRuntimePlanStep>,
#[serde(default)]
active_plan_step_index: Option<u32>,
#[serde(default)]
observations: Vec<String>,
#[serde(default)]
recent_tool_calls: Vec<AgentRuntimeToolCallRecord>,
#[serde(default)]
pending_tool_action: Option<AgentRuntimePendingToolActionSummary>,
#[serde(default)]
task_queue: AgentRuntimeTaskQueueSummary,
#[serde(default)]
allowed_tools: Vec<String>,
#[serde(default)]
tool_policy: AgentRuntimeToolPolicySnapshot,
#[serde(default)]
applied_steer_cursor: u64,
#[serde(default)]
applied_steer_refs: Vec<AgentRuntimeSteerRef>,
#[serde(default)]
queued_steer_count: u32,
#[serde(default)]
context_usage: AgentRuntimeContextUsage,
#[serde(default)]
last_response: Option<String>,
#[serde(default)]
error: Option<String>,
#[serde(default)]
started_at: u64,
#[serde(default)]
updated_at: u64,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeContextUsage {
#[serde(default)]
estimated_input_tokens: u64,
#[serde(default)]
auto_compact_token_limit: u64,
#[serde(default)]
last_prompt_tokens: Option<u64>,
#[serde(default)]
last_completion_tokens: Option<u64>,
#[serde(default)]
last_total_tokens: Option<u64>,
#[serde(default)]
compaction_revision: u64,
#[serde(default)]
compaction_count: u64,
#[serde(default)]
last_compaction_trigger: Option<String>,
#[serde(default)]
last_compacted_at: Option<u64>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeSteerRef {
#[serde(default)]
steer_id: String,
#[serde(default)]
sequence: u64,
#[serde(default)]
message_id: String,
#[serde(default)]
instruction_sha256: String,
#[serde(default)]
content_chars: u32,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeToolPolicySnapshot {
#[serde(default = "default_agent_runtime_run_profile")]
run_profile: String,
#[serde(default)]
run_profile_binding_fingerprint: String,
#[serde(default)]
allowed_tools: Vec<String>,
#[serde(default)]
auto_tools: Vec<String>,
#[serde(default)]
confirm_tools: Vec<String>,
#[serde(default)]
denied_tools: Vec<String>,
#[serde(default)]
updated_at: u64,
}
impl Default for AgentRuntimeToolPolicySnapshot {
fn default() -> Self {
Self {
run_profile: default_agent_runtime_run_profile(),
run_profile_binding_fingerprint: String::new(),
allowed_tools: Vec::new(),
auto_tools: Vec::new(),
confirm_tools: Vec::new(),
denied_tools: Vec::new(),
updated_at: 0,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeToolCallRecord {
#[serde(default)]
action_id: Option<String>,
#[serde(default)]
tool: String,
#[serde(default)]
status: String,
#[serde(default)]
action_fingerprint: Option<String>,
#[serde(default)]
input_summary: Option<String>,
#[serde(default)]
reason: Option<String>,
#[serde(default)]
summary: String,
#[serde(default)]
detail: Option<String>,
#[serde(default)]
updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimePendingToolActionSummary {
#[serde(default)]
action_id: String,
#[serde(default)]
action_fingerprint: String,
#[serde(default)]
tool: String,
#[serde(default)]
input_summary: Option<String>,
#[serde(default)]
reason: Option<String>,
#[serde(default)]
requested_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimePlanStep {
#[serde(default)]
index: u32,
#[serde(default)]
title: String,
#[serde(default)]
status: String,
#[serde(default)]
detail: Option<String>,
#[serde(default)]
updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeTaskQueueSummary {
#[serde(default)]
total: u32,
#[serde(default)]
pending: u32,
#[serde(default)]
running: u32,
#[serde(default)]
waiting_for_confirmation: u32,
#[serde(default)]
waiting_for_user_input: u32,
#[serde(default)]
paused: u32,
#[serde(default)]
cancelled: u32,
#[serde(default)]
completed: u32,
#[serde(default)]
failed: u32,
#[serde(default)]
latest_run_id: Option<String>,
#[serde(default)]
updated_at: u64,
}
impl Default for AgentRuntimeTaskQueueSummary {
fn default() -> Self {
Self {
total: 0,
pending: 0,
running: 0,
waiting_for_confirmation: 0,
waiting_for_user_input: 0,
paused: 0,
cancelled: 0,
completed: 0,
failed: 0,
latest_run_id: None,
updated_at: 0,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeEvent {
#[serde(default)]
schema_version: String,
#[serde(default)]
agent_id: String,
#[serde(default)]
task_id: String,
#[serde(default)]
session_id: String,
#[serde(default)]
run_id: String,
#[serde(default)]
source: String,
#[serde(default)]
event_type: String,
#[serde(default)]
event_id: String,
#[serde(default)]
action_id: Option<String>,
#[serde(default)]
status: String,
#[serde(default)]
phase: String,
#[serde(default)]
summary: String,
#[serde(default)]
public_text: Option<String>,
#[serde(default)]
detail: Option<String>,
#[serde(default)]
updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct AgentRuntimeResponseStream {
schema_version: String,
agent_id: String,
task_id: String,
session_id: String,
run_id: String,
request_kind: String,
request_slot: String,
applied_steer_cursor: u64,
response_revision: u64,
sequence: u64,
status: String,
accumulated_text: String,
finish_reason: Option<String>,
started_at: u64,
updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeTaskRecord {
#[serde(default)]
schema_version: String,
#[serde(default)]
agent_id: String,
#[serde(default)]
task_id: String,
#[serde(default)]
session_id: String,
#[serde(default)]
run_id: String,
#[serde(default)]
source: String,
#[serde(default = "default_agent_runtime_run_profile")]
run_profile: String,
#[serde(default)]
run_profile_binding_fingerprint: String,
#[serde(default)]
parent_agent_id: Option<String>,
#[serde(default)]
parent_run_id: Option<String>,
#[serde(default)]
delegation_id: Option<String>,
#[serde(default)]
goal_id: Option<String>,
#[serde(default)]
goal_revision: u64,
#[serde(default)]
goal_status: Option<String>,
#[serde(default)]
task: String,
#[serde(default)]
status: String,
#[serde(default)]
phase: String,
#[serde(default)]
current_action: String,
#[serde(default)]
terminal_detail: Option<String>,
#[serde(default)]
error: Option<String>,
#[serde(default)]
updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct AgentGoalRecord {
schema_version: String,
project_id: String,
goal_id: String,
agent_id: String,
session_id: String,
run_id: String,
revision: u64,
status: String,
outcome: String,
constraints: Vec<String>,
verification: Vec<String>,
#[serde(default)]
completion_evidence: Vec<String>,
#[serde(default)]
response_fingerprint: Option<String>,
created_at: u64,
#[serde(default)]
pause_requested_at: Option<u64>,
#[serde(default)]
paused_at: Option<u64>,
#[serde(default)]
completed_at: Option<u64>,
#[serde(default)]
cleared_at: Option<u64>,
#[serde(default)]
error: Option<String>,
updated_at: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentGoalMutationResult {
goal: AgentGoalRecord,
runtime: AgentRuntimeResult,
provider_interrupted: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeResult {
state: AgentRuntimeState,
#[serde(skip_serializing_if = "Option::is_none")]
accepted_run_id: Option<String>,
session_path: String,
event_path: String,
task_path: String,
task_queue: AgentRuntimeTaskQueueSummary,
recent_events: Vec<AgentRuntimeEvent>,
recent_tasks: Vec<AgentRuntimeTaskRecord>,
response_stream: Option<AgentRuntimeResponseStream>,
user_input_request: Option<AgentRuntimeUserInputRequestView>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeSteerResult {
runtime: AgentRuntimeResult,
steer_id: String,
sequence: u64,
status: String,
provider_interrupted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
assistant_reply: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
interrupt_decision: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
decision_reason: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorAgentRuntimeUpdateEvent {
project_path: String,
agent_id: String,
run_id: String,
status: String,
phase: String,
manifest_invalidated: bool,
runtime: AgentRuntimeResult,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorManifestInvalidatedEvent {
project_path: String,
agent_id: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorManifestInvalidationRelayEnvelope {
token: String,
event: GameCreatorManifestInvalidatedEvent,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct GameCreatorManifestInvalidationEventSink {
port: u16,
token: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorAgentProgressEvent {
project_path: String,
stage: String,
message: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorDirectTurnUpdateEvent {
project_path: String,
turn_id: String,
sequence: u64,
status: String,
activity: Option<String>,
accumulated_text: Option<String>,
updated_at: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorLlmConfigStatus {
agent_mode: String,
configured: bool,
api_key_present: bool,
base_url: Option<String>,
model: Option<String>,
api_kind: String,
reasoning_effort: String,
stream: bool,
web_search_enabled: bool,
context_window_tokens: u64,
auto_compact_token_limit: u64,
tool_output_token_limit: u64,
request_timeout_ms: u64,
max_retries: u32,
retry_backoff_ms: u64,
error: Option<String>,
agents: Vec<GameCreatorAgentLlmConfigStatus>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorAgentLlmConfigStatus {
agent_mode: String,
agent_id: String,
label: String,
configured: bool,
api_key_present: bool,
base_url: Option<String>,
model: Option<String>,
api_kind: String,
reasoning_effort: String,
stream: bool,
web_search_enabled: bool,
context_window_tokens: u64,
auto_compact_token_limit: u64,
tool_output_token_limit: u64,
request_timeout_ms: u64,
max_retries: u32,
retry_backoff_ms: u64,
error: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorAppConfigFile {
agent_mode: Option<String>,
llm: Option<GameCreatorLlmConfigFile>,
agent_llm: Option<BTreeMap<String, GameCreatorLlmConfigFile>>,
editor_api: Option<GameCreatorEditorApiConfigFile>,
planning: Option<GameCreatorPlanningConfigFile>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorPlanningConfigFile {
#[serde(skip_serializing_if = "Option::is_none")]
capability_enabled: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorLlmConfigFile {
#[serde(skip_serializing_if = "Option::is_none")]
api_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
base_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
api_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
reasoning_effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
web_search_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
context_window_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_compact_token_limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_output_token_limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
request_timeout_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
max_retries: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
retry_backoff_ms: Option<u64>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorEditorApiConfigFile {
base_url: Option<String>,
api_key: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorAppConfig {
#[serde(default = "default_game_creator_agent_mode")]
agent_mode: String,
llm: GameCreatorLlmConfig,
#[serde(default)]
agent_llm: BTreeMap<String, GameCreatorLlmConfigFile>,
editor_api: GameCreatorEditorApiConfig,
#[serde(default)]
planning: GameCreatorPlanningConfig,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorPlanningConfig {
#[serde(default = "default_game_creator_planning_capability_enabled")]
capability_enabled: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorLlmConfig {
api_key: String,
base_url: String,
model: String,
api_kind: String,
reasoning_effort: String,
stream: bool,
web_search_enabled: bool,
#[serde(default = "default_game_creator_llm_context_window_tokens")]
context_window_tokens: u64,
#[serde(default = "default_game_creator_llm_auto_compact_token_limit")]
auto_compact_token_limit: u64,
#[serde(default = "default_game_creator_llm_tool_output_token_limit")]
tool_output_token_limit: u64,
request_timeout_ms: u64,
max_retries: u32,
retry_backoff_ms: u64,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorEditorApiConfig {
base_url: String,
api_key: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorAppConfigView {
path: String,
config: GameCreatorAppConfig,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeContextCompactionResult {
agent_id: String,
session_id: String,
run_id: Option<String>,
trigger: String,
revision: u64,
estimated_tokens_before: u64,
estimated_tokens_after: u64,
prompt_tokens: Option<u64>,
completion_tokens: Option<u64>,
total_tokens: Option<u64>,
covered_agent_messages: u64,
covered_project_messages: u64,
covered_observations: u64,
reused: bool,
compacted_at: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRunControlResult {
run_id: String,
status: String,
lifecycle_status: String,
next_step: String,
message: String,
activity_path: String,
output_path: String,
context_bundle_path: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct UploadLocalAssetResult {
id: String,
local_path: String,
absolute_path: String,
manifest_path: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct CreateUiDesignResourceResult {
asset: UploadLocalAssetResult,
manifest: GameCreationAppManifest,
committed_project_revision: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ImportCanvasExportResult {
import_root: String,
metadata_path: String,
imported_count: usize,
assets: Vec<UploadLocalAssetResult>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct SyncCanvasProjectAssetsResult {
canvas_project_id: String,
import_root: String,
imported_count: usize,
assets: Vec<UploadLocalAssetResult>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalImportResult {
assets: Vec<ImportedAsset>,
}
type RemoteImportResult = LocalImportResult;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ImportedAsset {
id: String,
local_path: String,
asset_kind: Option<String>,
}
#[derive(Debug, Eq, PartialEq)]
struct GeneratedPlatformArtAssetSlice {
name: String,
width: u32,
height: u32,
local_path: String,
resource_id: Option<String>,
asset_object_id: Option<String>,
content_sha256: String,
pixel_sha256: String,
}
#[derive(Debug, Eq, PartialEq)]
struct GeneratedPlatformArtAsset {
asset: UploadLocalAssetResult,
slices: Vec<GeneratedPlatformArtAssetSlice>,
resource_id: Option<String>,
asset_object_id: Option<String>,
task_id: Option<String>,
model: Option<String>,
warning: Option<String>,
slice_warning: Option<String>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct OpenCanvasProjectResult {
url: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CanvasExportMetadata {
project_title: String,
exported_at: String,
layers: Vec<CanvasExportLayerMetadata>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CanvasExportLayerMetadata {
title: String,
file: Option<String>,
visible: CanvasExportVisibleMetadata,
export_error: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CanvasExportVisibleMetadata {
#[serde(rename = "type")]
layer_type: String,
model: String,
task: String,
object: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalGameMemoryResult {
scope: String,
path: String,
content: String,
exists: bool,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalAgentMemoryResult {
task_id: String,
path: String,
content: String,
exists: bool,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LimitedLocalCommandResult {
command_id: String,
status: String,
output: String,
log_path: String,
updated_at: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectFileEntry {
path: String,
kind: String,
size: u64,
modified_at: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ListLocalProjectFilesResult {
project_path: String,
files: Vec<LocalProjectFileEntry>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectFileResult {
path: String,
absolute_path: String,
content: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectFileMutationResult {
path: String,
absolute_path: String,
deleted: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalConversationMessage {
role: String,
content: String,
agent_id: Option<String>,
}
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalConversationMessageRecord {
schema_version: String,
role: String,
content: String,
agent_id: Option<String>,
message_id: Option<String>,
updated_at: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalConversationResult {
path: String,
agent_id: Option<String>,
session_id: Option<String>,
messages: Vec<LocalConversationMessageRecord>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentConversationSessionRecord {
session_id: String,
title: String,
created_at: u64,
updated_at: u64,
archived_at: Option<u64>,
message_count: u64,
legacy: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
forked_from_session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
forked_message_count: Option<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentConversationSessionListResult {
path: String,
agent_id: String,
active_session_id: String,
sessions: Vec<AgentConversationSessionRecord>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ProjectPermissionPolicy {
denied_commands: Vec<String>,
confirm_commands: Vec<String>,
#[serde(default)]
agent_policies: BTreeMap<String, ProjectAgentPermissionPolicy>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ProjectAgentPermissionPolicy {
#[serde(default)]
denied_commands: Vec<String>,
#[serde(default)]
confirm_commands: Vec<String>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ProjectPermissionPolicyView {
path: String,
policy: ProjectPermissionPolicy,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectIndexedFile {
path: String,
size: u64,
checksum: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectIndexResult {
project_path: String,
index_path: String,
file_count: usize,
total_bytes: u64,
files: Vec<LocalProjectIndexedFile>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectCheckpointResult {
checkpoint_id: String,
checkpoint_path: String,
file_count: usize,
total_bytes: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectExportPackageResult {
package_path: String,
package_relative_path: String,
file_count: usize,
total_bytes: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectExportPackageSummary {
package_path: String,
package_relative_path: String,
total_bytes: u64,
modified_at: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectExportPackagesResult {
project_path: String,
packages: Vec<LocalProjectExportPackageSummary>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectDiffEntry {
path: String,
status: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectDiffResult {
checkpoint_id: String,
added: Vec<LocalProjectDiffEntry>,
changed: Vec<LocalProjectDiffEntry>,
deleted: Vec<LocalProjectDiffEntry>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectRestoreResult {
checkpoint_id: String,
restored_count: usize,
deleted_count: usize,
}
const DEFAULT_GAME_INDEX_HTML: &str = r#"<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Genarrative Game Draft</title>
<style>
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; }
main { width: min(720px, calc(100vw - 32px)); }
</style>
</head>
<body><main>还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。</main></body>
</html>
"#;
const DEFAULT_EDITOR_BASE_URL: &str = "http://127.0.0.1:3000";
const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json";
const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json";
const GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER: &str = "codex_app_server";
const GAME_CREATOR_AGENT_MODE_CODEX_CLI: &str = "codex_cli";
const GAME_CREATOR_AGENT_MODE_PROVIDER: &str = "provider";
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://dev.genarrative.world/gpt/v1";
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-5.6-sol";
const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses";
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "max";
const DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS: u64 = 128_000;
const DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT: u64 = 64_000;
const DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT: u64 = 12_000;
const DEFAULT_GAME_CREATOR_LLM_MAX_RETRIES: u32 = 2;
fn default_game_creator_agent_mode() -> String {
GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string()
}
fn default_game_creator_llm_context_window_tokens() -> u64 {
DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS
}
fn default_game_creator_llm_auto_compact_token_limit() -> u64 {
DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT
}
fn default_game_creator_llm_tool_output_token_limit() -> u64 {
DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT
}
fn default_game_creator_planning_capability_enabled() -> bool {
true
}
const DEFAULT_CANVAS_SYNC_API_BASE_URL: &str = "https://dev.genarrative.world";
const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json");
const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 320000;
const GAME_CREATOR_CHAT_AGENT_MAX_OUTPUT_TOKENS: u32 = 1800;
const GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS: u32 = 900;
const GAME_CREATOR_ROLE_AGENT_MAX_OUTPUT_TOKENS: u32 = 1200;
const GAME_CREATOR_REQUIRED_LLM_AGENT_IDS: [&str; 3] = [
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"planner",
"generator",
];
const MIN_GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 1_000;
const GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 180_000;
const GAME_CREATOR_AGENT_LOOP_MAX_PASSES: u8 = 3;
const GAME_CREATOR_AGENT_TOOL_CALL_MAX: u16 = GAME_CREATION_AGENT_TOOL_CALL_MAX;
const GAME_CREATOR_AGENT_RUN_HISTORY_MAX_COUNT: usize = 100;
const GAME_CREATOR_AGENT_DB_SCHEMA_VERSION: &str = "game-creator-agent-db.v1";
const PROJECT_BLACKBOARD_MEMORY_PATH: &str = "memory/blackboard.md";
const PROJECT_PERMISSION_POLICY_PATH: &str = ".agent/policy.json";
const PROJECT_INDEX_PATH: &str = ".agent/project.index.json";
const PROJECT_WRITE_LOCK_PATH: &str = ".agent/project.lock";
const LOCAL_CONVERSATION_SCHEMA_VERSION: &str = "game-creator-conversation.v1";
const AGENT_CONVERSATION_SESSION_SCHEMA_VERSION: &str = "game-creator-agent-sessions.v1";
const AGENT_RUNTIME_SCHEMA_VERSION: &str = "game-creator-agent-runtime.v1";
const AGENT_RUNTIME_RUN_PROFILE_STANDARD: &str = "standard";
const AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD: &str = "autonomous-game-build";
const AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS: usize = 8_000;
const AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS: usize = 10_000;
const AGENT_RUNTIME_RECENT_EVENT_LIMIT: usize = 20;
const AGENT_RUNTIME_RECENT_TASK_LIMIT: usize = 12;
const GAME_CREATOR_CONVERSATION_CONTEXT_MAX_MESSAGES: usize = 12;
fn default_agent_runtime_run_profile() -> String {
AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string()
}
const MAX_CANVAS_EXPORT_FILES: usize = 500;
const MAX_CANVAS_EXPORT_BYTES: u64 = 512 * 1024 * 1024;
const MAX_PROJECT_EXPORT_PACKAGE_FILES: usize = 1200;
const MAX_PROJECT_EXPORT_PACKAGE_BYTES: u64 = 512 * 1024 * 1024;
const GAME_CREATOR_AGENT_ARTIFACT_PATHS: [&str; 15] = [
".agent/agent.db",
".agent/spec.md",
".agent/findings.md",
".agent/logs/agent.log",
".agent/logs/command.log",
".agent/logs/preview.log",
"memory/session.md",
"memory/project.md",
PROJECT_BLACKBOARD_MEMORY_PATH,
"game/game_design.md",
"game/balance.json",
"assets/manifest.art.json",
"assets/manifest.audio.json",
"exports/README.md",
"game/index.html",
];
static GAME_CREATOR_RUNTIME_CONFIG_DIR: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
impl Default for GameCreatorAppConfig {
fn default() -> Self {
Self {
agent_mode: default_game_creator_agent_mode(),
llm: GameCreatorLlmConfig::default(),
agent_llm: BTreeMap::new(),
editor_api: GameCreatorEditorApiConfig::default(),
planning: GameCreatorPlanningConfig::default(),
}
}
}
impl Default for GameCreatorPlanningConfig {
fn default() -> Self {
Self {
capability_enabled: default_game_creator_planning_capability_enabled(),
}
}
}
impl Default for GameCreatorLlmConfig {
fn default() -> Self {
Self {
api_key: String::new(),
base_url: DEFAULT_GAME_CREATOR_LLM_BASE_URL.to_string(),
model: DEFAULT_GAME_CREATOR_LLM_MODEL.to_string(),
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
reasoning_effort: DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT.to_string(),
stream: true,
web_search_enabled: false,
context_window_tokens: DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS,
auto_compact_token_limit: DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT,
tool_output_token_limit: DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT,
request_timeout_ms: GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS,
max_retries: DEFAULT_GAME_CREATOR_LLM_MAX_RETRIES,
retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
}
}
}
impl Default for GameCreatorEditorApiConfig {
fn default() -> Self {
Self {
base_url: DEFAULT_CANVAS_SYNC_API_BASE_URL.to_string(),
api_key: String::new(),
}
}
}
#[derive(Clone, Copy, Debug)]
struct AgentRoleDefinition {
id: &'static str,
role: &'static str,
task_id: &'static str,
tool_id: &'static str,
brief_path_name: &'static str,
}
#[derive(Clone, Copy, Debug)]
struct AgentGroupDefinition {
id: &'static str,
label: &'static str,
role: &'static str,
brief_path_name: &'static str,
roles: &'static [AgentRoleDefinition],
}
include!(concat!(env!("OUT_DIR"), "/agent_runtime_prompt_bundle.rs"));
const GAME_CREATOR_LEGACY_CHAT_AGENT_CONFIG_ID: &str = "chat";
struct GameCreatorLlmAgentStatusDefinition {
agent_id: String,
label: String,
}
fn game_creator_llm_agent_status_definitions() -> Vec<GameCreatorLlmAgentStatusDefinition> {
let mut agents = vec![
GameCreatorLlmAgentStatusDefinition {
agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
label: format!("{} Agent", PROJECT_SUPERVISOR_AGENT_DEFINITION.label),
},
GameCreatorLlmAgentStatusDefinition {
agent_id: "planner".to_string(),
label: "Planner".to_string(),
},
GameCreatorLlmAgentStatusDefinition {
agent_id: "orchestrator".to_string(),
label: "Orchestrator".to_string(),
},
GameCreatorLlmAgentStatusDefinition {
agent_id: "generator".to_string(),
label: "Generator".to_string(),
},
GameCreatorLlmAgentStatusDefinition {
agent_id: "evaluator".to_string(),
label: "Evaluator".to_string(),
},
];
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
for role in group.roles {
if agents.iter().any(|agent| agent.agent_id == role.task_id) {
continue;
}
agents.push(GameCreatorLlmAgentStatusDefinition {
agent_id: role.task_id.to_string(),
label: format!("{} / {}", group.label, role.role),
});
}
}
agents
}
#[derive(Clone, Debug)]
struct AgentPassArtifactPaths {
draft_json: String,
design_markdown: String,
balance_json: String,
art_manifest_json: String,
audio_manifest_json: String,
publish_readme: String,
game_html: String,
handoff_markdown: String,
}
#[derive(Clone, Debug)]
struct AgentRoleBrief {
group_definition: AgentGroupDefinition,
role_definition: AgentRoleDefinition,
markdown: String,
relative_path: String,
memory_relative_path: String,
status: String,
tool_id: String,
summary: String,
}
#[derive(Clone, Debug)]
struct AgentGroupBrief {
definition: AgentGroupDefinition,
markdown: String,
relative_path: String,
role_briefs: Vec<AgentRoleBrief>,
}
#[derive(Clone, Debug)]
struct AgentPassAgenda {
relative_path: String,
task_graph_relative_path: String,
active_task_ids: Vec<String>,
carried_task_ids: Vec<String>,
dependency_waves: Vec<Vec<String>>,
repair_focus: Vec<String>,
repair_routes: Vec<GameCreationAgentRepairRouteTrace>,
summary: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct LlmGameDraft {
title: String,
design_markdown: String,
balance: serde_json::Value,
art_manifest: serde_json::Value,
audio_manifest: serde_json::Value,
publish_readme: String,
handoffs: Vec<LlmAgentHandoff>,
game_html: String,
handoff_summary: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct LlmAgentHandoff {
group: String,
role: String,
summary: String,
outputs: Vec<String>,
next: String,
}
const DIAGNOSTIC_LOG_MAX_BYTES: u64 = 256 * 1024;
static DIAGNOSTIC_LOG_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
static STARTUP_PANIC_LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
static STARTUP_ERROR_DIALOG_SHOWN: AtomicBool = AtomicBool::new(false);
fn diagnostic_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn append_bounded_diagnostic_line_with_limit(
path: &Path,
line: &str,
max_bytes: u64,
) -> std::io::Result<()> {
let _guard = DIAGNOSTIC_LOG_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = open_secure_diagnostic_log(path)?;
if file.metadata()?.len() >= max_bytes {
file.seek(SeekFrom::Start(0)).map_err(|error| {
std::io::Error::new(error.kind(), format!("seek current log: {error}"))
})?;
let mut previous_content = Vec::new();
std::io::Read::by_ref(&mut file)
.take(max_bytes.saturating_add(1))
.read_to_end(&mut previous_content)
.map_err(|error| {
std::io::Error::new(error.kind(), format!("read current log: {error}"))
})?;
let previous_path = path.with_extension("previous.log");
let mut previous = open_secure_diagnostic_log(&previous_path)?;
previous.set_len(0).map_err(|error| {
std::io::Error::new(error.kind(), format!("truncate previous log: {error}"))
})?;
previous.write_all(&previous_content).map_err(|error| {
std::io::Error::new(error.kind(), format!("write previous log: {error}"))
})?;
previous.flush().map_err(|error| {
std::io::Error::new(error.kind(), format!("flush previous log: {error}"))
})?;
file.set_len(0).map_err(|error| {
std::io::Error::new(error.kind(), format!("truncate current log: {error}"))
})?;
}
file.seek(SeekFrom::End(0))
.map_err(|error| std::io::Error::new(error.kind(), format!("seek log end: {error}")))?;
writeln!(file, "{} {line}", diagnostic_timestamp())?;
file.flush()
}
fn open_secure_diagnostic_log(path: &Path) -> std::io::Result<File> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"diagnostic log must be a regular file",
));
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
let mut options = OpenOptions::new();
options.read(true).write(true).create(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
let file = options.open(path)?;
let metadata = file.metadata()?;
if !metadata.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"diagnostic log must be a regular file",
));
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if metadata.nlink() != 1 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"diagnostic log must not be a hardlink",
));
}
}
#[cfg(windows)]
crate::runner::validate_windows_regular_file_handle(&file, "diagnostic log")
.map_err(std::io::Error::other)?;
Ok(file)
}
pub(crate) fn append_bounded_diagnostic_line(path: &Path, line: &str) -> std::io::Result<()> {
append_bounded_diagnostic_line_with_limit(path, line, DIAGNOSTIC_LOG_MAX_BYTES)
}
fn redact_windows_absolute_paths(value: &str) -> String {
let bytes = value.as_bytes();
let mut output = String::with_capacity(value.len());
let mut cursor = 0;
while cursor < bytes.len() {
let previous_allows_drive_path = cursor == 0 || !bytes[cursor - 1].is_ascii_alphanumeric();
let is_drive_path = previous_allows_drive_path
&& cursor + 2 < bytes.len()
&& bytes[cursor].is_ascii_alphabetic()
&& bytes[cursor + 1] == b':'
&& matches!(bytes[cursor + 2], b'\\' | b'/');
if !is_drive_path {
let ch = value[cursor..]
.chars()
.next()
.expect("valid character boundary");
output.push(ch);
cursor += ch.len_utf8();
continue;
}
output.push_str("<absolute-path>");
cursor += 3;
while cursor < bytes.len()
&& !bytes[cursor].is_ascii_whitespace()
&& !matches!(bytes[cursor], b'\"' | b'\'' | b',' | b';')
{
cursor += 1;
}
}
output
}
fn redact_unix_absolute_paths(value: &str) -> String {
let chars = value.chars().collect::<Vec<_>>();
let mut output = String::with_capacity(value.len());
let mut cursor = 0;
while cursor < chars.len() {
let previous_allows_path = cursor == 0
|| chars[cursor - 1].is_whitespace()
|| matches!(chars[cursor - 1], '=' | '(' | ':' | ':');
let is_url_separator = chars.get(cursor + 1) == Some(&'/');
if chars[cursor] != '/' || !previous_allows_path || is_url_separator {
output.push(chars[cursor]);
cursor += 1;
continue;
}
output.push_str("<absolute-path>");
cursor += 1;
while cursor < chars.len()
&& !chars[cursor].is_whitespace()
&& !matches!(chars[cursor], '"' | '\'' | ',' | ';')
{
cursor += 1;
}
}
output
}
pub(crate) fn sanitize_diagnostic_message(value: &str, private_root: Option<&Path>) -> String {
let mut sanitized = value.replace(['\r', '\n'], " ");
if let Some(root) = private_root {
let root = root.to_string_lossy();
if !root.is_empty() {
sanitized = sanitized.replace(root.as_ref(), "<appdata>");
}
}
let lowercase = sanitized.to_ascii_lowercase();
if [
"authorization",
"bearer ",
"api_key",
"apikey",
"api key",
"x-api-key",
"token=",
"token:",
"credential",
]
.iter()
.any(|marker| lowercase.contains(marker))
{
return "<sensitive diagnostic details redacted>".to_string();
}
sanitized = redact_unix_absolute_paths(&redact_windows_absolute_paths(&sanitized));
sanitized.chars().take(2_048).collect()
}
fn show_startup_error_dialog(log_path: &Path) {
eprintln!("Genarrative startup failed; see {}", log_path.display());
}
#[derive(Clone, Debug)]
struct GameCreatorAgentLoopResult {
run_id: String,
draft: LlmGameDraft,
spec_markdown: String,
findings_markdown: String,
passes: u8,
steps: Vec<GameCreationAgentRunStep>,
}
fn game_creator_gui_run_event_requests_runner_shutdown(event: &tauri::RunEvent) -> bool {
matches!(event, tauri::RunEvent::Exit)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GameCreatorGuiRunnerShutdownOutcome {
NotRequested,
Requested,
Failed(GameCreatorGuiRunnerShutdownFailure),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GameCreatorGuiRunnerShutdownFailure {
EndpointUnavailable,
RunnerUnresponsive,
ProcessIdentity,
PlatformUnsupported,
LockTimeout,
Other,
}
impl GameCreatorGuiRunnerShutdownFailure {
fn code(self) -> &'static str {
match self {
Self::EndpointUnavailable => "endpoint_unavailable",
Self::RunnerUnresponsive => "runner_unresponsive",
Self::ProcessIdentity => "process_identity",
Self::PlatformUnsupported => "platform_unsupported",
Self::LockTimeout => "lock_timeout",
Self::Other => "other",
}
}
}
fn classify_game_creator_gui_runner_shutdown_error(
error: &str,
) -> GameCreatorGuiRunnerShutdownFailure {
if error.contains("进程启动身份")
|| error.contains("pid 已")
|| error.contains("pidfd")
|| error.contains("进程句柄")
{
GameCreatorGuiRunnerShutdownFailure::ProcessIdentity
} else if error.contains("当前平台不支持") || error.contains("macOS 不提供") {
GameCreatorGuiRunnerShutdownFailure::PlatformUnsupported
} else if error.contains("实例锁") || error.contains("owner 锁") {
GameCreatorGuiRunnerShutdownFailure::LockTimeout
} else if error.contains("endpoint") {
GameCreatorGuiRunnerShutdownFailure::EndpointUnavailable
} else if error.contains("响应") || error.contains("连接 Agent Runner") {
GameCreatorGuiRunnerShutdownFailure::RunnerUnresponsive
} else {
GameCreatorGuiRunnerShutdownFailure::Other
}
}
fn resolve_game_creator_gui_runner_shutdown<F>(
event: &tauri::RunEvent,
shutdown: F,
) -> GameCreatorGuiRunnerShutdownOutcome
where
F: FnOnce() -> Result<(), String>,
{
if !game_creator_gui_run_event_requests_runner_shutdown(event) {
return GameCreatorGuiRunnerShutdownOutcome::NotRequested;
}
match shutdown() {
Ok(()) => GameCreatorGuiRunnerShutdownOutcome::Requested,
Err(error) => GameCreatorGuiRunnerShutdownOutcome::Failed(
classify_game_creator_gui_runner_shutdown_error(&error),
),
}
}
fn handle_game_creator_gui_run_event(event: &tauri::RunEvent) {
if matches!(event, tauri::RunEvent::Exit) {
if let Err(error) = agent::shutdown_game_creator_codex_app_servers() {
eprintln!("agent.direct_codex.gui_exit.shutdown_failed: {error}");
}
}
match resolve_game_creator_gui_runner_shutdown(event, shutdown_external_agent_runner) {
GameCreatorGuiRunnerShutdownOutcome::NotRequested => {}
GameCreatorGuiRunnerShutdownOutcome::Requested => {
eprintln!("agent.runner.gui_exit.shutdown_requested")
}
GameCreatorGuiRunnerShutdownOutcome::Failed(failure) => {
eprintln!("agent.runner.gui_exit.shutdown_failed.{}", failure.code())
}
}
}
/// Tauri 的全局异步 runtime 默认由 `TokioRuntime::new()` 建出来,worker 线程吃
/// tokio 默认栈。Runtime 的 agent turn 调用链深到本仓库另一处专门给自己的后台
/// 线程配了 AGENT_RUNTIME_BACKGROUND_WORKER_STACK_BYTES;但凡经 async_runtime::spawn
/// 派发的活(例如静态委派的父 run 唤醒)都落在这些默认栈的 worker 上,同一段代码
/// 在那里直接 `thread 'tokio-rt-worker' has overflowed its stack` 把整个进程 abort
/// 掉——现场表现是父 run 认领委派回执那一刻 Agent Runner 无声消失,调用方只看到
/// 连接超时。这里在任何异步派发之前把全局 runtime 换成同样栈尺寸的实例。
///
/// `async_runtime::set` 只接受 handle 且要求底层 Runtime 常驻,所以这里刻意泄漏。
fn build_agent_runtime_async_runtime() -> Result<tokio::runtime::Runtime, String> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(crate::agent::AGENT_RUNTIME_BACKGROUND_WORKER_STACK_BYTES)
.build()
.map_err(|error| format!("创建全局异步 runtime 失败:{error}"))
}
#[cfg(not(test))]
fn install_agent_runtime_async_runtime_with_deep_stack() {
let runtime = match build_agent_runtime_async_runtime() {
Ok(runtime) => runtime,
Err(error) => {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
};
tauri::async_runtime::set(runtime.handle().clone());
// 进程生命周期内必须持有,否则 handle 立即失效。
Box::leak(Box::new(runtime));
}
#[cfg(windows)]
fn run_windows_acl_repair_if_requested(args: &[String]) -> Option<i32> {
let [
command,
path,
target_user_sid_flag,
target_user_sid,
authorization_flag,
nonce,
scope_flag,
scope_value,
] = args
else {
if args.first().map(String::as_str) == Some("--repair-private-acl") {
eprintln!(
"用法:--repair-private-acl <路径> --target-user-sid <SID> --authorization <票据> --scope <managed|user-selected>"
);
return Some(1);
}
return None;
};
if command != "--repair-private-acl"
|| target_user_sid_flag != "--target-user-sid"
|| authorization_flag != "--authorization"
|| scope_flag != "--scope"
{
eprintln!(
"用法:--repair-private-acl <路径> --target-user-sid <SID> --authorization <票据> --scope <managed|user-selected>"
);
return Some(1);
}
let path = std::path::PathBuf::from(path);
let scope = match config::parse_windows_acl_repair_scope(scope_value) {
Ok(scope) => scope,
Err(error) => {
eprintln!("{error}");
return Some(1);
}
};
let result = config::consume_windows_acl_repair_authorization(
&path,
target_user_sid,
nonce,
scope,
)
.and_then(|()| config::repair_game_creator_private_acl_for_user_sid(&path, target_user_sid));
match result {
Ok(()) => Some(0),
Err(error) => {
eprintln!("AGC ACL 提权修复失败:{error}");
Some(1)
}
}
}
#[cfg(test)]
mod async_runtime_stack_tests {
/// 每帧固定占 16 KiB,用 black_box 挡住优化,让递归深度直接换算成栈用量。
fn consume_stack(depth: usize) -> u64 {
let mut frame = [0_u8; 16 * 1024];
frame[depth % frame.len()] = depth as u8;
let sum = std::hint::black_box(&frame)
.iter()
.map(|byte| u64::from(*byte))
.sum::<u64>();
if depth == 0 {
sum
} else {
sum + consume_stack(depth - 1)
}
}
/// 全局异步 runtime 的 worker 必须和 Runtime 自己的后台线程用同一份栈预算。
/// 掉了 thread_stack_size 时这条不是断言失败而是整个测试进程被 abort——这正是
/// 线上的失效形态:Agent Runner 在父 run 认领委派回执时无声消失。
#[test]
fn async_runtime_workers_hold_a_call_chain_that_overflows_the_default_stack() {
const FRAMES: usize = 192; // 192 × 16 KiB = 3 MiB,超出 tokio 默认栈,远低于 16 MiB
let runtime = super::build_agent_runtime_async_runtime().expect("build async runtime");
let handled =
runtime.block_on(async { tokio::spawn(async { consume_stack(FRAMES - 1) }).await });
assert!(handled.is_ok(), "{handled:?}");
}
}
#[cfg(not(test))]
fn main() {
install_agent_runtime_async_runtime_with_deep_stack();
let mut args = std::env::args().skip(1).collect::<Vec<_>>();
if let Some(exit_code) = run_direct_tools_mcp_if_requested(&args) {
std::process::exit(exit_code);
}
#[cfg(windows)]
if let Some(exit_code) = run_windows_acl_repair_if_requested(&args) {
std::process::exit(exit_code);
}
#[cfg(target_os = "linux")]
if command_sandbox_trampoline::is_trampoline_mode(&args) {
match command_sandbox_trampoline::run_trampoline() {
Ok(exit_code) => std::process::exit(exit_code),
Err(_) => std::process::exit(125),
}
}
#[cfg(target_os = "linux")]
if command_sandbox_trampoline::is_process_session_trampoline_mode(&args) {
match command_sandbox_trampoline::run_process_session_trampoline() {
Ok(exit_code) => std::process::exit(exit_code),
Err(_) => std::process::exit(125),
}
}
#[cfg(target_os = "linux")]
if is_process_session_child_mode(&args) {
match run_process_session_child(&args) {
Ok(exit_code) => std::process::exit(exit_code),
Err(_) => std::process::exit(125),
}
}
let runtime_config_dir = match take_cli_runtime_config_dir(&mut args) {
Ok(config_dir) => config_dir,
Err(error) => {
eprintln!("{error}");
std::process::exit(1);
}
};
if args.first().map(String::as_str) == Some("--agent-runner") {
let gui_owner_required = match args.as_slice() {
[_] => false,
[_, option] if option == "--gui-owner-required" => true,
_ => {
eprintln!(
"用法:--agent-runner [--gui-owner-required] --config-dir <AppData 绝对路径>"
);
std::process::exit(1);
}
};
let Some(config_dir) = runtime_config_dir else {
eprintln!("Agent Runner 必须显式传入 --config-dir <AppData 绝对路径>");
std::process::exit(1);
};
if let Err(error) = load_platform_session_fixture_from_env(&config_dir) {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
set_game_creator_runtime_config_dir(config_dir.clone());
if let Err(error) = run_external_agent_runner_server(config_dir, gui_owner_required) {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
return;
}
match parse_cli_command(&args) {
Ok(Some(mut command)) => {
let config_dir =
match prepare_cli_command_paths(&mut command, runtime_config_dir.as_deref()) {
Ok(config_dir) => config_dir,
Err(error) => {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
};
if let Some(config_dir) = config_dir {
if let Err(error) = load_platform_session_fixture_from_env(&config_dir) {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
if command.requires_external_agent_runner() {
let configured = if command.is_read_only_status() {
configure_external_agent_runner_read_only(&config_dir)
} else {
configure_external_agent_runner(&config_dir)
};
if let Err(error) = configured {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
}
set_game_creator_runtime_config_dir(config_dir);
}
if command.requires_started_external_agent_runner() {
if let Err(error) = ensure_external_agent_runner_started() {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
}
if let Err(error) = run_cli_command(command) {
eprintln!("agent.run.failed: {error}");
std::process::exit(1);
}
return;
}
Ok(None) => {}
Err(error) => {
eprintln!("{error}");
std::process::exit(1);
}
}
if let Some(config_dir) = runtime_config_dir {
set_game_creator_runtime_config_dir(config_dir);
}
let mut tauri_context = tauri::generate_context!();
let startup_log: Option<PathBuf> = None;
let setup_log = startup_log.clone();
let app = tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(context_menu::init())
.manage(game_creator_preview_registry())
.manage(ProjectResourcePreviewReadManager::default())
.setup(move |app| {
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.setup.begin");
let _ = append_bounded_diagnostic_line(path, "startup.appdata.configure.begin");
}
configure_game_creator_runtime_config_dir(app.handle()).inspect_err(|error| {
if let Some(path) = setup_log.as_deref() {
let details = sanitize_diagnostic_message(&error.to_string(), path.parent());
let _ = append_bounded_diagnostic_line(
path,
&format!("startup.appdata.configure.failed details={details}"),
);
show_startup_error_dialog(path);
}
})?;
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.appdata.configure.complete");
}
let config_dir = game_creator_runtime_config_dir().ok_or_else(|| {
let error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"客户端 AppData 配置目录未初始化",
);
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(
path,
"startup.appdata.resolve.failed details=config-dir-uninitialized",
);
show_startup_error_dialog(path);
}
error
})?;
load_platform_session_fixture_from_env(&config_dir).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("加载平台登录态测试 fixture 失败:{error}"),
)
})?;
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.runner.configure.begin");
}
configure_external_agent_runner(&config_dir)
.inspect_err(|error| {
if let Some(path) = setup_log.as_deref() {
let details =
sanitize_diagnostic_message(error, Some(config_dir.as_path()));
let _ = append_bounded_diagnostic_line(
path,
&format!("startup.runner.configure.failed details={details}"),
);
show_startup_error_dialog(path);
}
})
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("配置 Agent Runner 失败:{error}"),
)
})?;
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.runner.configure.complete");
}
let gui_owner_lock = acquire_external_agent_runner_gui_owner_lock(&config_dir)
.inspect_err(|error| {
if let Some(path) = setup_log.as_deref() {
let details =
sanitize_diagnostic_message(error, Some(config_dir.as_path()));
let _ = append_bounded_diagnostic_line(
path,
&format!("startup.runner.owner-lock.failed details={details}"),
);
show_startup_error_dialog(path);
}
})
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("获取 GUI owner 锁失败:{error}"),
)
})?;
let gui_owner_epoch = gui_owner_lock.owner_epoch().to_string();
app.manage(gui_owner_lock);
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.runner.start.begin");
}
set_game_creator_agent_runtime_update_app_handle(app.handle().clone());
let manifest_event_sink =
start_game_creator_manifest_invalidation_event_sink(app.handle().clone())?;
attach_external_agent_runner_gui_owner(&manifest_event_sink, &gui_owner_epoch)
.inspect_err(|error| {
if let Some(path) = setup_log.as_deref() {
let details =
sanitize_diagnostic_message(error, Some(config_dir.as_path()));
let _ = append_bounded_diagnostic_line(
path,
&format!("startup.runner.attach-owner.failed details={details}"),
);
show_startup_error_dialog(path);
}
})
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("绑定 Agent Runner GUI owner 失败:{error}"),
)
})?;
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.runner.start.complete");
}
if let Some(path) = setup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.setup.complete");
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
create_automatic_local_game_project,
init_local_game_project,
import_local_godot_project,
is_local_project_directory_non_empty,
inspect_local_project_directory,
pick_local_project_directory,
pick_local_file,
open_local_project_directory,
open_local_project_plan_gdd_markdown,
control_agent_run,
generate_local_game_draft,
chat_with_game_creator_agent,
chat_with_game_creator_role_agent,
chat_with_game_creator_role_agent_stream,
chat_with_game_creator_direct_codex,
start_game_creator_agent_runtime_task,
start_game_creator_supervisor_runtime_task,
compact_game_creator_agent_runtime_context,
read_game_creator_agent_goal,
start_game_creator_agent_goal,
edit_game_creator_agent_goal,
pause_game_creator_agent_goal,
resume_game_creator_agent_goal,
clear_game_creator_agent_goal,
steer_game_creator_agent_runtime_task,
cancel_game_creator_agent_runtime_task,
retry_game_creator_agent_runtime_task,
confirm_retry_game_creator_agent_runtime_task,
confirm_game_creator_agent_runtime_task,
reject_game_creator_agent_runtime_task,
answer_game_creator_agent_runtime_user_input,
decide_game_creator_plan_gdd,
hydrate_game_creator_plan_gdd_state,
read_game_creator_agent_runtime,
read_game_creator_agent_runtimes,
resume_game_creator_agent_runtime_tasks,
confirm_resume_game_creator_agent_runtime_tasks,
schedule_game_creator_agent_ready_tasks,
check_game_creator_llm_config,
read_platform_account_session_generation,
install_platform_account_session,
clear_platform_account_session,
read_game_creator_app_config,
write_game_creator_app_config,
upload_local_asset,
register_local_asset,
create_ui_design_resource,
derive_local_project_resource,
list_pending_local_project_resource_edits,
resume_local_project_resource_edit,
request_local_project_resource_edit_service_identity_confirmation,
confirm_local_project_resource_edit_service_identity,
archive_failed_local_project_resource_edit,
normalize_local_project_raster_resource,
import_canvas_asset,
import_canvas_export,
sync_canvas_project_assets,
import_ui_editor_assets,
prepare_ui_editor_project_fonts,
read_ui_editor_font_bytes,
check_ui_editor_font_glyph_coverage,
suggest_ui_design_semantic,
recognize_ui,
merge_ui,
bind_components,
load_ui_design_state,
save_ui_design_state,
ensure_ui_design_resource_for_prototype,
generate_platform_art_asset,
open_canvas_project,
get_game_creation_agent_capabilities,
get_limited_local_commands,
run_limited_local_command,
append_local_permission_log,
list_local_project_files,
import_local_project_image_assets,
read_local_project_file,
read_local_project_image_preview,
read_local_project_text_preview,
read_local_project_media_preview,
cancel_local_project_resource_preview_scope,
write_local_project_file,
delete_local_project_file,
read_local_game_memory,
read_local_agent_memory,
write_local_agent_memory,
write_local_game_memory,
delete_local_game_memory,
list_game_creator_agent_sessions,
create_game_creator_agent_session,
fork_game_creator_agent_session,
set_active_game_creator_agent_session,
archive_game_creator_agent_session,
read_local_conversation,
append_local_conversation_message,
build_local_project_index,
create_local_project_checkpoint,
export_local_project_package,
list_local_project_export_packages,
diff_local_project_checkpoint,
restore_local_project_checkpoint,
read_project_permission_policy,
write_project_permission_policy,
open_game_creator_workspace_window,
open_game_creator_launcher_window,
open_project_supervisor_chat_window,
start_local_game_preview,
activate_local_game_preview,
stop_local_game_preview,
stop_local_game_preview_if_matches,
get_local_game_preview_status,
read_local_project_resource_canvas_layout,
read_local_project_resource_graph,
update_local_project_resource_canvas_layout,
create_local_project_asset_canvas_draft,
read_local_project_asset_canvas_draft,
discover_local_project_asset_canvas_draft,
update_local_project_asset_canvas_draft,
acknowledge_local_project_asset_canvas_candidate_layers,
import_local_project_asset_canvas_images,
store_local_project_asset_canvas_media,
stage_local_project_asset_canvas_image,
finalize_local_project_asset_canvas_generation_failure,
archive_failed_local_project_asset_canvas_generation,
generate_local_project_asset_canvas_image,
recover_local_project_asset_canvas_generations,
confirm_local_project_asset_canvas_generation_service_identity,
read_local_project_asset_canvas_media,
discard_local_project_asset_canvas_draft,
recover_local_project_asset_canvas_transactions,
commit_local_project_asset,
commit_local_project_asset_canvas_candidate,
get_local_game_project_revision,
get_local_game_manifest
])
.build(tauri_context);
let app = match app {
Ok(app) => {
if let Some(path) = startup_log.as_deref() {
let _ = append_bounded_diagnostic_line(path, "startup.build.complete");
}
app
}
Err(error) => {
if let Some(path) = startup_log.as_deref() {
let details = sanitize_diagnostic_message(&error.to_string(), path.parent());
let _ = append_bounded_diagnostic_line(
path,
&format!("startup.build.failed details={details}"),
);
show_startup_error_dialog(path);
}
eprintln!("failed to build Genarrative AI Game Creator shell: {error}");
std::process::exit(1);
}
};
app.run(move |_app_handle, event| handle_game_creator_gui_run_event(&event));
}
#[cfg(test)]
mod diagnostic_log_tests {
use super::*;
#[test]
fn bounded_diagnostic_log_rotates_and_keeps_only_one_previous_file() {
let directory = tempfile::tempdir().expect("create diagnostics directory");
let path = directory.path().join("startup.log");
let first_record = "x".repeat(128);
append_bounded_diagnostic_line_with_limit(&path, &first_record, 64)
.expect("write first record");
append_bounded_diagnostic_line_with_limit(&path, "second-record", 64)
.expect("rotate diagnostic log");
let current = fs::read_to_string(&path).expect("read current diagnostic log");
let previous = fs::read_to_string(path.with_extension("previous.log"))
.expect("read previous diagnostic log");
assert!(current.contains("second-record"));
assert!(previous.contains(&"x".repeat(32)));
}
#[test]
fn diagnostic_message_redacts_sensitive_values_and_absolute_paths() {
assert_eq!(
sanitize_diagnostic_message("Authorization: Bearer secret", None),
"<sensitive diagnostic details redacted>"
);
assert_eq!(
sanitize_diagnostic_message(r"failed at C:\private\project\game.json", None),
"failed at <absolute-path>"
);
assert_eq!(
sanitize_diagnostic_message("failed at /home/example/private/game.json", None),
"failed at <absolute-path>"
);
}
#[test]
fn diagnostic_log_rejects_hardlink_targets_including_rotation_backup() {
let directory = tempfile::tempdir().expect("create diagnostics directory");
let outside = directory.path().join("outside.txt");
fs::write(&outside, "outside-unchanged").expect("write outside target");
let path = directory.path().join("startup.log");
fs::hard_link(&outside, &path).expect("create diagnostic hardlink");
assert!(append_bounded_diagnostic_line(&path, "must-not-write").is_err());
assert_eq!(
fs::read_to_string(&outside).expect("read outside target"),
"outside-unchanged"
);
fs::remove_file(&path).expect("remove diagnostic hardlink");
fs::write(&path, "rotate-me").expect("write diagnostic file");
let previous = path.with_extension("previous.log");
fs::hard_link(&outside, &previous).expect("create previous hardlink");
assert!(append_bounded_diagnostic_line_with_limit(&path, "blocked", 1).is_err());
assert_eq!(
fs::read_to_string(&outside).expect("read outside target after rotation"),
"outside-unchanged"
);
}
#[cfg(unix)]
#[test]
fn diagnostic_log_rejects_symlink_targets() {
use std::os::unix::fs::symlink;
let directory = tempfile::tempdir().expect("create diagnostics directory");
let outside = directory.path().join("outside.txt");
fs::write(&outside, "outside-unchanged").expect("write outside target");
let path = directory.path().join("startup.log");
symlink(&outside, &path).expect("create diagnostic symlink");
assert!(append_bounded_diagnostic_line(&path, "must-not-write").is_err());
assert_eq!(
fs::read_to_string(&outside).expect("read outside target"),
"outside-unchanged"
);
}
#[cfg(windows)]
#[test]
fn diagnostic_log_rejects_windows_symlink_or_reparse_targets_when_supported() {
use std::os::windows::fs::symlink_file;
let directory = tempfile::tempdir().expect("create diagnostics directory");
let outside = directory.path().join("outside.txt");
fs::write(&outside, "outside-unchanged").expect("write outside target");
let path = directory.path().join("startup.log");
if symlink_file(&outside, &path).is_err() {
return;
}
assert!(append_bounded_diagnostic_line(&path, "must-not-write").is_err());
assert_eq!(
fs::read_to_string(&outside).expect("read outside target"),
"outside-unchanged"
);
}
}
#[cfg(test)]
mod tests;
pub mod ui_editor;