diff --git a/.agents/dev/CONTRIBUTING.md b/.agents/dev/CONTRIBUTING.md index 5a22306..0f8e64f 100644 --- a/.agents/dev/CONTRIBUTING.md +++ b/.agents/dev/CONTRIBUTING.md @@ -113,6 +113,23 @@ current phase status). To add a new capability: implement it in `internal/httpapi/impl.go` - Problem+JSON errors via `internal/httpapi/problem.go` — RFC 9457 format - Cursor pagination, If-Match/ETag, idempotency keys, SSE streaming +- **Non-OpenAPI routes carve-out:** ~10 routes are registered manually on + the chi router in `internal/httpapi/server.go` rather than generated from + `openapi.yaml`. These fall into three categories: + 1. **Auth/infra** (`/healthz`, `/api/v1/auth/oidc-*`, `/oidc-callback`) — + must bypass the auth middleware or aren't JSON API endpoints. + 2. **SSE override** (`/api/v1/events/stream`) — in the spec but + re-registered manually because the strict handler can't `Flush()` per + event. + 3. **Ad-hoc aggregations** (`/api/v1/knowledge/recent`, + `/api/v1/knowledge/content/{id}`, `/api/v1/activity/recent`, + `/api/v1/activity/session/{id}`, `/api/v1/learning/timeline`, + `/api/v1/learning/trend`) — return derived/aggregate shapes that don't + map cleanly to a schema type. If one of these stabilizes, promote it + to `openapi.yaml` with a proper schema and migrate the `serve*` + function to a strict handler. + The full list with reasons is in the "Non-OpenAPI routes" comment block + at the top of `NewHandler` in `server.go`. ## Testing philosophy diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 19cf279..9565a83 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -97,6 +97,25 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand MaxAge: 86400, })) + // ─── Non-OpenAPI routes (carve-out) ──────────────────────────────── + // + // These routes are registered manually on the chi router rather than + // generated from api/openapi.yaml. Each has a structural reason it + // can't go through the strict-server codegen: + // + // /healthz — infra liveness probe, no auth, no /api/v1 prefix + // /api/v1/auth/oidc-* — auth flow, must run before auth middleware + // /oidc-callback — standalone HTML page, not a JSON API + // /api/v1/events/stream — in OpenAPI but re-registered for SSE Flush() + // /api/v1/knowledge/recent — ad-hoc aggregation, no schema type yet + // /api/v1/knowledge/content/{id} — returns raw markdown, not a gen type + // /api/v1/activity/recent — recency-ordered, not paginated + // /api/v1/activity/session/{id} — session-scoped aggregation + // /api/v1/learning/timeline — derived view, no backing schema type + // /api/v1/learning/trend — derived view, no backing schema type + // + // See .agents/dev/CONTRIBUTING.md §OpenAPI codegen for the policy. + // Liveness — no auth, no audit (plan SG18). Not exposed via Caddy. r.Get("/healthz", func(w http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithTimeout(req.Context(), 2*time.Second) @@ -173,23 +192,27 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand // Custom (non-OpenAPI) route: recency-ordered knowledge + stats for the // Knowledge page's "what the system has learned" view. Registered after // HandlerWithOptions so it wins over any generated catch-all. + // (See "Non-OpenAPI routes" carve-out block above.) r.With(combinedAuth(cfg, false)).Get("/api/v1/knowledge/recent", s.serveRecentKnowledge) // Custom (non-OpenAPI) route: full markdown content for a knowledge // entity (document/investigation/runbook) by its own id or slug — the // generated /api/v1/knowledge/{id} route (GetEntityKnowledge) answers a // different question (knowledge referencing this entity), not this one. + // (See "Non-OpenAPI routes" carve-out block above.) r.With(combinedAuth(cfg, false)).Get("/api/v1/knowledge/content/{id}", s.serveKnowledgeContent) // Custom (non-OpenAPI) routes: the global activity feed (recency-ordered, // unlike ListExecutions which sorts by target for pagination) and the // per-session "what did this session do" digest. + // (See "Non-OpenAPI routes" carve-out block above.) r.With(combinedAuth(cfg, false)).Get("/api/v1/activity/recent", s.serveRecentActivity) r.With(combinedAuth(cfg, false)).Get("/api/v1/activity/session/{id}", s.serveSessionDigest) // Learning view: capability timeline + success trend, both derived from // executions (real, growing data) rather than the patterns/skills tables, // which are correctly modeled but have no writers anywhere yet. + // (See "Non-OpenAPI routes" carve-out block above.) r.With(combinedAuth(cfg, false)).Get("/api/v1/learning/timeline", s.serveLearningTimeline) r.With(combinedAuth(cfg, false)).Get("/api/v1/learning/trend", s.serveLearningTrend) diff --git a/plans/2026-07-17-codebase-review-and-cleanup.md b/plans/2026-07-17-codebase-review-and-cleanup.md index 7b2a21f..eeb0428 100644 --- a/plans/2026-07-17-codebase-review-and-cleanup.md +++ b/plans/2026-07-17-codebase-review-and-cleanup.md @@ -432,7 +432,7 @@ the gitignore comment); `build` target ensures `bin/` exists; `clean` removes `b | R8 | Add `eslint`+`prettier`+`vitest` to `web/`; wire `svelte-check`+`tsc` into CI; add `web/` CI job | M | Low | ✅ done | | R9 | Define `OikosEvent` discriminated union; eliminate ~15 `any` sites in web | S | Low | | R10 | Replace `` in `ActivityTimeline.svelte:103`; fix `state_referenced_locally` warnings | S | Low | ✅ done | -| R11 | Add the 8 manually-registered `serve*` routes to `openapi.yaml` (or document the carve-out) | S | Low | +| R11 | Add the 8 manually-registered `serve*` routes to `openapi.yaml` (or document the carve-out) | S | Low | ✅ done — documented the carve-out | | R12 | Add `docs/mbse/README.md` "Last verified" header + scheduled re-verification; normalize ADR 0013/0014 template | S | Low | | R13 | Reconcile on-client path (`/opt/homelab/` vs `/opt/homelab-context/`) across AGENTS.md + CLIENTS.md | S | Low | | R14 | Install `golangci-lint`/`staticcheck`/`govulncheck` locally + in CI | S | Low |