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

@ -7,6 +7,7 @@
mod context;
mod embedder;
mod memory;
mod permission;
mod profile;
mod project;
mod skill;
@ -24,6 +25,7 @@ pub use embedder::{
RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
};
pub use memory::{index_token_size, FsMemoryStore, NaiveMemoryRecall};
pub use permission::FsPermissionStore;
pub use profile::{FsEmbedderProfileStore, FsProfileStore};
pub use project::FsProjectStore;
pub use skill::FsSkillStore;

View File

@ -0,0 +1,65 @@
//! Filesystem-backed [`PermissionStore`] for project agent permissions.
use std::sync::Arc;
use async_trait::async_trait;
use domain::ports::{FileSystem, FsError, PermissionStore, RemotePath, StoreError};
use domain::{Project, ProjectPermissions};
const PERMISSIONS_FILE: &str = "permissions.json";
/// JSON-file implementation for `<project>/.ideai/permissions.json`.
#[derive(Clone)]
pub struct FsPermissionStore {
fs: Arc<dyn FileSystem>,
}
impl FsPermissionStore {
/// Builds the store from an injected filesystem port.
#[must_use]
pub fn new(fs: Arc<dyn FileSystem>) -> Self {
Self { fs }
}
fn path(project: &Project) -> RemotePath {
let root = project.root.as_str().trim_end_matches(['/', '\\']);
RemotePath::new(format!("{root}/.ideai/{PERMISSIONS_FILE}"))
}
async fn ensure_ideai(&self, project: &Project) -> Result<(), StoreError> {
let root = project.root.as_str().trim_end_matches(['/', '\\']);
self.fs
.create_dir_all(&RemotePath::new(format!("{root}/.ideai")))
.await
.map_err(|e| StoreError::Io(e.to_string()))
}
}
#[async_trait]
impl PermissionStore for FsPermissionStore {
async fn load_permissions(&self, project: &Project) -> Result<ProjectPermissions, StoreError> {
match self.fs.read(&Self::path(project)).await {
Ok(bytes) => {
serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
}
Err(FsError::NotFound(_)) => Ok(ProjectPermissions::default()),
Err(e) => Err(StoreError::Io(e.to_string())),
}
}
async fn save_permissions(
&self,
project: &Project,
permissions: &ProjectPermissions,
) -> Result<(), StoreError> {
self.ensure_ideai(project).await?;
let mut bytes = serde_json::to_vec_pretty(permissions)
.map_err(|e| StoreError::Serialization(e.to_string()))?;
bytes.push(b'\n');
self.fs
.write(&Self::path(project), &bytes)
.await
.map_err(|e| StoreError::Io(e.to_string()))
}
}