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
¶
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
¶
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 |
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
|
|
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 |
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 |
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
|
|
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 |
prune(*, older_than_s)
async
¶
Delete records created more than older_than_s seconds ago; the count deleted.
Raises:
| Type | Description |
|---|---|
ConfigError
|
|
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 |
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
|
|
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 |
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 |
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.
|
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
|
|
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 |
Also exported here, documented under the canonical path: ApprovalDecision, ApprovalRequest, ApprovalStoreCorrupt, AutoApprove, AutoDeny, CallbackApproval, Decision, DeferredApproval, MemoryApprovalStore, RedisApprovalStore, SQLiteApprovalStore.