
Nasiko is the OpenRuntime for agents, coding harnesses, frameworks and tools.
Find the coding agents already running. See what each one costs, by harness and model. Route them to the models you choose while developers keep using their existing harness workflows.
📚 Documentation • 💬 Discord • 🐛 Report a bug
Table of Contents
- What is Nasiko?
- Start here: see what is already running
- Coding agents: reporting and routing
- Features
- Why this runs in the call path
- Architecture
- Requirements
- Quick Start: Docker only (no Rust needed)
- Setup guides by operating system
- Deploying your own agents
- Environment Variables
- Project Structure
- Troubleshooting
- Project Activity
- Documentation & Links
- Support
- Contributing
- License
What is Nasiko?
Your developers are already running coding agents. Claude Code, Codex, Cursor, OpenCode, often more than one at a time, each with its own dashboard and its own billing unit. At the end of the month there is a real bill, and nobody can say who spent it, on which model, or what it produced.
Nasiko finds those agents, puts their spend in one schema, and hands back the model choice the harness made for you. An OpenRuntime belongs to no vendor inside it: your cost history, your policy and your model choice stay with you rather than with whichever harness you happened to install.
Two properties hold the rest of this together, and they are not the same thing.
Non-invasive discovery is about your architecture. Nasiko can detect the harnesses you already run without changing their configuration. Reporting and routing are explicit opt-ins that add hooks or update harness settings, but they require no wrapper or SDK in your application code.
Non-intrusive operation is about your day. After one-time setup, developers keep using the same harness commands and interfaces. OpenCode must be restarted after its plugin is installed, and Codex asks you to trust the new Nasiko hooks.

Nasiko Dashboard: deploy agents, route traffic, manage tools, and watch traces.
Start here: see what is already running
Install the current CLI from this checkout, then run its read-only discovery command:
git clone https://github.com/Nasiko-Labs/nasiko.git && cd nasiko
cargo install --path cli/ --force
nasiko agents discover
nasiko agents discover reads your machine, prints what it finds, and exits. You get a row per
supported harness: whether it is detected, whether it is reporting to Nasiko, its version, and
where its config lives. Discovery changes no harness settings, requires no account, and sends
nothing off the machine.
If
nasiko agents discoveris not listed bynasiko agents --help, reinstall with the--forcecommand above. Older builds use the same0.1.0version number, sonasiko --versionalone cannot tell you whether the command is present.
| Harness | Discovery and session reporting | LLM routing |
|---|---|---|
| Claude Code | yes | yes |
| Codex | yes | yes |
| OpenCode | yes | yes |
| Cursor CLI | yes | not yet |
Those four are the supported set today and we will continue to add support for me. If you have a request please create an issue!
The CLI is a Rust crate, so this path needs Rust. Reporting and routing also need an active control plane and valid login: see Quick Start.
Coding agents: reporting and routing
Nasiko manages the coding-agent CLIs already installed on your machine: it can record what they
do, route their LLM calls, or both. The two are independent opt-ins. agents uninstall removes
local reporting hooks but preserves the registered agent and its history; disconnect restores
routing settings, though a running harness may need to be stopped or disconnected with --force.
Session reporting
nasiko agents discover # DETECTED / CONNECTED / VERSION / CONFIG per agent
nasiko agents install # claude, codex, opencode, or cursor
nasiko agents install --no-content # omit prompt and response text
nasiko agents uninstall
nasiko agents sync # flush queued session-turn events to the control plane
Auto-install also fires on nasiko connect , nasiko use, and
nasiko auth login, but only when authenticated and only for agents with no existing install
record. Routing commands such as nasiko connect claude do not trigger auto-install. Nasiko never
silently rebinds an already-installed agent to a new cluster; rebind explicitly with
nasiko agents install .
Nasiko installs each harness's supported event hooks: Claude uses Stop, OpenCode reports on
session.idle, and Codex and Cursor use their richer hook event sets. Completed turns are queued
locally under ~/.nasiko/integrations/queue/ and delivered to
POST /api/telemetry/coding-agent/events/batch, so a control plane that is briefly unreachable
costs you nothing. Permanently-failed deliveries (after a cluster is deleted or renamed, say) land
in ~/.nasiko/integrations/rejected/ and are safe to delete.
Ingested turns show up as chat sessions right away (nasiko sessions, nasiko history ).
Traces, token counts, and cost additionally require CODING_AGENT_OTLP_ENDPOINT, covered below.
Routing: give the harness back the model choice
Some harnesses arrive tied to a provider by default. Claude Code defaults to Anthropic; Codex defaults to OpenAI. That choice arrived with the tool rather than with you.
Register a provider and key with Nasiko once, then point a harness at Nasiko instead of at its vendor. Inbound wire protocol and outbound provider are decoupled, so an Anthropic-format request from Claude Code is served by your OpenAI key:
nasiko llm-config create --name my-openai --provider openai --model gpt-4o \
--api-key-secret OPENAI_API_KEY --secret-value "$OPENAI_API_KEY"
nasiko connect claude --config my-openai
The full set of bindings:
nasiko connect claude --config
nasiko connect codex --config
nasiko connect opencode --config
nasiko disconnect # reverse the settings/plugin changes
nasiko status # show current binding
connect claude configures Claude Code's apiKeyHelper and ANTHROPIC_BASE_URL. connect codex
adds a model_providers.nasiko entry and command-based auth to Codex's config.toml. Their
credential helpers obtain a one-hour JWT from POST /api/agents/{id}/llm-token; Codex refreshes
its credential on a timer rather than minting one for every request. Claude Code sends the
credential in the x-api-key header rather than Authorization; the router accepts both.
connect opencode instead installs plugins/nasiko-llm-router.js under OpenCode's config
directory (respecting OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME), registers a nasiko provider,
and makes nasiko/router the default model for new OpenCode sessions only. OpenCode fixes a
session's model at creation time in its own database, so resuming an existing session will not
route it. Start a new one, or pick "Nasiko Router" explicitly.
Inbound wire protocol and outbound provider are fully decoupled: nasiko connect claude --config my-openai-config routes Claude Code's Anthropic-format traffic to OpenAI.
~/.claude/settings.jsonand OpenCode's config are per-user, global files. Connecting an agent affects every Claude Code / OpenCode process on the machine, not just the current project.
LLM config management
nasiko llm-config create --name --provider --model
nasiko llm-config list
nasiko llm-config update [--provider ...] [--model ...]
nasiko llm-config set-default
nasiko llm-config attach --agent # attach to a deployed agent
nasiko llm-config detach --agent
nasiko llm-config get # resolved routing config for an agent
nasiko llm-config providers # valid provider/model values + pricing
Set a config's model for predictable routing. If it is unset, the router falls back through tier
models and then the platform default; list shows provider/? for the unset value.
Server configuration
Everything is env-driven. server/.env.example is the complete annotated
reference — every variable the server reads, grouped, with its default; the root
.env.example is the shorter docker-compose quick start. Two that are easy to
miss:
| Variable | Purpose |
|---|---|
AGENT_JWT_SECRET |
Required for local coding-agent routing, including custom LLM configs. Signs agent identity tokens separately from user-login JWTs and provider credentials. Missing/empty ⇒ /api/agents/{id}/llm-token returns 503; gateway authentication fails closed. |
CODING_AGENT_OTLP_ENDPOINT |
Server-side OTLP/HTTP export for coding-agent telemetry. Compose already sets http://otel-collector:4318; for a host-run server with local infra, set http://localhost:4318 in server/.env. Unset leaves receipts pending without an export worker. |
Features
Three things break first when an agent estate grows: spend nobody can attribute, permissions nobody can enumerate, and a fleet nobody can run as one system. The feature set is grouped accordingly.
TokenOps
| Feature | What it does |
|---|---|
| LLM Router | Agents get an OPENAI_BASE_URL and a short-lived identity token instead of a real key. The router resolves provider, model, and key server-side. No agent and no log ever sees a real API key. |
| Cost and token attribution | Usage and cost are collected from gen_ai.* span attributes and broken out per agent and per model, rather than arriving as a single provider invoice you have to reverse-engineer. |
| One trace per interaction | Every dispatch and proxy hop emits a real OTel span, so a request is one end-to-end trace across every agent hop, with cost attached. |
Policy, security, and governance
| Feature | What it does |
|---|---|
| Single ingress, always proxied | Agents are never publicly reachable. Every agent-to-agent call is proxied through the server, so limits and tracing apply at every hop rather than only the first. |
| Access control, for agents as well as people | User-to-agent ownership and grants, plus an agent-to-agent allowlist. The two gate every proxy call independently. |
| MCP Gateway | One permanent URL gives every agent a merged, permission-filtered view of Composio toolkits and custom MCP servers, without the agent ever holding the credentials. An agent discovers only what it was granted. |
| Flow guards | Redis-backed cascade limits (depth, fan-out, token budget, timeout, cycle detection) stop a loop between two agents from quietly consuming your budget. |
| Encrypted secrets | Per-agent secrets are encrypted at rest with AES-256-GCM and injected only at deploy time, so they stay out of your repo. |
Orchestration
| Feature | What it does |
|---|---|
| Deploy anything that speaks A2A | nasiko deploy builds, pushes to the embedded registry, and runs it. No external registry required, and no SDK to adopt. |
| Intelligent routing engine | A 3-stage pipeline: shortlist by embedding similarity, rerank on conversation context, then an LLM makes the final pick. Callers do not need to know your fleet. |
| Embedded OCI registry | Self-hosted, S3-backed, with layer dedup, so nasiko push and nasiko deploy need nothing external. |
| CLI-first, no lock-in | nasiko new, run, chat, deploy. Bring your own LLM provider, and change it later. |
Why this runs in the call path
There is no version of this that works from the outside, and saying so plainly is the shortest way to explain the architecture below.
Every coding-agent vendor meters in its own private unit, and none of them offers an ingest endpoint for another vendor's usage. There is no invoice reconciliation that produces one number, and no procurement policy that produces one either. To attribute spend across vendors you have to be in the path where the calls happen.
Enforcement has the same shape. A budget cap or a tool allowlist is only real if it sits where the request goes, rather than in a review meeting or in an environment variable a developer can unset.
That constraint is what the next section is a picture of.
Architecture
Nasiko is a single process with no separate gateway. Every inter-agent call is proxied back through the server, the single chokepoint where flow limits, ACLs, and observability are enforced. Durable state lives in Postgres, Redis, and S3 (RustFS), with optional observability via Tempo / Loki / the OTel Collector.
flowchart LR
subgraph Clients["Clients"]
UI["Web Dashboard (embedded)"]
CLI["nasiko CLI"]
end
subgraph CP["nasiko-server (single control-plane process)"]
direction TB
API["REST API (agents, builds, uploads)"]
AUTH["Auth (session JWT, TLS, rate-limit, ACLs)"]
OIDC["OIDC client (SSO)"]
ROUTE["Routing engine (shortlist, rerank, select)"]
PROXY["A2A Proxy (generic agent reverse-proxy)"]
MCP["MCP Gateway (tools/list, tools/call, OAuth)"]
LLM["LLM Router (OpenAI-compatible egress)"]
OCI["Embedded OCI registry (/v2/*)"]
FLOW["Flow guards (depth, fan-out, token budget, cycles)"]
SECRETS["Secrets engine (AES-256-GCM)"]
GITHUB["GitHub App integration"]
end
subgraph Infra["Backing services (Docker)"]
PG[(Postgres)]
RD[(Redis)]
S3[(RustFS S3)]
OTEL["OTel Collector"]
TEMPO["Tempo"]
LOKI["Loki"]
end
subgraph Agents["Agent containers (Docker runtime)"]
A1["Agent A"]
A2["Agent B"]
A3["Agent C"]
end
UI --> API
CLI --> API
CLI -. "push / pull images" .-> OCI
API --> ROUTE
API --> GITHUB
ROUTE --> FLOW
OIDC --> AUTH
SECRETS --> API
AUTH -. gates .-> API
AUTH -. gates .-> MCP
AUTH -. gates .-> OCI
AUTH -. gates .-> PROXY
ROUTE -- "selected agent, direct call" --> Agents
PROXY -. "proxied A2A calls (bypasses routing)" .-> A1
PROXY -. "proxied A2A calls (bypasses routing)" .-> A2
PROXY -. "proxied A2A calls (bypasses routing)" .-> A3
OCI -- "image pull at deploy" --> Agents
Agents -- "OPENAI_BASE_URL" --> LLM
Agents -- "tools/list, tools/call" --> MCP
OCI --> S3
FLOW --> RD
SECRETS --> PG
AUTH --> PG
CP --> OTEL --> TEMPO
OTEL --> LOKI
Every request to an agent is either dispatched by the routing engine or proxied generically, and both paths originate inside the server. Agents never receive a direct, public request, and both call back out into the LLM Router and MCP Gateway rather than holding real API keys or tool credentials.
Requirements
| Component | Minimum version | Why |
|---|---|---|
| Docker Engine + Compose V2 | Compose V2 plugin (the docker compose command, not the legacy standalone docker-compose v1 binary) |
docker-compose.yml uses the extended depends_on: condition: service_healthy syntax; the Docker-only path needs nothing else. |
| Rust | 1.85+ (stable) | The workspace targets edition = "2024" (see [Cargo.toml](Cargo.toml)), stabilized in Rust 1.85, and is only needed for the CLI / Path B developer setup, not the Docker-only path. |
Quick Start: Docker only (no Rust needed)
The fastest way to run Nasiko requires only Docker (with Compose). The server builds itself from source inside Docker.
1. Clone and configure
git clone https://github.com/Nasiko-Labs/nasiko.git
cd nasiko
cp .env.example .env
Edit .env before starting:
SECRETS_ENCRYPTION_KEY: generate withopenssl rand -base64 32(exactly 32 decoded bytes).JWT_SECRET: generate withopenssl rand -base64 48for user-login tokens.AGENT_JWT_SECRET: generate a separate value withopenssl rand -base64 48. Required for local coding-agent LLM routing (nasiko connect claude|codex|opencode --config), including custom LLM configs. Keep this signing secret on the server; clients obtain short-lived tokens.ADMIN_USERNAME/ADMIN_PASSWORD: first-login credentials; replace the demo password.OPENAI_API_KEY: a real provider key for the default OpenAI-backed routing/chat setup. Local coding agents using a custom LLM config can instead use its stored provider credentials; those credentials do not replaceAGENT_JWT_SECRET.
Keep the supplied S3_* credentials aligned with the bundled RustFS service. They are development
credentials, not automatically generated secrets. Do not regenerate SECRETS_ENCRYPTION_KEY on
routine restarts: existing encrypted secrets need the same key.
No manual telemetry URLs are needed for Compose. It sets TEMPO_URL, LOKI_URL,
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_COLLECTOR_ENDPOINT, and CODING_AGENT_OTLP_ENDPOINT, as well as
DATABASE_URL, REDIS_URL, S3_ENDPOINT, MCP_GATEWAY_PUBLIC_URL, LLM_GATEWAY_BASE_URL, and
DOCKER_AGENT_NETWORK. These Compose environment entries override .env values. Local coding
clients connect to http://localhost:8080 (or your reachable server URL), not the Docker-only
http://server:8080 address.
After editing an existing .env, run docker compose up -d to recreate the server with the new
values; docker compose restart alone does not reload its environment.
2. Start the platform
docker compose up -d
This builds the server image and starts the full stack: Postgres · Redis · RustFS (S3) · OTel Collector · Tempo · Loki · nasiko-server.
- First build takes a few minutes (compiles Rust inside Docker), and subsequent builds are fast.
- Open http://localhost:8080 for the dashboard and log in with
ADMIN_USERNAME/ADMIN_PASSWORD(defaultadmin/changeme).
docker compose logs -f server # follow server logs
docker compose down # stop everything
docker compose up -d --build # rebuild after pulling new changes
No Docker? Use the Developer / Rust setup below.
Setup guides by operating system
You have two supported paths:
| Path | Requires | Best for |
|---|---|---|
| A. Docker-only | Docker only | Anyone who just wants to run the platform |
| B. Source / Rust | Rust + just |
Contributors, developers, hot-reload |
Path A: Docker-only
Windows
- Install Docker Desktop -> https://www.docker.com/products/docker-desktop/
- Open Docker Desktop and wait until the engine is running.
- In a terminal (PowerShell or Git Bash):
git clone https://github.com/Nasiko-Labs/nasiko.git
cd nasiko
Copy-Item .env.example .env
# edit .env -> set OPENAI_API_KEY and ADMIN_PASSWORD
docker compose up -d
- Open http://localhost:8080 and log in.
Windows troubleshooting: see the Troubleshooting section (port conflicts, line endings, encryption key, WSL, Docker Desktop).
macOS
- Install Docker Desktop for Mac -> https://www.docker.com/products/docker-desktop/
- Open Docker Desktop until the engine is running.
- In Terminal:
git clone https://github.com/Nasiko-Labs/nasiko.git
cd nasiko
cp .env.example .env
# edit .env -> set OPENAI_API_KEY and ADMIN_PASSWORD
docker compose up -d
- Open http://localhost:8080 and log in.
host.docker.internalresolves out of the box on Docker Desktop (macOS + Windows), so agents can reach the MCP gateway without extra setup.
Linux
- Install Docker engine + Compose plugin -> https://docs.docker.com/engine/install/
- Add your user to the
dockergroup and re-login:
sudo usermod -aG docker "$USER"
newgrp docker
- In a terminal:
git clone https://github.com/Nasiko-Labs/nasiko.git
cd nasiko
cp .env.example .env
# edit .env -> set OPENAI_API_KEY and ADMIN_PASSWORD
docker compose up -d
- Open http://localhost:8080 and log in.
Linux note: native Docker does not provide
host.docker.internalautomatically. If agents report[Errno -2] Name or service not known, run Docker with--add-host host.docker.internal:host-gatewayor setMCP_GATEWAY_PUBLIC_URLto the bridge IP (see Troubleshooting).
Toolchain setup: Rust, just, etc. (for the CLI / Path B)
Windows
# 1. Rust (installs rustup + stable toolchain)
winget install --id Rustlang.Rustup -e
# (reopen your terminal, then verify)
rustc --version; cargo --version
# 2. `just` command runner + cargo-watch (after Rust is installed)
cargo install just cargo-watch
# 3. If you plan to build native Windows binaries, also install the C++ linkers:
winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
Building the CLI does not require the C++Build Tools, since it uses a pure-Rust toolchain. The C++ linkers are only needed if native crates (e.g.
ring) fail to link on the MSVC toolchain.
macOS
# 1. Xcode Command Line Tools (provides the C toolchain/linker)
xcode-select --install
# 2. Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# 3. `just` + cargo-watch
cargo install just cargo-watch
Linux (Debian/Ubuntu)
# 1. Build dependencies (cc, OpenSSL)
sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev
# 2. Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# 3. `just` + cargo-watch
cargo install just cargo-watch
# 4. Docker engine + compose (if not using Docker Desktop)
# See https://docs.docker.com/engine/install/ubuntu/
After setup, verify everything:
rustc --version && cargo --version
just --version
docker --version && docker compose version
Path B: Developer / Rust setup
Requires Rust (rustup), **[just](https://github.com/casey/just)**
(cargo install just), and Docker.
# 1. Start infrastructure only (Postgres, Redis, RustFS, OTel stack)
just infra
# 2. Configure the server env
cp server/.env.example server/.env
# edit server/.env -> set OPENAI_API_KEY at minimum
# 3. Run the server natively (hot-reload)
just dev
# ...or without hot-reload:
just run
The server runs on http://localhost:8080. just dev auto-rebuilds on changes (needs
[cargo-watch](https://github.com/watchexec/cargo-watch)).
Useful dev commands:
just check # cargo check --workspace
just clippy # lint (zero-warnings policy)
just test-unit # fast hermetic unit tests (no infra needed)
just test # unit + integration tests (needs: just infra)
Building from source:
cargo build --release -p nasiko # CLI binary
cargo build --release -p nasiko-server # Server binary
Deploying your own agents
Past the coding agents you already run, Nasiko deploys agents you write yourself. They can be in
Python, Rust, Go, or TypeScript. At runtime they must speak
A2A v1.0 exactly; source deployments also include an
AgentCard.json and Dockerfile for metadata and packaging. Nasiko hardcodes the
A2A-Version: 1.0 header on every outbound A2A request, so the target agent must accept the v1.0
wire format. See [docs/A2A_PROTOCOL.md](docs/A2A_PROTOCOL.md) for the detail. There is no
proprietary agent format and no SDK to adopt.
This path needs Rust to build the CLI from source (it is a separate cli/ crate).
Docker is required for local image builds; nasiko upload
instead sends source to the server to build and deploy without local Docker.
Install the CLI
# from the repo root
cargo install --path cli/ --force
# ...or build a standalone binary
cargo build --release -p nasiko
Add it to your PATH if it is not already (Cargo's bin dir: ~/.cargo/bin).
Deploy your first agent
nasiko connect http://localhost:8080
nasiko auth login # log in with ADMIN_USERNAME / ADMIN_PASSWORD
nasiko new openai my-agent && cd my-agent # scaffold from a template
nasiko deploy . # build, push, and deploy
nasiko chat "Hello there" # talk to your agent (message must contain a space,
# or use --agent: nasiko chat --agent my-agent "Hello")
You can also deploy agents directly from the dashboard UI: upload source, import from GitHub, or pull from the artifact registry.
Handy CLI commands
| Command | Description |
|---|---|
nasiko connect |
Register a control plane and switch to it |
nasiko auth login |
Authenticate with the active cluster |
nasiko new [template] [name] |
Scaffold a new agent project |
nasiko build / nasiko run |
Build the agent image / build + run it locally |
nasiko push / nasiko deploy |
Push image / build-push-deploy to the cluster |
nasiko upload [source] |
Upload source; the server builds it (no local Docker) |
nasiko ps |
List running agents |
nasiko logs -f |
Stream (and follow) agent logs |
nasiko stop / start / restart / scale |
Agent lifecycle |
nasiko rm --name |
Terminate + deregister an agent (positional id only accepts a UUID) |
nasiko chat |
Interactive or one-shot A2A chat |
nasiko secrets set |
Configure encrypted per-agent secrets |
nasiko mcp |
Manage MCP Gateway connectors and tool permissions |
nasiko observe |
Observability: sessions, traces, spans, stats, FinOps |
nasiko maf |
Multi-agent flow workflows (create/run/inspect) |
nasiko registry |
Browse the artifact registry |
nasiko github |
GitHub integration (status/repos/connect/disconnect/clone) |
Run nasiko --help for the full, workflow-ordered command list.
Give your agent MCP tools
Every deployed agent can call tools through the MCP Gateway, a single JSON-RPC endpoint that merges Composio toolkits and your own MCP servers into one permission-filtered catalog. The agent never holds a third-party credential: it authenticates to the gateway, and the gateway calls the tool with the user's stored connection.
A complete working example of everything in this section and the next is in
agents/general-assistant-1.1.1.zip. It's a Pythona2a-sdkagent that uses whatever MCP tools it is granted, asks the user instead of guessing, and pauses for tool approvals. Deploy it withnasiko upload agents/general-assistant-1.1.1.zip. Then typehitl input test,hitl auth test,hitl options testorhitl multiselect testto check the HITL wiring without an LLM key. Version 1.1.1 pinsopentelemetry-util-genai==1.1b0: version 1.2b0 breaks the OpenAI instrumentor's import, leaving request traces visible but LLM token usage missing from TokenOps.
1. What the platform injects. At deploy time every agent container gets two env vars
(when MCP_GATEWAY_PUBLIC_URL is set on the server, which compose does for you):
| Env var | Meaning |
|---|---|
MCP_GATEWAY_URL |
The gateway's JSON-RPC endpoint (/api/mcp) |
MCP_GATEWAY_TOKEN |
This agent's own gateway credential, minted at deploy. Send it as a Bearer token. |
2. Make the agent a client. Plain JSON-RPC over HTTP is enough, no MCP SDK required. Two
methods: tools/list (discover the catalog at runtime) and tools/call (invoke one).
import os, uuid, httpx
async def mcp(client, method, params=None, traceparent=None):
headers = {"Authorization": f"Bearer {os.environ['MCP_GATEWAY_TOKEN']}"}
if traceparent:
headers["traceparent"] = traceparent # required, see below
body = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
if params is not None:
body["params"] = params
resp = (await client.post(os.environ["MCP_GATEWAY_URL"], json=body, headers=headers)).json()
if "error" in resp:
err = resp["error"]
raise McpError(err["code"], err["message"], err.get("data") or {})
return resp["result"]
tools = (await mcp(client, "tools/list", traceparent=tp))["tools"]
result = await mcp(client, "tools/call", {"name": tool_name, "arguments": args}, tp)
Each tools/list entry has name, description and an inputSchema (JSON Schema), so it maps
1:1 onto an OpenAI/Anthropic function-tool definition. Hand the list to your LLM and route any
tool call it makes back through tools/call.
3. Forward the inbound traceparent, untouched. The gateway authorizes tools/call by
checking that your agent is a participant in the live flow the trace id names. Without the right
traceparent, every call is rejected with 403, even with a valid token. Read the header from the
A2A request you are serving and send that exact value. Two gotchas, both seen in practice:
- OTel
httpxauto-instrumentation overwritestraceparentwith one from the ambient span, which is not the inbound flow's span, so calls 403 with "does not resolve to a live flow". Suppress instrumentation for the gateway request only:from opentelemetry.context import attach, detach, set_value from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: resp = await client.post(url, json=body, headers=headers) finally: detach(token) - Read
traceparentper request, not from a ContextVar. Ina2a-sdk, a resumed task runs on the worker created by the original request, so a ContextVar holds a stale trace id. Read it fromcontext.call_context.state["headers"]["traceparent"]insideexecute().
4. Handle the gateway's error codes. These are what make a tool call pause instead of fail
(codes from mcp-gateway/src/types.rs):
| Code | Meaning | What the agent should do |
|---|---|---|
-32000 |
TOOL_BLOCKED: the tool's stance for this agent is block |
Return the error to the LLM as a normal tool result |
-32001 |
TOOL_ASK: the stance is ask, and a human has not approved it yet |
Pause with auth_required (see HITL) |
-32002 |
AUTH_REQUIRED: the connector's own credential is missing or broken |
Pause with auth_required |
For the two pause codes, error.data carries the gateway's own HITL request id:
hitl_request_id, or hitl_request_ids (a list) for a batched call such as Composio's
COMPOSIO_MULTI_EXECUTE_TOOL. Read both keys.
5. Grant the agent access. An agent only sees the tools it was granted:
nasiko mcp catalog # what can be connected
nasiko mcp connect --toolkit github # connect your account (OAuth/API key)
nasiko mcp agent-tools connectors my-agent # what my-agent can see
nasiko mcp agent-tools enable my-agent # turn a connector on for my-agent
nasiko mcp agent-tools set-rule my-agent "GITHUB_DELETE_*" ask # allow | ask | block
nasiko mcp agent-tools tools my-agent # catalog + effective stance per tool
Bring your own MCP server with nasiko mcp connector register (a URL), or upload /
upload-github to have the platform build and host it. See
docs/MCP_GATEWAY_DESIGN.md for gateway internals.
Human-in-the-loop (HITL)
An agent can stop mid-task and wait for a person, then resume the same task with their answer.
This is plain A2A with no Nasiko SDK: the agent ends its turn in one of two paused task states, and
the platform does the rest. It records a pending request, shows it in the chat UI and
nasiko chat, and sends the human's reply back to the agent as the next message on that task.
| A2A state | Use it when |
|---|---|
TASK_STATE_INPUT_REQUIRED |
The agent is missing information it shouldn't guess (a name, date, amount, recipient...) |
TASK_STATE_AUTH_REQUIRED |
A human must grant something first: a tool approval (TOOL_ASK) or a connector re-auth |
1. Pause. With a2a-sdk (Python), emit the state with a message and optional metadata:
await updater.update_status(
TaskState.TASK_STATE_INPUT_REQUIRED,
message=updater.new_agent_message([new_text_part("Which repo should I file this in?")]),
metadata={"messages": llm_history, "pending_tool_call_id": call.id}, # your resume state
)
The message text becomes the question shown to the human. metadata is persisted on the task and
handed back to you on resume, so store everything you need to continue there (the LLM conversation
so far, the pending tool call). Use a persistent task store if the agent can restart while a task
is paused, because InMemoryTaskStore loses it.
Some metadata keys are hoisted onto the question so clients can render them:
| Key | Effect |
|---|---|
auth_url, provider |
auth_required: the client shows an "Authorize with provider" link |
expected_input |
input_required: hint about the expected answer |
options, header, multi_select, allow_custom_input |
input_required: show selectable choices instead of a free-text box (see below) |
hitl_request_id |
auth_required that mirrors a gateway TOOL_ASK / AUTH_REQUIRED: links the pause to the gateway's request, so approving it actually grants the tool |
2. Resume. The human's reply arrives as a new message on the same task id.
context.current_task is the stored task. Its status.state is still the paused state, and its
metadata is what you saved. Branch on that state:
stored = context.current_task
if stored and stored.status.state == TaskState.TASK_STATE_INPUT_REQUIRED:
meta = MessageToDict(stored.metadata) # metadata comes back as a protobuf Struct
history = meta["messages"] + [{"role": "tool",
"tool_call_id": meta["pending_tool_call_id"],
"content": user_text}]
# ...continue the LLM loop with `history`
elif stored and stored.status.state == TaskState.TASK_STATE_AUTH_REQUIRED:
# retry the exact tool call the human just approved; pause again if it is still TOOL_ASK
...
For an auth_required pause, the reply text is confirmed or denied. Don't treat any other
value as approval.
Selectable options (optional). Add these to input_required metadata to give the human
clickable choices:
metadata = {
"header": "Format",
"options": [
{"label": "Summary", "description": "Brief overview"},
{"label": "Detailed"},
],
"multi_select": False, # default false
"allow_custom_input": True, # also allow a typed answer; default false
}
On resume you get back the chosen label verbatim. For multi-select, each picked label (and any
custom text) is on its own line. Labels must be non-empty and unique, with at most 20 options. A
malformed block is dropped and the pause falls back to a plain text question, with a server-side
warning (types/src/a2a.rs).
Making an LLM agent pause reliably. Give the model a synthetic ask_human(question, options?)
tool and a finish_task(message) tool alongside the MCP tools, and call it with
tool_choice="required". With "auto", models often ask a clarifying question as plain chat
text, which you can't tell apart from a final answer. When every turn must be a tool call, "paused"
and "done" are told apart by the tool called, not by guessing at the text:
ask_human → input_required, gateway TOOL_ASK/AUTH_REQUIRED → auth_required,
finish_task → complete.
Answering a pause outside the chat UI. nasiko chat prompts for pauses inline. For other
clients, pending requests are at GET /api/hitl/pending and GET /api/hitl/{id}, and are answered
with POST /api/hitl/{id}/resolve. The body depends on the request kind:
| Kind | Resolve body |
|---|---|
input_required |
{"answer": "acme/api"}; for multi-select, {"answer": ["Intro", "Security"], "custom_answer": "..."} |
auth_required |
{"auth_action": "start"}, then {"auth_action": "confirm"} after authorizing |
tool approval (TOOL_ASK) |
{"decision": "approve" | "reject", "scope": "once" | "session"} |
Environment Variables
Configuration is env-driven; startup-required keys are validated separately from feature-specific
requirements such as local coding-agent routing. See .env.example for the Compose
quick start and server/.env.example for the detailed reference.
docker-compose.yml supplies infrastructure and telemetry URLs and the agent network; do not
replace those with host-local addresses in the Compose setup.
| Variable | Purpose | Default |
|---|---|---|
OPENAI_API_KEY |
LLM provider for the router + agents | optional (sk-...) |
SECRETS_ENCRYPTION_KEY |
Base64 32-byte AES-256-GCM key | required |
ADMIN_USERNAME / ADMIN_PASSWORD |
Bootstrap admin account | admin / changeme |
JWT_SECRET |
JWT signing secret | required |
S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEY / S3_REGION |
S3 settings; credentials must match RustFS | supplied by .env.example; S3_SECRET_KEY required |
OCI_STORAGE_BUCKET |
Bucket for embedded OCI registry blobs/manifests | nasiko-artifacts (no override needed) |
AGENT_RUNTIME |
Container runtime (docker in OSS) |
docker |
DATABASE_URL / REDIS_URL / S3_ENDPOINT |
Infra connections | set by compose |
COMPOSIO_API_KEY |
Composio platform (MCP toolkits) | optional |
SEED_TOOLKITS |
Composio toolkits to auto-register at boot | optional |
MCP_GATEWAY_PUBLIC_URL |
Public URL injected into agents for the MCP gateway | set by compose |
SEED_AGENTS |
Space-separated images auto-deployed at boot | optional |
AGENT_JWT_SECRET |
Signs agent LLM-router identity tokens, including custom config routing | required for local coding-agent routing; generate your own |
CODING_AGENT_OTLP_ENDPOINT |
Server-side OTLP/HTTP export for coding-agent telemetry | set by Compose; host-run server must set it |
ROUTER_MODEL / EMBEDDING_MODEL |
Routing-engine models | see config/ |
NASIKO_FLOW_MAX_DEPTH / NASIKO_FLOW_MAX_FAN_OUT / NASIKO_FLOW_MAX_TOKENS |
Flow-guard cascade limits | see config/ |
Project Structure
server/ Control plane: Axum routes, auth, agent proxy, build worker, embedded UI
orchestrator/ Routing engine: semantic agent selection (shortlist, rerank, select)
mcp-gateway/ MCP Gateway: connectors, tool aggregation, per-agent permissions, OAuth
llm-router/ Provider-agnostic OpenAI-compatible egress proxy for agent LLM calls
runtime/ ContainerRuntime trait + DockerRuntime (bollard)
auth/ AuthService trait + OSS implementation (JWT login, RBAC hooks)
flow/ FlowGuard: anti-DoS cascade limits + live flow events
secrets/ AES-256-GCM encryption for agent secrets at rest
oci/ Embedded OCI Distribution v2 registry (S3-backed, layer dedup)
observability/ OTel init, Tempo/Loki clients, DB-backed model pricing
agent-proxy/ Agent ID -> running-container endpoint resolution
github/ GitHub OAuth + repo import for source-based deploys
types/ A2A protocol + registry types
config/ Single env-driven Config struct
utils/ Shared helpers
cli/ nasiko binary (agent developer CLI, sync HTTP via ureq)
agents/ Example and seed agents (each a standalone A2A container)
migrations/ Postgres migrations (sqlx, run automatically at startup)
ui/ Frontend (vanilla JS web components, embedded in the server binary)
docs/ Design docs (architecture, protocol, conventions)
Troubleshooting
Quick fixes: one command
| Problem | One command |
|---|---|
CLI won't compile: link.exe not found / cc not found (Windows) |
Use the Docker-only path, or winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" then reopen the terminal |
CLI won't compile: dlltool ... Invalid bfd target |
winget install MSYS2.MSYS2 then add C:\msys64\mingw64\bin to PATH ahead of C:\MinGW, or switch to MSVC |
: command not found when sourcing .env |
sed -i 's/\r$//' server/.env |
invalid SECRETS_ENCRYPTION_KEY at startup |
sed -i.bak "s/^SECRETS_ENCRYPTION_KEY=.*/SECRETS_ENCRYPTION_KEY=$(openssl rand -base64 32)/" .env |
address already in use on ports 9000/4317/4318 |
Stop Docker Desktop, then wsl --shutdown (Windows) and rerun docker compose up -d |
permission denied on Docker socket |
sudo usermod -aG docker "$USER" && newgrp docker (*nix/WSL) |
WSL ext4.vhdx: path not found |
wsl --unregister Ubuntu && wsl --install -d Ubuntu |
Agent upload -> 500 agents_owner_id_fkey |
Log out and back in, or docker compose down -v && docker compose up -d then log in fresh |
| Server can't reach Postgres | docker compose up -d and wait for healthy |
Agent Name or service not known (Linux Docker) |
Recreate with --add-host host.docker.internal:host-gateway |
SEED_TOOLKITS is set but COMPOSIO_API_KEY is not at startup |
SEED_TOOLKITS is optional and commented out in the template. If you enable it, also set COMPOSIO_API_KEY, or leave both unset if Composio is not needed. |
Windows
| Symptom | Cause / fix |
|---|---|
link.exe not found / linker 'cc' not found when building the CLI |
MSVC C++Build Tools not installed. Use the Docker-only path (no Rust), or install VS Build Tools with the "Desktop development with C++" workload. |
error: dlltool ... Invalid bfd target |
A broken 32-bit MinGW (C:\MinGW) cannot build 64-bit. Install a real 64-bit MinGW-w64 (e.g. MSYS2) or switch to the MSVC toolchain. |
: command not found when sourcing .env |
Windows line endings (CRLF) break bash source. Convert: sed -i 's/\r$//' server/.env |
invalid SECRETS_ENCRYPTION_KEY ... Invalid padding |
Invalid key in .env. Generate one: openssl rand -base64 32 |
address already in use on ports 9000/4317/4318 |
Two Docker engines fighting (Docker Desktop + WSL native). Keep one; run wsl --shutdown, reopen, docker compose up -d |
permission denied ... Docker daemon socket (inside WSL) |
Add user to docker group: sudo usermod -aG docker $USER, then re-login |
Wsl ... ext4.vhdx: path not found |
Corrupt WSL distro. wsl --unregister Ubuntu then wsl --install -d Ubuntu |
Agent upload -> 500 / agents_owner_id_fkey |
Stale login token from an old DB. Log out, log back in (or docker compose down -v + fresh login) |
macOS
| Symptom | Cause / fix |
|---|---|
linker 'cc' not found |
xcode-select --install (Command Line Tools) missing |
permission denied ... Docker daemon |
Start Docker Desktop and wait for the engine |
address already in use |
Another process on ports 9000/4317/4318. lsof -i :9000 to find it. |
Linux
| Symptom | Cause / fix |
|---|---|
permission denied ... Docker socket |
sudo usermod -aG docker $USER then log out/in (or newgrp docker) |
error: linker 'cc' not found (building CLI) |
Missing build tools: sudo apt install -y build-essential pkg-config libssl-dev |
Agent [Errno -2] Name or service not known |
host.docker.internal is not provided by native Docker. See the Linux note in Path A, or set MCP_GATEWAY_PUBLIC_URL to the bridge IP |
First cargo build very slow |
Normal, it compiles the whole workspace. Prefer a native clone over a mounted/9p filesystem. |
All platforms
| Symptom | Fix |
|---|---|
failed to connect to Postgres at startup |
Infra is not up yet. Run docker compose up -d (or just infra) and wait for healthy |
docker: command not found |
Docker not installed/running. Install Docker. |
| Dashboard will not load | Verify docker compose ps shows server as Up; open http://localhost:8080 |
Project Activity
Documentation & Links
- Official docs: docs.nasiko.com — guides, API reference, and concepts
- Design docs:
[docs/](docs/): architecture, the A2A protocol, agent lifecycle, MCP Gateway internals, CLI design, networking - A2A protocol: https://github.com/a2aproject/a2a-spec
- Rust toolchain: https://rustup.rs
- Docker: https://docs.docker.com/get-docker/
**justcommand runner**: https://github.com/casey/just**cargo-watch**(hot-reload): https://github.com/watchexec/cargo-watch- Versus shields: https://shieldcn.dev (premium README badges & charts)
Support
Questions, ideas, or stuck on setup? Join the community on Discord:
Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, code conventions, and the PR flow.
License
Apache-2.0. See [LICENSE](LICENSE).
Built with love by the Nasiko team. Stars, issues, and PRs are always welcome.