huaweicloud-devkit 1.1.0-next.13 → 1.1.0-next.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.0-next.13",
3
+ "version": "1.1.0-next.14",
4
4
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
5
5
  "type": "module",
6
6
  "files": [
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.0-next.13",
20
+ "version": "1.1.0-next.14",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -20,7 +20,7 @@
20
20
  "mcpServers": "./.mcp.json",
21
21
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
22
22
  "skills": "./skills/",
23
- "version": "1.1.0-next.13",
23
+ "version": "1.1.0-next.14",
24
24
  "author": {
25
25
  "name": "HuaweiCloud Mate",
26
26
  "url": "https://github.com/huaweicloud"
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.0-next.13",
20
+ "version": "1.1.0-next.14",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.0-next.13",
3
+ "version": "1.1.0-next.14",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.0-next.13",
3
+ "version": "1.1.0-next.14",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.0-next.13",
5
+ "version": "1.1.0-next.14",
6
6
  "family": "bundle-plugin",
7
7
  "bundleFormat": "codex",
8
8
  "description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
@@ -95,10 +95,15 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
95
95
  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
96
96
  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
97
97
  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
98
- 5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`
99
- 6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs and DevBridge tunnels from previous deployments can cause port conflicts and quota errors. Run cleanup immediately after connect:
98
+ 5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`. The `source` parameter identifies the calling agent (valid values: `'CLI'`, `'WEb'`, `'VSCODE'`, `'CURSO'`, `'WEBIDE'`, etc. — case-sensitive). The `git` parameter (with `repo_url`, `repo_name`, `target_path`) is accepted but does NOT auto-clone the repository — always clone manually.
99
+ 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:
100
100
 
101
101
  ```bash
102
+ # Kill stale Node.js web processes from previous deployments
103
+ pkill -9 -f "next-server" 2>/dev/null || true
104
+ pkill -9 -f "next start" 2>/dev/null || true
105
+ pkill -9 -f "nuxt" 2>/dev/null || true
106
+ sleep 1
102
107
  # Remove stale nginx configs from previous deployments
103
108
  sudo rm -f /etc/nginx/conf.d/*.conf /etc/nginx/conf.d/*.conf.bak 2>/dev/null
104
109
  # Remove stale DevBridge tunnels
@@ -202,7 +207,7 @@ fi
202
207
  export PATH=$PATH:$HOME/.huawei/bin # installer only writes ~/.bashrc; session shells do not re-source it
203
208
  ```
204
209
 
205
- **Login** (non-interactive; credentials injected by `huaweicloud_sandbox_credentials` are available at `/tmp/hw_creds.sh`):
210
+ **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:
206
211
 
207
212
  ```bash
208
213
  source /tmp/hw_creds.sh 2>/dev/null
@@ -349,6 +354,16 @@ if command -v apt-get >/dev/null 2>&1; then echo "PKG_MGR=apt"; elif command -v
349
354
 
350
355
  Use the detected `PKG_MGR` for all package installations below.
351
356
 
357
+ **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.
358
+
359
+ **GitCode SSL**: if `git clone` from GitCode fails with SSL certificate errors:
360
+
361
+ ```bash
362
+ git config --global http.sslVerify false
363
+ ```
364
+
365
+ Then retry the clone. This is a known sandbox environment limitation.
366
+
352
367
  #### 3b: Install nginx (before project upload)
353
368
 
354
369
  ```bash
@@ -422,6 +437,15 @@ echo "NODE_ENV=${NODE_ENV:-development}"
422
437
 
423
438
  This must run via `exec_with_session` so the exported variables persist for subsequent build commands in the same session.
424
439
 
440
+ **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:
441
+
442
+ ```bash
443
+ # Prisma requires .env (not .env.local) — symlink if needed
444
+ if [ -f .env.local ] && [ ! -f .env ]; then ln -sf .env.local .env 2>/dev/null || cp .env.local .env; fi
445
+ # Then re-source
446
+ set -a && source .env 2>/dev/null; set +a
447
+ ```
448
+
425
449
  #### 4b: Install Dependencies
426
450
 
427
451
  Use `exec_one_shot` for install (no shared state needed). Skip if `node_modules` already exists:
@@ -444,6 +468,18 @@ Node v24+ uses musl-based binaries on some sandbox images, which may break nativ
444
468
  - For rollup 4 projects, consider `npm install rollup@3` as fallback
445
469
  - If webpack/rollup native addon errors persist, add `--ignore-scripts` then manually rebuild: `npm rebuild`
446
470
 
471
+ **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:
472
+
473
+ ```bash
474
+ cd /workspace/<dirname>
475
+ if [ -f prisma/schema.prisma ]; then
476
+ echo "Prisma schema detected — initializing database..."
477
+ npx prisma db push --skip-generate 2>/dev/null || npx prisma migrate deploy 2>/dev/null || echo "WARN: skip db init"
478
+ fi
479
+ ```
480
+
481
+ > `--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.
482
+
447
483
  #### 4c: Build
448
484
 
449
485
  **Timeout strategy by framework type:**
@@ -587,6 +623,17 @@ chmod -R +x "$REAL_ROOT/node_modules/.bin" 2>/dev/null || true
587
623
 
588
624
  This prevents the most common deployment failure: nginx 500 with `stat() ... Permission denied` caused by missing `o+x` on intermediate directories.
589
625
 
626
+ #### 4e: Write Deployment Fingerprint
627
+
628
+ After a successful build, write a deployment fingerprint into the output directory. This enables `deploy_check` to verify the nginx-served content belongs to the current deployment (not a stale process from an earlier session):
629
+
630
+ ```bash
631
+ echo "deployed-$(date +%s)-<dirname>" > /workspace/<dirname>/<outputDir>/.deploy_fingerprint
632
+ chmod o+r /workspace/<dirname>/<outputDir>/.deploy_fingerprint 2>/dev/null || true
633
+ ```
634
+
635
+ > For SSR proxy type, nginx does not serve static files from `.next` — the fingerprint check will gracefully SKIP in `deploy_check`. The fingerprint is still written as a deployment marker for debugging.
636
+
590
637
  ### Step 5: Port Availability Check
591
638
 
592
639
  **Before configuring nginx or starting the app**, verify the target ports are free. Port conflicts from previous deployments cause silent failures:
@@ -654,7 +701,7 @@ Use `huaweicloud_sandbox_deploy_nginx` to write the correct template, fix direct
654
701
  - `port` — listen port from framework detection
655
702
  - `project` — project dir name under `/workspace`
656
703
  - `output_dir` — build output dir relative to `/workspace/<project>`
657
- - `node_port` — Node.js app port (required only when `nginx_type=proxy`)
704
+ - `node_port` — Node.js app port for SSR proxy. Defaults to `<port> + 1` if omitted, and the result includes `nodePort` so you know which port to bind the Node process to.
658
705
  - `public_port` — public listen port for SSR proxy (optional, defaults to `port`)
659
706
 
660
707
  The tool automatically handles:
@@ -682,7 +729,21 @@ If the status code is not 2xx/3xx:
682
729
  ### Step 6: Start the App [REQUIRED]
683
730
 
684
731
  - **Static (SPA/SSG/cross-platform)**: nginx is already serving. Skip.
685
- - **SSR**: run `<serveCmd>` via `exec_with_session` to start the Node process in background.
732
+ - **SSR**: `PORT=<nodePort>` prefix is REQUIRED before `<serveCmd>`. nginx `proxy_pass` targets `<nodePort>`, not `<port>` — the two must differ. `deployNginx` returns `nodePort` in its result (defaults to `<port> + 1` for proxy type).
733
+
734
+ **Runtime environment variables**: SSR apps often need `DATABASE_URL`, `NEXTAUTH_URL`, `NEXTAUTH_SECRET`, etc. at runtime. Before starting, verify env vars from Step 4a are still loaded, and re-source `.env` if the shell session was reset:
735
+
736
+ ```bash
737
+ cd /workspace/<dirname>
738
+ # Re-load env vars (SSR apps need them at runtime, not just build time)
739
+ if [ -f .env ]; then set -a && source .env 2>/dev/null; set +a; echo "Loaded .env"; fi
740
+ if [ -f .env.local ]; then set -a && source .env.local 2>/dev/null; set +a; echo "Loaded .env.local"; fi
741
+ # Verify critical vars
742
+ echo "DATABASE_URL=${DATABASE_URL:-<NOT SET>}"
743
+ echo "NEXTAUTH_URL=${NEXTAUTH_URL:-<NOT SET>}"
744
+ ```
745
+
746
+ Then start with `PORT=<nodePort> <serveCmd>` via `exec_with_session` to run the Node process in background.
686
747
 
687
748
  ### Step 7: Expose via DevBridge [REQUIRED — deployment incomplete without this]
688
749
 
@@ -771,6 +832,8 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
771
832
  | Call deploy_check before success | Always call `huaweicloud_sandbox_deploy_check` before reporting deployment success. A green nginx status does not mean the tunnel is accessible — verify end-to-end with the tool. |
772
833
  | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
773
834
  | Long commands prefer one-shot | `exec_one_shot` creates a fresh connection per call — more stable for builds, installs, and scripts >30s. See [Tool Selection Guide](#tool-selection-guide). |
835
+ | SSR nginx/Node ports must differ | nginx `proxy_pass` targets `<nodePort>`, not `<port>`. `deploy_nginx` auto-defaults `nodePort` to `<port>+1` — always start the Node process with `PORT=<nodePort>` to match. Same-port = EADDRINUSE. |
836
+ | HTTP 200 ≠ correct content | A green HTTP check does not guarantee the right project is serving — old processes from a previous session bound to the same port will still return 200. `deploy_check` verifies the deployment fingerprint to catch this. |
774
837
  | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
775
838
  | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
776
839
  | Projects live in `/workspace` | Clone/install project code under `/workspace/<repo-name>` (filesystem-root workspace mount, not `$HOME/workspace`), never in `/tmp` — ephemeral locations lose the project when the sandbox session restarts |
@@ -42,6 +42,7 @@ sudo tee /etc/nginx/conf.d/app.conf > /dev/null << 'NGINX_EOF'
42
42
  server {
43
43
  listen <publicPort>;
44
44
  server_name _;
45
+ large_client_header_buffers 4 32k;
45
46
 
46
47
  location / {
47
48
  proxy_pass http://127.0.0.1:<nodePort>;
@@ -54,6 +55,9 @@ server {
54
55
  proxy_set_header X-Forwarded-Proto $scheme;
55
56
  proxy_cache_bypass $http_upgrade;
56
57
  proxy_read_timeout 60s;
58
+ proxy_buffer_size 128k;
59
+ proxy_buffers 4 256k;
60
+ proxy_busy_buffers_size 256k;
57
61
  }
58
62
  }
59
63
  NGINX_EOF
@@ -157,10 +157,13 @@ function readPkg(projectPath) {
157
157
  }
158
158
  }
159
159
 
160
- function detectPackageManager(projectPath) {
160
+ function detectPackageManager(projectPath, pkg) {
161
161
  if (existsSync(join(projectPath, 'pnpm-lock.yaml'))) return 'pnpm';
162
162
  if (existsSync(join(projectPath, 'yarn.lock'))) return 'yarn';
163
163
  if (existsSync(join(projectPath, 'package-lock.json'))) return 'npm';
164
+ if (existsSync(join(projectPath, 'bun.lockb'))) return 'bun';
165
+ const declared = (pkg?.packageManager || '').split('@')[0];
166
+ if (declared && ['pnpm', 'yarn', 'npm', 'bun'].includes(declared)) return declared;
164
167
  return 'npm';
165
168
  }
166
169
 
@@ -234,10 +237,24 @@ function frameworkResult(fw, pm, projectPath) {
234
237
  return patchCommands({ ...fw, packageManager: pm, rootDir: projectPath }, pm);
235
238
  }
236
239
 
240
+ function readVitepressOutDir(projectPath) {
241
+ const configFiles = ['config.mts', 'config.ts', 'config.mjs', 'config.js'];
242
+ for (const cf of configFiles) {
243
+ const configPath = join(projectPath, '.vitepress', cf);
244
+ if (!existsSync(configPath)) continue;
245
+ try {
246
+ const content = readFileSync(configPath, 'utf8');
247
+ const match = content.match(/outDir\s*:\s*['"]([^'"]+)['"]/);
248
+ if (match) return match[1];
249
+ } catch {}
250
+ }
251
+ return null;
252
+ }
253
+
237
254
  export function detectFramework(projectPath) {
238
255
  const pkg = readPkg(projectPath);
239
256
  const deps = collectDeps(pkg);
240
- const pm = detectPackageManager(projectPath);
257
+ const pm = detectPackageManager(projectPath, pkg);
241
258
 
242
259
  if (
243
260
  existsSync(join(projectPath, 'pnpm-workspace.yaml')) ||
@@ -287,7 +304,12 @@ export function detectFramework(projectPath) {
287
304
  }
288
305
 
289
306
  if (existsSync(join(projectPath, '.vitepress'))) {
290
- return frameworkResult(FRAMEWORKS.vitepress, pm, projectPath);
307
+ const outDir = readVitepressOutDir(projectPath);
308
+ return frameworkResult(
309
+ outDir ? { ...FRAMEWORKS.vitepress, outputDir: outDir } : FRAMEWORKS.vitepress,
310
+ pm,
311
+ projectPath,
312
+ );
291
313
  }
292
314
 
293
315
  if (
@@ -1,4 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
2
5
 
3
6
  import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
4
7
  import { getProxySettings } from './proxy/proxy-config.mjs';
@@ -53,10 +56,19 @@ export async function runHcloud(args, options = {}) {
53
56
  throw new Error('Unreachable retry state.');
54
57
  }
55
58
 
59
+ function discoverHcloudPath() {
60
+ if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
61
+ const candidates =
62
+ process.platform === 'win32'
63
+ ? [join(homedir(), 'hcloud', 'hcloud.exe')]
64
+ : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
65
+ return candidates.find((c) => existsSync(c)) || null;
66
+ }
67
+
56
68
  function runHcloudOnce(plan, options) {
57
69
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
58
70
  const forceKillAfterMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_AFTER_MS;
59
- const executable = options.executable || options.env?.HCLOUD_BIN || process.env.HCLOUD_BIN || 'hcloud';
71
+ const executable = options.executable || options.env?.HCLOUD_BIN || discoverHcloudPath() || 'hcloud';
60
72
  const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
61
73
  const cwd = options.cwd || undefined;
62
74
 
@@ -7,6 +7,12 @@ const HDKIT_BASE_URL =
7
7
  async function hdkitRequest(method, path, body, timeoutMs = 300000) {
8
8
  const { ak, sk, securitytoken } = getCredentials();
9
9
 
10
+ if (!ak || !sk) {
11
+ throw new Error(
12
+ 'Huawei Cloud credentials are not configured. ' +
13
+ 'Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
14
+ );
15
+ }
10
16
  const headers = {
11
17
  'Content-Type': 'application/json',
12
18
  'X-HW-AK': ak,
@@ -49,10 +55,12 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
49
55
  }
50
56
 
51
57
  if (!resp.ok) {
52
- const err = new Error(data.message || `hdkitservice error: ${data.code || resp.status}`);
53
- err.code = data.code;
58
+ const code = data.code || `HTTP_${resp.status}`;
59
+ const trace = data.traceId ? ` [trace: ${data.traceId}]` : '';
60
+ const err = new Error(`${code}: ${data.message || 'hdkitservice error'}${trace}`);
61
+ err.code = code;
54
62
  err.status = resp.status;
55
- err.traceId = data.traceId; // 后端实际返回驼峰 traceId
63
+ err.traceId = data.traceId;
56
64
  throw err;
57
65
  }
58
66
 
@@ -694,7 +694,7 @@ export async function deployNginx(
694
694
  workspaceId,
695
695
  { nginxType, port, project, outputDir, nodePort, publicPort, configName },
696
696
  username = 'root',
697
- timeoutMs = 30000,
697
+ timeoutMs = 60000,
698
698
  ) {
699
699
  if (!workspaceId) {
700
700
  throw new Error('sandbox deploy nginx: workspace_id is required.');
@@ -703,6 +703,53 @@ export async function deployNginx(
703
703
  throw new Error('sandbox deploy nginx: nginxType, port, project, and outputDir are required.');
704
704
  }
705
705
 
706
+ const nginxCheck = await execOneShot(
707
+ workspaceId,
708
+ 'command -v nginx >/dev/null 2>&1 && echo "INSTALLED" || echo "MISSING"',
709
+ username,
710
+ 10000,
711
+ );
712
+ if (!String(nginxCheck.stdout || '').includes('INSTALLED')) {
713
+ throw new Error(
714
+ 'sandbox deploy nginx: nginx is not installed. Install it first:\n' +
715
+ ' Detect OS: source /etc/os-release && echo $ID\n' +
716
+ ' apt: sudo apt-get update -qq && sudo apt-get install -y -qq nginx\n' +
717
+ ' yum: sudo yum install -y nginx\n' +
718
+ ' dnf: sudo dnf install -y nginx\n' +
719
+ 'Alternatively, skip nginx and use Python HTTP server (see nginx-templates.md).',
720
+ );
721
+ }
722
+
723
+ const listenPort = publicPort || port;
724
+ const basePort = nginxType === 'proxy' ? listenPort : port;
725
+
726
+ let targetPort = basePort;
727
+ let portWarning;
728
+ const maxPortAttempts = 10;
729
+ for (let offset = 0; offset < maxPortAttempts; offset += 1) {
730
+ targetPort = basePort + offset;
731
+ try {
732
+ const portCheck = await execOneShot(
733
+ workspaceId,
734
+ `ss -tlnp 2>/dev/null | grep -q ":${targetPort} " && echo "IN_USE" || echo "FREE"`,
735
+ username,
736
+ 10000,
737
+ );
738
+ if (!String(portCheck.stdout || '').includes('IN_USE')) break;
739
+ if (offset === 0) {
740
+ portWarning = `Port ${basePort} is in use — auto-assigned port ${targetPort}`;
741
+ }
742
+ } catch {}
743
+ if (offset === maxPortAttempts - 1) {
744
+ throw new Error(
745
+ `sandbox deploy nginx: all ports ${basePort}-${basePort + maxPortAttempts - 1} are in use. Free a port and try again.`,
746
+ );
747
+ }
748
+ }
749
+
750
+ const effectiveNodePort =
751
+ nginxType === 'proxy' ? (nodePort && nodePort !== listenPort ? nodePort : listenPort + 1) : undefined;
752
+
706
753
  const projectPath = `/workspace/${project}`;
707
754
  const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
708
755
 
@@ -715,7 +762,7 @@ fi`;
715
762
 
716
763
  const templates = {
717
764
  spa: `server {
718
- listen ${port};
765
+ listen ${targetPort};
719
766
  root ${outputPath};
720
767
  index index.html;
721
768
 
@@ -729,11 +776,12 @@ fi`;
729
776
  }
730
777
  }`,
731
778
  proxy: `server {
732
- listen ${publicPort || port};
779
+ listen ${listenPort};
733
780
  server_name _;
781
+ large_client_header_buffers 4 32k;
734
782
 
735
783
  location / {
736
- proxy_pass http://127.0.0.1:${nodePort};
784
+ proxy_pass http://127.0.0.1:${effectiveNodePort};
737
785
  proxy_http_version 1.1;
738
786
  proxy_set_header Upgrade $http_upgrade;
739
787
  proxy_set_header Connection 'upgrade';
@@ -743,10 +791,13 @@ fi`;
743
791
  proxy_set_header X-Forwarded-Proto $scheme;
744
792
  proxy_cache_bypass $http_upgrade;
745
793
  proxy_read_timeout 60s;
794
+ proxy_buffer_size 128k;
795
+ proxy_buffers 4 256k;
796
+ proxy_busy_buffers_size 256k;
746
797
  }
747
798
  }`,
748
799
  static: `server {
749
- listen ${port};
800
+ listen ${targetPort};
750
801
  root ${outputPath};
751
802
  index index.html;
752
803
 
@@ -776,7 +827,7 @@ fi`;
776
827
  `chmod -R o+rX "$REAL_PROJECT" 2>/dev/null || true`,
777
828
  `find "$REAL_PROJECT" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
778
829
  `find "$REAL_PROJECT" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
779
- `if pgrep -x nginx > /dev/null 2>&1; then sudo nginx -s reload 2>/dev/null; else sudo nginx; fi`,
830
+ `if pgrep -x nginx > /dev/null 2>&1; then sudo nginx -s reload 2>/dev/null || { sudo killall -9 nginx 2>/dev/null; sleep 1; sudo nginx; }; else sudo nginx; fi`,
780
831
  ].join('\n');
781
832
 
782
833
  const result = await execOneShot(workspaceId, cmd, username, timeoutMs);
@@ -795,15 +846,17 @@ fi`;
795
846
  return {
796
847
  ok: result.exitCode === 0,
797
848
  nginxType,
798
- port: nginxType === 'proxy' ? publicPort || port : port,
849
+ port: targetPort,
850
+ nodePort: effectiveNodePort || undefined,
799
851
  outputPath,
800
852
  projectPath,
801
853
  exitCode: result.exitCode,
802
854
  stdout: result.stdout,
803
855
  nextStep: 'expose_via_devbridge',
804
- warning: !tunnelActive
805
- ? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
806
- : undefined,
856
+ warning:
857
+ (!tunnelActive
858
+ ? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
859
+ : portWarning) || undefined,
807
860
  };
808
861
  }
809
862
 
@@ -843,6 +896,22 @@ export async function deployCheck(
843
896
  `fi`,
844
897
  ``,
845
898
  `TOTAL=$((TOTAL+1))`,
899
+ `FINGERPRINT_FILE="${outputPath}/.deploy_fingerprint"`,
900
+ `FINGERPRINT_EXPECTED=$(cat "$FINGERPRINT_FILE" 2>/dev/null)`,
901
+ `if [ -n "$FINGERPRINT_EXPECTED" ]; then`,
902
+ ` FINGERPRINT_ACTUAL=$(curl -s http://localhost:${port}/.deploy_fingerprint 2>/dev/null)`,
903
+ ` if [ "$FINGERPRINT_EXPECTED" = "$FINGERPRINT_ACTUAL" ]; then`,
904
+ ` echo "content_verified:PASS"`,
905
+ ` PASS=$((PASS+1))`,
906
+ ` else`,
907
+ ` echo "content_verified:FAIL (fingerprint mismatch — nginx may be serving stale content from a previous deployment)"`,
908
+ ` fi`,
909
+ `else`,
910
+ ` echo "content_verified:SKIP (no fingerprint file)"`,
911
+ ` TOTAL=$((TOTAL-1))`,
912
+ `fi`,
913
+ ``,
914
+ `TOTAL=$((TOTAL+1))`,
846
915
  `if devbridge ls 2>/dev/null | grep -q "Tunnel URL"; then`,
847
916
  ` echo "devbridge_tunnel:PASS"`,
848
917
  ` PASS=$((PASS+1))`,
@@ -552,7 +552,7 @@ export const TOOL_DEFINITIONS = [
552
552
  'Workspace ID from huaweicloud_sandbox_connect return value. Required - must be passed explicitly when HW_WORKSPACE_ID is not set.',
553
553
  },
554
554
  username: { type: 'string', description: 'Login username (default: root)' },
555
- timeout_ms: { type: 'number', description: 'Per-command execution timeout in milliseconds (default: 120000)' },
555
+ timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 60000)' },
556
556
  },
557
557
  },
558
558
  },
@@ -905,7 +905,7 @@ export async function callTool(name, args = {}) {
905
905
  );
906
906
  }
907
907
  const sandboxUser7 = args.username || 'root';
908
- const sandboxTimeout7 = args.timeout_ms || 30000;
908
+ const sandboxTimeout7 = args.timeout_ms || 60000;
909
909
  return await deployNginx(
910
910
  sandboxWsId7,
911
911
  {
@@ -962,7 +962,8 @@ export async function callTool(name, args = {}) {
962
962
  return connectResult;
963
963
  }
964
964
  case 'huaweicloud_sandbox_credentials': {
965
- const credResult = await hdkitCredentials(args.session_id, args.dev_stage_id, args.enable_sts !== false);
965
+ const devStageId = args.dev_stage_id || getCurrentWorkspaceId();
966
+ const credResult = await hdkitCredentials(args.session_id, devStageId, args.enable_sts !== false);
966
967
  const sandboxWsIdCred = args.dev_stage_id || getCurrentWorkspaceId();
967
968
  if (sandboxWsIdCred) {
968
969
  try {