docs: add Claude Code + CubeSandbox integration guide and example#694
docs: add Claude Code + CubeSandbox integration guide and example#694LangQi99 wants to merge 3 commits into
Conversation
Adds an end-to-end integration for running Anthropic Claude Code — the terminal-native AI coding agent — inside CubeSandbox MicroVMs, addressing part of TencentCloud#644 (Claude Code sub-direction). - examples/claude-code-integration/: runnable project with a cubesandbox-base + Node 20 + @anthropic-ai/claude-code Dockerfile, headless one-shot demo (run_claude.py), pause/resume across turns (resume_claude.py), and CubeEgress inject rule so the API key never enters the sandbox (network_policy.py). English + Chinese README. - docs/guide/integrations/claude-code.md and its zh counterpart: full integration guide covering template build, direct-vs-vault key flows, snapshot-based long-task resume, and troubleshooting; both language index pages updated. Verified locally: docker build succeeds; envd /health returns 204; claude --version reports 2.1.197; every CLI flag used in the scripts (--print, --allowedTools, --verbose, --output-format stream-json) is supported by the current CLI; all Python scripts compile with py_compile.
| "destroying it. The sandbox_id is printed on exit.", | ||
| ) | ||
| p.add_argument( | ||
| "--verbose", |
There was a problem hiding this comment.
Suggestion (rename flag): The argparse flag --verbose actually enables --output-format stream-json, transforming the output into machine-readable JSON events. These are semantically different concerns — verbosity and output format. A user reaching for --verbose to debug something will get JSON they may not expect.
Consider renaming to --stream-json to accurately describe what it does, or decouple the two so each can be set independently:
| key from this dict entirely and let CubeEgress attach it on the wire. | ||
| """ | ||
| envs: dict[str, str] = {} | ||
| for name in ( |
There was a problem hiding this comment.
Undocumented env vars risk: build_agent_env() silently forwards CLAUDE_CODE_USE_BEDROCK and CLAUDE_CODE_USE_VERTEX into the sandbox, but neither the integration guide nor the README environment variable tables documents these. A user pointing ANTHROPIC_BASE_URL at a Bedrock endpoint without also knowing to set CLAUDE_CODE_USE_BEDROCK=true will get an authentication failure from Claude Code — the CLI uses different auth logic for Bedrock vs direct Anthropic. Same applies to Vertex.
Please add these to the env variable tables in both the guide (§3) and the README (§2), noting which gateway each enables.
| envs = build_agent_env() | ||
|
|
||
| print(f"[cube] creating sandbox from template={template_id}") | ||
| sandbox = Sandbox.create(template=template_id, timeout=1800) |
There was a problem hiding this comment.
Sandbox leak risk: If Sandbox.create() succeeds but accessing .sandbox_id raises (e.g., transient API error), sandbox is assigned but code never enters the try block, so finally cleanup never runs. Move the .sandbox_id access inside the try block to guarantee cleanup.
| print("\n=== rationale.md ===") | ||
| print(rationale.stdout) | ||
|
|
||
| return 0 |
There was a problem hiding this comment.
Exit code not propagated: main() always returns 0, but the two run_claude() calls at lines 78 and 92 can fail. The exit codes are printed to stdout but never captured or checked. A user running python resume_claude.py && echo "ok" would see "ok" even when the agent task failed. Consider tracking accumulated failures and returning a non-zero exit code when any invocation fails.
| && curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \ | ||
| && apt-get install -y --no-install-recommends nodejs \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ |
There was a problem hiding this comment.
npm cache bloat: npm install -g downloads and caches the package tarball in /root/.npm/. This cache is never cleaned, permanently inflating the image by ~10-30 MB. Since apt-get cleanup (rm -rf /var/lib/apt/lists/*) is already on line 33 in the same RUN chain, adding npm cache clean --force after claude --version would eliminate the bloat without adding a layer.
| @@ -0,0 +1,24 @@ | |||
| # Required: Cube API server address | |||
There was a problem hiding this comment.
Security: missing .gitignore for .env in the example directory. The env.example instructs users to copy it to .env and fill in secrets (API keys, template IDs, cluster URLs). Without a .gitignore in examples/claude-code-integration/, git status will show the .env file and it could be accidentally committed. Consider adding a .gitignore with .env in this directory as a safety measure for users who fork/clone the repo.
Code review: PR #694 — Claude Code + CubeSandbox integrationOverall impression: a well-structured, comprehensive PR. The code quality is high, the documentation is thorough and consistent across both languages, and the security-sensitive patterns (shlex.quote, credential vault, per-exec env forwarding) are handled correctly. Below are the findings I consider noteworthy after reviewing the reviewer reports. Noteworthy issues1. Sandbox resource leak with When 2. Non-reproducible Docker builds (
3. Doc snippet doesn't match version table's The version table advertises version pinning via 4. The docstring says the key "only lives in the operator's rule list", but the rule list (containing the key) is transmitted to and stored by the CubeAPI control plane as part of 5. These variables are documented in Minor / optional suggestions
Review performed by automated reviewer agents (code-quality, security, documentation-accuracy). Findings synthesized and assessed by the lead reviewer. |
Applies six actionable review comments from cubesandboxbot on TencentCloud#694: - run_claude.py: rename `--verbose` to `--stream-json` so the flag name actually describes what it does (enable stream-json output). Verbosity and output format are now decoupled semantically. - env_utils.py + docs + env.example: document `CLAUDE_CODE_USE_BEDROCK` and `CLAUDE_CODE_USE_VERTEX`. Both are silently forwarded by the helper, and users pointing `ANTHROPIC_BASE_URL` at a Bedrock/Vertex endpoint need to know these must also be set. Added to env var tables in the bilingual guides and READMEs, with mutual-exclusion note. - resume_claude.py: move `.sandbox_id` access inside the try block so a transient error there can't leak the sandbox past `finally`. Track per-turn agent failures and propagate a non-zero exit code to callers. - Dockerfile: `npm cache clean --force` + `rm -rf /root/.npm` after the global install. Local rebuild dropped image size from 1.33 GB to 287 MB. - examples/claude-code-integration/.gitignore: ignore .env, __pycache__, and *.pyc so a user who copies env.example -> .env cannot accidentally commit their API key. Verified locally: docker rebuild succeeds; envd /health still 204; claude --version still 2.1.197; cache is gone from /root/.npm/_cacache; all four Python scripts still compile.
|
Thanks for the review — pushed
Local re-verification after the changes:
The end-to-end run against a live cluster + real Anthropic key still needs a maintainer with cluster access, since I don't have one on hand. |
|
|
||
| envs = build_agent_env() | ||
| print(f"[cube] creating sandbox from template={template_id}") | ||
| with Sandbox.create(template=template_id, timeout=args.timeout) as sandbox: |
There was a problem hiding this comment.
Bug: --pause flag is defeated by the context manager
When --pause is used, the sandbox is paused on line 171, but then the with block exits, and the context manager's __exit__ almost certainly calls kill() — destroying the just-paused sandbox. The --pause flag is effectively a no-op.
Compare with resume_claude.py, which correctly manages the sandbox lifecycle via try/finally (no context manager) precisely so the sandbox can outlive the lexical scope.
Recommendation: Either remove the --pause flag, or manage the sandbox lifecycle manually (like resume_claude.py) when pause is active:
sandbox = Sandbox.create(template=template_id, timeout=args.timeout)
try:
...
if args.pause:
sandbox.pause()
finally:
if not args.pause:
sandbox.kill()| on_stderr=lambda m: sys.stderr.write(m), | ||
| ) | ||
| print(f"\n[claude] exit_code={result.exit_code}") | ||
| return result.exit_code or 0 |
There was a problem hiding this comment.
Bug: result.exit_code or 0 conflates None with 0
If the E2B SDK returns None for exit_code (which some command-execution APIs do in error scenarios), None or 0 evaluates to 0, and the caller treats it as success. The failures counter on lines 82/96 won't increment, masking a real failure.
Recommendation: Use this instead to distinguish "exit code 0 (success)" from "no exit code returned (error)":
return result.exit_code if result.exit_code is not None else 1| print(f"[preflight] {result.stdout.strip()}") | ||
|
|
||
|
|
||
| def upload_seed(sandbox: Sandbox, workspace: str, seed: str) -> None: |
There was a problem hiding this comment.
Security: --seed can upload arbitrary local files into the sandbox
The upload_seed function reads any local file the user specifies and writes it into the sandbox where the Claude Code agent can read and potentially exfiltrate it (by including file contents in its API calls to api.anthropic.com). While the destination safely uses only seed_path.name (preventing remote path traversal), there is no source-path restriction. A user could inadvertently upload SSH keys, cloud credentials, or other sensitive files.
Recommendation: Either add a warning in the --seed help text like:
"WARNING: This file will be readable by the agent inside the sandbox. "
"Do not point this at credentials, SSH keys, or other secrets."
or validate that the file is within an expected project directory.
| envs = build_agent_env() | ||
|
|
||
| print(f"[cube] creating sandbox from template={template_id}") | ||
| sandbox = Sandbox.create(template=template_id, timeout=1800) |
There was a problem hiding this comment.
Cleanup risk: Sandbox.create() outside a context manager
Unlike run_claude.py, this script uses a bare Sandbox.create() call. If Sandbox.create() itself throws, sandbox is never assigned, causing a NameError in the finally block that silently eats the original exception. Additionally, if Sandbox.connect(sid) fails on line 95, sandbox still references the paused sandbox, and calling .kill() on it may not properly release resources.
Recommendation: Guard the finally block with a check that sandbox was assigned before calling .kill():
finally:
if 'sandbox' in locals() and sandbox:
try:
sandbox.kill()
except Exception as exc:
print(f"[cleanup] sandbox.kill() raised {exc!r}")|
|
||
| envs = build_agent_env() | ||
| print(f"[cube] creating sandbox from template={template_id}") | ||
| with Sandbox.create(template=template_id, timeout=args.timeout) as sandbox: |
There was a problem hiding this comment.
Bug: --pause flag is defeated by the context manager
When --pause is used, the sandbox is paused on line 171, but then the with block exits, and the context manager's __exit__ almost certainly calls kill() — destroying the just-paused sandbox. The --pause flag is effectively a no-op.
Compare with resume_claude.py, which correctly manages the sandbox lifecycle via try/finally (no context manager) precisely so the sandbox can outlive the lexical scope.
Recommendation: Either remove the --pause flag, or manage the sandbox lifecycle manually (like resume_claude.py) when pause is active:
| on_stderr=lambda m: sys.stderr.write(m), | ||
| ) | ||
| print(f"\n[claude] exit_code={result.exit_code}") | ||
| return result.exit_code or 0 |
There was a problem hiding this comment.
Bug: result.exit_code or 0 conflates None with 0
If the E2B SDK returns None for exit_code (which some command-execution APIs do in error scenarios), None or 0 evaluates to 0, and the caller treats it as success. The failures counter on lines 82/96 won't increment, masking a real failure.
Recommendation: Use return result.exit_code if result.exit_code is not None else 1 to distinguish "exit code 0 (success)" from "no exit code returned (error)."
| print(f"[preflight] {result.stdout.strip()}") | ||
|
|
||
|
|
||
| def upload_seed(sandbox: Sandbox, workspace: str, seed: str) -> None: |
There was a problem hiding this comment.
Security: --seed can upload arbitrary local files into the sandbox
The upload_seed function reads any local file the user specifies and writes it into the sandbox where the Claude Code agent can read and potentially exfiltrate it (by including file contents in its API calls to api.anthropic.com). While the destination safely uses only seed_path.name (preventing remote path traversal), there is no source-path restriction. A user could inadvertently upload SSH keys, cloud credentials, or other sensitive files.
Recommendation: Either add a warning in the --seed help text, or validate that the file is within an expected project directory.
| envs = build_agent_env() | ||
|
|
||
| print(f"[cube] creating sandbox from template={template_id}") | ||
| sandbox = Sandbox.create(template=template_id, timeout=1800) |
There was a problem hiding this comment.
Cleanup risk: Sandbox.create() outside a context manager
Unlike run_claude.py, this script uses a bare Sandbox.create() call (not inside a with block). If Sandbox.create() itself throws, sandbox is never assigned, causing a NameError in the finally block that silently eats the original exception. Additionally, if Sandbox.connect(sid) fails on line 95, sandbox still references the paused sandbox, and calling .kill() on a paused sandbox may not properly release resources.
Recommendation: When Sandbox.create() is not inside a with statement, guard the finally block with a check that sandbox was assigned if 'sandbox' in locals() and sandbox: before calling .kill().
Applies four follow-up review comments on TencentCloud#694 (bot round 2): - run_claude.py `--pause`: was a no-op because the `with Sandbox.create()` context manager killed the sandbox on scope exit. Switch to explicit try/finally with `sandbox.kill()` only when `--pause` is off, so a paused sandbox actually survives the script. - run_claude.py `--seed`: add a WARNING in the help text noting the file is readable by the agent inside the sandbox and should not point at credentials, SSH keys, or other secrets. - resume_claude.py exit code: `result.exit_code or 0` conflates None with 0 and masks a failure when the SDK returns no exit code. Return 1 in that case, otherwise return the real exit_code. - resume_claude.py finally guard: move `Sandbox.create(...)` inside the `try` and skip `sandbox.kill()` when `sandbox is None`, so a failure in `create()` no longer causes a NameError that swallows the original exception.
|
Round-2 auto-review surfaced four real bugs in
Verified locally: all four Python scripts still |
| if args.pause: | ||
| info = sandbox.pause() | ||
| print(json.dumps( | ||
| {"paused": True, "sandbox_id": sandbox.sandbox_id, "info": info}, | ||
| default=str, | ||
| )) | ||
|
|
||
| exit_code = result.exit_code | ||
| return 0 if exit_code == 0 else (exit_code if exit_code is not None else 1) | ||
| finally: |
There was a problem hiding this comment.
Resource leak: sandbox not killed when --pause is combined with an early error.
If a user passes --pause but preflight_agent() fails (or any error occurs before sandbox.pause() is reached), the finally block skips cleanup because not args.pause is still False. The sandbox remains alive until its timeout expires, wasting cluster resources.
Consider tracking whether the pause was actually performed:
paused = False
try:
...
if args.pause:
info = sandbox.pause()
paused = True
...
finally:
if not paused:
sandbox.kill()|
|
||
| ARG DEBIAN_FRONTEND=noninteractive | ||
| ARG NODE_MAJOR=20 | ||
| ARG CLAUDE_CODE_VERSION=latest |
There was a problem hiding this comment.
Default latest produces non-reproducible builds.
CLAUDE_CODE_VERSION=latest means the same docker build command produces different images on different days. A breaking change upstream can silently break CI/CD. Consider pinning a specific version as the default (and documenting it in the README build steps), or at minimum adding a comment urging users to pin for production deployments.
|
|
||
| ENV CLAUDE_CODE_HOME=/root/.claude | ||
| WORKDIR /workspace | ||
| EXPOSE 49983 |
There was a problem hiding this comment.
Code snippet doesn't support the --build-arg version pinning advertised in the version table.
The version table (line 32) says the CLI version can be pinned via --build-arg CLAUDE_CODE_VERSION=x.y.z, but the Dockerfile snippet here uses a hardcoded npm install -g @anthropic-ai/claude-code with no ARG or variable reference. The actual Dockerfile does include ARG CLAUDE_CODE_VERSION=latest and @${CLAUDE_CODE_VERSION} — please align the snippet with the real file so readers who copy it also get reproducible builds.
|
Refs #644 |
|
Hello, thank you for your contribution! Since we have a large number of participants in this event, to speed up the PR review process, could you please attach some screenshots and logs (both are needed) showing the verification process and results for this PR? Thank you for your participation! |
fslongjin
left a comment
There was a problem hiding this comment.
Hello~Can you add more test results/screenshots which can prove this feature can work in a REAL CubeSandbox cluster?
Summary
Addresses the Claude Code sub-direction of #644 with an end-to-end
integration guide plus a runnable example project.
examples/claude-code-integration/:Dockerfile— stacks Node.js 20 + Anthropic's official@anthropic-ai/claude-codeon top ofghcr.io/tencentcloud/cubesandbox-base:2026.16so envd is already listening on:49983.run_claude.py— headless one-shot Agent driver (upload optional seed →claude --print --allowedTools ...→ stream stdout back).resume_claude.py— pause/resume across two turns; verifies/workspace+/root/.claude/survive the snapshot.network_policy.py— recommended credential vault pattern: default-deny egress, allow onlyapi.anthropic.com, CubeEgressinjectattachesx-api-keyon the wire so the key never enters the VM.README.md/README_zh.md,env.example,requirements.txt,env_utils.py._template.mdfrontmatter and referenced in the index tables:docs/guide/integrations/claude-code.mddocs/zh/guide/integrations/claude-code.mdCovers the acceptance criteria from #644:
docs/guide/integrationssystem on the existing template.Test plan
Verified locally without a live cluster (I don't have one on this laptop):
docker build --platform linux/amd64succeeds (~125 s).envd /healthinside the running image returns 204 (readiness probe will pass).claude --version->2.1.197 (Claude Code);envd -version->0.5.13;node -v->v20.20.2.git,python3,rg,jqall present.claude --helpconfirms every flag used by the scripts (--print,--allowedTools,--verbose,--output-format stream-json,--dangerously-skip-permissions) is supported.py_compile.compile(..., doraise=True).python run_claude.py,python resume_claude.py,python network_policy.pybefore merge).Notes for the maintainers
openai-agents-example/openclaw-integration.Closes part of #644 (Claude Code sub-direction). CodeBuddy / OpenCode remain open for other contributors.