fix(permissions): validate network access flow
This commit is contained in:
@ -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;
|
||||
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,6 +343,7 @@ impl AgentSessionFactory for FakeFactory {
|
||||
_requester: Option<&str>,
|
||||
_env: &[(String, String)],
|
||||
_sandbox: Option<&domain::sandbox::SandboxPlan>,
|
||||
_structured_policy: Option<&domain::ports::StructuredProviderLaunchPolicy>,
|
||||
) -> Result<Arc<dyn AgentSession>, AgentSessionError> {
|
||||
Ok(Arc::new(FakeSession {
|
||||
id: SessionId::from_uuid(Uuid::from_u128(7)),
|
||||
@ -398,7 +399,16 @@ async fn fake_factory_supports_only_structured_profiles_and_starts() {
|
||||
};
|
||||
let cwd = ProjectPath::new("/srv/run").unwrap();
|
||||
let session = factory
|
||||
.start(&structured, &ctx, &cwd, &SessionPlan::None, None, &[], None)
|
||||
.start(
|
||||
&structured,
|
||||
&ctx,
|
||||
&cwd,
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("factory starts a session");
|
||||
assert_eq!(session.id(), SessionId::from_uuid(Uuid::from_u128(7)));
|
||||
|
||||
Reference in New Issue
Block a user