What Sevorix Enforces, and Where It Stops
A control whose limits you cannot describe is a control you cannot evaluate. These are ours, stated in full.
What This Page Is
Most security products publish a feature list. A feature list tells you what a control does when it works, and nothing about the conditions under which it does not. For a runtime enforcement product those conditions are the entire evaluation, so we publish them.
Everything below is drawn from the same engineering documentation our own developers work from. Where a guarantee is conditional, the condition is stated. Where a gap exists and is not closed, it is named as a gap rather than dressed up as a mitigation.
The adversary in this model is the agent itself: compromised, prompt-injected, or simply mistaken, running with the privileges of the user who launched it. Assume it is multithreaded, assume it is actively hostile, and assume it has read this page. Everything Sevorix does is designed against that reader.
Which Platform This Describes
Everything below describes Sevorix on Linux, WSL2 included. That is where the enforcement path is built on seccomp, BPF LSM, network namespaces and cgroups, and every guarantee on this page is stated in terms of those primitives.
macOS has none of them. On a Mac — Sevorix Lite on Intel or Apple Silicon, Sevorix Pro on Apple Silicon — enforcement is the policy engine applied through the local proxy and the sevsh shell wrapper. The syscall hold, the enforcement tiers, the namespace-only route out, the root-owned bind-mounted shell and the cgroup freeze described below are not part of that build. If macOS is the platform you are evaluating, read this page as a description of what the Linux build adds, not of what you are running.
The Observation Boundary
The hardest limit comes first, because everything else on this page is conditional on it: a process not launched through sevsh is not observed at all. Both enforcement tiers intercept only processes spawned inside a Sevorix session. A binary started in another terminal, a service already running under systemd, a container brought up out of band — none of these are constrained by policy, and none of them appear in the traffic log. Sevorix does not claim host-wide coverage and does not attempt it.
In practice the boundary is established at launch: sevorix integrations start runs Claude Code, Codex, or the agent of your choice through the supervised shell, and everything that process tree goes on to spawn inherits the boundary. Coverage is therefore a deployment property rather than an installation property, and it is worth auditing directly — sevorix status lists the registered sessions, and a process that is not in one is not covered.
Within that boundary, the guard is not something the guarded party can remove.
- The agent cannot replace the shell it was given: sevsh is installed root-owned at a path outside any directory the agent's user can write, and bind-mounted over /bin/bash and /bin/sh as root. A location writable by the party being constrained would let that party replace the constraint.
- The check and the mount cannot be raced apart: Before mounting, the launcher verifies that the binary and every directory above it are root-owned and not group- or other-writable, and that its SHA-256 matches the digest the installer recorded from the installed bytes. The binary is opened on a descriptor first, and all of those checks — plus the mount itself — go through that descriptor. Checking a path and then mounting that path is a time-of-check/time-of-use race; the descriptor guarantees the inode that was hashed is the inode that gets mounted.
- The digest is recorded at install, not baked in at build: A legitimate upgrade replaces the binary and its recorded digest together and keeps passing. A binary that arrived any other way has no matching digest and is refused.
- Rewriting its own environment moves nothing: Egress is constrained by the network namespace the session hands the agent, which has no interface but the one it was given. The proxy is in the path because there is no other route out, not because an environment variable politely points at it.
Preventive Enforcement and Advisory Enforcement
Sevorix intercepts syscalls using seccomp user notification, which suspends the calling thread at the syscall boundary and holds it there until policy answers. The kernel's own documentation for that facility states plainly that it "can not be used to implement a security policy". We agree with the manual page, and we are precise about which of our verdicts the caveat actually applies to.
The reason is one flag. When the supervisor allows a call by telling the kernel to continue it, the kernel runs the syscall as originally issued — which means it re-reads the pointer arguments at execution time. A verdict that was reached by reading those same arguments out of the target's memory can therefore be defeated by a second thread in the target rewriting the buffer while the call sits suspended. That is a genuine time-of-check/time-of-use window.
So every allow is classified by how it was reached, and delivered accordingly:
| How the verdict was reached | How the allow is delivered | Race-free? |
|---|---|---|
| No rule consulted the target's memory | The kernel continues the syscall as issued | Yes — there was no time of check |
| Path-checked, and the syscall is emulatable: open, openat, creat, unlink, unlinkat, rmdir, rename, renameat, renameat2 | The supervisor performs the syscall itself against the validated path and hands back the result | Yes — the kernel never executes the call, so it never re-reads the pointer |
| Path-checked, and the syscall is not emulatable: execve, execveat, connect, and others like them | The kernel continues the syscall; the event is reported as advisory | No — enforcement here is advisory |
| Path-checked, but not safely emulatable in this environment | Denied, EPERM, recorded as unverifiable | Fails closed |
Emulation requires that the supervisor can faithfully stand in for the target: the same mount namespace and root, identical credentials, and the same umask when a file is being created. Emulated opens additionally need Linux 5.9 or later. Where any of that does not hold, the syscall is denied rather than continued — continuing would silently reinstate the vulnerability on exactly the decisions that needed protecting.
One clarification, because the table above is easy to misread: denials are not affected by any of this. A matching deny returns EPERM before execution, on every syscall we intercept. The caveat is about approvals — specifically an approved execve() or connect() whose rule matched on a path — and never about blocks. A blocked syscall is blocked.
An advisory allow is still policy-evaluated, still logged, and still becomes a block if the policy says block. What it does not guarantee is that the path the supervisor approved is the path the kernel goes on to execute, in the specific case of a multithreaded target actively racing the check. If that adversary is in your model for execve and connect, treat those approvals as detection rather than prevention, and put the preventive weight on deny rules — which do not carry the caveat.
Path Rules, and the Hardlink Gap
A path rule that matched only the literal string an agent passed would be trivially evaded, with no race required at all. A rule on /etc/shadow would miss /etc/./shadow, miss a bare shadow after the agent had changed directory into /etc, and miss a symlink the agent created a moment earlier. That is a simpler and more easily exploited class than the timing window above, so it is closed.
Every path pattern is matched against both the string the target passed and its canonical form, resolved in the target's own filesystem view: . and .. collapsed, symlinks followed, relative paths resolved against the target's working directory or the directory descriptor an *at syscall named, all scoped to the target's root. Resolution runs through openat2 with RESOLVE_IN_ROOT so it happens in the target's namespace rather than the supervisor's, and the descriptors it pins are handed to the emulation path above — so an emulated syscall acts on the exact inode that was checked instead of walking the path a second time. The raw string stays a candidate too, so rules written against literal paths keep working and no existing rule becomes weaker.
Because openat2 is required for this, a path pattern fails to compile on kernels older than 5.6 rather than being enforced in a form that can be walked around. A rule that cannot be enforced correctly does not load at all.
- Deny and allow are deliberately asymmetric: A deny matches if any candidate path matches, and a path that cannot be read counts as a match. An allow matches only if every candidate does, and an unreadable path never does. An exception that cannot be verified is not granted.
- Hardlinks are not covered: A hardlink is a real second name for one inode, so there is nothing to resolve away — the second name is as legitimate as the first. Catching it requires inode identity, which a regular-expression path pattern cannot express. This is an open gap, not a mitigated one.
- What bounds the hardlink gap: fs.protected_hardlinks=1, the default on current Linux kernels, prevents an unprivileged process from hardlinking a file it does not own. The residual exposure is therefore rules protecting files the agent itself owns. If your hosts have that sysctl turned off, the exposure is wider — it is worth checking rather than assuming.
Enforcement Tiers, and What DEGRADED Means
Syscall interception runs at one of two tiers. Standard applies a seccomp user-notification filter per session and is always available on Linux. Advanced layers BPF LSM hooks on top of it, and requires both a kernel booted with bpf in its active LSM list and an explicit opt-in in your settings — neither one alone is enough.
The tier in force is the minimum of what you configured and what the running kernel actually provides. Detection never upgrades a session: a kernel that could support BPF LSM but has no opt-in set stays on Standard, so Advanced remains a decision you make rather than one made for you. Detection does downgrade.
A downgrade does not stop the daemon. Standard-tier enforcement — the proxy, the policy engine, the supervised shell, seccomp — is real protection, and discarding all of it because one layer is unavailable would leave the agent entirely unguarded. What a downgrade does instead is announce itself, in four places:
- At startup: The daemon logs a warning naming the tier you asked for and the tier it could actually provide.
- In the CLI: sevorix status carries a DEGRADED marker. This is the check to run when you want a one-line answer about a specific host.
- In the Observatory: An amber badge in place of the normal tier indicator.
- Over the API: GET /api/version returns "enforcement_tier_degraded": true. This is the field to assert on if you are monitoring tier state from configuration management or a health check.
Stock WSL2 kernels do not have BPF LSM available and cannot be given it, since there is no way to pass custom boot parameters. Advanced is permanently unavailable there and Standard remains fully functional — a WSL2 deployment reporting DEGRADED is expected, not broken.
The startup probe establishes that the kernel can attach BPF LSM programs. It does not establish that every hook did attach. If a hook fails to load for some other reason — missing BTF, a kernel and loader mismatch — the daemon logs a warning and continues, and the reported tier will still read Advanced. The detection closes the common case, which is any kernel without bpf active. It does not close that one, and we would rather say so than let a green badge imply more than it verifies.
When a Review Cannot Contain the Agent
When an action lands in the yellow lane and is routed to a human, Sevorix suspends the agent's process tree — a cgroup freeze — before it announces the review. It is worth being precise about what that freeze is for. The flagged action itself is always held regardless: the caller is blocked on the response and cannot proceed either way. What the freeze buys is containment of the rest of the session, because without it every other process in the agent's tree keeps forking and exec'ing throughout the review window, and a review that cannot suspend the agent provides no containment at all.
The freeze runs through a small root-owned helper installed alongside the product. If that helper is missing, has no passwordless sudo rule, or the agent is not running inside a registered session cgroup, the suspension cannot happen. That case is configurable, and it is never silent.
- A zero exit code is not evidence of a freeze: The daemon confirms the state against the kernel itself, reading the cgroup's own event file for the frozen flag rather than trusting the helper's exit status. A freeze is reported as successful only when the kernel says it landed.
- containment: "required" is the default, and fails closed: The flagged action is blocked rather than held in a review that provides no containment. An event naming the cause and the remedy is broadcast, and the Observatory raises a banner. An unrecognised value for this setting falls back to required — a typo must not quietly downgrade containment.
- containment: "best_effort" reviews without suspension, and says so: The review proceeds, the failure is still announced, and the pending card is marked NOT FROZEN. It is never reported as frozen and never passed over in silence. The Observatory renders three distinct states — frozen, containing, not frozen — because silence was previously indistinguishable from a freeze that had not landed yet.
- Recovery needs no restart: Containment is re-evaluated on every flagged action, so installing the helper, adding the sudoers rule, or starting the agent through a supervised session restores reviews immediately.
The emergency stop follows the same discipline, on the endpoint where a false report of success is most dangerous. A session is reported killed only after the kernel confirms its cgroup is empty. A partial kill emits a failure event carrying a per-session reason and remedy, rather than the event the Observatory reads as "this session is over". A session that survived stays registered, because deregistering it would remove the operator's only handle on a live and possibly compromised agent. And held commands are not released on a failed kill — releasing them would hand a still-running agent an immediate allow on everything it had pending, for anyone whose timeout action is set to allow.
What the Scanners Can and Cannot See
The syscall layer is not the only path an action can take. Shell commands, outbound requests, and the response bodies flowing back to the agent are evaluated by the policy engine, and the honest limits there are different in kind — they are about visibility and budget rather than timing.
- Encrypted responses need TLS interception: HTTPS response bodies are only visible when TLS interception is enabled. Without it a CONNECT is an opaque byte pipe and there is nothing to scan. If inbound policies are loaded in that configuration the daemon warns at startup rather than reporting clean scans of content it never read.
- Inbound scanning has a budget, and the remainder is reported: Response bodies are far larger than request bodies, so they get a segment and wall-clock budget rather than a hard cap. Whatever is left when the budget runs out is reported explicitly as an unscanned remainder. It is never treated as clean.
- That budget is deliberately not fail-closed by default: Response size is chosen by the party being defended against. An unconditional fail-closed would let any site make itself unreadable by padding a page, turning the defence into a denial of service against your own agent. The default forwards partially-scanned content and records a flag-level event; block and allow are both available if your deployment prefers one. A classifier that is genuinely unavailable still fails closed to a block in every setting — that failure is not attacker-selectable.
- Scannability is decided by the bytes, not the header: Content-Type is chosen by the server being fetched, which is exactly the party this defends against — so a response carrying an injection labelled as an image or an octet stream must not skip the scanner. Scanning is gated on sniffing the actual bytes instead. A skipped body still logs an event, because "not scanned" must never be visually indistinguishable from "scanned clean".
- Classifier artifacts are pinned by content digest: The model and tokenizer for each classifier are pinned to a SHA-256 and an exact byte size, verified at download and again at load. A mismatch fails closed and there is no bypass flag: a tampered classifier that scores every injection as benign is indistinguishable from a working one at runtime, so an unverifiable artifact is refused rather than trusted.
- Scoring has a ceiling, and does not partially score past it: Content is scored in overlapping windows so an injection is not missed by reading only the opening tokens. Past a hard ceiling, sized from measured model latency against the scoring deadline, content is rejected outright and fails closed rather than being partially read and called clean.
The Rules Every Verdict Obeys
A threat model is only as good as the invariants underneath it. These are the ones that decide what happens when policies conflict, when a check cannot be completed, and when something the verdict depends on is unreachable.
- An allow supersedes everything, by design: A matching allow short-circuits the whole evaluation, so a policy set can deny broadly and carve out narrow exceptions — deny all aws, allow aws s3 ls. Allows run in a separate earlier pass, so precedence never depends on where an exception happens to sit in a file or a role.
- A user allow never overrides a built-in block: Built-in policies run in their own earlier pass and return before the user pass starts. They exist to stop an agent rewriting its own session binding, which is exactly what an agent-authored policy file would target.
- An allow that cannot be evaluated does not allow: If a checker or classifier cannot render a verdict, the configured action is replaced by a block and returned immediately, and no other matching allow can rescue it. Otherwise a broken checker becomes attacker-selectable by picking input that also matches a healthy exception.
- A checker that cannot answer forces a block: An external checker that is missing, dies on a signal, or fails to exit within its deadline forces a block regardless of what the policy's action said. Only a clean non-zero exit counts as a real "no match".
- Session control is token-gated and fails closed: Registering, killing, freezing, or re-roling a session requires a per-session 256-bit token, compared in constant time. A missing header, a wrong token, or a daemon with no token configured all yield 401. The supervised shell strips that token from every child environment, because the child is the agent being sandboxed and the token authorises killing the session supervising it.
- The jury can only narrow: The multi-model consensus engine sits inside the deterministic floor, never above it. It only ever sees actions the compiled policy already permits, and its only power is to take one away. A member that errors or times out counts as a block, and a hung jury does not open a door.
- A PID file is not evidence of a process: Operating systems recycle PIDs, and a PID file outlives a daemon that crashed or was killed. Nothing is signalled that has not first been positively identified against the kernel's own record of which binary is running at that PID — so a stop or an emergency kill cannot silently signal an unrelated process and report success.
Out of Scope
So that this page bounds itself rather than leaving the bound to be inferred, the following are explicit non-claims. Sevorix does not defend against them and does not attempt to.
- An adversary who already holds root on the host: Root can unload the enforcement, replace the supervised shell, or rewrite policy directly. Sevorix constrains the agent, not the owner of the machine.
- Kernel exploits and privilege escalation: Enforcement is built on seccomp, BPF LSM, and cgroups. An attacker who can subvert those primitives is operating below the layer Sevorix works at.
- Anything outside a supervised session: A process not launched through sevsh is not observed at all. Coverage is a property of how you deploy, and it is the first thing to audit.
- The hypervisor, the hardware, and physical access: Out of scope entirely.
- What you send to a jury you configured: If you enable the consensus engine, the action under review and its accumulated session context are sent to the model providers you configured, under your own API keys, as written — we do not mask or tokenize it on the way out. That is a disclosure boundary you choose: the jury is off until you configure it, you pick the panel, and every deterministic verdict is still made locally.
- The correctness of the policies you write: Sevorix enforces the rulebook it is given. A rule that does not match the thing you meant to stop will not stop it. sevorix validate checks a command against your policy set without running it, and it is the cheapest way to find that out before an agent does.
Evaluate the enforcement path yourself.
The source is open, the binary runs on your own hardware, and our engineers will walk any claim on this page with you in detail.