v2.3 · June 2026

Kynara User Guide

Everything you need to register agents, define access policies, govern approvals, and audit every decision made by your AI agents — in real time.

Overview

Kynara is an AI agent permission control plane. It sits between your AI agents and the resources they act on, evaluating every tool call against your organisation's policies before allowing it to proceed.

🛡️ Policy enforcement

RBAC + ABAC rules evaluated in priority order. First matching decision wins; the default is deny.

📋 Tamper-evident audit

Every allow, deny, and approval is SHA-256 hash-chained. Any tampering is mathematically detectable.

⚡ Sub-millisecond decisions

In-process SDK cache and optional Go sidecar for local evaluation at <1ms latency.

👥 Human-in-the-loop

Policies can require approval before sensitive actions execute. Agents pause and wait.

🔑 JIT grants

Time-bound break-glass permission elevations with justification and ticket link.

🔬 Policy replay

Simulate a proposed policy change against 30 days of real decisions before deploying.

Architecture at a glance

Your agent sends a decision request to Kynara describing the action, resource, and principal. Kynara evaluates your policies (including any active JIT grants), returns allow, deny, or require_approval, and appends an immutable record to the audit log.

Agent (Python / TypeScript / AutoGen / CrewAI / OpenAI / Anthropic...)
  │  SDK wraps tool call
  ▼
kynara.check(subject, action, resource, context)
  │
  ├─ Option A: HTTPS → Central API  (p95 <8ms)
  └─ Option B: HTTP  → Go Sidecar   (p95 <1ms) ──batches telemetry──▶ Central API
                           ↓
             Policy engine evaluates: hard gates → JIT grants → RBAC → ABAC
                           ↓
          { effect: "allow" | "deny" | "require_approval" }
                           ↓
                   Audit event appended (hash-chained)

Quick start

Get from zero to your first policy decision in under five minutes.

  1. Create your account at kynara.ai. The free plan includes 3 seats and 10,000 policy decisions per month.

  2. Register your first agent. Navigate to Agents → New agent. Give it a display name, choose a supervision mode, and set a daily action budget. Copy the Agent ID — you'll need it in your code.

  3. Create an API key. Go to Settings → API Keys → New API Key. Copy it immediately — it is shown only once.

  4. Create a policy. Navigate to Policies → New policy. For example: effect allow, actions file.read, resources file. Test it in the integrated Simulator panel.

  5. Bind the policy to your agent via the Bindings tab.

  6. Call the decision API from your agent:

# Python SDK
pip install kynara-sdk

from kynara_sdk import Kynara, permission_required
from kynara_sdk.context import set_current_kynara

set_current_kynara(Kynara.from_env())  # reads KYNARA_BASE_URL, KYNARA_API_KEY, KYNARA_AGENT_ID

@permission_required("file.read", resource_arg="file_id", resource_type="file")
def read_file(file_id: str):
    return open(file_id).read()
# Or call the REST API directly
curl -X POST https://kynara.ai/api/v1/decisions/check \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "subject_type": "agent",
    "subject_id": "<agent-uuid>",
    "action": "file.read",
    "resource": { "type": "file", "id": "reports/q4.csv", "attrs": {} },
    "context": { "user_id": "u_123" }
  }'
💡
No policy = deny. If no policy matches a request, Kynara returns deny by default. Start with broad allow policies and narrow them down — not the other way around.

Example 1 — CRM Assistant

A CRM assistant agent that reads contacts and deals, creates notes, and requires human approval before writing or modifying contact records outside business hours.

🗺️
Setup order: Scope Catalog → Role → Agent → Policy → Bind policy to agent. Always register your scopes first so they appear in the role editor's scope picker.

Step 1 — Register scopes in the Scope Catalog

Navigate to Scope Catalog → New Tool and create four entries:

NamespaceNameScope stringRiskDescription
crmcontacts.readcrm:contacts.readlowRead a CRM contact by ID or email.
crmcontacts.writecrm:contacts.writemediumCreate or update a CRM contact record.
crmdeals.readcrm:deals.readlowRead deal pipeline data.
crmnotes.createcrm:notes.createlowAppend a note to a contact or deal.

For each entry, the Input Schema describes what parameters the agent passes at decision time. Example for crm:contacts.write:

{
  "type": "object",
  "properties": {
    "contact_id": {
      "type": "string",
      "description": "UUID of the contact being modified"
    },
    "fields_changed": {
      "type": "array",
      "items": { "type": "string" },
      "description": "List of field names being updated, e.g. [\"email\", \"phone\"]"
    },
    "is_bulk_operation": {
      "type": "boolean",
      "description": "True if this update affects more than one contact"
    }
  },
  "required": ["contact_id"]
}

These properties map to resource.attrs in the decision request. Your policy conditions can reference them as ctx.resource.attrs.is_bulk_operation. See the Input Schema reference for a full field breakdown.

Step 2 — Create a Role

Navigate to Roles → New Role.

FieldValue
NameCRM Agent — Read + Notes
DescriptionAllows the CRM assistant to read contacts/deals and create notes. Write operations are excluded — add them via a separate role only when required.
Scopescrm:contacts.read, crm:deals.read, crm:notes.create

Use the scope picker to search and select each scope from the catalog. Do not add crm:contacts.write here — that scope will be governed by a policy condition instead of being freely granted.

Step 3 — Create the Agent

Navigate to Agents → New agent.

FieldValue
Display nameCRM Assistant
Slugcrm-assistant
Supervision modehuman_supervised
Daily action budget500

After creating the agent, open its detail page and go to the Roles tab → Add role → select CRM Agent — Read + Notes.

Step 4 — Create a Policy

Navigate to Policies → New policy. This policy requires approval for contact writes outside business hours.

FieldValue
Display nameCRM write — require approval off-hours
Effectrequire_approval
Priority100
Scopescrm:contacts.write
Resource typescrm.contact
ConditionMatches requests outside 09:00–18:00 (see below)
{
  "op": "not",
  "args": [
    { "op": "time_between", "args": ["ctx.context.time", "09:00", "18:00"] }
  ]
}

Then create a second policy that allows writes during business hours:

display_name: "CRM write — allow during business hours"
effect:       allow
priority:     200
scopes:       crm:contacts.write
condition:    { "op": "time_between", "args": ["ctx.context.time", "09:00", "18:00"] }

Bind both policies to the agent from the Bindings tab on each policy page (subject selector: agent:<agent-uuid>).

Step 5 — Test with a permission check

POST /api/v1/decisions/check
{
  "subject_type": "agent",
  "subject_id":   "<crm-assistant-uuid>",
  "action":       "crm:contacts.write",
  "resource": {
    "type": "crm.contact",
    "id":   "contact_8812",
    "attrs": {
      "contact_id":      "contact_8812",
      "fields_changed":  ["email", "phone"],
      "is_bulk_operation": false
    }
  },
  "context": {
    "time":       "22:30",          // Outside business hours → require_approval
    "ip":         "203.0.113.42",
    "ip_country": "US",
    "user_id":    "u_sales_rep_007",
    "session_id": "sess_abc123",
    "request_id": "req_xyz456"
  }
}

// Response:
{
  "effect":            "require_approval",
  "matched_policy_id": "<policy-uuid>",
  "reason":            "policy: CRM write — require approval off-hours",
  "approval_id":       "<approval-uuid>"
}

Try the same request with "time": "10:00" and it returns allow.

Example 2 — Infra Manager

An infrastructure automation agent that can read logs freely, but requires human approval before restarting services or allocating disk — and is blocked entirely outside the EU/US geofence.

Step 1 — Register scopes in the Scope Catalog

NamespaceNameScope stringRiskDescription
infralogs.readinfra:logs.readlowStream or query server logs.
infrarestartinfra:restarthighRestart a named service or container.
infradisk.allocateinfra:disk.allocatehighResize or allocate a disk volume.

Input schema for infra:restart:

{
  "type": "object",
  "properties": {
    "service_name": {
      "type": "string",
      "description": "Name of the service or container to restart, e.g. \"api-server\""
    },
    "environment": {
      "type": "string",
      "enum": ["production", "staging", "development"],
      "description": "Target environment. Policies can deny production restarts unconditionally."
    },
    "reason": {
      "type": "string",
      "description": "Human-readable justification for the restart (shown to approvers)"
    }
  },
  "required": ["service_name", "environment"]
}

Input schema for infra:disk.allocate:

{
  "type": "object",
  "properties": {
    "volume_id": {
      "type": "string",
      "description": "Cloud provider volume ID, e.g. \"vol-0a1b2c3d\""
    },
    "size_gb": {
      "type": "number",
      "description": "Requested size in gigabytes. Policies can cap at a maximum."
    },
    "region": {
      "type": "string",
      "description": "Cloud region, e.g. \"us-east-1\". Used in geo-restriction conditions."
    }
  },
  "required": ["volume_id", "size_gb"]
}

Step 2 — Create a Role

FieldValue
NameInfra Operator
DescriptionFull infrastructure access — restart, disk allocation, and log reading.
Scopesinfra:logs.read, infra:restart, infra:disk.allocate

Step 3 — Create the Agent

FieldValue
Display nameInfra Manager
Sluginfra-manager
Supervision modehuman_supervised
Daily action budget100 (restarts are expensive, keep the budget tight)

Open the agent's detail page → Roles tab → Add roleInfra Operator.

Step 4 — Create Policies

Policy A — Allow log reads anywhere, anytime

display_name: "Infra — allow log reads"
effect:       allow
priority:     100
scopes:       infra:logs.read
condition:    {}    // no condition = matches all requests

Policy B — Geofence: block all infra actions outside US/EU

display_name: "Infra — deny non-US/EU"
effect:       deny
priority:     200
scopes:       infra:restart, infra:disk.allocate
condition:
{
  "op": "not",
  "args": [{
    "op": "in",
    "args": ["ctx.context.ip_country", ["US", "DE", "GB", "FR", "NL", "IE"]]
  }]
}

Policy C — Require approval for restarts and disk allocation

display_name: "Infra — require approval for destructive ops"
effect:       require_approval
priority:     300
scopes:       infra:restart, infra:disk.allocate
condition:    {}    // catches everything not already denied by Policy B

Bind all three policies to the agent. The engine evaluates them in priority order: 100 → 200 → 300. A log read hits Policy A (allow) immediately. A restart from an unknown country hits Policy B (deny). A restart from a valid country skips Policy B (condition doesn't match) and hits Policy C (require_approval).

Step 5 — Test with a permission check

// ✅ Log read from India — ALLOW (Policy A matches before geofence)
POST /api/v1/decisions/check
{
  "subject_type": "agent",
  "subject_id":   "<infra-manager-uuid>",
  "action":       "infra:logs.read",
  "resource": { "type": "server", "id": "prod-api-01", "attrs": {} },
  "context": {
    "time":       "03:00",
    "ip_country": "IN",
    "request_id": "req_log_001"
  }
}
// → { "effect": "allow" }

// ❌ Restart from China — DENY (Policy B geofence)
{
  "action": "infra:restart",
  "resource": {
    "type": "service",
    "id":   "api-server",
    "attrs": {
      "service_name": "api-server",
      "environment":  "production",
      "reason":       "high memory usage"
    }
  },
  "context": { "ip_country": "CN", "time": "14:00" }
}
// → { "effect": "deny", "reason": "policy: Infra — deny non-US/EU" }

// ⏳ Restart from US — REQUIRE APPROVAL (Policy C)
{
  "action": "infra:restart",
  "resource": {
    "type": "service",
    "id":   "api-server",
    "attrs": {
      "service_name": "api-server",
      "environment":  "production",
      "reason":       "high memory usage"
    }
  },
  "context": { "ip_country": "US", "time": "14:00", "user_id": "u_devops_42" }
}
// → { "effect": "require_approval", "approval_id": "apr_..." }

Example 3 — Employee Profile Agent

An HR agent that can read and update employee profile fields, but is blocked from accessing salary data unless the requesting user is in the HR admin group, and is always denied during non-working hours.

Step 1 — Register scopes in the Scope Catalog

NamespaceNameScope stringRiskDescription
hrprofile.readhr:profile.readlowRead basic employee profile fields (name, title, department).
hrprofile.updatehr:profile.updatemediumUpdate mutable profile fields (title, department, manager).
hrsalary.readhr:salary.readcriticalRead compensation data. Restricted to HR admin users only.

Input schema for hr:profile.update:

{
  "type": "object",
  "properties": {
    "employee_id": {
      "type": "string",
      "description": "Internal employee ID being updated"
    },
    "fields": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Names of the fields being changed, e.g. [\"title\", \"department\"]"
    },
    "self_service": {
      "type": "boolean",
      "description": "True if the employee is updating their own record. Policies can allow self-service updates without approval."
    }
  },
  "required": ["employee_id", "fields"]
}

Input schema for hr:salary.read:

{
  "type": "object",
  "properties": {
    "employee_id": {
      "type": "string",
      "description": "Employee whose compensation data is being accessed"
    },
    "requester_role": {
      "type": "string",
      "description": "Role of the requesting user, e.g. \"hr_admin\", \"manager\", \"employee\". Policies use this to gate access."
    },
    "purpose": {
      "type": "string",
      "enum": ["compensation_review", "offer_creation", "audit"],
      "description": "Declared purpose for the access — logged in the audit trail."
    }
  },
  "required": ["employee_id", "requester_role"]
}

Step 2 — Create Roles

Create two roles to reflect the different access levels:

Role nameScopesWho gets it
HR Agent — Profile Access hr:profile.read, hr:profile.update All HR agents by default
HR Agent — Salary Access hr:salary.read Agents operating on behalf of an HR admin user only

Step 3 — Create the Agent

FieldValue
Display nameEmployee Profile Agent
Slugemployee-profile
Supervision modehuman_supervised
Daily action budget1000

Assign both roles to the agent. The agent now has all three scopes in its grant set. Policies will narrow what's actually allowed at decision time.

Step 4 — Create Policies

Policy A — Allow profile read/update during business hours

display_name: "HR — allow profile ops during business hours"
effect:       allow
priority:     100
scopes:       hr:profile.read, hr:profile.update
condition:
{ "op": "time_between", "args": ["ctx.context.time", "08:00", "19:00"] }

Policy B — Deny all ops outside business hours

display_name: "HR — deny all ops outside hours"
effect:       deny
priority:     200
scopes:       hr:profile.read, hr:profile.update, hr:salary.read
condition:
{
  "op": "not",
  "args": [{ "op": "time_between", "args": ["ctx.context.time", "08:00", "19:00"] }]
}

Policy C — Allow salary reads only for hr_admin requester role

display_name: "HR — salary reads for hr_admin only"
effect:       allow
priority:     300
scopes:       hr:salary.read
condition:
{
  "op": "eq",
  "args": ["ctx.resource.attrs.requester_role", "hr_admin"]
}

Policy D — Deny all other salary reads

display_name: "HR — deny salary reads by default"
effect:       deny
priority:     400
scopes:       hr:salary.read
condition:    {}    // catch-all for any salary read not matched above

Step 5 — Test with permission checks

// ✅ Profile read during hours — ALLOW
{
  "subject_type": "agent",
  "subject_id":   "<employee-profile-uuid>",
  "action":       "hr:profile.read",
  "resource": {
    "type": "employee",
    "id":   "emp_2291",
    "attrs": { "employee_id": "emp_2291" }
  },
  "context": {
    "time":       "10:30",
    "ip_country": "US",
    "user_id":    "u_manager_55",
    "session_id": "sess_hr_001"
  }
}
// → { "effect": "allow" }

// ❌ Salary read by a non-admin — DENY (Policy C doesn't match, Policy D denies)
{
  "action": "hr:salary.read",
  "resource": {
    "type": "employee_salary",
    "id":   "emp_2291",
    "attrs": {
      "employee_id":    "emp_2291",
      "requester_role": "manager",
      "purpose":        "compensation_review"
    }
  },
  "context": { "time": "11:00", "user_id": "u_manager_55" }
}
// → { "effect": "deny", "reason": "policy: HR — deny salary reads by default" }

// ✅ Salary read by hr_admin — ALLOW (Policy C matches)
{
  "action": "hr:salary.read",
  "resource": {
    "type": "employee_salary",
    "id":   "emp_2291",
    "attrs": {
      "employee_id":    "emp_2291",
      "requester_role": "hr_admin",
      "purpose":        "offer_creation"
    }
  },
  "context": { "time": "11:00", "user_id": "u_hr_admin_03" }
}
// → { "effect": "allow" }
Non-escalation guarantee: If you pass on_behalf_of_user_id, the agent's effective scopes are automatically intersected with the human user's scopes — so even if the agent has hr:salary.read in its role, it cannot use that scope while acting on behalf of a user who doesn't also have it.

Context JSON — field reference

The context object is a free-form map you pass alongside every decision request. It provides the runtime environment that ABAC conditions evaluate against — things like current time, client IP, the user's role, and any custom attributes your policies need. Fields you don't pass default to null in the evaluation engine.

Field Type Example What it's used for
time string (HH:MM) "14:30" Evaluated by the time_between condition operator. Pass the current local time (or a UTC time) to enable business-hours policies. If omitted, time_between conditions always evaluate to false.
ip string (IP address) "203.0.113.42" Logged in the audit event for forensic traceability. Kynara's server-side GeoIP lookup also derives ip_country from this value automatically if you don't supply it explicitly.
ip_country string (ISO 3166-1 alpha-2) "US" Used in in / eq conditions for geographic access control. You can pass this explicitly (e.g. from your own GeoIP lookup) or omit it and let the server derive it from ip. Referenced in policies as ctx.context.ip_country.
user_id string "u_sales_007" The human user on whose behalf the agent is acting, for logging and ABAC conditions that reference user attributes. Distinct from on_behalf_of_user_id — that field is for the non-escalation intersection; context.user_id is purely informational for conditions and the audit log.
session_id string "sess_abc123" Groups a sequence of related decisions in the audit log. Useful for reconstructing everything an agent did during a single user session or workflow run. No policy operators act on this field natively; use it for audit correlation.
request_id string "req_xyz456" A trace ID from your own infrastructure (e.g. a distributed tracing span ID or an HTTP request ID). Stored in the audit event so you can cross-reference Kynara decisions with your application logs. Not evaluated by any condition operator; purely a correlation handle.
env string "production" Identifies the deployment environment. Policies can reference this as ctx.context.env to enforce stricter rules in production than in staging — e.g. always require approval in production, allow freely in development.
mfa_verified boolean true Pass true when the human user has recently authenticated with MFA. Policies can use { "op": "eq", "args": ["ctx.context.mfa_verified", true] } as a gate for high-risk actions — e.g. only allow salary reads when MFA is confirmed.
user_role string "hr_admin" A custom role or group string from your identity system. Referenced by conditions as ctx.context.user_role to allow different behaviour for different user classes without needing separate agents.
any custom key string / number / boolean "data_classification": "PII" You can pass any additional key-value pairs your policies need. They're accessible in condition ASTs as ctx.context.<key>. Good examples: tenant_id, cost_center, approval_ticket, risk_score.
💡
Context ≠ Resource attrs. context describes the environment at request time (who, when, where, how). resource.attrs describes the object being acted on (what). Keep them separate: time-of-day goes in context; the amount_cents of a payment goes in resource.attrs.

How conditions reference context

Every condition field path starts with ctx. to distinguish it from literal values:

// Check time-of-day
{ "op": "time_between", "args": ["ctx.context.time", "09:00", "18:00"] }

// Check country
{ "op": "in", "args": ["ctx.context.ip_country", ["US", "GB", "DE"]] }

// Check environment
{ "op": "eq", "args": ["ctx.context.env", "production"] }

// Check a resource attribute (resource.attrs.amount_cents > 10000)
{ "op": "gt", "args": ["ctx.resource.attrs.amount_cents", 10000] }

// Check the requester's role (from resource.attrs)
{ "op": "eq", "args": ["ctx.resource.attrs.requester_role", "hr_admin"] }

// Combine conditions
{
  "op": "and",
  "args": [
    { "op": "eq", "args": ["ctx.context.env", "production"] },
    { "op": "eq", "args": ["ctx.context.mfa_verified", true] }
  ]
}

Input Schema — field reference

Each Scope Catalog entry has an optional Input Schema — a JSON Schema object that documents the parameters your tool accepts. This schema appears in the Kynara UI to help policy authors understand what's available in resource.attrs, and it can be imported into your agent code so it knows what to send.

Schema structure

{
  "type": "object",          // always "object" for tool inputs
  "properties": {            // map of parameter name → JSON Schema descriptor
    "param_name": {
      "type":        "string | number | boolean | array | object",
      "description": "Human-readable explanation shown in the Kynara UI and to policy authors",
      "enum":        ["option_a", "option_b"],  // optional — restricts to fixed values
      "default":     "some_value"               // optional — shown as the presumed default
    }
  },
  "required": ["param_name"]  // params the agent MUST always send
}

Field-by-field breakdown

Field Required What it means
type Yes The JSON type of this parameter: string, number, boolean, array, or object. Kynara validates that the value passed in resource.attrs matches this type at decision time.
description Strongly recommended Plain-English explanation of what this parameter means. This is what policy authors read when deciding which resource.attrs fields to use in conditions. Be specific — e.g. "Amount in cents, not dollars" rather than "Amount".
enum No An array of the only permitted values. When present, Kynara validates incoming values at decision time and rejects any value not in the list. Ideal for fields like environment or risk_level where a free-form string would be a policy bypass risk.
default No The value assumed when the parameter is absent. Documented only — Kynara does not inject defaults. Use this to communicate to agent authors what the omitted-value behaviour is.
required (top-level array) No Lists the parameter names the agent must send. If a required parameter is missing from resource.attrs, Kynara returns a validation error before the policy engine runs. Required parameters are the ones your conditions rely on — if they're absent, the condition can't be evaluated safely.
items (for array types) No Describes the schema of each element when type is "array". Example: "items": { "type": "string" } for a list of field names.

How the schema relates to policies

The input schema is the contract between your agent code and your policies. When you write a condition like:

{ "op": "gt", "args": ["ctx.resource.attrs.amount_cents", 10000] }

…you're depending on your agent always passing amount_cents in resource.attrs. The input schema makes this dependency explicit and enforces it. If amount_cents is in required, Kynara will reject any decision request that doesn't include it — preventing a badly-written agent from accidentally bypassing the condition.

Does the schema replace policies?

No — they serve completely different purposes:

Input SchemaPolicy
Describes what parameters exist and their types Describes what is allowed and under what conditions
Validates that required fields are present Evaluates those fields against rules to produce allow / deny / require_approval
Static — doesn't change at request time Dynamic — evaluated fresh on every request
Helps policy authors understand what to reference Is the actual enforcement gate

Do agents need to re-read the schema on every call?

No. The schema is a static contract defined when the tool is registered. Agent code reads the schema once — typically at initialization or during a build step — and uses it to know which fields to pass in resource.attrs. If you later update the schema (e.g. add a new required field), agent developers must update their code to pass the new field. Kynara will start rejecting requests that are missing it from the moment the schema is saved.

⚠️
Schema changes are breaking changes. Adding a field to required is equivalent to a breaking API change — any agent code that doesn't send that field will start receiving validation errors immediately. Always add new required fields in a two-phase deploy: add as optional first, update all agents to send it, then promote to required.

Key concepts

Agents

An agent is a registered identity for an AI process. Every agent has a unique ID, an API key, a supervision mode, a daily action budget, and role assignments. Agents are the subject of policy evaluation.

Bounded authority

An agent's effective permissions are always the intersection of the agent's own role and the role of the supervising human on whose behalf it is acting. An agent can never have more authority than the person who dispatched it — regardless of how its policies are configured.

Policies

A policy is an RBAC + ABAC rule that defines what an agent can do. Each policy has a priority, actions, resource types, conditions (optional), and an effect: allow, deny, or require_approval. Policies are evaluated in ascending priority order; the first match wins.

Decision outcomes

OutcomeMeaning
allowThe action is permitted. The agent may proceed. Decision cached for the policy's TTL.
denyThe action is blocked. Raised as PermissionDenied in the SDK before any side effect occurs.
require_approvalA human must approve before the action can proceed. The agent pauses; an approval_url is returned.

JIT grants

A JIT (Just-in-Time) grant is a time-bound, break-glass permission elevation. It temporarily widens an agent's effective scopes without modifying any permanent policy. Every grant requires a justification and optionally a ticket link, and is fully recorded in the audit chain.

Scope Catalog

The Scope Catalog (formerly "Tools") is the registry of callable functions your agents expose. Each entry has a namespace, name, risk class, input schema, and scope string. Policies and role editors reference scope strings from this catalog.

Supervision modes

ModeBehaviour
autonomousDecisions are evaluated against policies with no additional human checkpoint.
human_supervisedHigh-sensitivity actions escalate to a human reviewer as configured by policy.

Dashboard

The dashboard gives you a real-time view of agent activity. It shows data for the last 24 hours (chart) and 7 days (stat cards), and surfaces an onboarding checklist for new organisations.

Stat cards

CardWhat it shows
Active agentsAgents currently enabled.
Decisions (7d)Total decision events in the past 7 days.
Approvals pendingrequire_approval decisions awaiting a human reviewer.
Denials (7d)deny outcomes in the past 7 days.

Decision volume chart

Area chart showing allow, require_approval, and deny events bucketed by hour for the last 24 hours.

Onboarding checklist

New organisations see a guided checklist: register an agent, create a policy, bind it, make your first decision. The checklist disappears once all steps are complete.

Agents

Creating an agent

  1. Navigate to Agents → New agent.

  2. Enter a display name, optional description, and choose a supervision mode.

  3. Set a daily action budget to cap runaway execution.

  4. Click Create agent. Copy the Agent ID from the detail page URL.

Kill switch

Every agent has a kill switch accessible from its detail page. When triggered, the agent is immediately disabled across all sessions — all subsequent decision requests return deny. Re-enabling requires an explicit admin action, which is itself recorded in the audit log. The agent.killed webhook fires immediately.

Access summary

The Access Summary tab on the agent detail page shows a live view of the agent's effective scopes — combining its role assignments and any active JIT grants — so you can quickly see exactly what the agent is currently permitted to do.

Role assignments

The Roles tab on the agent detail page lets you assign roles to the agent. Each role grants a set of scopes (permissions). The agent's effective scope set is the union of all active role assignments. You can add or remove roles at any time — changes take effect immediately and are recorded in the audit log.

Policy bindings

Each policy shown on the agent detail page is labelled Direct (bound to this agent specifically) or Org-wide (bound to all agents via *). Org-wide bindings are managed from the Policies page.

Policies & Simulator

Creating a policy

  1. Navigate to Policies → New policy.

  2. Enter a display name, set a priority (lower = evaluated first), and choose an effect.

  3. Specify scopes (e.g., payments.refund.issue) and optionally resource types.

  4. Add an ABAC condition in the JSON editor (e.g., time-of-day, IP, data classification). Leave empty to match all requests.

  5. Use the Simulator panel (below the condition editor) to test before saving.

  6. Toggle Enabled and click Save.

💡
Simulator is embedded. The policy simulator lives inside the Policy Editor — there is no separate Simulator nav item. Open any policy and scroll down to the Simulator panel to test it against a hypothetical subject, action, resource, and context.

Condition grammar

Conditions are JSON ASTs. The engine supports an allow-listed set of operators — no dynamic code evaluation. Example:

{
  "op": "not",
  "args": [
    { "op": "time_between", "args": ["ctx.context.time", "09:00", "18:00"] }
  ]
}

Operators: and, or, not, eq, neq, gt, gte, lt, lte, in, contains, starts_with, ends_with, time_between, has_scope.

Argument-level policies (allowlists)

Conditions can read the arguments of a tool call via ctx.resource.attrs.<key> — so you can enforce intent, not just the action. Map the relevant argument (a Slack channel, an email recipient, an amount) into the decision's resource attributes, then allowlist it. Ready-made templates for these ship in the policy editor (Load example).

Slack — only post to approved channels:

{ "op": "in",
  "args": ["ctx.resource.attrs.channel", ["C0123ALLOWED", "C0456ALLOWED"]] }

Gmail — only email internal recipients:

{ "op": "ends_with",
  "args": ["ctx.resource.attrs.recipient", "@yourcompany.com"] }

Require approval for external recipients (set the policy effect to require_approval):

{ "op": "not",
  "args": [ { "op": "ends_with", "args": ["ctx.resource.attrs.recipient", "@yourcompany.com"] } ] }

Evaluation order

Policies are evaluated in ascending priority order. The first match wins. If no policy matches, the outcome is deny. Place broad deny rules at low priority numbers and narrower allow rules higher to build an allow-list model.

Disabling a policy

Setting a policy to disabled excludes it from evaluation without removing its bindings — useful for temporarily suspending a rule during an incident.

Policy Replay NEW

Before deploying a new or modified policy, simulate its impact against real historical decisions to understand exactly what would flip — before it affects live agents.

🔬
Why replay? A policy that looks correct in the simulator may have unintended consequences at scale. Replay shows you the real blast radius across up to 30 days and 100,000 historical decisions.

Running a replay

  1. Open a policy (new or existing) in the Policy Editor.

  2. Click Replay against history below the condition editor.

  3. Choose a lookback window (1–30 days).

  4. Click Run Replay and wait for results (typically <10 seconds).

  5. Review the flip counts and click any affected decision ID to inspect its full context.

  6. If the impact looks acceptable, click Save to deploy.

Replay results

FieldMeaning
total_eventsNumber of historical decision events evaluated.
flippedEvents whose outcome would change under the new policy.
allow → denyPreviously-allowed actions that would now be blocked.
allow → require_approvalPreviously-allowed actions that would now require human sign-off.
deny → allowPreviously-blocked actions that would now be permitted.

Results cap at 30 days × 100k events. For larger windows, use scripts/kynara-cli.py with the offline replayer.

Approvals

When a policy returns require_approval, Kynara creates an approval request and the agent pauses. A human reviewer must approve or deny the specific action before the agent can proceed.

Reviewing approvals

  1. Click Approvals in the left sidebar — a badge shows the pending count.

  2. Open a pending request. It shows the agent, action, resource, inputs, matched policy, and a computed risk score.

  3. Review the agent's historical denial rate for this action type to gauge risk.

  4. Click Approve or Deny and enter a mandatory justification.

Notification channels

Your agent can surface the approval_url to reviewers via any channel — Slack message, email, PagerDuty, or your own app UI. Subscribe to the decision.approval_requested webhook to push notifications automatically.

Approval expiry

Approvals expire after 24 hours by default (configurable per policy). An expired approval counts as deny.

JIT Grants NEW

JIT (Just-in-Time) grants give an agent a temporary, time-bound permission elevation for break-glass scenarios — without modifying any permanent policy.

⚠️
Use sparingly. JIT grants bypass normal policy evaluation for their duration. Always provide a valid justification and ticket link so the grant is traceable in the audit log.

Creating a grant

  1. Click JIT Grants in the sidebar (or Settings → JIT Grants).

  2. Click + New Grant.

  3. Enter the scope to grant (e.g., crm:write), duration in minutes, a justification, and a ticket URL.

  4. Click Create Grant. The grant is active immediately.

Revocation

Grants expire automatically after the requested duration. To revoke early, open the grant and click Revoke. Both creation and revocation are recorded in the audit chain with event_type = jit_grant.created / jit_grant.revoked.

Guardrails NEW

Guardrails provide a second defense layer beyond per-action policies. They watch the stream of events your agents emit in real time and automatically revoke access when predefined thresholds are exceeded.

Setting up an integration

  1. Click Guardrails in the sidebar.

  2. Click + New Integration and give it a name (e.g., your agent's name).

  3. Copy the generated Webhook URL — your agent runtime POSTs events here.

Configuring threshold rules

  1. Click the Rules tab → + New Rule.

  2. Set a threshold (e.g., 5 events), time window (e.g., 5 minutes), and severity filter (high, critical).

  3. Set the action to revoke.

  4. Click Save Rule.

When the threshold is breached, Kynara immediately revokes the agent's access. The agent.killed webhook fires. This is the primary defense against prompt injection attacks — because Kynara sits outside the LLM's trust boundary, no instruction written into a prompt can change the guardrail evaluation.

Audit log

The audit log records every event that passes through Kynara. It is append-only and SHA-256 hash-chained — tampering with any entry breaks the chain and is detected on the next integrity check.

Event types

Event typeWhen it fires
policy.decisionEvery time the decision engine evaluates a request.
agent.createdA new agent is registered.
agent.killedKill switch activated; guardrail threshold exceeded.
policy.created / updated / deletedPolicy lifecycle events.
jit_grant.created / revokedBreak-glass grant lifecycle.
approval.approved / deniedHuman resolved a pending approval request.
auth.login / logout / ssoUser authentication events with IP, user agent, device fingerprint.
member.invited / removedTeam membership changes.
admin.*All admin and superadmin actions.
audit.chain_brokenIntegrity verifier detected a gap in the hash chain.
permissions_changedRole assignment or JIT grant changed an agent's effective scopes.

Chain verification

Click Verify Chain on the Audit page to run a full SHA-256 chain replay. A green banner confirms integrity. You can also verify offline using scripts/verify_chain_offline.py — no API dependency required.

CSV export

Click Export CSV to download all filtered events. Enterprise plans support streaming nightly exports to S3-compatible storage.

Compliance note: The audit log is designed to satisfy SOC 2 Type II, ISO 27001, GDPR, and HIPAA log-integrity requirements. The hash chain means any deletion or modification of a record is detectable on the next integrity check.

Scope Catalog

The Scope Catalog (formerly "Tools") is the registry of callable functions your agents expose. Each entry defines a namespace, name, risk class, input schema, and scope string that policies and role editors can reference.

Registering a tool

  1. Click Scope Catalog → New Tool.

  2. Enter a namespace (e.g., payments), name (e.g., refund.issue), and risk class (low / medium / high / critical).

  3. Add an optional JSON Schema for the tool's inputs.

  4. Set the scope string (e.g., payments.refund.issue) — this is what roles grant and policies check.

  5. Click Save.

Scope picker in role editor

When creating or editing a role, the scope picker lets you search and select scopes directly from the Scope Catalog — no manual typing required.

Webhooks NEW

Subscribe to Kynara events to receive real-time HMAC-signed HTTP notifications in your own systems — Slack bots, PagerDuty, SIEM, approval workflows, or any HTTPS endpoint.

Creating a webhook

  1. Click Settings → Webhooks → + New Webhook.

  2. Enter your HTTPS endpoint URL.

  3. Select the events you want to subscribe to.

  4. Click Save, then Send Test Event to verify your endpoint.

🔒
HTTPS only. Webhook endpoint URLs must use https://. HTTP URLs and URLs that resolve to private/internal network addresses are rejected at creation time.

Verifying signatures

Each delivery includes an X-Kynara-Signature header in the format sha256=v1,<hex>. Verify it before processing the payload:

import hmac, hashlib

def verify_webhook(secret: str, body: bytes, sig_header: str) -> bool:
    computed = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    expected = "sha256=v1," + computed
    return hmac.compare_digest(expected, sig_header)

See Webhook events reference for the full list of subscribable events.

Settings

API keys

Settings → API Keys lets you create, view, and revoke API keys with per-key scope restrictions and rate limits. Keys are prefixed sk_live_ and shown only once — store them in a secret manager.

🔐
HMAC-secured storage. API keys are stored as HMAC-SHA256 hashes keyed to the server secret — not plain SHA-256. This means an attacker who obtains the api_keys table cannot brute-force keys without also knowing the server secret. If you rotate JWT_SECRET, all existing API keys become invalid and must be re-issued — revoke and recreate them after any secret rotation.

SSO configuration

Settings → SSO is where owners configure Okta, Azure AD, Google Workspace, or any SAML 2.0 / OIDC-compliant provider. Multiple SSO connections can coexist. See the SSO section for step-by-step instructions.

Danger zone

ActionEffect
Rotate API keysImmediately invalidates all existing API keys and issues new ones. All agents lose access until re-configured.
Revoke all sessionsSigns out all active users. Useful after a suspected credential compromise.
Delete organizationPermanently deletes the org and all data. Requires typing the org name to confirm. Irreversible.

Team members

Seat roles

RolePermissions
OwnerFull access. Manages billing, SSO, members, org settings, and danger zone.
AdminCreates and modifies agents, policies, tools, and JIT grants. Cannot manage billing.
DeveloperCan access Dashboard, Agents, Scope Catalog, and How It Works. Cannot view Roles, Policies, Audit log, Billing, or Settings.
AuditorRead-only access to Audit log, Policies, and Roles. Cannot create or modify any records.

Inviting members

  1. Navigate to Settings → Members → + Invite member.

  2. Enter the invitee's email and choose a role.

  3. Click Send invite. The invitation appears in Pending Invites until accepted.

To resend or cancel a pending invite, click the ... menu next to it. Role assignments are restricted by your own role — you cannot grant a role above your own.

Billing

Plans

Free

3 seats · 10,000 decisions/mo · 30-day audit retention. No credit card required.

Pro $49/mo

15 seats · 500,000 decisions/mo · 1-year retention · SSO · Webhooks · Priority support.

Enterprise

Unlimited seats & decisions · Custom retention · HIPAA BAA · SLA · Dedicated deployment.

Quota enforcement

When the monthly decision quota is reached, the decision endpoint returns deny with reason: "quota_exceeded" until the billing cycle resets or the plan is upgraded. Seat limits block new member invitations when the seat cap is reached.

Upgrading

  1. Navigate to Billing → Upgrade plan.

  2. Select Pro or Enterprise in the modal.

  3. Click Upgrade to Pro — you'll be redirected to Stripe Checkout. Enterprise upgrades open a sales email.

Superadmin NEW

Superadmin accounts have platform-wide management access — they can view all organisations, create new ones, and invite members to any org regardless of their own membership.

⚠️
Restricted access. Superadmin status is set at the database level and is not configurable through the UI. All superadmin actions are recorded with event_type = admin.superadmin.* in the audit chain.

Admin Console

Superadmins see an Admin Console link in the top-right user menu. The console shows all organisations (with member counts and plan details), all users across all orgs, and platform-wide usage metrics.

REST API

The Kynara REST API is at https://kynara.ai/api/v1. All endpoints require a JWT in Authorization: Bearer <token> or an API key in X-Kynara-Key: sk_live_.... Full OpenAPI 3.1 spec is available at /api/docs.

Auth

POST/api/v1/auth/login
POST/api/v1/auth/refresh
POST/api/v1/auth/register
POST/api/v1/auth/forgot-password
POST/api/v1/auth/reset-password
POST/api/v1/auth/sso/okta/start
🚦
Auth rate limits (per IP): /login 10 req/min · /refresh 20 req/min · /register, /forgot-password, /reset-password 5 req/min each. Exceeding a limit returns 429 Too Many Requests.

Agents

GET/api/v1/agents
POST/api/v1/agents
GET/api/v1/agents/{agent_id}
GET/api/v1/agents/{agent_id}/access-summary

Decisions

POST/api/v1/decisions/check
{
  "subject_type": "agent",           // "agent" | "user" | "api_key"
  "subject_id": "uuid",
  "on_behalf_of_user_id": null,      // optional — for delegated agents
  "action": "payments.refund.issue",
  "resource": {
    "type": "payment",
    "id": "pay_123",
    "attrs": { "amount_cents": 5000 }
  },
  "context": { "user_id": "u_123", "time": "03:00", "ip": "1.2.3.4" }
}
→ { "effect": "allow" | "deny" | "require_approval",
    "matched_policy_id": "uuid",
    "granted_scopes": ["payments.refund.issue"],
    "rbac_pass": true,
    "reason": "...",
    "approval_id": "..." }

Policies

GET/api/v1/policies
POST/api/v1/policies
PUT/api/v1/policies/{policy_id}
POST/api/v1/policy-replay
GET/api/v1/policy-bundle

JIT Grants

GET/api/v1/jit-grants
POST/api/v1/jit-grants
DEL/api/v1/jit-grants/{grant_id}

Approvals

GET/api/v1/approvals
GET/api/v1/approvals/{approval_id}
POST/api/v1/approvals/{approval_id}/approve
POST/api/v1/approvals/{approval_id}/deny

Audit

GET/api/v1/audit/events

Query params: limit, offset, after_cursor, event_type, agent_id, outcome, from, to, format=csv.

POST/api/v1/audit/verify

Webhooks

GET/api/v1/webhooks
POST/api/v1/webhooks
DEL/api/v1/webhooks/{endpoint_id}

Guardrails

POST/api/v1/guardrails/events
POST/api/v1/guardrails/rules

Billing

GET/api/v1/billing/usage
GET/api/v1/billing/invoices
POST/api/v1/billing/checkout

Python SDK

pip install kynara-sdk

Decorator

from kynara_sdk import Kynara, permission_required
from kynara_sdk.context import set_current_kynara

set_current_kynara(Kynara.from_env())  # reads KYNARA_BASE_URL, KYNARA_API_KEY, KYNARA_AGENT_ID

@permission_required("crm:read", resource_arg="contact_id", resource_type="crm.contact")
def read_contact(contact_id: str) -> dict:
    return crm_client.get_contact(contact_id)

Context manager

with kynara.guard("payments.refund.issue",
                  resource={"type": "payment", "id": payment_id,
                            "attrs": {"amount_cents": 50000}}) as grant:
    result = issue_refund(payment_id)
    # success auto-recorded on clean exit; error auto-recorded on raise

Manual check

decision = kynara.check(
    subject=("agent", os.environ["KYNARA_AGENT_ID"]),
    action="payments.refund.issue",
    resource={"type": "payment.refund", "id": refund_id, "attrs": {"amount_cents": 5000}},
    context={"session_id": session_id},
)
if decision.effect == "allow":
    process_refund(refund_id)
elif decision.effect == "require_approval":
    notify_approver(decision.approval_url)
else:
    raise PermissionError(decision.reason)

Failure modes

SettingBehaviour when Kynara unreachable
fail_closed=True (default)Raises KynaraUnavailable — agent is blocked.
fail_closed=FalseTreats as allow and logs locally — only use for read-only tools.

TypeScript / Node SDK NEW

npm install @kynara/sdk

guarded() wrapper

import { Kynara, guarded, PermissionDenied, ApprovalRequired } from "@kynara/sdk";

const client = Kynara.fromEnv(); // reads KYNARA_BASE_URL, KYNARA_API_KEY, KYNARA_AGENT_ID

const issueRefund = guarded({
  client,
  action: "payments.refund.issue",
  resource: (refundId: string, amountCents: number) => ({
    type: "payment.refund",
    id: refundId,
    attrs: { amount_cents: amountCents, currency: "USD" },
  }),
}, async (refundId: string, amountCents: number) => {
  return await processRefund(refundId, amountCents);
});

try {
  await issueRefund("r_123", 500_00);
} catch (e) {
  if (e instanceof ApprovalRequired) notifyApprover(e.approvalUrl);
  else if (e instanceof PermissionDenied) auditDeny(e.decision.reason);
  else throw e;
}

Express middleware

import { requirePermission } from "@kynara/sdk/express";

app.post("/refunds/:id",
  requirePermission({
    client,
    action: "payments.refund.issue",
    resource: (req) => ({ type: "payment.refund", id: req.params.id, attrs: req.body }),
  }),
  refundController,
);

LangChain.js

import { KynaraCallbackHandler } from "@kynara/sdk/langchain";
const executor = new AgentExecutor({
  agent, tools,
  callbacks: [new KynaraCallbackHandler(client, "agent_crm_assistant")],
});

Agent framework integrations NEW

Kynara integrates with every major Python and TypeScript agent framework. See the sdk/examples/ directory for complete, runnable examples.

FrameworkIntegration pointExample file
LangChain / LangGraph (Python)KynaraCallbackHandler on on_tool_startsdk/examples/crm_agent.py
LangChain.js (TypeScript)KynaraCallbackHandler from @kynara/sdk/langchainsdk-ts/src/langchain.ts
Microsoft AutoGenWrapper around registered tool functionssdk/examples/autogen_agent.py
CrewAIkynara_guard decorator on BaseTool._runsdk/examples/crewai_agent.py
OpenAI Assistants / ChatGuard in the tool-call dispatch loopsdk/examples/openai_assistants.py
Anthropic tool useIntercept ToolUseBlock before dispatchsdk/examples/anthropic_tool_use.py
Express.js routesrequirePermission middlewaresdk-ts/src/express.ts

Go Sidecar NEW

For high-throughput workloads where per-action API latency must be below 1ms, run the Go decision-cache sidecar alongside your agent.

docker run --rm -p 7070:7070 \
  -e KYNARA_API_KEY=$KEY \
  -e KYNARA_BASE_URL=https://api.kynara.ai \
  ghcr.io/kynara/decision-cache:latest

Point your SDK at the sidecar:

# Python
kynara = Kynara(api_key=KEY, base_url="http://localhost:7070", agent_id=AGENT_ID)

# TypeScript
const client = new Kynara({ apiKey: KEY, baseUrl: "http://localhost:7070", agentId: AGENT_ID });
Pathp95 latency
SDK in-process cache hit~200µs
Go sidecar (local bundle)<1ms
Central API over LAN<8ms

The sidecar fetches a JWS Ed25519–signed policy bundle every 30 seconds and evaluates locally. Decisions stream back to the central audit log in 5-second batches. require_approval decisions always go to the central API — they are never evaluated locally.

Policy-as-code CLI NEW

Manage Kynara policies through git using scripts/kynara-cli.py — no pip install required (stdlib only).

export KYNARA_BASE_URL=https://api.kynara.ai
export KYNARA_API_KEY=sk_live_...

# Pull the live bundle to a JSON file
python scripts/kynara-cli.py pull --out policies.json

# Diff a local change against live (run this in CI on every PR)
python scripts/kynara-cli.py diff --bundle policies.json

# Push (dry-run first)
python scripts/kynara-cli.py push --bundle policies.json --dry-run
python scripts/kynara-cli.py push --bundle policies.json

# Verify the bundle checksum matches the server
python scripts/kynara-cli.py verify --bundle policies.json

GitOps workflow

  1. Run pull to export the live bundle to policies.json and commit it to git.

  2. Edit policies.json in a feature branch and open a PR.

  3. A CI step runs diff and posts the impact report as a PR comment.

  4. After review, merge and run push on merge to main.

SSO / Okta

Pro and Enterprise plans support SAML 2.0 and OIDC via Okta, Azure AD, Google Workspace, or any compatible provider. Multiple SSO connections can coexist per org.

Configuring Okta OIDC

  1. In Okta admin, create a new OIDC – Web Application. Set the sign-in redirect URI to https://kynara.ai/api/v1/auth/sso/okta/callback.

  2. Copy the Client ID, Client Secret, and Okta domain.

  3. In Kynara, navigate to Settings → SSO → + New SSO Connection.

  4. Choose Okta OIDC, enter the Okta domain, Client ID, and Client Secret.

  5. Click Test Connection before enabling.

⚠️
Do not disable password login until you have verified at least one owner can sign in via SSO. If SSO misconfiguration locks you out, contact support.

SIEM integration NEW

Stream Kynara audit events into your existing SIEM using the stateless polling cursor API. No persistent connection required.

# Initial call — no cursor
GET /api/v1/audit/events?limit=1000
→ { "events": [...], "next_cursor": "eyJ..." }

# Subsequent calls — pass the cursor from the previous response
GET /api/v1/audit/events?limit=1000&after_cursor=eyJ...
→ { "events": [...], "next_cursor": "eyJ..." }

Store next_cursor between poll cycles. When events is empty, you're caught up.

Supported SIEMs

SIEMSetup
SplunkInstall the Kynara Add-on; configure the polling cursor URL + API key as a scripted input.
DatadogConfigure a Datadog Log Pipeline with the cursor endpoint as a custom log source.
Elastic / ECSInstall the Filebeat module with the cursor endpoint; events map to ECS field names.
Microsoft SentinelUse the Sentinel data connector; analytic rules for agent.killed and audit.chain_broken are pre-built.

Navigate to Settings → Integrations or Audit → SIEM Setup for guided setup instructions for each platform.

Policy schema reference

{
  "display_name": "Deny off-hours refunds",
  "description": "Require approval for refunds issued outside business hours.",
  "priority": 100,
  "effect": "require_approval",   // "allow" | "deny" | "require_approval"
  "actions": ["payments.refund.issue"],  // "actions" in API = "scopes" in UI
  "resource_types": ["payment"],  // [] or ["*"] = match all
  "condition": {                  // optional JSON AST
    "op": "not",
    "args": [
      { "op": "time_between", "args": ["ctx.context.time", "09:00", "18:00"] }
    ]
  },
  "is_enabled": true
}

Field reference

FieldTypeRequiredDescription
display_namestringYesHuman-readable name shown in the UI.
priorityintegerYesEvaluation order. Lower = evaluated first. Use gaps (100, 200) for easy insertion.
effectenumYesallow, deny, or require_approval.
actionsstring[]NoScope strings (shown as "Scopes" in the UI). Empty or ["*"] = all scopes. Wildcards supported: payments.*, infra:*.
resource_typesstring[]NoResource type strings. Empty = all types.
conditionobjectNoJSON AST condition tree. Leave null to match all requests unconditionally.
is_enabledbooleanNoDefaults to true. Disabled policies are skipped during evaluation.

Decision outcomes reference

allow

A matching policy with effect: allow was found. Decision is cached in the SDK for ttl_seconds (default 5s). On cache hit, enforcement adds ~200µs.

deny

Either a matching effect: deny policy was found, or no policy matched at all (default-deny). The SDK raises PermissionDenied before any side effect occurs. Deny decisions are never cached.

require_approval

A matching effect: require_approval policy was found. The SDK raises ApprovalRequired with approval_url and decision_id. The agent should pause and surface the URL to a human reviewer. Approval decisions are never cached — every request is re-evaluated.

Webhook events reference

EventWhen it fires
decision.allowedAgent action permitted by policy.
decision.deniedAgent action blocked by policy.
decision.approval_requestedPolicy returned require_approval.
decision.approvedHuman approved a pending request.
decision.rejectedHuman rejected a pending approval.
agent.createdA new agent was registered.
agent.killedKill switch activated or guardrail threshold exceeded.
agent.permissions_changedRole assignment or JIT grant changed an agent's effective scopes.
policy.changedPolicy created, updated, or deleted.
audit.chain_brokenIntegrity verifier detected a hash chain gap.
approval.expiredA pending approval request timed out (default 24 h).

All deliveries are HMAC-signed with X-Kynara-Signature: sha256=v1,<hex> and include a timestamp to prevent replay attacks.

FAQ

What happens if the Kynara API is unreachable?

Kynara is fail-closed. If the API (or Go sidecar) cannot be reached, the SDK raises KynaraUnavailable — equivalent to deny. Never default to allow on timeout. Both SDKs enforce this automatically (fail_closed=True / failClosed: true by default).

Can I have policies that apply to all agents?

Yes. Create a policy binding with the * selector to apply a policy org-wide, or scope it to a single agent with agent:<id>.

How does the MCP Gateway decide which tools an agent can see?

Each MCP tool is mapped to a Kynara scope; the gateway evaluates that scope per agent and only advertises tools that resolve to allow or require_approval, hiding anything that would be denied. See MCP Gateway.

Can I import agent identities from Okta?

Yes — connect Okta under Identity Providers to sync agent identities into Kynara and optionally map Okta groups to Kynara roles.

MCP Gateway

The MCP Gateway places Kynara in front of any Model Context Protocol (MCP) server so that every tool call is authorized per agent against your policies — and agents only ever see the tools they are allowed to use (least-privilege discovery).

How it works

  1. Register your upstream MCP server under Enforcement → MCP Gateway (URL or stdio command, a scope prefix, and a fail mode).
  2. Point the Kynara MCP wrapper at it and set KYNARA_MCP_SERVER_ID and KYNARA_API_KEY. On start, the wrapper discovers the upstream tools and registers them with Kynara.
  3. Each discovered tool is auto-mapped to a Kynara scope (e.g. mcp.crm.contacts.read). You can edit the scope, set a risk class, or pin an effect override per tool.
  4. On every call the wrapper resolves the tool’s scope and asks the decision engine for allow, deny, or require_approval — exactly like any other Kynara action.

Least-privilege discovery

When an agent lists tools, the wrapper asks Kynara which tools that agent may call and hides the rest, so a denied capability is never even advertised. A tool pinned to deny can never be invoked, even by name.

Fail mode

Each server has a per-server fail mode: closed denies calls when the policy engine is unreachable (the secure default), open allows them. Mapped scopes flow through the same RBAC/ABAC, non-escalation, approval, and tamper-evident audit machinery as the rest of Kynara.

Identity Providers (Okta agent sync)

Connect Okta to import your AI-agent identities into Kynara and keep them in sync. Imported agents become standard Kynara agents, so your policies, approvals, and audit apply to them automatically.

Set up

  1. Go to Administration → Identity Providers → Connect Okta.
  2. Enter your Okta org URL and an SSWS API token (Okta → Security → API → Tokens). The token is stored encrypted.
  3. Choose a sync mode: Agents (Okta’s first-class agent identities, /api/v1/agents) or Group (members of a designated Okta group).
  4. Click Test to verify connectivity, then Sync now.

Role mapping

Optionally map Okta group names to Kynara role slugs. When an imported agent belongs to a mapped group, Kynara grants that role via a configured on-behalf user. Without an on-behalf user, sync imports identities only (no role grants).

Lifecycle

Sync is idempotent — agents are matched by their Okta id, so re-syncing updates rather than duplicates. Enable Deactivate removed to disable agents that no longer exist in Okta. Run sync on demand, or wire it to your scheduler.