Skip to content

Commit ff83a4d

Browse files
authored
Merge pull request #3584 from huangruiteng/codex/issue3539-provider-credential-boundary
fix(benchmark): isolate provider credentials from agents
2 parents f52ff59 + cbc0dbb commit ff83a4d

16 files changed

Lines changed: 906 additions & 141 deletions

‎benchmark/deepswe/README.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,14 @@ The treatment also needs three independent product-path proofs:
140140
Prepare the latter two with
141141
`benchmark_toolkit.native_codex_profile.install_native_codex_profile`. Do not copy
142142
`SKILL.md` files into a runner image. Generate the first input with
143-
`render_native_codex_goal_prompt`, build app-server's environment with
144-
`native_codex_app_server_environment(profile, provider_env_key=..., base_env=...)`,
145-
then separately exclude that key from agent shell and tool processes. The
146-
credential-free `native_codex_profile_environment` remains the default for CLI
147-
and preflight work. Set
143+
`render_native_codex_goal_prompt`, keep app-server on the credential-free
144+
`native_codex_profile_environment`, and route its provider through
145+
`serve_runner_owned_provider_gateway`. The app-server receives only the gateway
146+
URL and a fixed non-secret env sentinel. A Linux host-side worker must also run
147+
inside `native_codex_isolation`; filtering child env alone does not prevent a
148+
danger-full-access agent from reading parent process environments or ambient
149+
HOME files. `native_codex_app_server_shell_policy_args` remains defense in depth
150+
for model-created shells, not the credential boundary. Set
148151
`NativeGoalConfig.required_skill_ids=profile.required_skill_ids`. The runtime then
149152
uses `skills/list` before thread creation and fails before model work unless Codex
150153
actually discovers the installed skill set. A filesystem check alone is not

‎benchmark/native_codex_goal.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
compact_native_codex_profile_receipt,
3737
inspect_native_codex_profile,
3838
install_native_codex_profile,
39-
native_codex_app_server_environment,
39+
native_codex_app_server_shell_policy_args,
4040
native_codex_profile_environment,
4141
render_native_codex_goal_prompt,
4242
)
@@ -60,7 +60,7 @@
6060
"compact_native_goal_receipt",
6161
"inspect_native_codex_profile",
6262
"install_native_codex_profile",
63-
"native_codex_app_server_environment",
63+
"native_codex_app_server_shell_policy_args",
6464
"native_codex_profile_environment",
6565
"observe_native_goal_event",
6666
"probe_native_goal_process",

‎benchmark/tests/test_native_codex_profile.py‎

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
compact_native_codex_profile_receipt,
1414
inspect_native_codex_profile,
1515
install_native_codex_profile,
16-
native_codex_app_server_environment,
16+
native_codex_app_server_shell_policy_args,
1717
native_codex_profile_environment,
1818
render_native_codex_goal_prompt,
1919
)
@@ -84,51 +84,47 @@ def test_compact_profile_receipt_excludes_local_paths() -> None:
8484
assert "/private" not in rendered
8585

8686

87-
def test_app_server_environment_explicitly_admits_only_provider_value(
88-
tmp_path: Path,
89-
) -> None:
87+
def test_profile_environment_excludes_ambient_provider_values(tmp_path: Path) -> None:
9088
profile = _fake_profile(tmp_path)
9189
base_env = {
9290
"PATH": "/usr/bin:/bin",
9391
"CODEX_GOAL_API_KEY": "provider-value",
9492
"UNRELATED_PRIVATE_VALUE": "must-not-pass",
9593
}
96-
original_base_env = dict(base_env)
97-
9894
profile_env = native_codex_profile_environment(profile, base_env=base_env)
99-
app_server_env = native_codex_app_server_environment(
100-
profile,
101-
provider_env_key="CODEX_GOAL_API_KEY",
102-
base_env=base_env,
103-
)
10495

10596
assert "CODEX_GOAL_API_KEY" not in profile_env
10697
assert "UNRELATED_PRIVATE_VALUE" not in profile_env
107-
assert app_server_env["CODEX_GOAL_API_KEY"] == "provider-value"
108-
assert "UNRELATED_PRIVATE_VALUE" not in app_server_env
109-
assert app_server_env["CODEX_HOME"] == str(profile.codex_home)
110-
assert base_env == original_base_env
98+
assert profile_env["CODEX_HOME"] == str(profile.codex_home)
11199

112100

113-
def test_app_server_environment_rejects_invalid_or_missing_provider_key(
114-
tmp_path: Path,
115-
) -> None:
116-
profile = _fake_profile(tmp_path)
101+
def test_native_codex_app_server_shell_policy_is_explicit_and_fail_closed() -> None:
102+
args = native_codex_app_server_shell_policy_args(
103+
excluded_env_keys=("SECOND_PROVIDER_KEY", "PRIMARY_PROVIDER_KEY"),
104+
)
105+
assert args == (
106+
"-c",
107+
'shell_environment_policy.inherit="core"',
108+
"-c",
109+
"shell_environment_policy.ignore_default_excludes=false",
110+
"-c",
111+
(
112+
'shell_environment_policy.include_only=["HOME", "LANG", "LC_ALL", '
113+
'"LC_CTYPE", "LOGNAME", "PATH", "SHELL", "SSL_CERT_DIR", '
114+
'"SSL_CERT_FILE", "TERM", "TMPDIR", "TZ", "USER"]'
115+
),
116+
"-c",
117+
'shell_environment_policy.exclude=["PRIMARY_PROVIDER_KEY", "SECOND_PROVIDER_KEY"]',
118+
)
117119

118-
with pytest.raises(ValueError, match="provider_env_key"):
119-
native_codex_app_server_environment(
120-
profile,
121-
provider_env_key="invalid-key",
122-
base_env={"invalid-key": "value"},
123-
)
124-
with pytest.raises(
125-
NativeCodexProfileError,
126-
match="provider_environment_value_missing:CODEX_GOAL_API_KEY",
127-
):
128-
native_codex_app_server_environment(
129-
profile,
130-
provider_env_key="CODEX_GOAL_API_KEY",
131-
base_env={"PATH": "/usr/bin:/bin"},
120+
121+
@pytest.mark.parametrize("excluded_env_key", ("", "invalid-key", "WITH SPACE"))
122+
def test_native_codex_app_server_shell_policy_rejects_unsafe_keys(
123+
excluded_env_key: str,
124+
) -> None:
125+
with pytest.raises(ValueError, match="excluded_env_keys"):
126+
native_codex_app_server_shell_policy_args(
127+
excluded_env_keys=(excluded_env_key,),
132128
)
133129

134130

‎benchmark/widesearch/README.md‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,23 @@ Local WideSearch(公开 web 表格填充 benchmark,200 题)practice:同
1616
- Verifier:官方 `widesearch_evaluator.py`(SR / 行级 / 项级 F1 + LLM judge,
1717
judge 复用 ARK 模型)。
1818

19-
## 答案隔离边界(重要)
19+
## 隔离边界(重要)
2020

21-
本地路径是**过程性隔离**,不是沙盒强隔离:
21+
native runner 现在默认 fail closed,并组合两层 runner-owned 边界:
2222
- 每次 run 用独立 fresh workspace,run 前清空,杜绝旧答案误用;
2323
- answer 必须存在、非空、mtime 晚于 run 开始(否则判定失败);
2424
- evaluator 在 agent 结束后独立后置执行,只读 sealed answer + gold;
25-
- 但 agent(codex)拥有宿主全盘读权限(`workspace-write` 不挡读;macOS
26-
`sandbox-exec` 会破坏 codex 运行时,已实测证伪),因此**不能假定 agent 读不到
27-
gold**。任何结论都限定为内部能力论证,发布权威结果需真沙盒
28-
(pier/colima docker 或云端 sealed-dual 强隔离)。
25+
- upstream provider key 只在 namespace 外的 runner-owned loopback gateway;
26+
app-server 只收到 gateway URL 与固定非秘密 sentinel;
27+
- app-server 在 Linux user/mount/PID namespace 的合成 root 中运行,只暴露
28+
fresh workspace、每次 run 新建的正式 LoopX profile 与只读系统 runtime;
29+
runner/祖先进程环境、ambient HOME/CODEX_HOME、gold 和 provider 文件均不可见。
30+
31+
`shell_environment_policy` 只是 defense in depth,不能代替上述 OS authority
32+
boundary。当前 native runner 需要 Linux unprivileged namespace primitives;macOS
33+
或禁用 user namespace 的 Linux 会以
34+
`widesearch_native_isolation_unavailable_use_pier` 失败,不会退回 ambient
35+
`danger-full-access`。这些平台请使用下文 Pier/Colima 路径。
2936

3037
## 运行
3138

@@ -44,8 +51,9 @@ uv run --python 3.12 --with dateparser==1.2.2 \
4451
--arm treatment --case ws_en_001 --data-root <data-root>
4552
```
4653

47-
模型凭据:`ARK_OPENAI_BASE_URL` / `ARK_OPENAI_API_KEY` / `ARK_OPENAI_MODEL`
48-
(本地可从 cc-switch `volcengine-ark-deepseek` 读取)。
54+
runner-owned gateway 凭据:`ARK_OPENAI_BASE_URL` / `ARK_OPENAI_API_KEY`;模型名:
55+
`ARK_OPENAI_MODEL`。真实 key 不进入 app-server 环境、正式 profile 或 agent
56+
namespace。不要把这些值写入数据目录、job 模板或命令行。
4957

5058
## Hosted Responses API 兼容(重要)
5159

‎benchmark/widesearch/pier/README.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
Reusable, provider-neutral pier wiring for the WideSearch real-sandbox path.
44
Copy these templates into a local pier dataset root and fill the `<PLACEHOLDER>`
55
values. Do not commit credentials, local absolute paths, raw runs, or verifier
6-
output; inject secrets through the job environment at runtime.
6+
output. Inject judge secrets only into the separate verifier; keep the model
7+
provider secret in the runner-owned gateway, outside the agent container.
78

89
Layout expected by pier (task root):
910

@@ -35,9 +36,10 @@ Key wiring facts:
3536
used by the pier agent setup; Debian dash does not support it).
3637
- Gold lives only in the verifier image; the agent image never contains it.
3738

38-
Model credentials: for countable runs the agent image should reach the hosted
39-
model through a local gateway so the API key is not shell-readable in the
40-
container (matching `provider_credential_shell_excluded` in the integrity
41-
attestation). Use `network_access: "permitted_solving"` in the integrity policy
42-
for web-research benchmarks (see
43-
`loopx.capabilities.benchmark_toolkit.integrity`).
39+
Model credentials: countable runs must route the agent image through a
40+
runner-owned local gateway. The real provider credential stays in the gateway
41+
process, outside the agent container; the Codex provider config receives only a
42+
fixed non-secret sentinel. This is the concrete boundary required by
43+
`provider_credential_shell_excluded` in the integrity attestation. Use
44+
`network_access: "permitted_solving"` in the integrity policy for web-research
45+
benchmarks (see `loopx.capabilities.benchmark_toolkit.integrity`).

‎benchmark/widesearch/pier/job.yaml.template‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Pier job for the WideSearch real-sandbox path.
22
# - Keep jobs_dir / datasets under $HOME: colima does not bind-mount macOS
33
# /tmp, so verifier bind mounts of /logs/verifier silently fail there.
4-
# - Inject model credentials through the environment; do not bake secrets into
5-
# the job file. For countable runs the agent image should reach the model
6-
# through a local gateway so the key is not shell-readable in the container.
4+
# - Do not inject provider credentials into the agent container. The agent must
5+
# reach the hosted model through a runner-owned local gateway. The sentinel
6+
# below satisfies Codex's provider config without granting an upstream secret.
77
job_name: widesearch-<RUN_LABEL>
88
jobs_dir: <JOBS_DIR_UNDER_HOME>
99
n_attempts: 1
@@ -24,14 +24,13 @@ agents:
2424
approval_policy = "never"
2525
[model_providers.custom]
2626
name = "hosted-responses"
27-
base_url = "<MODEL_BASE_URL>"
28-
env_key = "OPENAI_API_KEY"
27+
base_url = "<RUNNER_LOCAL_GATEWAY_BASE_URL>"
28+
env_key = "LOOPX_MODEL_PROVIDER_SENTINEL"
2929
wire_api = "responses"
3030
[tools]
3131
web_search = true
3232
env:
33-
OPENAI_BASE_URL: "<MODEL_BASE_URL>"
34-
OPENAI_API_KEY: "<INJECT_VIA_RUNTIME_ENV>"
33+
LOOPX_MODEL_PROVIDER_SENTINEL: "local-gateway-no-provider-secret"
3534
datasets:
3635
- path: <DATASET_ROOT_UNDER_HOME>
3736
task_names: ["<CASE_ID>"]

0 commit comments

Comments
 (0)