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
30 changes: 26 additions & 4 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,18 @@ still owns endpoint access, actual model/tool qualification and private evidence

With `OPENAI_BASE_URL` unset, login-file behavior is unchanged, including the
required `CODEX_AUTH_JSON_PATH`; an ambient API key alone does not select the
custom route. Change routes only between fresh attempts. Checkpoint continuation
must retain the captured endpoint/configuration and supply fresh credentials
externally. This routing repair enables matched experiments under the
custom route. Change providers only between separately admitted attempts.
Checkpoint continuation retains the sealed source configuration and supplies
fresh credentials externally. For a fresh SForge receiver, the Host binds its
current credential-gateway URL before native session admission; an ephemeral
gateway port is transport, not a reason to reuse a dead source route. The runner
renders the destination with the ordinary worker's settings owner and verifies
that only this route and any explicitly selected model/effort differ. Both full
configuration digests and session profiles remain bound to the new receiver.
Ordinary within-trial configuration changes still reject. Endpoint access and
the same upstream provider remain the operator's separately verified contract;
a URL or configuration digest does not prove provider identity or quality.
This routing repair enables matched experiments under the
[long-horizon research contract](../docs/architecture/rfcs/long-horizon-harness-benchmark-research-program-v0.md);
it establishes neither sustained operation nor a solver-quality improvement.

Expand Down Expand Up @@ -1099,7 +1108,14 @@ also match `--model`/`--effort`. The native session participant records both
configuration digests, verifies the original session profile and full retained
history, then binds the new profile to a distinct destination session. Retry
uses that same source/destination intent; it cannot switch an already running
receiver. Without the option, exact-configuration admission is unchanged.
receiver. Direct Host admission without the option still requires exact configuration.
The SForge Host may additionally rebind its existing custom provider's
credential-gateway URL from the current launch environment. This applies only
to a fresh checkpoint receiver: the provider name, wire API, credential source,
tools and permission settings must stay fixed. It adds no operator option or
permission to edit an active receiver. The original source configuration stays
sealed; admission records the full source/destination configuration and profile
digests through the existing transition provenance.
The host controller exposes the same explicit transition as
`--destination-model-config`; this remains a CLI/Host journey, with frontend
restore support still open. Changing the model changes the experiment treatment:
Expand All @@ -1115,6 +1131,12 @@ It grants no extra native submissions, cooldown relief, permission or time.
`--destination-model-config`;frontend restore 旅程仍未交付。模型变化是新的实验因素,
共享 checkpoint 本身不能证明机制收益。

SForge 宿主可在全新 checkpoint 接收端准入前,将既有自定义 provider 的凭据网关
URL 绑定到当前启动环境;临时端口变化不应迫使接收端继续使用已失效的源网关。
Provider 名称、wire API、凭据来源、工具和权限保持一致,完整源配置仍封存,
既有 transition 记录保留源与目标配置和 session profile 摘要。此处没有新增操作选项,
也不允许修改活动接收端;同一上游 provider 的资格仍需独立核验。

Warm verification hashes the complete checkpoint with a read window of up to
60 seconds, capped by the original execution deadline. It rejects a read that
finishes after that deadline. The launcher reuses this verified metadata before
Expand Down
94 changes: 56 additions & 38 deletions benchmark/runtime/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,46 +115,17 @@ def validate_model_catalog(model_catalog: Path, model: str) -> dict:
)
return entry

def prepare_codex_home(
home: Path,
*,
execution: Execution,
workspace: Path,
model: str,
effort: str,
base_url: str,
api_key: str,
wire_api: str,
skills: Path | None,
web_search: str = "disabled",
model_catalog: Path | None = None,
model_catalog_destination: Path | None = None,
) -> None:
"""Materialize fixed inputs once; never delete sessions between iterations.
def codex_settings(
*, execution: Execution, workspace: Path, model: str, effort: str,
base_url: str, wire_api: str, web_search: str = "disabled",
model_catalog: Path | None = None, model_catalog_destination: Path | None = None,
) -> str:
"""Render fixed settings for the worker and its fresh checkpoint receiver.

A different effective configuration requires a different trial. Secrets are
kept out of the settings identity and may rotate without resetting history.

``model_catalog`` is required for slugs Codex does not ship built-in
(e.g. gateway aliases such as ``gpt-5.6-sol``). Without a catalog entry
Codex treats the model as unknown and offers a reduced tool surface, and
the ``web_search`` setting has no effect. A catalog entry is not
capability-only: it can select a tool mode and carry ``model_messages``
that become prompt content. Entries whose tool mode would strip the
function-tool surface (for example ``code_mode_only``, which makes the
real client send ``tools: []``) are rejected before a trial; qualify a
pinned binary/catalog pair at the first-request boundary with
``benchmark.runtime.toolface`` before relying on the rendered tools.
Host-side installers may set ``model_catalog_destination`` to the absolute
guest path where those validated catalog bytes are staged. It affects only
rendering; the host source must still pass the same validation.
Rendering does not create a home, change a session or handle credentials.
"""
if not model or not effort:
raise ValueError("model and effort are required")
if base_url and not api_key:
raise ValueError("custom provider requires OPENAI_API_KEY")
if not base_url and not (home / "auth.json").is_file():
raise ValueError("provide a gateway and API key, or stage CODEX_AUTH_JSON_PATH")
if wire_api not in {"responses", "chat"}:
raise ValueError("unsupported provider wire_api")
if web_search not in {"disabled", "cached", "live"}:
Expand All @@ -165,8 +136,7 @@ def prepare_codex_home(
model_catalog is None or not model_catalog_destination.is_absolute()
):
raise ValueError("catalog destination requires a validated catalog and an absolute guest path")
home.mkdir(parents=True, exist_ok=True)
settings = "\n".join(
return "\n".join(
[
f"model = {json.dumps(model)}",
f"model_reasoning_effort = {json.dumps(effort)}",
Expand Down Expand Up @@ -213,6 +183,54 @@ def prepare_codex_home(
)
+ [""]
)


def prepare_codex_home(
home: Path,
*,
execution: Execution,
workspace: Path,
model: str,
effort: str,
base_url: str,
api_key: str,
wire_api: str,
skills: Path | None,
web_search: str = "disabled",
model_catalog: Path | None = None,
model_catalog_destination: Path | None = None,
) -> None:
"""Materialize fixed inputs once; never delete sessions between iterations.

A different effective configuration requires a different trial. Secrets are
kept out of the settings identity and may rotate without resetting history.

``model_catalog`` is required for slugs Codex does not ship built-in
(e.g. gateway aliases such as ``gpt-5.6-sol``). Without a catalog entry
Codex treats the model as unknown and offers a reduced tool surface, and
the ``web_search`` setting has no effect. A catalog entry is not
capability-only: it can select a tool mode and carry ``model_messages``
that become prompt content. Entries whose tool mode would strip the
function-tool surface (for example ``code_mode_only``, which makes the
real client send ``tools: []``) are rejected before a trial; qualify a
pinned binary/catalog pair at the first-request boundary with
``benchmark.runtime.toolface`` before relying on the rendered tools.
Host-side installers may set ``model_catalog_destination`` to the absolute
guest path where those validated catalog bytes are staged. It affects only
rendering; the host source must still pass the same validation.
"""
if not model or not effort:
raise ValueError("model and effort are required")
if base_url and not api_key:
raise ValueError("custom provider requires OPENAI_API_KEY")
if not base_url and not (home / "auth.json").is_file():
raise ValueError("provide a gateway and API key, or stage CODEX_AUTH_JSON_PATH")
settings = codex_settings(
execution=execution, workspace=workspace, model=model, effort=effort,
base_url=base_url, wire_api=wire_api, web_search=web_search,
model_catalog=model_catalog, model_catalog_destination=model_catalog_destination,
)
home.mkdir(parents=True, exist_ok=True)
config = home / "config.toml"
if config.exists() and config.read_text(encoding="utf-8") != settings:
raise ValueError("Codex settings changed within a trial; use a new trial")
Expand Down
21 changes: 19 additions & 2 deletions benchmark/runtime/sforge.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from benchmark.edgebench.feedback import FEEDBACK_MODES
from benchmark.edgebench.feedback_hook import FEEDBACK_PAYLOAD

from .codex import DEFAULT_REPLAN_AFTER_TURNS, Execution, prepare_codex_home
from .codex import DEFAULT_REPLAN_AFTER_TURNS, Execution, codex_settings, prepare_codex_home
from .codex_offline import CodexOffline, MODEL_CATALOG_REMOTE, stage_portable_python
from .harbor import (
BenchmarkCodex, _CONTROL, _CODEX_HOME, _PYTHON, _SCHEDULER_STATE, _SRC,
Expand Down Expand Up @@ -317,7 +317,24 @@ def _install_worker(self, backend, handle, log_dir, logger):
)
asyncio.run(self.runtime.install(self.environment))
if self.warm_start is not None:
research_paths = asyncio.run(self.warm_start.stage(self.environment, self.cwd))
route_settings = {}
if runtime_base_url:
worker_env = self.runtime._worker_env(cwd=self.cwd)
route_settings = {
"provider_base_url": runtime_base_url,
"effective_config": codex_settings(
execution=self.runtime.execution, workspace=Path(self.cwd),
model=worker_env["MODEL_NAME"], effort=worker_env["REASONING_EFFORT"],
base_url=runtime_base_url,
wire_api=worker_env["CODEX_WIRE_API"],
web_search=worker_env["LOOPX_WEB_SEARCH"],
model_catalog=self.runtime._model_catalog_source,
model_catalog_destination=(Path(MODEL_CATALOG_REMOTE)
if self.runtime._model_catalog_staged else None),
).encode("utf-8"),
}
research_paths = asyncio.run(self.warm_start.stage(
self.environment, self.cwd, **route_settings))
self._warm_command("workspace", user="root")
result = backend.exec_run(handle,
["chown", "-R", "-P", "agent:agent", self.cwd, *research_paths], user="root")
Expand Down
49 changes: 44 additions & 5 deletions benchmark/runtime/sforge_warm_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import shutil
import tempfile
import time
import tomllib

from loopx.control_plane.effect_runtime import effect_runtime_result
from benchmark.runtime.layout import _CODEX_HOME, _CONTROL, _SRC
Expand Down Expand Up @@ -96,14 +97,41 @@ def _verify(self, workspace: str) -> dict:
raise ValueError("Original warm start deadline expired during checkpoint verification")
return metadata

async def stage(self, environment, workspace: str) -> list[str]:
async def stage(self, environment, workspace: str, *,
effective_config: bytes | None = None,
provider_base_url: str | None = None) -> list[str]:
from loopx.hosts.checkpoint_retention import checkpoint_bank_use
with checkpoint_bank_use(self.root):
return await self._stage(environment, workspace)
return await self._stage(environment, workspace,
effective_config=effective_config, provider_base_url=provider_base_url)

async def _stage(self, environment, workspace: str) -> list[str]:
async def _stage(self, environment, workspace: str, *,
effective_config: bytes | None = None,
provider_base_url: str | None = None) -> list[str]:
metadata = self._verify(workspace)
research = _research_components(self.root, metadata, Path(workspace), self.research_component_ids)
requested = self.destination_config or self.config
transport_config = None
if effective_config is not None:
from loopx.hosts.codex_checkpoint import checkpoint_destination_config

# The runner renders its actual launch through the ordinary worker's
# settings owner. Never change the operator's model/effort selection
# merely because a destination was rendered successfully.
before = tomllib.loads(requested.read_text(encoding="utf-8"))
after = tomllib.loads(effective_config.decode("utf-8"))
if any(before.get(key) != after.get(key) for key in ("model", "model_reasoning_effort")):
raise ValueError("Checkpoint launch differs from its declared model and reasoning effort")
with tempfile.TemporaryDirectory(prefix="checkpoint-route-") as folder:
target = Path(folder).resolve() / "config.toml"
target.write_bytes(effective_config)
checkpoint_destination_config(self.config,
metadata["native_session"]["config_sha256"], target,
provider_base_url=provider_base_url)
if effective_config != requested.read_bytes():
transport_config = effective_config
elif provider_base_url is not None and effective_config is None:
raise ValueError("Checkpoint Host route requires its rendered destination settings")
# Install tools first without creating the native home. Stage inert
# participants outside the workspace so restoring its bytes cannot
# replace the active controller, registry or installed executable.
Expand All @@ -115,7 +143,16 @@ async def _stage(self, environment, workspace: str) -> list[str]:
source_todo_id=self.source_todo_id, deadline_epoch=self.deadline_epoch,
normal_cadence=self.normal_cadence)
protected_files = [config, f"{_CONTROL}/warm-start.json"]
if self.destination_config is not None:
if transport_config is not None:
target = f"{_CONTROL}/restored-model-config.toml"
with tempfile.TemporaryDirectory(prefix="checkpoint-route-") as folder:
path = Path(folder) / "config.toml"
path.write_bytes(transport_config)
await environment.upload_file(path, target)
payload["destination_config"] = target
payload["provider_base_url"] = provider_base_url
protected_files.append(target)
elif self.destination_config is not None:
target = f"{_CONTROL}/restored-model-config.toml"
await environment.upload_file(self.destination_config, target)
payload["destination_config"] = target
Expand Down Expand Up @@ -302,7 +339,9 @@ def admit_warm_session(env: dict[str, str], declaration: dict) -> dict:
result = admit_native_checkpoint_session(env, Path(declaration["root"]), declaration["sha256"],
Path(declaration["config"]), sandbox=env["LOOPX_CODEX_SANDBOX"], timeout=timeout,
**({"destination_config": Path(declaration["destination_config"])}
if declaration.get("destination_config") is not None else {}))
if declaration.get("destination_config") is not None else {}),
**({"provider_base_url": declaration["provider_base_url"]}
if declaration.get("provider_base_url") is not None else {}))
_phase_read_timeout(declaration, 90)
return result

Expand Down
55 changes: 55 additions & 0 deletions benchmark/tests/test_checkpoint_model_transition.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,61 @@ def test_model_transition_requires_an_explicit_model(settings, replacement):
checkpoint_destination_config(source, digest, target)


@pytest.fixture
def gateway_settings(tmp_path):
source, target = tmp_path / "source.toml", tmp_path / "target.toml"
source.write_text('model="original"\nmodel_reasoning_effort="high"\n'
'model_provider="gateway"\nsandbox_mode="read-only"\n'
'[model_providers.gateway]\nbase_url="http://gateway:4001/v1"\n'
'wire_api="responses"\nenv_key="OPENAI_API_KEY"\n'
'[model_providers.other]\nbase_url="https://other.invalid/v1"\n')
target.write_text(source.read_text().replace(':4001/', ':4002/'))
return source, target, hashlib.sha256(source.read_bytes()).hexdigest()


def test_gateway_rebind_is_explicit_and_retains_full_configuration_provenance(gateway_settings):
source, target, digest = gateway_settings
before = source.read_bytes()
with pytest.raises(ValueError, match="only model and reasoning effort"):
checkpoint_destination_config(source, digest, target)
data, transition = checkpoint_destination_config(source, digest, target,
provider_base_url="http://gateway:4002/v1")
assert data == target.read_bytes() and source.read_bytes() == before
assert transition["source_config_sha256"] == digest
assert transition["destination_config_sha256"] == hashlib.sha256(data).hexdigest()
assert transition["source_model"] == transition["destination_model"] == "original"


@pytest.mark.parametrize("old,new", [
('wire_api="responses"', 'wire_api="chat"'),
('env_key="OPENAI_API_KEY"', 'env_key="OTHER_KEY"'),
('sandbox_mode="read-only"', 'sandbox_mode="danger-full-access"'),
('https://other.invalid/v1', 'https://redirect.invalid/v1'),
('model_provider="gateway"', 'model_provider="other"'),
])
def test_gateway_rebind_cannot_change_other_inputs(gateway_settings, old, new):
source, target, digest = gateway_settings
target.write_text(target.read_text().replace(old, new))
with pytest.raises(ValueError):
checkpoint_destination_config(source, digest, target,
provider_base_url="http://gateway:4002/v1")


def test_gateway_rebind_requires_matching_destination_and_current_launch(gateway_settings, monkeypatch):
from loopx.hosts import codex_checkpoint as owner

source, target, digest = gateway_settings
with pytest.raises(ValueError, match="current Host route"):
checkpoint_destination_config(source, digest, target, provider_base_url="http://foreign:4002/v1")
with pytest.raises(ValueError, match="explicit destination settings"):
checkpoint_destination_config(source, digest, provider_base_url="http://gateway:4002/v1")
monkeypatch.setattr(owner, "fork_native_checkpoint", lambda *a, **kw: pytest.fail("must not fork"))
with pytest.raises(ValueError, match="current launch"):
owner.admit_native_checkpoint_session({"OPENAI_BASE_URL": "http://foreign:4002/v1"},
source.parent, digest, source, sandbox="read-only", timeout=30,
destination_config=target, provider_base_url="http://gateway:4002/v1")


def test_transition_provenance_survives_exact_session_observations_and_qualified_refresh(tmp_path):
from loopx.control_plane.turn_driver.codex_sessions import (
_replace_checkpoint_codex_session, _store_codex_cli_session, load_codex_cli_session,
Expand Down
Loading
Loading