after moving all to cipher
This commit is contained in:
@@ -22,6 +22,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import atif_session
|
||||
import browser_note
|
||||
from harbor.agents.installed.claude_code import ClaudeCode
|
||||
from harbor.models.trial.paths import EnvironmentPaths
|
||||
|
||||
@@ -101,7 +102,7 @@ _AGENT_CLI_NOTE_FALLBACK = (
|
||||
)
|
||||
|
||||
|
||||
def _toolset_note() -> str:
|
||||
def _toolset_note(with_browser: bool = False, with_read: bool = False) -> str:
|
||||
"""The toolset note appended to Claude Code's stock ``--print`` system prompt
|
||||
(via ``--append-system-prompt``) for the canonical reduced toolset.
|
||||
|
||||
@@ -118,9 +119,23 @@ def _toolset_note() -> str:
|
||||
file is missing."""
|
||||
path = Path(__file__).resolve().parent / "toolset_note.md"
|
||||
try:
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
note = path.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return _AGENT_CLI_NOTE_FALLBACK
|
||||
note = _AGENT_CLI_NOTE_FALLBACK
|
||||
# Order matters: the Read correction must come AFTER the base note, because it supersedes
|
||||
# that note's "there are no Read/Grep/Glob tools" line. Shipping the base note alone to a
|
||||
# Read-enabled agent would be a false statement about its own toolset.
|
||||
if with_read:
|
||||
read_note = Path(__file__).resolve().parent / "toolset_note_read.md"
|
||||
try:
|
||||
note = f"{note}\n\n{read_note.read_text(encoding='utf-8').strip()}"
|
||||
except OSError:
|
||||
_log.warning("toolset_note_read.md missing; Read correction omitted")
|
||||
if with_browser:
|
||||
extra = browser_note.browser_note()
|
||||
if extra:
|
||||
note = f"{note}\n\n{extra}"
|
||||
return note
|
||||
|
||||
|
||||
class PreinstalledClaudeCode(ClaudeCode):
|
||||
@@ -138,6 +153,10 @@ class PreinstalledClaudeCode(ClaudeCode):
|
||||
in the trial config's ``agent.import_path``.)
|
||||
"""
|
||||
|
||||
# Set by _probe_browser() during install(); read by build_cli_flags(). Declared
|
||||
# here so the full-toolset subclass (which skips the probe) still has a value.
|
||||
_has_browser = False
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "claude-code-reduced-toolset"
|
||||
@@ -176,6 +195,14 @@ class PreinstalledClaudeCode(ClaudeCode):
|
||||
editor CLI) and reuses ``_ensure_claude_binary`` directly."""
|
||||
await self._stage_agent_cli(environment)
|
||||
await self._ensure_claude_binary(environment)
|
||||
await self._probe_browser(environment)
|
||||
|
||||
async def _probe_browser(self, environment) -> None:
|
||||
"""Record whether this image ships the Playwright `pw` wrapper, so the toolset
|
||||
note mentions the browser only on images that have one. Runs during install(),
|
||||
which harbor calls before build_cli_flags() reads the result. The probe and the
|
||||
note text are shared with the codex adapter via browser_note.py."""
|
||||
self._has_browser = await browser_note.probe_browser(environment)
|
||||
|
||||
async def _ensure_claude_binary(self, environment) -> None:
|
||||
"""Reuse the claude binary already baked into the task image instead
|
||||
@@ -243,7 +270,8 @@ class PreinstalledClaudeCode(ClaudeCode):
|
||||
subclass overrides this back to stock ``ClaudeCode.build_cli_flags``.
|
||||
"""
|
||||
flags = super().build_cli_flags()
|
||||
extra = f"--tools Bash --append-system-prompt {shlex.quote(_toolset_note())}"
|
||||
note = _toolset_note(with_browser=self._has_browser)
|
||||
extra = f"--tools Bash --append-system-prompt {shlex.quote(note)}"
|
||||
return f"{flags} {extra}" if flags else extra
|
||||
|
||||
async def _claude_format_session_path(self, environment, env, session_uuid: str) -> str:
|
||||
@@ -845,6 +873,47 @@ class SnapshotClaudeCode(PreinstalledClaudeCode):
|
||||
# once every snapshot session.jsonl is re-recorded in the reduced format.
|
||||
|
||||
|
||||
class _BrowserToolsetMixin:
|
||||
"""The canonical reduced toolset PLUS the ``Read`` built-in, for tasks that opt into a
|
||||
browser: a screenshot is only useful to an agent that can look at it, and ``Read`` is what
|
||||
turns a PNG on disk into an image the model actually sees.
|
||||
|
||||
This is a SEPARATE AGENT, not a flag, per the rule that the toolset is chosen by which class
|
||||
harbor runs and the class name records it — so a benchmark row can never silently compare an
|
||||
agent that could see against one that couldn't.
|
||||
|
||||
Two things to be clear-eyed about:
|
||||
* ``Read`` is not image-only. It also reads text files, PDFs and notebooks, so these tasks
|
||||
get back a file-reading built-in the reduced toolset deliberately removes. There is no
|
||||
narrower built-in; an image-only MCP tool was rejected because the canonical agent avoids
|
||||
MCP (see the async-MCP startup race note at the top of this file).
|
||||
* Tasks on this agent are not comparable with tasks on the canonical one. That is the point
|
||||
of the distinct name.
|
||||
"""
|
||||
|
||||
def build_cli_flags(self) -> str:
|
||||
flags = ClaudeCode.build_cli_flags(self)
|
||||
note = _toolset_note(with_browser=self._has_browser, with_read=True)
|
||||
extra = f"--tools Bash,Read --append-system-prompt {shlex.quote(note)}"
|
||||
return f"{flags} {extra}" if flags else extra
|
||||
|
||||
|
||||
class BrowserPreinstalledClaudeCode(_BrowserToolsetMixin, PreinstalledClaudeCode):
|
||||
"""Reduced toolset + Read, manual (non-snapshot) tasks."""
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "claude-code-reduced-toolset-browser"
|
||||
|
||||
|
||||
class BrowserSnapshotClaudeCode(_BrowserToolsetMixin, SnapshotClaudeCode):
|
||||
"""Reduced toolset + Read, snapshot tasks."""
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "snapshot-claude-code-reduced-toolset-browser"
|
||||
|
||||
|
||||
class _FullToolsetMixin:
|
||||
"""Override the canonical reduced toolset back to Claude Code's stock full
|
||||
built-in toolset: no str_replace_editor CLI to stage, and no --tools / note.
|
||||
|
||||
Reference in New Issue
Block a user