You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Token on every route, a safer browser tool, a real health check, tests in CI - #37
Makes the sandbox that is on main today safe to leave running and simpler to trust, without changing what it does.
Includes #26 (resource limits and workspace mounts) as its base; merge that first or this one, either order works. Supersedes #27, whose server changes are already on main; its installer hint is here.
What changes
Every MCP and REST route needs Authorization: Bearer <token> (only GET /health is open). The installer generates the token once into ~/.coderunner/api-token, passes it to the container by name, and refuses an image that ignores it. Docker instructions and Compose publish the port on 127.0.0.1 only. Until now anything that could reach port 8222 could run code in the sandbox; with Docker that was the LAN.
The browser tool opens only public http(s) URLs, with a timeout, and always closes the browser. From inside the sandbox, loopback and private addresses reach the Jupyter server, the host gateway and the LAN.
Code sent to a kernel is never sent a second time by the retry loop; only "could not reach the kernel" is retried.
/health answers 503 when the Jupyter server behind the kernels is down, so the installer and the container healthcheck mean something.
Skill archives are bounded to 200 MB unpacked and a bad one is set aside instead of failing every list_skills call.
Unit tests (tests/) and a Tests workflow (ruff, shellcheck, unittest) on every pull request, installing the same requirements.txt the image uses.
Usability: the installer no longer downgrades a newer Apple container to its pin (1.5.0 would have become 1.2.2), refuses to run under sudo (which put everything under /var/root), and prints the claude mcp add command with the token when the sandbox is ready. The README's client snippets carry the header; the Claude Code plugin predates the token and needs an update of its own.
How it was verified
Image built from this branch with Apple container 1.5.0 on macOS 26 and run as a throwaway container: /health reports the Jupyter server, an anonymous request gets 401 and the token 200, test-e2e.sh passes 11 of 11 (the wrong-token check is new), the browser tool refuses http://127.0.0.1:8888 and file:///etc/passwd, and test-sessions.py passes.
macOS ships bash 3.2, where expanding an empty array under 'set -u' is a
fatal 'unbound variable' error. MOUNT_SPECS, NORMALIZED_MOUNTS and
VOLUME_ARGS are all empty when no --mount is passed, which breaks once
this branch is combined with the 'set -u' added in #24.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The server in the sandbox had no authentication: anything that could reach
port 8222 (every process on the Mac; the LAN, when Docker published the port
on all interfaces) could run code in it. Every route except GET /health now
needs Authorization: Bearer <token>; a missing or weak server token fails
closed with 503.
The installer generates the token once into ~/.coderunner/api-token (0600),
or saves the CODERUNNER_API_TOKEN you export, passes it to the container by
name, and checks that anonymous requests are refused and the token is accepted
before reporting the sandbox ready. Docker Compose and the Docker instructions
publish the port on 127.0.0.1 only. The examples, the Claude Desktop proxy and
test-sessions.py send the header.
Verify: tests/test_auth.py; ./install.sh then curl without and with the header.
- The browser tool opens only http and https URLs whose host resolves to a
public address: from inside the sandbox, loopback, link-local and private
addresses reach the Jupyter server, the host gateway and the LAN. Pages
load with a 30 s timeout, the browser is always closed, and the text is
cut at 200,000 characters.
- Code that was sent to a kernel is never sent again. The retry loop only
moves to another kernel when the first could not be reached; a connection
lost during a run, or a run that timed out, is reported as an error, since
the code may have had side effects.
- /health answers 503 unless the Jupyter server that runs the kernels
answers too, so the installer and the container healthcheck see a sandbox
whose kernels cannot start.
- Skill archives are bounded to 200 MB unpacked, and a bad one is set aside
as .zip.bad instead of failing every list_skills call.
- The shared directory and the Jupyter URL come from the environment, so
server.py imports outside the image; tests/test_server.py covers the
request validation, the URL guard, skill archives, session ids and the
health check, and tests/test_auth.py the token. A Tests workflow installs
requirements.txt and runs them with ruff and shellcheck on every pull
request.
- The Claude Desktop proxy and the OpenAI example read the token from
~/.coderunner/api-token when CODERUNNER_API_TOKEN is not set; test-e2e.sh
sends it and checks that a wrong one is refused.
- README: when the sandbox helps with Claude Code (#13), and what to check
when Claude Desktop shows the tool as disabled (#7).
- The installer replaced any installed Apple container whose version was not
its pin, which on a Mac with 1.5.0 meant a downgrade to 1.2.2 with sudo.
It now installs only when the tool is missing or older than the pin.
- Run as root, the installer put the token, the venv and the assets under
/var/root where the clients run as the user never find them. It refuses to
run as root; the steps that need it ask for sudo themselves. The README
and the skills guide no longer say sudo ./install.sh.
- When the sandbox is ready the installer prints the claude mcp add command
with the token, and the README's Claude Code, Gemini CLI and OpenCode
snippets carry the header. The Claude Code plugin predates the token and
the README says so until it is updated.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unresolved critical flaws permit private-network browser access, duplicate execution through unsafe retries, and an unauthenticated code-execution service remaining exposed after installer failure.
This PR hardens CodeRunner’s sandbox interfaces and installation flow, building on the resource and workspace controls from #26. It also adds automated checks and updates client setup guidance.
Changes:
Adds bearer authentication across MCP and REST routes, token-aware clients, and loopback-only Docker port publishing.
Adds browser URL checks, safer execution retries, Jupyter-backed health checks, and skill archive limits.
Adds unit tests and CI checks, plus installer improvements and Claude integration documentation.
File
Description
tests/test_server.py
Tests URL validation, archives, sessions, and health.
tests/test_auth.py
Tests authentication and fail-closed behavior.
test-sessions.py
Authenticates session test requests.
test-e2e.sh
Adds authenticated requests and wrong-token checks.
SKILLS-README.md
Removes sudo from installation instructions.
server.py
Integrates authentication and runtime safeguards.
README.md
Documents credentials, resource controls, and client setup.
install.sh
Manages tokens, resource settings, mounts, and readiness checks.
…box that ignores the token
- The browser tool checks each request the page makes (redirects, frames,
subresources) the way it checks the first URL, through a Playwright route;
the address test uses is_global, which also refuses the shared 100.64/10
space, and the DNS lookup runs in a thread with a ten-second bound instead
of on the event loop. A name that changes its address between the check
and the browser's own resolution is not caught, and the code says so.
- A send that raises after the request may have reached the kernel; it now
counts as sent, so only failures before the send are retried.
- Any failure to unpack a skill archive sets it aside, including the
RuntimeError a password-protected zip raises.
- When the running image ignores the token, the installer stops the sandbox
before reporting the failure instead of leaving it reachable.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Critical bypasses through redirects, DNS rebinding, and unguarded browser traffic still allow access to private services, including unauthenticated local servers.
umask only restricts newly created files; it does not change permissions when this redirection overwrites an existing token file. Running ensure_token with a pre-existing mode-0644 file leaves it at 0644, exposing the code-execution credential to other local accounts when the parent directory is traversable. Enforce mode 0600 before writing, and fail if either securing or saving the credential fails.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the sandbox that is on main today safe to leave running and simpler to trust, without changing what it does.
Includes #26 (resource limits and workspace mounts) as its base; merge that first or this one, either order works. Supersedes #27, whose server changes are already on main; its installer hint is here.
What changes
Authorization: Bearer <token>(onlyGET /healthis open). The installer generates the token once into~/.coderunner/api-token, passes it to the container by name, and refuses an image that ignores it. Docker instructions and Compose publish the port on 127.0.0.1 only. Until now anything that could reach port 8222 could run code in the sandbox; with Docker that was the LAN./healthanswers 503 when the Jupyter server behind the kernels is down, so the installer and the container healthcheck mean something.list_skillscall.tests/) and a Tests workflow (ruff, shellcheck, unittest) on every pull request, installing the samerequirements.txtthe image uses.containerto its pin (1.5.0 would have become 1.2.2), refuses to run under sudo (which put everything under /var/root), and prints theclaude mcp addcommand with the token when the sandbox is ready. The README's client snippets carry the header; the Claude Code plugin predates the token and needs an update of its own.How it was verified
/healthreports the Jupyter server, an anonymous request gets 401 and the token 200,test-e2e.shpasses 11 of 11 (the wrong-token check is new), the browser tool refuseshttp://127.0.0.1:8888andfile:///etc/passwd, andtest-sessions.pypasses.python -m unittest discover -s tests(10 tests), ruff and shellcheck clean.Published images predate the token and are refused by the installer; build from this checkout until a new image is published.