# Approvals and Sensitive Operations

Approvals mediate individual tool calls after arguments are produced and before
the tool executes. They are not model-output validation and do not sandbox
ordinary embedded Python.

## Risk Classes

Kedi uses three ordered risk classes:

- `read_only`: observes non-sensitive state;
- `mutating`: changes state or performs an effect;
- `sensitive`: accesses secrets or otherwise requires stronger review.

Custom Kedi and Python tools default to `mutating`. A Python `@kedi.tool` can
declare a different risk:

```python
import kedi


@kedi.tool(risk="read_only")
def list_public_files() -> list[str]:
    ...
```

An argument-aware risk resolver may elevate a call, but cannot downgrade a
tool's static risk.

## Default Policy

Without an explicit policy, `read_only` calls are allowed and `mutating` or
`sensitive` calls are denied. This is the Python/runtime fail-closed default.

The interactive `kedi` CLI installs a prompt policy for risky calls. It offers
Allow once, Deny, and Allow always for this run. “Always” is scoped to the
current process, tool, and risk level; it is never persisted.

In non-interactive execution, an unanswered prompt is denied.

## Static Policies

```kedi
> approval: allow
```

`allow` permits registered mutating and sensitive tools in that scope. Use it
only when the complete tool surface and arguments are already trusted.

```kedi
> approval: deny
```

`deny` refuses risky calls. Read-only calls remain automatically allowed.
`edit` is not a static mode.

## Dynamic Policy

Define a sync or async handler in Python and select it:

````kedi
```
from pathlib import Path
from kedi import ApprovalDecision

def review_tool(request):
    if request.tool_name != "write_report":
        return ApprovalDecision.deny(reason="tool is outside this workflow")

    safe_path = Path("reports") / Path(request.arguments["path"]).name
    return ApprovalDecision.edit(
        {**request.arguments, "path": str(safe_path)},
        reason="redirected to the reports directory",
    )
```

> approval: `review_tool`
````

The handler must return `ApprovalDecision.allow()`,
`ApprovalDecision.deny()`, or `ApprovalDecision.edit(arguments)`. Returning a
string or arbitrary mapping is an error.

### LLM-backed approval helper

The built-in `helpers` module provides an LLM-backed dynamic handler that uses
the active model:

```kedi
> import: helpers
> approval: `llm_approval`
```

!!! warning "Experimental and limited-context"
    Approval requests do not currently include a call reason. `llm_approval`
    therefore judges with limited information: the tool name, description,
    declared risk, and call arguments. This helper is not a stable feature and
    must not be treated as a complete authorization boundary.

## Approval Request

The immutable request contains:

| Field | Meaning |
| --- | --- |
| `tool_name` | Registered tool name |
| `arguments` | Deep-copied, read-only argument mapping |
| `risk` | Effective risk after argument-aware elevation |
| `adapter_shortname` | Active adapter when known |
| `description` | Tool description when available |
| `metadata` | Deep-copied tool metadata when available |

The handler cannot mutate the request in place. It must return `edit` with a new
complete argument mapping.

## Edited Arguments

Edited arguments are reclassified and revalidated against the tool signature,
custom Kedi models, and schema before invocation. An edit that redirects a
normal path to `.env` can therefore become `sensitive` and face subsequent
policy checks. Invalid edited types fail before the tool runs.

Only an `edit` decision may carry replacement arguments. Allow and deny cannot
smuggle an edit payload.

## Sensitive Files

The bundled filesystem tools classify `.env` and `.env.*` as secrets. Normal
`read_text_file(path)` refuses them. The caller must set
`secret_files=True`, which elevates the call to `sensitive`, and approval must
then permit it.

Writes and patches targeting secret filenames are likewise elevated before
mutation. The opt-in flag requests review; it does not itself authorize access.

## Scope and Descendants

Approval is lexical and restored when a scope exits. A procedure directive
applies to its following tool calls; a profile carries its policy when used.

Subagents have their own policy, but an ancestor's approval ceiling remains in
force. A child cannot replace a parent deny with allow. Parent edits are
reclassified and checked by child policy. This makes delegation monotonically
restrictive rather than a route around approval.

## Limits of Approval

Approval wraps registered tool calls. It does not intercept arbitrary I/O in a
Python block, a package prelude, or third-party code. Treat embedded Python as
trusted executable code and use host isolation when that boundary matters.
