> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Policy Engine

> One ordered stack of Policy hooks judges every line, command, VFS call and session write.

Every line, command, VFS call and session write passes one ordered stack
of `Policy` objects. [Profiles](/home/permissions) compile into it, the
[route policy](/home/route-policy) places lines through it, and a host
adds its own.

A policy overrides only the hooks it needs and returns an answer, or
`None` for no opinion. A hook that raises refuses the action, naming the
policy.

```python theme={null}
from mirage import Workspace
from mirage.policy import CommandContext, Deny, Policy

class NoForcePush(Policy):
    async def pre_command(self, ctx: CommandContext) -> Deny | None:
        if ctx.program[:2] == ("git", "push") and "--force" in ctx.argv:
            return Deny("force pushes go through an operator")
        return None

ws = Workspace(mounts, policies=[NoForcePush()])
```

```typescript theme={null}
const noForcePush: Policy = {
  async preCommand(ctx) {
    if (ctx.program?.[0] === 'git' && ctx.program?.[1] === 'push' &&
        ctx.argv.includes('--force'))
      return { kind: 'deny', reason: 'force pushes go through an operator' }
    return null
  },
}
const ws = new Workspace(mounts, { policies: [noForcePush] })
```

Add more later with `ws.policies.add(...)`. A hook a policy does not
override is never called.

## Hooks

| Hook | Fires | May answer |
| - | - | - |
| `pre_command` | per command, before it runs | Deny, Ask |
| `pre_execute` | per line, after admission | Deny, Route |
| `pre_ops` | per VFS call, before any backend or cache read | Deny, Ask |
| `pre_session` | per session write (`export`, `X=1`, `printf -v`, ...) | Deny |
| `post_ops` | after a VFS call | Deny, Limit |
| `post_execute` | after a line | Limit |

Any other answer raises `PolicyError`.

* `pre_command` reads a `CommandContext`: the command, `argv`, `paths`,
  `operands`, `program` (a CLI's verb path), the cwd and the session.
* `pre_execute` reads the [route policy's](/home/route-policy) context.
  Nested lines
  (`$( )`, `eval`, `source`, `xargs`) keep the outer line's placement.
* `pre_ops` runs thousands of times under one `grep -r`: keep it cheap.
  Its Deny is `EACCES`. Its Ask reaches the host only when no line is
  running (a file tool, `session.vfs`); inside a line it refuses.

## Answers

| Answer | Effect |
| - | - |
| `None` | no opinion |
| `Deny(reason)` | `<cmd>: Permission denied`, exit 126; `EACCES` at the VFS and session doors |
| `Deny(reason, DenyScope.OPERAND)` | the command's own error for that operand (`cat: /vault/k: Permission denied`, exit 1) |
| `Ask(reason)` | refused until a host answers; see [Asks](/home/permissions#asks) |
| `Route(runtime)` | that runtime serves the commands it captures |
| `Limit(...)` | bounds a result (`max_bytes`, `max_lines`, `timeout_seconds`); limits merge to the tightest |

stderr never carries the reason. It rides the result's `refusal` record
(`kind`, `reason`, `policy`, `scope`, `ask_id`); text surfaces append
`policy denied: <reason>`.

Answers rank by kind: Hide, then Deny, then Ask, then Route and Limit.
A Deny beats any Ask, and two Routes to different runtimes refuse the
line. Within a kind, the first registered speaks. A policy can only
tighten.

A **Hide** is the built-in's alone: the path is `ENOENT`, leaves no
record, and no surface names it.

## Order

The stack: `MountRootPolicy`, `OutputCapPolicy`, the profile's
`PermissionsPolicy` and `ScriptPolicy`, `policies=` in order, then
`ws.policies.add()`. The route policy answers `pre_execute` first; the
hide answers `pre_ops` first and the mount mode last.

A line runs in four steps:

1. **Syntax**: an unparsable line exits 2.
2. **Admission**: every command is judged before any runs. A refused or
   asked line never runs halfway and is never placed.
3. **Placement**: `pre_execute`.
4. **Execution**: each command clears its own gate with its expanded
   words, each VFS call `pre_ops`, each session write `pre_session`.

A raising policy denies; only the route policy's errors raise to the
caller.

## What a profile compiles to

* **Setup**: `cwd`, `env`.
* **Visibility**: `paths`, `vars`, `processes.list`, `commands.allow`.
* **Policies**: mount modes, `commands` rules, `policy:`,
  `command_limits`, `processes.max`.

A rule naming paths and no command is **POSIX level** and holds at every
door. Any other rule is **command level** and holds where a line is
judged.

## Which door enforces what

| Door | Command level | POSIX level |
| - | - | - |
| `ws.shell`, `session.shell`, the `shell` / `ls` / `grep` tools, SSH, codex-exec commands | yes | yes |
| the `read` / `write` / `edit` / `glob` tools, `ws.vfs`, `session.vfs`, FUSE, FSKit, SFTP, a runtime guest's file calls | no | yes |

A VFS-only door logs the command-level rules it skips. The agent's tools
follow the profile: `session.tools.names()` lists only the tools the
session can use, and MCP, RPC and the agent adapters offer exactly that.

## Scripts

A policy script (`policy:`, `route_policy`, a runtime's `script:`) runs
on the workspace's evaluator runtime, reads `ctx` and returns data; after
10 seconds the line is refused. A script CLI (`clis:`) runs as the
session, judged like any line.

## Explain

`session.explain` shows what a line or a VFS call would do, and every
policy's answer, without doing it. See [Explain](/home/permissions#explain).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.