diff --git a/Dockerfile b/Dockerfile index 95e4c4a..52d4fee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,12 @@ COPY apps/gateway/pyproject.toml apps/gateway/ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --no-install-workspace -# Copy source trees, then install the workspace packages on top. +# Copy source trees, then install the workspace packages on top. Every +# workspace member that uv.lock installs must be present — sdks/python is a +# member too, and a full sync fails without it. COPY packages/ packages/ COPY apps/gateway/ apps/gateway/ +COPY sdks/python/ sdks/python/ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev @@ -23,9 +26,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \ FROM python:3.12-slim-bookworm AS runtime WORKDIR /app +# Workspace members are installed editable, so the venv points back into +# these source trees — copy every member the venv references. COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/packages packages/ COPY --from=builder /app/apps/gateway apps/gateway/ +COPY --from=builder /app/sdks/python sdks/python/ ENV PATH="/app/.venv/bin:$PATH" \ PYTHONUNBUFFERED=1 \ @@ -33,5 +39,8 @@ ENV PATH="/app/.venv/bin:$PATH" \ EXPOSE 8000 -# Full gateway server is wired in Phase 3, Step 3.1. -CMD ["python", "-c", "import rag_gateway; print('AgentContextOS', rag_gateway.__version__)"] +# Serve the gateway via the config-driven entrypoint (v1.0.1): when +# RAG_CONFIG_PATH names a mounted rag.yaml the app is built from it +# (fail-fast on a broken config); unset, the noop-wired app serves every +# route with zero infrastructure. See docs/reference/gateway.md. +CMD ["uvicorn", "rag_gateway.serve:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index 45772e9..84c023b 100644 --- a/README.md +++ b/README.md @@ -110,13 +110,11 @@ Without a gateway URL it renders seed data (every page carries a "Demo data" bad | Channel | Install | Notes | |---------|---------|-------| -| **Container** | `docker run -p 8000:8000 ghcr.io/officialcodework/agentcontextos/rag-gateway:v1.0.0 uvicorn rag_gateway.app:app --host 0.0.0.0 --port 8000` | cosign-signed, SBOM-attested | -| **Helm** | `helm install rag oci://ghcr.io/officialcodework/agentcontextos/charts/rag-platform` | chart source in [`infra/helm/rag-platform/`](infra/helm/rag-platform/); wire backends via `values.yaml` | +| **Container** | `docker run -p 8000:8000 ghcr.io/officialcodework/agentcontextos/rag-gateway:1.0.1` | serves on `:8000` out of the box (noop wiring); mount a `rag.yaml` + set `RAG_CONFIG_PATH` for real backends — [config-driven serving](docs/reference/gateway.md#config-driven-serving); cosign-signed, SBOM-attested | +| **Helm** | `helm install rag oci://ghcr.io/officialcodework/agentcontextos/charts/rag-platform` | chart source in [`infra/helm/rag-platform/`](infra/helm/rag-platform/); put your full `rag.yaml` in `values.yaml` under `config.ragYaml` | | **PyPI** | `pip install rag-platform` | meta-package pins all components to a reproducible server stack | | **Air-gapped** | signed offline bundle + `install.sh` | [air-gap install guide](docs/guides/airgap-install.md) | -> **Known gap at v1.0.0:** the published image's *default* CMD prints the version instead of starting the server — pass the `uvicorn` serve command explicitly as shown above. The Helm chart inherits the same gap (its pods rely on the image default), so review the chart against your serve command before first install. A config-driven serve entrypoint is tracked for v1.0.1. - One `vX.Y.Z` tag publishes every channel — signed (cosign keyless) with SBOMs. Details: [packaging & distribution](docs/guides/packaging-distribution.md). ## Talk to it from your stack diff --git a/TRACKER.md b/TRACKER.md index e6e97a5..d15c09d 100644 --- a/TRACKER.md +++ b/TRACKER.md @@ -16,10 +16,11 @@ | **Last updated** | 2026-06-10 | | **Current phase** | Phase 7 — Pilot, Harden, GA (**10 / 10 steps ✅**) — **all phases complete** | | **Overall** | **84 / 84 steps ✅ — v1.0.0 GA** | -| **Next action** | **🎉 GA reached.** Engineering scope (Phases 0–7) complete; `v1.0.0` cut. Remaining items are external/process (external pentest, SOC 2 audit, registry first-publish, signed referenceable customers, on-call staffing + status page, launch assets) — tracked in the [GA readiness checklist](docs/ga/ga-readiness-checklist.md). Post-GA: the V1.1 backlog. | +| **Next action** | **🎉 GA reached.** Engineering scope (Phases 0–7) complete; `v1.0.0` cut. Remaining items are external/process (external pentest, SOC 2 audit, registry first-publish, signed referenceable customers, on-call staffing + status page, launch assets) — tracked in the [GA readiness checklist](docs/ga/ga-readiness-checklist.md). Post-GA: cut the **v1.0.1** patch tag (serve-path fix below; note: GitHub Actions runs are currently blocked by an account **billing/spending-limit** failure — every `docker.yml` run since GA failed at job start, so the GHCR image was never actually published), then the V1.1 backlog. | **Recently shipped** +- **v1.0.1** ✅ *(patch)* — **production serve path fixed end-to-end.** The image's default CMD (a Phase-0 placeholder that printed the version and exited) now serves via the new config-driven **`rag_gateway.serve:create_app`** uvicorn-factory entrypoint: `RAG_CONFIG_PATH` set → `build_app_from_config(load(path))`, unset → noop `build_app()`, set-but-broken → **fail fast** (`ConfigError`), never a silent noop fallback. The image **build** itself was also broken — the full `uv sync` needs every workspace member on disk but `sdks/python` was never copied (`Distribution not found`); both stages now copy it. New public **`GET /readyz`** backs the Helm chart's readiness probe (liveness stays `/healthz`). The chart's ConfigMap previously rendered an *invalid* `rag.yaml` (`env:/log_level:/service:` against the strict `extra='forbid'` schema → guaranteed CrashLoop); it now renders a minimal valid document with a **`config.ragYaml`** passthrough for full platform configs (`${VAR}` env interpolation keeps secrets out of the ConfigMap), chart `0.2.0` / `appVersion` `1.0.1` (the old `0.1.0` appVersion pointed the default image tag at a tag that never existed), `values.dev.yaml` `env: development` → `local`. Docs: image tags are published *without* the `v` prefix — `:v1.0.0`-style pulls in README/release-notes never matched. [release notes](docs/release-notes/v1.0.1.md) [#185](https://github.com/officialCodeWork/AgentContextOS/pull/185) - **7.10** ✅ **GA cutover — v1.0.0** 🎉 — the close of an 8-phase, 84-step build. Cut **`v1.0.0`** with **Semantic Versioning** over the drift-gated public contracts (REST/OpenAPI · gRPC proto · `rag.yaml` · SDKs · `ragctl`) + a deprecation policy ([ADR-0052](docs/adr/ADR-0052-ga-cutover.md)); the `rag-platform` meta bumped to 1.0.0. GA **release notes** ([v1.0.0](docs/release-notes/v1.0.0.md)) + a **GA readiness checklist** ([ga-readiness-checklist](docs/ga/ga-readiness-checklist.md)) that maps every Phase 7 exit gate to a CI gate / in-repo artifact, with inherently-external items (external pentest, SOC 2 audit, registry first-publish, signed customers, on-call staffing, launch assets) marked **(external)** — the same in-repo-machine vs external-deliverable split used across Phase 7. The `v1.0.0` tag fans out to every channel via `docker.yml` + `release.yml` + `release-airgap.yml`. - **7.9** ✅ Billing & metering — per-tenant usage accounting, **mirroring the 5.6c cost pattern** (dataclasses in `rag-observability`, the gateway wraps a Pydantic response → **no `dist/schemas` churn**). New `rag_observability.billing`: a `UsageMeter` (observe-only per-tenant, per-dimension counters: queries / docs / storage / reranker / LLM tokens) fed O(1) from `record_request_usage` independent of quotas; `GET /v1/billing/usage` serves the per-tenant dashboard; pure `generate_invoice` + `PricingModel`/`load_pricing` price an invoice **from `marketplace/pricing.yaml`** (a price change is config) and **reconcile with metered usage ±0.5% by construction**; a `BillingProvider` Protocol seam for Stripe / marketplace metering (degrade-open). Inert by default (`cfg.billing`); `ragctl billing` drives meter→invoice→reconcile; **no governed SPI call** (policy-coverage linter unchanged); only `dist/openapi` + `dist/rag.schema` regenerate. Deferred: the concrete Stripe/marketplace adapters, the docs/reranker/storage feed points, an admin usage card. [ADR-0051](docs/adr/ADR-0051-billing-metering.md) [#182](https://github.com/officialCodeWork/AgentContextOS/pull/182) - **7.8** ✅ Support, SLAs & on-call — the support model + ops runbooks, **grounded in the platform's own signals**. Four support tiers + per-tier SLA targets (matching `marketplace/pricing.yaml`) in [`support-sla.md`](docs/guides/support-sla.md); a new `docs/runbooks/` area with **one runbook per alert type** (`alerts.md` — acl/tenant escape · `breaker.opened` · `drift.detected` · cost anomaly · `quota.exceeded` · injection spike · gateway degraded · latency/availability SLO · `eval.regression` · ingest), each naming the exact event / `/v1/status/*` / SLO that raises it; an incident-response process + a blameless postmortem template; the status page reads **measured** health (`/v1/status/health` + Grafana SLOs), not manual toggles. **Paging reuses the Step 3.9 webhook system** (PagerDuty as a subscriber — no new code). Deferred: hosted status-page deploy, the PagerDuty account, staffing. [ADR-0050](docs/adr/ADR-0050-support-sla-oncall.md) [#181](https://github.com/officialCodeWork/AgentContextOS/pull/181) @@ -76,8 +77,8 @@ | 4 | Reliability | 6 | **6** | 0 | | 5 | Eval & Observability | 7 | **7** | 0 | | 6 | Governance & Tenancy | 10 | **10** | 0 | -| 7 | Pilot, Harden, GA | 10 | **3** | 7 | -| **Total** | | **84** | **77** | **7** | +| 7 | Pilot, Harden, GA | 10 | **10** | 0 | +| **Total** | | **84** | **84** | **0** | --- @@ -1061,6 +1062,8 @@ Complete log of every PR. Routine Dependabot bumps are grouped; everything else | [#181](https://github.com/officialCodeWork/AgentContextOS/pull/181) | 2026-06-10 | docs(support): support tiers + SLA + per-alert runbooks + incident response (Step 7.8) | | [#182](https://github.com/officialCodeWork/AgentContextOS/pull/182) | 2026-06-10 | feat(billing): per-tenant usage metering + usage API + invoice generation (Step 7.9) | | [#183](https://github.com/officialCodeWork/AgentContextOS/pull/183) | 2026-06-10 | chore(release): GA cutover — v1.0.0 release notes + readiness checklist + SemVer policy (Step 7.10) | +| [#184](https://github.com/officialCodeWork/AgentContextOS/pull/184) | 2026-06-10 | docs: GA polish — site publishing scope, HLD v1.2, ragctl reference, port + link fixes | +| [#185](https://github.com/officialCodeWork/AgentContextOS/pull/185) | 2026-06-10 | fix(gateway): v1.0.1 — container/Helm serve path: config-driven entrypoint + /readyz + valid chart config | | #78–#80, #116–#118 | Open | Dependabot bumps — awaiting merge | | #81 | Closed | Dependabot bump — superseded | diff --git a/apps/gateway/src/rag_gateway/app.py b/apps/gateway/src/rag_gateway/app.py index 38dc6e4..6d7a18f 100644 --- a/apps/gateway/src/rag_gateway/app.py +++ b/apps/gateway/src/rag_gateway/app.py @@ -3,13 +3,15 @@ The gateway composes Phase 2 components without re-implementing any of them. Each pipeline (understanding, retrieval, rerank, pack, optional LLM) is injectable via :func:`build_app` so tests -can swap stubs. Production deployments will replace the default -noop wiring with real backends sourced from ``rag.yaml`` once the -corpus-router config work lands in Step 3.5. +can swap stubs. Production deployments replace the default noop +wiring with real backends sourced from ``rag.yaml`` via +:func:`rag_gateway.wiring.build_app_from_config`; containers serve it +through :func:`rag_gateway.serve.create_app` (``uvicorn --factory``). Routes: * ``GET /healthz`` — public liveness probe. +* ``GET /readyz`` — public readiness probe (wiring is complete). * ``GET /v1/info`` — public service descriptor. * ``POST /v1/ingest/document`` — multipart ingest (Step 1.10). * ``POST /v1/query`` — full RAG query (Step 3.1). @@ -849,6 +851,12 @@ def build_app( async def healthz() -> dict[str, str]: return {"status": "ok"} + @app.get("/readyz", tags=["health"]) + async def readyz() -> dict[str, str]: + """Readiness probe — all wiring happens in the app factory, so a + served response means the app is fully constructed and routable.""" + return {"status": "ready"} + @app.get("/v1/info", tags=["health"]) async def info() -> dict[str, Any]: return { @@ -877,6 +885,7 @@ async def info() -> dict[str, Any]: "WS /v1/status/ws", "GET /v1/connectors/status", "GET /healthz", + "GET /readyz", "GET /v1/info", "GET /openapi.json", "GET /docs", diff --git a/apps/gateway/src/rag_gateway/serve.py b/apps/gateway/src/rag_gateway/serve.py new file mode 100644 index 0000000..8995e39 --- /dev/null +++ b/apps/gateway/src/rag_gateway/serve.py @@ -0,0 +1,66 @@ +"""Production serve entrypoint — the container / Helm run path (v1.0.1). + +``uvicorn rag_gateway.serve:create_app --factory`` is the gateway image's +default ``CMD``. :func:`create_app` picks the wiring from the environment: + +* ``RAG_CONFIG_PATH`` set — load + validate that ``rag.yaml`` via + :func:`rag_config.load` and build the config-driven app with + :func:`rag_gateway.wiring.build_app_from_config`. A set-but-broken path + (missing file, bad YAML, schema violation) raises + :class:`~rag_core.errors.ConfigError` so the process fails fast — a typo'd + mount must crash the pod loudly, never silently serve noop wiring. +* ``RAG_CONFIG_PATH`` unset/empty — serve :func:`rag_gateway.app.build_app`'s + noop wiring, which boots every route with zero infrastructure (the same + app the quickstart and ``ragctl`` drive in-process). + +The Helm chart mounts its rendered config at ``/app/config/rag.yaml`` and +sets ``RAG_CONFIG_PATH`` to match; a bare ``docker run`` serves the noop app. +See ``docs/reference/gateway.md`` ("Config-driven serving"). +""" + +from __future__ import annotations + +import os + +from fastapi import FastAPI +from rag_config import load +from rag_core import get_logger + +from rag_gateway.app import build_app +from rag_gateway.wiring import build_app_from_config + +_log = get_logger(__name__) + +#: Environment variable naming the rag.yaml to serve. The Helm chart sets it +#: to the mounted ConfigMap path; compose files / docker run may set it to any +#: readable path inside the container. +RAG_CONFIG_PATH_ENV = "RAG_CONFIG_PATH" + + +def create_app() -> FastAPI: + """Build the gateway app for ``uvicorn --factory`` serving. + + Raises + ------ + ConfigError + If ``RAG_CONFIG_PATH`` is set but the file is missing, unparseable, + or fails ``RagConfig`` schema validation. + """ + path = os.environ.get(RAG_CONFIG_PATH_ENV, "").strip() + if not path: + _log.info( + "gateway.serve.start", + extra={"event_kind": "gateway.serve.start", "wiring": "noop"}, + ) + return build_app() + + cfg = load(path) + _log.info( + "gateway.serve.start", + extra={ + "event_kind": "gateway.serve.start", + "wiring": "config", + "config_path": path, + }, + ) + return build_app_from_config(cfg) diff --git a/apps/gateway/tests/test_app.py b/apps/gateway/tests/test_app.py index ab381dd..8c58c23 100644 --- a/apps/gateway/tests/test_app.py +++ b/apps/gateway/tests/test_app.py @@ -17,6 +17,13 @@ def test_healthz_returns_ok() -> None: assert resp.json() == {"status": "ok"} +def test_readyz_returns_ready() -> None: + client = _client() + resp = client.get("/readyz") + assert resp.status_code == 200 + assert resp.json() == {"status": "ready"} + + def test_info_lists_endpoints() -> None: resp = _client().get("/v1/info") assert resp.status_code == 200 diff --git a/apps/gateway/tests/test_serve.py b/apps/gateway/tests/test_serve.py new file mode 100644 index 0000000..b07a774 --- /dev/null +++ b/apps/gateway/tests/test_serve.py @@ -0,0 +1,58 @@ +"""Serve entrypoint tests — ``create_app`` factory wiring (v1.0.1). + +Covers the container/Helm run path: ``RAG_CONFIG_PATH`` unset serves the +noop app; set + valid builds the config-driven app; set + broken fails +fast (never a silent noop fallback in production). +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient +from rag_core.errors import ConfigError +from rag_gateway.serve import RAG_CONFIG_PATH_ENV, create_app + + +def test_unset_env_serves_noop_app(monkeypatch) -> None: + monkeypatch.delenv(RAG_CONFIG_PATH_ENV, raising=False) + client = TestClient(create_app()) + assert client.get("/healthz").json() == {"status": "ok"} + assert client.get("/readyz").json() == {"status": "ready"} + + +def test_blank_env_serves_noop_app(monkeypatch) -> None: + monkeypatch.setenv(RAG_CONFIG_PATH_ENV, " ") + client = TestClient(create_app()) + assert client.get("/readyz").status_code == 200 + + +def test_valid_config_builds_config_driven_app(tmp_path: Path, monkeypatch) -> None: + cfg = tmp_path / "rag.yaml" + cfg.write_text( + 'version: "1"\nplatform:\n name: serve-test\n env: production\n', + encoding="utf-8", + ) + monkeypatch.setenv(RAG_CONFIG_PATH_ENV, str(cfg)) + client = TestClient(create_app()) + assert client.get("/healthz").json() == {"status": "ok"} + assert client.get("/readyz").json() == {"status": "ready"} + # The config-driven app serves the full surface, not just probes. + assert client.get("/v1/info").status_code == 200 + + +def test_missing_config_file_fails_fast(monkeypatch) -> None: + monkeypatch.setenv(RAG_CONFIG_PATH_ENV, "/nonexistent/rag.yaml") + with pytest.raises(ConfigError): + create_app() + + +def test_invalid_config_fails_fast(tmp_path: Path, monkeypatch) -> None: + bad = tmp_path / "rag.yaml" + # RagConfig is strict (extra='forbid') — unknown keys must crash the pod, + # not silently fall back to noop wiring. + bad.write_text("not_a_rag_config_key: 1\n", encoding="utf-8") + monkeypatch.setenv(RAG_CONFIG_PATH_ENV, str(bad)) + with pytest.raises(ConfigError): + create_app() diff --git a/dist/openapi.json b/dist/openapi.json index 7f9d4c3..e9b9437 100644 --- a/dist/openapi.json +++ b/dist/openapi.json @@ -4891,6 +4891,32 @@ ] } }, + "/readyz": { + "get": { + "description": "Readiness probe — all wiring happens in the app factory, so a\nserved response means the app is fully constructed and routable.", + "operationId": "readyz_readyz_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "title": "Response Readyz Readyz Get", + "type": "object" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Readyz", + "tags": [ + "health" + ] + } + }, "/scim/v2/Groups": { "get": { "operationId": "list_groups_scim_v2_Groups_get", diff --git a/dist/openapi.yaml b/dist/openapi.yaml index 1f3695a..9e152cc 100644 --- a/dist/openapi.yaml +++ b/dist/openapi.yaml @@ -4266,6 +4266,25 @@ paths: summary: Healthz tags: - health + /readyz: + get: + description: 'Readiness probe — all wiring happens in the app factory, so a + + served response means the app is fully constructed and routable.' + operationId: readyz_readyz_get + responses: + '200': + content: + application/json: + schema: + additionalProperties: + type: string + title: Response Readyz Readyz Get + type: object + description: Successful Response + summary: Readyz + tags: + - health /scim/v2/Groups: get: operationId: list_groups_scim_v2_Groups_get diff --git a/docs/README.md b/docs/README.md index 830beb9..3e9d076 100644 --- a/docs/README.md +++ b/docs/README.md @@ -261,6 +261,7 @@ incident-response process and the postmortem template. | File | Description | |------|-------------| | [v1.0.0.md](release-notes/v1.0.0.md) | **GA release notes (Step 7.10)** — highlights across retrieval / surfaces / governance / reliability / observability / operability; one-command install per channel; the 8-phase journey; versioning + support | +| [v1.0.1.md](release-notes/v1.0.1.md) | **v1.0.1 patch** — fixes the container/Helm run path: the image's default CMD now serves the gateway via the config-driven `rag_gateway.serve:create_app` entrypoint (`RAG_CONFIG_PATH`), `GET /readyz` backs the chart's readiness probe, and the chart renders a *valid* `rag.yaml` (with a `config.ragYaml` passthrough) | ## ga/ diff --git a/docs/guides/packaging-distribution.md b/docs/guides/packaging-distribution.md index f77030f..8a0839e 100644 --- a/docs/guides/packaging-distribution.md +++ b/docs/guides/packaging-distribution.md @@ -10,11 +10,20 @@ how a single release tag publishes them all — signed and SBOM-attested. | PyPI (server) | `pip install rag-platform` (or `"rag-platform[all]"`) | | PyPI (SDK) | `pip install agentcontextos` | | npm (SDK) | `npm i @agentcontextos/sdk` | -| Container (gateway) | `docker pull ghcr.io/officialcodework/agentcontextos/rag-gateway:vX` | -| Container (admin-ui) | `docker pull ghcr.io/officialcodework/agentcontextos/admin-ui:vX` | +| Container (gateway) | `docker pull ghcr.io/officialcodework/agentcontextos/rag-gateway:X.Y.Z` | +| Container (admin-ui) | `docker pull ghcr.io/officialcodework/agentcontextos/admin-ui:X.Y.Z` | | Helm (OCI) | `helm install rag oci://ghcr.io/officialcodework/agentcontextos/charts/rag-platform --version X` | | Air-gap | download the release bundle, run `install.sh` (Step 6.9) | +Image tags follow the release version without the `v` prefix (`1.0.1`, plus a +floating `1.0`), as emitted by `docker/metadata-action`'s semver patterns. + +The gateway image serves on `:8000` out of the box — its default CMD runs +`uvicorn rag_gateway.serve:create_app --factory` (v1.0.1), which builds the +config-driven app when `RAG_CONFIG_PATH` names a mounted `rag.yaml` and the +noop-wired app otherwise. See +[gateway.md — config-driven serving](../reference/gateway.md#config-driven-serving). + The [`rag-platform`](../../packaging/rag-platform/README.md) PyPI meta-package pins the workspace component distributions, so `pip install rag-platform==X` resolves a reproducible server stack. SDKs live under [`sdks/`](../../sdks). diff --git a/docs/reference/gateway.md b/docs/reference/gateway.md index 8fd9654..eed3281 100644 --- a/docs/reference/gateway.md +++ b/docs/reference/gateway.md @@ -7,6 +7,7 @@ | Route | Purpose | |-------|---------| | `GET /healthz` | Public liveness probe. | +| `GET /readyz` | Public readiness probe — wiring completes in the app factory, so `ready` means fully constructed and routable. | | `GET /v1/info` | Public service descriptor (version + endpoint list). | | `POST /v1/ingest/document` | Multipart document ingest (Step 1.10). | | `POST /v1/query` | Full RAG: understanding → router → optional rerank / pack / LLM generate. | @@ -256,6 +257,36 @@ app = build_app( See [architecture/gateway.md](../architecture/gateway.md) for the design rationale and the dependency injection pattern. +## Config-driven serving + +`rag_gateway.serve:create_app` (v1.0.1) is the production serve entrypoint for containers and Helm — a [uvicorn factory](https://www.uvicorn.org/#application-factories) that picks its wiring from the environment: + +| `RAG_CONFIG_PATH` | App served | +|-------------------|------------| +| set to a readable `rag.yaml` | `build_app_from_config(load(path))` — config-driven backends, corpora, tenants, governance | +| unset / empty | `build_app()` — noop wiring; every route serves with zero infrastructure | +| set but missing / invalid | **fails fast** with `ConfigError` — a typo'd mount crashes the pod loudly instead of silently serving noop wiring in production | + +```bash +# The gateway image's default CMD — no arguments needed: +uvicorn rag_gateway.serve:create_app --factory --host 0.0.0.0 --port 8000 + +# Bare container — serves the noop-wired app on :8000: +docker run -p 8000:8000 ghcr.io/officialcodework/agentcontextos/rag-gateway:1.0.1 + +# Config-driven — mount a rag.yaml and point RAG_CONFIG_PATH at it: +docker run -p 8000:8000 \ + -v "$PWD/rag.yaml:/app/config/rag.yaml:ro" \ + -e RAG_CONFIG_PATH=/app/config/rag.yaml \ + ghcr.io/officialcodework/agentcontextos/rag-gateway:1.0.1 +``` + +### Internals — the Helm flow + +The chart wires this end-to-end: it renders `.Values.config.ragYaml` (or a minimal valid default) into a ConfigMap, mounts it at `/app/config/rag.yaml`, and sets `RAG_CONFIG_PATH` to match. `${VAR}` / `${VAR:-default}` references inside the file are interpolated from the container environment at load time, so secrets stay in `Secret`-injected env vars (e.g. `${RAG_POSTGRES_PASSWORD}`) and never land in the ConfigMap. `RagConfig` is strict (`extra='forbid'`): an unknown key fails validation and the pod CrashLoops with the offending field path in the error — by design, so a malformed config is impossible to miss. + +Probes: `GET /healthz` is liveness, `GET /readyz` is readiness. All wiring happens inside the app factory before uvicorn binds the socket, so a `ready` response means the app is fully constructed and routable. + ## Related - [architecture/gateway.md](../architecture/gateway.md) — design + middleware rationale. diff --git a/docs/reference/rest-api.md b/docs/reference/rest-api.md index b836725..048e490 100644 --- a/docs/reference/rest-api.md +++ b/docs/reference/rest-api.md @@ -180,6 +180,19 @@ Healthz |--------|--------|-------------| | 200 | `object` | Successful Response | +#### `GET /readyz` + +Readyz + +Readiness probe — all wiring happens in the app factory, so a +served response means the app is fully constructed and routable. + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `object` | Successful Response | + #### `GET /v1/info` Info diff --git a/docs/release-notes/v1.0.1.md b/docs/release-notes/v1.0.1.md new file mode 100644 index 0000000..215a7b7 --- /dev/null +++ b/docs/release-notes/v1.0.1.md @@ -0,0 +1,62 @@ +# AgentContextOS v1.0.1 — patch + +A PATCH release per the [SemVer policy](../adr/ADR-0052-ga-cutover.md): it fixes +the container / Helm run path so the shipped image and chart serve the gateway +out of the box. No public contract changes beyond one additive route. + +## Fixed + +**Container default CMD serves the gateway.** The image's `CMD` was a stale +Phase-0 placeholder that printed the version and exited. It now runs +`uvicorn rag_gateway.serve:create_app --factory --host 0.0.0.0 --port 8000`, +so `docker run -p 8000:8000 …` serves immediately. + +**Image build repaired.** The Dockerfile's full `uv sync` requires every +workspace member on disk, but `sdks/python` (a member since Step 3.7) was never +copied — `docker build` failed at the second sync with +`Distribution not found at: …/sdks/python`. Both the builder and runtime stages +now copy it (members install editable, so the venv references the source tree). + +**Config-driven serve entrypoint.** New `rag_gateway.serve:create_app`: when +`RAG_CONFIG_PATH` names a mounted `rag.yaml`, the app is built from it via +`build_app_from_config(load(path))`; unset, the noop-wired `build_app()` serves +every route with zero infrastructure. A set-but-broken path (missing file, bad +YAML, schema violation) **fails fast** with `ConfigError` — a typo'd mount +crashes the pod loudly instead of silently serving noop wiring in production. +See [gateway.md — config-driven serving](../reference/gateway.md#config-driven-serving). + +**`GET /readyz`.** The Helm chart's readiness probe targeted `/readyz`, which +did not exist. It is now a real public route (liveness stays `/healthz`); all +wiring happens in the app factory before the socket binds, so `ready` means +fully constructed and routable. + +**Helm chart renders a *valid* `rag.yaml`.** The ConfigMap previously emitted +`env:/log_level:/service:` keys that the strict `RagConfig` schema +(`extra='forbid'`) rejects. It now renders a minimal valid document by default, +and a new `config.ragYaml` value mounts your complete platform config verbatim; +`${VAR}` / `${VAR:-default}` references are interpolated from the container +environment at load time so secrets stay in `Secret`-injected env vars. +`values.dev.yaml`'s `env: development` (not a valid `PlatformEnv`) is now +`local`. Chart `0.2.0`, `appVersion` `1.0.1` (also fixing the default image tag, +which pointed at a non-existent `0.1.0`). + +**Documented image tags corrected.** Image tags are emitted *without* the `v` +prefix (`1.0.1`, floating `1.0`); docs previously showed `:v1.0.0`-style tags +that the release pipeline never produces. + +## Install + +```bash +docker pull ghcr.io/officialcodework/agentcontextos/rag-gateway:1.0.1 +helm install rag oci://ghcr.io/officialcodework/agentcontextos/charts/rag-platform --version 0.2.0 +pip install rag-platform # full self-hosted server stack + ragctl +``` + +## Upgrade notes + +- No action needed for programmatic deployments (`build_app` / + `build_app_from_config` are unchanged). +- If you worked around the v1.0.0 gap by overriding the container command, + the override can be dropped — the default CMD now serves. +- Helm users who templated the old ConfigMap keys (`env` / `log_level` / + `service`) should move their platform config into `config.ragYaml`. diff --git a/infra/helm/rag-platform/Chart.yaml b/infra/helm/rag-platform/Chart.yaml index 709709a..18fb863 100644 --- a/infra/helm/rag-platform/Chart.yaml +++ b/infra/helm/rag-platform/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: rag-platform description: AgentContextOS — production-grade multi-tenant RAG platform type: application -version: 0.1.0 -appVersion: "0.1.0" +version: 0.2.0 +appVersion: "1.0.1" keywords: - rag - llm diff --git a/infra/helm/rag-platform/templates/_helpers.tpl b/infra/helm/rag-platform/templates/_helpers.tpl index 66e8798..364bece 100644 --- a/infra/helm/rag-platform/templates/_helpers.tpl +++ b/infra/helm/rag-platform/templates/_helpers.tpl @@ -67,3 +67,15 @@ Gateway image reference — falls back to Chart.AppVersion. {{- $tag := .Values.image.tag | default .Chart.AppVersion }} {{- printf "%s:%s" .Values.image.repository $tag }} {{- end }} + +{{/* +Default rag.yaml — a minimal *valid* RagConfig document (the schema is strict: +unknown keys fail validation and the gateway fails fast at startup). Serves +the noop backends; real deployments set .Values.config.ragYaml instead. +*/}} +{{- define "rag-platform.defaultRagYaml" -}} +version: "1" +platform: + name: {{ .Values.config.service.name | default "rag-gateway" }} + env: {{ .Values.config.env }} +{{- end }} diff --git a/infra/helm/rag-platform/templates/configmap.yaml b/infra/helm/rag-platform/templates/configmap.yaml index f1bd83c..9fc4022 100644 --- a/infra/helm/rag-platform/templates/configmap.yaml +++ b/infra/helm/rag-platform/templates/configmap.yaml @@ -6,9 +6,13 @@ metadata: labels: {{- include "rag-platform.labels" . | nindent 4 }} data: + # Mounted at /app/config/rag.yaml and read by the gateway's serve + # entrypoint via RAG_CONFIG_PATH (rag_gateway.serve). Must be a valid + # RagConfig document — the schema is strict, so a bad config crashes the + # pod at startup instead of silently serving noop wiring. Set + # .Values.config.ragYaml to your full platform config; ${VAR} / + # ${VAR:-default} references are interpolated from the container + # environment at load time (use them for the secret-injected env vars, + # e.g. ${RAG_POSTGRES_PASSWORD}, so secrets never land in this ConfigMap). rag.yaml: | - env: {{ .Values.config.env }} - log_level: {{ .Values.config.log_level }} - service: - name: {{ .Values.config.service.name | default "rag-gateway" }} - version: {{ .Values.config.service.version | default .Chart.AppVersion }} +{{ default (include "rag-platform.defaultRagYaml" .) .Values.config.ragYaml | indent 4 }} diff --git a/infra/helm/rag-platform/values.dev.yaml b/infra/helm/rag-platform/values.dev.yaml index b006c9e..60638a6 100644 --- a/infra/helm/rag-platform/values.dev.yaml +++ b/infra/helm/rag-platform/values.dev.yaml @@ -23,7 +23,7 @@ podDisruptionBudget: affinity: {} config: - env: development + env: local # PlatformEnv: local | staging | production log_level: DEBUG backends: diff --git a/infra/helm/rag-platform/values.yaml b/infra/helm/rag-platform/values.yaml index 957230e..99c6cab 100644 --- a/infra/helm/rag-platform/values.yaml +++ b/infra/helm/rag-platform/values.yaml @@ -83,14 +83,36 @@ affinity: topologyKey: kubernetes.io/hostname # --------------------------------------------------------------------------- -# Application configuration — mounted as /app/config/rag.yaml +# Application configuration — rendered into the ConfigMap, mounted as +# /app/config/rag.yaml, and read by the gateway's serve entrypoint via +# RAG_CONFIG_PATH (rag_gateway.serve). # --------------------------------------------------------------------------- config: - env: production - log_level: INFO + env: production # platform.env in the rendered rag.yaml: local | staging | production + log_level: INFO # exported to the pod as RAG_LOG_LEVEL service: name: rag-gateway - version: "0.1.0" + + # Full rag.yaml content (a string). When set it is mounted verbatim instead + # of the minimal default above — paste your complete platform config here + # (backends, tenants, corpora, retrieval, governance). It must be a valid + # RagConfig document: the schema is strict, and the gateway fails fast on an + # invalid file (by design — a typo'd config must never silently serve noop + # wiring). ${VAR} / ${VAR:-default} references are interpolated from the + # container environment at load time, so point secrets at the env vars this + # chart injects (e.g. ${RAG_POSTGRES_PASSWORD}) and they never land in the + # ConfigMap. Example: + # ragYaml: | + # version: "1" + # platform: + # name: rag-gateway + # env: production + # backends: + # vector_store: + # provider: qdrant + # config: + # url: "http://${RAG_QDRANT_HOST}:${RAG_QDRANT_HTTP_PORT}" + ragYaml: "" # --------------------------------------------------------------------------- # Backend connection settings (injected as env vars from Secret/ConfigMap) diff --git a/packages/ragctl/tests/test_airgap.py b/packages/ragctl/tests/test_airgap.py index a41609c..f9ed310 100644 --- a/packages/ragctl/tests/test_airgap.py +++ b/packages/ragctl/tests/test_airgap.py @@ -72,9 +72,9 @@ def test_render_sha256sums_is_sorted_and_two_spaced() -> None: def test_chart_helpers_read_real_chart() -> None: - assert airgap.chart_app_version(CHART) == "0.1.0" - ref = airgap.gateway_image_ref(CHART, "v0.1.0") - assert ref == "ghcr.io/officialcodework/agentcontextos/rag-gateway:v0.1.0" + assert airgap.chart_app_version(CHART) == "1.0.1" + ref = airgap.gateway_image_ref(CHART, "1.0.1") + assert ref == "ghcr.io/officialcodework/agentcontextos/rag-gateway:1.0.1" def test_gateway_image_ref_falls_back_to_app_version(tmp_path: Path) -> None: diff --git a/tests/ingest/test_pipeline_document.py b/tests/ingest/test_pipeline_document.py index d31d504..23f0280 100644 --- a/tests/ingest/test_pipeline_document.py +++ b/tests/ingest/test_pipeline_document.py @@ -148,9 +148,7 @@ async def test_indexed_embeddings_are_corpus_scoped( assert result.status == IngestStatus.ingested query_vec = [0.0] * 8 - hits = await vector_store.retrieve_ids( - ctx, query_vec, top_k=100, corpus_ids=[doc.corpus_id] - ) + hits = await vector_store.retrieve_ids(ctx, query_vec, top_k=100, corpus_ids=[doc.corpus_id]) assert len(hits) == result.embedding_count assert all(h.corpus_id == doc.corpus_id for h in hits) diff --git a/tests/redteam/conftest.py b/tests/redteam/conftest.py index 718fcf9..8df1a21 100644 --- a/tests/redteam/conftest.py +++ b/tests/redteam/conftest.py @@ -21,9 +21,7 @@ _REDTEAM_DIR = Path(__file__).parent -def pytest_collection_modifyitems( - config: pytest.Config, items: list[pytest.Item] -) -> None: +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: for item in items: if _REDTEAM_DIR in Path(str(item.fspath)).parents: item.add_marker(pytest.mark.redteam)