Skip to content

Commit 32c9582

Browse files
committed
sandbox: add Kotlin support to sandbox execution
- Install kotlinc with SHA256 checksum in Dockerfile - Add execute_kotlin() handler - Register Kotlin in /languages, /execute, /syntax-check, and v3-service - Add test suite with CI gating (skipif no kotlinc) - Add containerized smoke test for Kotlin - Add docs Full fix for #29
1 parent 923b833 commit 32c9582

8 files changed

Lines changed: 318 additions & 11 deletions

File tree

.github/workflows/test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,30 @@ jobs:
380380
- name: cleanup
381381
if: always()
382382
run: docker rm -f sandbox-test || true
383+
384+
kotlin-container-smoke:
385+
name: kotlin sandbox smoke (containerized)
386+
runs-on: ubuntu-latest
387+
steps:
388+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
389+
- name: build sandbox image
390+
run: docker build -t atlas-sandbox-test ./sandbox
391+
- name: start sandbox
392+
run: |
393+
docker run -d --name sandbox-test -p 8020:8020 atlas-sandbox-test
394+
sleep 5
395+
- name: smoke test /execute
396+
run: |
397+
curl -sf -X POST http://localhost:8020/execute \
398+
-H 'Content-Type: application/json' \
399+
-d '{"code":"fun main() { println(\"ok\") }","language":"kotlin"}' \
400+
| python3 -c "import sys,json; d=json.load(sys.stdin); assert d['success'], d; print('PASS: /execute')"
401+
- name: smoke test /syntax-check
402+
run: |
403+
curl -sf -X POST http://localhost:8020/syntax-check \
404+
-H 'Content-Type: application/json' \
405+
-d '{"code":"fun main() { println(\"ok\") }","language":"kotlin"}' \
406+
| python3 -c "import sys,json; d=json.load(sys.stdin); assert d['valid'], d; print('PASS: /syntax-check')"
407+
- name: cleanup
408+
if: always()
409+
run: docker rm -f sandbox-test || true

SUPPORT_MATRIX.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ timeouts/output caps; **syntax** = compile/parse check only.
149149
| Bash / sh | executed | Supported |
150150
| HTML / XML / JSON / YAML | syntax | Supported |
151151
| Java | executed | Preview (installed in default sandbox; CI smoke test containerized; host-runner tests skipif-gated) |
152-
| Kotlin / Ruby / PHP || Roadmap (will ship as separate toolchain images, not in the default sandbox) |
152+
| Kotlin | executed | Preview (installed in default sandbox; CI smoke test containerized; host-runner tests skipif-gated) |
153+
| Ruby / PHP || Roadmap (will ship as separate toolchain images, not in the default sandbox) |
153154

154155
## Feature paths
155156

docs/ARCHITECTURE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ Wait injection appends "Wait, let me reconsider.\n" to request a longer reasonin
329329

330330
**Phase 2: Verification and Selection**
331331

332-
- **Build Verification**: Python (`py_compile`), TypeScript (`tsc --noEmit`), JavaScript (`node --check`), Go (`go build`), Java (`javac`), Rust (`rustc` on the sandbox `/execute` path; `Cargo.toml` projects are detected with `cargo build`, and `cargo check` is accepted only via the build-command allowlist), C/C++ (full `gcc`/`g++` compile with `-Wall` on `/execute`; `-fsyntax-only` applies only to the `/syntax-check` route), Shell (`bash -n`). Framework overrides for Next.js, React, Flask, Django, Express.
332+
- **Build Verification**: Python (`py_compile`), TypeScript (`tsc --noEmit`), JavaScript (`node --check`), Go (`go build`), Java (`javac`), Kotlin (`kotlinc`), Rust (`rustc` on the sandbox `/execute` path; `Cargo.toml` projects are detected with `cargo build`, and `cargo check` is accepted only via the build-command allowlist), C/C++ (full `gcc`/`g++` compile with `-Wall` on `/execute`; `-fsyntax-only` applies only to the `/syntax-check` route), Shell (`bash -n`). Framework overrides for Next.js, React, Flask, Django, Express.
333333
- **S* Tiebreaking** (2+ passing): generates edge-case inputs, runs both candidates, majority wins
334334
- **Lens Selection** (1 passing or fallback): sort by C(x) energy, lowest wins
335335

@@ -535,6 +535,7 @@ graph LR
535535
TS["TypeScript\ntsc --noEmit + tsx"]
536536
Go["Go 1.22\ngo build + run"]
537537
Java["Java 21\njavac + java -cp"]
538+
Kotlin["Kotlin 2.4.0\nkotlinc + java -jar"]
538539
Rust["Rust stable\nrustc + run"]
539540
C["C / C++\ngcc/g++ -Wall"]
540541
Bash["Bash\nbash -n + run"]
@@ -550,7 +551,7 @@ graph LR
550551
style support fill:#333,color:#fff
551552
```
552553

553-
Language aliases accepted: `py`/`python3` (Python), `js`/`node` (JavaScript), `ts` (TypeScript), `golang` (Go), `java` (Java), `rs` (Rust), `c++` (C++), `sh`/`shell` (Bash). Max execution time: 300s in the Docker deployment (compose sets `MAX_EXECUTION_TIME=${ATLAS_SANDBOX_MAX_EXECUTION_TIME:-300}` to match the proxy's 5-min `run_command` cap; the bare code default is 60s). Memory, CPU, and process caps are container-level: compose sets `mem_limit ${ATLAS_SANDBOX_MEM:-4g}`, `cpus ${ATLAS_SANDBOX_CPUS:-2}`, and `pids_limit ${ATLAS_SANDBOX_PIDS:-1024}`; `atlas init` writes host-appropriate values (~75% of RAM and cores) into `.env`. Two workspace paths: **`/execute`** (V3 candidate-test path) uses an ephemeral scratch dir under `/tmp/sandbox` (tmpfs); **`/shell`** (the agent's `run_command` route, plus `/jobs/*` for background processes) runs against `/workspace` — the bind-mounted project root from `ATLAS_PROJECT_DIR` (Docker) or hostPath `${ATLAS_PROJECTS_DIR}` (K3s), the same path the proxy sees.
554+
Language aliases accepted: `py`/`python3` (Python), `js`/`node` (JavaScript), `ts` (TypeScript), `golang` (Go), `java` (Java), `kt`/`kts` (Kotlin), `rs` (Rust), `c++` (C++), `sh`/`shell` (Bash). Max execution time: 300s in the Docker deployment (compose sets `MAX_EXECUTION_TIME=${ATLAS_SANDBOX_MAX_EXECUTION_TIME:-300}` to match the proxy's 5-min `run_command` cap; the bare code default is 60s). Memory, CPU, and process caps are container-level: compose sets `mem_limit ${ATLAS_SANDBOX_MEM:-4g}`, `cpus ${ATLAS_SANDBOX_CPUS:-2}`, and `pids_limit ${ATLAS_SANDBOX_PIDS:-1024}`; `atlas init` writes host-appropriate values (~75% of RAM and cores) into `.env`. Two workspace paths: **`/execute`** (V3 candidate-test path) uses an ephemeral scratch dir under `/tmp/sandbox` (tmpfs); **`/shell`** (the agent's `run_command` route, plus `/jobs/*` for background processes) runs against `/workspace` — the bind-mounted project root from `ATLAS_PROJECT_DIR` (Docker) or hostPath `${ATLAS_PROJECTS_DIR}` (K3s), the same path the proxy sees.
554555

555556
---
556557

docs/TROUBLESHOOTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ except curses.error:
730730

731731
**Symptom:** Asking ATLAS to write an HTML/CSS/JSON file causes a ~5-minute pause with PR-CoT repair attempts and LLM timeouts. The file eventually lands via the direct-write fallback.
732732

733-
**What's happening:** The V3 smoke check is language-aware — it derives language from the target file's extension and routes to the right checker (`.py` → Python compile, `.js``node --check`, `.ts``tsc --noEmit`, `.go``gofmt -e`, `.java``javac`, `.rs``rustc`, `.sh``bash -n`, `.html``html.parser`, `.xml``ElementTree`, `.json``json.loads`, `.yaml``yaml.safe_load`). An unrecognized extension falls back to Python and fails, which cascades into repair. Note `.c`/`.cpp`/`.h` are not in the extension map (`_ext_to_lang` in `v3-service/main.py`), so C/C++ files hit the Python fallback even though the sandbox itself has C/C++ checkers.
733+
**What's happening:** The V3 smoke check is language-aware — it derives language from the target file's extension and routes to the right checker (`.py` → Python compile, `.js``node --check`, `.ts``tsc --noEmit`, `.go``gofmt -e`, `.java``javac`, `.kt``kotlinc`, `.rs``rustc`, `.sh``bash -n`, `.html``html.parser`, `.xml``ElementTree`, `.json``json.loads`, `.yaml``yaml.safe_load`). An unrecognized extension falls back to Python and fails, which cascades into repair. Note `.c`/`.cpp`/`.h` are not in the extension map (`_ext_to_lang` in `v3-service/main.py`), so C/C++ files hit the Python fallback even though the sandbox itself has C/C++ checkers.
734734

735735
If `/v3/generate` receives an approved project build command, V3 emits a `build_verify` event after syntax/self-test verification. The command runs in an ephemeral sandbox workspace with the candidate overlaid onto the project, so failed build evidence blocks `passed=true` without writing the candidate into the real checkout. Overlay snapshots skip dependency caches, secrets, model/data artifacts, symlinks, and large files, and enforce file-count and byte limits. If a project needs heavyweight dependencies to build, install them inside the sandbox workspace as part of the explicit verification workflow.
736736

@@ -861,7 +861,7 @@ docker compose logs sandbox
861861

862862
**Symptom:** Sandbox returns an error for a specific language.
863863

864-
**Supported languages (execution):** Python, JavaScript, TypeScript, Go, Java, Rust, C, C++, Bash. Syntax-only checks (`/syntax-check`, V3 smoke check) additionally cover HTML, XML, JSON, and YAML.
864+
**Supported languages (execution):** Python, JavaScript, TypeScript, Go, Java, Kotlin, Rust, C, C++, Bash. Syntax-only checks (`/syntax-check`, V3 smoke check) additionally cover HTML, XML, JSON, and YAML.
865865

866866
Check available runtimes:
867867
```bash

sandbox/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
3030
&& rm -rf /var/lib/apt/lists/*
3131
RUN java --version && javac --version
3232

33+
# Install Kotlin 2.4.0 (JVM target — requires Java, installed above)
34+
ARG KOTLIN_VERSION=2.4.0
35+
ARG KOTLIN_SHA256=ba1b9e6eb6ddc3275079224f2e9ea4a2b02eef7d59ce2d38404f04b22613c20a
36+
37+
RUN curl -fsSL -o /tmp/kotlin.zip \
38+
"https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip" \
39+
&& echo "${KOTLIN_SHA256} /tmp/kotlin.zip" | sha256sum -c - \
40+
&& unzip -q /tmp/kotlin.zip -d /opt \
41+
&& rm /tmp/kotlin.zip \
42+
&& /opt/kotlinc/bin/kotlinc -version
43+
44+
ENV PATH="/opt/kotlinc/bin:${PATH}"
45+
3346
# Install Rust (rustup, stable toolchain) under /opt so the runtime
3447
# user (non-root) can read it. RUSTUP_HOME stays /opt/rustup at runtime
3548
# (read-only toolchain); CARGO_HOME is repointed to the user's tmpfs

sandbox/executor_server.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Multi-language sandbox execution server.
33
4-
Supports: Python, JavaScript/TypeScript, Go, Java, Rust, C/C++, Bash/Shell
4+
Supports: Python, JavaScript/TypeScript, Go, Java, Kotlin, Rust, C/C++, Bash/Shell
55
Provides isolated code execution with resource limits and structured error reporting.
66
77
Security / trust model (load-bearing — read before "fixing" CodeQL alerts):
@@ -118,6 +118,7 @@ async def _correlation_id(request, call_next):
118118
"json",
119119
"yaml", "yml",
120120
"java",
121+
"kotlin", "kt", "kts",
121122
}
122123

123124
def normalize_language(lang: str) -> str:
@@ -148,6 +149,8 @@ def normalize_language(lang: str) -> str:
148149
return "yaml"
149150
if lang in ("java",):
150151
return "java"
152+
if lang in ("kotlin", "kt", "kts",):
153+
return "kotlin"
151154
return lang
152155

153156

@@ -198,6 +201,7 @@ def list_languages():
198201
"typescript": ["tsc", "--version"],
199202
"go": ["go", "version"],
200203
"java": ["javac", "--version"],
204+
"kotlin": ["kotlinc", "-version"],
201205
"rust": ["rustc", "--version"],
202206
"c": ["gcc", "--version"],
203207
"cpp": ["g++", "--version"],
@@ -759,10 +763,10 @@ def execute_code(request: ExecuteRequest):
759763
"""Execute code in isolated environment."""
760764
lang = normalize_language(request.language)
761765

762-
if lang not in ("python", "javascript", "typescript", "go", "java", "rust", "c", "cpp", "bash"):
766+
if lang not in ("python", "javascript", "typescript", "go", "java", "kotlin", "rust", "c", "cpp", "bash"):
763767
raise HTTPException(
764768
status_code=400,
765-
detail=f"Language '{request.language}' not supported. Supported: python, javascript, typescript, go, java, rust, c, cpp, bash"
769+
detail=f"Language '{request.language}' not supported. Supported: python, javascript, typescript, go, java, kotlin, rust, c, cpp, bash"
766770
)
767771

768772
workspace = tempfile.mkdtemp(dir=WORKSPACE_BASE)
@@ -967,6 +971,31 @@ def _syntax_check_impl(lang: str, code: str, workspace: Path, filename: Optional
967971
if not errors and stderr.strip():
968972
errors.append(stderr.strip().split("\n")[-1])
969973

974+
elif lang == "kotlin":
975+
fpath = workspace / (filename or "Source.kt")
976+
fpath.parent.mkdir(parents=True, exist_ok=True)
977+
fpath.write_text(code)
978+
classes_dir = workspace / "synccheck_out"
979+
classes_dir.mkdir(exist_ok=True)
980+
981+
# No syntax-only check in kotlinc. Full compile to temp
982+
# output dir is the check, same approach as Java
983+
result = _run_cmd(
984+
["kotlinc", "-d", str(classes_dir), str(fpath)],
985+
timeout=30, cwd=workspace
986+
)
987+
if result["returncode"] != 0:
988+
stderr = result.get("stderr", "")
989+
for line in stderr.splitlines():
990+
# kotlinc emits errors as either an `e:`-prefixed line or a
991+
# `file.kt:L:C: error:` line. Match both — but NOT a bare
992+
# "error" substring, which also hits `w:` warning lines that
993+
# merely mention the word (false-positive syntax failures).
994+
if line.strip().startswith("e:") or ": error:" in line:
995+
errors.append(line.strip())
996+
if not errors and stderr.strip():
997+
errors.append(stderr.strip().split("\n")[-1])
998+
970999
elif lang == "rust":
9711000
fpath = workspace / (filename or "check.rs")
9721001
fpath.write_text(code)
@@ -1348,6 +1377,40 @@ def execute_java(code, test_code, workspace, timeout, stdin=None, **_):
13481377
execution_time_ms=int((time.time() - start) * 1000),
13491378
)
13501379

1380+
# --- Kotlin ---
1381+
1382+
def execute_kotlin(code, test_code, workspace, timeout, stdin=None, **_):
1383+
start = time.time()
1384+
fpath = workspace / "Source.kt"
1385+
fpath.write_text(code)
1386+
jar_path = workspace / "output.jar"
1387+
1388+
# Compile
1389+
r = _run_cmd(
1390+
["kotlinc", "-include-runtime", "-d", str(jar_path), str(fpath)],
1391+
60, cwd=workspace
1392+
)
1393+
1394+
if not r["success"]:
1395+
return ExecuteResponse(
1396+
success=False, compile_success=False,
1397+
tests_run=0, tests_passed=0,
1398+
stdout="", stderr=r["stderr"],
1399+
error_type="CompileError", error_message=r["stderr"][:500],
1400+
execution_time_ms=int((time.time() - start) * 1000),
1401+
)
1402+
1403+
# Run
1404+
r = _run_cmd(["java", "-jar", str(jar_path)], timeout, cwd=workspace, stdin=stdin)
1405+
return ExecuteResponse(
1406+
success=r["success"], compile_success=True,
1407+
tests_run=1, tests_passed=1 if r["success"] else 0,
1408+
stdout=r["stdout"], stderr=r["stderr"],
1409+
error_type=_classify_error(r["stderr"]) if not r["success"] else None,
1410+
error_message=r["stderr"][:500] if not r["success"] else None,
1411+
execution_time_ms=int((time.time() - start) * 1000),
1412+
)
1413+
13511414
# --- Rust ---
13521415

13531416
def execute_rust(code, test_code, workspace, timeout, stdin=None, **_):
@@ -1483,6 +1546,7 @@ def execute_bash(code, test_code, workspace, timeout, stdin=None, **_):
14831546
"cpp": execute_cpp,
14841547
"bash": execute_bash,
14851548
"java": execute_java,
1549+
"kotlin": execute_kotlin,
14861550
}
14871551

14881552

0 commit comments

Comments
 (0)