# The invocation contour

> One transport for every model call, enforced in CI; an ordered tool pipeline with declared risk and a fail-closed approval stage; a resolved-config endpoint

Source: https://aipril.dev/docs/harness/invocation

## One transport

Every LLM call goes through a single transport, directly or through the metering decorator composed
over it. The transport owns the request, the timeout, retry with error classification, and token-usage
accounting.

It carries tool definitions, tool choice including `required`, streaming and usage reporting - so no
call site has a structural reason to build its own request.

**Enforced in CI.** A source-scanning boundary test fails the build on any direct call site outside a
two-entry allowlist: the transport itself, and the provider connection test, which probes an unsaved
configuration and must not be metered.

Retry is opt-in and bounded, defaulting to a single attempt. Falling back to a different provider
mid-request is not part of it.

## Metering composes over the transport, not inside it

Credit metering takes the call as a callback: it opens a reservation, runs what it is given, and
settles against reported usage. It is transport-agnostic by construction.

The separation is load-bearing. Retry and error classification sit in the transport, below the
reservation - so a network retry does not extend the lifetime of a held credit hold.

**Billing is idempotent per attempt.** Each attempt derives a fresh request id. Against the credit
engine's replay rule, a request id held constant across attempts does not double-bill - it unbills,
because writes after the first are treated as replays.

A tenant running its own provider key is not metered by the platform.

## The tool pipeline

```mermaid
flowchart TD
    CALL["Tool call proposed by the model"] --> RISK["Resolve declared risk<br/>read · write · external"]
    RISK --> GUARD["Guardrails<br/>deterministic, no human"]
    GUARD -->|"blocked"| DENY["Denied<br/>own status and reason code"]
    GUARD --> APPR{"Needs a person?"}
    APPR -->|"read"| RUN["Execute"]
    APPR -->|"write · external"| ASK["Approval record<br/>durable, single-use, argument-bound"]
    ASK -->|"absent · unavailable · throws"| DENY
    ASK -->|"approved"| RUN
    RUN --> POST["Post-dispatch guardrails<br/>redaction"]
    POST --> TRAIL["Audit trail with trace"]
```

Stages run in order, at one insertion point per side of dispatch, not per branch.

- **Guardrails before approval.** Deterministic checks run first; a call that cannot be allowed never
  reaches a human.
- **Fail-closed approval.** A `write` or `external` call is denied when there is no approver, when the
  approver returns `unavailable`, and when the approver throws. A
  [digital employee](https://aipril.dev/docs/ai-organisation/employees.md) runs unattended, so an unanswerable approval is
  a denial by construction.
- **Approval is argument-bound.** The record is durable and single-use, and authorises those
  arguments - not that tool in general.
- **A denial is its own event**, with its own status and a reason code separating a guardrail block
  from a refused approval.

## Risk is declared

Risk is a property of a registered tool, not a judgment made at the moment of use. Every MCP tool
declares one; chat-only surfaces are covered by a static table beside the registry.

- **A completeness test** covers every tool name offerable in chat, so a new tool cannot ship
  unclassified.
- **An unknown name escalates.** A dynamically activated tenant skill is not statically enumerable;
  an unresolvable name maps to `external` and routes through a human decision instead of defaulting
  to safe or being hard-denied.

## Guardrails, both sides of dispatch

**Pre-dispatch** guardrails can block a call before it runs. The shipped rule refuses a call that
would send personal data to a third-party channel - a messenger notification, a proxied external
tool.

**Post-dispatch** guardrails redact arguments and output on the way to the audit trail, so the record
of an action is not itself a store of personal data.

Two pipelines, not one: sending data outward and recording it internally are different risks with
different verdicts.

## Resolved configuration

An admin-gated endpoint returns the state a live turn for a given agent would resolve to: model, tool
scope, prompt hash - after provider configuration, per-agent overrides, domain scoping and prompt
assembly have been applied.

Where an agent builds a curated tool list instead of taking the domain-filtered registry scope, the
response carries an explicit caveat field marking the part of the list it does not cover.

## Related

- [The decision trace](https://aipril.dev/docs/harness/traces.md) - what the pipeline writes when it is done.
- [Budgets and metering](https://aipril.dev/docs/harness/budgets.md) - the money side of the same transport.
- [Tools and MCP](https://aipril.dev/docs/harness/tools-and-mcp.md) - what the gate is gating, and under whose authority
  a tool runs.
- [Built for a multi-tenant platform](https://aipril.dev/docs/harness/built-for-a-platform.md) - why these defaults are
  set the way they are.
