Add Gin API sandbox example#683
Conversation
43d3721 to
83b9e77
Compare
| # Runtime flow: | ||
| # 1. cubesandbox-base entrypoint starts envd on :49983. | ||
| # 2. SDK writes Agent-generated go.mod / main.go / test files into /workspace/app. | ||
| # 3. SDK runs go mod tidy / go test / go build. |
There was a problem hiding this comment.
Documentation mismatch: The comment says the SDK runs go test, but official_sdk_gin_run.py only runs go mod tidy and go build. Either fix the comment or add go test to the script (which would also require writing _test.go files).
| # Workspace used by SDK. | ||
| # Agent-generated files will be written into /workspace/app. | ||
| RUN mkdir -p /workspace/app /workspace/go /workspace/.cache/go-build \ | ||
| && chmod -R 777 /workspace |
There was a problem hiding this comment.
Overly permissive: chmod -R 777 on the entire /workspace tree grants world-writable access to every file. Any process in the container — including the compiled Go binary or a compromised dependency — can read, modify, or delete the Go module cache, build artifacts, and source files. Consider using 755 and restricting write access to only the paths that need it.
|
|
||
| # Persist Go module proxy config inside the image. | ||
| # This does not install project dependencies; it only changes where Go downloads modules from. | ||
| RUN go env -w GOPROXY=https://goproxy.cn,direct \ |
There was a problem hiding this comment.
Consider consolidating layers. This RUN, the symlink/profile setup (line 54), and the mkdir/chmod (line 66) could all be merged into the Go install layer (line 48) to reduce image layers and one redundant go binary invocation.
|
|
||
| load_dotenv(".env", override=True) | ||
|
|
||
| template_id = os.environ["CUBE_TEMPLATE_ID"] |
There was a problem hiding this comment.
Missing env var validation: A bare KeyError is raised if CUBE_TEMPLATE_ID is unset. Since load_dotenv(.env) silently does nothing if .env doesn't exist, the error message is unhelpful. Consider using os.getenv() with a guard that explains how to set up the environment, e.g. Copy .env.example to .env and fill in the values.
| from dotenv import load_dotenv | ||
| from e2b_code_interpreter import Sandbox | ||
|
|
||
| load_dotenv(".env", override=True) |
There was a problem hiding this comment.
override=True silently overwrites existing env vars. If E2B_API_KEY is already set in the system environment (e.g., by CI/CD), the .env file value replaces it without warning. Consider using override=False (the default) so system-set variables take precedence.
| "pgrep -f '^/tmp/gin-app$' | xargs -r kill; " | ||
| "rm -f /tmp/gin.log; " | ||
| "nohup /tmp/gin-app > /tmp/gin.log 2>&1 & " | ||
| "sleep 2; " |
There was a problem hiding this comment.
Race condition: The fixed sleep 2 assumes the server will be ready within 2 seconds, which may not hold in a cold or loaded sandbox. echo started always prints regardless of whether the process actually started. Consider replacing with a polling loop: for i in $(seq 1 10); do if curl -sf localhost:8080/health >/dev/null 2>&1; then echo ready; break; fi; sleep 0.5; done
| @@ -0,0 +1,3 @@ | |||
| e2b | |||
| python-dotenv | |||
| requests No newline at end of file | |||
There was a problem hiding this comment.
Unused dependency: requests is listed but never imported or used in official_sdk_gin_run.py (the script uses curl inside the sandbox for HTTP tests). Remove it unless there's a planned use. Also consider pinning versions for reproducibility.
| --image gin-api-sandbox:latest \ | ||
| --writable-layer-size 5Gi \ | ||
| --expose-port 49983 \ | ||
| --expose-port 49999 \ |
There was a problem hiding this comment.
Port 49999 is unexplained. This is exposed and set as the health probe, but the example never starts a listener on it. The Deployment Notes say "Port 49999 is used for the template health probe" but don't explain who handles it. A reader may wonder why no service is bound to that port. Consider adding a note that CubeSandbox infrastructure handles the health probe on this port automatically.
83b9e77 to
61f3963
Compare
| } | ||
| ''' | ||
|
|
||
| with Sandbox.create(template=template_id) as sandbox: |
There was a problem hiding this comment.
Suggestion: Wrap the execution block in if __name__ == "__main__":.
Without this guard, importing any function from this file would trigger sandbox creation as a side effect. For an example that others may reference, following standard Python practice helps prevent accidental execution.
def main():
with Sandbox.create(template=template_id) as sandbox:
...
if __name__ == "__main__":
main()| print(result.stderr) | ||
|
|
||
| print("\n== write agent generated files ==") | ||
| sandbox.files.write("/workspace/app/go.mod", go_mod) |
There was a problem hiding this comment.
Suggestion: Check the return values of sandbox.files.write() calls.
If a write fails (disk full, permission denied, invalid path), the script won't detect it and will proceed to go mod tidy which will then fail with a confusing error. At minimum log the result, and ideally stop on failure.
| print(result.stdout) | ||
| print(result.stderr) | ||
|
|
||
| print("\n== test GET /health ==") |
There was a problem hiding this comment.
Suggestion: Consider using curl -sSf (adding -f/--fail) to make these tests fail on HTTP errors.
Currently -sS returns exit code 0 for any valid HTTP response including 404 or 500, so these "tests" only verify the server is reachable — not that the endpoints return correct results. The server-startup polling above (line 187) correctly uses -f, but the actual endpoint tests don't.
Also consider checking that the response body contains expected content (e.g., "status":"ok"), or adding -f and checking result.exit_code.
There was a problem hiding this comment.
Thanks for the suggestion. I updated the endpoint checks to use curl -fsS and added response body assertions so the script fails on HTTP errors or unexpected responses.
| from dotenv import load_dotenv | ||
| from e2b_code_interpreter import Sandbox | ||
|
|
||
| load_dotenv(".env", override=False) |
There was a problem hiding this comment.
Suggestion: Use __file__-based path for the .env file.
load_dotenv(os.path.join(os.path.dirname(__file__), ".env"), override=False)Relative ".env" silently fails to load if the script is invoked from a different working directory. The subsequent require_env call will then raise a RuntimeError but with a misleading message that points to copying .env.example, while the actual issue is that .env is in a different directory.
There was a problem hiding this comment.
import os
from dotenv import load_dotenv
from e2b_code_interpreter import Sandbox
script_dir = os.path.dirname(os.path.abspath(file))
load_dotenv(os.path.join(script_dir, ".env"), override=False)
def require_env(name: str) -> str:
value = os.getenv(name)
if not value:
raise RuntimeError(
f"Missing required environment variable: {name}. "
"Copy examples/gin-api-sandbox/.env.example to examples/gin-api-sandbox/.env and fill in the values, "
"or export it in your shell environment."
)
return value
| && chmod 755 /workspace \ | ||
| && chmod -R 755 /workspace/app /workspace/go /workspace/.cache/go-build \ | ||
| && go env -w GOPROXY=https://goproxy.cn,direct GOSUMDB=sum.golang.google.cn | ||
| WORKDIR /workspace/app |
There was a problem hiding this comment.
Suggestion: Add a USER directive for defense-in-depth.
The container runs as root end-to-end. If the Gin application were compromised (e.g., via a vulnerable dependency), the attacker gains root within the sandbox. Consider creating a non-root user:
RUN useradd -m -s /bin/bash ginsvc && chown -R ginsvc:ginsvc /workspace
USER ginsvcThis follows the principle of least privilege even in a sandboxed environment.
There was a problem hiding this comment.
Thanks for the suggestion. I updated the image to run as the existing non-root user from the CubeSandbox base image instead of root. I also adjusted ownership for /workspace and the required log files used by the inherited entrypoint. I verified that the template health probe starts correctly and that the SDK example can write files, run go mod tidy, build the Gin app, start the server, and pass all endpoint checks.
|
|
||
| # envd uses 49983. | ||
| # Agent-generated Gin services will usually listen on 8080. | ||
| EXPOSE 49983 8080 |
There was a problem hiding this comment.
Suggestion: Add EXPOSE 49999 with a comment explaining its role, or add a comment here noting the discrepancy.
The README's cubemastercli template command (--expose-port 49999 --probe 49999) requires port 49999 for the health probe, but the Dockerfile only lists 49983 and 8080. Someone reading only the Dockerfile won't know this port is needed for template registration.
There was a problem hiding this comment.
Thanks for pointing this out. I clarified that port 49999 is reserved for the CubeSandbox template health probe and is handled by the CubeSandbox infrastructure. I also exposed 49999 in the Dockerfile with a matching comment.
|
|
||
| ```bash | ||
| cd /workspace/app && go mod tidy | ||
| cd /workspace/app && go build -o /tmp/gin-app . |
There was a problem hiding this comment.
Suggestion: The README shows only nohup /tmp/gin-app > /tmp/gin.log 2>&1 & but the actual script (lines 179-202) uses a richer command that also kills prior processes, cleans old logs, and polls /health for up to 10 seconds before proceeding. Consider either showing the actual command or adding a note explaining that the script includes readiness-wait logic.
61f3963 to
298cea0
Compare
|
|
||
| # Install Go, configure shell environment, and prepare SDK workspace. | ||
| # This does not install project dependencies; it only configures the Go runtime. | ||
| RUN wget -O /tmp/go.tgz https://golang.google.cn/dl/go${GO_VERSION}.linux-amd64.tar.gz \ |
There was a problem hiding this comment.
Security: Go tarball downloaded without checksum verification
The Go tarball is downloaded via wget but there is no sha256sum -c verification afterwards. If the download is corrupted (network issue) or the mirror is compromised, the image will silently accept a bad/malicious Go binary.
Go publishes checksums alongside each tarball. Consider adding verification like:
RUN wget -O /tmp/go.tgz ... \
&& wget -O /tmp/go.tgz.asc ... \
&& echo "<expected-sha256> /tmp/go.tgz" | sha256sum -c - \
&& rm -rf /usr/local/go \
&& tar -C /usr/local -xzf /tmp/go.tgz
Or, when using a stable Go version, pin the expected checksum as a build arg.
There was a problem hiding this comment.
Thanks for pointing this out. I added a pinned SHA256 checksum for the Go tarball and verify it with sha256sum -c - before extracting it.
| def write_file_checked(sandbox: Sandbox, path: str, content: str) -> None: | ||
| try: | ||
| result = sandbox.files.write(path, content) | ||
| except Exception as exc: | ||
| raise RuntimeError(f"Failed to write file {path}: {exc}") from exc | ||
|
|
||
| if result is not None: | ||
| print(f"write result for {path}: {result}") | ||
|
|
||
| verify = sandbox.commands.run( | ||
| f"test -f {shlex.quote(path)}", | ||
| timeout=30, | ||
| ) | ||
| if verify.exit_code != 0: | ||
| raise RuntimeError( | ||
| f"File write verification failed for {path}\n" | ||
| f"stdout: {verify.stdout}\n" | ||
| f"stderr: {verify.stderr}" | ||
| ) |
There was a problem hiding this comment.
Code Quality: write_file_checked doubles sandbox round-trips
Each call to write_file_checked writes a file and then immediately runs a separate test -f command in the sandbox to verify it exists. This doubles the number of API calls — 6 file writes become 12 sandbox operations, adding unnecessary latency (~500ms+ each).
sandbox.files.write() already returns success/failure (or raises on error). Remove lines 29-38 or gate the verification behind a debug flag. If content integrity is a concern, consider using a content hash instead of test -f.
| def run_http_check(sandbox: Sandbox, name: str, command: str, expected: str) -> None: | ||
| result = sandbox.commands.run(command, timeout=30) | ||
| print(f"\n== test {name} ==") | ||
| print("exit_code:", result.exit_code) | ||
| print(result.stdout) | ||
| print(result.stderr) | ||
|
|
||
| if result.exit_code != 0: | ||
| raise RuntimeError( | ||
| f"HTTP check failed for {name}\n" | ||
| f"stdout: {result.stdout}\n" | ||
| f"stderr: {result.stderr}" | ||
| ) | ||
|
|
||
| if expected not in result.stdout: | ||
| raise RuntimeError( | ||
| f"Unexpected response for {name}. Expected to find {expected!r}\n" | ||
| f"stdout: {result.stdout}" | ||
| ) |
There was a problem hiding this comment.
Test Reliability: Substring JSON matching is brittle
run_http_check uses Python's in operator for substring matching against JSON responses (e.g., '"status":"ok"'). Go's json.Marshal produces compact JSON without spaces by default, but this is an implementation detail — different Go versions or Gin configurations could include whitespace, and the check would silently fail.
Additionally, a response like {"error":"'status':'ok' not found"} would falsely pass the "status":"ok" substring check since there's no JSON structure validation.
Consider parsing the response as JSON and comparing specific fields, or at minimum make the expected string matching more resilient (e.g., strip whitespace, use regex).
| result = sandbox.commands.run( | ||
| "pkill -f '^/tmp/gin-app$' >/dev/null 2>&1 || true; " | ||
| "rm -f /tmp/gin.log; " | ||
| "nohup /tmp/gin-app > /tmp/gin.log 2>&1 & " | ||
| "app_pid=$!; " | ||
| "i=0; " | ||
| "while [ $i -lt 20 ]; do " | ||
| " if curl -fsS --max-time 2 http://127.0.0.1:8080/health >/dev/null 2>&1; then " | ||
| " echo \"server ready, pid=${app_pid}\"; " | ||
| " exit 0; " | ||
| " fi; " | ||
| " if ! kill -0 ${app_pid} >/dev/null 2>&1; then " | ||
| " echo \"server process exited before becoming ready\"; " | ||
| " cat /tmp/gin.log || true; " | ||
| " exit 1; " | ||
| " fi; " | ||
| " i=$((i + 1)); " | ||
| " sleep 0.5; " | ||
| "done; " | ||
| "echo \"server failed to become ready within 10 seconds\"; " | ||
| "cat /tmp/gin.log || true; " | ||
| "exit 1", | ||
| timeout=30, | ||
| ) |
There was a problem hiding this comment.
Readability: 24-line shell one-liner is hard to read and maintain
The server startup logic packs process management (pkill, nohup), a polling loop, health check, error recovery, and log dump into a single command string. This is hard to read, hard to debug, and hard to modify.
Consider either:
- Extracting this into a small shell script file (e.g.,
start_and_wait.sh) and callingsandbox.commands.run("./start_and_wait.sh") - Splitting into separate
sandbox.commands.run()calls: start the process, then poll for readiness in Python withtime.sleep()
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends \ | ||
| ca-certificates \ | ||
| curl \ | ||
| wget \ | ||
| git \ | ||
| tar \ | ||
| procps \ | ||
| && rm -rf /var/lib/apt/lists/* |
There was a problem hiding this comment.
Reproducibility: Apt packages not version-pinned
The apt-get install command (lines 37-45) does not pin package versions. Each docker build pulls whatever version the Debian repo currently serves, making builds non-reproducible. If using the base image tag :latest, a future docker build could silently pick up different package versions — or worse, a compromised package.
Consider either:
- Pinning the base image to a specific digest rather than
:latest - Pinning apt package versions (e.g.,
curl=7.88.1-10+deb12u8)
Signed-off-by: hyy321 <1970476830@qq.com>
298cea0 to
7e0de6b
Compare
|
Updated the PR based on the review comments. Main changes:
I also re-tested the updated example successfully:
|
chenhengqi
left a comment
There was a problem hiding this comment.
Don't think we need this example.
If you want to get involved, consider helping us refactor CubeMaster with gin:
CubeSandbox/CubeMaster/pkg/service/httpservice/cube/cube.go
Lines 55 to 58 in 48d080e
|
Got it, thanks for the suggestion. I’ll take a look at CubeMaster/pkg/service/httpservice/cube/cube.go and try to refactor the existing net/http style handlers into Gin routes. I’ll open a draft PR after I understand the current routing logic. |
This PR adds a Gin API sandbox example.
It includes:
Verified locally with CubeSandbox deployment.
Related to #682