## tools.* — call Executa tools from the UI (sync + async jobs)

*Host API · tools.**

Drive any installed Executa tool directly from the **anna-app bundle** (iframe), bypassing the LLM. Useful for deterministic UI actions — "click this button → run this tool" — where you don't want the model in the loop.

**Auth chain.** Bundle calls `anna.tools.{list,invoke}(...)` from the SDK proxy → postMessage → `POST /api/v1/anna-apps/runtime/rpc` with `X-Anna-App-Token` (minted by `window.create`) + session cookie → `_h_tools_*` handler in `anna_app_core.dispatcher`. The token is never exposed to JS that didn't go through the SDK.

**ACL is two-layer.** A tool must be (a) declared in `manifest.required_executas` or `optional_executas`, AND (b) matched by an entry in `manifest.ui.host_api.tools` (`"required:*"`, `"optional:*"`, the bare `tool_id`, or any `<prefix>:<tool_id>`). `tools.list` returns exactly the intersection; `tools.invoke` re-checks per call and rejects with `permission_denied`.

**Timeout policy** (`tools.invoke` only). Caller-supplied `timeoutMs` is clamped to `[ANNA_APP_TOOLS_INVOKE_TIMEOUT_MIN_MS, _MAX_MS]` (defaults **1 s / 90 s**). When omitted, the Agent falls back to the plugin's manifest `tool_def.timeout`, or `_DEFAULT_MS` (**65 s**). Host wall-clock wait is `clamped + GRACE_MS` (default **2 s**) so an in-flight ack/cancel has room to land before `tool_timeout` fires. See RFC `docs/design/anna-app-tools-invoke-timeout.md`. Work that may exceed 90 s must use the **async job channel** — `tools.invokeAsync` returns a `jobId` immediately (no held-open HTTP request anywhere); progress arrives via `tool_job` window events + `tools.getJob` polling; `tools.cancelJob` / `tools.listJobs` complete the lifecycle. RFC `docs/design/anna-app-tools-invoke-async-jobs.md`.

**Envelope stripping.** The raw NATS reply is `{success, data:{success, data:<payload>, error?}}`. The host unwraps both layers: outer-layer failure → `executa_unavailable`, inner-layer failure → `tool_failed`, otherwise the plugin payload (forced to `object`) lands on the iframe Promise.

- **tools.list** — Project the manifest's tool grants. Returns `{tools: [{tool_id, status?}]}` filtered through the `host_api.tools` whitelist; the optional `status` (`available` / `deploying` / `unavailable`, dispatcher ≥ 0.14.0) reflects live deploy-reconciliation facts — disable buttons before a click fails, and pair with `anna.tools.onChanged(cb)` (SDK ≥ 0.12.0) to re-list when `tools.changed` fires after a deploy lands. Pure ACL — no Agent / NATS round-trip; safe to call at bundle boot to decide which buttons to render.

- **tools.invoke** — Call one tool by id with structured `args` and an optional clamped `timeoutMs` (**max 90 s** — the public edge's held-request ceiling). Routes through the user's online Executa Agent over NATS, strips two envelope layers, and returns the plugin payload — or a stable error code (`tool_timeout`, `tool_failed`, `executa_unavailable`, `agent_unavailable`, `permission_denied`, `invalid_arg`). Calls that may outlive 90 s belong on `tools.invokeAsync`.  `timeoutMs`

- **tools.invokeAsync** — Start a long tool call as a host-owned **job** and return `{jobId, state: "queued", deadlineMs}` immediately — no long-held HTTP request. `timeoutMs` is the job deadline (default 30 min, clamp 60 s–24 h); `clientTag` (≤120 chars) is your reload-recovery key. Progress/terminal updates are pushed as `tool_job` window events and read authoritatively via `tools.getJob`. SDK sugar: `anna.tools.invokeAsyncAwait(args, {onProgress, signal})`.  `async job` `clientTag`

- **tools.getJob** — Authoritative job snapshot + incremental progress: `{jobId, sinceSeq?, limit?}` → `JobSnapshot` with `state`, `result` (succeeded), structured `error` (failed/cancelled/expired), and the progress slice with `seq > sinceSeq` (per-job ring of the latest 500). Also the polling fallback and the reload-recovery read — events are an optimisation, this is the truth.  `sinceSeq`

- **tools.cancelJob** — Cooperative, **idempotent** cancel: active job → agent-side graceful task cancel (same plugin's other invokes are untouched; process-group kill only as 500 ms fallback) and `state: "cancelled"`; terminal job → `{cancelled: false}` no-op. Optional `reason` (≤200 chars) lands in the job's error message.  `idempotent`

- **tools.listJobs** — Recent jobs for the current user — the reload-recovery entry point. Filter by `tool_id`, `state` (single or array), `clientTag`, `since` (RFC3339, ≤7 d lookback); `limit` 1–100 (default 20). Returns `{jobs, truncated}` sorted newest-first. Cross-user rows never appear (ownership is enforced row-level).  `recovery`

---

## Detailed reference

### tools.list

*Host API method · iframe surface*

Return every Executa `tool_id` this window is *currently allowed* to invoke. Pure ACL projection — no Agent round-trip, no NATS call. Useful at bundle boot to decide which UI buttons to render before the user clicks.

**Signature**

```ts
anna.tools.list(args?: {}, opts?: {timeoutMs?: number}) => Promise<{tools: Array<{tool_id: string, status?: 'available'|'deploying'|'unavailable'}>}>
```

**Direction:** Bundle (iframe) → Host RPC → manifest ACL

### tools.invoke

*Host API method · iframe surface*

Invoke a single Executa tool *directly from the bundle*, bypassing the LLM. The host enforces the `host_api.tools` whitelist, mints credentials, negotiates a deadline, calls the user's online Executa Agent over NATS, and returns the plugin's payload — two envelope layers stripped — to the iframe.

**Signature**

```ts
anna.tools.invoke(args: {tool_id: string, method?: string, args?: object, timeoutMs?: number}, opts?: {timeoutMs?: number}) => Promise<object>
```

**Direction:** Bundle (iframe) → Host RPC → Executa Agent (NATS) → plugin

**Parameters**

- `tool_id` (string, required) — Server-minted Executa tool id; must be present in the `tools.list` projection.
    - Format is opaque. Mint-only ids look like `tool-{handle}-{slug}-{uniq}` (no separator); legacy ids use `plugin.tool` or `plugin__tool` — both still routed by `_split_tool_id`.
    - Must match an entry in `manifest.ui.host_api.tools`. The runtime gate `_is_tool_allowed` accepts `required:*`, `optional:*`, the bare id, or any ref ending in `:<tool_id>`; otherwise `permission_denied`. For authoring, the CLI `anna-app validate` only permits the canonical forms `required:*` / `optional:*` / `required:<id>` / `optional:<id>` / bare `<id>` / `bundled:<handle>`.
    - Reject reasons (HostRpcError `invalid_arg`): missing, empty, or non-string.
- `method` (string | undefined, optional) — Plugin-side method name. **Required** for mint-only `tool_id`s that contain no `.` or `__` separator.
    - When omitted, the host falls back to splitting `tool_id` on the first `__` or `.` separator — failing with `invalid_arg` if neither is present.
    - When provided, `tool_id` is used verbatim as `plugin_name` and `method` becomes `tool_name` on the NATS call.
    - Must be a string when set; non-string types raise `invalid_arg`.
- `args` (object, optional) — Structured tool arguments forwarded verbatim to the plugin as `arguments`.
    - JSON-serialised by the SDK; functions, BigInts, and cyclic refs are rejected at `postMessage` time.
    - Schema is plugin-defined — see the Executa's manifest `tools[].parameters`. The host does not validate field-level shape; an unknown field is silently passed through (or rejected by the plugin).
- `timeoutMs` (int | undefined, optional) — Caller-requested plugin-facing deadline in **milliseconds**. Clamped server-side to `[ANNA_APP_TOOLS_INVOKE_TIMEOUT_MIN_MS, _MAX_MS]` (defaults 1 000 / **90 000** — the public edge's held-request ceiling, forum #199). Calls that need more must use `tools.invokeAsync`.
    - Booleans are rejected explicitly (`invalid_arg`) — `True`/`False` would otherwise clamp via the `bool ⊂ int` quirk.
    - When omitted, the Agent falls back to the per-tool `timeout` declared in the Executa manifest, or `ANNA_APP_TOOLS_INVOKE_TIMEOUT_DEFAULT_MS` (65 000) if neither side declares one.
    - Host wall-clock wait = clamped value + `ANNA_APP_TOOLS_INVOKE_TIMEOUT_GRACE_MS` (2 000) so an in-flight ack/cancel has room to land before `tool_timeout` fires.
    - SDK default per-call timeout is 70 000 ms (`DEFAULT_TIMEOUTS_BY_NS.tools`); pass a larger value here *and* in `opts.timeoutMs` if the plugin needs longer.
    - Clamp-down is observable: when a clamped call later times out, `error.details` carries `requested_timeout_ms` / `max_timeout_ms` alongside the effective `timeout_ms`.

### tools.invokeAsync

*Host API method · iframe surface*

Start a long-running tool call as a host-owned **job** and return immediately with a `jobId`. No held-open HTTP request anywhere in the chain — this is the only correct channel for tools that may exceed the 90 s synchronous ceiling. Execution state lives in the host DB; updates arrive via `tool_job` window events (push) and `tools.getJob` (authoritative pull).

**Signature**

```ts
anna.tools.invokeAsync(args: {tool_id: string, method?: string, args?: object, timeoutMs?: number, clientTag?: string}) => Promise<{jobId: string, state: "queued", deadlineMs: number}>
```

**Direction:** Bundle (iframe) → Host RPC (instant) → job row + NATS long command → Executa Agent → plugin

**Parameters**

- `tool_id` (string, required) — Server-minted Executa tool id; same whitelist gate (`host_api.tools`) as `tools.invoke`.
    - Mint-only ids (`tool-{handle}-{slug}-{uniq}`) carry no separator — pass the plugin method via `method`.
- `method` (string | undefined, optional) — Plugin-side method name; **required** for mint-only `tool_id`s (same splitting rules as `tools.invoke`).
- `args` (object, optional) — Structured tool arguments, forwarded verbatim to the plugin. Size cap 64 KB (`invalid_arg` beyond).
- `timeoutMs` (int | undefined, optional) — The **job deadline** — how long the plugin may run. Clamped to [60 000, 86 400 000] (60 s – 24 h). Unlike the sync channel this is not edge-bound: no HTTP request is held open.
    - Deadline elapsed without a terminal state → job flips to `expired` with error `tool_timeout`, and the host sends the agent a cancel.
    - Values below 60 s are raised to 60 s — calls that fit under 90 s should use plain `tools.invoke` instead.
- `clientTag` (string | undefined, optional) — Caller-defined correlation tag (≤120 chars), stored on the job row and filterable in `tools.listJobs` — the key to re-adopting jobs after an iframe reload.

### tools.getJob

*Host API method · iframe surface*

Authoritative snapshot of one job + an incremental progress slice. This is the single source of truth: `tool_job` push events are a latency optimisation and may be dropped — `getJob` never lies. Also the reload-recovery read and the polling fallback used by `invokeAsyncAwait`.

**Signature**

```ts
anna.tools.getJob(args: {jobId: string, sinceSeq?: number, limit?: number}) => Promise<JobSnapshot>
```

**Direction:** Bundle (iframe) → Host RPC → job row read (no Agent round-trip)

**Parameters**

- `jobId` (string, required) — Job id from `invokeAsync` / `listJobs`; must match `^tjob_[0-9a-f]{32}$`.
- `sinceSeq` (int, optional) — Return only progress events with `seq > sinceSeq`. Feed back the previous response's `lastSeq` for cheap incremental reads.
- `limit` (int, optional) — Max progress events in the slice (1–500).

### tools.cancelJob

*Host API method · iframe surface*

Cooperative, idempotent cancel. Active job: the row flips to `cancelled` immediately (user-visible), then the agent gracefully cancels the command task — the plugin process and its other concurrent invokes are untouched; a process-group kill is only the 500 ms fallback for a truly stuck task. Terminal job: `{cancelled: false}` no-op.

**Signature**

```ts
anna.tools.cancelJob(args: {jobId: string, reason?: string}) => Promise<{jobId: string, state: JobState, cancelled: boolean}>
```

**Direction:** Bundle (iframe) → Host RPC → job transition + `_cancel_invoke` fast-path command → Agent

**Parameters**

- `jobId` (string, required) — Job to cancel; `^tjob_[0-9a-f]{32}$`.
- `reason` (string | undefined, optional) — Free-text audit note (≤200 chars); lands in the job's error message.

### tools.listJobs

*Host API method · iframe surface*

Enumerate the current user's recent jobs — the reload-recovery entry point. After an iframe reload (or in a second window) call `listJobs({clientTag, state: ["queued","running"]})` to find in-flight work, then drive each job with `getJob({sinceSeq})`.

**Signature**

```ts
anna.tools.listJobs(args?: {tool_id?: string, state?: JobState | JobState[], clientTag?: string, since?: string, limit?: number}) => Promise<{jobs: JobSnapshot[], truncated: boolean}>
```

**Direction:** Bundle (iframe) → Host RPC → job rows read (no Agent round-trip)

**Parameters**

- `tool_id` (string | undefined, optional) — Filter to one tool id.
- `state` (string | string[] | undefined, optional) — One or more of queued | running | succeeded | failed | cancelled | expired.
- `clientTag` (string | undefined, optional) — Exact match on the tag passed at `invokeAsync` time — scope recovery to YOUR batch.
- `since` (string | undefined, optional) — RFC3339 lower bound on `createdAt`; lookback capped at 7 days.
- `limit` (int, optional) — Max rows (1–100), newest-first.
