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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ surface is governed by [`COMPATIBILITY.md`](COMPATIBILITY.md).

## [Unreleased]

### Added
- **One-command docker-compose quickstart.** [`examples/quickstart-compose`](examples/quickstart-compose)
brings up the daemon, a stand-in mock LLM, and a tiny looping agent with a single
`docker compose up` — so a newcomer watches the deterministic loop budget hard-stop
a runaway agent (HTTP 402, `loop_budget_exceeded`) with **no API key** and no local
Go/Python setup. The daemon pulls fresh on each run so a stale cached image can't
skew the demo; a short README shows how to swap the mock for a real provider.

## [0.7.0] - 2026-06-14

Reach and scale. RiskKernel now plugs into the whole Python agent ecosystem —
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ It is **not** another gateway (LiteLLM/Portkey own routing), **not** another obs

## Quickstart (60 seconds)

> **No key, one command?** [`examples/quickstart-compose`](examples/quickstart-compose)
> is a `docker compose up` demo that hard-stops a runaway agent with no API key and
> no local setup — the fastest way to see the loop-killer.

Run the daemon with your key (nothing leaves your machine except calls to the
provider you choose). Unconfigured, every run gets a safe default budget —
$5 / 100 loops / 1 hour — so nothing is ever unbounded; here we set an explicit
Expand Down
64 changes: 64 additions & 0 deletions examples/quickstart-compose/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Quickstart (docker compose) — watch RiskKernel kill a runaway agent

The fastest way to see RiskKernel work: **one command**, no API key, no local Go or
Python. `docker compose up` brings up the governance daemon and a tiny agent that
loops forever — and you watch the deterministic loop budget hard-stop it.

```bash
cd examples/quickstart-compose
docker compose up
```

You'll see the agent's calls go through, then get cut off:

```
call 1 -> 200 OK tokens=1600 cost=$...
call 2 -> 200 OK tokens=1600 cost=$...
call 3 -> 200 OK tokens=1600 cost=$...
call 4 -> 200 OK tokens=1600 cost=$...
call 5 -> 200 OK tokens=1600 cost=$...

call 6 -> HTTP 402 RiskKernel HALTED the run:
{"code":"loop_budget_exceeded","message":"..."}

The kill came from RiskKernel's deterministic loop budget — the
agent script never chose to stop.
```

`docker compose up` exits when the agent is halted. Tear it down with:

```bash
docker compose down
```

## What's running

Three small services (see [`docker-compose.yml`](docker-compose.yml)):

| Service | Image | Role |
|---|---|---|
| `mock-llm` | `nginx:alpine` | A stand-in OpenAI-compatible upstream that returns one canned completion with token usage — so the demo needs **no real provider and no API key**. |
| `riskkernel` | `ghcr.io/prashar32/riskkernel:latest` | The governance daemon. A hard **loop budget of 5** (`RISKKERNEL_DEFAULT_LOOPS=5`); the other dimensions are unlimited so the loop budget is the sole, unambiguous enforcer. Forwards model calls to the mock. |
| `sample-agent` | `curlimages/curl` | A minimal "agent" — [`agent.sh`](agent.sh) just loops through the proxy under one run id and never stops on its own. RiskKernel stops it. |

The agent changes nothing about how it calls the model — it's a plain
OpenAI-style `POST /v1/chat/completions`. The budget enforcement, the metering, and
the 402 kill all come from RiskKernel sitting in front.

## Use it for real

Swap the mock for a real provider and govern your own app — still one env var on
the app side:

1. Give the daemon a real key and drop the mock override. In `docker-compose.yml`,
set `ANTHROPIC_API_KEY` (or `OPENAI_API_KEY`) on the `riskkernel` service and
remove the `RISKKERNEL_OPENAI_BASE_URL` line (and the `mock-llm` service).
2. Pick the budget you want, e.g. `RISKKERNEL_DEFAULT_DOLLARS=5` for a hard $5
per-run ceiling alongside (or instead of) the loop cap.
3. Point your existing app at the proxy — one env var, no code change:
`OPENAI_BASE_URL=http://localhost:7070/v1` (or `ANTHROPIC_BASE_URL=http://localhost:7070`).

Your keys stay in the daemon's environment, never in app config, and nothing is
sent anywhere except the provider call you already make. See the repo
[README](../../README.md) for budgets-as-config, crash-resume, approvals, and the
OpenTelemetry integration.
44 changes: 44 additions & 0 deletions examples/quickstart-compose/agent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/sh
# A deliberately runaway "agent": it just keeps calling the model through the
# RiskKernel proxy, sharing one run id, and never decides to stop on its own. The
# proxy meters each call and — once the run's loop budget is spent — refuses the
# next one with HTTP 402. The kill comes from RiskKernel, not from this script.
set -eu

PROXY="${PROXY:-http://riskkernel:7070}"
RUN_ID="quickstart-$(date +%s)" # a fresh run each time so re-runs start clean

echo "----------------------------------------------------------------"
echo " RiskKernel quickstart: a runaway agent vs a hard loop budget"
echo " loop budget = 5 model calls (no API key — a mock LLM stands in)"
echo "----------------------------------------------------------------"
echo ""

i=1
while [ "$i" -le 20 ]; do
code="$(curl -s -o /tmp/body -D /tmp/hdr -w '%{http_code}' \
-X POST "$PROXY/v1/chat/completions" \
-H 'Content-Type: application/json' \
-H "X-RiskKernel-Run-Id: $RUN_ID" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"keep going"}]}')"

if [ "$code" = "200" ]; then
toks="$(awk 'tolower($1)=="x-riskkernel-tokens:"{gsub(/\r/,"");print $2}' /tmp/hdr)"
cost="$(awk 'tolower($1)=="x-riskkernel-cost-usd:"{gsub(/\r/,"");print $2}' /tmp/hdr)"
echo " call $i -> 200 OK tokens=${toks:-?} cost=\$${cost:-?}"
else
echo ""
echo " call $i -> HTTP $code RiskKernel HALTED the run:"
sed 's/^/ /' /tmp/body
echo ""
echo ""
echo " The kill came from RiskKernel's deterministic loop budget — the"
echo " agent script never chose to stop. Point your own app at the proxy"
echo " and the same budget protects it. (See README: Use it for real.)"
exit 0
fi
i=$((i + 1))
done

echo "ERROR: expected RiskKernel to halt the loop within 20 calls, but it did not." >&2
exit 1
49 changes: 49 additions & 0 deletions examples/quickstart-compose/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: riskkernel-quickstart

# One command — `docker compose up` — to watch RiskKernel hard-stop a runaway agent,
# with NO API key and no local Go/Python setup. Three tiny services:
# mock-llm a stand-in OpenAI-compatible upstream (one canned reply) so the
# demo needs no real provider or key
# riskkernel the governance daemon: a hard loop budget, forwarding to the mock
# sample-agent a minimal "agent" that just loops through the proxy until it's killed
#
# To govern a REAL app instead, see the "Use it for real" section in README.md.

services:
mock-llm:
image: nginx:alpine
volumes:
- ./mock-llm.conf:/etc/nginx/conf.d/default.conf:ro

riskkernel:
image: ghcr.io/prashar32/riskkernel:latest
# Always pull, so a stale locally-cached :latest can't make the demo behave
# like an older RiskKernel (the quickstart should "just work" on any machine).
pull_policy: always
depends_on:
- mock-llm
ports:
- "7070:7070"
environment:
# The star of the demo: a runaway loop is killed after 5 model calls.
RISKKERNEL_DEFAULT_LOOPS: "5"
# Unlimited on every other dimension so the LOOP budget is the sole enforcer
# and the halt reason is unambiguous.
RISKKERNEL_DEFAULT_DOLLARS: "0"
RISKKERNEL_DEFAULT_TOKENS: "0"
RISKKERNEL_DEFAULT_SECONDS: "0"
# A dummy key activates the OpenAI provider; the call is intercepted and
# forwarded to the mock above — no real provider, no real key leaves anywhere.
OPENAI_API_KEY: "sk-quickstart-mock"
RISKKERNEL_OPENAI_BASE_URL: "http://mock-llm"

sample-agent:
image: curlimages/curl:8.11.1
depends_on:
riskkernel:
condition: service_healthy
volumes:
- ./agent.sh:/agent.sh:ro
entrypoint: ["sh", "/agent.sh"]
environment:
PROXY: "http://riskkernel:7070"
11 changes: 11 additions & 0 deletions examples/quickstart-compose/mock-llm.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# A stand-in OpenAI-compatible upstream for the quickstart: it answers any request
# with one canned chat completion (including token usage), so the demo needs no real
# provider and no API key. RiskKernel forwards to this instead of a real model.
server {
listen 80;

location / {
default_type application/json;
return 200 '{"id":"chatcmpl-mock","object":"chat.completion","created":0,"model":"gpt-4o","choices":[{"index":0,"message":{"role":"assistant","content":"Canned reply from the mock LLM — the quickstart needs no API key."},"finish_reason":"stop"}],"usage":{"prompt_tokens":1200,"completion_tokens":400,"total_tokens":1600}}';
}
}