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.
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 │
│ │ │ │
└────────────────────────────────┘ └─────────────────────────────┘
| 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 |
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.
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 8000cd runtime && pip install -e .
kanly-runtime connect \
--url http://localhost:8000 \
--api-key your-secret-key \
--handlers handlers.py# 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"}'| 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.
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)Server-side HTTP call. No runtime needed. Good for external APIs.
{
"type": "webhook",
"name": "notify_slack",
"url": "https://hooks.slack.com/services/...",
"method": "POST"
}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
}Connect to any MCP server running on your machine.
{
"type": "mcp",
"server": "filesystem",
"uri": "npx @modelcontextprotocol/server-filesystem /home/user"
}- 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
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 |
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.
MIT. See LICENSE.