fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(eval): restore headless tools and enforce benchmark egress - #2947

Merged
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile
Aug 13, 2026
Merged

fix(eval): restore headless tools and enforce benchmark egress#2947
hqhq1025 merged 13 commits into
apache:mainfrom
hqhq1025:codex/eval-maka-tool-profile

Conversation

@hqhq1025

Copy link
Copy Markdown
Contributor

Summary

  • restore a versioned headless-coding-v1 Hosted Execution profile for Maka Eval without reviving the removed Headless package
  • constrain the DeepSeek Responses provider surface to Bash, Read, Glob, Grep, apply_patch, and runtime-owned ArchiveRead; exclude memory, task, goal, skill, web, automation, and parent-agent tools
  • remove WebSearch, WebFetch, and FetchURL from all eight harness request surfaces, with structural filtering at the external metering proxy
  • enforce subject-only egress through Harbor's Docker network namespace and an isolated per-cell MITM proxy; direct egress fails even after unsetting proxy variables or using --noproxy
  • recursively normalize and block benchmark/public-solution contamination URLs, including Terminal-Bench repos, registries, HF traces/datasets, public trajectories, pinned revision URLs, patch mirrors, and r.jina.ai wrappers
  • collect bounded egress rule hits as attempt artifacts with rule ID, host, normalized path, bytes, and SHA-256
  • restore Maka runtime artifacts: standalone runtime.sqlite with WAL contents, runtime policy, stdout/stderr, manifests, byte counts, and SHA-256 across settled, timeout, cancellation, and failure paths

Root causes

  • refactor: replace Headless with minimal Eval kernel #2605 removed the dedicated Headless execution path, but the replacement Runtime Host Eval path inherited the full product tool surface.
  • Runtime memory trigger tools are injected below the composer boundary, so limiting only composer tools was incomplete.
  • Proxy environment variables alone were bypassable and also did not cover libraries that ignore them.
  • The previously validated Maka artifact fix was not present on the current main-based tool-profile branch; additionally, relay cleanup deleted the fixed stdout artifact path.

Validation

Local:

  • Runtime Host full tests: 863/863
  • Eval Node tests: 24/24
  • relay contract: 3/3
  • relay lifecycle: 2/2
  • egress normalization/fail-closed tests: 3/3
  • Harbor pre-construction network policy test: 1/1
  • relay artifact lifecycle test: 1/1
  • Runtime Host and Eval typecheck after rebasing onto 8925d4353
  • Biome and git diff --check

Real host (dhb):

  • provider trace confirmed DeepSeek V4 Flash sees exactly 6 tools: apply_patch, ArchiveRead, Bash, Glob, Grep, Read
  • forced-network probe: ordinary proxied HTTPS = 200; recursive Jina benchmark URL = 451; direct curl --noproxy '*' failed with exit 35
  • Maka canary passed: score 1, 155,139 tokens, $0.003990603, 167.8s
  • canary produced a 1,581,056-byte standalone runtime.sqlite with SHA-256 plus runtime policy, stderr, manifest, and empty bounded egress audit; the final stdout retention fix has a dedicated regression test
  • maka-eval-egress-proxy:12.2.3 image built successfully on the VMSS host

Deployment boundary

Eval semantics and enforcement live in this PR. The machine-local prerequisite is only the pinned maka-eval-egress-proxy:12.2.3 image; the former shared systemd proxy experiment was removed. Harbor download and verifier networking remain unchanged; only Agent.run() is forced through the per-cell proxy.

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Real-host external seven-arm canary completed on the PR code line (count-dataset-tokens, fresh outputs, 7 cells only):

HarnessStatusScoreTokensDurationWeb tools removed at proxy
Codexcompleted11,405,515245.7s0
Claude Codecompleted1367,469547.8s0
Reasonixcompleted1753,925138.5s0
OpenCodecompleted1314,299150.4s0
Kimi Codecompleted1504,020149.2s17
ZCodecompleted1300,310113.4s0
Picompleted1142,663104.5s0

Evidence:

  • 7/7 completed, 7/7 score 1, no incomplete cells.
  • All seven egress audit artifacts were empty: no benchmark/public-solution contamination URL was attempted on this task.
  • Real trajectory scan found no WebSearch, WebFetch, or FetchURL tool call.
  • Kimi still proposed web tools in 17 provider requests; the structural metering-proxy projection removed them before model dispatch. The other harness-native configs removed them before that boundary.
  • A follow-up Claude-only diagnostic run passed with score 1 and recorded the actual provider request shape:
    • models = ["deepseek-v4-flash"]
    • toolNames = ["Bash", "Edit", "Read"]
    • removedWebTools = 0
    • no managed-settings permission warning, no web tool calls, empty egress audit.

This confirms the earlier deepseek-v4-pro value seen in Claude's streamed compatibility response was not the requested model; the metered provider requests are explicitly deepseek-v4-flash.

Astro-Han

This comment was marked as outdated.

@Astro-Han
Astro-Han dismissed their stale reviewAugust 13, 2026 08:58

Superseded by the same review posted as a non-blocking comment.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent adversarial reviewers. Findings verified against source in an AI-assisted pass; the severity split and the merge/follow-up decision are mine.

Summary

The profile mechanism, fail-closed bindToolsByName, egress URL normalization, and WAL-checkpoint artifact capture are all the right shape.

The blocker is narrower than the finding count suggests: the profile freezes tool names only. The system prompt, tool schemas, and execution boundary are still product defaults, so a product change still moves scores under an unchanged profile version. Items 1–4 are symptoms of that gap plus one placement decision — patching them individually leaves the structure intact.

Must fix before merge

1. Freeze the system prompt.interactive-run-composer.ts:199 only branches on childInstruction, so a profiled run still gets product identity, personalization, skills, and workspace instructions. assembleMainSessionSystemPrompt already takes identity?: boolean (main-session-prompt.ts:34). Without this, README.md:29's "freeze" claim does not hold.

2. Strip run_in_background / pty from Bash in this profile. Scoring correctness, not resources: the normal path calls waitForResidenciesExcept('runtime-resource') (hosted-execution-runner.ts:63), so a background process keeps mutating /app after the execution returns and the verifier races — silently. The cancel path waits for a process that cannot be stopped and burns the cell. Adding StopBackgroundTask is insufficient; it still relies on the model cleaning up.

3. Add a wire-level contract test. Pin a hash of the system prompt and full tool schema from the first provider request of a real hosted.execution.start. Composer-level assertions miss it: routeApplyPatchTools and bindToolResultArchiveDecoder (ai-sdk-backend.ts:1129) rewrite the tool set after binding.

4. Move toolProfile into SessionCreateInput. Reason is protocol cost, not present risk — once it ships next to maxSteps, relocating it is breaking; today it is a field move that also deletes HostedExecutionToolProfileRegistry (~100 net lines). It closes two findings by construction: the registry dies in the runner's finally while the session is durable, so a second turn.start or a refreshIdleBackends rebuild silently restores the full surface (hosted-execution-tool-profile.ts:50); and resolveAvailableToolNames / resolveNewSessionToolNames (execution-composition.ts:674, :740) rebuild a composer without the binding, so the projected and executed surfaces disagree. Reading from backendContext.header next to permissionMode fixes both.

Follow-up issues

All three are silent when they fail, and none trigger under the current config:

  • Pin the egress image by digest.docker-compose-egress-proxy.yaml:17 uses a mutable local tag and the executor does not verify image identity, so a stale same-tag image applies different rules.
  • apply_subject_egress_policy must fail closed.run_trial.py:135 returns silently when MAKA_EVAL_EGRESS_ALLOWED_HOST is unset — enforcement skipped, no error, no artifact, run still completes.
  • Delete webTools. Its tool-surface job is now toolProfile's, and webSearch.enabled: false duplicates the core default (runtime-policy.ts:182). What is left — incognito and the networkProxy config — has nothing to do with the name. Write the policy unconditionally.

On keeping this as one PR

Splitting means re-running the canary that already passed, so it is not worth it. Two consequences: independent revert is lost (an egress rollback takes the profile fix with it), and packages/runtime-host/** is a public contract change needing independent human review — suggest stating in the review record that the subtree was read line by line, so the requirement is met by the record rather than the PR boundary. If item 4 touches more callers than expected, splitting the Runtime Host part out at that point is the only cut worth making.

Trade-offs worth recording in the PR body

  • The egress filter is a blocklist; google.com/search?q=terminal+bench stays open (+ does not match). State the threat model — accidental contamination vs. deliberate lookup — so it is not read as a complete guarantee.
  • test_relay_artifacts.py exits 127 on macOS for lack of setsid.
中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立对抗性审查方。finding 已在 AI 辅助的流程中对照源码核对;严重程度划分与合并/后续的取舍由我决定。

结论

profile 机制、fail-closed 的 bindToolsByName、egress URL 归一化、WAL checkpoint 的 artifact 捕获,形状都是对的。

阻断项比 finding 数量显示的更集中:profile 只冻结了工具名。 system prompt、工具 schema、执行边界仍是产品默认值,所以 profile 版本不变时,产品侧的改动依然会移动分数。第 1–4 项是这个缺口加上一个位置选择的症状,逐个打补丁会让 CI 变绿但结构问题原样保留。

合并前必修

1. 冻结 system prompt。interactive-run-composer.ts:199 只判断 childInstruction,所以 profile 命中的运行仍会拿到产品 identity、personalization、skills 和 workspace instructions。assembleMainSessionSystemPrompt 已经支持 identity?: booleanmain-session-prompt.ts:34)。不做这条,README.md:29 宣称的 "freeze" 不成立。

2. 在此 profile 内裁掉 Bash 的 run_in_background / pty 这是评分正确性问题而非资源问题:正常完成路径调用 waitForResidenciesExcept('runtime-resource')hosted-execution-runner.ts:63),后台进程会在 execution 返回后继续修改 /app,verifier 观察到竞态——且是静默的。取消路径则会等待一个无法停止的进程,烧掉整格。补 StopBackgroundTask 不够,它仍依赖模型主动清理。

3. 补 wire 级契约测试。 拦截真实 hosted.execution.start 的第一条 provider request,对 system prompt 和完整工具 schema 取 hash 钉死。composer 层断言盖不住:routeApplyPatchToolsbindToolResultArchiveDecoderai-sdk-backend.ts:1129)会在绑定之后改写工具集。

4. 把 toolProfile 移进 SessionCreateInput 理由是协议成本而非当前风险——一旦作为 maxSteps 的兄弟字段发布,之后再移就是 breaking change;现在做只是移动一个字段,同时删掉 HostedExecutionToolProfileRegistry(净减约 100 行)。它还顺带从构造上消灭两条 finding:registry 在 runner 的 finally 里销毁而 session 是持久实体,所以第二次 turn.startrefreshIdleBackends 重建会静默恢复完整工具面(hosted-execution-tool-profile.ts:50);而 resolveAvailableToolNames / resolveNewSessionToolNamesexecution-composition.ts:674:740)重建 composer 时不带绑定,导致投影面与执行面不一致。改从 backendContext.header 读(与 permissionMode 并列)两者同时消失。

转 issue 跟进

三条的共同点:当前配置下不触发,而失效时是静默的。

  • egress 镜像按 digest 固定。docker-compose-egress-proxy.yaml:17 用的是可变本地 tag,executor 也不校验镜像身份,机器上存着同 tag 的旧镜像就会应用不同规则。
  • apply_subject_egress_policy 必须 fail closed。run_trial.py:135MAKA_EVAL_EGRESS_ALLOWED_HOST 缺失时静默返回——强制被跳过,无报错、无 artifact,运行照常完成。
  • 删掉 webTools 它的工具面职责现在归 toolProfile,而 webSearch.enabled: false 与 core 默认值重复(runtime-policy.ts:182)。剩下的 incognito 和 networkProxy 配置与这个名字毫无关系。policy 应无条件写入。

关于不拆 PR

拆分意味着重跑已经通过的 canary,不划算。两个代价:失去独立 revert 能力(回滚 egress 会把 profile 修复一起带走);packages/runtime-host/** 属公共契约变更,需要独立人工评审——建议在 review 记录里注明该子树已逐行阅读,用记录而非 PR 边界满足评审强度要求。如果第 4 项牵动的调用方超出预期,那时把 Runtime Host 部分拆出来是唯一值得拆的一刀。

建议写进 PR 正文的取舍

  • egress filter 是 blocklist,google.com/search?q=terminal+bench 仍然开放(+ 不匹配正则)。建议写明威胁模型——防意外污染还是防主动查找——避免被读成完备保证。
  • test_relay_artifacts.py 在 macOS 因缺 setsid 退出码 127。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Addressed the review on the latest main base. The Runtime Host public-contract subtree was re-read line by line across the Session type, protocol codec, storage normalization, create fingerprint, backend factory, available-tool projection, Hosted runner, and provider-wire test.

Merge blockers

  1. System prompt frozen

    • headless-coding-v1 now uses the exact former Headless default prompt.
    • The profiled composer does not read product identity, personalization, Skill catalog, workspace instructions, or Memory prompt fragments.
    • Prompt SHA-256 is pinned from a real DeepSeek Responses request.
  2. Foreground-only Bash

    • The profile projects a strict Bash schema containing only command and optional timeout_ms.
    • run_in_background and pty are absent from both the model schema and accepted arguments.
  3. Wire-level contract

    • A real hosted.execution.start test captures the first main /v1/responses request.
    • Pinned hashes:
      • prompt: sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271
      • complete tools array: sha256:ea1f293096e5e209ae49346f46b0e8ff9b54ae17452a5a23149ad7233afaeafc
    • The provider-visible tools are exactly ArchiveRead, Bash, Glob, Grep, Read, apply_patch.
  4. Profile moved into durable Session creation

    • toolProfile is now part of SessionCreateInput, persisted in SessionHeader, validated by storage, and included in the stable create fingerprint.
    • The process-local registry is deleted.
    • Backend creation, Memory-trigger admission, and available-tool projection read backendContext.header.toolProfile / the durable header.
    • The wire test starts a second Turn on the same Session and verifies identical prompt/tool hashes.

Follow-ups from the review

  • Missing required egress host now fails closed; covered by a Python regression test.
  • webTools was deleted. Maka Eval policy is written unconditionally; tool-surface authority belongs to the Session profile.
  • URL normalization now also blocks terminal+bench; README explicitly records the blocklist threat model.
  • Registry digest publication/pinning requires a published registry manifest and is tracked in eval: publish and pin the egress proxy image by registry digest #2953. A machine-local image ID is not used as a fake portable digest.
  • The relay artifact regression no longer executes setsid; lifecycle tests that require GNU setsid already use skipUnless.

Validation

  • Runtime Host: 900/900
  • Eval Node: 30/30
  • relay contract: 10/10
  • relay lifecycle: 12/12
  • egress filter: 3/3
  • egress policy: 2/2
  • relay artifact: 1/1
  • SessionHeader SQLite round-trip: 1/1
  • typecheck, Biome, git diff --check

Real-host Maka smoke on the reviewed implementation:

  • score: 1
  • tokens: 323,992
  • cost: $0.0062324306
  • duration: 337.3s
  • standalone runtime.sqlite: 2,191,360 bytes with SHA-256
  • stdout/stderr/runtime policy/manifest/egress audit all collected with hashes
  • egress audit empty

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review: Claude Code (primary), with Codex and kimi-k3-256k run as independent fresh-eye adversarial reviewers on the updated branch. Findings verified against source in an AI-assisted pass; the severity calls and the merge decision are mine.

Approving. All four blockers from the previous round are addressed, and the wire-level contract test is stronger than what I asked for — pinning the full provider request across two turns is the strongest available assertion that the profile actually holds. The remaining findings are P2 and below; two of them are net deletions, so they are worth doing here rather than in a follow-up.

Worth doing before merge

  • egress_filter.py:25 never sees the hostname.lowered is built from path and query only, so https://terminal-bench.io/tasks/answers and https://sub.tbench.ai/x both return None — only the exact tbench.ai match fires. Fix is lowered = f"{host}{path_query}".lower() plus a suffix match for tbench.ai. Worth noting the audit log is archived by sha256 in harness-executor.ts:836 as complete evidence, so the gap also overstates what the artifact proves.
  • Delete create_harbor_trial (run_trial.py:147). It reimplements Trial.create through two private methods and has already drifted — the copy drops the source_trial / RegradeTrial branch. resolve_trial_network_plan reads self.task.config during run(), so trial = await trial_type.create(config) followed by mutating trial.task is equivalent and removes the fork. Net deletion.
  • Roll back the economy change for boundTools (interactive-run-composer.ts:159).main applies !process.env.MAKA_DISABLE_DEFERRED_TOOLS unconditionally; the new ternary also disables deferred loading on the product sub-agent path, which is a user-visible change with no test and no stated reason. economy: input.boundToolNames ? false : ... keeps the eval ceiling exact and leaves sub-agents alone. If the broader change is intended, it deserves its own commit.
  • Suppress and record artifact collection failures on the success path (relay_agent.py:114, :153). The same call is wrapped in contextlib.suppress on the exception paths at :196 and :204, and _persist_subject_outputs raises when mkdir fails — so a scorable attempt becomes an infra failure if /logs is not writable. The asymmetry reads as an oversight rather than a decision. Suppress plus a structured collection-error record (the shape already exists at maka-artifacts.ts:98) keeps both properties.
  • Reject egressProxy for pier at decode time (harness-executor.ts:791).decodeOptions accepts it for any framework, but the pier branch at run_trial.py:129 never calls apply_subject_egress_policy — the proxy runs and the env vars are injected while namespace enforcement silently does not exist. No such spec exists today; a throw keeps it that way.
  • Freeze the turn tail. The profile replaces the base system prompt but still reuses the product turnTailPrompt, which appends date, cwd, and git state to the user message (ai-sdk-backend.ts:1721). The persisted fingerprint (execution-model-composition.ts:265) covers only the base prompt and tool catalog, so the same profile version can produce different provider input across dates or branches — which is exactly the property this PR exists to guarantee.

One structural suggestion

RunCompositionSnapshot (core/src/run-composition.ts) already carries baseSystemPromptHash / toolCatalogHash / toolNames and is already written per Run by execution-model-composition.ts:276. Extending it to cover the full provider-visible prefix and surfacing it through the hosted execution projection into the Attempt Result would make "are these two runs comparable" a query rather than an archaeology exercise against a test constant. Worth doing because a hash pinned only in a test file invites fixing a red diff by editing the constant while the profile is still called v1. If it touches more callers than expected, that is the one cut worth splitting out.

Follow-up issues

Pin the egress image by digest; mount only mitmproxy-ca-cert.pem into the subject rather than the whole state volume (the mitmproxy confdir puts the CA private key there); drop the unconditional ICMP accept in network-policy:47-48 or narrow it to a stated destination; write a truncation marker when the audit log hits MAX_AUDIT_BYTES; treat a missing audit file as a failure rather than passing verification. Also some cleanup: hostedExecutionToolNames has no production caller, and the compose-file regex assertions in lifecycle-boundaries.test.ts assert YAML text shape rather than semantics.

Deliberately not suggesting

Collapsing the profile into a single application point, and versioning the egress topology into one egressPolicy field. Both are real — the profile applies at three separate places today, and the six egress fields in the experiment JSON are all bound to hardcoded values — but there is one profile and one topology, so the right time is when the second one arrives and the shape is known.

中文对照

AI 辅助审查:Claude Code 为主,Codex 与 kimi-k3-256k 作为独立 fresh-eye 对抗性审查方,针对更新后的分支重跑。finding 已在 AI 辅助的流程中对照源码核对;严重程度判断与合并决定由我负责。

approve。上一轮四项必修全部落实,wire 级契约测试比我要求的更强——钉住跨两轮 turn 的完整 provider request,是"profile 确实生效"这件事最强的可用断言。其余 finding 均为 P2 及以下,其中两条是净删代码,值得在本 PR 里顺手做掉而非另开。

合并前值得做

  • egress_filter.py:25 从不看主机名。lowered 只由 path 和 query 构造,因此 https://terminal-bench.io/tasks/answershttps://sub.tbench.ai/x 都返回 None,只有精确匹配的 tbench.ai 会命中。修法是 lowered = f"{host}{path_query}".lower() 并把 tbench.ai 改为后缀匹配。附带一提:审计日志的 sha256 在 harness-executor.ts:836 被当作完整证据归档,所以这个缺口也让该 artifact 的可信度标注高于实际。
  • 删掉 create_harbor_trialrun_trial.py:147)。 它用两个私有方法重写了 Trial.create,且已经发生漂移——副本丢掉了 source_trial / RegradeTrial 分支。resolve_trial_network_plan 是在 run() 期从 self.task.config 现算的,所以 trial = await trial_type.create(config) 之后再改 trial.task 完全等价,并消除这个 fork。净删代码。
  • 回退 boundToolseconomy 改动(interactive-run-composer.ts:159)。main 上是无条件 !process.env.MAKA_DISABLE_DEFERRED_TOOLS;新的三元式把产品子代理路径的 deferred 加载也一并关掉了,属于用户可见变更,无测试、无理由说明。economy: input.boundToolNames ? false : ... 既保住 eval 侧的精确天花板,又不动子代理。若确实想改子代理行为,它值得单独一个 commit。
  • 正常路径的 artifact 采集改为 suppress + 记录(relay_agent.py:114:153)。 同一调用在 :196:204 的异常路径上被 contextlib.suppress 包着,而 _persist_subject_outputsmkdir 失败时会 raise——于是 /logs 不可写时,一个本可评分的 attempt 会变成 infra failure。这个不对称更像疏漏而非设计。suppress 加一条结构化 collection-error 记录(maka-artifacts.ts:98 已有该形状)可以两者兼得。
  • decode 期拒绝 pier 的 egressProxyharness-executor.ts:791)。decodeOptions 对任何 framework 都接受它,但 run_trial.py:129 的 pier 分支从不调用 apply_subject_egress_policy——代理照跑、环境变量照注,而命名空间级强制静默不存在。当前没有这样的 spec,一个 throw 可以保证以后也没有。
  • 冻结 turn tail。 profile 替换了基础 system prompt,但仍复用产品的 turnTailPrompt,后者会把日期、cwd 和 git 状态追加到 user message(ai-sdk-backend.ts:1721)。持久化的 fingerprint(execution-model-composition.ts:265)只覆盖基础 prompt 与工具 catalog,因此同一 profile 版本在不同日期或分支下会产生不同的 provider 输入——而这正是本 PR 要保证的性质。

一条结构性建议

RunCompositionSnapshotcore/src/run-composition.ts)已经携带 baseSystemPromptHash / toolCatalogHash / toolNames,也已由 execution-model-composition.ts:276 逐 Run 写入。把它扩展到覆盖完整的 provider-visible 前缀,并经 hosted execution projection 落进 Attempt Result,可以让"这两次跑分可比吗"变成一次查询,而不是对着测试常量做考古。值得做的理由是:哈希只钉在测试文件里时,diff 变红最省事的修法就是改常量,而 profile 仍然叫 v1。如果它牵动的调用方超出预期,那是唯一值得拆出去的一刀。

转 issue 跟进

egress 镜像按 digest 固定;只把 mitmproxy-ca-cert.pem 挂进 subject,而不是整个 state 卷(mitmproxy 的 confdir 会把 CA 私钥放在里面);去掉 network-policy:47-48 无条件的 ICMP accept,或收窄到明确目标;审计日志达到 MAX_AUDIT_BYTES 时写一条截断标记;审计文件缺失时判为失败而非照常通过。另有一些清理:hostedExecutionToolNames 没有生产调用方;lifecycle-boundaries.test.ts 里对 compose 文件的正则断言检查的是 YAML 文本形状而非语义。

刻意不建议的

把 profile 收敛成单点施加,以及把 egress 拓扑版本化成一个 egressPolicy 字段。两者都是真实的——profile 目前在三处分别生效,experiment JSON 里那六个 egress 字段也全绑死在硬编码值上——但现在只有一个 profile、一个拓扑,正确的时机是第二个出现、形状明确的时候。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han