# Approve risky actions

Some actions should not run until a person says yes: deleting data, sending email, moving money. Mark them with `@ApprovalRequired`, and the executor refuses to run them unless the call carries an approved `ApprovalToken`.

The framework checks tokens; it does not request them. Your code mints the token, shows it to a reviewer, records the decision and passes the approved token with the call.

## Mark the action

<!-- java-contract: src/main/java/com/example/tnsai/docs/SecurityApprovalsAccountActionsExample.java -->
```java
public static class AccountActions {
    @ActionSpec(type = ActionType.LOCAL, description = "Delete a user account permanently")
    @ApprovalRequired(reason = "Permanent data deletion")
    public String deleteUser(String userId) {
        return "deleted " + userId;
    }
}
```

`reason` defaults to `"Sensitive operation"` and appears in the refusal message. Put `@ApprovalRequired` on the method. `@Security(approvalRequired = true)` is metadata only: nothing enforces it.

## Mint, approve, run

<!-- java-contract: src/main/java/com/example/tnsai/docs/SecurityApprovalsRequestAndRunExample.java -->
```java
static String requestAndRun(Agent agent, AgentPrincipal principal, String userId) {
    ApprovalTokenStore store = new InMemoryApprovalTokenStore();

    // 1. Mint a pending token bound to the agent's principal and the action's method name.
    ApprovalToken token = store.createToken(principal.id(), "deleteUser",
        Map.of("userId", userId), Duration.ofMinutes(5));

    // 2. Show token.getTokenId() to a reviewer; their decision comes back through your own endpoint.
    store.approve(token.getTokenId(), "admin@example.com");

    // 3. Pass the approved token with the call. The executor checks and consumes it.
    Map<String, Object> params = new HashMap<>();
    params.put("userId", userId);
    params.put("_approval_token", store.getToken(token.getTokenId()).orElseThrow());
    try {
        return (String) agent.executeAction("deleteUser", params);
    } catch (ApprovalRequiredException e) {
        return "refused: " + e.getMessage();       // missing, unbound, pending, rejected, expired or used
    }
}
```

Before the action body runs, the executor:

1. Throws `ApprovalRequiredException` if `_approval_token` is missing or is not an `ApprovalToken`.
2. Throws if the executor has no principal. `AgentBuilder` requires one, so agents it builds always have it.
3. Checks that the token's agent id equals the principal's `id()` and its action name equals the action's name. This check comes first, so a caller with the wrong binding cannot burn the token.
4. Consumes the token. A single-use token moves to `CONSUMED`; a token that is pending, rejected, expired or already consumed is refused.
5. Removes `_approval_token` from the arguments, unless the method declares a parameter with that name.

The token's `parameters` map is for the reviewer. The executor does not compare it with the call's arguments, so check them yourself if the approval covers specific values.

Do not call `store.validateAndConsume(...)` before passing the token. It consumes a single-use token, and the executor then refuses the call.

## Token lifecycle

| Status | Meaning |
|--------|---------|
| `PENDING` | Created, waiting for a decision |
| `APPROVED` | Approved; a multi-use token stays here after each use |
| `REJECTED` | Rejected, with `getRejectedBy()` and `getRejectionReason()` |
| `EXPIRED` | The time to live passed before use |
| `CONSUMED` | A single-use token that authorized one call |

`approve(approver)` succeeds on a pending or approved token that has not expired. `reject(rejector, reason)` succeeds only on a pending one. Tokens from `createToken` are single-use. For a multi-use token, build it with `ApprovalToken.builder()` and `singleUse(false)`, then `store.save(token)`. The builder's default time to live is five minutes.

## Store tokens

`ApprovalTokenStore` holds tokens between the request and the decision: `createToken`, `save`, `getToken`, `approve`, `reject`, `listPending`, `listPendingForAgent`, `cleanupExpired` and `delete`. The only shipped implementation is `InMemoryApprovalTokenStore`, which loses its tokens on restart. For tokens that survive restarts or span several processes, implement `ApprovalTokenStore` over your own database.

## Ask mid-action with InterruptService

`InterruptService` pauses the calling thread until a person answers, without a token. Use it inside an action or a plan step when the question only comes up at run time.

<!-- java-contract: src/main/java/com/example/tnsai/docs/SecurityApprovalsInterruptServiceExample.java -->
```java
static InterruptService interruptService() {
    return new InterruptService(
        request -> System.out.println("Needs a decision: " + request.interruptId() + " " + request.payload()),
        Duration.ofMinutes(5));
}

static boolean askFirst(InterruptService interrupts, String userId) {
    InterruptResult result = interrupts.requestInterrupt(
        InterruptReason.HUMAN_APPROVAL,
        Map.of("action", "deleteUser", "userId", userId),
        Duration.ofMinutes(10));                  // blocks until resume, cancel or timeout
    return result.shouldProceed();
}

static void decide(InterruptService interrupts, String interruptId, boolean approved) {
    interrupts.resume(interruptId, approved,
        approved ? Map.of("approver", "admin@example.com") : Map.of("reason", "Too risky"));
}
```

The listener receives each `InterruptRequest` (id, reason, payload, timeout, creation time). Send it to your UI, and call `resume` or `cancel` from the handler that receives the answer. The framework provides no HTTP route for this. `requestInterruptAsync` returns a `CompletableFuture<InterruptResult>` instead of blocking.

`shouldProceed()` is true only for `APPROVED`. The other outcomes are `REJECTED`, `TIMED_OUT`, `CANCELLED` and `ERROR`. `InterruptReason` has `HUMAN_APPROVAL`, `UPLOAD_REQUIRED`, `POLICY_VIOLATION`, `ERROR_RECOVERY`, `CONFIRMATION` and `CUSTOM`.

## Related

- [Enforce permissions and content rules](enforcement.md)
- [Guardrails](guardrails.md)
- [Actions](../agents/actions.md)
- [Server tool approval](../server/tool-approval.md)
- [Annotation runtime status](../annotations/runtime-status.md)
