merge(infra): injection sandbox handles managed temp — #190 (QA verte)
This commit is contained in:
@ -2077,6 +2077,7 @@ pub async fn launch_agent(
|
|||||||
allow_structured_alongside_pty: false,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: request.cell_kind == Some(CellKind::Chat),
|
require_structured: request.cell_kind == Some(CellKind::Chat),
|
||||||
fresh_start: request.fresh_start,
|
fresh_start: request.fresh_start,
|
||||||
|
managed_temp_exposure: application::ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(ErrorDto::from)?;
|
.map_err(ErrorDto::from)?;
|
||||||
|
|||||||
@ -17,9 +17,9 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
AgentContextStore, AgentRuntime, AgentSessionFactory, ContextInjectionPlan, EventBus,
|
AgentContextStore, AgentRuntime, AgentSessionFactory, ContextInjectionPlan, EventBus,
|
||||||
FileSystem, FsError, IdGenerator, MemoryQuery, MemoryRecall, PermissionStore, PreparedContext,
|
FileSystem, FsError, IdGenerator, LocalPath, MemoryQuery, MemoryRecall, PermissionStore,
|
||||||
ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan, SkillStore,
|
PreparedContext, ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan,
|
||||||
SpawnSpec, StructuredProviderLaunchPolicy, SystemPermissionStore,
|
SkillStore, SpawnSpec, StructuredProviderLaunchPolicy, SystemPermissionStore,
|
||||||
};
|
};
|
||||||
use domain::profile::{
|
use domain::profile::{
|
||||||
resolve_effort, EffortSelection, McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter,
|
resolve_effort, EffortSelection, McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter,
|
||||||
@ -39,6 +39,7 @@ use domain::live_state::WorkStatus;
|
|||||||
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::layout::{persist_doc, resolve_doc};
|
use crate::layout::{persist_doc, resolve_doc};
|
||||||
|
use crate::managed_temp::{ResolveManagedTemp, ResolveManagedTempInput};
|
||||||
use crate::model_server::{
|
use crate::model_server::{
|
||||||
EnsureLocalModelServer, EnsureLocalModelServerInput, ModelServerUseGuard,
|
EnsureLocalModelServer, EnsureLocalModelServerInput, ModelServerUseGuard,
|
||||||
};
|
};
|
||||||
@ -965,6 +966,7 @@ impl ChangeAgentProfile {
|
|||||||
allow_structured_alongside_pty: false,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: false,
|
require_structured: false,
|
||||||
fresh_start: false,
|
fresh_start: false,
|
||||||
|
managed_temp_exposure: ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Some(output.session))
|
Ok(Some(output.session))
|
||||||
@ -1148,6 +1150,17 @@ pub struct LaunchAgentInput {
|
|||||||
/// Cold restart request: ignore the cell's previous conversation id and create
|
/// Cold restart request: ignore the cell's previous conversation id and create
|
||||||
/// a new provider/CLI conversation for this activation.
|
/// a new provider/CLI conversation for this activation.
|
||||||
pub fresh_start: bool,
|
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
|
/// 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(
|
fn enrich_sandbox_plan_with_codex_git_roots(
|
||||||
plan: Option<SandboxPlan>,
|
plan: Option<SandboxPlan>,
|
||||||
codex_git_writable_roots: &[String],
|
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> {
|
) -> Option<SandboxPlan> {
|
||||||
let mut plan = plan?;
|
let mut plan = plan?;
|
||||||
if codex_git_writable_roots.is_empty() {
|
if writable_roots.is_empty() {
|
||||||
return Some(plan);
|
return Some(plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
for root in codex_git_writable_roots {
|
for root in writable_roots {
|
||||||
match plan
|
match plan
|
||||||
.allowed
|
.allowed
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
@ -1434,6 +1461,26 @@ fn enrich_sandbox_plan_with_codex_git_roots(
|
|||||||
Some(plan)
|
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> {
|
fn projection_model(profile: &AgentProfile) -> Option<&str> {
|
||||||
profile.model.as_deref()
|
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 {
|
fn toml_string_literal(value: &str) -> String {
|
||||||
serde_json::to_string(value).expect("string serialization cannot fail")
|
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**
|
/// missing store or a missing/undecryptable secret **fails the launch**
|
||||||
/// (never spawns with a dead/absent key).
|
/// (never spawns with a dead/absent key).
|
||||||
secret_store: Option<Arc<dyn SecretStore>>,
|
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.
|
/// Explicit intent for structured profiles when structured ports are absent.
|
||||||
structured_routing_mode: StructuredRoutingMode,
|
structured_routing_mode: StructuredRoutingMode,
|
||||||
}
|
}
|
||||||
@ -1607,6 +1682,7 @@ impl LaunchAgent {
|
|||||||
live_state_lean: None,
|
live_state_lean: None,
|
||||||
local_model_server: None,
|
local_model_server: None,
|
||||||
secret_store: None,
|
secret_store: None,
|
||||||
|
managed_temp_resolver: None,
|
||||||
structured_routing_mode: StructuredRoutingMode::HumanPtyFallback,
|
structured_routing_mode: StructuredRoutingMode::HumanPtyFallback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1636,6 +1712,14 @@ impl LaunchAgent {
|
|||||||
self
|
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
|
/// 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
|
/// `# État du projet` (status + intent des autres agents) est injecté dans le
|
||||||
/// convention file. Sans cet appel (cas legacy / tests existants), aucune section
|
/// 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()
|
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
|
/// 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 :
|
/// [`Self::resolve_memory`]. L'injection n'a lieu que si :
|
||||||
/// - la cellule porte une `conversation_id` ([`LaunchAgentInput::conversation_id`]),
|
/// - la cellule porte une `conversation_id` ([`LaunchAgentInput::conversation_id`]),
|
||||||
@ -2235,6 +2344,11 @@ impl LaunchAgent {
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
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(
|
spec.sandbox = compile_sandbox_plan(
|
||||||
effective_permissions.as_ref(),
|
effective_permissions.as_ref(),
|
||||||
&SandboxContext {
|
&SandboxContext {
|
||||||
@ -2244,11 +2358,19 @@ impl LaunchAgent {
|
|||||||
);
|
);
|
||||||
spec.sandbox =
|
spec.sandbox =
|
||||||
enrich_sandbox_plan_with_codex_git_roots(spec.sandbox, &codex_git_writable_roots);
|
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(
|
let structured_policy = build_structured_launch_policy(
|
||||||
&profile,
|
&profile,
|
||||||
effective_permissions.as_ref(),
|
effective_permissions.as_ref(),
|
||||||
network_permission,
|
network_permission,
|
||||||
&codex_git_writable_roots,
|
&codex_writable_roots,
|
||||||
);
|
);
|
||||||
let resolved_effort = resolve_effort(
|
let resolved_effort = resolve_effort(
|
||||||
profile.model_reasoning_effort.as_deref(),
|
profile.model_reasoning_effort.as_deref(),
|
||||||
|
|||||||
@ -35,11 +35,11 @@ pub use lifecycle::{
|
|||||||
ChangeAgentProfileOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput,
|
ChangeAgentProfileOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput,
|
||||||
DeleteAgent, DeleteAgentInput, HandoffProvider, InjectedLiveRow, LaunchAgent, LaunchAgentInput,
|
DeleteAgent, DeleteAgentInput, HandoffProvider, InjectedLiveRow, LaunchAgent, LaunchAgentInput,
|
||||||
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, ListedAgentCapabilities,
|
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, ListedAgentCapabilities,
|
||||||
LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry, ProviderSessionProvider,
|
LiveStateLeanProvider, ManagedTempExposureRequest, McpRuntime, PermissionProjectorRegistry,
|
||||||
ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, StructuredRoutingMode,
|
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||||
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort,
|
StructuredRoutingMode, StructuredSessionDescriptor, UpdateAgentContext,
|
||||||
UpdateAgentEffortInput, UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET,
|
UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput, UpdateAgentEffortOutput,
|
||||||
DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||||
};
|
};
|
||||||
pub use model_catalogue::{
|
pub use model_catalogue::{
|
||||||
claude_model_catalogue, codex_model_catalogue, ListClaudeModels, ListClaudeModelsOutput,
|
claude_model_catalogue, codex_model_catalogue, ListClaudeModels, ListClaudeModelsOutput,
|
||||||
|
|||||||
@ -59,15 +59,15 @@ pub use agent::{
|
|||||||
ListClaudeModelsOutput, ListCodexModels, ListCodexModelsOutput, ListOpenCodeProviders,
|
ListClaudeModelsOutput, ListCodexModels, ListCodexModelsOutput, ListOpenCodeProviders,
|
||||||
ListOpenCodeProvidersOutput, ListProfiles, ListProfilesOutput, ListResumableAgents,
|
ListOpenCodeProvidersOutput, ListProfiles, ListProfilesOutput, ListResumableAgents,
|
||||||
ListResumableAgentsInput, ListResumableAgentsOutput, ListedAgentCapabilities,
|
ListResumableAgentsInput, ListResumableAgentsOutput, ListedAgentCapabilities,
|
||||||
LiveStateLeanProvider, McpRuntime, OpenCodeProviderCatalogEntry, PermissionProjectorRegistry,
|
LiveStateLeanProvider, ManagedTempExposureRequest, McpRuntime, OpenCodeProviderCatalogEntry,
|
||||||
ProfileAvailability, ProfileModelCatalogEntry, ProviderSessionProvider, ReadAgentContext,
|
PermissionProjectorRegistry, ProfileAvailability, ProfileModelCatalogEntry,
|
||||||
ReadAgentContextInput, ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput,
|
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||||
ResumableAgent, SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent, SaveOpenCodeProviderProfile,
|
||||||
SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput,
|
SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfile,
|
||||||
SessionLimitService, StructuredRoutingMode, StructuredSessionDescriptor, TurnOutcome,
|
SaveProfileInput, SaveProfileOutput, SessionLimitService, StructuredRoutingMode,
|
||||||
UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput,
|
StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext, UpdateAgentContextInput,
|
||||||
UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS,
|
UpdateAgentEffort, UpdateAgentEffortInput, UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET,
|
||||||
LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
CODEX_SUBMIT_DELAY_MS, LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
||||||
};
|
};
|
||||||
pub use background::{
|
pub use background::{
|
||||||
BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput,
|
BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput,
|
||||||
|
|||||||
@ -40,9 +40,9 @@ use crate::memory::HarvestMemoryFromTurn;
|
|||||||
|
|
||||||
use crate::agent::{
|
use crate::agent::{
|
||||||
drain_with_readiness_and_announcements_outcome, AnnouncementPublisher, CreateAgentFromScratch,
|
drain_with_readiness_and_announcements_outcome, AnnouncementPublisher, CreateAgentFromScratch,
|
||||||
CreateAgentInput, LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, McpRuntime,
|
CreateAgentInput, LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput,
|
||||||
ReattachDecision, SessionLimitService, TurnOutcome, UpdateAgentContext,
|
ManagedTempExposureRequest, McpRuntime, ReattachDecision, SessionLimitService, TurnOutcome,
|
||||||
UpdateAgentContextInput, CODEX_SUBMIT_DELAY_MS,
|
UpdateAgentContext, UpdateAgentContextInput, CODEX_SUBMIT_DELAY_MS,
|
||||||
};
|
};
|
||||||
use crate::background::{SpawnBackgroundCommand, SpawnBackgroundCommandInput};
|
use crate::background::{SpawnBackgroundCommand, SpawnBackgroundCommandInput};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
@ -1734,6 +1734,7 @@ impl OrchestratorService {
|
|||||||
allow_structured_alongside_pty: false,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: false,
|
require_structured: false,
|
||||||
fresh_start: false,
|
fresh_start: false,
|
||||||
|
managed_temp_exposure: ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -2513,6 +2514,7 @@ impl OrchestratorService {
|
|||||||
allow_structured_alongside_pty: false,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: false,
|
require_structured: false,
|
||||||
fresh_start: false,
|
fresh_start: false,
|
||||||
|
managed_temp_exposure: ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -2595,6 +2597,7 @@ impl OrchestratorService {
|
|||||||
allow_structured_alongside_pty: true,
|
allow_structured_alongside_pty: true,
|
||||||
require_structured: true,
|
require_structured: true,
|
||||||
fresh_start: false,
|
fresh_start: false,
|
||||||
|
managed_temp_exposure: ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@ -29,11 +29,12 @@ use domain::permission::{
|
|||||||
ProjectionContext, ProjectorKey,
|
ProjectionContext, ProjectorKey,
|
||||||
};
|
};
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
AgentContextStore, AgentRuntime, AgentSession, AgentSessionError, AgentSessionFactory,
|
AgentContextStore, AgentRuntime, AgentSession, AgentSessionError, AgentSessionFactory, Clock,
|
||||||
ContextInjectionPlan, DirEntry, EventBus, EventStream, ExitStatus, FileSystem, FsError,
|
ContextInjectionPlan, DirEntry, EventBus, EventStream, ExitStatus, FileSystem, FsError,
|
||||||
IdGenerator, MemoryError, MemoryQuery, MemoryRecall, OutputStream, PermissionStore,
|
IdGenerator, LocalPath, ManagedTempStore, MemoryError, MemoryQuery, MemoryRecall, OutputStream,
|
||||||
PreparedContext, ProfileStore, PtyError, PtyHandle, PtyPort, RemotePath, ReplyStream,
|
PermissionStore, PreparedContext, ProfileStore, PtyError, PtyHandle, PtyPort, RemotePath,
|
||||||
RuntimeError, SessionPlan, SkillStore, SpawnSpec, StoreError, SystemPermissionStore,
|
ReplyStream, RuntimeError, SessionPlan, SkillStore, SpawnSpec, StoreError,
|
||||||
|
SystemPermissionStore,
|
||||||
};
|
};
|
||||||
use domain::profile::{
|
use domain::profile::{
|
||||||
AgentProfile, ContextInjection, EffortSelection, McpCapability, McpConfigStrategy,
|
AgentProfile, ContextInjection, EffortSelection, McpCapability, McpConfigStrategy,
|
||||||
@ -42,17 +43,22 @@ use domain::profile::{
|
|||||||
use domain::project::{Project, ProjectPath};
|
use domain::project::{Project, ProjectPath};
|
||||||
use domain::remote::RemoteRef;
|
use domain::remote::RemoteRef;
|
||||||
use domain::skill::{Skill, SkillScope};
|
use domain::skill::{Skill, SkillScope};
|
||||||
|
use domain::{
|
||||||
|
ManagedTempAllocation, ManagedTempError, ManagedTempGcReport, ManagedTempHandle,
|
||||||
|
ManagedTempKind, ManagedTempLease, ManagedTempState,
|
||||||
|
};
|
||||||
use domain::{MemoryIndexEntry, MemorySlug, MemoryType, NetworkPolicy};
|
use domain::{MemoryIndexEntry, MemorySlug, MemoryType, NetworkPolicy};
|
||||||
use domain::{PermissionSet, ProjectPermissions, ProjectSystemPermissions, SystemPermissionSet};
|
use domain::{PermissionSet, ProjectPermissions, ProjectSystemPermissions, SystemPermissionSet};
|
||||||
use domain::{PtySize, SessionId, SkillId, SkillRef};
|
use domain::{PtySize, SessionId, SkillId, SkillRef};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
CloseTerminal, CreateAgentFromScratch, CreateAgentInput, DeleteAgent, DeleteAgentInput,
|
AppError, CloseTerminal, CreateAgentFromScratch, CreateAgentInput, DeleteAgent,
|
||||||
LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, LiveSessions,
|
DeleteAgentInput, LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, LiveSessions,
|
||||||
PermissionProjectorRegistry, ReadAgentContext, ReadAgentContextInput, StructuredRoutingMode,
|
ManagedTempExposureRequest, PermissionProjectorRegistry, ReadAgentContext,
|
||||||
StructuredSessions, TerminalSessions, UpdateAgentContext, UpdateAgentContextInput,
|
ReadAgentContextInput, ResolveManagedTemp, StructuredRoutingMode, StructuredSessions,
|
||||||
UpdateAgentEffort, UpdateAgentEffortInput,
|
TerminalSessions, UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort,
|
||||||
|
UpdateAgentEffortInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -717,6 +723,68 @@ impl EventBus for SpyBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FakeManagedTempStore {
|
||||||
|
leases: Mutex<HashMap<ManagedTempHandle, ManagedTempLease>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ManagedTempLease, ManagedTempError> {
|
||||||
|
Err(ManagedTempError::Store(
|
||||||
|
"allocate is not used by these tests".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve(
|
||||||
|
&self,
|
||||||
|
handle: &ManagedTempHandle,
|
||||||
|
_now_ms: u64,
|
||||||
|
) -> Result<ManagedTempLease, ManagedTempError> {
|
||||||
|
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<ManagedTempLease, ManagedTempError> {
|
||||||
|
Err(ManagedTempError::Store(
|
||||||
|
"release is not used by these tests".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn gc(&self, _now_ms: u64) -> Result<ManagedTempGcReport, ManagedTempError> {
|
||||||
|
Ok(ManagedTempGcReport { deleted: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FixedClock;
|
||||||
|
|
||||||
|
impl Clock for FixedClock {
|
||||||
|
fn now_millis(&self) -> i64 {
|
||||||
|
1_000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SeqIds(Mutex<u128>);
|
struct SeqIds(Mutex<u128>);
|
||||||
impl SeqIds {
|
impl SeqIds {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
@ -1220,6 +1288,7 @@ fn launch_input_with_project(agent_id: AgentId, project: Project) -> LaunchAgent
|
|||||||
allow_structured_alongside_pty: false,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: false,
|
require_structured: false,
|
||||||
fresh_start: 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);
|
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<dyn ManagedTempStore>,
|
||||||
|
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) ⇒
|
/// (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.
|
/// no projection at all, even with a full registry and a posed policy.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@ -51,7 +51,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
ChangeAgentProfile, ChangeAgentProfileInput, LaunchAgent, LaunchAgentInput,
|
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,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: false,
|
require_structured: false,
|
||||||
fresh_start: false,
|
fresh_start: false,
|
||||||
|
managed_temp_exposure: ManagedTempExposureRequest::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -759,6 +759,7 @@ impl WakeSessionProvider for AppWakeSessionProvider {
|
|||||||
allow_structured_alongside_pty: true,
|
allow_structured_alongside_pty: true,
|
||||||
require_structured: false,
|
require_structured: false,
|
||||||
fresh_start: false,
|
fresh_start: false,
|
||||||
|
managed_temp_exposure: application::ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| WakeError::Session(err.to_string()))?;
|
.map_err(|err| WakeError::Session(err.to_string()))?;
|
||||||
@ -880,6 +881,7 @@ impl AgentResumer for AppAgentResumer {
|
|||||||
allow_structured_alongside_pty: false,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: false,
|
require_structured: false,
|
||||||
fresh_start: false,
|
fresh_start: false,
|
||||||
|
managed_temp_exposure: application::ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -2240,6 +2242,7 @@ impl BackendCore {
|
|||||||
}) as Arc<dyn LiveStateLeanProvider>)
|
}) as Arc<dyn LiveStateLeanProvider>)
|
||||||
.with_local_model_server(Arc::clone(&ensure_local_model_server))
|
.with_local_model_server(Arc::clone(&ensure_local_model_server))
|
||||||
.with_secret_store(Arc::clone(&secret_store_port))
|
.with_secret_store(Arc::clone(&secret_store_port))
|
||||||
|
.with_managed_temp_resolver(Arc::clone(&resolve_managed_temp))
|
||||||
.with_structured(
|
.with_structured(
|
||||||
Arc::clone(&session_factory),
|
Arc::clone(&session_factory),
|
||||||
Arc::clone(&structured_sessions),
|
Arc::clone(&structured_sessions),
|
||||||
@ -2278,6 +2281,7 @@ impl BackendCore {
|
|||||||
}) as Arc<dyn LiveStateLeanProvider>)
|
}) as Arc<dyn LiveStateLeanProvider>)
|
||||||
.with_local_model_server(Arc::clone(&ensure_local_model_server))
|
.with_local_model_server(Arc::clone(&ensure_local_model_server))
|
||||||
.with_secret_store(Arc::clone(&secret_store_port))
|
.with_secret_store(Arc::clone(&secret_store_port))
|
||||||
|
.with_managed_temp_resolver(Arc::clone(&resolve_managed_temp))
|
||||||
.with_structured(
|
.with_structured(
|
||||||
Arc::clone(&session_factory),
|
Arc::clone(&session_factory),
|
||||||
Arc::clone(&structured_sessions),
|
Arc::clone(&structured_sessions),
|
||||||
|
|||||||
@ -1518,6 +1518,7 @@ async fn execute_launch_agent_for_ws(
|
|||||||
allow_structured_alongside_pty: false,
|
allow_structured_alongside_pty: false,
|
||||||
require_structured: request.cell_kind == Some(backend::dto::CellKind::Chat),
|
require_structured: request.cell_kind == Some(backend::dto::CellKind::Chat),
|
||||||
fresh_start: request.fresh_start,
|
fresh_start: request.fresh_start,
|
||||||
|
managed_temp_exposure: application::ManagedTempExposureRequest::default(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(ErrorDto::from)?;
|
.map_err(ErrorDto::from)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user