OpenShell policies in Bromure, and the reasoning behind every connection
NVIDIA's OpenShell describes what a coding agent may touch in one policy file. Bromure Agentic Coding 5.4 enforces those policies in the agent's VM, with Landlock and seccomp inside and the network rules on your Mac, and watches the guest kernel with a sentry module. It then links every network connection to the processes that opened it, the tool call that started them, and the reasoning the agent gave for that call.
A coding agent needs real access to be useful, and a policy says how much. It works best as a single document you can read, check and enforce, instead of settings scattered across a firewall, mount options and the agent's own configuration.
NVIDIA's OpenShell
OpenShell is NVIDIA's open source runtime for agents, released under the Apache 2.0 license. We built on its policy format, where one YAML document covers four areas:
filesystem_policyandlandlock: which paths the agent may read and write, enforced by the Landlock security module in the Linux kernel.process: which user and group the agent runs as.network_policies: which destinations each program may reach, and which requests it may send there, down to the HTTP method and path. Anything not allowed is denied: "OpenShell denies every outbound connection from a sandbox unless a rule in network_policies allows it."
OpenShell also ships a prover that uses an SMT solver to check whether one policy allows anything beyond another. An organization can set a boundary and get a yes or no on every change, instead of reading diffs.
It is a careful design from people who have clearly studied how agents fail, and it is open, so we started from it.
OpenShell in Bromure
In Bromure Agentic Coding 5.4, a workspace can use an OpenShell policy in place
of Bromure's own firewall rules. We took the policy format and its semantics,
not the code: the engine is our own, on the Mac, plus our own components in
the Linux guest. To make sure the two agree, we ran our engine and OpenShell's
reference engine on 125,666 generated cases across network rules, HTTP rules,
MCP, GraphQL, WebSocket and destination address checks. They disagreed on none.
That testing turned up path tricks such as ..%3b and double-encoded slashes
(%252F), which we now handle the way OpenShell does.
Bromure differs from OpenShell in where enforcement runs. OpenShell starts sandboxes through a gateway and a container runtime. Bromure runs each workspace in its own disposable Linux VM on your Mac, so the policy is split between the two sides:
- In the VM: Landlock for the filesystem, seccomp,
no_new_privsand the switch to the policy's user. The next section covers how. - On the Mac: the network rules. Every connection leaves the VM through
Bromure's virtual switch, which checks the destination and port, and its
TLS proxy, which checks HTTP methods and paths, MCP, GraphQL and WebSocket
messages. The Mac resolves and pins the addresses, so DNS can't be used to
slip past an
allowed_ipsrule. Root inside the VM can't edit a rule that lives outside it.
The policy can also name the program allowed to make a connection. With
strict mode on, Bromure checks which binary opened the connection, by hash: curl
may reach api.github.com, and a Python script may not. Combined with an HTTP
rule such as GET-only on api.github.com/repos/**, a DELETE is refused at
the proxy. Landlock can't do any of this: it only confines files.
When an organization sets a boundary from bromure.io, Bromure checks every
workspace policy against it with NVIDIA's openshell-prover when it is
installed, and with a stricter built-in check when it isn't. A policy that
leaves the boundary can't be saved.
We made one deliberate change: each section applies only if it is present.
A policy without filesystem_policy leaves Landlock off, and one without
process switches no user. OpenShell fills in defaults because its drivers
always send a full document. In Bromure, adopting OpenShell never silently
breaks a workspace. That gives three levels:
| The policy has | Applied in the VM | What it guarantees |
|---|---|---|
| Network rules only | Nothing; the agent keeps sudo | The network policy, and observation by the kernel sentry |
filesystem_policy | Landlock, applied while still root | Filesystem confinement that holds even against root |
A process section, or strict mode | Landlock, seccomp, no_new_privs, a non-root user; sudo and docker revoked | All of the above, no path to root, and no way to stop Bromure's own processes |
A few fields are accepted and not enforced. The credential rewrite and signing fields are the main ones: Bromure already keeps real credentials on the Mac and swaps them in on the way out, so the agent only ever holds stand-ins.
Inside the VM: Landlock, seccomp and a privilege drop
The guest half is a reimplementation of the enforcement code in OpenShell's
sandbox-linux crate. It is written in Python with ctypes, because it has to
run inside the VM at boot, where there is no Rust toolchain. It makes the same
system calls the Rust landlock and seccompiler crates make, and a
differential harness builds OpenShell's real crates and compares the two path
by path and system call by system call.
A root supervisor, bromure-sandboxd, starts every agent session. It moves the
session into its own cgroup, the sandbox cgroup, then confines it in three
steps.
Landlock. Landlock lets an unprivileged process restrict itself, and every process it starts, to a list of paths. The supervisor:
- calls
landlock_create_rulesetwith the rights the ruleset governs. On the guest's 6.8 kernel (Landlock ABI 4) that is executing, reading and writing files, listing directories, creating and removing each kind of file, renaming across directories (REFER) and truncating; - opens each path in
read_onlyandread_writewithO_PATHand adds it withlandlock_add_rule, as aPATH_BENEATHrule carrying the read rights or all of them; - calls
landlock_restrict_self.
From then on, any access the ruleset governs and no rule grants fails with
EACCES, whoever asks. Landlock checks the task, not the user, so sudo
doesn't widen it. A process can stack more rulesets but never remove one, and
that is why a filesystem change needs a VM restart while network rules apply
live. landlock.compatibility decides what happens on a kernel without the
ABI: best_effort carries on with what the kernel offers, and
hard_requirement refuses to start the session.
seccomp. In strict mode, or when the policy has a process section, the
supervisor sets no_new_privs and installs OpenShell's three seccomp filters:
- The main filter allows by default and blocks what can get around the
sandbox. That includes
ptrace,bpf,io_uring_setup,userfaultfd,perf_event_open,process_vm_readvandprocess_vm_writev, the whole mount family, andsetns. It blocksmemfd_createandexecveatwithAT_EMPTY_PATH, because running code from an anonymous file bypasses Landlock. It blocksunshareandclonewithCLONE_NEWUSER, because a new user namespace hands out capabilities. And it blocks sockets in the packet, Bluetooth and vsock families, and netlink other than routing. - A compatibility filter answers
clone3withENOSYS. A seccomp filter can read register values but not follow pointers, andclone3passes its flags through a pointer.ENOSYSmakes glibc fall back toclone, whose flags the main filter can check. - A hardening filter stops the session from reaching its supervisor, which runs
as the same user: no
ptrace,process_vm_*,pidfd_getfdorkcmp, and no signals to the supervisor's process or group.
We diverge from OpenShell twice, and only in what gets reported or how signals
are aimed. Our filters carry SECCOMP_FILTER_FLAG_LOG, so a refused system
call reaches the kernel's audit hook, where the sentry sees it. And the
signal rule is narrowed so that shell job control (kill %1) still works in an
interactive terminal.
The privilege drop. run_as_user switches the session to the policy's user,
with none of the default user's groups, so no sudo and no docker. The
user's home is mounted into the session through an idmapped mount, so files the
session creates still belong to the default user on disk, and the agent's
configuration still applies.
A sentry in the guest kernel
To record what an agent actually did, Bromure loads its own kernel module into the VM, the sentry. Its design assumes the agent can become root inside the VM, so nothing root could stop counts as evidence: capture, buffering and transport all run in the kernel.
Capture. The sentry registers kprobes from kernel space:
- program starts, at
security_bprm_committed_creds, with the command line and the process's start time; - credential changes: the
setuidandsetgidfamilies,capsetandcommit_creds; ptrace, module loads and unloads,bpf,kexec_load,mount,unshareandsetns;- new network flows: TCP when the first SYN goes out (
tcp_connect), UDP on the first send to a destination, and ICMP and raw sockets, each with the chain of up to eight processes behind it; - refused system calls, at
audit_seccomp; - refused file operations, through kretprobes on the kernel's file security
hooks:
security_file_open, and thesecurity_path_*hooks for mkdir, rmdir, unlink, symlink, link, rename and truncate. When a hook returnsEACCESorEPERMfor a process in the sandbox cgroup, that's a denial. Executing a file Landlock doesn't allow shows up as a refused open withFMODE_EXEC.
The supervisor gives the sentry the sandbox cgroup's ID once, through a module parameter that can be set but never changed. Root can't point it somewhere else and have the agent's activity labelled as Bromure's own.
Transport. A kernel thread opens an AF_VSOCK socket to the Mac with
sock_create_kern. The socket has no file descriptor, so it doesn't appear
under any process in /proc and nothing in user space can close it. Events
go through a 256-slot ring, and each kind is rate-limited to 64 a second, with
bursts of up to 256. Each frame is a 4-byte length followed by JSON.
Staying loaded. The module has no exit function, so the kernel refuses to
unload it, and it holds a reference to itself on top. Right after loading it,
the loader raises the kernel's lockdown to integrity, or confidentiality
when the workspace requires the sentry. From then on root can't load an
unsigned module, write kernel memory through /dev/mem or kexec into a
kernel without the sentry.
Proving it is alive. Every second the sentry sends a heartbeat with a sequence number, counts of dropped and rate-limited events, and how many probes are armed. That count alone can be fooled: switching off every kprobe at once through debugfs leaves each probe's own flags untouched, so the count still looks healthy while nothing fires. To catch that, the sentry keeps a kprobe on an empty function of its own and calls it once per heartbeat. If the hit counter doesn't move, probes aren't firing, whatever the reason, and the heartbeat says so.
Proving it is the sentry. Each load generates a 32-byte secret. The first
hello carries it, and the Mac pins it for that boot. A reconnect never repeats
the secret. It sends
sha256(secret, boot_id, connection number) instead, so a process that read
the secret out of memory after the first hello still can't impersonate the
module. A second sentry, a wrong secret, a reused connection number, a gap in
the sequence or 15 seconds of silence all count as tampering. Root's remaining
move is to crash or power off the VM, which the Mac sees as silence.
Distribution. Modules aren't built in the VM. CI builds one for each of the 20 Ubuntu 24.04 kernel releases, twice, and the two builds have to match byte for byte. Each module and the catalog listing them are signed with an ed25519 key, and the app refuses a module whose signature or hash doesn't check out.
One lesson from building it: for a while, the file-denial probe compared an
unsigned return value against -EACCES and never fired once. The tests only
checked that the probe was registered, which it always was. They now check
that a denial produces an event.
Judging the events. On the Mac, events are judged by what they mean, not by which system call fired. Each one adds weight to a score, and 20 within 60 seconds trips the alarm:
| Event | Weight |
|---|---|
| Tampering with the sentry, or root gained from inside a strict sandbox | 20 |
An unsigned module, a BPF load, a kexec, an attempt to lower lockdown; one program refused 20 times in a minute | 10 |
A ptrace, or a system call refused by seccomp | 3 |
| A mount or a namespace change; dropped events | 2 |
| A file operation refused by the sandbox | 1 |
A sudo in a workspace that allows sudo | 0, shown on the timeline |
An alarm that fires in the first minute teaches people to ignore the real ones. So Bromure's own housekeeping runs outside the sandbox and never shows up as a denial, and events from Bromure assembling the VM at boot are counted, not alarmed on, within a fixed boot window. When the alarm trips, Bromure can quarantine the workspace: it cuts the VM's network and pauses it.
Lineage: the reasoning behind every connection
A network log shows that something in the VM connected to a host, but not why. When an agent does the work, the why is what you need to know.
Bromure can tell you, because it sees both ends. Its AI proxy sits between the agent and the model provider and reads each response before the agent acts, so it knows every tool call, the command it will run, and what the model wrote just before it. The sentry reports every program the VM starts, with its command line, and every new connection, with the chain of processes behind it. Bromure joins the two.
Say you ask the agent to add a health check that retries your payments API.
It decides it needs an HTTP client and runs npm install axios. npm fetches
the package from registry.npmjs.org, which the policy allows. Then a
postinstall script in the download starts and tries to reach
telemetry.example.net, which no rule allows. The connection is refused at the
proxy on the Mac, and the lineage shows how it happened:


The agent never typed a command that reached telemetry.example.net. A plain
network log would show a Node process talking to an unknown host; the lineage
shows it was a dependency's install script, under the npm install the agent
ran for a stated reason.
The join works in three steps:
- Tool call. The AI proxy keeps every tool call from the model's response
for ten minutes: its ID (
tool_use_id, or OpenAI'scall_id), the command, and up to 2,000 characters of what the model wrote before it in the same turn. - Shell. From the sentry's program starts, the Mac keeps a process table
for each VM, keyed by pid and start time, so a reused pid can't inherit
someone else's history. A shell started under the agent within 30 seconds
of the response is tied to the tool call when the command appears in its
command line, once the agent's own quoting is undone. Claude Code, for
instance, runs commands as
bash -c '… eval '"'"'<command>'"'"' …'. That is an exact match. When the agent rewrote the command, Bromure falls back on the first word, the timing and the ancestry, and labels the match as fuzzy. - Decision. A flow is matched to the switch's allow or deny on its protocol, destination, port and source port within 5 seconds, and to the proxy's decision when it inspected the request.
Every connection below the shell inherits the link. That includes traffic through Bromure's proxy bridge, which would otherwise all look like connections to localhost. The bridge starts each stream to the Mac with a line naming the client's source port, so the Mac can map the local connection to the real destination.
In the Security window, Show the Whole Chain on a network row opens that view.
Some models go straight to the tool call without writing anything first. Then the view shows the prompt and the call's stated intent instead. Background services and anything started outside a tool call are marked as not traced to an agent.
Lineage also changes what happens after a block. An agent that only sees a connection reset or a permission error takes it for a fault and tries another host, port or tool, which from the outside looks like an agent going rogue. Bromure tells the agent, in its own session, that the policy refused the request and why, and to ask you rather than work around it. The Sandbox drawer of a running workspace lists what was blocked, with an Allow button for each, and the policy advisor drafts rules from the denials for you to approve or not.


The agent's reasoning is always shown on your own Security Timeline. It is sent to an organization's console on bromure.io only when that organization turns it on, which it is not by default, and never for private profiles. Tokens on command lines are redacted before anything leaves the Mac.
Turning it on
OpenShell policies, the kernel sentry and lineage arrive in Bromure Agentic Coding 5.4. They are off by default. To turn them on for a workspace, open Settings, then the workspace's Sandbox Policy, and set Sandbox engine to OpenShell + Sentry. You can write the policy there, or import an existing OpenShell YAML file.


- Network rules apply live. Filesystem changes take effect at the next VM start, because Landlock can only narrow what a running process may do.
- The sentry runs in best-effort mode by default. Set it to Required and Bromure raises the kernel's lockdown to its strictest level, and treats a sentry that never connects as tampering.
- With the sentry required, a workspace's shell is ready in 4 seconds when its kernel's module is cached, and in 6 seconds when Bromure has to fetch it.
To see it at work, watch Claudia's three episodes: she writes a policy, tunes it while an agent works, and traces a 2 AM supply-chain attack back to the agent's reasoning.
Thank you to the OpenShell team at NVIDIA for publishing the policy format, the reference engine and the prover in the open. We built on all three.