fix(permissions): validate network access flow

This commit is contained in:
2026-07-26 10:52:46 +02:00
parent 3047dc9195
commit 13fb538880
70 changed files with 4784 additions and 158 deletions

View File

@ -2,11 +2,15 @@
use std::sync::Arc;
use domain::ports::{AgentSessionFactory, SessionPlan, StructuredSessionEnvironmentPreparer};
use domain::ports::{
AgentSessionFactory, SessionPlan, StructuredProviderLaunchPolicy,
StructuredSessionEnvironmentPreparer, SystemPermissionStore,
};
use domain::profile::StructuredAdapter;
use domain::AgentProfile;
use domain::{
AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent, EventBus,
IssueRef, IssueStore, ProfileId, ProfileStore, Project, SessionId,
IssueRef, IssueStore, NetworkPolicy, ProfileId, ProfileStore, Project, SessionId,
};
use crate::terminal::StructuredSessions;
@ -22,6 +26,7 @@ pub struct OpenTicketAssistant {
structured: Arc<StructuredSessions>,
policies: Arc<dyn AgentToolPolicyStore>,
events: Arc<dyn EventBus>,
system_permissions: Option<Arc<dyn SystemPermissionStore>>,
}
/// Input for [`OpenTicketAssistant`].
@ -68,9 +73,17 @@ impl OpenTicketAssistant {
structured,
policies,
events,
system_permissions: None,
}
}
/// Wires the project system permission store used for assistant launches.
#[must_use]
pub fn with_system_permission_store(mut self, store: Arc<dyn SystemPermissionStore>) -> Self {
self.system_permissions = Some(store);
self
}
/// Executes the open flow.
///
/// Reopening an already-live assistant for the same ticket replaces the old
@ -136,6 +149,9 @@ impl OpenTicketAssistant {
return Err(AppError::from(err));
}
};
let structured_policy = self
.build_ticket_assistant_structured_policy(&input.project, &profile, &environment)
.await?;
let session = self
.factory
.start(
@ -146,6 +162,9 @@ impl OpenTicketAssistant {
Some(&requester),
&environment.env,
None,
structured_policy
.as_ref()
.or(environment.structured_policy.as_ref()),
)
.await
.map_err(|err| {
@ -180,6 +199,38 @@ impl OpenTicketAssistant {
issue_ref: input.issue_ref,
})
}
async fn build_ticket_assistant_structured_policy(
&self,
project: &Project,
profile: &AgentProfile,
environment: &domain::ports::StructuredSessionEnvironment,
) -> Result<Option<StructuredProviderLaunchPolicy>, AppError> {
if profile.structured_adapter != Some(StructuredAdapter::Codex) {
return Ok(environment.structured_policy.clone());
}
let network = self.resolve_project_default_network(project).await?;
Ok(Some(StructuredProviderLaunchPolicy::Codex {
sandbox_mode: "workspace-write".to_owned(),
writable_roots: vec![project.root.as_str().to_owned()],
network_access: codex_network_access(network),
}))
}
async fn resolve_project_default_network(
&self,
project: &Project,
) -> Result<Option<NetworkPolicy>, AppError> {
let Some(store) = &self.system_permissions else {
return Ok(None);
};
let doc = store.load_system_permissions(project).await?;
Ok(doc.project_default.and_then(|set| set.network))
}
}
fn codex_network_access(network: Option<NetworkPolicy>) -> bool {
matches!(network, Some(NetworkPolicy::Allow))
}
fn ticket_assistant_requester(project: &Project, issue_ref: IssueRef) -> String {