Guides

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.

10 min read2323 words

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/*.

Govern an Agent End-to-End | Developer Docs | KLA Control Plane