cageforge-policy 0.5.0

Filesystem and network policies for Rust process sandboxes
Documentation

Independent project: Cageforge is not affiliated with, sponsored by, or endorsed by OpenAI.

This crate is a supporting component of the cageforge crate, a cross-platform Rust sandbox for AI agents and untrusted code.

cageforge-policy

Read the shared configuration guide for TOML profiles, symbolic paths, local IPC, and first-launch resource rules.

cageforge-policy is the platform-independent policy model for Cageforge. It describes filesystem and network boundaries, validates them, resolves symbolic path scopes against a caller-provided runtime context, and evaluates access for concrete paths and network destinations.

Use it directly in a Rust project when permissions need to travel from a configuration or orchestration layer to an execution backend. The same policy values can be read from TOML through cageforge-config, narrowed with cageforge-policy-compose, and then passed to a Linux, macOS, or Windows backend.

When to use it

Use this crate when your application needs a typed description of what a process may access. It is the portable policy layer, so it can be used with TOML, JSON, Rust builders, or an application-specific configuration system.

The normal sequence is:

  1. Build a SandboxPolicy with validated filesystem and network rules.
  2. Give symbolic filesystem selectors a runtime PathResolutionContext; include with_current_directory when a command uses a relative cwd.
  3. Query policy values for preparation and capability checks.
  4. If an outer limit exists, pass the policy through cageforge-policy-compose.
  5. Give the resulting constraints to a native backend.

A native backend must perform the final enforcement step. This crate makes portable lexical and policy decisions; filesystem I/O, DNS resolution, and operating-system sandbox setup belong to the backend.

Workspace role

cageforge-policy is the portable filesystem and network policy layer.

Crate Role in the relationship
cageforge-path Supplies shared lexical path equality, containment, and case semantics.
cageforge-config Builds validated policies from a configuration format.
cageforge-policy-compose Narrows policy decisions with an outer policy ceiling.
cageforge Re-exports the policy model for the unified application-facing API.
Backend integrations Consume the validated policy and lower it to native enforcement.

The crate is the shared policy value between those layers. A project can pair it with its own configuration format and backend while keeping the same validated policy semantics.

Library API and ownership

Policy fields are private. Queries return shared references, slices, Option<&T>, or copyable enum values. Constructors and with_* methods build new values and validate input. Builders that could otherwise create contradictory policy states are fallible, and path inputs reject NUL characters and parent traversal before backend compilation. The API does not expose mutable collections or public fields that could bypass policy invariants. This keeps both direct library use and backend compilation on the same validated API.

PathSelector is opaque. Create it with absolute, workspace, workspace_root, root, minimal, tmpdir, or slash_tmp; callers cannot construct an unchecked selector by writing a public enum payload.

Public API

Type Purpose
SandboxPolicy Combines filesystem and network policy.
FilesystemPolicy and FilesystemRule Describes restricted, unrestricted, or externally enforced filesystem access.
FilesystemDecision Distinguishes local read/write/deny results from an externally enforced boundary.
PathSelector and PathResolutionContext Represents absolute, system-root, workspace, minimal-runtime, temporary-directory, and runtime current-directory scopes.
PathPattern Represents validated absolute or workspace-relative globs.
AccessMode Expresses Read, Write, or Deny.
NetworkPolicy Describes network enforcement ownership, domain/socket defaults, and typed Local IPC endpoints; enabled() keeps local destinations denied, while unrestricted() removes that local restriction explicitly.
LocalNetworkAccess Controls whether resolved non-public and special-purpose addresses are allowed.
NetworkDecision Distinguishes local allow/deny from externally owned network enforcement.
ResolvedNetworkTarget Keeps one normalized host and its exact resolved socket addresses together for a safe connection check.
ConnectionAuthorization and AuthorizedSocketAddr Returns the exact checked address that a network backend may use.
DomainRule, UnixSocketRule, and LocalIpcRule Adds validated network destinations and platform-neutral local-IPC endpoints.
LocalIpcEndpoint, AbsolutePath, and NamedPipeName Distinguishes Unix sockets from Windows named pipes without converting one transport into another.
PolicyError Reports invalid paths, patterns, domains, contexts, and policy combinations.

PolicyError is a dedicated library error enum. Callers can match path, pattern, context, and policy-rule failures without parsing display strings.

Local IPC is declared through the common endpoint model:

use cageforge_policy::{DomainAccess, LocalIpcEndpoint, NetworkPolicy};

let policy = NetworkPolicy::disabled()
    .with_local_ipc(
        LocalIpcEndpoint::unix_socket("/run/tool/service.sock")?,
        DomainAccess::Allow,
    )?;
# Ok::<(), Box<dyn std::error::Error>>(())

LocalIpcEndpoint::windows_named_pipe("\\\\.\\pipe\\tool-service") is the corresponding Windows form. Native support is decided by backend capability preflight; an unsupported endpoint fails closed before process creation.

The built-in SandboxPolicy::read_only, SandboxPolicy::workspace, and SandboxPolicy::full_access constructors are Cageforge presets. They are not legacy configuration aliases and do not preserve a second policy system.

Quick start

The policy model is independent of path discovery. The harness or backend provides the paths that special selectors should resolve to:

use cageforge_policy::{
    AccessMode, FilesystemDecision, FilesystemPolicy, FilesystemRule, NetworkPolicy,
    PathResolutionContext, PathSelector, SandboxPolicy,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let workspace = std::env::current_dir()?;
    let context = PathResolutionContext::new().with_workspace_root(workspace.clone())?;
    let policy = SandboxPolicy::new(
        FilesystemPolicy::restricted([FilesystemRule::new(
            PathSelector::workspace_root(),
            AccessMode::Write,
        )]),
        NetworkPolicy::disabled(),
    );

    policy.validate()?;
    let access = policy
        .filesystem()
        .access_for_path(&workspace.join("src/lib.rs"), &context)?;
    assert_eq!(access, FilesystemDecision::Write);
    Ok(())
}

access_for_path requires an absolute path and rejects NUL characters and parent traversal. Rules are recursive. Any matching deny rule wins. Among read/write rules, the most-specific resolved path wins and equal specificity uses Read over Write. Restricted policies protect .git below every writable scope by default. Add more protected relative paths with with_additional_protected_relative_path; the explicit dangerously_allow_git_write method is available only for trusted callers and may still be narrowed by a policy composer or rejected by a backend. Call normalized before handing duplicate rules to a backend when a canonical rule list is needed; composition canonicalizes both policies before retaining them in EffectiveSandbox. Concrete path and glob comparisons follow native filesystem case rules: POSIX matching is case-sensitive, while Windows matching is case-insensitive. PathPattern equality, hashing, and ordering use the same native matching identity; as_str() preserves the declared spelling for diagnostics and serialization. The portable crate does not resolve symlinks; that remains a native backend decision. Built-in protected metadata follows the same native path case rules. Windows drive/UNC device and verbatim aliases are normalized by cageforge-path before identity and containment comparisons. When inspecting a symbolic selector with FilesystemPolicy::access_for, pass the same runtime context explicitly; a selector with no resolved paths is denied.

PathSelector::root() is a symbolic request for every system root supplied in PathResolutionContext. POSIX callers normally provide /; Windows callers may provide multiple drive or UNC roots. The policy crate never discovers these roots itself. Glob rules are portable deny rules only. They support *, ?, recursive **, character classes such as [a-z], negative classes such as [!secret], and ranges. Read/write globs are rejected with PolicyError::UnsupportedGlobAccess until a backend capability contract can prove support on every target platform.

PathResolutionContext::with_current_directory records the absolute runtime directory against which a backend resolves a relative command working directory or validates the cwd that a command would otherwise inherit. It is a declaration only and never changes the process cwd.

A read-only carve-out must be below its writable scope. Concrete absolute and workspace-relative selectors that are visibly outside the parent are rejected when the rule is built. Symbolic selectors are retained when their relationship can only be determined after a backend resolves its runtime paths.

Filesystem model

PathSelector supports native absolute paths, system roots, paths relative to every workspace root, a minimal runtime scope, the platform temporary directory, and the conventional /tmp scope. Special selectors are resolved only from PathResolutionContext; the crate never guesses a workspace or touches a symlink.

FilesystemRule can target a selector or a validated deny glob. A writable rule can carry read-only subpaths. A concrete target can use MissingPathBehavior::Skip when a backend should ignore an absent path rather than create it or fail preparation. FilesystemPolicy::external records that another trusted sandbox owns the filesystem boundary. Its queries return FilesystemDecision::ExternallyEnforced, never local Deny, so a backend cannot silently apply the wrong interpretation.

Network model

NetworkPolicy separates enforcement ownership from the default behavior for domains and Unix socket paths. Domain inputs are normalized like the upstream host boundary: case is folded, trailing dots are removed, host ports are ignored, bracketed IPv6 literals are unwrapped, and IPv4/IPv6 literals are canonicalized. Malformed host/port forms, including missing, non-numeric, or out-of-range ports, are rejected. Domain glob matchers are compiled once when the rule is created, so repeated policy queries do not recompile patterns. *.example.com matches subdomains but not the apex, while **.example.com matches the apex and its subdomains. Domain patterns also support *, ?, character classes such as [a-c], negative classes such as [!x], and ranges within a host label, such as region*.example.com; wildcard characters never change host normalization or the explicit apex semantics of the prefixed forms.

Use decision_for_domain only for declarative host-policy inspection; it is not an authorization to connect because it does not contain a resolved address. For a connection, construct ResolvedNetworkTarget and immediately call authorize_connection with the exact SocketAddr that the backend will use. Only ConnectionAuthorization::Allowed contains an AuthorizedSocketAddr; a backend consumes it with into_socket_addr when handing the exact address to its connection operation. The authorization token is intentionally not Copy or Clone; a changed or freshly resolved address is denied. Use decision_for_unix_socket when a backend needs the complete result. These methods return NetworkDecision::Allow, NetworkDecision::Deny, or NetworkDecision::ExternallyEnforced. The authorize_connection method returns an Allowed value containing the exact checked socket address. Unix socket checks also return the complete NetworkDecision, preserving the distinction between local denial and external enforcement.

UnixSocketRule equality and hashing use the same native path identity as Unix-socket matching and policy normalization. A rule matches one exact native socket path. It grants no directory or path prefix. On Windows this makes case variants one rule identity; on POSIX, case remains significant. The path() accessor still preserves the declared spelling for diagnostics.

Network policy is independent from filesystem policy. Disabled mode denies destinations, while external mode records that another trusted boundary owns network enforcement. A project can connect these values to a proxy, firewall, or native network mechanism in the backend it uses. The default LocalNetworkAccess::Deny also protects domain rules from DNS rebinding and recognizes localhost as a local hostname before trusting DNS results. A backend passes every resolved address to decision_for_domain_with_resolved_ips. For an ordinary hostname, an empty list means that resolution failed or timed out and is denied. An IP literal does not need DNS resolution, so an empty list is valid for the literal itself; non-public literals still require an exact IP-literal allow or LocalNetworkAccess::Allow. An exact localhost allow opts into loopback addresses only. Other private, metadata, link-local, documentation, benchmarking, translation-local, NAT64-embedded non-global, discard-only, dummy, deprecated site-local, and reserved IPv4/IPv6 destinations require LocalNetworkAccess::Allow; explicitly globally reachable special-purpose anycast addresses remain ordinary public targets. The policy crate performs no DNS or network I/O. It also cannot prove that a caller actually connected to the checked address; the native backend must use the target snapshot instead of resolving the hostname again.

The handoff to network code is explicit:

DNS results + exact SocketAddr
            │
            ▼
ResolvedNetworkTarget
            │
            ▼
authorize_connection
            │
            ▼
AuthorizedSocketAddr::into_socket_addr()
            │
            ▼
connect to that exact address

decision_for_domain is useful for inspecting a host rule, but it is not a connection authorization because it does not bind the decision to an address.

Using it with other crates

cageforge-config is one way to create a SandboxPolicy from named TOML profiles. cageforge-policy-compose is the optional narrowing layer when an application needs to apply an outer safety limit. A backend then consumes the validated decisions for its platform.

The policy crate is also suitable on its own: callers can construct the model with Rust builders and provide their own runtime path context.

API reference: cageforge-policy on docs.rs.

Repository: github.com/m62624/cageforge.