# Huawei Sandbox

> Use when creating, connecting, or managing Huawei Cloud Sandbox instances and workspace terminals, or when a task needs a temporary runtime to deploy, run, or preview a web application. Covers sandbox lifecycle (check-user, sign-agreement, connect, release), session-based terminal execution, and credential injection. Triggers on: sandbox, workspace, terminal, web app deployment, deploy web app, preview app, deploy github, github repo, deploy repo, website, static site, web application, hwlink, devstation, hdkitservice, remote exec. NOT for: ECS instances (use huawei-ecs), CCE clusters (use huawei-cce).

- Skill: `huaweicloud/huawei-sandbox-2` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add huaweicloud/huawei-sandbox-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/huaweicloud/huawei-sandbox-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: huaweicloud (https://skillmd.com/u/huaweicloud)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/huaweicloud/huawei-sandbox-2

---


# Huawei Cloud Sandbox

**STOP - Do not answer from general knowledge.** Follow the procedure below.

## Target-Selection Gate (MANDATORY — do not skip)

When the developer asks to **deploy / host / publish / 上线 / 部署 / 发布 / 搭建** a website, web app, or static site WITHOUT naming a specific target, you MUST present the target options and obtain an explicit choice BEFORE calling any sandbox lifecycle tool (`huaweicloud_sandbox_check_user`, `huaweicloud_sandbox_connect`, `huaweicloud_sandbox_upload_project`, `huaweicloud_sandbox_deploy_nginx`, etc.).

Use the `question` tool and present, in this exact order:

1. **沙箱 (Sandbox)** — temporary runtime, instant preview URL (recommended for quick preview)
2. **OBS 静态托管** — long-term static hosting + CDN + custom domain
3. **ECS** — server-based hosting
4. **CCE** — container-based hosting

Hard rules:

- **NEVER infer or default the target** when it is unstated. Calling `huaweicloud_sandbox_connect` before this choice is made is a violation.
- Only skip the question when: (a) the developer already named a target (e.g. "部署到 ECS", "用 OBS"), or (b) the developer explicitly asked for a temporary / sandbox / preview runtime.
- "部署到华为云" / "deploy to Huawei Cloud" / "上线" alone is NOT a named target — still run the gate.
- After the choice: sandbox → continue with this skill; OBS → `huawei-obs`; ECS → `huawei-ecs` / `huawei-iac`; CCE → `huawei-cce`.

## Overview

Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace terminal execution. Covers sandbox lifecycle via hdkitservice API and remote terminal command execution via hwlink protocol.

## Activation

- **Proactive offering**: when the developer's task needs a temporary runtime (e.g. "deploy this web app", "run this app and preview it"), offer the sandbox proactively — the developer never has to say "use sandbox". Prompt: "This task can be satisfied by a sandbox — use it?"
- **Static website hosting — present options, sandbox first (MANDATORY)**: when the developer asks to deploy/host/publish a static website or web app without naming a target, do NOT silently pick a service and do NOT default to the sandbox. Run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) via the `question` tool first. Present, in order: ① 沙箱 (recommended — temporary runtime, instant preview URL) ② OBS 静态托管 (long-term static hosting/CDN) ③ ECS ④ CCE. Recommended prompt: "建议优先部署到沙箱（临时运行环境，可预览访问），也可选择 OBS 静态托管、ECS 或 CCE，你想部署到哪里？" Only if the developer, after being asked, expresses no preference may you proceed with the sandbox.
- **Detect web apps in deploy-a-repo prompts**: prompts like "部署 GitHub - <owner>/<repo>" or "deploy <github repo>" do not say "web app". Clone or inspect the repository first (package.json with dev/serve/build scripts, index.html, frontend framework dependencies such as vue/react/angular/vite, static sites, Dockerfile running a web server). If it is a web application, offer the sandbox first: "检测到该项目是 Web 应用，建议优先使用沙箱部署（临时运行环境，可预览访问），是否使用？"
- **Clone into the sandbox workspace directory**: always put project code under `/workspace/<repo-name>` (create the directory if missing) — `/workspace` is the sandbox's dedicated workspace mount at the filesystem root, not `$HOME/workspace`. Never use `/tmp` or other ephemeral locations. This keeps the project with the sandbox session, is easy to reference for serving/exposing, and survives session-level restarts of the shell.
- **Deployment must end with a public URL**: after deploying and exposing the app with DevBridge, always return the tunnel URL to the developer as the final result — a deployment without an accessible link is incomplete.
- **Do not intercept a specified target**: if the task already names a deployment target (ECS, CCE, an existing server), follow that target instead of offering the sandbox. Offer the sandbox only when the task needs a temporary runtime or no target is specified.
- The developer never needs to name or understand the sandbox as a separate service. Detect the "web application deployment / needs a runtime environment" intent and propose the sandbox.

## MCP Tools

### User Verification (Prerequisites)

| Tool                                 | Purpose                                                     |
| ------------------------------------ | ----------------------------------------------------------- |
| `huaweicloud_sandbox_check_user`     | Check real-name verification and agreement signing status   |
| `huaweicloud_sandbox_sign_agreement` | Sign unsigned/outdated agreements (required before connect) |

### Local Detection

| Tool                           | Purpose                                                    |
| ------------------------------ | ---------------------------------------------------------- |
| `huaweicloud_detect_framework` | Scan local project, return framework type + build commands |

### Sandbox Lifecycle

| Tool                              | Purpose                                                                  |
| --------------------------------- | ------------------------------------------------------------------------ |
| `huaweicloud_sandbox_connect`     | Connect to sandbox (one user one instance, reuses existing if available) |
| `huaweicloud_sandbox_credentials` | Inject temporary AK/SK into a running sandbox                            |

### Terminal Execution

| Tool                                    | Purpose                                                                     |
| --------------------------------------- | --------------------------------------------------------------------------- |
| `huaweicloud_sandbox_exec_with_session` | Session-based execution (state persists; best for interactive work)         |
| `huaweicloud_sandbox_exec_one_shot`     | One-shot execution (fresh connection; best for long/heavy commands)         |
| `huaweicloud_sandbox_upload_file`       | Upload a local file into the sandbox (chunked base64 write + md5 verify)    |
| `huaweicloud_sandbox_upload_project`    | Upload a local project directory to sandbox (HTTP tunnel, tar.gz + extract) |
| `huaweicloud_sandbox_deploy_nginx`      | Deploy nginx config with permissions fix and reload in one call             |
| `huaweicloud_sandbox_deploy_check`      | Run deployment completeness check (nginx, DevBridge, URL, QR if needed)     |
| `huaweicloud_sandbox_close_session`     | Close a persistent terminal session                                         |

### Tool Selection Guide

| Scenario                           | Use                                | Why                                                          |
| ---------------------------------- | ---------------------------------- | ------------------------------------------------------------ |
| `cd`, env setup, command chains    | `exec_with_session`                | Needs shared shell state across calls                        |
| `npm install`, `apt-get`, builds   | `exec_one_shot`                    | Long-running (>30s), no state needed, more stable            |
| `curl`, health checks, quick tests | Either — `exec_one_shot` preferred | Stateless, fast                                              |
| Server startup (background)        | `exec_with_session`                | Need to `nohup ... &` then check output in same session      |
| Deployment scripts                 | `exec_one_shot+shot`               | Long script, fresh connection avoids session timeouts        |
| nginx configuration                | `deploy_nginx`                     | Auto-generates correct template + permissions + reload       |
| Deployment completeness check      | `deploy_check`                     | Verifies nginx, DevBridge, URL, QR before reporting success  |
| Single file upload (<1MB)          | `upload_file`                      | Base64 chunked, reliable for small files                     |
| Project directory upload (>1MB)    | `upload_project`                   | HTTP tunnel, much faster than base64 for multi-file projects |

**Timeout tuning**: default is 120s. For commands expected to run longer (e.g. large builds), pass `timeout_ms` explicitly:

```json
{ "timeout_ms": 300000 }
```

**Session recovery**: if `exec_with_session` returns `session is not ready`, the WebSocket connection has dropped. Do NOT retry the same session — fall back to `exec_one_shot` for that command instead. To recover state (cd, env vars), reconstruct them explicitly in the one-shot command.

**Timeout recovery**: if `exec_one_shot` returns a timeout error, check whether partial output is available before declaring failure:

- For build commands: check `tail -30 /tmp/build.log` — the build may have completed but the tee pipe didn't flush before timeout
- For long scripts: split into independent `exec_one_shot` calls (max 5 sub-commands per call, 15s timeout per call)
- Do NOT retry the same composite command — split and retry individual steps

## Workflow

**Step 0 — Target-Selection Gate (MANDATORY)**: before any sandbox lifecycle tool, confirm the target. If the developer asked to deploy/host/publish a site without naming a target, STOP and run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) first; do NOT call `huaweicloud_sandbox_connect` until it is resolved. Proceed with this workflow only if the target is already confirmed as sandbox (or a temporary runtime).

Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:

1. **Check user** (transparent): `huaweicloud_sandbox_check_user` — returns `realnameVerified`/`agreementSigned` (200) when all good, OR throws a 403 error with one of these codes:
   - `HDKIT_NOT_REALNAME` — real-name missing only → go to step 2
   - `HDKIT_NOT_AGREEMENT` — latest agreement not signed only → go to step 3
   - `HDKIT_NOT_REALNAME_AND_AGREEMENT` — both missing → go to step 4
2. **Real-name verification only** (`HDKIT_NOT_REALNAME`): tell the developer once, "Huawei Cloud requires real-name verification before using the sandbox — please complete it in the Huawei Cloud console (实名认证)." and stop — do not retry `connect` in a loop
3. **Sign agreement only** (`HDKIT_NOT_AGREEMENT`): **STOP and do NOT sign on your own.** Ask the developer: "Huawei Cloud sandbox requires signing the latest developer service agreement. May I sign it for you?" Then **wait for the developer to explicitly agree** (e.g. "签署" / "确认" / "sign it"). Only after explicit consent call `huaweicloud_sandbox_sign_agreement` and return its result (`signed`/`signedCount`) to the developer. **Never sign a legal agreement on the developer's behalf without their explicit, unambiguous consent.** Do not expose the underlying sandbox/DevBridge service as a separate entity the developer must understand or sign up for
4. **Both missing** (`HDKIT_NOT_REALNAME_AND_AGREEMENT`): present **both** requirements together in one message — the real-name verification steps (console, step 2) **and** the agreement-signing request (step 3, wait for explicit consent) — so the developer can complete both at once
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`, `expiresAt` (STS credential expiry). The `source` parameter identifies the calling agent (valid values: `CLI`, `WEB`, `VSCODE`, `WEBVNC`, `WEBPTY`, `WEBIDE`, `CURSOR`, etc. — case-sensitive, all uppercase). When the `git` parameter is provided (with `repo_url`, `target_path`, optional `repo_branch`), code is automatically transferred to the sandbox: local `git clone` followed by `uploadProject` (falls back to direct sandbox clone when local git is unavailable). Check `_repoStatus` in the response: `uploaded_from_local`, `cloned_in_sandbox`, or `already_exists` (reconnect).
6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs, DevBridge tunnels, and stale web processes from previous deployments can cause port conflicts and quota errors. Run cleanup immediately after connect:

   ```bash
   # Kill stale Node.js web processes from previous deployments
   pkill -9 -f "next-server" 2>/dev/null || true
   pkill -9 -f "next start" 2>/dev/null || true
   pkill -9 -f "nuxt" 2>/dev/null || true
   sleep 1
   # Remove stale nginx configs from previous deployments
   sudo rm -f /etc/nginx/conf.d/*.conf /etc/nginx/conf.d/*.conf.bak 2>/dev/null
   # Remove stale DevBridge tunnels
   devbridge delete-all 2>/dev/null || true
   # Reload nginx to apply
   sudo nginx -s reload 2>/dev/null || true
   ```

7. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox. Pass `session_id` **or** `dev_stage_id` (at least one; both come from `sandbox_connect`, and `dev_stage_id` may be omitted to reuse the most recent connect). The tool validates the AK/SK against IAM first — an invalid SK is rejected here instead of failing later with `APIGW.0301` during exec. Example:

   ```json
   { "dev_stage_id": "<dev_stage_id from connect>" }
   ```

   After injection, credentials are also written to `/tmp/hw_creds.sh` for shell access (includes `HW_PROJECT_ID` when resolvable):

   ```bash
   # Source credentials in any sandbox shell before using hcloud/devbridge:
   source /tmp/hw_creds.sh 2>/dev/null && echo "Credentials loaded"
   ```

   If `source /tmp/hw_creds.sh` returns empty, the credentials injection may not have completed — re-run `huaweicloud_sandbox_credentials`.

8. **Execute commands**: use `huaweicloud_sandbox_exec_with_session` for interactive work (cd, env setup), or `huaweicloud_sandbox_exec_one_shot` for long-running builds/installs. See [Tool Selection Guide](#tool-selection-guide).
9. **Upload project** (if deploying local code): `huaweicloud_sandbox_upload_project` to transfer a local project directory into `/workspace/<name>` — uses HTTP tunnel, much faster than base64 for multi-file projects. Use `huaweicloud_sandbox_upload_file` for single files.
10. **Expose the deployed app and return the public URL**: set up a public access link for the app port and give the URL to the developer as the final result (see "Expose the deployed app (public URL)") — this step uses an internal tunnel mechanism that must never be mentioned to the developer

## File Transfer (local → sandbox)

### upload_project (preferred for directories)

`huaweicloud_sandbox_upload_project` packages a local directory as tar.gz, uploads it to the sandbox via HTTP tunnel, and extracts it. This is the fastest way to transfer multi-file projects.

**Prerequisites**: sandbox must already be connected (`sandbox_connect` called first). The tool handles everything internally — no manual setup needed.

```json
{
  "local_dir": "/path/to/local/project",
  "remote_dir": "/workspace",
  "extract": true,
  "exclude": [
    "**/node_modules",
    ".git",
    "__pycache__",
    ".next",
    ".nuxt",
    ".output",
    ".turbo",
    ".cache",
    ".swc",
    "dist",
    "coverage",
    "*.pyc"
  ]
}
```

- `local_dir` (required): local project directory
- `remote_dir` (optional, default `/workspace`): parent directory on sandbox
- `extract` (optional, default `true`): extract tar.gz after upload
- `exclude` (optional): patterns to exclude from archive. **For web apps, always exclude dependency directories** (`node_modules`, `.next`, `.nuxt`, `.output`, `.turbo`, `.cache`) — these will be re-installed/built in the sandbox.
- Result includes `md5` and `md5Verified` for integrity check

### upload_file (for single files)

`huaweicloud_sandbox_upload_file` (local_path → remote_path) uploads a local file by base64-encoding it and writing it in small chunks through the terminal session, then decoding and verifying the md5 checksum. Use it for individual files, especially small ones (<1MB).

### Exec channel size limit

`huaweicloud_sandbox_exec_with_session` and `huaweicloud_sandbox_exec_one_shot` are fragile for long commands and large inline content. Do NOT paste a file's base64 directly into a single command:

| Payload                     | Result                                 |
| --------------------------- | -------------------------------------- |
| ~30KB base64 in one command | exec timeout                           |
| ~12KB base64 in one command | session crash (`session is not ready`) |
| ~4–5KB normal command       | OK                                     |

Keep individual exec commands small (≤ ~4KB). For larger content, use `huaweicloud_sandbox_upload_file` instead.

### OBS transfer fallback

When the upload tool is unavailable, transfer through OBS: upload to a public-read object locally, then pull it into the sandbox.

```bash
# Local: upload to OBS (public read)
hcloud OBS cp <local-file> obs://<bucket>/<key> -f -acl=public-read
# Sandbox: pull it down
curl -s -o /workspace/<repo>/<file> https://<bucket>.obs.<region>.myhuaweicloud.com/<key>
```

> Requires OBS credentials (`huaweicloud_setup_obs_config`). Bucket ACL does not cascade to objects — set object-level `-acl=public-read` for anonymous reads.

## Expose the deployed app (public URL)

Expose a deployed web app to a public URL and return that URL to the developer.

**User-facing language rule**: the tunnel mechanism below is an internal implementation detail. Never mention "DevBridge", "tunnel", or the tunnel CLI to the developer. Tell the developer only: "正在生成访问地址..." while exposing, then hand over the final URL.

**Install** (internal tooling; skip if `devbridge version` already works):

```bash
if command -v devbridge >/dev/null 2>&1; then
  echo "devbridge already installed"
else
  curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh | bash
fi
export PATH=$PATH:$HOME/.huawei/bin   # installer only writes ~/.bashrc; session shells do not re-source it
```

**Login** (non-interactive; credentials from `huaweicloud_sandbox_credentials` are available via `/tmp/hw_creds.sh`). If `source /tmp/hw_creds.sh` returns empty, the credentials injection has expired (sandbox session reconnection resets them) — re-run `huaweicloud_sandbox_credentials` first:

```bash
source /tmp/hw_creds.sh 2>/dev/null
devbridge auth login --huaweicloud --access-key "$HW_ACCESS_KEY" --secret-key "$HW_SECRET_KEY"
```

- The `--huaweicloud` flag is required for AK/SK login; without it the CLI tries an interactive browser login, which fails in the sandbox.
- Credentials are stored in `/tmp/hw_creds.sh` (chmod 600) — source it before login, never echo the values.
- Verify with `devbridge auth status`. If `$HW_ACCESS_KEY` is empty, ensure `huaweicloud_sandbox_credentials` was called first.

**Expose** (run the web server and the tunnel in the background, then read the URL from the log; the app lives in the workspace mount, e.g. `/workspace/<repo-name>`):

```bash
# 0. Pre-cleanup: kill old processes and stale tunnels
pkill -f "devbridge host" 2>/dev/null || true
sleep 2
devbridge delete-all 2>/dev/null || true

# 1. Start tunnel
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
sleep 10 && cat /tmp/host.log

# 2. Extract tunnel URL and health-check before returning
TUNNEL_URL=$(grep -oP 'Tunnel URL: \K.*' /tmp/host.log)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then
  echo "Tunnel verified: $TUNNEL_URL (HTTP $HTTP_CODE)"
else
  echo "WARN: Tunnel URL unreachable (HTTP $HTTP_CODE). Rebuilding tunnel..."
  pkill -f "devbridge host" && sleep 2
  devbridge delete-all 2>/dev/null || true
  nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
  sleep 10
  TUNNEL_URL=$(grep -oP 'Tunnel URL: \K.*' /tmp/host.log)
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
  echo "Retry tunnel: $TUNNEL_URL (HTTP $HTTP_CODE)"
fi
```

**Never return a tunnel URL without verifying it first** — a stale URL (from a killed tunnel process) will silently fail. Always curl-check before giving the URL to the developer.

**Quota recovery**: if the tunnel creation fails with `10006: quota exceeded`:

```bash
# Step A: List all tunnels (both active and stale)
devbridge list -j
# Step B: Remove all stale tunnels
devbridge delete-all
# Step C: Retry tunnel creation
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
sleep 10 && cat /tmp/host.log
```

This eliminates the most common deployment failure — historical tunnels from previous sessions accumulating past the max=10 quota.

- The public URL has the form `https://<id>-<port>.cn-north-4-bridge.myhuaweicloud.com` (from the `Tunnel URL:` line).
- **Return this URL to the developer as the deployment result link.** Keep the host process running (do not close the session before handing over the URL).
- Tunnel `description` (`-d`) accepts only Chinese characters, letters, and digits (0-64). Symbols such as `-`/`_`/spaces are rejected (`Invalid tunnel description`).
- Internal docs: https://huaweicloud.github.io/devspace-devbridge/

**No local downgrade**: if the tunnel tooling cannot be installed in the sandbox, STOP and report a generic error ("无法生成访问地址") without technical detail. Never install it on the developer's local machine — a local install would defeat the purpose of sandbox deployment.

## Web Application Deployment

When deploying a web application to the sandbox, build the app inside the sandbox before exposing it. Source code is uploaded, dependencies installed, and the framework built in the sandbox environment.

### Step 1: Detect Framework Locally

**Always call `huaweicloud_detect_framework` first** before connecting to the sandbox. It scans the local project and returns:

- `type`: `spa` | `ssr` | `ssg` | `cross-platform` | `monorepo` | `static`
- `framework`: framework name
- `packageManager`: `npm` | `yarn` | `pnpm`
- `installCmd` / `buildCmd` / `outputDir` / `port`
- For SSR: also `serveCmd` and `checkUrl`
- For nginx: `nginxType` (`spa` | `proxy` | `static`)
- For Monorepo: `subApps` list with individual framework detection

If detection returns `null`, the project is not a recognized web app. Stop and tell the developer.

If detection returns `type: "monorepo"`, show the `subApps` list to the developer and ask which sub-app to deploy. Then re-detect that sub-app's framework.

### Step 2: Connect and Upload

Follow the standard [Workflow](#workflow) steps 1-6 to connect to the sandbox, then:

```json
{
  "local_dir": "<projectPath>",
  "remote_dir": "/workspace",
  "exclude": [
    "**/node_modules",
    ".git",
    "__pycache__",
    ".next",
    ".nuxt",
    ".output",
    ".turbo",
    ".cache",
    ".swc",
    "dist",
    "coverage",
    "*.pyc"
  ]
}
```

**Always exclude build artifacts and dependency directories** — they will be re-installed/built inside the sandbox:

| Pattern           | Why excluded                                   |
| ----------------- | ---------------------------------------------- |
| `**/node_modules` | Dependencies — reinstall in sandbox            |
| `.git`            | Version control — not needed for deployment    |
| `__pycache__`     | Python bytecode cache                          |
| `.next`           | Next.js build output — rebuild in sandbox      |
| `.nuxt`           | Nuxt build cache — rebuild in sandbox          |
| `.output`         | Nuxt production output — rebuild in sandbox    |
| `.turbo`          | Turborepo cache — re-run in sandbox            |
| `.cache`          | Generic tool cache (Parcel, Storybook, etc.)   |
| `.swc`            | Taro/Webpack SWC cache — regenerate in sandbox |
| `dist`            | Build output — rebuild in sandbox              |
| `coverage`        | Test coverage reports — not needed for deploy  |
| `*.pyc`           | Python compiled files                          |

**Post-upload permission fix**: after `upload_project` extracts the project, fix file permissions lost during transfer (native binaries from other platforms, .bin symlinks):

```bash
# Fix executable permissions on node_modules/.bin (lost during cross-platform transfer)
chmod -R +x /workspace/<dirname>/node_modules/.bin 2>/dev/null || true
# Fix world-read on all files (sandbox default umask may restrict)
chmod -R o+rX /workspace/<dirname> 2>/dev/null || true
```

### Step 3: Sandbox Environment Readiness

Install OS-level dependencies **before** uploading the project (independent of project code, can run in parallel if desired).

#### 3a: Detect OS and package manager

```bash
source /etc/os-release 2>/dev/null
echo "OS_DETECTED=${ID:-unknown}|${ID_LIKE:-}"
if command -v apt-get >/dev/null 2>&1; then echo "PKG_MGR=apt"; elif command -v yum >/dev/null 2>&1; then echo "PKG_MGR=yum"; elif command -v dnf >/dev/null 2>&1; then echo "PKG_MGR=dnf"; elif command -v apk >/dev/null 2>&1; then echo "PKG_MGR=apk"; else echo "PKG_MGR=unknown"; fi
```

Use the detected `PKG_MGR` for all package installations below.

**Architecture awareness**: the sandbox runs Linux aarch64 (ARM64). Native binaries built on x64 (Windows/macOS Intel) will not execute. Always install dependencies and build inside the sandbox. For projects with native addons (Taro `@swc/core`, Prisma, `esbuild`, `node-gyp`), local x64 pre-build + upload of `dist/` output is a viable alternative when sandbox builds fail.

**GitCode SSL**: if `git clone` from GitCode fails with SSL certificate errors, use a one-shot override (do NOT set it globally — that would disable cert verification for every repo):

```bash
git -c http.sslVerify=false clone <repo-url>
```

Then retry the clone. This bypasses SSL verification only for this single clone.

**index.html overwritten by the platform landing page**: when a repo is cloned **inside** the sandbox (fallback path, used only when the operator machine has no git), the DevStation portal-template init may asynchronously write a default GitCode/AtomGit landing page over the repo's `index.html`, leaving a blank page after deploy. Symptoms: `git show HEAD:index.html` shows the user's original page but `/workspace/<repo>/index.html` is a Vue SPA shell referencing `cdn-static.gitcode.com`. Recovery:

```bash
cd /workspace/<repo> && git checkout HEAD -- index.html
```

Prevention: `sandbox_connect` clones locally and uploads the finished tree by default (`_repoStatus: 'uploaded_from_local'`), which never triggers this race — keep local git available so the fallback in-sandbox clone is not used. The overwrite itself is DevStation platform behavior; report persistent occurrences to the platform team.

#### 3b: Install nginx (before project upload)

```bash
# Use the detected PKG_MGR from step 3a
case "$PKG_MGR" in
  apt) sudo apt-get update -qq && sudo apt-get install -y -qq nginx ;;
  yum) sudo yum install -y nginx ;;
  dnf) sudo dnf install -y nginx ;;
esac
sudo nginx -t && echo "nginx: ready"
```

If nginx cannot be installed, skip to Python HTTP server fallback (see `references/nginx-templates.md`).

#### 3c: Verify remaining tools

Before installing project dependencies, verify the sandbox has the required runtime tools. **Run each check as a separate `exec_one_shot` call with 15s timeout** — do not bundle all checks into one command. A single hung subcommand (e.g., `make --version` or `hugo version`) will timeout the entire check, blocking deployment:

```
Check 1: node --version       (timeout: 15s)
Check 2: npm --version         (timeout: 15s)
Check 3: nginx -v 2>&1         (timeout: 15s)
Check 4: git --version         (timeout: 15s)
Check 5: python3 --version     (timeout: 15s)
Check 6: curl --version | head -1 (timeout: 15s)
Check 7: wget --version | head -1 (timeout: 15s)
Check 8: make --version | head -1 (timeout: 15s)
Check 9: pnpm --version        (timeout: 15s)
Check 10: yarn --version       (timeout: 15s)
Check 11: hugo version         (timeout: 15s)
Check 12: devbridge version    (timeout: 15s)
```

For each check, parse the output: if stdout contains `MISSING:` or the tool wasn't found, install it. **Skip framework-specific tools not needed for the current project** (e.g., skip Hugo for React apps).

**Install only missing tools** — parse the pre-flight output and install only tools reported as `MISSING`. Use OS-aware commands:

| Missing Tool | Install Command (apt)                                                                                                                                                                                                 | Install Command (yum/dnf)   |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| Node.js      | Follow [Node.js in the sandbox](#nodejs-in-the-sandbox)                                                                                                                                                               | Same                        |
| nginx        | `sudo apt-get update -qq && sudo apt-get install -y -qq nginx`                                                                                                                                                        | `sudo yum install -y nginx` |
| curl         | `sudo apt-get update -qq && sudo apt-get install -y -qq curl`                                                                                                                                                         | `sudo yum install -y curl`  |
| wget         | `sudo apt-get update -qq && sudo apt-get install -y -qq wget`                                                                                                                                                         | `sudo yum install -y wget`  |
| make         | `sudo apt-get update -qq && sudo apt-get install -y -qq make`                                                                                                                                                         | `sudo yum install -y make`  |
| pnpm         | `npm i -g pnpm`                                                                                                                                                                                                       | Same                        |
| yarn         | `npm i -g yarn`                                                                                                                                                                                                       | Same                        |
| Hugo         | `curl -fsSL https://github.com/gohugoio/hugo/releases/download/v0.140.0/hugo_extended_0.140.0_linux-amd64.tar.gz -o /tmp/hugo.tar.gz && sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo && rm /tmp/hugo.tar.gz` | Same                        |
| DevBridge    | `curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh \| bash && export PATH=$PATH:$HOME/.huawei/bin`                                                                                           | Same                        |

**If Node.js is missing**, install it first — all build workflows depend on it. Stop and report to the developer if Node.js installation fails.

### Step 4: Install and Build

#### 4a: Inject Environment Variables

Before any project commands, parse `.env*` files and inject them into the shell environment. Prisma, Drizzle, and other ORM/database tools do NOT auto-read framework-level env files:

```bash
cd /workspace/<dirname>
# Load env files if present (most specific first)
for f in .env.local .env.development.local .env.development .env; do
  if [ -f "$f" ]; then
    set -a && source "$f" 2>/dev/null; set +a
    echo "Loaded env: $f"
  fi
done
# Verify key variables for common tools
echo "DATABASE_URL=${DATABASE_URL:-<NOT SET>}"
echo "NODE_ENV=${NODE_ENV:-development}"
```

This must run via `exec_with_session` so the exported variables persist for subsequent build commands in the same session.

**Prisma / ORM compatibility**: Prisma CLI (`prisma generate`, `prisma db push`, `prisma migrate`) only reads `.env` by default, NOT `.env.local` or `.env.development`. If the project uses `.env.local` for `DATABASE_URL`, link it before any Prisma command:

```bash
# Prisma requires .env (not .env.local) — symlink if needed
if [ -f .env.local ] && [ ! -f .env ]; then ln -sf .env.local .env 2>/dev/null || cp .env.local .env; fi
# Then re-source
set -a && source .env 2>/dev/null; set +a
```

#### 4b: Install Dependencies

Use `exec_one_shot` for install (no shared state needed). Skip if `node_modules` already exists:

```bash
cd /workspace/<dirname> && [ -d node_modules ] && echo "SKIP: node_modules exists" || <installCmd>
```

Wait for install to complete. For large projects on aarch64 sandboxes (1000+ packages), set `timeout_ms` to 180000 (3 min).

**Node version compatibility**: if `npm install` fails with native module errors (e.g., `rollup 4`, `@esbuild`, `node-gyp`), check the Node version:

```bash
node -v
```

Node v24+ uses musl-based binaries on some sandbox images, which may break native addons built for glibc. If native modules fail:

- Try `npm install --force` or `npm install --legacy-peer-deps`
- For rollup 4 projects, consider `npm install rollup@3` as fallback
- If webpack/rollup native addon errors persist, add `--ignore-scripts` then manually rebuild: `npm rebuild`

**Prisma / database initialization**: if `prisma/schema.prisma` exists in the project, initialize the database after install and before build. Prisma Client generation (`prisma generate`) is usually handled by `postinstall`, but `prisma db push` (SQLite) or `prisma migrate deploy` (PostgreSQL/MySQL) must be run manually:

```bash
cd /workspace/<dirname>
if [ -f prisma/schema.prisma ]; then
  echo "Prisma schema detected — initializing database..."
  npx prisma db push --skip-generate 2>/dev/null || npx prisma migrate deploy 2>/dev/null || echo "WARN: skip db init"
fi
```

> `--skip-generate` avoids redundant generation when `postinstall` already ran `prisma generate`. For SQLite, `DATABASE_URL="file:./dev.db"` must be set in `.env`/`.env.local` before this step.

#### 4c: Build

**Timeout strategy by framework type:**

| Type                           | timeout_ms      | Rationale                             |
| ------------------------------ | --------------- | ------------------------------------- |
| SPA / SSG                      | 300000 (5 min)  | Vite/Webpack builds typically < 3 min |
| Cross-platform (Taro, uni-app) | 900000 (15 min) | Webpack5 H5 slow on aarch64, 7-8 min  |
| SSR (Next.js, Nuxt)            | 600000 (10 min) | Full-stack compilation + SSG pages    |
| Monorepo                       | 600000 (10 min) | Multiple apps, shared packages        |
| `null` (no build)              | N/A             | Skip                                  |

**Build with `exec_one_shot`:**

```bash
cd /workspace/<dirname> && [ -d <outputDir> ] && echo "SKIP: <outputDir> exists" || (umask 022 && <buildCmd> 2>&1 | tee /tmp/build.log)
```

Always pipe build output through `tee /tmp/build.log` — captures stderr+stdout so diagnostics are available even if the command times out.

**OutDir verification**: before building for the first time, check the project's actual output directory (not just the default from framework detection). Projects can override outDir in config (e.g., VitePress `outDir: '../dist'`):

```bash
# Check for custom outDir in common config files
grep -r "outDir\|outputDir\|dest\|distDir" /workspace/<dirname>/.vitepress/config.* 2>/dev/null || true
```

If a custom outDir is found, use that instead of the framework-detected default for all subsequent checks.

**Post-build output verification**: after a successful build, verify the actual `index.html` location. Framework-returned `outputDir` may be inaccurate — either a subdirectory (e.g., uni-app v3: `dist` → `dist/build/h5`) or a completely different top-level path (e.g., Vite `build.outDir` pointing to `portal/public`). Search the whole project (skipping `node_modules`), not just `<outputDir>`:

```bash
# Find the real index.html after build (search whole project, not just the default outputDir)
REAL_INDEX=$(find /workspace/<dirname> -path '*/node_modules' -prune -o -name index.html -type f -print 2>/dev/null | head -1)
if [ -n "$REAL_INDEX" ] && [ -f "$REAL_INDEX" ]; then
  REAL_OUTDIR=$(dirname "$REAL_INDEX")
  echo "Actual output dir: $REAL_OUTDIR"
  # Use REAL_OUTDIR for nginx config instead of framework-reported outputDir
fi
```

If `REAL_OUTDIR` differs from `<outputDir>`, use `REAL_OUTDIR` for all subsequent steps (nginx config, port check, etc.).

**Post-timeout recovery**: if `exec_one_shot` returns a timeout error (Request timed out), do NOT fail immediately. First dump any captured build log, then check the output directory:

```bash
# If timeout occurred, show captured output and verify build
if timeout_error; then
  echo "=== Build log (tail) ==="
  tail -30 /tmp/build.log 2>/dev/null
  echo "=== Checking output ==="
  if [ -d <outputDir> ] && [ "$(ls -A <outputDir> 2>/dev/null)" ]; then
    # Verify at least one key output file exists (not just empty dir from broken build)
    if [ -f <outputDir>/index.html ] || [ -f <outputDir>/server.js ] || [ -f <outputDir>/app.js ]; then
      echo "Build output detected despite timeout — continuing with deployment"
    else
      echo "ERROR: Output directory exists but missing expected files (index.html/server.js). Build may have failed silently."
      echo "Full log: /tmp/build.log"
      exit 1
    fi
  else
    echo "ERROR: Build did not complete. Output directory empty or missing."
    echo "Full log: /tmp/build.log"
    exit 1
  fi
fi
```

For SSR frameworks, also verify the server entry point exists: `test -f <outputDir>/server.js || test -f node_modules/next/dist/server/next-server.js`.

**Build progress visibility**: for very large builds, touch a marker file before starting and use `exec_with_session` to poll intermediate logs:

```bash
# Before build:
touch /tmp/build-start && echo "Build started at $(date)"

# During build via exec_with_session (separate call for polling):
cat .next/trace 2>/dev/null | tail -5  # Next.js build trace
# or
tail -5 /tmp/build.log 2>/dev/null
```

- `cd /workspace/<dirname>/<subAppPath>` for Monorepo sub-apps.
- For `pnpm` projects, `node_modules` may be at the workspace root. Check both the sub-app dir and the workspace root.
- For Hugo/static sites where `installCmd` is `null`, skip install entirely.
- For static sites where `buildCmd` is `null`, skip build entirely.

#### 4c-aux: Build Failure Response

Build failures fall into two categories. Handle them differently:

| Failure Type            | Behavior                                                                                                                                           |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Timeout** (timed out) | Check `/tmp/build.log` tail and output directory — build may have completed but `tee` pipe didn't flush. See Post-timeout recovery above.          |
| **Non-zero exit code**  | **STOP immediately.** The build engine explicitly rejected the output. Do NOT retry, modify source, or tweak env vars. Follow the procedure below. |

**When a build exits with non-zero exit code:**

1. **STOP** — do NOT retry, do NOT modify source code, do NOT change environment variables
2. **Extract the error** from `/tmp/build.log`:
   ```bash
   tail -30 /tmp/build.log
   ```
3. **Present the failure to the developer** with:
   - The app name and build command that failed
   - The key error message (last meaningful lines from the build log)
   - A brief diagnosis of the likely cause
4. 

…(truncated)
