A modular .NET agentic platform for software-delivery workflows. A central orchestrator coordinates specialist agents — Requirements → Code → Tests → QA — over a provider-agnostic LLM gateway, on a multi-tenant runtime with row-level isolation and a Blazor AgentOS desktop UI.
AgentOs turns a plain-English user story into reviewed, test-backed C# scaffolding. Five agents
collaborate under a central orchestrator; a QA agent scores requirement–code–test consistency and
loops until convergence or an iteration cap. The platform itself is a modular monolith — each
feature (LLM gateway, pipeline runtime, identity, tenants, settings store, integrations, remote
agent transport) is a self-contained IModule with its own DI surface, EF Core context, and
Postgres schema, so any one of them can later ship as a standalone NuGet package.
Status: pre-1.0, actively developed on
refactor/modular-monolith. The core pipeline, gateway, modular runtime, and multi-tenant identity are working; public surfaces may shift beforev1.0.
Contents: Why AgentOs · Concepts · Architecture · Quick start · Modules · LLM gateway · Configuration · Multi-tenant · API · Extending · Deploy · Contributing
- Modular by construction. Every feature is an
IModulediscovered by reflection. Hosts wire the whole platform with a singleservices.AddModulesFromAssemblies(cfg, …)call; no module references another's runtime types — only contracts inDomain/SharedKernel. - Provider-agnostic LLM gateway. Claude (Anthropic.SDK), Azure OpenAI (
Azure.AI.OpenAI), MAF (Microsoft Agent Framework), and a remote dev-IDE agent all register as keyedILlmClientunder their canonical name. Swap a provider inappsettings.json; no code change. - Multi-tenant from day one. Row-level isolation via EF Core global query filters; a
Keycloak-backed OIDC
tenantclaim drivesITenantContext. An operator mode keeps a single pseudo-tenant for unauthenticated local runs. - Per-module DbContext + schema.
pipeline.*,tenants.*,config.*live in their own schemas with their own migration histories. Each module owns its persistence end-to-end. - Resilient gateway. Multi-key round-robin with HTTP 429 cooldown /
Retry-After, exponential backoff on transient failures. - Cloud-ready out of the box. A .NET Aspire AppHost wires Postgres + Keycloak + Api + Web; an
azd updeploys to Azure Container Apps. - Operator-friendly desktop. A Blazor Server AgentOS desktop — windowed UI with a Start menu, dock, light/dark themes, live pipeline runner, drag-and-drop workflow editor, and runtime settings to rotate LLM keys without a redeploy.
| Concept | Lives in | Notes |
|---|---|---|
| Module | IModule (SharedKernel) |
One assembly = one module. Optional IEndpointModule (HTTP) + IInitializableModule (startup hook). Discovered + invoked by ModuleLoader. |
| Agent | Modules.Pipeline.Agents |
A specialist: IRequirementAgent, ICodingAgent, ITestingAgent, IQaAgent, IOrchestratorAgent. Consumes ILlmClient via ILlmClientFactory. |
| LLM client | Domain.Llm.ILlmClient |
One method: SendAsync(LlmRequest, ct) → LlmResponse. Each provider impl registers as a keyed singleton. |
| Tenant context | SharedKernel.Identity.ITenantContext |
Per-request scope: tenant id, user id, roles. DbContexts filter by TenantId; repos stamp on write. |
| App config store | Modules.AppConfig.IAppConfigStore |
Encrypted runtime key-value store (DataProtection). Powers per-tenant LLM key overrides and the Settings UI. |
src/
├── AgentOs.Domain/ # DTOs, ILlmClient, ILlmClientFactory, exceptions (BCL-only)
├── AgentOs.SharedKernel/ # IModule contracts + ModuleLoader + ITenantContext + IAuthTokenProvider
├── AgentOs.Modules.AppConfig/ # Encrypted KV store, AppConfigDbContext (schema: config)
├── AgentOs.Modules.Llm/ # Gateway, providers, key pool, runtime overrides
├── AgentOs.Modules.Pipeline/ # Agents + prompts + orchestrator + PipelineDbContext (schema: pipeline)
├── AgentOs.Modules.Identity/ # JwtAuth + DefaultTenantContext + HttpTenantContext + /auth
├── AgentOs.Modules.Tenants/ # Keycloak admin + TenantsDbContext (schema: tenants) + /tenants
├── AgentOs.Modules.Integration/# GitHub PR + dotnet build verifier (+ BuildVerifierTool)
├── AgentOs.Modules.Tools/ # ITool contract + IToolRegistry + IToolPolicy + IToolInvocationLog
├── AgentOs.Modules.Mcp/ # MCP client — connects to external MCP servers, registers their tools
├── AgentOs.Modules.RemoteAgent/# SignalR hub + transport + RemoteAgentLlmClient
├── AgentOs.Api/ # ASP.NET Core minimal API (+ Scalar UI). Composition root only.
├── AgentOs.Web/ # Blazor Server "Agent Studio". Composition root only.
├── AgentOs.AppHost/ # .NET Aspire orchestration
├── AgentOs.RemoteAgent/ # Standalone IDE-side agent (dev machine)
└── AgentOs.ServiceDefaults/ # OpenTelemetry, health checks, HTTP resilience
tests/ # xUnit (~180 unit/integration) + Playwright E2E
Module dependency rule: a module references Domain + SharedKernel only. Cross-module
references are explicit and minimal (Llm → AppConfig for hydrated overrides; Tenants → Identity
for the JWT scheme; RemoteAgent → Llm for the keyed registration shape; Integration → AppConfig
and → Tools so BuildVerifierTool registers via AddTool<>()). Hosts ref every module.
Agents call tools (build_verifier, MCP-exposed GitHub / filesystem actions, custom ITools)
through the LLM gateway: LlmRequest.Tools = ["build_verifier"] causes PooledChatLlmClient to
resolve each name via IToolRegistry, adapt the ITool into a Microsoft.Extensions.AI.AIFunction,
and wrap the chat client with FunctionInvokingChatClient so the tool-call loop runs transparently
inside SendAsync. Every invocation passes through IToolPolicy (gate) and IToolInvocationLog
(evidence) so refused and successful calls alike are auditable.
AgentOs.Modules.Mcp connects to upstream MCP servers at startup and registers their tools
under the prefix {server}.{tool} — local and remote tools live in the same registry. The Api
host also serves MCP itself at /mcp (Streamable HTTP), exposing run_pipeline, list_runs,
and get_run so any MCP host (Claude Desktop, VS Code, custom orchestrator) can drive AgentOs.
End result: a tool mesh where AgentOs is both an MCP consumer and an MCP provider.
Pipeline dataflow:
UserStory ─► RequirementAgent ─► RequirementSpec ─► CodingAgent ─► CodeArtifact
│
▼
PipelineResult ◄── QaAgent ◄── TestingAgent
│
└── PipelineRunRepository (pipeline.pipeline_runs + run_metrics)
QA scores the triple and decides "ship it" or "iterate" (capped by NMax).
Prerequisites: .NET 10 SDK (pinned via global.json). For the multi-tenant smoke also
Docker (used by Aspire to run Postgres + Keycloak). An Anthropic and/or Azure OpenAI key is
required for live runs.
git clone https://github.com/hoangsnowy/AgentOs.git
cd agentos
dotnet restore AgentOs.slnx
dotnet build AgentOs.slnx -c Release
dotnet test AgentOs.slnx -c ReleaseRun the API on its own (Scalar UI at https://localhost:5080/scalar/v1):
dotnet run --project src/AgentOs.ApiOr run the whole stack — Aspire wires Postgres + Keycloak + Api + Web and forwards env vars:
dotnet run --project infra/AgentOs.AppHostDrive the end-to-end pipeline:
TOKEN=$(curl -s -X POST https://localhost:5080/auth/token \
-H "Content-Type: application/json" \
-d '{"user":"operator","password":"operator"}' | jq -r .token)
curl -X POST https://localhost:5080/pipeline \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"userStory":"An admin can create, view, edit and delete products; users browse by category.","nMax":3}'The pair
RemoteAgentprovider can dispatch LLM calls to a dev-machine agent at zero API cost when a remote agent is paired (see Modules).
| Module | Schema | Endpoints | Notes |
|---|---|---|---|
| AppConfig | config |
/settings* |
Encrypted KV (Microsoft.AspNetCore.DataProtection). Per-tenant. |
| Llm | — | /llm/test (Api shell) |
Gateway, keyed ILlmClient per provider, multi-key pool with 429 failover. |
| Pipeline | pipeline |
/pipeline, /pipeline/stream, /requirement, /code, /test, /qa, /runs* |
Agents + orchestrator + run history (jsonb artifact). |
| Identity | — | /auth/token |
JWT bearer (HS256 operator / RS256 Keycloak). Tenant policies (Operator, Admin, Member). |
| Tenants | tenants |
/tenants/me, /tenants, /tenants/{id}/members |
Tenant registry + Keycloak admin REST client for provisioning. |
| Integration | — | — | IGitHubPrService + IBuildVerifier. Used by the Pipeline orchestrator. |
| RemoteAgent | — | /hubs/remote-agent (SignalR) |
Dispatches LLM calls to a paired dev-machine agent (zero API tokens). |
Agents depend only on ILlmClient; LlmClientFactory.Create(providerName) resolves a keyed
implementation registered under its canonical name. Supported provider keys:
| Key | Backend | Owning module |
|---|---|---|
Claude |
Anthropic.SDK (pooled over multi-key) | Modules.Llm |
AzureOpenAI |
Azure.AI.OpenAI (pooled over multi-key) |
Modules.Llm |
MAF |
Microsoft Agent Framework over Azure OpenAI | Modules.Llm |
RemoteAgent |
SignalR-dispatched dev-machine agent | Modules.RemoteAgent |
Llm:ForceProvider (config) and the runtime overrides set from the Settings UI override the
per-agent provider. A runtime IRuntimeOverrides is hydrated at startup from AppConfig so a
process restart keeps the operator's last-saved keys.
| Agent | Default provider | Default model |
|---|---|---|
| Orchestrator | Claude | claude-haiku-4-5 |
| Requirement | Claude | claude-sonnet-4 |
| Coding | AzureOpenAI | gpt-4.1 |
| Testing | AzureOpenAI | gpt-4o-mini |
| QA | Claude | claude-haiku-4-5 |
Illustrative only — reassign any agent to any provider in appsettings.json.
Set keys via user-secrets — never commit them:
cd src/AgentOs.Api
dotnet user-secrets set "Llm:Claude:ApiKey" "sk-ant-..."
dotnet user-secrets set "Llm:AzureOpenAi:ApiKey" "..."
dotnet user-secrets set "Llm:AzureOpenAi:Endpoint" "https://<resource>.openai.azure.com"Per-agent provider/model lives in appsettings.json:
{
"Llm": {
"Claude": { "ApiKey": "", "ApiKeys": [] },
"AzureOpenAi": { "Endpoint": "", "ApiKey": "", "ApiKeys": [], "Model": "gpt-4.1" }
},
"Agents": {
"Orchestrator": { "Provider": "Claude", "Model": "claude-haiku-4-5", "Temperature": 0.3, "MaxTokens": 2000 },
"Requirement": { "Provider": "Claude", "Model": "claude-sonnet-4", "Temperature": 0.1, "MaxTokens": 2000 },
"Coding": { "Provider": "AzureOpenAI", "Model": "gpt-4.1", "Temperature": 0.2, "MaxTokens": 4000 },
"Testing": { "Provider": "AzureOpenAI", "Model": "gpt-4o-mini", "Temperature": 0.2, "MaxTokens": 3000 },
"Qa": { "Provider": "Claude", "Model": "claude-haiku-4-5", "Temperature": 0.1, "MaxTokens": 1500 }
}
}Give a provider a pool; the gateway round-robins across keys, cools any key that returns HTTP 429
(honoring Retry-After), and routes to the next. One throttled key never stalls the pipeline.
{
"Llm": {
"Claude": { "ApiKeys": ["sk-ant-a", "sk-ant-b", "sk-ant-c"] },
"AzureOpenAi": { "ApiKeys": ["key-east", "key-west"] }
}
}The effective pool combines ApiKeys + the single ApiKey + any Settings runtime override
(deduped). Azure keys in a pool share the configured endpoint.
ConnectionStrings:DefaultConnection (Postgres) is the only required wiring; each module
attaches its own DbContext with its own schema + migration history. The Aspire AppHost provisions
this automatically. To run the Api standalone against an existing Postgres:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" \
"Host=localhost;Port=5432;Database=agentos;Username=postgres;Password=postgres"To boot stateless (no DB) — useful for unit tests or quick smoke runs:
$env:Persistence__RequireDatabase = "false" # PowerShell
export Persistence__RequireDatabase=false # bash…which swaps in the in-memory Null*Repository implementations.
Auth:Mode |
Tenant context | Token | Provisioning |
|---|---|---|---|
operator (default) |
DefaultTenantContext → tenant "default", role admin |
HS256 from /auth/token |
n/a (one pseudo-tenant) |
keycloak |
HttpTenantContext → reads tenant claim + realm roles |
RS256 validated against Keycloak JWKS | POST /tenants provisions a tenant + admin user via IKeycloakAdminClient |
The Aspire AppHost runs Keycloak with an auto-imported agentic realm (infra/keycloak/), so a
full Keycloak-mode smoke is one dotnet run --project infra/AgentOs.AppHost away.
Row-level isolation: PipelineRun, RunMetric, Orchestration, and AppConfig rows carry a
TenantId column and a global EF query filter that reads ITenantContext.TenantId. Writes stamp
the tenant id; reads only see your own.
All /pipeline*, /requirement, /code, /test, /qa, /runs*, /settings*, and /tenants*
endpoints require a JWT bearer token. POST /auth/token exchanges operator credentials for one
(operator mode); Keycloak mode expects an RS256 token in the Authorization header. /health and
/ are public.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/auth/token |
none | Exchange operator credentials for a JWT |
GET |
/tenants/me |
any | Resolved tenant + user + roles for the current request |
GET |
/tenants |
Admin | List every tenant |
POST |
/tenants |
Admin | Provision a tenant + admin user (Keycloak) |
POST |
/tenants/{id}/members |
Admin | Invite a member into a tenant |
POST |
/requirement |
any | Run the Requirement agent on its own |
POST |
/code |
any | Run the Coding agent on its own |
POST |
/test |
any | Run the Testing agent on its own |
POST |
/qa |
any | Run the QA agent on its own |
POST |
/pipeline |
any | Run the full end-to-end flow |
POST |
/pipeline/stream |
any | Run the pipeline, streaming progress over SSE |
GET |
/runs, /runs/{id} |
any | List / fetch persisted runs |
GET |
/settings/{prefix} |
any | Read encrypted runtime config under a prefix |
POST |
/settings |
any | Set / rotate a runtime config value |
POST |
/llm/test |
any | Probe the configured provider with a minimal call |
GET |
/health |
none | Liveness + LLM readiness |
- Implement
ILlmClientin a module that owns the SDK reference. - Register it as a keyed singleton under a canonical name:
services.AddKeyedSingleton<ILlmClient, MyProviderClient>("MyProvider");
- Add an alias to
LlmClientFactory.NormalizeKeyif you want a friendlier user-facing name.
Use the bundled agent-scaffold skill (/agent-scaffold {Name}) — it generates the contract,
implementation, DI registration, and xUnit stubs.
- Create a new
AgentOs.Modules.Xclass library; referenceDomain+SharedKernelonly. - Implement
IModule(+IEndpointModule/IInitializableModuleif relevant) inXModule.cs. - If you persist state, add an
XDbContext(schemax) + a design-time factory. - Add the module assembly to the host's
AddModulesFromAssembliescall. - (Optional) Generate the Initial migration:
dotnet ef migrations add Initial \ --project src/AgentOs.Modules.X --startup-project src/AgentOs.Modules.X \ --output-dir Persistence/Migrations --context XDbContext
azd up # provision + deploy to Azure Container Apps via the Aspire AppHostSee docs/DEPLOYMENT.md for the full deployment guide and docs/SETUP.md for local secrets + CI notes.
Issues and PRs are welcome — see CONTRIBUTING.md for the dev workflow and SECURITY.md for reporting vulnerabilities.
- Build + test:
dotnet test AgentOs.slnx -c Release. CI runs the same on every push and PR. - Style:
Nullable+TreatWarningsAsErrorsare on across the solution. - Commits: Conventional Commits.
- The repo ships project-specific Claude Code skills under
.claude/skills/(agent-scaffold,prompt-tune,cost-report) — they load automatically when you open the project root.