Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,34 @@ 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

# ── Runtime stage ─────────────────────────────────────────────────────────────
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 \
PYTHONDONTWRITEBYTECODE=1

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"]
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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** |

---

Expand Down Expand Up @@ -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 |

Expand Down
15 changes: 12 additions & 3 deletions apps/gateway/src/rag_gateway/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down
66 changes: 66 additions & 0 deletions apps/gateway/src/rag_gateway/serve.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions apps/gateway/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions apps/gateway/tests/test_serve.py
Original file line number Diff line number Diff line change
@@ -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()
26 changes: 26 additions & 0 deletions dist/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 19 additions & 0 deletions dist/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/

Expand Down
Loading
Loading