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.
RBAC + ABAC rules evaluated in priority order. First matching decision wins; the default is deny.
Every allow, deny, and approval is SHA-256 hash-chained. Any tampering is mathematically detectable.
In-process SDK cache and optional Go sidecar for local evaluation at <1ms latency.
Policies can require approval before sensitive actions execute. Agents pause and wait.
Time-bound break-glass permission elevations with justification and ticket link.
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.
Create your account at kynara.ai. The free plan includes 3 seats and 10,000 policy decisions per month.
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.
Create an API key. Go to Settings → API Keys → New API Key. Copy it immediately — it is shown only once.
Create a policy. Navigate to Policies → New policy. For example: effect
allow, actionsfile.read, resourcesfile. Test it in the integrated Simulator panel.Bind the policy to your agent via the Bindings tab.
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" }
}'
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.
Step 1 — Register scopes in the Scope Catalog
Navigate to Scope Catalog → New Tool and create four entries:
| Namespace | Name | Scope string | Risk | Description |
|---|---|---|---|---|
crm | contacts.read | crm:contacts.read | low | Read a CRM contact by ID or email. |
crm | contacts.write | crm:contacts.write | medium | Create or update a CRM contact record. |
crm | deals.read | crm:deals.read | low | Read deal pipeline data. |
crm | notes.create | crm:notes.create | low | Append 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.
| Field | Value |
|---|---|
| Name | CRM Agent — Read + Notes |
| Description | Allows the CRM assistant to read contacts/deals and create notes. Write operations are excluded — add them via a separate role only when required. |
| Scopes | crm: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.
| Field | Value |
|---|---|
| Display name | CRM Assistant |
| Slug | crm-assistant |
| Supervision mode | human_supervised |
| Daily action budget | 500 |
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.
| Field | Value |
|---|---|
| Display name | CRM write — require approval off-hours |
| Effect | require_approval |
| Priority | 100 |
| Scopes | crm:contacts.write |
| Resource types | crm.contact |
| Condition | Matches 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
| Namespace | Name | Scope string | Risk | Description |
|---|---|---|---|---|
infra | logs.read | infra:logs.read | low | Stream or query server logs. |
infra | restart | infra:restart | high | Restart a named service or container. |
infra | disk.allocate | infra:disk.allocate | high | Resize 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
| Field | Value |
|---|---|
| Name | Infra Operator |
| Description | Full infrastructure access — restart, disk allocation, and log reading. |
| Scopes | infra:logs.read, infra:restart, infra:disk.allocate |
Step 3 — Create the Agent
| Field | Value |
|---|---|
| Display name | Infra Manager |
| Slug | infra-manager |
| Supervision mode | human_supervised |
| Daily action budget | 100 (restarts are expensive, keep the budget tight) |
Open the agent's detail page → Roles tab → Add role → Infra 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
| Namespace | Name | Scope string | Risk | Description |
|---|---|---|---|---|
hr | profile.read | hr:profile.read | low | Read basic employee profile fields (name, title, department). |
hr | profile.update | hr:profile.update | medium | Update mutable profile fields (title, department, manager). |
hr | salary.read | hr:salary.read | critical | Read 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 name | Scopes | Who 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
| Field | Value |
|---|---|
| Display name | Employee Profile Agent |
| Slug | employee-profile |
| Supervision mode | human_supervised |
| Daily action budget | 1000 |
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" }
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 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 Schema | Policy |
|---|---|
| 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.
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
| Outcome | Meaning |
|---|---|
| allow | The action is permitted. The agent may proceed. Decision cached for the policy's TTL. |
| deny | The action is blocked. Raised as PermissionDenied in the SDK before any side effect occurs. |
| require_approval | A 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
| Mode | Behaviour |
|---|---|
autonomous | Decisions are evaluated against policies with no additional human checkpoint. |
human_supervised | High-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
| Card | What it shows |
|---|---|
| Active agents | Agents currently enabled. |
| Decisions (7d) | Total decision events in the past 7 days. |
| Approvals pending | require_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
Navigate to Agents → New agent.
Enter a display name, optional description, and choose a supervision mode.
Set a daily action budget to cap runaway execution.
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
Navigate to Policies → New policy.
Enter a display name, set a priority (lower = evaluated first), and choose an effect.
Specify scopes (e.g.,
payments.refund.issue) and optionally resource types.Add an ABAC condition in the JSON editor (e.g., time-of-day, IP, data classification). Leave empty to match all requests.
Use the Simulator panel (below the condition editor) to test before saving.
Toggle Enabled and click Save.
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.
Running a replay
Open a policy (new or existing) in the Policy Editor.
Click Replay against history below the condition editor.
Choose a lookback window (1–30 days).
Click Run Replay and wait for results (typically <10 seconds).
Review the flip counts and click any affected decision ID to inspect its full context.
If the impact looks acceptable, click Save to deploy.
Replay results
| Field | Meaning |
|---|---|
total_events | Number of historical decision events evaluated. |
flipped | Events whose outcome would change under the new policy. |
allow → deny | Previously-allowed actions that would now be blocked. |
allow → require_approval | Previously-allowed actions that would now require human sign-off. |
deny → allow | Previously-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
Click Approvals in the left sidebar — a badge shows the pending count.
Open a pending request. It shows the agent, action, resource, inputs, matched policy, and a computed risk score.
Review the agent's historical denial rate for this action type to gauge risk.
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.
Creating a grant
Click JIT Grants in the sidebar (or Settings → JIT Grants).
Click + New Grant.
Enter the scope to grant (e.g.,
crm:write), duration in minutes, a justification, and a ticket URL.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
Click Guardrails in the sidebar.
Click + New Integration and give it a name (e.g., your agent's name).
Copy the generated Webhook URL — your agent runtime POSTs events here.
Configuring threshold rules
Click the Rules tab → + New Rule.
Set a threshold (e.g., 5 events), time window (e.g., 5 minutes), and severity filter (high, critical).
Set the action to
revoke.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 type | When it fires |
|---|---|
policy.decision | Every time the decision engine evaluates a request. |
agent.created | A new agent is registered. |
agent.killed | Kill switch activated; guardrail threshold exceeded. |
policy.created / updated / deleted | Policy lifecycle events. |
jit_grant.created / revoked | Break-glass grant lifecycle. |
approval.approved / denied | Human resolved a pending approval request. |
auth.login / logout / sso | User authentication events with IP, user agent, device fingerprint. |
member.invited / removed | Team membership changes. |
admin.* | All admin and superadmin actions. |
audit.chain_broken | Integrity verifier detected a gap in the hash chain. |
permissions_changed | Role 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.
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
Click Scope Catalog → New Tool.
Enter a namespace (e.g.,
payments), name (e.g.,refund.issue), and risk class (low / medium / high / critical).Add an optional JSON Schema for the tool's inputs.
Set the scope string (e.g.,
payments.refund.issue) — this is what roles grant and policies check.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
Click Settings → Webhooks → + New Webhook.
Enter your HTTPS endpoint URL.
Select the events you want to subscribe to.
Click Save, then Send Test Event to verify your endpoint.
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.
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
| Action | Effect |
|---|---|
| Rotate API keys | Immediately invalidates all existing API keys and issues new ones. All agents lose access until re-configured. |
| Revoke all sessions | Signs out all active users. Useful after a suspected credential compromise. |
| Delete organization | Permanently deletes the org and all data. Requires typing the org name to confirm. Irreversible. |
Team members
Seat roles
| Role | Permissions |
|---|---|
| Owner | Full access. Manages billing, SSO, members, org settings, and danger zone. |
| Admin | Creates and modifies agents, policies, tools, and JIT grants. Cannot manage billing. |
| Developer | Can access Dashboard, Agents, Scope Catalog, and How It Works. Cannot view Roles, Policies, Audit log, Billing, or Settings. |
| Auditor | Read-only access to Audit log, Policies, and Roles. Cannot create or modify any records. |
Inviting members
Navigate to Settings → Members → + Invite member.
Enter the invitee's email and choose a role.
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
3 seats · 10,000 decisions/mo · 30-day audit retention. No credit card required.
15 seats · 500,000 decisions/mo · 1-year retention · SSO · Webhooks · Priority support.
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
Navigate to Billing → Upgrade plan.
Select Pro or Enterprise in the modal.
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.
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
/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
Decisions
{
"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
JIT Grants
Approvals
Audit
Query params: limit, offset, after_cursor, event_type, agent_id, outcome, from, to, format=csv.
Webhooks
Guardrails
Billing
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
| Setting | Behaviour when Kynara unreachable |
|---|---|
fail_closed=True (default) | Raises KynaraUnavailable — agent is blocked. |
fail_closed=False | Treats 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.
| Framework | Integration point | Example file |
|---|---|---|
| LangChain / LangGraph (Python) | KynaraCallbackHandler on on_tool_start | sdk/examples/crm_agent.py |
| LangChain.js (TypeScript) | KynaraCallbackHandler from @kynara/sdk/langchain | sdk-ts/src/langchain.ts |
| Microsoft AutoGen | Wrapper around registered tool functions | sdk/examples/autogen_agent.py |
| CrewAI | kynara_guard decorator on BaseTool._run | sdk/examples/crewai_agent.py |
| OpenAI Assistants / Chat | Guard in the tool-call dispatch loop | sdk/examples/openai_assistants.py |
| Anthropic tool use | Intercept ToolUseBlock before dispatch | sdk/examples/anthropic_tool_use.py |
| Express.js routes | requirePermission middleware | sdk-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 });
| Path | p95 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
Run
pullto export the live bundle topolicies.jsonand commit it to git.Edit
policies.jsonin a feature branch and open a PR.A CI step runs
diffand posts the impact report as a PR comment.After review, merge and run
pushon 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
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.Copy the Client ID, Client Secret, and Okta domain.
In Kynara, navigate to Settings → SSO → + New SSO Connection.
Choose Okta OIDC, enter the Okta domain, Client ID, and Client Secret.
Click Test Connection before enabling.
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
| SIEM | Setup |
|---|---|
| Splunk | Install the Kynara Add-on; configure the polling cursor URL + API key as a scripted input. |
| Datadog | Configure a Datadog Log Pipeline with the cursor endpoint as a custom log source. |
| Elastic / ECS | Install the Filebeat module with the cursor endpoint; events map to ECS field names. |
| Microsoft Sentinel | Use 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
| Field | Type | Required | Description |
|---|---|---|---|
display_name | string | Yes | Human-readable name shown in the UI. |
priority | integer | Yes | Evaluation order. Lower = evaluated first. Use gaps (100, 200) for easy insertion. |
effect | enum | Yes | allow, deny, or require_approval. |
actions | string[] | No | Scope strings (shown as "Scopes" in the UI). Empty or ["*"] = all scopes. Wildcards supported: payments.*, infra:*. |
resource_types | string[] | No | Resource type strings. Empty = all types. |
condition | object | No | JSON AST condition tree. Leave null to match all requests unconditionally. |
is_enabled | boolean | No | Defaults 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
| Event | When it fires |
|---|---|
decision.allowed | Agent action permitted by policy. |
decision.denied | Agent action blocked by policy. |
decision.approval_requested | Policy returned require_approval. |
decision.approved | Human approved a pending request. |
decision.rejected | Human rejected a pending approval. |
agent.created | A new agent was registered. |
agent.killed | Kill switch activated or guardrail threshold exceeded. |
agent.permissions_changed | Role assignment or JIT grant changed an agent's effective scopes. |
policy.changed | Policy created, updated, or deleted. |
audit.chain_broken | Integrity verifier detected a hash chain gap. |
approval.expired | A 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
- Register your upstream MCP server under Enforcement → MCP Gateway (URL or stdio command, a scope prefix, and a fail mode).
- Point the Kynara MCP wrapper at it and set
KYNARA_MCP_SERVER_IDandKYNARA_API_KEY. On start, the wrapper discovers the upstream tools and registers them with Kynara. - 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. - On every call the wrapper resolves the tool’s scope and asks the decision engine for
allow,deny, orrequire_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
- Go to Administration → Identity Providers → Connect Okta.
- Enter your Okta org URL and an SSWS API token (Okta → Security → API → Tokens). The token is stored encrypted.
- Choose a sync mode: Agents (Okta’s first-class agent identities,
/api/v1/agents) or Group (members of a designated Okta group). - 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.