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 测试 补录平台会话身份与凭据分离的实施计划、共享记忆与决策记录
404 lines
16 KiB
Rust
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,
|
|
});
|
|
}
|
|
}
|