Skip to main content

SecurityPolicy

Struct SecurityPolicy 

Source
pub struct SecurityPolicy {
Show 22 fields pub autonomy: AutonomyLevel, pub workspace_dir: PathBuf, pub workspace_only: bool, pub allowed_commands: Vec<String>, pub forbidden_paths: Vec<String>, pub allowed_roots: Vec<PathBuf>, pub allowed_roots_read_only: Vec<PathBuf>, pub allowed_roots_write_only: Vec<PathBuf>, pub max_actions_per_hour: u32, pub max_cost_per_day_cents: u32, pub require_approval_for_medium_risk: bool, pub block_high_risk_commands: bool, pub shell_env_passthrough: Vec<String>, pub shell_timeout_secs: u64, pub allowed_tools: Option<Vec<String>>, pub excluded_tools: Option<Vec<String>>, pub auto_approve: Vec<String>, pub always_ask: Vec<String>, pub sandbox_enabled: Option<bool>, pub sandbox_backend: Option<String>, pub firejail_args: Vec<String>, pub tracker: PerSenderTracker,
}
Expand description

Security policy enforced on all tool executions.

Three cross-agent allowlist tiers drive the multi-agent design:

  • allowed_roots: read AND write. Populated from RiskProfileConfig.allowed_roots and from AccessMode::ReadWrite grants in agent.workspace.access.
  • allowed_roots_read_only: read but NOT write. Populated from AccessMode::Read grants.
  • allowed_roots_write_only: write but NOT read. Populated from AccessMode::Write grants. The bot can append/overwrite under the path but file_read / pdf_read / glob_search / content_search reject it.

Read-side tools call SecurityPolicy::is_resolved_path_readable, which sees allowed_rootsallowed_roots_read_only plus the universal POSIX device files. Write-side tools call SecurityPolicy::is_resolved_path_allowed, which sees allowed_rootsallowed_roots_write_only. The two tiers stay disjoint by construction so AccessMode::Write and AccessMode::Read grant exactly what they say.

Fields§

§autonomy: AutonomyLevel§workspace_dir: PathBuf§workspace_only: bool§allowed_commands: Vec<String>§forbidden_paths: Vec<String>§allowed_roots: Vec<PathBuf>

Directories the agent can read AND write under. Includes RiskProfileConfig.allowed_roots plus any cross-agent AccessMode::ReadWrite grants resolved from agent.workspace.access at policy construction time.

§allowed_roots_read_only: Vec<PathBuf>

Directories the agent can read but NOT write under. Populated from cross-agent AccessMode::Read grants at policy construction time. Empty when no read-only cross-agent access is configured.

§allowed_roots_write_only: Vec<PathBuf>

Directories the agent can write but NOT read under. Populated from cross-agent AccessMode::Write grants at policy construction time. Empty when no write-only cross-agent access is configured. Read-side tools (file_read, pdf_read, glob_search, content_search) ignore this list; write-side tools (file_write, file_edit, git_operations) honor it.

§max_actions_per_hour: u32§max_cost_per_day_cents: u32§require_approval_for_medium_risk: bool§block_high_risk_commands: bool§shell_env_passthrough: Vec<String>§shell_timeout_secs: u64§allowed_tools: Option<Vec<String>>

Tool name allowlist. None is unrestricted (default for agents without an explicit risk_profile.allowed_tools setting). Some(vec![]) denies every tool. Some(list) admits only the listed names. Enforced at the agent loop’s tool-dispatch site.

§excluded_tools: Option<Vec<String>>

Tool name denylist. Subtracts from the allowed set (whether the allowed set comes from allowed_tools or from the unrestricted default). None and Some(vec![]) both mean “exclude nothing”.

§auto_approve: Vec<String>

Tools that never require approval in this profile. Mirrors RiskProfileConfig.auto_approve.

§always_ask: Vec<String>

Tools that always require approval in this profile. Mirrors RiskProfileConfig.always_ask.

§sandbox_enabled: Option<bool>

Whether the sandbox is enabled for this profile. None inherits the global default at the call site.

§sandbox_backend: Option<String>

Sandbox backend identifier (e.g. "firejail", "landlock"). None inherits the global default.

§firejail_args: Vec<String>

Extra arguments forwarded to firejail when sandbox_backend resolves to "firejail".

§tracker: PerSenderTracker

Implementations§

Source§

impl SecurityPolicy

Source

pub fn is_tool_allowed(&self, name: &str) -> bool

True when name is admissible under the current policy.

allowed_tools = None is unrestricted; Some(list) is the allowlist. excluded_tools always subtracts.

Source§

impl SecurityPolicy

Source

pub fn command_risk_level(&self, command: &str) -> CommandRiskLevel

Classify command risk. Any high-risk segment marks the whole command high.

Source

pub fn validate_command_execution( &self, command: &str, approved: bool, ) -> Result<CommandRiskLevel, String>

Validate full command execution policy (allowlist + risk gate).

Source

pub fn is_command_allowed(&self, command: &str) -> bool

Check if a shell command is allowed.

Validates the entire command string, not just the first word:

  • Blocks subshell operators (`, $() that hide arbitrary execution
  • Splits on command separators (|, &&, ||, ;, newlines) and validates each sub-command against the allowlist
  • Blocks single & background chaining (&& remains supported)
  • Blocks shell redirections (<, >, >>) that can bypass path policy
  • Blocks dangerous arguments (e.g. find -exec, git config)
Source

pub fn forbidden_path_argument(&self, command: &str) -> Option<String>

Return the first path-like argument blocked by path policy.

This is best-effort token parsing for shell commands and is intended as a safety gate before command execution.

Source

pub fn is_path_allowed(&self, path: &str) -> bool

Check if a file path is allowed (no path traversal, within workspace)

Source

pub fn is_resolved_path_readable(&self, resolved: &Path) -> bool

Validate that a resolved path is readable by the current security policy. Used by read-side tools (file_read, pdf_read, glob_search, content_search) that should honor the read-write allowed_roots AND the read-only allowed_roots_read_only lists, plus the universal POSIX device files (/dev/null, /dev/zero, /dev/random, /dev/urandom) that operators legitimately use for shell- idiom CLI commands and standard input/output redirection.

Importantly: this method does NOT consult allowed_roots_write_only. AccessMode::Write grants write access without read access; surfacing those paths through a read-side tool would silently elevate the grant.

Write-side tools (file_write, file_edit, git_operations, shell write paths) call Self::is_resolved_path_allowed instead.

Source

pub fn is_resolved_path_allowed(&self, resolved: &Path) -> bool

Validate that a resolved path is inside the workspace or an allowed root for write-side tools. Call this AFTER joining workspace_dir + relative path and canonicalizing.

Sees allowed_roots (read+write) AND allowed_roots_write_only (write-only). Read-only allowlist entries are NOT honored; that’s the read-side tier.

Source

pub fn is_runtime_config_path(&self, resolved: &Path) -> bool

Source

pub fn runtime_config_violation_message(&self, resolved: &Path) -> String

Source

pub fn resolved_path_violation_message(&self, resolved: &Path) -> String

Source

pub fn can_act(&self) -> bool

Check if autonomy level permits any action at all

Source

pub fn enforce_tool_operation( &self, operation: ToolOperation, operation_name: &str, ) -> Result<(), String>

Enforce policy for a tool operation.

Read operations are always allowed by autonomy/rate gates. Act operations require non-readonly autonomy and available action budget.

Source

pub fn record_action(&self) -> bool

Record an action for the current sender and check if rate-limited. Returns true if allowed, false if budget exhausted.

Source

pub fn is_rate_limited(&self) -> bool

Check if the current sender would be rate-limited without recording.

Source

pub fn resolve_tool_path(&self, path: &str) -> PathBuf

Resolve a user-provided path for tool use.

Expands ~ prefixes and resolves relative paths against the workspace directory. This should be called after is_path_allowed to obtain the filesystem path that the tool actually operates on.

Source

pub fn is_under_allowed_root(&self, path: &str) -> bool

Check whether the given raw path (before canonicalization) falls under an allowed_roots (read+write) OR allowed_roots_write_only entry. Tilde expansion is applied to the path before comparison. This is useful for tool-level pre-checks that want to allow absolute paths the policy explicitly permits to write.

Write-side semantics. Use this from write-side tools (file_write, git_operations, shell). Read-side tools should use Self::is_under_any_allowed_root so a cross-agent AccessMode::Read grant allows the read.

Source

pub fn is_under_read_only_allowed_root(&self, path: &str) -> bool

Check whether the given raw path falls under a read-only allowed root. Returns false for the read-write list; callers that want the union should use Self::is_under_any_allowed_root.

Populated for multi-agent: an agent’s workspace.access entries with AccessMode::Read become read-only roots on the policy.

Source

pub fn is_under_any_allowed_root(&self, path: &str) -> bool

Check whether the given raw path falls under allowed_roots (rw), allowed_roots_read_only, OR allowed_roots_write_only. Read-side tools (file_read, pdf_read, glob_search, content_search) call Self::is_resolved_path_readable for the resolved-path form, which intentionally excludes the write-only tier. This raw-path helper is the union of all three, used where read+write tools share an entry point and the resolved-path check splits the directionality afterward.

Source

pub fn ensure_no_escalation_beyond( &self, parent: &SecurityPolicy, ) -> Result<(), EscalationViolation>

Verify this policy does not escalate any permission beyond parent (SubAgent inheritance subset check).

Subset rules:

  • Every allowed_roots entry on self must appear on parent.allowed_roots. (Read+write grants can never be wider than the parent’s read+write list.)
  • Every allowed_roots_read_only entry on self must appear on parent.allowed_roots OR on parent.allowed_roots_read_only. (A SubAgent can downgrade a parent’s rw root to read-only, but it cannot grant read access to a path the parent could not even read.)
  • Every allowed_commands entry on self must appear on parent.allowed_commands.
  • self.workspace_only must be true whenever parent.workspace_only is true. A SubAgent cannot disable workspace_only when the parent enforces it.
  • self.max_actions_per_hour <= parent.max_actions_per_hour and self.max_cost_per_day_cents <= parent.max_cost_per_day_cents. A SubAgent cannot raise the parent’s rate or cost ceiling.

Returns Err(EscalationViolation) describing the first violation found. Callers should reject the spawn on Err so a misconfigured override never lands as a constructed policy.

Source

pub fn from_risk_profile( risk_profile: &RiskProfileConfig, workspace_dir: &Path, ) -> SecurityPolicy

Legacy entry point: build a SecurityPolicy from a risk profile without a runtime profile. Budget caps default to zero (interpreted as “no enforcement”). Tests and pre-multi-agent callsites use this; production code should call from_profiles or for_agent so the runtime profile’s budget caps actually take effect.

Source

pub fn from_profiles( risk_profile: &RiskProfileConfig, runtime_profile: Option<&RuntimeProfileConfig>, workspace_dir: &Path, ) -> SecurityPolicy

Build a SecurityPolicy from a resolved risk + runtime profile pair.

Authorization fields (autonomy level, allowlists, sandbox) come from the risk profile. Budget caps (max_actions_per_hour, max_cost_per_day_cents, shell_timeout_secs) come from the runtime profile but are enforced with parent-subset discipline on SubAgent spawn (see ensure_no_escalation_beyond).

Source

pub fn for_agent( config: &Config, agent_alias: &str, ) -> Result<SecurityPolicy, Error>

Resolve the risk + runtime profiles owned by agent_alias and build a SecurityPolicy. Bails when the agent isn’t configured or when its risk_profile field doesn’t name a configured profile — there is no global fallback, every security context is per-agent. Missing runtime_profile falls back to zero budgets (treated as “inherit / no enforcement”), matching the previous default when the budget fields lived on the risk profile.

Source

pub fn prompt_summary(&self) -> String

Render a human-readable summary of the active security constraints suitable for injection into the LLM system prompt.

Giving the LLM visibility into these constraints prevents it from wasting tokens on commands / paths that will be rejected at runtime. See issue #2404.

Trait Implementations§

Source§

impl Clone for SecurityPolicy

Source§

fn clone(&self) -> SecurityPolicy

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for SecurityPolicy

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl Default for SecurityPolicy

Source§

fn default() -> SecurityPolicy

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more