# Eve

Source: https://permdock.com/docs/adapters/eve

permdock/eve turns PermDock decisions into Eve tool approval policies and approval response policies, takes principal and actor from the durable session's auth, and stores pending approvals in a pluggable ApprovalStore.

`permdock/eve` connects the [Decision](/docs/concepts/decisions) model to Eve's `approval` hook on `defineTool`. Eve already pauses a session durably and renders the approval on every channel; what it asks the application for is the policy that decides whether a call may run, must wait for a person, or is refused, and a second policy that decides who may press Approve. Both come out of one `createPermDock` call, typed against your permissions, fail-closed.

## Purpose [#purpose]

Eve's [human-in-the-loop](https://eve.dev/docs/human-in-the-loop) hook receives the session context plus `{ toolName, toolInput, approvedTools, callId }` and returns an AI SDK 7 approval status: `"not-applicable"` to continue, `"user-approval"` to pause, `"approved"` or `"denied"` (optionally `{ type, reason }`) to decide in code. It offers `never()`, `once()` and `always()` helpers and leaves anything input- or caller-dependent to a custom function. Its `response` policy receives the submitted `response` (`decision` plus the authenticated `principal`) and decides whether that person may approve *this* call, and the docs note that a shared request stays pending when a responder is rejected so another eligible approver can act. That is a policy decision point with an approval store missing in the middle; `permdock/eve` supplies both from the application's PermDock policy and an [ApprovalStore](/docs/adapters/approvals).

## API [#api]

```ts
import { defineTool } from "eve/tools";
import { createPermDock } from "permdock/eve";

const DeleteInput = z.object({ id: z.string() });
const RefundInput = z.object({ chargeId: z.string(), amount: z.number() });

const { approval, approvalFor, permdock } = createPermDock(policy, {
  tools: {
    delete_post: {
      permission: permissions.post.delete,
      data: (input) => loadPost(DeleteInput.parse(input).id),
    },
    refund: {
      permission: permissions.charge.refund,
      data: (input) => loadCharge(RefundInput.parse(input).chargeId),
    },
  },
  store, // ApprovalStore; memoryApprovalStore() when omitted
  approvers: { roles: ["finance-admin"] }, // who may resolve; see below
  delegation: () => ({
    scopes: [permissions.post.delete.scope, permissions.charge.refund.scope],
  }),
});

export default defineTool({
  description: "Refund a charge.",
  inputSchema: RefundInput,
  approval, // { request, response } built from the tools map
  async execute(input, ctx) {
    // permdock(ctx) is the request-scoped PermDock for checks inside the tool
    (await permdock(ctx)).assert(
      permissions.charge.refund,
      await loadCharge(input.chargeId),
    );
    return refund(input);
  },
});

// Or gate a single tool without the map:
approval: approvalFor(permissions.charge.refund, (input) =>
  loadCharge(RefundInput.parse(input).chargeId),
);
```

* `approval` is an Eve `{ request, response }` pair, each taking the one context object Eve passes (`EveApprovalContext` for `request`, `EveResponseContext` for `response`). `request` looks the tool name up in `tools` (own keys only), resolves the resource, calls `decide` and maps the outcome. `response` checks the responder against the approval record and the `approvers` rule.
* `approvalFor(permission, data?)` builds the same pair for one tool inline.
* `data` receives the tool input as `unknown`; parse it with the tool's schema before loading the resource.
* `permdock(ctx)` returns a request-scoped `PermDock` from `ctx.session` for checks inside `execute` or in other Eve code. It builds a fresh frozen instance on every call, so a replayed step never sees state from an earlier one.
* `tenant` resolves the active tenant from the same context on every call; the `tools` map has no tenant dimension, because the tenant is part of the subject, not of the tool.
* `store` is the [ApprovalStore](/docs/adapters/approvals). `approvers` is either `{ roles }` (approver must hold one of these roles in the PermDock policy) or a function `(responder, request) => boolean`. It is an extra restriction on top of `grant.approval.by`; both must pass. The actor rule (an agent never approves its own call) is enforced regardless.

### Subject from the session [#subject-from-the-session]

Eve exposes two authenticated principals on `ctx.session.auth`: `initiator`, who created the session, and `current`, who sent this turn. Each has `principalId`, `principalType`, `authenticator` and `attributes`. `permdock/eve` maps them:

| PermDock subject part | Eve source | Notes |
| --- | --- | --- |
| `principal` | `session.auth.initiator` | The human the session acts for; roles come from `policy.subject` or `attributes` you declare as server-set |
| `actor` | `session.auth.current` when it is not the initiator, else the Eve app principal | `kind: 'eve'`; a schedule-dispatched turn (`authenticator: 'app'`, `principalId: 'eve:app'`) is an actor with no human `current` |
| `delegation` | The `delegation` option, resolved from the same context | No default. Every eve turn has an actor, so without `delegation` every tool is denied with reason `no-delegation` ([delegation](/docs/security/delegation)) |

The `subject` and `actor` options override these defaults when an application encodes identity differently. Model output, `toolInput` and `attributes` that Eve marks as user-supplied never build a subject ([authentication](/docs/concepts/authentication)).

## Request lifecycle [#request-lifecycle]

1. Eve is about to execute a tool and calls `approval.request` with the session and `toolInput`.
2. The adapter validates `toolInput` against the resource schema when a `data` resolver exists, loads the resource and calls `decide`.
3. The outcome maps to Eve's vocabulary:

| Decision outcome | `approval.request` returns | Eve behaviour |
| --- | --- | --- |
| `granted` | `"not-applicable"` | Tool runs without a prompt |
| `approval-required` | `"user-approval"` | Session parks at `session.waiting`; `input.requested` emitted; an `ApprovalRequest` with `token` is written to the store |
| `denied` | `{ type: "denied", reason }` | Tool does not run; the model receives `reason` built from denials and alternatives |
| unmapped tool, invalid input, thrown resolver | `{ type: "denied", reason }` | Fail closed |

4. A person presses Approve or Cancel on a channel. Eve calls `approval.response` with `response.decision` and the authenticated `response.principal` (the responder). The adapter finds the record for the call (from this instance's bounded session cache, or by recomputing the token from the request and the session initiator, so any replica can answer), refuses a responder whose `principalId` is the request's actor, applies `approvers`, maps the responder through the same `subject` as an initiator so `grant.approval.by` sees their roles, memberships and tenant, and calls `resolveApproval` (`approved` for Approve, `rejected` for Cancel, so the re-check denies), returning `{ status: "allowed" }`. Otherwise it returns `{ status: "rejected", reason }` and Eve keeps the request pending for another responder. The initiator cannot approve their own request unless the grant sets `approval: { distinct: false }`.
5. Eve re-runs `approval.request` for the same call before `execute` (the AI SDK re-check) and runs the tool unless the answer is a denial. The adapter recomputes the token: an approved record is consumed and the answer is `"not-applicable"`, so the tool runs once; a record still pending means nobody approved and the answer is a denial with `approval-pending`; a consumed, rejected or expired record is a denial too.
6. Every step emits `on('decision')`; the ask, the answer and the resumed decision share `token`.

`"not-applicable"` is used only for `granted`. It is Eve's word for "continue", and the adapter never reaches it without a positive decision, so the fail-open path `@ai-sdk/policy-opa` has does not exist here.

## What it validates [#what-it-validates]

* `toolInput` against the resource's Standard Schema when `data` is declared; Eve's `toolInput` may be `undefined`, which is a validation failure and therefore a denial.
* The subject comes from `session.auth`, never from `toolInput` or the model.
* The responder in `approval.response` is Eve's authenticated principal; it must not be the actor, must satisfy `approvers`, and its verdict is recorded in the store.
* Replays: the token is recomputed on resume. Eve's `approvedTools` (session-scoped sticky approvals) is never consulted, and `once()`-style reuse is not offered; each call has its own request, which is also what Eve's docs recommend for non-idempotent side effects across replays.

## How denials surface [#how-denials-surface]

* `{ type: "denied", reason }` with `reason` built from the Decision: the failing roles, the reason kind and the `alternatives` list, phrased for the model ("charge.refund denied: member (condition). Alternatives: charge.read.").
* A rejected responder gets `{ status: "rejected", reason: "approver is the actor of this request" }` (or the `approvers` reason); the request stays pending, matching Eve's semantics.
* Expired or mismatched approvals on resume are denials with `detail` `approval-expired` or `approval-mismatch`.

## Example app [#example-app]

`apps/examples/eve-agent`: HTTP harness on `127.0.0.1:3474` with `GET /health`, driving the pair with Eve's session shapes. `GET /list_posts` returns `not-applicable`. `GET /delete_post?call=…` returns `user-approval`; `POST /approve?call=…` answers as an admin reviewer; the next `GET /delete_post` for that call is the re-check and returns `not-applicable` once, and a further one is denied. No model API key. `tests/integration` also runs the pair through the `toolApproval` Eve 0.58 builds in its harness, inside a multi-step AI SDK `streamText` loop against a Postgres `ApprovalStore`. This example is also the deploy template for the PermDock Cloud listing on the Vercel Marketplace: swapping `memoryApprovalStore()` for `cloud().approvals` is the only change ([Cloud adapter](/docs/adapters/cloud)).

## Related standards [#related-standards]

* [Approvals](/docs/security/approvals) and [approvals](/docs/adapters/approvals): the token, the store, the approver rules.
* [Delegation](/docs/security/delegation): the actor half of the subject for agent turns.
* [OWASP Agentic Top 10](/docs/security/owasp-agentic): ASI02 tool misuse via per-tool permissions and input validation.
* [AI SDK adapter](/docs/adapters/ai-sdk): Eve's approval status vocabulary is the AI SDK 7 one.
