Govern an Agent End-to-End
Register an agent, work a human approval, inspect its trace, and export sealed evidence through the KLA Control Plane.
This guide uses a claims-triage agent that can recommend a refund. The flow covers agent registration, a governed execution, human approval, trace inspection, and evidence export.
The examples use the KLA CLI for generated control-plane procedures and curl for evidence REST routes. The CLI reads KLA_ACCESS_TOKEN. The API host and tenant are explicit so the same commands can run against a deployed dev tenant.
export KLA_API_URL='https://api.dev.kla.digital'
export KLA_ACCESS_TOKEN='<short-lived access token>'
export KLA_TENANT_ID='<tenant external ID>'
# Build the repository CLI while the package is private, then expose it in this shell.
corepack enable
pnpm install --frozen-lockfile
pnpm kla --help
KLA_REPO_ROOT="$(git rev-parse --show-toplevel)"
kla() { KLA_CLI_CWD="$PWD" pnpm --dir "$KLA_REPO_ROOT" --silent kla "$@"; }
kla auth test
kla api doctor
KLA_TENANT_ID configures the CLI context's x-kla-tenant-id routing hint. The verified tenant claim remains authoritative. A request that selects another tenant uses x-kla-tenant-external-id; the caller must hold an active membership in that tenant.
This walkthrough assumes a tenant with an approved policy and a deployed workflow that can create a pending approval. Registration, policy binding, release approval, deployment, and execution are tenant writes. Use a tenant you control and a second principal for maker-checker approval.
sequenceDiagram participant Dev as Developer participant Agent as Claims agent participant KLA as KLA Control Plane participant Desk as Decision Desk participant Room as Evidence Room Dev->>KLA: Register with agents.create Agent->>KLA: Start executions.execute KLA->>Desk: approvals.getPending returns a pending request Desk->>KLA: approvals.decide approves the request Dev->>KLA: Read traces.getTrace and traces.verify Dev->>Room: POST /v1/evidence/export
Step 1: Register the agent
agents.create is a tRPC mutation. The generated CLI command sends it through the authenticated /v1 tRPC transport.
- Endpoint:
POST /v1/agents.create
Create a registration manifest in the format accepted by the agent registry:
cat > /tmp/claims-triage-registration.yaml <<'YAML'
name: Claims Triage Agent
flowId: claims_triage
description: Triages inbound claims and prepares refund recommendations for review.
model:
provider: openai
name: gpt-4o
pipeline:
- type: llm_generate
name: triage_claim
config:
template: Review the claim and recommend the next action with supporting facts.
owners:
- email: [email protected]
tools:
- lookup_claim
- process_refund
humanOversightRequired: true
YAML
Register it:
AGENT_JSON=$(kla agent create --file /tmp/claims-triage-registration.yaml)
echo "$AGENT_JSON"
AGENT_ID=$(jq -r '.id // .agentId' <<<"$AGENT_JSON")
Keep the returned agent identifier. A newly registered agent needs an approved, deployed policy binding before executions.execute can start a governed run. The CLI lifecycle is:
POLICY_ID='<approved policy ID>'
kla agent policy bind "$AGENT_ID" \
--policy "$POLICY_ID" \
--environment sandbox \
--description 'Attach the refund approval policy.'
The sandbox bind creates or updates the agent's pending sandbox draft and its managed workflow. It does not make a production release deployable. agents.deployVersion requires the immutable release manifest to carry a production governedExecution binding.
Create that explicit production release manifest. A new UUID gives the managed production workflow its own identity; the first workflow version is 1:
WORKFLOW_ID="$(node -e "console.log(require('node:crypto').randomUUID())")"
cp /tmp/claims-triage-registration.yaml /tmp/claims-triage.yaml
cat >> /tmp/claims-triage.yaml <<YAML
governedExecution:
policyId: ${POLICY_ID}
workflowId: ${WORKFLOW_ID}
workflowVersion: 1
environment: production
YAML
RELEASE_JSON=$(kla agent release create "$AGENT_ID" \
--manifest @/tmp/claims-triage.yaml \
--description 'Publish the claims triage release.' \
--justification 'The release adds the governed refund workflow.' \
--change-type minor)
echo "$RELEASE_JSON"
VERSION_ID=$(jq -r '.version.id // .versionId // .id' <<<"$RELEASE_JSON")
The release command accepts @/tmp/claims-triage.yaml as a structured file input. The API validates the UUID, workflow version, policy ID, and production environment, then persists the managed workflow binding before creating the immutable release.
The reviewer credential must identify a different authenticated principal. The server applies the tenant's role and two-person rules to every lifecycle command.
Use the same API URL and tenant with a short-lived reviewer token scoped to the approval command:
export KLA_REVIEWER_ACCESS_TOKEN='<reviewer short-lived access token>'
KLA_ACCESS_TOKEN="$KLA_REVIEWER_ACCESS_TOKEN" \
kla agent release approve "$VERSION_ID" --yes
After the reviewer approves the release, restore the maker credential before deploying it:
export KLA_ACCESS_TOKEN='<maker short-lived access token>'
kla agent release deploy "$VERSION_ID" --yes
Step 2: Start a governed execution
Run the agent in the sandbox with a claim that should reach the approval policy:
RUN_JSON=$(kla agent run "$AGENT_ID" \
--environment sandbox \
--input '{"claim_id":"clm_9921","requested_refund":1250.00}')
echo "$RUN_JSON"
EXECUTION_ID=$(jq -r '.executionId' <<<"$RUN_JSON")
kla api executions get --input "{\"id\":\"$EXECUTION_ID\"}"
executions.execute returns an executionId. A policy outcome of require_approval leaves the execution waiting for a reviewer. The execution status and event history are available through executions.get and executions.getEvents.
Step 3: Find and approve the pending request
approvals.getPending lists the tenant's pending Decision Requests. The execution filter returns the request for this run:
- Endpoint:
GET /v1/approvals.getPending
# A 202 PENDING result queues approval creation asynchronously. Retry the lookup until the request
# is visible. Rate limits, 5xx responses, and network failures stay retryable for 60 seconds.
APPROVAL_ID=''
PENDING_JSON='{}'
POLL_DEADLINE=$((SECONDS + 60))
while (( SECONDS < POLL_DEADLINE )); do
PENDING_ATTEMPT_STATUS=0
PENDING_ATTEMPT_STDERR_FILE="$(mktemp)"
PENDING_ATTEMPT_OUTPUT=$(kla api approvals getPending \
--input "{\"executionId\":\"$EXECUTION_ID\",\"limit\":20}" 2>"$PENDING_ATTEMPT_STDERR_FILE") || \
PENDING_ATTEMPT_STATUS=$?
PENDING_ATTEMPT_ERROR="$(cat "$PENDING_ATTEMPT_STDERR_FILE")"
rm -f "$PENDING_ATTEMPT_STDERR_FILE"
if [[ -n "$PENDING_ATTEMPT_ERROR" ]]; then
printf '%s\n' "$PENDING_ATTEMPT_ERROR" >&2
fi
if (( PENDING_ATTEMPT_STATUS != 0 )); then
if printf '%s\n' "$PENDING_ATTEMPT_ERROR" | grep -Eqi \
'(^|[^0-9])(429|5[0-9][0-9])([^0-9]|$)|network|timed out|timeout|ECONNRESET|ECONNREFUSED|ENOTFOUND|fetch failed|socket hang up|temporarily unavailable'; then
echo 'Transient approvals.getPending failure; retrying until the 60-second deadline.' >&2
PENDING_JSON="$PENDING_ATTEMPT_OUTPUT"
sleep 2
continue
fi
echo 'approvals.getPending returned a non-retryable authorization or schema error.' >&2
if [[ -n "$PENDING_ATTEMPT_OUTPUT" ]]; then
printf '%s\n' "$PENDING_ATTEMPT_OUTPUT" >&2
fi
exit 1
fi
PENDING_JSON="$PENDING_ATTEMPT_OUTPUT"
if ! APPROVAL_ID=$(jq -r '.approvals[0].approvalId // empty' <<<"$PENDING_JSON"); then
echo 'approvals.getPending returned invalid JSON.' >&2
exit 1
fi
if [[ -n "$APPROVAL_ID" ]]; then
break
fi
sleep 2
done
if [[ -z "$APPROVAL_ID" ]]; then
echo "No pending approval appeared for $EXECUTION_ID within 60 seconds." >&2
echo "$PENDING_JSON" >&2
exit 1
fi
echo "$PENDING_JSON"
The reviewer submits approvals.decide with the approval identifier, a decision, and a reason. The decision input accepts approve, reject, or escalate. This example uses the second reviewer credential:
- Endpoint:
POST /v1/approvals.decide
cat > /tmp/approval-decision.json <<JSON
{
"approvalId": "${APPROVAL_ID}",
"decision": "approve",
"decisionAcknowledged": true,
"reason": "Claim documentation reviewed and refund authorized."
}
JSON
KLA_ACCESS_TOKEN="$KLA_REVIEWER_ACCESS_TOKEN" \
kla api approvals decide --input @/tmp/approval-decision.json
# The decision can return before the worker and pending ledger finish. Keep polling the execution
# until it reaches a terminal state. A 202/PENDING response remains expected during this window.
export KLA_ACCESS_TOKEN='<maker short-lived access token>'
EXECUTION_JSON='{}'
EXECUTION_STATUS=''
EXECUTION_STATUS_LOWER=''
EXECUTION_DEADLINE=$((SECONDS + 120))
while (( SECONDS < EXECUTION_DEADLINE )); do
EXECUTION_ATTEMPT_STATUS=0
EXECUTION_ATTEMPT_STDERR_FILE="$(mktemp)"
EXECUTION_ATTEMPT_OUTPUT=$(kla api executions get \
--input "{\"id\":\"$EXECUTION_ID\"}" 2>"$EXECUTION_ATTEMPT_STDERR_FILE") || \
EXECUTION_ATTEMPT_STATUS=$?
EXECUTION_ATTEMPT_ERROR="$(cat "$EXECUTION_ATTEMPT_STDERR_FILE")"
rm -f "$EXECUTION_ATTEMPT_STDERR_FILE"
if [[ -n "$EXECUTION_ATTEMPT_ERROR" ]]; then
printf '%s\n' "$EXECUTION_ATTEMPT_ERROR" >&2
fi
if (( EXECUTION_ATTEMPT_STATUS != 0 )); then
if printf '%s\n' "$EXECUTION_ATTEMPT_ERROR" | grep -Eqi \
'(^|[^0-9])(202|429|5[0-9][0-9])([^0-9]|$)|PENDING|network|timed out|timeout|ECONNRESET|ECONNREFUSED|ENOTFOUND|fetch failed|socket hang up|temporarily unavailable'; then
echo 'Transient execution status response; retrying until the 120-second deadline.' >&2
sleep 2
continue
fi
echo 'executions.get failed while waiting for the approved run.' >&2
if [[ -n "$EXECUTION_ATTEMPT_OUTPUT" ]]; then
printf '%s\n' "$EXECUTION_ATTEMPT_OUTPUT" >&2
fi
exit 1
fi
EXECUTION_JSON="$EXECUTION_ATTEMPT_OUTPUT"
if ! EXECUTION_STATUS=$(jq -r '.status // empty' <<<"$EXECUTION_JSON"); then
echo 'executions.get returned invalid JSON.' >&2
exit 1
fi
EXECUTION_STATUS_LOWER=$(printf '%s' "$EXECUTION_STATUS" | tr '[:upper:]' '[:lower:]')
case "$EXECUTION_STATUS_LOWER" in
completed|failed|blocked|cancelled|timeout|stale)
break
;;
pending|running|gated|waiting_for_input)
sleep 2
;;
'')
echo 'executions.get returned no status.' >&2
echo "$EXECUTION_JSON" >&2
exit 1
;;
*)
echo "executions.get returned an unknown status: $EXECUTION_STATUS" >&2
echo "$EXECUTION_JSON" >&2
exit 1
;;
esac
done
if [[ ! "$EXECUTION_STATUS_LOWER" =~ ^(completed|failed|blocked|cancelled|timeout|stale)$ ]]; then
echo "Execution $EXECUTION_ID did not reach a terminal state within 120 seconds." >&2
echo "$EXECUTION_JSON" >&2
exit 1
fi
if [[ "$EXECUTION_STATUS_LOWER" != 'completed' ]]; then
echo "Execution $EXECUTION_ID ended with status $EXECUTION_STATUS; trace and evidence checks require completed status." >&2
echo "$EXECUTION_JSON" >&2
exit 1
fi
echo "$EXECUTION_JSON"
The execution resumes according to the deployed workflow. A rejection leaves the consequential action unexecuted.
Step 4: Inspect and verify the trace
The worker can finish the execution before the audit ledger projection is visible. Read the event history, wait for the approval audit entry, and use the returned identifiers for the trace and proof calls.
- Endpoint:
GET /v1/traces.getTrace - Endpoint:
GET /v1/traces.verify - Endpoint:
GET /v1/audit.listByResource
EVENTS_OUTPUT=$(kla api executions getEvents \
--input "{\"id\":\"$EXECUTION_ID\",\"limit\":200}")
if ! jq -e 'type == "object" and (.events | type == "array")' <<<"$EVENTS_OUTPUT" >/dev/null; then
echo 'executions.getEvents returned invalid JSON or no event list.' >&2
echo "$EVENTS_OUTPUT" >&2
exit 1
fi
EVENTS_JSON="$EVENTS_OUTPUT"
TRACE_ID=$(jq -r --argjson events "$EVENTS_JSON" '
[
.traceId,
.trace_id,
($events.events[]? | .traceId, .trace_id, .metadata?.traceId, .metadata?.trace_id, .data?.traceId, .data?.trace_id)
]
| map(select(type == "string" and length > 0))
| first // empty
' <<<"$EXECUTION_JSON")
if [[ -z "$TRACE_ID" ]]; then
echo "No trace identifier was returned for completed execution $EXECUTION_ID." >&2
echo "$EVENTS_JSON" >&2
exit 1
fi
# Approval decisions and requests receive audit IDs asynchronously. Poll both the approval detail
# and audit projection so the proof call uses a materialized ID from this run.
AUDIT_ID=''
APPROVAL_DETAIL_JSON='{}'
AUDIT_JSON='{}'
LEDGER_DEADLINE=$((SECONDS + 60))
while (( SECONDS < LEDGER_DEADLINE )); do
DETAIL_ATTEMPT_STATUS=0
DETAIL_ATTEMPT_STDERR_FILE="$(mktemp)"
DETAIL_ATTEMPT_OUTPUT=$(kla api approvals getDetail \
--input "{\"approvalId\":\"$APPROVAL_ID\"}" 2>"$DETAIL_ATTEMPT_STDERR_FILE") || \
DETAIL_ATTEMPT_STATUS=$?
DETAIL_ATTEMPT_ERROR="$(cat "$DETAIL_ATTEMPT_STDERR_FILE")"
rm -f "$DETAIL_ATTEMPT_STDERR_FILE"
if [[ -n "$DETAIL_ATTEMPT_ERROR" ]]; then
printf '%s\n' "$DETAIL_ATTEMPT_ERROR" >&2
fi
if (( DETAIL_ATTEMPT_STATUS != 0 )); then
if printf '%s\n' "$DETAIL_ATTEMPT_ERROR" | grep -Eqi \
'(^|[^0-9])(429|5[0-9][0-9])([^0-9]|$)|network|timed out|timeout|ECONNRESET|ECONNREFUSED|ENOTFOUND|fetch failed|socket hang up|temporarily unavailable'; then
echo 'Transient approval detail response; retrying ledger lookup.' >&2
sleep 2
continue
fi
echo 'approvals.getDetail failed while waiting for its audit identifier.' >&2
if [[ -n "$DETAIL_ATTEMPT_OUTPUT" ]]; then
printf '%s\n' "$DETAIL_ATTEMPT_OUTPUT" >&2
fi
exit 1
fi
APPROVAL_DETAIL_JSON="$DETAIL_ATTEMPT_OUTPUT"
if ! jq -e 'type == "object"' <<<"$APPROVAL_DETAIL_JSON" >/dev/null; then
echo 'approvals.getDetail returned invalid JSON.' >&2
echo "$APPROVAL_DETAIL_JSON" >&2
exit 1
fi
AUDIT_ID=$(jq -r '
[
.decisionReceipt.auditEventId,
.metadata.decisionReceipt.auditEventId,
.metadata.requestAuditEventId
]
| map(select(type == "string" and length > 0))
| first // empty
' <<<"$APPROVAL_DETAIL_JSON")
if [[ -n "$AUDIT_ID" ]]; then
break
fi
AUDIT_ATTEMPT_STATUS=0
AUDIT_ATTEMPT_STDERR_FILE="$(mktemp)"
AUDIT_ATTEMPT_OUTPUT=$(kla api audit listByResource \
--input "{\"resource\":\"approval\",\"resourceId\":\"$APPROVAL_ID\",\"limit\":100,\"includeTotal\":true}" 2>"$AUDIT_ATTEMPT_STDERR_FILE") || \
AUDIT_ATTEMPT_STATUS=$?
AUDIT_ATTEMPT_ERROR="$(cat "$AUDIT_ATTEMPT_STDERR_FILE")"
rm -f "$AUDIT_ATTEMPT_STDERR_FILE"
if [[ -n "$AUDIT_ATTEMPT_ERROR" ]]; then
printf '%s\n' "$AUDIT_ATTEMPT_ERROR" >&2
fi
if (( AUDIT_ATTEMPT_STATUS != 0 )); then
if printf '%s\n' "$AUDIT_ATTEMPT_ERROR" | grep -Eqi \
'(^|[^0-9])(429|5[0-9][0-9])([^0-9]|$)|network|timed out|timeout|ECONNRESET|ECONNREFUSED|ENOTFOUND|fetch failed|socket hang up|temporarily unavailable'; then
echo 'Transient audit projection response; retrying ledger lookup.' >&2
sleep 2
continue
fi
echo 'audit.listByResource failed while waiting for its audit identifier.' >&2
if [[ -n "$AUDIT_ATTEMPT_OUTPUT" ]]; then
printf '%s\n' "$AUDIT_ATTEMPT_OUTPUT" >&2
fi
exit 1
fi
AUDIT_JSON="$AUDIT_ATTEMPT_OUTPUT"
if ! jq -e 'type == "object" and (.entries | type == "array")' <<<"$AUDIT_JSON" >/dev/null; then
echo 'audit.listByResource returned invalid JSON.' >&2
echo "$AUDIT_JSON" >&2
exit 1
fi
AUDIT_ID=$(jq -r --arg approval_id "$APPROVAL_ID" '
[
.entries[]?
| select(
(.resourceId == $approval_id) or
(.resource_id == $approval_id) or
(.details?.approvalId == $approval_id) or
(.details?.approval_id == $approval_id)
)
| (.id // .eventId // .event_id)
]
| map(select(type == "string" and length > 0))
| first // empty
' <<<"$AUDIT_JSON")
if [[ -n "$AUDIT_ID" ]]; then
break
fi
sleep 2
done
if [[ -z "$AUDIT_ID" ]]; then
echo "No materialized audit identifier appeared for approval $APPROVAL_ID within 60 seconds." >&2
echo "$APPROVAL_DETAIL_JSON" >&2
echo "$AUDIT_JSON" >&2
exit 1
fi
RESULT_JSON='{}'
MERKLE_ROOT=''
RESULT_DEADLINE=$((SECONDS + 60))
while (( SECONDS < RESULT_DEADLINE )); do
RESULT_ATTEMPT_STATUS=0
RESULT_ATTEMPT_STDERR_FILE="$(mktemp)"
RESULT_ATTEMPT_OUTPUT=$(kla api executions result \
--input "{\"id\":\"$EXECUTION_ID\"}" 2>"$RESULT_ATTEMPT_STDERR_FILE") || \
RESULT_ATTEMPT_STATUS=$?
RESULT_ATTEMPT_ERROR="$(cat "$RESULT_ATTEMPT_STDERR_FILE")"
rm -f "$RESULT_ATTEMPT_STDERR_FILE"
if [[ -n "$RESULT_ATTEMPT_ERROR" ]]; then
printf '%s\n' "$RESULT_ATTEMPT_ERROR" >&2
fi
if (( RESULT_ATTEMPT_STATUS != 0 )); then
if printf '%s\n' "$RESULT_ATTEMPT_ERROR" | grep -Eqi \
'(^|[^0-9])(202|429|5[0-9][0-9])([^0-9]|$)|PENDING|network|timed out|timeout|ECONNRESET|ECONNREFUSED|ENOTFOUND|fetch failed|socket hang up|temporarily unavailable'; then
echo 'Transient execution result response; retrying evidence materialization.' >&2
sleep 2
continue
fi
echo 'executions.result failed while waiting for the evidence anchor.' >&2
if [[ -n "$RESULT_ATTEMPT_OUTPUT" ]]; then
printf '%s\n' "$RESULT_ATTEMPT_OUTPUT" >&2
fi
exit 1
fi
RESULT_JSON="$RESULT_ATTEMPT_OUTPUT"
if ! MERKLE_ROOT=$(jq -r '.evidence.merkleRoot // empty' <<<"$RESULT_JSON"); then
echo 'executions.result returned invalid JSON.' >&2
echo "$RESULT_JSON" >&2
exit 1
fi
if [[ -n "$MERKLE_ROOT" ]]; then
break
fi
sleep 2
done
if [[ -z "$MERKLE_ROOT" ]]; then
echo "The evidence anchor for completed execution $EXECUTION_ID was not materialized within 60 seconds." >&2
echo "$RESULT_JSON" >&2
exit 1
fi
TRACE_INPUT=$(jq -cn \
--arg trace_id "$TRACE_ID" \
--arg tenant_id "$KLA_TENANT_ID" \
'{traceId: $trace_id, tenantId: $tenant_id, includeSensitiveData: false}')
kla api traces getTrace --input "$TRACE_INPUT"
PROOF_INPUT=$(jq -cn --arg tenant "$KLA_TENANT_ID" --arg audit "$AUDIT_ID" \
'{tenant: $tenant, auditId: $audit}')
PROOF_JSON=$(kla api traces verify --input "$PROOF_INPUT")
PROOF_VALID=$(jq -r '.valid // false' <<<"$PROOF_JSON")
PROOF_NONCE=$(jq -r '.nonce // empty' <<<"$PROOF_JSON")
PROOF_TX_ID=$(jq -r '.ledgerAttestation.txId // empty' <<<"$PROOF_JSON")
if [[ "$PROOF_VALID" != 'true' || -z "$PROOF_NONCE" ]]; then
echo "traces.verify did not validate audit $AUDIT_ID." >&2
echo "$PROOF_JSON" >&2
exit 1
fi
echo "$PROOF_JSON"
echo "Trace: $TRACE_ID"
echo "Audit: $AUDIT_ID"
echo "Evidence Merkle root: $MERKLE_ROOT"
echo "Proof nonce: $PROOF_NONCE"
echo "Proof ledger transaction: ${PROOF_TX_ID:-unavailable}"
traces.getTrace reads the spans for the returned trace and keeps sensitive data masked by default. traces.verify generates and checks a proof for the returned audit identifier. The proof response includes a nonce and, when the ledger attestation is available, its transaction identifier. Both calls need the corresponding trace permissions.
Step 5: Export sealed evidence
The API's evidence export is a REST route. Export by execution identifier, then read the manifest and download the verified archive:
- Endpoint:
POST /v1/evidence/export
EXPORT_JSON=$(curl -sS -X POST "$KLA_API_URL/v1/evidence/export" \
-H "Authorization: Bearer $KLA_ACCESS_TOKEN" \
-H "x-kla-tenant-external-id: $KLA_TENANT_ID" \
-H 'Content-Type: application/json' \
-d "{\"runId\":\"$EXECUTION_ID\",\"format\":\"json\",\"includeProofs\":true}")
echo "$EXPORT_JSON"
EXPORT_ID=$(jq -r '.exportId' <<<"$EXPORT_JSON")
The response includes an exportId, a manifest, and a download URL. The API exposes the manifest and archive through these routes:
- Endpoint:
GET /v1/evidence/exports/$EXPORT_ID - Endpoint:
GET /v1/evidence/exports/$EXPORT_ID/download
curl -sS "$KLA_API_URL/v1/evidence/exports/$EXPORT_ID" \
-H "Authorization: Bearer $KLA_ACCESS_TOKEN" \
-H "x-kla-tenant-external-id: $KLA_TENANT_ID" | jq
curl -sS -f "$KLA_API_URL/v1/evidence/exports/$EXPORT_ID/download" \
-H "Authorization: Bearer $KLA_ACCESS_TOKEN" \
-H "x-kla-tenant-external-id: $KLA_TENANT_ID" \
-o "./evidence-$EXPORT_ID.zip"
The CLI wraps the same two routes:
kla evidence export --days 7 --out ./evidence
kla evidence verify --bundle "./evidence-$EXPORT_ID.zip" --out ./evidence-report
kla evidence verify works on the downloaded bundle and makes no network request. Its report records the bundle specification, manifest digest, Merkle root, and verification result.
The completed loop is: register with agents.create, start executions.execute, resolve the pending request through approvals.getPending and approvals.decide, inspect and verify with traces.*, and export through /v1/evidence/*.
