fix(sandbox): writable roots Codex dérivées des permissions effectives du menu agent — #197 (QA verte)
This commit is contained in:
@ -7,7 +7,9 @@ use application::{
|
||||
StructuredSessions,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::permission::{EffectivePermissions, Posture};
|
||||
use domain::permission::{
|
||||
Capability, Effect, EffectivePermissions, PathScope, PermissionRule, Posture,
|
||||
};
|
||||
use domain::ports::{
|
||||
AgentSession, AgentSessionError, AgentSessionFactory, PermissionStore, ReplyStream,
|
||||
RuntimeError, SessionPlan, StoreError, StructuredProviderLaunchPolicy,
|
||||
@ -415,6 +417,15 @@ fn permission_doc(project_defaults: Option<PermissionSet>) -> ProjectPermissions
|
||||
ProjectPermissions::new(project_defaults, Vec::new())
|
||||
}
|
||||
|
||||
fn file_rule(capability: Capability, effect: Effect, patterns: &[&str]) -> PermissionRule {
|
||||
PermissionRule::file(
|
||||
capability,
|
||||
effect,
|
||||
PathScope::new(patterns.iter().map(|pattern| (*pattern).to_owned())).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn open_then_close_ticket_assistant_sets_policy_injects_context_and_emits_events() {
|
||||
let issue = issue(7);
|
||||
@ -642,6 +653,7 @@ async fn codex_ticket_assistant_uses_project_network_permission_for_structured_p
|
||||
let factory = Arc::new(FakeFactory::default());
|
||||
let system_doc =
|
||||
ProjectSystemPermissions::new(Some(SystemPermissionSet::new(policy)), Vec::new());
|
||||
let permission_doc = permission_doc(Some(PermissionSet::new(Vec::new(), Posture::Allow)));
|
||||
let open = OpenTicketAssistant::new(
|
||||
Arc::new(FakeIssues {
|
||||
issue: issue.clone(),
|
||||
@ -656,6 +668,7 @@ async fn codex_ticket_assistant_uses_project_network_permission_for_structured_p
|
||||
policies,
|
||||
events,
|
||||
)
|
||||
.with_permission_store(Arc::new(FakePermissionStore(permission_doc)))
|
||||
.with_system_permission_store(Arc::new(FakeSystemPermissionStore(system_doc)));
|
||||
|
||||
open.execute(OpenTicketAssistantInput {
|
||||
@ -678,3 +691,98 @@ async fn codex_ticket_assistant_uses_project_network_permission_for_structured_p
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_ticket_assistant_uses_project_default_write_roots_without_overgranting() {
|
||||
let issue = issue(7);
|
||||
let profile_id = ProfileId::from_uuid(Uuid::from_u128(9));
|
||||
let factory = Arc::new(FakeFactory::default());
|
||||
let open = OpenTicketAssistant::new(
|
||||
Arc::new(FakeIssues {
|
||||
issue: issue.clone(),
|
||||
}),
|
||||
Arc::new(FakeProfiles {
|
||||
profile: profile(profile_id).with_structured_adapter(StructuredAdapter::Codex),
|
||||
}),
|
||||
Arc::new(FakeAssistantContext::default()),
|
||||
Arc::new(FakeEnvironmentPreparer::default()),
|
||||
factory.clone(),
|
||||
Arc::new(StructuredSessions::new()),
|
||||
Arc::new(FakePolicies::default()),
|
||||
Arc::new(SpyBus::default()),
|
||||
)
|
||||
.with_permission_store(Arc::new(FakePermissionStore(permission_doc(Some(
|
||||
PermissionSet::new(
|
||||
vec![file_rule(Capability::Write, Effect::Allow, &["out/**"])],
|
||||
Posture::Deny,
|
||||
),
|
||||
)))));
|
||||
|
||||
open.execute(OpenTicketAssistantInput {
|
||||
project: project(),
|
||||
issue_ref: issue.reference(),
|
||||
profile_id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let starts = factory.starts.lock().unwrap();
|
||||
assert_eq!(
|
||||
starts[0].6,
|
||||
Some(StructuredProviderLaunchPolicy::Codex {
|
||||
sandbox_mode: "workspace-write".to_owned(),
|
||||
writable_roots: vec!["/tmp/project/out".to_owned()],
|
||||
network_access: false,
|
||||
}),
|
||||
"ticket assistant Codex policy must use permission-derived write roots"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_ticket_assistant_broad_write_with_subpath_deny_fails_closed() {
|
||||
let issue = issue(7);
|
||||
let profile_id = ProfileId::from_uuid(Uuid::from_u128(9));
|
||||
let factory = Arc::new(FakeFactory::default());
|
||||
let open = OpenTicketAssistant::new(
|
||||
Arc::new(FakeIssues {
|
||||
issue: issue.clone(),
|
||||
}),
|
||||
Arc::new(FakeProfiles {
|
||||
profile: profile(profile_id).with_structured_adapter(StructuredAdapter::Codex),
|
||||
}),
|
||||
Arc::new(FakeAssistantContext::default()),
|
||||
Arc::new(FakeEnvironmentPreparer::default()),
|
||||
factory.clone(),
|
||||
Arc::new(StructuredSessions::new()),
|
||||
Arc::new(FakePolicies::default()),
|
||||
Arc::new(SpyBus::default()),
|
||||
)
|
||||
.with_permission_store(Arc::new(FakePermissionStore(permission_doc(Some(
|
||||
PermissionSet::new(
|
||||
vec![
|
||||
file_rule(Capability::Write, Effect::Allow, &["**"]),
|
||||
file_rule(Capability::Write, Effect::Deny, &["secret/**"]),
|
||||
],
|
||||
Posture::Deny,
|
||||
),
|
||||
)))));
|
||||
|
||||
open.execute(OpenTicketAssistantInput {
|
||||
project: project(),
|
||||
issue_ref: issue.reference(),
|
||||
profile_id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let starts = factory.starts.lock().unwrap();
|
||||
assert_eq!(
|
||||
starts[0].6,
|
||||
Some(StructuredProviderLaunchPolicy::Codex {
|
||||
sandbox_mode: "read-only".to_owned(),
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
}),
|
||||
"ticket assistant must fail closed when Codex cannot represent a subpath write deny"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user