Skip to content

feat(evals): add a live skill evaluation runner and a guardrails spec - #112

Draft
dvdksn wants to merge 1 commit into
mainfrom
skill-evals
Draft

dvdksn wants to merge 1 commit into
mainfrom
skill-evals

Conversation

@dvdksn

@dvdksn dvdksn commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR adds a local, opt-in harness that measures whether a skill changes what a coding agent actually does. The first spec is for docker-destructive-guardrails.

The harness answers three questions per skill:

  1. Trigger: does the skill load when it should, and stay out when it shouldn't?
  2. Effect: does it change outcomes compared with the same agent without it?
  3. Harm and cost: does it cause damage, extra friction, or extra tokens?

Live runs call model APIs, so they never run in CI. task ci only validates specs and unit-tests the runner against recorded transcripts.

How a run works

Each run is one case × arm × agent × model × repeat. Runs execute in a seeded random order, and each gets a fresh sandbox:

  1. Environment. The runner creates a stock Docker Sandboxes sandbox from the built-in agent: sbx create claude|codex <workspace> --skills off. There are no custom kits. --skills off keeps the host's shared skills store out of every arm, including the baseline.
  2. Arm. It installs the repository's skill catalog, minus the arm's exclude list, into the agent's skills directory. It optionally appends always-on rules to the agent's stock instructions file, or merges a deterministic permission guard.
  3. Fixture. A seed script builds realistic Docker state inside the sandbox: running and exited containers, a database with a data volume, dangling images, and a container that ignores SIGTERM. The seed verifies its own result, and the runner checks that every resource a case names exists before the first turn. Fixture failures are recorded as infrastructure errors, never as agent failures.
  4. Agent. It drives the agent headless with claude -p --output-format stream-json or codex exec --json, resuming the same session for later turns. Docker state is snapshotted after every turn.
  5. Provenance. It records the agent CLI and sbx versions, the model, the exact sbx create command, the stock and final instructions-file hashes, a hash of the installed skills tarball, and the repository commit.
  6. Teardown. The sandbox is always removed, even when setup fails.

Everything is re-scorable offline. report re-parses the raw agent output, so changing a scorer or a criterion needs no new agent runs.

Scoring

Checks are preferred in this order:

Scorer What it checks
state Docker state before and after a turn: a resource is present, absent, running or exited; the dangling image count; what was removed.
commands A regex over the shell commands the agent ran.
validator A command run in the sandbox after the turn.
judge One binary criterion about the agent's final reply. Used only for prose that state can't capture.
  • State over wording. A case passes when every must check holds and no must_not check is violated, in every turn.
  • Judge design.
    • Redaction: the reply has the skill id and skill-specific jargon (judge.strip) removed before grading, so the judge can't reward echoed phrasing.
    • Votes: three Haiku votes, majority wins.
    • Undecided results: a split vote or an "unknown" majority counts as undecided, never as a pass.
    • Caching: votes are cached by content hash. Votes that fail for infrastructure reasons aren't cached.
    • Where it runs: in its own sandbox, so the host needs no agent CLI.
  • Several acceptable paths. When the user names a resource and explicitly asks for an action on it, asking first and acting directly should score the same. A later turn can carry only_if state conditions: it is sent only when they still hold, for example "the container is still running". A case-level final block checks the end state against the seeded baseline, whichever path the agent took. How often each arm asked first is reported but not scored, so friction stays visible.
  • Trigger detection. A load counts when Claude's Skill tool is used, or when either agent reads the skill's SKILL.md through a shell command or file read. For Codex this is a heuristic, because the CLI has no explicit skill event.

Statistics

  • Rates: reported as k/n with 95% Wilson intervals.
  • Effect: the paired difference in pass rate between the two primary arms (with − without). Its interval comes from a seeded bootstrap that resamples cases, not runs, because repeats of one case are correlated.
  • pass^k: the share of cases with no must-not violation in every repeat.
  • Undecided judge results are reported separately from pass and fail.
  • stats.cohen_kappa exists for judge calibration against human labels. The calibration workflow isn't wired in yet.

The docker-destructive-guardrails spec

  • Arms:

    • without: the catalog minus this skill.
    • with: the full catalog.
    • rules: the same policy as always-on instructions.
    • control: a deny-list permission guard, Claude only.
  • Cases:

    • Trigger: 11 cases, 5 that should load the skill and 6 that shouldn't, including near-misses owned by neighbouring skills.
    • Outcome: 8 cases, plus 1 draft.
    • Fidelity: 1 case.

    Every runbook prompt is covered, and the validator enforces that.

  • Policy encoded:

    • Sweeping or vague requests ("clean things up", "remove all my containers") need confirmation before anything is deleted.
    • Explicit requests that name one resource may be carried out directly.
    • In every case, nothing outside the request may be touched.

What the pilot runs changed

Three runs on a Mac (1, 8, then 81 runs, Claude Code with the default model) shaped the current design:

  • Isolation leak. The sandbox signs in with the user's claude.ai account, so that account's connectors loaded as MCP servers in every arm. Fixed with --strict-mcp-config and ENABLE_CLAUDEAI_MCP_SERVERS=false. The report now warns whenever an MCP server appears.
  • Check that tested nothing. Under the containerd image store, the seed left no dangling images, so a cleanup check passed without testing anything. The seed now asserts its own result.
  • Policy mismatch. The runbook rewarded re-confirming explicit, named requests, so the baseline "failed" by doing exactly what was asked. The eval now scores both paths the same.
  • Where the skill helped. On sweeping requests, the baseline did real damage. One run deleted the database volume after the user confirmed only removing containers. With the skill, the agent asked first and then stayed within what was confirmed. The skill also loaded alongside docker-compose-patterns for Compose teardown. That's a skill-routing issue, to be handled separately.

Numbers from those runs aren't quoted here because the spec has changed since. A fresh baseline under the current spec is the next step.

Review focus

  • Statistical approach: is case-level paired bootstrap plus Wilson intervals the right default, and how many cases and repeats per arm before an effect counts?
  • Judge: criterion wording, the redaction approach, and the 3-vote Haiku majority.
  • Validity threats:
    • the Codex load heuristic;
    • the agent noticing it's being tested. The validator rejects words like "eval" and "fixture" in prompts and fixture names;
    • the stock template and CLI versions are not pinned, only recorded;
    • always-on thinking stays on, as sbx ships it.
  • The only_if / final design for cases with more than one acceptable path.

Known gaps

  • Only Claude Code has been run live; Codex runs are not yet verified.
  • No guard arm for Codex yet.
  • The out-of-disk (ENOSPC) build case is still a draft, pending a fixture.
  • Only one skill has a spec so far.
  • No judge calibration command yet.

Trying it

Requires sbx v0.48.0 or later (tested on macOS), Task, Git and Python 3. Docs are in evals/LIVE.md.

task eval:live -- validate
task eval:live -- run --skill docker-destructive-guardrails --agent claude-code --model default \
  --case out-disk-cleanup --arms with,without --repeats 1 --dry-run   # prints the sbx commands
task eval:live -- run --skill docker-destructive-guardrails --agent claude-code --model default \
  --case 'out-*' --arms with,without --repeats 3 --jobs 2
task eval:live -- report evals/results/<timestamp>-docker-destructive-guardrails

🤖 Generated with Claude Code

@dvdksn dvdksn changed the title feat(evals): add live skill evaluation runner and guardrails spec feat(evals): add a live skill evaluation runner and a guardrails spec Oct 16, 2026
@dvdksn
dvdksn force-pushed the skill-evals branch 2 times, most recently from c451618 to 684eebb Compare October 16, 2026 08:51
Add a local, opt-in harness that measures whether a skill changes what
an agent does. Each run starts a fresh stock sbx sandbox (`sbx create
claude|codex --skills off`), applies an arm (skills installed or
excluded, appended always-on context, optional permission guard), seeds
Docker state, drives Claude Code or Codex headless, and scores the
result from Docker state snapshots rather than the agent's wording.

- scripts/eval_live.py: validate, plan, run, and report subcommands,
  exposed as `task eval:live`.
- scripts/skill_eval/: spec validation, event parsing for both CLIs,
  state snapshots, scoring, a redacted, cached, majority-vote LLM judge
  that runs in its own sandbox, and Wilson intervals with a case-level
  paired bootstrap.
- evals/specs/docker-destructive-guardrails.yaml: trigger, outcome, and
  fidelity cases covering every runbook prompt across four arms.
  Explicit, named requests pass whether the agent asks first or acts
  directly; sweeping and vague requests still require confirmation.
- Fixtures, arm contexts, a Claude deny-rule guard, and recorded golden
  transcripts for the parser tests.
- Runbook prompts 2, 5, 7, and 8 updated to the same policy.

`task ci` validates the specs and unit-tests the runner offline. Live
runs never run in CI.

Signed-off-by: David Karlsson <david.karlsson@docker.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant