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:
128
crates/application/tests/permission_usecases.rs
Normal file
128
crates/application/tests/permission_usecases.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use application::{
|
||||
ResolveAgentPermissions, ResolveAgentPermissionsInput, UpdateAgentPermissions,
|
||||
UpdateAgentPermissionsInput, UpdateProjectPermissions, UpdateProjectPermissionsInput,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::ids::{AgentId, ProjectId};
|
||||
use domain::ports::{PermissionStore, StoreError};
|
||||
use domain::project::{Project, ProjectPath};
|
||||
use domain::remote::RemoteRef;
|
||||
use domain::{PermissionSet, Posture, ProjectPermissions};
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakePermissionStore {
|
||||
doc: Mutex<ProjectPermissions>,
|
||||
saves: Mutex<usize>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PermissionStore for FakePermissionStore {
|
||||
async fn load_permissions(&self, _project: &Project) -> Result<ProjectPermissions, StoreError> {
|
||||
Ok(self.doc.lock().unwrap().clone())
|
||||
}
|
||||
|
||||
async fn save_permissions(
|
||||
&self,
|
||||
_project: &Project,
|
||||
permissions: &ProjectPermissions,
|
||||
) -> Result<(), StoreError> {
|
||||
*self.doc.lock().unwrap() = permissions.clone();
|
||||
*self.saves.lock().unwrap() += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn project() -> Project {
|
||||
Project::new(
|
||||
ProjectId::new_random(),
|
||||
"permissions",
|
||||
ProjectPath::new("/home/me/proj").unwrap(),
|
||||
RemoteRef::local(),
|
||||
1_700_000_000_000,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_project_permissions_replaces_defaults_and_persists() {
|
||||
let store = Arc::new(FakePermissionStore::default());
|
||||
let use_case = UpdateProjectPermissions::new(store.clone());
|
||||
|
||||
let out = use_case
|
||||
.execute(UpdateProjectPermissionsInput {
|
||||
project: project(),
|
||||
permissions: Some(PermissionSet::new(vec![], Posture::Ask)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
out.permissions.project_defaults.unwrap().fallback(),
|
||||
Posture::Ask
|
||||
);
|
||||
assert_eq!(*store.saves.lock().unwrap(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_agent_permissions_adds_and_removes_sparse_override() {
|
||||
let store = Arc::new(FakePermissionStore::default());
|
||||
let use_case = UpdateAgentPermissions::new(store.clone());
|
||||
let agent = AgentId::new_random();
|
||||
|
||||
use_case
|
||||
.execute(UpdateAgentPermissionsInput {
|
||||
project: project(),
|
||||
agent_id: agent,
|
||||
permissions: Some(PermissionSet::new(vec![], Posture::Deny)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.doc
|
||||
.lock()
|
||||
.unwrap()
|
||||
.agent_permissions(agent)
|
||||
.unwrap()
|
||||
.fallback(),
|
||||
Posture::Deny
|
||||
);
|
||||
|
||||
let out = use_case
|
||||
.execute(UpdateAgentPermissionsInput {
|
||||
project: project(),
|
||||
agent_id: agent,
|
||||
permissions: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(out.permissions.agent_permissions(agent).is_none());
|
||||
assert_eq!(*store.saves.lock().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_agent_permissions_returns_effective_policy() {
|
||||
let agent = AgentId::new_random();
|
||||
let store = Arc::new(FakePermissionStore {
|
||||
doc: Mutex::new(ProjectPermissions::new(
|
||||
Some(PermissionSet::new(vec![], Posture::Ask)),
|
||||
vec![],
|
||||
)),
|
||||
saves: Mutex::new(0),
|
||||
});
|
||||
let use_case = ResolveAgentPermissions::new(store);
|
||||
|
||||
let out = use_case
|
||||
.execute(ResolveAgentPermissionsInput {
|
||||
project: project(),
|
||||
agent_id: agent,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.effective.unwrap().fallback(), Posture::Ask);
|
||||
}
|
||||
Reference in New Issue
Block a user