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

# NSP Action Safety: Reversibility Tiers and Verification

> How NSP's reversibility tiers, confidence gating, verify expressions, and SDK enforcement prevent agents from accidentally executing destructive actions.

Every action that changes application state passes through a multi-layer safety gate before the daemon dispatches it to the target runtime. The gate is not advisory — it actively rejects requests that don't meet the requirements for the action's reversibility tier. Understanding these tiers and how to satisfy their requirements is the most important safety topic for anyone building production agents on NSP.

## The Three Reversibility Tiers

Each action schema declares a `reversibility` field that classifies what the action does to application state. Treat this classification as a **binding safety contract**, not a hint.

<AccordionGroup>
  <Accordion title="read — No state change">
    The action reads data from the application without modifying anything. There is no confidence gate and no verification requirement. These actions are safe to call at any confidence level, including during probe warm-up.

    **Examples:** `get_thread`, `search_inbox`, `list_contacts`, `read_document`, `get_draft`
  </Accordion>

  <Accordion title="reversible_write — Undoable change">
    The action changes application state, but the change can be undone (for example, moving an email to a folder, starring a message, or creating a draft). A minimum confidence score of **0.80** is required. A `verify_expression` is not mandated but is strongly recommended.

    **Examples:** `archive`, `label`, `move_to_folder`, `mark_read`, `star`, `create_draft`, `update_draft`
  </Accordion>

  <Accordion title="irreversible_write — Permanent change">
    The action causes a permanent side effect that cannot be undone once it executes. Sending an email, deleting a record permanently, making a purchase, and submitting a form all fall into this tier.

    This tier has the strictest requirements:

    * Confidence must be **≥ 0.95**
    * `verify` must be `true`
    * A `verify_expression` must be provided

    **Examples:** `send_reply`, `delete_permanently`, `purchase`, `submit_form`, `send_message`
  </Accordion>
</AccordionGroup>

## Confidence Gating Table

The daemon checks the `confidence` field of the current `SubstrateState` before dispatching any write action. If confidence falls below the required threshold, the action is rejected.

| Reversibility Tier   | Minimum Confidence | `verify_expression` Required |
| -------------------- | ------------------ | ---------------------------- |
| `read`               | None               | No                           |
| `reversible_write`   | 0.80               | No (recommended)             |
| `irreversible_write` | 0.95               | **Yes — always**             |

<Note>
  The effective confidence used for gating is `min(schema.confidence, state.confidence)`. If the probe ran in DOM fallback mode, `state.confidence` may be 0.70–0.85, which blocks `irreversible_write` actions until a higher-quality V8 probe cycle completes. Call `GET /state?fresh=true` to trigger a fresh probe cycle immediately.
</Note>

## The verify\_expression Requirement

For every `irreversible_write` action, you must provide a `verify_expression` in your request body. After the action executes, the daemon waits `post_action_wait_ms` (default: 200 ms) for the application to settle, then evaluates the expression against the updated `SubstrateState.objects`. If the expression evaluates to `false`, the daemon returns `422` with a `verification_failed` error.

```json theme={null}
{
  "action": "send_reply",
  "parameters": {
    "thread_id": "thread_001",
    "body": "Thanks for the report — I'll follow up by EOD."
  },
  "verify": true,
  "verify_expression": "inbox.sent_count > 0",
  "verify_timeout_ms": 5000
}
```

### verify\_expression Syntax

Expressions use a lightweight, restricted evaluator — not arbitrary JavaScript. The following patterns are supported:

| Pattern                | Example                                               | Description                                      |
| ---------------------- | ----------------------------------------------------- | ------------------------------------------------ |
| `field == value`       | `inbox.unread_count == 0`                             | Equality check                                   |
| `field != value`       | `compose.is_open != false`                            | Inequality check                                 |
| `field > value`        | `inbox.sent_count > 5`                                | Numeric greater-than                             |
| `field >= value`       | `confidence >= 0.95`                                  | Numeric greater-than-or-equal                    |
| `changed(field)`       | `changed(inbox.unread_count)`                         | Field changed between pre- and post-action state |
| `exists(field)`        | `exists(compose.draft_id)`                            | Field is present in post-action state            |
| `context.url == "url"` | `context.url == "https://mail.google.com/sent"`       | URL comparison                                   |
| `prev.field != field`  | `prev.inbox.sent_count != inbox.sent_count`           | Explicit pre/post comparison                     |
| `expr && expr`         | `flight.is_armed == true && flight.altitude_m > 10.0` | Logical AND                                      |

<Warning>
  Expressions that reference `fetch(`, `eval(`, `XMLHttpRequest`, `import(`, or `document.cookie` are rejected by the expression validator before execution. Expressions are evaluated inside the daemon process, not inside the browser tab.
</Warning>

Expressions are limited to 2048 bytes by default and must evaluate to a boolean.

### Verification Timeout

Use `verify_timeout_ms` to control how long the daemon polls for the post-action state to satisfy your expression. Set this to match the expected latency of the underlying operation — email sends typically need 3–8 seconds; database writes might need up to 30 seconds.

```json theme={null}
{
  "verify_expression": "inbox.sent_count > 0",
  "verify_timeout_ms": 8000
}
```

If the expression is still `false` after the timeout, the daemon returns `422`:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "verification_failed",
    "message": "Post-action verification failed for expression: inbox.sent_count > 0"
  }
}
```

## The Daemon's Three-Layer Safety Gate

Requests to `POST /substrate/v1/action` pass through three sequential checks before reaching the action dispatcher:

```text theme={null}
POST /substrate/v1/action
  │
  ├─ Layer 1: CDP Builtin Gate
  │   Is this action in SAFETY_REQUIRED_BUILTINS?
  │   → verify_expression required if yes
  │
  ├─ Layer 2: Schema Reversibility Gate
  │   Is action.reversibility == irreversible_write?
  │   → verify_expression required if yes
  │
  └─ Layer 3: Heuristic Dangerous Name Gate
      PID cannot be resolved AND action name contains a danger keyword?
      → verify_expression required (or request rejected)
```

The heuristic gate in Layer 3 catches action names containing words like `delete`, `remove`, `destroy`, `drop`, `purge`, `wipe`, `reset`, `clear_all`, `empty`, `terminate`, `shutdown`, or `kill` when the daemon cannot resolve the target process to a known schema. This is a safety net for race conditions during probe warm-up; it is not a substitute for proper schema reversibility declarations.

**Error response when `verify_expression` is missing for an irreversible action:**

```json theme={null}
{
  "error": {
    "code": "missing_verify_expression",
    "message": "Action 'send_reply' is irreversible (IrreversibleWrite) and requires verify_expression. Add: {\"verify_expression\": \"changed('relevant.field')\"}"
  }
}
```

## How the Python SDK Enforces Action Safety

The Python SDK adds a client-side pre-flight check that runs **before any HTTP request is made**. When you call `session.execute()`, the SDK inspects the cached action schema. If the action's reversibility is `IRREVERSIBLE_WRITE` and you passed `verify=False` (or omitted `verify_expression`), the SDK raises `NSPIrreversibleNotConfirmedError` immediately — no network round-trip is wasted.

```python theme={null}
import nelieo_nsp as nsp

# This raises NSPIrreversibleNotConfirmedError before any HTTP call:
await session.execute("send_reply", parameters={...}, verify=False)

# Correct — verify=True with an expression:
await session.execute(
    "send_reply",
    parameters={
        "thread_id": "thread_001",
        "body": "Confirmed for the 3 PM call.",
    },
    verify=True,
    verify_expression="inbox.sent_count > 0",
    verify_timeout_ms=8000,
)
```

Catch `NSPIrreversibleNotConfirmedError` at the call site if you need to surface a clear error to users or logs:

```python theme={null}
try:
    await session.execute("send_reply", parameters={...}, verify=True,
                          verify_expression="inbox.sent_count > 0")
except nsp.NSPIrreversibleNotConfirmedError as e:
    print(f"Blocked: {e.message}")
    # e.action    — action name
    # e.code      — "irreversible_not_confirmed"
```

### Handling NSPConfidenceTooLowError

If confidence falls below the required threshold at execution time, the SDK raises `NSPConfidenceTooLowError`. Refresh the probe and retry:

```python theme={null}
try:
    await session.execute("send_reply", parameters={...}, verify=True,
                          verify_expression="inbox.sent_count > 0")
except nsp.NSPConfidenceTooLowError as e:
    print(f"Confidence too low: {session.confidence:.2f} (need 0.95)")
    # Trigger a fresh probe cycle and wait for it to complete.
    await asyncio.sleep(5)
    await session.refresh(fresh_probe=True)
    # Retry once after the fresh probe.
    await session.execute("send_reply", parameters={...}, verify=True,
                          verify_expression="inbox.sent_count > 0")
```

## Best Practices for Production Agents

<Steps>
  <Step title="Always pass verify=True for irreversible actions">
    The SDK and daemon both enforce this, but making it explicit in every call site documents your intent and prevents confusion when reading the code later.

    ```python theme={null}
    result = await session.execute(
        "send_reply",
        parameters={"thread_id": thread_id, "body": body},
        verify=True,
        verify_expression=f"inbox.sent_count > {current_sent_count}",
    )
    ```
  </Step>

  <Step title="Write specific verify_expression strings">
    Vague expressions like `"inbox.sent_count > 0"` will pass even if a previous send already incremented the counter. When you know the pre-action state, use it to write a tighter assertion.

    ```python theme={null}
    # Before the action, capture the current value:
    before_count = session.state.get_int("inbox.sent_count")

    await session.execute(
        "send_reply",
        parameters={...},
        verify=True,
        # Assert the count incremented by exactly one:
        verify_expression=f"inbox.sent_count == {before_count + 1}",
    )
    ```
  </Step>

  <Step title="Set verify_timeout_ms to match the operation's expected latency">
    Network-dependent actions (email sending, form submissions, API calls) may take several seconds to settle. Default `verify_timeout_ms` is 5000 ms, which suits most UI interactions. Raise it for slower back-end operations.

    ```python theme={null}
    await session.execute(
        "submit_purchase",
        parameters={...},
        verify=True,
        verify_expression="checkout.order_id != null",
        verify_timeout_ms=15000,  # 15 seconds for a payment gateway
    )
    ```
  </Step>

  <Step title="Handle NSPConfidenceTooLowError gracefully">
    Don't let a transient low-confidence state crash your agent. Wait for the probe to reach full confidence before retrying irreversible actions — forcing a `fresh=true` probe cycle is the fastest way to recover.

    ```python theme={null}
    except nsp.NSPConfidenceTooLowError:
        await session.refresh(fresh_probe=True)
        await asyncio.sleep(3)
        # Retry the action once confidence is restored.
    ```
  </Step>
</Steps>
