Deploy autonomous agents that use your tools, with guardrails you control.
Kanly is an open-source platform for running headless AI agents — triggered by APIs and webhooks, executing tools on your infrastructure, pausing for human approval when it matters. Every step is traced.
Not a chatbot framework. Not a personal assistant. A deployment target for agents that do real work.
Why Kanly
The problem: You want agents that can touch your real systems — run CLI commands, hit internal APIs, read your codebase. But you don't want to give an LLM full YOLO access to your infrastructure, and you don't want to babysit it in a terminal.
The solution: Kanly splits the brain from the hands. The agentic loop (LLM reasoning) runs on the server. Tool execution happens on your machine, behind your firewall, with your approval rules. Secrets never leave your infra.
┌─ Kanly Server ────────────────┐ ┌─ Your Machine ──────────────┐
│ │ │ │
│ Agent definitions │ WS │ Your tool handlers │
│ LLM orchestration loop │◄─────►│ MCP servers │
│ Webhook tools (HTTP) │ │ CLI commands │
│ Full trace capture │ │ Approval gates │
│ │ │ │
└────────────────────────────────┘ └─────────────────────────────┘
How it's different
| Claude Code / Cursor | OpenClaw | LangGraph Cloud | Kanly | |
|---|---|---|---|---|
| Mode | Interactive — you're in the terminal | Chat — you message it | Headless | Headless |
| Trigger | You type | You message | API | API / webhook / cron |
| Tools | Built-in | Community skills (26% had vulnerabilities) | Cloud-side | Your code, your machine |
| Approval | Popup (you're already there) | Binary: full access or sandbox | None | Per-tool, async (Slack, terminal, webhook) |
| Agents | One session | One do-everything bot | Many | Many purpose-built agents |
| Secrets | Local | Local | Cloud | Never leave your machine |
Example: Agent that fixes GitHub issues
An agent that reads a GitHub issue, explores the codebase, writes a fix, runs tests, and opens a PR — pausing for your approval before pushing.
1. Write your tool handlers:
# handlers.py import subprocess, json async def read_issue(arguments: dict) -> str: url = arguments["issue_url"] result = subprocess.run(["gh", "issue", "view", url, "--json", "title,body"], capture_output=True, text=True) return result.stdout async def search_code(arguments: dict) -> str: result = subprocess.run(["rg", arguments["query"], "--type", "py", "-l"], capture_output=True, text=True) return result.stdout or "No matches found" async def read_file(arguments: dict) -> str: with open(arguments["path"]) as f: return f.read() async def edit_file(arguments: dict) -> str: with open(arguments["path"]) as f: content = f.read() content = content.replace(arguments["old"], arguments["new"]) with open(arguments["path"], "w") as f: f.write(content) return "File updated" async def run_tests(arguments: dict) -> str: result = subprocess.run(["pytest", "--tb=short"], capture_output=True, text=True) return result.stdout + result.stderr async def create_pr(arguments: dict) -> str: subprocess.run(["git", "checkout", "-b", arguments["branch"]], check=True) subprocess.run(["git", "add", "-A"], check=True) subprocess.run(["git", "commit", "-m", arguments["title"]], check=True) return "Branch created and committed. Waiting for push approval."
2. Define the agent:
curl -X POST http://localhost:8000/agents \ -H "Authorization: Bearer $KANLY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "issue-fixer", "model": "anthropic/claude-sonnet-4", "system_prompt": "You fix GitHub issues. Read the issue, explore the code, write a fix, run tests. If tests pass, create a PR branch. Never push without approval.", "max_steps": 30, "tools": [ {"type": "custom", "name": "read_issue", "description": "Read a GitHub issue", "parameters": {"type": "object", "properties": {"issue_url": {"type": "string"}}, "required": ["issue_url"]}}, {"type": "custom", "name": "search_code", "description": "Search codebase with ripgrep", "parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}, {"type": "custom", "name": "read_file", "description": "Read a file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}, {"type": "custom", "name": "edit_file", "description": "Edit a file by replacing text", "parameters": {"type": "object", "properties": {"path": {"type": "string"}, "old": {"type": "string"}, "new": {"type": "string"}}, "required": ["path", "old", "new"]}}, {"type": "custom", "name": "run_tests", "description": "Run the test suite", "parameters": {"type": "object", "properties": {}}}, {"type": "custom", "name": "create_pr", "description": "Create a branch and commit changes", "parameters": {"type": "object", "properties": {"branch": {"type": "string"}, "title": {"type": "string"}}, "required": ["branch", "title"]}}, {"type": "cli", "name": "git_push", "command_pattern": "git push origin {args}", "auto_approve": false} ] }'
3. Start the runtime:
kanly-runtime connect --url http://localhost:8000 --api-key $KANLY_API_KEY --handlers handlers.py4. Trigger it (from a GitHub webhook, cron, or manually):
curl -X POST http://localhost:8000/agents/issue-fixer/runs \ -H "Authorization: Bearer $KANLY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"message": "Fix this issue: https://github.com/yourorg/yourrepo/issues/42"}'
The agent works autonomously — reads the issue, explores code, writes a fix, runs tests. When it tries to git push, the runtime pauses and asks for your approval. You approve, the PR gets opened. Full trace of every step is captured.
Quickstart
1. Start the server
cd server && pip install -e . export KANLY_API_KEY="your-secret-key" export KANLY_OPENAI_API_KEY="sk-..." export KANLY_OPENAI_BASE_URL="https://openrouter.ai/api/v1" uvicorn kanly.app:app --host 0.0.0.0 --port 8000
2. Start the runtime
cd runtime && pip install -e . kanly-runtime connect \ --url http://localhost:8000 \ --api-key your-secret-key \ --handlers handlers.py
3. Create an agent and run it
# Create curl -X POST http://localhost:8000/agents \ -H "Authorization: Bearer your-secret-key" \ -H "Content-Type: application/json" \ -d '{ "name": "my-agent", "model": "anthropic/claude-sonnet-4", "system_prompt": "You are a helpful assistant.", "tools": [...] }' # Run curl -X POST http://localhost:8000/agents/my-agent/runs \ -H "Authorization: Bearer your-secret-key" \ -H "Content-Type: application/json" \ -d '{"message": "Do the thing"}'
Configuration
| Variable | Description | Required |
|---|---|---|
KANLY_API_KEY |
API key for authenticating requests | Yes |
KANLY_OPENAI_API_KEY |
API key for the LLM provider | Yes |
KANLY_OPENAI_BASE_URL |
Base URL for OpenAI-compatible API | Yes |
KANLY_HANDLERS |
Path to custom tool handlers file (runtime) | No |
Works with any OpenAI-compatible API — OpenRouter, OpenAI, Anthropic (via proxy), local vLLM, Ollama, etc.
Tool Types
Custom
You define the schema, you write the handler. When the LLM calls the tool, Kanly dispatches it to your runtime.
{
"type": "custom",
"name": "query_db",
"description": "Query the user database",
"parameters": {
"type": "object",
"properties": {
"sql": { "type": "string", "description": "SQL query to execute" }
},
"required": ["sql"]
}
}# handlers.py async def query_db(arguments: dict) -> str: # your code, your database, your machine result = await db.execute(arguments["sql"]) return json.dumps(result)
Webhook
Server-side HTTP call. No runtime needed. Good for external APIs.
{
"type": "webhook",
"name": "notify_slack",
"url": "https://hooks.slack.com/services/...",
"method": "POST"
}CLI
Shell commands on your machine. auto_approve: false means the runtime pauses and asks before executing.
{
"type": "cli",
"name": "deploy",
"command_pattern": "kubectl apply -f {args}",
"auto_approve": false
}MCP
Connect to any MCP server running on your machine.
{
"type": "mcp",
"server": "filesystem",
"uri": "npx @modelcontextprotocol/server-filesystem /home/user"
}Security
- Tool allowlists — agents can only call tools in their
toolsarray - Approval gates — CLI tools default to
auto_approve: false, runtime pauses for human confirmation - No secret leakage — tools execute on your machine, credentials never touch the server
- Shell injection prevention — CLI arguments are validated against injection patterns
- Full audit trail — every LLM call, tool dispatch, and result is traced with timestamps
API Reference
All endpoints except /health require Authorization: Bearer <KANLY_API_KEY>.
| Method | Endpoint | Description |
|---|---|---|
POST |
/agents |
Create an agent |
GET |
/agents |
List agents |
GET |
/agents/{name} |
Get agent |
PATCH |
/agents/{name} |
Update agent |
DELETE |
/agents/{name} |
Delete agent |
POST |
/agents/{name}/runs |
Trigger a run |
GET |
/runs/{run_id} |
Get run status |
GET |
/runs/{run_id}/trace |
Full execution trace |
WS |
/runtime/ws |
Runtime WebSocket |
The Name
Kanly is the formal war of assassins between Great Houses in Frank Herbert's Dune — regulated conflict with clear rules of engagement. Agents with rules.
License
MIT. See LICENSE.