feat(infra): tmp partagé inter-projets géré par IdeA (handles opaques, leases, GC, projection sandbox) — #189 (QA verte)

- Domaine: port ManagedTempStore + modèle (handle opaque, kind Directory/File, état Active/Released, allocation/lease, expiry).
- Application: use cases Allocate/Resolve/Release/GcManagedTemp, horodatés via Clock.
- Infrastructure: impl FS FsManagedTempStore sous app_data_dir/managed-tmp (root global inter-projets, métadonnées par handle, GC des leases expirés/libérés).
- Exposition MCP contrôlée (ManagedTempToolProvider, catalogue) + câblage sandbox: les agents résolvent un handle au lieu d'inventer un path libre.
- Tests: application/infrastructure managed_temp.
This commit is contained in:
2026-08-13 13:34:33 +02:00
parent d7dfbcdfcb
commit fdca55ecd4
22 changed files with 2000 additions and 72 deletions

View File

@ -0,0 +1,122 @@
//! Managed temporary storage model.
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::ids::ProjectId;
use crate::ports::LocalPath;
/// Opaque handle for an IdeA-managed temporary allocation.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ManagedTempHandle(String);
impl ManagedTempHandle {
/// Builds a handle.
///
/// # Errors
/// [`ManagedTempError::Invalid`] when the handle is empty or path-like.
pub fn new(value: impl Into<String>) -> Result<Self, ManagedTempError> {
let value = value.into();
if value.is_empty()
|| value.len() > 128
|| !value
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(ManagedTempError::Invalid(
"invalid managed temp handle".to_owned(),
));
}
Ok(Self(value))
}
/// Returns the raw handle.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
/// Physical allocation kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ManagedTempKind {
/// A directory allocation.
Directory,
/// An empty file allocation.
File,
}
/// Lease lifecycle state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ManagedTempState {
/// The allocation can still be resolved.
Active,
/// The allocation was explicitly released and is GC-eligible.
Released,
}
/// Allocation request persisted by the store.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManagedTempAllocation {
/// Opaque handle allocated by the application layer.
pub handle: ManagedTempHandle,
/// Physical allocation kind.
pub kind: ManagedTempKind,
/// Optional project that requested the allocation.
pub owner_project_id: Option<ProjectId>,
/// Short human/debug purpose.
pub purpose: String,
/// Creation time, epoch milliseconds.
pub created_at_ms: u64,
/// Optional expiry time, epoch milliseconds.
pub expires_at_ms: Option<u64>,
}
/// Resolved managed temporary lease.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManagedTempLease {
/// Opaque handle.
pub handle: ManagedTempHandle,
/// Physical allocation kind.
pub kind: ManagedTempKind,
/// Optional project that requested the allocation.
pub owner_project_id: Option<ProjectId>,
/// Short human/debug purpose.
pub purpose: String,
/// Managed local path. It is produced by IdeA, never accepted as user input.
pub path: LocalPath,
/// Lease state.
pub state: ManagedTempState,
/// Creation time, epoch milliseconds.
pub created_at_ms: u64,
/// Last successful resolve time, epoch milliseconds.
pub last_access_at_ms: u64,
/// Optional expiry time, epoch milliseconds.
pub expires_at_ms: Option<u64>,
/// Explicit release time, epoch milliseconds.
pub released_at_ms: Option<u64>,
}
/// GC outcome.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManagedTempGcReport {
/// Number of deleted allocations.
pub deleted: usize,
}
/// Managed temporary store errors.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ManagedTempError {
/// Handle does not exist or is no longer resolvable.
#[error("managed temp not found")]
NotFound,
/// Invalid input or persisted metadata.
#[error("managed temp invalid: {0}")]
Invalid(String),
/// Store I/O or serialization failed.
#[error("managed temp store failed: {0}")]
Store(String),
}