RFD 075: Tool Sandbox and Access Policy
- Status: Discussion
- Category: Design
- Authors: Jean Mertz git@jeanmertz.com
- Date: 2026-04-01
- Extends: RFD 016
- Requires: RFD 076
Summary
This RFD introduces OS-level sandboxing for subprocess-based tools. Sandbox profiles are generated from the access policy defined in RFD 076 and applied per tool invocation using platform-native mechanisms: sandbox-exec on macOS, Landlock on Linux, and restricted tokens with job objects on Windows. This RFD also extends RFD 076's AccessPolicy with CommandRule for subprocess spawn restrictions, and defines the environment variable isolation model for tool subprocesses. Unconfigured tools receive a default sandbox (workspace read-write, no network, minimal environment).
Motivation
JP runs LLM-selected tools as subprocesses on the user's machine. Today, the only safeguard is RunMode::Ask — a permission prompt before execution. Once the user approves, the subprocess runs with the user's full OS privileges: it can read any file, access the network, spawn other processes, and modify anything the user can.
This is a structural problem, not a configuration oversight:
Tools can access files outside the workspace. A
modify_filetool receiving an absolute path argument like/etc/hostsor~/.ssh/id_rsawill happily read or write it. TheRunMode::Askprompt shows the arguments, but a user skimming a long argument list can miss a dangerous path.Tools can access the network. A shell-based tool could
curldata to an external server. Nothing prevents exfiltration of workspace contents through a tool subprocess.RunModeis all-or-nothing. The current permission model is "run this tool: yes or no." There is no way to say "run this tool, but only let it read files insrc/" or "run this tool, but block network access." The granularity is per-invocation, not per-capability.The init wizard warns about this explicitly. The
jp initcommand tells users that "externally supplied tools cannot be restricted" and that tools "can potentially run any command on your system." This is honest, but the right answer is to fix the problem, not document it.
RFD 076 defines a typed access policy (AccessPolicy) that declares what resources a tool can access — filesystem paths, network URIs, environment variables — and a cooperative enforcement model where tools self-check their grants. That model addresses the policy surface and provides good error messages, but it is cooperative: a buggy or malicious tool can ignore the policy.
RFD 016 defines a sandbox model for WASM plugins with per-capability configuration and inquiry-based permission prompts. The WASM sandbox is architecturally sound (WASM has no ambient capabilities), but it only applies to WASM plugins. Subprocess-based tools — which are the vast majority of tools today and will remain common — have no equivalent protection.
This RFD adds the mandatory enforcement layer: OS-level sandboxing that the tool subprocess cannot bypass. It consumes RFD 076's AccessPolicy types to generate platform-specific sandbox profiles, extends the policy with subprocess spawn restrictions (CommandRule), and defines environment variable isolation for tool subprocesses.
Design
Relationship to RFD 076 (Tool Access Grants)
This RFD and RFD 076 address the same access policy at different enforcement layers:
| Concern | RFD 076 | RFD 075 |
|---|---|---|
| Policy | Defines AccessPolicy types | Consumes them |
| Enforcement | Tool self-checks (cooperative) | OS-level sandbox (mandatory) |
| Failure mode | Helpful error message | Raw permission denied |
| Bypass | Tool can ignore | OS enforces |
| Config key | conversation.tools.*.access | Same |
The access policy is configured once via conversation.tools.*.access. RFD 076's cooperative layer checks it at the application level with clear error messages. This RFD's OS layer enforces it at the kernel level as a hard boundary.
The two layers complement each other: RFD 076 provides a good user experience (clear errors naming denied capabilities and listing configured grants). This RFD provides a security boundary (the OS prevents access regardless of what the tool code does).
Extending AccessPolicy with CommandRule
RFD 076 defines AccessPolicy with three resource types: filesystem (fs), network (net), and environment variables (env). This RFD adds a fourth: subprocess commands.
/// Extension to AccessPolicy for subprocess spawn restrictions.
///
/// When `None`, the tool may spawn any subprocess.
/// When `Some`, only listed programs are allowed.
pub commands: Option<HashMap<String, CommandRule>>,pub struct CommandRule {
/// Allowed argument prefixes.
///
/// Each entry is a sequence of values that must match the start
/// of the actual arguments. `**` as the last element allows any
/// remaining arguments.
///
/// If absent, any arguments are permitted.
pub args: Option<Vec<Vec<String>>>,
/// Environment variables forwarded to this command.
///
/// Restricts which of the tool's allowed env vars are passed to
/// this specific child process.
pub envs: Vec<String>,
}CommandRule cannot be self-enforced by tools (the tool IS the subprocess), so it exists for OS-level enforcement (this RFD) and WASM host enforcement (RFD 016). It is included in AccessPolicy to keep the policy surface unified — one config block, multiple enforcement layers.
Example configuration using RFD 076's access key with the commands extension:
[conversation.tools.cargo_check]
source = "local"
command = ".config/jp/tools/target/release/jp-tools cargo check"
[[conversation.tools.cargo_check.access.fs]]
path = "."
read = true
write = true
[conversation.tools.cargo_check.access.commands.cargo]
args = [["check", "**"]]
[conversation.tools.my_script]
source = "local"
command = "./scripts/deploy.sh"
[[conversation.tools.my_script.access.fs]]
path = "."
read = true
write = true
[[conversation.tools.my_script.access.net]]
host = "api.example.com"
scheme = "https"
allow = true
[[conversation.tools.my_script.access.env]]
name = "DEPLOY_TOKEN"
read = trueDefault policy
The OS sandbox always applies to subprocess tools, even when no access config is present. Tools without explicit access configuration receive these defaults:
| Capability | Default |
|---|---|
| Filesystem | Workspace root, read-write |
| Network | Denied |
| Commands | Unrestricted (no subprocess filtering) |
| Environment | Minimal set only (PATH, HOME, |
USER, LANG, locale) |
These defaults are deliberately permissive for filesystem access (read-write) to avoid breaking existing tools. The primary security value of the defaults is containing tools to the workspace and blocking network access. As the ecosystem matures and tools gain explicit access configuration, the defaults can be tightened.
When no explicit access config is present, JP materializes the default policy as an AccessPolicy and includes it in the tool's Context. The tool sees access: Some(default_policy) — never None — so it can self-check against the same restrictions the OS enforces. This ensures the cooperative layer (RFD 076) and the OS layer always operate on the same policy. Context.access is None only when OS-level sandboxing is unavailable (unsupported platform, fallback mode).
When explicit access config is present, it replaces the defaults entirely. The OS sandbox is generated from the configured AccessPolicy, and RFD 076's merge semantics apply: fs, net, and env are each a MergeableVec, which defaults to append across config layers. Users opt into replace, prepend, or dedup per field via the explicit Merged form (see RFD 076's Cross-layer merging section).
Platform enforcement
macOS: sandbox-exec
macOS provides sandbox-exec, which applies a Scheme-based sandbox profile to a process. JP generates a profile from the tool's AccessPolicy and launches the tool process under it.
JP generates profile → sandbox-exec -p <profile> <command> <args>The mapping from AccessPolicy to SBPL:
| AccessPolicy field | SBPL rule |
|---|---|
FsRule { read: true } | (allow file-read* (subpath "<resolved-path>")) |
FsRule { write: true } | (allow file-write* (subpath "<resolved-path>")) |
NetRule with port/scheme | (allow network-outbound (remote tcp "*:<port>")) |
NetRule without port/scheme | (allow network-outbound (remote tcp)) |
| No network rules | (deny network*) |
RFD 076's fine-grained create/update/delete distinctions map to a single file-write* on macOS — sandbox-exec does not distinguish write sub-operations. RFD 076's host and path_prefix fields have no direct SBPL equivalent: sandbox-exec operates at the TCP layer and cannot filter by hostname or HTTP path. Host and path filtering is enforced cooperatively (RFD 076); OS-level enforcement on macOS is port-based only. The scheme field is translated to its default port (80 for http, 443 for https) when no explicit port is specified.
Example generated profile for a read-write workspace tool with no network:
(version 1)
(deny default)
(allow process-exec)
(allow file-read*
(subpath "/path/to/workspace"))
(allow file-write*
(subpath "/path/to/workspace"))
(deny network*)Limitations:
sandbox-execis deprecated by Apple but still functional as of macOS 15. There is no replacement API with equivalent functionality for third-party applications. The profile language (SBPL) is undocumented — all available references are from reverse engineering. Apple has progressively restricted sandbox profile capabilities in recent releases. If Apple removessandbox-execin a future release, JP falls back to no OS-level enforcement on macOS (the cooperative policy from RFD 076 remains active). A future RFD should investigate alternative macOS sandboxing (App Sandbox entitlements via XPC services,posix_spawnwith manual restriction) before this becomes urgent.- Profile generation must handle path escaping and symlink resolution.
sandbox-execcannot restrict which programs the subprocess spawns in a fine-grained way — it can denyprocess-execentirely or allow it entirely.commandsrestrictions are enforced at the application level, not the OS level on macOS.
Linux: Landlock
Landlock is a Linux security module (available since kernel 5.13) that allows unprivileged processes to restrict their own filesystem access. Unlike seccomp (which filters syscalls), Landlock operates at the filesystem level — it restricts which paths a process can access, which maps directly to RFD 076's FsRule model.
The mapping from AccessPolicy to Landlock:
| AccessPolicy field | Landlock flags |
|---|---|
FsRule { read: true } | `AccessFs::ReadFile |
| ...` | |
| ...` | |
| AccessFs::RemoveDir` | |
| (kernel 6.7+) |
Landlock preserves more of RFD 076's granularity than sandbox-exec for filesystem access — it can distinguish read-only from write access per path, and with kernel 6.7+, it can restrict TCP connections by port. As with sandbox-exec, Landlock has no hostname or HTTP-path awareness; RFD 076's host and path_prefix fields are enforced cooperatively only.
// Pseudocode for Landlock ruleset creation from AccessPolicy
let mut ruleset = Ruleset::new();
for rule in &policy.fs {
let resolved = resolve_path(workspace_root, rule.path());
let mut flags = AccessFs::empty();
if rule.read() { flags |= AccessFs::ReadFile | AccessFs::ReadDir; }
if rule.create() { flags |= AccessFs::MakeDir | AccessFs::MakeReg; }
if rule.update() { flags |= AccessFs::WriteFile; }
if rule.delete() { flags |= AccessFs::RemoveFile | AccessFs::RemoveDir; }
ruleset = ruleset.add_rule(PathBeneath::new(resolved, flags))?;
}
ruleset.restrict_self()?;JP uses Landlock by setting up the ruleset in the child process after fork() but before exec(). This restricts the tool process without affecting JP itself.
On older kernels without Landlock, or without AccessNet support (pre-6.7), enforcement falls back gracefully: a warning is logged and the cooperative policy (RFD 076) remains the only protection.
Seccomp-bpf is an alternative that filters at the syscall level. It is more powerful but also more complex and fragile — blocking the wrong syscall can crash the process in unpredictable ways. Landlock is preferred because its filesystem-level model maps directly to AccessPolicy. Seccomp may be added as a supplementary layer in the future.
Windows: best-effort enforcement
Windows provides two mechanisms that partially address the sandbox requirements:
Restricted tokens strip privileges from a process token before spawning. Job objects restrict the subprocess's ability to spawn child processes, access the network, and consume resources.
Windows filesystem permissions are ACL-based, not path-prefix-based. Expressing "allow only these paths" without configuring NTFS ACLs on a per-invocation basis is expensive and fragile. The initial Windows implementation provides coarser enforcement than macOS/Linux:
- Network: Job objects can restrict network access (coarse on/off).
- Child processes: Job objects can restrict subprocess spawning.
- Filesystem: No per-path restriction in the initial implementation. The cooperative policy (RFD 076) is the primary filesystem control on Windows.
If a more capable Windows sandboxing approach becomes feasible (e.g., using Windows Sandbox or AppContainers), a future RFD will address it. The current design provides what value it can without overinvesting in a platform where the OS mechanisms don't map cleanly to the policy model.
Unsupported platforms and fallback
On platforms where none of the above mechanisms are available, JP logs a warning at startup and operates without OS-level enforcement. The cooperative policy from RFD 076 and RunMode permission prompts remain active.
The warning is shown once per session, not per tool invocation:
Warning: OS-level tool sandboxing is not available on this platform.
Tools run with your full user permissions. Use `run = "ask"` for untrusted tools.Interaction with RunMode
The sandbox system and RunMode serve different purposes and stack:
| Concern | RunMode | Sandbox |
|---|---|---|
| Question | "Should this tool run at all?" | "What can this tool do while running?" |
| Granularity | Per-invocation | Per-capability |
| Timing | Before execution | During execution |
A tool with run = "unattended" and a restrictive access policy is safe: it runs without permission prompts but can only access what the policy allows. A tool with run = "ask" and no access policy is the current behavior: the user approves each invocation but the tool has the default sandbox restrictions.
The recommended configuration for most tools:
[conversation.tools.my_tool]
run = "unattended"
[[conversation.tools.my_tool.access.fs]]
path = "."
read = true
write = trueThis is "trust but verify" — the tool runs without interruption, but the OS prevents it from escaping the workspace. For external or untrusted tools, combine run = "ask" with a restrictive access policy for defense in depth.
Sensitive path protection
Certain paths trigger a strong warning when a tool's access policy includes them, regardless of how the policy is configured. These paths represent high-value secrets that should rarely be exposed to tool subprocesses:
~/.ssh/— SSH keys~/.gnupg/or~/.gpg/— GPG keys~/.aws/credentials— AWS credentials~/.config/gcloud/— Google Cloud credentials~/.kube/config— Kubernetes credentials~/.docker/config.json— Docker credentials- Files matching
**/.envand**/.env.*patterns
JP ships this list as a built-in default, updated with each release. Users can extend the list with additional paths:
[conversation.tools.*.access]
sensitive_paths = ["~/.myapp/secrets", "**/.secret.*"]User-specified paths are added to (not replace) the built-in list. The built-in list cannot be reduced via configuration.
Sensitive paths are a warning list, not a deny list. When a tool's access.fs rules grant access to a sensitive path, the OS sandbox honors the grant — the tool gets access. But JP surfaces an inquiry prompt with a clear warning explaining what the tool will be able to access before allowing it. The user can approve or deny. This applies on first use, not on every invocation — the user's response is remembered for the session. To permanently grant a tool access to a sensitive path without prompts, the user adds the access.fs entry and configures run = "unattended" for that tool — the warning is shown once per session regardless.
Tools without explicit access config receive the default sandbox (workspace read-write), which excludes sensitive paths outside the workspace by definition. Sensitive paths inside the workspace (e.g., .env files) are included in the default sandbox but trigger the warning if the tool's stderr output indicates it accessed them.
Environment variable isolation
By default, tool subprocesses receive a minimal environment: PATH, HOME, USER, LANG, and locale variables. All other environment variables are stripped.
Tools that need specific environment variables declare them via RFD 076's EnvRule:
[[conversation.tools.my_tool.access.env]]
name = "GITHUB_TOKEN"
read = true
[[conversation.tools.my_tool.access.env]]
name = "AWS_*"
read = trueOnly variables matching EnvRule entries with read = true are forwarded to the subprocess, in addition to the minimal set. This prevents accidental leakage of secrets through the subprocess environment.
For tools with CommandRule entries, per-command envs further restrict which of the tool's allowed env vars are forwarded to specific child processes.
Profile generation and caching
Sandbox profiles are generated from AccessPolicy at tool resolution time (once per jp query invocation, not per tool call). The generated profile is cached for the duration of the session.
On macOS, the profile is written to a temporary file and passed to sandbox-exec -f. On Linux, the Landlock ruleset is constructed in memory. On Windows, the job object is created once and reused.
Path resolution happens at profile generation time: relative paths in access.fs rules are resolved against the workspace root. Symlinks are resolved to avoid sandbox bypasses via symlink traversal.
Drawbacks
Platform inconsistency. The three enforcement mechanisms have different strengths.
sandbox-execis capable but deprecated and uses an undocumented profile language. Landlock is sound but requires kernel 5.13+. Windows provides only coarse-grained enforcement in the initial implementation. Users on different platforms get different levels of protection.sandbox-execdeprecation risk. Apple has deprecatedsandbox-execwith no public replacement. All SBPL documentation comes from reverse engineering. Apple could remove it in any macOS release, leaving macOS without OS-level enforcement until an alternative is developed. This is the most significant platform risk since macOS is a primary development platform for many JP users.Performance overhead. Sandbox setup adds latency to tool spawning. On macOS,
sandbox-execadds a wrapper process. On Linux, Landlock ruleset creation adds ~1ms. These costs are per-tool-call but small relative to the tool's own execution time and LLM latency.False denials. The default sandbox may break tools that legitimately need access beyond the workspace root (e.g., tools that read system headers, access package caches, or interact with databases). Users must diagnose the sandbox violation and add the appropriate
accessentry. OS-level denials produce raw permission errors that may be difficult to diagnose without context.Configuration burden. Adding
accessconfig is additional work for tool authors. The defaults are designed to cover common cases, but tools with unusual requirements (build tools that read toolchain directories, tools that need network access) need explicit configuration.Environment variable isolation is a behavioral change. Today, tool subprocesses inherit the full environment. This RFD strips all variables except a minimal set. Tools relying on
CARGO_HOME,RUSTUP_HOME, proxy settings, or other environment variables will need explicitEnvRuleentries.
Alternatives
No OS-level sandboxing (cooperative only)
Rely entirely on RFD 076's cooperative enforcement and a future VFS IPC protocol for access control. A future RFD is expected to introduce runtime = "vfs" tools that access host resources through mediated IPC; until that exists, every tool runs as runtime = "stdio" (the current default) and would get no mandatory restrictions beyond RunMode.
Rejected because runtime = "stdio" is the default and will remain the most common mode. Leaving the majority of tools without mandatory protection defeats the purpose. Cooperative enforcement is valuable for error messages but does not prevent a buggy or malicious tool from ignoring the policy.
Container-based sandboxing
Run each tool in a lightweight container (e.g., a micro-VM or namespace sandbox). This provides strong isolation but adds significant complexity and startup latency, and is not available on all platforms (notably Windows and macOS have limited namespace support).
Rejected as disproportionate for the threat model. JP's tools are typically short-lived commands that the user has configured. The threat is accidental over-reach (a path argument the LLM chose badly), not adversarial exploitation. OS-level filesystem restrictions are sufficient for this threat model.
Seccomp-only on Linux
Use seccomp-bpf instead of Landlock for all Linux enforcement. Seccomp is more powerful (it can filter any syscall) but also more fragile — blocking the wrong syscall crashes the process. The filesystem-level model of Landlock maps directly to AccessPolicy and is safer to use.
Seccomp may be added as a supplementary layer in the future, but Landlock is the primary mechanism.
Non-Goals
Sandboxing JP itself. This RFD restricts tool subprocesses, not the JP process. JP needs full access to the filesystem, network, and process table to function.
Sandboxing MCP tools. MCP tools run on external servers. The server's security is the server operator's responsibility. JP trusts MCP tool results the same way it trusts any network response.
Restricting LLM API calls. The LLM provider connection is not subject to the sandbox system. It is configured separately via provider settings.
Fine-grained syscall filtering. This RFD targets filesystem, network, and subprocess restrictions — the capabilities most relevant to tool execution. Low-level syscall filtering (e.g., blocking
ptrace,mount) is out of scope.Replacing
RunMode. The sandbox complementsRunMode, it does not replace it.RunModecontrols whether a tool runs. The sandbox controls what the tool can do while running.Post-hoc detection of OS sandbox violations. When the OS sandbox denies an operation, the tool receives a raw permission error. JP cannot intercept OS-level denials or reliably detect them by scanning stderr — the heuristic for distinguishing sandbox denials from other permission errors is inherently imprecise. This RFD does not attempt runtime detection of sandbox violations. If a tool fails due to a sandbox restriction, the user diagnoses it from the tool's error output and adds the appropriate
accessentry. A future RFD may explore detection heuristics or structured error reporting from tools.
Risks and Open Questions
sandbox-execremoval timeline. Apple has deprecatedsandbox-execbut provided no timeline for removal and no public replacement API. If removed, macOS falls back to no OS-level enforcement. A future RFD should investigate alternative macOS sandboxing before this becomes urgent.Landlock kernel version adoption. Landlock requires kernel 5.13+. Network restrictions require 6.7+. What is the minimum kernel version we should target? Most modern distributions ship 5.15+ but CI environments and older servers may not.
Symlink and mount-point handling. Sandbox path restrictions use resolved absolute paths. Symlinks inside the workspace that point outside it create a potential bypass. JP resolves all symlinks at sandbox setup time. Should it also deny symlink traversal to out-of-scope targets?
Convergence with RFD 016. This RFD proposes adding
CommandRuleto RFD 076'sAccessPolicy. RFD 016 independently definesCommandRuleandSandboxConfigfor WASM plugins. Once this RFD is accepted, RFD 016's WASM plugin config should adopt the sharedAccessPolicytypes. Is this a clean migration, or does the WASM sandbox have requirements thatAccessPolicydoesn't cover?Windows filesystem restrictions. The initial Windows implementation provides only coarse-grained enforcement (network on/off, child process restriction). Is this acceptable for a first release, or should Windows support be deferred entirely until a more capable mechanism is identified?
Environment variable discovery. Stripping all env vars by default will break tools that depend on undocumented environment variables (toolchain paths, proxy settings, editor preferences). How do users discover which variables a tool needs? Should JP log which variables were stripped when a tool fails?
Implementation Plan
Phase 1: CommandRule in AccessPolicy
- Add
commands: Option<HashMap<String, CommandRule>>toAccessPolicyinjp_tool, extending RFD 076's types. - Add
commandsfield support to theaccessconfig injp_config. - Unit tests for command matching logic.
- Dependency: RFD 076 Phase 1 (types in
jp_tool).
Phase 2: Environment variable isolation
- Modify tool subprocess spawning in
jp_llmto callenv_clear()and forward only the minimal set (PATH,HOME,USER,LANG, locale). - When
access.envrules are present, forward matching variables in addition to the minimal set. - When no
accessconfig is present, apply the same minimal-set default. - Update JP's built-in tools and documentation for any newly required
EnvRuleentries. - Dependency: Phase 1.
Phase 3: macOS sandbox-exec integration
- Implement SBPL profile generation from
AccessPolicy. - Modify tool subprocess spawning to wrap with
sandbox-execwhen available. - Handle path resolution and symlink traversal.
- Implement sensitive path warnings.
- Add integration tests that verify sandbox denials (macOS CI only).
- Feature-gate behind
#[cfg(target_os = "macos")]. - Dependency: Phase 2.
Phase 4: Linux Landlock integration
- Implement Landlock ruleset generation from
AccessPolicy. - Apply ruleset in child process after
fork()beforeexec(). - Detect kernel support and fall back gracefully.
- Add integration tests (Linux CI only).
- Feature-gate behind
#[cfg(target_os = "linux")]. - Dependency: Phase 2.
Phase 5: Windows best-effort enforcement
- Implement job object creation for network and process restrictions.
- Spawn tool process with job assignment.
- No per-path filesystem restriction in this phase.
- Add integration tests (Windows CI only).
- Feature-gate behind
#[cfg(target_os = "windows")]. - Dependency: Phase 2.
References
- RFD 076 — Tool access grants: defines
AccessPolicytypes, cooperative enforcement, filesystem/network/environment access rules. This RFD consumes those types for OS-level enforcement and extends them withCommandRule. - RFD 016 — WASM plugin architecture, sandbox configuration model, secret scrubbing, inquiry-based permissions. This RFD extends its sandboxing concept to subprocess tools.
- RFD 009 — Stateful tool protocol. Sandboxing applies to both one-shot and stateful tool sessions.
- Apple sandbox-exec man page
- Apple Sandbox SBPL Reference
- Landlock documentation
- landlock-rs crate
- Windows Job Objects
- Windows Restricted Tokens
- Chromium sandbox design (Windows)