Skip to content

feat(runner): record code, corpus, and agent revisions in run-meta.json - #347

Open
vaibhavdabas16 wants to merge 6 commits into
TIGER-AI-Lab:mainfrom
vaibhavdabas16:feat/run-provenance
Open

feat(runner): record code, corpus, and agent revisions in run-meta.json#347
vaibhavdabas16 wants to merge 6 commits into
TIGER-AI-Lab:mainfrom
vaibhavdabas16:feat/run-provenance

Conversation

@vaibhavdabas16

Copy link
Copy Markdown
Contributor

Advances #309 §1. Does not close it — see the scoping note below, which is the part I'd most like a maintainer's read on.

Why this, and not the dsh adapter

#309 §1 asks for two things: a deepseek-harness adapter, and "Record the dsh package version or Git commit, plugin set, model configuration, corpus revision, and ClawBench commit in run-meta.json."

I did the second and deliberately stopped short of the first. Adding a harness here is mechanical — a directory of scripts plus a harnesses.yaml entry — but the setup-*.sh and run-*.sh contents depend entirely on dsh's actual CLI surface, config format, and transcript shape, and DeepSeek Harness is in developer preview. I can't build the image to check, so I'd be writing a plausible-looking run-deepseek-harness.sh that would probably be wrong in ways nobody could see from the diff. That seemed worse than not writing it. Happy to do the adapter as a follow-up if someone can share a working dsh invocation, or point me at the preview docs.

The provenance half turned out to be a real gap, and a general one — it applies to every harness, not just dsh.

The gap

run-meta.json records what ran — task, model, harness name, image ids — but not the revisions those names resolved to. Two runs of "openclaw on v2" a month apart are indistinguishable in the artifact even when the agent, the corpus, and ClawBench itself have all moved. That makes a published leaderboard row labelled rather than reproducible, and it blocks §2's "fixed task IDs, corpus commit, dsh revision" and §4's "document the exact reproduction commands and environment" regardless of which harness is being added.

What lands

A provenance block alongside runtime:

"provenance": {
  "clawbench_version": "0.10.0",
  "commit": "3f3599d...", "branch": "main", "dirty": false,
  "corpus": {"suite": "v2", "path": "test-cases/v2", "revision": "62ee923..."},
  "harness": {
    "name": "openclaw",
    "image_id": "sha256:...",
    "agent_version": "2026.3.13",
    "pinned_versions": {"openclaw": "2026.3.13"}
  }
}
  • corpus.revision is the last commit that touched that suite — two runs sharing it saw the same task text.
  • harness.pinned_versions is parsed from the version pins in the harness Dockerfile (opencode-ai@1.4.4, @playwright/mcp@0.0.70, pip install x==1.2). It reads what the image was built from rather than shelling into a running container, so it costs nothing at run time and works for a harness that is already gone. This answers the issue's "package version and plugin set" for any harness. Verified against the bundled Dockerfiles: openclaw, opencode, claude-code, browser-use all resolve; null (no pins) correctly reports nothing.

Failure behaviour, which is most of the design

Every lookup is best-effort and returns None rather than raising. A PyPI install has no git checkout; a container host may have no git; a task from an explicit --cases-dir has a history that isn't ClawBench's to claim. A missing provenance field must never fail a run that otherwise succeeded.

One distinction worth flagging: dirty: null means the lookup failed, which is not the same claim as false. Anyone filtering runs by "clean tree" should check is False, not truthiness — the cookbook section says so explicitly.

Lookups are lru_cached because a batch builds one block per task and the answers cannot change inside a process.

Testing

tests/test_provenance.py — 16 cases: pin extraction for each bundled harness, that FROM node:24-slim and COPY --from=...uv:0.11.6 are not mistaken for agent pins, pip-style pins, corpus resolution for bundled and external case dirs, and that a missing checkout or missing git binary yields nulls rather than an exception. test_results_and_metadata.py asserts the block reaches run-meta.json and reuses the same image id as runtime.

Full suite passes locally (291 passed, 10 skipped); ruff and pyright clean.

run-meta.json recorded what was run — task, model, harness name, image ids
— but not the revisions those names resolved to. Two runs of "openclaw on
v2" a month apart are indistinguishable in the artifact even when the
agent, the corpus, and ClawBench itself have all moved, which makes a
published leaderboard row labelled rather than reproducible.

This collects the ClawBench version, commit, branch and dirty state; the
corpus suite and the revision of the commit that last touched it; and the
agent and plugin versions pinned by the harness Dockerfile — reading the
pins the image was built from rather than asking a running container.

Every lookup is best-effort and returns None rather than raising: a PyPI
install has no git repository and a container host may have no git at all,
and a missing provenance field must never fail a run that otherwise
succeeded. `dirty: None` deliberately means "the lookup failed", which is
not the same claim as False. Lookups are cached because a batch run builds
one block per task and the answers cannot change within a process.
Adds `provenance` alongside `runtime`, reusing the harness image id that
_runtime_meta already resolves rather than inspecting the image twice.
Adds a Provenance section to the trace cookbook covering the block's
shape, what corpus.revision and harness.pinned_versions actually mean,
and why every field can be null — with the filter to apply before
comparing runs across code versions.

Tests cover pin extraction for each bundled harness, that base-image and
COPY lines are not mistaken for agent pins, pip-style pins, corpus
resolution for bundled and external case dirs, and that a missing git
checkout or git binary yields nulls rather than an exception.
"branch": _git(repo, "rev-parse", "--abbrev-ref", "HEAD"),
# `git status` succeeding with no output means a clean tree; a failed
# lookup returns None, which is not the same claim as "clean".
"dirty": (status != "") if status is not None else None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dirty would never be False.
Even on a clean checkout, the _git() returns None since it converts empty string to None.



@lru_cache(maxsize=32)
def harness_pins(harness: str) -> dict[str, str]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks the version pinned in the Dockerfile and setup scripts, but they're not necessarily the version being ran due to the existence of --no-build flag.
This should be set to either Unknown of None under such cases.

# `pkg==1.2.3` for pip. This reads the pins the image was built from rather
# than asking the agent for its version, which would need a running container.
_NPM_PIN_RE = re.compile(r"(?<![\w@/])((?:@[\w.-]+/)?[\w.-]+)@(\d[\w.+-]*)")
_PIP_PIN_RE = re.compile(r"([\w.-]+)==(\d[\w.+-]*)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not handling cases with [] such as litellm[proxy].

# Version pins declared in a harness Dockerfile: `pkg@1.2.3` for npm and
# `pkg==1.2.3` for pip. This reads the pins the image was built from rather
# than asking the agent for its version, which would need a running container.
_NPM_PIN_RE = re.compile(r"(?<![\w@/])((?:@[\w.-]+/)?[\w.-]+)@(\d[\w.+-]*)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only handles @Version but not @revisions.

@Perry2004 Perry2004 added bug Something isn't working enhancement New feature or request labels Sep 9, 2026
Four defects Perry2004 caught, all real:

`dirty` could never be False. `_git()` collapsed empty output into None, so
a clean `git status --porcelain` — which succeeds with no output — was
indistinguishable from a failed lookup. `_git()` now returns None only when
the command could not run, and "" when it ran and said nothing; callers
that want a non-empty value ask for it explicitly.

Dockerfile pins were reported as fact even under --no-build, where the
image can be arbitrarily older than the Dockerfile on disk. Pins are now
claimed only when this run built the image, with a `pins_source` field of
"dockerfile" or "unverified" saying which. The signal is set by
docker_build() rather than read off the --no-build flag, because
clawbench-batch builds once and then runs every child with --no-build —
keying off the flag would have marked the entire batch path unverified.

Pip extras dropped the pin entirely: `litellm[proxy]==1.77.3` matched
nothing at all, so the pin vanished silently rather than being recorded.

Only `name@version` was recognised, so revision pins — `pkg@github:o/r#sha`,
`pkg@git+https://…#ref`, and pip's PEP 508 `pkg @ git+…@ref` — were missed.
That is exactly how a preview-stage agent like DeepSeek Harness is pinned,
which is the case TIGER-AI-Lab#309 needs. A floating dist-tag (`@next`) is still not
collected: it names a moving target, so recording it as a pin would be a
false claim.
The clean/dirty test monkeypatched `_git` to return "", bypassing the very
`or None` conversion that made `dirty` unable to ever be False — the mock
asserted the intended behaviour while the real code did the opposite. It
now runs against a real temporary repository, and fails against the old
implementation.

Adds coverage for pip extras, all three revision-pin spellings, dist-tags
being excluded, URL userinfo not being mistaken for a pin, and pins being
claimed only for a harness whose image this run built.
Records which pin spellings are collected and why a floating dist-tag is
not, and explains when pinned_versions describes the image that actually
ran — including that a batch run reports "dockerfile" while a bare
clawbench-run --no-build reports "unverified".
@vaibhavdabas16

Copy link
Copy Markdown
Contributor Author

Thanks — all four were real, and the first two were the interesting ones. Fixed in b7bc2e0…78754ab.

dirty could never be False. Correct, and worse than it looked: _git() ended in return result.stdout.strip() or None, so a clean git status --porcelain — which succeeds with no output — came back as None, identical to a failed lookup. My own test asserted dirty is False and passed only because it monkeypatched _git to return "", bypassing the exact conversion that caused the bug. The mock asserted the intent while the code did the opposite.

_git() now returns None only when the command could not run, and "" when it ran and said nothing; callers wanting a non-empty value ask explicitly. The test runs against a real temporary git repo — I verified it fails against the old implementation rather than just passing against the new one.

--no-build. Also correct. One wrinkle worth flagging: keying off the flag alone would have been wrong in the opposite direction, because clawbench-batch builds the image once (batch.py:734) and then passes --no-build to every child. Trusting the flag would have marked the entire batch path — i.e. how leaderboard numbers are actually produced — as unverified.

So the signal is set by docker_build() itself, and children inherit it through the environment the same way CONTAINER_ENGINE already does. Pins now carry pins_source:

Value Meaning
"dockerfile" image built from this checkout during this run; pins are what ran
"unverified" image reused via --no-build; pinned_versions and agent_version are null

A batch run reports "dockerfile"; a bare clawbench-run --no-build reports "unverified". image_id is still reported either way, since that remains a fact about the run.

Extras. litellm[proxy]==1.77.3 matched nothing — the pin vanished silently rather than being recorded under a wrong name. Now {"litellm[proxy]": "1.77.3"}.

Revisions. Now handles pkg@github:owner/repo#<sha>, pkg@git+https://…#<ref>, and pip's PEP 508 pkg @ git+https://…@<ref>. That is precisely how a developer-preview agent gets pinned, so it matters for the dsh adapter this PR is groundwork for.

One deliberate exclusion: a floating dist-tag (pkg@next, pkg@latest) is not collected. It names a moving target, so recording it as a pin would be a false claim — agent_version is null for a harness pinned that way. Happy to reverse that if you'd rather see the tag recorded.

On "and setup scripts": I checked, and no bundled harness pins versions in its setup-*.sh — all installs are in the Dockerfiles. So Dockerfile-only parsing is complete today, but it would silently miss a future harness that installs in setup. Say the word if you want the setup scripts scanned too.

CI is green; the full suite passes locally (300 passed, 10 skipped).

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

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants