feat(infra): temp IdeA généralisé à tous les agents + purge du store managed-tmp au shutdown d'IdeA — #200 (QA verte)
This commit is contained in:
@ -134,9 +134,9 @@ pub use layout::{
|
||||
};
|
||||
pub use managed_temp::{
|
||||
AllocateManagedTemp, AllocateManagedTempInput, AllocateManagedTempOutput, GcManagedTemp,
|
||||
GcManagedTempOutput, ReleaseManagedTemp, ReleaseManagedTempInput, ReleaseManagedTempOutput,
|
||||
ResolveManagedTemp, ResolveManagedTempInput, ResolveManagedTempOutput,
|
||||
MANAGED_TEMP_DEFAULT_TTL_MS, MANAGED_TEMP_PURPOSE_MAX_CHARS,
|
||||
GcManagedTempOutput, PurgeManagedTemp, PurgeManagedTempOutput, ReleaseManagedTemp,
|
||||
ReleaseManagedTempInput, ReleaseManagedTempOutput, ResolveManagedTemp, ResolveManagedTempInput,
|
||||
ResolveManagedTempOutput, MANAGED_TEMP_DEFAULT_TTL_MS, MANAGED_TEMP_PURPOSE_MAX_CHARS,
|
||||
};
|
||||
pub use mcp_tool_permissions::{
|
||||
McpToolPermissionCatalogue, ReadMcpToolPermissions, ReadMcpToolPermissionsInput,
|
||||
|
||||
@ -191,6 +191,32 @@ impl GcManagedTemp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Output for [`PurgeManagedTemp`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PurgeManagedTempOutput {
|
||||
/// Purge report.
|
||||
pub report: ManagedTempGcReport,
|
||||
}
|
||||
|
||||
/// Eagerly purges all managed temporary allocations.
|
||||
pub struct PurgeManagedTemp {
|
||||
store: Arc<dyn ManagedTempStore>,
|
||||
}
|
||||
|
||||
impl PurgeManagedTemp {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn ManagedTempStore>) -> Self {
|
||||
Self { store }
|
||||
}
|
||||
|
||||
/// Executes the purge.
|
||||
pub async fn execute(&self) -> Result<PurgeManagedTempOutput, AppError> {
|
||||
let report = self.store.purge_all().await?;
|
||||
Ok(PurgeManagedTempOutput { report })
|
||||
}
|
||||
}
|
||||
|
||||
fn now_ms(clock: &Arc<dyn Clock>) -> u64 {
|
||||
u64::try_from(clock.now_millis()).unwrap_or(0)
|
||||
}
|
||||
|
||||
@ -818,6 +818,12 @@ impl ManagedTempStore for FakeManagedTempStore {
|
||||
async fn gc(&self, _now_ms: u64) -> Result<ManagedTempGcReport, ManagedTempError> {
|
||||
Ok(ManagedTempGcReport { deleted: 0 })
|
||||
}
|
||||
|
||||
async fn purge_all(&self) -> Result<ManagedTempGcReport, ManagedTempError> {
|
||||
let deleted = self.leases.lock().unwrap().len();
|
||||
self.leases.lock().unwrap().clear();
|
||||
Ok(ManagedTempGcReport { deleted })
|
||||
}
|
||||
}
|
||||
|
||||
struct FixedClock;
|
||||
@ -4665,13 +4671,17 @@ async fn launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path() {
|
||||
let store = Arc::new(FakeManagedTempStore::default());
|
||||
store.set_allocation_path(tmp.clone());
|
||||
let (allocator, _resolver, releaser) = managed_temp_usecases(&store);
|
||||
let profile = codex_profile().with_projector(ProjectorKey::Codex);
|
||||
let profile = profile(
|
||||
pid(9),
|
||||
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||
)
|
||||
.with_projector(ProjectorKey::Claude);
|
||||
let (launch, agent, _fs, pty, _sessions) = launch_with_projection(
|
||||
profile,
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "AGENTS.md".to_owned(),
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
Some(full_registry()),
|
||||
None,
|
||||
Some(perm_doc(Posture::Ask)),
|
||||
);
|
||||
let launch = launch.with_launch_managed_temp(allocator, releaser);
|
||||
@ -4691,6 +4701,51 @@ async fn launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path() {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn launch_structured_non_codex_uses_managed_tmp_for_env_and_sandbox_without_writable_roots() {
|
||||
let root = fresh_temp_dir("launch-tmp-structured-non-codex");
|
||||
let tmp = root.join("managed-tmp-main").to_string_lossy().into_owned();
|
||||
let store = Arc::new(FakeManagedTempStore::default());
|
||||
store.set_allocation_path(tmp.clone());
|
||||
let (allocator, _resolver, releaser) = managed_temp_usecases(&store);
|
||||
let profile = profile(
|
||||
pid(9),
|
||||
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||
)
|
||||
.with_projector(ProjectorKey::Claude)
|
||||
.with_structured_adapter(StructuredAdapter::Claude);
|
||||
let (mut launch, agent, _fs, pty, _sessions) = launch_with_projection(
|
||||
profile,
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
None,
|
||||
Some(perm_doc(Posture::Ask)),
|
||||
);
|
||||
let factory = FakeStructuredFactory::new(trace(), sid(891));
|
||||
let structured = Arc::new(StructuredSessions::new());
|
||||
launch = launch
|
||||
.with_launch_managed_temp(allocator, releaser)
|
||||
.with_structured_routing_mode(StructuredRoutingMode::RequireStructured)
|
||||
.with_structured(Arc::new(factory.clone()), Arc::clone(&structured));
|
||||
|
||||
launch
|
||||
.execute(launch_input_with_project_root(agent.id, &root))
|
||||
.await
|
||||
.expect("structured launch");
|
||||
|
||||
assert!(pty.spawns().is_empty());
|
||||
let env = factory.envs().remove(0);
|
||||
let env: HashMap<_, _> = env.into_iter().collect();
|
||||
assert_eq!(env.get("TMPDIR"), Some(&tmp));
|
||||
assert_eq!(env.get("TMP"), Some(&tmp));
|
||||
assert_eq!(env.get("TEMP"), Some(&tmp));
|
||||
assert_eq!(factory.policies(), vec![None]);
|
||||
let sandbox = factory.sandboxes().remove(0).expect("sandbox plan");
|
||||
assert!(sandbox_has_rw_root(&sandbox, &tmp));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn launch_structured_codex_uses_same_managed_tmp_for_env_sandbox_and_writable_roots() {
|
||||
let root = fresh_temp_dir("launch-tmp-structured");
|
||||
|
||||
@ -10,7 +10,7 @@ use domain::{
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
AllocateManagedTemp, AllocateManagedTempInput, AppError, ResolveManagedTemp,
|
||||
AllocateManagedTemp, AllocateManagedTempInput, AppError, PurgeManagedTemp, ResolveManagedTemp,
|
||||
ResolveManagedTempInput, MANAGED_TEMP_DEFAULT_TTL_MS, MANAGED_TEMP_PURPOSE_MAX_CHARS,
|
||||
};
|
||||
|
||||
@ -90,6 +90,12 @@ impl ManagedTempStore for FakeManagedTempStore {
|
||||
async fn gc(&self, _now_ms: u64) -> Result<ManagedTempGcReport, ManagedTempError> {
|
||||
Ok(ManagedTempGcReport { deleted: 0 })
|
||||
}
|
||||
|
||||
async fn purge_all(&self) -> Result<ManagedTempGcReport, ManagedTempError> {
|
||||
let deleted = self.leases.lock().unwrap().len();
|
||||
self.leases.lock().unwrap().clear();
|
||||
Ok(ManagedTempGcReport { deleted })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -132,6 +138,31 @@ async fn allocate_uses_generated_opaque_handle_and_default_ttl() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn purge_managed_temp_calls_store_and_returns_report() {
|
||||
let store = Arc::new(FakeManagedTempStore::default());
|
||||
let allocate = AllocateManagedTemp::new(
|
||||
Arc::clone(&store) as Arc<dyn ManagedTempStore>,
|
||||
Arc::new(FixedIds),
|
||||
Arc::new(FixedClock),
|
||||
);
|
||||
allocate
|
||||
.execute(AllocateManagedTempInput {
|
||||
owner_project_id: None,
|
||||
kind: ManagedTempKind::Directory,
|
||||
purpose: "purge me".to_owned(),
|
||||
ttl_ms: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let purge = PurgeManagedTemp::new(Arc::clone(&store) as Arc<dyn ManagedTempStore>);
|
||||
|
||||
let out = purge.execute().await.unwrap();
|
||||
|
||||
assert_eq!(out.report.deleted, 1);
|
||||
assert!(store.leases.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allocate_rejects_empty_or_zero_ttl_and_bounds_purpose_metadata() {
|
||||
let store = Arc::new(FakeManagedTempStore::default());
|
||||
|
||||
Reference in New Issue
Block a user