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

# Reusable Approval Scopes

> Approve a command once and cover every trailing-arg variant with an arity-derived prefix glob

Reusable scopes let one persistent approval cover every trailing-arg variant of the same command — approve `git status` and future `git status -s` runs never re-prompt.

<Warning>
  **Planned feature — not yet in the current SDK release.**
  `reusable_scope`, `suggest_scope_pattern`, and the `derived` field on `PersistentApproval` are being added in [PraisonAI PR #2576](https://github.com/MervinPraison/PraisonAI/pull/2576). Until that PR is merged and synced here, calling `manager.approve(..., reusable_scope=True)` will raise `TypeError`. Use explicit glob patterns with the current API instead:

  ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.permissions import PermissionManager, PersistentApproval
  from praisonaiagents.permissions.rules import PermissionAction

  manager = PermissionManager()
  manager.approve("bash:git status *", True, scope="always")
  ```
</Warning>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Reusable Scope Derivation"
        A[🤖 Agent runs<br/>bash:git status -s] --> B{Approve?}
        B -->|literal| L[💾 stores<br/>bash:git status -s]
        B -->|reusable_scope=True| D[⚙️ derive_pattern]
        D --> P[💾 stores<br/>bash:git status *]
        P --> M{Later:<br/>bash:git status .}
        M -->|matches| OK[✅ Auto-allow]
    end

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef choice fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef store fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef derive fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class A agent
    class B,M choice
    class L,P store
    class D derive
    class OK ok
```

## Quick Start

<Steps>
  <Step title="Enable reusable scopes on your Agent">
    Add one line to opt in — approval fatigue drops immediately:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    agent = Agent(
        name="Shell Assistant",
        instructions="Run git commands the user asks for",
        tools=["shell"],
        approval={"reusable_scope": True},
    )

    agent.start("Show me the status and then the last five commits")
    ```

    The first time the agent runs `git status -s`, the user sees the prompt once. Every subsequent `git status <flags>` variant is auto-approved for the session.
  </Step>

  <Step title="Preview and store scopes programmatically">
    For advanced workflows — CLIs, approval UIs, or testing — call the `PermissionManager` API directly:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.permissions import PermissionManager

    mgr = PermissionManager()

    suggested = mgr.suggest_scope_pattern("bash:git status -s")
    print(suggested)
    # => "bash:git status *"

    mgr.approve(
        target="bash:git status -s",
        approved=True,
        scope="always",
        reusable_scope=True,
    )
    ```

    `suggest_scope_pattern()` lets you surface the derived pattern in your UI before committing it, so users can review what `always` will cover.
  </Step>

  <Step title="Inspect the suggested pattern before saving">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.permissions import PermissionManager

    manager = PermissionManager()

    suggested = manager.suggest_scope_pattern("bash:git status -s")
    print(suggested)  # 'bash:git status *'

    approval = manager.approve(
        target="bash:git status -s",
        approved=True,
        scope="always",
        pattern=suggested,
    )
    ```

    Pass `pattern=` to override the derived pattern with any string you prefer.
  </Step>

  <Step title="Persistence round-trip">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import tempfile
    from praisonaiagents.permissions import PermissionManager

    with tempfile.TemporaryDirectory() as d:
        mgr1 = PermissionManager(storage_dir=d)
        mgr1.approve(
            "bash:git log --oneline",
            approved=True,
            scope="always",
            reusable_scope=True,
        )

        # Reload from disk
        mgr2 = PermissionManager(storage_dir=d)
        print(mgr2.check("bash:git log --oneline -5").is_allowed)  # True
    ```
  </Step>
</Steps>

***

## How It Works

When `reusable_scope=True` and `scope` is `"session"` or `"always"`, `PermissionManager.approve()` calls `suggest_scope_pattern()` internally. That delegates to `arity.derive_pattern()`, which looks up the command in a small arity table and builds a glob.

| Situation                                          | Behaviour                                                                    |
| -------------------------------------------------- | ---------------------------------------------------------------------------- |
| Bare command (`bash:git`)                          | Kept literal — approving `git` alone never auto-approves `git push`.         |
| Compound command (`bash:cd /tmp && rm x`)          | Kept literal — second op never swallowed into the reusable scope.            |
| Pipe / redirect / substitution (\|, `>`, `$(...)`) | Kept literal.                                                                |
| Non-shell target (`read:`, `write:`, `edit:`, …)   | Unchanged — only `bash:`/`shell:` are generalised.                           |
| Already-globbed target (`bash:git *`)              | Unchanged.                                                                   |
| `scope="once"`                                     | Always literal, even with `reusable_scope=True`.                             |
| User-authored `bash:rm *` glob                     | Keeps exact `fnmatch` semantics — a bare `bash:rm` still does **not** match. |

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Mgr as PermissionManager
    participant Arity as arity.derive_pattern

    User->>Mgr: approve("bash:git status -s", reusable_scope=True, scope="always")
    Mgr->>Arity: derive_pattern("bash:git status -s")
    Arity-->>Mgr: "bash:git status *"
    Mgr->>Mgr: store PersistentApproval(pattern="bash:git status *", derived=True)
    Mgr-->>User: PersistentApproval
    Note over User,Mgr: Future check("bash:git status") → auto-allow
```

The stored `PersistentApproval` has `derived=True`, which enables a bare-prefix fallback in `matches()`: `bash:git status *` (derived) also matches the bare `bash:git status`.

| Situation                                            | Stored pattern               | Matches                                                              |
| ---------------------------------------------------- | ---------------------------- | -------------------------------------------------------------------- |
| `reusable_scope=False` (default)                     | Literal `bash:git status -s` | Only that exact command                                              |
| `reusable_scope=True`, known command                 | Derived `bash:git status *`  | Any `git status …` — including bare `git status`                     |
| `reusable_scope=True`, bare command (`bash:git`)     | Literal `bash:git`           | Only bare `git` — never all git subcommands                          |
| `reusable_scope=True`, compound (`cd /tmp && rm x`)  | Literal (unchanged)          | Only that compound command                                           |
| `reusable_scope=True`, non-shell (`read:/etc/hosts`) | Literal (unchanged)          | Non-shell prefixes are never generalised                             |
| `pattern="bash:git *"` explicit override             | User's exact string          | `derived=False` — exact `fnmatch` semantics, no bare-prefix fallback |

***

## The Arity Table

`arity.derive_pattern` uses this built-in table to decide how many leading tokens to keep as the reusable prefix.

| Category         | Commands                                                            | Arity            | Example                                                        |
| ---------------- | ------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------- |
| Version control  | `git`, `gh`, `hg`, `svn`                                            | 2                | `git status -s` → `bash:git status *`                          |
| Package managers | `npm`, `yarn`, `pnpm`, `pip`, `cargo`, `go`, `poetry`, `uv`, `make` | 2                | `npm run build` → `bash:npm run *`                             |
| Containers       | `docker`                                                            | 2                | `docker pull ubuntu` → `bash:docker pull *`                    |
| Containers       | `docker compose`                                                    | 2                | `docker compose up -d` → `bash:docker compose *`               |
| Containers       | `kubectl`, `helm`                                                   | 2                | `kubectl get pods` → `bash:kubectl get *`                      |
| Python tooling   | `python`, `python3`                                                 | 2                | `python -m pytest` → `bash:python -m *`                        |
| Python tooling   | `ruff`                                                              | 2                | `ruff check .` → `bash:ruff check *`                           |
| Python tooling   | `pytest`                                                            | 1                | `pytest tests/` → `bash:pytest *`                              |
| System           | `apt`, `apt-get`, `brew`, `systemctl`                               | 2                | `apt install pkg` → `bash:apt install *`                       |
| Unknown / other  | —                                                                   | 1 (conservative) | `foo bar baz` → `bash:foo *` (only if there are trailing args) |

<Note>
  Multi-word keys win over single-word keys. `docker compose` (a 2-word key, arity 2) takes precedence over `docker` (arity 2), so `docker compose up -d` derives `bash:docker compose *`. Commands not in the table default to arity 1 — only the first token is kept.
</Note>

### Behaviour table

**Escape hatches — these always stay literal:**

| Condition                                  | Example target             | Stored as           |
| ------------------------------------------ | -------------------------- | ------------------- |
| Non-shell target                           | `read:/etc/hosts`          | `read:/etc/hosts`   |
| Already globbed                            | `bash:git status *`        | `bash:git status *` |
| Contains `&&`, `\|\|`, `\|`, `;`, `&`      | `bash:cd /tmp && rm -rf x` | exact string        |
| Contains `$(`, backtick, `>`, `<`, newline | `bash:echo $(whoami)`      | exact string        |
| Bare single token equals the full command  | `bash:git`                 | `bash:git`          |

The bare-single-token rule is the most important: approving `bash:git` stays literal — it never automatically covers `bash:git push --force`.

### Extend the arity table for your own tools

Pass a custom `arity_map` to `derive_pattern()` to add commands not in the built-in table:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.permissions import derive_pattern

my_tools = {"mytool": 2, "mytool sub": 3}

pattern = derive_pattern("bash:mytool sub action --flag", arity_map=my_tools)
print(pattern)
# => "bash:mytool sub action *"
```

***

## What Never Gets Generalised

`derive_pattern` is conservative. These cases always return the literal target unchanged:

* **Non-shell targets** — anything that doesn't start with `bash:` or `shell:`.
* **Empty commands** — `bash:` with no command string.
* **Existing globs** — targets already containing `*` or `?`.
* **Shell control operators** — targets containing `&&`, `||`, `|`, `;`, `&`, `$(`, `` ` ``, `>`, `<`, or a newline.
* **Bare single-token commands** — `bash:git`, `bash:ls` — never become `bash:git *`.

This prevents a compound-command approval from silently expanding its scope to cover a second unrelated command.

***

## Common Patterns

**Approve once, cover all trailing-arg variants**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.permissions import PermissionManager

manager = PermissionManager()

manager.approve("bash:git status -s", True, scope="always", reusable_scope=True)

manager.check("bash:git status").is_allowed          # True
manager.check("bash:git status --short").is_allowed  # True
manager.check("bash:git status").is_allowed          # True (bare form)
manager.check("bash:git push").is_allowed            # False — new prompt
```

**Bare command stays literal**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
manager.approve("bash:git", True, scope="always", reusable_scope=True)

approval = manager.check("bash:git")
print(approval.pattern)  # 'bash:git'  — NOT 'bash:git *'
```

**Compound commands stay literal**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
manager.approve("bash:cd /tmp && rm x", True, scope="always", reusable_scope=True)

approval = manager.check("bash:cd /tmp && rm x")
print(approval.pattern)  # 'bash:cd /tmp && rm x'  — literal, not generalised
```

### Session vs Always scope

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.permissions import PermissionManager

mgr = PermissionManager()

mgr.approve(
    target="bash:npm run build",
    approved=True,
    scope="session",
    reusable_scope=True,
)

mgr.approve(
    target="bash:git status -s",
    approved=True,
    scope="always",
    reusable_scope=True,
)
```

Use `session` for exploratory work and `always` only for commands you trust unconditionally across all future sessions.

### Preview before storing

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.permissions import PermissionManager

mgr = PermissionManager()

targets = [
    "bash:git log --oneline",
    "bash:npm run test",
    "bash:docker ps",
]

for t in targets:
    print(f"{t!r}  →  {mgr.suggest_scope_pattern(t)!r}")
```

```
'bash:git log --oneline'  →  'bash:git log *'
'bash:npm run test'       →  'bash:npm run *'
'bash:docker ps'          →  'bash:docker *'
```

**Preview and override before saving**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.permissions import PermissionManager

manager = PermissionManager()

target = "bash:npm run build"
preview = manager.suggest_scope_pattern(target)
# preview = "bash:npm run *"

# User narrows the scope — override with explicit pattern
manager.approve(
    target=target,
    approved=True,
    scope="session",
    pattern="bash:npm run build",  # explicit override: stays literal
)
```

**Custom arity map for project-specific commands**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.permissions.arity import derive_pattern

# Teach the engine about your own CLI tool
custom_map = {"mycli": 2}
pattern = derive_pattern("bash:mycli deploy prod", arity_map=custom_map)
print(pattern)  # "bash:mycli deploy *"
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use reusable_scope=True for routine shell commands">
    Routine VCS and build commands (`git status`, `npm run`, `pytest`) are the best candidates. They have stable subcommand prefixes and many trailing-arg variants.
  </Accordion>

  <Accordion title="Preview the scope before storing">
    Call `suggest_scope_pattern()` in your CLI or approval UI so users can see exactly what `always` will cover before confirming. A pattern like `bash:rm *` deserves a careful second look:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.permissions import PermissionManager

    mgr = PermissionManager()
    pattern = mgr.suggest_scope_pattern("bash:rm -rf /tmp/build")
    print(f"This will auto-approve: {pattern}")
    ```
  </Accordion>

  <Accordion title="Prefer session over always for reusable scopes">
    A bad derived pattern that uses `always` persists across all future sessions. Use `session` by default for reusable scopes — if the pattern turns out to be safe, upgrade it to `always` deliberately:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    mgr.approve(target="bash:git status -s", approved=True, scope="session", reusable_scope=True)
    ```
  </Accordion>

  <Accordion title="Pass an explicit pattern= to override the suggestion">
    If `suggest_scope_pattern` returns a pattern that is broader or narrower than you want, override it with `pattern=`. The stored approval will have `derived=False`, keeping exact `fnmatch` semantics.
  </Accordion>

  <Accordion title="Compound approvals are intentionally narrow">
    A `cd /tmp && rm x` approval covers only that exact string. If you want to reuse two operations independently, create two separate approvals — one for `cd` variants and one for `rm` variants.
  </Accordion>

  <Accordion title="Non-shell targets stay literal — that is intentional">
    Path approvals such as `read:/etc/hosts` or `write:/tmp/report.txt` must remain exact. File paths that look like prefixes (`read:/etc/`) would grant access to all files under `/etc/`, which is a security anti-pattern. The literal-only behaviour for non-shell targets is by design.
  </Accordion>

  <Accordion title="Extend ARITY for your own CLI tools">
    If your agent uses an internal tool (`mytool`) or a domain-specific CLI (`kubectl`), pass a custom `arity_map` to `derive_pattern()` so the prefix is derived correctly:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.permissions import derive_pattern

    corp_tools = {
        "deploy": 2,
        "deploy rollback": 2,
    }

    pattern = derive_pattern("bash:deploy rollback service-a --dry-run", arity_map=corp_tools)
    # => "bash:deploy rollback *"
    ```
  </Accordion>

  <Accordion title="Not a replacement for deny rules">
    A `deny` rule on `bash:rm *` still wins. Reusable scopes only extend `allow` approvals — deny is enforced at the rule level. See [Command-Aware Permissions](/docs/features/command-aware-permissions).
  </Accordion>
</AccordionGroup>

***

## API Reference

### `PermissionManager.approve()`

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
manager.approve(
    target: str,
    approved: bool,
    scope: str = "once",           # "once" | "session" | "always"
    agent_name: Optional[str] = None,
    reusable_scope: bool = False,  # derive prefix glob when True
    pattern: Optional[str] = None, # override target and derivation
) -> PersistentApproval
```

| Parameter        | Type          | Default  | Description                                                                |
| ---------------- | ------------- | -------- | -------------------------------------------------------------------------- |
| `target`         | `str`         | —        | The target that was approved/denied (e.g. `"bash:git status -s"`)          |
| `approved`       | `bool`        | —        | Whether approved (`True`) or denied (`False`)                              |
| `scope`          | `str`         | `"once"` | `"once"`, `"session"`, or `"always"`                                       |
| `agent_name`     | `str \| None` | `None`   | Scope to a specific agent                                                  |
| `reusable_scope` | `bool`        | `False`  | When `True` and scope is `session`/`always`, derive a reusable prefix glob |
| `pattern`        | `str \| None` | `None`   | Explicit pattern — overrides `target` and `reusable_scope`                 |

### `PermissionManager.suggest_scope_pattern()`

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
manager.suggest_scope_pattern(target: str) -> str
```

Exception-safe wrapper around `arity.derive_pattern`. Returns the derived glob pattern, or the original `target` unchanged if derivation is not possible.

### `PersistentApproval.derived`

| Field     | Type   | Default | Description                                                                                                                                                                                             |
| --------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `derived` | `bool` | `False` | `True` when the pattern was auto-generated by `derive_pattern()`. Enables the bare-prefix fallback in `matches()`. Loaded from `approvals.json` — older files default to `False` (backward compatible). |

<Card title="PermissionManager API Reference" icon="code" href="/docs/features/permissions">
  Full `PermissionManager` configuration and rule options
</Card>

***

## Related

<CardGroup cols={2}>
  <Card title="Interactive Approval" icon="shield-check" href="/docs/features/interactive-approval">
    Terminal-prompt approval flow — `[A]` persists a narrow command-prefix rule, `[T]` grants blanket tool access
  </Card>

  <Card title="Declarative Permissions" icon="shield-halved" href="/docs/features/declarative-permissions">
    Pre-declare allow/deny rules in YAML, CLI, or Python
  </Card>

  <Card title="Permissions" icon="shield" href="/docs/features/permissions">
    Full `PermissionManager` Python SDK reference
  </Card>

  <Card title="Command-Aware Permissions" icon="shield-halved" href="/docs/features/command-aware-permissions">
    How compound shell commands are evaluated
  </Card>

  <Card title="Durable Approvals" icon="database" href="/docs/features/durable-approvals">
    Persist approvals across restarts
  </Card>

  <Card title="Workspace Boundary" icon="folder-lock" href="/docs/features/workspace">
    Restrict agent file access to the project directory
  </Card>
</CardGroup>
