From 4e9b205b40d2a8200e8b9b07d9241e8261afa011 Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 13 Aug 2026 14:08:36 +0200 Subject: [PATCH] =?UTF-8?q?feat(infra):=20injection=20sandbox=20des=20hand?= =?UTF-8?q?les=20managed=20temp=20r=C3=A9solus=20dans=20les=20sessions=20a?= =?UTF-8?q?gents=20=E2=80=94=20#190?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/app-tauri/src/commands.rs | 1 + crates/application/src/agent/lifecycle.rs | 134 ++++++++++++- crates/application/src/agent/mod.rs | 10 +- crates/application/src/lib.rs | 18 +- .../application/src/orchestrator/service.rs | 9 +- crates/application/tests/agent_lifecycle.rs | 188 +++++++++++++++++- .../application/tests/structured_launch_d3.rs | 3 +- crates/backend/src/lib.rs | 4 + crates/web-server/src/lib.rs | 1 + 9 files changed, 335 insertions(+), 33 deletions(-) diff --git a/crates/app-tauri/src/commands.rs b/crates/app-tauri/src/commands.rs index 51b9f7cc..80b258ca 100644 --- a/crates/app-tauri/src/commands.rs +++ b/crates/app-tauri/src/commands.rs @@ -2077,6 +2077,7 @@ pub async fn launch_agent( allow_structured_alongside_pty: false, require_structured: request.cell_kind == Some(CellKind::Chat), fresh_start: request.fresh_start, + managed_temp_exposure: application::ManagedTempExposureRequest::default(), }) .await .map_err(ErrorDto::from)?; diff --git a/crates/application/src/agent/lifecycle.rs b/crates/application/src/agent/lifecycle.rs index 48ea6363..93ab3933 100644 --- a/crates/application/src/agent/lifecycle.rs +++ b/crates/application/src/agent/lifecycle.rs @@ -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, } /// OS/runtime facts injected by the composition root (`app-tauri`) to materialise @@ -1411,13 +1424,27 @@ fn resolve_git_dir(git_marker: &Path) -> Option { fn enrich_sandbox_plan_with_codex_git_roots( plan: Option, codex_git_writable_roots: &[String], +) -> Option { + enrich_sandbox_plan_with_rw_roots(plan, codex_git_writable_roots) +} + +fn enrich_sandbox_plan_with_managed_temp_roots( + plan: Option, + managed_temp_roots: &[String], +) -> Option { + enrich_sandbox_plan_with_rw_roots(plan, managed_temp_roots) +} + +fn enrich_sandbox_plan_with_rw_roots( + plan: Option, + writable_roots: &[String], ) -> Option { 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, roots: &[String]) { + for root in roots { + if !target.iter().any(|existing| existing == root) { + target.push(root.clone()); + } + } +} + +fn dedupe_paths(paths: impl IntoIterator) -> Vec { + 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>, + /// Resolves opaque managed-temp handles at launch time. A non-empty + /// [`ManagedTempExposureRequest`] fails closed when this dependency is absent. + managed_temp_resolver: Option>, /// 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) -> 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, 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(), diff --git a/crates/application/src/agent/mod.rs b/crates/application/src/agent/mod.rs index 77a478f6..9d011020 100644 --- a/crates/application/src/agent/mod.rs +++ b/crates/application/src/agent/mod.rs @@ -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, diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs index 6da1b719..66fec58d 100644 --- a/crates/application/src/lib.rs +++ b/crates/application/src/lib.rs @@ -59,15 +59,15 @@ pub use agent::{ ListClaudeModelsOutput, ListCodexModels, ListCodexModelsOutput, ListOpenCodeProviders, ListOpenCodeProvidersOutput, ListProfiles, ListProfilesOutput, ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ListedAgentCapabilities, - LiveStateLeanProvider, McpRuntime, OpenCodeProviderCatalogEntry, PermissionProjectorRegistry, - ProfileAvailability, ProfileModelCatalogEntry, ProviderSessionProvider, ReadAgentContext, - ReadAgentContextInput, ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput, - ResumableAgent, SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput, - SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput, - SessionLimitService, StructuredRoutingMode, StructuredSessionDescriptor, TurnOutcome, - UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput, - UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS, - LIVE_STATE_INJECT_MAX, RESUME_PROMPT, + LiveStateLeanProvider, ManagedTempExposureRequest, McpRuntime, OpenCodeProviderCatalogEntry, + PermissionProjectorRegistry, ProfileAvailability, ProfileModelCatalogEntry, + ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, + ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent, SaveOpenCodeProviderProfile, + SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfile, + SaveProfileInput, SaveProfileOutput, SessionLimitService, StructuredRoutingMode, + StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext, UpdateAgentContextInput, + UpdateAgentEffort, UpdateAgentEffortInput, UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET, + CODEX_SUBMIT_DELAY_MS, LIVE_STATE_INJECT_MAX, RESUME_PROMPT, }; pub use background::{ BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput, diff --git a/crates/application/src/orchestrator/service.rs b/crates/application/src/orchestrator/service.rs index 06357a52..986d93a9 100644 --- a/crates/application/src/orchestrator/service.rs +++ b/crates/application/src/orchestrator/service.rs @@ -40,9 +40,9 @@ use crate::memory::HarvestMemoryFromTurn; use crate::agent::{ drain_with_readiness_and_announcements_outcome, AnnouncementPublisher, CreateAgentFromScratch, - CreateAgentInput, LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, McpRuntime, - ReattachDecision, SessionLimitService, TurnOutcome, UpdateAgentContext, - UpdateAgentContextInput, CODEX_SUBMIT_DELAY_MS, + CreateAgentInput, LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, + ManagedTempExposureRequest, McpRuntime, ReattachDecision, SessionLimitService, TurnOutcome, + UpdateAgentContext, UpdateAgentContextInput, CODEX_SUBMIT_DELAY_MS, }; use crate::background::{SpawnBackgroundCommand, SpawnBackgroundCommandInput}; use crate::error::AppError; @@ -1734,6 +1734,7 @@ impl OrchestratorService { allow_structured_alongside_pty: false, require_structured: false, fresh_start: false, + managed_temp_exposure: ManagedTempExposureRequest::default(), }) .await?; @@ -2513,6 +2514,7 @@ impl OrchestratorService { allow_structured_alongside_pty: false, require_structured: false, fresh_start: false, + managed_temp_exposure: ManagedTempExposureRequest::default(), }) .await?; @@ -2595,6 +2597,7 @@ impl OrchestratorService { allow_structured_alongside_pty: true, require_structured: true, fresh_start: false, + managed_temp_exposure: ManagedTempExposureRequest::default(), }) .await?; diff --git a/crates/application/tests/agent_lifecycle.rs b/crates/application/tests/agent_lifecycle.rs index c56199c3..05eacd48 100644 --- a/crates/application/tests/agent_lifecycle.rs +++ b/crates/application/tests/agent_lifecycle.rs @@ -29,11 +29,12 @@ use domain::permission::{ ProjectionContext, ProjectorKey, }; use domain::ports::{ - AgentContextStore, AgentRuntime, AgentSession, AgentSessionError, AgentSessionFactory, + AgentContextStore, AgentRuntime, AgentSession, AgentSessionError, AgentSessionFactory, Clock, ContextInjectionPlan, DirEntry, EventBus, EventStream, ExitStatus, FileSystem, FsError, - IdGenerator, MemoryError, MemoryQuery, MemoryRecall, OutputStream, PermissionStore, - PreparedContext, ProfileStore, PtyError, PtyHandle, PtyPort, RemotePath, ReplyStream, - RuntimeError, SessionPlan, SkillStore, SpawnSpec, StoreError, SystemPermissionStore, + IdGenerator, LocalPath, ManagedTempStore, MemoryError, MemoryQuery, MemoryRecall, OutputStream, + PermissionStore, PreparedContext, ProfileStore, PtyError, PtyHandle, PtyPort, RemotePath, + ReplyStream, RuntimeError, SessionPlan, SkillStore, SpawnSpec, StoreError, + SystemPermissionStore, }; use domain::profile::{ AgentProfile, ContextInjection, EffortSelection, McpCapability, McpConfigStrategy, @@ -42,17 +43,22 @@ use domain::profile::{ use domain::project::{Project, ProjectPath}; use domain::remote::RemoteRef; use domain::skill::{Skill, SkillScope}; +use domain::{ + ManagedTempAllocation, ManagedTempError, ManagedTempGcReport, ManagedTempHandle, + ManagedTempKind, ManagedTempLease, ManagedTempState, +}; use domain::{MemoryIndexEntry, MemorySlug, MemoryType, NetworkPolicy}; use domain::{PermissionSet, ProjectPermissions, ProjectSystemPermissions, SystemPermissionSet}; use domain::{PtySize, SessionId, SkillId, SkillRef}; use uuid::Uuid; use application::{ - CloseTerminal, CreateAgentFromScratch, CreateAgentInput, DeleteAgent, DeleteAgentInput, - LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, LiveSessions, - PermissionProjectorRegistry, ReadAgentContext, ReadAgentContextInput, StructuredRoutingMode, - StructuredSessions, TerminalSessions, UpdateAgentContext, UpdateAgentContextInput, - UpdateAgentEffort, UpdateAgentEffortInput, + AppError, CloseTerminal, CreateAgentFromScratch, CreateAgentInput, DeleteAgent, + DeleteAgentInput, LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, LiveSessions, + ManagedTempExposureRequest, PermissionProjectorRegistry, ReadAgentContext, + ReadAgentContextInput, ResolveManagedTemp, StructuredRoutingMode, StructuredSessions, + TerminalSessions, UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort, + UpdateAgentEffortInput, }; // --------------------------------------------------------------------------- @@ -717,6 +723,68 @@ impl EventBus for SpyBus { } } +#[derive(Default)] +struct FakeManagedTempStore { + leases: Mutex>, +} + +impl FakeManagedTempStore { + fn insert(&self, lease: ManagedTempLease) { + self.leases + .lock() + .unwrap() + .insert(lease.handle.clone(), lease); + } +} + +#[async_trait] +impl ManagedTempStore for FakeManagedTempStore { + async fn allocate( + &self, + _allocation: ManagedTempAllocation, + ) -> Result { + Err(ManagedTempError::Store( + "allocate is not used by these tests".to_owned(), + )) + } + + async fn resolve( + &self, + handle: &ManagedTempHandle, + _now_ms: u64, + ) -> Result { + self.leases + .lock() + .unwrap() + .get(handle) + .filter(|lease| lease.state == ManagedTempState::Active) + .cloned() + .ok_or(ManagedTempError::NotFound) + } + + async fn release( + &self, + _handle: &ManagedTempHandle, + _now_ms: u64, + ) -> Result { + Err(ManagedTempError::Store( + "release is not used by these tests".to_owned(), + )) + } + + async fn gc(&self, _now_ms: u64) -> Result { + Ok(ManagedTempGcReport { deleted: 0 }) + } +} + +struct FixedClock; + +impl Clock for FixedClock { + fn now_millis(&self) -> i64 { + 1_000 + } +} + struct SeqIds(Mutex); impl SeqIds { fn new() -> Self { @@ -1220,6 +1288,7 @@ fn launch_input_with_project(agent_id: AgentId, project: Project) -> LaunchAgent allow_structured_alongside_pty: false, require_structured: false, fresh_start: false, + managed_temp_exposure: ManagedTempExposureRequest::default(), } } @@ -4242,6 +4311,107 @@ async fn codex_git_enrichment_canonicalizes_and_avoids_parent_grants() { let _ = std::fs::remove_dir_all(&base); } +#[tokio::test] +async fn managed_temp_exposure_enriches_codex_policy_and_os_sandbox_from_same_resolution() { + let root = fresh_temp_dir("managed-temp-exposure"); + let managed_root = root.join("tmp").join("agent-output"); + std::fs::create_dir_all(&managed_root).unwrap(); + let managed_root = managed_root.to_string_lossy().into_owned(); + let handle = ManagedTempHandle::new("managed-temp-handle-1").unwrap(); + let store = Arc::new(FakeManagedTempStore::default()); + store.insert(ManagedTempLease { + handle: handle.clone(), + kind: ManagedTempKind::Directory, + owner_project_id: None, + purpose: "test".to_owned(), + path: LocalPath::new(managed_root.clone()), + state: ManagedTempState::Active, + created_at_ms: 1, + last_access_at_ms: 1, + expires_at_ms: Some(10_000), + released_at_ms: None, + }); + + let profile = codex_profile() + .with_projector(ProjectorKey::Codex) + .with_structured_adapter(StructuredAdapter::Codex); + let (mut launch, agent, _fs, pty, _s) = launch_with_projection( + profile, + Some(ContextInjectionPlan::File { + target: "AGENTS.md".to_owned(), + }), + Some(full_registry()), + Some(perm_doc(Posture::Ask)), + ); + let resolver = Arc::new(ResolveManagedTemp::new( + Arc::clone(&store) as Arc, + Arc::new(FixedClock), + )); + let factory = FakeStructuredFactory::new(trace(), sid(891)); + let structured = Arc::new(StructuredSessions::new()); + launch = launch + .with_managed_temp_resolver(resolver) + .with_structured_routing_mode(StructuredRoutingMode::RequireStructured) + .with_structured(Arc::new(factory.clone()), Arc::clone(&structured)); + + let mut input = launch_input_with_project_root(agent.id, &root); + input.managed_temp_exposure = ManagedTempExposureRequest { + handles: vec![handle.as_str().to_owned(), handle.as_str().to_owned()], + }; + launch.execute(input).await.expect("structured launch"); + + assert!( + pty.spawns().is_empty(), + "structured route must not spawn PTY" + ); + assert_eq!( + factory.policies(), + vec![Some(domain::ports::StructuredProviderLaunchPolicy::Codex { + sandbox_mode: "workspace-write".to_owned(), + writable_roots: vec![managed_root.clone()], + network_access: false, + })], + "Codex writable_roots must come from the resolved managed-temp path" + ); + let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); + assert_eq!( + sandbox.allowed, + vec![domain::sandbox::PathGrant { + abs_root: managed_root, + access: domain::sandbox::PathAccess::RW, + }], + "OS sandbox grants must match the Codex writable roots" + ); + let _ = std::fs::remove_dir_all(&root); +} + +#[tokio::test] +async fn managed_temp_exposure_fails_closed_without_resolver_before_spawn() { + let profile = codex_profile().with_projector(ProjectorKey::Codex); + let (launch, agent, _fs, pty, _s) = launch_with_projection( + profile, + Some(ContextInjectionPlan::File { + target: "AGENTS.md".to_owned(), + }), + Some(full_registry()), + Some(perm_doc(Posture::Ask)), + ); + + let mut input = launch_input(agent.id); + input.managed_temp_exposure = ManagedTempExposureRequest { + handles: vec!["managed-temp-handle-1".to_owned()], + }; + let err = launch.execute(input).await.expect_err("must fail closed"); + + assert!( + matches!(err, AppError::Invalid(message) if message.contains("resolver is not configured")) + ); + assert!( + pty.spawns().is_empty(), + "managed-temp exposure failure must happen before spawning the agent" + ); +} + /// (1d) A non-projectable profile (no projector, no CLAUDE.md, no Codex signal) ⇒ /// no projection at all, even with a full registry and a posed policy. #[tokio::test] diff --git a/crates/application/tests/structured_launch_d3.rs b/crates/application/tests/structured_launch_d3.rs index ec239df5..b426af34 100644 --- a/crates/application/tests/structured_launch_d3.rs +++ b/crates/application/tests/structured_launch_d3.rs @@ -51,7 +51,7 @@ use uuid::Uuid; use application::{ ChangeAgentProfile, ChangeAgentProfileInput, LaunchAgent, LaunchAgentInput, - StructuredRoutingMode, StructuredSessions, TerminalSessions, + ManagedTempExposureRequest, StructuredRoutingMode, StructuredSessions, TerminalSessions, }; // --------------------------------------------------------------------------- @@ -773,6 +773,7 @@ fn launch_input(agent_id: AgentId) -> LaunchAgentInput { allow_structured_alongside_pty: false, require_structured: false, fresh_start: false, + managed_temp_exposure: ManagedTempExposureRequest::default(), } } diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index fd088878..32f9c9e1 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -759,6 +759,7 @@ impl WakeSessionProvider for AppWakeSessionProvider { allow_structured_alongside_pty: true, require_structured: false, fresh_start: false, + managed_temp_exposure: application::ManagedTempExposureRequest::default(), }) .await .map_err(|err| WakeError::Session(err.to_string()))?; @@ -880,6 +881,7 @@ impl AgentResumer for AppAgentResumer { allow_structured_alongside_pty: false, require_structured: false, fresh_start: false, + managed_temp_exposure: application::ManagedTempExposureRequest::default(), }) .await?; @@ -2240,6 +2242,7 @@ impl BackendCore { }) as Arc) .with_local_model_server(Arc::clone(&ensure_local_model_server)) .with_secret_store(Arc::clone(&secret_store_port)) + .with_managed_temp_resolver(Arc::clone(&resolve_managed_temp)) .with_structured( Arc::clone(&session_factory), Arc::clone(&structured_sessions), @@ -2278,6 +2281,7 @@ impl BackendCore { }) as Arc) .with_local_model_server(Arc::clone(&ensure_local_model_server)) .with_secret_store(Arc::clone(&secret_store_port)) + .with_managed_temp_resolver(Arc::clone(&resolve_managed_temp)) .with_structured( Arc::clone(&session_factory), Arc::clone(&structured_sessions), diff --git a/crates/web-server/src/lib.rs b/crates/web-server/src/lib.rs index ad2a7362..d72e1ef8 100644 --- a/crates/web-server/src/lib.rs +++ b/crates/web-server/src/lib.rs @@ -1518,6 +1518,7 @@ async fn execute_launch_agent_for_ws( allow_structured_alongside_pty: false, require_structured: request.cell_kind == Some(backend::dto::CellKind::Chat), fresh_start: request.fresh_start, + managed_temp_exposure: application::ManagedTempExposureRequest::default(), }) .await .map_err(ErrorDto::from)?;