Skip to content

Commit d7d4905

Browse files
author
zynx
committed
Merge remote-tracking branch 'origin/main' into aionissue/task-20260701-002-004
# Conflicts: # crates/aionui-conversation/src/service.rs
2 parents 3c76a16 + 3840b77 commit d7d4905

78 files changed

Lines changed: 4929 additions & 3227 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/aionui-ai-agent/src/agent_task.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@ mod aionrs_config_option_tests {
531531
session_mode: None,
532532
extra_mcp_servers: std::collections::HashMap::new(),
533533
bedrock_config: None,
534+
runtime_env: Vec::new(),
534535
}
535536
}
536537

crates/aionui-ai-agent/src/factory/acp.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::agent_task::AgentInstance;
44
use crate::error::AgentError;
55
use crate::factory::AgentFactoryDeps;
66
use crate::factory::acp_assembler::{WorkspaceInfo, assemble_acp_params};
7+
use crate::factory::acp_launch_policy::{AcpLaunchPolicyInput, apply_acp_launch_policy};
78
use crate::factory::context::FactoryContext;
89
use crate::manager::acp::{AcpAgentManager, CatalogForwarder};
910
use crate::session_context::AcpSessionBuildContext;
@@ -51,19 +52,15 @@ pub(super) async fn build(
5152

5253
let mut command_spec =
5354
resolve_agent_command_spec(&meta, &ctx.workspace, &ctx.conversation_id, deps.broadcaster.clone()).await?;
54-
if meta.backend.as_deref() == Some("claude") {
55-
let cc_switch_env = crate::cc_switch::read_claude_provider_env();
56-
if !cc_switch_env.is_empty() {
57-
let keys: Vec<&str> = cc_switch_env.keys().map(|k| k.as_str()).collect();
58-
for (name, value) in &cc_switch_env {
59-
command_spec.env.push(aionui_common::EnvVar {
60-
name: name.clone(),
61-
value: value.clone(),
62-
});
63-
}
64-
tracing::info!(?keys, "cc-switch: env vars injected");
65-
}
66-
}
55+
apply_acp_launch_policy(
56+
&mut command_spec,
57+
AcpLaunchPolicyInput {
58+
metadata: &meta,
59+
config: &config,
60+
session_snapshot: build_context.session_snapshot.as_ref(),
61+
runtime_env: &ctx.runtime_env,
62+
},
63+
);
6764
let session_snapshot = build_context.session_snapshot;
6865

6966
// Load user-configured MCP servers from the DB so they reach
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use crate::cc_switch;
2+
use crate::manager::acp::mode_normalize::normalize_requested_mode;
3+
use crate::shared_kernel::PersistedSessionState;
4+
use aionui_api_types::{AcpBuildExtra, AgentMetadata};
5+
use aionui_common::CommandSpec;
6+
7+
const CODEX_CONFIG_FLAG: &str = "-c";
8+
const CODEX_ENV_POLICY_INHERIT_ALL: &str = "shell_environment_policy.inherit=all";
9+
const CODEX_ENV_POLICY_CLEAR_INCLUDE_ONLY: &str = "shell_environment_policy.include_only=[]";
10+
const CODEX_WINDOWS_UNELEVATED_SANDBOX: &str = "windows.sandbox=\"unelevated\"";
11+
12+
pub(super) struct AcpLaunchPolicyInput<'a> {
13+
pub metadata: &'a AgentMetadata,
14+
pub config: &'a AcpBuildExtra,
15+
pub session_snapshot: Option<&'a PersistedSessionState>,
16+
pub runtime_env: &'a [(String, String)],
17+
}
18+
19+
pub(super) fn apply_acp_launch_policy(command_spec: &mut CommandSpec, input: AcpLaunchPolicyInput<'_>) {
20+
apply_codex_runtime_config_args(
21+
command_spec,
22+
input.metadata,
23+
initial_mode_from_build_context(input.metadata, input.config, input.session_snapshot).as_deref(),
24+
);
25+
append_runtime_env(command_spec, input.runtime_env);
26+
append_claude_provider_env(command_spec, input.metadata);
27+
}
28+
29+
fn append_runtime_env(command_spec: &mut CommandSpec, runtime_env: &[(String, String)]) {
30+
for (name, value) in runtime_env {
31+
command_spec.env.push(aionui_common::EnvVar {
32+
name: name.clone(),
33+
value: value.clone(),
34+
});
35+
}
36+
}
37+
38+
fn append_claude_provider_env(command_spec: &mut CommandSpec, metadata: &AgentMetadata) {
39+
if metadata.backend.as_deref() != Some("claude") {
40+
return;
41+
}
42+
43+
let cc_switch_env = cc_switch::read_claude_provider_env();
44+
if cc_switch_env.is_empty() {
45+
return;
46+
}
47+
48+
let keys: Vec<&str> = cc_switch_env.keys().map(|key| key.as_str()).collect();
49+
for (name, value) in &cc_switch_env {
50+
command_spec.env.push(aionui_common::EnvVar {
51+
name: name.clone(),
52+
value: value.clone(),
53+
});
54+
}
55+
tracing::info!(?keys, "cc-switch: env vars injected");
56+
}
57+
58+
fn initial_mode_from_build_context(
59+
metadata: &AgentMetadata,
60+
config: &AcpBuildExtra,
61+
session_snapshot: Option<&PersistedSessionState>,
62+
) -> Option<String> {
63+
session_snapshot
64+
.and_then(|snapshot| snapshot.current_mode_id.as_ref())
65+
.map(|mode| normalize_requested_mode(metadata, mode.as_str()))
66+
.or_else(|| {
67+
config
68+
.session_mode
69+
.as_ref()
70+
.map(|mode| normalize_requested_mode(metadata, mode))
71+
})
72+
.filter(|mode| !mode.is_empty())
73+
}
74+
75+
fn apply_codex_runtime_config_args(
76+
command_spec: &mut CommandSpec,
77+
metadata: &AgentMetadata,
78+
initial_mode: Option<&str>,
79+
) {
80+
if metadata.backend.as_deref() != Some("codex") {
81+
return;
82+
}
83+
84+
push_codex_config_arg(command_spec, CODEX_ENV_POLICY_INHERIT_ALL);
85+
push_codex_config_arg(command_spec, CODEX_ENV_POLICY_CLEAR_INCLUDE_ONLY);
86+
87+
let sandbox_mode = codex_sandbox_mode_for_requested_mode(initial_mode);
88+
push_codex_config_arg(command_spec, &format!("sandbox_mode=\"{sandbox_mode}\""));
89+
if sandbox_mode == "danger-full-access" {
90+
push_codex_config_arg(command_spec, CODEX_WINDOWS_UNELEVATED_SANDBOX);
91+
}
92+
}
93+
94+
fn push_codex_config_arg(command_spec: &mut CommandSpec, value: &str) {
95+
command_spec.args.push(CODEX_CONFIG_FLAG.to_owned());
96+
command_spec.args.push(value.to_owned());
97+
}
98+
99+
fn codex_sandbox_mode_for_requested_mode(mode: Option<&str>) -> &'static str {
100+
match mode.map(str::trim) {
101+
Some("full-access" | "yoloNoSandbox") => "danger-full-access",
102+
_ => "workspace-write",
103+
}
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use super::*;
109+
110+
fn agent_metadata_with_backend(backend: Option<&str>) -> AgentMetadata {
111+
AgentMetadata {
112+
id: "agent-1".into(),
113+
icon: None,
114+
name: "Test ACP".into(),
115+
name_i18n: None,
116+
description: None,
117+
description_i18n: None,
118+
backend: backend.map(str::to_owned),
119+
agent_type: aionui_common::AgentType::Acp,
120+
agent_source: aionui_api_types::AgentSource::Builtin,
121+
agent_source_info: aionui_api_types::AgentSourceInfo::default(),
122+
enabled: true,
123+
available: true,
124+
command: None,
125+
resolved_command: None,
126+
args: vec![],
127+
env: vec![],
128+
native_skills_dirs: None,
129+
behavior_policy: aionui_api_types::BehaviorPolicy::default(),
130+
yolo_id: Some("full-access".into()),
131+
sort_order: 0,
132+
team_capable: false,
133+
last_check_status: None,
134+
last_check_kind: None,
135+
last_check_error_code: None,
136+
last_check_error_message: None,
137+
last_check_error_details: None,
138+
last_check_guidance: None,
139+
last_check_latency_ms: None,
140+
last_check_at: None,
141+
last_success_at: None,
142+
last_failure_at: None,
143+
handshake: aionui_api_types::AgentHandshake::default(),
144+
has_command_override: false,
145+
env_override_key_count: 0,
146+
}
147+
}
148+
149+
#[test]
150+
fn apply_acp_launch_policy_adds_runtime_env_and_codex_full_access_config() {
151+
let mut command_spec = CommandSpec {
152+
command: "node".into(),
153+
args: vec!["codex-acp.js".into()],
154+
env: vec![],
155+
cwd: None,
156+
};
157+
let metadata = agent_metadata_with_backend(Some("codex"));
158+
let config = AcpBuildExtra {
159+
session_mode: Some("full-access".into()),
160+
..Default::default()
161+
};
162+
163+
apply_acp_launch_policy(
164+
&mut command_spec,
165+
AcpLaunchPolicyInput {
166+
metadata: &metadata,
167+
config: &config,
168+
session_snapshot: None,
169+
runtime_env: &[("AIONUI_CONVERSATION_ID".into(), "conv-1".into())],
170+
},
171+
);
172+
173+
assert_eq!(
174+
command_spec.args,
175+
vec![
176+
"codex-acp.js",
177+
"-c",
178+
"shell_environment_policy.inherit=all",
179+
"-c",
180+
"shell_environment_policy.include_only=[]",
181+
"-c",
182+
"sandbox_mode=\"danger-full-access\"",
183+
"-c",
184+
"windows.sandbox=\"unelevated\"",
185+
]
186+
);
187+
assert!(
188+
command_spec
189+
.env
190+
.iter()
191+
.any(|entry| entry.name == "AIONUI_CONVERSATION_ID" && entry.value == "conv-1")
192+
);
193+
}
194+
195+
#[test]
196+
fn apply_acp_launch_policy_skips_codex_config_for_non_codex_agents() {
197+
let mut command_spec = CommandSpec {
198+
command: "node".into(),
199+
args: vec!["claude-agent-acp.js".into()],
200+
env: vec![],
201+
cwd: None,
202+
};
203+
let metadata = agent_metadata_with_backend(Some("claude"));
204+
let config = AcpBuildExtra::default();
205+
206+
apply_acp_launch_policy(
207+
&mut command_spec,
208+
AcpLaunchPolicyInput {
209+
metadata: &metadata,
210+
config: &config,
211+
session_snapshot: None,
212+
runtime_env: &[],
213+
},
214+
);
215+
216+
assert_eq!(command_spec.args, vec!["claude-agent-acp.js"]);
217+
}
218+
219+
#[test]
220+
fn initial_mode_from_build_context_prefers_persisted_snapshot() {
221+
let mut snapshot = PersistedSessionState::default();
222+
snapshot.current_mode_id = Some(crate::shared_kernel::ModeId::new("full-access"));
223+
let config = AcpBuildExtra {
224+
session_mode: Some("auto".into()),
225+
..Default::default()
226+
};
227+
228+
let mode =
229+
initial_mode_from_build_context(&agent_metadata_with_backend(Some("codex")), &config, Some(&snapshot));
230+
231+
assert_eq!(mode.as_deref(), Some("full-access"));
232+
}
233+
}

crates/aionui-ai-agent/src/factory/aionrs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ pub(super) async fn build(
167167
session_mode: overrides.session_mode,
168168
extra_mcp_servers,
169169
bedrock_config,
170+
runtime_env: ctx.runtime_env,
170171
};
171172

172173
if let Some(system_prompt) = config.system_prompt.as_deref()

crates/aionui-ai-agent/src/factory/context.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub(super) struct FactoryContext {
99
pub conversation_id: String,
1010
pub workspace: String,
1111
pub is_custom_workspace: bool,
12+
pub runtime_env: Vec<(String, String)>,
1213
}
1314

1415
impl FactoryContext {
@@ -17,6 +18,55 @@ impl FactoryContext {
1718
conversation_id: context.conversation.conversation_id.clone(),
1819
workspace: context.workspace.path.clone(),
1920
is_custom_workspace: context.workspace.is_custom,
21+
runtime_env: context.runtime_env.clone(),
2022
})
2123
}
2224
}
25+
26+
#[cfg(test)]
27+
mod tests {
28+
use super::*;
29+
use crate::session_context::{
30+
AcpSessionBuildContext, AgentSessionContext, AgentSessionKind, ConversationContext, WorkspaceContext,
31+
};
32+
use aionui_common::{AgentType, ProviderWithModel};
33+
34+
#[tokio::test]
35+
async fn resolve_preserves_runtime_env() {
36+
let context = AgentSessionContext {
37+
conversation: ConversationContext {
38+
conversation_id: "conv-1".into(),
39+
user_id: "user-1".into(),
40+
agent_type: AgentType::Acp,
41+
source: None,
42+
},
43+
workspace: WorkspaceContext {
44+
path: "/tmp/workspace".into(),
45+
stored_path: "/tmp/workspace".into(),
46+
is_custom: true,
47+
},
48+
model: ProviderWithModel {
49+
provider_id: "provider".into(),
50+
model: "model".into(),
51+
use_model: None,
52+
},
53+
skills: vec![],
54+
runtime_env: vec![("AIONUI_USER_ID".into(), "user-1".into())],
55+
team: None,
56+
kind: AgentSessionKind::Acp(Box::new(AcpSessionBuildContext {
57+
config: Default::default(),
58+
team: None,
59+
belongs_to_team: false,
60+
session_id: None,
61+
session_snapshot: None,
62+
})),
63+
};
64+
65+
let ctx = FactoryContext::resolve(&context).await.unwrap();
66+
67+
assert_eq!(
68+
ctx.runtime_env,
69+
vec![("AIONUI_USER_ID".to_owned(), "user-1".to_owned())]
70+
);
71+
}
72+
}

crates/aionui-ai-agent/src/factory/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod acp_assembler;
22

33
mod acp;
4+
mod acp_launch_policy;
45
pub(crate) mod aionrs;
56
mod context;
67

0 commit comments

Comments
 (0)