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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ For CI and evals, `config.sample.toml` includes `[engines.mock]` so the enforcem

![Identical workers, each under its own light](docs/engines.png)

Ringer ships with three worker lanes: **Codex CLI** is the built-in default, and `config.sample.toml` carries verified engine blocks for **Grok Build CLI** (works as-is once you `grok login`) and **OpenCode + OpenRouter** (one edit: point `bin` at the sandbox wrapper in your clone). Anything else with a headless CLI is a config block away:
Ringer ships with built-in **Codex CLI** and **Cursor Agent** lanes. `config.sample.toml` also carries engine blocks for **Grok Build CLI** and **OpenCode + OpenRouter**. Anything else with a headless CLI is a config block away:

```toml
[engines.mymodel]
Expand All @@ -164,6 +164,18 @@ args_template = ["run", "{spec}", "--dir", "{taskdir}"]

Per-task `"engine": "mymodel"` routes work to it — the invariants (stdin closed, process-group kill, executed verification, raw logs) apply to every engine identically.

### Cursor account lane

Install and authenticate Cursor Agent in the same POSIX environment that runs Ringer:

```bash
curl https://cursor.com/install -fsS | bash
cursor-agent login
cursor-agent models
```

Route a task with `"engine": "cursor"` and an explicit slug from `cursor-agent models`. Ringer invokes Cursor headlessly with stream JSON, an explicit workspace, `--force`, and `--sandbox enabled`. Full access switches to `--sandbox disabled` only when both Ringer gates allow it. A listed slug is not proof of account entitlement, so run a one-task probe before assigning a batch.

### The universal harness: OpenCode + OpenRouter

Unless a model ships its own first-class harness (Codex does), OpenCode is the harness that runs it — one engine block covers every OpenRouter-served model. `config.sample.toml` includes a ready-to-uncomment engine whose `{model}` placeholder is filled per task from the manifest's `"model"` field, with `model_default` as the fallback. The shipped default is OpenRouter's `z-ai/glm-5.2` — roughly $0.74/M input and $2.33/M output (2026-07), about 20-30x cheaper output than frontier coding models; a complete write-code-and-pass-the-check task lands around a penny.
Expand Down
8 changes: 8 additions & 0 deletions config.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ token_regex = "tokens\\s+used\\s*:?\\s*([0-9][0-9,]*)"
# default reads the `model:` header; override it here only if that format changes.
model_report_regex = "(?m)^model:[ \\t]*([^ \\t\\r\\n]+)[ \\t]*\\r?$"

# Cursor Agent is built in. Install: curl https://cursor.com/install -fsS | bash
# then `cursor-agent login`. Set each task's model field to an exact slug from
# `cursor-agent models`; listed models can still be unavailable to the account.
# Uncomment only to override the built-in engine.
# [engines.cursor]
# bin = "cursor-agent"
# model_report_aliases = { "Composer 2.5 Fast" = "composer-2.5-fast", "Cursor Grok 4.5 Medium Fast" = "grok-4.5-fast-high" }

# Grok Build CLI (xAI). Install: curl -fsSL https://x.ai/cli/install.sh | bash
# (or: npm install -g @xai-official/grok), then `grok login` — OAuth on a
# SuperGrok or X Premium Plus plan. Headless mode (-p) runs a full agentic
Expand Down
76 changes: 72 additions & 4 deletions ringer.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ class EngineConfig:
# its own "model" — this is what makes a harness engine (OpenCode) model
# agnostic instead of hard-coding one model into the command line.
model_default: str = ""
# Canonicalizes display labels emitted by harnesses that do not report the
# same stable slug accepted by their --model option.
model_report_aliases: tuple[tuple[str, str], ...] = ()

@property
def process_name(self) -> str:
Expand Down Expand Up @@ -891,6 +894,37 @@ def built_in_codex_engine() -> EngineConfig:
)


def built_in_cursor_engine() -> EngineConfig:
resolved = shutil.which("cursor-agent") or "cursor-agent"
return EngineConfig(
name="cursor",
bin=resolved,
args_template=(
"--print",
"--output-format",
"stream-json",
"--trust",
"--workspace",
"{taskdir}",
"--force",
"{access_args}",
"--model",
"{model}",
"{engine_args}",
"{spec}",
),
full_access_args=("--sandbox", "disabled"),
sandbox_args=("--sandbox", "enabled"),
token_regex=None,
model_report_regex=r'"model"\s*:\s*"([^"]+)"',
model_report_aliases=(
("Composer 2.5 Fast", "composer-2.5-fast"),
("Cursor Grok 4.5 Medium Fast", "grok-4.5-fast-high"),
),
model_default="",
)


def load_eval_config(raw: Any, state_dir: Path) -> EvalConfig:
if raw is None:
raw = {}
Expand Down Expand Up @@ -927,7 +961,10 @@ def load_hud_port(raw: Any) -> int:


def load_engines(raw: Any) -> dict[str, EngineConfig]:
engines: dict[str, EngineConfig] = {DEFAULT_ENGINE_NAME: built_in_codex_engine()}
engines: dict[str, EngineConfig] = {
DEFAULT_ENGINE_NAME: built_in_codex_engine(),
"cursor": built_in_cursor_engine(),
}
if raw is None:
return engines
if not isinstance(raw, dict):
Expand Down Expand Up @@ -979,6 +1016,25 @@ def load_engines(raw: Any) -> dict[str, EngineConfig]:
raise ValueError(
f"engines.{clean_name}.model_report_regex must have a capture group"
)
aliases_raw = section.get("model_report_aliases")
if aliases_raw is None:
model_report_aliases = base.model_report_aliases if base else ()
else:
if not isinstance(aliases_raw, dict):
raise ValueError(
f"engines.{clean_name}.model_report_aliases must be a TOML table"
)
alias_items: list[tuple[str, str]] = []
for reported_raw, canonical_raw in aliases_raw.items():
reported = str(reported_raw).strip()
canonical = str(canonical_raw).strip()
if not reported or not canonical:
raise ValueError(
f"engines.{clean_name}.model_report_aliases keys and values "
"must not be empty"
)
alias_items.append((reported, canonical))
model_report_aliases = tuple(alias_items)
model_default = str(
section.get("model_default", base.model_default if base else "")
).strip()
Expand All @@ -991,6 +1047,7 @@ def load_engines(raw: Any) -> dict[str, EngineConfig]:
token_regex=token_regex,
model_report_regex=model_report_regex,
model_default=model_default,
model_report_aliases=model_report_aliases,
)
return engines

Expand Down Expand Up @@ -8460,7 +8517,11 @@ 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)
reported_model = parse_reported_model(
output_tail,
engine.model_report_regex,
dict(engine.model_report_aliases),
)
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 @@ -8780,14 +8841,20 @@ def parse_token_count(text: str, token_regex: str | None = DEFAULT_TOKEN_REGEX)
return int(matches[-1].replace(",", ""))


def parse_reported_model(text: str, model_report_regex: str | None) -> str | None:
def parse_reported_model(
text: str,
model_report_regex: str | None,
model_report_aliases: dict[str, str] | None = None,
) -> str | None:
if not model_report_regex:
return None
match = re.search(model_report_regex, text, flags=re.IGNORECASE)
if match is None or match.lastindex is None:
return None
value = match.group(1).strip()
return value or None
if not value:
return None
return (model_report_aliases or {}).get(value, value)


def effective_model_from_command(command: list[str]) -> str:
Expand Down Expand Up @@ -8912,6 +8979,7 @@ def print_steering_notes(manifest: Manifest, config: AppConfig) -> None:

ENGINE_INSTALL_HINTS = {
"codex": "install it with `npm install -g @openai/codex` (or `brew install --cask codex`), then run `codex login`",
"cursor": "install it with `curl https://cursor.com/install -fsS | bash`, then run `cursor-agent login`",
"opencode": "install it with `curl -fsSL https://opencode.ai/install | bash`, then run `opencode auth login`",
}

Expand Down
81 changes: 81 additions & 0 deletions tests/test_cursor_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
from __future__ import annotations

import unittest
from pathlib import Path

from ringer import (
ENGINE_INSTALL_HINTS,
build_worker_command,
load_engines,
parse_reported_model,
)


class CursorEngineContractTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = load_engines(None)["cursor"]

def test_requires_explicit_model_and_keeps_sandbox_enabled(self) -> None:
self.assertEqual("", self.engine.model_default)
taskdir = Path("/tmp/cursor-task")
command = build_worker_command(
self.engine,
taskdir=taskdir,
spec="write result.txt",
full_access=False,
model="composer-2.5-fast",
)
self.assertEqual(
[
self.engine.bin,
"--print",
"--output-format",
"stream-json",
"--trust",
"--workspace",
str(taskdir),
"--force",
"--sandbox",
"enabled",
"--model",
"composer-2.5-fast",
"write result.txt",
],
command,
)

def test_full_access_explicitly_disables_cursor_sandbox(self) -> None:
command = build_worker_command(
self.engine,
taskdir=Path("/tmp/cursor-task"),
spec="write result.txt",
full_access=True,
model="grok-4.5-fast-high",
)
self.assertIn("disabled", command)
self.assertNotIn("enabled", command)

def test_stream_model_labels_are_canonicalized_to_request_slugs(self) -> None:
aliases = dict(self.engine.model_report_aliases)
fixtures = {
"composer-2.5-fast": "composer-2.5-fast",
"Composer 2.5 Fast": "composer-2.5-fast",
"Cursor Grok 4.5 Medium Fast": "grok-4.5-fast-high",
}
for reported, expected in fixtures.items():
with self.subTest(reported=reported):
line = f'{{"type":"system","subtype":"init","model":"{reported}"}}'
self.assertEqual(
expected,
parse_reported_model(
line,
self.engine.model_report_regex,
aliases,
),
)
self.assertIn("https://cursor.com/install", ENGINE_INSTALL_HINTS["cursor"])


if __name__ == "__main__":
unittest.main(verbosity=2)