Skip to content

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.

ActionRaised byRaised when
fs:readStrands Shell, MontyA file or directory is read, listed, inspected, or entered
fs:writeStrands Shell, MontyA file is written or created, a directory is created, permissions change, or a symlink is made
fs:deleteStrands Shell, MontyA file or directory is removed
fs:moveStrands Shell, MontyA file or directory is renamed
fs:otherReservedNever raised today
shell:execStrands ShellA command line runs a command the shell implements
shell:spawnStrands Shell, MCP brokerA command line starts a host binary, or Box starts a stdio MCP server
net:connectEgress gatewayA connection is opened to a host
http:requestEgress gatewayAn HTTP request is sent
mcp:callMCP broker, egress gatewayAn MCP request is sent to a local or remote server
<server>::Action::"<tool>"MCP broker, egress gatewayA tools/call names a tool the server listed

Each fs:* action carries the same input:

FieldTypeDescription
pathStringThe resolved absolute path. Under your home it’s spelled ~/…. From Monty, . and .. are resolved but a symlink keeps the script’s spelling.
operationEnumThe specific operation, below.
Actionoperation values (Box::<Enum>::"<value>")
fs:readFsReadOperation: read_content, read_metadata, enumerate, read_link, change_dir, exec
fs:writeFsWriteOperation: write_content, create_dir, set_permissions, symlink
fs:deleteFsDeleteOperation: remove_file, remove_dir
fs:moveFsMoveOperation: rename
fs:otherFsOtherOperation: 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.

FieldTypeshell:execshell:spawnDescription
commandStringYesYesThe program and its expanded arguments, joined by spaces.
programStringYesYesWhat the first word resolved to. Write rules on this field.
program_pathStringNoYesThe binary’s canonical path, ~/… under your home.
arg1StringOptionalOptionalThe first argument. Guard with has.
arg2StringOptionalOptionalThe second argument. Guard with has.
arg_countLongYesYesThe number of arguments.
cwdStringYesYesThe working directory.
credential_readsSet of StringNoOptionalCredential-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.

FieldTypenet:connecthttp:requestDescription
hostStringYesYesLowercased, without a trailing dot.
portLongYesYes
ipStringOptionalNoThe resolved address the gateway will dial. Guard with has.
methodStringNoYesSuch as GET or POST.
pathStringNoYesThe URL path, without the query string.
body_bytesLongNoYesOn ::request, the body bytes offered; on ::response, the request bytes delivered, headers included.
interceptedBoolNoYesWhether 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.

FieldTypeDescription
serverStringThe [mcp.<name>] name.
methodStringThe 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.
toolString, optionalPresent for tools/call.
promptString, optionalPresent for prompts/get.
uriString, optionalPresent 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.

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>.

EventRecorded whenExtra fields
::requestThe action is asked for, before the decision. Includes denied attempts.
::responseThe action ran.output.<field>, where the action has outputs
::errorThe action was allowed and then failed.

In a pattern, _ matches any value. The Dogwood guide covers binding and correlating values.

CauseRule shownMeaning
permittedThe permit’s @idA permit matched and no forbid did.
forbiddenThe forbid’s @idA 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.
enforcementenforcement:<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.

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 arg1 without a has guard.
  • Gives one @id to more than one rule.
  • Scopes a rule to fs:other alone.
  • In an fs:* or shell:spawn rule, compares a path or program_path to a home path spelled absolutely, or a directory spelled with a trailing slash.
  • Calls an information provider.
  • Writes a cap as a permit: a permit with a when temporal clause, beside a permit that 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.

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_dir and Box’s machine state, and any operation on the box.toml and 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 rule enforcement: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.

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*")
};