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 "Agent Studio" 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 "Agent Studio" — 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
├── 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). Hosts ref every module.
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 http://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 http://localhost:5080/auth/token \
-H "Content-Type: application/json" \
-d '{"user":"operator","password":"operator"}' | jq -r .token)
curl -X POST http://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.