Skip to content
Open
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
2286195
feat: capture LLM trajectories across auth modes
bingran-you Aug 29, 2026
9bc7d36
chore: update RestrictedPython security fix
bingran-you Aug 29, 2026
77fcef6
style: format trajectory capture modules
bingran-you Aug 29, 2026
4c3ae11
fix: harden mixed-auth trajectory capture
bingran-you Aug 29, 2026
f905374
fix: isolate reused rollouts and scene roles
bingran-you Aug 29, 2026
744d43c
fix: scope native session collection
bingran-you Aug 29, 2026
ffe3dca
fix: reset retried Claude capture state
bingran-you Aug 29, 2026
effc132
fix: preserve provider rows across runtime switches
bingran-you Aug 29, 2026
a324ccb
fix: fail closed on incomplete capture artifacts
bingran-you Aug 29, 2026
ee19fbb
fix: bind native capture to ACP sessions
bingran-you Aug 29, 2026
69ed435
fix: fail closed on capture finalization
bingran-you Aug 29, 2026
bee8001
fix: track collector ownership before readiness
bingran-you Aug 29, 2026
2f11725
fix: harden provisional capture cleanup
bingran-you Aug 29, 2026
cedef52
fix: preserve native sessions across reconnects
bingran-you Aug 29, 2026
4b671b5
fix: isolate multi-target native capture
bingran-you Aug 29, 2026
c1abd52
fix: reconcile partial native captures
bingran-you Aug 29, 2026
fa6cbf5
fix: reject empty legacy trajectory artifacts
bingran-you Aug 29, 2026
72f478d
fix: attribute translated provider model aliases
bingran-you Aug 29, 2026
ae2ccb1
fix: stamp provider route identity in capture
bingran-you Aug 29, 2026
16d14bc
fix: refresh stitched capture manifests
bingran-you Aug 29, 2026
93bfddd
fix: require sidecars for schema-v2 captures
bingran-you Aug 29, 2026
eee5952
fix: fail closed on lost continuation calls
bingran-you Aug 29, 2026
5426420
fix: finalize continuation capture artifacts
bingran-you Aug 29, 2026
59883c6
fix: refinalize continuation after cleanup
bingran-you Aug 29, 2026
f6daaf2
fix: quiesce capture and bind provider roles
bingran-you Aug 29, 2026
11e0168
fix: journal late calls and refine role matching
bingran-you Aug 29, 2026
6fa3151
fix: close capture trust and shutdown gaps
bingran-you Aug 29, 2026
767dcde
fix: retire quiesce handler before acknowledgement
bingran-you Aug 29, 2026
9c788d0
fix: invalidate stale LiteLLM attempt journals
bingran-you Aug 29, 2026
b709ec1
fix: enforce provider capture custody boundary
bingran-you Aug 29, 2026
89e720d
refactor: isolate continuation trajectory artifacts
bingran-you Aug 29, 2026
c151761
fix: close native capture review gaps
bingran-you Aug 29, 2026
6043514
fix: preserve canonical provider requests
bingran-you Aug 29, 2026
5b962c6
fix: preserve provider capture fidelity
bingran-you Aug 29, 2026
87166fb
fix: fail closed on capture provenance
bingran-you Aug 29, 2026
f0746ab
fix: preserve audit continuation results
bingran-you Aug 29, 2026
206c13d
test: split native capture resilience cases
bingran-you Aug 29, 2026
750de4a
fix: fail closed on unverified capture states
bingran-you Aug 29, 2026
58c1e04
fix: reject incomplete audit capture metadata
bingran-you Aug 29, 2026
ad2f666
fix: validate audit completion per role
bingran-you Aug 29, 2026
f5a38de
fix: preserve repeated replay completion
bingran-you Aug 29, 2026
9bf15d8
fix repeated continuation model provenance
bingran-you Aug 29, 2026
3885e0d
require redacted payloads for training
bingran-you Aug 29, 2026
8dad07a
Harden provider capture artifact custody
bingran-you Aug 29, 2026
914e5ff
Keep agent-visible provider credentials audit-only
bingran-you Aug 29, 2026
3d6d198
Strip alternate credentials from proxy agents
bingran-you Aug 29, 2026
142910a
Harden proxy credential and continuation journals
bingran-you Aug 29, 2026
0aa771c
Sanitize Codex config and package redactor data
bingran-you Aug 29, 2026
e139e62
Preserve direct Codex provider settings
bingran-you Aug 29, 2026
1fabaf0
refactor llm capture runtime custody
bingran-you Aug 29, 2026
ebd0e04
reject contradictory capture manifests
bingran-you Aug 30, 2026
e685ab6
close llm capture provenance gaps
bingran-you Aug 30, 2026
0e4ed71
preserve opencode manifest parity
bingran-you Aug 30, 2026
e113d0e
Harden proxy config and role capture admission
bingran-you Aug 30, 2026
33590d3
Require attribution and isolate subscription auth
bingran-you Aug 30, 2026
d8342c6
Harden proxy auth config homes
bingran-you Aug 30, 2026
ee73718
Harden effective proxy auth homes
bingran-you Aug 30, 2026
42e3d99
fix(auth): make proxy credential cleanup race-safe
bingran-you Aug 30, 2026
d4557e9
fix(auth): scope stale process isolation
bingran-you Aug 30, 2026
88fc1b5
fix(auth): gate every proxy agent on process isolation
bingran-you Aug 30, 2026
7183e73
fix(auth): sanitize Claude credential settings
bingran-you Aug 30, 2026
b2ca3f3
fix(auth): replace stale Pi providers in proxy mode
bingran-you Aug 30, 2026
aa078e0
fix(auth): scrub stale Pi config before proxy launch
bingran-you Aug 30, 2026
df51305
fix(capture): preserve role usage and block privilege regain
bingran-you Aug 30, 2026
200b107
test(capture): use matching Linux user group
bingran-you Aug 30, 2026
c3ac7f2
fix: preserve zero-call OAuth completion
bingran-you Aug 30, 2026
f071fd5
fix: aggregate mixed-auth token usage
bingran-you Aug 30, 2026
3f58679
fix: honor primary role-scoped credentials
bingran-you Aug 30, 2026
1f18b77
fix: isolate credentials across agent roles
bingran-you Aug 30, 2026
b21b1aa
fix: quiesce native capture roles safely
bingran-you Aug 30, 2026
5646fe8
fix: restore OAuth credentials on role reconnect
bingran-you Aug 30, 2026
bd768e0
fix: close proxy and continuation provenance gaps
bingran-you Aug 30, 2026
b45e2aa
fix: isolate all OpenCode config authorities
bingran-you Aug 30, 2026
2725574
fix: validate audit artifacts before completion
bingran-you Aug 30, 2026
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
13 changes: 11 additions & 2 deletions docs/agent-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,24 @@ and what it is:
agent/ — agent-side logs
trajectory/acp_trajectory.jsonl — the full agent trace (every ACP event:
prompts, tool calls, outputs)
trajectory/llm_trajectory.jsonl — raw provider requests/responses captured
by the usage-tracking proxy
trajectory/llm_trajectory.jsonl — always-present LLM exchange log
trajectory/llm_trajectory.manifest.json
— source/fidelity/completeness for that log
trainer/verifiers.jsonl — trainer-ready scored trajectory record
trainer/atif.json — the trajectory in ATIF interchange
format (omitted if the trajectory is empty)
trainer/adp.jsonl — the trajectory in ADP format
verifier/reward.txt — raw verifier reward
verifier/test-stdout.txt — verifier stdout (and ctrf.json when the
test emits a CTRF report)

`llm_trajectory.jsonl` is an audit artifact for every run, not an unconditional
training claim. Only a `complete` manifest with `provider_wire` fidelity and
positive provider token usage is training-ready. Native OAuth/session capture
and continuation replay-proxy ingress stay available for audit but are marked
lower-fidelity and excluded from training. A sandbox-local proxy that shares
root custody with the agent records that additional custody limitation.

Also note the job-level summary.json plus aggregated results.jsonl,
verifiers.jsonl, and adp.jsonl in the job directory. The trainer/ files and the job-level
aggregates are written by current BenchFlow releases; if they are missing,
Expand Down
9 changes: 9 additions & 0 deletions docs/continue-runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ request/response pairs from the original run. `bench eval continue`:
with a stitched `llm_trajectory.jsonl` (recorded prefix + live suffix) and
`continued_from` provenance — a drop-in replacement for the timed-out entry.

The stitched manifest preserves both capture boundary and custody. The live
suffix is observed at replay-proxy ingress, before the forwarder and LiteLLM can
replace, filter, or transform the provider request. It is therefore always
labeled `replay_proxy` / `agent_session` with an incomplete provider request,
even when the proxy is host-owned. If replay and the untrusted agent also share
root custody in the sandbox, the manifest records that additional limitation.
The suffix remains useful for audit and continuity, but any continued run with
a live suffix is excluded from training-ready provider evidence.

Because the agent rebuilds its own state by re-doing its own steps, no
reverse-engineering of OpenHands internals is needed, and the result is a single
continuous run rather than a fresh agent on a warm filesystem.
Expand Down
33 changes: 32 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ Each run writes under `--jobs-dir` (default `jobs/`):
prompts.json # prompts sent to the agent
trajectory/
acp_trajectory.jsonl # full agent trace (ACP events)
llm_trajectory.jsonl # raw provider requests/responses (when the usage-tracking proxy captured exchanges)
llm_trajectory.jsonl # always present; LLM exchanges at the fidelity described below
llm_trajectory.manifest.json # capture source, fidelity, completeness, and errors
trainer/
verifiers.jsonl # trainer-ready scored trajectory (Verifiers/ORS record)
atif.json # ATIF trajectory record (omitted if the trajectory is empty)
Expand All @@ -213,6 +214,36 @@ Each run writes under `--jobs-dir` (default `jobs/`):
test-stdout.txt # verifier stdout
```

`llm_trajectory.jsonl` is created when the rollout directory is initialized,
including for setup failures and tasks that make no model call. Its sidecar is
the source of truth for interpreting the JSONL:

| Agent/auth path | Primary source | `capture_fidelity` |
|---|---|---|
| API key through the BenchFlow gateway (including Azure) | LiteLLM provider request/response capture | `provider_wire` |
| Claude Code subscription/OAuth | Claude Code raw API-body files correlated by local OTLP logs | `agent_session` |
| Claude Code subscription/OAuth fallback | Claude Code native session JSONL | `agent_session` |
| Codex subscription/OAuth | Codex native session JSONL | `agent_session` |
| Continued-run live suffix | Replay-proxy ingress before provider transformation | `agent_session` |

The manifest status is `complete`, `partial`, `no_model_call`, or
`capture_failed`. Mixed-role rollouts merge API-key and native-subscription
exchanges into the same JSONL; `role_captures` records each prepared role's
scene role, agent, model, auth mode, source, fidelity, completeness, and exchange
count. Continued runs retain the source role entries as the `recorded` leg and
append a separate `live` leg, so a model or auth switch remains auditable. The
live continuation request is captured before provider transformation and is
therefore audit-only even when its replay proxy is host-owned.
Missing or ambiguously attributed roles make the rollout-level capture
`partial`. Reconstructed `agent_session` rows remain useful for audit and viewer
workflows, but trainer exports fail closed unless the manifest says the capture
is complete provider-wire data and every successful exchange has positive token
usage. Claude's raw-body surface is deliberately audit-only because Claude Code
writes it inside the agent-controlled sandbox; its API shape does not make its
custody provider-trusted. It can also contain provider-redacted
extended-thinking blocks. BenchFlow applies its normal secret redaction before
publishing the JSONL.

### Reading results

Exit code 0 means the pipeline completed — it is not a pass/fail signal. A
Expand Down
10 changes: 9 additions & 1 deletion docs/running-any-benchmark.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,20 @@ Every layer terminates at the *same* output contract, written per rollout under
| `results.jsonl` | Verifiers/Prime-RL shaped rollout row |
| `rewards.jsonl` | The reward record for the rollout (ORS / OpenReward shape) |
| `trajectory/acp_trajectory.jsonl` | Full agent trace as ACP events |
| `trajectory/llm_trajectory.jsonl` | Raw provider requests/responses (when captured) |
| `trajectory/llm_trajectory.jsonl` | Always-present LLM exchange log (possibly empty for `no_model_call`) |
| `trajectory/llm_trajectory.manifest.json` | Capture source, fidelity, completeness, and errors |
| `trainer/verifiers.jsonl` | Trainer-ready scored trajectory (Verifiers record) |
| `trainer/atif.json` | ATIF trajectory record |
| `trainer/adp.jsonl` | ADP trajectory record |
| `verifier/` | Raw verifier output (`reward.txt`, `ctrf.json`, stdout) |

The JSONL file is always an audit artifact; its sidecar determines whether it
is also training-ready. Training requires complete `provider_wire` capture with
positive provider token usage. Native OAuth/session capture and sandbox-local
capture that shares root custody with the agent are retained but marked
lower-fidelity, so exporters reject them rather than treating agent-writable
records as trusted provider evidence.

Hosted runs share this artifact contract too (see the `hosted_env.py` module
docstring), with `source.type="hosted_env"` / `trajectory_source="hosted_env"`
marking the lineage.
Expand Down
54 changes: 54 additions & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from __future__ import annotations

import tempfile
from pathlib import Path
from typing import Any

from hatchling.builders.hooks.plugin.interface import BuildHookInterface

_RUNTIME_RESOURCES = (
(
"src/benchflow/trajectories/redaction.py",
"benchflow/trajectories/resources/canonical_redaction.py.txt",
),
(
"src/benchflow/continue_run/sandbox_replay_runtime.py",
"benchflow/continue_run/resources/sandbox_replay_runtime.py.txt",
),
)


class CustomBuildHook(BuildHookInterface):
PLUGIN_NAME = "custom"

def initialize(self, _version: str, build_data: dict[str, Any]) -> None:
if self.target_name != "wheel":
return
self._generated_paths: list[Path] = []
for source_name, resource_name in _RUNTIME_RESOURCES:
source = Path(self.root, source_name)
with tempfile.NamedTemporaryFile(
prefix="benchflow-runtime-resource-",
suffix=".txt",
delete=False,
) as generated:
generated.write(source.read_bytes())
generated_path = Path(generated.name)
self._generated_paths.append(generated_path)
build_data["force_include"][str(generated_path)] = resource_name

def finalize(
self,
_version: str,
_build_data: dict[str, Any],
_artifact_path: str,
) -> None:
self._discard_generated()

def clean(self, _versions: list[str]) -> None:
self._discard_generated()

def _discard_generated(self) -> None:
for generated in getattr(self, "_generated_paths", []):
generated.unlink(missing_ok=True)
self._generated_paths = []
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,16 @@ build-backend = "hatchling.build"
only-include = [
"src",
"tests",
"hatch_build.py",
"README.md",
"CHANGELOG.md",
"LICENSE",
"pyproject.toml",
]

[tool.hatch.build.hooks.custom]
path = "hatch_build.py"

[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "-m 'not live and not integration'"
Expand Down
2 changes: 1 addition & 1 deletion src/benchflow/acp/container_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from benchflow.sandbox.process import LiveProcess
from benchflow.sandbox.process._base import _ANSI_CSI_RE, _ANSI_OSC_RE
from benchflow.trajectories.types import redact_trajectory_text
from benchflow.trajectories.redaction import redact_trajectory_text

from .transport import Transport, decode_json_rpc_message

Expand Down
38 changes: 38 additions & 0 deletions src/benchflow/acp/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from benchflow.acp.container_transport import ContainerTransport
from benchflow.acp.selection import selected_acp_transport
from benchflow.acp.types import McpServerSpec
from benchflow.agents.opencode_config import opencode_provider_reset_command
from benchflow.agents.protocol import ACPSessionAdapter
from benchflow.agents.providers import (
find_provider,
Expand Down Expand Up @@ -72,6 +73,41 @@
_OPENHANDS_DISABLE_SUBAGENTS_ENV = "BENCHFLOW_OPENHANDS_DISABLE_SUBAGENTS"


def _harden_proxy_agent_launch(
agent: str, agent_launch: str, agent_env: dict[str, str]
) -> str:
"""Insert agent-side config hardening after proxy selection."""

if not agent_env.get("BENCHFLOW_LITELLM_MODEL_ALIAS"):
return agent_launch
if agent == "opencode":
# OpenCode merges several global filenames, ~/.opencode, project
# configs, an arbitrary file/directory, and inline JSON. Proxy mode
# must expose exactly the manifest-owned config that the wrapper
# registers below. Pin XDG to the same canonical agent home, disable
# project discovery, and remove every supported alternate env source
# before the process imports its configuration flags.
isolate_sources = (
"unset OPENCODE_CONFIG OPENCODE_CONFIG_DIR "
"OPENCODE_CONFIG_CONTENT XDG_CONFIG_HOME && "
'export HOME="${BENCHFLOW_AGENT_HOME:-$HOME}" && '
'export XDG_CONFIG_HOME="$HOME/.config" && '
"export OPENCODE_DISABLE_PROJECT_CONFIG=1"
)
return (
f"{isolate_sources} && {opencode_provider_reset_command()} "
f"&& {agent_launch}"
)
if agent == "mimo":
# MiMo's manifest-owned launcher replaces both canonical config files
# in proxy mode, but the CLI also honors this arbitrary alternate path.
# Do not let an image-baked config bypass the capture proxy. Direct mode
# deliberately retains the override as part of the caller's provider
# configuration.
return f"unset MIMOCODE_CONFIG && {agent_launch}"
return agent_launch


def _acp_handshake_timeout_sec() -> float:
"""Effective timeout for the pre-prompt ACP handshake, in seconds.

Expand Down Expand Up @@ -610,6 +646,8 @@ async def connect_acp(
agent_launch = " ".join(parts)
logger.info(f"Resolved agent path: {agent_launch}")

agent_launch = _harden_proxy_agent_launch(agent, agent_launch, agent_env)

if sandbox_user:
agent_launch = build_priv_drop_cmd(agent_launch, sandbox_user)
logger.info(f"Agent sandboxed as: {sandbox_user}")
Expand Down
88 changes: 70 additions & 18 deletions src/benchflow/agents/codex_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,52 +20,104 @@ def codex_provider_id(provider_name: str | None) -> str:
return f"{_CODEX_PROVIDER_ID_PREFIX}{safe_name or 'provider'}"


def apply_codex_provider_config(
def apply_codex_custom_provider_config(
agent_env: dict[str, str],
*,
base_url: str,
model: str | None,
provider_name: str,
strict: bool = False,
) -> None:
"""Create or update Codex's model provider entry in ``agent_env``."""
"""Update caller-owned Codex config for a direct custom provider."""

raw_config = agent_env.get(CODEX_CONFIG_ENV)
if not raw_config:
config: dict[str, Any] = {}
else:
try:
config = json.loads(raw_config)
except json.JSONDecodeError as exc:
if strict:
raise ValueError(f"{CODEX_CONFIG_ENV} must be valid JSON") from exc
return
raise ValueError(f"{CODEX_CONFIG_ENV} must be valid JSON") from exc
if not isinstance(config, dict):
if strict:
raise ValueError(f"{CODEX_CONFIG_ENV} must decode to a JSON object")
return
raise ValueError(f"{CODEX_CONFIG_ENV} must decode to a JSON object")

configured_provider_id = agent_env.get(CODEX_MODEL_PROVIDER_ENV) or config.get(
"model_provider"
)
provider_id = (
agent_env.get(CODEX_MODEL_PROVIDER_ENV)
or config.get("model_provider")
or codex_provider_id(provider_name)
configured_provider_id
if isinstance(configured_provider_id, str) and configured_provider_id
else codex_provider_id(provider_name)
)
providers = config.get("model_providers")
providers = {} if not isinstance(providers, dict) else dict(providers)
provider = providers.get(provider_id)
provider = dict(provider) if isinstance(provider, dict) else {}
providers_value = config.get("model_providers")
providers = {} if not isinstance(providers_value, dict) else dict(providers_value)
provider_value = providers.get(provider_id)
provider = dict(provider_value) if isinstance(provider_value, dict) else {}
provider.setdefault("name", provider_name)
provider["base_url"] = base_url
provider.setdefault("env_key", "OPENAI_API_KEY")
provider["env_key"] = "OPENAI_API_KEY"
provider.setdefault("wire_api", "responses")
provider.setdefault("supports_websockets", False)

_write_codex_provider_config(
agent_env,
config=config,
providers=providers,
provider_id=provider_id,
provider=provider,
model=model,
base_url=base_url,
provider_name=provider_name,
)


def apply_codex_proxy_config(
agent_env: dict[str, str],
*,
base_url: str,
model: str | None,
provider_name: str,
) -> None:
"""Replace Codex config with one BenchFlow-owned proxy provider."""

provider_id = codex_provider_id(provider_name)
_write_codex_provider_config(
agent_env,
config={},
providers={},
provider_id=provider_id,
provider={
"name": provider_name,
"base_url": base_url,
"env_key": "OPENAI_API_KEY",
"wire_api": "responses",
"supports_websockets": False,
},
model=model,
base_url=base_url,
provider_name=provider_name,
)


def _write_codex_provider_config(
agent_env: dict[str, str],
*,
config: dict[str, Any],
providers: dict[str, Any],
provider_id: str,
provider: dict[str, Any],
model: str | None,
base_url: str,
provider_name: str,
) -> None:
"""Serialize one already-resolved Codex provider configuration."""

providers[provider_id] = provider
config["model_providers"] = providers
config["model_provider"] = provider_id
if model:
config["model"] = model

agent_env[CODEX_MODEL_PROVIDER_ENV] = str(provider_id)
agent_env[CODEX_MODEL_PROVIDER_ENV] = provider_id
agent_env[CODEX_CONFIG_ENV] = json.dumps(config, separators=(",", ":"))
_apply_codex_default_auth_request(
agent_env,
Expand Down
Loading
Loading