Guides

Govern an Existing Agent with a Policy

Simulate, review, publish, and bind a fail-closed payment policy to an Agent that already exists in the KLA Control Plane.

11 min read2448 words

This guide adds a reviewed policy to an Agent that already exists in the KLA Control Plane. The policy allows payments.create calls of EUR 10,000 or less, routes larger payments to the Decision Desk, and blocks every payment request that matches neither rule. The published version is scoped to this one Agent, so other production Agents in the tenant are unaffected.

An Agent-scoped draft can be submitted for review only after the Agent carries a governed environment, and an Agent receives a governed environment only through a deployed policy binding. The sequence therefore has two policy versions: version 1.0.0 is reviewed and bound to give the Agent its environment and is never published; version 1.1.0 names the Agent in its scope, is published, and is bound as the enforced policy.

Every command is copy-ready. The commands create records in the selected tenant, so use a tenant you control.

Before you start

You need:

  • the kla CLI built from the repository as described in the installation guide;
  • jq;
  • an existing Agent whose payment tool is named payments.create and receives a numeric amount and a string currency;
  • a maker access token for a member who can create, simulate, and publish policies and configure and deploy Agents;
  • a checker access token for a different member who holds the kla:approver role, which reviews policies, decides Decision Requests, and approves Agent Releases.

The CLI reads KLA_ACCESS_TOKEN. The maker token stays exported for the whole guide. Each checker command receives the checker token for that one command. Keep both tokens out of files and command arguments.

export KLA_API_URL='https://api.dev.kla.digital'
export KLA_TENANT_ID='<tenant external ID>'
export KLA_ACCESS_TOKEN='<maker short-lived access token>'
export KLA_CHECKER_ACCESS_TOKEN='<checker short-lived access token>'

KLA_REPO_ROOT="$(git rev-parse --show-toplevel)"
kla() { node "$KLA_REPO_ROOT/cli/dist/index.js" "$@"; }

kla auth status > maker-auth.json
KLA_ACCESS_TOKEN="$KLA_CHECKER_ACCESS_TOKEN" kla auth status > checker-auth.json
jq -e '.token.subject | type == "string"' maker-auth.json
jq -e '.token.subject | type == "string"' checker-auth.json
jq -en --slurpfile maker maker-auth.json --slurpfile checker checker-auth.json \
  '$maker[0].token.subject != $checker[0].token.subject'
jq -e '.token.roles | index("kla:approver") != null' checker-auth.json

kla api tenants getSettings | jq -e '.governance.publishApprovalMode == "two_person"'

kla auth status prints the non-secret claims of each token. The two subjects must differ: the API rejects a review or approval by the identity that made the request. The tenant publication mode must be two_person; in single_actor mode the publish step records its own approval and Step 7 has no Decision Request to decide.

Step 1: Identify the Agent

Search the Agent Registry, copy the UUID from the result, and save the Agent record. The Agent must have no pending draft: a policy binding is merged into an existing draft, and the checker would then approve unrelated changes together with the binding.

kla agent list --search 'payment'
export AGENT_UUID='<agent UUID>'
kla agent get "$AGENT_UUID" > agent-before.json
jq -e --arg agent "$AGENT_UUID" '.id == $agent' agent-before.json

kla agent release history "$AGENT_UUID" > agent-history-before.json
jq -e '[.versions[] | select(.is_draft == true)] | length == 0' agent-history-before.json

If a pending draft exists, its manifest is in that history output. Have the checker approve or the maker discard it before continuing.

The policy below names the tool payments.create and the arguments amount and currency. When your Agent uses another tool or argument name, change those values in the policy and in both Simulation inputs.

Step 2: Write the fail-closed policy

Save this file as policy.template.json. Version 1.0.0 is scoped to the production governed environment and is the bootstrap version. Step 6 narrows the scope to the Agent for version 1.1.0.

{
  "schemaVersion": "1.0.0",
  "policyId": "existing-agent-payment-controls",
  "workspaceId": "TENANT_EXTERNAL_ID",
  "name": "Existing agent payment controls",
  "description": "Allows EUR payments of 10,000 or less and routes larger EUR payments to review.",
  "status": "draft",
  "version": "1.0.0",
  "scope": {
    "environments": ["production"]
  },
  "defaultDecision": "block",
  "rules": [
    {
      "ruleId": "allow-managed-execution-gate",
      "name": "Allow the managed Process execution gate",
      "interceptionPoint": "input",
      "when": {
        "expression": {
          "==": [{ "var": "context.action.actionType" }, "policy_gate"]
        }
      },
      "then": {
        "decision": "allow",
        "reason": "The execution may start; each payment tool call is evaluated separately.",
        "reasonCodes": ["execution_gate_allowed"]
      },
      "execution": { "mode": "deterministic" },
      "evidence": {
        "evaluatedFields": ["context.action.actionType"],
        "artifactRefs": []
      }
    },
    {
      "ruleId": "allow-payment-up-to-10000",
      "name": "Allow positive payment up to EUR 10,000",
      "interceptionPoint": "tool_call",
      "when": {
        "expression": {
          "and": [
            { "in": [{ "var": "context.action.actionType" }, ["tool_input", "tool_call"]] },
            { "==": [{ "var": "context.action.toolName" }, "payments.create"] },
            { "==": [{ "var": "context.action.toolArgs.currency" }, "EUR"] },
            { ">": [{ "var": "context.action.toolArgs.amount" }, 0] },
            { "<=": [{ "var": "context.action.toolArgs.amount" }, 10000] }
          ]
        }
      },
      "then": {
        "decision": "allow",
        "reason": "Payment is within the approved automatic limit.",
        "reasonCodes": ["payment_within_automatic_limit"]
      },
      "execution": { "mode": "deterministic" },
      "evidence": {
        "evaluatedFields": [
          "context.action.actionType",
          "context.action.toolName",
          "context.action.toolArgs.currency",
          "context.action.toolArgs.amount"
        ],
        "artifactRefs": []
      }
    },
    {
      "ruleId": "review-payment-over-10000",
      "name": "Review payment over EUR 10,000",
      "interceptionPoint": "tool_call",
      "when": {
        "expression": {
          "and": [
            { "in": [{ "var": "context.action.actionType" }, ["tool_input", "tool_call"]] },
            { "==": [{ "var": "context.action.toolName" }, "payments.create"] },
            { "==": [{ "var": "context.action.toolArgs.currency" }, "EUR"] },
            { ">": [{ "var": "context.action.toolArgs.amount" }, 10000] }
          ]
        }
      },
      "then": {
        "decision": "require_approval",
        "reason": "Payment exceeds the approved automatic limit.",
        "reasonCodes": ["payment_over_automatic_limit"],
        "approverGroup": "kla:approver",
        "remediation": {
          "summary": "Obtain a payment approval before execution.",
          "steps": []
        }
      },
      "execution": { "mode": "deterministic" },
      "evidence": {
        "evaluatedFields": [
          "context.action.actionType",
          "context.action.toolName",
          "context.action.toolArgs.currency",
          "context.action.toolArgs.amount"
        ],
        "artifactRefs": []
      }
    }
  ]
}

The first rule allows the execution-level gate. A bound policy becomes the first policy_gate of the Agent's managed Process, and that gate is evaluated with actionType policy_gate and the policy ID as toolName; without this rule the fail-closed default would stop every run before any tool call. The two payment rules apply to tool-call requests, whose actionType is tool_input or tool_call, and are mutually exclusive. A missing, zero, or negative amount, another currency, or another tool matches neither rule and resolves to the defaultDecision of block. The > 0 guard matters: a missing amount evaluates as 0 in rule expressions and would otherwise satisfy <= 10000. approverGroup becomes the required role of the Decision Request, so it names the configured kla:approver role. Policy lint rejects a defaultDecision of allow with POLICY_DEFAULT_DECISION_FAIL_OPEN.

Insert the tenant external ID and create the bootstrap draft:

jq --arg workspace "$KLA_TENANT_ID" '.workspaceId = $workspace' \
  policy.template.json > policy.json

export POLICY_ID='existing-agent-payment-controls'
export BOOTSTRAP_VERSION='1.0.0'
export POLICY_VERSION='1.1.0'

kla policy registry create --file policy.json
kla policy registry get "$POLICY_ID" --version "$BOOTSTRAP_VERSION" \
  | jq -e '.versions[0].status == "draft"'

Step 3: Run two durable Simulations

A Simulation sends a GateContext, the request envelope the KLA Policy Engine evaluates, to the draft. The Policy Registry stores each completed Simulation with an evidence reference, and review readiness requires a stored Simulation of the exact draft content.

Save the low-value GateContext as simulate-500.template.json:

{
  "context": {
    "schemaVersion": "1.0.0",
    "occurredAt": "2026-09-06T12:00:00Z",
    "identifiers": {
      "workspaceId": "TENANT_EXTERNAL_ID",
      "environment": "production"
    },
    "agentId": "AGENT_UUID",
    "action": {
      "actionType": "tool_input",
      "toolName": "payments.create",
      "toolArgs": { "amount": 500, "currency": "EUR" },
      "environment": "production"
    }
  }
}

Save the high-value GateContext as simulate-15000.template.json:

{
  "context": {
    "schemaVersion": "1.0.0",
    "occurredAt": "2026-09-06T12:00:00Z",
    "identifiers": {
      "workspaceId": "TENANT_EXTERNAL_ID",
      "environment": "production"
    },
    "agentId": "AGENT_UUID",
    "action": {
      "actionType": "tool_input",
      "toolName": "payments.create",
      "toolArgs": { "amount": 15000, "currency": "EUR" },
      "environment": "production"
    }
  }
}

Insert the tenant and Agent identifiers, then run both Simulations. kla policy registry simulate sends the Idempotency-Key header the route requires.

for amount in 500 15000; do
  jq --arg workspace "$KLA_TENANT_ID" --arg agent "$AGENT_UUID" \
    '.context.identifiers.workspaceId = $workspace | .context.agentId = $agent' \
    "simulate-${amount}.template.json" > "simulate-${amount}.json"
done

kla policy registry simulate "$POLICY_ID" "$BOOTSTRAP_VERSION" \
  --input @simulate-500.json > simulation-500.json
jq -e '
  .decision.decision == "allow" and
  .decision.primaryBasis.ruleId == "allow-payment-up-to-10000" and
  (.simulation.simulationId | type == "string") and
  (.simulation.evidenceId | type == "string")
' simulation-500.json

kla policy registry simulate "$POLICY_ID" "$BOOTSTRAP_VERSION" \
  --input @simulate-15000.json > simulation-15000.json
jq -e '
  .decision.decision == "require_approval" and
  .decision.primaryBasis.ruleId == "review-payment-over-10000" and
  .decision.approval.assigneeGroup == "kla:approver" and
  (.simulation.simulationId | type == "string") and
  (.simulation.evidenceId | type == "string")
' simulation-15000.json

Both checks must pass before the draft is submitted. Any later edit to the draft changes its content hash and requires a new Simulation.

Step 4: Submit and approve the bootstrap version

The maker submits version 1.0.0 and the checker approves it. The version stays approved and is never published, so it never takes effect for any gate.

kla policy registry submit-review "$POLICY_ID" "$BOOTSTRAP_VERSION" \
  --comment 'Threshold outcomes and fail-closed default verified by Simulation.'

KLA_ACCESS_TOKEN="$KLA_CHECKER_ACCESS_TOKEN" \
  kla policy registry review "$POLICY_ID" "$BOOTSTRAP_VERSION" \
  --decision approve \
  --comment 'Rule scope, outcomes, and Simulation evidence reviewed.' \
  --yes

kla policy registry get "$POLICY_ID" --version "$BOOTSTRAP_VERSION" \
  | jq -e '.versions[0].status == "approved"'

A 409 response with readiness lists the blocking check. required_simulation_missing means the stored Simulation does not match the current draft content; rerun Step 3.

Step 5: Give the Agent its governed environment

kla agent policy bind accepts an approved, published, or active policy. It writes the binding into a new Agent draft, creates the managed Process for the governed environment, and returns the draft UUID. The checker approves the draft, which publishes it as an immutable Release, and the maker deploys the Release. agent release deploy requires a production binding and moves the Agent's live manifest, which is where the governed environment is read from during policy review.

kla agent policy bind "$AGENT_UUID" \
  --policy "$POLICY_ID" \
  --environment production \
  --description 'Bind the reviewed payment control to establish the production environment.' \
  > agent-binding-bootstrap.json

jq -e --arg agent "$AGENT_UUID" '.success == true and .agentId == $agent' \
  agent-binding-bootstrap.json
export BOOTSTRAP_DRAFT_UUID="$(jq -er '.draftId' agent-binding-bootstrap.json)"

KLA_ACCESS_TOKEN="$KLA_CHECKER_ACCESS_TOKEN" \
  kla agent release approve "$BOOTSTRAP_DRAFT_UUID" \
  --notes 'Bootstrap binding for the production governed environment reviewed.' \
  --yes | jq -e '.success == true'

kla agent release deploy "$BOOTSTRAP_DRAFT_UUID" --yes > agent-rollout-bootstrap.json

kla agent get "$AGENT_UUID" \
  | jq -e '.manifest.governedExecution.environment == "production"'

From this point until Step 8 completes, the Agent's managed Process references an unpublished policy version and its runs fail closed.

Step 6: Create, simulate, and review the Agent-scoped version

Version 1.1.0 has the same rules and names the Agent in scope.agentIds. The Simulation inputs from Step 3 are reused.

jq --arg workspace "$KLA_TENANT_ID" --arg agent "$AGENT_UUID" --arg version "$POLICY_VERSION" \
  '.workspaceId = $workspace | .version = $version | .scope.agentIds = [$agent]' \
  policy.template.json > policy-agent-scoped.json

kla policy registry create --file policy-agent-scoped.json

kla policy registry simulate "$POLICY_ID" "$POLICY_VERSION" \
  --input @simulate-500.json | jq -e '.decision.decision == "allow"'
kla policy registry simulate "$POLICY_ID" "$POLICY_VERSION" \
  --input @simulate-15000.json | jq -e '.decision.decision == "require_approval"'

kla policy registry submit-review "$POLICY_ID" "$POLICY_VERSION" \
  --comment 'Agent-scoped version; outcomes verified by Simulation.'

KLA_ACCESS_TOKEN="$KLA_CHECKER_ACCESS_TOKEN" \
  kla policy registry review "$POLICY_ID" "$POLICY_VERSION" \
  --decision approve \
  --comment 'Agent scope, outcomes, and Simulation evidence reviewed.' \
  --yes

kla policy registry get "$POLICY_ID" --version "$POLICY_VERSION" \
  | jq -e '.versions[0].status == "approved"'

The review-readiness check resolves every Agent in scope.agentIds in the Agent Registry and requires each one to carry a governed environment that matches scope.environments. Step 5 satisfied that for this Agent.

Step 7: Publish

The maker requests publication. In two_person mode the request returns pendingApproval: true and a Decision Request identifier.

export KLA_EFFECTIVE_FROM="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

kla policy registry publish "$POLICY_ID" "$POLICY_VERSION" \
  --effective-from "$KLA_EFFECTIVE_FROM" \
  --reason 'Activate the reviewed payment control.' \
  --yes > publication-request.json

jq -e '.success == true and .pendingApproval == true and .status == "PENDING"' \
  publication-request.json
export APPROVAL_UUID="$(jq -er '.approvalId' publication-request.json)"

The checker decides the Decision Request through approvals.decide:

jq -n --arg approvalId "$APPROVAL_UUID" '{
  approvalId: $approvalId,
  decision: "approve",
  decisionAcknowledged: true,
  reason: "Reviewed policy version approved for publication."
}' > approval-decision.json

KLA_ACCESS_TOKEN="$KLA_CHECKER_ACCESS_TOKEN" \
  kla api approvals decide --input @approval-decision.json > publication-decision.json
jq -e '.decision == "approve" and .status == "APPROVED"' publication-decision.json

Publication runs after the decision is recorded. Wait for the version to reach published or active, then read the compiled policy pack.

for attempt in $(seq 1 30); do
  kla policy registry get "$POLICY_ID" --version "$POLICY_VERSION" > published-policy.json
  jq -e '.versions[0].status == "published" or .versions[0].status == "active"' \
    published-policy.json > /dev/null && break
  sleep 2
done
jq -e '.versions[0].status == "published" or .versions[0].status == "active"' \
  published-policy.json

kla policy registry compiled "$POLICY_ID" "$POLICY_VERSION" > compiled-policy.json
jq -e --arg policy "$POLICY_ID" --arg version "$POLICY_VERSION" '
  .integrity.policyId == $policy and
  .integrity.policyVersion == $version and
  .integrity.manifestHashVerified == true
' compiled-policy.json

integrity.verification.status is PASS when the tenant signs policy packs and SKIPPED when signing is off in that environment. Stop if the version stays unpublished or manifestHashVerified is false.

Step 8: Bind the published version

The binding selects the active or published version of the policy, so the same bind command now pins version 1.1.0 and updates the managed Process. The checker approves the new draft and the maker deploys it.

kla agent policy bind "$AGENT_UUID" \
  --policy "$POLICY_ID" \
  --environment production \
  --description 'Bind the published Agent-scoped payment control.' \
  > agent-binding.json

jq -e --arg agent "$AGENT_UUID" '.success == true and .agentId == $agent' agent-binding.json
export DRAFT_UUID="$(jq -er '.draftId' agent-binding.json)"

kla agent release history "$AGENT_UUID" > agent-history-draft.json
jq -e --arg release "$DRAFT_UUID" --arg policy "$POLICY_ID" --arg version "$POLICY_VERSION" '
  any(.versions[];
    .id == $release and
    .is_draft == true and
    .manifest.governedExecution.policyId == $policy and
    .manifest.governedExecution.policyVersion == $version)
' agent-history-draft.json

KLA_ACCESS_TOKEN="$KLA_CHECKER_ACCESS_TOKEN" \
  kla agent release approve "$DRAFT_UUID" \
  --notes 'Published policy binding and managed Process reviewed.' \
  --yes | jq -e '.success == true'

kla agent release deploy "$DRAFT_UUID" --yes > agent-rollout.json

The history check shows the checker that the draft carries the version 1.1.0 binding before approval.

Step 9: Verify

Read the Agent and its Release history.

kla agent get "$AGENT_UUID" > agent-after.json
jq -e --arg policy "$POLICY_ID" --arg version "$POLICY_VERSION" '
  .manifest.governedExecution.policyId == $policy and
  .manifest.governedExecution.policyVersion == $version and
  .manifest.governedExecution.environment == "production" and
  (.manifest.governedExecution.workflowId | type == "string") and
  any(.passport.activePolicies[]; .policyId == $policy and .version == $version)
' agent-after.json

kla agent release history "$AGENT_UUID" > agent-history.json
jq -e --arg release "$DRAFT_UUID" '
  any(.versions[];
    .id == $release and
    .is_draft == false and
    .is_current_version == true and
    .approved_at != null and
    .deployed_at != null)
' agent-history.json

Run both Simulations against the published version and keep the results with the change record.

kla policy registry simulate "$POLICY_ID" "$POLICY_VERSION" \
  --input @simulate-500.json | jq -e '.decision.decision == "allow"'

kla policy registry simulate "$POLICY_ID" "$POLICY_VERSION" \
  --input @simulate-15000.json | jq -e '.decision.decision == "require_approval"'

You now have a published, Agent-scoped policy whose Simulations resolve EUR 500 to allow and EUR 15,000 to require_approval, and a deployed Agent Release that binds it. A live payments.create call above EUR 10,000 from this Agent opens a Decision Request on the Decision Desk for the kla:approver role. The Govern an Agent End-to-End guide covers the execution, approval, and evidence export that follow.

Govern an Existing Agent with a Policy | Developer Docs | KLA Control Plane