fix doc
This commit is contained in:
parent
4880d1dd87
commit
410ae18d2d
@ -56,6 +56,8 @@ LLM Provider Adapters (OpenAI, Anthropic, Azure, Mistral, Ollama)
|
|||||||
|
|
||||||
**Frontend:** React 18 + TypeScript + Vite, shadcn/ui, recharts. Routes protected via OIDC (Keycloak); `web/src/auth/` manages the auth flow. API clients live in `web/src/api/`.
|
**Frontend:** React 18 + TypeScript + Vite, shadcn/ui, recharts. Routes protected via OIDC (Keycloak); `web/src/auth/` manages the auth flow. API clients live in `web/src/api/`.
|
||||||
|
|
||||||
|
**Documentation site** (`http://localhost:3000/docs`): public, no auth required. Root: `web/src/pages/docs/` — sections: getting-started, installation, api-reference (8 endpoints), guides (6), deployment (3), security (2), changelog. Layout components: `DocLayout.tsx` (sidebar + content + TOC), `DocSidebar.tsx` (with search), `DocBreadcrumbs.tsx`, `DocPagination.tsx`. Shared components: `components/CodeBlock.tsx`, `Callout.tsx`, `ApiEndpoint.tsx`, `ParamTable.tsx`, `TableOfContents.tsx`. Nav structure: `web/src/pages/docs/nav.ts`. Uses `@tailwindcss/typography` (added as devDependency) for prose rendering.
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -69,6 +71,7 @@ proto/pii/v1/ # gRPC .proto definitions
|
|||||||
migrations/ # golang-migrate SQL files (up/down pairs)
|
migrations/ # golang-migrate SQL files (up/down pairs)
|
||||||
clickhouse/ # ClickHouse DDL applied at startup via ApplyDDL()
|
clickhouse/ # ClickHouse DDL applied at startup via ApplyDDL()
|
||||||
web/ # React frontend (Vite, src/pages, src/components, src/api)
|
web/ # React frontend (Vite, src/pages, src/components, src/api)
|
||||||
|
src/pages/docs/ # Public documentation site (no auth); nav.ts defines sidebar structure
|
||||||
test/ # Integration tests (test/integration/, //go:build integration) + k6 load tests (test/k6/)
|
test/ # Integration tests (test/integration/, //go:build integration) + k6 load tests (test/k6/)
|
||||||
deploy/ # Helm, Kubernetes manifests, Terraform (EKS), Prometheus/Grafana, alertmanager
|
deploy/ # Helm, Kubernetes manifests, Terraform (EKS), Prometheus/Grafana, alertmanager
|
||||||
clickhouse/ # ClickHouse config overrides for Docker (e.g. listen-ipv4.xml — forces IPv4)
|
clickhouse/ # ClickHouse config overrides for Docker (e.g. listen-ipv4.xml — forces IPv4)
|
||||||
|
|||||||
28
web/package-lock.json
generated
28
web/package-lock.json
generated
@ -32,6 +32,7 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.5.5",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
@ -2034,6 +2035,33 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
"version": "0.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||||
|
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.90.20",
|
"version": "5.90.20",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.5.5",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
|||||||
@ -257,7 +257,7 @@ export function LandingPage() {
|
|||||||
<a href="#securite" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Sécurité</a>
|
<a href="#securite" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Sécurité</a>
|
||||||
<a href="#personas" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Pour qui</a>
|
<a href="#personas" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Pour qui</a>
|
||||||
<a href="http://localhost:8090/playground" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Playground</a>
|
<a href="http://localhost:8090/playground" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Playground</a>
|
||||||
<a href="http://localhost:8090/docs" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Docs</a>
|
<Link to="/docs" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Docs</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/login"
|
to="/login"
|
||||||
style={{
|
style={{
|
||||||
@ -716,7 +716,7 @@ export function LandingPage() {
|
|||||||
<BtnOutline to="/login">
|
<BtnOutline to="/login">
|
||||||
Se connecter au dashboard
|
Se connecter au dashboard
|
||||||
</BtnOutline>
|
</BtnOutline>
|
||||||
<BtnOutline href="http://localhost:8090/docs">
|
<BtnOutline to="/docs">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
Documentation
|
Documentation
|
||||||
</BtnOutline>
|
</BtnOutline>
|
||||||
@ -741,15 +741,9 @@ export function LandingPage() {
|
|||||||
Veylant<span style={{ color: brandLight }}> IA</span>
|
Veylant<span style={{ color: brandLight }}> IA</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div style={{ display: "flex", gap: "1.5rem" }}>
|
<div style={{ display: "flex", gap: "1.5rem" }}>
|
||||||
{[
|
<Link to="/docs" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Documentation</Link>
|
||||||
{ label: "Documentation", href: "http://localhost:8090/docs" },
|
<a href="http://localhost:8090/playground" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Playground</a>
|
||||||
{ label: "Playground", href: "http://localhost:8090/playground" },
|
<a href="mailto:demo@veylant.io" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Contact</a>
|
||||||
{ label: "Contact", href: "mailto:demo@veylant.io" },
|
|
||||||
].map((l) => (
|
|
||||||
<a key={l.label} href={l.href} style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>
|
|
||||||
{l.label}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: ".8rem", color: dim }}>© 2026 Veylant. Conçu pour l'entreprise européenne.</span>
|
<span style={{ fontSize: ".8rem", color: dim }}>© 2026 Veylant. Conçu pour l'entreprise européenne.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
151
web/src/pages/docs/ChangelogPage.tsx
Normal file
151
web/src/pages/docs/ChangelogPage.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { Callout } from "./components/Callout";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const v100Changes = {
|
||||||
|
features: [
|
||||||
|
"AI proxy with OpenAI-compatible API (/v1/chat/completions)",
|
||||||
|
"3-layer PII detection: regex + spaCy NER + LLM validation",
|
||||||
|
"PII pseudonymization with AES-256-GCM Redis mappings",
|
||||||
|
"Intelligent routing engine (PostgreSQL JSONB rules, in-memory cache, priority ASC)",
|
||||||
|
"RBAC: admin, manager, user, auditor roles with model restrictions",
|
||||||
|
"Streaming SSE support (PII anonymization on request, not response)",
|
||||||
|
"Circuit breaker per provider (threshold=5, TTL=60s) with fallback chain",
|
||||||
|
"Token-bucket rate limiting (per-tenant + per-user, DB overrides)",
|
||||||
|
"ClickHouse append-only audit logs with async batch writer",
|
||||||
|
"GDPR Article 30 processing registry with PDF/JSON export",
|
||||||
|
"EU AI Act risk classification questionnaire (forbidden/high/limited/minimal)",
|
||||||
|
"DPIA template generation",
|
||||||
|
"GDPR Art. 15 (Right of Access) and Art. 17 (Right to Erasure) endpoints",
|
||||||
|
"Feature flags (PostgreSQL + in-memory fallback)",
|
||||||
|
"React 18 dashboard with 11 pages (French UI)",
|
||||||
|
"Public AI playground at /playground (IP rate-limited 20/min)",
|
||||||
|
"Swagger UI at /docs",
|
||||||
|
"Provider adapters: OpenAI, Anthropic, Azure, Mistral, Ollama",
|
||||||
|
"Prometheus metrics + Grafana dashboards (overview + SLO)",
|
||||||
|
"7 Prometheus alerts with Alertmanager (PagerDuty + Slack)",
|
||||||
|
"HPA autoscaling 3→15 replicas (CPU 70% + memory 80%)",
|
||||||
|
"Blue/green deployment with Istio VirtualService (<5s rollback)",
|
||||||
|
"Terraform EKS v1.31, 3-AZ, eu-west-3",
|
||||||
|
"PostgreSQL backup CronJob (daily 02:00 UTC, S3, 7-day retention)",
|
||||||
|
"k6 load tests: smoke, load, stress, soak scenarios",
|
||||||
|
"HashiCorp Vault integration for API key rotation (90-day cycle)",
|
||||||
|
"AES-256-GCM prompt encryption",
|
||||||
|
"Production SLO dashboard (99.5% availability)",
|
||||||
|
"6 operational runbooks",
|
||||||
|
"Admin guide + integration guide + onboarding scripts",
|
||||||
|
"Pentest scope + remediation report (0 Critical, 0 High)",
|
||||||
|
"Commercial materials: one-pager, pitch deck, battle card",
|
||||||
|
"CHANGELOG.md with full v1.0.0 history",
|
||||||
|
"GitHub Actions release pipeline (multi-arch Docker + Helm OCI + GitHub Release)",
|
||||||
|
],
|
||||||
|
bugfixes: [
|
||||||
|
"Rate limit 429 responses now include Retry-After: 1 header (RFC 6585)",
|
||||||
|
"CSP header updated to allow dashboard iframe embeds",
|
||||||
|
"CORS allowed origins now configurable (server.allowed_origins) instead of wildcard",
|
||||||
|
"Routing rule cache race condition on concurrent updates fixed",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const v110Roadmap = [
|
||||||
|
{ priority: "Must-have", item: "Layer 3 LLM validation for PII (ambiguous cases)" },
|
||||||
|
{ priority: "Must-have", item: "Per-routing-rule PII control (enable/disable per rule)" },
|
||||||
|
{ priority: "Must-have", item: "Native OpenAI SDK (Go) for seamless migration" },
|
||||||
|
{ priority: "Should-have", item: "ML anomaly detection on prompt patterns" },
|
||||||
|
{ priority: "Should-have", item: "Shadow AI discovery (network traffic analysis)" },
|
||||||
|
{ priority: "Should-have", item: "Physical multi-tenant isolation (separate DB schemas)" },
|
||||||
|
{ priority: "Should-have", item: "SIEM integrations (Splunk, Elastic)" },
|
||||||
|
{ priority: "Could-have", item: "Custom model adapters via plugin API" },
|
||||||
|
{ priority: "Could-have", item: "Prompt caching (provider-level + in-proxy)" },
|
||||||
|
{ priority: "Could-have", item: "Cost budget alerts via email/Slack (direct)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ChangelogPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="changelog">Changelog</h1>
|
||||||
|
<p>
|
||||||
|
All notable changes to Veylant IA are documented here. Versioning follows{" "}
|
||||||
|
<a
|
||||||
|
href="https://semver.org"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Semantic Versioning
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="v100">v1.0.0 — February 25, 2026</h2>
|
||||||
|
<Callout type="tip" title="General Availability">
|
||||||
|
First production release. Pentest passed (0 Critical, 0 High). 2 pilot clients migrated.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h3 id="features">Features</h3>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{v100Changes.features.map((f) => (
|
||||||
|
<li key={f} className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="bug-fixes">Bug Fixes</h3>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{v100Changes.bugfixes.map((b) => (
|
||||||
|
<li key={b} className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{b}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="v110-roadmap">v1.1.0 — Planned (Q2 2026)</h2>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Priorities sourced from 2 pilot client sessions (MoSCoW method). See{" "}
|
||||||
|
<code>docs/feedback-backlog.md</code> for the full backlog.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Priority</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Feature</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{v110Roadmap.map((item) => (
|
||||||
|
<tr key={item.item} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<span
|
||||||
|
className={`text-xs px-1.5 py-0.5 rounded font-medium ${
|
||||||
|
item.priority === "Must-have"
|
||||||
|
? "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300"
|
||||||
|
: item.priority === "Should-have"
|
||||||
|
? "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300"
|
||||||
|
: "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.priority}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5 text-sm text-muted-foreground">{item.item}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="migration">Migration & Compatibility</h2>
|
||||||
|
<p>
|
||||||
|
V1.0.0 has no breaking changes from the beta releases used by pilot clients. The{" "}
|
||||||
|
<code>/v1/chat/completions</code> API is fully backward-compatible with the OpenAI API
|
||||||
|
format. Any client using the OpenAI SDK will continue to work without changes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
See the <Link to="/docs/installation/docker">Installation guide</Link> for upgrade
|
||||||
|
instructions from beta.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
web/src/pages/docs/DocBreadcrumbs.tsx
Normal file
28
web/src/pages/docs/DocBreadcrumbs.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
import { NAV_SECTIONS } from "./nav";
|
||||||
|
|
||||||
|
export function DocBreadcrumbs() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const section = NAV_SECTIONS.find((s) => s.items.some((i) => i.path === pathname));
|
||||||
|
const item = section?.items.find((i) => i.path === pathname);
|
||||||
|
|
||||||
|
if (!section || !item) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="flex items-center gap-1.5 text-sm text-muted-foreground mb-6">
|
||||||
|
<Link to="/docs" className="hover:text-foreground transition-colors">
|
||||||
|
Docs
|
||||||
|
</Link>
|
||||||
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
<span>{section.title}</span>
|
||||||
|
{item.path !== "/docs" && (
|
||||||
|
<>
|
||||||
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-foreground">{item.title}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
web/src/pages/docs/DocLayout.tsx
Normal file
51
web/src/pages/docs/DocLayout.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
|
import { DocSidebar } from "./DocSidebar";
|
||||||
|
import { DocBreadcrumbs } from "./DocBreadcrumbs";
|
||||||
|
import { DocPagination } from "./DocPagination";
|
||||||
|
import { TableOfContents } from "./components/TableOfContents";
|
||||||
|
|
||||||
|
export function DocLayout() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const isHome = pathname === "/docs";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-background">
|
||||||
|
{/* Left sidebar */}
|
||||||
|
<DocSidebar />
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="flex flex-1 min-w-0">
|
||||||
|
{/* Article */}
|
||||||
|
<main className="flex-1 min-w-0 px-8 py-10 overflow-y-auto max-h-screen">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{!isHome && <DocBreadcrumbs />}
|
||||||
|
<div
|
||||||
|
data-doc-content
|
||||||
|
className="prose prose-sm sm:prose-base max-w-none
|
||||||
|
prose-headings:font-bold prose-headings:tracking-tight
|
||||||
|
prose-h1:text-3xl prose-h1:mb-2
|
||||||
|
prose-h2:text-xl prose-h2:mt-8 prose-h2:mb-3
|
||||||
|
prose-h3:text-base prose-h3:mt-6 prose-h3:mb-2
|
||||||
|
prose-p:text-muted-foreground prose-p:leading-7
|
||||||
|
prose-li:text-muted-foreground prose-li:leading-7
|
||||||
|
prose-strong:text-foreground
|
||||||
|
prose-code:text-sm prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none
|
||||||
|
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
{!isHome && <DocPagination />}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Right TOC (hidden on home page and small screens) */}
|
||||||
|
{!isHome && (
|
||||||
|
<div className="hidden xl:block w-56 shrink-0 px-6 py-10 overflow-y-auto max-h-screen">
|
||||||
|
<TableOfContents />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
web/src/pages/docs/DocPagination.tsx
Normal file
43
web/src/pages/docs/DocPagination.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { getPrevNext } from "./nav";
|
||||||
|
|
||||||
|
export function DocPagination() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const { prev, next } = getPrevNext(pathname);
|
||||||
|
|
||||||
|
if (!prev && !next) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between mt-12 pt-6 border-t">
|
||||||
|
{prev ? (
|
||||||
|
<Link
|
||||||
|
to={prev.path}
|
||||||
|
className="group flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 group-hover:-translate-x-0.5 transition-transform" />
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-xs uppercase tracking-wide mb-0.5">Previous</div>
|
||||||
|
<div className="font-medium">{prev.title}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
{next ? (
|
||||||
|
<Link
|
||||||
|
to={next.path}
|
||||||
|
className="group flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors text-right"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide mb-0.5">Next</div>
|
||||||
|
<div className="font-medium">{next.title}</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
web/src/pages/docs/DocSidebar.tsx
Normal file
139
web/src/pages/docs/DocSidebar.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { NavLink, Link } from "react-router-dom";
|
||||||
|
import { Search, Shield, X } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { NAV_SECTIONS, ALL_NAV_ITEMS } from "./nav";
|
||||||
|
|
||||||
|
export function DocSidebar() {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const filtered =
|
||||||
|
query.trim().length > 0
|
||||||
|
? ALL_NAV_ITEMS.filter(
|
||||||
|
(i) =>
|
||||||
|
i.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
i.section.toLowerCase().includes(query.toLowerCase())
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="w-72 shrink-0 hidden lg:flex flex-col h-full border-r bg-background">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-2 px-5 py-4 border-b">
|
||||||
|
<div className="h-7 w-7 rounded-md bg-primary flex items-center justify-center">
|
||||||
|
<Shield className="h-4 w-4 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link to="/docs" className="font-bold text-sm hover:text-primary transition-colors">
|
||||||
|
Veylant IA Docs
|
||||||
|
</Link>
|
||||||
|
<span className="ml-2 text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded font-mono">
|
||||||
|
v1.0.0
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="px-4 py-3 border-b">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search docs..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
className="w-full pl-8 pr-7 py-1.5 text-sm bg-muted rounded-md border-0 outline-none focus:ring-1 focus:ring-primary placeholder:text-muted-foreground"
|
||||||
|
/>
|
||||||
|
{query && (
|
||||||
|
<button
|
||||||
|
onClick={() => setQuery("")}
|
||||||
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav */}
|
||||||
|
<nav className="flex-1 overflow-y-auto py-4 px-3">
|
||||||
|
{filtered ? (
|
||||||
|
/* Search results */
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground px-2 mb-2">
|
||||||
|
{filtered.length} result{filtered.length !== 1 ? "s" : ""}
|
||||||
|
</p>
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground px-2">No pages found.</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-0.5">
|
||||||
|
{filtered.map((item) => (
|
||||||
|
<li key={item.path}>
|
||||||
|
<NavLink
|
||||||
|
to={item.path}
|
||||||
|
end={item.path === "/docs"}
|
||||||
|
onClick={() => setQuery("")}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
cn(
|
||||||
|
"flex flex-col px-3 py-2 rounded-md text-sm transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-primary/10 text-primary font-semibold border-l-2 border-primary"
|
||||||
|
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<span className="text-xs text-muted-foreground mt-0.5">{item.section}</span>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Full nav tree */
|
||||||
|
<div className="space-y-6">
|
||||||
|
{NAV_SECTIONS.map((section) => (
|
||||||
|
<div key={section.title}>
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-3 mb-1.5">
|
||||||
|
{section.title}
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-0.5">
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<li key={item.path}>
|
||||||
|
<NavLink
|
||||||
|
to={item.path}
|
||||||
|
end={item.path === "/docs"}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
cn(
|
||||||
|
"block px-3 py-1.5 rounded-md text-sm transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-primary/10 text-primary font-semibold border-l-2 border-primary pl-[10px]"
|
||||||
|
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-4 py-3 border-t">
|
||||||
|
<Link
|
||||||
|
to="/dashboard"
|
||||||
|
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<Shield className="h-3.5 w-3.5" />
|
||||||
|
Back to Dashboard
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
203
web/src/pages/docs/DocsHomePage.tsx
Normal file
203
web/src/pages/docs/DocsHomePage.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Rocket,
|
||||||
|
Package,
|
||||||
|
Plug,
|
||||||
|
Shield,
|
||||||
|
GitBranch,
|
||||||
|
Scale,
|
||||||
|
Ship,
|
||||||
|
Lock,
|
||||||
|
ArrowRight,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Callout } from "./components/Callout";
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
icon: Rocket,
|
||||||
|
title: "Quick Start",
|
||||||
|
description: "Get Veylant IA running in 5 minutes with Docker Compose.",
|
||||||
|
href: "/docs/getting-started/quick-start",
|
||||||
|
color: "bg-blue-50 dark:bg-blue-950/30 text-blue-600 dark:text-blue-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Package,
|
||||||
|
title: "Installation",
|
||||||
|
description: "Configure services, providers, and environment variables.",
|
||||||
|
href: "/docs/installation/docker",
|
||||||
|
color: "bg-purple-50 dark:bg-purple-950/30 text-purple-600 dark:text-purple-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Plug,
|
||||||
|
title: "API Reference",
|
||||||
|
description: "Full reference for all REST endpoints, parameters, and responses.",
|
||||||
|
href: "/docs/api/chat-completions",
|
||||||
|
color: "bg-green-50 dark:bg-green-950/30 text-green-600 dark:text-green-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: "PII & Privacy",
|
||||||
|
description: "3-layer PII detection, pseudonymization, and Redis-backed mappings.",
|
||||||
|
href: "/docs/guides/pii",
|
||||||
|
color: "bg-red-50 dark:bg-red-950/30 text-red-600 dark:text-red-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: GitBranch,
|
||||||
|
title: "Routing Rules",
|
||||||
|
description: "Route AI requests by role, department, model, or sensitivity.",
|
||||||
|
href: "/docs/guides/routing",
|
||||||
|
color: "bg-amber-50 dark:bg-amber-950/30 text-amber-600 dark:text-amber-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Scale,
|
||||||
|
title: "Compliance",
|
||||||
|
description: "GDPR Article 30 registry, AI Act risk classification, and DPIA.",
|
||||||
|
href: "/docs/guides/compliance",
|
||||||
|
color: "bg-teal-50 dark:bg-teal-950/30 text-teal-600 dark:text-teal-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Ship,
|
||||||
|
title: "Deployment",
|
||||||
|
description: "Docker, Kubernetes Helm chart, and blue/green with Istio.",
|
||||||
|
href: "/docs/deployment/kubernetes",
|
||||||
|
color: "bg-indigo-50 dark:bg-indigo-950/30 text-indigo-600 dark:text-indigo-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Lock,
|
||||||
|
title: "Security",
|
||||||
|
description: "Zero Trust, mTLS, AES-256-GCM encryption, and audit trails.",
|
||||||
|
href: "/docs/security/model",
|
||||||
|
color: "bg-slate-50 dark:bg-slate-950/30 text-slate-600 dark:text-slate-400",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DocsHomePage() {
|
||||||
|
return (
|
||||||
|
<div className="py-4">
|
||||||
|
{/* Hero */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<span className="text-xs font-semibold bg-primary/10 text-primary px-2 py-0.5 rounded-full">
|
||||||
|
v1.0.0 — Generally Available
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-extrabold tracking-tight text-foreground mb-4">
|
||||||
|
Veylant IA Documentation
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-muted-foreground max-w-2xl leading-relaxed">
|
||||||
|
Everything you need to build, configure, and operate your enterprise AI governance
|
||||||
|
platform. Prevent Shadow AI, enforce PII anonymization, ensure GDPR compliance, and
|
||||||
|
control costs across all LLM usage.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* What's new */}
|
||||||
|
<Callout type="tip" title="What's new in v1.0.0">
|
||||||
|
Pentest passed (0 Critical, 0 High), 2 pilot clients migrated, blue/green deployment with
|
||||||
|
Istio, HPA autoscaling (3→15 replicas), 7 Prometheus alerts, SLO dashboard (99.5%), and 6
|
||||||
|
operational runbooks.{" "}
|
||||||
|
<Link to="/docs/changelog" className="text-primary hover:underline font-medium">
|
||||||
|
Read the full changelog →
|
||||||
|
</Link>
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
{/* Cards grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||||
|
{cards.map((card) => (
|
||||||
|
<Link
|
||||||
|
key={card.href}
|
||||||
|
to={card.href}
|
||||||
|
className="group flex items-start gap-4 rounded-xl border bg-card p-5 hover:shadow-md transition-all hover:border-primary/30"
|
||||||
|
>
|
||||||
|
<div className={`p-2.5 rounded-lg shrink-0 ${card.color}`}>
|
||||||
|
<card.icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-semibold text-foreground">{card.title}</h3>
|
||||||
|
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-0.5 transition-all" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">
|
||||||
|
{card.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick links */}
|
||||||
|
<div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div className="rounded-xl border bg-card p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Sparkles className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-semibold">Popular pages</span>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{[
|
||||||
|
{ title: "Chat Completions API", href: "/docs/api/chat-completions" },
|
||||||
|
{ title: "Routing Rules Engine", href: "/docs/guides/routing" },
|
||||||
|
{ title: "RBAC & Permissions", href: "/docs/guides/rbac" },
|
||||||
|
{ title: "GDPR Compliance", href: "/docs/guides/compliance" },
|
||||||
|
].map((l) => (
|
||||||
|
<li key={l.href}>
|
||||||
|
<Link
|
||||||
|
to={l.href}
|
||||||
|
className="text-sm text-muted-foreground hover:text-primary transition-colors flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
{l.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border bg-card p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Package className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-semibold">AI Providers</span>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{[
|
||||||
|
"OpenAI (gpt-4o, gpt-4-turbo)",
|
||||||
|
"Anthropic (claude-3-5-sonnet)",
|
||||||
|
"Azure OpenAI",
|
||||||
|
"Mistral AI",
|
||||||
|
"Ollama (self-hosted)",
|
||||||
|
].map((p) => (
|
||||||
|
<li key={p} className="text-sm text-muted-foreground flex items-center gap-1.5">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||||
|
{p}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border bg-card p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Plug className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-semibold">Compatibility</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
OpenAI-compatible API — drop-in replacement:
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{[
|
||||||
|
"openai Python SDK",
|
||||||
|
"openai Node.js SDK",
|
||||||
|
"LangChain",
|
||||||
|
"LlamaIndex",
|
||||||
|
"Any HTTP client",
|
||||||
|
].map((c) => (
|
||||||
|
<li key={c} className="text-sm text-muted-foreground flex items-center gap-1.5">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||||
|
{c}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
web/src/pages/docs/api-reference/AdminCompliancePage.tsx
Normal file
143
web/src/pages/docs/api-reference/AdminCompliancePage.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ApiEndpoint } from "../components/ApiEndpoint";
|
||||||
|
import { ParamTable } from "../components/ParamTable";
|
||||||
|
|
||||||
|
export function AdminCompliancePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="admin-compliance">Admin — Compliance</h1>
|
||||||
|
<p>
|
||||||
|
GDPR Article 30 processing registry, EU AI Act risk classification, and data subject
|
||||||
|
rights (access and erasure).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="Required role">
|
||||||
|
Compliance endpoints require <code>admin</code> role. Auditors can read but not modify.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="processing-registry">GDPR Article 30 — Processing Registry</h2>
|
||||||
|
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/compliance/entries" description="List all processing activities in the GDPR Art. 30 registry." />
|
||||||
|
<ApiEndpoint method="POST" path="/v1/admin/compliance/entries" description="Create a new processing activity entry." />
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/compliance/entries/{id}" description="Get a specific processing entry." />
|
||||||
|
<ApiEndpoint method="PUT" path="/v1/admin/compliance/entries/{id}" description="Update a processing activity entry." />
|
||||||
|
|
||||||
|
<ParamTable
|
||||||
|
title="Processing Entry Fields"
|
||||||
|
params={[
|
||||||
|
{ name: "use_case_name", type: "string", required: true, description: "Name of the AI use case (e.g. 'Legal contract analysis')." },
|
||||||
|
{ name: "purpose", type: "string", required: true, description: "Processing purpose as required by GDPR Art. 5." },
|
||||||
|
{ name: "legal_basis", type: "string", required: true, description: "Legal basis: legitimate_interest | contract | legal_obligation | consent | vital_interests | public_task" },
|
||||||
|
{ name: "data_categories", type: "[]string", required: true, description: "Categories of personal data: name, email, phone, id_number, financial, health, etc." },
|
||||||
|
{ name: "retention_period", type: "string", required: true, description: "Data retention period (e.g. '3 years', '90 days')." },
|
||||||
|
{ name: "security_measures", type: "string", required: true, description: "Technical and organizational security measures." },
|
||||||
|
{ name: "controller_name", type: "string", required: true, description: "Data controller name and contact." },
|
||||||
|
{ name: "recipients", type: "[]string", required: false, description: "Third parties receiving the data." },
|
||||||
|
{ name: "processors", type: "[]string", required: false, description: "Data processors (LLM providers, cloud services)." },
|
||||||
|
{ name: "dpia_required", type: "boolean", required: false, default: "false", description: "Whether a DPIA (Art. 35) is required." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl -X POST http://localhost:8090/v1/admin/compliance/entries \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"use_case_name": "Legal contract analysis",
|
||||||
|
"purpose": "Automated review of supplier contracts for risk identification",
|
||||||
|
"legal_basis": "legitimate_interest",
|
||||||
|
"data_categories": ["name", "financial", "company_data"],
|
||||||
|
"retention_period": "3 years",
|
||||||
|
"security_measures": "AES-256-GCM encryption, PII anonymization, audit logs",
|
||||||
|
"controller_name": "Acme Corp — dpo@acme.com",
|
||||||
|
"processors": ["Anthropic (Claude via Veylant IA proxy)"]
|
||||||
|
}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="ai-act">EU AI Act Classification</h2>
|
||||||
|
<ApiEndpoint method="POST" path="/v1/admin/compliance/entries/{id}/classify" description="Run the AI Act risk questionnaire for a processing entry and classify the risk level." />
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl -X POST http://localhost:8090/v1/admin/compliance/entries/entry-uuid/classify \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"autonomous_decisions": false,
|
||||||
|
"biometric_data": false,
|
||||||
|
"critical_infrastructure": false,
|
||||||
|
"sensitive_data": true,
|
||||||
|
"transparency_required": true
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"risk_level": "limited",
|
||||||
|
"score": 2,
|
||||||
|
"description": "Limited risk — transparency obligations apply. Users must be informed they are interacting with an AI system.",
|
||||||
|
"actions_required": [
|
||||||
|
"Display AI disclosure in the user interface",
|
||||||
|
"Document in GDPR Art. 30 registry",
|
||||||
|
"Annual review recommended"
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>Risk level mapping:</p>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Score</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Level</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ score: "5", level: "Forbidden", desc: "System must not be deployed. Example: social scoring, real-time biometric surveillance in public spaces.", color: "text-red-600" },
|
||||||
|
{ score: "3–4", level: "High", desc: "Strict conformity assessment required before deployment. DPIA mandatory.", color: "text-orange-600" },
|
||||||
|
{ score: "1–2", level: "Limited", desc: "Transparency obligations: users must be informed they interact with AI.", color: "text-amber-600" },
|
||||||
|
{ score: "0", level: "Minimal", desc: "Minimal risk. Voluntary code of conduct recommended.", color: "text-green-600" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.level} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.score}</td>
|
||||||
|
<td className={`px-4 py-2.5 font-semibold text-xs ${row.color}`}>{row.level}</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.desc}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="gdpr-rights">GDPR Subject Rights</h2>
|
||||||
|
|
||||||
|
<h3 id="access">Article 15 — Right of Access</h3>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/compliance/gdpr/access/{user_id}" description="Return all personal data stored for a specific user (prompt history, audit entries, pseudonymization mappings)." />
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/admin/compliance/gdpr/access/user-uuid \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Returns: audit entries, PII mapping references, user profile data`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 id="erasure">Article 17 — Right to Erasure</h3>
|
||||||
|
<ApiEndpoint method="POST" path="/v1/admin/compliance/gdpr/erase/{user_id}" description="Pseudonymize or delete all personal data for a user. Audit log metadata is retained but PII is scrubbed." />
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl -X POST http://localhost:8090/v1/admin/compliance/gdpr/erase/user-uuid \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"reason": "User request under GDPR Art. 17"}'`}
|
||||||
|
/>
|
||||||
|
<Callout type="warning" title="ClickHouse is append-only">
|
||||||
|
ClickHouse audit logs cannot be deleted. The erasure endpoint scrubs PII from prompt
|
||||||
|
content and pseudonymizes user identifiers, but the request metadata (token counts, cost,
|
||||||
|
timestamps) is retained for compliance reporting.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
web/src/pages/docs/api-reference/AdminFlagsPage.tsx
Normal file
98
web/src/pages/docs/api-reference/AdminFlagsPage.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ApiEndpoint } from "../components/ApiEndpoint";
|
||||||
|
|
||||||
|
export function AdminFlagsPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="admin-flags">Admin — Feature Flags</h1>
|
||||||
|
<p>
|
||||||
|
Feature flags control behavior at runtime without redeployment. Flags are stored in
|
||||||
|
PostgreSQL with an in-memory cache (updated every 30 seconds). If PostgreSQL is
|
||||||
|
unavailable, the in-memory defaults are used.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="Required role">
|
||||||
|
Feature flag management requires the <code>admin</code> role.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="list">List All Flags</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/flags" description="Return all feature flags and their current values." />
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/admin/flags \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{"name": "pii_detection", "enabled": true, "description": "Enable PII detection pipeline"},
|
||||||
|
{"name": "pii_pseudonymization","enabled": true, "description": "Store PII mappings in Redis"},
|
||||||
|
{"name": "audit_logging", "enabled": true, "description": "Write to ClickHouse audit log"},
|
||||||
|
{"name": "playground", "enabled": true, "description": "Public playground endpoint"},
|
||||||
|
{"name": "streaming", "enabled": true, "description": "SSE streaming support"},
|
||||||
|
{"name": "cost_tracking", "enabled": true, "description": "Track token costs per request"},
|
||||||
|
{"name": "circuit_breaker", "enabled": true, "description": "Provider circuit breaker"}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="get">Get a Flag</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/flags/{name}" description="Get the current value of a named flag." />
|
||||||
|
|
||||||
|
<h2 id="set">Set a Flag</h2>
|
||||||
|
<ApiEndpoint method="PUT" path="/v1/admin/flags/{name}" description="Enable or disable a feature flag." />
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Disable PII detection (e.g. for debugging)
|
||||||
|
curl -X PUT http://localhost:8090/v1/admin/flags/pii_detection \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"enabled": false}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="delete">Delete a Flag</h2>
|
||||||
|
<ApiEndpoint method="DELETE" path="/v1/admin/flags/{name}" description="Delete a feature flag (it reverts to the compiled-in default)." />
|
||||||
|
|
||||||
|
<h2 id="defaults">Default Flag Values</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Flag</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Default</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Effect when disabled</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ flag: "pii_detection", def: "true", effect: "Prompts forwarded without PII scanning" },
|
||||||
|
{ flag: "pii_pseudonymization", def: "true", effect: "PII detected but not stored in Redis" },
|
||||||
|
{ flag: "audit_logging", def: "true", effect: "Requests not written to ClickHouse" },
|
||||||
|
{ flag: "playground", def: "true", effect: "POST /playground/analyze returns 404" },
|
||||||
|
{ flag: "streaming", def: "true", effect: "SSE requests return 400" },
|
||||||
|
{ flag: "cost_tracking", def: "true", effect: "Token cost not computed or stored" },
|
||||||
|
{ flag: "circuit_breaker", def: "true", effect: "Failures not counted, no fallback" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.flag} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.flag}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs">
|
||||||
|
<span className="bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 px-1.5 py-0.5 rounded font-medium">
|
||||||
|
{row.def}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.effect}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Callout type="warning" title="Production safety">
|
||||||
|
Disabling <code>audit_logging</code> or <code>pii_detection</code> in production may
|
||||||
|
violate GDPR obligations and internal SLA requirements. Always coordinate with your DPO
|
||||||
|
before making changes.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
web/src/pages/docs/api-reference/AdminLogsPage.tsx
Normal file
133
web/src/pages/docs/api-reference/AdminLogsPage.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ApiEndpoint } from "../components/ApiEndpoint";
|
||||||
|
import { ParamTable } from "../components/ParamTable";
|
||||||
|
|
||||||
|
export function AdminLogsPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="admin-logs">Admin — Audit Logs & Costs</h1>
|
||||||
|
<p>
|
||||||
|
Query the immutable audit trail and cost breakdown for AI requests. All data is stored in
|
||||||
|
ClickHouse (append-only — no DELETE operations).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="audit-logs">Audit Logs</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/logs" description="Query audit log entries with optional filters." />
|
||||||
|
|
||||||
|
<ParamTable
|
||||||
|
title="Query Parameters"
|
||||||
|
params={[
|
||||||
|
{ name: "user_id", type: "string", required: false, description: "Filter by user UUID." },
|
||||||
|
{ name: "provider", type: "string", required: false, description: "Filter by provider: openai | anthropic | azure | mistral | ollama" },
|
||||||
|
{ name: "model", type: "string", required: false, description: "Filter by model used." },
|
||||||
|
{ name: "from", type: "string (ISO 8601)", required: false, description: "Start of time range, e.g. 2026-01-01T00:00:00Z" },
|
||||||
|
{ name: "to", type: "string (ISO 8601)", required: false, description: "End of time range." },
|
||||||
|
{ name: "has_pii", type: "boolean", required: false, description: "Filter entries where PII was detected." },
|
||||||
|
{ name: "limit", type: "integer", required: false, default: "50", description: "Max entries to return (max 1000)." },
|
||||||
|
{ name: "offset", type: "integer", required: false, default: "0", description: "Pagination offset." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl "http://localhost:8090/v1/admin/logs?from=2026-01-01T00:00:00Z&has_pii=true&limit=10" \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "log-uuid",
|
||||||
|
"tenant_id": "tenant-uuid",
|
||||||
|
"user_id": "user-uuid",
|
||||||
|
"user_email": "alice@acme.com",
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model_requested": "gpt-4o",
|
||||||
|
"model_used": "claude-3-5-sonnet-20241022",
|
||||||
|
"prompt_tokens": 128,
|
||||||
|
"completion_tokens": 345,
|
||||||
|
"total_tokens": 473,
|
||||||
|
"cost_usd": 0.003412,
|
||||||
|
"latency_ms": 1423,
|
||||||
|
"pii_detected": true,
|
||||||
|
"pii_entities": ["PERSON", "EMAIL_ADDRESS"],
|
||||||
|
"policy_matched": "Legal → Anthropic",
|
||||||
|
"status_code": 200,
|
||||||
|
"timestamp": "2026-01-15T14:32:11Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 142,
|
||||||
|
"limit": 10,
|
||||||
|
"offset": 0
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="info" title="Audit-of-the-audit">
|
||||||
|
All accesses to audit logs are themselves logged. This satisfies the "audit-of-the-audit"
|
||||||
|
requirement for sensitive compliance use cases.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="costs">Cost Breakdown</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/costs" description="Aggregate cost breakdown by provider, model, or department." />
|
||||||
|
|
||||||
|
<ParamTable
|
||||||
|
title="Query Parameters"
|
||||||
|
params={[
|
||||||
|
{ name: "group_by", type: "string", required: false, default: "provider", description: "Grouping dimension: provider | model | department | user" },
|
||||||
|
{ name: "from", type: "string (ISO 8601)", required: false, description: "Start of billing period." },
|
||||||
|
{ name: "to", type: "string (ISO 8601)", required: false, description: "End of billing period." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl "http://localhost:8090/v1/admin/costs?group_by=provider&from=2026-01-01T00:00:00Z" \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"key": "openai",
|
||||||
|
"request_count": 1423,
|
||||||
|
"total_tokens": 2840000,
|
||||||
|
"prompt_tokens": 1200000,
|
||||||
|
"completion_tokens": 1640000,
|
||||||
|
"total_cost_usd": 28.40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "anthropic",
|
||||||
|
"request_count": 231,
|
||||||
|
"total_tokens": 462000,
|
||||||
|
"prompt_tokens": 230000,
|
||||||
|
"completion_tokens": 232000,
|
||||||
|
"total_cost_usd": 6.93
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"period_from": "2026-01-01T00:00:00Z",
|
||||||
|
"period_to": "2026-01-31T23:59:59Z"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="rate-limits">Rate Limit Overrides</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/rate-limits" description="List per-tenant rate limit overrides." />
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/rate-limits/{tenant_id}" description="Get rate limit configuration for a specific tenant." />
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Get rate limit for a tenant
|
||||||
|
curl http://localhost:8090/v1/admin/rate-limits/tenant-uuid \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"tenant_id": "tenant-uuid",
|
||||||
|
"rpm": 500, # requests per minute
|
||||||
|
"tpm": 50000, # tokens per minute
|
||||||
|
"daily_token_limit": 1000000
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
124
web/src/pages/docs/api-reference/AdminPoliciesPage.tsx
Normal file
124
web/src/pages/docs/api-reference/AdminPoliciesPage.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ApiEndpoint } from "../components/ApiEndpoint";
|
||||||
|
import { ParamTable } from "../components/ParamTable";
|
||||||
|
|
||||||
|
export function AdminPoliciesPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="admin-policies">Admin — Routing Policies</h1>
|
||||||
|
<p>
|
||||||
|
Routing policies (rules) define how AI requests are dispatched to providers. Rules are
|
||||||
|
evaluated in ascending priority order; the first matching rule wins.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="Required role">
|
||||||
|
All <code>/v1/admin/policies</code> endpoints require the <code>admin</code> or{" "}
|
||||||
|
<code>manager</code> role.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="list">List Policies</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/policies" description="Return all routing rules for the current tenant, sorted by priority." />
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/admin/policies \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"name": "Legal → Anthropic",
|
||||||
|
"priority": 10,
|
||||||
|
"provider": "anthropic",
|
||||||
|
"target_model": "claude-3-5-sonnet-20241022",
|
||||||
|
"fallback_providers": ["openai"],
|
||||||
|
"conditions": [
|
||||||
|
{"field": "user.department", "operator": "eq", "value": "legal"}
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"created_at": "2026-01-15T10:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="create">Create Policy</h2>
|
||||||
|
<ApiEndpoint method="POST" path="/v1/admin/policies" description="Create a new routing rule." />
|
||||||
|
<ParamTable
|
||||||
|
params={[
|
||||||
|
{ name: "name", type: "string", required: true, description: "Human-readable rule name." },
|
||||||
|
{ name: "priority", type: "integer", required: true, description: "Evaluation order (lower = first). Must be unique per tenant." },
|
||||||
|
{ name: "provider", type: "string", required: true, description: "Target provider: openai | anthropic | azure | mistral | ollama" },
|
||||||
|
{ name: "target_model", type: "string", required: false, description: "Override the model requested by the client." },
|
||||||
|
{ name: "fallback_providers", type: "[]string", required: false, description: "Ordered list of fallback providers if the primary fails or circuit is open." },
|
||||||
|
{ name: "conditions", type: "[]Condition", required: false, description: "Array of conditions (all must match, AND-joined). Empty = catch-all." },
|
||||||
|
{ name: "enabled", type: "boolean", required: false, default: "true", description: "Whether the rule is active." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 id="conditions">Condition Fields</h3>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Field</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Operators</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Value example</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ field: "user.role", ops: "eq, neq, in, nin", ex: '"admin"' },
|
||||||
|
{ field: "user.department", ops: "eq, neq, in, nin, contains", ex: '"legal"' },
|
||||||
|
{ field: "request.model", ops: "eq, neq, in, nin", ex: '"gpt-4o"' },
|
||||||
|
{ field: "request.sensitivity", ops: "eq, neq, gte, lte", ex: '"high"' },
|
||||||
|
{ field: "request.use_case", ops: "eq, neq, contains, matches", ex: '"code_review"' },
|
||||||
|
{ field: "request.token_estimate", ops: "gte, lte", ex: '"4000"' },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.field} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.field}</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.ops}</td>
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs text-blue-600 dark:text-blue-400">{row.ex}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl -X POST http://localhost:8090/v1/admin/policies \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"name": "Legal → Anthropic",
|
||||||
|
"priority": 10,
|
||||||
|
"provider": "anthropic",
|
||||||
|
"target_model": "claude-3-5-sonnet-20241022",
|
||||||
|
"fallback_providers": ["openai"],
|
||||||
|
"conditions": [
|
||||||
|
{"field": "user.department", "operator": "eq", "value": "legal"}
|
||||||
|
]
|
||||||
|
}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="update">Update Policy</h2>
|
||||||
|
<ApiEndpoint method="PUT" path="/v1/admin/policies/{id}" description="Update an existing routing rule (full replacement)." />
|
||||||
|
|
||||||
|
<h2 id="delete">Delete Policy</h2>
|
||||||
|
<ApiEndpoint method="DELETE" path="/v1/admin/policies/{id}" description="Permanently delete a routing rule." />
|
||||||
|
|
||||||
|
<h2 id="seed">Seed from Template</h2>
|
||||||
|
<ApiEndpoint method="POST" path="/v1/admin/policies/seed/{template}" description="Create a pre-built set of routing rules from a template." />
|
||||||
|
<p>Available templates:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>default</code> — Single catch-all rule routing to OpenAI</li>
|
||||||
|
<li><code>rbac</code> — Rules per RBAC role with model restrictions</li>
|
||||||
|
<li><code>department</code> — Rules per department (legal, hr, engineering, finance)</li>
|
||||||
|
<li><code>cost-optimized</code> — Routes to cheaper models for simple queries</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
web/src/pages/docs/api-reference/AdminUsersPage.tsx
Normal file
91
web/src/pages/docs/api-reference/AdminUsersPage.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ApiEndpoint } from "../components/ApiEndpoint";
|
||||||
|
import { ParamTable } from "../components/ParamTable";
|
||||||
|
|
||||||
|
export function AdminUsersPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="admin-users">Admin — Users</h1>
|
||||||
|
<p>
|
||||||
|
Manage users within a tenant. Users are synchronized from Keycloak but can have additional
|
||||||
|
metadata (department, cost overrides) managed through the admin API.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="Required role">
|
||||||
|
Requires <code>admin</code> role. Managers can read users but cannot create or delete.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="list">List Users</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/users" description="Return all users for the current tenant." />
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/admin/users \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "user-uuid",
|
||||||
|
"email": "alice@acme.com",
|
||||||
|
"name": "Alice Martin",
|
||||||
|
"role": "user",
|
||||||
|
"department": "legal",
|
||||||
|
"tenant_id": "tenant-uuid",
|
||||||
|
"created_at": "2026-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="create">Create User</h2>
|
||||||
|
<ApiEndpoint method="POST" path="/v1/admin/users" description="Create or sync a user in the tenant database." />
|
||||||
|
<ParamTable
|
||||||
|
params={[
|
||||||
|
{ name: "email", type: "string", required: true, description: "User email (must match Keycloak account)." },
|
||||||
|
{ name: "name", type: "string", required: true, description: "Display name." },
|
||||||
|
{ name: "role", type: "string", required: true, description: "RBAC role: admin | manager | user | auditor" },
|
||||||
|
{ name: "department", type: "string", required: false, description: "Department name — used in routing rule conditions." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="get">Get User</h2>
|
||||||
|
<ApiEndpoint method="GET" path="/v1/admin/users/{id}" description="Return a single user by ID." />
|
||||||
|
|
||||||
|
<h2 id="update">Update User</h2>
|
||||||
|
<ApiEndpoint method="PUT" path="/v1/admin/users/{id}" description="Update user metadata (role, department)." />
|
||||||
|
|
||||||
|
<h2 id="roles">RBAC Role Reference</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Role</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Inference</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Models</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Admin API</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Audit/Compliance</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ role: "admin", infer: "Yes", models: "All", admin: "Full", audit: "Read" },
|
||||||
|
{ role: "manager", infer: "Yes", models: "All", admin: "Read-Write policies/users", audit: "Read" },
|
||||||
|
{ role: "user", infer: "Yes", models: "rbac.user_allowed_models only", admin: "None", audit: "None" },
|
||||||
|
{ role: "auditor", infer: "No", models: "—", admin: "None", audit: "Full read + export" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.role} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs font-semibold">{row.role}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs">{row.infer}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.models}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.admin}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.audit}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
web/src/pages/docs/api-reference/AuthenticationPage.tsx
Normal file
133
web/src/pages/docs/api-reference/AuthenticationPage.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function AuthenticationPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="authentication">Authentication</h1>
|
||||||
|
<p>
|
||||||
|
All <code>/v1/*</code> endpoints require a Bearer JWT in the{" "}
|
||||||
|
<code>Authorization</code> header. Veylant IA validates the token against Keycloak (OIDC)
|
||||||
|
or uses a mock verifier in development mode.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="bearer-token">Bearer Token</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/chat/completions \\
|
||||||
|
-H "Authorization: Bearer <your-access-token>" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hi"}]}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="dev-mode">Development Mode</h2>
|
||||||
|
<Callout type="info" title="Development mode auth bypass">
|
||||||
|
When <code>server.env=development</code> and Keycloak is unreachable, the proxy uses a{" "}
|
||||||
|
<code>MockVerifier</code>. Any non-empty Bearer token is accepted. The authenticated user
|
||||||
|
is injected as <code>admin@veylant.dev</code> with <code>admin</code> role and tenant ID{" "}
|
||||||
|
<code>dev-tenant</code>.
|
||||||
|
</Callout>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Any string works as the token in dev mode
|
||||||
|
curl http://localhost:8090/v1/chat/completions \\
|
||||||
|
-H "Authorization: Bearer dev-token" \\
|
||||||
|
...`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="keycloak-flow">Production: Keycloak OIDC Flow</h2>
|
||||||
|
<p>In production, clients obtain a token via the standard OIDC Authorization Code flow:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Redirect user to Keycloak login page</li>
|
||||||
|
<li>User authenticates; Keycloak redirects back with an authorization code</li>
|
||||||
|
<li>Exchange code for tokens at the token endpoint</li>
|
||||||
|
<li>Use the <code>access_token</code> as the Bearer token</li>
|
||||||
|
</ol>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Token endpoint (replace values)
|
||||||
|
curl -X POST \\
|
||||||
|
http://localhost:8080/realms/veylant/protocol/openid-connect/token \\
|
||||||
|
-d "grant_type=password" \\
|
||||||
|
-d "client_id=veylant-proxy" \\
|
||||||
|
-d "username=admin@veylant.dev" \\
|
||||||
|
-d "password=admin123"
|
||||||
|
|
||||||
|
# Response includes:
|
||||||
|
{
|
||||||
|
"access_token": "eyJhbGci...",
|
||||||
|
"expires_in": 300,
|
||||||
|
"refresh_token": "eyJhbGci...",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="jwt-claims">JWT Claims</h2>
|
||||||
|
<p>The proxy extracts the following claims from the JWT:</p>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Claim</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Source</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ claim: "sub", source: "Standard JWT", desc: "User ID (UUID)" },
|
||||||
|
{ claim: "email", source: "Standard JWT", desc: "User email" },
|
||||||
|
{ claim: "realm_access.roles", source: "Keycloak extension", desc: "RBAC roles: admin, manager, user, auditor" },
|
||||||
|
{ claim: "veylant_tenant_id", source: "Keycloak mapper", desc: "Tenant UUID" },
|
||||||
|
{ claim: "department", source: "Keycloak user attribute", desc: "Department name for routing rules" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.claim} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.claim}</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.source}</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.desc}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="test-users">Pre-configured Test Users</h2>
|
||||||
|
<p>The Keycloak realm export includes these users for testing:</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Admin user (full access)
|
||||||
|
username: admin@veylant.dev
|
||||||
|
password: admin123
|
||||||
|
roles: admin
|
||||||
|
|
||||||
|
# Regular user (restricted to allowed models)
|
||||||
|
username: user@veylant.dev
|
||||||
|
password: user123
|
||||||
|
roles: user`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="error-responses">Auth Error Responses</h2>
|
||||||
|
<p>Authentication errors always return OpenAI-format JSON:</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`// 401 — Missing or invalid token
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "authentication_error",
|
||||||
|
"message": "invalid or missing authorization token",
|
||||||
|
"code": "invalid_api_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 403 — Valid token, insufficient role
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "permission_error",
|
||||||
|
"message": "role 'user' does not have access to model 'gpt-4o'. Allowed: gpt-4o-mini",
|
||||||
|
"code": "permission_denied"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
194
web/src/pages/docs/api-reference/ChatCompletionsPage.tsx
Normal file
194
web/src/pages/docs/api-reference/ChatCompletionsPage.tsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ApiEndpoint } from "../components/ApiEndpoint";
|
||||||
|
import { ParamTable } from "../components/ParamTable";
|
||||||
|
|
||||||
|
export function ChatCompletionsPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="chat-completions">Chat Completions</h1>
|
||||||
|
<p>
|
||||||
|
The primary inference endpoint. Fully compatible with the OpenAI Chat Completions API —
|
||||||
|
switch your <code>base_url</code> to Veylant IA and all existing SDK calls work unchanged.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ApiEndpoint
|
||||||
|
method="POST"
|
||||||
|
path="/v1/chat/completions"
|
||||||
|
description="Create an AI chat completion with automatic PII anonymization, routing, and audit logging."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="info" title="OpenAI-compatible">
|
||||||
|
This endpoint is a superset of the OpenAI Chat Completions API. All standard OpenAI
|
||||||
|
parameters are supported and forwarded to the upstream provider. Veylant IA adds governance
|
||||||
|
on top without changing the request/response schema.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="request">Request Body</h2>
|
||||||
|
<ParamTable
|
||||||
|
params={[
|
||||||
|
{ name: "model", type: "string", required: true, description: "Model ID to use (e.g. gpt-4o, claude-3-5-sonnet-20241022). The routing engine may override this." },
|
||||||
|
{ name: "messages", type: "array", required: true, description: "List of messages in the conversation. Each message has role (system|user|assistant) and content." },
|
||||||
|
{ name: "stream", type: "boolean", required: false, default: "false", description: "If true, responses are streamed as Server-Sent Events (SSE). Compatible with EventSource and the OpenAI SDK streaming API." },
|
||||||
|
{ name: "temperature", type: "float", required: false, default: "1.0", description: "Sampling temperature (0–2). Forwarded to the provider." },
|
||||||
|
{ name: "max_tokens", type: "integer", required: false, description: "Maximum tokens to generate. Forwarded to the provider." },
|
||||||
|
{ name: "top_p", type: "float", required: false, description: "Nucleus sampling. Forwarded to the provider." },
|
||||||
|
{ name: "n", type: "integer", required: false, default: "1", description: "Number of completions. Note: PII anonymization applies to each choice." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="example-request">Example Request</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/chat/completions \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are a helpful assistant."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Summarize the contract for John Smith (john@acme.com)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"temperature": 0.7
|
||||||
|
}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="pii-anonymization">PII Anonymization in Action</h2>
|
||||||
|
<p>
|
||||||
|
Before the request is forwarded to the LLM, the PII service scans all message content.
|
||||||
|
Detected entities are anonymized in the prompt. The audit log records what was found.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`// Original user message:
|
||||||
|
"Summarize the contract for John Smith (john@acme.com)."
|
||||||
|
|
||||||
|
// After PII anonymization (what the LLM sees):
|
||||||
|
"Summarize the contract for [PERSON] ([EMAIL_ADDRESS])."
|
||||||
|
|
||||||
|
// Audit log records:
|
||||||
|
{
|
||||||
|
"pii_entities": [
|
||||||
|
{"type": "PERSON", "original": "John Smith", "start": 31, "end": 41},
|
||||||
|
{"type": "EMAIL_ADDRESS", "original": "john@acme.com", "start": 43, "end": 56}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="response">Response</h2>
|
||||||
|
<p>Responses are identical to the OpenAI API format:</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`{
|
||||||
|
"id": "chatcmpl-veylant-8f3k2j",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": 1735000000,
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Here is a summary of the contract..."
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 28,
|
||||||
|
"completion_tokens": 142,
|
||||||
|
"total_tokens": 170
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="streaming">Streaming (SSE)</h2>
|
||||||
|
<p>
|
||||||
|
Set <code>"stream": true</code> to receive chunks via Server-Sent Events. PII
|
||||||
|
anonymization applies to the <strong>request</strong> before it's sent upstream — not to
|
||||||
|
the streamed response. This keeps streaming latency minimal.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/chat/completions \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [{"role": "user", "content": "Tell me a story"}],
|
||||||
|
"stream": true
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Response (streaming chunks):
|
||||||
|
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Once"},"index":0}]}
|
||||||
|
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"delta":{"content":" upon"},"index":0}]}
|
||||||
|
data: [DONE]`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="python"
|
||||||
|
code={`from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(base_url="http://localhost:8090/v1", api_key="dev-token")
|
||||||
|
|
||||||
|
stream = client.chat.completions.create(
|
||||||
|
model="gpt-4o",
|
||||||
|
messages=[{"role": "user", "content": "Tell me a story"}],
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
for chunk in stream:
|
||||||
|
print(chunk.choices[0].delta.content or "", end="", flush=True)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="errors">Error Responses</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`// 429 — Rate limit exceeded
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "rate_limit_error",
|
||||||
|
"message": "rate limit exceeded, retry after 1 second",
|
||||||
|
"code": "rate_limit_exceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Headers:
|
||||||
|
// Retry-After: 1
|
||||||
|
|
||||||
|
// 503 — Provider unavailable (circuit breaker open)
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "provider_error",
|
||||||
|
"message": "all providers unavailable",
|
||||||
|
"code": "service_unavailable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 400 — Bad request
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"message": "messages field is required",
|
||||||
|
"code": "invalid_request"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="routing-override">How Routing Affects the Model</h2>
|
||||||
|
<p>
|
||||||
|
The routing engine evaluates rules against the request context. A matched rule can override
|
||||||
|
the requested model or select a different provider. The <code>model</code> in the response
|
||||||
|
reflects the model actually used (which may differ from what was requested).
|
||||||
|
</p>
|
||||||
|
<Callout type="tip">
|
||||||
|
Use the <code>GET /v1/admin/logs</code> endpoint to see <code>model_requested</code> vs{" "}
|
||||||
|
<code>model_used</code> in the audit trail.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
136
web/src/pages/docs/api-reference/PiiAnalysisPage.tsx
Normal file
136
web/src/pages/docs/api-reference/PiiAnalysisPage.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ApiEndpoint } from "../components/ApiEndpoint";
|
||||||
|
import { ParamTable } from "../components/ParamTable";
|
||||||
|
|
||||||
|
export function PiiAnalysisPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="pii-analysis">PII Analysis</h1>
|
||||||
|
<p>
|
||||||
|
Analyze text for PII on demand. This endpoint exposes the same 3-layer detection pipeline
|
||||||
|
used internally for prompt anonymization — useful for testing policies, building pre-flight
|
||||||
|
checks, or auditing content.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ApiEndpoint
|
||||||
|
method="POST"
|
||||||
|
path="/v1/pii/analyze"
|
||||||
|
description="Detect and optionally anonymize PII entities in a text string."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="request">Request Body</h2>
|
||||||
|
<ParamTable
|
||||||
|
params={[
|
||||||
|
{ name: "text", type: "string", required: true, description: "The text to analyze for PII entities." },
|
||||||
|
{ name: "anonymize", type: "boolean", required: false, default: "false", description: "If true, return an anonymized version of the text with PII replaced." },
|
||||||
|
{ name: "store_mapping", type: "boolean", required: false, default: "false", description: "If true and anonymize=true, store the original→synthetic mapping in Redis (AES-256-GCM encrypted, TTL-based). Enables de-pseudonymization." },
|
||||||
|
{ name: "language", type: "string", required: false, default: "fr", description: "Language hint for NER (fr | en). Affects which spaCy model is used." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="example">Example</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/pii/analyze \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"text": "Call John Smith at +33 6 12 34 56 78 or john.smith@acme.com. His IBAN is FR76 3000 6000 0112 3456 7890 189.",
|
||||||
|
"anonymize": true
|
||||||
|
}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="response">Response</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`{
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"type": "PERSON",
|
||||||
|
"text": "John Smith",
|
||||||
|
"start": 5,
|
||||||
|
"end": 15,
|
||||||
|
"score": 0.98,
|
||||||
|
"layer": "ner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PHONE_NUMBER",
|
||||||
|
"text": "+33 6 12 34 56 78",
|
||||||
|
"start": 19,
|
||||||
|
"end": 37,
|
||||||
|
"score": 1.0,
|
||||||
|
"layer": "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "EMAIL_ADDRESS",
|
||||||
|
"text": "john.smith@acme.com",
|
||||||
|
"start": 41,
|
||||||
|
"end": 60,
|
||||||
|
"score": 1.0,
|
||||||
|
"layer": "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "IBAN_CODE",
|
||||||
|
"text": "FR76 3000 6000 0112 3456 7890 189",
|
||||||
|
"start": 73,
|
||||||
|
"end": 106,
|
||||||
|
"score": 1.0,
|
||||||
|
"layer": "regex"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anonymized_text": "Call [PERSON] at [PHONE_NUMBER] or [EMAIL_ADDRESS]. His IBAN is [IBAN_CODE].",
|
||||||
|
"entity_count": 4,
|
||||||
|
"latency_ms": 23
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="entity-types">Supported Entity Types</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Type</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Layer</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Examples</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ type: "EMAIL_ADDRESS", layer: "regex", ex: "user@example.com" },
|
||||||
|
{ type: "PHONE_NUMBER", layer: "regex", ex: "+33 6 12 34 56 78, 06.12.34.56.78" },
|
||||||
|
{ type: "IBAN_CODE", layer: "regex", ex: "FR76 3000 6000 0112 3456 7890 189" },
|
||||||
|
{ type: "CREDIT_CARD", layer: "regex", ex: "4532 1234 5678 9012" },
|
||||||
|
{ type: "SSN", layer: "regex", ex: "1 85 12 75 108 112 48" },
|
||||||
|
{ type: "PERSON", layer: "ner", ex: "John Smith, Marie Dupont" },
|
||||||
|
{ type: "LOCATION", layer: "ner", ex: "Paris, 75001, rue de Rivoli" },
|
||||||
|
{ type: "ORGANIZATION", layer: "ner", ex: "Acme Corp, Banque de France" },
|
||||||
|
{ type: "DATE_TIME", layer: "ner", ex: "12/03/1985, le 3 mars" },
|
||||||
|
{ type: "NRP", layer: "ner", ex: "Nationality, religion, political party" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.type} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.type}</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${row.layer === "regex" ? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300" : "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300"}`}>
|
||||||
|
{row.layer}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.ex}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="playground">Interactive Playground</h2>
|
||||||
|
<Callout type="tip" title="Public playground">
|
||||||
|
Try PII detection without authentication at{" "}
|
||||||
|
<a href="http://localhost:8090/playground" target="_blank" rel="noopener noreferrer">
|
||||||
|
http://localhost:8090/playground
|
||||||
|
</a>
|
||||||
|
. The playground endpoint (<code>POST /playground/analyze</code>) is IP-rate-limited to
|
||||||
|
20 requests/minute and requires no Bearer token.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
web/src/pages/docs/components/ApiEndpoint.tsx
Normal file
38
web/src/pages/docs/components/ApiEndpoint.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
|
||||||
|
interface ApiEndpointProps {
|
||||||
|
method: HttpMethod;
|
||||||
|
path: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodColors: Record<HttpMethod, string> = {
|
||||||
|
GET: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
|
||||||
|
POST: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
|
||||||
|
PUT: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
|
||||||
|
PATCH: "bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300",
|
||||||
|
DELETE: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ApiEndpoint({ method, path, description }: ApiEndpointProps) {
|
||||||
|
return (
|
||||||
|
<div className="my-4 rounded-lg border bg-card overflow-hidden">
|
||||||
|
<div className="flex items-center gap-3 px-4 py-3">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-xs font-bold px-2 py-0.5 rounded font-mono uppercase tracking-wide",
|
||||||
|
methodColors[method]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{method}
|
||||||
|
</span>
|
||||||
|
<code className="text-sm font-mono text-foreground">{path}</code>
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<div className="px-4 pb-3 text-sm text-muted-foreground border-t">{description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
web/src/pages/docs/components/Callout.tsx
Normal file
64
web/src/pages/docs/components/Callout.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Info, AlertTriangle, XCircle, Lightbulb } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type CalloutType = "info" | "warning" | "danger" | "tip";
|
||||||
|
|
||||||
|
interface CalloutProps {
|
||||||
|
type?: CalloutType;
|
||||||
|
title?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configs: Record<
|
||||||
|
CalloutType,
|
||||||
|
{ icon: React.ElementType; borderColor: string; bgColor: string; titleColor: string; iconColor: string }
|
||||||
|
> = {
|
||||||
|
info: {
|
||||||
|
icon: Info,
|
||||||
|
borderColor: "border-blue-400",
|
||||||
|
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
||||||
|
titleColor: "text-blue-800 dark:text-blue-200",
|
||||||
|
iconColor: "text-blue-500",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
icon: AlertTriangle,
|
||||||
|
borderColor: "border-amber-400",
|
||||||
|
bgColor: "bg-amber-50 dark:bg-amber-950/30",
|
||||||
|
titleColor: "text-amber-800 dark:text-amber-200",
|
||||||
|
iconColor: "text-amber-500",
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
icon: XCircle,
|
||||||
|
borderColor: "border-red-400",
|
||||||
|
bgColor: "bg-red-50 dark:bg-red-950/30",
|
||||||
|
titleColor: "text-red-800 dark:text-red-200",
|
||||||
|
iconColor: "text-red-500",
|
||||||
|
},
|
||||||
|
tip: {
|
||||||
|
icon: Lightbulb,
|
||||||
|
borderColor: "border-green-400",
|
||||||
|
bgColor: "bg-green-50 dark:bg-green-950/30",
|
||||||
|
titleColor: "text-green-800 dark:text-green-200",
|
||||||
|
iconColor: "text-green-500",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Callout({ type = "info", title, children }: CalloutProps) {
|
||||||
|
const { icon: Icon, borderColor, bgColor, titleColor, iconColor } = configs[type];
|
||||||
|
const defaultTitles: Record<CalloutType, string> = {
|
||||||
|
info: "Note",
|
||||||
|
warning: "Warning",
|
||||||
|
danger: "Danger",
|
||||||
|
tip: "Tip",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex gap-3 rounded-lg border-l-4 p-4 my-4", borderColor, bgColor)}>
|
||||||
|
<Icon className={cn("h-5 w-5 mt-0.5 shrink-0", iconColor)} />
|
||||||
|
<div className="text-sm leading-relaxed">
|
||||||
|
<p className={cn("font-semibold mb-1", titleColor)}>{title ?? defaultTitles[type]}</p>
|
||||||
|
<div className="text-foreground/80">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
web/src/pages/docs/components/CodeBlock.tsx
Normal file
48
web/src/pages/docs/components/CodeBlock.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Copy, Check } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
code: string;
|
||||||
|
language?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlock({ code, language = "bash", className }: CodeBlockProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await navigator.clipboard.writeText(code.trim());
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative rounded-lg overflow-hidden my-4 group not-prose", className)}>
|
||||||
|
{/* Header bar */}
|
||||||
|
<div className="flex items-center justify-between bg-zinc-800 px-4 py-2">
|
||||||
|
<span className="text-xs text-zinc-400 font-mono uppercase tracking-wide">{language}</span>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="flex items-center gap-1.5 text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-3.5 w-3.5 text-green-400" />
|
||||||
|
<span className="text-green-400">Copied!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
<span>Copy</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Code body */}
|
||||||
|
<pre className="bg-zinc-900 text-zinc-100 overflow-x-auto p-4 text-sm leading-relaxed">
|
||||||
|
<code className="font-mono">{code.trim()}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
web/src/pages/docs/components/ParamTable.tsx
Normal file
56
web/src/pages/docs/components/ParamTable.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface Param {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
required?: boolean;
|
||||||
|
description: string;
|
||||||
|
default?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParamTableProps {
|
||||||
|
params: Param[];
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParamTable({ params, title = "Parameters" }: ParamTableProps) {
|
||||||
|
return (
|
||||||
|
<div className="my-4">
|
||||||
|
{title && <h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">{title}</h4>}
|
||||||
|
<div className="rounded-lg border overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Name</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Type</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Required</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{params.map((p, i) => (
|
||||||
|
<tr key={p.name} className={cn("border-b last:border-0", i % 2 === 0 ? "" : "bg-muted/20")}>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<code className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">{p.name}</code>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<span className="text-xs font-mono text-blue-600 dark:text-blue-400">{p.type}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
{p.required ? (
|
||||||
|
<span className="text-xs text-red-600 dark:text-red-400 font-medium">required</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
optional{p.default ? ` (${p.default})` : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground">{p.description}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
web/src/pages/docs/components/TableOfContents.tsx
Normal file
94
web/src/pages/docs/components/TableOfContents.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
interface TocItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTocItems(): TocItem[] {
|
||||||
|
const [items, setItems] = useState<TocItem[]>([]);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Small delay to allow page to render
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const headings = Array.from(document.querySelectorAll("[data-doc-content] h2, [data-doc-content] h3"));
|
||||||
|
const tocItems: TocItem[] = headings.map((el) => ({
|
||||||
|
id: el.id,
|
||||||
|
title: el.textContent ?? "",
|
||||||
|
level: el.tagName === "H2" ? 2 : 3,
|
||||||
|
}));
|
||||||
|
setItems(tocItems);
|
||||||
|
}, 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useActiveId(ids: string[]): string {
|
||||||
|
const [activeId, setActiveId] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const visible = entries.filter((e) => e.isIntersecting);
|
||||||
|
if (visible.length > 0) {
|
||||||
|
setActiveId(visible[0].target.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: "0% 0% -70% 0%", threshold: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
ids.forEach((id) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) observer.observe(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [ids]);
|
||||||
|
|
||||||
|
return activeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableOfContents() {
|
||||||
|
const items = useTocItems();
|
||||||
|
const activeId = useActiveId(items.map((i) => i.id));
|
||||||
|
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="sticky top-20">
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||||
|
On this page
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.id}>
|
||||||
|
<a
|
||||||
|
href={`#${item.id}`}
|
||||||
|
className={cn(
|
||||||
|
"block text-sm leading-5 py-0.5 transition-colors",
|
||||||
|
item.level === 3 && "pl-4",
|
||||||
|
activeId === item.id
|
||||||
|
? "text-primary font-medium"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById(item.id)?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
web/src/pages/docs/deployment/BlueGreenPage.tsx
Normal file
107
web/src/pages/docs/deployment/BlueGreenPage.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function BlueGreenPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="blue-green">Blue/Green Deployment</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA supports zero-downtime deployments via blue/green traffic switching with Istio.
|
||||||
|
Two Helm releases (<code>veylant-proxy-blue</code> and <code>veylant-proxy-green</code>)
|
||||||
|
run simultaneously; traffic is switched via an Istio VirtualService patch.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Rollback time">
|
||||||
|
Traffic rollback is under <strong>5 seconds</strong> — just patch the VirtualService weight.
|
||||||
|
No pod restarts required.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="architecture">Architecture</h2>
|
||||||
|
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-relaxed overflow-x-auto">
|
||||||
|
<pre>{`Internet
|
||||||
|
│
|
||||||
|
Istio Gateway
|
||||||
|
│
|
||||||
|
VirtualService (veylant-proxy)
|
||||||
|
├── weight: 100 → Service: veylant-proxy-blue (ACTIVE)
|
||||||
|
└── weight: 0 → Service: veylant-proxy-green (STANDBY)
|
||||||
|
|
||||||
|
Helm releases:
|
||||||
|
veylant-proxy-blue: 3 replicas, image: 1.0.0 ← current production
|
||||||
|
veylant-proxy-green: 3 replicas, image: 1.1.0 ← new version (deployed but no traffic)`}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="deploy-new-version">Deploying a New Version</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Step 1: Deploy to the inactive slot (green)
|
||||||
|
make deploy-green IMAGE_TAG=1.1.0
|
||||||
|
|
||||||
|
# Step 2: Verify green is healthy
|
||||||
|
kubectl rollout status deployment/veylant-proxy-green -n veylant
|
||||||
|
curl http://green.internal.veylant.local/healthz
|
||||||
|
|
||||||
|
# Step 3: Switch traffic (blue → green)
|
||||||
|
kubectl patch virtualservice veylant-proxy -n veylant --type=json -p '[
|
||||||
|
{"op":"replace","path":"/spec/http/0/route/0/weight","value":0},
|
||||||
|
{"op":"replace","path":"/spec/http/0/route/1/weight","value":100}
|
||||||
|
]'
|
||||||
|
|
||||||
|
# Step 4: Verify production traffic on green
|
||||||
|
kubectl logs -l app=veylant-proxy,slot=green -n veylant --tail=20`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="rollback">Rollback</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Instant rollback — switch traffic back to blue
|
||||||
|
make deploy-rollback ACTIVE_SLOT=blue
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
kubectl patch virtualservice veylant-proxy -n veylant --type=json -p '[
|
||||||
|
{"op":"replace","path":"/spec/http/0/route/0/weight","value":100},
|
||||||
|
{"op":"replace","path":"/spec/http/0/route/1/weight","value":0}
|
||||||
|
]'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="make-commands">Make Commands</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Command</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ cmd: "make deploy-blue IMAGE_TAG=X.Y.Z", action: "Deploy IMAGE_TAG to blue slot (Helm upgrade)" },
|
||||||
|
{ cmd: "make deploy-green IMAGE_TAG=X.Y.Z", action: "Deploy IMAGE_TAG to green slot (Helm upgrade)" },
|
||||||
|
{ cmd: "make deploy-rollback ACTIVE_SLOT=blue", action: "Switch 100% traffic to blue slot" },
|
||||||
|
{ cmd: "make deploy-rollback ACTIVE_SLOT=green", action: "Switch 100% traffic to green slot" },
|
||||||
|
{ cmd: "make helm-dry-run", action: "Render Helm templates without deploying" },
|
||||||
|
{ cmd: "make helm-deploy IMAGE_TAG=X.Y.Z", action: "Deploy to staging (requires KUBECONFIG)" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.cmd} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.cmd}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.action}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="release-pipeline">Release Pipeline</h2>
|
||||||
|
<p>
|
||||||
|
Tagging a version (<code>v*</code>) triggers the GitHub Actions release pipeline:
|
||||||
|
</p>
|
||||||
|
<ol>
|
||||||
|
<li>Multi-arch Docker build (amd64/arm64) → pushed to GHCR</li>
|
||||||
|
<li>Helm chart packaged as OCI artifact → pushed to GHCR</li>
|
||||||
|
<li>GitHub Release created with CHANGELOG.md notes extracted automatically</li>
|
||||||
|
<li>Trivy image scan (CRITICAL/HIGH blocking)</li>
|
||||||
|
<li>gitleaks secret detection</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
web/src/pages/docs/deployment/DockerPage.tsx
Normal file
71
web/src/pages/docs/deployment/DockerPage.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function DockerPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="docker-deployment">Docker Compose Deployment</h1>
|
||||||
|
<p>
|
||||||
|
For small to medium deployments (single server, staging), Docker Compose is the recommended
|
||||||
|
approach. The production configuration uses the same services as local development with
|
||||||
|
hardened settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="production-config">Production Configuration</h2>
|
||||||
|
<Callout type="warning" title="Before production deployment">
|
||||||
|
Ensure you have set: <code>server.env=production</code>, a strong <code>crypto.key</code>,
|
||||||
|
TLS certificates for all services, PostgreSQL with TLS, and proper secrets management
|
||||||
|
(HashiCorp Vault recommended).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Production environment variables (set via secrets manager, not .env)
|
||||||
|
VEYLANT_SERVER_ENV=production
|
||||||
|
VEYLANT_SERVER_PORT=8090
|
||||||
|
VEYLANT_CRYPTO_KEY=$(openssl rand -base64 32)
|
||||||
|
VEYLANT_DATABASE_URL=postgres://veylant_app:STRONG_PASSWORD@postgres:5432/veylant?sslmode=require
|
||||||
|
VEYLANT_REDIS_URL=redis://:REDIS_PASSWORD@redis:6379
|
||||||
|
VEYLANT_CLICKHOUSE_DSN=clickhouse://clickhouse:9000/veylant?dial_timeout=5s
|
||||||
|
VEYLANT_KEYCLOAK_BASE_URL=https://keycloak.yourdomain.com
|
||||||
|
VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-...
|
||||||
|
VEYLANT_PII_FAIL_OPEN=false`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="build">Building the Production Image</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Build multi-arch image (amd64 + arm64)
|
||||||
|
docker buildx build \\
|
||||||
|
--platform linux/amd64,linux/arm64 \\
|
||||||
|
--tag ghcr.io/veylant/ia-gateway:1.0.0 \\
|
||||||
|
--push .
|
||||||
|
|
||||||
|
# Run with production config
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="health-check">Health Checks</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Check all services
|
||||||
|
make health # curl localhost:8090/healthz
|
||||||
|
|
||||||
|
# Check individual services
|
||||||
|
curl http://localhost:8090/healthz
|
||||||
|
curl http://localhost:8091/healthz # PII service`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="backup">Database Backup</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# PostgreSQL backup (runs daily at 02:00 UTC via CronJob in Kubernetes)
|
||||||
|
pg_dump -h postgres -U veylant -d veylant \\
|
||||||
|
| gzip > backup-$(date +%Y%m%d).sql.gz
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
gunzip -c backup-20260115.sql.gz | psql -h postgres -U veylant -d veylant`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
117
web/src/pages/docs/deployment/KubernetesPage.tsx
Normal file
117
web/src/pages/docs/deployment/KubernetesPage.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function KubernetesPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="kubernetes">Kubernetes Deployment (Helm)</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA ships a production-grade Helm chart at{" "}
|
||||||
|
<code>deploy/helm/veylant-proxy/</code>. The chart includes Deployment, Service, HPA,
|
||||||
|
PodDisruptionBudget, and ServiceMonitor resources.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="prerequisites">Prerequisites</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Kubernetes 1.28+ (EKS v1.31 tested)</li>
|
||||||
|
<li>Helm 3.12+</li>
|
||||||
|
<li>Metrics Server (for HPA)</li>
|
||||||
|
<li>Prometheus Operator (optional, for ServiceMonitor)</li>
|
||||||
|
<li>Istio 1.20+ (for blue/green deployment)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="install">Install the Chart</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Add the Helm repository (GHCR OCI)
|
||||||
|
helm pull oci://ghcr.io/veylant/charts/veylant-proxy --version 1.0.0
|
||||||
|
|
||||||
|
# Install with production values
|
||||||
|
helm upgrade --install veylant-proxy \\
|
||||||
|
oci://ghcr.io/veylant/charts/veylant-proxy \\
|
||||||
|
--version 1.0.0 \\
|
||||||
|
--namespace veylant \\
|
||||||
|
--create-namespace \\
|
||||||
|
-f deploy/helm/veylant-proxy/values-production.yaml \\
|
||||||
|
--set image.tag=1.0.0 \\
|
||||||
|
--set config.crypto.key=$(openssl rand -base64 32) \\
|
||||||
|
--set config.providers.openai.apiKey=$OPENAI_API_KEY`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="values">Production Values</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`# values-production.yaml
|
||||||
|
replicaCount: 3
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/veylant/ia-gateway
|
||||||
|
tag: "1.0.0"
|
||||||
|
|
||||||
|
autoscaling:
|
||||||
|
enabled: true
|
||||||
|
minReplicas: 3
|
||||||
|
maxReplicas: 15
|
||||||
|
targetCPUUtilizationPercentage: 70
|
||||||
|
targetMemoryUtilizationPercentage: 80
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "1Gi"
|
||||||
|
|
||||||
|
config:
|
||||||
|
server:
|
||||||
|
env: production
|
||||||
|
pii:
|
||||||
|
fail_open: false # return 503 on PII failure in production
|
||||||
|
|
||||||
|
podDisruptionBudget:
|
||||||
|
enabled: true
|
||||||
|
minAvailable: 1`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="hpa">Horizontal Pod Autoscaler</h2>
|
||||||
|
<p>
|
||||||
|
The HPA scales from 3 to 15 replicas based on CPU (70%) and memory (80%) utilization.
|
||||||
|
Scale-up is fast (30s stabilization); scale-down is conservative (5 minutes) to avoid
|
||||||
|
thrashing.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Check HPA status
|
||||||
|
kubectl get hpa -n veylant
|
||||||
|
|
||||||
|
# Watch scaling events
|
||||||
|
kubectl describe hpa veylant-proxy -n veylant`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="terraform">Terraform (AWS EKS)</h2>
|
||||||
|
<p>
|
||||||
|
Infrastructure as Code for a production EKS cluster is in{" "}
|
||||||
|
<code>deploy/terraform/</code>:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>EKS v1.31, 3-AZ node groups (t3.large)</li>
|
||||||
|
<li>S3 bucket for PostgreSQL backups (7-day retention)</li>
|
||||||
|
<li>IRSA for pod-level AWS permissions</li>
|
||||||
|
<li>VPC, subnets, security groups</li>
|
||||||
|
</ul>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`cd deploy/terraform
|
||||||
|
terraform init
|
||||||
|
terraform plan -var="region=eu-west-3" -var="cluster_name=veylant-prod"
|
||||||
|
terraform apply`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="warning" title="Terraform state">
|
||||||
|
Configure remote state (S3 + DynamoDB locking) before running in production. The default
|
||||||
|
local state is not suitable for team use.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
web/src/pages/docs/getting-started/KeyConceptsPage.tsx
Normal file
127
web/src/pages/docs/getting-started/KeyConceptsPage.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
const concepts = [
|
||||||
|
{
|
||||||
|
term: "Tenant",
|
||||||
|
definition:
|
||||||
|
"A logical unit of isolation — typically a company or business unit. All data in PostgreSQL is isolated by tenant_id via Row-Level Security. A tenant can have multiple users, multiple routing rules, and separate cost quotas.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Routing Rule",
|
||||||
|
definition:
|
||||||
|
"A policy that matches incoming AI requests based on conditions (user role, department, model, token estimate, sensitivity) and routes them to a specific provider with optional fallback. Rules are sorted by priority (lower = evaluated first). First match wins.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "PII (Personally Identifiable Information)",
|
||||||
|
definition:
|
||||||
|
"Data that can identify a person: names, email addresses, phone numbers, IBANs, SSNs, credit card numbers, etc. Veylant IA detects and anonymizes PII in prompts before they leave your network.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Pseudonymization",
|
||||||
|
definition:
|
||||||
|
"A reversible PII replacement technique. Detected PII tokens are replaced with synthetic identifiers (e.g., PERSON_001) and the original→synthetic mapping is stored in Redis (AES-256-GCM encrypted, TTL-based). The LLM works with the synthetic data; the response can optionally be de-pseudonymized.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Audit Log",
|
||||||
|
definition:
|
||||||
|
"An immutable record of every AI request: tenant, user, model, provider, token counts, cost, PII entities detected, policy matched, latency, and response status. Stored in ClickHouse (append-only). Retention via TTL policies — no DELETE operations.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Provider Adapter",
|
||||||
|
definition:
|
||||||
|
"A Go interface (Send, Stream, Validate, HealthCheck) implemented for each LLM provider. The routing engine selects the adapter; all adapters return OpenAI-format responses regardless of the upstream API.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Circuit Breaker",
|
||||||
|
definition:
|
||||||
|
"A per-provider failure counter. When failures exceed a threshold (default: 5), the breaker opens and the provider is bypassed for a TTL period (default: 60s). The fallback chain in the routing rule is used instead.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "RBAC",
|
||||||
|
definition:
|
||||||
|
"Role-Based Access Control. Four roles: admin (full access), manager (read-write policies and users), user (inference only, restricted models), auditor (read-only logs and compliance, no inference). Roles are embedded in the Keycloak JWT.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Feature Flag",
|
||||||
|
definition:
|
||||||
|
"A boolean or string flag stored in PostgreSQL with an in-memory cache. Used to gate features without redeployment. Falls back to in-memory defaults if the database is unavailable.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "GDPR Article 30",
|
||||||
|
definition:
|
||||||
|
"The GDPR requirement to maintain a Record of Processing Activities (ROPA). Veylant IA provides a built-in registry with fields for use case, legal basis, data categories, retention period, recipients, and processors.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "EU AI Act",
|
||||||
|
definition:
|
||||||
|
"EU regulation classifying AI systems by risk level: forbidden, high, limited, or minimal. Veylant IA's compliance module helps you classify each use case through a structured questionnaire and generates PDF reports.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "SLO (Service Level Objective)",
|
||||||
|
definition:
|
||||||
|
"Veylant IA targets 99.5% availability and p95 latency < 500ms. These are tracked in the production Grafana dashboard with an error budget that updates in real time.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function KeyConceptsPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="key-concepts">Key Concepts</h1>
|
||||||
|
<p>
|
||||||
|
This glossary explains the core abstractions you'll encounter when working with Veylant IA.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Reading order">
|
||||||
|
If you're new to Veylant IA, read{" "}
|
||||||
|
<a href="/docs">What is Veylant IA?</a> first, then come back here before
|
||||||
|
diving into the API reference or guides.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="glossary">Glossary</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4 my-4">
|
||||||
|
{concepts.map((c) => (
|
||||||
|
<div key={c.term} className="rounded-lg border bg-card p-4">
|
||||||
|
<dt className="font-semibold text-foreground mb-1.5">{c.term}</dt>
|
||||||
|
<dd className="text-sm text-muted-foreground leading-relaxed">{c.definition}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="request-lifecycle">Request Lifecycle</h2>
|
||||||
|
<p>What happens when a client sends a request to <code>POST /v1/chat/completions</code>:</p>
|
||||||
|
|
||||||
|
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-loose overflow-x-auto">
|
||||||
|
<pre>{`1. Request arrives at Go proxy (:8090)
|
||||||
|
2. RequestID middleware → generate X-Request-ID
|
||||||
|
3. SecurityHeaders middleware → set CSP, HSTS, COOP headers
|
||||||
|
4. CORS middleware → validate Origin header
|
||||||
|
5. Auth middleware → validate Bearer JWT (Keycloak or mock)
|
||||||
|
→ extract tenant_id, user_id, role, department from claims
|
||||||
|
6. RateLimit middleware → check per-tenant token bucket (Redis)
|
||||||
|
→ if exceeded: 429 with Retry-After header
|
||||||
|
7. RBAC check → validate role has access to requested model
|
||||||
|
8. Routing engine → evaluate rules (priority ASC, first match)
|
||||||
|
→ select provider + fallback chain
|
||||||
|
9. PII detection → gRPC call to PII service (<50ms budget)
|
||||||
|
→ anonymize/pseudonymize prompt
|
||||||
|
10. Circuit breaker check → skip if provider is open
|
||||||
|
11. Provider adapter → forward to LLM (stream or batch)
|
||||||
|
12. Audit logger → async ClickHouse write (non-blocking)
|
||||||
|
13. Response returned to client`}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="multi-tenancy">Multi-tenancy Model</h2>
|
||||||
|
<p>
|
||||||
|
Veylant IA uses <strong>logical isolation</strong> via PostgreSQL Row-Level Security (RLS).
|
||||||
|
The application connects as role <code>veylant_app</code> and sets{" "}
|
||||||
|
<code>app.tenant_id</code> per session using a middleware. All queries automatically filter
|
||||||
|
by tenant without requiring explicit <code>WHERE</code> clauses in application code.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Physical isolation (separate database instances per tenant) is a V2 feature. See the
|
||||||
|
feedback backlog.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
167
web/src/pages/docs/getting-started/QuickStartPage.tsx
Normal file
167
web/src/pages/docs/getting-started/QuickStartPage.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function QuickStartPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="quick-start">Quick Start</h1>
|
||||||
|
<p>
|
||||||
|
This guide gets you from zero to a running Veylant IA instance in under 5 minutes using
|
||||||
|
Docker Compose.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="Prerequisites">
|
||||||
|
You need <strong>Docker 24+</strong> and <strong>Docker Compose v2</strong> installed.
|
||||||
|
Clone the repository and ensure ports 8090, 8080, 5432, 6379, 8123, 3000, and 3001 are
|
||||||
|
free.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="step-1-clone">Step 1 — Clone the repository</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`git clone https://github.com/veylant/ia-gateway.git
|
||||||
|
cd ia-gateway`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="step-2-configure">Step 2 — Configure environment</h2>
|
||||||
|
<p>
|
||||||
|
Copy the sample environment file and add at least one LLM provider API key. For a minimal
|
||||||
|
setup, OpenAI is enough.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`cp .env.example .env
|
||||||
|
|
||||||
|
# Edit .env and set:
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
# Optional:
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Development mode">
|
||||||
|
In <code>server.env=development</code> (the default), all external services degrade
|
||||||
|
gracefully. Keycloak is bypassed (mock JWT), PostgreSQL failures disable routing, ClickHouse
|
||||||
|
failures disable audit logs. This means you can start the proxy even if some services
|
||||||
|
haven't fully initialized yet.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="step-3-start">Step 3 — Start the stack</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`make dev
|
||||||
|
|
||||||
|
# Or directly:
|
||||||
|
docker compose up --build`}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
This starts 9 services: PostgreSQL, Redis, ClickHouse, Keycloak, the Go proxy, PII
|
||||||
|
detection service, Prometheus, Grafana, and the React dashboard.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Wait for the proxy to print <code>server listening on :8090</code>. First startup takes
|
||||||
|
~2 minutes while Keycloak initializes and database migrations run.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="step-4-verify">Step 4 — Verify the stack</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Health check
|
||||||
|
curl http://localhost:8090/healthz
|
||||||
|
# {"status":"ok","version":"1.0.0"}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Service</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">URL</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Credentials</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ service: "AI Proxy", url: "http://localhost:8090", creds: "—" },
|
||||||
|
{ service: "React Dashboard", url: "http://localhost:3000", creds: "dev mode (no auth)" },
|
||||||
|
{ service: "Keycloak Admin", url: "http://localhost:8080", creds: "admin / admin" },
|
||||||
|
{ service: "Grafana", url: "http://localhost:3001", creds: "admin / admin" },
|
||||||
|
{ service: "Prometheus", url: "http://localhost:9090", creds: "—" },
|
||||||
|
{ service: "API Docs", url: "http://localhost:8090/docs", creds: "—" },
|
||||||
|
{ service: "Playground", url: "http://localhost:8090/playground", creds: "—" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.service} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-medium">{row.service}</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<code className="text-xs">{row.url}</code>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.creds}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="step-5-first-call">Step 5 — Make your first AI call</h2>
|
||||||
|
<p>
|
||||||
|
In development mode, the proxy uses a mock JWT verifier. Pass any Bearer token and the
|
||||||
|
request will be authenticated as <code>admin@veylant.dev</code>.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/chat/completions \\
|
||||||
|
-H "Authorization: Bearer dev-token" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [{"role": "user", "content": "Hello, Veylant!"}]
|
||||||
|
}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>Or use the OpenAI Python SDK with a changed base URL:</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="python"
|
||||||
|
code={`from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
base_url="http://localhost:8090/v1",
|
||||||
|
api_key="dev-token", # any string in dev mode
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-4o",
|
||||||
|
messages=[{"role": "user", "content": "Hello, Veylant!"}],
|
||||||
|
)
|
||||||
|
print(response.choices[0].message.content)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="step-6-dashboard">Step 6 — Explore the dashboard</h2>
|
||||||
|
<p>
|
||||||
|
Open <code>http://localhost:3000</code> to see the React dashboard. In development mode,
|
||||||
|
you're automatically logged in as <code>Dev Admin</code>. You'll see:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Overview</strong> — request counts, costs, and tokens consumed
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Playground IA</strong> — test prompts with live PII detection visualization
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Policies</strong> — create and manage routing rules
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Compliance</strong> — GDPR Article 30 registry and AI Act questionnaire
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Next: Configure a routing rule">
|
||||||
|
Try creating a routing rule in the dashboard that sends all requests from the{" "}
|
||||||
|
<code>legal</code> department to Anthropic instead of OpenAI. See{" "}
|
||||||
|
<a href="/docs/guides/routing">Routing Rules Engine</a> for the full guide.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="stop-the-stack">Stop the stack</h2>
|
||||||
|
<CodeBlock language="bash" code={`make dev-down\n# Stops all containers and removes volumes`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
172
web/src/pages/docs/getting-started/WhatIsVeylantPage.tsx
Normal file
172
web/src/pages/docs/getting-started/WhatIsVeylantPage.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export function WhatIsVeylantPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="what-is-veylant">What is Veylant IA?</h1>
|
||||||
|
<p>
|
||||||
|
<strong>Veylant IA</strong> is a B2B SaaS platform that acts as an intelligent proxy and
|
||||||
|
gateway for enterprise AI consumption. It sits between your organization's applications and
|
||||||
|
LLM providers (OpenAI, Anthropic, Azure, Mistral, Ollama), enforcing governance policies on
|
||||||
|
every request.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="OpenAI-compatible API">
|
||||||
|
Veylant IA implements the OpenAI API format — specifically{" "}
|
||||||
|
<code>/v1/chat/completions</code>. Your existing OpenAI SDK clients work without
|
||||||
|
modification; just change the base URL.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="core-value-proposition">Core Value Proposition</h2>
|
||||||
|
<p>Four problems Veylant IA solves for enterprise IT and compliance teams:</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 my-4">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "Shadow AI Prevention",
|
||||||
|
desc: "Every AI call flows through the proxy — no direct provider access possible. Full audit trail of who sent what to which model.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PII Anonymization",
|
||||||
|
desc: "3-layer detection (regex + spaCy NER + LLM validation) anonymizes sensitive data before it leaves your perimeter. <50ms latency.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "GDPR & EU AI Act Compliance",
|
||||||
|
desc: "Built-in Article 30 processing registry, AI Act risk classification, DPIA templates, and subject access/erasure rights.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Cost Control",
|
||||||
|
desc: "Per-tenant and per-user token budgets, circuit breakers per provider, cost breakdown by department and model.",
|
||||||
|
},
|
||||||
|
].map((card) => (
|
||||||
|
<div key={card.title} className="rounded-lg border bg-card p-4">
|
||||||
|
<h3 className="font-semibold text-sm mb-1">{card.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">{card.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="architecture">Architecture Overview</h2>
|
||||||
|
<p>
|
||||||
|
Veylant IA is a <strong>modular monolith</strong> (not microservices) with two distinct
|
||||||
|
runtimes:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-relaxed overflow-x-auto">
|
||||||
|
<pre>{`API Gateway (Traefik / Nginx)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Go Proxy [cmd/proxy] — :8090
|
||||||
|
├── Auth middleware (OIDC/Keycloak JWT validation)
|
||||||
|
├── Rate limiter (token-bucket, per-tenant + per-user)
|
||||||
|
├── PII gRPC client → PII Service :50051
|
||||||
|
├── Routing engine (PostgreSQL JSONB rules, in-memory cache)
|
||||||
|
├── Provider adapters (OpenAI / Anthropic / Azure / Mistral / Ollama)
|
||||||
|
├── Audit logger → ClickHouse (async batch)
|
||||||
|
└── Admin REST API (/v1/admin/*)
|
||||||
|
│ gRPC <2ms
|
||||||
|
▼
|
||||||
|
PII Detection Service [FastAPI + grpc.aio] — :8091 / :50051
|
||||||
|
├── Layer 1: Regex (IBAN, email, phone, SSN, credit cards)
|
||||||
|
├── Layer 2: Presidio + spaCy NER (names, addresses, orgs)
|
||||||
|
└── Layer 3: LLM validation (ambiguous cases)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LLM Provider (OpenAI / Anthropic / Azure / Mistral / Ollama)
|
||||||
|
|
||||||
|
Data Layer:
|
||||||
|
PostgreSQL 16 — config, users, policies, registry (RLS multi-tenancy)
|
||||||
|
ClickHouse — append-only audit logs and analytics
|
||||||
|
Redis 7 — sessions, rate limiting, PII pseudonymization mappings
|
||||||
|
Keycloak — IAM, SSO, SAML 2.0 / OIDC federation`}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="key-components">Key Components</h2>
|
||||||
|
|
||||||
|
<h3 id="go-proxy">Go Proxy</h3>
|
||||||
|
<p>
|
||||||
|
The core of Veylant IA. Written in Go 1.24, it handles all incoming AI requests, applies
|
||||||
|
governance policies, and routes them to the appropriate LLM provider. It exposes:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>/v1/chat/completions</code> — OpenAI-compatible inference endpoint
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>/v1/pii/analyze</code> — on-demand PII detection
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>/v1/admin/*</code> — governance API (policies, users, audit logs, compliance)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>/healthz</code>, <code>/docs</code>, <code>/playground</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="pii-service">PII Detection Service</h3>
|
||||||
|
<p>
|
||||||
|
A Python FastAPI + gRPC service running 3 detection layers in under 50ms. Anonymizes or
|
||||||
|
pseudonymizes PII in prompts before they reach the upstream LLM. Pseudonymized mappings are
|
||||||
|
stored in Redis (AES-256-GCM encrypted, TTL-based).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="routing-engine">Routing Engine</h3>
|
||||||
|
<p>
|
||||||
|
Rules stored in PostgreSQL (JSONB conditions), cached in memory. Routes requests to
|
||||||
|
providers based on user role, department, model requested, sensitivity score, token
|
||||||
|
estimate, and more. First-match wins; lower priority number = evaluated first.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="supported-providers">Supported LLM Providers</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Provider</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Models</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Streaming</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ provider: "OpenAI", models: "gpt-4o, gpt-4-turbo, gpt-3.5-turbo", stream: "Yes", status: "GA" },
|
||||||
|
{ provider: "Anthropic", models: "claude-3-5-sonnet, claude-3-opus", stream: "Yes", status: "GA" },
|
||||||
|
{ provider: "Azure OpenAI", models: "GPT-4 deployments", stream: "Yes", status: "GA" },
|
||||||
|
{ provider: "Mistral AI", models: "mistral-large, mistral-medium", stream: "Yes", status: "GA" },
|
||||||
|
{ provider: "Ollama", models: "llama3, mistral, codellama, ...", stream: "Yes", status: "GA" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.provider} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-medium">{row.provider}</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground font-mono text-xs">{row.models}</td>
|
||||||
|
<td className="px-4 py-2.5 text-green-600 dark:text-green-400">{row.stream}</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<span className="text-xs bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 px-2 py-0.5 rounded font-medium">
|
||||||
|
{row.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="next-steps">Next Steps</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<Link to="/docs/getting-started/quick-start">Quick Start</Link> — run the full stack in 5
|
||||||
|
minutes
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to="/docs/getting-started/concepts">Key Concepts</Link> — learn the core
|
||||||
|
abstractions
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to="/docs/api/chat-completions">API Reference</Link> — integrate your first
|
||||||
|
application
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
web/src/pages/docs/guides/CircuitBreakerGuide.tsx
Normal file
118
web/src/pages/docs/guides/CircuitBreakerGuide.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
|
||||||
|
export function CircuitBreakerGuide() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="circuit-breaker">Circuit Breaker & Failover</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA includes a per-provider circuit breaker that prevents cascading failures when an
|
||||||
|
LLM provider is degraded or unreachable.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="states">Circuit Breaker States</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 my-4">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
state: "Closed",
|
||||||
|
color: "border-green-400",
|
||||||
|
bg: "bg-green-50 dark:bg-green-950/30",
|
||||||
|
desc: "Normal operation. Requests are forwarded to the provider. Failures are counted.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: "Open",
|
||||||
|
color: "border-red-400",
|
||||||
|
bg: "bg-red-50 dark:bg-red-950/30",
|
||||||
|
desc: "Provider bypassed. All requests use the fallback chain. Stays open for open_ttl seconds.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: "Half-Open",
|
||||||
|
color: "border-amber-400",
|
||||||
|
bg: "bg-amber-50 dark:bg-amber-950/30",
|
||||||
|
desc: "Testing if provider has recovered. One probe request sent. Success → Closed; Failure → Open.",
|
||||||
|
},
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item.state} className={`rounded-lg border-l-4 p-4 ${item.color} ${item.bg}`}>
|
||||||
|
<h3 className="font-semibold text-sm mb-2">{item.state}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="configuration">Configuration</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`circuit_breaker:
|
||||||
|
threshold: 5 # consecutive failures to open the breaker
|
||||||
|
open_ttl: 60s # how long to stay open before half-open probe`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Per-provider isolation">
|
||||||
|
Each provider has an independent circuit breaker. A failing Azure deployment does not affect
|
||||||
|
OpenAI or Anthropic calls.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="fallback">Fallback Chain</h2>
|
||||||
|
<p>
|
||||||
|
When the primary provider's circuit is open, the routing engine uses the{" "}
|
||||||
|
<code>fallback_providers</code> array from the matched routing rule:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`{
|
||||||
|
"provider": "azure",
|
||||||
|
"fallback_providers": ["anthropic", "openai"],
|
||||||
|
"conditions": [{"field": "user.department", "operator": "eq", "value": "legal"}]
|
||||||
|
}
|
||||||
|
// If azure is open → try anthropic → if anthropic is open → try openai → if all fail → 503`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="check-status">Checking Status</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/admin/providers/status \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{"provider": "openai", "state": "closed", "failures": 0, "last_failure": null},
|
||||||
|
{"provider": "anthropic", "state": "closed", "failures": 0, "last_failure": null},
|
||||||
|
{"provider": "azure", "state": "open", "failures": 5, "last_failure": "2026-01-15T14:30:00Z"},
|
||||||
|
{"provider": "mistral", "state": "half-open", "failures": 3, "last_failure": "2026-01-15T14:28:00Z"},
|
||||||
|
{"provider": "ollama", "state": "closed", "failures": 0, "last_failure": null}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="prometheus">Prometheus Alert</h2>
|
||||||
|
<p>
|
||||||
|
The <code>CircuitBreakerOpen</code> alert fires when any provider is in the open state:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`- alert: CircuitBreakerOpen
|
||||||
|
expr: veylant_circuit_breaker_state > 0
|
||||||
|
for: 0m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Provider {{ $labels.provider }} circuit breaker is open"
|
||||||
|
description: "The circuit breaker for {{ $labels.provider }} has opened after repeated failures."`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="graceful-degradation">Development Mode Degradation</h2>
|
||||||
|
<Callout type="info">
|
||||||
|
In <code>server.env=development</code>, Veylant IA degrades gracefully if services are
|
||||||
|
unreachable:
|
||||||
|
<ul className="mt-2 space-y-1">
|
||||||
|
<li><strong>Keycloak unreachable</strong> → MockVerifier (auth bypassed)</li>
|
||||||
|
<li><strong>PostgreSQL unreachable</strong> → routing disabled, feature flags use in-memory defaults</li>
|
||||||
|
<li><strong>ClickHouse unreachable</strong> → audit logging disabled</li>
|
||||||
|
<li><strong>PII service unreachable</strong> → PII skipped if <code>fail_open=true</code></li>
|
||||||
|
</ul>
|
||||||
|
In <code>production</code> mode, any of the above causes a fatal startup error.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
154
web/src/pages/docs/guides/ComplianceGuide.tsx
Normal file
154
web/src/pages/docs/guides/ComplianceGuide.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export function ComplianceGuide() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="compliance">GDPR & EU AI Act Compliance</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA includes a built-in compliance module for GDPR Article 30 record-keeping and EU
|
||||||
|
AI Act risk classification. It is designed to serve as your primary compliance tool for AI
|
||||||
|
deployments.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="gdpr-art30">GDPR Article 30 — Record of Processing Activities</h2>
|
||||||
|
<p>
|
||||||
|
Article 30 requires organizations to maintain a written record of all data processing
|
||||||
|
activities. For AI systems, this means documenting each use case where personal data may be
|
||||||
|
processed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="ropa-fields">Required ROPA Fields</h3>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Field</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">GDPR Requirement</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Example</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ field: "use_case_name", req: "Name of the processing activity", ex: "Legal contract analysis" },
|
||||||
|
{ field: "purpose", req: "Art. 5(1)(b) — purpose limitation", ex: "Automated risk identification in supplier contracts" },
|
||||||
|
{ field: "legal_basis", req: "Art. 6 — lawfulness of processing", ex: "legitimate_interest" },
|
||||||
|
{ field: "data_categories", req: "Art. 30(1)(c) — categories of data subjects and data", ex: "name, email, financial" },
|
||||||
|
{ field: "retention_period", req: "Art. 5(1)(e) — storage limitation", ex: "3 years" },
|
||||||
|
{ field: "security_measures", req: "Art. 32 — security of processing", ex: "AES-256-GCM, PII anonymization" },
|
||||||
|
{ field: "controller_name", req: "Art. 30(1)(a) — controller identity", ex: "Acme Corp — dpo@acme.com" },
|
||||||
|
{ field: "processors", req: "Art. 30(1)(d) — recipients of data", ex: "Anthropic (via Veylant IA proxy)" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.field} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.field}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.req}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground italic">{row.ex}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 id="legal-bases">Legal Bases</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>consent</code> — User has given explicit consent (Art. 6(1)(a))</li>
|
||||||
|
<li><code>contract</code> — Processing necessary for a contract (Art. 6(1)(b))</li>
|
||||||
|
<li><code>legal_obligation</code> — Required by law (Art. 6(1)(c))</li>
|
||||||
|
<li><code>vital_interests</code> — Protecting someone's life (Art. 6(1)(d))</li>
|
||||||
|
<li><code>public_task</code> — Public interest or official authority (Art. 6(1)(e))</li>
|
||||||
|
<li><code>legitimate_interest</code> — Legitimate interests of the controller (Art. 6(1)(f))</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="ai-act">EU AI Act Risk Classification</h2>
|
||||||
|
<p>
|
||||||
|
The EU AI Act (effective August 2024, full enforcement from August 2026) classifies AI
|
||||||
|
systems into four risk categories.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-3 my-4">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
level: "Forbidden",
|
||||||
|
color: "border-red-400 bg-red-50 dark:bg-red-950/30",
|
||||||
|
badge: "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300",
|
||||||
|
score: "Score 5",
|
||||||
|
desc: "Cannot be deployed. Examples: social scoring systems, real-time biometric surveillance in public spaces, AI that exploits vulnerable groups.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "High Risk",
|
||||||
|
color: "border-orange-400 bg-orange-50 dark:bg-orange-950/30",
|
||||||
|
badge: "bg-orange-100 dark:bg-orange-900/40 text-orange-700 dark:text-orange-300",
|
||||||
|
score: "Score 3–4",
|
||||||
|
desc: "Requires conformity assessment before deployment. DPIA mandatory. Examples: AI in hiring, credit scoring, education grading, critical infrastructure.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "Limited Risk",
|
||||||
|
color: "border-amber-400 bg-amber-50 dark:bg-amber-950/30",
|
||||||
|
badge: "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300",
|
||||||
|
score: "Score 1–2",
|
||||||
|
desc: "Transparency obligations apply. Users must be informed they interact with AI. Examples: chatbots, recommendation systems, customer service AI.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "Minimal Risk",
|
||||||
|
color: "border-green-400 bg-green-50 dark:bg-green-950/30",
|
||||||
|
badge: "bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300",
|
||||||
|
score: "Score 0",
|
||||||
|
desc: "Minimal or no risk. Voluntary code of conduct recommended. Examples: spam filters, AI-powered search, content recommendation.",
|
||||||
|
},
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item.level} className={`flex items-start gap-3 rounded-lg border-l-4 p-4 ${item.color}`}>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className={`text-xs font-bold px-2 py-0.5 rounded ${item.badge}`}>
|
||||||
|
{item.level}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{item.score}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="dpia">Data Protection Impact Assessment (DPIA)</h2>
|
||||||
|
<p>
|
||||||
|
A DPIA is mandatory under GDPR Art. 35 for high-risk processing activities. High-risk AI
|
||||||
|
systems under the AI Act also trigger DPIA requirements. Veylant IA generates DPIA template
|
||||||
|
documents from the Admin → Compliance → Reports tab.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="reports">Compliance Reports</h2>
|
||||||
|
<p>Available report formats via the API:</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# GDPR Article 30 registry — PDF
|
||||||
|
GET /v1/admin/compliance/reports/art30.pdf
|
||||||
|
|
||||||
|
# GDPR Article 30 registry — JSON export
|
||||||
|
GET /v1/admin/compliance/reports/art30.json
|
||||||
|
|
||||||
|
# AI Act risk classification report — PDF
|
||||||
|
GET /v1/admin/compliance/reports/ai-act.pdf
|
||||||
|
|
||||||
|
# DPIA template — PDF
|
||||||
|
GET /v1/admin/compliance/reports/dpia/{entry_id}.pdf
|
||||||
|
|
||||||
|
# Audit log export — CSV
|
||||||
|
GET /v1/admin/logs/export?from=2026-01-01T00:00:00Z&to=2026-01-31T23:59:59Z`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Audit-of-the-audit">
|
||||||
|
All accesses to compliance reports and audit logs are themselves logged. This satisfies
|
||||||
|
data protection authority requirements for meta-logging of sensitive data access.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="next-steps">Working with Compliance</h2>
|
||||||
|
<p>
|
||||||
|
See the <Link to="/docs/api/admin/compliance">Admin — Compliance API</Link> for full
|
||||||
|
endpoint documentation, or navigate to{" "}
|
||||||
|
<strong>Dashboard → Compliance</strong> to use the visual interface.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
web/src/pages/docs/guides/MonitoringGuide.tsx
Normal file
162
web/src/pages/docs/guides/MonitoringGuide.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
|
||||||
|
export function MonitoringGuide() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="monitoring">Monitoring & Alerting</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA exposes Prometheus metrics and ships pre-built Grafana dashboards and
|
||||||
|
Alertmanager rules for production operations.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="prometheus">Prometheus Metrics</h2>
|
||||||
|
<p>
|
||||||
|
The proxy exposes metrics at <code>GET /metrics</code> (Prometheus text format, scraped
|
||||||
|
every 15 seconds).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Metric</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Type</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ metric: "veylant_requests_total", type: "Counter", desc: "Total requests by provider, model, status_code, tenant_id" },
|
||||||
|
{ metric: "veylant_request_duration_seconds", type: "Histogram", desc: "End-to-end request latency (p50, p95, p99)" },
|
||||||
|
{ metric: "veylant_pii_entities_total", type: "Counter", desc: "PII entities detected by type" },
|
||||||
|
{ metric: "veylant_tokens_total", type: "Counter", desc: "Tokens consumed by provider and model" },
|
||||||
|
{ metric: "veylant_cost_usd_total", type: "Counter", desc: "USD cost by provider and model" },
|
||||||
|
{ metric: "veylant_circuit_breaker_state", type: "Gauge", desc: "Circuit breaker state (0=closed, 1=open, 2=half-open) by provider" },
|
||||||
|
{ metric: "veylant_rate_limit_rejections_total", type: "Counter", desc: "Rate limit rejections by tenant_id" },
|
||||||
|
{ metric: "veylant_pii_pipeline_duration_seconds", type: "Histogram", desc: "PII detection pipeline latency" },
|
||||||
|
{ metric: "veylant_db_connections", type: "Gauge", desc: "PostgreSQL connection pool (open, idle, in-use)" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.metric} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.metric}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs">
|
||||||
|
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${row.type === "Counter" ? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300" : row.type === "Gauge" ? "bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300" : "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300"}`}>
|
||||||
|
{row.type}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.desc}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="grafana-dashboards">Grafana Dashboards</h2>
|
||||||
|
<p>
|
||||||
|
Two dashboards are pre-provisioned in Grafana (<code>http://localhost:3001</code>,
|
||||||
|
admin/admin):
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 my-4">
|
||||||
|
<div className="rounded-lg border bg-card p-4">
|
||||||
|
<h3 className="font-semibold text-sm mb-2">Proxy Overview</h3>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
|
<li>Request rate (req/min)</li>
|
||||||
|
<li>Error rate (%)</li>
|
||||||
|
<li>Latency p50/p95/p99</li>
|
||||||
|
<li>Token consumption over time</li>
|
||||||
|
<li>PII entities by type</li>
|
||||||
|
<li>Provider distribution (pie chart)</li>
|
||||||
|
<li>DB connection pool</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-card p-4">
|
||||||
|
<h3 className="font-semibold text-sm mb-2">Production SLO</h3>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
|
<li>SLO: 99.5% availability</li>
|
||||||
|
<li>Error budget (burn rate)</li>
|
||||||
|
<li>p95 latency SLO (<500ms)</li>
|
||||||
|
<li>Error budget remaining (%)</li>
|
||||||
|
<li>Active incidents timeline</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="alerts">Prometheus Alerts</h2>
|
||||||
|
<p>
|
||||||
|
7 alert rules are configured in <code>deploy/prometheus/rules.yml</code> and integrated
|
||||||
|
with Alertmanager:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-3 my-4">
|
||||||
|
{[
|
||||||
|
{ name: "HighLatency", severity: "warning", desc: "p95 latency > 500ms for 2 minutes", expr: "histogram_quantile(0.95, veylant_request_duration_seconds_bucket) > 0.5" },
|
||||||
|
{ name: "HighErrorRate", severity: "critical", desc: "Error rate > 5% for 1 minute", expr: "rate(veylant_requests_total{status_code=~'5..'}[1m]) / rate(veylant_requests_total[1m]) > 0.05" },
|
||||||
|
{ name: "CircuitBreakerOpen", severity: "warning", desc: "Any provider circuit breaker is open", expr: "veylant_circuit_breaker_state > 0" },
|
||||||
|
{ name: "ProxyDown", severity: "critical", desc: "Proxy health check failing", expr: "up{job='veylant-proxy'} == 0" },
|
||||||
|
{ name: "CertExpiry", severity: "warning", desc: "TLS certificate expires in < 7 days", expr: "probe_ssl_earliest_cert_expiry - time() < 7 * 86400" },
|
||||||
|
{ name: "DBConnPoolExhausted", severity: "warning", desc: "DB connection pool > 90% utilized", expr: "veylant_db_connections{state='in_use'} / (veylant_db_connections{state='in_use'} + veylant_db_connections{state='idle'}) > 0.9" },
|
||||||
|
{ name: "PIIAnomaly", severity: "warning", desc: "PII detection rate spiked > 3x baseline", expr: "rate(veylant_pii_entities_total[5m]) > 3 * avg_over_time(rate(veylant_pii_entities_total[5m])[1h:])" },
|
||||||
|
].map((alert) => (
|
||||||
|
<div key={alert.name} className="rounded-lg border bg-card p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="font-mono text-sm font-semibold">{alert.name}</span>
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${alert.severity === "critical" ? "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300" : "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300"}`}>
|
||||||
|
{alert.severity}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">{alert.desc}</p>
|
||||||
|
<code className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded block overflow-x-auto">
|
||||||
|
{alert.expr}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="alertmanager">Alertmanager Routing</h2>
|
||||||
|
<p>Alerts are routed based on severity:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Critical</strong> → PagerDuty (immediate on-call notification)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Warning</strong> → Slack (#veylant-alerts channel)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`# deploy/alertmanager/alertmanager.yml
|
||||||
|
route:
|
||||||
|
group_by: ['alertname', 'severity']
|
||||||
|
group_wait: 30s
|
||||||
|
group_interval: 5m
|
||||||
|
repeat_interval: 12h
|
||||||
|
receiver: slack-warnings
|
||||||
|
|
||||||
|
routes:
|
||||||
|
- match:
|
||||||
|
severity: critical
|
||||||
|
receiver: pagerduty-critical
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
- name: pagerduty-critical
|
||||||
|
pagerduty_configs:
|
||||||
|
- routing_key: PAGERDUTY_ROUTING_KEY
|
||||||
|
- name: slack-warnings
|
||||||
|
slack_configs:
|
||||||
|
- channel: '#veylant-alerts'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="slo">SLO Definition</h2>
|
||||||
|
<Callout type="info" title="Production SLO — v1.0.0">
|
||||||
|
<strong>Availability:</strong> 99.5% (allows ~3.6 hours downtime/month)
|
||||||
|
<br />
|
||||||
|
<strong>Latency:</strong> p95 < 500ms for chat completion requests
|
||||||
|
<br />
|
||||||
|
<strong>PII pipeline:</strong> p99 < 50ms
|
||||||
|
<br />
|
||||||
|
<strong>Error rate:</strong> < 0.5% (5xx responses)
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
147
web/src/pages/docs/guides/PiiGuide.tsx
Normal file
147
web/src/pages/docs/guides/PiiGuide.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function PiiGuide() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="pii-detection">PII Detection & Anonymization</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA intercepts all AI prompts and runs a 3-layer PII detection pipeline before
|
||||||
|
forwarding to the LLM. The entire pipeline must complete in <strong>under 50ms</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="pipeline">Detection Pipeline</h2>
|
||||||
|
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-relaxed overflow-x-auto">
|
||||||
|
<pre>{`Incoming prompt text
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Layer 1: Regex (< 1ms)
|
||||||
|
─ IBAN: /[A-Z]{2}\\d{2}[A-Z0-9]{11,30}/
|
||||||
|
─ Email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/
|
||||||
|
─ Phone: /(?:\\+33|0)[1-9](?:[\\s.-]?\\d{2}){4}/
|
||||||
|
─ SSN: /\\d{1}\\s?\\d{2}\\s?\\d{2}\\s?\\d{2}\\s?\\d{3}\\s?\\d{3}\\s?\\d{2}/
|
||||||
|
─ Credit card: /\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}/
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Layer 2: Presidio + spaCy NER (15–40ms)
|
||||||
|
─ PERSON: "John Smith", "Marie Dupont"
|
||||||
|
─ LOCATION: "Paris", "75001", "rue de Rivoli"
|
||||||
|
─ ORGANIZATION: "Acme Corp", "Banque de France"
|
||||||
|
─ DATE_TIME: "12/03/1985"
|
||||||
|
─ NRP: nationality, religion, political group
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Layer 3: LLM validation (optional, V1.1)
|
||||||
|
─ For ambiguous cases scored 0.5–0.8
|
||||||
|
─ Sends short validation prompts to a fast LLM
|
||||||
|
─ Corrects false positives and false negatives
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Anonymized prompt → forwarded to LLM provider`}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="anonymization">Anonymization vs Pseudonymization</h2>
|
||||||
|
<p>Veylant IA supports two modes:</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 my-4">
|
||||||
|
<div className="rounded-lg border bg-card p-4">
|
||||||
|
<h3 className="font-semibold text-sm mb-2">Anonymization (default)</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
PII is replaced with a type label. No mapping stored. Irreversible.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="text"
|
||||||
|
code={`Input: "Call John at +33 6 12 34 56 78"
|
||||||
|
Output: "Call [PERSON] at [PHONE_NUMBER]"`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-card p-4">
|
||||||
|
<h3 className="font-semibold text-sm mb-2">Pseudonymization</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
PII is replaced with a synthetic token. Mapping stored in Redis (encrypted, TTL-based).
|
||||||
|
Reversible.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="text"
|
||||||
|
code={`Input: "Call John at +33 6 12 34 56 78"
|
||||||
|
Output: "Call PERSON_001 at PHONE_001"
|
||||||
|
Mapping: PERSON_001 → "John" (Redis, AES-256-GCM, TTL=3600s)`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="redis-mapping">Redis Pseudonymization Mapping</h2>
|
||||||
|
<p>
|
||||||
|
When pseudonymization is enabled, the PII service stores mappings in Redis using the
|
||||||
|
following key structure:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="text"
|
||||||
|
code={`Key: pii:{tenant_id}:{user_id}:{session_id}:{token}
|
||||||
|
Value: AES-256-GCM encrypted JSON {"original": "John Smith", "type": "PERSON"}
|
||||||
|
TTL: 3600 seconds (configurable)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="latency">Latency Budget</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Layer</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Typical latency</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">P99 latency</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ layer: "Layer 1: Regex", typ: "< 1ms", p99: "< 2ms" },
|
||||||
|
{ layer: "Layer 2: NER (spaCy)", typ: "15–30ms", p99: "45ms" },
|
||||||
|
{ layer: "Redis write (pseudonymization)", typ: "< 2ms", p99: "5ms" },
|
||||||
|
{ layer: "Total pipeline (gRPC)", typ: "20–35ms", p99: "50ms" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.layer} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 text-sm">{row.layer}</td>
|
||||||
|
<td className="px-4 py-2.5 text-sm text-muted-foreground">{row.typ}</td>
|
||||||
|
<td className="px-4 py-2.5 text-sm text-muted-foreground">{row.p99}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Callout type="warning" title="Fail-open behavior">
|
||||||
|
If the PII service is unreachable and <code>pii.fail_open=true</code> (default in
|
||||||
|
development), requests are forwarded without anonymization and a warning is logged. In
|
||||||
|
production, set <code>pii.fail_open=false</code> to return 503 instead.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="disabling">Disabling PII per Request</h2>
|
||||||
|
<p>
|
||||||
|
There is no per-request PII bypass. PII is controlled globally via the{" "}
|
||||||
|
<code>pii_detection</code> feature flag or the <code>pii.enabled</code> config key.
|
||||||
|
Fine-grained per-rule PII control is planned for V1.1.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="testing">Testing PII Detection</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Use the analyze endpoint directly
|
||||||
|
curl http://localhost:8090/v1/pii/analyze \\
|
||||||
|
-H "Authorization: Bearer $TOKEN" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"text": "Contact Marie Dupont at marie.dupont@orange.fr or +33 1 23 45 67 89",
|
||||||
|
"anonymize": true
|
||||||
|
}'`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Run Python unit tests for the PII service
|
||||||
|
pytest services/pii/tests/test_regex.py
|
||||||
|
pytest services/pii/tests/test_pipeline.py
|
||||||
|
pytest services/pii/tests/test_pseudo.py`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
web/src/pages/docs/guides/RbacGuide.tsx
Normal file
126
web/src/pages/docs/guides/RbacGuide.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
|
||||||
|
export function RbacGuide() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="rbac">RBAC & Permissions</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA enforces Role-Based Access Control on every request. Roles are embedded in the
|
||||||
|
Keycloak JWT and cannot be elevated at runtime.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="roles">Roles</h2>
|
||||||
|
<div className="space-y-4 my-4">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
role: "admin",
|
||||||
|
color: "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300",
|
||||||
|
description: "Full access. Can manage policies, users, providers, feature flags, and read all compliance/audit data. Has unrestricted model access.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "manager",
|
||||||
|
color: "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300",
|
||||||
|
description: "Read-write access to routing policies and users. Can run AI inference with any model. Cannot manage feature flags or access compliance reports.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
color: "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300",
|
||||||
|
description: "Inference only. Restricted to the model list in rbac.user_allowed_models (default: gpt-4o-mini, mistral-medium). No admin API access.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "auditor",
|
||||||
|
color: "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300",
|
||||||
|
description: "Read-only access to audit logs and compliance data. Cannot call /v1/chat/completions. Intended for compliance officers and DPOs.",
|
||||||
|
},
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item.role} className="rounded-lg border bg-card p-4 flex items-start gap-3">
|
||||||
|
<span className={`text-xs font-bold px-2 py-0.5 rounded font-mono shrink-0 ${item.color}`}>
|
||||||
|
{item.role}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="permission-matrix">Permission Matrix</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Endpoint</th>
|
||||||
|
<th className="text-center px-3 py-2.5 font-semibold">admin</th>
|
||||||
|
<th className="text-center px-3 py-2.5 font-semibold">manager</th>
|
||||||
|
<th className="text-center px-3 py-2.5 font-semibold">user</th>
|
||||||
|
<th className="text-center px-3 py-2.5 font-semibold">auditor</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ ep: "POST /v1/chat/completions", admin: "✓", manager: "✓", user: "✓ (limited models)", auditor: "✗" },
|
||||||
|
{ ep: "POST /v1/pii/analyze", admin: "✓", manager: "✓", user: "✓", auditor: "✓" },
|
||||||
|
{ ep: "GET /v1/admin/policies", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
|
||||||
|
{ ep: "POST/PUT/DELETE /v1/admin/policies", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
|
||||||
|
{ ep: "GET/POST/PUT /v1/admin/users", admin: "✓", manager: "read only", user: "✗", auditor: "✗" },
|
||||||
|
{ ep: "GET /v1/admin/logs", admin: "✓", manager: "✓", user: "✗", auditor: "✓" },
|
||||||
|
{ ep: "GET /v1/admin/costs", admin: "✓", manager: "✓", user: "✗", auditor: "✓" },
|
||||||
|
{ ep: "GET /v1/admin/compliance/*", admin: "✓", manager: "✗", user: "✗", auditor: "✓" },
|
||||||
|
{ ep: "POST /v1/admin/compliance/*", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
|
||||||
|
{ ep: "GET/PUT/DELETE /v1/admin/flags", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
|
||||||
|
{ ep: "GET /v1/admin/providers/status", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.ep} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.ep}</td>
|
||||||
|
{[row.admin, row.manager, row.user, row.auditor].map((v, i) => (
|
||||||
|
<td key={i} className={`px-3 py-2.5 text-xs text-center ${v === "✗" ? "text-muted-foreground" : v === "✓" ? "text-green-600 dark:text-green-400 font-medium" : "text-amber-600 dark:text-amber-400"}`}>
|
||||||
|
{v}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="model-restrictions">Model Restrictions</h2>
|
||||||
|
<p>
|
||||||
|
Users with the <code>user</code> role can only access models listed in{" "}
|
||||||
|
<code>rbac.user_allowed_models</code>. Requests to other models are rejected with 403:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`# config.yaml
|
||||||
|
rbac:
|
||||||
|
user_allowed_models:
|
||||||
|
- "gpt-4o-mini"
|
||||||
|
- "mistral-medium-latest"
|
||||||
|
- "claude-3-haiku-20240307"`}
|
||||||
|
/>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`// 403 response when user requests gpt-4o:
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "permission_error",
|
||||||
|
"message": "role 'user' does not have access to model 'gpt-4o'. Allowed: gpt-4o-mini, mistral-medium-latest, claude-3-haiku-20240307",
|
||||||
|
"code": "permission_denied"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Admin and manager bypass model restrictions">
|
||||||
|
The <code>admin</code> and <code>manager</code> roles have unrestricted model access —{" "}
|
||||||
|
<code>user_allowed_models</code> does not apply to them.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="keycloak-setup">Setting Up Roles in Keycloak</h2>
|
||||||
|
<p>Assign roles to users in Keycloak:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Log in to Keycloak Admin Console (http://localhost:8080, admin/admin)</li>
|
||||||
|
<li>Go to <strong>Realm: veylant</strong> → <strong>Users</strong></li>
|
||||||
|
<li>Select a user → <strong>Role Mappings</strong> → <strong>Realm Roles</strong></li>
|
||||||
|
<li>Assign one of: <code>admin</code>, <code>manager</code>, <code>user</code>, <code>auditor</code></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
146
web/src/pages/docs/guides/RoutingGuide.tsx
Normal file
146
web/src/pages/docs/guides/RoutingGuide.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function RoutingGuide() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="routing-rules">Routing Rules Engine</h1>
|
||||||
|
<p>
|
||||||
|
The routing engine matches incoming AI requests against a prioritized list of rules and
|
||||||
|
dispatches them to the appropriate LLM provider. Rules are stored in PostgreSQL (JSONB
|
||||||
|
conditions) and cached in memory.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="evaluation-order">Evaluation Order</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Rules are sorted <strong>ascending by priority</strong> (lower number = evaluated first)</li>
|
||||||
|
<li><strong>First match wins</strong> — once a rule matches, evaluation stops</li>
|
||||||
|
<li>All conditions within a rule are <strong>AND-joined</strong></li>
|
||||||
|
<li>An empty <code>conditions</code> array is a <strong>catch-all</strong> that matches everything</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Callout type="tip" title="Priority design">
|
||||||
|
Use gaps between priorities (10, 20, 30, ...) so you can insert rules later without
|
||||||
|
renumbering. The catch-all rule should have the highest priority number (e.g. 999).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="condition-fields">Condition Fields & Operators</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Field</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Supported Operators</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Value Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ field: "user.role", ops: "eq, neq, in, nin", type: "string" },
|
||||||
|
{ field: "user.department", ops: "eq, neq, in, nin, contains", type: "string" },
|
||||||
|
{ field: "request.model", ops: "eq, neq, in, nin", type: "string (model ID)" },
|
||||||
|
{ field: "request.sensitivity", ops: "eq, neq, gte, lte", type: "low | medium | high" },
|
||||||
|
{ field: "request.use_case", ops: "eq, neq, contains, matches", type: "string" },
|
||||||
|
{ field: "request.token_estimate", ops: "gte, lte", type: "integer (token count)" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.field} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.field}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.ops}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.type}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="examples">Example Rule Configurations</h2>
|
||||||
|
|
||||||
|
<h3 id="example-department">Route legal department to Anthropic</h3>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`{
|
||||||
|
"name": "Legal → Claude",
|
||||||
|
"priority": 10,
|
||||||
|
"provider": "anthropic",
|
||||||
|
"target_model": "claude-3-5-sonnet-20241022",
|
||||||
|
"fallback_providers": ["openai"],
|
||||||
|
"conditions": [
|
||||||
|
{"field": "user.department", "operator": "eq", "value": "legal"}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 id="example-cost-optimize">Cost-optimize small requests</h3>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`{
|
||||||
|
"name": "Small requests → GPT-4o-mini",
|
||||||
|
"priority": 20,
|
||||||
|
"provider": "openai",
|
||||||
|
"target_model": "gpt-4o-mini",
|
||||||
|
"conditions": [
|
||||||
|
{"field": "request.token_estimate", "operator": "lte", "value": "1000"},
|
||||||
|
{"field": "request.sensitivity", "operator": "eq", "value": "low"}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 id="example-multi-condition">HR department, high-sensitivity requests</h3>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`{
|
||||||
|
"name": "HR high-sensitivity → Azure GPT-4",
|
||||||
|
"priority": 15,
|
||||||
|
"provider": "azure",
|
||||||
|
"fallback_providers": ["anthropic", "openai"],
|
||||||
|
"conditions": [
|
||||||
|
{"field": "user.department", "operator": "eq", "value": "hr"},
|
||||||
|
{"field": "request.sensitivity", "operator": "eq", "value": "high"}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 id="example-catchall">Catch-all fallback</h3>
|
||||||
|
<CodeBlock
|
||||||
|
language="json"
|
||||||
|
code={`{
|
||||||
|
"name": "Default → OpenAI GPT-4o",
|
||||||
|
"priority": 999,
|
||||||
|
"provider": "openai",
|
||||||
|
"target_model": "gpt-4o",
|
||||||
|
"fallback_providers": ["mistral"],
|
||||||
|
"conditions": []
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="fallback-chain">Fallback Chain</h2>
|
||||||
|
<p>
|
||||||
|
The <code>fallback_providers</code> array defines ordered fallbacks. The routing engine
|
||||||
|
tries providers in order:
|
||||||
|
</p>
|
||||||
|
<ol>
|
||||||
|
<li>Check if primary provider's circuit breaker is open → skip if open</li>
|
||||||
|
<li>Try primary provider → if error, try next fallback</li>
|
||||||
|
<li>Continue through the fallback chain</li>
|
||||||
|
<li>If all providers fail → return 503</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2 id="caching">Rule Caching</h2>
|
||||||
|
<p>
|
||||||
|
Rules are loaded from PostgreSQL and cached in memory. The cache is invalidated when a
|
||||||
|
rule is created, updated, or deleted via the admin API. You can also force a reload:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Restart the proxy to force rule cache reload
|
||||||
|
# (or wait for the TTL refresh — default 60s)
|
||||||
|
docker compose restart proxy`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="info" title="Rule cache TTL">
|
||||||
|
The in-memory rule cache refreshes every 60 seconds from PostgreSQL. Admin API changes
|
||||||
|
(create/update/delete) invalidate the cache immediately via a shared channel.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
web/src/pages/docs/installation/ConfigurationPage.tsx
Normal file
152
web/src/pages/docs/installation/ConfigurationPage.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
import { ParamTable } from "../components/ParamTable";
|
||||||
|
|
||||||
|
export function ConfigurationPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="configuration">Configuration Reference</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA is configured via <code>config.yaml</code> at the repository root. Any key can
|
||||||
|
be overridden via an environment variable using the <code>VEYLANT_</code> prefix and
|
||||||
|
replacing <code>.</code> with <code>_</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="Environment variable override">
|
||||||
|
<code>server.port: 9000</code> → <code>VEYLANT_SERVER_PORT=9000</code>
|
||||||
|
<br />
|
||||||
|
<code>pii.timeout_ms: 200</code> → <code>VEYLANT_PII_TIMEOUT_MS=200</code>
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="full-config">Full config.yaml</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`server:
|
||||||
|
port: 8090
|
||||||
|
env: development # development | production
|
||||||
|
read_timeout: 30s
|
||||||
|
write_timeout: 60s
|
||||||
|
allowed_origins:
|
||||||
|
- "http://localhost:3000"
|
||||||
|
|
||||||
|
database:
|
||||||
|
url: "postgres://veylant:veylant@localhost:5432/veylant?sslmode=disable"
|
||||||
|
max_open_conns: 25
|
||||||
|
max_idle_conns: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
url: "redis://localhost:6379"
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
dsn: "clickhouse://localhost:9000/veylant?dial_timeout=5s"
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
base_url: "http://localhost:8080"
|
||||||
|
realm: "veylant"
|
||||||
|
client_id: "veylant-proxy"
|
||||||
|
|
||||||
|
pii:
|
||||||
|
enabled: true
|
||||||
|
grpc_addr: "localhost:50051"
|
||||||
|
timeout_ms: 100
|
||||||
|
fail_open: true # if true, continue on PII service failure
|
||||||
|
|
||||||
|
providers:
|
||||||
|
openai:
|
||||||
|
api_key: "" # or VEYLANT_PROVIDERS_OPENAI_API_KEY
|
||||||
|
base_url: "https://api.openai.com/v1"
|
||||||
|
anthropic:
|
||||||
|
api_key: ""
|
||||||
|
base_url: "https://api.anthropic.com"
|
||||||
|
azure:
|
||||||
|
api_key: ""
|
||||||
|
endpoint: ""
|
||||||
|
api_version: "2024-02-01"
|
||||||
|
mistral:
|
||||||
|
api_key: ""
|
||||||
|
base_url: "https://api.mistral.ai/v1"
|
||||||
|
ollama:
|
||||||
|
base_url: "http://localhost:11434"
|
||||||
|
|
||||||
|
rbac:
|
||||||
|
user_allowed_models:
|
||||||
|
- "gpt-4o-mini"
|
||||||
|
- "mistral-medium"
|
||||||
|
# admin and manager have unrestricted model access
|
||||||
|
|
||||||
|
circuit_breaker:
|
||||||
|
threshold: 5 # failures before opening
|
||||||
|
open_ttl: 60s # how long to stay open
|
||||||
|
|
||||||
|
rate_limit:
|
||||||
|
default_rpm: 1000 # requests per minute per tenant
|
||||||
|
default_tpm: 100000 # tokens per minute per tenant
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
path: "/metrics"
|
||||||
|
|
||||||
|
audit:
|
||||||
|
batch_size: 100
|
||||||
|
flush_interval: 5s
|
||||||
|
|
||||||
|
crypto:
|
||||||
|
key: "" # 32-byte AES key (base64), REQUIRED in production
|
||||||
|
|
||||||
|
vault:
|
||||||
|
enabled: false
|
||||||
|
addr: "http://localhost:8200"
|
||||||
|
token: ""`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="server">Server Settings</h2>
|
||||||
|
<ParamTable
|
||||||
|
title="server.*"
|
||||||
|
params={[
|
||||||
|
{ name: "server.port", type: "int", required: false, default: "8090", description: "HTTP server port" },
|
||||||
|
{ name: "server.env", type: "string", required: false, default: "development", description: "Environment: development | production. In production, all service failures are fatal." },
|
||||||
|
{ name: "server.allowed_origins", type: "[]string", required: false, default: '["http://localhost:3000"]', description: "CORS allowed origins for /v1/* routes" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="pii-config">PII Settings</h2>
|
||||||
|
<ParamTable
|
||||||
|
title="pii.*"
|
||||||
|
params={[
|
||||||
|
{ name: "pii.enabled", type: "bool", required: false, default: "true", description: "Enable PII detection. When false, prompts are forwarded as-is." },
|
||||||
|
{ name: "pii.grpc_addr", type: "string", required: false, default: "localhost:50051", description: "gRPC address of the PII service" },
|
||||||
|
{ name: "pii.timeout_ms", type: "int", required: false, default: "100", description: "PII detection timeout in milliseconds (latency budget: <50ms)" },
|
||||||
|
{ name: "pii.fail_open", type: "bool", required: false, default: "true", description: "If true, continue on PII failure (request proceeds unredacted). If false, return 503." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="production-checklist">Production Checklist</h2>
|
||||||
|
<Callout type="warning" title="Required in production">
|
||||||
|
When <code>server.env=production</code>, these values must be set or the proxy fails to
|
||||||
|
start:
|
||||||
|
</Callout>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>crypto.key</code> — 32-byte AES-256 key (base64 encoded)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>database.url</code> — PostgreSQL DSN with TLS
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>keycloak.base_url</code> — reachable Keycloak instance
|
||||||
|
</li>
|
||||||
|
<li>At least one provider API key</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Generate a secure AES-256 key
|
||||||
|
openssl rand -base64 32
|
||||||
|
|
||||||
|
# Set in environment (never commit to git)
|
||||||
|
export VEYLANT_CRYPTO_KEY="$(openssl rand -base64 32)"`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
web/src/pages/docs/installation/DockerComposePage.tsx
Normal file
133
web/src/pages/docs/installation/DockerComposePage.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function DockerComposePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="docker-compose">Docker Compose Setup</h1>
|
||||||
|
<p>
|
||||||
|
The recommended way to run Veylant IA locally or in a single-server staging environment is
|
||||||
|
Docker Compose. The full stack is defined in <code>docker-compose.yml</code> at the
|
||||||
|
repository root.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="services">Services</h2>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Service</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Image</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Port</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Purpose</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ service: "proxy", image: "Custom (Go, distroless)", port: "8090", purpose: "Main AI gateway" },
|
||||||
|
{ service: "pii", image: "Custom (Python FastAPI)", port: "8091 / 50051", purpose: "PII detection" },
|
||||||
|
{ service: "postgres", image: "postgres:16-alpine", port: "5432", purpose: "Config, users, policies" },
|
||||||
|
{ service: "redis", image: "redis:7-alpine", port: "6379", purpose: "Sessions, rate limits, PII maps" },
|
||||||
|
{ service: "clickhouse", image: "clickhouse:24.3-alpine", port: "8123 / 9000", purpose: "Audit logs & analytics" },
|
||||||
|
{ service: "keycloak", image: "keycloak:24.0", port: "8080", purpose: "IAM & SSO" },
|
||||||
|
{ service: "prometheus", image: "prom/prometheus:v2.53.0", port: "9090", purpose: "Metrics scraper" },
|
||||||
|
{ service: "grafana", image: "grafana:11.3.0", port: "3001", purpose: "Dashboards" },
|
||||||
|
{ service: "web", image: "node:20-alpine", port: "3000", purpose: "React dashboard" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.service} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs font-medium">{row.service}</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.image}</td>
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.port}</td>
|
||||||
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.purpose}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="commands">Make Commands</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Start the full stack (build images + run)
|
||||||
|
make dev
|
||||||
|
|
||||||
|
# Tail logs from all services
|
||||||
|
make dev-logs
|
||||||
|
|
||||||
|
# Stop all containers and remove volumes
|
||||||
|
make dev-down
|
||||||
|
|
||||||
|
# Check proxy health
|
||||||
|
make health
|
||||||
|
|
||||||
|
# Open Swagger UI in browser (proxy must be running)
|
||||||
|
make docs`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="startup-order">Startup Order & Health Checks</h2>
|
||||||
|
<p>Services start in dependency order:</p>
|
||||||
|
<ol>
|
||||||
|
<li>PostgreSQL → Redis → ClickHouse (databases)</li>
|
||||||
|
<li>Keycloak (waits for PostgreSQL health check)</li>
|
||||||
|
<li>PII service (independent)</li>
|
||||||
|
<li>Go proxy (waits for PostgreSQL, uses <code>service_started</code> for others)</li>
|
||||||
|
<li>React web (waits for proxy)</li>
|
||||||
|
<li>Prometheus → Grafana (monitoring)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<Callout type="warning" title="Proxy health check">
|
||||||
|
The proxy Docker image uses <code>distroless/static</code> — no shell, no{" "}
|
||||||
|
<code>wget</code>. Services that depend on the proxy use{" "}
|
||||||
|
<code>condition: service_started</code> rather than a health check command.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="first-run">First Run: Database Migrations</h2>
|
||||||
|
<p>
|
||||||
|
On first start, the proxy automatically applies PostgreSQL migrations (9 migration files)
|
||||||
|
and ClickHouse DDL. You can also run migrations manually:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Apply all pending migrations
|
||||||
|
make migrate-up
|
||||||
|
|
||||||
|
# Check current migration version
|
||||||
|
make migrate-status
|
||||||
|
|
||||||
|
# Roll back last migration
|
||||||
|
make migrate-down`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="proto">Protocol Buffer Generation</h2>
|
||||||
|
<p>
|
||||||
|
If the <code>gen/</code> or <code>services/pii/gen/</code> directories are missing (e.g.,
|
||||||
|
fresh clone), regenerate the gRPC stubs before starting:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Requires buf CLI: brew install buf
|
||||||
|
make proto
|
||||||
|
|
||||||
|
# Lint proto definitions
|
||||||
|
make proto-lint`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
The PII service starts but rejects all gRPC requests if <code>services/pii/gen/</code> is
|
||||||
|
missing. Run <code>make proto</code> first.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="logs">Viewing Logs</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# All services
|
||||||
|
make dev-logs
|
||||||
|
|
||||||
|
# Single service
|
||||||
|
docker compose logs -f proxy
|
||||||
|
docker compose logs -f pii
|
||||||
|
docker compose logs -f postgres`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
web/src/pages/docs/installation/ProvidersPage.tsx
Normal file
116
web/src/pages/docs/installation/ProvidersPage.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function ProvidersPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="provider-setup">Provider Setup</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA supports 5 LLM providers out of the box. Each provider implements the{" "}
|
||||||
|
<code>provider.Adapter</code> interface (<code>Send</code>, <code>Stream</code>,{" "}
|
||||||
|
<code>Validate</code>, <code>HealthCheck</code>).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Callout type="info" title="Adding a new provider">
|
||||||
|
Implement the <code>provider.Adapter</code> interface in{" "}
|
||||||
|
<code>internal/provider/<name>/</code> and register it in{" "}
|
||||||
|
<code>cmd/proxy/main.go</code>. No other changes needed.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="openai">OpenAI</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`providers:
|
||||||
|
openai:
|
||||||
|
api_key: "sk-..." # VEYLANT_PROVIDERS_OPENAI_API_KEY
|
||||||
|
base_url: "https://api.openai.com/v1" # optional override`}
|
||||||
|
/>
|
||||||
|
<p>Supported models: <code>gpt-4o</code>, <code>gpt-4o-mini</code>, <code>gpt-4-turbo</code>, <code>gpt-3.5-turbo</code></p>
|
||||||
|
|
||||||
|
<h2 id="anthropic">Anthropic</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`providers:
|
||||||
|
anthropic:
|
||||||
|
api_key: "sk-ant-..." # VEYLANT_PROVIDERS_ANTHROPIC_API_KEY
|
||||||
|
base_url: "https://api.anthropic.com" # optional`}
|
||||||
|
/>
|
||||||
|
<p>Supported models: <code>claude-3-5-sonnet-20241022</code>, <code>claude-3-opus-20240229</code>, <code>claude-3-haiku-20240307</code></p>
|
||||||
|
|
||||||
|
<h2 id="azure">Azure OpenAI</h2>
|
||||||
|
<p>
|
||||||
|
Azure requires a deployment name instead of a model name. The proxy maps OpenAI model IDs
|
||||||
|
to Azure deployment names via the routing rule's <code>target_model</code> field.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`providers:
|
||||||
|
azure:
|
||||||
|
api_key: "..." # VEYLANT_PROVIDERS_AZURE_API_KEY
|
||||||
|
endpoint: "https://YOUR_RESOURCE.openai.azure.com"
|
||||||
|
api_version: "2024-02-01" # Azure API version`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="mistral">Mistral AI</h2>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`providers:
|
||||||
|
mistral:
|
||||||
|
api_key: "..." # VEYLANT_PROVIDERS_MISTRAL_API_KEY
|
||||||
|
base_url: "https://api.mistral.ai/v1" # optional`}
|
||||||
|
/>
|
||||||
|
<p>Supported models: <code>mistral-large-latest</code>, <code>mistral-medium-latest</code>, <code>mistral-small-latest</code></p>
|
||||||
|
|
||||||
|
<h2 id="ollama">Ollama (Self-hosted)</h2>
|
||||||
|
<p>
|
||||||
|
Ollama requires no API key. Ensure the Ollama server is reachable from the proxy container.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="yaml"
|
||||||
|
code={`providers:
|
||||||
|
ollama:
|
||||||
|
base_url: "http://host.docker.internal:11434" # from Docker to host`}
|
||||||
|
/>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Pull a model before routing to it
|
||||||
|
ollama pull llama3
|
||||||
|
ollama pull mistral
|
||||||
|
ollama pull codellama`}
|
||||||
|
/>
|
||||||
|
<Callout type="tip" title="GPU acceleration">
|
||||||
|
Ollama automatically uses GPU if available. For Docker, add{" "}
|
||||||
|
<code>--gpus all</code> to the container runtime command or use the{" "}
|
||||||
|
<code>deploy.resources.reservations.devices</code> key in docker-compose.yml.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="provider-status">Check Provider Status</h2>
|
||||||
|
<p>
|
||||||
|
The admin API exposes circuit breaker state for all providers:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`curl http://localhost:8090/v1/admin/providers/status \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{"provider": "openai", "state": "closed", "failures": 0},
|
||||||
|
{"provider": "anthropic", "state": "closed", "failures": 0},
|
||||||
|
{"provider": "azure", "state": "open", "failures": 5},
|
||||||
|
{"provider": "mistral", "state": "closed", "failures": 1},
|
||||||
|
{"provider": "ollama", "state": "closed", "failures": 0}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>Circuit breaker states:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>closed</strong> — Normal operation, requests forwarded</li>
|
||||||
|
<li><strong>open</strong> — Provider bypassed, fallback chain used</li>
|
||||||
|
<li><strong>half-open</strong> — Testing if provider has recovered</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
web/src/pages/docs/nav.ts
Normal file
88
web/src/pages/docs/nav.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
export interface NavItem {
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavSection {
|
||||||
|
title: string;
|
||||||
|
items: NavItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NAV_SECTIONS: NavSection[] = [
|
||||||
|
{
|
||||||
|
title: "Getting Started",
|
||||||
|
items: [
|
||||||
|
{ title: "What is Veylant IA?", path: "/docs/getting-started/what-is-veylant" },
|
||||||
|
{ title: "Quick Start", path: "/docs/getting-started/quick-start" },
|
||||||
|
{ title: "Key Concepts", path: "/docs/getting-started/concepts" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Installation",
|
||||||
|
items: [
|
||||||
|
{ title: "Docker Compose", path: "/docs/installation/docker" },
|
||||||
|
{ title: "Configuration", path: "/docs/installation/configuration" },
|
||||||
|
{ title: "Provider Setup", path: "/docs/installation/providers" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "API Reference",
|
||||||
|
items: [
|
||||||
|
{ title: "Authentication", path: "/docs/api/authentication" },
|
||||||
|
{ title: "Chat Completions", path: "/docs/api/chat-completions" },
|
||||||
|
{ title: "PII Analysis", path: "/docs/api/pii" },
|
||||||
|
{ title: "Admin — Policies", path: "/docs/api/admin/policies" },
|
||||||
|
{ title: "Admin — Users", path: "/docs/api/admin/users" },
|
||||||
|
{ title: "Admin — Audit Logs", path: "/docs/api/admin/logs" },
|
||||||
|
{ title: "Admin — Compliance", path: "/docs/api/admin/compliance" },
|
||||||
|
{ title: "Admin — Feature Flags", path: "/docs/api/admin/flags" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Guides",
|
||||||
|
items: [
|
||||||
|
{ title: "PII Detection & Anonymization", path: "/docs/guides/pii" },
|
||||||
|
{ title: "Routing Rules Engine", path: "/docs/guides/routing" },
|
||||||
|
{ title: "RBAC & Permissions", path: "/docs/guides/rbac" },
|
||||||
|
{ title: "GDPR & AI Act Compliance", path: "/docs/guides/compliance" },
|
||||||
|
{ title: "Monitoring & Alerting", path: "/docs/guides/monitoring" },
|
||||||
|
{ title: "Circuit Breaker & Failover", path: "/docs/guides/circuit-breaker" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Deployment",
|
||||||
|
items: [
|
||||||
|
{ title: "Docker Compose", path: "/docs/deployment/docker" },
|
||||||
|
{ title: "Kubernetes (Helm)", path: "/docs/deployment/kubernetes" },
|
||||||
|
{ title: "Blue/Green Deployment", path: "/docs/deployment/blue-green" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Security",
|
||||||
|
items: [
|
||||||
|
{ title: "Security Model", path: "/docs/security/model" },
|
||||||
|
{ title: "API Key Management", path: "/docs/security/api-keys" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Changelog",
|
||||||
|
items: [{ title: "v1.0.0 (Latest)", path: "/docs/changelog" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Flatten all nav items into a list, preserving section context */
|
||||||
|
export const ALL_NAV_ITEMS: (NavItem & { section: string })[] = NAV_SECTIONS.flatMap((s) =>
|
||||||
|
s.items.map((item) => ({ ...item, section: s.title }))
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Get previous and next pages for pagination */
|
||||||
|
export function getPrevNext(currentPath: string): {
|
||||||
|
prev: NavItem | null;
|
||||||
|
next: NavItem | null;
|
||||||
|
} {
|
||||||
|
const idx = ALL_NAV_ITEMS.findIndex((i) => i.path === currentPath);
|
||||||
|
return {
|
||||||
|
prev: idx > 0 ? ALL_NAV_ITEMS[idx - 1] : null,
|
||||||
|
next: idx < ALL_NAV_ITEMS.length - 1 ? ALL_NAV_ITEMS[idx + 1] : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
86
web/src/pages/docs/security/ApiKeysPage.tsx
Normal file
86
web/src/pages/docs/security/ApiKeysPage.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { CodeBlock } from "../components/CodeBlock";
|
||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function ApiKeysPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="api-keys">API Key Management</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA never stores plain-text API keys. Provider keys (OpenAI, Anthropic, etc.) are
|
||||||
|
stored in HashiCorp Vault and rotated on a 90-day cycle. User-facing API keys use a
|
||||||
|
prefix+hash scheme.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="key-format">API Key Format</h2>
|
||||||
|
<p>
|
||||||
|
Veylant IA user API keys follow the format <code>sk-vyl_{"{prefix}"}{"{hash}"}</code>:
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="text"
|
||||||
|
code={`sk-vyl_ab12cd34ef56gh78ij90kl12mn34op56qr78st90
|
||||||
|
|
||||||
|
─────┬──── ──────┬──── ──────────────────────────────────────────
|
||||||
|
prefix display hash (SHA-256, truncated, stored in DB)
|
||||||
|
prefix`}
|
||||||
|
/>
|
||||||
|
<ul>
|
||||||
|
<li>The display prefix (<code>ab12cd34</code>) is stored in plaintext for key identification in the dashboard</li>
|
||||||
|
<li>The full key is SHA-256 hashed; only the hash is stored in PostgreSQL</li>
|
||||||
|
<li>If a key is compromised, only the prefix reveals which key to revoke — not the key value</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="provider-keys">Provider API Keys</h2>
|
||||||
|
<Callout type="warning" title="Never commit provider keys">
|
||||||
|
Provider API keys (OpenAI, Anthropic, etc.) must never be committed to the repository.
|
||||||
|
Use environment variables or HashiCorp Vault.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Set via environment variable (recommended for docker-compose)
|
||||||
|
export VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-...
|
||||||
|
|
||||||
|
# Or load from HashiCorp Vault
|
||||||
|
vault kv put secret/veylant/providers openai_api_key=sk-... anthropic_api_key=sk-ant-...
|
||||||
|
|
||||||
|
# Vault agent sidecar injects secrets as environment variables at runtime`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="rotation">Key Rotation</h2>
|
||||||
|
<p>Provider API keys are rotated on a 90-day cycle via Vault:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Generate new API key from the provider portal</li>
|
||||||
|
<li>Write new key to Vault: <code>vault kv patch secret/veylant/providers openai_api_key=sk-new...</code></li>
|
||||||
|
<li>Vault agent syncs the new value to running pods automatically (no restart needed)</li>
|
||||||
|
<li>Revoke the old key from the provider portal</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2 id="secret-detection">Secret Detection in CI</h2>
|
||||||
|
<p>
|
||||||
|
Every commit is scanned by <strong>gitleaks</strong> for accidentally committed secrets.
|
||||||
|
Any string matching common API key patterns (starting with <code>sk-</code>,{" "}
|
||||||
|
<code>sk-ant-</code>, etc.) blocks the CI pipeline.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Run gitleaks locally before committing
|
||||||
|
gitleaks detect --source .
|
||||||
|
|
||||||
|
# The Semgrep rule also catches sk- literals in Go/Python source:
|
||||||
|
# ID: hardcoded-api-key`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 id="audit">Key Usage Auditing</h2>
|
||||||
|
<p>
|
||||||
|
Every AI request records which API key (by prefix) was used in the ClickHouse audit log.
|
||||||
|
This allows you to trace the source of any request to a specific service or developer.
|
||||||
|
</p>
|
||||||
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code={`# Find all requests from a specific API key prefix
|
||||||
|
curl "http://localhost:8090/v1/admin/logs?api_key_prefix=ab12cd34" \\
|
||||||
|
-H "Authorization: Bearer $TOKEN"`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
web/src/pages/docs/security/SecurityModelPage.tsx
Normal file
125
web/src/pages/docs/security/SecurityModelPage.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { Callout } from "../components/Callout";
|
||||||
|
|
||||||
|
export function SecurityModelPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 id="security-model">Security Model</h1>
|
||||||
|
<p>
|
||||||
|
Veylant IA is designed with a Zero Trust security model. Every component assumes the
|
||||||
|
network is hostile and authenticates and authorizes each request independently.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="zero-trust">Zero Trust Architecture</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 my-4">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "mTLS Between Services",
|
||||||
|
desc: "All internal service-to-service communication uses mutual TLS. The proxy, PII service, PostgreSQL, Redis, and ClickHouse all authenticate each other via certificates.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "TLS 1.3 Externally",
|
||||||
|
desc: "External traffic uses TLS 1.3 minimum. TLS 1.0 and 1.1 are disabled at the Traefik/nginx gateway level.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "JWT Validation",
|
||||||
|
desc: "Every API request carries a signed JWT. The proxy validates the signature against Keycloak's JWKS endpoint on every request (cached with TTL).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Network Policies",
|
||||||
|
desc: "Kubernetes NetworkPolicies restrict pod-to-pod communication. Only the proxy can reach the PII service; only Prometheus can scrape /metrics.",
|
||||||
|
},
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item.title} className="rounded-lg border bg-card p-4">
|
||||||
|
<h3 className="font-semibold text-sm mb-2">{item.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="encryption">Encryption at Rest</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Prompt storage</strong> — Encrypted with AES-256-GCM using the{" "}
|
||||||
|
<code>crypto.key</code> config value (application-level, independent of disk encryption)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>PII pseudonymization mappings</strong> — Encrypted in Redis with AES-256-GCM per
|
||||||
|
mapping entry
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>API keys</strong> — Stored as SHA-256 hashes only. The prefix (e.g.{" "}
|
||||||
|
<code>sk-vyl_ab12cd34</code>) is kept for display, but the full key is never stored
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>HashiCorp Vault</strong> — Provider API keys and the crypto key are stored in
|
||||||
|
Vault; 90-day rotation cycle
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="audit-security">Audit-of-the-Audit</h2>
|
||||||
|
<p>
|
||||||
|
All accesses to audit logs and compliance reports are themselves logged. This two-level
|
||||||
|
audit trail satisfies requirements for sensitive data access monitoring.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="semgrep">Custom Security Rules (SAST)</h2>
|
||||||
|
<p>
|
||||||
|
CI enforces custom Semgrep rules (<code>.semgrep.yml</code>) that catch common security
|
||||||
|
issues specific to this codebase:
|
||||||
|
</p>
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="w-full text-sm border rounded-lg overflow-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b">
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">Rule</th>
|
||||||
|
<th className="text-left px-4 py-2.5 font-semibold">What it catches</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ rule: "context-background-in-handler", catches: "context.Background() in HTTP handlers — use r.Context() to propagate cancellation" },
|
||||||
|
{ rule: "sql-string-concat", catches: "SQL string concatenation — use parameterized queries ($1, $2, ...)" },
|
||||||
|
{ rule: "sensitive-field-in-log", catches: "Logging password, api_key, token, secret, Authorization, email, prompt" },
|
||||||
|
{ rule: "hardcoded-api-key", catches: "String literals starting with sk- hardcoded in source" },
|
||||||
|
{ rule: "request-body-without-limit", catches: "json.NewDecoder(r.Body) without http.MaxBytesReader" },
|
||||||
|
{ rule: "python-eval-exec", catches: "eval() or exec() on variables in the PII service" },
|
||||||
|
].map((row) => (
|
||||||
|
<tr key={row.rule} className="border-b last:border-0">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-xs">{row.rule}</td>
|
||||||
|
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.catches}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="pentest">Penetration Test Results (v1.0.0)</h2>
|
||||||
|
<Callout type="tip" title="Pentest passed — February 2026">
|
||||||
|
Grey-box penetration test completed June 9–20, 2026. Results:
|
||||||
|
<ul className="mt-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Critical:</strong> 0
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>High:</strong> 0
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Medium:</strong> 2 (remediated before launch)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Low:</strong> 3 (accepted risk, backlog)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
Full report available to enterprise customers under NDA.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<h2 id="ci-pipeline">CI Security Pipeline</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Semgrep</strong> — SAST on every PR</li>
|
||||||
|
<li><strong>Trivy</strong> — Container image scan (CRITICAL/HIGH blocking)</li>
|
||||||
|
<li><strong>gitleaks</strong> — Secret detection in commits and history</li>
|
||||||
|
<li><strong>OWASP ZAP</strong> — DAST (non-blocking, main branch only)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,6 +15,36 @@ import { CostsPage } from "@/pages/CostsPage";
|
|||||||
import { CompliancePage } from "@/pages/CompliancePage";
|
import { CompliancePage } from "@/pages/CompliancePage";
|
||||||
import { NotFoundPage } from "@/pages/NotFoundPage";
|
import { NotFoundPage } from "@/pages/NotFoundPage";
|
||||||
|
|
||||||
|
// Documentation site
|
||||||
|
import { DocLayout } from "@/pages/docs/DocLayout";
|
||||||
|
import { DocsHomePage } from "@/pages/docs/DocsHomePage";
|
||||||
|
import { WhatIsVeylantPage } from "@/pages/docs/getting-started/WhatIsVeylantPage";
|
||||||
|
import { QuickStartPage } from "@/pages/docs/getting-started/QuickStartPage";
|
||||||
|
import { KeyConceptsPage } from "@/pages/docs/getting-started/KeyConceptsPage";
|
||||||
|
import { DockerComposePage } from "@/pages/docs/installation/DockerComposePage";
|
||||||
|
import { ConfigurationPage } from "@/pages/docs/installation/ConfigurationPage";
|
||||||
|
import { ProvidersPage as DocProvidersPage } from "@/pages/docs/installation/ProvidersPage";
|
||||||
|
import { AuthenticationPage } from "@/pages/docs/api-reference/AuthenticationPage";
|
||||||
|
import { ChatCompletionsPage } from "@/pages/docs/api-reference/ChatCompletionsPage";
|
||||||
|
import { PiiAnalysisPage } from "@/pages/docs/api-reference/PiiAnalysisPage";
|
||||||
|
import { AdminPoliciesPage } from "@/pages/docs/api-reference/AdminPoliciesPage";
|
||||||
|
import { AdminUsersPage } from "@/pages/docs/api-reference/AdminUsersPage";
|
||||||
|
import { AdminLogsPage } from "@/pages/docs/api-reference/AdminLogsPage";
|
||||||
|
import { AdminCompliancePage } from "@/pages/docs/api-reference/AdminCompliancePage";
|
||||||
|
import { AdminFlagsPage } from "@/pages/docs/api-reference/AdminFlagsPage";
|
||||||
|
import { PiiGuide } from "@/pages/docs/guides/PiiGuide";
|
||||||
|
import { RoutingGuide } from "@/pages/docs/guides/RoutingGuide";
|
||||||
|
import { RbacGuide } from "@/pages/docs/guides/RbacGuide";
|
||||||
|
import { ComplianceGuide } from "@/pages/docs/guides/ComplianceGuide";
|
||||||
|
import { MonitoringGuide } from "@/pages/docs/guides/MonitoringGuide";
|
||||||
|
import { CircuitBreakerGuide } from "@/pages/docs/guides/CircuitBreakerGuide";
|
||||||
|
import { DockerPage } from "@/pages/docs/deployment/DockerPage";
|
||||||
|
import { KubernetesPage } from "@/pages/docs/deployment/KubernetesPage";
|
||||||
|
import { BlueGreenPage } from "@/pages/docs/deployment/BlueGreenPage";
|
||||||
|
import { SecurityModelPage } from "@/pages/docs/security/SecurityModelPage";
|
||||||
|
import { ApiKeysPage } from "@/pages/docs/security/ApiKeysPage";
|
||||||
|
import { ChangelogPage } from "@/pages/docs/ChangelogPage";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
@ -50,4 +80,45 @@ export const router = createBrowserRouter([
|
|||||||
{ path: "*", element: <NotFoundPage /> },
|
{ path: "*", element: <NotFoundPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Documentation site — public, no auth required
|
||||||
|
{
|
||||||
|
path: "/docs",
|
||||||
|
element: <DocLayout />,
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <DocsHomePage /> },
|
||||||
|
// Getting Started
|
||||||
|
{ path: "getting-started/what-is-veylant", element: <WhatIsVeylantPage /> },
|
||||||
|
{ path: "getting-started/quick-start", element: <QuickStartPage /> },
|
||||||
|
{ path: "getting-started/concepts", element: <KeyConceptsPage /> },
|
||||||
|
// Installation
|
||||||
|
{ path: "installation/docker", element: <DockerComposePage /> },
|
||||||
|
{ path: "installation/configuration", element: <ConfigurationPage /> },
|
||||||
|
{ path: "installation/providers", element: <DocProvidersPage /> },
|
||||||
|
// API Reference
|
||||||
|
{ path: "api/authentication", element: <AuthenticationPage /> },
|
||||||
|
{ path: "api/chat-completions", element: <ChatCompletionsPage /> },
|
||||||
|
{ path: "api/pii", element: <PiiAnalysisPage /> },
|
||||||
|
{ path: "api/admin/policies", element: <AdminPoliciesPage /> },
|
||||||
|
{ path: "api/admin/users", element: <AdminUsersPage /> },
|
||||||
|
{ path: "api/admin/logs", element: <AdminLogsPage /> },
|
||||||
|
{ path: "api/admin/compliance", element: <AdminCompliancePage /> },
|
||||||
|
{ path: "api/admin/flags", element: <AdminFlagsPage /> },
|
||||||
|
// Guides
|
||||||
|
{ path: "guides/pii", element: <PiiGuide /> },
|
||||||
|
{ path: "guides/routing", element: <RoutingGuide /> },
|
||||||
|
{ path: "guides/rbac", element: <RbacGuide /> },
|
||||||
|
{ path: "guides/compliance", element: <ComplianceGuide /> },
|
||||||
|
{ path: "guides/monitoring", element: <MonitoringGuide /> },
|
||||||
|
{ path: "guides/circuit-breaker", element: <CircuitBreakerGuide /> },
|
||||||
|
// Deployment
|
||||||
|
{ path: "deployment/docker", element: <DockerPage /> },
|
||||||
|
{ path: "deployment/kubernetes", element: <KubernetesPage /> },
|
||||||
|
{ path: "deployment/blue-green", element: <BlueGreenPage /> },
|
||||||
|
// Security
|
||||||
|
{ path: "security/model", element: <SecurityModelPage /> },
|
||||||
|
{ path: "security/api-keys", element: <ApiKeysPage /> },
|
||||||
|
// Changelog
|
||||||
|
{ path: "changelog", element: <ChangelogPage /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("@tailwindcss/typography")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user