feat(infra): injection sandbox des handles managed temp résolus dans les sessions agents — #190
This commit is contained in:
@ -17,9 +17,9 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, AgentSessionFactory, ContextInjectionPlan, EventBus,
|
||||
FileSystem, FsError, IdGenerator, MemoryQuery, MemoryRecall, PermissionStore, PreparedContext,
|
||||
ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan, SkillStore,
|
||||
SpawnSpec, StructuredProviderLaunchPolicy, SystemPermissionStore,
|
||||
FileSystem, FsError, IdGenerator, LocalPath, MemoryQuery, MemoryRecall, PermissionStore,
|
||||
PreparedContext, ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan,
|
||||
SkillStore, SpawnSpec, StructuredProviderLaunchPolicy, SystemPermissionStore,
|
||||
};
|
||||
use domain::profile::{
|
||||
resolve_effort, EffortSelection, McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter,
|
||||
@ -39,6 +39,7 @@ use domain::live_state::WorkStatus;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::layout::{persist_doc, resolve_doc};
|
||||
use crate::managed_temp::{ResolveManagedTemp, ResolveManagedTempInput};
|
||||
use crate::model_server::{
|
||||
EnsureLocalModelServer, EnsureLocalModelServerInput, ModelServerUseGuard,
|
||||
};
|
||||
@ -965,6 +966,7 @@ impl ChangeAgentProfile {
|
||||
allow_structured_alongside_pty: false,
|
||||
require_structured: false,
|
||||
fresh_start: false,
|
||||
managed_temp_exposure: ManagedTempExposureRequest::default(),
|
||||
})
|
||||
.await?;
|
||||
Ok(Some(output.session))
|
||||
@ -1148,6 +1150,17 @@ pub struct LaunchAgentInput {
|
||||
/// Cold restart request: ignore the cell's previous conversation id and create
|
||||
/// a new provider/CLI conversation for this activation.
|
||||
pub fresh_start: bool,
|
||||
/// Launch-scoped request to expose IdeA-managed temporary handles inside the
|
||||
/// spawned agent sandbox. The handles remain opaque at the application boundary;
|
||||
/// paths are resolved only by IdeA during this launch and are never returned.
|
||||
pub managed_temp_exposure: ManagedTempExposureRequest,
|
||||
}
|
||||
|
||||
/// Explicit launch-scoped managed-temp exposure request.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct ManagedTempExposureRequest {
|
||||
/// Opaque managed temporary handles to resolve and expose for this launch.
|
||||
pub handles: Vec<String>,
|
||||
}
|
||||
|
||||
/// OS/runtime facts injected by the composition root (`app-tauri`) to materialise
|
||||
@ -1411,13 +1424,27 @@ fn resolve_git_dir(git_marker: &Path) -> Option<PathBuf> {
|
||||
fn enrich_sandbox_plan_with_codex_git_roots(
|
||||
plan: Option<SandboxPlan>,
|
||||
codex_git_writable_roots: &[String],
|
||||
) -> Option<SandboxPlan> {
|
||||
enrich_sandbox_plan_with_rw_roots(plan, codex_git_writable_roots)
|
||||
}
|
||||
|
||||
fn enrich_sandbox_plan_with_managed_temp_roots(
|
||||
plan: Option<SandboxPlan>,
|
||||
managed_temp_roots: &[String],
|
||||
) -> Option<SandboxPlan> {
|
||||
enrich_sandbox_plan_with_rw_roots(plan, managed_temp_roots)
|
||||
}
|
||||
|
||||
fn enrich_sandbox_plan_with_rw_roots(
|
||||
plan: Option<SandboxPlan>,
|
||||
writable_roots: &[String],
|
||||
) -> Option<SandboxPlan> {
|
||||
let mut plan = plan?;
|
||||
if codex_git_writable_roots.is_empty() {
|
||||
if writable_roots.is_empty() {
|
||||
return Some(plan);
|
||||
}
|
||||
|
||||
for root in codex_git_writable_roots {
|
||||
for root in writable_roots {
|
||||
match plan
|
||||
.allowed
|
||||
.iter_mut()
|
||||
@ -1434,6 +1461,26 @@ fn enrich_sandbox_plan_with_codex_git_roots(
|
||||
Some(plan)
|
||||
}
|
||||
|
||||
fn append_unique_paths(target: &mut Vec<String>, roots: &[String]) {
|
||||
for root in roots {
|
||||
if !target.iter().any(|existing| existing == root) {
|
||||
target.push(root.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dedupe_paths(paths: impl IntoIterator<Item = LocalPath>) -> Vec<String> {
|
||||
let mut roots = Vec::new();
|
||||
for path in paths {
|
||||
let root = path.as_str().to_owned();
|
||||
if !roots.iter().any(|existing| existing == &root) {
|
||||
roots.push(root);
|
||||
}
|
||||
}
|
||||
roots.sort();
|
||||
roots
|
||||
}
|
||||
|
||||
fn projection_model(profile: &AgentProfile) -> Option<&str> {
|
||||
profile.model.as_deref()
|
||||
}
|
||||
@ -1477,6 +1524,31 @@ fn append_codex_pty_model_overrides(profile: &AgentProfile, spec: &mut SpawnSpec
|
||||
}
|
||||
}
|
||||
|
||||
fn append_codex_pty_managed_temp_roots(
|
||||
profile: &AgentProfile,
|
||||
permissions: Option<&EffectivePermissions>,
|
||||
managed_temp_roots: &[String],
|
||||
spec: &mut SpawnSpec,
|
||||
) {
|
||||
if managed_temp_roots.is_empty() || !is_codex_profile(profile) {
|
||||
return;
|
||||
}
|
||||
if codex_structured_sandbox_mode(permissions) != "workspace-write" {
|
||||
return;
|
||||
}
|
||||
for root in managed_temp_roots {
|
||||
if spec
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|window| window == ["--add-dir", root.as_str()])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
spec.args.push("--add-dir".to_owned());
|
||||
spec.args.push(root.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn toml_string_literal(value: &str) -> String {
|
||||
serde_json::to_string(value).expect("string serialization cannot fail")
|
||||
}
|
||||
@ -1559,6 +1631,9 @@ pub struct LaunchAgent {
|
||||
/// missing store or a missing/undecryptable secret **fails the launch**
|
||||
/// (never spawns with a dead/absent key).
|
||||
secret_store: Option<Arc<dyn SecretStore>>,
|
||||
/// Resolves opaque managed-temp handles at launch time. A non-empty
|
||||
/// [`ManagedTempExposureRequest`] fails closed when this dependency is absent.
|
||||
managed_temp_resolver: Option<Arc<ResolveManagedTemp>>,
|
||||
/// Explicit intent for structured profiles when structured ports are absent.
|
||||
structured_routing_mode: StructuredRoutingMode,
|
||||
}
|
||||
@ -1607,6 +1682,7 @@ impl LaunchAgent {
|
||||
live_state_lean: None,
|
||||
local_model_server: None,
|
||||
secret_store: None,
|
||||
managed_temp_resolver: None,
|
||||
structured_routing_mode: StructuredRoutingMode::HumanPtyFallback,
|
||||
}
|
||||
}
|
||||
@ -1636,6 +1712,14 @@ impl LaunchAgent {
|
||||
self
|
||||
}
|
||||
|
||||
/// Injects the managed-temp resolver used to turn opaque launch-scoped handles
|
||||
/// into IdeA-owned local paths just before sandbox planning.
|
||||
#[must_use]
|
||||
pub fn with_managed_temp_resolver(mut self, resolver: Arc<ResolveManagedTemp>) -> Self {
|
||||
self.managed_temp_resolver = Some(resolver);
|
||||
self
|
||||
}
|
||||
|
||||
/// Branche le provider de **live-state lean (lot LS4)** : au lancement, l'aperçu
|
||||
/// `# État du projet` (status + intent des autres agents) est injecté dans le
|
||||
/// convention file. Sans cet appel (cas legacy / tests existants), aucune section
|
||||
@ -1795,6 +1879,31 @@ impl LaunchAgent {
|
||||
self.recall.recall(root, &query).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn resolve_managed_temp_exposure(
|
||||
&self,
|
||||
request: &ManagedTempExposureRequest,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
if request.handles.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let Some(resolver) = self.managed_temp_resolver.as_ref() else {
|
||||
return Err(AppError::Invalid(
|
||||
"managed temp exposure requested but resolver is not configured".to_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
let mut paths = Vec::with_capacity(request.handles.len());
|
||||
for handle in &request.handles {
|
||||
let output = resolver
|
||||
.execute(ResolveManagedTempInput {
|
||||
handle: handle.clone(),
|
||||
})
|
||||
.await?;
|
||||
paths.push(output.lease.path);
|
||||
}
|
||||
Ok(dedupe_paths(paths))
|
||||
}
|
||||
|
||||
/// Résout **best-effort** le handoff de reprise de la cellule (lot P7), calqué sur
|
||||
/// [`Self::resolve_memory`]. L'injection n'a lieu que si :
|
||||
/// - la cellule porte une `conversation_id` ([`LaunchAgentInput::conversation_id`]),
|
||||
@ -2235,6 +2344,11 @@ impl LaunchAgent {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let managed_temp_writable_roots = self
|
||||
.resolve_managed_temp_exposure(&input.managed_temp_exposure)
|
||||
.await?;
|
||||
let mut codex_writable_roots = codex_git_writable_roots.clone();
|
||||
append_unique_paths(&mut codex_writable_roots, &managed_temp_writable_roots);
|
||||
spec.sandbox = compile_sandbox_plan(
|
||||
effective_permissions.as_ref(),
|
||||
&SandboxContext {
|
||||
@ -2244,11 +2358,19 @@ impl LaunchAgent {
|
||||
);
|
||||
spec.sandbox =
|
||||
enrich_sandbox_plan_with_codex_git_roots(spec.sandbox, &codex_git_writable_roots);
|
||||
spec.sandbox =
|
||||
enrich_sandbox_plan_with_managed_temp_roots(spec.sandbox, &managed_temp_writable_roots);
|
||||
append_codex_pty_managed_temp_roots(
|
||||
&profile,
|
||||
effective_permissions.as_ref(),
|
||||
&managed_temp_writable_roots,
|
||||
&mut spec,
|
||||
);
|
||||
let structured_policy = build_structured_launch_policy(
|
||||
&profile,
|
||||
effective_permissions.as_ref(),
|
||||
network_permission,
|
||||
&codex_git_writable_roots,
|
||||
&codex_writable_roots,
|
||||
);
|
||||
let resolved_effort = resolve_effort(
|
||||
profile.model_reasoning_effort.as_deref(),
|
||||
|
||||
@ -35,11 +35,11 @@ pub use lifecycle::{
|
||||
ChangeAgentProfileOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput,
|
||||
DeleteAgent, DeleteAgentInput, HandoffProvider, InjectedLiveRow, LaunchAgent, LaunchAgentInput,
|
||||
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, ListedAgentCapabilities,
|
||||
LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry, ProviderSessionProvider,
|
||||
ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, StructuredRoutingMode,
|
||||
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort,
|
||||
UpdateAgentEffortInput, UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET,
|
||||
DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||
LiveStateLeanProvider, ManagedTempExposureRequest, McpRuntime, PermissionProjectorRegistry,
|
||||
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||
StructuredRoutingMode, StructuredSessionDescriptor, UpdateAgentContext,
|
||||
UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput, UpdateAgentEffortOutput,
|
||||
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||
};
|
||||
pub use model_catalogue::{
|
||||
claude_model_catalogue, codex_model_catalogue, ListClaudeModels, ListClaudeModelsOutput,
|
||||
|
||||
Reference in New Issue
Block a user