# `SuperPi.Plugins.Permissions`
[🔗](https://github.com/oleksandr/super-pi-beam/blob/v0.1.0/lib/super_pi/plugins/permissions.ex#L1)

Permissions, Execution Modes, & Workspace Confinement Plugin (`SuperPi.Extension` behaviour).

Enforces execution modes and workspace boundaries:
- **`dangerouslySkipPermissions: true`**: Auto-approves all tool executions without prompting.
- **`executionMode: "plan"`**: Allows read-only inspection tools (`read`, `grep`, `find`, `ls`, `tool_search`),
  tools whose only effect is `read`, and network reads (see Tool effects). Blocks everything else
  (`write`, `edit`, `bash`, state changes).
- **`executionMode: "accept-edits"`**: Allows `read`, `write`, `edit`, `grep`, `find`, `ls` inside trusted workspaces
  and checks `permissions.allow` (`command(...)` entries in `settings.json`) for `bash` commands.
- **`allowNonWorkspaceAccess`**: Defaults to `false`. File tools may only touch `workspacePaths` and
  `trustedWorkspaces`. Set it to `true` to lift the confinement.

## Tool effects

A tool declares what a call does (`SuperPi.Tools.effects/2`): `read`, `fs_write`, `exec`,
`net_read`, `net_write`, `state_write`. The decision is a pure function of effects, mode and policy
(`decide/4`):

| Effects | `plan` | `accept-edits` | `auto` |
|---|---|---|---|
| only `read` | allow | allow | allow |
| `net_read`, with `read` | allow unless `network_in_plan_mode: false` | allow | allow |
| `state_write`, with `read` and `net_read` | block | allow | allow |
| `fs_write`, `exec`, `net_write`, or no declared effects | block | block (the `write` and `edit` tools and allowed shell commands excepted) | allow |

Network policy keys, orthogonal to the mode: `network: "allow" | "allowlist" | "deny"` (default
`"allow"`) and `network_in_plan_mode` (default `true`). `deny` refuses every tool with a network
effect. `allowlist` allows a call whose `url` argument has a host in `networkAllowHosts` (an entry
matches the host and its subdomains) and refuses a call without one.

A harness can set `maxExecutionMode` in `pi.agent`. A mode above it is lowered to it (`clamp_mode/1`).

## Policy

Policy is data. It merges, lowest priority first, from:

1. `config :super_pi, :permissions, ...` (keys such as `allowNonWorkspaceAccess`, `permissions`)
2. `<app data dir>/settings.json`
3. the session's `settingsOverride`

`permissions.deny` and `permissions.allow` hold `command(...)` patterns. A `deny` match blocks
a shell command in every mode. `allow` opens commands in `accept-edits` mode.

## Modes for remote callers

A Gateway, RPC or MCP caller can request a mode. `remote_mode/1` limits the request:

    config :super_pi, :permissions,
      remote_default_mode: "accept-edits",
      remote_max_mode: "accept-edits"

A request may select any mode up to `remote_max_mode` (`plan` < `accept-edits` < `auto`).
The ceiling defaults to the default mode, so a request can only tighten the mode unless
config raises the ceiling. `sanitize_remote_agent/1` also removes keys that would let a
remote caller lift the policy (`dangerouslySkipPermissions`, `settingsOverride`, paths).

# `apply_remote_mode`

```elixir
@spec apply_remote_mode(map(), term()) :: map()
```

Sets `executionMode` in `agent_cfg` for a remote caller.

With `requested` of `nil`, the harness default is limited to the ceiling. A harness default
that is not a permission mode (for example `"interactive"`) is left as it is.

# `clamp_mode`

```elixir
@spec clamp_mode(map()) :: map()
```

Lowers `agent_cfg["executionMode"]` to the harness ceiling `agent_cfg["maxExecutionMode"]`.
A config without a ceiling, or with a mode at or below it, is returned as it is.

# `command_allowed?`

Checks whether a shell command matches any `command(...)` pattern in `settings.json`'s `permissions.allow` list.
Matches exact commands (`command(git status)`) or command prefixes (`command(head)` matching `head -n 20 file.txt`).

# `decide`

```elixir
@spec decide(String.t(), map(), map(), map()) :: :allow | {:block, String.t()}
```

The decision for one call, as a pure function of the tool, its arguments, the agent config and
the effective settings. `before_tool/3` loads the last two and decides through `evaluate/4`.

The decision depends on the tool's effects (`SuperPi.Tools.effects/2`), the mode and the policy,
and it never loosens as the mode rises (`plan` < `accept-edits` < `auto`).

# `description`

# `evaluate`

```elixir
@spec evaluate(String.t(), map(), map(), map()) ::
  :allow | {:block, String.t()} | {:prompt, String.t(), String.t()}
```

Like `decide/4`, with a third answer: `{:prompt, reason, risk}` for a block that a human may lift.
Exactly two blocks are promptable: a shell command that is not in `permissions.allow`, and a tool
that `accept-edits` does not allow by default (every MCP tool among them). A `permissions.deny`
match, a mode block, a workspace boundary and a network policy block are never promptable.

# `id`

# `load_effective_settings`

# `name`

# `network_policy`

```elixir
@spec network_policy(map()) :: String.t()
```

The effective `network` policy: `"allow"` (default), `"allowlist"` or `"deny"`.

# `rank_of`

```elixir
@spec rank_of(term()) :: 0..2
```

Rank of a mode: `plan` 0, `accept-edits` 1, `auto` 2. An unknown mode ranks as `plan`, the strictest.

# `remote_mode`

```elixir
@spec remote_mode(term()) :: String.t()
```

Returns the mode that a remote caller gets for a request.

`requested` is the mode that the caller asked for, or `nil`. An unknown or missing value gives
`remote_default_mode`. A value above `remote_max_mode` is lowered to it.

# `sanitize_remote_agent`

```elixir
@spec sanitize_remote_agent(map()) :: map()
```

Removes the keys from a client-supplied agent map that would lift the permission policy,
and limits `executionMode` with `remote_mode/1`.

# `within?`

```elixir
@spec within?(String.t(), String.t()) :: boolean()
```

True when `path` is `root` or inside it, after following symlinks component by component.
Both must be absolute. A link inside `root` that points outside it is not inside.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
