Skip to content

docs: add OpenCode integration example#766

Open
Sam-Hui-dot wants to merge 3 commits into
TencentCloud:masterfrom
Sam-Hui-dot:codex/opencode-integration-644
Open

docs: add OpenCode integration example#766
Sam-Hui-dot wants to merge 3 commits into
TencentCloud:masterfrom
Sam-Hui-dot:codex/opencode-integration-644

Conversation

@Sam-Hui-dot

@Sam-Hui-dot Sam-Hui-dot commented Jul 5, 2026

Copy link
Copy Markdown

Refs #644

Summary

This PR adds an OpenCode integration example for CubeSandbox under examples/opencode-integration.

The example covers template construction, headless OpenCode execution, pause/resume persistence, restricted egress, CubeEgress credential injection, and bilingual documentation.

What It Demonstrates

  • OpenCode sandbox template built from cubesandbox-base
  • Headless coding workflow with opencode run --auto --format json
  • One-shot coding task with deterministic verification:
    • seeds a small Python project in /workspace
    • asks OpenCode to implement calculator.add
    • runs python3 -m unittest discover -v
    • verifies result.md contains OPENCODE_CUBE_OK
  • Pause/resume workflow:
    • verifies workspace and OpenCode state persistence across sandbox pause/resume
    • continues the coding task after reconnecting to the same sandbox
    • verifies OPENCODE_RESUME_OK
  • Restricted egress workflow:
    • creates the sandbox with allow_internet_access=False
    • allows only the configured LLM host
    • injects provider credentials through CubeEgress headers
    • keeps real provider keys out of the sandbox environment
    • verifies OPENCODE_EGRESS_OK
  • Provider configuration for OpenAI, Anthropic, DeepSeek, and OpenRouter style model names
  • English and Chinese README files plus docs integration under docs/guide/integrations

Validation

Static checks:

cd examples/opencode-integration
python3 -m py_compile env_utils.py _opencode_common.py run_opencode.py resume_opencode.py network_policy.py
python3 -m unittest test_env_utils test_commands -v
bash -n ./build-template.sh
git diff --check -- examples/opencode-integration docs/guide/integrations docs/zh/guide/integrations

Image checks:

docker build --platform linux/amd64 -t opencode-cube:verify .
docker run --rm opencode-cube:verify opencode --version
docker run --rm opencode-cube:verify node --version
docker run --rm opencode-cube:verify python3 --version

End-to-end local CubeSandbox checks:

python3 run_opencode.py
python3 resume_opencode.py
python3 network_policy.py --skip-agent
python3 network_policy.py

Observed result:

  • Template image built successfully
  • Template registered successfully in local dev-env
  • One-shot OpenCode task completed with DeepSeek
  • In-sandbox unit tests passed
  • Pause/resume preserved workspace and OpenCode state
  • Restricted egress blocked non-LLM traffic
  • CubeEgress credential injection allowed LLM access without exposing the real provider key in the sandbox
  • Docs build passed with npm run docs:build

Notes

Assisted-by: Codex:GPT-5

Assisted-by: Codex:GPT-5
Signed-off-by: Sam-Hui-dot <19303092837@163.com>
@Sam-Hui-dot Sam-Hui-dot requested a review from tinklone as a code owner July 5, 2026 14:29
@cubesandboxbot

cubesandboxbot Bot commented Jul 5, 2026

Copy link
Copy Markdown

PR Review: docs: add OpenCode integration example

Overall, this is a well-structured addition. The code is clean, the architecture is clear (config in env_utils.py, shared sandbox helpers in _opencode_common.py, thin orchestration in the three driver scripts), and security practices are strong — default-deny egress, CubeEgress credential injection, secret redaction in output, proper shlex.quote() usage, and finally-block sandbox cleanup.

Inline Comments (key findings)

Inline comments were posted on specific code lines for:

  1. redact_secrets() performance — iterates all env vars on every streaming output chunk; should cache the secret values (line 114)
  2. stream=True loses error diagnostics — streaming callbacks consume stdout/stderr, so failure reports show empty output (line 42)
  3. Hardcoded Anthropic API version2023-06-01 will go stale; should be configurable via env var (env_utils.py:228)
  4. shell_join() trusts callers to sanitize — no built-in escaping, relies on callers using shlex.quote() (env_utils.py:194)
  5. Dockerfile layer caching — combined apt+NodeSource layer invalidates entirely on Node version change (Dockerfile:24)
  6. Fragile SDK introspection — reflects on e2b SDK internals for env vs envs parameter name (_opencode_common.py:77)

Test Coverage

The test suite is solid for redaction and configuration-reading paths. The most notable gaps:

  • run_command() only tested on the fallback path (env kwarg), not the primary path (envs kwarg) used by all three integration scripts
  • warn_direct_secret_env() untested — this security-warning function could silently break
  • shell_join() untested — used in main scripts for command construction
  • opencode_model() has no happy-path test (only the failure case is tested)

The three main scripts (run_opencode.py, resume_opencode.py, network_policy.py) have no unit tests, which is acceptable for integration examples that require live infrastructure.

Documentation Accuracy

Both English and Chinese docs are accurate, consistent with each other, and correctly reflect the code. All cross-language links resolve correctly. No actionable issues found.

Security

Security posture is strong overall. Notable patterns worth preserving:

  • Default-deny egress with explicit allow rules + host+SNI matching
  • CubeEgress credential injection so the sandbox only sees a placeholder key
  • include_secrets=False path that omits all API keys from sandbox env
  • Secret redaction in both streaming output and error messages
  • shlex.quote() used consistently on user-controlled shell values

The one pattern flagged by the docs themselves is the NodeSource curl | bash pattern in the Dockerfile, which the docs already acknowledge should be pinned for production.

Summary

This PR is in good shape. The inline comments above detail the most actionable improvements. The strongest recommendation is to cache the secret values in redact_secrets() — it's a one-line change that eliminates redundant O(n*m) work on every streaming chunk during long agent sessions.

Overall positive review. 👍


try:
return sandbox.commands.run(command, **kwargs)
except TypeError as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TypeError catch uses a string-match heuristic ("envs" not in str(exc)) against the exception message. If sandbox.commands.run raises a TypeError for an unrelated reason where the word "envs" happens to appear in the error text, this silently takes the fallback path and likely raises a confusing follow-up error. Consider probing the SDK API once at module import time instead of per-call exception-driven fallback.

return getattr(sandbox, "sandbox_id", getattr(sandbox, "id", "unknown"))


def secret_is_present(value: str | None) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secret_is_present() (line 64) and redact_env() (line 68) are defined here but never imported or called by any script in this package. Remove if unused, or add a comment explaining intended future use.

return value


def optional(name: str, default: str = "") -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using or here means env vars explicitly set to "", "0", or "false" are indistinguishable from "not set" and silently replaced with the default. For OPENCODE_DISABLE_AUTO_UPDATE, a user who unsets it would unintentionally revert to "1" instead of empty. Consider os.environ.get(name) or explicit None-check if distinguishing empty-from-unset matters.

return explicit
provider_specific = os.environ.get(f"{provider_name.upper()}_BASE_URL")
return provider_specific or ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The {PROVIDER_NAME}_BASE_URL fallback (e.g., OPENAI_BASE_URL) is undocumented — it's not mentioned in .env.example or any of the four doc files. Users working with custom endpoints may not discover it. Consider adding it as a commented option in .env.example.

python3 \
python3-pip \
ripgrep \
&& curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Piping a fetched script directly to bash offers no integrity verification (no checksum, no GPG signature). If the NodeSource CDN were compromised, the resulting template image could contain injected code. For an example this is tolerable, but consider documenting the risk so downstream users know not to cargo-cult this into production pipelines.

@@ -0,0 +1,60 @@
# Copyright (c) 2026 Tencent Inc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import env_utils works only when run from examples/opencode-integration/. Other example test suites in this repo (e.g., test_dev_sidecar.py) explicitly insert the directory into sys.path for portability. Without this, the tests cannot be discovered by a parent-directory pytest invocation or CI runner.


try:
return sandbox.commands.run(command, **kwargs)
except TypeError as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TypeError catch uses a string-match heuristic ("envs" not in str(exc)) against the exception message. If sandbox.commands.run raises a TypeError for an unrelated reason where the word "envs" happens to appear in the error text, this silently takes the fallback path and likely raises a confusing follow-up error. Consider probing the SDK API once at module import time instead of per-call exception-driven fallback.

return getattr(sandbox, "sandbox_id", getattr(sandbox, "id", "unknown"))


def secret_is_present(value: str | None) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secret_is_present() (line 64) and redact_env() (line 68) are defined here but never imported or called by any script in this package. Remove if unused, or add a comment explaining intended future use.

return value


def optional(name: str, default: str = "") -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using or here means env vars explicitly set to "", "0", or "false" are indistinguishable from "not set" and silently replaced with the default. For OPENCODE_DISABLE_AUTO_UPDATE, a user who unsets it would unintentionally revert to "1" instead of empty. Consider os.environ.get(name) or explicit None-check if distinguishing empty-from-unset matters.

return explicit
provider_specific = os.environ.get(f"{provider_name.upper()}_BASE_URL")
return provider_specific or ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The {PROVIDER_NAME}_BASE_URL fallback (e.g., OPENAI_BASE_URL) is undocumented — it's not mentioned in .env.example or any of the four doc files. Users working with custom endpoints may not discover it. Consider adding it as a commented option in .env.example.

python3 \
python3-pip \
ripgrep \
&& curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Piping a fetched script directly to bash offers no integrity verification (no checksum, no GPG signature). If the NodeSource CDN were compromised, the resulting template image could contain injected code. For an example this is tolerable, but consider documenting the risk so downstream users know not to cargo-cult this into production pipelines.

@@ -0,0 +1,60 @@
# Copyright (c) 2026 Tencent Inc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import env_utils works only when run from examples/opencode-integration/. Other example test suites in this repo (e.g., test_dev_sidecar.py) explicitly insert the directory into sys.path for portability. Without this, the tests cannot be discovered by a parent-directory pytest invocation or CI runner.

Assisted-by: Codex:GPT-5
Signed-off-by: Sam-Hui-dot <19303092837@163.com>


def stream_writer(stream) -> Callable[[object], None]:
def write(chunk: object) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: streaming output does not redact secrets

stream_writer writes directly to sys.stdout/sys.stderr without passing through redact_secrets(). If the agent executes a command that echoes the API key (e.g., echo $OPENAI_API_KEY), the key is printed in plaintext on the terminal.

Unlike ensure_success (which redacts output before raising), the streaming path is unguarded. Consider wrapping the writer to also redact secrets, or add a documentation note that run_opencode.py / resume_opencode.py are for local trusted use only and shared clusters should use network_policy.py (which is immune to this since it uses CubeEgress injection).

return bool(value and value.strip() and not value.strip().startswith("<"))


def redact_secrets(text: str) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redact_secrets iterates in arbitrary order — can leave partial secrets unreplaced

(_opencode_common.py:101-106)

The function calls str.replace(value, "<redacted>") for each env var in insertion order. If one secret value is a prefix of another (e.g., A_KEY=sk-foo and B_KEY=sk-foobar), the shorter value's replacement corrupts the longer one in the string, leaving <redacted>bar in the output.

This was flagged independently by both the security and code-quality reviewers. The fix is to sort values by length descending before processing (longest match first):

return {key: value for key, value in kwargs.items() if key in params}


def _is_secret_env(name: str) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly broad substring matching

any(marker in name.upper() ...) matches on substring containment, not suffix/prefix. An innocuous env var like CSRF_TOKEN_EXPIRY_SECONDS would be treated as a secret because it contains both TOKEN and SECRET, causing 3600 to be redacted from output. Consider using suffix matching (endswith) instead.

)


def load_local_dotenv() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent skip when no .env is found

If neither candidate .env exists (e.g., user forgot to create it), the function returns silently. The next call to required() then raises SystemExit("Missing required environment variable: ...") with no indication that the root cause is a missing .env file. Consider printing a warning to stderr when none of the candidate paths exist.

sandbox_id = sandbox_identifier(sandbox)
print(f"Sandbox ready: {sandbox_id}")

version_result = run_command(sandbox, "opencode --version", timeout=60)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary version-check round trip on every run

opencode --version is already pinned by OPENCODE_VERSION in the Dockerfile and verified at build time. This adds 0.1–2s of overhead per invocation with no diagnostic value that the build-time check doesn't already provide. Consider gating behind a --verbose flag or removing it.

Assisted-by: Codex:GPT-5
Signed-off-by: Sam-Hui-dot <19303092837@163.com>
return bool(value and value.strip() and not value.strip().startswith("<"))


def redact_secrets(text: str) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: redact_secrets() re-scans all env vars on every streaming chunk

This is called from stream_writer() on every stdout/stderr chunk during a long OpenCode run. It iterates os.environ, filters by secret-suffix heuristics, sorts by length, and performs O(n*m) string replacement — every single chunk. The secret values are static for the process lifetime.

Suggestion: Cache the discovered values with lru_cache or compute them once at module level:

@lru_cache(maxsize=1)
def _get_secret_values() -> tuple[str, ...]:
    values = {
        value for name, value in os.environ.items()
        if _is_secret_env(name) and _secret_is_present(value)
    }
    return tuple(sorted(values, key=len, reverse=True))

Then redact_secrets only iterates the cached tuple. This eliminates thousands of redundant env-var scans during a long streaming session.

kwargs = {"cwd": cwd, "timeout": timeout, "user": user}
kwargs = {key: value for key, value in kwargs.items() if value is not None}
if envs:
kwargs[_command_env_kwarg(type(sandbox.commands))] = envs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debugging concern: stream=True may clear stdout/stderr from the result object

When stream=True, the on_stdout/on_stderr callbacks consume the output. The returned result object may have empty .stdout and .stderr attributes (depending on the e2b/cubesandbox SDK behavior). If the command subsequently fails, ensure_success() reports empty stdout/stderr, making debugging very difficult.

Suggestion: Buffer the stream output in the stream_writer closure and attach it back to the result, or log the last N lines to a separate buffer so failure diagnostics are useful.

if provider_name == "anthropic":
return [
{"header": "x-api-key", "secret": secret, "format": "${SECRET}"},
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded Anthropic API version — will go stale over time

"2023-06-01" is hardcoded as the Anthropic API version. When Anthropic deprecates this version in the future, the script will silently send an outdated version header and receive errors, with no obvious connection to this value. Additionally, registering it as a CubeEgress "secret" (via the "secret" key in the inject dict) is semantically misleading — it's a version identifier, not a credential.

Suggestion: Pull from an environment variable (e.g., ANTHROPIC_VERSION) with "2023-06-01" as a default, so it can be updated without a code change.

return " ".join(shlex.quote(arg) for arg in args)


def shell_join(*parts: str) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell_join() trusts callers to sanitize inputs

All current callers properly use shlex.quote() on values before joining, so there's no exploitable path today. However, shell_join is a public exported function that performs no escaping itself. If a future caller forgets to quote, the result is shell injection.

Suggestion: Either (a) document the expectation by renaming to _shell_join_unsafe with a docstring noting that inputs must be pre-quoted, or (b) accept a list of arguments rather than pre-quoted strings and quote internally.

gnupg \
jq \
less \
procps \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dockerfile layer caching: apt packages and NodeSource setup in one layer

If NODE_MAJOR or OPENCODE_VERSION changes, the merged apt-get install ... curl nodesource.com && apt-get install nodejs layer is fully invalidated, causing all apt packages to reinstall even though none changed.

Suggestion: Split into two layers:

# Layer 1: base packages (stable)
RUN apt-get update && apt-get install -y --no-install-recommends \
        bash ca-certificates curl git gnupg jq less procps \
        python3 python3-pip ripgrep \
    && rm -rf /var/lib/apt/lists/*

# Layer 2: NodeSource (changes with NODE_MAJOR)
RUN 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/*

This preserves the cached base-package layer across Node version bumps — minor for a demo but helpful during iterative CI builds.



@lru_cache(maxsize=None)
def _command_env_kwarg(commands_type: type) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile SDK introspection for env parameter naming

_command_env_kwarg() reflects on the e2b/cubesandbox SDK's commands.run signature to determine whether the parameter is called env or envs. This couples the example to internal SDK naming. If a future SDK version renames the parameter (e.g., to environment), _filter_supported_kwargs silently drops the env dict and the agent runs without credentials.

Suggestion: Add a comment documenting which SDK versions use which parameter name, so upgrade diagnostics are clearer. A more robust approach would be to try both names and accept whichever works.

@fslongjin

Copy link
Copy Markdown
Member

Refs #644

@fslongjin

Copy link
Copy Markdown
Member

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants