Skip to content

Policy Format ​

The reference for Sevorix policy files: the shape of a rule, the match types available, the contexts a rule can be scoped to, and the precedence rules that decide which action wins when more than one rule matches.

Where policies live ​

PathContents
~/.sevorix/policies/Policy files. Each .json file holds either a single policy object or an array of them.
~/.sevorix/roles/Role files. Each names a set of policy ids.
~/.config/sevorix/policies.jsonLegacy single-file fallback, loaded only if no directory policies are found.

Changes on disk are not picked up automatically. Apply them with:

bash
sevorix session reload

A reload re-runs the daemon's original load — including any --roles restriction it was started with, which a reload cannot widen.

A policy, field by field ​

json
{
  "id": "block-drop",
  "type": "Simple",
  "pattern": "DROP TABLE",
  "action": "Block",
  "context": "Shell",
  "kill": false
}
FieldRequiredDescription
idyesUnique identifier. This is the string a role file references.
typeyesThe match type: Simple, Regex, Executable, McpTool, or MlClassifier (pro).
actionyesBlock, Flag, Allow, or Modify.
contextnoWhere the policy applies: Shell, Network, Syscall, Mcp, Inbound, or All. Defaults to All.
killnoSyscall context only. If true, SIGKILLs the traced process instead of returning EPERM. Defaults to false.
syscallconditionalSyscall context only. The syscall name, or an array of them. Required on Regex and Executable policies in Syscall context.
transformconditionalRequired when action is Modify. Describes how to rewrite the matched content.

Type-specific fields (pattern, server/tool, model and its thresholds) sit alongside these at the top level — see each match type below.

Match types ​

Simple ​

Substring match against the content. The simplest and fastest option.

json
{ "id": "block-drop", "type": "Simple", "pattern": "DROP TABLE",
  "action": "Block", "context": "Shell" }

Regex ​

Full regular expression match. A pattern that fails to compile is a load error, reported by sevorix config check.

json
{ "id": "deny-all-aws", "type": "Regex", "pattern": "^\\s*aws\\s",
  "action": "Block", "context": "Shell" }

Remember that JSON string escaping applies on top of regex escaping — a literal backslash in the pattern is written \\ in the file.

Executable ​

Pipes the content to an external command's stdin. Exit code 0 means the policy matched; a non-zero exit means it did not.

json
{ "id": "exec_check_forbidden", "type": "Executable",
  "pattern": "grep -q forbidden", "action": "Block" }

The failure contract matters more than the happy path. A checker that cannot render a verdict — a missing binary, a spawn failure, death by signal, or no exit within a 5-second deadline — forces a Block regardless of what the policy's own action says. A checker that never ran must not be indistinguishable from one that ran and found nothing.

Two consequences worth planning around:

  • A checker that must read all of its input before deciding has to actually read it. Sevorix treats an exited child's exit code as its answer even if the write to its stdin was cut short, because whether the payload outruns the pipe buffer is chosen by the content being scanned, not by the policy.
  • Executable policies are arbitrary code execution by construction. This is why pulling them from the Hub requires an explicit opt-in (sevorix hub pull --allow-executable).

McpTool ​

Structured match against an MCP tool call by server identity and/or tool name, rather than substring-matching a serialized payload. At least one of server and tool must be set — a policy with neither is rejected at load time.

json
[
  { "id": "flag-mcp-fs-write", "type": "McpTool",
    "server": "filesystem", "tool": "write_file",
    "action": "Flag", "context": "Mcp" },

  { "id": "block-mcp-any-database-tool", "type": "McpTool",
    "server": "database",
    "action": "Block", "context": "Mcp" }
]

Omitting tool matches every tool on that server; omitting server matches that tool name on any server.

MlClassifier (pro) ​

Scores the content with a locally installed prompt-injection classifier.

json
{
  "id": "prompt-injection-check",
  "type": "MlClassifier",
  "model": "protectai-v2",
  "block_threshold": 0.9,
  "flag_threshold": 0.5,
  "action": "Block",
  "context": "Network"
}

model names a classifier installed with sevorix models pull <name>, which lands in ~/.sevorix/models/<name>/. The score is compared against both thresholds: at or above block_threshold it blocks, at or above flag_threshold it flags, otherwise it allows.

Four things about this type differ from the others:

  • action is ignored. It is accepted for schema consistency, but the effective action is always computed from the thresholds.
  • action: "Allow" is rejected at load time. Since the action comes from the thresholds, such a policy could only ever be a silently-useless exception.
  • A missing or unloadable model fails closed — the content is blocked.
  • Syscall context is not supported. Only Shell, Network, Inbound and All.

Contexts ​

context scopes a policy to one channel. A policy fires only when its context matches the request and its id appears in the session's active role.

ContextApplies to
ShellCommands passing through sevsh before execution
NetworkOutbound only — the request line and body the agent is sending through the proxy
InboundContent flowing back to the agent: HTTP response bodies from fetched pages and tool/API calls
SyscallSyscalls intercepted by seccomp, optionally with eBPF
McpMCP tools/call requests, before they reach the MCP server
All (default)Every context above

Network does not cover responses

Network describes agent egress and never fires on a response body. That split is deliberate: every Network policy written before Inbound existed described egress, and broadening them onto the return path would change what existing configurations mean — an egress policy on "DROP TABLE" would start blocking any page that merely mentions it. Untrusted third-party content also generally warrants stricter thresholds than the agent's own requests, which needs two scopes to express. Use All when you genuinely want both.

Inbound policies can only see HTTPS responses when TLS interception is enabled. Without it, a CONNECT is an opaque byte pipe and there is nothing to scan; startup logs a warning if inbound policies are loaded in that configuration.

Actions and precedence ​

Four actions are available:

ActionEffect
BlockDeterministic rejection — Red Lane. The action never reaches the system.
FlagYellow Lane. Held for human or Jury review, except on the syscall channel, where a flag is an Observatory notification only.
AllowExplicit permission. Supersedes everything below it — see next section.
ModifyRewrites the content before forwarding it. Requires a transform.

Allow supersedes everything ​

A matching Allow policy short-circuits the whole evaluation. If any Allow policy matches, the content is permitted — no Block, Flag or Modify policy that also matches is applied.

This is what makes the deny-broadly, allow-explicitly pattern work:

json
[
  { "id": "deny-all-aws",    "type": "Regex", "pattern": "^\\s*aws\\s",
    "action": "Block", "context": "Shell" },
  { "id": "allow-aws-s3-ls", "type": "Regex", "pattern": "^\\s*aws\\s+s3\\s+ls(\\s|$)",
    "action": "Allow", "context": "Shell" }
]
bash
$ sevorix validate "aws s3 ls" -r agent -C Shell
{ "verdict": "ALLOW", "lane": "GREEN", ... }

$ sevorix validate "aws iam delete-user --user-name root" -r agent -C Shell
{ "verdict": "BLOCK", "lane": "RED", ... }

Full precedence, highest first:

  1. Built-in Block (pro) — absolute. A user Allow cannot override it.
  2. Allow — supersedes everything below.
  3. Block — first match wins.
  4. Modify, then Flag — first match of each, applied only if nothing blocked.

Six consequences you can rely on:

  • Order does not matter. Allow policies are evaluated in a separate pass before everything else, so an exception works wherever it sits in the file or in the role's list.
  • Allow is scoped by context like any other policy. A Shell exception does not apply on the Network path.
  • An Allow that cannot be evaluated does not allow. If an Allow policy's Executable checker is missing, hangs, or dies, the content is blocked rather than permitted — and no other matching Allow can rescue it. Otherwise a broken checker would become attacker-selectable by choosing input that also matches a healthy exception.
  • MlClassifier policies cannot be Allow — rejected at load time.
  • A match-everything Allow (.*, .+, ^, "") disables the rest of that role's enforcement. This is legal, since a role-wide escape hatch is occasionally what you want, but it is logged loudly at startup and reported by sevorix config check.
  • Built-in policies are not overridable. Pro builds embed a small set of compile-time policies that run in their own earlier pass — currently focused on stopping an agent from rewriting its own session-binding environment variables to redirect itself to a less restrictive session, which is exactly what an agent-authored policy file would target. List them with sevorix config list-builtins.

Modify and transforms ​

A Modify policy rewrites matched content before it is forwarded or executed. The transform field takes one of two shapes:

json
{
  "id": "redact-keys", "type": "Regex", "pattern": "AKIA[0-9A-Z]{16}",
  "action": "Modify", "context": "Network",
  "transform": { "type": "Regex", "pattern": "AKIA[0-9A-Z]{16}",
                 "replacement": "[REDACTED]" }
}
TransformBehaviour
RegexReplaces every match of pattern with replacement. Redaction and argument-stripping ("replacement": "") are both special cases of this.
ExecutablePipes the content to a command's stdin; its stdout becomes the transformed content, but only on exit code 0.

A transform failure — a pattern that will not compile, a command that will not spawn, exits non-zero, or exceeds the 5-second deadline — fails closed to a Block. It never falls back to forwarding the original: a Modify policy exists because its author judged the unmodified content unsafe.

Roles: what actually activates a policy ​

A policy file on disk does nothing on its own. A policy fires only when its id is listed in the session's active role. This is the single most common reason a newly written policy appears to be ignored.

A role file in ~/.sevorix/roles/ looks like this:

json
{
  "name": "developer",
  "policies": ["deny-all-aws", "allow-aws-s3-ls", "block-drop"],
  "is_dynamic": false
}
FieldDescription
nameThe role name, as used by --role and session set-role.
policiesThe policy ids this role activates. A reference to an id that does not exist is a load error.
is_dynamicReserved for roles whose membership is computed rather than fixed. Set false for ordinary roles.

Selecting a role:

bash
# At startup (pro)
sevorix start --role developer

# Restrict which roles the daemon loads at all (any edition)
sevorix start --roles developer,readonly

# Change the active role on a running session
sevorix session set-role developer

You can also set sevsh.default_role in ~/.sevorix/settings.json to seed new sessions. If that role is not loaded, startup fails with a message naming it rather than starting with no enforcement.

No role means everything is blocked

If a session has no role configured, all traffic is blocked by default. This is the intended fail-closed behaviour, not a misconfiguration to work around — but it does mean a fresh install with no roles will appear to break every command until you assign one.

--roles restricts which roles the daemon loads at all, and that restriction survives a policy reload — POST /api/policies/reload cannot widen a daemon's role set.

Syscall policies ​

Syscall policies have requirements the other contexts do not.

The syscall field. It names the syscall (or syscalls) to intercept, as a string or an array:

  • On a Simple policy it is optional. If absent, the pattern is used as the syscall name.
  • On a Regex or Executable policy in Syscall context it is required — Sevorix cannot infer which syscall to intercept from a path regex alone. Startup fails if it is missing.
json
{
  "id": "block-etc-writes", "type": "Regex", "pattern": "^/etc/.*",
  "syscall": ["unlink", "unlinkat", "rename"],
  "action": "Block", "context": "Syscall", "kill": false
}

On a Regex policy the pattern becomes a path filter, matched against the syscall's filesystem path arguments. On a Simple policy with no path filter, every invocation of the named syscall matches.

kill: true sends SIGKILL to the traced process instead of returning EPERM. Use it for violations where letting the process observe and handle a denial is not acceptable.

Paths are matched after resolution, not as raw strings. A path filter is checked against both the literal string the process passed and its canonical form, resolved inside that process's own filesystem view: . and .. collapsed, symlinks followed, relative paths resolved against the process's working directory. Without this, /etc/./shadow, a bare shadow after chdir("/etc"), and an agent-created symlink would each evade a rule on /etc/shadow. The raw string stays a candidate too, so rules written against literal paths keep working.

The matching is deliberately asymmetric: a Block 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 fully verified simply does not apply, and the denial below it does.

Exceptions route through seccomp, not eBPF. A syscall carrying an Allow is enforced through the ordered seccomp rule set. The proactive eBPF filter denies by syscall number with no path filter and no ordering, so it cannot express a carve-out at all; such a syscall is omitted from it, with a warning.

Kernel requirements. Path filters need Linux 5.6 or newer (they rely on openat2, and a policy fails to compile on older kernels rather than being enforced in a bypassable form). Fully race-free enforcement of path-checked opens needs 5.9 or newer.

Enforcement on some syscalls is advisory

Where Sevorix can stand in for the process and perform a path-checked syscall itself — open, openat, creat, unlink, unlinkat, rmdir, rename, renameat, renameat2 — enforcement is exact, because the kernel never executes the original call. Where it cannot — notably execve, execveat and connect — the decision is reached by reading the process's memory, and the kernel re-reads those same pointers when it runs the syscall. A second thread in the target can rewrite the buffer in between. Such decisions are reported as advisory. Where emulation is possible but cannot be done faithfully (a different mount namespace, different credentials), the syscall is denied rather than continued.

Hardlinks are not covered. A hardlink is a genuine second path to one inode, so there is nothing to resolve away; catching it would need inode identity, which a path regex cannot express. The kernel's default fs.protected_hardlinks=1 prevents an unprivileged process hardlinking a file it does not own, so the exposure is limited to rules protecting files the agent itself owns.

Inbound scanning settings ​

Inbound content is far larger than an outbound request body, so it is scanned against a budget rather than a hard cap. HTML is reduced to readable text, split into overlapping segments, and scored until the segment or wall-clock budget runs out. Whatever is left is reported explicitly as an unscanned remainder rather than treated as clean.

Configure it in ~/.sevorix/settings.json:

json
{
  "inbound_scan": {
    "enabled": true,
    "oversize_action": "flag",
    "max_segments": 3,
    "budget_ms": 2500,
    "max_buffer_bytes": "8MB"
  }
}
SettingDefaultDescription
enabledtrueMaster switch for inbound scanning.
oversize_action"flag"What to do with content that could not be fully scanned: flag forwards it and records a FLAG-level event, block refuses to forward what it could not read, allow forwards it with an event logged.
max_segments3How many segments of extracted text to score before declaring a remainder (roughly 21 KB of text).
budget_ms2500Wall-clock ceiling for scanning one response.
max_buffer_bytes"8MB"Largest response body buffered for scanning. Bodies beyond it take oversize_action.

oversize_action is deliberately not fail-closed by default. Response size is chosen by the party being defended against, so unconditional fail-closed would let any site make itself unreadable to a Sevorix-protected agent by padding a page. A classifier that is genuinely unavailable still fails closed in every setting — that failure is not attacker-selectable.

Whether a body is scannable is decided by sniffing the actual bytes, not by the Content-Type header, since that header is chosen by the server being defended against. Bodies that are skipped as genuinely binary still emit an allow-level event: a skip an attacker can trigger must leave a trace, because "not scanned" must never look identical to "scanned clean".

Validating your policies ​

bash
# Parse every policy and role file, report errors and broad-Allow warnings
sevorix config check

# Ask the engine what it would do with a specific input
sevorix validate "DROP TABLE users" -r developer -C Shell

# Apply on-disk changes to a running daemon
sevorix session reload

sevorix validate prints a JSON verdict and exits 1 on BLOCK, 0 otherwise — see the CLI Reference for the output shape. Note that it answers a one-shot question about one role, so unlike daemon startup it does not fail on an unrelated role's dangling policy reference.

Further reading ​

  • Overview — the traffic lanes, enforcement tiers, and how the three interception points fit together.
  • CLI Reference — every command and flag.

Runtime containment for autonomous AI agents.