← Open Source
Nasiko-Labs

nasiko

The Open Runtime for AI Agents

InfrastructureDeploy & ServeOrchestrationRust
Open on GitHub
Momentum
+2stars in 24 hours+0.0%
9.41k
Stars
2.07k
Forks
+400
This week
30
Contributors
Created 2026-02-12 · Updated 2026-10-05 · #2404 today
Top developers
README

Nasiko

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

GitHub stars GitHub forks Latest release License: Apache-2.0

Built with Rust Open issues Pull requests CI PRs Welcome

Table of Contents

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

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 discover is not listed by nasiko agents --help, reinstall with the --force command above. Older builds use the same 0.1.0 version number, so nasiko --version alone 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.json and 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 with openssl rand -base64 32 (exactly 32 decoded bytes).
  • JWT_SECRET: generate with openssl rand -base64 48 for user-login tokens.
  • AGENT_JWT_SECRET: generate a separate value with openssl 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 replace AGENT_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 (default admin / 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

  1. Install Docker Desktop -> https://www.docker.com/products/docker-desktop/
  2. Open Docker Desktop and wait until the engine is running.
  3. 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
  1. Open http://localhost:8080 and log in.

Windows troubleshooting: see the Troubleshooting section (port conflicts, line endings, encryption key, WSL, Docker Desktop).

macOS

  1. Install Docker Desktop for Mac -> https://www.docker.com/products/docker-desktop/
  2. Open Docker Desktop until the engine is running.
  3. 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
  1. Open http://localhost:8080 and log in.

host.docker.internal resolves out of the box on Docker Desktop (macOS + Windows), so agents can reach the MCP gateway without extra setup.

Linux

  1. Install Docker engine + Compose plugin -> https://docs.docker.com/engine/install/
  2. Add your user to the docker group and re-login:
 sudo usermod -aG docker "$USER"
 newgrp docker
  1. 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
  1. Open http://localhost:8080 and log in.

Linux note: native Docker does not provide host.docker.internal automatically. If agents report [Errno -2] Name or service not known, run Docker with --add-host host.docker.internal:host-gateway or set MCP_GATEWAY_PUBLIC_URL to 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 Python a2a-sdk agent that uses whatever MCP tools it is granted, asks the user instead of guessing, and pauses for tool approvals. Deploy it with nasiko upload agents/general-assistant-1.1.1.zip. Then type hitl input test, hitl auth test, hitl options test or hitl multiselect test to check the HITL wiring without an LLM key. Version 1.1.1 pins opentelemetry-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 httpx auto-instrumentation overwrites traceparent with 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 traceparent per request, not from a ContextVar. In a2a-sdk, a resumed task runs on the worker created by the original request, so a ContextVar holds a stale trace id. Read it from context.call_context.state["headers"]["traceparent"] inside execute().

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

Issues over time

GitHub stars Total commits Pull requests

Documentation & Links

Support

Questions, ideas, or stuck on setup? Join the community on Discord:

discord.com/invite/HmnfkTfjFv

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.