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:
@ -45,29 +45,31 @@ use crate::dto::{
|
||||
parse_layout_id, parse_memory_slug, parse_model_server_id, parse_node_id, parse_profile_id,
|
||||
parse_project_id, parse_session_id, parse_skill_id, parse_task_id, parse_template_id,
|
||||
parse_ticket_id, save_model_server_input, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
AppExitWorkGuardStateDto, AssignSkillRequestDto, AttachBackgroundTaskResultDto,
|
||||
AttachLiveAgentRequestDto, AttachLiveAgentResponseDto, BackgroundTaskDto, CellKind,
|
||||
ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ChatAttachmentDto, ChatAttachmentInputDto,
|
||||
ChatDraftScopeRequestDto, ChatPromptAttachmentDto, CloneOpenCodeProfileFromSeedRequestDto,
|
||||
CloneProfileFromSeedRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto,
|
||||
CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto,
|
||||
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
|
||||
CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
|
||||
DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
|
||||
EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto,
|
||||
ExecuteSlashCommandRequestDto, ExecuteSlashCommandResponseDto, FirstRunStateDto,
|
||||
FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto,
|
||||
GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto,
|
||||
HealthRequestDto, HealthResponseDto, ImportChatAttachmentsRequestDto,
|
||||
ImportChatAttachmentsResponseDto, InspectConversationRequestDto, InterruptAgentRequestDto,
|
||||
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto,
|
||||
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto,
|
||||
ModelServerConfigListDto, OpenCodeProviderListDto, OpenTerminalRequestDto,
|
||||
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProfileModelCatalogDto, ProjectDto,
|
||||
ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto,
|
||||
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
||||
ReadChatDraftResponseDto, ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto,
|
||||
RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
||||
AllocateManagedTempRequestDto, AppExitWorkGuardStateDto, AssignSkillRequestDto,
|
||||
AttachBackgroundTaskResultDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
||||
BackgroundTaskDto, CellKind, ChangeAgentProfileDto, ChangeAgentProfileRequestDto,
|
||||
ChatAttachmentDto, ChatAttachmentInputDto, ChatDraftScopeRequestDto, ChatPromptAttachmentDto,
|
||||
CloneOpenCodeProfileFromSeedRequestDto, CloneProfileFromSeedRequestDto,
|
||||
ConfigureProfilesRequestDto, ConversationDetailsDto, CreateAgentFromTemplateRequestDto,
|
||||
CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto,
|
||||
CreateProjectRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto,
|
||||
DeleteLayoutRequestDto, DeleteLayoutResultDto, DeliveredDelegationRequestDto,
|
||||
DetectProfilesRequestDto, DetectProfilesResponseDto, EmbedderEnginesDto, EmbedderProfileDto,
|
||||
EmbedderProfileListDto, ErrorDto, ExecuteSlashCommandRequestDto,
|
||||
ExecuteSlashCommandResponseDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto,
|
||||
GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto,
|
||||
GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto,
|
||||
ImportChatAttachmentsRequestDto, ImportChatAttachmentsResponseDto,
|
||||
InspectConversationRequestDto, InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto,
|
||||
LayoutOperationDto, ListLayoutsDto, LiveAgentListDto, ManagedTempGcResponseDto,
|
||||
ManagedTempHandleRequestDto, ManagedTempLeaseDto, ManagedTempLeaseResponseDto, MemoryDto,
|
||||
MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto, ModelServerConfigListDto,
|
||||
OpenCodeProviderListDto, OpenTerminalRequestDto, PreviewModelServerCommandDto, ProfileDto,
|
||||
ProfileListDto, ProfileModelCatalogDto, ProjectDto, ProjectListDto,
|
||||
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto,
|
||||
ProjectWorkStateDto, ReadAgentContextResponseDto, ReadChatDraftResponseDto,
|
||||
ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto,
|
||||
RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
||||
ResolveAgentPermissionsRequestDto, ResolveAgentPermissionsResponseDto,
|
||||
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
|
||||
ResumableAgentListDto, SaveChatDraftRequestDto, SaveEmbedderProfileRequestDto,
|
||||
@ -2310,6 +2312,96 @@ pub async fn clear_chat_draft(
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `managed_temp_allocate` — allocate an IdeA-managed temporary file/directory.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] for malformed ids, invalid TTL/purpose, or store failure.
|
||||
#[tauri::command]
|
||||
pub async fn managed_temp_allocate(
|
||||
request: AllocateManagedTempRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ManagedTempLeaseResponseDto, ErrorDto> {
|
||||
let owner_project_id = request
|
||||
.owner_project_id
|
||||
.as_deref()
|
||||
.map(parse_project_id)
|
||||
.transpose()?;
|
||||
let output = state
|
||||
.allocate_managed_temp
|
||||
.execute(application::AllocateManagedTempInput {
|
||||
owner_project_id,
|
||||
kind: request.kind,
|
||||
purpose: request.purpose,
|
||||
ttl_ms: request.ttl_ms,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
Ok(ManagedTempLeaseResponseDto {
|
||||
lease: ManagedTempLeaseDto::from(output.lease),
|
||||
})
|
||||
}
|
||||
|
||||
/// `managed_temp_resolve` — resolve an active IdeA-managed temporary handle.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] for malformed handles, missing/released/expired handles,
|
||||
/// or store failure.
|
||||
#[tauri::command]
|
||||
pub async fn managed_temp_resolve(
|
||||
request: ManagedTempHandleRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ManagedTempLeaseResponseDto, ErrorDto> {
|
||||
let output = state
|
||||
.resolve_managed_temp
|
||||
.execute(application::ResolveManagedTempInput {
|
||||
handle: request.handle,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
Ok(ManagedTempLeaseResponseDto {
|
||||
lease: ManagedTempLeaseDto::from(output.lease),
|
||||
})
|
||||
}
|
||||
|
||||
/// `managed_temp_release` — release one IdeA-managed temporary handle.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] for malformed handles, missing handles, or store failure.
|
||||
#[tauri::command]
|
||||
pub async fn managed_temp_release(
|
||||
request: ManagedTempHandleRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ManagedTempLeaseResponseDto, ErrorDto> {
|
||||
let output = state
|
||||
.release_managed_temp
|
||||
.execute(application::ReleaseManagedTempInput {
|
||||
handle: request.handle,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
Ok(ManagedTempLeaseResponseDto {
|
||||
lease: ManagedTempLeaseDto::from(output.lease),
|
||||
})
|
||||
}
|
||||
|
||||
/// `managed_temp_gc` — delete released or expired managed temporary allocations.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] for store failure.
|
||||
#[tauri::command]
|
||||
pub async fn managed_temp_gc(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ManagedTempGcResponseDto, ErrorDto> {
|
||||
let output = state
|
||||
.gc_managed_temp
|
||||
.execute()
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
Ok(ManagedTempGcResponseDto {
|
||||
deleted: output.report.deleted,
|
||||
})
|
||||
}
|
||||
|
||||
/// `agent_send` — send a prompt to a live **structured** (chat) session and pump
|
||||
/// the turn's reply events to the frontend over `on_reply` (ARCHITECTURE §17.7).
|
||||
///
|
||||
|
||||
@ -18,6 +18,7 @@ pub mod commands;
|
||||
pub mod dto;
|
||||
pub mod embedded_server;
|
||||
pub mod events;
|
||||
pub mod managed_temp;
|
||||
pub mod mcp_bridge;
|
||||
pub mod mcp_endpoint;
|
||||
pub mod openai_tools;
|
||||
@ -345,6 +346,10 @@ pub fn run() {
|
||||
commands::read_chat_draft,
|
||||
commands::save_chat_draft,
|
||||
commands::clear_chat_draft,
|
||||
commands::managed_temp_allocate,
|
||||
commands::managed_temp_resolve,
|
||||
commands::managed_temp_release,
|
||||
commands::managed_temp_gc,
|
||||
commands::agent_send,
|
||||
commands::import_chat_attachments,
|
||||
commands::cancel_resume,
|
||||
|
||||
314
crates/app-tauri/src/managed_temp.rs
Normal file
314
crates/app-tauri/src/managed_temp.rs
Normal file
@ -0,0 +1,314 @@
|
||||
//! Desktop-owned MCP managed-temp tool provider.
|
||||
//!
|
||||
//! This module maps public `idea_managed_temp_*` tools to the application
|
||||
//! managed-temp use cases. Tool arguments never include filesystem paths.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use application::{
|
||||
AllocateManagedTemp, AllocateManagedTempInput, GcManagedTemp, ReleaseManagedTemp,
|
||||
ReleaseManagedTempInput, ResolveManagedTemp, ResolveManagedTempInput,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::{ManagedTempKind, Project};
|
||||
use infrastructure::{ManagedTempToolError, ManagedTempToolProvider};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::dto::{ManagedTempGcResponseDto, ManagedTempLeaseDto, ManagedTempLeaseResponseDto};
|
||||
|
||||
/// Concrete provider for the public MCP managed-temp tools.
|
||||
#[derive(Clone)]
|
||||
pub struct AppManagedTempToolProvider {
|
||||
/// Allocation use case.
|
||||
pub allocate: Arc<AllocateManagedTemp>,
|
||||
/// Resolve use case.
|
||||
pub resolve: Arc<ResolveManagedTemp>,
|
||||
/// Release use case.
|
||||
pub release: Arc<ReleaseManagedTemp>,
|
||||
/// Garbage-collection use case.
|
||||
pub gc: Arc<GcManagedTemp>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ManagedTempToolProvider for AppManagedTempToolProvider {
|
||||
async fn handle_managed_temp_tool(
|
||||
&self,
|
||||
project: &Project,
|
||||
_requester: &str,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
) -> Result<Value, ManagedTempToolError> {
|
||||
let result = match name {
|
||||
"idea_managed_temp_allocate" => {
|
||||
let output = self
|
||||
.allocate
|
||||
.execute(AllocateManagedTempInput {
|
||||
owner_project_id: Some(project.id),
|
||||
kind: managed_temp_kind(required_str(&arguments, "kind")?)?,
|
||||
purpose: required_str(&arguments, "purpose")?.to_owned(),
|
||||
ttl_ms: optional_u64(&arguments, "ttlMs")?,
|
||||
})
|
||||
.await
|
||||
.map_err(managed_temp_error)?;
|
||||
json!(ManagedTempLeaseResponseDto {
|
||||
lease: ManagedTempLeaseDto::from(output.lease)
|
||||
})
|
||||
}
|
||||
"idea_managed_temp_resolve" => {
|
||||
let output = self
|
||||
.resolve
|
||||
.execute(ResolveManagedTempInput {
|
||||
handle: required_str(&arguments, "handle")?.to_owned(),
|
||||
})
|
||||
.await
|
||||
.map_err(managed_temp_error)?;
|
||||
json!(ManagedTempLeaseResponseDto {
|
||||
lease: ManagedTempLeaseDto::from(output.lease)
|
||||
})
|
||||
}
|
||||
"idea_managed_temp_release" => {
|
||||
let output = self
|
||||
.release
|
||||
.execute(ReleaseManagedTempInput {
|
||||
handle: required_str(&arguments, "handle")?.to_owned(),
|
||||
})
|
||||
.await
|
||||
.map_err(managed_temp_error)?;
|
||||
json!(ManagedTempLeaseResponseDto {
|
||||
lease: ManagedTempLeaseDto::from(output.lease)
|
||||
})
|
||||
}
|
||||
"idea_managed_temp_gc" => {
|
||||
let output = self.gc.execute().await.map_err(managed_temp_error)?;
|
||||
json!(ManagedTempGcResponseDto {
|
||||
deleted: output.report.deleted
|
||||
})
|
||||
}
|
||||
other => {
|
||||
return Err(ManagedTempToolError::new(
|
||||
"notFound",
|
||||
format!("unknown managed temp tool: {other}"),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn managed_temp_error(err: application::AppError) -> ManagedTempToolError {
|
||||
let code = match err.code() {
|
||||
"NOT_FOUND" => "notFound",
|
||||
"INVALID" => "invalid",
|
||||
"STORE" => "store",
|
||||
"FILESYSTEM" => "filesystem",
|
||||
_ => "internal",
|
||||
};
|
||||
ManagedTempToolError::new(code, err.to_string())
|
||||
}
|
||||
|
||||
fn required_str<'a>(arguments: &'a Value, key: &str) -> Result<&'a str, ManagedTempToolError> {
|
||||
arguments
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| ManagedTempToolError::new("invalid", format!("missing {key}")))
|
||||
}
|
||||
|
||||
fn optional_u64(arguments: &Value, key: &str) -> Result<Option<u64>, ManagedTempToolError> {
|
||||
match arguments.get(key) {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(value) => value
|
||||
.as_u64()
|
||||
.map(Some)
|
||||
.ok_or_else(|| ManagedTempToolError::new("invalid", format!("{key} must be a u64"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn managed_temp_kind(raw: &str) -> Result<ManagedTempKind, ManagedTempToolError> {
|
||||
match raw {
|
||||
"directory" => Ok(ManagedTempKind::Directory),
|
||||
"file" => Ok(ManagedTempKind::File),
|
||||
_ => Err(ManagedTempToolError::new(
|
||||
"invalid",
|
||||
"kind must be `directory` or `file`",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use application::{
|
||||
AllocateManagedTemp, GcManagedTemp, ReleaseManagedTemp, ResolveManagedTemp,
|
||||
MANAGED_TEMP_DEFAULT_TTL_MS,
|
||||
};
|
||||
use domain::ports::{Clock, IdGenerator, LocalPath, ManagedTempStore};
|
||||
use domain::{
|
||||
ManagedTempAllocation, ManagedTempError, ManagedTempGcReport, ManagedTempHandle,
|
||||
ManagedTempLease, ManagedTempState, ProjectId, ProjectPath, RemoteRef,
|
||||
};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct FixedClock;
|
||||
|
||||
impl Clock for FixedClock {
|
||||
fn now_millis(&self) -> i64 {
|
||||
50_000
|
||||
}
|
||||
}
|
||||
|
||||
struct FixedIds;
|
||||
|
||||
impl IdGenerator for FixedIds {
|
||||
fn new_uuid(&self) -> Uuid {
|
||||
Uuid::from_u128(0x189)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeStore {
|
||||
allocations: Mutex<Vec<ManagedTempAllocation>>,
|
||||
leases: Mutex<HashMap<ManagedTempHandle, ManagedTempLease>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ManagedTempStore for FakeStore {
|
||||
async fn allocate(
|
||||
&self,
|
||||
allocation: ManagedTempAllocation,
|
||||
) -> Result<ManagedTempLease, ManagedTempError> {
|
||||
let lease = ManagedTempLease {
|
||||
handle: allocation.handle.clone(),
|
||||
kind: allocation.kind,
|
||||
owner_project_id: allocation.owner_project_id,
|
||||
purpose: allocation.purpose.clone(),
|
||||
path: LocalPath::new(format!("/managed/{}", allocation.handle.as_str())),
|
||||
state: ManagedTempState::Active,
|
||||
created_at_ms: allocation.created_at_ms,
|
||||
last_access_at_ms: allocation.created_at_ms,
|
||||
expires_at_ms: allocation.expires_at_ms,
|
||||
released_at_ms: None,
|
||||
};
|
||||
self.allocations.lock().unwrap().push(allocation);
|
||||
self.leases
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(lease.handle.clone(), lease.clone());
|
||||
Ok(lease)
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
&self,
|
||||
handle: &ManagedTempHandle,
|
||||
now_ms: u64,
|
||||
) -> Result<ManagedTempLease, ManagedTempError> {
|
||||
let mut lease = self
|
||||
.leases
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(handle)
|
||||
.cloned()
|
||||
.ok_or(ManagedTempError::NotFound)?;
|
||||
lease.last_access_at_ms = now_ms;
|
||||
Ok(lease)
|
||||
}
|
||||
|
||||
async fn release(
|
||||
&self,
|
||||
handle: &ManagedTempHandle,
|
||||
now_ms: u64,
|
||||
) -> Result<ManagedTempLease, ManagedTempError> {
|
||||
let mut leases = self.leases.lock().unwrap();
|
||||
let lease = leases.get_mut(handle).ok_or(ManagedTempError::NotFound)?;
|
||||
lease.state = ManagedTempState::Released;
|
||||
lease.released_at_ms = Some(now_ms);
|
||||
Ok(lease.clone())
|
||||
}
|
||||
|
||||
async fn gc(&self, _now_ms: u64) -> Result<ManagedTempGcReport, ManagedTempError> {
|
||||
Ok(ManagedTempGcReport { deleted: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
fn project() -> Project {
|
||||
Project::new(
|
||||
ProjectId::from_uuid(Uuid::from_u128(7)),
|
||||
"demo",
|
||||
ProjectPath::new("/tmp/managed-temp-provider-project").unwrap(),
|
||||
RemoteRef::local(),
|
||||
1_000,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn provider(store: Arc<FakeStore>) -> AppManagedTempToolProvider {
|
||||
let store_port = store as Arc<dyn ManagedTempStore>;
|
||||
AppManagedTempToolProvider {
|
||||
allocate: Arc::new(AllocateManagedTemp::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::new(FixedIds),
|
||||
Arc::new(FixedClock),
|
||||
)),
|
||||
resolve: Arc::new(ResolveManagedTemp::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::new(FixedClock),
|
||||
)),
|
||||
release: Arc::new(ReleaseManagedTemp::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::new(FixedClock),
|
||||
)),
|
||||
gc: Arc::new(GcManagedTemp::new(store_port, Arc::new(FixedClock))),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_temp_allocate_tool_uses_current_project_and_generated_handle() {
|
||||
let store = Arc::new(FakeStore::default());
|
||||
let project = project();
|
||||
let result = provider(Arc::clone(&store))
|
||||
.handle_managed_temp_tool(
|
||||
&project,
|
||||
"agent",
|
||||
"idea_managed_temp_allocate",
|
||||
json!({ "kind": "directory", "purpose": "scratch" }),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result["lease"]["ownerProjectId"], project.id.to_string());
|
||||
assert_eq!(
|
||||
result["lease"]["handle"],
|
||||
"00000000-0000-0000-0000-000000000189"
|
||||
);
|
||||
assert_eq!(
|
||||
result["lease"]["path"],
|
||||
"/managed/00000000-0000-0000-0000-000000000189"
|
||||
);
|
||||
let allocations = store.allocations.lock().unwrap();
|
||||
assert_eq!(allocations.len(), 1);
|
||||
assert_eq!(allocations[0].owner_project_id, Some(project.id));
|
||||
assert_eq!(
|
||||
allocations[0].expires_at_ms,
|
||||
Some(50_000 + MANAGED_TEMP_DEFAULT_TTL_MS)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_temp_resolve_tool_rejects_path_like_handle() {
|
||||
let err = provider(Arc::new(FakeStore::default()))
|
||||
.handle_managed_temp_tool(
|
||||
&project(),
|
||||
"agent",
|
||||
"idea_managed_temp_resolve",
|
||||
json!({ "handle": "../escape" }),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code, "invalid");
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ use domain::ports::{
|
||||
ToolSpec,
|
||||
};
|
||||
use domain::{AgentToolPolicy, IssueRef, McpToolPolicy, Project};
|
||||
use infrastructure::{TemplateToolProvider, TicketToolProvider};
|
||||
use infrastructure::{ManagedTempToolProvider, TemplateToolProvider, TicketToolProvider};
|
||||
use serde_json::Value;
|
||||
|
||||
const PROJECT_ROOT_ARG: &str = "__ideaProjectRoot";
|
||||
@ -28,6 +28,7 @@ pub struct AppOpenAiToolInvoker {
|
||||
mcp_tool_permissions: Arc<dyn McpToolPermissionStore>,
|
||||
ticket_tools: Arc<dyn TicketToolProvider>,
|
||||
template_tools: Arc<dyn TemplateToolProvider>,
|
||||
managed_temp_tools: Arc<dyn ManagedTempToolProvider>,
|
||||
}
|
||||
|
||||
/// Proxy injecté avant que l'orchestrateur soit construit, puis lié dans la
|
||||
@ -101,6 +102,7 @@ impl AppOpenAiToolInvoker {
|
||||
mcp_tool_permissions: Arc<dyn McpToolPermissionStore>,
|
||||
ticket_tools: Arc<dyn TicketToolProvider>,
|
||||
template_tools: Arc<dyn TemplateToolProvider>,
|
||||
managed_temp_tools: Arc<dyn ManagedTempToolProvider>,
|
||||
) -> Self {
|
||||
Self {
|
||||
orchestrator,
|
||||
@ -109,6 +111,7 @@ impl AppOpenAiToolInvoker {
|
||||
mcp_tool_permissions,
|
||||
ticket_tools,
|
||||
template_tools,
|
||||
managed_temp_tools,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -215,6 +218,20 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
||||
return serde_json::to_string(&value)
|
||||
.map_err(|e| ToolInvocationError::Execution(format!("JSON template tool: {e}")));
|
||||
}
|
||||
if infrastructure::orchestrator::mcp::tools::is_managed_temp_tool(name) {
|
||||
let value = self
|
||||
.managed_temp_tools
|
||||
.handle_managed_temp_tool(&project, &requester, name, value)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let detail =
|
||||
serde_json::to_string(&e.to_value()).unwrap_or_else(|_| e.to_string());
|
||||
ToolInvocationError::Execution(detail)
|
||||
})?;
|
||||
return serde_json::to_string(&value).map_err(|e| {
|
||||
ToolInvocationError::Execution(format!("JSON managed temp tool: {e}"))
|
||||
});
|
||||
}
|
||||
let command = infrastructure::orchestrator::mcp::map_tool_call(name, &value, &requester)
|
||||
.map_err(|e| match e {
|
||||
infrastructure::orchestrator::mcp::ToolMapError::UnknownTool(tool) => {
|
||||
@ -395,7 +412,7 @@ mod tests {
|
||||
AgentId, AgentMcpToolPolicyOverride, McpToolPolicy, Project, ProjectId,
|
||||
ProjectMcpToolPermissions, ProjectPath, RemoteRef, StoreError, Workspace,
|
||||
};
|
||||
use infrastructure::{TemplateToolError, TicketToolError};
|
||||
use infrastructure::{ManagedTempToolError, TemplateToolError, TicketToolError};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -546,6 +563,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeManagedTempTools {
|
||||
calls: Mutex<Vec<(String, String, Value)>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ManagedTempToolProvider for FakeManagedTempTools {
|
||||
async fn handle_managed_temp_tool(
|
||||
&self,
|
||||
_project: &Project,
|
||||
requester: &str,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
) -> Result<Value, ManagedTempToolError> {
|
||||
self.calls.lock().unwrap().push((
|
||||
requester.to_owned(),
|
||||
name.to_owned(),
|
||||
arguments.clone(),
|
||||
));
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"requester": requester,
|
||||
"handle": arguments.get("handle").and_then(Value::as_str).unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_managed_temp_tools() -> Arc<FakeManagedTempTools> {
|
||||
Arc::new(FakeManagedTempTools::default())
|
||||
}
|
||||
|
||||
fn project() -> Project {
|
||||
Project::new(
|
||||
ProjectId::from_uuid(Uuid::from_u128(1)),
|
||||
@ -602,6 +650,7 @@ mod tests {
|
||||
mcp_permissions(ProjectMcpToolPermissions::default()),
|
||||
ticket_tools.clone(),
|
||||
template_tools.clone(),
|
||||
fake_managed_temp_tools(),
|
||||
);
|
||||
|
||||
for tool in ["idea_ticket_list", "idea_template_list"] {
|
||||
@ -693,6 +742,7 @@ mod tests {
|
||||
)),
|
||||
Arc::new(FakeTicketTools::default()),
|
||||
Arc::new(FakeTemplateTools::default()),
|
||||
fake_managed_temp_tools(),
|
||||
);
|
||||
|
||||
let names = invoker
|
||||
@ -733,6 +783,7 @@ mod tests {
|
||||
mcp_permissions(allow_doc(agent, &["idea_ticket_update_carnet"])),
|
||||
ticket_tools.clone(),
|
||||
template_tools,
|
||||
fake_managed_temp_tools(),
|
||||
);
|
||||
|
||||
let result = invoker
|
||||
@ -779,6 +830,7 @@ mod tests {
|
||||
mcp_permissions(allow_doc(agent, &["idea_template_update"])),
|
||||
ticket_tools,
|
||||
template_tools.clone(),
|
||||
fake_managed_temp_tools(),
|
||||
);
|
||||
|
||||
let result = invoker
|
||||
@ -853,6 +905,7 @@ mod tests {
|
||||
mcp_permissions(ProjectMcpToolPermissions::default()),
|
||||
ticket_tools.clone(),
|
||||
template_tools,
|
||||
fake_managed_temp_tools(),
|
||||
);
|
||||
|
||||
let denied = invoker
|
||||
|
||||
@ -12,11 +12,12 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use backend::BackendCore;
|
||||
use domain::PersistedPluginLayoutWindow;
|
||||
use infrastructure::{TemplateToolProvider, TicketToolProvider};
|
||||
use infrastructure::{ManagedTempToolProvider, TemplateToolProvider, TicketToolProvider};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::ChatBridge;
|
||||
use crate::embedded_server::EmbeddedServerController;
|
||||
use crate::managed_temp::AppManagedTempToolProvider;
|
||||
use crate::pty::PtyBridge;
|
||||
use crate::templates::AppTemplateToolProvider;
|
||||
use crate::tickets::AppTicketToolProvider;
|
||||
@ -90,6 +91,13 @@ impl AppState {
|
||||
update: Arc::clone(&core.update_template),
|
||||
delete: Arc::clone(&core.delete_template),
|
||||
}) as Arc<dyn TemplateToolProvider>);
|
||||
core.managed_temp_tool_binder
|
||||
.bind(Arc::new(AppManagedTempToolProvider {
|
||||
allocate: Arc::clone(&core.allocate_managed_temp),
|
||||
resolve: Arc::clone(&core.resolve_managed_temp),
|
||||
release: Arc::clone(&core.release_managed_temp),
|
||||
gc: Arc::clone(&core.gc_managed_temp),
|
||||
}) as Arc<dyn ManagedTempToolProvider>);
|
||||
|
||||
Self {
|
||||
core,
|
||||
|
||||
Reference in New Issue
Block a user