OpenShellSandbox runs your agent's tools in an NVIDIA OpenShell sandbox.

run_bash and the file tools (read_file, write_file, edit_file, delete, glob, grep, list_directory and generate_file) run inside the sandbox, with the same output as on the host. The loop, the model calls, memory and hooks stay in your process. OpenShell isolates the commands with Landlock and seccomp and decides network access per program, so the agent reaches only the hosts you allow.

Requirements

Install the extra:

pip install "harnessx[openshell]"

You also need a running OpenShell gateway. Check it with the OpenShell CLI:

openshell status

On macOS, run the gateway with its MicroVM driver. Docker Desktop's Linux kernel has no Landlock, which OpenShell requires. Set compute_driver = "vm" in ~/.config/openshell/gateway.toml. See the OpenShell repository for installing the gateway.

Run an agent in a sandbox

Pass the sandbox to the agent with the filesystem and bash bundles:

import asyncio
from harnessx import Agent, AgentConfig
from harnessx.openshell import OpenShellSandbox

async def main():
    sandbox = OpenShellSandbox(
        project="./repo",
        allow=["github.com", "pypi.org"],  # hosts commands may reach; nothing else is
        secrets=["GITHUB_TOKEN"],          # usable inside, never readable there
    )
    async with Agent(AgentConfig(), tools=["filesystem", "bash"], sandbox=sandbox) as agent:
        result = await agent.run("Run the tests and fix the failure")
        result.raise_for_status()
    # ./repo now has the agent's changes

asyncio.run(main())

To see it work end to end, run the example. With no --project it builds a small project with one failing test, lets the agent fix it inside the sandbox, and copies the fix back:

uv run python examples/08-sandboxes/openshell_backend.py
uv run python examples/08-sandboxes/openshell_backend.py --project ./repo --allow pypi.org --secret GITHUB_TOKEN

File tools you registered on a ToolRegistry before creating the agent move into the sandbox too, with their registration options. base_path does not apply to a sandbox that owns the file tools: the sandbox decides which paths exist, and passing one raises ValueError.

What the agent does with the sandbox

You never start, copy into or clean up the sandbox. Unlike a local Sandbox, an OpenShellSandbox belongs to the agent you give it to. One sandbox serves one agent; giving it to a second agent raises ValueError.

  • Created on first use. The sandbox is created on the first tool call that needs it. A run that only talks creates none.
  • Project copied in. The project folder is copied to /sandbox/project, leaving out .venv, node_modules, __pycache__ and .DS_Store. Change the list with exclude= (shell patterns, matched against each name).
  • Changes copied back. After every run, and when the agent closes, files the sandbox changed or deleted are copied back to project.
  • Local edits kept. If you edited a file locally while the agent was working, your version is kept and the sandbox's version is written beside it as <file>.sandbox. A file deleted in the sandbox but edited locally is kept.
  • Deleted on close. The sandbox is deleted when the agent closes. keep=True leaves it running.
  • Named after the session. The name is derived from the agent's session and recorded in its metadata. A resumed session finds a kept sandbox again and does not copy the project in a second time.

sandbox.last_sync holds a SyncReport from the last copy back, with updated, deleted and conflicts lists of project-relative paths. await sandbox.sync() copies back on demand and returns the same report.

Paths the model can use

The model uses project-relative paths, such as src/app.py. A local absolute path into the project is translated to the sandbox, so /home/you/repo/src/app.py reaches the same file. Paths outside the project and its inputs are refused.

inputs= adds local files or folders, copied in read-only and never copied back. The model reaches them by their local paths:

sandbox = OpenShellSandbox(project="./repo", inputs=["~/data/fixtures"])

A write to an input is refused.

Network access

Set allow, secrets or policy, and HarnessX sends OpenShell a policy that closes network access except for what you list. A host you allow is reachable from every command the agent runs, whichever program in it connects. With none of the three, HarnessX sends no policy and the sandbox runs under your gateway's default policy, so set at least one of them when the network should be closed.

Allow hosts

allow lists the hosts commands may reach. The port is 443 unless you write "host:port":

sandbox = OpenShellSandbox(project="./repo", allow=["pypi.org", "example.com:8443"])

A few well-known hosts bring the hosts they need with them:

You allow Also allowed
github.com api.github.com, codeload.github.com, objects.githubusercontent.com, raw.githubusercontent.com
pypi.org files.pythonhosted.org
huggingface.co cdn-lfs.huggingface.co, cas-bridge.xethub.hf.co

allow takes a list. A bare string raises TypeError.

Secrets

secrets names environment variables that commands can use but never read. Inside the sandbox the variable holds a placeholder. OpenShell replaces it with the real value only on requests to that secret's hosts. The secret's hosts are allowed too.

These names know their hosts:

Variable Hosts
GITHUB_TOKEN github.com, api.github.com
GH_TOKEN github.com, api.github.com
HF_TOKEN huggingface.co
NPM_TOKEN registry.npmjs.org
ANTHROPIC_API_KEY api.anthropic.com
OPENAI_API_KEY api.openai.com

For any other variable, map it to its host or a list of hosts:

sandbox = OpenShellSandbox(
    project="./repo",
    secrets={"MY_TOKEN": "api.example.com", "OTHER_TOKEN": ["a.example.com", "b.example.com"]},
)

Every secret must be set in your environment when you create the sandbox, or the constructor raises ValueError. A name that is not in the table above must be given as a mapping. HarnessX creates an OpenShell provider for the secrets and removes it when it deletes the sandbox.

OpenShell policies and providers

policy= takes an OpenShell policy as a YAML file path, YAML text or a mapping. It replaces HarnessX's restrictive default as the starting point. providers= attaches existing OpenShell providers by name. allow and secrets add to both:

sandbox = OpenShellSandbox(
    project="./repo",
    policy="openshell-policy.yaml",
    providers=["my-provider"],
    allow=["pypi.org"],
)

await sandbox.policy() returns the sandbox's live policy as a mapping.

When the policy refuses a connection

A refused connection reaches the command only as "Permission denied". HarnessX reads OpenShell's audit log, and run_bash tells the model which host was refused and that the refusal comes from the policy, not from the network:

[blocked by the sandbox's network policy: example.com:443. This is not a network fault; ask the user to allow it.]

SandboxResult.denials carries the same list. So does the SANDBOX_EXEC hook: ctx.data["denials"] in SandboxExecData, with tier set to "openshell". The agent hands its own hooks to the sandbox, so an observer on the agent sees every command:

from harnessx import HookEvent

async def report_denials(ctx):
    if ctx.data["tier"] == "openshell" and ctx.data["denials"]:
        print("Refused:", ", ".join(ctx.data["denials"]))

agent.hooks.on(HookEvent.SANDBOX_EXEC, report_denials)

See Middleware & hooks for the other fields.

Review drafted rules

OpenShell drafts network rules from blocked connections. await sandbox.pending_rules() lists them as PendingRule objects with id, rule_name, hosts, binary and rationale. Approve a rule to add it to the sandbox's policy, or reject it with a reason:

for rule in await sandbox.pending_rules():
    print(rule.rule_name, rule.hosts, rule.binary, rule.rationale)
    if rule.hosts == ["pypi.org:443"]:
        await sandbox.approve_rule(rule)  # applies to the next connection
    else:
        await sandbox.reject_rule(rule, "not needed for this task")

print(await sandbox.policy())

Command timeouts

timeout_seconds (default 30) is the longest any single command may run. run_bash passes the model's timeout (30 seconds unless the model asks for more), and the shorter of the two applies. A command that runs out of time is reported to the model as timed out.

To let the agent run long test suites or builds, raise timeout_seconds; the model can then ask for a longer timeout. The tool's own registry timeout (300 seconds by default) still applies.

Use the sandbox without an agent

Outside an agent you own the sandbox. Use it as an async context manager. It starts on entry, then copies back and deletes on exit:

import asyncio
from harnessx.openshell import OpenShellSandbox

async def main():
    async with OpenShellSandbox(project="./repo", timeout_seconds=300) as sandbox:
        result = await sandbox.execute_command("python3 -m pytest -q")
        print(result.exit_code, result.stdout)

asyncio.run(main())

execute(code) runs Python code with python3 in the sandbox, so the image must have it.

Options

All options except project are keyword-only.

Option Default What it does
project "." Local folder the agent works on. Must exist. Copied in on first use; changes are copied back
inputs () Extra local files or folders, copied in read-only and never copied back
allow () Hosts commands may reach, "host" or "host:port", with well-known companions
secrets () Environment variables usable but unreadable inside: a list of well-known names or a mapping of name to hosts
policy None OpenShell policy (YAML path, YAML text or mapping) to start from instead of HarnessX's default
providers () Existing OpenShell providers to attach, by name
image None OCI image for the sandbox. None uses the gateway's default
exclude None Names (shell patterns) left out of both copies. None is .venv, node_modules, __pycache__, .DS_Store
keep False Leave the sandbox running when the agent closes
name None Sandbox name. None derives one from the agent's session
workspace "default" OpenShell workspace
gateway None Gateway name. None is the active one (OPENSHELL_GATEWAY or the CLI's)
timeout_seconds 30 Longest a single command may run, in seconds
client None An openshell.SandboxClient to use instead of connecting to gateway. The sandbox does not close a client you pass

See Code execution for the local sandbox tiers and Permissions for approving tool calls. The sandbox limits what a command can reach; permissions decide whether the command runs at all.