Skip to content
Merged
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
76 changes: 73 additions & 3 deletions ringer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
SELF_UPDATE_STATE_FILE = "self-update.json"
DEFAULT_TOKEN_REGEX = r"tokens\s+used\s*:?\s*([0-9][0-9,]*)"
DEFAULT_CODEX_MODEL_REPORT_REGEX = r"(?m)^model:[ \t]*([^ \t\r\n]+)[ \t]*\r?$"
# Harnesses announce their model in a banner at startup, but the capture buffer
# the banner is scraped from keeps only the TAIL of a worker's output. A long
# run therefore scrolls its own identity out of the buffer before anyone reads
# it. Retaining the first bytes too costs nothing and is the only thing that
# makes attribution independent of how much the worker went on to say.
WORKER_HEAD_CAPTURE_BYTES = 64 * 1024
ACTIVITY_TAIL_BYTES = 2048
ACTIVITY_TEXT_LIMIT = 80
ARTIFACT_WRAPPER_TAIL_BYTES = 256 * 1024
Expand Down Expand Up @@ -1875,6 +1881,7 @@ def lint_manifest(

findings.extend(unreachable_deliverable_findings(manifest))
findings.extend(sandbox_unreachable_deliverable_findings(manifest, config))
findings.extend(unpinned_model_findings(manifest, config))

if not allow_noncanonical_route:
findings.extend(
Expand Down Expand Up @@ -2218,6 +2225,46 @@ def sandbox_unreachable_deliverable_findings(
return findings


def unpinned_model_findings(
manifest: Manifest, config: "AppConfig | None"
) -> list[str]:
"""Tasks whose engine will pick a model that nothing here records.

An engine template with the OPTIONAL {model_args} placeholder drops the
flag entirely when no model is pinned, so the harness silently falls back
to its own configured default. The work is fine; the eval row is not. The
model column ends up empty and the attempt lands under '(unattributed
legacy rows)' in ./ringer.py models — invisible to routing forever, since
the default in force at run time is not recorded anywhere and cannot be
reconstructed afterwards.

A warning, not an ERROR: the run itself is legitimate and blocking it
would be wrong. This only fires where an author gets no other signal —
the required-{model} form already raises in validate_manifest_engines.
"""
if config is None:
return []
findings: list[str] = []
for task in manifest.tasks:
engine = config.engines.get(task.engine)
if engine is None:
continue
if any("{model}" in item for item in engine.args_template):
continue # already a hard error when unpinned
if "{model_args}" not in engine.args_template:
continue # engine takes no model at all; nothing to pin
if task.model or engine.model_default:
continue
findings.append(
f"{task.key}: no model pinned and engines.{task.engine}.model_default is "
f"unset, so {task.engine} will quietly use its own default and the eval row "
"records no model. Set the task's \"model\" field or "
f"engines.{task.engine}.model_default in config.toml — an unattributed "
"attempt cannot be recovered later."
)
return findings


def instructs_git_commit(spec: str) -> bool:
lower = spec.lower()
start = 0
Expand Down Expand Up @@ -9559,7 +9606,9 @@ async def _run_worker(self, runtime: TaskRuntime, spec: str, attempt: int) -> Wo
f"[ringer.py] engine: {runtime.task.engine}\n"
f"[ringer.py] command: {shell_command_for_display(display_cmd)} < /dev/null\n",
)
capture = RollingBytes(max_bytes=1_000_000)
capture = RollingBytes(
max_bytes=1_000_000, head_bytes=WORKER_HEAD_CAPTURE_BYTES
)
try:
log_fh = log_path.open("ab")
except OSError as exc:
Expand Down Expand Up @@ -9603,7 +9652,13 @@ async def _run_worker(self, runtime: TaskRuntime, spec: str, attempt: int) -> Wo
self.active_processes.pop(proc.pid, None)
output_tail = capture.text()
tokens = parse_token_count(output_tail, engine.token_regex)
reported_model = parse_reported_model(output_tail, engine.model_report_regex)
# Tail first so a run that resolves today resolves identically; the head
# only ever fills in an answer that was previously lost. Token counts
# need no such fallback — harnesses report those when they finish, so
# they are in the tail by construction.
reported_model = parse_reported_model(
output_tail, engine.model_report_regex
) or parse_reported_model(capture.head_text(), engine.model_report_regex)
if timed_out:
append_text(log_path, f"\n[ringer.py] worker timed out after {runtime.task.timeout_s}s\n")
append_text(log_path, f"[ringer.py] attempt {attempt} exited rc={proc.returncode}\n")
Expand Down Expand Up @@ -9812,11 +9867,23 @@ def _log_path(self, task: TaskSpec, taskdir: Path) -> Path:


class RollingBytes:
def __init__(self, max_bytes: int) -> None:
"""A bounded tail of a stream, optionally keeping a bounded head as well.

The head exists for one-shot banners a harness prints before it starts
working: they are gone from the tail the moment the worker outruns
max_bytes, and nothing downstream can tell that apart from a harness that
never announced itself.
"""

def __init__(self, max_bytes: int, head_bytes: int = 0) -> None:
self.max_bytes = max_bytes
self.head_bytes = head_bytes
self.data = bytearray()
self.head = bytearray()

def extend(self, chunk: bytes) -> None:
if len(self.head) < self.head_bytes:
self.head.extend(chunk[: self.head_bytes - len(self.head)])
self.data.extend(chunk)
overflow = len(self.data) - self.max_bytes
if overflow > 0:
Expand All @@ -9825,6 +9892,9 @@ def extend(self, chunk: bytes) -> None:
def text(self) -> str:
return bytes(self.data).decode("utf-8", errors="replace")

def head_text(self) -> str:
return bytes(self.head).decode("utf-8", errors="replace")


class AsyncFileCloser:
def __init__(self, fh: Any) -> None:
Expand Down
69 changes: 69 additions & 0 deletions tests/test_identity_evidence.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
#!/usr/bin/env python3
from __future__ import annotations

import asyncio
import contextlib
import io
import json
import os
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path

from ringer import (
DEFAULT_CODEX_MODEL_REPORT_REGEX,
DEFAULT_TOKEN_REGEX,
AppConfig,
ArtifactConfig,
EngineConfig,
EvalConfig,
Manifest,
RingerRunner,
RollingBytes,
VerifyResult,
WorkerResult,
build_models_api_payload,
Expand Down Expand Up @@ -105,6 +110,70 @@ def test_codex_report_regex_captures_synthetic_header(self) -> None:
output = "OpenAI Codex v0.144.0\n--------\nmodel: gpt-5.6-sol\nprovider: openai\n"
self.assertEqual("gpt-5.6-sol", parse_reported_model(output, engine.model_report_regex))

def test_head_capture_keeps_a_banner_the_tail_has_dropped(self) -> None:
capture = RollingBytes(max_bytes=1024, head_bytes=256)
capture.extend(b"model: gpt-5.6-sol\n")
capture.extend(b"x" * 4096)
self.assertNotIn("model:", capture.text())
self.assertIn("model: gpt-5.6-sol", capture.head_text())

def test_worker_is_attributed_after_outrunning_the_capture_buffer(self) -> None:
"""The banner is printed once, then buried under megabytes of work.

Driven through the real _run_worker so the fallback is proven where it
actually has to fire: scraping the head in isolation would pass just as
well with the call site left unchanged.
"""
model = self.reported_model_for_worker(filler_bytes=2_000_000)
self.assertEqual("gpt-5.6-sol", model)

def test_short_worker_still_attributed_from_the_tail(self) -> None:
self.assertEqual("gpt-5.6-sol", self.reported_model_for_worker(filler_bytes=0))

def reported_model_for_worker(self, *, filler_bytes: int) -> str | None:
script = (
"import sys\n"
"sys.stdout.write('OpenAI Codex v0.144.0\\nmodel: gpt-5.6-sol\\n')\n"
f"sys.stdout.write('x' * {filler_bytes})\n"
"sys.stdout.write('\\ntokens used: 1234\\n')\n"
)
engine = EngineConfig(
name="codex",
bin=sys.executable,
args_template=("-c", script),
full_access_args=(),
sandbox_args=(),
model_default="gpt-5.6-sol",
token_regex=DEFAULT_TOKEN_REGEX,
model_report_regex=DEFAULT_CODEX_MODEL_REPORT_REGEX,
)
manifest = Manifest.from_obj(
{
"run_name": "head-capture",
"workdir": str(self.root / "headwork"),
"tasks": [
{
"key": "task",
"spec": "Emit a banner and then a great deal of output.",
"check": "true",
}
],
}
)
runner = RingerRunner(
manifest,
config=self.config(self.root / "head-runs.jsonl", engine),
identity="tester",
dashboard_enabled=False,
)
runtime = runner.runtimes[0]
runtime.taskdir.mkdir(parents=True, exist_ok=True)
runtime.log_path.parent.mkdir(parents=True, exist_ok=True)
with contextlib.redirect_stdout(io.StringIO()):
worker = asyncio.run(runner._run_worker(runtime, runtime.task.spec, 1))
self.assertEqual(0, worker.returncode)
return worker.reported_model

def test_reported_model_wins_and_resolved_model_is_fallback(self) -> None:
rows = self.log_attempts(
WorkerResult(0, False, 12, reported_model="gpt-5.7"),
Expand Down
84 changes: 84 additions & 0 deletions tests/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,90 @@ def test_w12_tilde_deliverable_counts_as_outside(self) -> None:
)
self.assertEqual(1, len(findings), findings)

def w13_config(self, *, model_default: str = "") -> AppConfig:
"""Engines covering all three model-pinning shapes.

'codex' is the built-in optional form ({model_args}), the one that
drops the flag and loses attribution in silence. 'pinned' requires a
model, so validate_manifest_engines already refuses it unpinned and
this rule must not double-report. 'modelless' takes none at all.
"""
root = Path(tempfile.mkdtemp())
base = self.w12_config()
return AppConfig(
path=None,
identity_default=None,
state_dir=root,
dashboard_port_base=8787,
hud_port=8700,
hud_app_path=None,
allow_full_access=False,
eval=EvalConfig(backend="jsonl", jsonl_path=root / "eval.jsonl"),
engines={
"codex": EngineConfig(
name="codex",
bin="codex",
args_template=("exec", "{model_args}", "{spec}"),
full_access_args=(),
sandbox_args=("--sandbox", "workspace-write"),
model_default=model_default,
),
"pinned": EngineConfig(
name="pinned",
bin="pinned",
args_template=("exec", "-m", "{model}", "{spec}"),
full_access_args=(),
sandbox_args=(),
),
"modelless": EngineConfig(
name="modelless",
bin="modelless",
args_template=("run", "{spec}"),
full_access_args=(),
sandbox_args=(),
),
},
artifact=base.artifact,
)

def w13_findings(
self, task_extra: dict[str, object], *, model_default: str = ""
) -> list[str]:
task = self.task()
task.update(task_extra)
manifest = self.manifest([task])
return [
item
for item in lint_manifest(manifest, config=self.w13_config(model_default=model_default))
if "no model pinned" in item
]

def test_w13_unpinned_optional_model_engine_warns(self) -> None:
# The observer-triad shape (2026-07-29..31): a config with no
# [engines.codex] section at all, so no -m reaches the worker and 15
# attempts over 1MB of output logged no model at all.
findings = self.w13_findings({"engine": "codex"})
self.assertEqual(1, len(findings), findings)
self.assertIn("engines.codex.model_default is unset", findings[0])

def test_w13_task_model_or_engine_default_is_clean(self) -> None:
self.assertEqual([], self.w13_findings({"engine": "codex", "model": "gpt-5.6-sol"}))
self.assertEqual([], self.w13_findings({"engine": "codex"}, model_default="gpt-5.6-sol"))

def test_w13_does_not_double_report_the_hard_error_form(self) -> None:
# A required {model} unpinned is already a ValueError in
# validate_manifest_engines; warning here too would just be noise.
self.assertEqual([], self.w13_findings({"engine": "pinned"}))

def test_w13_silent_for_engines_that_take_no_model(self) -> None:
self.assertEqual([], self.w13_findings({"engine": "modelless"}))

def test_w13_is_a_warning_not_a_blocking_error(self) -> None:
# `run` blocks only on ERROR:-prefixed findings. The run is legitimate;
# only its provenance is lost, so it must still be allowed to proceed.
findings = self.w13_findings({"engine": "codex"})
self.assertFalse(findings[0].startswith("ERROR:"), findings[0])

def test_templates_are_clean(self) -> None:
# Every kit ships one or more manifest skeletons (manifest.json plus
# optional manifest-round*.json for multi-round kits).
Expand Down
Loading