Skip to content
adhar-ioPublic

About

Adhar AI Repository

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

31 Commits

Folders and files

Repository files navigation

Adhar β€” Open Cloud-Native Foundation

Adhar AI β€” The Agentic Layer of the Adhar Platform

Adhar AI MCP Python Tools License Slack

Adhar β€’ Built with ❀️ for developers!


πŸ€– What is Adhar AI?

Sanskrit: ΰ€…ΰ€§ΰ€Ύΰ€° (Adhāra) – Foundation

Adhar AI is the agent runtime behind the Adhar platform's ai/adhar-ai package (ADR-0024). Configure one LLM key and the platform gains the ability to investigate, explain, scaffold, provision and remediate β€” through its existing control surfaces, never around them.

Operating an internal developer platform demands expert fluency across Kubernetes, ArgoCD, Gitea, Crossplane, Keycloak, Vault, the LGTM stack, Kyverno and OpenCost. Adhar AI is how the platform answers for itself. Ask it why an application is degraded and it reads the real pods, the real logs, the real sync status and the real burn rate, then tells you β€” citing every source. Ask it to fix the problem and it opens a pull request.

⚠️ Active development. APIs and tool schemas may change. Images are published to GHCR as ghcr.io/adhar-io/adhar-ai-{runtime,mcp-<domain>} β€” see Project status.


πŸ›‘οΈ The rule that shapes everything

Read tools read. Write tools open a pull request. Nothing applies to a cluster.

An agent holding kubectl apply and a cluster-admin token is a catastrophe waiting for a prompt injection. Adhar's whole thesis is GitOps-first, policy-enforced, RBAC-scoped, reviewable change, so an AI layer that bypassed that thesis would be a regression rather than a feature.

Every write tool here commits to a branch in Gitea and opens a PR against the GitOps repo. There is no kubectl apply path in this codebase, and the ServiceAccount the platform binds is read-only. A human merges, ArgoCD reconciles, and the change is as reviewable and revertible as any other. Every artifact the agent creates carries adhar.io/origin: adhar-ai and a branch prefixed adhar-ai/.

The property is structural, not configured. A test asserts, at the source level, that every write tool's body reaches open_pr and contains no apply/kubectl/patch_/delete_ call, and that the PR module imports no Kubernetes, AWS, Azure or GCP client at all. The agent's authority is exactly a contributor's, and it cannot be raised by a setting.


🧩 What ships here

One image, three entrypoints:

Component What it is Entrypoint
πŸ”Œ MCP tool servers one server per domain, streamable HTTP at /mcp on :8080 adhar-ai mcp --domain <domain>
🧠 Agent runtime eight specialist agents, durable tasks, event-driven operators and a tool-use chat loop adhar-ai runtime
πŸšͺ LLM gateway provider-agnostic, OpenAI-compatible β€” local development only adhar-ai gateway

In the platform, LLM traffic and the federated MCP endpoint are served by agentgateway rather than by the gateway in this repo (ADR-0025); the bundled gateway stays as the dependency-free local-dev fallback.


🧰 Tool inventory

adhar-ai tools prints this as JSON; it is the same contract as contract/tools.json, and CI fails on any drift between the two.

Domain πŸ‘€ Read tools ✍️ Write tools (PR-only)
cluster list_pods, describe, get_events, logs, resource_health, list_resources, insights β€”
gitops app_status, sync_status, app_diff propose_change
provision list_xrs, xr_status propose_xr
observability promql, logql, traceql, slo_burn, correlate β€”
security findings, policy_explain, posture propose_exception
cost cost_by, budget_status, showback β€”
catalog search_packages, template_params scaffold
delivery pipeline_runs, pipeline_run, builds, build_logs, promotions, previews β€”

35 tools across 8 domains, 4 of them writes. There is deliberately no kubectl_apply, argo_sync, helm_install or cloud-mutation tool, and a test asserts each of those names is absent from every domain.

delivery is the newest domain and the one that closed the widest gap. The others could say a Deployment was unhealthy, an Application OutOfSync, an image refused by policy β€” none could say why the build that should have produced that image failed, which is the question developers ask most. It reads Tekton runs, kpack builds, Kargo promotions and preview environments, and build_logs resolves a run name to the pod, then to the failing container, and tails it. Every package behind it is optional, so a platform without Tekton gets installed: false naming the package to enable β€” a fact the model can tell apart from "nothing is failing".

Outward, the eight servers are one federated endpoint β€” https://mcp.<host>/mcp on agentgateway, which multiplexes them all, validates the Keycloak token and authorizes per tool (read tools need platform-developer, the PR-opening tools need platform-admin). External agents β€” Claude Code, an IDE, ChatOps β€” drive Adhar through exactly these governed tools with one URL and one token. Tool names arrive prefixed by their server (gitops_propose_change), which is what the gateway's authorization rules key on.

The servers themselves no longer validate tokens: that control moved to the data plane, once, in front of all eight. They still receive the caller's bearer token (agentgateway sets preserveToken: true) so RBAC-scoped reads run as the user.

πŸ‘‰ Full reference: docs/TOOLS.md


πŸͺœ Staged autonomy

The runtime reads its stage from the adhar-ai-config ConfigMap and never acts above it. Each rung differs from the one below in a way you can observe:

Stage Behaviour
read-only write tools are not offered at all, and are refused if called anyway
suggest (default) writes allowed; the first pull request ends the run, so a human reads one proposal rather than a chain
approve-to-apply the run continues after a PR, so the agent can verify its own proposal; a human still merges every one
scoped runs unattended, and is therefore confined to the narrower writePolicy.scoped allow-list β€” empty by default, so this rung permits nothing until an operator enumerates it

Authority only ever narrows as it flows. The ConfigMap sets a ceiling, a request may ask for less, an operator policy may be stricter still, and an unauthenticated caller is pinned to read-only regardless. No path widens it, and there is no rung at which the model chooses its own scope β€” the allow-list is read from the ConfigMap, never from a tool argument, because an argument is something a prompt injection can set.

Five operators run on events rather than a schedule: alert-triage (Alertmanager webhook), drift-explain (ArgoCD OutOfSync), cost-advisor (OpenCost), upgrade-preflight, and build-failure (Tekton CloudEvents) β€” the one a developer actually reads, because it arrives with the failing step already quoted. Each emits a structured finding. Whether that finding becomes a PR is the stage's decision, not the model's.


πŸ§‘β€πŸš€ Specialist agents, durable work and quiet automation

A single assistant with thirty-five tools answers every question the same average way. Eight specialists do not, and none of them can do more than the one assistant could β€” one per stage of platform work:

Agent Answers Ceiling
🚨 incident alerts, crashloops, outages, SLO burn, "why is this broken?" suggest
🚚 delivery failed builds, pipelines, images, preview environments suggest
🚒 release promotion, drift, sync failures, rollback approve-to-apply
πŸ§ͺ data Spark jobs, streams, warehouses, ML workloads suggest
πŸ’° cost spend, budgets, showback, right-sizing suggest
πŸ” security policy, findings, CVEs, compliance evidence approve-to-apply
πŸ—οΈ platform scaffolding, packages, golden paths, Crossplane, upgrades suggest
πŸ“– guide how-to, onboarding, conventions read-only
🧭 generalist everything else β€” routing always terminates here suggest

Routing is lexical and tuned on a real failure mode: guide used to list the natural ways to open a question ("what is", "where is", "how do i"), phrases scored double a word, and so the most common phrasings landed on the least capable agent β€” "what is k8sgpt reporting right now" went to the read-only teacher with two tools. Question form now scores a fraction of question subject, ties break on the more specific match rather than on roster order, and a question with no subject of its own still reaches guide.

Each agent's ceiling narrows and never widens: a read-only agent stays read-only for a platform administrator at scoped, because the narrowing belongs to the role rather than to the request. Routing is lexical, not a model call β€” spending a completion to decide who should spend a completion doubles latency on every request. An agent may hand work on, but only to a colleague it declared, only with a stated reason, and only four times before the chain is refused as a loop.

/chat and /tasks run the same roster. Both route, both apply the agent's ceiling, both scope the tools and the grounding. The only difference is the lifetime: /chat answers inside the request and writes no task row, because one row per chat message would bury the real work. POST /tasks returns an id immediately; the work runs behind a bounded worker pool, survives a restart where a database is configured, and reports its own state:

queued ──► planning ──► awaiting_approval ──► running ──► done | failed | cancelled

Above suggest, the agent writes a plan without acting and the task stops. Releasing it needs the same platform-admin credential the PR-opening tools do, because a task that can change the platform is exactly as privileged as a write.

Ten chores ship, all off and all in dry-run. Certificate expiry, drift, orphaned resources, failing scorecards, cost outliers, stale findings, cluster insights, failing builds, stale previews, runbook rot. Enabling one and letting it act are two separate decisions, and maxProposals bounds what a single run can open β€” the difference between a helpful Monday morning and an unreviewable flood.

The agent turns up where the work is: a Slack thread, a pull-request review that declares itself, a failed pipeline. A thread is a conversation, so a follow-up continues rather than starting fresh. Every inbound payload is treated as attacker-influenced, and the structural guarantee holds whatever it says.

It knows what it cannot answer. GET /capabilities is derived from the live tool surface rather than hand-written, so it never promises something that no longer exists. GET /coverage is the queue of questions the platform answered badly, clustered by how often each has been asked β€” which is the list somebody writes the missing runbook from.

πŸ‘‰ Full reference: docs/AGENTS.md


🧭 Every user, every stage β€” and a test that proves it

An assistant with thirty-five tools still fails the person who never finds out that any of it applies to them. Two failures look identical from outside: the capability is absent (nobody wired a tool, which is what every question about a failed build used to be), or it is unreachable β€” it exists and the person asks a colleague instead, while usage metrics say everything is fine.

GET /assist is the map that makes both visible: a persona Γ— stage matrix where each cell names the agent, the tools, a question that really routes there, and the surfaces it works on.

learn scaffold build deploy operate optimise secure evolve
newcomer βœ… βœ…
developer βœ… βœ… βœ… βœ… βœ…
data-engineer βœ… βœ… βœ… βœ…
sre βœ… βœ… βœ…
platform-engineer βœ… βœ… βœ… βœ…
security-engineer βœ… βœ…
finops βœ…
curl -sS "$RUNTIME/assist?persona=developer&stage=build"
curl -sS "$RUNTIME/assist?entrypoint=chore"     # what arrives without being asked
curl -sS "$RUNTIME/capabilities"                # …plus `forYou`, for the caller's own token

Each persona is inferred from the caller's Keycloak groups, then refined by what they just asked: an admin asking a cost question is being a finops person for the next five minutes, and those are the offers worth showing them.

It is a gate, not a brochure. tests/test_assist.py asserts that every offer names an agent that exists and tools that exist, that it only names tools its own agent may call, that every example question actually routes to the agent it names (or to one that can hand off to it), and that gaps() is empty β€” every persona helped at every stage it claims. Add a persona, delete a tool or narrow a roster and the suite fails with the offer that became a lie. gaps ships in the response too, so a hole is visible to a reader and not only to CI.

Answers that read like a colleague's

The answer contract in the system prompt covers three things, each of which was a complaint first:

  • Process before answering. Start wide (insights, sync_status, pipeline_runs β€” the thing you were asked about is often one of several with one cause), then narrow. Once there is a hypothesis, check the one thing that would disprove it. An empty tool result is not an answer: try the adjacent tool, then say what could not be established. "The build failed" is a restatement; "the go-build step exited 2 on a missing module" is an answer.
  • Conversational, not a report. Plain prose, second person, no "Based on my analysis" β€” open with the finding. Bullets only for a genuine list; prose for anything with a because in it. One concrete offer of a next action is welcome; generic offers of further help are not.
  • Accurate about what it knows. Observed, inferred and could-not-establish are three different statements, and the answer says which. Identifiers and error strings are quoted exactly, because a reworded error cannot be searched for. Every number carries its unit and window.

The scenario evals grade these as properties rather than phrasing β€” including, now, that an answer opens with its finding and contains real prose rather than only bullets.


πŸ” Who may drive it

The runtime is the one Adhar AI surface that does not sit behind agentgateway β€” it publishes its own hostname so the Console, the adhar ai CLI, Alertmanager and ArgoCD notifications can reach it directly. So it authenticates for itself:

Caller Credential Route
People (Console, adhar ai) Keycloak JWT, verified against the realm JWKS /chat
Machines (Alertmanager, ArgoCD) shared webhook bearer token /operators/{name}/event

With no credential at all, a caller is still answered β€” and pinned to read-only. Investigation stays open to anyone who can reach the port; authority requires a credential. Set ADHAR_AI_REQUIRE_AUTH=true to refuse the request outright instead, which is the right posture once the platform's own clients are wired up.

The runtime's HTTP surface

Route What it does
GET /healthz posture: connected MCP servers, grounding mode, auth, findings store
GET /config the effective autonomy policy, read back
POST /chat one agent run, routed to a specialist β€” an answer, or a proposed pull request
POST /operators/{name}/event operator webhook (Alertmanager, ArgoCD notifications)
GET /findings what the operators concluded
GET /knowledge what the knowledge base holds, by origin and kind
POST /knowledge add a note, runbook or incident write-up
POST /knowledge/search retrieve grounding without running the agent
POST /knowledge/refresh re-derive knowledge now
POST /feedback say whether an answer's grounding helped
POST /tasks start work that outlives the request
GET /tasks Β· GET /tasks/{id} what is running, and what one task did
POST /tasks/{id}/approve release or reject a plan β€” needs a writer
GET /agents Β· POST /agents/route the roster, and who would take this
POST /journeys/{surface} Slack, a pull request, a failed pipeline
GET /chores Β· POST /chores/{name}/run the catalogue, and one run now
GET /capabilities what the platform can do right now, derived not declared
GET /assist who it helps, at which stage, with what β€” and where the gaps are
GET /coverage what it could not answer β€” the queue of runbooks to write

/healthz is the only one left open β€” it is the readiness probe, and it presents no credential. Everything else is gated, so ADHAR_AI_REQUIRE_AUTH=true closes every route that carries cluster detail.

πŸ‘‰ Threat model and controls: docs/SECURITY.md


🏭 Built to be left running

An agent that works on a good day is a demo. These are the controls that make it safe to leave running on a bad one.

Concern What is there
πŸ“Š Observability /metrics on every component, two ServiceMonitors, a 12-panel Grafana dashboard, optional OTel tracing with GenAI semantic conventions
πŸ’° Cost tokens and realized spend per model, and a refusal to estimate cost when the gateway does not report it
πŸ” Resilience retry with full jitter on what is worth retrying, per-dependency circuit breakers, timeouts on everything outbound
🚦 Admission per-caller rate limiting, idempotent operator webhooks, graceful draining on SIGTERM
πŸ”’ Safety outbound credential masking, an optional model allow-list, policy refusals counted as a first-class signal
🎯 Quality gates a behavioural red-team suite and a scenario eval suite, both in CI

Two of these deserve a sentence each.

Duplicate delivery is real. Alertmanager retries, and so does ArgoCD notifications. Without deduplication one flapping alert becomes N agent runs and, above read-only, N near-identical pull requests for one problem. Operator events are keyed on the event body with volatile timestamps stripped, so a retry replays the first finding instead of doing the work again.

adhar_ai_denied_total is the security signal. A steady trickle of policy refusals is the system working. A spike is either a misconfiguration or something trying to make the agent exceed its authority β€” and it is the only metric that tells those apart from "the agent is quiet today".

πŸ‘‰ Full detail: docs/PRODUCTION.md


πŸ“š Grounding β€” a knowledge base, not a docs folder

An agent that only knows Kubernetes in general is a search engine with extra steps. Adhar AI builds a knowledge base of your platform and keeps it current. Six sources, re-derived on a schedule:

Source What it answers
πŸ“˜ Documentation "why is it built this way", "how do I do X"
🧰 Tool inventory "what can you actually do for me"
πŸ“¦ Package catalogue "what is installed, what does it depend on"
☸️ Live cluster "what is running right now, and is it healthy"
πŸ” Operator findings "has the platform noticed this before"
πŸ“ Human notes "what did we decide, and what did we learn"

Retrieval is hybrid. Vector similarity finds things phrased differently from the question; Postgres full-text finds the exact identifier the user typed (CreateContainerConfigError, CompositeCluster) that an embedding blurs away. Platform questions contain a lot of exact identifiers, so the two are fused by reciprocal rank rather than chosen between.

It learns. Notes are indexed the moment they are written, so a postmortem typed at 02:00 is retrievable at 02:01. Every operator finding is fed back, so the next similar alert retrieves what the last investigation concluded. And every answer returns the chunk ids behind it, so POST /feedback teaches the ranking what actually helped.

It is incremental, which is what makes keeping it current affordable. A refresh re-embeds only what changed β€” measured on Adhar's own corpus, a second pass over 1,299 chunks rewrote nothing and made zero embedding calls.

It degrades without a cliff:

You have You get
pgvector + an embedding endpoint hybrid vector and lexical retrieval
pgvector, no provider key full-text over the same indexed corpus
no database in-process BM25 over the docs tree
no docs either honest: "no grounding is available"

So an unkeyed platform is still grounded. GET /healthz says which rung you are on, and every grounding block names its source and which path found it.

πŸ‘‰ Full detail: docs/KNOWLEDGE.md


πŸ”€ Providers

local β€” no key. The model runs on the platform (ai/llm-d: vLLM behind the llm-d router with an agentgateway sidecar β€” Qwen3.6-27B-FP8 on one GPU, or Qwen2.5-0.5B on CPU) and agentgateway routes every local/* model name there. Both profiles answer to local/default. The hosted key stays primary; ADHAR_AI_LLM_SECONDARY_MODEL=local/default (set by the platform manifests) makes the self-hosted model the secondary every agentic feature falls back to when the primary fails β€” never on a budget or policy refusal β€” and /healthz β†’ llm.answered_by says which one answered. ADHAR_AI_LLM_PROVIDER=local makes it primary instead.

Everything here speaks /v1/chat/completions and /v1/models to whatever gateway LLM_GATEWAY_URL points at, and always names a model in the body. Which backend that model reaches depends on which gateway is in front:

In the platform (agentgateway) Locally (adhar-ai gateway)
Provider choice the model name: claude-* β†’ Anthropic, gpt-*/o[1-9]-* β†’ OpenAI, local/* β†’ in-cluster vLLM ADHAR_AI_LLM_PROVIDER: anthropic (default), openai, azure, any OpenAI-compatible base URL, ollama
Credentials ANTHROPIC_API_KEY + OPENAI_API_KEY in the adhar-ai-llm Secret, read by the proxy ANTHROPIC_API_KEY / OPENAI_API_KEY in the process env
Budgets conditional rate limits per Keycloak group, in the gateway package BUDGET_* env in this process

LLM_GATEWAY_URL may be given with or without the /v1 suffix β€” the platform sets it with, docker compose without, and the client normalizes it to exactly one /v1 either way. Token and request budgets are enforced in whichever gateway is in front, so a runaway agent costs a 429 rather than a bill.


⚑ Quick start

uv sync --extra rag                    # Python 3.12+
uv run adhar-ai tools                  # the tool inventory β€” no cluster needed
uv run adhar-ai mcp --domain cluster   # :8081/mcp
uv run adhar-ai runtime                # :8082
docker compose up                      # the whole stack: gateway, all eight
                                       # MCP servers, the runtime and pgvector

Then ask it something:

curl -s localhost:8082/chat \
  -H 'content-type: application/json' \
  -d '{"prompt":"which applications are out of sync, and why?"}' | jq

πŸ‘‰ The full walkthrough β€” every entrypoint, a real cluster, an external agent, and what to expect at each step β€” is docs/GETTING_STARTED.md.


πŸ“– Documentation

Guide What it covers
πŸš€ Getting Started From uv sync to a grounded answer to an opened PR, step by step
πŸ›οΈ Architecture How the three components, the eight servers and the data plane fit together
🧰 Tool Reference Every one of the 35 tools: arguments, backend, failure mode
πŸ”Œ Connect a client Claude Code, Cursor, Claude Desktop or your own agent on the Adhar MCP endpoint, in one command
🧠 Knowledge Base What the agent knows, how it stays current, and how it learns
πŸ€– Agents & Automation The specialist roster, the persona Γ— stage map, durable tasks, approvals, chores and journeys
🏭 Production Metrics, resilience, admission control, safety and the quality gates
βš™οΈ Operations Every setting, the health surface, and how to diagnose it when it is wrong
πŸ” Security Threat model, the write path, authentication, autonomy, prompt injection
🚒 Releasing One command cuts a release; what it checks, what it refuses, and the rollout that follows
🀝 Contributing Adding a tool or an operator without breaking the guarantees

πŸ§ͺ Development and verification

uv sync --extra rag
uv run pytest -q                       # 663 tests (686 with a database)
uv run ruff check src tests
uv run mypy src
uv run adhar-ai tools | diff -u contract/tools.json -   # the Go-CLI contract

The last one is not a formatting check. contract/tools.json is the schema snapshot the Adhar Go CLI is written against, so a diff there is a breaking change rather than a refactor.

Beyond the unit suite

Unit tests drive tools in-process and fake the toolbox, which is fast and blind to a whole class of defect. Every bug in the list below was found by something in this section while the unit suite was green, so both harnesses are worth running.

./hack/e2e-local.sh

Stands up all eight MCP servers and the runtime over real HTTP and asserts the seam between them: the transport's Host-header handling, the MCP client's result unwrapping, and whether an unconfigured backend's error text survives the trip to the model. A second runtime then runs against a scripted OpenAI-compatible server (hack/stub-llm.py), which drives the agentic layer end to end β€” a task that outlives its request, a handoff recorded on the task, a Slack thread that remembers its first message, a dry-run chore that proposes nothing. It needs no cluster, no key and no database: what it asserts is either true offline or honestly reported as unavailable, which is the property being tested. It runs in CI.

docker run -d --name adhar-rag -p 15432:5432 \
  -e POSTGRES_USER=adhar_ai -e POSTGRES_PASSWORD=adhar_ai \
  -e POSTGRES_DB=adhar_ai_rag pgvector/pgvector:pg16

ADHAR_AI_RAG_DSN=postgresql://adhar_ai:adhar_ai@127.0.0.1:15432/adhar_ai_rag \
  uv run hack/verify-knowledge.py --docs ../adhar/docs \
  --packages ../adhar/platform/stack/packages

Checks the knowledge base against a real pgvector: schema, ingestion from every source, incrementality, hybrid retrieval, exact-identifier lookup, immediate note availability, feedback ranking and deletion propagation. The same database makes the 13 skipped tests run:

ADHAR_AI_TEST_DSN=postgresql://adhar_ai:adhar_ai@127.0.0.1:15432/adhar_ai_rag \
  uv run pytest -q                     # 279 passed

LLM_GATEWAY_URL defaults to the platform data plane (http://adhar-ai-gateway.adhar-system.svc.cluster.local:8080/v1), which is unresolvable off-cluster on purpose β€” it is the production wiring, and every local entrypoint above overrides it. docker compose sets LLM_GATEWAY_URL=http://gateway:8080; for a bare uv run adhar-ai runtime, export the same.

Against a real platform, point it at the cluster's services:

export ADHAR_AI_ARGOCD_URL=http://argo-cd-argocd-server.adhar-system.svc.cluster.local
export ADHAR_AI_GITEA_API_URL=http://gitea-http.adhar-system.svc.cluster.local:3000/api/v1
export ADHAR_AI_PROMETHEUS_URL=http://prometheus-kube-prometheus-prometheus.adhar-system.svc.cluster.local:9090
export ADHAR_AI_LLM_PROVIDER=anthropic ANTHROPIC_API_KEY=...

Every setting has an ADHAR_AI_-prefixed name and an unprefixed fallback, which is how the platform manifests inject them. src/adhar_ai/config.py is the list, and docs/OPERATIONS.md is the annotated version.

Cutting a release

./hack/release.sh                      # the next PATCH: X.Y.Z -> X.Y.(Z+1)
./hack/release.sh --dry-run            # the same checks, nothing written
./hack/release.sh 0.4.0                # a minor or major bump, named explicitly

One command publishes a version. It refuses unless you are on main with a clean tree in sync with origin; writes the new version into every place that states one (pyproject.toml, src/adhar_ai/__init__.py, the README badge, uv.lock); and then runs the same gates CI runs β€” ruff, mypy, the contract/tools.json snapshot, pytest. Only if all of them pass does it commit, tag and push. Any failure restores the tree, so a refused release leaves nothing behind β€” including the version bump it had already written.

A red ci on main is invisible until you try to release. images and ci are separate workflows, and images does not depend on ci: a commit whose lint or tests fail still builds, signs and publishes :latest, so the registry looks healthy while main is not. Worse, CI's steps are sequential β€” a lint failure skips type-check, tests, the contract snapshot and the end-to-end run, so one over-long line hides every other gate. That is how v0.3.3 was followed by three commits with a failing ci that nothing surfaced until hack/release.sh refused to cut 0.3.4. The habit that avoids it: run the gates above (or ./hack/release.sh --dry-run, which is the same set) before pushing, and treat ci on main as the release gate it is.

After the tag, images.yml builds and signs the nine images and release.yml then publishes the GitHub Release with the tool contract and the SBOM. The platform tracks :latest, so there are no manifest changes to make β€” roll the pods, and /healthz reports the version and commit now running:

kubectl rollout restart deployment -n adhar-system -l adhar.io/origin=adhar-ai

πŸ‘‰ Full detail: docs/RELEASING.md


πŸ—οΈ How the platform consumes this

The ai/adhar-ai package in the platform repo is opt-in and disabled by default, so the platform runs unaffected until someone supplies a key. When enabled it deploys the runtime and one MCP Deployment per domain (all this image, differing only by ADHAR_AI_MCP_DOMAIN), wires the adhar-ai-llm Secret through Vault and External Secrets, creates the Keycloak adhar-ai client, and applies a Kyverno adhar-ai-guardrails policy over what the agent may touch.

It does not deploy the gateway from this repo. The AI data plane is ai/agentgateway β€” upstream agentgateway, configured through Gateway API. It is the production data plane for both AI protocols:

Endpoint What it does
πŸšͺ LLM https://ai.<host>/v1/chat/completions routes on the model name; holds the provider keys server-side; masks credential-shaped strings; per-group token budgets
πŸ”Œ MCP https://mcp.<host>/mcp federates the eight servers into one tool list; authorizes per tool
🧠 Agent https://agent.<host>/ this repo's runtime (/chat, /findings, operator webhooks) β€” the one AI surface not behind the data plane

πŸ“ Project status

Against the platform roadmap's Phase 3 agentic entry.

Capability Status
MCP-native tools, 8 domains, PR-only writes βœ… 663 tests, 686 with a database
Federated MCP over streamable HTTP at /mcp βœ… verified against the Host headers a Pod actually receives
GitOps-safe runtime, four-rung autonomy ladder βœ… each rung behaviourally distinct and tested
Authentication on the runtime's own surface βœ… Keycloak JWT, webhook token, outbound service-account token
Knowledge base β€” 6 sources, hybrid retrieval, learning loop βœ… verified against a real pgvector: 1,299 chunks, 0 re-embeds on an unchanged pass
Durable findings βœ… Postgres-backed, degrades to memory
Production controls β€” metrics, tracing, resilience, admission, safety βœ… /metrics on every component, dashboard drift-tested against the registry
Quality gates β€” behavioural red team, scenario evals βœ… both in CI; the live eval graded a real model and found a real bug
Specialist agents, routing, handoff with a depth cap βœ… reached by /chat as well as /tasks; ceiling narrowing and refusal paths tested; handoff verified over real HTTP
Durable tasks, plan-and-approve, bounded workers βœ… state machine refuses illegal moves; approval needs a write credential
Knowledge graph over the same Postgres βœ… verified against a real database: traversal, blast radius, cycles, per-origin refresh
Chores, journeys, capability catalogue, coverage gaps βœ… every chore ships off and in dry-run, asserted as policy
Build and delivery assistance (delivery domain, delivery agent, build-failure operator, failing-builds chore) βœ… Tekton/kpack/Kargo/preview shapes read and summarised; optional-package and RBAC paths tested
Assistance mapped to every persona and stage (GET /assist) βœ… 7 personas Γ— 8 stages, 24 offers, 0 gaps β€” and the suite fails if an offer names an agent, a tool or a question that does not hold
Data and ML workload assistance (data agent) βœ… reaches Spark, Argo Workflows, Kafka and CNPG through list_resources with verified API versions
Live run against a real LLM and a real cluster βœ… see below
Container images on GHCR βœ… nine names from one build, Cosign-keyless signed with an SPDX SBOM attached
The running build is identifiable βœ… /healthz reports version and the commit revision, baked in at build time
GPU run of the ai/vllm profile ⏳ needs a GPU node pool
Self-hosted inference via ai/llm-d (router + agentgateway sidecar β†’ vLLM), CPU profile and Qwen3.6-27B-FP8 GPU profile, wired as the secondary model of every agentic feature ⏳ CPU profile ran 2026-09-24; GPU profile and the fallback path await a GPU cluster
Tool re-discovery without a runtime restart (MCPToolbox.refresh()) βœ… unit-tested (a rolled MCP server used to need a restart)

What a live run proved

Exercised against a real LLM through the gateway, a real bootstrapping Adhar cluster and a real Gitea β€” not mocks:

  • The agent diagnosed a live cluster. Three tool calls against a half-bootstrapped adhar-system, and every specific claim checked out: a missing argocd-redis Secret, Cilium returning putEndpointIdTooManyRequests, and a dex container exiting 20.
  • It refused to invent data. Asked for CPU figures with no Prometheus configured, it named the backend and the environment variable that fixes it rather than producing a number.
  • It opened a real pull request. Correct adhar-ai/ branch, adhar-ai label, provenance block with an audit id, commit trailer β€” and main untouched, which is the guarantee that matters.
  • Embeddings are real. 1536 dimensions with genuine semantic structure: a paraphrase scored 0.39 against its original where unrelated text scored 0.12.

That run also found four defects the unit suite could not, all since fixed:

Defect Why it mattered
The Gitea client doubled /api/v1 the first real pull request 404'd, and the docs told you to configure it the broken way
MCP sessions never recovered from a server restart a rolling Deployment cost that domain its tools forever, while /healthz still called it connected
A transport failure raised CancelledError except Exception does not catch it, so it killed the whole agent run
The runtime sent no bearer to the LLM gateway which runs JWT validation in Strict mode, so no agent run could reach a model

Still outstanding. The components have not yet run as Deployments in a cluster β€” the live run drove them from a workstation against that cluster's services. The images are published and the platform package already points at them, so this is a rollout rather than a build.

One caveat that applies to every Adhar-owned component, this one and the Console alike: the manifests track :latest with imagePullPolicy: Always, and an unchanged Deployment spec is not drift. ArgoCD will not restart pods when a new :latest is pushed, so picking up a new build needs kubectl rollout restart deployment -n adhar-system -l app.kubernetes.io/part-of=adhar-ai (or any change that recreates the pods). /healthz now reports the revision it is actually running, so the result can be checked rather than assumed.


πŸ“„ Licence

Apache 2.0, same as the platform.

Part of the Adhar open internal developer platform.

About

Adhar AI Repository

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages