practicode
Advanced tools
| .git | ||
| .practicode | ||
| .pytest_cache | ||
| .superpowers | ||
| .venv | ||
| node_modules | ||
| problems | ||
| submissions | ||
| target |
+22
| FROM node:24-bookworm-slim AS node | ||
| FROM rust:1-bookworm | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends ca-certificates python3 default-jdk-headless \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| COPY --from=node /usr/local/bin/node /usr/local/bin/node | ||
| WORKDIR /opt/practicode | ||
| COPY Cargo.toml Cargo.lock ./ | ||
| COPY src ./src | ||
| COPY assets ./assets | ||
| RUN cargo build --release --locked \ | ||
| && install -m 0755 target/release/practicode /usr/local/bin/practicode \ | ||
| && rm -rf target /usr/local/cargo/registry /usr/local/cargo/git | ||
| ENV HOME=/tmp | ||
| ENV PATH=/usr/local/cargo/bin:$PATH | ||
| ENV PRACTICODE_NO_UPDATE_CHECK=1 | ||
| WORKDIR /workspace | ||
| ENTRYPOINT ["practicode"] |
+102
-1
@@ -8,2 +8,4 @@ #!/usr/bin/env node | ||
| const root = path.resolve(__dirname, ".."); | ||
| const packageJson = require(path.join(root, "package.json")); | ||
| const dockerImage = `practicode-sandbox:${packageJson.version}`; | ||
| const exe = path.join( | ||
@@ -15,2 +17,3 @@ root, | ||
| ); | ||
| const args = process.argv.slice(2); | ||
@@ -34,2 +37,100 @@ function rustInstallCommand() { | ||
| function dockerInstallCommand() { | ||
| if (process.platform === "win32") { | ||
| return "winget install -e --id Docker.DockerDesktop"; | ||
| } | ||
| if (process.platform === "darwin") { | ||
| return "brew install --cask docker"; | ||
| } | ||
| return "Ubuntu: https://docs.docker.com/engine/install/ubuntu/ | Debian: https://docs.docker.com/engine/install/debian/"; | ||
| } | ||
| function printDockerHelp() { | ||
| console.error("practicode: Docker is required for --docker sandbox mode."); | ||
| console.error(`Install Docker: ${dockerInstallCommand()}`); | ||
| console.error("Start Docker, then run practicode --docker again."); | ||
| console.error("More options: https://docs.docker.com/get-docker/"); | ||
| } | ||
| function runDockerSandbox(forwardedArgs) { | ||
| const version = spawnSync("docker", ["version"], { stdio: "ignore" }); | ||
| if (version.error || version.status !== 0) { | ||
| if (version.error) { | ||
| console.error(`practicode: failed to run docker: ${version.error.message}`); | ||
| } | ||
| printDockerHelp(); | ||
| process.exit(1); | ||
| } | ||
| const build = spawnSync("docker", ["build", "-t", dockerImage, root], { | ||
| stdio: "inherit", | ||
| }); | ||
| if (build.error) { | ||
| console.error(`practicode: failed to build Docker sandbox: ${build.error.message}`); | ||
| printDockerHelp(); | ||
| process.exit(1); | ||
| } | ||
| if (build.status !== 0) { | ||
| console.error("practicode: Docker sandbox build failed."); | ||
| printDockerHelp(); | ||
| process.exit(build.status ?? 1); | ||
| } | ||
| const runArgs = [ | ||
| "run", | ||
| "--rm", | ||
| process.stdin.isTTY ? "-it" : "-i", | ||
| "--init", | ||
| "--network", | ||
| "none", | ||
| "--cpus", | ||
| "2", | ||
| "--memory", | ||
| "1g", | ||
| "--pids-limit", | ||
| "256", | ||
| "--cap-drop", | ||
| "ALL", | ||
| "--security-opt", | ||
| "no-new-privileges", | ||
| "--read-only", | ||
| "--tmpfs", | ||
| "/tmp:rw,nosuid,nodev,size=256m,mode=1777", | ||
| "--mount", | ||
| `type=bind,source=${process.cwd()},target=/workspace`, | ||
| "-w", | ||
| "/workspace", | ||
| "-e", | ||
| `TERM=${process.env.TERM || "xterm-256color"}`, | ||
| "-e", | ||
| "HOME=/tmp", | ||
| "-e", | ||
| "PRACTICODE_NO_UPDATE_CHECK=1", | ||
| ]; | ||
| if (process.env.COLORTERM) { | ||
| runArgs.push("-e", `COLORTERM=${process.env.COLORTERM}`); | ||
| } | ||
| if (typeof process.getuid === "function" && typeof process.getgid === "function") { | ||
| runArgs.push("--user", `${process.getuid()}:${process.getgid()}`); | ||
| } | ||
| runArgs.push(dockerImage, ...forwardedArgs); | ||
| const run = spawnSync("docker", runArgs, { | ||
| cwd: process.cwd(), | ||
| stdio: "inherit", | ||
| }); | ||
| if (run.error) { | ||
| console.error(`practicode: failed to run Docker sandbox: ${run.error.message}`); | ||
| printDockerHelp(); | ||
| process.exit(1); | ||
| } | ||
| process.exit(run.status ?? 1); | ||
| } | ||
| const dockerIndex = args.indexOf("--docker"); | ||
| if (dockerIndex !== -1) { | ||
| args.splice(dockerIndex, 1); | ||
| runDockerSandbox(args); | ||
| } | ||
| if (!existsSync(exe)) { | ||
@@ -52,3 +153,3 @@ const build = spawnSync( | ||
| const run = spawnSync(exe, process.argv.slice(2), { | ||
| const run = spawnSync(exe, args, { | ||
| cwd: process.cwd(), | ||
@@ -55,0 +156,0 @@ stdio: "inherit", |
+1
-1
@@ -456,3 +456,3 @@ # This file is automatically @generated by Cargo. | ||
| name = "practicode" | ||
| version = "0.1.17" | ||
| version = "0.1.18" | ||
| dependencies = [ | ||
@@ -459,0 +459,0 @@ "anyhow", |
+1
-1
| [package] | ||
| name = "practicode" | ||
| version = "0.1.17" | ||
| version = "0.1.18" | ||
| edition = "2024" | ||
@@ -5,0 +5,0 @@ description = "Local-first coding-test practice in a Rust terminal UI with optional AI help." |
+3
-1
| { | ||
| "name": "practicode", | ||
| "version": "0.1.17", | ||
| "version": "0.1.18", | ||
| "description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.", | ||
@@ -30,2 +30,4 @@ "license": "MIT", | ||
| "Cargo.toml", | ||
| "Dockerfile", | ||
| ".dockerignore", | ||
| "LICENSE", | ||
@@ -32,0 +34,0 @@ "README.md", |
+32
-0
@@ -129,2 +129,33 @@ # practicode | ||
| ### Docker sandbox | ||
| If you do not want submissions to run directly on your host, use the npm launcher sandbox: | ||
| ```bash | ||
| practicode --docker | ||
| ``` | ||
| The launcher builds a local `practicode-sandbox:<version>` image, then runs the TUI in Docker with the current directory mounted at `/workspace`. The container runs without network access, with a read-only root filesystem, a writable `/tmp`, dropped Linux capabilities, `no-new-privileges`, and CPU/memory/process limits. Your current directory is still writable so `.practicode/`, `problems/`, and `submissions/` can be saved. | ||
| Install Docker first if needed: | ||
| ```bash | ||
| # macOS | ||
| brew install --cask docker | ||
| # Windows | ||
| winget install -e --id Docker.DockerDesktop | ||
| # Ubuntu / Debian | ||
| # Use Docker's official apt repository: | ||
| # Ubuntu: https://docs.docker.com/engine/install/ubuntu/ | ||
| # Debian: https://docs.docker.com/engine/install/debian/ | ||
| ``` | ||
| After starting Docker, check the sandbox: | ||
| ```bash | ||
| practicode --docker --smoke | ||
| ``` | ||
| ## Update | ||
@@ -206,2 +237,3 @@ | ||
| - `/run` executes your local submission as a normal process. It is not an OS sandbox. | ||
| - `practicode --docker` runs the TUI and judge in a restricted Docker container, but Docker is still a shared-kernel container runtime, not a guarantee against every escape. | ||
| - `/run` scrubs inherited environment variables and hides case input/expected output in failure logs. | ||
@@ -208,0 +240,0 @@ - `/hint`, AI-backed `/next`, and `/generate` send the current problem/submission context to the selected provider CLI. |
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1265033
0.38%76
2.7%9387
1.01%262
13.91%6
200%