Sign inSign up

javdet/nib-backend

By javdet

•Updated 15 days ago

NIB backend — Go API server and agent orchestrator for infrastructure planning & execution

Image
Integration & delivery
Machine learning & AI
Developer tools
0

3.8K

javdet/nib-backend repository overview

⁠NIB — backend

API server and agent orchestrator for NIB (Neuro Infrastructure Builder) — a tool for planned infrastructure work. You describe a task; NIB decomposes it into stages, works each stage out into elementary steps (code changes, commands, clicks in a web console), derives the verification checks and the rollback plan, and then either executes the steps itself or hands the operator instructions detailed enough to follow without guessing.

This image is the engine of the stack. It runs the agent loop against an OpenAI-compatible LLM endpoint, keeps plans and chats in PostgreSQL, searches the knowledge base through pgvector, calls out to remote MCP servers for tools, and launches agent containers on the host Docker daemon or as Kubernetes Jobs.

It is one of four images: nib-backend (this one), nib-frontend⁠ (the web UI), nib-kb⁠ (knowledge-base MCP server) and nib-agent⁠ (the coding-agent runner).


⁠How it works

Work happens in modes, and the backend implements all of them:

  • Main — the orchestrator, and the only mode a plan starts in. It does no work itself: it decides which specialist the request needs, launches it as a sub-agent, relays questions back to the operator and reports each result in the same chat.
  • Decompose — breaks the task into stages. Input can be a chat description or an issue pulled from a tracker over MCP.
  • Plan — fans out one sub-agent per stage of the DAG, planning in parallel, then runs one more that derives the rollback from all of them.
  • Execute — carries the steps out. One execution at a time, guarded by a database lease; force-stop from the action row or by asking in the chat.
  • Discuss — free-form conversation. The only mode whose system prompt is editable at runtime.
  • Incident — deep root-cause analysis (in development).

Around those sit the things that make the agent's output specific to your estate: rules (your accepted principles per infrastructure component, injected into detailed planning when the agent identifies the relevant systems), skills (markdown playbooks the agent loads on demand), variables and secrets (encrypted at rest, substituted into MCP server definitions only at call time) and the knowledge base.


⁠Quick start

The backend needs PostgreSQL with the pgvector extension. On its own:

docker run -d --name nib-backend \
  -p 8080:8080 -p 9090:9090 \
  -e DB_HOST=postgres -e DB_PORT=5432 \
  -e DB_USER=nib -e DB_PASSWORD=nib -e DB_NAME=nib -e DB_SSLMODE=disable \
  -e LLM_API_KEY="$LLM_API_KEY" \
  -e SECRETS_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
  -v nibdata:/app/data \
  javdet/nib-backend:latest

The whole stack — Postgres, backend, frontend, knowledge base — comes up together with the Compose file in the repository:

git clone https://github.com/javdet/nib && cd nib
cp .env.example .env          # set LLM_API_KEY
docker compose up -d          # UI on http://localhost:8080

or with the bundled Helm chart:

helm install nib ./deploy/helm/nib -n nib --create-namespace \
  --set secrets.llmApiKey="$LLM_API_KEY"

Database migrations from /app/migrations are applied on startup — no separate migration step.


⁠Configuration

Configuration is a config.yaml (baked into the image at /app/config.yaml, mount your own over it) with environment variables taking precedence. That order is deliberate: Compose and Helm pass METRICS_*, DB_* and SERVER_* as env, and a stale config.yaml left on the data volume must not win over them.

⁠Server, database, storage
VariableDefaultPurpose
SERVER_HOST / SERVER_PORT0.0.0.0 / 8080REST API listener
DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME / DB_SSLMODE—PostgreSQL connection; the database needs pgvector
MIGRATIONS_PATH/app/migrationsSQL migrations applied at startup
DATA_DIR/app/dataPersistent state — see the volume layout below
SEED_DIR/app/seedDefaults copied into DATA_DIR on first start
LOG_LEVELinfodebug logs full tool-call arguments and responses
⁠LLM

The backend talks to a single OpenAI-compatible endpoint for both chat and embeddings.

VariablePurpose
LLM_API_KEY (fallback OPENAI_API_KEY)API key
LLM_BASE_URLe.g. https://api.openai.com/v1, https://openrouter.ai/api/v1, https://generativelanguage.googleapis.com/v1beta/openai/
LLM_MODEL (fallback OPENAI_MODEL)Model id as the provider spells it (OpenRouter uses provider/model slugs)
LLM_APIresponses (/v1/responses) or chat (/v1/chat/completions). Some models reject function tools on chat completions unless reasoning is off, so tools + reasoning needs responses; use chat for gateways whose responses support is absent or unverified
LLM_REASONING_EFFORTnone, minimal, low, medium, high, xhigh
LLM_PROVIDERProvider preset / OpenRouter fallback behaviour
OPENAI_EMBEDDING_MODELEmbedding model for knowledge-base search. Must match kb_collections.embedding_model exactly — it is compared as a string

Details, including the OpenRouter fallback, are in docs/llm-providers.md.

⁠Executor and agent containers
VariablePurpose
DOCKER_HOSTLocal executor — set to unix:///var/run/docker.sock and mount the socket
EXECUTOR_LLM_API_KEY / EXECUTOR_LLM_MODELCredentials and model handed to the agent container (may differ from the orchestrator's)
EXECUTOR_GIT_API_TOKENToken the agent uses to push branches and open PRs
AGENT_WEBHOOK_TOKENShared secret authenticating the agent's callback to /api/v1/agent-runner/webhook

Executor type is chosen in Settings → Executor: Disabled (default, Execute action greyed out on code steps), Local (Docker socket), or Remote → Kubernetes. For Kubernetes the backend renders backend/internal/executor/templates/job.yaml.tmpl (override it by dropping your own at {DATA_DIR}/job.yaml.tmpl), authenticating either from a kubeconfig / in-cluster service account or from an API-server URL plus a bearer token held in Variables → Secrets.

⁠Secrets, OAuth, integrations
VariablePurpose
SECRETS_ENCRYPTION_KEYEncrypts the secrets stored in the database. Set it before storing anything; losing it means losing the stored secrets
OAUTH_CALLBACK_BASE_URLMust match the origin the browser reaches the UI on
FRONTEND_BASE_URLWhere to send the user back after an OAuth round trip
ATLASSIAN_CLIENT_ID / ATLASSIAN_CLIENT_SECRET / ATLASSIAN_MCP_URLJira/Confluence over MCP
⁠Agent loop budgets (config.yaml, agent: section)
KeyDefaultMeaning
maxIterations30Completion rounds in the main agent loop; tool calls re-trigger the LLM until it returns none
planFanoutConcurrency4Stage sub-agents planning in parallel
stageMaxIterations15Round budget for one stage sub-agent
planFanoutTimeoutMinutes60Deadline for a whole fan-out, which outlives the request that started it
actionExecConcurrency1Clamped to 1 by design — two agents changing the same infrastructure can undo each other's work, and an operator watching one chat cannot follow two
actionExecMaxIterations20Round budget for one action execution
actionExecTimeoutMinutes30Deadline for one action run

⁠Volume layout

/app/data is declared as a VOLUME, so even a bare docker run gets persistent storage. The entrypoint builds the layout at runtime, because the volume starts empty on a fresh install:

/app/data
├── logs/            # log.file writes here; the backend does not create this dir itself
├── rules/           # infrastructure rules, editable in the UI
├── skills/          # {name}.md playbooks with name/description frontmatter
├── prompts/         # runtime override for the Discuss system prompt (discuss.md)
├── tools/           # per-mode allow lists + action-plan tool schemas
├── knowledgebase/   # last uploaded source document per collection
└── mcp.json         # remote MCP server definitions

Seeding is copy-if-absent and recorded, so a skill you edited, renamed or deleted stays that way across restarts and upgrades, while a skill added in a later release still lands on an existing install. Per-mode tool allow lists are not copy-if-absent — they are embedded in the binary and reconciled at startup, because copy-if-absent left newly shipped tools dead on upgraded installs.

Mount a named volume or a PVC. Bind-mounting single files inside DATA_DIR will break: the backend rewrites them with a temp file plus rename(2), which fails with EBUSY on a bind-mounted file.


⁠Ports, health, metrics

PortPurpose
8080REST API under /api/v1 (plans, chats, DAG, rules, skills, secrets, variables, MCP servers, executor config, OAuth callbacks, the agent-runner webhook)
9090Prometheus metrics at /metrics

Healthcheck: curl -sf http://localhost:8080/api/v1/health, 15 s interval with a 60 s start period. Version is served at GET /api/v1/version and shown at the bottom of the UI sidebar.

Metrics live on a separate listener rather than a route on the API: both the chart's ingress and the frontend's nginx proxy only the /api prefix, so /metrics stays unreachable from the public host while an in-cluster scraper on the pod port still gets it. It is unauthenticated — the metric names alone reveal plan counts, the chosen model and accumulated LLM cost — so keep the port internal. The chart can render a ServiceMonitor (only when monitoring.coreos.com/v1 exists in the cluster) or you can use prometheus.io/scrape pod annotations.

Exposed families include the Go and process collectors plus:

nib_build_info, nib_start_time_seconds, nib_db_up, nib_db_ping_duration_seconds
nib_http_requests_total, nib_http_request_duration_seconds, nib_http_requests_in_flight
nib_plans, nib_plan_status_transitions_total, nib_plans_refresh_*
nib_agent_turns_total, nib_agent_rounds, nib_agent_tool_calls_total, nib_agent_tool_failures_total
nib_llm_requests_total, nib_llm_request_duration_seconds, nib_llm_embedding_tokens_total
nib_plan_fanout_*, nib_action_exec_*, nib_executor_runs_total, nib_agent_runner_cost_usd_total
nib_execution_lease_held, nib_execution_lease_acquisitions_total, nib_execution_force_stops_total
nib_mcp_tools_discovered, nib_db_pool_*

Plans-by-status costs one query plus one plan-state read per plan, so it refreshes on an interval (metrics.refreshSeconds, default 30 s) rather than on every scrape. METRICS_PORT must differ from SERVER_PORT or the backend refuses to start. Full reference: docs/metrics.md.


⁠Image contents

  • nib — the API server and agent orchestrator
  • toolcatalog — MCP tool indexing helper
  • /app/migrations, /app/config.yaml, /app/seed (default skills, tool schemas, mcp.json)
  • Runtime base debian:bookworm-slim rather than Alpine, with bash, curl, jq, dnsutils, iproute2, netcat-openbsd, openssl, procps and telnet — an infrastructure tool whose container you cannot debug from the inside is a poor trade for a few MB
  • Static CGO_ENABLED=0 Go binaries, cross-compiled natively for each target arch (no QEMU)
  • Version stamped at build time into internal/version

⁠Tags and platforms

latest · vX.Y.Z (release, matching the repository's VERSION file) · <git-sha> (exact commit). Built for linux/amd64 and linux/arm64.

Pin vX.Y.Z in production and keep backend, frontend and kb on the same tag — they are released together from one repository.


  • Source, documentation and issues: https://github.com/javdet/nib⁠
  • Helm chart and Compose files: deploy/ and docker-compose.yml in the repository
  • Licence: Nib Non-Commercial License 1.0 (MIT-based, with attribution and a non-commercial-use restriction). Read LICENSE before deploying this in a commercial setting.

Tag summary

Content type

Image

Digest

sha256:707ef4408…

Size

77 MB

Last updated

15 days ago

docker pull javdet/nib-backend