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

@ -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(),