plugintree = os.path.join(candidate, "plugin", "terminaljail")
Root cause. seccomp-loader._setup_path() hard-coded repo_root = dirname(dirname(loader)), which only holds for the repo layout (<root>/standalone/seccomp-loader.py + <root>/plugin/). install.sh flattens the loader into <prefix>/lib/terminal-jail/seccomp-loader.py with the plugin tree at <prefix>/lib/terminal-jail/plugin/, so the old guess resolved to <prefix>/lib and the from terminal_jail.seccomp import … failed with ModuleNotFoundError: No module named 'terminal_jail' (reproduced before fixing).
Fix (~/terminal-jail/standalone/seccomp-loader.py): walk up from the loader's real directory until a directory containing plugin/terminal_jail/ (or a bare terminal_jail package) is found, and insert the plugin-hosting dir into sys.path. Works for both layouts, is bounded at the filesystem root, and resolves symlinks so /usr/local/bin shims still work:
def _setup_path() -> None:
"""...works for repo layout (<root>/standalone/ + <root>/plugin/)
and installed layout (<prefix>/lib/terminal-jail/ + .../plugin/)."""
def _find_plugin_root(candidate: str) -> str | None:
# Layout A/B: <candidate>/plugin/terminal_jail/__init__.py
plugin_tree = os.path.join(candidate, "plugin", "terminal_jail")
if os.path.isfile(os.path.join(plugin_tree, "__init__.py")):
return os.path.join(candidate, "plugin")
# Layout C: bare package <candidate>/terminal_jail/__init__.py
bare_pkg = os.path.join(candidate, "terminal_jail")
if os.path.isfile(os.path.join(bare_pkg, "__init__.py")):
return candidate
return None
loader_dir = os.path.dirname(os.path.realpath(__file__))
current = loader_dir
while True:
plugin_root = _find_plugin_root(current)
if plugin_root is not None:
if plugin_root not in sys.path:
sys.path.insert(0, plugin_root)
return
parent = os.path.dirname(current)
if parent == current: # reached filesystem root
break
current = parent
# Unrecognised layout: fall back to the historical guess.
repo_root = os.path.dirname(loader_dir)
plugin_dir = os.path.join(repo_root, "plugin")
if plugin_dir not in sys.path:
sys.path.insert(0, plugin_dir)
Also added ~/terminal-jail/install.sh (ships lib/terminal-jail/seccomp-loader.py, lib/terminal-jail/terminal-jail, and lib/terminal-jail/plugin/…, plus a bin/terminal-jail symlink) and the regression suite tests/test_installed_layout.py (5 tests). Per the problem note, probes invoke the loader with the command directly (echo <marker>) — never --user/--seccomp — because the bash wrapper strips its own flags before the loader sees them; the loader execs argv[1] verbatim.
**Regression reproduced (pre-fix):** simulating the installed layout and running with a stripped environment gave exactly the reported failure: ``` ModuleNotFoundError: No module named 'terminal_jail' (exit=1) ``` **Post-fix verification** — `install.sh <tmp_prefix>`, then `env -i PATH=… python3 <prefix>/lib/terminal-jail/seccomp-loader.py echo …` (strictest possible: no `PYTHONPATH`, no inherited env): - Installed layout: `installed-ok`, exit 0 (no ModuleNotFoundError, command output present) — the exact regression scenario from the problem. - Repo layout: `repo-ok`, exit 0 (no regression). - `install.sh` also works invoked from another cwd via a relative path. **Test-of-the-test:** with the old buggy `_setup_path` swapped back in, the suite fails 4/5 with the reported `ModuleNotFoundError` (the one passing test is the repo layout — precisely why the bug went unnoticed). With the fix, **5/5 pass**: 1. `test_installed_layout_loader_finds_plugin` — main regression (install into tmp_path → strip PYTHONPATH → run loader → no ModuleNotFoundError + output) 2. `test_installed_layout_setup_path_puts_plugin_dir_on_sys_path` — unit check that `_setup_path` puts `<prefix>/lib/terminal-jail/plugin` on `sys.path` and `import terminal_jail.seccomp` succeeds 3. `test_repo_layout_loader_still_works` — old layout preserved 4. `test_symlinked_loader_finds_plugin` — realpath resolution through a shim symlink 5. `test_loader_contract_command_direct_not_wrapper_flags` — passing `--user` to the loader execs a nonexistent binary (`FileNotFoundError`), pinning the "loader takes the command directly" contract **Edge cases / observations:** - **`TERMINAL_JAIL_SECCOMP=1`:** the probe dies with SIGSYS (exit 159) — but this is **pre-existing and identical on the untouched original loader** (`/usr/local/lib/terminal-jail`), i.e. a property of the hand-built BPF filter in this kernel, orthogonal to path resolution. Out of scope; the mandated regression test uses the default (seccomp-off) path, which passes cleanly. - `unshare` is `EPERM` in this container, so wrapper-level probes aren't runnable here; the problem-specified test targets the loader directly, which is what the suite does. - `install.sh` prunes `__pycache__` at copy time (runtime pyc regeneration afterwards is expected Python behavior).
{"model": "deepseek-v4-flash", "problem_class": "python-packaging-path-resolution", "result": "passed", "tests": 5}Root cause. seccomp-loader._setup_path() hard-coded repo_root = dirname(dirname(loader)), which only holds for the repo layout (<root>/standalone/seccomp-loader.py + <root>/plugin/). install.sh flattens the loader into <prefix>/lib/terminal-jail/seccomp-loader.py with the plugin tree at <prefix>/lib/terminal-jail/plugin/, so the old guess resolved to <prefix>/lib and the from terminal_jail.seccomp import … failed with ModuleNotFoundError: No module named 'terminal_jail' (reproduced before fixing).
Fix (~/terminal-jail/standalone/seccomp-loader.py): walk up from the loader's real directory until a directory containing plugin/terminal_jail/ (or a bare terminal_jail package) is found, and insert the plugin-hosting dir into sys.path. Works for both layouts, is bounded at the filesystem root, and resolves symlinks so /usr/local/bin shims still work:
def _setup_path() -> None:
"""...works for repo layout (<root>/standalone/ + <root>/plugin/)
and installed layout (<prefix>/lib/terminal-jail/ + .../plugin/)."""
def _find_plugin_root(candidate: str) -> str | None:
# Layout A/B: <candidate>/plugin/terminal_jail/__init__.py
plugin_tree = os.path.join(candidate, "plugin", "terminal_jail")
if os.path.isfile(os.path.join(plugin_tree, "__init__.py")):
return os.path.join(candidate, "plugin")
# Layout C: bare package <candidate>/terminal_jail/__init__.py
bare_pkg = os.path.join(candidate, "terminal_jail")
if os.path.isfile(os.path.join(bare_pkg, "__init__.py")):
return candidate
return None
loader_dir = os.path.dirname(os.path.realpath(__file__))
current = loader_dir
while True:
plugin_root = _find_plugin_root(current)
if plugin_root is not None:
if plugin_root not in sys.path:
sys.path.insert(0, plugin_root)
return
parent = os.path.dirname(current)
if parent == current: # reached filesystem root
break
current = parent
# Unrecognised layout: fall back to the historical guess.
repo_root = os.path.dirname(loader_dir)
plugin_dir = os.path.join(repo_root, "plugin")
if plugin_dir not in sys.path:
sys.path.insert(0, plugin_dir)
Also added ~/terminal-jail/install.sh (ships lib/terminal-jail/seccomp-loader.py, lib/terminal-jail/terminal-jail, and lib/terminal-jail/plugin/…, plus a bin/terminal-jail symlink) and the regression suite tests/test_installed_layout.py (5 tests). Per the problem note, probes invoke the loader with the command directly (echo <marker>) — never --user/--seccomp — because the bash wrapper strips its own flags before the loader sees them; the loader execs argv[1] verbatim.
**Regression reproduced (pre-fix):** simulating the installed layout and running with a stripped environment gave exactly the reported failure: ``` ModuleNotFoundError: No module named 'terminal_jail' (exit=1) ``` **Post-fix verification** — `install.sh <tmp_prefix>`, then `env -i PATH=… python3 <prefix>/lib/terminal-jail/seccomp-loader.py echo …` (strictest possible: no `PYTHONPATH`, no inherited env): - Installed layout: `installed-ok`, exit 0 (no ModuleNotFoundError, command output present) — the exact regression scenario from the problem. - Repo layout: `repo-ok`, exit 0 (no regression). - `install.sh` also works invoked from another cwd via a relative path. **Test-of-the-test:** with the old buggy `_setup_path` swapped back in, the suite fails 4/5 with the reported `ModuleNotFoundError` (the one passing test is the repo layout — precisely why the bug went unnoticed). With the fix, **5/5 pass**: 1. `test_installed_layout_loader_finds_plugin` — main regression (install into tmp_path → strip PYTHONPATH → run loader → no ModuleNotFoundError + output) 2. `test_installed_layout_setup_path_puts_plugin_dir_on_sys_path` — unit check that `_setup_path` puts `<prefix>/lib/terminal-jail/plugin` on `sys.path` and `import terminal_jail.seccomp` succeeds 3. `test_repo_layout_loader_still_works` — old layout preserved 4. `test_symlinked_loader_finds_plugin` — realpath resolution through a shim symlink 5. `test_loader_contract_command_direct_not_wrapper_flags` — passing `--user` to the loader execs a nonexistent binary (`FileNotFoundError`), pinning the "loader takes the command directly" contract **Edge cases / observations:** - **`TERMINAL_JAIL_SECCOMP=1`:** the probe dies with SIGSYS (exit 159) — but this is **pre-existing and identical on the untouched original loader** (`/usr/local/lib/terminal-jail`), i.e. a property of the hand-built BPF filter in this kernel, orthogonal to path resolution. Out of scope; the mandated regression test uses the default (seccomp-off) path, which passes cleanly. - `unshare` is `EPERM` in this container, so wrapper-level probes aren't runnable here; the problem-specified test targets the loader directly, which is what the suite does. - `install.sh` prunes `__pycache__` at copy time (runtime pyc regeneration afterwards is expected Python behavior).
{"model": "deepseek-v4-flash", "problem_class": "python-packaging-path-resolution", "result": "passed", "tests": 5}