Skip to content

Commit 2bdddc6

Browse files
author
Florian Standhartinger
committed
kernel: don't let the workspace shadow the stdlib
Found by running the real binary for the first time. The run happened to be in /tmp, which contained a stray inspect.py, and the kernel never booted — 'python -m' prepends the cwd to sys.path, so that file shadowed the stdlib module that dataclasses imports. This is not an edge case for a coding agent. It works inside other people's repositories, and those are full of files called types.py, token.py, code.py and parser.py. Any one of them would have taken the kernel down on that repo and nowhere else. Two changes: the kernel now starts in the workspace via current_dir, so relative paths in a cell mean what the model expects; and PYTHONSAFEPATH keeps that directory off sys.path. The regression test plants inspect.py, types.py and token.py in the workspace. It fails without the fix — verified, not assumed. Worth noting the model diagnosed this correctly and unaided from the traceback we surface, named the shadowing file, and offered three fixes. The error path works.
1 parent 86dba36 commit 2bdddc6

2 files changed

Lines changed: 43 additions & 0 deletions

File tree

codex-rs/chutes-rlm/src/kernel.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,20 @@ impl RlmKernel {
140140
.stdout(Stdio::piped())
141141
.stderr(Stdio::piped())
142142
.kill_on_drop(true)
143+
// Start in the workspace rather than inheriting the agent's cwd,
144+
// so relative paths in a cell mean what the model expects.
145+
.current_dir(&config.cwd)
143146
.env("PYTHONUNBUFFERED", "1")
147+
// ...but do NOT put that directory on sys.path.
148+
//
149+
// `python -m` prepends the cwd, so any file in the user's repo named
150+
// like a stdlib module shadows it and the kernel dies before it
151+
// boots. Not hypothetical: the first run of the real binary happened
152+
// to be in /tmp, which contained a stray `inspect.py`, and the kernel
153+
// never came up. A coding agent works inside other people's
154+
// repositories, and those are full of `types.py`, `token.py`,
155+
// `code.py` and `parser.py`.
156+
.env("PYTHONSAFEPATH", "1")
144157
.env(
145158
"CHUTESCODER_RLM_MAX_REPR",
146159
config.max_repr_bytes.to_string(),

codex-rs/chutes-rlm/src/kernel_tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,33 @@ async fn a_kernel_that_ignores_its_deadline_still_only_costs_one_turn() {
426426
);
427427
kernel.shutdown().await;
428428
}
429+
430+
#[tokio::test(flavor = "multi_thread")]
431+
async fn a_stdlib_shadowing_file_in_the_workspace_does_not_kill_the_kernel() {
432+
require_python!();
433+
// A coding agent works inside other people's repositories, and those are
434+
// full of files named like stdlib modules. `python -m` prepends the CWD to
435+
// sys.path, so without PYTHONSAFEPATH a stray `inspect.py` takes the kernel
436+
// down before it boots. Found by running the real binary in /tmp.
437+
let dir = tempfile::tempdir().expect("tempdir");
438+
for shadow in ["inspect.py", "types.py", "token.py"] {
439+
std::fs::write(dir.path().join(shadow), "raise RuntimeError('shadowed')\n")
440+
.expect("write shadow module");
441+
}
442+
443+
let config = KernelConfig {
444+
package_path: Some(package_path()),
445+
cwd: dir.path().to_string_lossy().to_string(),
446+
..Default::default()
447+
};
448+
let (dispatch, _) = StubDispatch::new();
449+
// The kernel now starts *in* config.cwd, so this really does put the
450+
// shadowing files where `python -m` would otherwise find them first.
451+
let kernel = RlmKernel::start(config, dispatch, None)
452+
.await
453+
.expect("the kernel must boot even next to stdlib-shadowing files");
454+
455+
let res = kernel.execute("import inspect, types, token\n'ok'", None).await.expect("cell runs");
456+
assert_eq!(res.repr.as_deref(), Some("ok"), "{res:?}");
457+
kernel.shutdown().await;
458+
}

0 commit comments

Comments
 (0)