ruby-skill-bench 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +171 -38
- data/docs/architecture.md +10 -1
- data/docs/docker.md +45 -0
- data/docs/first-eval-guide.md +7 -7
- data/docs/testing-guide.md +1 -1
- data/lib/skill_bench/agent/react_agent/loop_runner.rb +44 -9
- data/lib/skill_bench/agent/react_agent/step.rb +7 -1
- data/lib/skill_bench/cli/batch_result_printer.rb +45 -0
- data/lib/skill_bench/cli/eval/eval_options.rb +4 -0
- data/lib/skill_bench/cli/help_printer.rb +10 -2
- data/lib/skill_bench/cli/init_command.rb +2 -1
- data/lib/skill_bench/cli/result_printer.rb +1 -1
- data/lib/skill_bench/cli/run_command.rb +47 -9
- data/lib/skill_bench/cli/validate_command.rb +242 -0
- data/lib/skill_bench/cli.rb +3 -0
- data/lib/skill_bench/client.rb +43 -1
- data/lib/skill_bench/clients/all.rb +2 -0
- data/lib/skill_bench/clients/base_client.rb +12 -1
- data/lib/skill_bench/clients/base_url_validator.rb +105 -0
- data/lib/skill_bench/clients/provider_config.rb +41 -2
- data/lib/skill_bench/clients/provider_schemas.rb +4 -0
- data/lib/skill_bench/clients/providers/mistral.rb +47 -0
- data/lib/skill_bench/commands/init.rb +5 -0
- data/lib/skill_bench/commands/skill_new.rb +3 -1
- data/lib/skill_bench/config/applier.rb +2 -0
- data/lib/skill_bench/config/defaults.rb +2 -0
- data/lib/skill_bench/config/facade_readers.rb +7 -0
- data/lib/skill_bench/config/facade_writers.rb +17 -0
- data/lib/skill_bench/config/json_loader.rb +1 -1
- data/lib/skill_bench/config/store.rb +29 -0
- data/lib/skill_bench/config.rb +18 -0
- data/lib/skill_bench/constants.rb +27 -0
- data/lib/skill_bench/evaluation/runner.rb +20 -3
- data/lib/skill_bench/execution/context_hydrator.rb +52 -11
- data/lib/skill_bench/execution/docker/.dockerignore +2 -0
- data/lib/skill_bench/execution/docker/Dockerfile +24 -0
- data/lib/skill_bench/execution/sandbox.rb +113 -27
- data/lib/skill_bench/judge/judge.rb +4 -0
- data/lib/skill_bench/judge/prompt.rb +42 -6
- data/lib/skill_bench/models/config.rb +32 -0
- data/lib/skill_bench/output_formatter.rb +60 -1
- data/lib/skill_bench/package_verifier.rb +3 -1
- data/lib/skill_bench/rails/skill_templates.rb +19 -5
- data/lib/skill_bench/services/agent_spawner_service.rb +7 -3
- data/lib/skill_bench/services/batch_runner_service.rb +111 -0
- data/lib/skill_bench/services/compare_option_parser.rb +1 -0
- data/lib/skill_bench/services/cost_calculator.rb +91 -0
- data/lib/skill_bench/services/html_formatter.rb +289 -0
- data/lib/skill_bench/services/json_formatter.rb +19 -1
- data/lib/skill_bench/services/junit_formatter.rb +74 -24
- data/lib/skill_bench/services/provider_resolver.rb +5 -2
- data/lib/skill_bench/services/response_cache.rb +130 -0
- data/lib/skill_bench/services/runner_service.rb +88 -4
- data/lib/skill_bench/services/summary_formatter.rb +90 -0
- data/lib/skill_bench/services/template_registry.rb +43 -9
- data/lib/skill_bench/services/trend_recorder_service.rb +29 -2
- data/lib/skill_bench/tools/registry.rb +29 -3
- data/lib/skill_bench/tools/run_command.rb +171 -19
- data/lib/skill_bench/trend_tracker/persistence.rb +27 -10
- data/lib/skill_bench/trend_tracker.rb +5 -5
- data/lib/skill_bench/version.rb +1 -1
- data/lib/skill_bench.rb +2 -3
- metadata +18 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60f6da81bf101f4bb1e5a7b86112aa96fa430ca9a1db7f1331a47e51b194ea73
|
|
4
|
+
data.tar.gz: 7f50c1e8a1891a93f0028f98c6a660e1d35ecfaf2104390d94a895ff983d9c93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a828560ad084e9bd1d3336c9002c8b17d47a12955bed6d53e3dd174ef0e971dab24699ab5da40d3445ece5d06afa29e9354e9d033e5d8a11b5c749872487c1cd
|
|
7
|
+
data.tar.gz: b81d9dc73aa78be923e8fdecbf789aa5078e1f68c40d9330a0e11ca9632579dc8635e647a3f08730e1a478f3ea16a0e5379fae1b008b461f1f48d6d4714a9e3e
|
data/README.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
|
|
6
5
|

|
|
7
6
|
|
|
7
|
+
[](https://deepwiki.com/igmarin/ruby-skill-bench)
|
|
8
|
+
|
|
8
9
|
*A high-fidelity evaluation engine for benchmarking AI agent skills across any stack (Rails-first, but extensible).*
|
|
9
10
|
|
|
10
11
|
## Part of the AI Skill Ecosystem
|
|
@@ -30,7 +31,7 @@ See the [Ecosystem Overview](https://github.com/igmarin/agent-mcp-runtime/blob/m
|
|
|
30
31
|
- **Isolated Git Sandboxes**: Every run operates in a temporary repo. Clean diffs, zero side-effects, 100% reproducibility.
|
|
31
32
|
- **Blind Judging with Dimensions**: LLM judge scores baseline and context independently across 5 canonical dimensions (Correctness, Skill Adherence, Code Quality, Test Coverage, Documentation). Eval authors configure weights and thresholds via `criteria.json`.
|
|
32
33
|
- **Sophisticated ReAct Loop**: Employs a robust `Thought → Tool → Observation` loop to handle complex, multi-step engineering tasks.
|
|
33
|
-
- **Multi-Provider Ecosystem**: Native support for **OpenAI**, **Anthropic**, **Google Gemini**, **Azure OpenAI**, **Ollama**, **Groq**, **DeepSeek**, and **OpenCode**.
|
|
34
|
+
- **Multi-Provider Ecosystem**: Native support for **OpenAI**, **Anthropic**, **Google Gemini**, **Azure OpenAI**, **Ollama**, **Groq**, **DeepSeek**, **Mistral**, and **OpenCode**.
|
|
34
35
|
- **Standardized Intelligence**: Consistent reporting format regardless of the underlying LLM provider.
|
|
35
36
|
|
|
36
37
|
---
|
|
@@ -64,11 +65,14 @@ CLI / API → RunnerService → Sandbox + ReAct Agent → LLM Client Layer → P
|
|
|
64
65
|
| **Ollama** | — | `:ollama` |
|
|
65
66
|
| **Groq** | `SKILL_BENCH_GROQ_API_KEY` | `:groq` |
|
|
66
67
|
| **DeepSeek** | `SKILL_BENCH_DEEPSEEK_API_KEY` | `:deepseek` |
|
|
68
|
+
| **Mistral** | `SKILL_BENCH_MISTRAL_API_KEY` | `:mistral` |
|
|
67
69
|
| **OpenCode** | `SKILL_BENCH_OPENCODE_API_KEY`, `SKILL_BENCH_OPENCODE_BASE_URL` | `:opencode` |
|
|
68
70
|
|
|
69
71
|
> **Note:** Environment variables are loaded automatically. You can also configure provider settings in `skill-bench.json` (created by `skill-bench init`).
|
|
70
72
|
>
|
|
71
73
|
> **OpenCode requires a custom `base_url`:** OpenCode does not host a public LLM API. You must provide your own OpenAI-compatible endpoint (e.g. a LiteLLM proxy, self-hosted vLLM, or company gateway) via the `base_url` config key. Without it, the provider will fail with "Base URL not set for Opencode".
|
|
74
|
+
>
|
|
75
|
+
> **Mistral** uses Mistral's OpenAI-compatible chat completions API (default model `mistral-large-latest`). Set `SKILL_BENCH_MISTRAL_API_KEY` and scaffold it with `skill-bench init --mistral`.
|
|
72
76
|
|
|
73
77
|
### Command Allowlist
|
|
74
78
|
|
|
@@ -79,6 +83,7 @@ By default, no shell commands are permitted. You must configure `allowed_command
|
|
|
79
83
|
"provider": "openai",
|
|
80
84
|
"max_execution_time": 30,
|
|
81
85
|
"allowed_commands": ["rspec", "bundle", "ruby", "git"],
|
|
86
|
+
"allow_host_execution": false,
|
|
82
87
|
"config": {
|
|
83
88
|
"api_key": null,
|
|
84
89
|
"model": "gpt-4o"
|
|
@@ -87,6 +92,8 @@ By default, no shell commands are permitted. You must configure `allowed_command
|
|
|
87
92
|
```
|
|
88
93
|
|
|
89
94
|
> **Security:** The agent can only execute commands on this list. Dangerous commands (bash, curl, sudo, etc.) are always blocked regardless of configuration.
|
|
95
|
+
>
|
|
96
|
+
> **Where commands run:** Allowed commands run inside a temporary git **sandbox directory** (a copy of your eval files). When Docker is available and the packaged `evaluator-sandbox` image can start, commands run via `docker exec` with hardened flags (`--network none`, non-root, dropped capabilities). When Docker is **not** available, host execution **fails closed**: it is disabled by default and must be explicitly enabled with `"allow_host_execution": true`. With it disabled (the default), `run_command` refuses to execute on the host. Enable it only when you accept that allowed commands run directly on your machine inside the temp sandbox directory. See [docs/docker.md](docs/docker.md).
|
|
90
97
|
|
|
91
98
|
### Configuration Hierarchy
|
|
92
99
|
|
|
@@ -137,7 +144,9 @@ skill-bench init --openai
|
|
|
137
144
|
}
|
|
138
145
|
```
|
|
139
146
|
|
|
140
|
-
**Available providers:** `--openai`, `--anthropic`, `--gemini`, `--ollama`, `--azure`, `--groq`, `--deepseek`, `--opencode`
|
|
147
|
+
**Available providers:** `--openai`, `--anthropic`, `--gemini`, `--ollama`, `--azure`, `--groq`, `--deepseek`, `--mistral`, `--opencode`
|
|
148
|
+
|
|
149
|
+
**Zero-config offline path:** `skill-bench init --mock` scaffolds a minimal offline config that needs no API key and no network — `{"provider":"mock","max_execution_time":30}`. Use it to try the full flow (and run the bundled examples) before wiring up a real provider.
|
|
141
150
|
|
|
142
151
|
Use `--force` to overwrite an existing config.
|
|
143
152
|
|
|
@@ -338,7 +347,7 @@ skill-bench run my-first-eval --skill=my-service
|
|
|
338
347
|
3. **Context run** — Agent receives `task.md` + `SKILL.md` as prompt → produces output B
|
|
339
348
|
4. **Blind judging** — LLM judge scores output A and output B independently across the dimensions defined in `criteria.json`
|
|
340
349
|
5. **Delta computation** — Compare scores, compute deltas, apply pass/fail logic
|
|
341
|
-
6. **History recording** — Store result in `.skill-bench-
|
|
350
|
+
6. **History recording** — Store result in `.skill-bench-trends.json` for trend tracking
|
|
342
351
|
|
|
343
352
|
Provider is read from `skill-bench.json` — no `--provider` flag needed.
|
|
344
353
|
|
|
@@ -350,11 +359,54 @@ skill-bench run my-first-eval --skill=skill-a --skill=skill-b
|
|
|
350
359
|
|
|
351
360
|
Both skill contexts are concatenated and sent to the agent. The judge evaluates whether the combined context improves results.
|
|
352
361
|
|
|
353
|
-
**Output Formats:**
|
|
362
|
+
**Output Formats:** `--format human` (default), `json`, `junit`, or `html`.
|
|
363
|
+
|
|
364
|
+
- Human-readable (default) — full delta table, iteration timeline, and a `Tokens: N | Est. Cost: $X.XXXX` line.
|
|
365
|
+
- JSON: `--format json` — machine-readable, including top-level `tokens` and `cost` fields.
|
|
366
|
+
- JUnit XML: `--format junit` — for CI test reporting.
|
|
367
|
+
- HTML: `--format html` — a self-contained, shareable report (styles inlined, no external assets) with the delta table and iteration timeline. Redirect it to a file:
|
|
354
368
|
|
|
355
|
-
|
|
356
|
-
-
|
|
357
|
-
|
|
369
|
+
```bash
|
|
370
|
+
skill-bench run my-first-eval --skill=my-service --format html > report.html
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Pre-flight Checks: `validate` / `doctor`
|
|
376
|
+
|
|
377
|
+
Before spending tokens on a run, sanity-check your setup. `skill-bench validate` (aliased as `doctor`) runs read-only pre-flight checks — it never runs an eval and never makes a network call:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
skill-bench validate
|
|
381
|
+
# or, identically:
|
|
382
|
+
skill-bench doctor
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
It runs three checks and prints a `PASS` / `FAIL` / `SKIP` line for each:
|
|
386
|
+
|
|
387
|
+
1. **criteria** — validates the criteria JSON (default `criteria.json`, override with `--criteria PATH`). Skipped if the default file is absent.
|
|
388
|
+
2. **config** — schema-checks `skill-bench.json` (default, override with `--config PATH`): `provider` is required and must be a known provider, `max_execution_time` must be a positive integer, and `config` (when present) must be an object.
|
|
389
|
+
3. **provider key** — reports whether the configured provider's API key is present (the `mock` provider needs none).
|
|
390
|
+
|
|
391
|
+
A passing report exits `0`:
|
|
392
|
+
|
|
393
|
+
```text
|
|
394
|
+
skill-bench validate
|
|
395
|
+
|
|
396
|
+
[PASS] criteria criteria.json is valid
|
|
397
|
+
[PASS] config skill-bench.json matches the expected shape
|
|
398
|
+
[PASS] provider key openai credentials present
|
|
399
|
+
|
|
400
|
+
All checks passed.
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
A failure exits non-zero and names what is wrong:
|
|
404
|
+
|
|
405
|
+
```text
|
|
406
|
+
[FAIL] provider key openai is missing: api_key
|
|
407
|
+
|
|
408
|
+
1 check(s) failed.
|
|
409
|
+
```
|
|
358
410
|
|
|
359
411
|
---
|
|
360
412
|
|
|
@@ -427,6 +479,22 @@ The `--variant` spec supports two forms:
|
|
|
427
479
|
- `pack:<name>` — resolve via registry manifest
|
|
428
480
|
- `/absolute/path` or `relative/path` — use a direct path
|
|
429
481
|
|
|
482
|
+
### Response Caching (opt-in, `--cache`)
|
|
483
|
+
|
|
484
|
+
LLM responses can be cached so identical requests reuse a previous result instead of calling the provider again. Caching is **off by default**. Enable it per run with `--cache`, or set the `SKILL_BENCH_CACHE` environment variable to a truthy value (`1`, `true`, `yes`, or `on`):
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
# Per-run flag
|
|
488
|
+
skill-bench run my-first-eval --skill=my-service --cache
|
|
489
|
+
|
|
490
|
+
# Or via the environment
|
|
491
|
+
SKILL_BENCH_CACHE=1 skill-bench run my-first-eval --skill=my-service
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The cache is in-memory (process-lifetime) and content-addressed: the key is a SHA-256 digest of the provider, model, system prompt, messages, tools, and temperature, so only truly identical requests share an entry. The `mock` and null providers are never cached.
|
|
495
|
+
|
|
496
|
+
This pays off most with `compare`, which runs the skill-less baseline twice with identical inputs — with caching enabled, the repeated baseline reuses the cached response instead of making a second call.
|
|
497
|
+
|
|
430
498
|
---
|
|
431
499
|
|
|
432
500
|
## File Reference: What Lives on Disk
|
|
@@ -446,6 +514,7 @@ SkillBench creates and manages three files in your project. Understanding them h
|
|
|
446
514
|
"provider": "openai",
|
|
447
515
|
"max_execution_time": 300,
|
|
448
516
|
"allowed_commands": ["rspec", "bundle", "ruby", "git"],
|
|
517
|
+
"allow_host_execution": false,
|
|
449
518
|
"config": {
|
|
450
519
|
"api_key": "sk-...",
|
|
451
520
|
"model": "gpt-4o",
|
|
@@ -458,10 +527,11 @@ SkillBench creates and manages three files in your project. Understanding them h
|
|
|
458
527
|
- Configuration is loaded in this order: **code defaults** → `~/.skill-bench.json` (user-wide) → `./skill-bench.json` (local) → **environment variables**. Later sources override earlier ones.
|
|
459
528
|
- If `api_key` is `null`, SkillBench looks for the matching environment variable (e.g. `SKILL_BENCH_OPENAI_API_KEY`).
|
|
460
529
|
- `allowed_commands` is a **safeguard**, not a convenience. By default the agent cannot run *any* shell command. Add only what your evals need.
|
|
530
|
+
- `allow_host_execution` (default `false`) gates whether `run_command` may run on the host when no container isolation is active. When Docker starts a container successfully, host execution is not used. When Docker is unavailable, leaving this `false` means `run_command` **fails closed** (refuses to execute). Set it to `true` only if you accept that allowed commands run directly on your machine inside the temporary sandbox directory.
|
|
461
531
|
|
|
462
532
|
---
|
|
463
533
|
|
|
464
|
-
### `.skill-bench-
|
|
534
|
+
### `.skill-bench-trends.json` — Evaluation History (Auto-Generated)
|
|
465
535
|
|
|
466
536
|
**What it is:** A JSON array that records every successful eval run. SkillBench appends to it automatically. It stores the timestamp, eval name, skill names, scores, and deltas so you can track improvement over time.
|
|
467
537
|
|
|
@@ -497,13 +567,13 @@ TREND: baseline ↑ (+2), context ↑ (+7)
|
|
|
497
567
|
|
|
498
568
|
The trend compares the current run against the *previous run of the same eval + skill*. This tells you at a glance whether your latest skill edit made things better or worse.
|
|
499
569
|
|
|
500
|
-
**Pro tip:**
|
|
570
|
+
**Pro tip:** `.skill-bench-trends.json` is git-ignored by default (via the `.skill-bench-trends.json*` line in `.gitignore`). If you want to share trend data with your team, remove that line so the file can be committed.
|
|
501
571
|
|
|
502
572
|
---
|
|
503
573
|
|
|
504
|
-
### `.skill-bench-
|
|
574
|
+
### `.skill-bench-trends.json.bak` — Backup (Auto-Generated)
|
|
505
575
|
|
|
506
|
-
**What it is:** A
|
|
576
|
+
**What it is:** A snapshot of the *previous* good version of `.skill-bench-trends.json`, copied just before each new write. (The first run has no prior version yet, so it creates no `.bak`.) If the main file gets corrupted (e.g. you kill the process mid-write), SkillBench automatically falls back to the `.bak` file.
|
|
507
577
|
|
|
508
578
|
**Who edits it:** Nobody. It is a safety net.
|
|
509
579
|
|
|
@@ -541,7 +611,7 @@ Read the output carefully. Look at **two things:**
|
|
|
541
611
|
### Step 3: Inspect the History
|
|
542
612
|
|
|
543
613
|
```bash
|
|
544
|
-
cat .skill-bench-
|
|
614
|
+
cat .skill-bench-trends.json | jq '.[-1]'
|
|
545
615
|
```
|
|
546
616
|
|
|
547
617
|
This shows the latest entry. Focus on the dimension with the smallest delta — that is where your skill is weakest.
|
|
@@ -729,6 +799,7 @@ These 5 dimensions are **mandatory** in every `criteria.json`. You can add custo
|
|
|
729
799
|
Eval: my-first-eval
|
|
730
800
|
Skill: my-service
|
|
731
801
|
Provider: openai
|
|
802
|
+
Tokens: 18432 | Est. Cost: $0.0934
|
|
732
803
|
═══════════════════════════════════════════════════════
|
|
733
804
|
|
|
734
805
|
=== BASELINE ITERATIONS ===
|
|
@@ -774,8 +845,9 @@ These 5 dimensions are **mandatory** in every `criteria.json`. You can add custo
|
|
|
774
845
|
- **CONTEXT:** The agent's score *with* the skill. This is the "aided" performance.
|
|
775
846
|
- **DELTA:** `CONTEXT - BASELINE`. How much the skill helped.
|
|
776
847
|
- **TOTAL:** Sum of all dimension scores. Max possible is 100.
|
|
777
|
-
- **TREND:** Comparison against the previous run of the same eval + skill (from `.skill-bench-
|
|
848
|
+
- **TREND:** Comparison against the previous run of the same eval + skill (from `.skill-bench-trends.json`). Shows whether scores are improving over time.
|
|
778
849
|
- **VERDICT:** `PASS` only if `CONTEXT >= pass_threshold` AND `DELTA >= minimum_delta`.
|
|
850
|
+
- **Tokens / Est. Cost:** The header shows total tokens used across the run and an estimated USD cost as `Tokens: N | Est. Cost: $X.XXXX`. The cost is approximate — it comes from a built-in per-model price table (`Services::CostCalculator`) and shows `—` when the model isn't in that table. JSON output (`--format json`) exposes the same data as top-level `tokens` and `cost` fields.
|
|
779
851
|
|
|
780
852
|
**Iteration timeline:**
|
|
781
853
|
|
|
@@ -827,7 +899,7 @@ Your eval result depends on **both** conditions. Here is every scenario:
|
|
|
827
899
|
|
|
828
900
|
## Reliability & Security
|
|
829
901
|
|
|
830
|
-
- **
|
|
902
|
+
- **Allowlist-Gated Execution**: The agent can only run commands you add to `allowed_commands`; with an empty allowlist it can run nothing. Commands prefer Docker isolation when available (see [docs/docker.md](docs/docker.md)); otherwise they would run on the host inside the temporary sandbox directory — so host execution is **disabled by default** and must be explicitly opted into with `allow_host_execution: true`.
|
|
831
903
|
- **Command Blocklist**: Dangerous commands (`bash`, `sh`, `python`, `curl`, etc.) are always blocked, even if listed in `allowed_commands`.
|
|
832
904
|
- **Path Validation**: Eval paths are validated to prevent directory traversal attacks.
|
|
833
905
|
- **Atomic History Writes**: Benchmark history uses file locking to prevent corruption from concurrent writes.
|
|
@@ -836,7 +908,7 @@ Your eval result depends on **both** conditions. Here is every scenario:
|
|
|
836
908
|
- **Traceability**: Every thought and tool call is logged with full backtrace for post-mortem analysis.
|
|
837
909
|
- **Robust Error Recovery**: Handles provider outages and rate limits gracefully with standardized error logging.
|
|
838
910
|
- **XML-Safe Output**: JUnit XML output is properly escaped to prevent injection attacks.
|
|
839
|
-
- **Test Coverage**:
|
|
911
|
+
- **Test Coverage**: 700+ tests covering core engine, CLI commands, and all provider clients. Run `bundle exec rake test` to see the current count.
|
|
840
912
|
|
|
841
913
|
## Testing
|
|
842
914
|
|
|
@@ -855,9 +927,18 @@ bundle exec ruby -Itest test/integration_test.rb
|
|
|
855
927
|
|
|
856
928
|
**Test Structure:**
|
|
857
929
|
|
|
858
|
-
- `test/
|
|
859
|
-
- `test/agent_eval/` —
|
|
930
|
+
- `test/agent/` — Agent runtime tests
|
|
931
|
+
- `test/agent_eval/` — Agent evaluation tests
|
|
932
|
+
- `test/cli/` — CLI command tests
|
|
860
933
|
- `test/clients/` — Provider client tests
|
|
934
|
+
- `test/evaluator/` — Core evaluation engine tests
|
|
935
|
+
- `test/history_recorder/` — Benchmark history persistence tests
|
|
936
|
+
- `test/models/` — Domain model tests
|
|
937
|
+
- `test/registry/` — Skill/eval registry tests
|
|
938
|
+
- `test/services/` — Service layer tests
|
|
939
|
+
- `test/skills/` — Skill loading tests
|
|
940
|
+
- `test/tools/` — Agent tool tests
|
|
941
|
+
- Plus several top-level `test/*_test.rb` files (e.g. `integration_test.rb`, `evaluation_runner_test.rb`, `trend_tracker_test.rb`).
|
|
861
942
|
|
|
862
943
|
---
|
|
863
944
|
|
|
@@ -886,17 +967,24 @@ Ruby Skill Bench is designed with security as a primary concern. The system exec
|
|
|
886
967
|
- **Command Allowlist:** Only explicitly allowed commands can be executed
|
|
887
968
|
- **Dangerous Commands Blocklist:** Dangerous commands (bash, curl, sudo, etc.) are always blocked
|
|
888
969
|
- **Shell Tokenization:** Commands are tokenized before execution to prevent shell injection
|
|
889
|
-
- **
|
|
970
|
+
- **Fail-Closed Host Execution:** When no container is active, commands would run on the host inside a temporary sandbox directory. `run_command` refuses to execute unless `allow_host_execution: true` is set; it is **disabled by default**. Prefer installing Docker and building the image (`bundle exec rake docker:build`) so isolation activates automatically.
|
|
971
|
+
|
|
972
|
+
> **The allowlist is the only real authorization control — and it only checks the base command.** `run_command` authorizes by the first token of the command (`rake`, `find`, `git`, …); it does **not** inspect arguments. Shell tokenization stops metacharacter injection, but it does **not** sandbox what an allowlisted binary can do. Because many common tools are general-purpose execution wrappers, **allowlisting any one of them is equivalent to granting arbitrary host code execution** — for example `rake -e '...'`, `rspec -e`, `make` (arbitrary recipes), `find . -exec ...`, or `git` (hooks, `-c core.fsmonitor=...`, `! ...` aliases). Combined with the fail-closed model above (`run_command` refuses to run on the host unless `allow_host_execution` is explicitly enabled — see `HOST_EXECUTION_REFUSED` in `run_command.rb`), the practical guidance is: **keep `allowed_commands` as minimal as possible — empty for untrusted skills** — and treat every entry as if you were handing the skill a shell.
|
|
973
|
+
>
|
|
974
|
+
> An **optional, default-off** `command_argument_constraints` setting can refuse commands whose arguments contain configured substrings/flags (for example blocking `-e` or `-exec`). It is a defense-in-depth speed bump, **not** a sandbox, and is unset by default; the allowlist remains the control that matters.
|
|
890
975
|
|
|
891
976
|
#### Docker Security Hardening
|
|
892
977
|
|
|
893
|
-
|
|
978
|
+
> **Status:** Container isolation **ships** when a Docker daemon is available and the packaged build context can produce `evaluator-sandbox:<version>`. Build with `bundle exec rake docker:build` (see [docs/docker.md](docs/docker.md)). Without Docker, `run_command` remains fail-closed on the host unless `allow_host_execution` is set.
|
|
894
979
|
|
|
895
|
-
|
|
980
|
+
Containers launch with hardened security settings:
|
|
981
|
+
|
|
982
|
+
- **Non-root User:** Containers run as the host `uid:gid`
|
|
896
983
|
- **Privilege Prevention:** `--security-opt no-new-privileges` prevents privilege escalation
|
|
897
|
-
- **Capability Dropping:**
|
|
984
|
+
- **Capability Dropping:** `--cap-drop ALL` with only `CHOWN`/`DAC_OVERRIDE` restored for volume ops
|
|
898
985
|
- **Network Isolation:** `--network none` disables network access
|
|
899
|
-
- **
|
|
986
|
+
- **Sandbox mount:** Host temp sandbox is bind-mounted at `/sandbox` read-write
|
|
987
|
+
- **Read-only root (future):** Not yet applied; tracked as follow-up hardening
|
|
900
988
|
|
|
901
989
|
#### Resource Limits
|
|
902
990
|
|
|
@@ -922,12 +1010,10 @@ When Docker is available, containers are launched with hardened security setting
|
|
|
922
1010
|
|
|
923
1011
|
### Reporting Security Issues
|
|
924
1012
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
3. **Allow Time for Fix:** Give maintainers time to address the issue before disclosure
|
|
930
|
-
4. **Follow Responsible Disclosure:** Follow responsible disclosure practices
|
|
1013
|
+
To report a security vulnerability, please follow the process in
|
|
1014
|
+
[SECURITY.md](SECURITY.md). **Do not open a public issue** — use GitHub's
|
|
1015
|
+
private vulnerability reporting (Security tab) or email the maintainer at
|
|
1016
|
+
[ismael.marin@gmail.com](mailto:ismael.marin@gmail.com).
|
|
931
1017
|
|
|
932
1018
|
---
|
|
933
1019
|
|
|
@@ -955,9 +1041,9 @@ If you discover a security vulnerability:
|
|
|
955
1041
|
- **Solution:** Increase `max_execution_time` in your `skill-bench.json` or simplify the task
|
|
956
1042
|
- **Check:** Verify the command isn't hanging or waiting for input
|
|
957
1043
|
|
|
958
|
-
**Problem:** "
|
|
959
|
-
- **
|
|
960
|
-
- **
|
|
1044
|
+
**Problem:** "Command execution refused: no sandbox isolation is active and 'allow_host_execution' is not enabled"
|
|
1045
|
+
- **Cause:** No Docker container was started (daemon missing, image missing, or build failed), so commands would run on the host. SkillBench fails closed by default rather than run un-isolated.
|
|
1046
|
+
- **Solution:** Install Docker and run `bundle exec rake docker:build`, **or** set `"allow_host_execution": true` in `skill-bench.json` to permit allowed commands on the host (inside the temporary sandbox directory). Prefer Docker when possible.
|
|
961
1047
|
|
|
962
1048
|
**Problem:** "Context hydration failed"
|
|
963
1049
|
- **Solution:** Verify the source path exists and is a directory
|
|
@@ -1006,15 +1092,62 @@ If you encounter issues not covered here:
|
|
|
1006
1092
|
|
|
1007
1093
|
## CI/CD Integration
|
|
1008
1094
|
|
|
1009
|
-
|
|
1095
|
+
### Batch Runs
|
|
1096
|
+
|
|
1097
|
+
Run every eval at once instead of one at a time:
|
|
1098
|
+
|
|
1099
|
+
```bash
|
|
1100
|
+
# Every eval under the default evals/ directory
|
|
1101
|
+
skill-bench run --all --skill=my-service
|
|
1102
|
+
|
|
1103
|
+
# Or point at a specific directory
|
|
1104
|
+
skill-bench run --evals-dir path/to/evals --skill=my-service
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
A batch run exits `0` only when **every** eval passes and non-zero if any fail, so the process exit code is itself a CI gate. Two formats are built for batch consumption:
|
|
1108
|
+
|
|
1109
|
+
- `--summary` emits an aggregate JSON gate — `passed` / `failed` / `total` counts, summed `tokens` and `cost`, and the `worst_delta` eval (the smallest context-minus-baseline delta in the batch). Archive it as a single machine-readable artifact:
|
|
1110
|
+
|
|
1111
|
+
```bash
|
|
1112
|
+
skill-bench run --all --skill=my-service --summary
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
- `--format junit` aggregates the batch into one JUnit document with **one `<testcase>` per eval** (a `<failure>` child for each failing eval), so test reporters show per-eval results:
|
|
1116
|
+
|
|
1117
|
+
```bash
|
|
1118
|
+
skill-bench run --all --skill=my-service --format junit > junit.xml
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### GitHub Action
|
|
1122
|
+
|
|
1123
|
+
Downstream repos can gate a skill change on every push or PR with the bundled composite action. Add a step that references `igmarin/ruby-skill-bench@v1`:
|
|
1124
|
+
|
|
1125
|
+
```yaml
|
|
1126
|
+
# .github/workflows/skill-bench.yml
|
|
1127
|
+
name: skill-bench
|
|
1128
|
+
on: [pull_request]
|
|
1129
|
+
|
|
1130
|
+
jobs:
|
|
1131
|
+
skill-bench:
|
|
1132
|
+
runs-on: ubuntu-latest
|
|
1133
|
+
steps:
|
|
1134
|
+
- uses: actions/checkout@v4
|
|
1135
|
+
- uses: igmarin/ruby-skill-bench@v1
|
|
1136
|
+
with:
|
|
1137
|
+
evals-dir: evals # directory scanned for evals (default: evals)
|
|
1138
|
+
skill: skills/my-service # skill applied to every eval (default: "")
|
|
1139
|
+
format: junit # human | json | junit | html (default: junit)
|
|
1140
|
+
ruby-version: "3.3" # Ruby for ruby/setup-ruby (default: 3.3)
|
|
1141
|
+
args: --summary # extra flags appended verbatim (e.g. --summary, --pack NAME)
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
The action installs the gem and runs `skill-bench run --all --evals-dir <evals-dir> --format <format>` (adding `--skill` when set and appending `args` verbatim). The run step's exit code is the gate. For a full copy-paste workflow template, see [`examples/ci/`](examples/ci/).
|
|
1145
|
+
|
|
1146
|
+
> The gem's own repository CI (`.github/workflows/ci.yml`) runs the test suite — rubocop, reek, and minitest against Ruby 3.3 and 3.4, on push and pull requests — and is separate from the reusable action above.
|
|
1010
1147
|
|
|
1011
|
-
|
|
1012
|
-
- Tests against Ruby 3.3 and 3.4
|
|
1013
|
-
- Executes rubocop, reek, and minitest
|
|
1014
|
-
- Outputs JUnit XML for test reporting
|
|
1148
|
+
To preview the machine-readable output locally:
|
|
1015
1149
|
|
|
1016
1150
|
```bash
|
|
1017
|
-
# Run locally with CI output
|
|
1018
1151
|
skill-bench run my-eval --skill=my-skill --format json
|
|
1019
1152
|
```
|
|
1020
1153
|
|
data/docs/architecture.md
CHANGED
|
@@ -172,9 +172,11 @@ project-root/
|
|
|
172
172
|
│ └── my-first-eval/
|
|
173
173
|
│ ├── task.md # Agent prompt
|
|
174
174
|
│ └── criteria.json # Scoring rules
|
|
175
|
-
└── .skill-bench-
|
|
175
|
+
└── .skill-bench-trends.json # Benchmark history (auto-generated)
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
A `.skill-bench-trends.json.bak` file is created automatically as a backup of the trend file.
|
|
179
|
+
|
|
178
180
|
### Skill Discovery
|
|
179
181
|
|
|
180
182
|
Skills are discovered recursively. These are all valid:
|
|
@@ -198,3 +200,10 @@ The eval directory must contain at minimum:
|
|
|
198
200
|
|
|
199
201
|
- `task.md` — the agent prompt
|
|
200
202
|
- `criteria.json` — the scoring rules (optional; defaults to empty criteria if missing)
|
|
203
|
+
|
|
204
|
+
## Command isolation
|
|
205
|
+
|
|
206
|
+
Agent tool commands prefer a Docker container (`evaluator-sandbox`) when available.
|
|
207
|
+
See [docker.md](docker.md) for the image contract and `rake docker:build`.
|
|
208
|
+
Without Docker, `run_command` refuses host execution unless `allow_host_execution` is true.
|
|
209
|
+
|
data/docs/docker.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Evaluator sandbox Docker image
|
|
2
|
+
|
|
3
|
+
Part of milestone **container-isolation-v1**. This document is the image contract
|
|
4
|
+
for `evaluator-sandbox`, the container SkillBench uses for isolated `run_command`
|
|
5
|
+
execution when a Docker daemon is available.
|
|
6
|
+
|
|
7
|
+
## Contract (MVP)
|
|
8
|
+
|
|
9
|
+
| Item | Value |
|
|
10
|
+
|------|--------|
|
|
11
|
+
| Image name | `evaluator-sandbox` (`Constants::Sandbox::DOCKER_IMAGE_NAME`) |
|
|
12
|
+
| Tags | `evaluator-sandbox:<SkillBench::VERSION>` and `evaluator-sandbox:latest` |
|
|
13
|
+
| Base | `ruby:3.4-bookworm` (aligns with CI Ruby 3.4; 3.3 hosts still run evals via host Ruby for the orchestrator) |
|
|
14
|
+
| Preinstalled | Ruby (from base), `git`, minimal Debian tools from base image |
|
|
15
|
+
| Working directory | `/sandbox` (host sandbox dir is bind-mounted here read-write) |
|
|
16
|
+
| Default process | `sleep infinity` — container stays up for `docker exec` |
|
|
17
|
+
| Network at runtime | **none** (`--network none`) — no `apt`/`bundle install` during evals |
|
|
18
|
+
| User | Host `uid:gid` via `docker run --user` |
|
|
19
|
+
| Privileges | `no-new-privileges`, `cap-drop ALL`, add back `CHOWN` + `DAC_OVERRIDE` only |
|
|
20
|
+
|
|
21
|
+
## Build
|
|
22
|
+
|
|
23
|
+
From the repo root (or any install that includes the packaged context):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle exec rake docker:build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This builds from `lib/skill_bench/execution/docker` and tags:
|
|
30
|
+
|
|
31
|
+
- `evaluator-sandbox:<version>` (from `SkillBench::VERSION`)
|
|
32
|
+
- `evaluator-sandbox:latest`
|
|
33
|
+
|
|
34
|
+
## Multi-arch notes
|
|
35
|
+
|
|
36
|
+
Build on the machine (or CI runner) that will run evals. Docker Desktop on Apple
|
|
37
|
+
Silicon produces arm64 images; GitHub `ubuntu-latest` produces amd64. Do not
|
|
38
|
+
assume a single cached image works across both without multi-platform build.
|
|
39
|
+
|
|
40
|
+
## Out of scope (MVP)
|
|
41
|
+
|
|
42
|
+
- Full Bundler/dev toolchain prebake
|
|
43
|
+
- Per-eval custom Dockerfiles
|
|
44
|
+
- Read-only root filesystem (documented as future hardening)
|
|
45
|
+
- gVisor / Firecracker
|
data/docs/first-eval-guide.md
CHANGED
|
@@ -268,7 +268,7 @@ Provider is read from `skill-bench.json` — no `--provider` flag needed.
|
|
|
268
268
|
2. Agent runs **with** skill context → produces context output
|
|
269
269
|
3. Judge scores both independently → per-dimension scores
|
|
270
270
|
4. Engine computes deltas → applies pass/fail logic
|
|
271
|
-
5. Result is recorded in `.skill-bench-
|
|
271
|
+
5. Result is recorded in `.skill-bench-trends.json` for trend tracking
|
|
272
272
|
|
|
273
273
|
**Run with multiple skills:**
|
|
274
274
|
|
|
@@ -346,7 +346,7 @@ Both skill contexts are concatenated. The judge evaluates whether the combined c
|
|
|
346
346
|
| **BASELINE** | Score without skill (unaided performance). Think: "How well does the AI do on its own?" |
|
|
347
347
|
| **CONTEXT** | Score with skill (aided performance). Think: "How well does the AI do when it reads my skill?" |
|
|
348
348
|
| **DELTA** | Improvement = CONTEXT - BASELINE. Think: "How many points did my skill add?" |
|
|
349
|
-
| **TREND** | Change since the *previous* run of this exact eval + skill. Stored in `.skill-bench-
|
|
349
|
+
| **TREND** | Change since the *previous* run of this exact eval + skill. Stored in `.skill-bench-trends.json`. |
|
|
350
350
|
| **VERDICT** | PASS only if CONTEXT >= threshold AND DELTA >= minimum_delta. Both must be true. |
|
|
351
351
|
| **Iterations** | ReAct loop steps for each run: thought → tools → observation. Helps you understand *how* the agent worked. |
|
|
352
352
|
| **What went well** | Dimensions scoring ≥ 80% of max, with judge reasoning. Strengths of your skill. |
|
|
@@ -417,10 +417,10 @@ Your first run probably will not pass. That is normal. Here is how to improve.
|
|
|
417
417
|
|
|
418
418
|
### Use the History File
|
|
419
419
|
|
|
420
|
-
After each run, SkillBench appends to `.skill-bench-
|
|
420
|
+
After each run, SkillBench appends to `.skill-bench-trends.json`. You can read it to track progress:
|
|
421
421
|
|
|
422
422
|
```bash
|
|
423
|
-
cat .skill-bench-
|
|
423
|
+
cat .skill-bench-trends.json | jq '.[-1]'
|
|
424
424
|
```
|
|
425
425
|
|
|
426
426
|
Look at the dimension with the **smallest delta**. That is where your skill is weakest. Open `SKILL.md` and add a concrete rule targeting that dimension.
|
|
@@ -467,7 +467,7 @@ Created by `skill-bench init`. Stores provider, API key, model, timeout, and all
|
|
|
467
467
|
}
|
|
468
468
|
```
|
|
469
469
|
|
|
470
|
-
### `.skill-bench-
|
|
470
|
+
### `.skill-bench-trends.json` — Evaluation History (Auto-Generated)
|
|
471
471
|
|
|
472
472
|
A JSON array recording every successful eval run. SkillBench writes it automatically. It stores timestamps, eval names, skill names, scores, and deltas. This powers the **TREND** line in your output.
|
|
473
473
|
|
|
@@ -487,9 +487,9 @@ A JSON array recording every successful eval run. SkillBench writes it automatic
|
|
|
487
487
|
|
|
488
488
|
**Tip:** Commit this file to git if you want to share trend data with your team.
|
|
489
489
|
|
|
490
|
-
### `.skill-bench-
|
|
490
|
+
### `.skill-bench-trends.json.bak` — Backup (Auto-Generated)
|
|
491
491
|
|
|
492
|
-
A
|
|
492
|
+
A snapshot of the previous good version of the history file, copied just before each new write. If the main file gets corrupted, SkillBench recovers from this backup automatically. You never need to touch it.
|
|
493
493
|
|
|
494
494
|
---
|
|
495
495
|
|
data/docs/testing-guide.md
CHANGED
|
@@ -273,7 +273,7 @@ Both must be true. This prevents two failure modes:
|
|
|
273
273
|
TREND: baseline ↑ (+2), context ↑ (+7)
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
This compares the current run against the **previous run of the same eval + skill** (stored in `.skill-bench-
|
|
276
|
+
This compares the current run against the **previous run of the same eval + skill** (stored in `.skill-bench-trends.json`).
|
|
277
277
|
|
|
278
278
|
- `↑` = improved since last run
|
|
279
279
|
- `↓` = regressed since last run
|
|
@@ -16,6 +16,7 @@ module SkillBench
|
|
|
16
16
|
def self.call(initial_prompt, max_iterations, config)
|
|
17
17
|
messages = [{ role: 'user', content: initial_prompt }]
|
|
18
18
|
iterations_log = []
|
|
19
|
+
total_usage = empty_usage
|
|
19
20
|
step_count = 0
|
|
20
21
|
|
|
21
22
|
while step_count < max_iterations
|
|
@@ -24,24 +25,27 @@ module SkillBench
|
|
|
24
25
|
step_result = Step.call(messages, config)
|
|
25
26
|
iteration = step_result[:iteration]
|
|
26
27
|
iterations_log << attach_step_number(iteration, step_count) if iteration
|
|
28
|
+
total_usage = add_usage(total_usage, step_result[:usage])
|
|
27
29
|
|
|
28
30
|
unless step_result[:continue]
|
|
29
31
|
final_result = step_result[:result] || { success: false, response: { error: { message: 'Step returned no result' } } }
|
|
30
|
-
return
|
|
32
|
+
return finalize(final_result, iterations_log, total_usage)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
messages = step_result[:messages]
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
finalize(
|
|
37
39
|
{ success: false, response: { error: { message: Agent::ReactAgent::MAX_ITERATIONS_REACHED } } },
|
|
38
|
-
iterations_log
|
|
40
|
+
iterations_log,
|
|
41
|
+
total_usage
|
|
39
42
|
)
|
|
40
43
|
rescue StandardError => e
|
|
41
44
|
SkillBench::ErrorLogger.log_error(e, 'ReactAgent Error')
|
|
42
|
-
|
|
45
|
+
finalize(
|
|
43
46
|
{ success: false, response: { error: { message: e.message } } },
|
|
44
|
-
iterations_log
|
|
47
|
+
iterations_log,
|
|
48
|
+
total_usage
|
|
45
49
|
)
|
|
46
50
|
end
|
|
47
51
|
|
|
@@ -54,14 +58,45 @@ module SkillBench
|
|
|
54
58
|
iteration.merge(step_number: step_count)
|
|
55
59
|
end
|
|
56
60
|
|
|
57
|
-
# Merges the collected iterations into the
|
|
61
|
+
# Merges the collected iterations and accumulated usage into the response.
|
|
58
62
|
#
|
|
59
63
|
# @param result [Hash] The final result hash from the loop.
|
|
60
64
|
# @param iterations_log [Array<Hash>] Collected iteration metadata.
|
|
61
|
-
# @
|
|
62
|
-
|
|
65
|
+
# @param total_usage [Hash] Summed token usage across all iterations.
|
|
66
|
+
# @return [Hash] The result with :iterations and :usage injected into :response.
|
|
67
|
+
def self.finalize(result, iterations_log, total_usage)
|
|
63
68
|
response = result[:response] || {}
|
|
64
|
-
result.merge(response: response.merge(iterations: iterations_log))
|
|
69
|
+
result.merge(response: response.merge(iterations: iterations_log, usage: total_usage))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A zeroed token-usage accumulator.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash] Usage hash with prompt/completion/total token counts set to zero.
|
|
75
|
+
def self.empty_usage
|
|
76
|
+
{ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Adds a single step's usage onto a running total.
|
|
80
|
+
#
|
|
81
|
+
# @param total [Hash] The running usage total.
|
|
82
|
+
# @param usage [Hash, nil] A step's usage hash (may be nil or empty).
|
|
83
|
+
# @return [Hash] A new summed usage hash.
|
|
84
|
+
def self.add_usage(total, usage)
|
|
85
|
+
usage ||= {}
|
|
86
|
+
{
|
|
87
|
+
prompt_tokens: total[:prompt_tokens] + token_count(usage, :prompt_tokens),
|
|
88
|
+
completion_tokens: total[:completion_tokens] + token_count(usage, :completion_tokens),
|
|
89
|
+
total_tokens: total[:total_tokens] + token_count(usage, :total_tokens)
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Reads a token count from a usage hash, tolerating string keys.
|
|
94
|
+
#
|
|
95
|
+
# @param usage [Hash] The usage hash.
|
|
96
|
+
# @param key [Symbol] The usage key (e.g. :prompt_tokens).
|
|
97
|
+
# @return [Integer] The token count, or zero when absent.
|
|
98
|
+
def self.token_count(usage, key)
|
|
99
|
+
(usage[key] || usage[key.to_s] || 0).to_i
|
|
65
100
|
end
|
|
66
101
|
end
|
|
67
102
|
end
|