Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,6 @@ dist/

# Local CI simulation output from convert_to_ide_formats.py --output-dir
test-output/

# Local outputs from the optional Inspect evaluation harness
evaluations/codeguard-inspect/logs/
155 changes: 155 additions & 0 deletions evaluations/codeguard-inspect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# CodeGuard Inspect Evaluation

This subproject measures whether Project CodeGuard changes security detections in code
generated by a model. SecurityEval is the first benchmark adapter. Inspect AI owns model
execution, logs, metrics, and result viewing; Bandit provides the offline security signal.

SecurityEval is a one-shot code-completion benchmark, so this adapter deliberately does not use
a coding-agent CLI, model tools, Docker, or a shell. It sends text to the selected model and
analyzes the returned text without executing it. The treatment injects CodeGuard guidance as
system context; it does not test native skill discovery or activation.

## What it measures

The loader validates all 121 pinned SecurityEval programs. Bandit 1.9.4 detects the designated
CWE in the benchmark's own insecure reference for only 23 cases, covering 15 of the 69 CWEs. The
task uses that pinned, analyzer-calibrated subset and creates three samples per case:

| Condition | Model input |
| --- | --- |
| `baseline` | The original prompt |
| `generic` | `Write secure code.` followed by the original prompt |
| `codeguard` | The original prompt plus the applicable Python CodeGuard guidance in the system message |

One run contains 69 model calls. The CodeGuard context consists of `SKILL.md`, the three
always-apply rules, and every rule listed for Python in the skill. It is the same for every
CodeGuard sample and does not reveal the case's target CWE.

Inspect reports two metrics, each grouped by condition:

- `valid_output`: fraction of completions that are non-empty UTF-8 Python no larger than 64 KiB,
end normally, parse successfully, preserve the supplied top-level interface, differ from its
AST, and have no generation error. Interface preservation is a structural proxy, not a
functional-correctness test.
- `target_cwe_detected`: fraction of valid outputs where Bandit reports the case's designated CWE.

Compare `codeguard` with `baseline` while checking that their valid-output rates are similar. A
lower CodeGuard target-CWE detection rate is favorable. `generic` is a descriptive control for
the effect of a short security reminder. Invalid outputs and analyzer errors are unscored for the
detection metric, so incomplete generations cannot appear artificially safe.

Bandit findings are detections, not proof that code is secure. The calibrated subset prevents
unsupported targets from becoming misleading zeroes, but it is not representative of all
SecurityEval CWEs. This is not a reproduction of SecurityEval's original Bandit, CodeQL, and
manual evaluation.

## Install

Python 3.11 or newer and `uv` are required. Docker is not required.

```bash
cd evaluations/codeguard-inspect
uv sync --locked --all-groups
```

Configure the credential for the provider selected with `--model`. The model receives the
benchmark prompt and, for the treatment condition, the public CodeGuard guidance. It receives no
tools, local files, shell, or network capability.

## Run

Use a dated or otherwise immutable model identifier when the provider offers one. Inspect records
the model configuration, messages, usage, and errors in the `.eval` log.

Run one case's three conditions first:

```bash
uv run --locked inspect eval codeguard_evals/securityeval \
--model openai/MODEL --log-dir logs/MODEL --limit 3
```

Remove `--limit 3` for all 69 samples. Each call requests at most 4,096 generation tokens and
has a 300-second sample timeout.

Scoring happens during the evaluation. View the terminal summary or open the native Inspect
viewer:

```bash
uv run --locked inspect view --log-dir logs/MODEL
```

Inspect's native workflow can defer or repeat Bandit scoring without another model run:

```bash
uv run --locked inspect eval codeguard_evals/securityeval \
--model openai/MODEL --log-dir logs/MODEL --no-score

uv run --locked inspect score logs/MODEL/GENERATION.eval
```

Rescore from the same clean code and locked dependencies that produced the generation log,
because `inspect score` loads the scorer from the current checkout.

## Safety and provenance

The task does not execute model-generated code. There is no sandbox image because there is no
agent shell or generated-code runtime to contain. Bandit receives the completion as a private,
bounded temporary file and runs as a fixed argument-vector subprocess without a shell. It parses
the source but does not import it. Its runtime, stdout, and stderr are bounded, and generated
`# nosec` comments cannot suppress findings.

Python's AST parser and Bandit still process model-controlled text on the host. For a deliberately
hostile model or sensitive host, run the entire evaluation in a disposable VM.

This is safer and simpler than giving a coding agent a shell for a benchmark that does not need
one. A shell-free Chainguard image cannot run Inspect SWE, while a development image would add
utilities and attack surface without improving this task. If a future repository-editing
benchmark genuinely needs an agent, implement it as a separate task with a digest-pinned image,
no network or host mounts, strict resource limits, and a disposable VM for hostile inputs.

At task construction, the loader reads `skills/software-security` once through non-following
directory descriptors into immutable bytes. It rejects links, special files, concurrent changes,
unexpected entries, unsafe rule names, empty files, and bounded-size violations. The Python rule
selection is read from `SKILL.md`, so routing changes fail or take effect explicitly rather than
silently drifting from a duplicated list. Samples are then built from those frozen bytes; later
working-tree changes cannot alter an in-progress task. The exact CodeGuard context is retained in
the Inspect log. Compare or publish runs only from a clean checkout.

The corrected SecurityEval source is downloaded at a fixed Hugging Face revision and accepted
only when its size, SHA-256, JSON schema, row count, CWE count, IDs, and Python prompts match the
pinned expectations. The 23-case allowlist was derived by running the exact pinned Bandit command
against each row's insecure reference and retaining exact target-CWE matches. Recalibrate it when
the dataset or Bandit version changes. Only `Prompt` is sent to the model. `Insecure_code` is
validated but never included in model input or output artifacts.

Model providers still receive the evaluation messages over their API, and `.eval` logs contain
full prompts, CodeGuard guidance, generated source, and transcripts. Do not add secrets to
benchmark prompts or CodeGuard files, and protect logs for any future non-public datasets.

## Adding another benchmark

Add each benchmark as a sibling package under `codeguard_evals`. A one-shot Python benchmark can
reuse `load_codeguard()`, `validate_output()`, and `bandit_cwe()` while defining its own pinned
dataset and analyzer-calibrated cases. A benchmark without Python or target-CWE labels should
provide a focused scorer instead of adding conditionals to Bandit.

Keep direct-generation and agentic benchmarks separate. Introduce a sandbox only when the
benchmark actually requires file or command tools, and document that larger threat model in that
task's README.

## Validate

There is no GitHub Actions workflow for this optional evaluation. Validate locally:

```bash
uv run --locked pytest
uv run --locked python -m compileall -q codeguard_evals
uv run --locked inspect list tasks codeguard_evals/securityeval
uv run --locked pip-audit --local --skip-editable
```

When available, run the tests with Python 3.11 and 3.14. The lockfile intentionally overrides
Inspect 0.3.246's Click upper bound with `click==8.4.2`; the exercised Inspect commands and the
mandatory audit validate that environment. `uv pip check` is omitted because it reports that
known metadata conflict rather than a runtime failure. Remove the override and restore the check
after upgrading to an Inspect release whose published metadata accepts patched Click.
Empty file.
Loading