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

### Added
- **`riskkernel init`** — scaffolds a working starting point in one command: a `.env`
(provider key, default budget, the data dir where runs and crash-resume checkpoints
live) and a runnable, key-free `quickstart.py` (a governed loop the budget stops),
then prints the next steps. Never overwrites existing files.
- **SDK: resume a run after a crash.** `Runtime.resume_run(run_id)` attaches to an
existing governed run (it neither creates a new run nor cancels on error), so a
Python agent can pick its work back up from the last checkpoint after a `SIGKILL`.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ needed — and run it:

```bash
go install github.com/prashar32/riskkernel/cmd/riskkernel@latest
riskkernel serve
riskkernel init # scaffold a .env + a runnable example in the current dir
riskkernel serve # start the daemon (reads .env)
```

(or `make build` from a clone). Deeper control (loops, checkpoints, approval
Expand Down
62 changes: 62 additions & 0 deletions cmd/riskkernel/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
_ "embed"
"fmt"
"os"
"path/filepath"
)

//go:embed templates/env.tmpl
var envTemplate string

//go:embed templates/quickstart.py
var quickstartTemplate string

// runInit scaffolds a working starting point — a .env config and a runnable
// governed-loop example — into the target directory (default "."). It never
// overwrites an existing file, so it's safe to re-run.
func runInit(args []string) error {
dir := "."
if len(args) > 0 && args[0] != "" {
dir = args[0]
}
if err := os.MkdirAll(dir, 0o750); err != nil {
return fmt.Errorf("creating %s: %w", dir, err)
}

files := []struct{ name, content string }{
{".env", envTemplate},
{"quickstart.py", quickstartTemplate},
}
var created, skipped []string
for _, f := range files {
path := filepath.Join(dir, f.name)
if _, err := os.Stat(path); err == nil {
skipped = append(skipped, f.name)
continue
} else if !os.IsNotExist(err) {
return fmt.Errorf("checking %s: %w", path, err)
}
if err := os.WriteFile(path, []byte(f.content), 0o600); err != nil {
return fmt.Errorf("writing %s: %w", path, err)
}
created = append(created, f.name)
}

for _, name := range created {
fmt.Printf(" created %s\n", filepath.Join(dir, name))
}
for _, name := range skipped {
fmt.Printf(" kept %s (already present)\n", filepath.Join(dir, name))
}

fmt.Print(`
Next:
1. (optional) put your ANTHROPIC_API_KEY in .env — model calls need it; the loop demo doesn't
2. riskkernel serve # start the governance daemon (reads .env)
3. pip install "git+https://github.com/prashar32/riskkernel.git#subdirectory=sdks/python"
4. python quickstart.py # watch the governor stop a runaway loop
`)
return nil
}
54 changes: 54 additions & 0 deletions cmd/riskkernel/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestRunInit_ScaffoldsWorkingFiles(t *testing.T) {
dir := t.TempDir()
if err := runInit([]string{dir}); err != nil {
t.Fatalf("init: %v", err)
}
for _, name := range []string{".env", "quickstart.py"} {
b, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
t.Fatalf("expected %s to be created: %v", name, err)
}
if len(b) == 0 {
t.Fatalf("%s is empty", name)
}
}
// The .env carries the budget + data-dir config; quickstart wires a governed run.
env, _ := os.ReadFile(filepath.Join(dir, ".env"))
for _, key := range []string{"RISKKERNEL_DEFAULT_DOLLARS", "RISKKERNEL_DATA_DIR", "ANTHROPIC_API_KEY"} {
if !strings.Contains(string(env), key) {
t.Fatalf(".env is missing %s:\n%s", key, env)
}
}
if qs, _ := os.ReadFile(filepath.Join(dir, "quickstart.py")); !strings.Contains(string(qs), "governed_run") {
t.Fatalf("quickstart.py doesn't wire a governed_run:\n%s", qs)
}
}

func TestRunInit_DoesNotOverwrite(t *testing.T) {
dir := t.TempDir()
// A pre-existing .env with the user's own content must be preserved.
envPath := filepath.Join(dir, ".env")
mine := "ANTHROPIC_API_KEY=sk-mine\n"
if err := os.WriteFile(envPath, []byte(mine), 0o600); err != nil {
t.Fatal(err)
}
if err := runInit([]string{dir}); err != nil {
t.Fatalf("init: %v", err)
}
if got, _ := os.ReadFile(envPath); string(got) != mine {
t.Fatalf("init overwrote an existing .env: %q", got)
}
// …while the missing quickstart.py is still scaffolded.
if _, err := os.Stat(filepath.Join(dir, "quickstart.py")); err != nil {
t.Fatalf("quickstart.py should still be created: %v", err)
}
}
4 changes: 4 additions & 0 deletions cmd/riskkernel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// Usage:
//
// riskkernel init [dir] Scaffold a .env + runnable example to get started.
// riskkernel serve Run the governance daemon (default port 7070).
// riskkernel chat "<prompt>" One-shot model call — proves the provider path.
// riskkernel version Print build identity.
Expand Down Expand Up @@ -36,6 +37,8 @@ func main() {

var err error
switch cmd {
case "init":
err = runInit(args)
case "serve":
err = runServe(args)
case "chat":
Expand Down Expand Up @@ -70,6 +73,7 @@ func usage() {
fmt.Fprint(os.Stderr, `riskkernel — the risk engine for your AI agents

Usage:
riskkernel init [dir] Scaffold a .env + runnable example to get started
riskkernel serve Run the governance daemon (default :7070)
riskkernel chat "<prompt>" One-shot model call (proves the provider path)
riskkernel runs list List persisted governed runs
Expand Down
21 changes: 21 additions & 0 deletions cmd/riskkernel/templates/env.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# RiskKernel configuration — read by `riskkernel serve`. Secrets stay here; they
# are never written to the state store, never logged, never sent anywhere but the
# provider API.

# Your provider key. The daemon still boots without it; only model calls need it.
ANTHROPIC_API_KEY=

# Where the SQLite state lives: your runs, cost ledger, and crash-resume
# checkpoints. This file is yours — back it up to keep run history and resume.
RISKKERNEL_DATA_DIR=./data

# Default per-run budget for runs created without an explicit one. Leave these
# unset for safe defaults ($5 / 100 loops / 1h per run); set any value to take
# control (0 means unlimited for that dimension).
RISKKERNEL_DEFAULT_DOLLARS=
RISKKERNEL_DEFAULT_LOOPS=
RISKKERNEL_DEFAULT_SECONDS=

# Optional bearer token guarding the daemon's API. Empty = unauthenticated, for
# local use only; set one before exposing the port to anything untrusted.
RISKKERNEL_API_TOKEN=
26 changes: 26 additions & 0 deletions cmd/riskkernel/templates/quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""A governed agent loop, scaffolded by `riskkernel init`.

A deliberately runaway loop that RiskKernel's deterministic governor hard-stops at
its budget. No API key needed — the loop cap is enforced in the Go daemon, so you
can watch the kill with nothing running but `riskkernel serve`.

riskkernel serve # in another terminal
python quickstart.py
"""

import riskkernel as rk

rt = rk.Runtime() # talks to the daemon at http://localhost:7070

with rt.governed_run(name="quickstart", budget=rt.budget(loops=8)) as run:
print(f"run {run.id} — budget: loops=8\n")
step = 0
try:
while True: # a loop that would never stop on its own
run.step() # raises rk.BudgetExceeded at the cap
step += 1
print(f" step {step}")
except rk.BudgetExceeded as halt:
print(f"\nRiskKernel stopped it — {halt.reason}. The governor capped the loop at 8;")
print("the loop never decided to stop. Swap the budget, wrap your own agent, and go.")
Loading