feat(permissions): voie projection CLI (LP0→LP3) + checkpoint Codex/input

Jalon vert regroupant deux chantiers entrelacés dans le working tree,
indissociables au niveau fichier mais tous deux verts (cargo test
--workspace + tests frontend permissions au vert).

Permissions — voie « projection CLI » (advisory), complète :
- LP0 domaine pur : modèle PermissionSet/EffectivePermissions, resolve
  deny-wins + postures Allow<Ask<Deny (crates/domain/src/permission.rs).
- LP1 store : FsPermissionStore (.ideai/permissions.json).
- LP2 use cases : Get/Update project, Update agent override, Resolve.
- LP3 projecteurs Claude/Codex (settings.local.json / config.toml),
  câblage launch-path + PermissionProjectorRegistry, nettoyage des
  fichiers Replace orphelins au swap de profil (LP3-4), composition root
  + commandes Tauri, UI PermissionsPanel (projet + override agent).
- ports.rs : PermissionStore + FileSystem::remove_file (cleanup au swap).

Reste ouvert (hors scope, marqué dans le code) : LP4 enforcement OS
airtight (Landlock fichiers) + résumé de permissions injecté.

Inclut aussi le chantier Codex/input/sessions structurées en cours
(McpConfigStrategy, StructuredAdapter, gestion d'input) partageant les
mêmes fichiers (lifecycle.rs, commands.rs, dto.rs, state.rs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 20:39:18 +02:00
parent 46492506e1
commit 27597eb64e
41 changed files with 6513 additions and 269 deletions

View File

@ -18,9 +18,10 @@ use application::{
ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput,
McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadMemoryIndexInput,
ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput, RenameLayoutInput,
ResolveMemoryLinksInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
SyncAgentWithTemplateInput, UnassignSkillFromAgentInput, UpdateAgentContextInput,
UpdateMemoryInput, UpdateProjectContextInput, UpdateSkillInput,
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
SnapshotRunningAgentsInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
UpdateAgentContextInput, UpdateAgentPermissionsInput, UpdateMemoryInput,
UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
};
use domain::ports::PtyHandle;
@ -34,19 +35,22 @@ use crate::dto::{
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto,
GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto,
GitStageRequestDto, GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto,
InspectConversationRequestDto, InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto,
LayoutOperationDto, ListLayoutsDto, LiveAgentListDto, MemoryDto, MemoryIndexDto,
MemoryLinksDto, MemoryListDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto,
ProjectListDto, ReadAgentContextResponseDto, ReattachChatDto, ReattachResultDto,
EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto,
ErrorDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto,
GitCommitDto,
GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
ProjectPermissionsDto, ReadAgentContextResponseDto, ReattachChatDto, ReattachResultDto,
RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveProfileRequestDto,
SetActiveLayoutRequestDto, SkillDto, SkillListDto, SyncAgentWithTemplateRequestDto,
SyncResultDto, TemplateDto, TemplateListDto, TerminalClosedDto, TerminalSessionDto,
UnassignSkillRequestDto, UpdateAgentContextRequestDto, UpdateMemoryRequestDto,
UpdateProjectContextRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto, SkillListDto,
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
WriteTerminalRequestDto,
};
use crate::pty::{PtyBridge, PtyChunk};
@ -208,6 +212,87 @@ pub async fn update_project_context(
.map_err(ErrorDto::from)
}
/// `get_project_permissions` — read `.ideai/permissions.json`.
///
/// # Errors
/// Returns an [`ErrorDto`] on invalid project id or store failure.
#[tauri::command]
pub async fn get_project_permissions(
project_id: String,
state: State<'_, AppState>,
) -> Result<ProjectPermissionsDto, ErrorDto> {
let project = resolve_project(&project_id, &state).await?;
state
.get_project_permissions
.execute(application::GetProjectPermissionsInput { project })
.await
.map(|out| ProjectPermissionsDto(out.permissions))
.map_err(ErrorDto::from)
}
/// `update_project_permissions` — replace project default permissions.
///
/// # Errors
/// Returns an [`ErrorDto`] on invalid project id or store failure.
#[tauri::command]
pub async fn update_project_permissions(
request: UpdateProjectPermissionsRequestDto,
state: State<'_, AppState>,
) -> Result<ProjectPermissionsDto, ErrorDto> {
let project = resolve_project(&request.project_id, &state).await?;
state
.update_project_permissions
.execute(UpdateProjectPermissionsInput {
project,
permissions: request.permissions,
})
.await
.map(|out| ProjectPermissionsDto(out.permissions))
.map_err(ErrorDto::from)
}
/// `update_agent_permissions` — replace or remove one agent override.
///
/// # Errors
/// Returns an [`ErrorDto`] on invalid ids or store failure.
#[tauri::command]
pub async fn update_agent_permissions(
request: UpdateAgentPermissionsRequestDto,
state: State<'_, AppState>,
) -> Result<ProjectPermissionsDto, ErrorDto> {
let project = resolve_project(&request.project_id, &state).await?;
let agent_id = parse_agent_id(&request.agent_id)?;
state
.update_agent_permissions
.execute(UpdateAgentPermissionsInput {
project,
agent_id,
permissions: request.permissions,
})
.await
.map(|out| ProjectPermissionsDto(out.permissions))
.map_err(ErrorDto::from)
}
/// `resolve_agent_permissions` — resolve project defaults plus agent override.
///
/// # Errors
/// Returns an [`ErrorDto`] on invalid ids or store failure.
#[tauri::command]
pub async fn resolve_agent_permissions(
request: ResolveAgentPermissionsRequestDto,
state: State<'_, AppState>,
) -> Result<Option<EffectivePermissionsDto>, ErrorDto> {
let project = resolve_project(&request.project_id, &state).await?;
let agent_id = parse_agent_id(&request.agent_id)?;
state
.resolve_agent_permissions
.execute(ResolveAgentPermissionsInput { project, agent_id })
.await
.map(|out| out.effective.map(EffectivePermissionsDto))
.map_err(ErrorDto::from)
}
// ---------------------------------------------------------------------------
// Terminals (L3)
// ---------------------------------------------------------------------------
@ -1263,6 +1348,29 @@ pub async fn delegation_delivered(
Ok(())
}
/// `set_front_attached` — the write-portal reports whether a **frontend terminal cell**
/// is mounted for an agent (mount ⇒ `true`, unmount ⇒ `false`).
///
/// Routes to [`OrchestratorService::set_agent_front_attached`]. This is what lets the
/// mediator deliver a turn to a **headless** (background-delegated, cell-less) agent by
/// writing its PTY itself: with no mounted cell nobody consumes `DelegationReady`, so
/// the task would otherwise be lost (a delegated agent that never receives — and never
/// answers — its task). An agent **with** a cell keeps the frontend write-portal path.
///
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID`) for a malformed agent id.
#[tauri::command]
pub async fn set_front_attached(
request: FrontAttachedRequestDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let agent_id = parse_agent_id(&request.agent_id)?;
state
.orchestrator_service
.set_agent_front_attached(agent_id, request.attached);
Ok(())
}
/// `reattach_agent_chat` — re-bind a view to a **still-living** structured session
/// without re-sending or re-spawning it (ARCHITECTURE §17.6/§17.7).
///

View File

@ -1066,7 +1066,7 @@ use application::{
ChangeAgentProfileOutput, CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput,
ListAgentsOutput, ReadAgentContextOutput,
};
use domain::{Agent, TerminalSession};
use domain::{Agent, EffectivePermissions, PermissionSet, ProjectPermissions, TerminalSession};
/// An agent crossing the wire. [`Agent`] already serialises camelCase
/// (`id`, `name`, `contextPath`, `profileId`, `origin` tagged, `synchronized`),
@ -1135,6 +1135,52 @@ pub struct UpdateAgentContextRequestDto {
pub content: String,
}
// ---------------------------------------------------------------------------
// Permissions (LP1)
// ---------------------------------------------------------------------------
/// Full project permission document crossing the wire.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProjectPermissionsDto(pub ProjectPermissions);
/// Effective permissions crossing the wire.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct EffectivePermissionsDto(pub EffectivePermissions);
/// Request DTO for updating project default permissions.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateProjectPermissionsRequestDto {
/// Id of the owning project.
pub project_id: String,
/// New project defaults. `null` removes defaults.
pub permissions: Option<PermissionSet>,
}
/// Request DTO for updating one agent permission override.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateAgentPermissionsRequestDto {
/// Id of the owning project.
pub project_id: String,
/// Target agent id.
pub agent_id: String,
/// New override. `null` removes the override.
pub permissions: Option<PermissionSet>,
}
/// Request DTO for resolving one agent's effective permissions.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolveAgentPermissionsRequestDto {
/// Id of the owning project.
pub project_id: String,
/// Target agent id.
pub agent_id: String,
}
/// Request DTO for `update_project_context`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -1224,6 +1270,21 @@ pub struct DeliveredDelegationRequestDto {
pub ticket: String,
}
/// Request DTO for `set_front_attached`: the write-portal of an agent cell reports
/// whether a **frontend terminal cell is mounted** for `agentId` (`true` on mount,
/// `false` on unmount). The mediator uses it to choose, at delivery time, between
/// publishing `DelegationReady` (a cell will write it) and writing the turn into the
/// PTY itself (headless/background-delegated agent with no cell). The frontend sends
/// `{ request: { agentId, attached } }`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontAttachedRequestDto {
/// Id of the agent whose terminal cell mounted/unmounted.
pub agent_id: String,
/// `true` when the cell's write-portal is now active, `false` on teardown.
pub attached: bool,
}
/// Request DTO for `change_agent_profile` (§15.1): hot-swap an agent's runtime
/// profile, optionally relaunching its live session in place.
#[derive(Debug, Clone, Deserialize)]

View File

@ -127,6 +127,10 @@ pub fn run() {
commands::list_projects,
commands::read_project_context,
commands::update_project_context,
commands::get_project_permissions,
commands::update_project_permissions,
commands::update_agent_permissions,
commands::resolve_agent_permissions,
commands::open_terminal,
commands::write_terminal,
commands::resize_terminal,
@ -163,6 +167,7 @@ pub fn run() {
commands::agent_send,
commands::interrupt_agent,
commands::delegation_delivered,
commands::set_front_attached,
commands::reattach_agent_chat,
commands::close_agent_session,
commands::list_resumable_agents,

View File

@ -17,23 +17,25 @@ use application::{
CreateLayout, CreateMemory, CreateProject, CreateSkill, CreateTemplate, DeleteAgent,
DeleteEmbedderProfile, DeleteLayout, DeleteMemory, DeleteProfile, DeleteSkill, DeleteTemplate,
DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion,
FirstRunState, GetMemory, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog,
GitStage, GitStatus, GitUnstage, HealthUseCase, InspectConversation, LaunchAgent, ListAgents,
ListAgentsInput, ListEmbedderProfiles, ListLayouts, ListMemories, ListProfiles, ListProjects,
ListResumableAgents, ListSkills, ListTemplates, LiveAgentRegistry, LoadLayout, McpRuntime,
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
OrchestratorService, ReadAgentContext, ReadMemoryIndex, ReadProjectContext, RecallMemory,
ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout,
ResizeTerminal, ResolveMemoryLinks, SaveEmbedderProfile, SaveProfile, SetActiveLayout,
SnapshotRunningAgents, StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate,
TerminalSessions, UnassignSkillFromAgent, UpdateAgentContext, UpdateMemory,
UpdateProjectContext, UpdateSkill, UpdateTemplate, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
FirstRunState, GetMemory, GetProjectPermissions, GitBranches, GitCheckout, GitCommit, GitGraph,
GitInit, GitLog, GitStage, GitStatus, GitUnstage, HealthUseCase, InspectConversation,
LaunchAgent, ListAgents, ListAgentsInput, ListEmbedderProfiles, ListLayouts, ListMemories,
ListProfiles, ListProjects, ListResumableAgents, ListSkills, ListTemplates, LiveAgentRegistry,
LoadLayout, McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
PermissionProjectorRegistry,
OpenTerminal, OrchestratorService, ReadAgentContext, ReadMemoryIndex, ReadProjectContext,
RecallMemory, ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles,
RenameLayout, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks, SaveEmbedderProfile,
SaveProfile, SetActiveLayout, SnapshotRunningAgents, StructuredSessions, SuggestedThisSession,
SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent, UpdateAgentContext,
UpdateAgentPermissions, UpdateMemory, UpdateProjectContext, UpdateProjectPermissions,
UpdateSkill, UpdateTemplate, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
};
use domain::ports::{
AgentContextStore, AgentRuntime, AgentSessionFactory, Clock, Embedder, EmbedderEnvInspector,
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
MemoryRecall, MemoryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, SkillStore,
TemplateStore,
MemoryRecall, MemoryStore, PermissionStore, ProcessSpawner, ProfileStore, ProjectStore,
PtyPort, SkillStore, TemplateStore,
};
use domain::profile::{
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
@ -44,10 +46,11 @@ use serde_json::{json, Map, Value};
use uuid::Uuid;
use infrastructure::{
embedder_from_profile, AdaptiveMemoryRecall, ClaudeTranscriptInspector, CliAgentRuntime,
embedder_from_profile, AdaptiveMemoryRecall, ClaudePermissionProjector,
ClaudeTranscriptInspector, CliAgentRuntime, CodexPermissionProjector,
EmbedderEnvProbe, FsConversationLog, FsEmbedderProfileStore, FsEmbedderPromptStore,
FsHandoffStore, FsMemoryStore, FsOrchestratorWatcher, FsProfileStore, FsProjectStore,
FsProviderSessionStore, FsSkillStore, FsTemplateStore, Git2Repository,
FsHandoffStore, FsMemoryStore, FsOrchestratorWatcher, FsPermissionStore, FsProfileStore,
FsProjectStore, FsProviderSessionStore, FsSkillStore, FsTemplateStore, Git2Repository,
HeuristicHandoffSummarizer, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
LocalFileSystem, LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall,
OrchestratorWatchHandle, PortablePtyAdapter, StructuredSessionFactory, SystemClock,
@ -230,6 +233,14 @@ pub struct AppState {
pub inspect_conversation: Arc<InspectConversation>,
/// Project registry — used by agent commands to resolve a `Project` from an id.
pub project_store: Arc<dyn ProjectStore>,
/// Read the project permission document.
pub get_project_permissions: Arc<GetProjectPermissions>,
/// Update project-level default permissions.
pub update_project_permissions: Arc<UpdateProjectPermissions>,
/// Update one agent permission override.
pub update_agent_permissions: Arc<UpdateAgentPermissions>,
/// Resolve effective permissions for one agent.
pub resolve_agent_permissions: Arc<ResolveAgentPermissions>,
// --- Windows (L10) ---
/// Detach a tab into a new OS window (persists the workspace topology).
pub move_tab: Arc<MoveTabToNewWindow>,
@ -498,6 +509,10 @@ impl AppState {
let contexts = Arc::new(IdeaiContextStore::new(Arc::clone(&fs_port)));
let contexts_port = Arc::clone(&contexts) as Arc<dyn AgentContextStore>;
// --- Project permissions (LP1) ---
let permission_store = Arc::new(FsPermissionStore::new(Arc::clone(&fs_port)));
let permission_store_port = Arc::clone(&permission_store) as Arc<dyn PermissionStore>;
// --- Skill store (L12) ---
// Global skills live in the machine-local app-data dir; project skills are
// resolved per call from each project's `.ideai/` (so one store serves all
@ -632,6 +647,20 @@ impl AppState {
// PTH : tout profil (Claude/Codex inclus) ouvre une cellule terminal. Le code
// `launch_structured` reste en place (mort-code retiré au lot de nettoyage B-6).
let _session_factory = session_factory; // décâblé en B-2 (nettoyage B-6)
// --- Permission projectors (lot LP3-5) ---
// UN seul registre, source unique de vérité, injecté à l'identique dans
// `LaunchAgent` (projection au lancement) ET `ChangeAgentProfile` (nettoyage des
// fichiers orphelins au swap). Les deux projecteurs concrets vivent dans
// l'infrastructure, keyés par leur `ProjectorKey` via `with(...)`.
let permission_projectors = Arc::new(
PermissionProjectorRegistry::new()
.with(Arc::new(ClaudePermissionProjector)
as Arc<dyn domain::PermissionProjector>)
.with(Arc::new(CodexPermissionProjector)
as Arc<dyn domain::PermissionProjector>),
);
let launch_agent = Arc::new(
LaunchAgent::new(
Arc::clone(&contexts_port),
@ -646,6 +675,7 @@ impl AppState {
Arc::clone(&memory_recall_port),
Some(Arc::clone(&check_embedder_suggestion)),
)
.with_permission_store(Arc::clone(&permission_store_port))
// Reprise conversationnelle (lot P7) : à chaque (re)lancement, si la cellule
// porte une conversation et qu'un handoff existe (`<root>/.ideai/conversations/`),
// son résumé est réinjecté dans le convention file. Best-effort, additif :
@ -658,7 +688,11 @@ impl AppState {
// `<root>/.ideai/conversations/providers.json`. Best-effort, additif : une
// écriture en échec / pas d'id moteur ⇒ lancement normal, aucune écriture.
.with_provider_session_provider(Arc::new(AppProviderSessionProvider)
as Arc<dyn application::ProviderSessionProvider>),
as Arc<dyn application::ProviderSessionProvider>)
// Projection des permissions au (re)lancement (lot LP3-5) : avec le
// permission store câblé ci-dessus, `resolve` a une source ⇒ le projecteur
// du profil matérialise la config de permission de la CLI dans le run dir.
.with_permission_projectors(Arc::clone(&permission_projectors)),
);
// Hot-swap an agent's runtime profile (§15.1). Reuses the shared context/
@ -676,7 +710,10 @@ impl AppState {
Arc::clone(&launch_agent),
Arc::clone(&events_port),
)
.with_structured(Arc::clone(&structured_sessions)),
.with_structured(Arc::clone(&structured_sessions))
// Même registre que `LaunchAgent` (lot LP3-5) : au swap cross-profile, on
// nettoie les fichiers `Replace` orphelins de l'ancien profil avant relance.
.with_permission_projectors(Arc::clone(&permission_projectors)),
);
// Read-only inventory of resumable agent cells (§15.2). Reuses the shared
@ -707,6 +744,18 @@ impl AppState {
));
let project_store = Arc::clone(&store_port);
let get_project_permissions = Arc::new(GetProjectPermissions::new(Arc::clone(
&permission_store_port,
)));
let update_project_permissions = Arc::new(UpdateProjectPermissions::new(Arc::clone(
&permission_store_port,
)));
let update_agent_permissions = Arc::new(UpdateAgentPermissions::new(Arc::clone(
&permission_store_port,
)));
let resolve_agent_permissions = Arc::new(ResolveAgentPermissions::new(Arc::clone(
&permission_store_port,
)));
// --- Template store + use cases (L7) ---
let template_store = Arc::new(FsTemplateStore::new(
@ -856,8 +905,7 @@ impl AppState {
}
});
}
let input_mediator =
Arc::clone(&mediated_inbox) as Arc<dyn domain::input::InputMediator>;
let input_mediator = Arc::clone(&mediated_inbox) as Arc<dyn domain::input::InputMediator>;
// Registre des conversations par paire (cadrage C3) : un fil par paire, session
// vivante keyée par conversation (lève l'ambiguïté session/agent).
let conversation_registry = Arc::new(InMemoryConversationRegistry::new())
@ -957,6 +1005,10 @@ impl AppState {
list_resumable_agents,
inspect_conversation,
project_store,
get_project_permissions,
update_project_permissions,
update_agent_permissions,
resolve_agent_permissions,
create_template,
update_template,
list_templates,
@ -1069,9 +1121,20 @@ impl AppState {
// The project-id string the handshake guard compares against: the same
// hyphen-free hex form the endpoint encodes, which M5d's `--project` reuses.
let project_id = project.id.as_uuid().simple().to_string();
// Readiness de démarrage : quand le pont MCP d'un agent se connecte (initialize),
// libère son éventuel 1er tour différé (fix race cold-launch via signal MCP).
// L'id arrive en hex (handshake `requester`) ⇒ on le parse en AgentId ici (la
// composition root est la seule à connaître la frontière infra↔domaine).
let service_for_ready = Arc::clone(&self.orchestrator_service);
let ready_sink: Arc<dyn Fn(&str) + Send + Sync> = Arc::new(move |requester: &str| {
if let Ok(uuid) = Uuid::parse_str(requester) {
service_for_ready.release_agent_cold_start(AgentId::from_uuid(uuid));
}
});
let handle = McpServerHandle::start(
McpServer::new(Arc::clone(&self.orchestrator_service), project.clone())
.with_events(events),
.with_events(events)
.with_ready_sink(ready_sink),
endpoint,
listener,
project_id,
@ -1355,7 +1418,10 @@ fn codex_mcp_config_target(profile: &AgentProfile) -> Option<(&str, &str)> {
/// l'encodeur TOML partagé [`domain::McpServerWiring::to_config_toml`] (D2) — même
/// source de wiring que la déclaration `.mcp.json` Claude, donc zéro dérive.
fn mcp_server_entry_toml(profile: &AgentProfile, runtime: Option<&McpRuntime>) -> String {
let transport = profile.mcp.as_ref().map_or(McpTransport::Stdio, |m| m.transport);
let transport = profile
.mcp
.as_ref()
.map_or(McpTransport::Stdio, |m| m.transport);
let (command, args) = match runtime {
Some(rt) => (
rt.exe.clone(),