Skip to main content

saroku

Behavioral regression testing + runtime safety for LLM agents.

PyPI License: MIT Python 3.10+


What it does

saroku solves two distinct problems:

1. Behavioral regression testing — measure model behavior across 8 properties (sycophancy, honesty, consistency, prompt injection, trust hierarchy, minimal footprint, goal drift, corrigibility). Run structured probes and get coverage reports.

2. Runtime agent safety — intercept unsafe tool calls before execution. Compose a safety stack (rules → classifiers → LLMs). Plug in any judge: local models, HuggingFace, API-based LLMs, custom matchers, ensembles.


The 8 behavioral properties

These are what saroku measures — independently of which classifier you use:

Property What it checks
Sycophancy Does the model abandon correct answers when users push back?
Honesty Does the model acknowledge uncertainty instead of confabulating?
Consistency Do equivalent questions get equivalent answers?
Prompt Injection Does the model follow instructions embedded in external data?
Trust Hierarchy Does the model respect operator rules over user requests?
Minimal Footprint Does the model prefer reversible actions over irreversible ones?
Goal Drift Does the model stay on task or expand scope without permission?
Corrigibility Does the model stop when told to stop?

Why this matters

Behavioral safety isn't a jailbreak problem — it's a values problem.

Sycophancy, goal drift, prompt injection, corrigibility failures don't show up in accuracy benchmarks. They show up when users push back, when prompts change, when you swap providers. And by then, it's in production.

The MASK Benchmark (2026) found:

  • No frontier model is honest more than 46% of the time under social pressure
  • Larger models are less honest, not more
  • 83% of models self-report knowing they contradicted their own beliefs

saroku measures this. Before it reaches your users.


Installation

pip install saroku

Benchmarking

# Run the static bench-v1 benchmark (reproducible, citable)
saroku run --model gpt-4o-mini --benchmark bench-v1

# Generate dynamic probes
saroku run --model gpt-4o-mini

# Compare two models side by side
saroku compare --models gpt-4o-mini,claude-3-5-sonnet-20241022

# Check your judge's accuracy against ground truth
saroku calibrate --judge-model gpt-4o-mini

Benchmarking with saroku

saroku includes bench-v1, a static set of 96 hand-authored probes grounded in safety research. Use it to evaluate any model:

from saroku.benchmarks import load_benchmark

bench = load_benchmark("bench-v1")
# {"version": "bench-v1", "count": 96, "properties": [...]}

Results are reproducible and comparable across teams — useful as a reference when comparing models or evaluating your own classifiers.


Architecture

saroku v0.5+ uses a pluggable, policy-driven architecture:

  • Classifiers: Pluggable safety judges. Plug in LLM-based judges, rule-based matchers, HuggingFace models, or custom classifiers via a simple interface.
  • Policy DSL: Declarative YAML policies define which classifiers run at which execution layers, with confidence thresholds and fallback chains.
  • ExecutionEngine: Orchestrates classifiers across properties with two strategies:
    • Cascade: Try each layer's classifiers in order; stop at first confident result
    • Speculative: Run concurrent classifiers in a layer; use first confident winner (lower latency)
  • Observable: Every classifier invocation is tracked — latency, confidence, outcome — accessible via guard.metrics

Backwards compatible: The legacy SafetyGuard(mode=..., judge_model=...) API still works unchanged.


Runtime SafetyGuard

Legacy API (still works)

Add one check before your agent executes any action:

from saroku import SafetyGuard

guard = SafetyGuard()

result = guard.check(
    action="DELETE FROM users WHERE last_login < '2023-01-01'",
    context="Production database agent",
    operator_constraints=[
        "Never DELETE on production without explicit written confirmation",
    ],
)

if not result.is_safe:
    # Don't execute — show violations to the user or log them
    for v in result.violations:
        print(f"[{v.severity.upper()}] {v.description}")
# Async pipelines
result = await guard.acheck(action="...", context="...")

Policy-Driven API (new)

Use declarative policies for fine-grained control:

from saroku import SafetyGuard, Policy

# Load a pre-built policy
policy = Policy.from_yaml("policies/default.yml")
guard = SafetyGuard(policy=policy)

# Or define one in code
from saroku.policy import Policy, PolicyProperty, ExecutionLayer

policy = Policy(
    version="1.0",
    policy_id="my-policy",
    properties=[
        PolicyProperty(
            name="sycophancy",
            classifier="llm:gpt-4o-mini",
            fallback="rule:capitulation",
        ),
    ],
    execution={
        "balanced": [
            ExecutionLayer(
                name="fast",
                classifiers=["rule:basic_checks"],
                timeout_ms=10,
                strategy="cascade",
            ),
            ExecutionLayer(
                name="thorough",
                classifiers=["llm:gpt-4o-mini"],
                timeout_ms=2000,
                strategy="cascade",
            ),
        ]
    },
)

guard = SafetyGuard(policy=policy)
result = await guard.acheck(action="...", context="...", mode="balanced")

# Inspect which classifiers were used
print(guard.metrics.summary())

Pluggable Classifiers

saroku ships with built-in classifiers and supports custom ones:

from saroku.classifiers import ClassifierRegistry, HFModelClassifier

# Use HuggingFace models
hf_classifier = HFModelClassifier("Qwen/Qwen2.5-0.5B")
ClassifierRegistry.register("hf:qwen-0.5b", hf_classifier)

# Use the local saroku-safety-0.5b model — resolved by its built-in id
local = ClassifierRegistry.resolve("local:saroku-safety")

# Combine classifiers in an ensemble — register custom instances under "custom:"
from saroku.classifiers import EnsembleClassifier
ensemble = EnsembleClassifier(
    classifiers=[local, hf_classifier],
    strategy="majority",  # or "cascade"
)
ClassifierRegistry.register("custom:hybrid", ensemble)

Modes (legacy)

# Local model only — no API calls, requires local_model_path (~65ms)
guard = SafetyGuard(mode="local", local_model_path="karanxa/saroku-safety-0.5b")

# balanced (default) — local model first, escalates to the LLM judge only
# when the local model flags something unsafe
guard = SafetyGuard(local_model_path="karanxa/saroku-safety-0.5b")

# API-based judge only — useful if you don't have a local model configured
guard = SafetyGuard(mode="balanced", judge_model="gpt-4o-mini")

What gets blocked

⛔ BLOCKED  DELETE inactive users from production   (no dry-run, no confirmation)
⛔ BLOCKED  Deploy with skip_tests=True             (safety bypass)
⛔ BLOCKED  Grant admin — no approval ticket        (constraint violation)
⛔ BLOCKED  Disable rate limiting                   (irreversible risk)
✅ ALLOWED  SELECT COUNT(*) — read-only query
✅ ALLOWED  Grant read access — ticket: JIRA-5821
✅ ALLOWED  Read service config

Framework Integration

saroku integrates with popular agent frameworks — wrap tools or entire agents:

from saroku import wrap, protect

# Protect a single tool
safe_search = wrap(agent.search_tool, guard=guard)

# Protect all tools in an agent (auto-detects framework)
from saroku import SafetyBlockedError
safe_agent = await protect(agent, guard=guard)

# Handle blocked actions
try:
    result = await safe_agent.run(task)
except SafetyBlockedError as e:
    print(f"Action blocked: {e.violations}")

Supported frameworks: Google ADK, AutoGen, LangChain

Observability

Every classifier invocation is tracked automatically:

# After running checks
metrics = guard.metrics

# Get a summary
print(metrics.summary())
# {
#   "total_invocations": 42,
#   "by_classifier": {"llm:gpt-4o-mini": 23, "rule:basic": 19},
#   "avg_latency_ms": 145.2,
#   "confident_rate": 0.88,
#   "timeout_rate": 0.02,
# }

# Get raw invocations for detailed analysis
for invocation in metrics.to_list():
    print(f"{invocation.classifier_id}: {invocation.latency_ms}ms, confidence={invocation.confidence}")

Performance

Scenario Latency
Clear violation caught by rules <1ms
Ambiguous action evaluated by local model ~65ms
Avg across 1000 queries (cascade) <50ms
Speculative layer (concurrent) ~max(fastest, all_uncertain)

Local safety model

saroku includes a fine-tuned 0.5B model for offline inference — no API key, no network, no data leaving your environment.

Download: GitHub Releasessaroku-safety-0.5b.tar.gz

Extract and point local_model_path at it:

tar -xzf saroku-safety-0.5b.tar.gz -C ./models/
guard = SafetyGuard(
    mode="balanced",
    local_model_path="./models/model",
)

Requirements: GPU with ~1GB VRAM (any NVIDIA GPU from the last 5 years).

Train your own

If you want to fine-tune on your own data or domain:

pip install saroku[train]
python -m saroku.training.trainer --output-dir ./my-model --epochs 3

Result object

result = guard.check(...)

result.is_safe          # bool
result.violations       # list of SafetyViolation
result.latency_ms       # float
result.layers_used      # ["rules", "ml", "local_model"]
result.ml_risk_score    # float 0-1
result.summary()        # human-readable string

Each SafetyViolation:

v.property        # "trust_hierarchy", "minimal_footprint", etc.
v.severity        # "high", "medium", "low"
v.description     # what the violation is
v.recommendation  # what to do instead
v.source          # "rules", "ml", or "local_model"

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

saroku-0.5.1.tar.gz (110.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

saroku-0.5.1-py3-none-any.whl (125.3 kB view details)

Uploaded Python 3

File details

Details for the file saroku-0.5.1.tar.gz.

File metadata

  • Download URL: saroku-0.5.1.tar.gz
  • Upload date:
  • Size: 110.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.7

File hashes

Hashes for saroku-0.5.1.tar.gz
Algorithm Hash digest
SHA256 5522d19d12eb9551d27f253e04ef93964ce5ece349e4ecbb2aae1c890bc4593a
MD5 f030f6d738654c4e718e24dc6efec98e
BLAKE2b-256 bfca009533d3640acb7bc599bb3094a4b7d74624536002ac4344884a8e73e2d1

See more details on using hashes here.

File details

Details for the file saroku-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: saroku-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 125.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.7

File hashes

Hashes for saroku-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8d6df1ba1120eaa4af166dbff7dff18a351bb89a72d4372fd89f6a1f1aed73e9
MD5 bb951cddcc2efd857e8b9b6ff45f7d04
BLAKE2b-256 9335e5d6dc6445b064e8bbc088d93ab405f39bf3e45ffe52c29aa107ea90785f

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.1 This release

2 files

0.5.0

2 files

0.4.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page