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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ RADA_EVENT_BUS_MODE=inmemory

RADA_REDIS_URL=redis://localhost:6379/0
RADA_DATABASE_URL=postgresql+asyncpg://rada:rada@localhost:5432/rada
RADA_SQLITE_URL=sqlite+aiosqlite:///./rada.db
RADA_SQLITE_URL=sqlite:///./rada.db
RADA_API_KEY=

# Change these in real environments.
POSTGRES_USER=rada
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,26 @@ jobs:

- name: Unit tests
run: pytest tests/unit -q --strict-markers -m "not gpu and not integration"

- name: Integration tests
run: pytest tests/integration -q --strict-markers -m integration

dashboard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: dashboard/package-lock.json

- name: Install dashboard dependencies
working-directory: dashboard
run: npm ci

- name: Build dashboard
working-directory: dashboard
run: npm run build
8 changes: 8 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Integration (manual)

on:
workflow_dispatch:
pull_request:
branches: [main, develop]

jobs:
integration:
Expand All @@ -27,3 +29,9 @@ jobs:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t rada:ci .
- name: Smoke test image health
run: |
docker run -d --name rada-ci -p 18000:8000 rada:ci
sleep 5
curl -fsS http://127.0.0.1:18000/health
docker rm -f rada-ci
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ down:
docker compose down

test:
pytest
pytest tests/unit -m "not gpu and not integration"

lint:
ruff check .
24 changes: 22 additions & 2 deletions apps/streamlit/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import json
import os

import httpx
Expand All @@ -10,6 +11,14 @@
from rada.utils.metrics import get_metrics_snapshot

API_URL = os.getenv("RADA_API_URL", "http://localhost:8000")
API_KEY = os.getenv("RADA_API_KEY", "")


def _headers() -> dict[str, str]:
headers: dict[str, str] = {}
if API_KEY:
headers["X-API-Key"] = API_KEY
return headers


def _submit(*, client_action: str, decision_id: str, note: str) -> None:
Expand All @@ -21,7 +30,12 @@ def _submit(*, client_action: str, decision_id: str, note: str) -> None:
}
try:
with httpx.Client(timeout=10.0) as client:
client.post(f"{API_URL}/feedback/submit", json=payload)
response = client.post(
f"{API_URL}/feedback/submit",
json=payload,
headers=_headers(),
)
response.raise_for_status()
st.success(f"{client_action} submitted for {decision_id}")
except httpx.HTTPError as exc:
st.error(str(exc))
Expand All @@ -42,7 +56,13 @@ def _submit(*, client_action: str, decision_id: str, note: str) -> None:
st.subheader("Flagged decisions")
try:
with httpx.Client(timeout=10.0) as client:
pending = client.get(f"{API_URL}/feedback/pending").json().get("pending", [])
response = client.get(f"{API_URL}/feedback/pending", headers=_headers())
response.raise_for_status()
try:
pending = response.json().get("pending", [])
except json.JSONDecodeError as exc:
st.error(f"Invalid JSON from feedback API: {exc}")
pending = []
except httpx.HTTPError as exc:
st.error(f"Cannot reach feedback API at {API_URL}: {exc}")
pending = []
Expand Down
45 changes: 45 additions & 0 deletions dashboard/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,76 @@ server {
root /usr/share/nginx/html;
index index.html;

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;

location / {
try_files $uri $uri/ /index.html;
}

location /health {
proxy_pass http://rada:8000/health;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /metrics {
limit_except GET {
deny all;
}
proxy_pass http://rada:8000/metrics;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /ingest {
limit_except POST {
deny all;
}
proxy_pass http://rada:8000/ingest;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-API-Key $http_x_api_key;
}

location /bootstrap-demo {
limit_except POST {
deny all;
}
proxy_pass http://rada:8000/bootstrap-demo;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-API-Key $http_x_api_key;
}

location /audit/ {
limit_except GET {
deny all;
}
proxy_pass http://rada:8000/audit/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-API-Key $http_x_api_key;
}

location /feedback/ {
proxy_pass http://rada:8000/feedback/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-API-Key $http_x_api_key;
}
}
28 changes: 22 additions & 6 deletions dashboard/src/pages/Decisions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { apiFetch } from "../api/rest";

function parsePositiveNumber(value, field) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${field} must be a positive number`);
}
return parsed;
}

export function Decisions() {
const [symbol, setSymbol] = useState("BTCUSD");
const [price, setPrice] = useState("42000");
Expand All @@ -10,21 +18,30 @@ export function Decisions() {
const [audit, setAudit] = useState(null);
const [error, setError] = useState("");

const loadAudit = async (decisionId) => {
try {
const chain = await apiFetch(`/audit/decision/${decisionId}`);
setAudit(chain);
} catch (err) {
setError(`Audit fetch failed: ${err.message}`);
setAudit(null);
}
};

const ingest = useMutation({
mutationFn: async () => {
const body = {
symbol,
price: Number(price),
volume: Number(volume),
price: parsePositiveNumber(price, "Price"),
volume: parsePositiveNumber(volume, "Volume"),
timestamp: new Date().toISOString(),
};
return apiFetch("/ingest", { method: "POST", body: JSON.stringify(body) });
},
onSuccess: async (data) => {
setError("");
setLastDecisionId(data.decision_id);
const chain = await apiFetch(`/audit/decision/${data.decision_id}`);
setAudit(chain);
await loadAudit(data.decision_id);
},
onError: (err) => setError(err.message),
});
Expand All @@ -34,8 +51,7 @@ export function Decisions() {
onSuccess: async (data) => {
setError("");
setLastDecisionId(data.decision_id);
const chain = await apiFetch(`/audit/decision/${data.decision_id}`);
setAudit(chain);
await loadAudit(data.decision_id);
},
onError: (err) => setError(err.message),
});
Expand Down
21 changes: 15 additions & 6 deletions dashboard/src/pages/Overview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,24 @@ export function Overview() {
<p className="mt-2 text-sm text-rose-400">Unreachable: {health.error.message}</p>
)}
{health.isSuccess && (
<p className="mt-2 text-sm text-emerald-400">Healthy — {health.data.status}</p>
<p className="mt-2 text-sm text-emerald-400">
Healthy — {health.data?.status ?? "unknown"}
</p>
)}
</div>

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<MetricCard label="Decisions processed" value={legacy.decisions_processed ?? obs.decisions_total ?? "—"} />
<MetricCard label="Risk gate passes" value={legacy.risk_gate_passes ?? "—"} />
<MetricCard label="Reflection queued" value={legacy.reflection_enqueued ?? "—"} />
</div>
{metrics.isLoading && <p className="text-sm dark:text-slate-400">Loading metrics…</p>}
{metrics.isError && (
<p className="text-sm text-rose-400">Metrics unavailable: {metrics.error.message}</p>
)}

{metrics.isSuccess && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<MetricCard label="Decisions processed" value={legacy.decisions_processed ?? obs.decisions_total ?? "—"} />
<MetricCard label="Risk gate passes" value={legacy.risk_gate_passes ?? "—"} />
<MetricCard label="Reflection queued" value={legacy.reflection_enqueued ?? "—"} />
</div>
)}
</div>
);
}
21 changes: 14 additions & 7 deletions dashboard/src/pages/ReviewQueue.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { apiFetch } from "../api/rest";

export function ReviewQueue() {
const qc = useQueryClient();
const [note, setNote] = useState("");
const [notes, setNotes] = useState({});

const pending = useQuery({
queryKey: ["feedback-pending"],
Expand All @@ -15,8 +15,12 @@ export function ReviewQueue() {
const submit = useMutation({
mutationFn: (payload) =>
apiFetch("/feedback/submit", { method: "POST", body: JSON.stringify(payload) }),
onSuccess: () => {
setNote("");
onSuccess: (_data, variables) => {
setNotes((prev) => {
const next = { ...prev };
delete next[variables.decision_id];
return next;
});
qc.invalidateQueries({ queryKey: ["feedback-pending"] });
},
});
Expand All @@ -34,6 +38,7 @@ export function ReviewQueue() {

{pending.isLoading && <p className="text-sm dark:text-slate-400">Loading queue…</p>}
{pending.isError && <p className="text-sm text-rose-400">{pending.error.message}</p>}
{submit.isError && <p className="text-sm text-rose-400">Submit failed: {submit.error.message}</p>}

{items.length === 0 && pending.isSuccess && (
<p className="text-sm dark:text-slate-500">No pending reviews.</p>
Expand All @@ -42,7 +47,7 @@ export function ReviewQueue() {
<ul className="space-y-4">
{items.map((item) => (
<li
key={item.feedback_id}
key={item.feedback_id || item.decision_id}
className="rounded-lg border dark:border-slate-800 light:border-slate-200 p-4"
>
<div className="font-mono text-xs dark:text-slate-400">{item.decision_id}</div>
Expand All @@ -51,8 +56,10 @@ export function ReviewQueue() {
Review note
<input
className="mt-1 w-full rounded border dark:border-slate-700 light:border-slate-300 dark:bg-slate-900 light:bg-white px-3 py-2"
value={note}
onChange={(e) => setNote(e.target.value)}
value={notes[item.decision_id] || ""}
onChange={(e) =>
setNotes((prev) => ({ ...prev, [item.decision_id]: e.target.value }))
}
/>
</label>
<div className="mt-3 flex gap-2">
Expand All @@ -65,7 +72,7 @@ export function ReviewQueue() {
submit.mutate({
decision_id: item.decision_id,
action,
note,
note: notes[item.decision_id] || "",
reviewer: "dashboard",
})
}
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.monitoring.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ services:
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-CHANGE_ME}
depends_on:
- prometheus
5 changes: 4 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ services:
networks:
- default
environment:
RADA_ENV: production
RADA_DATA_STORE_MODE: sqlite
RADA_SQLITE_URL: sqlite:////app/data/rada.db
RADA_EVENT_BUS_MODE: redis
RADA_DATABASE_URL: postgresql://rada:${RADA_POSTGRES_PASSWORD:-rada}@postgres:5432/rada
RADA_REDIS_URL: redis://redis:6379/0
RADA_DATABASE_URL: postgresql://rada:${RADA_POSTGRES_PASSWORD:-CHANGE_ME}@postgres:5432/rada
RADA_API_KEY: ${RADA_API_KEY:-}
RADA_REASONER_MODE: mock
RADA_OTEL_ENABLED: ${RADA_OTEL_ENABLED:-false}
volumes:
Expand Down
5 changes: 4 additions & 1 deletion docs/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ Append-only audit events for every pipeline step.

## API

All audit endpoints require `X-API-Key` when `RADA_API_KEY` is set (required in production).

- `GET /audit/decision/{id}` — events for one decision
- `GET /audit/export` — NDJSON export
- `GET /audit/export?from=&to=&limit=` — NDJSON export (default limit 1000, max 10000)

## CLI

```bash
python scripts/export_audit.py --output audit.ndjson
python scripts/export_audit.py --db ./rada_audit.db --from 2026-06-01T00:00:00Z --limit 5000
```

Audit store rejects DELETE operations; events are immutable.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

project = "RADA"
author = "Risk-Aware AI"
release = "0.1.0"
release = "1.0.0"

extensions = [
"sphinx.ext.autodoc",
Expand Down
Loading
Loading