Operating instructions for AI agents working in this repository. The goal is to build and extend this kind of project (a .NET 10 Minimal API that wraps a system service — here: systemd units over D-Bus — with a fully documented, strongly typed REST + SSE surface) with minimal human intervention.
ServiceManagerBackend/— .NET 10 Minimal API (net10.0) that starts/stops/monitors whitelisted user systemd units via the session D-Bus bus.example/SystemdServiceController— reference CLI (verified pattern for the D-Bus layer).docs/— background reading: D-Bus fundamentals, the systemd1 D-Bus API, Tmds.DBus library. Seedocs/README.mdDECISIONS.md— read this first: non-obvious decisions, gotchas, and the D-Bus→HTTP error mapping. Everything in there was verified live or cost a debugging session..opencode/skills/— project-local skills with distilled best practices.
| Task | Command (run from repo root) |
|---|---|
| Build | dotnet build (expect 0 warnings, 0 errors) |
| Run | dotnet run --project ServiceManagerBackend --no-build --no-launch-profile |
| OpenAPI document | curl -s http://127.0.0.1:5199/openapi/v1.json (port from ASPNETCORE_URLS) |
| Swagger UI | http://127.0.0.1:5199/swagger |
Gotchas:
Properties/launchSettings.jsonoverridesASPNETCORE_URLSondotnet run— always pass--no-launch-profilewhen you need a known port for verification.- The app starts and serves
/swagger+/openapi/v1.jsoneven without a session bus; the D-Bus connection is lazy (first real request). So OpenAPI/doc changes can be verified by curl without any systemd involvement. - After changing API types or OpenAPI config: build, run, curl
/openapi/v1.json, inspect the affected schemas, stop the server.
microsoft-learnMCP (highest priority for any .NET / ASP.NET question). Official, version-aligned Microsoft documentation. Always query with the project's major version (here: .NET 10 /aspnetcore-10.0moniker) and prefermicrosoft_docs_search→microsoft_docs_fetch(full page) →microsoft_code_sample_search(code snippets). Do not answer .NET framework questions from memory when this MCP is available — the API surface changes between major versions (e.g. .NET 9 → 10 brokeOptionsBuilder.Validate, movedMicrosoft.OpenApi.Modelstypes, added OpenAPI transformers).- Project-local skills in
.opencode/skills/— load them when the task matches:Server-Sent events— native .NET 10 SSE:IAsyncEnumerable<SseItem<T>>+TypedResults.ServerSentEvents()+[EnumeratorCancellation].Controlling systemd services using D-Bus with TMDS library— Tmds.DBus.Protocol- source generator scaffolding.
aspnet— .NET 10 Minimal API patterns (TypedResults, OpenAPI 3.1, SSE, validation).responses— typed results / error response conventions.
DECISIONS.mdanddocs/— project history and verified behavior.- Ask user if cannot find a problem solution in a reasonable ammount of attempts.
websearch— only as a last resort (community blog posts are usually outdated or wrong for recent framework versions).
- Minimal APIs only, no controllers. Routes grouped per resource
(
IEndpointRouteBuilderextension methods inEndpoints/). - Typed results everywhere:
Results<NotFound<ErrorDto>, Ok<...>>etc. — no untypedResults.Jsonin endpoint signatures (it is only allowed inside helper methods that returnIResult). - Strongly typed SSE: the stream type is
SseItem<T>with a single concrete closed union typeT(here:JobStreamEvent, an abstract base record inApiDtos.cs; each event record derives from it). NeverSseItem<object>. - XML doc comments on every API DTO (
<summary>on the type,<param>per constructor parameter). This is what feeds the OpenAPI document — see playbook below. - DTOs live in
ApiDtos.cs(root). D-Bus mapping helpers stay private insideEndpoints/ServiceEndpoints.cs. - D-Bus error replies → HTTP: follow the table in
DECISIONS.md(UnitNotFound→404, Masked/NotActive→409, NoSuchUnit→502, AccessDenied→403, NoReply→504, else 500). - One session-bus connection for the app lifetime (DI singleton
SystemdClient, lazy connect).Manager.Subscribe()must be called before any signal — seeDECISIONS.mdfor the watch-before-call job pattern and race notes.
OpenAPI document generation is the built-in Microsoft.AspNetCore.OpenApi
(AddOpenApi() / app.MapOpenApi()). Swashbuckle.AspNetCore.SwashbuckleUI-style
generation is NOT used — Swashbuckle.AspNetCore.SwaggerUI is UI-only and just points
at /openapi/v1.json via options.SwaggerEndpoint(...).
To make XML doc comments on types/parameters appear in the OpenAPI document:
- csproj must have:
<GenerateDocumentationFile>true</GenerateDocumentationFile> <NoWarn>$(NoWarn);CS1591</NoWarn>
NoWarnfor CS1591 is required — otherwise every undocumented public member (thousands of them in Tmds.DBus.Generator-generated proxies) floods the build. Suppressing the missing comment warning does not affect the generated document; the source generator reads comments straight from the compilation. - No code wiring is needed:
AddOpenApi()overloads are intercepted automatically. <param>comments on a record's primary constructor DO map to the property descriptions in the generated schema (verified).- XML comments cannot be attached to lambda expressions — if an endpoint handler
needs docs, extract it to a method and reference that method from
MapXxx.
When a response type is an abstract base (e.g. SseItem<JobStreamEvent>), the document
shows only the empty base schema. Fix with a schema transformer
(ServiceManagerBackend/OpenApi/JobStreamEventSchemaTransformer.cs):
public sealed class MySchemaTransformer : IOpenApiSchemaTransformer
{
public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken ct)
{
if (context.JsonTypeInfo.Type != typeof(BaseType)) return;
schema.OneOf = [];
foreach (Type concrete in ConcreteTypes)
{
OpenApiSchema s = await context.GetOrCreateSchemaAsync(concrete, null, ct);
context.Document?.AddComponent(concrete.Name, s);
schema.OneOf.Add(new OpenApiSchemaReference(concrete.Name, context.Document));
}
}
}
// Program.cs:
builder.Services.AddOpenApi(options => options.AddSchemaTransformer<MySchemaTransformer>());API notes (bitten while building this — verify with microsoft-learn MCP if unsure):
GetOrCreateSchemaAsync(Type, ApiParameterDescription?, CancellationToken)— the second parameter is not optional, passnull.Microsoft.OpenApi2.x (bundled with .NET 10): model types live in the rootMicrosoft.OpenApinamespace — there is noMicrosoft.OpenApi.Models.- Per-route customization:
AddOpenApiOperationTransformer(...)(the oldWithOpenApi(op => ...)is deprecated, ASPDEPR002). - Transformer execution order: schema → operation → document transformers
(see
microsoft-learn: "Customize OpenAPI documents").
- Proxies are generated from
dbus-xml/*.xmlvia csprojAdditionalFiles(DBusGeneratorMode="Proxy"); no hand-written D-Bus client code. CompilerGeneratedFilesOutputPathmust stay under$(BaseIntermediateOutputPath)— a path inside the project directory compiles the emitted file twice (CS0101 storm).- For any new unit/job/manager member: extend the XML introspection files and rebuild;
consult the
Controlling systemd services using D-Bus with TMDS libraryskill.
- Work autonomously: read
DECISIONS.mdand relevant skills before coding, make the change, verify (build + runtime/curl checks when the change affects runtime behavior), and report the result. Do not ask permission for routine steps. - Ask user when a choice is genuinely ambiguous. Pick the option consistent with existing project conventions and note it in the final summary.
- Escalation exception to the no-intervention rule: if you cannot solve a problem
after a reasonable number of attempts (guideline: 3 distinct approaches), stop
trying and ask the user to adjust the environment — e.g. add or fix a SKILL
file under
.opencode/skills/, enable an MCP server (such asmicrosoft-learn), add a NuGet package, install a tool, or grant missing permissions. State concretely what was tried, what failed, and exactly which environment change would unblock. This request for environment adjustment is an explicit exception to the minimal-human-intervention requirement and is the correct behavior, not a failure. - Never commit unless explicitly asked.