Observing a syscall is not the same as refusing it
July 5, 2026
If you want to sandbox an AI agent, the first tool most people reach for is eBPF. Tetragon, Falco, something in that family. It is the right instinct for the wrong problem, and understanding why clarifies what an agent sandbox actually has to be.
What eBPF tooling is good at
Tetragon attaches to kernel hooks and observes what a process does at the syscall level, with rich context: which binary, which container, what arguments. It can kill a process on a matching event. It is fast, it needs no changes to the workload, and for detecting compromise across a fleet it is genuinely excellent.
The enforcement story is where it gets thinner for this use case, and the reasons are structural rather than a matter of policy quality.
Kill-on-detect is post-hoc. The observation and the enforcement action are separate events. There is a window, small but real, between a syscall being observed and the process being terminated. For most security monitoring this does not matter. For an agent whose single openat on a credentials file is the entire attack, terminating it a moment later means you have logged the breach rather than prevented it.
Argument-based policy is fragile at the kernel boundary. Deciding based on syscall arguments means resolving what those arguments actually refer to, and the classic time-of-check-to-time-of-use races have not gone away. Path resolution, symlinks, and file descriptors that get reused are all still live problems. Rules that filter on a path string are approximations of the thing you meant.
It sees syscalls, not intent. This is the real one. A tool call from an agent lands on the kernel as some ordinary sequence of file and socket operations. By the time it is an openat, everything about who authorized it, on whose behalf, and under what delegated scope has been stripped. eBPF can tell you a syscall happened. It cannot tell you the syscall was made in service of a request the agent was not authorized to fulfill, because that context does not exist at that layer.
You can see the shape of this in production agent sandboxes generally. Out-of-process policy enforcement with default-deny network egress genuinely reduces attack surface, and an agent cannot override constraints it does not control. But a legitimate API call that exfiltrates data looks exactly like ordinary traffic at the sandbox boundary. Enforcement at the wire tells you the connection is allowed. It cannot tell you the request should not have been made.
Why an application kernel is a different shape of answer
gVisor takes a different approach. Rather than filtering syscalls on their way to the host kernel, runsc intercepts them and implements them in a user-space application kernel called the Sentry. The workload’s syscalls are serviced by the Sentry, which makes a much smaller and more deliberate set of calls to the host on its own terms.
Two properties follow, and both matter here.
The Sentry is in the path, not beside it. It is not observing a syscall that is going to happen anyway. It is the thing that implements the syscall. A decision to refuse is a return value, not a race against a process that is already executing. There is no window because there is no separate enforcement action.
The Sentry holds real state. It maintains its own view of the filesystem, its own file descriptor table, its own network stack. It knows what a descriptor refers to because it created it. This is what makes decisions at this layer resolvable rather than approximate, and it is exactly what an argument-matching filter at the kernel boundary lacks.
The cost is honest and worth stating: you pay syscall overhead, some workloads are meaningfully slower, and gVisor’s syscall surface is not perfectly complete. For general container isolation those trade-offs are often not worth it. For a workload that is by construction executing attacker-influenced instructions, they are a reasonable price.
Why upstream is not sufficient
Here is where I think it gets interesting, and where I have spent most of my time.
Stock gVisor gives you a strong isolation boundary with a static policy. The container gets a filesystem view and a network configuration at start, and the Sentry enforces that. Good, and not enough for agents.
Agent authorization is not static. An agent receives a request, and what it should be allowed to do depends on what that specific request was authorized to accomplish, by whom, and through how many delegation hops. The permitted set changes per task and per hop. A policy fixed at container start cannot express that, because the thing it needs to condition on does not exist yet when the container starts.
So the Sentry has to become policy-aware in a way it currently is not. It already sits at exactly the right place: every syscall passes through it, and it holds the state to resolve what each one means. What it lacks is a channel through which an authorization context arrives and updates, and a hook in the syscall path that consults that context rather than a policy baked in at startup.
That is a fork, not a configuration. I would rather it were configuration. But the extension point for “consult a live, per-request authorization context on this syscall” does not exist upstream, and adding it means touching the Sentry.
The general principle I would pull out of this, beyond the specific tooling: the enforcement point for an agent has to sit where authorization context and syscall execution are both available. eBPF has the syscalls and none of the context. The application layer has the context and no ability to enforce. The application kernel is the only layer I have found where you can have both.
I will write about what the authorization context itself should contain separately, once that work is further along.