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

@ -3206,6 +3206,89 @@ pub struct ChatAttachmentDto {
pub created_at: u64,
}
/// Request for allocating an IdeA-managed temporary file/directory.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AllocateManagedTempRequestDto {
/// Optional project that requested this allocation.
#[serde(default)]
pub owner_project_id: Option<String>,
/// Physical allocation kind (`directory` or `file`).
pub kind: domain::ManagedTempKind,
/// Short human/debug purpose retained in metadata.
pub purpose: String,
/// Optional TTL override in milliseconds.
#[serde(default)]
pub ttl_ms: Option<u64>,
}
/// Request for resolving/releasing one managed temporary handle.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedTempHandleRequestDto {
/// Opaque managed temporary handle.
pub handle: String,
}
/// Managed temporary lease DTO.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedTempLeaseDto {
/// Opaque managed temporary handle.
pub handle: String,
/// Physical allocation kind.
pub kind: domain::ManagedTempKind,
/// Optional project that requested this allocation.
pub owner_project_id: Option<String>,
/// Short human/debug purpose.
pub purpose: String,
/// IdeA-controlled local path for the allocation data.
pub path: String,
/// Lease state.
pub state: domain::ManagedTempState,
/// Creation time, epoch milliseconds.
pub created_at_ms: u64,
/// Last successful access time, epoch milliseconds.
pub last_access_at_ms: u64,
/// Expiry time, epoch milliseconds.
pub expires_at_ms: Option<u64>,
/// Explicit release time, epoch milliseconds.
pub released_at_ms: Option<u64>,
}
impl From<domain::ManagedTempLease> for ManagedTempLeaseDto {
fn from(lease: domain::ManagedTempLease) -> Self {
Self {
handle: lease.handle.as_str().to_owned(),
kind: lease.kind,
owner_project_id: lease.owner_project_id.map(|id| id.to_string()),
purpose: lease.purpose,
path: lease.path.as_str().to_owned(),
state: lease.state,
created_at_ms: lease.created_at_ms,
last_access_at_ms: lease.last_access_at_ms,
expires_at_ms: lease.expires_at_ms,
released_at_ms: lease.released_at_ms,
}
}
}
/// Response containing one managed temporary lease.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedTempLeaseResponseDto {
/// Managed temporary lease.
pub lease: ManagedTempLeaseDto,
}
/// Managed temporary GC response.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedTempGcResponseDto {
/// Number of deleted allocations.
pub deleted: usize,
}
/// Canonical attachment projection attached to a streamed user prompt.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]

View File

@ -13,24 +13,24 @@ use std::sync::{Arc, Mutex};
use application::orchestrator::UpdateProjectContext as GuardedUpdateProjectContext;
use application::{
AddIssueAttachment, AgentResumer, AgentWakeService, AppError, AssignIssueAgent,
AssignSkillToAgent, AssignTicketToSprint, AttachLiveAgent, AuthenticateSession,
BackgroundCommandArchive, BulkDeleteIssues, BulkUpdateIssuePriority, BulkUpdateIssueStatus,
CancelBackgroundTask, ChangeAgentProfile, CheckEmbedderSuggestion, ClearChatDraft,
CloneOpenCodeProfileFromSeed, CloneProfileFromSeed, CloseProject, CloseTab, CloseTerminal,
CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch,
CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory, CreateProject, CreateSkill,
CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteIssue, DeleteLayout,
DeleteMemory, DeleteModelArtifact, DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint,
DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles,
DismissEmbedderSuggestion, EnsureLocalModelServer, ExecuteSlashCommand, FirstRunState,
GetAppExitWorkGuardState, GetLiveStateLean, GetMemory, GetProjectPermissions,
GetProjectSystemPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit,
GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn,
HealthUseCase, ImportChatAttachments, InspectConversation, InstallPluginFromArchive,
InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput,
LinkIssues, ListAgents, ListAgentsInput, ListClaudeModels, ListCodexModels, ListDevices,
ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
AddIssueAttachment, AgentResumer, AgentWakeService, AllocateManagedTemp, AppError,
AssignIssueAgent, AssignSkillToAgent, AssignTicketToSprint, AttachLiveAgent,
AuthenticateSession, BackgroundCommandArchive, BulkDeleteIssues, BulkUpdateIssuePriority,
BulkUpdateIssueStatus, CancelBackgroundTask, ChangeAgentProfile, CheckEmbedderSuggestion,
ClearChatDraft, CloneOpenCodeProfileFromSeed, CloneProfileFromSeed, CloseProject, CloseTab,
CloseTerminal, CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases,
CreateAgentFromScratch, CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory,
CreateProject, CreateSkill, CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile,
DeleteIssue, DeleteLayout, DeleteMemory, DeleteModelArtifact, DeleteModelServer, DeleteProfile,
DeleteSkill, DeleteSprint, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
DetectProfiles, DismissEmbedderSuggestion, EnsureLocalModelServer, ExecuteSlashCommand,
FirstRunState, GcManagedTemp, GetAppExitWorkGuardState, GetLiveStateLean, GetMemory,
GetProjectPermissions, GetProjectSystemPermissions, GetProjectWorkState, GitBranches,
GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
HarvestMemoryFromTurn, HealthUseCase, ImportChatAttachments, InspectConversation,
InstallPluginFromArchive, InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent,
LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput, ListClaudeModels, ListCodexModels,
ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects,
ListResumableAgents, ListSkills, ListSlashCommands, ListSprints, ListTemplates,
LiveAgentRegistry, LiveSessions, LiveStateLeanProvider, LiveStateProvider,
@ -44,21 +44,21 @@ use application::{
ReadIssueAttachment, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex,
ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
ReconcileLiveState, ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn,
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
ReorderSprints, ResizeTerminal, ResolveAgentCapabilities, ResolveAgentPermissions,
ResolveAgentSystemPermissions, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask,
ReviewPluginPackage, RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveChatDraft,
SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile, SaveProfile,
SessionLimitService, SetActiveLayout, SetPluginEnabled, SlashCommandRegistry,
SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent,
StructuredRoutingMode, StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate,
TerminalSessions, TouchDevice, UnassignSkillFromAgent, UnassignTicketFromSprint,
UninstallPlugin, UnlinkIssues, UpdateAgentContext, UpdateAgentEffort,
UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateAgentSystemPermissions,
UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateProjectSystemPermissions,
UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal,
AGENT_MEMORY_RECALL_BUDGET,
RecordTurnProvider, ReferenceProfiles, ReleaseManagedTemp, RenameDevice, RenameLayout,
RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentCapabilities,
ResolveAgentPermissions, ResolveAgentSystemPermissions, ResolveManagedTemp, ResolveMemoryLinks,
RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage, RevokeAllDevices, RevokeDevice,
RotateConversationLog, SaveChatDraft, SaveEmbedderProfile, SaveModelServer,
SaveOpenCodeProviderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
SetPluginEnabled, SlashCommandRegistry, SnapshotOpenWindows, SnapshotRunningAgents,
SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode, StructuredSessions,
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues,
UpdateAgentContext, UpdateAgentEffort, UpdateAgentMcpToolPermissions, UpdateAgentPermissions,
UpdateAgentSystemPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory,
UpdateProjectContext, UpdateProjectMcpToolPermissions, UpdateProjectPermissions,
UpdateProjectSystemPermissions, UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory,
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
};
use async_trait::async_trait;
use domain::ports::{
@ -66,11 +66,11 @@ use domain::ports::{
AgentWakePort, AssistantContextProvider, BackgroundTaskPortError, BackgroundTaskRunner,
BackgroundTaskStore, ChatAttachmentStore, ChatDraftStore, Clock, DeviceSessionStore, Embedder,
EmbedderEnvInspector, EmbedderProfileStore, EmbedderPromptStore, EnvironmentReader, EventBus,
FileSystem, GitPort, IdGenerator, IssueNumberAllocator, IssueStore, McpToolPermissionStore,
MemoryRecall, MemoryStore, ModelArtifactDownloader, PermissionStore, PluginManifestValidator,
PluginMcpSupervisor, PluginPackageStore, PluginRegistryStore, PluginStorageStore,
ProcessSpawner, ProfileStore, ProjectStore, PtyHandle, PtyPort, RuntimePermissionProbe,
ScheduledTask, Scheduler, SecretStore, SkillStore, SprintStore,
FileSystem, GitPort, IdGenerator, IssueNumberAllocator, IssueStore, ManagedTempStore,
McpToolPermissionStore, MemoryRecall, MemoryStore, ModelArtifactDownloader, PermissionStore,
PluginManifestValidator, PluginMcpSupervisor, PluginPackageStore, PluginRegistryStore,
PluginStorageStore, ProcessSpawner, ProfileStore, ProjectStore, PtyHandle, PtyPort,
RuntimePermissionProbe, ScheduledTask, Scheduler, SecretStore, SkillStore, SprintStore,
StructuredSessionEnvironmentPreparer, SystemPermissionStore, TemplateStore, ToolInvoker,
WakeError, WakeReason, WindowStateStore,
};
@ -94,7 +94,7 @@ use infrastructure::{
EmbeddedCompatibilityMatrix, EmbedderEnvProbe, ExternalMcpPluginSupervisor,
FsAssistantContextStore, FsBackgroundTaskStore, FsChatAttachmentStore, FsChatDraftStore,
FsConversationLog, FsDeviceSessionStore, FsEmbedderProfileStore, FsEmbedderPromptStore,
FsHandoffStore, FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore,
FsHandoffStore, FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsManagedTempStore,
FsMcpToolPermissionStore, FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher,
FsPermissionStore, FsPluginPackageStore, FsPluginRegistryStore, FsPluginStorageStore,
FsProfileStore, FsProjectStore, FsProviderSessionStore, FsSecretStore, FsSkillStore,
@ -102,8 +102,8 @@ use infrastructure::{
HeuristicHandoffSummarizer, HfModelArtifactDownloader, HttpOpenAiCompatibleProbe,
HttpProviderModelCatalogue, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalEnvironmentReader, LocalFileSystem,
LocalManagedProcess, LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall,
OrchestratorWatchHandle, PortablePtyAdapter, ProcessCliVersionReader,
LocalManagedProcess, LocalProcessSpawner, ManagedTempToolProvider, McpServer, MediatedInbox,
NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter, ProcessCliVersionReader,
ReadOnlyRuntimePermissionProbe, RwFileGuard, StructuredSessionFactory, SystemClock,
SystemMillisClock, TemplateToolProvider, TicketAssistantEnvironmentPreparer,
TicketToolProvider, TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator,
@ -900,6 +900,48 @@ impl AgentResumer for AppAgentResumer {
}
}
#[derive(Clone, Default)]
pub struct LateBoundManagedTempToolProvider {
inner: Arc<Mutex<Option<Arc<dyn ManagedTempToolProvider>>>>,
}
impl LateBoundManagedTempToolProvider {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn bind(&self, inner: Arc<dyn ManagedTempToolProvider>) {
*self.inner.lock().expect("managed temp tool mutex poisoned") = Some(inner);
}
}
#[async_trait]
impl ManagedTempToolProvider for LateBoundManagedTempToolProvider {
async fn handle_managed_temp_tool(
&self,
project: &Project,
requester: &str,
name: &str,
arguments: Value,
) -> Result<Value, infrastructure::ManagedTempToolError> {
let inner = self
.inner
.lock()
.expect("managed temp tool mutex poisoned")
.clone()
.ok_or_else(|| {
infrastructure::ManagedTempToolError::new(
"UNAVAILABLE",
"managed temp tool provider not bound".to_owned(),
)
})?;
inner
.handle_managed_temp_tool(project, requester, name, arguments)
.await
}
}
/// Everything a driving adapter needs at runtime.
///
/// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete
@ -1054,6 +1096,14 @@ pub struct BackendCore {
pub mark_issue_attachment_summarized: Arc<MarkIssueAttachmentSummarized>,
/// Import durable agent/chat attachments.
pub import_chat_attachments: Arc<ImportChatAttachments>,
/// Allocate an IdeA-managed temporary file/directory.
pub allocate_managed_temp: Arc<AllocateManagedTemp>,
/// Resolve an active IdeA-managed temporary handle.
pub resolve_managed_temp: Arc<ResolveManagedTemp>,
/// Release an IdeA-managed temporary handle.
pub release_managed_temp: Arc<ReleaseManagedTemp>,
/// Garbage-collect released or expired managed temporary allocations.
pub gc_managed_temp: Arc<GcManagedTemp>,
/// Read one persisted custom chat draft.
pub read_chat_draft: Arc<ReadChatDraft>,
/// Save one persisted custom chat draft.
@ -1084,6 +1134,8 @@ pub struct BackendCore {
pub(crate) ticket_tool_provider: Arc<dyn TicketToolProvider>,
/// MCP provider for the public `idea_template_*` tools.
pub(crate) template_tool_provider: Arc<dyn TemplateToolProvider>,
/// MCP provider for the public `idea_managed_temp_*` tools.
pub(crate) managed_temp_tool_provider: Arc<dyn ManagedTempToolProvider>,
/// Open an ephemeral AI assistant chat bound to one ticket.
pub open_ticket_assistant: Arc<OpenTicketAssistant>,
/// Close an ephemeral AI assistant chat bound to one ticket.
@ -1341,6 +1393,8 @@ pub struct BackendCore {
pub ticket_tool_binder: Arc<LateBoundTicketToolProvider>,
/// Late-bound concrete template MCP provider supplied by the driving adapter.
pub template_tool_binder: Arc<LateBoundTemplateToolProvider>,
/// Late-bound concrete managed-temp MCP provider supplied by the driving adapter.
pub managed_temp_tool_binder: Arc<LateBoundManagedTempToolProvider>,
}
/// Backend decision for attaching a UI subscriber to a background task.
@ -1753,6 +1807,8 @@ impl BackendCore {
let chat_attachment_store = Arc::new(FsChatAttachmentStore::new());
let chat_attachment_store_port =
Arc::clone(&chat_attachment_store) as Arc<dyn ChatAttachmentStore>;
let managed_temp_store = Arc::new(FsManagedTempStore::new(app_data_dir.clone()));
let managed_temp_store_port = Arc::clone(&managed_temp_store) as Arc<dyn ManagedTempStore>;
let chat_draft_store = Arc::new(FsChatDraftStore::new());
let chat_draft_store_port = Arc::clone(&chat_draft_store) as Arc<dyn ChatDraftStore>;
let create_issue = Arc::new(CreateIssue::new(
@ -1813,6 +1869,23 @@ impl BackendCore {
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&clock) as Arc<dyn Clock>,
));
let allocate_managed_temp = Arc::new(AllocateManagedTemp::new(
Arc::clone(&managed_temp_store_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&clock) as Arc<dyn Clock>,
));
let resolve_managed_temp = Arc::new(ResolveManagedTemp::new(
Arc::clone(&managed_temp_store_port),
Arc::clone(&clock) as Arc<dyn Clock>,
));
let release_managed_temp = Arc::new(ReleaseManagedTemp::new(
Arc::clone(&managed_temp_store_port),
Arc::clone(&clock) as Arc<dyn Clock>,
));
let gc_managed_temp = Arc::new(GcManagedTemp::new(
Arc::clone(&managed_temp_store_port),
Arc::clone(&clock) as Arc<dyn Clock>,
));
let read_chat_draft = Arc::new(ReadChatDraft::new(
Arc::clone(&store_port),
Arc::clone(&chat_draft_store_port),
@ -1885,6 +1958,9 @@ impl BackendCore {
let ticket_tool_provider: Arc<dyn TicketToolProvider> = ticket_tool_binder.clone();
let template_tool_binder = Arc::new(LateBoundTemplateToolProvider::new());
let template_tool_provider: Arc<dyn TemplateToolProvider> = template_tool_binder.clone();
let managed_temp_tool_binder = Arc::new(LateBoundManagedTempToolProvider::new());
let managed_temp_tool_provider: Arc<dyn ManagedTempToolProvider> =
managed_temp_tool_binder.clone();
let tool_policy_registry = Arc::new(ToolPolicyRegistry::new());
let tool_policy_store = Arc::clone(&tool_policy_registry) as Arc<dyn AgentToolPolicyStore>;
let system_permission_store = Arc::new(FsSystemPermissionStore::new(Arc::clone(&fs_port)));
@ -2958,6 +3034,7 @@ impl BackendCore {
Arc::clone(&mcp_tool_permission_store_port),
Arc::clone(&ticket_tool_provider),
Arc::clone(&template_tool_provider),
Arc::clone(&managed_temp_tool_provider),
)) as Arc<dyn ToolInvoker>);
let stop_live_agent = Arc::new(
@ -3061,6 +3138,10 @@ impl BackendCore {
read_issue_attachment,
mark_issue_attachment_summarized,
import_chat_attachments,
allocate_managed_temp,
resolve_managed_temp,
release_managed_temp,
gc_managed_temp,
read_chat_draft,
save_chat_draft,
clear_chat_draft,
@ -3076,6 +3157,7 @@ impl BackendCore {
unassign_ticket_from_sprint,
ticket_tool_provider,
template_tool_provider,
managed_temp_tool_provider,
open_ticket_assistant,
close_ticket_assistant,
tool_policy_registry,
@ -3183,6 +3265,7 @@ impl BackendCore {
plugin_manifest_validator: Arc::clone(&plugin_manifest_validator),
ticket_tool_binder,
template_tool_binder,
managed_temp_tool_binder,
}
}
@ -3274,6 +3357,7 @@ impl BackendCore {
.with_ready_sink(ready_sink)
.with_ticket_tools(Arc::clone(&self.ticket_tool_provider))
.with_template_tools(Arc::clone(&self.template_tool_provider))
.with_managed_temp_tools(Arc::clone(&self.managed_temp_tool_provider))
.with_tool_policies(Arc::clone(&self.tool_policy_registry))
.with_mcp_tool_permissions(Arc::clone(&self.mcp_tool_permission_store)),
endpoint,

View File

@ -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) => {
@ -407,7 +424,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;
@ -558,6 +575,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 issue_ref(raw: &str) -> IssueRef {
IssueRef::from_str(raw).unwrap()
}
@ -723,6 +771,7 @@ mod tests {
mcp_permissions(ProjectMcpToolPermissions::default()),
ticket_tools.clone(),
template_tools,
fake_managed_temp_tools(),
);
let denied = invoker
@ -788,6 +837,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"] {
@ -877,6 +927,7 @@ mod tests {
)),
Arc::new(FakeTicketTools::default()),
Arc::new(FakeTemplateTools::default()),
fake_managed_temp_tools(),
);
let tools = invoker
@ -916,6 +967,7 @@ mod tests {
)),
Arc::new(FakeTicketTools::default()),
Arc::new(FakeTemplateTools::default()),
fake_managed_temp_tools(),
);
let tools = invoker
@ -948,6 +1000,7 @@ mod tests {
mcp_permissions(allow_doc(agent, &["idea_ticket_update_carnet"])),
ticket_tools.clone(),
template_tools,
fake_managed_temp_tools(),
);
let result = invoker
@ -994,6 +1047,7 @@ mod tests {
mcp_permissions(allow_doc(agent, &["idea_template_update"])),
ticket_tools,
template_tools.clone(),
fake_managed_temp_tools(),
);
let result = invoker