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

# Permissions

> Profiles decide what each session sees and may run; asks are questions a host answers.

A **profile** decides what one session sees and may run. Profiles live on
the workspace; a session binds one at creation, for life. The
[policy engine](/home/policy/overview) enforces it; the
[route policy](/home/route-policy) picks runtimes.

```yaml theme={null}
mode: WRITE
mounts:
  /repo:
    vfs: ram
  /vault:
    vfs: ram

profiles:
  guarded:
    commands:
      allow: [ls, cat, grep, rm]
      ask:
        - reason: removal needs sign-off
          commands: [rm]
      deny:
        - reason: credentials are never read by hand
          paths: ["/vault/*"]
  auditor:
    commands:
      allow: [ls, cat, grep]
    paths:
      hide: [/vault]
```

```bash theme={null}
mirage workspace create workspace.yaml --id demo
mirage session create demo --id agent_a --profile guarded
mirage shell -w demo -s agent_a -c "cat /repo/notes.md"
```

In code: `Workspace(mounts, profiles=...)` and
`ws.create_session("agent_a", profile="guarded")` (TypeScript:
`new Workspace(mounts, { profiles })`,
`ws.createSession('agent_a', { profile: 'guarded' })`).

* No profile means `profiles.default` if defined, else unrestricted. The
  workspace's default session follows the same rule, so `default` also
  governs bare `ws.shell(...)` and `ws.vfs`.
* `ws.session("agent_a", profile="guarded")` returns one handle for both
  doors, `handle.shell(line)` and `handle.vfs.read(path)`. It creates the
  session or adopts an existing one. `Session(ws, "agent_a")` only adopts.
* **A line sets the session; a VFS call inherits it.**
  `ws.vfs.read(path, session_id=...)` applies only outside a running
  line.

## How rules are read

* **Command axis**: rules without paths, by verb: `deny` before `ask`.
  `allow` decides whether a word is a command at all.
* **Path axis**: rules with paths, and hides, by **anchor depth** (literal
  components before the first wildcard). The deeper entry wins whatever
  its verb: `/runbook/frozen/*` beats `/runbook/*`. Ties break by verb.
* **Hiding is not refusing.** A hidden path is `ENOENT` and leaves no
  record; a denied path is listed and fails when used.

### `commands`

`allow` is the session's tool set. A word not in it is
`command not found` (exit 127). Omitted means everything, empty means
nothing, and builtins count: `allow: [cat]` leaves no `echo`.

```yaml theme={null}
deny:
  - reason: pushes go through CI
    commands: [git push]    # whole lines, by prefix
  - reason: the rollback plan is frozen
    commands:               # one command, on these paths
      rm: ["/runbook/frozen/*"]
  - reason: credentials are never read by hand
    paths: ["/vault/*"]     # any command, these paths
```

A pattern is a prefix of the line: `git push` covers
`git push origin main`, `*` matches one word, a bare `*` every command.

| Rule | The agent reads |
| - | - |
| `deny` by command | `git: Permission denied`, exit 126 |
| `deny` by path | the command's own error, `cat: /vault/k: Permission denied`, exit 1 |
| `ask` | `rm: Permission denied`, exit 126, until a host answers |

The reason is only on the result's `refusal` record. Text surfaces append
`policy denied: <reason>` or `requires approval: <reason> (ask <id>)`.

A rule also covers what a command reaches on its own: every entry of a
walk (`grep -r`, `tar`, `cp -r`, `find -delete`), files a command opens
(`awk print >`, `sed w`) and cached bytes. A `grep` or `rg` that a
backend answers with its own search (Slack, Gmail, Postgres, GitHub, ...)
walks instead when a hide or a rule covers anything it would search. Only
rules with paths and no command reach the VFS-only doors (FUSE, `ws.vfs`,
SFTP).

Not covered yet: file access inside a CLI or interpreter (`git`,
`python3`) and in `getfattr`, `setfattr`, `ln`, `readlink`.

### `paths` and `vars`

```yaml theme={null}
paths:
  hide:
    - /vault
    - patterns: ["*.env"]
      reason: dotenv files carry credentials
  show:
    /vault/policies: read
```

`hide` takes exact paths (with their subtree) or patterns. `show` reopens
a deeper subtree with a mode. A hide's `reason` is for operators only.
`vars.hide` makes variables read as unset.

### `mounts`

Narrows one mount: a lower `mode`, patterns anchored at the mount, and
`ask` / `deny` rules for lines working inside it by cwd or operand.

```yaml theme={null}
profiles:
  oncall:
    mounts:
      /repo:
        mode: read
        paths:
          hide: ["*.env"]
      /runbook:
        commands:
          deny:
            - reason: the rollback plan is frozen
              commands:
                rm: ["/runbook/frozen/*"]
```

### `cwd`, `env`, `policy`

`cwd` and `env` seed the session. `policy` is a script defining any of
`pre_command(ctx)`, `pre_vfs(ctx)` and `pre_session(ctx)` (`preCommand`,
`preVfs`, `preSession` in JavaScript). `pre_command` returns `None`,
`{"deny": reason}` or `{"ask": reason}`; the others allow or deny. A file
defining no hook fails closed. The script may read files, through the
same gate.

```yaml theme={null}
profiles:
  reviewer:
    policy:
      script: ask_curl.py
      runtime: monty
```

```python ask_curl.py theme={null}
def pre_command(ctx):
    for path in ctx["command"]["paths"]:
        if "curl" in contents(path):
            return {"ask": "approve before a command reads it"}
    return None

def contents(path):
    try:
        return open(path).read()
    except OSError:
        return ""
```

## Asks

An `ask` holds the line. The agent reads `rm: Permission denied` at exit
126 with a `pending` record:

```json theme={null}
{"kind": "pending", "reason": "removal needs sign-off", "policy": "", "scope": "command", "ask_id": "5b25c31eb62e"}
```

The question waits in `ws.decisions` until a host answers:

* `allow`, scope `once` (default): the retry runs and spends it.
* `allow`, scope `session`: every line the rule covers, from now on.
* `deny`: the retry is refused; running the line again asks again.

An `ask` with paths and no command also asks from a file tool or
`session.vfs` when no line is running. `allow once` then covers one tool
call.

### From the CLI

```bash theme={null}
mirage workspace list-asks demo                # pending asks
mirage workspace list-asks demo --session agent_a --all
mirage workspace allow demo 5b25c31eb62e --note "reviewed"
mirage workspace allow demo 5b25c31eb62e --scope session
mirage workspace deny demo 5b25c31eb62e --note "not now"
```

### Over REST

```bash theme={null}
GET  /v1/workspaces/{id}/asks                # pending; ?all=true for every decision
GET  /v1/workspaces/{id}/asks?session_id=agent_a
POST /v1/workspaces/{id}/asks/{ask_id}
     {"answer": "allow", "scope": "once", "note": "reviewed"}
```

A record has `id`, `session_id`, `agent_id`, `command`, `argv`, `cwd`,
`paths`, `reason`, `outcome` (null while pending), `scope` and `note`.
An unknown id is 404, an answered one 409, and `deny` with scope
`session` 422.

### In code

`ws.decisions` has `pending(session_id)`, `list(session_id)` and
`answer(ask_id, outcome, scope, note)`. Pass `on_ask` (`onAsk`) to answer
inline while the line waits.

How a grant is spent:

* Every command of a line is judged before it runs. A grant belongs to
  one place on the line, so a command spelled twice asks twice, and a
  loop body is one place.
* Grants are spent when the line ends, whether or not the command ran.
* Nested lines (`$( )`, `eval`, `source`, `xargs`) use the outer line's
  grants: `echo $(cat secret.txt)` asks once.
* A background job keeps its line's grants until it ends.
* A refused retry spends the refusal; the next run asks again.

A command only expansion reveals (`cat $F`, `xargs cat`, `find -exec`, a
path after a `cd` to a variable) is asked at its own gate, possibly after
earlier commands ran; a pending answer does not undo them.

## Explain

To see what a profile would do to a line or a VFS call without running
it, use [Explain](/home/policy/explain).

## Examples

* Roles over the same mounts:
  [permissions.py](https://github.com/strukto-ai/mirage/blob/main/examples/python/permissions/permissions.py),
  [permissions.ts](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/permissions/permissions.ts).
* A coded policy beside a profile `policy`, and explain:
  [policy.py](https://github.com/strukto-ai/mirage/blob/main/examples/python/permissions/policy.py),
  [policy.ts](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/permissions/policy.ts);
  from YAML:
  [workspace.yaml](https://github.com/strukto-ai/mirage/blob/main/examples/python/permissions/workspace.yaml),
  [guard.py](https://github.com/strukto-ai/mirage/blob/main/examples/python/permissions/guard.py),
  [policy\_yaml.py](https://github.com/strukto-ai/mirage/blob/main/examples/python/permissions/policy_yaml.py),
  [policy\_yaml.ts](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/permissions/policy_yaml.ts).
* Asks answered inline:
  [ask.py](https://github.com/strukto-ai/mirage/blob/main/examples/python/permissions/ask.py),
  [ask.ts](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/permissions/ask.ts);
  from the ledger:
  [ask\_pending.py](https://github.com/strukto-ai/mirage/blob/main/examples/python/permissions/ask_pending.py),
  [ask\_pending.ts](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/permissions/ask_pending.ts).

Their output is pinned in CI.


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