forked from enderofwings/NexusOS
render_preview validates markup and hands it to the browser, which renders it in an opaque-origin sandboxed iframe. Nothing executes server-side. That model fits HTML/SVG/JSX and cannot fit C, Rust or Erlang, which need a real toolchain - so those get a second tool instead of a widened first one. The split is the feature: the model picks a track by picking a tool, rather than picking a `lang` value from an enum where half the entries run server-side and half do not. synapse/code_run.py compiles and runs one file in a throwaway directory and returns a ```nexus-run fence carrying the source and its captured output together, so a model cannot paste output without the code that produced it. Backticks in the source are re-encoded as ` - still valid JSON, and it cannot close the fence early. It is not a sandbox, and the module docstring says so up front. What it gives is containment by layers: consent (an action tool, gated by action_tool_policy, per-call Approve/Deny on "ask"), static screening, a scrubbed environment in a temp dir, wall-clock and POSIX rlimits, and a network namespace on Linux where unprivileged userns are available. Screening is a tripwire against a model reaching for `requests` out of habit, not a boundary against an adversary; layers 1 and 3-5 are the load-bearing ones. Backend RUN_LANGS and frontend run-langs.js are separate registries because the two sides need different things - one executes, one labels - and neither should depend on the other at runtime. tests/test_tools.py asserts the key sets and the fence tag stay equal, so drift fails the gate instead of rendering a run result under the wrong language. tests/snippet_probes/ is a data catalog rather than inlined cases, so adding a language is a data change and the meta-tests can assert every RUN_LANGS key has both a smoke probe and a screening probe. Probes skip cleanly on hosts without the toolchain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
309 lines
12 KiB
Python
309 lines
12 KiB
Python
"""The execution track: synapse/code_run.py.
|
|
|
|
Python is the only toolchain guaranteed present (it is the interpreter running
|
|
these tests), so it carries the behavioural coverage — limits, streams, exit
|
|
codes, isolation. The compiled languages are covered for the parts that hold
|
|
without their toolchain installed (screening, argv shape, the missing-toolchain
|
|
message) and skipped where they need it, so this file passes on a machine with
|
|
no clang and no rustc as well as on one with both.
|
|
"""
|
|
import shutil
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from synapse import code_run
|
|
|
|
|
|
def _requires(lang):
|
|
entry = code_run.RUN_LANGS[lang]
|
|
return pytest.mark.skipif(
|
|
not entry["tool"](), reason=f"no {lang} toolchain on this machine"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_every_lang_has_a_complete_driver():
|
|
for name, entry in code_run.RUN_LANGS.items():
|
|
assert entry["summary"], name
|
|
assert callable(entry["tool"]), name
|
|
assert callable(entry["source_name"]), name
|
|
assert callable(entry["run"]), name
|
|
assert callable(entry["critique"]), name
|
|
assert entry["install"], name
|
|
|
|
|
|
def test_aliases_resolve_to_real_langs():
|
|
for alias, target in code_run.ALIASES.items():
|
|
assert target in code_run.RUN_LANGS, alias
|
|
assert code_run.resolve_lang("C++") == "cpp"
|
|
assert code_run.resolve_lang(" Py ") == "python"
|
|
assert code_run.resolve_lang("javascript") == "javascript" # unknown passes through
|
|
|
|
|
|
def test_erlang_source_name_follows_the_dialect():
|
|
"""escript reads the same bytes two ways depending on the extension. Writing
|
|
a bare main/1 script to main.erl makes it a module with no exports, which
|
|
fails at load with an error that says nothing about the real problem."""
|
|
assert code_run._erlang_source_name("main(_) -> ok.") == "main.escript"
|
|
assert code_run._erlang_source_name("-module(main).\nmain(_) -> ok.") == "main.erl"
|
|
|
|
|
|
def test_erlang_escript_gets_a_shebang_when_missing():
|
|
"""OTP 28+ rejects shebang-less .escript with 'Premature end of file'."""
|
|
bare = "main(_) -> ok.\n"
|
|
out = code_run._erlang_write_source(bare)
|
|
assert out.startswith("#!/usr/bin/env escript\n")
|
|
assert out.endswith(bare)
|
|
already = "#!/usr/bin/env escript\nmain(_) -> ok.\n"
|
|
assert code_run._erlang_write_source(already) == already
|
|
module = "-module(main).\n-export([main/1]).\nmain(_) -> ok.\n"
|
|
assert code_run._erlang_write_source(module) == module
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Screening
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_clean_snippets_pass_screening():
|
|
assert code_run.critique("python", "print(sum(range(100)))") == []
|
|
assert code_run.critique("c", '#include <stdio.h>\nint main(void){puts("x");}') == []
|
|
assert code_run.critique("rust", 'fn main(){ println!("x"); }') == []
|
|
assert code_run.critique("erlang", 'main(_) -> io:format("x~n").') == []
|
|
|
|
|
|
@pytest.mark.parametrize("lang,source,needle", [
|
|
("python", "import socket\nprint(socket)", "network"),
|
|
("python", "import requests\nprint(requests)", "network"),
|
|
("python", "import subprocess\nsubprocess.run(['ls'])", "processes"),
|
|
("c", "#include <sys/socket.h>\nint main(void){return 0;}", "network"),
|
|
("c", '#include <stdlib.h>\nint main(void){system("ls");}', "processes"),
|
|
("rust", "fn main(){ std::process::Command::new(\"ls\"); }", "processes"),
|
|
("erlang", 'main(_) -> os:cmd("ls").', "processes"),
|
|
])
|
|
def test_screening_catches_the_obvious_reaches(lang, source, needle):
|
|
issues = code_run.critique(lang, source)
|
|
assert issues, f"{lang} snippet passed screening"
|
|
assert any(needle in i for i in issues), issues
|
|
|
|
|
|
def test_screening_catches_absolute_paths_and_urls():
|
|
assert any("absolute" in i for i in code_run.critique(
|
|
"python", 'data = open("/etc/passwd").read()\nprint(data)'))
|
|
# Escaped and raw Windows paths both count.
|
|
assert any("absolute" in i for i in code_run.critique(
|
|
"python", 'data = open("C:\\\\Users\\\\me").read()'))
|
|
assert any("absolute" in i for i in code_run.critique(
|
|
"python", r'data = open(r"C:\Users\me").read()'))
|
|
assert any("http" in i for i in code_run.critique(
|
|
"python", 'print("see https://example.com/data.csv")'))
|
|
|
|
|
|
def test_url_in_a_comment_is_not_a_network_reach():
|
|
"""A citation in a comment is not fetch(); bouncing it costs a useless retry."""
|
|
c = (
|
|
"/* spec: https://en.cppreference.com/w/c/string */\n"
|
|
"#include <stdio.h>\n"
|
|
"int main(void){ puts(\"ok\"); return 0; }\n"
|
|
)
|
|
assert code_run.critique("c", c) == []
|
|
|
|
|
|
def test_erlang_nproc_ceiling_is_relative_to_the_user(monkeypatch):
|
|
"""RLIMIT_NPROC is UID-scoped; an absolute 512 is not 512 of headroom."""
|
|
monkeypatch.setattr(code_run, "_user_process_count", lambda: 400)
|
|
assert code_run._max_procs_for("erlang") == 400 + code_run._ERLANG_PROC_HEADROOM
|
|
assert code_run._max_procs_for("python") == code_run._MAX_PROCS
|
|
monkeypatch.setattr(code_run, "_user_process_count", lambda: None)
|
|
assert code_run._max_procs_for("erlang") is None
|
|
|
|
|
|
def test_missing_entry_point_is_named_up_front():
|
|
"""Without this the user sees a linker error about _main, or an escript load
|
|
failure — neither of which says 'you forgot the entry point'."""
|
|
assert any("main()" in i for i in code_run.critique("c", "int add(int a){return a+1;}"))
|
|
assert any("main()" in i for i in code_run.critique("rust", "fn add(a: i32) -> i32 { a }"))
|
|
assert any("main/1" in i for i in code_run.critique("erlang", "add(A) -> A + 1."))
|
|
|
|
|
|
def test_empty_source_says_so_and_stops():
|
|
issues = code_run.critique("python", " ")
|
|
assert issues == ["source is empty or too short to run."]
|
|
|
|
|
|
def test_unknown_lang_is_refused_by_both_entry_points():
|
|
assert code_run.critique("brainfuck", "+++.")[0].startswith("cannot run")
|
|
assert code_run.run("brainfuck", "+++.")["ok"] is False
|
|
|
|
|
|
def test_oversized_source_is_refused_without_running():
|
|
issues = code_run.critique("python", "x = 1\n" * 40_000)
|
|
assert issues and "characters" in issues[0]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Execution (python: always available)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_stdout_and_exit_code_come_back():
|
|
out = code_run.run("python", "print('hello', 6 * 7)")
|
|
assert out["ok"] is True
|
|
assert out["stdout"].strip() == "hello 42"
|
|
assert out["exit_code"] == 0
|
|
|
|
|
|
def test_streams_are_kept_separate():
|
|
out = code_run.run("python", "import sys\nprint('o')\nprint('e', file=sys.stderr)")
|
|
assert out["stdout"].strip() == "o"
|
|
assert out["stderr"].strip() == "e"
|
|
|
|
|
|
def test_nonzero_exit_is_still_a_successful_run():
|
|
out = code_run.run("python", "raise SystemExit(3)")
|
|
assert out["ok"] is True
|
|
assert out["exit_code"] == 3
|
|
|
|
|
|
def test_a_traceback_is_returned_not_raised():
|
|
out = code_run.run("python", "print(1/0)")
|
|
assert out["ok"] is True
|
|
assert out["exit_code"] != 0
|
|
assert "ZeroDivisionError" in out["stderr"]
|
|
|
|
|
|
def test_stdin_is_piped_in():
|
|
out = code_run.run("python", "import sys\nprint(sys.stdin.read().upper())", stdin="abc")
|
|
assert out["stdout"].strip() == "ABC"
|
|
|
|
|
|
def test_a_program_reading_stdin_with_none_given_does_not_hang():
|
|
"""stdin is always closed rather than inherited: a snippet calling input()
|
|
with nothing piped in would otherwise block on the server's own stdin until
|
|
the wall clock killed it, five seconds of 'thinking' for nothing."""
|
|
out = code_run.run("python", "print(input('prompt: '))")
|
|
assert out["ok"] is True
|
|
assert "EOFError" in out["stderr"]
|
|
|
|
|
|
def test_an_infinite_loop_is_killed_and_explained():
|
|
out = code_run.run("python", "while True:\n pass")
|
|
assert out["ok"] is False
|
|
assert out["stage"] == "run"
|
|
assert "did not finish" in out["error"]
|
|
|
|
|
|
def test_output_is_truncated_not_streamed_whole():
|
|
out = code_run.run("python", f"print('x' * {code_run.MAX_OUTPUT * 3})")
|
|
assert len(out["stdout"]) < code_run.MAX_OUTPUT + 200
|
|
assert "truncated" in out["stdout"]
|
|
|
|
|
|
def test_the_working_directory_is_ephemeral():
|
|
"""Two runs must not see each other's files. The temp dir is the only thing
|
|
standing between a snippet and the user's cwd, so its lifetime is worth a
|
|
test rather than an assumption."""
|
|
first = code_run.run("python", "open('note.txt', 'w').write('hi')\nprint('wrote')")
|
|
assert first["ok"] is True, first
|
|
second = code_run.run("python", "import os\nprint(os.path.exists('note.txt'))")
|
|
assert second["stdout"].strip() == "False"
|
|
|
|
|
|
def test_the_child_does_not_inherit_the_servers_environment():
|
|
"""A snippet has no business reading the process environment it happens to
|
|
be spawned from — that is where an API key would be."""
|
|
import os
|
|
os.environ["NEXUS_RUN_LEAK_PROBE"] = "secret"
|
|
try:
|
|
out = code_run.run(
|
|
"python", "import os\nprint(os.environ.get('NEXUS_RUN_LEAK_PROBE'))"
|
|
)
|
|
finally:
|
|
os.environ.pop("NEXUS_RUN_LEAK_PROBE", None)
|
|
assert out["stdout"].strip() == "None"
|
|
|
|
|
|
def test_home_points_at_the_scratch_dir():
|
|
"""HOME is rewritten so a snippet writing a dotfile writes it somewhere that
|
|
gets deleted, rather than into the user's real home."""
|
|
out = code_run.run("python", "import os\nprint(os.path.expanduser('~'))")
|
|
assert "nexus-run-" in out["stdout"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Toolchains
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_a_missing_toolchain_explains_itself(monkeypatch):
|
|
"""The model has to be able to tell 'you cannot run this here' from 'your
|
|
code is wrong' — otherwise it rewrites a correct program repeatedly."""
|
|
monkeypatch.setitem(
|
|
code_run.RUN_LANGS["rust"], "tool", lambda: None
|
|
)
|
|
out = code_run.run("rust", 'fn main(){ println!("x"); }')
|
|
assert out["ok"] is False
|
|
assert out["stage"] == "toolchain"
|
|
assert "rustup.rs" in out["error"]
|
|
assert "Show the code instead" in out["error"]
|
|
|
|
|
|
@_requires("c")
|
|
def test_c_compiles_and_runs():
|
|
out = code_run.run("c", '#include <stdio.h>\nint main(void){printf("%d\\n", 6*7);return 0;}')
|
|
assert out["ok"] is True, out
|
|
assert out["stdout"].strip() == "42"
|
|
|
|
|
|
@_requires("c")
|
|
def test_a_compile_error_comes_back_as_a_compile_error():
|
|
"""Stage matters: the compiler's diagnostics are the useful payload, and
|
|
labelling this a run failure would hide that nothing ever executed."""
|
|
out = code_run.run("c", "int main(void){ return oops; }")
|
|
assert out["ok"] is False
|
|
assert out["stage"] == "compile"
|
|
assert "oops" in out["stderr"]
|
|
|
|
|
|
@_requires("cpp")
|
|
def test_cpp_compiles_and_runs():
|
|
out = code_run.run("cpp", '#include <iostream>\nint main(){std::cout << 6*7 << "\\n";}')
|
|
assert out["ok"] is True, out
|
|
assert out["stdout"].strip() == "42"
|
|
|
|
|
|
@_requires("rust")
|
|
def test_rust_compiles_and_runs():
|
|
out = code_run.run("rust", 'fn main(){ println!("{}", (1..=10).sum::<i32>()); }')
|
|
assert out["ok"] is True, out
|
|
assert out["stdout"].strip() == "55"
|
|
|
|
|
|
@_requires("erlang")
|
|
def test_erlang_runs_a_bare_escript():
|
|
out = code_run.run("erlang", 'main(_) -> io:format("~p~n", [lists:sum(lists:seq(1,10))]).')
|
|
assert out["ok"] is True, out
|
|
assert out["stdout"].strip() == "55"
|
|
|
|
|
|
@_requires("erlang")
|
|
def test_erlang_runs_a_module_form_script():
|
|
out = code_run.run(
|
|
"erlang",
|
|
"-module(main).\n-export([main/1]).\nmain(_) -> io:format(\"~p~n\", [7*6]).",
|
|
)
|
|
assert out["ok"] is True, out
|
|
assert out["stdout"].strip() == "42"
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="unshare is Linux-only")
|
|
def test_network_isolation_probe_is_honest():
|
|
"""Either we got a namespace or we did not — but the probe must never claim
|
|
one it did not verify, because the tool description promises 'no network' on
|
|
the strength of it."""
|
|
prefix = code_run._net_isolation()
|
|
assert prefix in ([], ["unshare", "-rn"])
|
|
if prefix:
|
|
assert shutil.which("unshare")
|