DSL Reference
Grammar
Section titled “Grammar”file := fragmentfragment := "fragment" STRING "{" frag_field* "}"frag_field := "schema_version" "=" INT | "selects_on" "=" selector | "offer_on" "=" offer | "requires" "=" list | os_tag? blockselector := "{" ( "commands" "=" list | "app_bundles" "=" list ),* "}"offer := "{" ( "group" "=" STRING | "files" "=" list ),* "}" // group requiredos_tag := "[" ("linux" | "macos") "]"
block := "fs" "{" fs_field* "}" | "net" "{" net_field* "}" | "env" "{" env_field* "}" | "native" "{" native_field* "}" | "args" "{" args_field* "}"
fs_field := ( "allow_read" | "allow_write" | "deny_read" | "deny_write" | "allow_read_metadata" | "allow_read_ancestors" | "allow_read_metadata_ancestors" ) list
net_field := net_list | host_block | socket_blocknet_list := ( "allow_tcp" | "allow_udp" | "deny_hosts" | "deny_tcp" | "deny_udp" | "mute_hosts" | "expose" | "allow_unix_sockets" | "deny_unix_sockets" ) listhost_block := "allow_http" ( STRING | list ) "{" host_field* "}"host_field := ( "allow_requests" | "deny_requests" ) list | graphql_blockgraphql_block := "graphql" STRING "{" graphql_field* "}"graphql_field := "allow_mutations" list | "max_request_bytes" "=" INTsocket_block := "allow_unix_sockets" list "{" "mode" "=" STRING "}"
env_field := ( "passthrough" | "set" | "set_default" | "deny" ) listnative_field := "seatbelt" listargs_field := ( "append" | "unless_given" ) list
list := "[" ( STRING ),* "]"Lists take no =. Scalars do. Every verb the DSL accepts appears above; the
tables below say what each one does.
A block may name the same list field more than once — to comment one group of entries, say — and every list is in force, as if written as one.
The fragment-level fields come first; the blocks (fs, net, env, native,
args) follow, each with the verbs the grammar allows inside it.
schema_version
Section titled “schema_version”schema_version = 1The DSL version the fragment is written for. 1 is the only version this
wardcell reads: a fragment stating any other is refused before a single rule is
applied, and one stating none is read as the current version. Adding an optional
field never needs a bump — a new version is for a change that would otherwise be
misread.
selects_on
Section titled “selects_on”selects_on = { commands = ["git", "gitk"], app_bundles = ["com.example.app"] }When the fragment applies automatically.
| Key | |
|---|---|
commands | matches the basename of the command being run, so git matches /usr/bin/git |
app_bundles | an exact match on a launched bundle’s identifier, read from its Info.plist; macOS only |
Both keys are optional and may appear together. A fragment with no selects_on
applies only when something names it — a --profile, or another fragment’s
requires. Refused in a workspace policy.
See selects_on for the rationale.
offer_on
Section titled “offer_on”offer_on = { group = "Toolchains", files = ["Cargo.toml", "*.xcodeproj"] } # offered under "Toolchains", pre-checked on these markersoffer_on = { group = "Tools" } # offered under "Tools", never pre-checkedWhether the fragment is a choice in
wardcell policy init: the
heading it sits under, and which marker files — if any — pre-check it. Always an
object.
| Key | |
|---|---|
group | required — the picker heading this fragment is shown under ("Toolchains", "Tools", "Integrations", or any name you choose) |
files | optional — basename patterns that pre-check the fragment when a checkout holds one. * is the only wildcard ("*.xcodeproj"); a pattern matches a file or directory entry. Omit it for a tool with no repository footprint |
Metadata the setup picker reads, never consulted when a command runs, and kept
strictly apart from selects_on. offer_on = true/false and an object with
no group are refused; omit offer_on to keep a fragment out of the picker.
See offer_on for how the picker uses it.
requires
Section titled “requires”requires = ["integrations-core/git", "toolchains/apple-toolchain-core"]Other fragments to pull in, each by its policy-root-relative path without the
.sbx extension. Expanded transitively — what a required fragment itself
requires comes in too — so a fragment names only its direct dependencies. A
cycle is a hard error (RequiresCycle), never silently resolved, and a name no
fragment under the policy root answers to is an error.
| Field | |
|---|---|
allow_read | read the path and everything under it |
allow_write | write the path and everything under it |
deny_read | never readable |
deny_write | never writable |
allow_read_metadata | stat only |
allow_read_ancestors | full read on each parent component, non-recursive |
allow_read_metadata_ancestors | stat on each parent component |
Paths expand the variables listed below.
Globs: * within a component, ** across components.
A trailing / on an allow_write path states that it names a directory, so
wardcell creates it before the run when it is not there — see
Filesystem rules. It is
not part of the path, and nothing else reads it.
| Field | |
|---|---|
allow_http "<target>" { } | inspected HTTP/HTTPS grant: one target, or a [ ... ] list of targets that share the block’s rules |
allow_tcp | opaque TCP; port required |
allow_udp | opaque UDP; port required |
deny_hosts | blocks a destination for every protocol |
deny_tcp, deny_udp | protocol-specific denies |
mute_hosts | keeps a host’s denials out of logs, drafts and approvals, without allowing or denying it; yields to an allow_http/deny_hosts naming the same host |
expose | inbound host-loopback ports |
allow_unix_sockets, deny_unix_sockets | Unix domain socket paths; allow_unix_sockets [...] { mode = "bind" } also permits binding (see Unix sockets) |
Host targets
Section titled “Host targets”Every net field but expose and the unix-socket ones names a destination:
target := hostpat | ipv4 | ipv4/cidr | "[" ipv6 "]" | "*"hostpat := label ( "." label )*label := literal | glob | "**"entry := target [ ":" ( digits | "*" ) ]A missing port means ports 80 and 443 for a host label, and is an error
under allow_tcp/allow_udp.
In a host pattern a glob is a label carrying *, each * matching any run of
characters within that one label (registry-*.npmjs.org); a whole-label
* fixes the depth (*.*.aws.com is exactly two subdomain labels); and **
spans one or more labels, so any-depth reach is **.example.com. The rightmost
label must be literal — a wildcard there would open a whole TLD — so “any host
at all” is only ever the bare *. See
Hosts and Ports for worked examples.
allow_http
Section titled “allow_http”An inspected grant: the connection is terminated and MITM’d so each request can be matched. Its fields are request-level and exist nowhere else — a method and a path mean nothing until it is known whose path they name.
| Field | |
|---|---|
allow_requests | METHOD /path/glob rules that permit a request |
deny_requests | rules that refuse one |
graphql "<path>" { } | body-level rules for one endpoint |
An allow_http block with no rule at all is refused: an empty block grants
nothing, and absence must never widen a policy.
Request rules
Section titled “Request rules”What allow_requests and deny_requests entries look like:
rule := method WS pathmethod := UPPERCASE-TOKEN | "*"path := "/" segment-glob*graphql
Section titled “graphql”Body-level rules for one endpoint path, inside an allow_http block.
| Field | |
|---|---|
allow_mutations | mutation field names, matched exactly |
max_request_bytes | integer; default 65536, ceiling 4194304 |
expose
Section titled “expose”Inbound host-loopback ports — the one net field that opens a listener rather
than reaching a destination.
entry := port | "127.0.0.1:" port | "[::1]:" port | "*"Loopback only.
Unix sockets
Section titled “Unix sockets”allow_unix_sockets names Unix domain socket paths (a literal, or a */**
glob) the sandbox may reach. A plain list grants connect only — the sandbox
may talk to a socket another process already set up:
net { allow_unix_sockets ["/var/run/docker.sock"] # dial the docker daemon}Adding { mode = "bind" } also grants bind — the sandbox may create and
listen on the socket, which a program serving on its own per-run socket needs.
A bound socket is connectable too, so the one grant covers both:
net { allow_unix_sockets ["/tmp/app/*.sock"] { mode = "bind" } # its own socket}mode is "bind", or "connect" for the connect-only grant a plain list
already gives; any other value is refused.
Bind is opt-in because it is the stronger right: a plain allow_unix_sockets
list never grants it, and a granted bind is scoped to the exact path or glob.
The one verb works on every backend with no hand-written native clause. See
the network overview for why binding where a trusted
service is expected is the risk this guards against.
| Field | |
|---|---|
passthrough | copy from the host environment if set |
set | "KEY=VALUE" |
set_default | "KEY=VALUE", only if not already set |
deny | remove, whatever set it |
Precedence: passthrough → set → runtime → set_default → deny.
Patterns may contain *.
Values expand the same tokens paths do, plus the proxy and plugin variables in
Variables below — chiefly the proxy CA certificate, in the several
forms different clients read. Which CA variable a given client reads is a
property of that client, so wardcell sets none itself; a fragment points the
variable its tool reads (CARGO_HTTP_CAINFO, NODE_EXTRA_CA_CERTS, …) at
$WARDCELL_PROXY_CA_CERT or the matching bundle/truststore/SPKI form. See
Environment Variables for the reasoning and
worked examples.
native
Section titled “native”| Field | |
|---|---|
seatbelt | raw SBPL, with tokens expanded, otherwise emitted verbatim and not validated |
| Field | |
|---|---|
append | switches added in front of the user’s own arguments, never replacing or reordering them |
unless_given | other spellings of the same option; the block adds nothing when the user gave any of them |
Added only, never element 0, a switch the user already gave is skipped, every addition is reported on stderr, and a workspace policy may not use either field. In front of the user’s arguments rather than after, because a fragment is selected on the program and the switch belongs to the program — placed at the end it would be parsed by whichever subcommand the user typed. See Program Arguments.
Variables
Section titled “Variables”$NAME tokens in an fs path or an env value are substituted by the backend
at render time, so the IR keeps exactly what you wrote. The two forms differ in
how a token is recognised: in an fs path a leading ~ expands to the home
directory and a token is replaced wherever its text appears; in an env
value ~ is literal and only a complete $NAME or ${NAME} is substituted,
so $HOMELESS is left alone. A name with no value in the current run — a proxy
variable when the policy grants no network, $APP_BUNDLE when the command is
not an application bundle — is left exactly as written, not blanked.
Context
Section titled “Context”| Variable | Expands to | When it has a value |
|---|---|---|
~, $HOME | the user’s home directory | always |
$WORKDIR | the directory wardcell run was invoked from | always |
$TMPDIR | the per-run temporary directory | always |
$WARDCELL_BINARY_PATH | the wardcell binary itself | always |
$WARDCELL_ZONE | this run’s zone, or zoneless where no zones are defined | always |
$WARDCELL_WORKDIR_ID | twelve hex characters standing for $WORKDIR — a per-workspace name short enough to hold a unix socket | always |
$GIT_COMMON_DIR | where the repository keeps its objects and refs: $WORKDIR/.git in an ordinary checkout, elsewhere in a linked worktree | the run starts inside a git repository |
$APP_BUNDLE | the launched application bundle’s path | macOS, launching an app bundle |
$DARWIN_USER_CACHE_DIR | this user’s own cache bucket beside $TMPDIR, where Metal shader caches and similar per-user state live | macOS |
Proxy and plugin
Section titled “Proxy and plugin”Set only when the run has an inspecting proxy — that is, when the policy grants
any network — and meant mainly for env values that point a tool at it. Which
CA variable a given client reads is a property of that client, which is why
wardcell sets none itself; see
Environment Variables.
| Variable | Expands to |
|---|---|
$WARDCELL_PROXY_URL | the proxy’s HTTP URL |
$WARDCELL_PROXY_CA_CERT | the proxy CA certificate, at the path this run’s sandboxed process can open |
$WARDCELL_PROXY_CA_BUNDLE | the same CA in the form a replacing CA mechanism reads |
$WARDCELL_PROXY_CA_TRUSTSTORE | the same CA as a Java truststore |
$WARDCELL_PROXY_CA_SPKI | base64(SHA-256(SubjectPublicKeyInfo)) — not a file, for a client that pins a key on its command line (Chromium, via args) |
$WARDCELL_PLUGIN_DIR | the run’s shared plugin directory, read-only to the sandbox, where a plugin materializes files a fragment then points a tool at |
All but $WARDCELL_PROXY_CA_SPKI substitute into an fs path as well as an env
value; SPKI is a value, not a path, so it expands only in an env value.
These are policy-path/value tokens, distinct from the process
environment variables
(WARDCELL_POLICY_ROOT, WARDCELL_SANDBOX, …) that wardcell reads from the shell.
Lexical
Section titled “Lexical”| Comments | # to end of line. Same-line comments become rule descriptions. |
| Strings | double-quoted. \" and \\ escape; other backslashes are literal, so regex-like values work. |
| Integers | bare digits |
| Booleans | true, false |
Errors
Section titled “Errors”Every error names a fragment and a line. The common ones:
unknown block kind / unknown field | a typo, caught rather than silently ignored |
invalid net entry | e.g. a missing port under allow_tcp |
allow_http block with no rules | empty blocks grant nothing and are refused |
BaseOverride | a fragment tried to loosen a base-layer deny |
SameLayerTie | two same-layer fragments disagree at equal specificity |
WorkspaceOverridesBase | a .wardcell.sbx reopened a base.sbx deny, or used native or args |
WorkspacePolicyInRoot | a .wardcell.sbx sits in the policy root; it belongs in the directory it is for |
WorkspacePolicyIsALink | a .wardcell.sbx is a symlink; it has to be the file itself |
WorkspacePolicyHasOtherNames | a .wardcell.sbx is hard-linked; a sandbox kept out by path reaches the file by its other name |
WorkspaceSelectsOn / WorkspaceOfferOn | a .wardcell.sbx carries selects_on or offer_on; a workspace policy already applies to its own directory, and is the result of such a choice rather than a source of it |
NetGrantConflict | one destination granted both inspected and opaque |