Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/runner/protocol.rs
T
kdletters 0594a90bdd
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 4m15s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m32s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m58s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m13s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m3s
Project CI / AI game creator shell Rust crates (push) Successful in 3m1s
Project CI / Frontend tests (push) Failing after 4m24s
Project CI / Repository checks (push) Successful in 4m1s
Project CI / Native shell tests (push) Successful in 7m45s
Project CI / AI game creator shell web tests (push) Failing after 3m4s
Project CI / Backend tests (push) Successful in 8m20s
合入平台会话身份与凭据分离
平台会话快照拆分身份代次与写入 revision,凭据续期轮换不再中断在途生成
AGC Runner 请求参数新增 platform_auth_revision,并按 revision 做单调写入判定
解决与多窗口共享 Runner 的冲突,保留 claim adopt/publish 与界面参与锁语义
同步刷新轮换竞争、appSurface 鉴权用例与客户端 API 测试
补录平台会话身份与凭据分离的实施计划、共享记忆与决策记录
2026-09-16 17:42:00 +08:00

404 lines
16 KiB
Rust

use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest as _, Sha256};
use std::collections::VecDeque;
use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
pub(crate) const EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION: u32 = 7;
pub(super) const EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME: &str = "agent-runner.endpoint.json";
pub(super) const EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME: &str = "agent-runner.lock";
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_PARTICIPANT_LOCK_FILE_NAME: &str =
"agent-runner.gui-participant.lock";
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_OWNER_CLAIM_FILE_NAME: &str =
"agent-runner.gui-owner.claim.json";
pub(super) const EXTERNAL_AGENT_RUNNER_PROJECT_OWNER_FILE_NAME: &str = "execution-owner.lock";
pub(super) const EXTERNAL_AGENT_RUNNER_PROJECT_OWNER_DIAGNOSTIC_FILE_NAME: &str =
"execution-owner.json";
pub(super) const EXTERNAL_AGENT_RUNNER_PROJECT_OWNER_PATH: &str =
".agent/runtime/execution-owner.lock";
pub(super) const EXTERNAL_AGENT_RUNNER_PROJECT_OWNER_DIAGNOSTIC_PATH: &str =
".agent/runtime/execution-owner.json";
pub(super) const EXTERNAL_AGENT_RUNNER_MAX_FRAME_BYTES: usize = 1024 * 1024;
pub(super) const EXTERNAL_AGENT_RUNNER_MAX_ENDPOINT_BYTES: u64 = 64 * 1024;
pub(super) const EXTERNAL_AGENT_RUNNER_MAX_OWNER_BYTES: u64 = 16 * 1024;
pub(super) const EXTERNAL_AGENT_RUNNER_MAX_CONNECTIONS: usize = 32;
pub(super) const EXTERNAL_AGENT_RUNNER_MAX_CACHED_REQUESTS: usize = 512;
pub(super) const EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE: &str = "runtime-wake-retryable";
pub(super) const EXTERNAL_AGENT_RUNNER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
pub(super) const EXTERNAL_AGENT_RUNNER_IO_TIMEOUT: Duration = Duration::from_secs(10);
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_CONNECT_TIMEOUT: Duration =
Duration::from_millis(250);
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_IO_TIMEOUT: Duration =
Duration::from_millis(750);
pub(super) const EXTERNAL_AGENT_RUNNER_FORCED_WORKER_DRAIN_TIMEOUT: Duration =
Duration::from_millis(250);
pub(super) const EXTERNAL_AGENT_RUNNER_FORCED_TOTAL_DRAIN_TIMEOUT: Duration =
Duration::from_millis(1_500);
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_FORCE_TERMINATE_GRACE: Duration =
Duration::from_millis(500);
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_EXIT_TIMEOUT: Duration = Duration::from_secs(2);
pub(super) const EXTERNAL_AGENT_RUNNER_CONTEXT_COMPACTION_IO_TIMEOUT: Duration =
Duration::from_secs(6 * 60);
pub(super) const EXTERNAL_AGENT_RUNNER_START_TIMEOUT: Duration = Duration::from_secs(30);
pub(super) const EXTERNAL_AGENT_RUNNER_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(2);
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_OWNER_CHECK_INTERVAL: Duration =
Duration::from_millis(100);
pub(super) const EXTERNAL_AGENT_RUNNER_GUI_OWNER_WATCHDOG_HARD_EXIT_TIMEOUT: Duration =
Duration::from_millis(1_750);
pub(super) const EXTERNAL_AGENT_RUNNER_LOOP_INTERVAL: Duration = Duration::from_millis(25);
#[cfg(target_os = "linux")]
pub(super) const EXTERNAL_AGENT_RUNNER_LINUX_EPHEMERAL_PORT_RANGE_PATH: &str =
"/proc/sys/net/ipv4/ip_local_port_range";
#[cfg(target_os = "linux")]
pub(super) const EXTERNAL_AGENT_RUNNER_LINUX_RESERVED_PORTS_PATH: &str =
"/proc/sys/net/ipv4/ip_local_reserved_ports";
#[cfg(target_os = "linux")]
pub(super) const EXTERNAL_AGENT_RUNNER_LINUX_UNPRIVILEGED_PORT_START_PATH: &str =
"/proc/sys/net/ipv4/ip_unprivileged_port_start";
#[cfg(target_os = "linux")]
pub(super) const EXTERNAL_AGENT_RUNNER_FALLBACK_PORT_START: u16 = 61_000;
pub(super) static EXTERNAL_AGENT_RUNNER_CONFIG_DIR: OnceLock<Mutex<Option<PathBuf>>> =
OnceLock::new();
pub(super) static EXTERNAL_AGENT_RUNNER_CONFIGURE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
pub(super) static EXTERNAL_AGENT_RUNNER_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(super) static EXTERNAL_AGENT_RUNNER_SERVER_PROCESS: AtomicBool = AtomicBool::new(false);
pub(super) static EXTERNAL_AGENT_RUNNER_GUI_OWNER_REQUIRED_CLIENT: AtomicBool =
AtomicBool::new(false);
pub(super) static EXTERNAL_AGENT_RUNNER_EXECUTABLE_FINGERPRINT: OnceLock<String> = OnceLock::new();
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerEndpoint {
pub(super) protocol_version: u32,
pub(super) pid: u32,
pub(super) boot_id: String,
pub(super) port: u16,
pub(super) token: String,
pub(super) heartbeat_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) executable_fingerprint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) process_start_identity: Option<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum ExternalAgentRunnerReuseDecision {
Reuse,
Retire,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerProjectExecutionOwnerRecord {
pub(super) protocol_version: u32,
pub(super) pid: u32,
pub(super) boot_id: String,
pub(super) acquired_at: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) recovered_from_boot_id: Option<String>,
}
impl ExternalAgentRunnerProjectExecutionOwnerRecord {
pub(super) fn validate_shape(&self) -> Result<(), String> {
if self.protocol_version == 0 || self.pid == 0 {
return Err("项目 execution-owner 协议版本或 pid 无效".to_string());
}
if self.boot_id.trim().is_empty() || self.boot_id.len() > 128 {
return Err("项目 execution-owner bootId 无效".to_string());
}
if self
.recovered_from_boot_id
.as_deref()
.is_some_and(|value| value.trim().is_empty() || value.len() > 128)
{
return Err("项目 execution-owner recoveredFromBootId 无效".to_string());
}
Ok(())
}
}
impl ExternalAgentRunnerEndpoint {
pub(super) fn validate_shape(&self) -> Result<(), String> {
if self.protocol_version == 0 || self.pid == 0 {
return Err("Agent Runner endpoint 缺少有效 pid".to_string());
}
if self.boot_id.trim().is_empty() || self.boot_id.len() > 128 {
return Err("Agent Runner endpoint bootId 无效".to_string());
}
if self.port == 0 {
return Err("Agent Runner endpoint 端口无效".to_string());
}
if self.token.len() < 32 || self.token.len() > 256 {
return Err("Agent Runner endpoint token 无效".to_string());
}
if self.executable_fingerprint.as_deref().is_some_and(|value| {
value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit())
}) {
return Err("Agent Runner endpoint executableFingerprint 无效".to_string());
}
if self
.process_start_identity
.as_deref()
.is_some_and(|value| value.is_empty() || value.len() > 128)
{
return Err("Agent Runner endpoint processStartIdentity 无效".to_string());
}
Ok(())
}
}
pub(super) fn external_agent_runner_endpoint_reuse_decision(
endpoint: &ExternalAgentRunnerEndpoint,
executable_fingerprint: &str,
) -> ExternalAgentRunnerReuseDecision {
if endpoint.protocol_version == EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION
&& endpoint.executable_fingerprint.as_deref() == Some(executable_fingerprint)
{
ExternalAgentRunnerReuseDecision::Reuse
} else {
ExternalAgentRunnerReuseDecision::Retire
}
}
pub(super) fn external_agent_runner_executable_fingerprint_at(
path: &Path,
) -> Result<String, String> {
let mut file = File::open(path)
.map_err(|error| format!("打开当前 Agent Runner 可执行文件失败:{error}"))?;
let metadata = file
.metadata()
.map_err(|error| format!("读取当前 Agent Runner 可执行文件元数据失败:{error}"))?;
if !metadata.is_file() {
return Err("当前 Agent Runner 可执行文件不是普通文件".to_string());
}
let mut digest = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = file
.read(&mut buffer)
.map_err(|error| format!("读取当前 Agent Runner 可执行文件失败:{error}"))?;
if read == 0 {
break;
}
digest.update(&buffer[..read]);
}
Ok(format!("{:x}", digest.finalize()))
}
pub(super) fn current_external_agent_runner_executable_fingerprint() -> Result<String, String> {
if let Some(fingerprint) = EXTERNAL_AGENT_RUNNER_EXECUTABLE_FINGERPRINT.get() {
return Ok(fingerprint.clone());
}
let executable = std::env::current_exe()
.map_err(|error| format!("定位当前 Agent Runner 可执行文件失败:{error}"))?;
let fingerprint = external_agent_runner_executable_fingerprint_at(&executable)?;
let _ = EXTERNAL_AGENT_RUNNER_EXECUTABLE_FINGERPRINT.set(fingerprint.clone());
Ok(EXTERNAL_AGENT_RUNNER_EXECUTABLE_FINGERPRINT
.get()
.cloned()
.unwrap_or(fingerprint))
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ExternalAgentRunnerStatus {
pub(crate) enabled: bool,
pub(crate) running: bool,
pub(crate) protocol_version: u32,
pub(crate) pid: Option<u32>,
pub(crate) boot_id: Option<String>,
pub(crate) port: Option<u16>,
pub(crate) heartbeat_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<String>,
}
impl ExternalAgentRunnerStatus {
pub(super) fn disabled() -> Self {
Self {
enabled: false,
running: false,
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
pid: None,
boot_id: None,
port: None,
heartbeat_at: None,
error: None,
}
}
pub(super) fn from_endpoint(endpoint: &ExternalAgentRunnerEndpoint, running: bool) -> Self {
Self {
enabled: true,
running,
protocol_version: endpoint.protocol_version,
pid: Some(endpoint.pid),
boot_id: Some(endpoint.boot_id.clone()),
port: Some(endpoint.port),
heartbeat_at: Some(endpoint.heartbeat_at),
error: None,
}
}
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) root: Option<String>,
#[serde(default, alias = "agentId", skip_serializing_if = "Option::is_none")]
pub(super) agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) run_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) action_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) steer_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) event_sink_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) event_sink_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) gui_owner_epoch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) gui_owner_session_revision: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) platform_user_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) platform_access_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) platform_api_base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) platform_auth_generation: Option<u64>,
/// 原生写入 revision:只用于 install / clear 的顺序判定。同一身份的凭据轮换会推进
/// revision,但不推进 `platform_auth_generation`(身份代次)。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) platform_auth_revision: Option<u64>,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerRequest {
pub(super) protocol_version: u32,
pub(super) request_id: String,
pub(super) token: String,
pub(super) method: String,
#[serde(default)]
pub(super) params: ExternalAgentRunnerRequestParams,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerProtocolError {
pub(super) code: String,
pub(super) message: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerResponse {
pub(super) protocol_version: u32,
pub(super) request_id: String,
pub(super) ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) error: Option<ExternalAgentRunnerProtocolError>,
}
impl ExternalAgentRunnerResponse {
pub(super) fn success(request_id: &str, result: Value) -> Self {
Self {
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
request_id: request_id.to_string(),
ok: true,
result: Some(result),
error: None,
}
}
pub(super) fn failure(request_id: &str, code: &str, message: impl Into<String>) -> Self {
Self {
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
request_id: request_id.to_string(),
ok: false,
result: None,
error: Some(ExternalAgentRunnerProtocolError {
code: code.to_string(),
message: message.into(),
}),
}
}
}
#[derive(Debug)]
pub(super) enum ExternalAgentRunnerFrameError {
Io(io::Error),
Oversize(u32),
}
impl std::fmt::Display for ExternalAgentRunnerFrameError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(error) => write!(formatter, "{error}"),
Self::Oversize(length) => write!(
formatter,
"Agent Runner frame 超过 {} 字节上限:{length}",
EXTERNAL_AGENT_RUNNER_MAX_FRAME_BYTES
),
}
}
}
impl From<io::Error> for ExternalAgentRunnerFrameError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
#[derive(Clone)]
pub(super) struct CachedExternalAgentRunnerResponse {
pub(super) request_id: String,
pub(super) fingerprint: String,
pub(super) response: ExternalAgentRunnerResponse,
}
#[derive(Default)]
pub(super) struct ExternalAgentRunnerRequestCache {
pub(super) entries: VecDeque<CachedExternalAgentRunnerResponse>,
}
impl ExternalAgentRunnerRequestCache {
pub(super) fn find(&self, request_id: &str) -> Option<&CachedExternalAgentRunnerResponse> {
self.entries
.iter()
.find(|entry| entry.request_id == request_id)
}
pub(super) fn insert(
&mut self,
request_id: String,
fingerprint: String,
response: ExternalAgentRunnerResponse,
) {
if self.entries.len() >= EXTERNAL_AGENT_RUNNER_MAX_CACHED_REQUESTS {
self.entries.pop_front();
}
self.entries.push_back(CachedExternalAgentRunnerResponse {
request_id,
fingerprint,
response,
});
}
}