Policy action reference
Every Box policy rule scopes by an action from this page and conditions on the action’s
context.input fields. Each action belongs to the Box namespace, written
Box::Action::"<name>". The principal is always Box::Agent::"self" and the resource is
always Box::Resource::"unused", so rules write both bare.
Actions
Section titled “Actions”| Action | Raised by | Raised when |
|---|---|---|
fs:read | Strands Shell, Monty | A file or directory is read, listed, inspected, or entered |
fs:write | Strands Shell, Monty | A file is written or created, a directory is created, permissions change, or a symlink is made |
fs:delete | Strands Shell, Monty | A file or directory is removed |
fs:move | Strands Shell, Monty | A file or directory is renamed |
fs:other | Reserved | Never raised today |
shell:exec | Strands Shell | A command line runs a command the shell implements |
shell:spawn | Strands Shell, MCP broker | A command line starts a host binary, or Box starts a stdio MCP server |
net:connect | Egress gateway | A connection is opened to a host |
http:request | Egress gateway | An HTTP request is sent |
mcp:call | MCP broker, egress gateway | An MCP request is sent to a local or remote server |
<server>::Action::"<tool>" | MCP broker, egress gateway | A tools/call names a tool the server listed |
Filesystem
Section titled “Filesystem”Each fs:* action carries the same input:
| Field | Type | Description |
|---|---|---|
path | String | The resolved absolute path. Under your home it’s spelled ~/…. From Monty, . and .. are resolved but a symlink keeps the script’s spelling. |
operation | Enum | The specific operation, below. |
| Action | operation values (Box::<Enum>::"<value>") |
|---|---|
fs:read | FsReadOperation: read_content, read_metadata, enumerate, read_link, change_dir, exec |
fs:write | FsWriteOperation: write_content, create_dir, set_permissions, symlink |
fs:delete | FsDeleteOperation: remove_file, remove_dir |
fs:move | FsMoveOperation: rename |
fs:other | FsOtherOperation: other |
fs:read with exec is a check that a file is executable, not running it.
A rename or symlink checks several legs in order, and the first denial wins: the source
as fs:move or fs:write, the source as fs:read, an existing destination as
fs:delete, then the destination. A relative symlink target is judged on the path it
resolves to from the link’s directory.
The ::response event carries output.result, a FsResponseResult:
completed, descriptor_issued, or indeterminate.
| Field | Type | shell:exec | shell:spawn | Description |
|---|---|---|---|---|
command | String | Yes | Yes | The program and its expanded arguments, joined by spaces. |
program | String | Yes | Yes | What the first word resolved to. Write rules on this field. |
program_path | String | No | Yes | The binary’s canonical path, ~/… under your home. |
arg1 | String | Optional | Optional | The first argument. Guard with has. |
arg2 | String | Optional | Optional | The second argument. Guard with has. |
arg_count | Long | Yes | Yes | The number of arguments. |
cwd | String | Yes | Yes | The working directory. |
credential_reads | Set of String | No | Optional | Credential-store files the started tool’s grants name. |
Each command raises one decision after parsing, expansion, and resolution, including
each stage of a pipeline and commands run from command substitution, find -exec,
xargs, source, and traps. Rule on program and the arg fields rather than
command: command joins the arguments, so it can’t be split back into them. A glob
raises fs:read enumerate for every directory it reads, from its literal prefix down
to each match.
shell:exec and shell:spawn record a ::response whatever the command’s exit status.
Its output.status is the exit status. A process ended by a signal reports 128 plus the
signal number, a permitted host binary that can’t start reports 126, a command with no
reported status reports -1, and a background & command records its status when it
ends. The workload can set the status of a shell:exec command, for example with a
shell function of the same name, so a rule that depends on a host binary succeeding
reads shell:spawn and pins program_path.
Network
Section titled “Network”| Field | Type | net:connect | http:request | Description |
|---|---|---|---|---|
host | String | Yes | Yes | Lowercased, without a trailing dot. |
port | Long | Yes | Yes | |
ip | String | Optional | No | The resolved address the gateway will dial. Guard with has. |
method | String | No | Yes | Such as GET or POST. |
path | String | No | Yes | The URL path, without the query string. |
body_bytes | Long | No | Yes | On ::request, the body bytes offered; on ::response, the request bytes delivered, headers included. |
intercepted | Bool | No | Yes | Whether the gateway terminated TLS for the request. |
net:connect is decided when the host resolves, without ip, and again for each
address, with ip. Permit on host and port: a permit that needs ip never passes
the first decision. Refuse addresses with a forbid that matches ip as a string, such
as context.input has ip && context.input.ip like "169.254.*". An IPv6 address that
carries an IPv4 address, such as ::ffff:169.254.169.254, reads as that IPv4 address,
and any other IPv6 address reads in its compressed lowercase form. Cedar’s ipaddr
functions don’t apply, and a policy that uses them fails to load.
The http:request ::response event carries output.status, the HTTP status code,
when a reply arrived.
| Field | Type | Description |
|---|---|---|
server | String | The [mcp.<name>] name. |
method | String | The JSON-RPC method, such as tools/call or tools/list. For the agent’s reply to a local server’s own request, the method of that request, such as roots/list. |
tool | String, optional | Present for tools/call. |
prompt | String, optional | Present for prompts/get. |
uri | String, optional | Present for resources/read. Box normalizes a file, http, or https URI before the decision, and forwards the normalized form. |
initialize, ping, server/discover, subscriptions/listen, and notifications pass
a local server’s broker undecided. A remote server’s initialize is decided. The
agent’s reply to a local server’s own request, such as roots/list, is decided as
mcp:call; a reply to ping passes undecided.
A per-tool action is named <server>::Action::"<tool>". The namespace is the server
name with every character other than a letter, digit, or _ replaced by _, a leading
digit prefixed with _, and a reserved word suffixed with _: builder-mcp becomes
builder_mcp.
Its context.input holds the tool’s arguments, typed from the server’s input schema.
Run box policy generate-schema to see them.
A per-tool action refines mcp:call. When no per-tool rule matches, the mcp:call
decision stands; only a matching forbid denies. Box learns a server’s tools from the replies to
the agent’s own tools/list and sends no list of its own. It holds the reply to the
last page until the tools’ rules enforce, and a later list that changes the tools
replaces them. Until the server’s tools are known, a typed call is denied as
policy_pending, and the agent can retry it. A server whose client never lists it, or
stops paging, stays unknown, and its typed calls are refused. Box refuses a
tools/call for a tool that isn’t in the server’s accepted tool catalog, including
every call before a catalog is accepted, with the rule enforcement:mcp-catalog.
Write policy for an MCP server
in the Box repository walks through narrowing a server.
Events
Section titled “Events”Temporal patterns name an action and one of three events, such as
Box::Action::"http:request"::response{ … }. Each event carries the action’s input
fields as input.<field>.
| Event | Recorded when | Extra fields |
|---|---|---|
::request | The action is asked for, before the decision. Includes denied attempts. | |
::response | The action ran. | output.<field>, where the action has outputs |
::error | The action was allowed and then failed. |
In a pattern, _ matches any value. The
Dogwood guide covers binding and
correlating values.
Decisions
Section titled “Decisions”| Cause | Rule shown | Meaning |
|---|---|---|
permitted | The permit’s @id | A permit matched and no forbid did. |
forbidden | The forbid’s @id | A forbid matched. |
no_match | <default-deny> | No permit matched. |
policy_pending | <policy-pending> | An MCP server’s tools aren’t known yet. |
internal_fault | <default-deny> | The request couldn’t be evaluated. |
enforcement | enforcement:<gate> | Box decided the action itself, outside the policy: for example reach-floor (the reachable paths check), mcp-catalog (a tool outside the server’s accepted catalog), native-egress (a tool or server that skips the gateway), or Box’s own startup traffic. |
A rule without an @id is named policy_<index> in decisions, counting from 0.
Load-time checks
Section titled “Load-time checks”Box refuses to load a policy that does any of the following. In a box with a stdio MCP server, Box checks only the syntax before the box starts, and the rest once it knows the servers’ tools.
- Fails to parse, or uses an unknown action, field, or type.
- Reads an optional field such as
arg1without ahasguard. - Gives one
@idto more than one rule. - Scopes a rule to
fs:otheralone. - In an
fs:*orshell:spawnrule, compares apathorprogram_pathto a home path spelled absolutely, or a directory spelled with a trailing slash. - Calls an information provider.
- Writes a cap as a
permit: apermitwith awhen temporalclause, beside apermitthat admits the same action with no condition. - Uses a temporal window longer than 24 hours.
Box warns, and still loads the policy, when a program literal contains /, when a
like pattern names your home after a *, or when a cap written as a permit sits
beside a conditioned permit for the same action.
What Box always refuses
Section titled “What Box always refuses”After policy permits a file operation through the box’s shell or Python, the reachable paths check refuses these, whatever the policy says:
- Box’s own state, through the box’s shell and Python. Every path in this box’s
box_dirand Box’s machine state, and any operation on thebox.tomland policy file this run loaded. - Paths outside the interpreters’ reach. Python file operations outside your home,
the agent’s
HOME, and the workspace, and any path whose spelling resolves to a different path, such as one through a symlink. A refusal is recorded with the ruleenforcement:reach-floor. The shell binds only those roots: a path outside them lives in the shell’s memory for one command and never touches the surrounding OS.
Refuse cloud metadata endpoints
Section titled “Refuse cloud metadata endpoints”Box doesn’t refuse cloud metadata endpoints on its own: the gateway keeps no list of refused destinations. Add these rules to every policy:
@id("metadata_hosts")forbid (principal, action == Box::Action::"net:connect", resource)when { context.input.host == "metadata.google.internal" || context.input.host == "metadata.azure.internal"};
@id("metadata_addresses")forbid (principal, action == Box::Action::"net:connect", resource)when { context.input has ip && ( context.input.ip like "169.254.*" || context.input.ip == "fd00:ec2::254" || context.input.ip like "fe8*" || context.input.ip like "fe9*" || context.input.ip like "fea*" || context.input.ip like "feb*")};