Skip to content

Repository files navigation

kgsm-api — KGSM Control Panel API

The per-host Control Panel aggregator API for the KGSM ecosystem. One deployable unit = one host = kgsm + its leaves + this API. It aggregates only its own host's leaves (metrics, assistant, watchdog, firewall) and serves the React SPA (kgsm-web) and other surfaces over a path-versioned REST + WebSocket surface at /api/v1. Cross-host "fleet" rollup is done client-side by the SPA — there is no /fleet endpoint.

It is a leaf-aggregator, not part of the engine: it reaches the engine only through kgsm-lib (the single C#↔engine chokepoint), scrapes the monitor's socket, and never fabricates a metric, status, or alert — measured, or explicitly null/unknown, never invented.

This is a from-scratch rewrite. An earlier .NET 9 attempt (which fabricated metrics) was scrapped; if you find references to SignalR hubs, /api/kgsm/... routes, or BlueprintsController, they describe that dead attempt, not this project.

Stack

  • .NET 10 (JIT) — controllers + EF Core (SQLite). Deliberately NOT Native AOT (the rest of the ecosystem is AOT; this is the one component where JIT is the right call — controllers and EF are AOT-incompatible, and the API is the broadest, highest-churn surface). See CLAUDE.md and docs/m0-aot-spike-findings.md for the decision record.
  • Classic Program + Startup structure; namespaces are TheKrystalShip.Api.*.
  • Persistence is only the API's own operational metadata (audit log, integrations, RAWG cache, host-identity overrides, metrics history) — the domain itself is live-scraped, never stored.

Layout

src/Api/
├── Program.cs · Startup.cs        # composition root (DI + pipeline)
├── Controllers/                   # thin HTTP controllers (/api/v1/*)
├── Contracts/                     # the wire DTOs (frozen per PLAN.md §6)
├── Services/                      # leaf clients + join/aggregation/auth/audit/commands
├── Realtime/                      # the GET /api/v1/stream WebSocket hub + pumps
├── Data/                          # EF Core (AppDbContext, entities)
├── Json/ · Infrastructure/        # JSON conventions + error envelope
tests/Api.Tests/                   # xUnit + WebApplicationFactory (faked seams)
scripts/smoke.sh                   # the HTTP/WS contract suite (the "mock frontend")
deploy/setup.sh · deploy.sh        # provision the host once · (re)deploy the systemd service

Commands

dotnet build kgsm-api.slnx                  # build (Debug)
dotnet run --project src/Api/Api.csproj     # run locally (binds Api__Urls, default :8080)
dotnet test kgsm-api.slnx                    # xUnit suite (401/403/tier matrix, contracts, behavior)
scripts/smoke.sh                             # build Release + run the HTTP/WS contract checks
./deploy/setup.sh                            # ONCE per host — asks for sudo; provisions the headless grant
./deploy/deploy.sh                           # build + (re)deploy the live systemd service (no sudo, no prompts)

Runtime config lives in src/Api/kgsm-api.settings.json, which declares the whole configurable surface with its defaults. An environment variable overrides one key by spelling that key's path with __ (Api__DomainPollMs) — env wins, which is how the systemd unit and the smoke configure a host — and a variable naming a key the file does not declare binds to nothing. A blank leaf endpoint reports its capability absent; auth is ON by default (Api__AuthDisabled=true is the loudly-logged dev escape hatch). Secrets are declared blank in that file and set for real only in the root-owned /etc/kgsm-api/kgsm-api.env.

Versioning

The API reports two distinct version axes — don't conflate them:

Axis Value Where it's surfaced What it means
Route version v1 (ApiInfo.ApiVersion) GET /api/v1version; Host DTO panelVersion The /api/v1 path segment. Additive-only, path-versioned — grow into reserved fields, never break. Changes only on a breaking API generation.
Build version <Version> + git SHA, e.g. 0.1.0+2e8e593692c3 GET /api/v1build; Host DTO identity.build The assembly InformationalVersion — the honest "which build is this host running?". Bumps every release.

How the build version is produced (src/Api/Api.csproj):

  • <Version> is the human-set semver (currently 0.1.0 — pre-1.0; v1.0 is the PLAN.md milestone target, not yet reached). Bump it here as real releases happen.
  • The git short SHA is appended automatically by the SetSourceRevisionId MSBuild target (git rev-parse --short=12 HEADSourceRevisionId), so the SDK stamps the InformationalVersion as <Version>+<sha>. Any build inside the git checkout gets the real commit.
  • Honest degradation: outside a git checkout (or with no git), the target no-ops and the version is just <Version> with no SHA — never a fabricated commit. A deploy can pin it explicitly with dotnet build -p:SourceRevisionId=<sha>.

Reading it at runtime:

curl -s http://127.0.0.1:8080/api/v1            # → { ..., "version": "v1", "build": "0.1.0+<sha>" }
curl -s http://127.0.0.1:8080/api/v1/hosts/<id> # → { ..., "identity": { "build": "0.1.0+<sha>", ... } }

GET /api/v1 is open (pre-auth) — the connect screen reads build/label/region before login; the fuller identity block (incl. OS/kernel) is auth-gated on GET /hosts/{id}. See the Host identity card row in PLAN.md §6.

Runtime leaf configuration — one-time setup

The Services panel lets an admin edit a leaf's configuration at runtime; the API delivers it as an override (never editing the leaf's own deployed config) and applies it by restarting the leaf. That needs a tiny bit of one-time privileged wiring — and ./deploy/setup.sh does it for you, by calling deploy/setup-leaf-config.sh as part of provisioning the host. So a fresh checkout reaches a fully-working state from setup.sh then deploy.sh; the wiring step is idempotent (a re-run is a no-op), which is why it lives in the script that already holds sudo rather than in every deploy.

What that wiring is:

  • A systemd drop-in per config-target leaf (monitor, watchdog, assistant, firewall) that layers an API-owned override env file (/var/lib/kgsm-api/leaf-overrides/<leaf>.env) on last, so the API's overrides win — the leaf never references the API.
  • A scoped polkit rule letting the service user systemctl restart only those four units (restart-family verbs only). This is the only ongoing privileged operation; the API renders the override files unprivileged in its own state dir. NoNewPrivileges=true on the API unit does not block it — systemctl restart is a polkit-authorized D-Bus call to PID 1, not an in-process escalation.

To run the wiring on its own, verify it, or undo it, see deploy/leaf-config/README.md. The config-target units are kept in lockstep with src/Api/Services/Leaves/LeafCatalog.cs.

Authoritative docs

This README is an overview; the authorities are:

  • PLAN.md — the milestone roadmap, principles, and the §6 cross-team contract registry (every frozen wire shape). The authority for this backend.
  • ../architecture.html — the frontend team's external-surface spec (REST/WS/SSE, auth Model A, the §6 conventions). The authority for the wire contracts.
  • ../system-architecture.md — the ecosystem keystone (topology, invariants, open decisions).
  • CLAUDE.md — working guidance, the locked stack decision, and the invariants ("never fabricate", "metric-presence ≠ status", additive-only, single-writer audit).

About

KGSM Web API

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages