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

@ -922,11 +922,12 @@ pub trait PermissionProjector: Send + Sync {
/// Computes the projection.
///
/// `eff == None` ⇒ the **empty** projection ([`PermissionProjection::empty`]):
/// we keep the CLI's native prompting (the product invariant of [`resolve`]).
/// `eff == None` means no filesystem/bash policy is projected. Implementations
/// may still project orthogonal system permissions, such as network env.
fn project(
&self,
eff: Option<&EffectivePermissions>,
network: Option<crate::system_permissions::NetworkPolicy>,
ctx: &ProjectionContext,
) -> PermissionProjection;

View File

@ -146,6 +146,28 @@ pub struct StructuredSessionEnvironment {
pub cwd: ProjectPath,
/// Environment variables to pass to the structured session.
pub env: Vec<(String, String)>,
/// Provider-specific launch policy to pass to the structured session.
pub structured_policy: Option<StructuredProviderLaunchPolicy>,
}
/// Provider-specific launch policy for structured/headless sessions.
///
/// This is intentionally separate from [`PermissionProjection`]: the projection is
/// a generic CLI advisory plan (files/argv/env) while structured adapters may need
/// a smaller, command-compatible contract. In particular, `codex exec` accepts
/// `--sandbox` and `--add-dir`, but must never receive the interactive
/// `--ask-for-approval` flag.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StructuredProviderLaunchPolicy {
/// Policy subset supported by `codex exec`.
Codex {
/// Codex sandbox mode (`read-only`, `workspace-write`, ...).
sandbox_mode: String,
/// Workspace roots to pass as repeated `--add-dir` values.
writable_roots: Vec<String>,
/// Whether Codex workspace-write sandbox network access must be enabled.
network_access: bool,
},
}
/// Errors returned while preparing the IdeA-owned ticket assistant context.
@ -1118,6 +1140,13 @@ pub trait AgentSessionFactory: Send + Sync {
/// franchit le port en tant que **valeur domaine** ([`crate::sandbox::SandboxPlan`]) ;
/// l'enforcer concret reste côté infra (injecté par instance dans la fabrique).
///
/// `structured_policy` est une projection optionnelle, provider-spécifique et
/// compatible avec la commande structurée. Elle ne remplace pas `sandbox` :
/// `sandbox` reste l'autorité OS, tandis que cette politique configure le CLI.
/// Elle est volontairement séparée de [`PermissionProjector`] pour éviter de
/// faire porter au projector le contrat exact de sous-commandes comme
/// `codex exec`.
///
/// # Errors
/// [`AgentSessionError::Start`] si la CLI/SDK est indisponible ou le mode
/// structuré ne peut s'initialiser.
@ -1130,6 +1159,7 @@ pub trait AgentSessionFactory: Send + Sync {
requester: Option<&str>,
env: &[(String, String)],
sandbox: Option<&crate::sandbox::SandboxPlan>,
structured_policy: Option<&StructuredProviderLaunchPolicy>,
) -> Result<Arc<dyn AgentSession>, AgentSessionError>;
}

View File

@ -1,8 +1,8 @@
//! System-level agent permissions, distinct from filesystem/bash permissions.
//!
//! This model stores the policy the user wants IdeA to apply. It does not claim
//! that an external assistant runtime can be elevated live: resolution combines
//! the wanted policy with a read-only runtime probe.
//! that an external assistant runtime can be elevated live: resolution exposes
//! the wanted policy separately from the observed runtime state.
use serde::{Deserialize, Serialize};
@ -235,11 +235,22 @@ pub struct RuntimePermissionSnapshot {
pub effective_network: NetworkPolicy,
/// Runtime lock details.
pub runtime_lock: RuntimeLock,
/// Control state exposed to the UI.
pub control: SystemPermissionControl,
/// Runtime control state, distinct from editing the persisted wanted policy.
pub runtime_control: SystemPermissionControl,
}
impl RuntimePermissionSnapshot {
/// Snapshot for an agent with no known active runtime lock.
#[must_use]
pub fn unobserved() -> Self {
const REASON: &str = "No active runtime network permission state is currently observed.";
Self {
effective_network: NetworkPolicy::Deny,
runtime_lock: RuntimeLock::none(),
runtime_control: SystemPermissionControl::read_only(REASON),
}
}
/// Snapshot for a runtime that IdeA cannot inspect or elevate.
#[must_use]
pub fn locked_uninspectable() -> Self {
@ -248,7 +259,7 @@ impl RuntimePermissionSnapshot {
Self {
effective_network: NetworkPolicy::Deny,
runtime_lock: RuntimeLock::locked("external-runtime", REASON),
control: SystemPermissionControl::read_only(REASON),
runtime_control: SystemPermissionControl::read_only(REASON),
}
}
}
@ -263,8 +274,10 @@ pub struct ResolvedAgentSystemPermissions {
pub effective: NetworkPolicy,
/// Runtime lock state.
pub runtime_lock: RuntimeLock,
/// Whether the UI can edit the policy live.
/// Whether the UI can edit the persisted wanted policy.
pub control: SystemPermissionControl,
/// Whether IdeA can inspect or change the active runtime state.
pub runtime_control: SystemPermissionControl,
}
/// Resolves wanted system permissions against the runtime snapshot.
@ -283,7 +296,8 @@ pub fn resolve_agent_system_permissions(
wanted,
effective,
runtime_lock: runtime.runtime_lock,
control: runtime.control,
control: SystemPermissionControl::editable(),
runtime_control: runtime.runtime_control,
}
}
@ -306,7 +320,7 @@ mod tests {
}
#[test]
fn locked_runtime_controls_effective_policy_without_fake_allow() {
fn locked_runtime_controls_effective_policy_without_making_wanted_read_only() {
let agent = AgentId::new_random();
let doc = ProjectSystemPermissions::default();
@ -319,6 +333,31 @@ mod tests {
assert_eq!(resolved.wanted, None);
assert_eq!(resolved.effective, NetworkPolicy::Deny);
assert_eq!(resolved.runtime_lock.state, RuntimeLockState::Locked);
assert_eq!(resolved.control.mode, SystemPermissionControlMode::ReadOnly);
assert_eq!(resolved.control.mode, SystemPermissionControlMode::Editable);
assert_eq!(
resolved.runtime_control.mode,
SystemPermissionControlMode::ReadOnly
);
}
#[test]
fn unobserved_runtime_has_no_active_lock_and_preserves_wanted_effective_policy() {
let agent = AgentId::new_random();
let doc = ProjectSystemPermissions::new(
Some(SystemPermissionSet::new(Some(NetworkPolicy::Allow))),
vec![],
);
let resolved =
resolve_agent_system_permissions(&doc, agent, RuntimePermissionSnapshot::unobserved());
assert_eq!(resolved.wanted, Some(NetworkPolicy::Allow));
assert_eq!(resolved.effective, NetworkPolicy::Allow);
assert_eq!(resolved.runtime_lock.state, RuntimeLockState::None);
assert_eq!(resolved.control.mode, SystemPermissionControlMode::Editable);
assert_eq!(
resolved.runtime_control.mode,
SystemPermissionControlMode::ReadOnly
);
}
}