diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..7fe690d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,84 @@ +name: Docs + +on: + pull_request: + paths: + - "docs/**" + - "website/**" + - "dist/openapi.json" + - "scripts/gen_api_reference.py" + - "tests/docs/**" + - ".github/workflows/docs.yml" + push: + branches: [main] + paths: + - "docs/**" + - "website/**" + - "dist/openapi.json" + - "scripts/gen_api_reference.py" + +permissions: + contents: read + +jobs: + api-reference-drift: + name: API reference drift + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Regenerate + diff rest-api.md + run: | + python scripts/gen_api_reference.py + git diff --exit-code docs/reference/rest-api.md \ + || { echo "::error::docs/reference/rest-api.md is stale — run 'task docs:api'"; exit 1; } + + quickstart-tests: + name: Quickstart doc tests + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - run: uv run pytest tests/docs -q + + spell-check: + name: Spell check + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: codespell-project/actions-codespell@v2 + with: + path: docs + skip: "*.svg,*.json,*.html,./dist,./docs/research" + ignore_words_list: "ba,nin,te,ist,fo,ot,assertIn,iif" + + link-check: + name: Link check (offline) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: lycheeverse/lychee-action@v2 + with: + args: "--offline --include-fragments docs/**/*.md website/README.md README.md" + fail: true + + build: + name: Docusaurus build + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Generate API reference + run: python scripts/gen_api_reference.py + - name: Build site + run: | + cd website + npm install + npm run build diff --git a/TRACKER.md b/TRACKER.md index 6d79eb9..231abeb 100644 --- a/TRACKER.md +++ b/TRACKER.md @@ -14,12 +14,13 @@ | | | |---|---| | **Last updated** | 2026-06-10 | -| **Current phase** | Phase 7 — Pilot, Harden, GA (**4 / 10 steps**) — Phases 0–6 ✅ complete | -| **Overall** | **78 / 84 steps** — Phases 0–6 complete | -| **Next action** | **Step 7.5 — Documentation site**: Docusaurus / MkDocs site; API reference generated from OpenAPI; quickstart + concept + how-to + ops guides; versioned docs; link-check + spell-check in CI. | +| **Current phase** | Phase 7 — Pilot, Harden, GA (**5 / 10 steps**) — Phases 0–6 ✅ complete | +| **Overall** | **79 / 84 steps** — Phases 0–6 complete | +| **Next action** | **Step 7.6 — Marketplace listings**: AWS / Azure / GCP Marketplace listings (SaaS offer + self-hosted AMI/Helm); each with pricing, SLA, support tiers, terms, integration guide; documented procurement path. | **Recently shipped** +- **7.5** ✅ Documentation site — a **Docusaurus 3** app (`website/`) that serves the repo `docs/` tree **in place** (single source of truth; `markdown.format: 'detect'` renders hand-written `.md` as CommonMark). The **REST API reference** (`docs/reference/rest-api.md`) is **generated + drift-gated** from `dist/openapi.json` by `scripts/gen_api_reference.py` (`task docs:api`) — never hand-edited. Doc honesty is a `tests/docs/` suite (every `ragctl` / `/v1/` reference in a quickstart must be real) + `lychee` offline link-check + `codespell`, wired into a new `docs` CI workflow. Deferred: Algolia search, versioned docs, the deploy workflow. [ADR-0047](docs/adr/ADR-0047-documentation-site.md) [#178](https://github.com/officialCodeWork/AgentContextOS/pull/178) - **7.4** ✅ Design-partner pilots — the pilot program shipped as a **repo-backed machine** (signing the referenceable partners is the external GTM deliverable, like the 7.3 pentest): an operator runbook + five fill-in templates (`docs/pilots/`), a **customer-support/KB kit** (`docs/pilots/customer-support/` — sample corpus with synthetic PII + a planted injection probe; domain-calibrated criteria), and **`ragctl pilot`** (`onboard` renders the per-tenant config + checklist; `report` assembles a weekly-KPI dashboard + **PASS/FAIL** verdict from the platform's own feedback / drift / cost signal components — the same ones behind `GET /v1/status/*`). KPIs are **pulled, not self-reported**. A worked case study runs it end-to-end on the kit with real `ragctl pilot report` output (satisfaction **+0.733**, **0/5** drift, cost **ok** → **PASS**) + the PII-redaction + injection-blocked security demo. No `dist`/SPI/config change. [#174](https://github.com/officialCodeWork/AgentContextOS/pull/174) - **7.3** ✅ Red-team / security — turns the governance stack into an **adversarial probe gate** across four classes (prompt injection / PII egress / ACL bypass / tenant escape) under a `redteam` marker + a first-class **`redteam-gate`** CI job (`task redteam`). Closes the verified injection gap: new **`rag-injection`** package — a pluggable `InjectionDetector` (dependency-free `HeuristicInjectionDetector`, regexes anchored on attack *grammar* so benign prose isn't flagged) + `PromptInjectionGuard.inspect` that drops hijack chunks **before the LLM**, paired with `INJECTION_RESISTANT_SYSTEM_PROMPT` + `build_user_message` so untrusted context is **fenced data in the user turn, never a system-trust position** (fixes the OpenAI-chat surface that put context in a `system` message); wired on `/v1/query` + `/v1/chat/completions` + MCP; off by default (`cfg.injection`); PII-free `injection.blocked` event. Deterministic gate: a ≥ 500 known + ≥ 500 generated corpus (`eval/redteam_v0/`) hits **96.6 % block** (≥ 95 % bar) at **0 false positives**, plus the no-system-position invariant end-to-end — building the corpus *hardened the detector* (~20 missed phrasings). New PII-egress probe over `PiiPolicyEngine` (zero leakage, second-detector verified); `pip-audit` is the CVE gate; the external pentest is a documented process item. Injection types stay internal (no attacker signal, `dist/schemas`/`openapi` untouched). [#166](https://github.com/officialCodeWork/AgentContextOS/pull/166) - **7.2** ✅ Chaos engineering — a deterministic in-process **kill-matrix gate** (`eval/gateway_chaos_v0/kill_matrix.py` + `tests/perf/test_chaos_kill_matrix.py`) that extends 7.1 from the three retrieval backends to the **full hot-path set** (vector/keyword/graph/embedder/retrieval_cache/reranker/llm): kill each backend in turn (100% unavailable) behind the real breakers + fallback, drive `/v1/query`, and assert **no single failure 5xx-es the gateway** (no 5xx, on-path retrieval breaker opens, expected degraded shape; a seeded keyword corpus + real `hydrate` make rerank/generate actually run). **Chaos fixed what it found** — the matrix exposed that a down **retrieval cache** or **reranker** 5xx-ed, so the gateway gained two minimal **degrade-open** guards (`gateway.cache.degraded` → miss on `/v1/query` + `/v1/retrieve`; `gateway.rerank.degraded` → retrieval-only, honouring `RerankPipeline`'s "caller decides" contract); LLM + embedder already degraded. **LitmusChaos cluster manifests** (`infra/chaos/`: gateway pod-delete + backend `pod-network-loss`/`latency` with httpProbe acceptance) as the cluster runbook + `task chaos-kill`. No `dist/`/SPI/config change. [#165](https://github.com/officialCodeWork/AgentContextOS/pull/165) @@ -806,7 +807,7 @@ New ground — the only prior crypto was HMAC signing. The V1 plan calls for en | 7.2 | Chaos engineering | ✅ | [#165](https://github.com/officialCodeWork/AgentContextOS/pull/165) — in-process **kill-matrix** gate (kill each hot-path backend → no 5xx, breaker opens, expected degraded shape); two new gateway **degrade-open** guards (retrieval cache + reranker); LitmusChaos cluster manifests (`infra/chaos/`) + `task chaos-kill` | | 7.3 | Red-team | ✅ | [#166](https://github.com/officialCodeWork/AgentContextOS/pull/166) — adversarial probe gate (injection / PII / ACL / tenant-escape) under a `redteam` marker + `redteam-gate` CI job; new **`rag-injection`** guard (≥ 95 % block, no untrusted chunk in a system-trust position) wired on every answer surface; PII-egress probe; `task redteam` | | 7.4 | Design partner onboarding | ✅ | [#174](https://github.com/officialCodeWork/AgentContextOS/pull/174) — repo-backed pilot program: operator runbook + 5 templates (`docs/pilots/`) + ADR-0046; **customer-support/KB kit** (corpus w/ PII + planted injection probe; domain criteria); **`ragctl pilot`** `onboard` + `report` (weekly KPIs + PASS/FAIL verdict from feedback/drift/cost signal components, KPIs pulled not self-reported); worked case study (real `pilot report` → PASS). Signing partners = external GTM item | -| 7.5 | Documentation site | ⏳ | Docusaurus / MkDocs site; API reference generated from OpenAPI; quickstart guides | +| 7.5 | Documentation site | ✅ | [#178](https://github.com/officialCodeWork/AgentContextOS/pull/178) — Docusaurus app (`website/`) serving `docs/` in place; **generated + drift-gated REST API reference** from `dist/openapi.json`; `tests/docs/` quickstart-honesty suite + `lychee`/`codespell`; `docs` CI workflow; `task docs:api`/`docs:build` | | 7.6 | Marketplace listings | ⏳ | AWS / Azure / GCP Marketplace AMI / Helm listings | | 7.7 | Packaging & distribution | ⏳ | PyPI publish (`rag-platform`), npm publish (`@ragplatform/sdk`), Docker Hub images | | 7.8 | Support & SLA | ⏳ | Support tiers defined; SLA dashboards; PagerDuty integration; runbooks | @@ -849,6 +850,14 @@ New ground — the only prior crypto was HMAC signing. The V1 plan calls for en - **7.4d — worked case study.** [docs/pilots/customer-support/case-study.md](docs/pilots/customer-support/case-study.md) runs the framework end-to-end on the kit with **real `ragctl pilot report` output** (satisfaction **+0.733**, **0/5** monitors drifting, cost **ok** → **PASS**) + the PII-redaction + injection-blocked security demonstration — framed honestly as a representative pilot (a real partner substitutes their corpus + users). - **Scope:** the framework + the first vertical kit + the `ragctl pilot` reader + the worked case study. **No new package / core or wire type / governed SPI call / `dist` change** — `pilot` is a pure `ragctl` reader over existing signal components, so the policy-coverage + schema/openapi-drift gates need no new entry. **Deferred / external:** sourcing + signing the real referenceable partners; the legal + internal-engineering kits; per-pilot golden-set *generation* tooling; a pilot KPI card in the admin console. All gates green (ruff, mypy --strict, RAG001, link-check). [ADR-0046](docs/adr/ADR-0046-design-partner-pilots.md), [guides/design-partner-pilots.md](docs/guides/design-partner-pilots.md), [reference/pilot.md](docs/reference/pilot.md). +### 7.5 — Documentation site ✅ [#178](https://github.com/officialCodeWork/AgentContextOS/pull/178) + +- A public docs site that **can't drift from the repo**: a **Docusaurus 3** app under `website/` whose docs plugin points at `../docs` (`routeBasePath: '/'`), so the PR-reviewed `docs/` tree *is* the site — no copy step. `markdown.format: 'detect'` renders hand-written `.md` as CommonMark (no MDX escaping of `<` / `{`). +- **The REST API reference is a generated, drift-gated artifact.** `scripts/gen_api_reference.py` (pure stdlib) renders the 43-path `dist/openapi.json` → `docs/reference/rest-api.md`; the `docs` CI workflow regenerates + diffs it, exactly like `dist/schemas` / the proto stubs — so the published API docs always match the gateway contract. `task docs:api` regenerates. +- **Doc honesty is tested, not asserted.** `tests/docs/` extracts every `ragctl ` and `/v1/...` path from the quickstart guides and fails if any isn't a registered CLI command / a real OpenAPI path — the lightest deterministic form of "every quickstart is exercised", no services needed (building it caught + fixed a newline-spanning regex false-positive). +- **New `docs` CI workflow** (`.github/workflows/docs.yml`): API-reference drift · quickstart doc-tests · `codespell` · `lychee --offline` link-check · full `npm run build`. `task docs:dev` / `docs:build` run/build locally. +- **Scope:** the site scaffold + generated reference + doc-tests + CI + `task` targets. **No code / `dist` change** beyond the new generated reference file. **Deferred:** Algolia DocSearch, versioned docs aligned to releases, the deploy workflow + custom domain, gRPC/MCP reference auto-generation, executable (vs existence-checked) snippets. Verified locally (the npm site build is a CI step; the Python generator + the 3 doc-tests run green). [ADR-0047](docs/adr/ADR-0047-documentation-site.md), [guides/documentation-site.md](docs/guides/documentation-site.md). + --- ## PR & Branch History @@ -997,6 +1006,10 @@ Complete log of every PR. Routine Dependabot bumps are grouped; everything else | [#165](https://github.com/officialCodeWork/AgentContextOS/pull/165) | 2026-06-08 | test(perf): chaos kill-matrix gate + cache/rerank degrade-open + LitmusChaos (Step 7.2) | | [#166](https://github.com/officialCodeWork/AgentContextOS/pull/166) | 2026-06-08 | feat(security): prompt-injection guard (rag-injection) + red-team probe gate (Step 7.3) | | [#174](https://github.com/officialCodeWork/AgentContextOS/pull/174) | 2026-06-10 | feat(pilot): design-partner pilot program — framework + customer-support kit + ragctl pilot + case study (Step 7.4) | +| [#175](https://github.com/officialCodeWork/AgentContextOS/pull/175) | 2026-06-10 | fix(core): carry corpus_id on Embedding for vector corpus scoping (ADR-0004 §3) | +| [#176](https://github.com/officialCodeWork/AgentContextOS/pull/176) | 2026-06-10 | fix(gateway): scope answers to the question's subject on both answer prompts | +| [#177](https://github.com/officialCodeWork/AgentContextOS/pull/177) | 2026-06-10 | feat(admin-ui): wire dashboard + audit page to live data; clearer trace help | +| [#178](https://github.com/officialCodeWork/AgentContextOS/pull/178) | 2026-06-10 | feat(docs): Docusaurus documentation site + generated API reference + doc-tests (Step 7.5) | | #78–#80, #116–#118 | Open | Dependabot bumps — awaiting merge | | #81 | Closed | Dependabot bump — superseded | diff --git a/Taskfile.yml b/Taskfile.yml index 354a1b6..bc9f8bd 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -184,6 +184,28 @@ tasks: cmds: - "{{.PYTHON}} scripts/check_openapi_drift.py" + # --------------------------------------------------------------------------- + # Documentation site (Step 7.5) — Docusaurus under website/, sourcing docs/ + # --------------------------------------------------------------------------- + docs:api: + desc: "Regenerate docs/reference/rest-api.md from dist/openapi.json" + cmds: + - "{{.PYTHON}} scripts/gen_api_reference.py" + + docs:dev: + desc: "Run the documentation site locally (http://localhost:3000)" + dir: website + cmds: + - npm install + - npm run dev + + docs:build: + desc: "Build the static documentation site → website/build/" + dir: website + cmds: + - npm install + - npm run build + sdk:gen: desc: "Generate all SDK artifacts (proto stubs + OpenAPI clients) into sdks/*/" cmds: diff --git a/docs/README.md b/docs/README.md index b7b4f1d..b9e46a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -103,6 +103,7 @@ | [router.md](reference/router.md) | `rag-retrieval` router (Step 2.10) — `RetrievalRouter`, `RouterConfig`, `BackendHealthTracker`, `HealthConfig`, `classify_shape`, `RoutingDecision`, `QueryShape`, `ragctl route` | | [agent-loop.md](reference/agent-loop.md) | `rag-retrieval` agent loop (Step 2.11 spike) — `AgentLoopV0`, `AgentLoopConfig`, `AgentLoopResult`, `AgentLoopIterResult`, `QueryUnderstander` / `UnderstoodQueryLike` Protocols, `ragctl agent-loop`, OTel + log schema | | [gateway.md](reference/gateway.md) | `rag-gateway` (Step 3.1, 🥈 milestone) — route catalogue (`/v1/query`, `/v1/retrieve`, `/v1/corpora`), request/response shapes, error envelope, OpenAPI 3.1, OTel + structured-log schema, production wiring | +| [rest-api.md](reference/rest-api.md) | **Generated** REST API reference (Step 7.5) — the full `/v1/*` endpoint catalogue (params · request/response schemas · status codes) rendered from the drift-gated `dist/openapi.json` by `scripts/gen_api_reference.py` (`task docs:api`) | | [grpc.md](reference/grpc.md) | `rag-gateway` gRPC contract (Step 3.2) — `rag.gateway.v1.RagService` RPC catalogue, streaming semantics, metadata contract, error envelope, status-code mapping, codegen + extension points | | [mcp.md](reference/mcp.md) | `rag-gateway` MCP contract (Step 3.3) — `query` / `retrieve` / `ingest` tool catalogue, argument tables, output models, tool↔request-model parity, `ToolError` envelope, `ragctl mcp-query` + `npx @ragplatform/mcp`, extension points | | [openai-compat.md](reference/openai-compat.md) | `rag-gateway` OpenAI-compatible API (Step 3.4) — `/v1/embeddings`, `/v1/chat/completions` (retrieval pre-fetch + streaming), `/v1/models`, the `rag` request/response extension, OpenAI error envelope, `ragctl chat` / `embeddings`, extension points | @@ -131,6 +132,7 @@ | [chaos-engineering.md](guides/chaos-engineering.md) | Chaos engineering runbook (Step 7.2): the two layers — the in-process **kill-matrix gate** (`task chaos-kill`: kill *each* hot-path backend in turn → no 5xx, breaker opens, expected degraded shape) vs **LitmusChaos** on a cluster (`infra/chaos/`: gateway pod-delete + backend network-loss/latency with httpProbe acceptance); the per-backend survival table; the two degrade-open gaps 7.2 hardened (retrieval cache + reranker); why graph is off the default read path; acceptance targets | | [red-team.md](guides/red-team.md) | Red-team / security runbook (Step 7.3): the four probe classes (prompt injection / PII egress / ACL bypass / tenant escape) + suites; running the gate (`task redteam`, the `redteam-gate` CI job, `pip-audit` for CVEs); the prompt-injection defense (filtering via `PromptInjectionGuard` + structural trust isolation via the hardened prompt); the corpus + acceptance (≥ 95 % block, ≤ 5 % false positives, no untrusted chunk in a system-trust position); enabling `cfg.injection`; the external-pentest process item | | [design-partner-pilots.md](guides/design-partner-pilots.md) | Design-partner pilot runbook (Step 7.4): the pilot lifecycle (qualify → onboard → run → review → graduate); roles; onboarding as a configured deployment; **signed success criteria** (quality/latency/integration/security) each tied to a platform signal; the **weekly KPI** dashboard pulled from `GET /v1/status/{metrics,feedback,drift,cost,health}` + the eval harness (assembled by `ragctl pilot report`); the intake → triage → incorporate → close feedback loop; acceptance (≥ 3 consecutive green weeks · 3 referenceable tenants) + the case study; the in-repo-machine vs external-GTM split | +| [documentation-site.md](guides/documentation-site.md) | The documentation site (Step 7.5): the Docusaurus app under `website/` that serves `docs/` in place; running/building it; the generated REST API reference; the CI gates (API-ref drift · quickstart doc-tests · spell-check · `lychee` link-check · site build); adding docs | | [curl-quickstart.md](guides/curl-quickstart.md) | 🥈 Curl-able RAG (Step 3.1): 5-minute walkthrough from `curl` to gateway response, including ingest, query, generate, OpenAPI | | [grpcurl-quickstart.md](guides/grpcurl-quickstart.md) | gRPC quickstart (Step 3.2): 5-minute walkthrough using `grpcurl` against the in-process server — health check, list corpora, server-streaming query, structured errors | | [mcp-quickstart.md](guides/mcp-quickstart.md) | MCP quickstart (Step 3.3): 5-minute walkthrough — `ragctl mcp-query`, running the stdio server, mounting `@ragplatform/mcp` in Claude Desktop, the three tools, error shape | @@ -196,6 +198,7 @@ broken, and what to fix before committing to the next phase. | [ADR-0044-chaos-engineering.md](adr/ADR-0044-chaos-engineering.md) | Decision (Step 7.2, Phase-7 hardening): **kill each backend, verify the fallback chain holds** — a deterministic in-process **kill-matrix CI gate** extending 7.1 to the full hot-path set (vector/keyword/graph/embedder/retrieval_cache/reranker/llm); kill one at a time → no 5xx, on-path retrieval breaker opens, expected degraded shape; seeded keyword corpus + real `hydrate` so rerank/generate actually run. **Chaos *fixes* what it finds** — the matrix exposed that a down retrieval-cache or reranker 5xx-ed, so the gateway gained two **degrade-open** guards (`gateway.cache.degraded` → miss; `gateway.rerank.degraded` → retrieval-only, honouring `RerankPipeline`'s "caller decides" contract); LLM + embedder already degraded. **Graph is off the seed-less read path** → its kill is survivable by construction (no breaker-open required). **Cluster chaos is a runbook** — LitmusChaos `infra/chaos/` (gateway pod-delete + backend `pod-network-loss`/`latency` with httpProbe acceptance). No `dist/`/SPI/config change (degrade kinds aren't registered events; kill wrappers are pure-raise). Deferred: latency-based breaker tripping, multi-kill-as-gate, Litmus-in-CI, soak; rejected leaving the holes documented-only, reranker fallback inside the pipeline, a synthetic graph query, pod-kill in CI | | [ADR-0045-red-team-security.md](adr/ADR-0045-red-team-security.md) | Decision (Step 7.3, Phase-7 hardening): turn the governance stack into an **adversarial probe gate** (injection / PII / ACL / tenant-escape) + close the injection gap. New **`rag-injection`** package: a pluggable `InjectionDetector` (dependency-free `HeuristicInjectionDetector`, attack-grammar regexes) + `PromptInjectionGuard.inspect` that drops hijack chunks, paired with the `INJECTION_RESISTANT_SYSTEM_PROMPT` so untrusted context is **fenced data in the user turn, never a system-trust position** (fixes the OpenAI-chat surface that injected context as a `system` turn); wired on `/v1/query` + `/v1/chat/completions` + MCP; off by default (`cfg.injection`). Deterministic CI gate: a ≥ 500 known + ≥ 500 generated corpus (`eval/redteam_v0/`) at **≥ 95 % block** + a **false-positive bound** + the no-system-position invariant end-to-end; building it *hardened the detector*. PII-egress probe over `PiiPolicyEngine` (zero leakage, second-detector verified); `redteam` marker + `redteam-gate` CI job; `pip-audit` is the CVE gate. Injection types are internal (no attacker signal, `dist` untouched). Deferred/process: the external pentest, a real ML classifier behind the seam; rejected a core SPI, surfacing the verdict, tuning to a circular 100 % | | [ADR-0046-design-partner-pilots.md](adr/ADR-0046-design-partner-pilots.md) | Decision (Step 7.4): ship the pilot **machine** in-repo (runbook + templates + per-vertical kits + a `ragctl pilot` KPI reader) while the partner **relationships** stay an external GTM deliverable (like the 7.3 pentest); each success criterion (quality / latency / integration / security) maps to a platform signal (eval / feedback / drift / cost / metrics / compliance) so KPIs are **pulled, not self-reported**; sliced 7.4a–d, leading with customer-support/KB, the framework vertical-extensible; no new package / core type / governed SPI call / `dist` change — the report reads existing status endpoints; rejected a pure-external motion, self-reported KPIs, a `rag-pilot` package, one generic kit | +| [ADR-0047-documentation-site.md](adr/ADR-0047-documentation-site.md) | Decision (Step 7.5): publish docs via **Docusaurus** sourcing the repo `docs/` tree **in place** (single source of truth, no copy); `markdown.format: 'detect'` so hand-written `.md` renders as CommonMark; the **REST API reference is generated + drift-gated** from `dist/openapi.json` (a committed file, not a build-time plugin); doc honesty is a `tests/docs/` suite (every `ragctl` / `/v1/` reference is real) + `lychee` + `codespell`; rejected MkDocs, copying `docs/`, an OpenAPI plugin | | [ADR-0042-compliance-posture.md](adr/ADR-0042-compliance-posture.md) | Decision (Step 6.10, Phase-6 capstone): add the three compliance pieces on top of the controls the platform already ships (audit/ACL/PII/BYOK/SSO/quotas). New `rag-compliance` package (config-free, like rag-feedback/rag-drift): `RetentionEnforcer` drives tenant-scoped `purge_*`; `compliance_posture`/`residency_ok` are pure. **Retention is a capability on the existing stores, not a new SPI** — non-abstract `purge_before`/`purge_tenant` (default no-op) on Feedback/Provenance, with `dry_run` in the SPI so a preview counts-without-deleting uniformly (ProvenanceStore has no `list`). **Audit is never purged in place** (the hash chain would break) — audit retention is the 6.6b WORM export; `audit_days` is advisory. **Right-to-erasure is always-on, tenant-self-service, two-flag** — `POST /v1/compliance/erase` erases the *calling* tenant's data (scope from the principal, never the body), dry-run by default, delete needs `dry_run=false` AND `confirm=true`. **Residency = declared per tenant + enforced at ingest** (`tenants[].data_region` vs `cfg.compliance.region` → 403), opt-in, a single-deployment assertion not multi-region routing. **The SOC 2 / GDPR mapping is a doc backed by a live posture** (`GET /v1/status/compliance` reports which controls are on, so the mapping is checkable). Deferred: subject-level (vs tenant-level) erasure, an admin retention-sweep endpoint, multi-region routing, automated audit-evidence bundles; rejected purging the audit chain, a `Purgeable` SPI mixin, a static doc with no live backing | | [ADR-0041-airgap-bundle.md](adr/ADR-0041-airgap-bundle.md) | Decision (Step 6.9): ship the platform as one signed, self-contained offline bundle (all runtime images + Helm chart + config + installer). Integrity reuses the WORM-export pattern (6.6b): a standard `SHA256SUMS` whose hash is pinned as `manifest.content_hash` is the **hard gate** — verifiable with nothing but `sha256sum`, no network/cosign — and a cosign signature **over `SHA256SUMS`** adds authenticity; the *same* `SHA256SUMS` drives the Python verifier and the standalone shell installer so they can't diverge. The shell/pwsh `install.{sh,ps1}` (shipped inside the bundle) need only docker+helm (air-gap hosts lack uv/the workspace); `ragctl airgap` holds the typed/tested build+verify logic (pure core separated from a stubbable docker/helm/cosign subprocess seam; `--dry-run` = a verifiable bundle minus image blobs, so the path is testable with no Docker). Digest-pinned manifest-driven image set (`infra/airgap/images.txt` + the chart-derived gateway image); key-based cosign is the air-gap recommendation (keyless needs Rekor + an identity policy), keyless is the connected-release path (`release-airgap.yml` on tags). Deferred: ctr/podman load, registry re-tag/push, multi-arch selection, bundling backend charts, TUF-rooted offline keyless verify; rejected `oras`/OCI (no registry to pull from in an air-gap), a second HMAC scheme (cosign already the signer), a pure-shell build (would escape mypy/tests) | | [ADR-0040-sso-scim.md](adr/ADR-0040-sso-scim.md) | Decision (Step 6.8): enterprise identity in two surfaces. **Federation** — `FederatedAuth` *is* an `Auth` SPI backend (the `authenticate(token, tenant_id) → Principal` seam already runs at the boundary, so wiring it is the whole integration — no middleware change); group claims → `acl_labels` so Step 6.3 push-down + 6.5 PII egress govern federated users unchanged (`authorize` stays a coarse allow — federation establishes *who*, the PDP decides *what*). Dependency-free defaults (stdlib HS256 JWT with full `exp`/`nbf`/`iss`/`aud` + constant-time compare; `defusedxml` SAML validating Issuer/Conditions/Audience) with asymmetric OIDC (PyJWT, `[oidc]`) + SAML XML-DSig (signxml, `[saml]`, injected verifier → fail-closed) behind extras; **algorithm-allowlist** designs out `alg:none`/RS↔HS confusion. Per-tenant IdP on `tenants[].sso` (reuses Step 6.1 config; no provider → bearer rejected, header-identity still works). **Provisioning** — SCIM 2.0 is a separate surface with its own per-tenant bearer token (`cfg.scim.tokens`, not a user JWT), a tenant-scoped `ScimStore` SPI (`NoopScimStore`) + `ScimService`, SCIM-shaped errors, disabled→404; no new governed SPI call (linter passes). PII-free `sso.*`/`scim.*` events (hashed subject, never email/userName). Deferred: JWKS rotation, SP-initiated SAML + metadata, SCIM bulk/`/Me`/ETag, directory-backed deprovisioning, admin-console card; rejected Authlib/python3-saml (heavy lxml/xmlsec on the default install), a dedicated SSO middleware, SCIM token on `TenantConfig` | diff --git a/docs/adr/ADR-0047-documentation-site.md b/docs/adr/ADR-0047-documentation-site.md new file mode 100644 index 0000000..1f74ea6 --- /dev/null +++ b/docs/adr/ADR-0047-documentation-site.md @@ -0,0 +1,67 @@ +# ADR-0047 — Documentation site: Docusaurus over the in-place `docs/` tree, generated API reference + +**Status:** Accepted +**Date:** 2026-06-10 +**Step:** 7.5 — Documentation site (Phase 7 — Pilot, Harden, GA) +**Related:** [guides/documentation-site.md](../guides/documentation-site.md), [3.7 SDK generation / OpenAPI contract](ADR-0016-sdk-generation.md), [schema-drift-gate](../guides/schema-drift-gate.md), [planning/phases/phase-7-pilot-ga.md](../../planning/phases/phase-7-pilot-ga.md) + +## Context + +Step 7.5 calls for a public documentation site (`docs.agentcontextos.io`): +quickstarts, concepts, how-to, reference (OpenAPI / gRPC / MCP / `rag.yaml` / SDKs), +ops, migration guides, with link-check + spell-check in CI and "every documented +quickstart exercised by a test." + +The repo already has a large, well-structured `docs/` tree (architecture / +reference / guides / adr / pilots / compliance / research) written at PR time per +the standing documentation rule, plus a **drift-gated** `dist/openapi.json` +contract. The decision is how to publish it without creating a second source of +truth that rots. + +## Decision + +**1. Docusaurus 3, sourcing `docs/` in place.** `website/` is a Docusaurus app +whose docs plugin points at `../docs` (`routeBasePath: '/'`) — the repo `docs/` +folder *is* the site content. No copy/sync step, so the site can never drift from +the docs reviewed in each PR. + +**2. `markdown.format: 'detect'`.** `.md` files render as CommonMark, so the +existing hand-written docs (full of `<` and `{` in prose and code) render without +MDX-escaping churn; `.mdx` is reserved for pages that want React components. + +**3. The REST API reference is generated from the OpenAPI contract, as a +drift-gated artifact.** `scripts/gen_api_reference.py` (pure stdlib) renders +`dist/openapi.json` → `docs/reference/rest-api.md`; CI regenerates and diffs it +(like `dist/schemas/` and the proto stubs). The published reference therefore +always matches the gateway. Rejected a Docusaurus OpenAPI *plugin* — it adds an +npm dependency and runs only at build time, whereas a committed generated file is +reviewable in the PR and gated the same way as every other generated artifact. + +**4. Doc honesty is a test, not a vibe.** `tests/docs/` asserts every +`ragctl ` and every `/v1/...` path shown in a quickstart actually +exists (in the Typer app / the OpenAPI spec) — the lightest, deterministic form of +"every quickstart exercised by a test," with no running services. Link integrity +is `lychee --offline`; spelling is `codespell`; the full site `npm run build` is +its own CI job. + +## Consequences + +- New `website/` Docusaurus scaffold, `scripts/gen_api_reference.py` + + `docs/reference/rest-api.md`, `tests/docs/`, `.github/workflows/docs.yml`, and + `task docs:api` / `docs:dev` / `docs:build`. +- The site is one `npm run build` away from deploy; hosting/deploy is a CI/infra + action (intentionally out of this PR, consistent with "ignore CI for now"). +- **Deferred:** Algolia DocSearch, versioned docs aligned to releases, the deploy + workflow + custom domain, gRPC/MCP reference auto-generation (today those live as + hand-written reference pages), executable (vs existence-checked) quickstart + snippets. + +## Alternatives considered + +- **MkDocs (Material).** Rejected — the plan specifies Docusaurus, and the + React/MDX ecosystem fits the SDK/interactive-reference roadmap better. +- **Copy `docs/` into `website/docs/`.** Rejected — a second copy drifts from the + PR-reviewed source; sourcing `../docs` in place keeps one source of truth. +- **A Docusaurus OpenAPI plugin for the API reference.** Rejected — a committed, + generated, drift-gated Markdown file is reviewable and gated like the other + `dist/` artifacts, with no extra build-time npm dependency. diff --git a/docs/guides/documentation-site.md b/docs/guides/documentation-site.md new file mode 100644 index 0000000..8aec82c --- /dev/null +++ b/docs/guides/documentation-site.md @@ -0,0 +1,67 @@ +# Guide: the documentation site (Step 7.5) + +How the public documentation site is built, run, and kept honest. + +## Overview + +The site is a [Docusaurus 3](https://docusaurus.io) app under +[`website/`](https://github.com/officialCodeWork/AgentContextOS/tree/main/website) +that serves the repository [`docs/`](../README.md) tree **in place** — `docs/` +stays the single source of truth, the site renders it. Published to +`docs.agentcontextos.io`. + +## Run it + +```bash +cd website +npm install +npm run dev # http://localhost:3000, live reload +npm run build # static site → website/build/ (what CI builds) +``` + +## What's generated vs hand-written + +| Content | Source | +|---------|--------| +| Everything under `docs/` | Hand-written Markdown (rendered as-is) | +| [REST API reference](../reference/rest-api.md) | **Generated** from `dist/openapi.json` by `scripts/gen_api_reference.py` | + +Regenerate the API reference (and fail CI on drift): + +```bash +task docs:api # python scripts/gen_api_reference.py +``` + +Because the reference is generated from the **drift-gated** OpenAPI contract, the +published API docs can never silently diverge from the gateway. + +## CI gates (`.github/workflows/docs.yml`) + +| Job | Checks | +|-----|--------| +| **API reference drift** | `rest-api.md` is in sync with `dist/openapi.json` | +| **Quickstart doc tests** | `tests/docs/` — every `ragctl ` and `/v1/...` path in a quickstart is real | +| **Spell check** | `codespell` over `docs/` | +| **Link check** | `lychee --offline` over `docs/` + READMEs | +| **Build** | `npm run build` compiles the whole site | + +## Adding documentation + +- Drop a `.md` file under the right `docs/
/` folder; it appears in the + autogenerated sidebar and on the site automatically. +- Add a row to [`docs/README.md`](../README.md) (the repo doc index) — required by + the per-PR documentation rule. +- `.md` renders as CommonMark (`markdown.format: 'detect'`), so `<` / `{` in prose + don't need MDX escaping. Use `.mdx` only when you want React components. + +## Conventions + +- Never hand-edit `docs/reference/rest-api.md` — it is overwritten by `task docs:api`. +- Keep quickstart command + path examples real; the `tests/docs/` suite fails the + build if a quickstart references a `ragctl` subcommand or REST path that doesn't + exist. + +## See also + +- [ADR-0047](../adr/ADR-0047-documentation-site.md) — the documentation-site decision +- [reference/rest-api.md](../reference/rest-api.md) — the generated REST reference diff --git a/docs/reference/rest-api.md b/docs/reference/rest-api.md new file mode 100644 index 0000000..ff39455 --- /dev/null +++ b/docs/reference/rest-api.md @@ -0,0 +1,917 @@ +--- +title: REST API reference +description: Generated from the committed OpenAPI 3.1 contract. +--- + +# REST API reference + +> **Generated** from `dist/openapi.json` by `scripts/gen_api_reference.py` — do not edit by hand. Regenerate with `task docs:api` (or `python scripts/gen_api_reference.py`). The spec itself is drift-gated in CI. + +**AgentContextOS Gateway** · version `0.10.0` · OpenAPI 3.1 + +## Endpoints + +### agent + +#### `POST /v1/agent` + +Run an agent loop over a goal, streaming events as SSE + +Drive the agent loop for ``body.goal`` and stream its events. + +See ``docs/reference/agent.md`` (``#post-v1agent``). + +**Request body** (`application/json`): `AgentRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `object` | SSE stream of AgentEvent frames (text/event-stream). | +| 400 | `—` | Invalid request body | +| 401 | `—` | Missing or invalid auth | +| 422 | `HTTPValidationError` | Validation Error | + +### audit + +#### `GET /v1/audit` + +The tenant's own audit events, newest-first (Step 6.6) + +Return the calling tenant's audit events (tamper-evident hash chain). + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `limit` | query | no | integer | | +| `action` | query | no | — | | +| `outcome` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `AuditListResponse` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | Audit read API disabled | +| 422 | `HTTPValidationError` | Validation Error | + +#### `POST /v1/audit/export` + +Signed WORM export of the tenant's audit log (Step 6.6b) + +Return a self-verifying export bundle of the calling tenant's events. + +The bundle is content-hashed and (when ``cfg.audit.export_secret`` is set) +HMAC-signed, so a consumer verifies it offline; ``chain_verified`` attests +the source log's whole-log hash chain was intact at export time. It is the +artifact an operator archives to immutable (WORM) storage. + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `AuditExport` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | Audit read API disabled | + +#### `GET /v1/audit/verify` + +Whole-log hash-chain integrity check (Step 6.6) + +Verify the whole-log SHA-256 hash chain — True iff no link was altered. + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `AuditVerifyResponse` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | Audit read API disabled | + +### compliance + +#### `POST /v1/compliance/erase` + +Erase the calling tenant's data — GDPR right-to-erasure (Step 6.10) + +Erase (or, by default, preview) the calling tenant's data across all stores. + +Returns an :class:`ErasureResult` with per-data-class counts. Deletes only +when ``dry_run=false`` **and** ``confirm=true``; otherwise it is a counting +preview (``dry_run=true`` in the result). + +**Request body** (`application/json`): `ComplianceEraseRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ErasureResult` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 422 | `HTTPValidationError` | Validation Error | + +### corpora + +#### `GET /v1/corpora` + +List corpora visible to the calling tenant + +Return every corpus the calling principal can read. + +See docs/reference/gateway.md +for the response shape + pagination semantics (paging is a +Phase 3.x add — v0 returns everything visible). + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `CorpusList` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | + +#### `GET /v1/corpora/{corpus_id}` + +Describe a single corpus + +Return metadata for one corpus, or 404 if not visible. + +Cross-tenant lookups (the corpus exists in a different tenant) +return 404 deliberately — the response shape doesn't +distinguish "not found" from "not visible" so attackers +can't probe for other tenants' corpora. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `corpus_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `Corpus` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | Corpus not found / not visible | +| 422 | `HTTPValidationError` | Validation Error | + +### feedback + +#### `POST /v1/feedback` + +Submit explicit feedback or an implicit signal about a query + +**Request body** (`application/json`): `FeedbackRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `FeedbackAck` | Successful Response | +| 422 | `GatewayError` | Invalid signal / rating | + +### health + +#### `GET /healthz` + +Healthz + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `object` | Successful Response | + +#### `GET /v1/info` + +Info + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `object` | Successful Response | + +### ingest + +#### `POST /v1/ingest/document` + +Ingest Document + +Single-document ingest — multipart upload (Step 1.10). + +**Request body** (`multipart/form-data`): `Body_ingest_document_v1_ingest_document_post` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `IngestResult` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +### openai + +#### `POST /v1/chat/completions` + +Create a chat completion with retrieval pre-fetch (OpenAI-compatible) + +RAG chat completion: retrieve → inject context → generate. + +See ``docs/reference/openai-compat.md`` (``#post-v1chatcompletions``). + +**Request body** (`application/json`): `ChatCompletionRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ChatCompletionResponse` | Successful Response | +| 400 | `OpenAIError` | Invalid request | +| 401 | `OpenAIError` | Missing or invalid auth | +| 403 | `OpenAIError` | ACL denied | +| 422 | `HTTPValidationError` | Validation Error | +| 429 | `OpenAIError` | Rate limit exceeded | +| 502 | `OpenAIError` | Downstream retrieval / LLM failure | + +#### `POST /v1/embeddings` + +Create embeddings (OpenAI-compatible) + +Embed text via the wired ``Embedder`` SPI. + +See ``docs/reference/openai-compat.md`` (``#post-v1embeddings``). + +**Request body** (`application/json`): `EmbeddingsRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `EmbeddingsResponse` | Successful Response | +| 400 | `OpenAIError` | Invalid request | +| 401 | `OpenAIError` | Missing or invalid auth | +| 422 | `HTTPValidationError` | Validation Error | +| 429 | `OpenAIError` | Quota / rate limit exceeded | + +#### `GET /v1/models` + +List models (OpenAI-compatible) — reports the wired LLM + embedder + +Advertise the wired LLM + embedder under OpenAI's model-list shape. + +Many OpenAI clients call this on startup to validate the endpoint. +The ids are the wired backends' canonical model names plus a +``rag-gateway`` alias clients can target generically. + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ModelList` | Successful Response | + +### queries + +#### `POST /v1/query` + +Full RAG query: retrieve → rerank → pack → optional LLM answer + +End-to-end RAG query. + +Runs the Phase 2 pipeline in sequence — understanding, +router, optional rerank, optional pack, optional LLM — and +returns the combined evidence envelope. See +docs/reference/gateway.md. + +**Request body** (`application/json`): `QueryRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `QueryResponse` | Successful Response | +| 400 | `GatewayError` | Invalid request body | +| 401 | `GatewayError` | Missing or invalid auth | +| 403 | `GatewayError` | ACL denied | +| 422 | `HTTPValidationError` | Validation Error | +| 429 | `GatewayError` | Tenant rate limit exceeded | +| 502 | `GatewayError` | Downstream retrieval failure | + +#### `GET /v1/query/{request_id}/trace` + +Per-query provenance + captured span trace (Step 5.1) + +Return the signed provenance record + captured spans for a query. + +Tenant identity comes from request headers (``Authorization`` / +``X-Tenant-Id``); a record is returned only to the tenant that produced +it. 404 when the id is unknown to the tenant or provenance is disabled. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `request_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `QueryTraceResponse` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | No provenance for this query id | +| 422 | `HTTPValidationError` | Validation Error | + +#### `POST /v1/retrieve` + +Retrieval-only: understanding + router; no rerank / pack / LLM + +Retrieval-only subset of ``/v1/query``. + +Useful when the caller brings their own reranker / packer / +LLM and just wants the policy-checked, fused chunk-ref list. + +**Request body** (`application/json`): `RetrieveRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `RetrieveResponse` | Successful Response | +| 400 | `GatewayError` | Invalid request body | +| 401 | `GatewayError` | Missing or invalid auth | +| 403 | `GatewayError` | ACL denied | +| 422 | `HTTPValidationError` | Validation Error | +| 502 | `GatewayError` | Downstream retrieval failure | + +### scim + +#### `GET /scim/v2/Groups` + +List Groups + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `startIndex` | query | no | integer | | +| `count` | query | no | integer | | +| `filter` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ScimListResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `POST /scim/v2/Groups` + +Create Group + +**Request body** (`application/json`): `ScimGroup` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 201 | `ScimGroup` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /scim/v2/Groups/{group_id}` + +Get Group + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `group_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ScimGroup` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `PUT /scim/v2/Groups/{group_id}` + +Replace Group + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `group_id` | path | yes | string | | + +**Request body** (`application/json`): `ScimGroup` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ScimGroup` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `DELETE /scim/v2/Groups/{group_id}` + +Delete Group + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `group_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 204 | `—` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /scim/v2/ResourceTypes` + +Resource Types + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `object` | Successful Response | + +#### `GET /scim/v2/ServiceProviderConfig` + +Service Provider Config + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `object` | Successful Response | + +#### `GET /scim/v2/Users` + +List Users + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `startIndex` | query | no | integer | | +| `count` | query | no | integer | | +| `filter` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ScimListResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `POST /scim/v2/Users` + +Create User + +**Request body** (`application/json`): `ScimUser` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 201 | `ScimUser` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /scim/v2/Users/{user_id}` + +Get User + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `user_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ScimUser` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `PUT /scim/v2/Users/{user_id}` + +Replace User + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `user_id` | path | yes | string | | + +**Request body** (`application/json`): `ScimUser` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ScimUser` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `PATCH /scim/v2/Users/{user_id}` + +Patch User + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `user_id` | path | yes | string | | + +**Request body** (`application/json`): `ScimPatchOp` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ScimUser` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `DELETE /scim/v2/Users/{user_id}` + +Delete User + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `user_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 204 | `—` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +### status + +#### `GET /v1/connectors/status` + +Connectors Status + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ConnectorsStatusResponse` | Successful Response | + +#### `GET /v1/status/breakers` + +Status Breakers + +List every per-backend circuit breaker + its current state (Step 4.4). + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `BreakersStatusResponse` | Successful Response | + +#### `POST /v1/status/breakers/{name}/force-close` + +Force Close Breaker + +Force one circuit breaker CLOSED — the operator one-click recovery (Step 4.4). + +404 when no breaker registry is wired or the name is unknown. If the +backend is still unhealthy the breaker simply re-trips on the next +failure run; this just gives an operator an immediate retry without +waiting out ``reset_timeout_s``. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `name` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `BreakerActionResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/compliance` + +Status Compliance + +The live compliance posture (Step 6.10). + +Reports which governance controls are enabled (audit / ACL / PII / BYOK / +SSO / SCIM / quotas / retention / residency), the deployment's data region, +and the retention windows — the machine-readable backing for the SOC 2 / +GDPR control-mapping docs. Carries only configuration booleans, no tenant +data, so it is safe on the status surface. + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `CompliancePosture` | Successful Response | + +#### `GET /v1/status/cost` + +Status Cost + +One tenant's recent-spend anomaly verdict (Step 5.6c). + +Tenant resolves from the ``tenant_id`` query param, else the +``X-Tenant-Id`` header, else ``"default"``. Returns +``insufficient_data`` when cost tracking is disabled or the tenant has +seen too few requests. Detection only — never rejects a request. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `tenant_id` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `CostStatusResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/drift` + +Status Drift + +Drift report across the five monitors (Step 5.5). + +Infra-scoped (no tenant). Computing the report also emits +``drift.detected`` for any monitor that has **newly** crossed its +threshold (idempotent per transition). Empty when drift is disabled. + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `DriftReport` | Successful Response | + +#### `POST /v1/status/drift/{metric}/rebaseline` + +Rebaseline Drift + +Promote a monitor's current window to its reference — "accept the new normal". + +The operator one-click "this is the new baseline" — 404 when drift is +disabled, 400 on an unknown metric. Returns the post-rebaseline snapshot. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `metric` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `DriftSnapshot` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/experiments` + +Status Experiments + +A/B experiment comparisons — control vs candidate per experiment (Step 5.7). + +Composes the per-variant sample windows held by the ``experiment_tracker`` +with the pure ``analyze_ab_experiment`` (lift + confidence interval). +Empty when experiments are disabled; a comparison below ``min_samples`` on +either side reports ``insufficient_data``. + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `ExperimentsStatusResponse` | Successful Response | + +#### `GET /v1/status/feedback` + +Status Feedback + +One tenant's aggregate feedback for the dashboard (Step 5.4). + +Tenant resolves from the ``tenant_id`` query param, else the +``X-Tenant-Id`` header, else ``"default"``. Returns a zeroed +:class:`~rag_core.types.FeedbackStats` when feedback is disabled or the +tenant has none yet. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `tenant_id` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `FeedbackStats` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/health` + +Status Health + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `HealthResponse` | Successful Response | + +#### `GET /v1/status/logs` + +Status Logs + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `limit` | query | no | integer | | +| `level` | query | no | — | | +| `event` | query | no | — | | +| `tenant` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `LogsResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/logs/stream` + +Status Logs Stream + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `level` | query | no | — | | +| `event` | query | no | — | | +| `tenant` | query | no | — | | +| `interval_ms` | query | no | integer | | +| `backlog` | query | no | integer | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `object` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/metrics` + +Status Metrics + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `MetricsResponse` | Successful Response | + +#### `GET /v1/status/quotas` + +Status Quotas + +One tenant's quota usage across every dimension (Step 4.5). + +Tenant resolves from the ``tenant_id`` query param, else the +``X-Tenant-Id`` header, else ``"default"``. Empty when quotas are off. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `tenant_id` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `QuotasStatusResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `POST /v1/status/quotas/{tenant_id}/reset` + +Reset Quota + +Clear a tenant's usage on one dimension (``?dimension=qps``) or all (Step 4.5). + +The operator one-click "give them headroom now" — 404 when quotas are +off, 400 on an unknown dimension. Returns the post-reset snapshot. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `tenant_id` | path | yes | string | | +| `dimension` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `QuotaActionResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/sso` + +Status Sso + +SSO / SCIM posture for the calling tenant (Step 6.8). + +Reports whether identity federation (``cfg.sso``) and SCIM provisioning +(``cfg.scim``) are enabled on the gateway, plus whether *this* tenant has +an IdP configured and which protocol it speaks. Tenant-scoped — it never +lists other tenants' config. Tenant resolves from the ``tenant_id`` query +param, else the ``X-Tenant-Id`` header, else the gateway default. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `tenant_id` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `SsoStatusResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/status/tenant` + +Status Tenant + +Resolved per-tenant logical-tenancy settings (Step 6.1). + +Surfaces what the ``TenantResolver`` resolves for a tenant — its +namespace, PII action, and ACL labels — so an operator can see the +effective config a tenant's requests run under. Tenant resolves from the +``tenant_id`` query param, else the ``X-Tenant-Id`` header, else the +gateway default. Reports safe defaults (``known=false``) when the tenant +is absent from ``rag.yaml`` or no resolver is wired. + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `tenant_id` | query | no | — | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `TenantStatusResponse` | Successful Response | +| 422 | `HTTPValidationError` | Validation Error | + +### webhooks + +#### `GET /v1/webhooks/subscriptions` + +List webhook subscriptions (secrets masked) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `SubscriptionList` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | + +#### `POST /v1/webhooks/subscriptions` + +Create a webhook subscription (signing secret returned once) + +**Request body** (`application/json`): `CreateSubscriptionRequest` (required) + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 201 | `WebhookSubscription` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 422 | `HTTPValidationError` | Validation Error | + +#### `GET /v1/webhooks/subscriptions/{subscription_id}` + +Describe a subscription (secret masked) + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `subscription_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `WebhookSubscription` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | Subscription not found | +| 422 | `HTTPValidationError` | Validation Error | + +#### `DELETE /v1/webhooks/subscriptions/{subscription_id}` + +Delete a subscription + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `subscription_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 204 | `—` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | Subscription not found | +| 422 | `HTTPValidationError` | Validation Error | + +#### `POST /v1/webhooks/subscriptions/{subscription_id}/test` + +Send a synthetic test event to a subscription + +| Name | In | Required | Type | Description | +|------|----|:--------:|------|-------------| +| `subscription_id` | path | yes | string | | + +**Responses** + +| Status | Schema | Description | +|--------|--------|-------------| +| 200 | `WebhookTestResult` | Successful Response | +| 401 | `GatewayError` | Missing or invalid auth | +| 404 | `GatewayError` | Subscription not found | +| 422 | `HTTPValidationError` | Validation Error | diff --git a/scripts/gen_api_reference.py b/scripts/gen_api_reference.py new file mode 100644 index 0000000..89da1c5 --- /dev/null +++ b/scripts/gen_api_reference.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Generate the REST API reference page from the committed OpenAPI spec. + +Reads ``dist/openapi.json`` (the drift-gated gateway contract) and writes a +single self-contained Markdown page the documentation site serves. Pure stdlib, +deterministic, no network — runs in CI and locally the same way. + +Usage: + python scripts/gen_api_reference.py [--out docs/reference/rest-api.md] +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_SPEC = ROOT / "dist" / "openapi.json" +DEFAULT_OUT = ROOT / "docs" / "reference" / "rest-api.md" + +_METHOD_ORDER = ["get", "post", "put", "patch", "delete"] +_MD_LINK = re.compile(r"\[([^\]]+)\]\([^)]+\)") + + +def _plain(text: str | None) -> str: + """Flatten Markdown links to their label (drop relative URLs that wouldn't + resolve from ``docs/reference/``) and trim — descriptions come from source + docstrings authored relative to other files.""" + return _MD_LINK.sub(r"\1", text or "").strip() + + +def _params_table(params: list[dict[str, Any]]) -> list[str]: + if not params: + return [] + rows = [ + "", + "| Name | In | Required | Type | Description |", + "|------|----|:--------:|------|-------------|", + ] + for p in params: + schema = p.get("schema", {}) + typ = schema.get("type", schema.get("$ref", "—").split("/")[-1]) + req = "yes" if p.get("required") else "no" + desc = _plain(p.get("description", "")).replace("\n", " ") + rows.append(f"| `{p.get('name', '')}` | {p.get('in', '')} | {req} | {typ} | {desc} |") + return rows + + +def _ref_name(schema: dict[str, Any]) -> str | None: + ref = schema.get("$ref") + if ref: + return ref.split("/")[-1] + items = schema.get("items", {}) + if items.get("$ref"): + return f"{items['$ref'].split('/')[-1]}[]" + return None + + +def _body_line(op: dict[str, Any]) -> str | None: + body = op.get("requestBody") + if not body: + return None + content = body.get("content", {}) + for ctype, media in content.items(): + name = _ref_name(media.get("schema", {})) or "object" + req = " (required)" if body.get("required") else "" + return f"**Request body** (`{ctype}`): `{name}`{req}" + return None + + +def _responses_block(op: dict[str, Any]) -> list[str]: + responses = op.get("responses", {}) + if not responses: + return [] + out = [ + "", + "**Responses**", + "", + "| Status | Schema | Description |", + "|--------|--------|-------------|", + ] + for code, resp in sorted(responses.items()): + desc = _plain(resp.get("description", "")).replace("\n", " ") + schema_name = "—" + for media in resp.get("content", {}).values(): + schema_name = _ref_name(media.get("schema", {})) or "object" + break + out.append(f"| {code} | `{schema_name}` | {desc} |") + return out + + +def render(spec: dict[str, Any]) -> str: + info = spec.get("info", {}) + lines: list[str] = [ + "---", + "title: REST API reference", + "description: Generated from the committed OpenAPI 3.1 contract.", + "---", + "", + "# REST API reference", + "", + "> **Generated** from `dist/openapi.json` by `scripts/gen_api_reference.py` — " + "do not edit by hand. Regenerate with `task docs:api` (or " + "`python scripts/gen_api_reference.py`). The spec itself is drift-gated in CI.", + "", + f"**{info.get('title', 'API')}** · version `{info.get('version', '?')}` · OpenAPI 3.1", + "", + ] + # Group paths by first tag (fallback: the path's first segment). + grouped: dict[str, list[tuple[str, str, dict[str, Any]]]] = {} + for path, item in sorted(spec.get("paths", {}).items()): + for method in _METHOD_ORDER: + op = item.get(method) + if not op: + continue + tags = op.get("tags") or [path.strip("/").split("/")[0] or "root"] + grouped.setdefault(tags[0], []).append((method.upper(), path, op)) + + lines.append("## Endpoints") + lines.append("") + for tag in sorted(grouped): + lines.append(f"### {tag}") + lines.append("") + for method, path, op in grouped[tag]: + summary = _plain(op.get("summary", "")) + lines.append(f"#### `{method} {path}`") + if summary: + lines.append("") + lines.append(summary) + desc = _plain(op.get("description", "")) + if desc and desc != summary: + lines.append("") + lines.append(desc) + lines.extend(_params_table(op.get("parameters", []))) + body = _body_line(op) + if body: + lines.extend(["", body]) + lines.extend(_responses_block(op)) + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--spec", type=Path, default=DEFAULT_SPEC) + ap.add_argument("--out", type=Path, default=DEFAULT_OUT) + args = ap.parse_args() + + spec = json.loads(args.spec.read_text(encoding="utf-8")) + md = render(spec) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(md, encoding="utf-8") + n_paths = len(spec.get("paths", {})) + print(f"[gen_api_reference] wrote {args.out.relative_to(ROOT)} ({n_paths} paths)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/docs/test_quickstart_snippets.py b/tests/docs/test_quickstart_snippets.py new file mode 100644 index 0000000..35be325 --- /dev/null +++ b/tests/docs/test_quickstart_snippets.py @@ -0,0 +1,97 @@ +"""Executable doc tests — the quickstart guides must reference *real* surfaces. + +These keep the documentation honest against the code + the OpenAPI contract: + +* every ``ragctl `` shown in a quickstart is a registered CLI command +* every ``/v1/...`` REST path shown in a quickstart exists in ``dist/openapi.json`` + +They are deterministic and need no running services — the Step 7.5 "every +documented quickstart is exercised by a test" deliverable in its lightest form. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +GUIDES = ROOT / "docs" / "guides" +OPENAPI = ROOT / "dist" / "openapi.json" + +_CODE_BLOCK = re.compile(r"```(?:bash|shell|sh|console)\n(.*?)```", re.DOTALL) +_RAGCTL = re.compile(r"\bragctl[ \t]+([a-z][a-z0-9-]*)") +_REST_PATH = re.compile(r"/v1/[A-Za-z0-9_{}/.-]+") + + +def _code_blocks(text: str) -> str: + return "\n".join(m.group(1) for m in _CODE_BLOCK.finditer(text)) + + +def _registered_ragctl_commands() -> set[str]: + from ragctl.main import app + + names: set[str] = set() + for cmd in app.registered_commands: + if cmd.name: + names.add(cmd.name) + elif cmd.callback is not None: + names.add(cmd.callback.__name__.replace("_", "-")) + for grp in app.registered_groups: + if grp.name: + names.add(grp.name) + return names + + +def _openapi_path_templates() -> list[list[str]]: + spec = json.loads(OPENAPI.read_text(encoding="utf-8")) + return [p.strip("/").split("/") for p in spec.get("paths", {})] + + +def _path_matches(documented: str, templates: list[list[str]]) -> bool: + doc_segs = documented.strip("/").split("/") + for tmpl in templates: + if len(tmpl) != len(doc_segs): + continue + if all(t.startswith("{") or t == d for t, d in zip(tmpl, doc_segs, strict=True)): + return True + return False + + +def _quickstarts() -> list[Path]: + return sorted(GUIDES.glob("*quickstart*.md")) + + +def test_quickstarts_exist() -> None: + assert _quickstarts(), "no quickstart guides found under docs/guides/" + + +def test_ragctl_subcommands_in_quickstarts_are_real() -> None: + valid = _registered_ragctl_commands() + assert valid, "could not introspect ragctl commands" + missing: dict[str, set[str]] = {} + for guide in _quickstarts(): + code = _code_blocks(guide.read_text(encoding="utf-8")) + used = {m.group(1) for m in _RAGCTL.finditer(code)} + bad = {c for c in used if c not in valid} + if bad: + missing[guide.name] = bad + assert not missing, f"quickstarts reference unknown ragctl subcommands: {missing}" + + +def test_rest_paths_in_quickstarts_exist_in_openapi() -> None: + templates = _openapi_path_templates() + assert templates, "no paths in dist/openapi.json" + unknown: dict[str, set[str]] = {} + for guide in _quickstarts(): + code = _code_blocks(guide.read_text(encoding="utf-8")) + # strip query strings / trailing punctuation before matching + paths = { + _REST_PATH.match(raw).group(0) # type: ignore[union-attr] + for raw in (p.split("?")[0].rstrip(".,)\"'`") for p in _REST_PATH.findall(code)) + if _REST_PATH.match(raw) + } + bad = {p for p in paths if not _path_matches(p, templates)} + if bad: + unknown[guide.name] = bad + assert not unknown, f"quickstarts reference REST paths absent from openapi.json: {unknown}" diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..e025ad0 --- /dev/null +++ b/website/README.md @@ -0,0 +1,42 @@ +# AgentContextOS documentation site + +[Docusaurus 3](https://docusaurus.io) site that serves the repository +[`../docs`](../docs) tree as . The repo `docs/` +folder stays the single source of truth — this site renders it, it does not copy +it. + +## Run locally + +```bash +cd website +npm install +npm run dev # http://localhost:3000 (live reload) +``` + +## Build (what CI does) + +```bash +npm run build # static site → website/build/ +npm run serve # preview the production build +``` + +## API reference + +The [REST API reference](../docs/reference/rest-api.md) is **generated** from the +committed `dist/openapi.json` contract — never hand-edited: + +```bash +task docs:api # or: python ../scripts/gen_api_reference.py +``` + +CI regenerates it and fails on drift, so the published reference always matches +the gateway's drift-gated OpenAPI spec. + +## Conventions + +- `.md` files render as CommonMark (`markdown.format: 'detect'`), so the existing + hand-written docs render without MDX escaping. +- New top-level sections under `../docs/
/` appear automatically in the + autogenerated sidebar. +- Broken links are checked in CI by [lychee](https://github.com/lycheeverse/lychee) + (`.github/workflows/docs.yml`), not by the Docusaurus build (set to `warn`). diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts new file mode 100644 index 0000000..315bddb --- /dev/null +++ b/website/docusaurus.config.ts @@ -0,0 +1,88 @@ +import type * as Preset from "@docusaurus/preset-classic"; +import type { Config } from "@docusaurus/types"; +import { themes as prismThemes } from "prism-react-renderer"; + +// The site sources the repository ../docs tree directly (single source of +// truth — no copy step). `markdown.format: 'detect'` parses .md as CommonMark so +// the existing hand-written docs (which contain `<` / `{`) don't trip the MDX +// compiler; only .mdx files use the MDX pipeline. +const config: Config = { + title: "AgentContextOS", + tagline: "Production-grade, multi-tenant RAG platform", + favicon: "img/favicon.ico", + + url: "https://docs.agentcontextos.io", + baseUrl: "/", + + organizationName: "officialCodeWork", + projectName: "AgentContextOS", + + onBrokenLinks: "warn", + onBrokenMarkdownLinks: "warn", + + markdown: { format: "detect" }, + + i18n: { defaultLocale: "en", locales: ["en"] }, + + presets: [ + [ + "classic", + { + docs: { + path: "../docs", + routeBasePath: "/", + sidebarPath: "./sidebars.ts", + editUrl: + "https://github.com/officialCodeWork/AgentContextOS/tree/main/", + }, + blog: false, + theme: { customCss: "./src/css/custom.css" }, + } satisfies Preset.Options, + ], + ], + + themeConfig: { + navbar: { + title: "AgentContextOS", + items: [ + { to: "/", label: "Docs", position: "left" }, + { to: "/reference/rest-api", label: "API", position: "left" }, + { + href: "https://github.com/officialCodeWork/AgentContextOS", + label: "GitHub", + position: "right", + }, + ], + }, + footer: { + style: "dark", + links: [ + { + title: "Docs", + items: [ + { label: "Architecture", to: "/architecture/RAG-Platform-HLD" }, + { label: "Guides", to: "/guides/curl-quickstart" }, + { label: "REST API", to: "/reference/rest-api" }, + ], + }, + { + title: "More", + items: [ + { + label: "GitHub", + href: "https://github.com/officialCodeWork/AgentContextOS", + }, + ], + }, + ], + copyright: `Copyright © ${new Date().getFullYear()} AgentContextOS.`, + }, + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + additionalLanguages: ["bash", "json", "yaml", "python", "protobuf"], + }, + } satisfies Preset.ThemeConfig, +}; + +export default config; diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..4dd8d1e --- /dev/null +++ b/website/package.json @@ -0,0 +1,30 @@ +{ + "name": "@agentcontextos/docs", + "version": "0.1.0", + "private": true, + "description": "AgentContextOS documentation site (Docusaurus 3) — sources the repo docs/ tree.", + "scripts": { + "dev": "docusaurus start", + "build": "docusaurus build", + "serve": "docusaurus serve", + "clear": "docusaurus clear", + "typecheck": "tsc" + }, + "dependencies": { + "@docusaurus/core": "^3.6.0", + "@docusaurus/preset-classic": "^3.6.0", + "clsx": "^2.1.1", + "prism-react-renderer": "^2.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^3.6.0", + "@docusaurus/tsconfig": "^3.6.0", + "@docusaurus/types": "^3.6.0", + "typescript": "~5.6.2" + }, + "engines": { + "node": ">=18.0" + } +} diff --git a/website/sidebars.ts b/website/sidebars.ts new file mode 100644 index 0000000..982fe98 --- /dev/null +++ b/website/sidebars.ts @@ -0,0 +1,11 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; + +// Auto-generated from the ../docs directory tree. Each subfolder (architecture, +// reference, guides, adr, pilots, compliance, research, spikes) becomes a +// category; ordering falls back to alphabetical unless a doc sets +// `sidebar_position` frontmatter. +const sidebars: SidebarsConfig = { + docsSidebar: [{ type: "autogenerated", dirName: "." }], +}; + +export default sidebars; diff --git a/website/src/css/custom.css b/website/src/css/custom.css new file mode 100644 index 0000000..7d7c45c --- /dev/null +++ b/website/src/css/custom.css @@ -0,0 +1,26 @@ +/** + * AgentContextOS docs theme — minimal overrides on the Docusaurus classic theme. + */ + +:root { + --ifm-color-primary: #2563eb; + --ifm-color-primary-dark: #1d4ed8; + --ifm-color-primary-darker: #1e40af; + --ifm-color-primary-darkest: #1e3a8a; + --ifm-color-primary-light: #3b82f6; + --ifm-color-primary-lighter: #60a5fa; + --ifm-color-primary-lightest: #93c5fd; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.08); +} + +[data-theme="dark"] { + --ifm-color-primary: #60a5fa; + --ifm-color-primary-dark: #3b82f6; + --ifm-color-primary-darker: #2563eb; + --ifm-color-primary-darkest: #1d4ed8; + --ifm-color-primary-light: #93c5fd; + --ifm-color-primary-lighter: #bfdbfe; + --ifm-color-primary-lightest: #dbeafe; + --docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.1); +} diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 0000000..6f3b11c --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "baseUrl": "." + }, + "exclude": [".docusaurus", "build"] +}