Human Approval for AI Agents
Human approval — human-in-the-loop (HITL) — is the mechanism that inserts a person into the execution path of an AI system before a sensitive action happens.
The critical distinction:
Human approval is a runtime state transition, not "ask a human in the prompt".
A prompt instruction ("ask the user before paying") is advice the model may or may not follow. A runtime approval gate suspends execution until an authorized person decides, and refuses to proceed otherwise. The system cannot proceed past the gate by improvising.
Why a State Transition, Not a Prompt Instruction
When an approval is implemented as a prompt instruction:
- The model may forget, misinterpret, or decide the user "already approved" based on context.
- There is no proof a human ever saw the request.
- There is no way to bind the approval to the exact call being authorized.
When approval is a runtime gate, the execution is physically suspended at the tool boundary, an approval challenge is created, and only a validated authorization token resumes it. The model has no path around the gate — it is not a participant in the decision.
The Approval Lifecycle
TramAI's approval lifecycle is governed by ApprovalStatus:
| State | Meaning |
|---|---|
PENDING | Created, awaiting a human decision |
APPROVED | Authorized by a decider |
DENIED | Rejected by a decider |
TIMED_OUT | Expired without a decision |
COMPLETED | The tool executed after approval |
The flow:
- Suspend — a tool execution triggers
RequireApproval. The engine creates an approval with a cryptographically boundApprovalBinding, persists the continuation, and throwsApprovalSuspendedExceptioncarrying the challenge. - Wait — the approval sits in
PENDINGuntil a human acts or it expires. - Decide — an external authorizer presents the approval token. The engine re-validates the full binding, compares the presented token digest using constant-time comparison, and atomically transitions the approval to
APPROVEDorDENIED. - Resume — on
APPROVED, the continuation is claimed, the original tool arguments are replayed from the sensitive replay envelope, and the tool executes. - Complete — the execution finishes and the approval moves to
COMPLETED.
Every transition emits audit events: suspended, resumed, completed, and uncertain outcome when authorization succeeded but tool execution failed.
Cryptographic Binding
An approval is not a free-floating "yes". The ApprovalBinding ties the approval token to the exact execution context:
- workflow run ID
- tool name
- SHA-256 digest of the tool arguments
- policy version
- workflow digest
- approval token digest (the raw token is never stored)
This means an approval granted for one call cannot be replayed against a different call, different arguments, or a different policy version. The token itself is a 256-bit secure random value, stored only as a digest, and validated with constant-time comparison against timing attacks.
Example: Configuration
val coordinator = DefaultApprovalGateCoordinator(
store = InMemoryApprovalStore(),
approvalIdGenerator = UuidApprovalIdGenerator,
approvalTokenGenerator = SecureRandomApprovalTokenGenerator,
approvalTokenDigester = Sha256ApprovalTokenDigester,
decisionValidator = AllowAnyApprovalDecisionValidator,
maxApprovalTtl = Duration.ofMinutes(15),
)
val tramai = SovereignTramai.builder()
.profile(profile)
.modelRegistry(registry)
.auditStore(auditStore)
.provider(ollamaProvider, name = "ollama", default = true)
.model("llama3.2", "ollama")
.approvalGateCoordinator(coordinator)
.approvalContinuationStore(continuationStore)
.suspendedInvocationStore(invocationStore)
.toolArgumentsDigester(Sha256ToolArgumentsDigester)
.build()
When a governed tool execution hits the approval gate, the engine throws ApprovalSuspendedException with the ApprovalChallenge (approval ID + raw token + expiry). The application surfaces that challenge to the authorizer.
Example: Resume
val command = AuthorizeResumeCommand(
approvalId = challenge.approvalId,
expectedVersion = 0L,
presentedToken = challenge.token,
consumedBy = "authorizer-user",
workflowRunId = "run-123",
toolName = "myTool",
argumentsDigest = Sha256ToolArgumentsDigester.digest(args),
policyVersion = "1.0",
workflowDigest = Sha256Digest.of("sha256:abc..."),
)
val result = runtime.resumeApprovalTyped<AssessmentResult>(command)
When to Use Human Approval
Use approval gates when:
- A tool execution can cause irreversible or high-impact side effects (payments, deletions, external sends).
- Policy requires demonstrable human oversight — the organization must prove a person authorized the action.
- You need audit events for every step of the approval lifecycle.
- You need to suspend and resume across process boundaries (durable continuation).
Do not use approval gates for:
- Rate limiting or throttling — use the policy engine's
Denyor application-level limits. - Authorization that does not require suspension — a plain policy decision is enough.
- Every single tool call — approvals add latency; gate the risky minority.
Limitations (v1)
- No nested approval — a tool execution inside an already-suspended workflow cannot trigger another approval gate (blocked explicitly in v1).
- No built-in UI — approval tokens are presented programmatically. A preview reviewer UI exists in the sovereign ops surfaces but is disabled by default; a full approval inbox is roadmap direction.
- Expiry cleanup — expired approvals remain in the store until swept.
- 15-minute maximum TTL —
DefaultApprovalGateCoordinatorcaps approval TTL (configurable viamaxApprovalTtl).
Related Documentation
- Approval Workflows — full implementation reference
- Tool governance — REQUIRE_APPROVAL at the tool boundary
- Runtime governance — where approval sits in the enforcement chain
- AI agent governance — the category pillar
- EU AI Act technical controls — human oversight and evidence
- Sovereign Mode — composed runtime with approval wiring
- TramAI on GitHub
