Skip to content

Approval

Confirm rules, approval ports and stores. Concepts: Approval, Approval stores.

Approval (HITL): confirm rules, approval ports, the approval store and approval memory.

ApprovalCallback = Callable[[ApprovalRequest], ApprovalDecision | bool | Awaitable[ApprovalDecision | bool]] module-attribute

ApprovalStatus = Literal['pending', 'approved', 'denied'] module-attribute

ConfirmRule = bool | Callable[[ToolCall, ToolContext], bool | Awaitable[bool]] module-attribute

AcceptedRule dataclass

A rule a person accepted: commit code (or config) and pass confirm as confirm=.

ApprovalContext

Bases: BaseModel

What reviewers decided before on requests like this one: advice, never a decision.

summary()

One line for an approval card, such as "3 similar: 2 approved, 1 denied (by ann)".

view()

The context as JSON for a page or an API, with its summary line.

ApprovalPort

Bases: Protocol

Decides whether a tool call may run; may raise ApprovalPending.

ApprovalRecord

Bases: BaseModel

One approval request, its status, its decision and whether a resume used it.

args_digest binds the decision to the requested kind, tool and arguments. It is always computed from request (stores may persist and index it); a given value that disagrees raises ToolUsageError, so a tampered row never validates.

model_copy(*, update=None, deep=False)

A copy; changing request or args_digest re-validates, so the digest stays bound.

ApprovalStore

Bases: Protocol

Where deferred approvals wait; swap the memory store for a database in production.

create and consume must be atomic: at most one unconsumed record per (run_id, call_id), and each decision is consumed exactly once.

Condition

Bases: BaseModel

One argument bounded to the evidence: == one value, in a set, between lo..hi.

describe()

The condition as text; names and values are quoted, so they cannot break a line.

holds(arguments)

True only when the field is present, of a seen type, and inside the bounds.

DynamoDBApprovalStore

ApprovalStore on one DynamoDB table (aioboto3 or aiobotocore), shared by every host.

client_factory() returns an async DynamoDB client, or the context manager that aioboto3.Session().client("dynamodb") returns; one is made per event loop. create is one transaction of two conditional puts (the record and the call's open marker); decide and consume compare-and-set the stored record, so each decision is consumed once. Every read that decides is strongly consistent; list reads a GSI (eventually consistent). Items carry expires_at ttl_s seconds after creation (None: never); enable DynamoDB TTL on it. Expired items read as absent. Backend errors propagate.

Raises:

Type Description
ConfigError

no callable client_factory, an empty table, or ttl_s not a finite number > 0 or None.

aclose() async

Close the running loop's client.

consume(approval_id) async

Mark a decided record used; True only for the caller that flipped it.

Raises:

Type Description
ApprovalNotFound

unknown or expired id.

create(request) async

Store a pending record (empty id / created_at filled in), or return the open one.

Raises:

Type Description
ToolUsageError

another call's record already has this approval id, or the record holds a value JSON cannot carry.

decide(approval_id, decision) async

Record the decision; only the first decide of a record succeeds.

Raises:

Type Description
ApprovalNotFound

unknown or expired id.

ApprovalAlreadyDecided

the record is no longer pending.

ToolUsageError

edited_args is not plain JSON.

find(run_id, call_id) async

The unconsumed record of the call, or None.

get(approval_id) async

The record. Raises: ApprovalNotFound (unknown or expired).

list(*, run_id=None, status=None, limit=100) async

Records newest first, filtered by run and status (expired ones are skipped).

Raises:

Type Description
ConfigError

unknown status or a negative limit.

table_definition(table) staticmethod

CreateTable arguments for the store's table (then enable TTL on expires_at).

PreparedApproval

Bases: Protocol

A port that can find-or-create a record without consuming it (DeferredApproval).

ProposedRule

Bases: BaseModel

Data only: skip asking for tool when every condition holds; inert until accept.

describe()

One line: the rule and its evidence.

RuleConfirm dataclass

A confirm= callable: False (no ask) only for tool, every field covered and in bounds, before review_by; True (ask) for anything else.

RuleEvidence

Bases: BaseModel

The decisions a proposal rests on, and those against it.

SQLAlchemyApprovalStore

ApprovalStore on a SQLAlchemy async engine (agent-adapter[sqlalchemy]), every host.

engine is a URL ("postgresql+asyncpg://...") or a callable returning an AsyncEngine; one engine is made per event loop (LoopLocal). create is INSERT ... ON CONFLICT DO NOTHING against a partial unique index on the call's unconsumed row; decide and consume are one UPDATE ... WHERE ... RETURNING each. metadata holds the table, for Alembic. Backend errors propagate (fail closed).

Raises:

Type Description
ConfigError

a bad engine or table; on first use, a dialect other than Postgres or SQLite, or a table without the store's columns or its partial unique index on the call's unconsumed row.

MissingDependency

SQLAlchemy is not installed.

aclose() async

Dispose of the running loop's engine.

consume(approval_id) async

Mark a decided record used; True only for the caller that flipped it.

Raises:

Type Description
ApprovalNotFound

unknown id.

create(request) async

Store a pending record (empty id / created_at filled in), or return the open one.

Raises:

Type Description
ToolUsageError

another call's record already has this approval id, or the record holds a value JSON cannot carry.

decide(approval_id, decision) async

Record the decision; only the first decide of a record succeeds.

Raises:

Type Description
ApprovalNotFound

unknown id.

ApprovalAlreadyDecided

the record is no longer pending.

ToolUsageError

edited_args is not plain JSON.

find(run_id, call_id) async

The unconsumed record of the call, or None.

get(approval_id) async

The record. Raises: ApprovalNotFound.

list(*, run_id=None, status=None, limit=100) async

Records newest first, filtered by run and status.

Raises:

Type Description
ConfigError

unknown status or a negative limit.

prune(*, older_than_s) async

Delete records created more than older_than_s seconds ago; the count deleted.

Raises:

Type Description
ConfigError

older_than_s is not a number >= 0.

SimilarDecision

Bases: BaseModel

One earlier decision on a similar request; matched names equal fields (nested: a.*).

accept(rule, *, accepted_by)

A person's explicit acceptance: config and code to commit, and the confirm= callable.

The rule stops skipping at accepted_at + review_days; after that every call asks again.

Raises:

Type Description
ConfigError

not a ProposedRule, or accepted_by empty, too long or not printable.

approval_context(request, records, *, min_similarity=DEFAULT_MIN_SIMILARITY, shown=DEFAULT_SHOWN)

Earlier decisions on the same tool with similar non-secret arguments, best first.

Only reads the records: the request still waits for its own decision.

Raises:

Type Description
ConfigError

min_similarity outside 0..1 or shown below 0.

ask_each(port, requests, *, vet=None) async

The port's decision for each request, in order; vet sees each before any is used.

A PreparedApproval is asked in two phases: every record is prepared; while any is undecided, ApprovalPending is raised for the first one and nothing is consumed; then vet runs, and exactly the prepared records are consumed (a record consumed or reset meanwhile by someone else denies its call). Other ports get request per call, in order. Build the requests identically on every pass.

Raises:

Type Description
ApprovalPending

the first undecided call, after every call was asked.

Exception

whatever vet raises, before any decision is consumed.

confirm_from_config(config)

The confirm= callable of a committed rule config (AcceptedRule.config).

Raises:

Type Description
ConfigError

other keys, a bad condition, a secret-named field or a bad review_by.

evaluate_confirm(rule, call, ctx) async

Whether call needs approval under a ToolSpec.confirm rule; the one implementation.

A bool rule is returned as is; a callable gets (call, ctx) and may be sync or async.

Raises:

Type Description
ToolUsageError

the rule is not a bool or callable, or returned a non-bool.

Exception

whatever the rule raised: logged without the arguments, never retried. ApprovalPending and LangGraph's GraphBubbleUp pass through without a log.

propose_rules(records, *, min_count=5, min_deciders=2, max_denied=0, review_days=90.0, tools=None)

Rules the approvals support: one per tool and argument shape, bounded to the seen values.

Only tool approvals decided as-is count as evidence. A shape whose fields cannot all be bounded (a nested value, or a text field with over MAX_CHOICES values) gets no rule.

Raises:

Type Description
ConfigError

min_count < 2, min_deciders < 1, max_denied < 0 or review_days <= 0.

request_digest(request)

SHA-256 hex of the canonical JSON of a request's kind, tool and original arguments.

Canonical form: sorted keys, (",", ":") separators, ensure_ascii=False, encoded UTF-8 with surrogatepass (lone surrogates stay distinct); JSON values only.

Raises:

Type Description
ToolUsageError

an argument is not plain JSON (bytes, tuple, set, other objects, non-string keys, NaN or infinity) or nests deeper than MAX_JSON_DEPTH; approvals fail closed on those.

Also exported here, documented under the canonical path: ApprovalDecision, ApprovalRequest, ApprovalStoreCorrupt, AutoApprove, AutoDeny, CallbackApproval, Decision, DeferredApproval, MemoryApprovalStore, RedisApprovalStore, SQLiteApprovalStore.