Approvals¶
zeroth.governance.approvals ¶
Governed approval workflows.
This package re-exports the key models, the database repository, and the
high-level service so callers can simply
from zeroth.governance.approvals import ....
ApprovalDecision ¶
Bases: StrEnum
The possible choices a human reviewer can make on an approval request.
APPROVE — accept as-is. REJECT — deny the action. EDIT_AND_APPROVE — modify the proposed payload and then accept it.
ApprovalRecord ¶
Bases: BaseModel
The main approval object that tracks a single approval request.
Created when an agent workflow hits a node that requires human sign-off. Contains everything a reviewer needs: a summary, rationale, the proposed payload, which actions are allowed, and (once answered) the resolution.
ApprovalResolution ¶
Bases: BaseModel
Holds the details of how an approval was resolved.
Stores who made the decision, what they decided, any edits they made to the payload, and when the decision happened.
ApprovalStatus ¶
Bases: StrEnum
Whether an approval request is still waiting or has been answered.
PENDING means no decision yet; RESOLVED means a human has responded.
HumanInteractionType ¶
Bases: StrEnum
The kind of interaction the system is requesting from a human.
For example, "approval" means the system needs a yes/no decision, while "clarification" means it needs more information to proceed.
ApprovalRepository ¶
Saves and loads approval records from an async database.
Use this when you need to persist approval requests so they survive restarts, or when you need to look up pending approvals by run, thread, or deployment.
scoped_for_deployment
classmethod
¶
scoped_for_deployment(
database: AsyncDatabase,
scope_context: ScopeContext | NullWorkspaceScopeContext,
deployment_ref: str,
) -> ApprovalRepository
Bind scheduled SLA reads to one deployment owner.
write
async
¶
Save an approval record to the database.
If a record with the same approval_id already exists, it will be updated. Returns the freshly-read record from the database.
get
async
¶
get(
approval_id: str,
*,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
deployment_ref: str | None = None,
graph_version_ref: str | None = None,
) -> ApprovalRecord | None
Look up one approval with every supplied scope predicate in SQL.
resolve_pending
async
¶
resolve_pending(
record: ApprovalRecord,
*,
require_sla_deadline: bool = False,
) -> ApprovalRecord | None
Atomically publish record only while its exact scoped row is pending.
require_sla_deadline adds sla_deadline IS NOT NULL to the
compare-and-set. Callers that CHANGE the stored status are already fenced
by the status = PENDING predicate below: the loser of a race sees the
winner's new status and matches zero rows. The alert-escalation latch is
the exception -- it deliberately leaves the row PENDING and moves only the
deadline -- so for it the status predicate is satisfied by every racer and
provides no fence at all. Keying on the column the latch actually clears
restores the one-winner property for that caller without changing the
predicate for anybody else.
list_pending
async
¶
list_pending(
*,
run_id: str | None = None,
thread_id: str | None = None,
deployment_ref: str | None = None,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
graph_version_ref: str | None = None,
) -> list[ApprovalRecord]
Return all approval records that are still waiting for a decision.
You can optionally filter by run_id, thread_id, or deployment_ref. Results are sorted by creation time.
list
async
¶
list(
*,
run_id: str | None = None,
thread_id: str | None = None,
deployment_ref: str | None = None,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
graph_version_ref: str | None = None,
) -> list[ApprovalRecord]
Return approval records, optionally filtered by run, thread, or deployment.
list_overdue
async
¶
Return overdue approvals for this repository's deployment owner.
ApprovalContinuation ¶
Bases: Protocol
The slice of the run orchestrator the approval workflow drives.
Structural, so the runtime orchestrator satisfies it without the approvals domain importing the orchestrator implementation.
record_approval_resolution
async
¶
record_approval_resolution(
*,
graph: Graph,
run: Run,
node: HumanApprovalNode,
output_payload: Mapping[str, Any],
approval_record: ApprovalRecord,
) -> Run
Record the result of a human approval decision on the run.
resume_graph
async
¶
Resume the paused run from its current node onward.
ApprovalService ¶
ApprovalService(
*,
repository: ApprovalRepository,
run_repository: RunRepository,
audit_repository: AuditRepository | None = None,
payload_sanitizer: PayloadSanitizer | None = None,
)
The main entry point for working with approvals.
Handles the full lifecycle: creating a pending approval when a workflow pauses, letting a human resolve it (approve / reject / edit-and-approve), and then resuming the run with the decision applied.
create_pending
async
¶
create_pending(
*,
run: Run,
node: HumanApprovalNode,
input_payload: dict[str, Any],
) -> ApprovalRecord
Create a new approval request and save it to the database.
Called when a workflow reaches a node that needs human sign-off. Builds the approval record from the run and node details, sanitizes sensitive data out of the payload, and persists it as "pending".
get
async
¶
get(
approval_id: str,
*,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
deployment_ref: str | None = None,
graph_version_ref: str | None = None,
) -> ApprovalRecord | None
Fetch a single approval record by its ID. Returns None if not found.
list_pending
async
¶
list_pending(
*,
run_id: str | None = None,
thread_id: str | None = None,
deployment_ref: str | None = None,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
graph_version_ref: str | None = None,
) -> list[ApprovalRecord]
Return all approvals that are still waiting for a human decision.
Optionally filter by run_id, thread_id, or deployment_ref.
list
async
¶
list(
*,
run_id: str | None = None,
thread_id: str | None = None,
deployment_ref: str | None = None,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
graph_version_ref: str | None = None,
) -> list[ApprovalRecord]
Return approval records for a run, thread, or deployment.
get_visible_to_deployment
async
¶
get_visible_to_deployment(
approval_id: str,
*,
deployment_ref: str,
graph_version_ref: str,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
) -> ApprovalRecord | None
Return an approval owned by this deployment or one of its child runs.
Child records keep their real child deployment/graph provenance. The deployment becomes an authorized view only when the scoped run chain reaches an ancestor with this exact deployment and graph version.
list_pending_visible_to_deployment
async
¶
list_pending_visible_to_deployment(
*,
deployment_ref: str,
graph_version_ref: str,
run_id: str | None = None,
thread_id: str | None = None,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
) -> list[ApprovalRecord]
List pending approvals whose scoped ancestry reaches a deployment.
list_visible_to_deployment
async
¶
list_visible_to_deployment(
*,
deployment_ref: str,
graph_version_ref: str,
run_id: str | None = None,
thread_id: str | None = None,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
) -> list[ApprovalRecord]
List all approval evidence visible through scoped run ancestry.
visible_ancestor_run
async
¶
visible_ancestor_run(
record: ApprovalRecord,
*,
deployment_ref: str,
graph_version_ref: str,
) -> Run | None
Find the exact in-scope ancestor served by a deployment.
The walk is bounded and cycle-detecting. Every row comes through the
deployment-bound RunRepository, so a forged parent id cannot cross
tenant/workspace scope even when the global run id is guessed.
schedule_ancestor_continuation
async
¶
schedule_ancestor_continuation(
approval_id: str,
*,
deployment_ref: str,
graph_version_ref: str,
) -> Run
Atomically notify the deployment worker about a resolved child gate.
The child stays paused under its own provenance. The root deployment's run is re-queued with a signed linkage record in the same database transaction; its subgraph executor later resumes only the exact child named by the durable pause metadata.
reconcile_ancestor_continuations
async
¶
Repair resolved-child notifications after a process interruption.
The resolved approval is itself the durable reconciliation source. A
deployment worker scans only records whose scoped ancestry reaches its
exact active graph, and only requeues ancestors still waiting on that
child. The transactional CAS and deterministic audit id in
schedule_ancestor_continuation make concurrent workers safe.
escalate
async
¶
escalate(
approval_id: str,
*,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
deployment_ref: str | None = None,
graph_version_ref: str | None = None,
) -> ApprovalRecord
Escalate an overdue approval based on its configured escalation action.
Supports three actions: - delegate: marks original ESCALATED, creates new approval for delegate - auto_reject: resolves original as REJECTED with system actor, then continues the run so the rejection actually fails it - alert (default): keeps the original PENDING so a human can still resolve it, but latches it out of the SLA sweep (nulls sla_deadline and marks urgency_metadata['escalated']). Flipping it to ESCALATED would hide it from list/get and make the run unresolvable forever.
If the approval is no longer pending, this is a no-op. That covers both double-escalation and, critically, a RESOLVED approval: SLA enforcement must not re-open a decision a human already made.
resolve
async
¶
resolve(
approval_id: str,
*,
decision: ApprovalDecision,
actor: ActorIdentity,
edited_payload: dict[str, Any] | None = None,
reason: str | None = None,
tenant_id: str | None = None,
workspace_id: str | None | object = _UNSCOPED,
deployment_ref: str | None = None,
graph_version_ref: str | None = None,
) -> ApprovalRecord
Record a decision and return its durable approval record.
schedule_continuation
async
¶
Prepare a resolved approval for durable worker pick-up.
Instead of driving the orchestrator inline (which conflicts with the worker-ownership model), this method: 1. Prepares the run state (as continue_run would before calling the orchestrator). 2. Transitions the run to PENDING so the worker's poll loop will claim it. 3. Clears the lease so any worker can pick it up.
The worker will call resume_graph on the next poll tick.
Only call this from the approval HTTP endpoint when the durable worker is active.
continue_run
async
¶
Resume a paused workflow run after an approval has been resolved.
If the decision was REJECT, the run is marked as failed immediately. If APPROVE or EDIT_AND_APPROVE, the run is handed back to the orchestrator to continue executing from the approval node onward.