From 8f15b83aba79715698df29537810b291215ad1cf Mon Sep 17 00:00:00 2001 From: suzmii Date: Thu, 27 Aug 2026 18:09:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20API=20Key=20app-server?= =?UTF-8?q?=20=E8=AE=A4=E8=AF=81=E9=87=8D=E8=AF=95=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API Key/provider-proxy 握手后主动关闭 remote-control 无 ChatGPT 登录态时将子进程日志级别收敛到 warn 补充 app-server 协议 fixture 回归覆盖与技术方案说明 --- .../src-tauri/src/agent/codex_app_server.rs | 95 ++++++++++++++++--- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 1 + 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index 2cbc4bc36..d3ea4bbc3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -69,6 +69,19 @@ impl CodexAppServerCredential { matches!(self, Self::AppDataKey { .. }) } + fn remote_control_disable_reason( + &self, + bridge_through_provider_proxy: bool, + ) -> Option<&'static str> { + if bridge_through_provider_proxy { + Some("provider-proxy-auth") + } else if self.uses_app_data_key() { + Some("api-key-auth") + } else { + None + } + } + fn direct_provider_route<'a>( &'a self, llm: &'a GameCreatorLlmConfig, @@ -1444,6 +1457,8 @@ impl CodexAppServerConnection { .then(|| credential.direct_provider_route(llm)) .flatten() .map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())); + let remote_control_disable_reason = + credential.remote_control_disable_reason(direct_provider_route.is_some()); let isolated_codex_home = prepare_isolated_game_creator_codex_home( working_dir.path(), credential, @@ -1549,6 +1564,12 @@ impl CodexAppServerConnection { .stderr(Stdio::piped()) .kill_on_drop(true); game_creator_codex_cli_minimal_environment(&mut command); + if remote_control_disable_reason.is_some() { + // API-key and provider-proxy sessions have no ChatGPT auth.json. + // Keep expected remote-control startup diagnostics out of the + // child log after the protocol-level disable below succeeds. + command.env("RUST_LOG", "warn"); + } if let Some(tool_bridge) = tool_bridge.as_ref() { command.env(DIRECT_TOOL_BRIDGE_URL_ENV, tool_bridge.url()); } @@ -1634,6 +1655,13 @@ impl CodexAppServerConnection { .notify("initialized", serde_json::json!({})) .await .map_err(platform_llm::LlmError::Transport)?; + if let Some(reason) = remote_control_disable_reason { + connection + .request("remoteControl/disable", serde_json::json!({})) + .await + .map_err(platform_llm::LlmError::Transport)?; + eprintln!("agent.codex_app_server.remote_control disabled reason={reason}"); + } if let Some(skill_root) = connection.inner._skill_root.as_ref() { connection .request( @@ -3213,8 +3241,8 @@ mod tests { std::fs::create_dir_all(&project_root).expect("project root"); std::fs::create_dir(&assets).expect("assets directory"); std::fs::create_dir(&agent).expect("agent directory"); - let workspace = resolve_direct_codex_game_workspace(&project_root) - .expect("resolve project workspace"); + let workspace = + resolve_direct_codex_game_workspace(&project_root).expect("resolve project workspace"); assert_eq!( workspace, project_root.canonicalize().expect("canonical project root") @@ -3703,12 +3731,15 @@ case "$initialize" in *'"method":"initialize"'*) ;; *) exit 85 ;; esac printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized case "$initialized" in *'"method":"initialized"'*) ;; *) exit 86 ;; esac +IFS= read -r remote_control_disable +case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 89 ;; esac +printf '%s\n' '{"id":2,"result":{}}' IFS= read -r extra_roots case "$extra_roots" in *'"method":"skills/extraRoots/set"'*) ;; *) exit 87 ;; esac -printf '%s\n' '{"id":2,"result":{}}' +printf '%s\n' '{"id":3,"result":{}}' IFS= read -r skills_list case "$skills_list" in *'"method":"skills/list"'*) ;; *) exit 88 ;; esac -printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}' +printf '%s\n' '{"id":4,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}' while IFS= read -r line; do :; done "#, ) @@ -4016,6 +4047,32 @@ while IFS= read -r line; do :; done ); } + #[test] + fn remote_control_is_disabled_when_the_isolated_home_has_no_chatgpt_auth() { + let api_key = CodexAppServerCredential::AppDataKey { + fingerprint: "api-key".to_string(), + }; + assert_eq!( + api_key.remote_control_disable_reason(false), + Some("api-key-auth") + ); + assert_eq!( + api_key.remote_control_disable_reason(true), + Some("provider-proxy-auth") + ); + + let auth_bridge = CodexAppServerCredential::AuthBridge { + fingerprint: "auth-bridge".to_string(), + auth_json: br#"{"tokens":{"access_token":"fixture"}}"#.to_vec(), + api_key: None, + }; + assert_eq!(auth_bridge.remote_control_disable_reason(false), None); + assert_eq!( + auth_bridge.remote_control_disable_reason(true), + Some("provider-proxy-auth") + ); + } + #[test] fn codex_app_server_auth_bridge_snapshot_drives_pool_and_isolated_home() { let temp = tempfile::tempdir().expect("temp dir"); @@ -4146,12 +4203,15 @@ case "$initialize" in *'"method":"initialize"'*) ;; *) exit 43 ;; esac printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized case "$initialized" in *'"method":"initialized"'*) ;; *) exit 44 ;; esac +IFS= read -r remote_control_disable +case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 47 ;; esac +printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start case "$thread_start" in *'"method":"thread/start"'*'"modelProvider":"genarrative_agc"'*) ;; *) exit 45 ;; esac -printf '%s\n' '{"id":2,"result":{"thread":{"id":"thread-1"}}}' +printf '%s\n' '{"id":3,"result":{"thread":{"id":"thread-1"}}}' IFS= read -r turn_start case "$turn_start" in *'"method":"turn/start"'*'"outputSchema"'*) ;; *) exit 46 ;; esac -printf '%s\n' '{"id":3,"result":{"turn":{"id":"turn-1","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":4,"result":{"turn":{"id":"turn-1","items":[],"status":"inProgress"}}}' printf '%s\n' '{"method":"turn/started","params":{"threadId":"thread-1","turn":{"id":"turn-1","items":[],"status":"inProgress"}}}' printf '%s\n' '{"method":"item/fileChange/patchUpdated","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"change-1","patch":"*** SECRET PATCH /private/project"}}' printf '%s\n' '{"method":"item/commandExecution/outputDelta","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"command-1","delta":"Bearer secret-command-output"}}' @@ -4267,14 +4327,17 @@ case "$initialize" in *'"method":"initialize"'*) ;; *) exit 70 ;; esac printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized case "$initialized" in *'"method":"initialized"'*) ;; *) exit 71 ;; esac +IFS= read -r remote_control_disable +case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 76 ;; esac +printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start case "$thread_start" in *'"method":"thread/start"'*'"approvalPolicy":"never"'*'"sandbox":"read-only"'*) ;; *) exit 72 ;; esac case "$thread_start" in *'workspace-write'*|*'workspaceWrite'*|*'writableRoots'*) exit 73 ;; esac -printf '%s\n' '{"id":2,"result":{"thread":{"id":"home-thread"}}}' +printf '%s\n' '{"id":3,"result":{"thread":{"id":"home-thread"}}}' IFS= read -r turn_start case "$turn_start" in *'"method":"turn/start"'*'"approvalPolicy":"never"'*) ;; *) exit 74 ;; esac case "$turn_start" in *'"sandboxPolicy"'*|*'workspaceWrite'*|*'writableRoots'*) exit 75 ;; esac -printf '%s\n' '{"id":3,"result":{"turn":{"id":"home-turn","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":4,"result":{"turn":{"id":"home-turn","items":[],"status":"inProgress"}}}' printf '%s\n' '{"method":"item/completed","params":{"threadId":"home-thread","turnId":"home-turn","item":{"id":"item-1","type":"fileChange"}}}' while IFS= read -r line; do :; done "#, @@ -4326,15 +4389,18 @@ while IFS= read -r line; do :; done IFS= read -r initialize printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized +IFS= read -r remote_control_disable +case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 77 ;; esac +printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start -printf '%s\n' '{"id":2,"result":{"thread":{"id":"thread-cancel"}}}' +printf '%s\n' '{"id":3,"result":{"thread":{"id":"thread-cancel"}}}' IFS= read -r turn_start sleep 0.2 -printf '%s\n' '{"id":3,"result":{"turn":{"id":"turn-cancel","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":4,"result":{"turn":{"id":"turn-cancel","items":[],"status":"inProgress"}}}' IFS= read -r interrupt case "$interrupt" in *'"method":"turn/interrupt"'*'"turnId":"turn-cancel"'*) ;; *) exit 51 ;; esac : > "$HOME/interrupt-seen" -printf '%s\n' '{"id":4,"result":{}}' +printf '%s\n' '{"id":5,"result":{}}' while IFS= read -r line; do :; done "#, ) @@ -4388,10 +4454,13 @@ while IFS= read -r line; do :; done IFS= read -r initialize printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized +IFS= read -r remote_control_disable +case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 78 ;; esac +printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start -printf '%s\n' '{"id":2,"result":{"thread":{"id":"thread-timeout"}}}' +printf '%s\n' '{"id":3,"result":{"thread":{"id":"thread-timeout"}}}' IFS= read -r turn_start -printf '%s\n' '{"id":3,"result":{"turn":{"id":"turn-timeout","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":4,"result":{"turn":{"id":"turn-timeout","items":[],"status":"inProgress"}}}' while IFS= read -r line; do :; done "#, ) diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 3f814a390..662ff14dc 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -210,6 +210,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 - 进程与节点:External Runner 按“有效 Agent LLM 凭据/Responses 路由 + `projectId/agentId/sessionId/runId`”隔离长期 `codex app-server --stdio`,即每个权威节点 run 直接持有自己的 Codex CLI 子进程与 ephemeral thread,每次完整权威请求映射 turn。同一节点 turn 串行,节点之间进程级隔离;单节点连接失败不得使其它节点同时失去终态。Codex thread 不写 durable recovery;节点完成、重启、retry、handoff 和 finalization 仍只认 AGC 账本。 - LLM 配置:`apiKind` 始终只接受 `openai_responses`;非空 Key 转换为 app-server model provider,base URL 生效,Key 仅走专用环境变量;空 Key 只桥接用户 Codex `auth.json`,不继承环境 `CODEX_API_KEY`。设置面板在 app-server 模式继续显示并保存 model、effort、stream、全局/逐 Agent Key 与路由配置;`openai_chat / anthropic` 明确提示切 `provider`,不得悄悄忽略。`stream=true` 接入 app-server 文本 delta;`webSearchEnabled=true` 只允许 DirectProject 经客户端审核的 `agc_web_search` 使用,不得启用 Codex 原生 webSearch 或任意网络。 - 安全与取消:临时 cwd、隔离 `CODEX_HOME` 与 OS HOME、read-only、network off、never approval,并在启动前关闭 web/multi-agent/shell/browser/plugin/image 等原生能力;取消从 turn-start pending 阶段就跟踪且只 interrupt 当前 turn。已发送 turn 后连接断开或终态丢失进入 reconciliation,只关闭当前节点进程且不重放同一 request slot;明确 failed/interrupted 不按 transport 重试。 +- remote-control 认证边界:没有 ChatGPT `auth.json` 的 API Key / provider-proxy app-server 会在 `initialize` 后、其它 RPC 前立即调用 `remoteControl/disable`,避免上游因 `desired_state=Unknown` 进入 1Hz 认证重试;只有实际桥接 ChatGPT 登录态的 AuthBridge 保持 remote-control 可用。禁用成功后 API Key 子进程使用 `RUST_LOG=warn` 收敛预期认证噪音,禁用 RPC 失败则连接创建失败关闭,不伪造 `auth.json` 或静默继续。 - 资源与退出:app-server pool 按实际凭据快照/base URL/API kind/CLI 版本和节点 run 身份隔离并做有界 LRU;空 AppData Key 必须读取同一份有界 `auth.json` 字节来生成池指纹并桥接隔离登录态,继承的 `CODEX_API_KEY` 始终移除,node thread 也只淘汰 inactive LRU。Runner 正常、强制和 watchdog 退出都显式关池,Linux child 绑定 parent-death signal,防止强杀 Runner 后遗留带凭据孤儿进程。stdout NDJSON 与 stderr 无换行记录均有硬上限;stderr 原文不写入诊断,只记录固定分类、总字节数、SHA-256 和可取得的退出状态。 - 旧配置迁移:既有 AppData 若没有 `agentMode`,只有全局和逐 Agent 路由均为 `openai_responses` 时迁移到 `codex_app_server`;存在 `openai_chat / anthropic` 时显式保留 `provider`,避免打开项目自动恢复时把所有节点批量写成 `invalid-config`。用户确认端点支持 Responses 后,可在设置中显式切换并保留原 model/base URL/API Key。 - 验收:fake JSON-RPC fixture、三态 UI/config、配置指纹、unknown-terminal 零重放、旧两种模式回归和显式 ignored 真实 smoke 全部通过后,才可视为模式切换完成。 -- 2.52.0 From 5a7abefe69c7b99a492362dbde3a486889ee268e Mon Sep 17 00:00:00 2001 From: suzmii Date: Thu, 27 Aug 2026 20:09:00 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20API=20Key=20app-server?= =?UTF-8?q?=20=E7=9A=84=E5=90=AF=E5=8A=A8=E7=BA=A7=20remote-control=20?= =?UTF-8?q?=E7=A6=81=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 Codex 内部启动环境变量提前禁用 remote-control 删除无效的 remoteControl/disable RPC 更新 fixture、测试和技术文档 --- .../src-tauri/src/agent/codex_app_server.rs | 54 ++++++++----------- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index d3ea4bbc3..5a1d30e9f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -11,6 +11,8 @@ use tokio::sync::{mpsc, oneshot, Mutex}; const GAME_CREATOR_CODEX_APP_SERVER_PROVIDER_ID: &str = "genarrative_agc"; const GAME_CREATOR_CODEX_APP_SERVER_API_KEY_ENV: &str = "GENARRATIVE_AGC_CODEX_API_KEY"; +const GAME_CREATOR_CODEX_APP_SERVER_REMOTE_CONTROL_DISABLED_ENV: &str = + "CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED"; const GAME_CREATOR_CODEX_AUTH_BRIDGE_API_BASE_URL: &str = "https://api.openai.com/v1"; const GAME_CREATOR_CODEX_APP_SERVER_PROTOCOL: &str = "genarrative-codex-app-server.v3"; const GAME_CREATOR_CODEX_APP_SERVER_LINE_MAX_BYTES: usize = 4 * 1024 * 1024; @@ -1566,8 +1568,12 @@ impl CodexAppServerConnection { game_creator_codex_cli_minimal_environment(&mut command); if remote_control_disable_reason.is_some() { // API-key and provider-proxy sessions have no ChatGPT auth.json. - // Keep expected remote-control startup diagnostics out of the - // child log after the protocol-level disable below succeeds. + // Disable remote-control before its websocket task can enter the + // authentication retry loop. + command.env( + GAME_CREATOR_CODEX_APP_SERVER_REMOTE_CONTROL_DISABLED_ENV, + "1", + ); command.env("RUST_LOG", "warn"); } if let Some(tool_bridge) = tool_bridge.as_ref() { @@ -1656,10 +1662,6 @@ impl CodexAppServerConnection { .await .map_err(platform_llm::LlmError::Transport)?; if let Some(reason) = remote_control_disable_reason { - connection - .request("remoteControl/disable", serde_json::json!({})) - .await - .map_err(platform_llm::LlmError::Transport)?; eprintln!("agent.codex_app_server.remote_control disabled reason={reason}"); } if let Some(skill_root) = connection.inner._skill_root.as_ref() { @@ -3723,6 +3725,7 @@ case "$GENARRATIVE_AGC_CODEX_API_KEY" in agc-provider-session-*) ;; *) exit 81 ;; esac +[ "$CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED" = "1" ] || exit 90 [ "$GENARRATIVE_AGC_CODEX_API_KEY" != "fixture-secret" ] || exit 82 case " $* " in *"fixture-secret"*) exit 83 ;; esac case " $* " in *'--disable hooks'*) ;; *) exit 84 ;; esac @@ -3731,15 +3734,12 @@ case "$initialize" in *'"method":"initialize"'*) ;; *) exit 85 ;; esac printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized case "$initialized" in *'"method":"initialized"'*) ;; *) exit 86 ;; esac -IFS= read -r remote_control_disable -case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 89 ;; esac -printf '%s\n' '{"id":2,"result":{}}' IFS= read -r extra_roots case "$extra_roots" in *'"method":"skills/extraRoots/set"'*) ;; *) exit 87 ;; esac -printf '%s\n' '{"id":3,"result":{}}' +printf '%s\n' '{"id":2,"result":{}}' IFS= read -r skills_list case "$skills_list" in *'"method":"skills/list"'*) ;; *) exit 88 ;; esac -printf '%s\n' '{"id":4,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}' +printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}' while IFS= read -r line; do :; done "#, ) @@ -4048,7 +4048,7 @@ while IFS= read -r line; do :; done } #[test] - fn remote_control_is_disabled_when_the_isolated_home_has_no_chatgpt_auth() { + fn remote_control_disable_policy_matches_the_credential_boundary() { let api_key = CodexAppServerCredential::AppDataKey { fingerprint: "api-key".to_string(), }; @@ -4203,15 +4203,12 @@ case "$initialize" in *'"method":"initialize"'*) ;; *) exit 43 ;; esac printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized case "$initialized" in *'"method":"initialized"'*) ;; *) exit 44 ;; esac -IFS= read -r remote_control_disable -case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 47 ;; esac -printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start case "$thread_start" in *'"method":"thread/start"'*'"modelProvider":"genarrative_agc"'*) ;; *) exit 45 ;; esac -printf '%s\n' '{"id":3,"result":{"thread":{"id":"thread-1"}}}' +printf '%s\n' '{"id":2,"result":{"thread":{"id":"thread-1"}}}' IFS= read -r turn_start case "$turn_start" in *'"method":"turn/start"'*'"outputSchema"'*) ;; *) exit 46 ;; esac -printf '%s\n' '{"id":4,"result":{"turn":{"id":"turn-1","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":3,"result":{"turn":{"id":"turn-1","items":[],"status":"inProgress"}}}' printf '%s\n' '{"method":"turn/started","params":{"threadId":"thread-1","turn":{"id":"turn-1","items":[],"status":"inProgress"}}}' printf '%s\n' '{"method":"item/fileChange/patchUpdated","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"change-1","patch":"*** SECRET PATCH /private/project"}}' printf '%s\n' '{"method":"item/commandExecution/outputDelta","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"command-1","delta":"Bearer secret-command-output"}}' @@ -4327,17 +4324,14 @@ case "$initialize" in *'"method":"initialize"'*) ;; *) exit 70 ;; esac printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized case "$initialized" in *'"method":"initialized"'*) ;; *) exit 71 ;; esac -IFS= read -r remote_control_disable -case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 76 ;; esac -printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start case "$thread_start" in *'"method":"thread/start"'*'"approvalPolicy":"never"'*'"sandbox":"read-only"'*) ;; *) exit 72 ;; esac case "$thread_start" in *'workspace-write'*|*'workspaceWrite'*|*'writableRoots'*) exit 73 ;; esac -printf '%s\n' '{"id":3,"result":{"thread":{"id":"home-thread"}}}' +printf '%s\n' '{"id":2,"result":{"thread":{"id":"home-thread"}}}' IFS= read -r turn_start case "$turn_start" in *'"method":"turn/start"'*'"approvalPolicy":"never"'*) ;; *) exit 74 ;; esac case "$turn_start" in *'"sandboxPolicy"'*|*'workspaceWrite'*|*'writableRoots'*) exit 75 ;; esac -printf '%s\n' '{"id":4,"result":{"turn":{"id":"home-turn","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":3,"result":{"turn":{"id":"home-turn","items":[],"status":"inProgress"}}}' printf '%s\n' '{"method":"item/completed","params":{"threadId":"home-thread","turnId":"home-turn","item":{"id":"item-1","type":"fileChange"}}}' while IFS= read -r line; do :; done "#, @@ -4389,18 +4383,15 @@ while IFS= read -r line; do :; done IFS= read -r initialize printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized -IFS= read -r remote_control_disable -case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 77 ;; esac -printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start -printf '%s\n' '{"id":3,"result":{"thread":{"id":"thread-cancel"}}}' +printf '%s\n' '{"id":2,"result":{"thread":{"id":"thread-cancel"}}}' IFS= read -r turn_start sleep 0.2 -printf '%s\n' '{"id":4,"result":{"turn":{"id":"turn-cancel","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":3,"result":{"turn":{"id":"turn-cancel","items":[],"status":"inProgress"}}}' IFS= read -r interrupt case "$interrupt" in *'"method":"turn/interrupt"'*'"turnId":"turn-cancel"'*) ;; *) exit 51 ;; esac : > "$HOME/interrupt-seen" -printf '%s\n' '{"id":5,"result":{}}' +printf '%s\n' '{"id":4,"result":{}}' while IFS= read -r line; do :; done "#, ) @@ -4454,13 +4445,10 @@ while IFS= read -r line; do :; done IFS= read -r initialize printf '%s\n' '{"id":1,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}' IFS= read -r initialized -IFS= read -r remote_control_disable -case "$remote_control_disable" in *'"method":"remoteControl/disable"'*) ;; *) exit 78 ;; esac -printf '%s\n' '{"id":2,"result":{}}' IFS= read -r thread_start -printf '%s\n' '{"id":3,"result":{"thread":{"id":"thread-timeout"}}}' +printf '%s\n' '{"id":2,"result":{"thread":{"id":"thread-timeout"}}}' IFS= read -r turn_start -printf '%s\n' '{"id":4,"result":{"turn":{"id":"turn-timeout","items":[],"status":"inProgress"}}}' +printf '%s\n' '{"id":3,"result":{"turn":{"id":"turn-timeout","items":[],"status":"inProgress"}}}' while IFS= read -r line; do :; done "#, ) diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 662ff14dc..750f83e6a 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -210,7 +210,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 - 进程与节点:External Runner 按“有效 Agent LLM 凭据/Responses 路由 + `projectId/agentId/sessionId/runId`”隔离长期 `codex app-server --stdio`,即每个权威节点 run 直接持有自己的 Codex CLI 子进程与 ephemeral thread,每次完整权威请求映射 turn。同一节点 turn 串行,节点之间进程级隔离;单节点连接失败不得使其它节点同时失去终态。Codex thread 不写 durable recovery;节点完成、重启、retry、handoff 和 finalization 仍只认 AGC 账本。 - LLM 配置:`apiKind` 始终只接受 `openai_responses`;非空 Key 转换为 app-server model provider,base URL 生效,Key 仅走专用环境变量;空 Key 只桥接用户 Codex `auth.json`,不继承环境 `CODEX_API_KEY`。设置面板在 app-server 模式继续显示并保存 model、effort、stream、全局/逐 Agent Key 与路由配置;`openai_chat / anthropic` 明确提示切 `provider`,不得悄悄忽略。`stream=true` 接入 app-server 文本 delta;`webSearchEnabled=true` 只允许 DirectProject 经客户端审核的 `agc_web_search` 使用,不得启用 Codex 原生 webSearch 或任意网络。 - 安全与取消:临时 cwd、隔离 `CODEX_HOME` 与 OS HOME、read-only、network off、never approval,并在启动前关闭 web/multi-agent/shell/browser/plugin/image 等原生能力;取消从 turn-start pending 阶段就跟踪且只 interrupt 当前 turn。已发送 turn 后连接断开或终态丢失进入 reconciliation,只关闭当前节点进程且不重放同一 request slot;明确 failed/interrupted 不按 transport 重试。 -- remote-control 认证边界:没有 ChatGPT `auth.json` 的 API Key / provider-proxy app-server 会在 `initialize` 后、其它 RPC 前立即调用 `remoteControl/disable`,避免上游因 `desired_state=Unknown` 进入 1Hz 认证重试;只有实际桥接 ChatGPT 登录态的 AuthBridge 保持 remote-control 可用。禁用成功后 API Key 子进程使用 `RUST_LOG=warn` 收敛预期认证噪音,禁用 RPC 失败则连接创建失败关闭,不伪造 `auth.json` 或静默继续。 +- remote-control 认证边界:没有 ChatGPT `auth.json` 的 API Key / provider-proxy app-server 在启动时设置 Codex 内部环境变量 `CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1`,让 remote-control 以 `desired_state=Disabled` 启动,避免上游进入 1Hz 认证重试;不再依赖需要 ChatGPT 登录态的 `remoteControl/disable` RPC。只有实际桥接 ChatGPT 登录态的 AuthBridge 保持 remote-control 可用。API Key 子进程同时使用 `RUST_LOG=warn` 收敛剩余预期噪音,不伪造 `auth.json` 或静默继续。 - 资源与退出:app-server pool 按实际凭据快照/base URL/API kind/CLI 版本和节点 run 身份隔离并做有界 LRU;空 AppData Key 必须读取同一份有界 `auth.json` 字节来生成池指纹并桥接隔离登录态,继承的 `CODEX_API_KEY` 始终移除,node thread 也只淘汰 inactive LRU。Runner 正常、强制和 watchdog 退出都显式关池,Linux child 绑定 parent-death signal,防止强杀 Runner 后遗留带凭据孤儿进程。stdout NDJSON 与 stderr 无换行记录均有硬上限;stderr 原文不写入诊断,只记录固定分类、总字节数、SHA-256 和可取得的退出状态。 - 旧配置迁移:既有 AppData 若没有 `agentMode`,只有全局和逐 Agent 路由均为 `openai_responses` 时迁移到 `codex_app_server`;存在 `openai_chat / anthropic` 时显式保留 `provider`,避免打开项目自动恢复时把所有节点批量写成 `invalid-config`。用户确认端点支持 Responses 后,可在设置中显式切换并保留原 model/base URL/API Key。 - 验收:fake JSON-RPC fixture、三态 UI/config、配置指纹、unknown-terminal 零重放、旧两种模式回归和显式 ignored 真实 smoke 全部通过后,才可视为模式切换完成。 -- 2.52.0