Permission Model
Koyal uses a review-first permission model. No trade executes without an explicit approval step. The system fails closed — halt is the default state on first boot.
Core Concepts
Global Halt Switch
The halt switch blocks all proposal execution globally.
- Default: halted on first boot until setup is complete
- CLI:
koyal halt on/koyal halt off - API:
POST /api/halt {"halted": true, "reason": "..."} - Dashboard: prominent red Halt button in the header
koyal halt status
# {"halted": false, "reason": null}
koyal halt on --reason "flash crash detected"
# Trading is now halted
Proposal Flow
Every trade follows this path:
AgentTick → TradeProposal (pending)
→ PermissionEngine.check()
→ Allow → execute() → write ledger
→ Deny → reject() → log reason
→ Prompt → approval queue → operator review → allow/deny
Prompt is the most common result for new bots or large trades. The operator reviews in the dashboard or via CLI and responds explicitly.
Approval Queue
Pending approvals appear in:
GET /api/permissions— listPOST /api/approvals/respond {"requestId": "...", "decision": "allow"}— respond- Dashboard: ApprovalBanner at top of page
Wallet Roles
Each wallet assigned to a bot has a role:
| Role | Description |
|---|---|
Observe | Read-only. Balance queries only. No trade permissions. |
Trade | Can execute swaps up to bot risk limits. |
Sweep | Can move funds out. Requires elevated approval. |
Bot Risk Config
Each bot has a risk config:
{
"maxPositionPct": 2.0,
"maxDrawdownPct": 5.0,
"maxDailyTrades": 10,
"maxSlippageBps": 100
}
The PermissionEngine enforces these limits before any trade executes.
PermissionEngine
PermissionEngine in koyal-runtime runs on every tick:
- Check global halt — deny immediately if halted
- Check bot lifecycle state — deny if not
live - Validate wallet role for the requested operation
- Check risk config bounds (position size, drawdown, daily trade count)
- Check bot-scoped policy rules from vault
bots/<id>.md - Return
Allow,Deny(reason), orPrompt{request_id}
Security Notes
- Private keys are never stored in plain text.
koyal-providersuses AES-256-GCM for encrypted credential storage. - The gateway binds to
127.0.0.1by default. Binding to0.0.0.0requires explicit operator action. - All API routes that modify state require the gateway to be in a non-halted state, or the route itself handles halt semantics.
- The MCP server only exposes read-safe tools by default; write tools require explicit bot context.