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:
@ -49,6 +49,7 @@ pub mod issue;
|
||||
pub mod layout;
|
||||
pub mod live_state;
|
||||
pub mod mailbox;
|
||||
pub mod managed_temp;
|
||||
pub mod markdown;
|
||||
pub mod mcp_tool_permissions;
|
||||
pub mod memory;
|
||||
@ -127,6 +128,10 @@ pub use mailbox::{
|
||||
AgentMailbox, AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket,
|
||||
TicketId,
|
||||
};
|
||||
pub use managed_temp::{
|
||||
ManagedTempAllocation, ManagedTempError, ManagedTempGcReport, ManagedTempHandle,
|
||||
ManagedTempKind, ManagedTempLease, ManagedTempState,
|
||||
};
|
||||
|
||||
pub use conversation::{
|
||||
Conversation, ConversationError, ConversationId, ConversationParty, ConversationRegistry,
|
||||
|
||||
122
crates/domain/src/managed_temp.rs
Normal file
122
crates/domain/src/managed_temp.rs
Normal 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),
|
||||
}
|
||||
@ -45,6 +45,10 @@ use crate::issue::{
|
||||
Issue, IssueAttachment, IssueAttachmentId, IssueCarnet, IssueIndexEntry, IssueListFilter,
|
||||
IssueNumber, IssueRef, IssueVersion,
|
||||
};
|
||||
use crate::managed_temp::{
|
||||
ManagedTempAllocation, ManagedTempError, ManagedTempGcReport, ManagedTempHandle,
|
||||
ManagedTempLease,
|
||||
};
|
||||
use crate::markdown::MarkdownDoc;
|
||||
use crate::mcp_tool_permissions::ProjectMcpToolPermissions;
|
||||
use crate::memory::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug};
|
||||
@ -1320,6 +1324,33 @@ pub trait ChatAttachmentStore: Send + Sync {
|
||||
) -> Result<Vec<ChatAttachment>, ChatAttachmentStoreError>;
|
||||
}
|
||||
|
||||
/// Store for IdeA-managed temporary allocations shared across projects.
|
||||
#[async_trait]
|
||||
pub trait ManagedTempStore: Send + Sync {
|
||||
/// Allocates a new managed temporary file/directory.
|
||||
async fn allocate(
|
||||
&self,
|
||||
allocation: ManagedTempAllocation,
|
||||
) -> Result<ManagedTempLease, ManagedTempError>;
|
||||
|
||||
/// Resolves an active handle and updates its last access timestamp.
|
||||
async fn resolve(
|
||||
&self,
|
||||
handle: &ManagedTempHandle,
|
||||
now_ms: u64,
|
||||
) -> Result<ManagedTempLease, ManagedTempError>;
|
||||
|
||||
/// Releases an allocation, making it eligible for GC.
|
||||
async fn release(
|
||||
&self,
|
||||
handle: &ManagedTempHandle,
|
||||
now_ms: u64,
|
||||
) -> Result<ManagedTempLease, ManagedTempError>;
|
||||
|
||||
/// Deletes released or expired allocations.
|
||||
async fn gc(&self, now_ms: u64) -> Result<ManagedTempGcReport, ManagedTempError>;
|
||||
}
|
||||
|
||||
/// Persistence port for project-scoped custom chat drafts.
|
||||
#[async_trait]
|
||||
pub trait ChatDraftStore: Send + Sync {
|
||||
|
||||
Reference in New Issue
Block a user