huaweicloud-devkit 1.1.9-next.1 → 1.1.9-next.2

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.1-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **[中文](README.zh-CN.md) | English**
10
10
 
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.1-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **中文 | [English](README.md)**
10
10
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
3
  "mcpName": "io.github.huaweicloud/huaweicloud-devkit",
4
- "version": "1.1.9-next.1",
4
+ "version": "1.1.9-next.2",
5
5
  "kooCliVersion": "7.3.12",
6
6
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
7
7
  "type": "module",
@@ -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.9-next.1",
20
+ "version": "1.1.9-next.2",
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.9-next.1",
23
+ "version": "1.1.9-next.2",
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.9-next.1",
20
+ "version": "1.1.9-next.2",
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.9-next.1",
3
+ "version": "1.1.9-next.2",
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.9-next.1",
3
+ "version": "1.1.9-next.2",
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.9-next.1",
5
+ "version": "1.1.9-next.2",
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.",
@@ -942,7 +942,7 @@ The tool checks:
942
942
  - **nginx_serving** — nginx responds with 2xx/3xx on the app port
943
943
  - **output_dir** — build output directory exists and is non-empty
944
944
  - **devbridge_tunnel** — DevBridge tunnel is active (version-aware: 0.1.x JSON / 0.2.x table)
945
- - **tunnel_url_accessible** — tunnel URL returns 200/304 with real content; a gateway migration placeholder page (「服务已迁移」 with HTTP 200) counts as FAIL
945
+ - **tunnel_url_accessible** — tunnel URL returns 200/304 with real content; these all count as FAIL: a gateway migration placeholder page (「服务已迁移」 with HTTP 200), `tunnel not found` (404 — no live host session on the gateway, even if the tunnel still appears in `devbridge list`), `port not configured` (404 — URL port not registered on the tunnel), and `bad gateway` (502 — tunnel alive but the local service is down). The check probes every listed tunnel and selects one that actually serves the port.
946
946
  - **qr_code** (cross-platform only) — QR image exists in output dir
947
947
 
948
948
  Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
@@ -965,36 +965,37 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
965
965
 
966
966
  ## Critical Warnings
967
967
 
968
- | Trap | Why |
969
- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
970
- | Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
971
- | Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
972
- | Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
973
- | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
974
- | devbridge auth: probe the build | `0.2.2-release` has two builds: image builds retain AK/SK login + env auto-read (fully automatic via `/tmp/hw_creds.sh`); release builds accept only API Key (`/tmp/hw_api_key`). Never assume — probe with `devbridge auth login --help 2>&1 \| grep -q -- '--access-key'` (Step 1 of "Expose the deployed app") |
975
- | devbridge 0.1.x is dead | Sandboxes created before Sep 2026 ship 0.1.13, which connects to a migrated gateway serving a 「服务已迁移」 placeholder with HTTP 200. Check `devbridge version` first and upgrade in place (Step 0) — old tunnels never survive the upgrade |
976
- | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win |
977
- | Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment |
978
- | Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
979
- | Deploy is not just nginx | Configuring nginx does NOT complete the deployment. Steps 7 (DevBridge expose) and deploy_check are REQUIRED — `deploy_nginx` returns `nextStep: expose_via_devbridge` as a reminder. Do not stop after nginx. |
980
- | 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. |
981
- | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
982
- | 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). |
983
- | 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. |
984
- | 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. |
985
- | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
986
- | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
987
- | 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 |
988
- | Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects |
989
- | Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB) |
990
- | Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket); if Node.js is missing, install it from the Huawei Cloud mirror (see "Node.js in the sandbox") |
991
- | Sandbox restart kills processes | After sandbox restarts, all user processes (nginx, Node.js, Python servers) are stopped. Re-run startup commands and verify ports are listening before proceeding. |
992
- | Cross-platform binaries incompatible | The sandbox runs Linux. Native binaries built on Windows/macOS (e.g., Prisma client, `node_modules/.prisma/`, platform-specific native addons) will not execute. Always install and build dependencies inside the sandbox, not locally. |
993
- | Cross-platform needs QR code | When `detect_framework` returns `type: "cross-platform"` (Taro, uni-app), generating a QR code image is **mandatory** — the deployment is incomplete without it. Check the Deployment Completion Check table in Step 7. |
994
- | Build fails do NOT auto-fix | When a build exits with non-zero exit code, STOP and present the error + fix options to the developer. Do not silently retry, modify configs, or change source files without explicit approval. See 4c-aux. |
995
- | Tunnel description: no spaces/hyphens | `devbridge create <name> -d 'hello world'` fails with `Invalid tunnel description: only Chinese characters, digits, letters, length 0-64`. The message is misleading — the real rule is no spaces/hyphens. Omit `-d` or use bare letters/digits. |
996
- | Use `host`, never `connect` | `devbridge connect` is the sender side (for on-prem machines dialing out). For public preview it loops `Connection failed, retrying...` forever with no hint. Always use `devbridge host <tunnelId> -p <port>`. |
997
- | Port drift detaches the tunnel | When deploy_nginx auto-increments to a free port, an existing tunnel stays bound to the old port — re-bind: `devbridge port create <tunnelId> -p <newPort> --protocol http -a` and restart `devbridge host`. deploy_nginx emits a warning when this happens. |
968
+ | Trap | Why |
969
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
970
+ | Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
971
+ | Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
972
+ | Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
973
+ | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
974
+ | devbridge auth: probe the build | `0.2.2-release` has two builds: image builds retain AK/SK login + env auto-read (fully automatic via `/tmp/hw_creds.sh`); release builds accept only API Key (`/tmp/hw_api_key`). Never assume — probe with `devbridge auth login --help 2>&1 \| grep -q -- '--access-key'` (Step 1 of "Expose the deployed app") |
975
+ | devbridge 0.1.x is dead | Sandboxes created before Sep 2026 ship 0.1.13, which connects to a migrated gateway serving a 「服务已迁移」 placeholder with HTTP 200. Check `devbridge version` first and upgrade in place (Step 0) — old tunnels never survive the upgrade |
976
+ | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win |
977
+ | Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment |
978
+ | Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
979
+ | Deploy is not just nginx | Configuring nginx does NOT complete the deployment. Steps 7 (DevBridge expose) and deploy_check are REQUIRED — `deploy_nginx` returns `nextStep: expose_via_devbridge` as a reminder. Do not stop after nginx. |
980
+ | 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. |
981
+ | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
982
+ | 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). |
983
+ | 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. |
984
+ | 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. |
985
+ | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
986
+ | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
987
+ | 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 |
988
+ | Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects |
989
+ | Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB) |
990
+ | Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket); if Node.js is missing, install it from the Huawei Cloud mirror (see "Node.js in the sandbox") |
991
+ | Sandbox restart kills processes | After sandbox restarts, all user processes (nginx, Node.js, Python servers) are stopped. Re-run startup commands and verify ports are listening before proceeding. |
992
+ | Cross-platform binaries incompatible | The sandbox runs Linux. Native binaries built on Windows/macOS (e.g., Prisma client, `node_modules/.prisma/`, platform-specific native addons) will not execute. Always install and build dependencies inside the sandbox, not locally. |
993
+ | Cross-platform needs QR code | When `detect_framework` returns `type: "cross-platform"` (Taro, uni-app), generating a QR code image is **mandatory** — the deployment is incomplete without it. Check the Deployment Completion Check table in Step 7. |
994
+ | Build fails do NOT auto-fix | When a build exits with non-zero exit code, STOP and present the error + fix options to the developer. Do not silently retry, modify configs, or change source files without explicit approval. See 4c-aux. |
995
+ | Tunnel description: no spaces/hyphens | `devbridge create <name> -d 'hello world'` fails with `Invalid tunnel description: only Chinese characters, digits, letters, length 0-64`. The message is misleading — the real rule is no spaces/hyphens. Omit `-d` or use bare letters/digits. |
996
+ | Use `host`, never `connect` | `devbridge connect` is the sender side (for on-prem machines dialing out). For public preview it loops `Connection failed, retrying...` forever with no hint. Always use `devbridge host <tunnelId> -p <port>`. |
997
+ | Port drift detaches the tunnel | When deploy_nginx auto-increments to a free port, an existing tunnel stays bound to the old port — re-bind: `devbridge port create <tunnelId> -p <newPort> --protocol http -a` and restart `devbridge host`. deploy_nginx emits a warning when this happens. |
998
+ | 404 "tunnel not found" ≠ content issue | The s2 gateway answers 404 with body `tunnel not found` when the host process is dead (the tunnel record may STILL show in `devbridge list`) and `port not configured` for an unregistered URL port — both mean the URL is dead, not that the app returned 404. `expose_tunnel`/`deploy_check` verify the probe body and refuse such URLs; a shared sandbox's second deployment also kills earlier URLs (global pre-clean), so re-expose and hand over the fresh URL. |
998
999
 
999
1000
  ## Node.js in the sandbox
1000
1001
 
@@ -28,6 +28,17 @@ const execFileAsync = promisify(execFile);
28
28
  const DEVBRIDGE_TUNNEL_DOMAIN = 'devbridge-s2.hwtunnel.com';
29
29
  // Migration placeholder page marker — a tunnel URL returning this body must be treated as unreachable.
30
30
  const DEVBRIDGE_MIGRATION_MARKER = '服务已迁移';
31
+ // Gateway-level tunnel error markers (verified against the live s2 gateway, Oct 2026).
32
+ // The gateway answers with exact text/plain bodies:
33
+ // 404 "tunnel not found" — no live host session on the gateway (host process dead or
34
+ // tunnel deleted); the tunnel RECORD may still exist in `list`
35
+ // 404 "port not configured" — the URL's port is not registered on the tunnel
36
+ // 502 "bad gateway" — host session alive, but the registered port has no local service
37
+ // A bare status check cannot distinguish these from app-generated 404/502 responses, so the
38
+ // checks below match the exact body AND gate on the HTTP status code.
39
+ const DEVBRIDGE_TUNNEL_NOT_FOUND_MARKER = 'tunnel not found';
40
+ const DEVBRIDGE_PORT_NOT_CONFIGURED_MARKER = 'port not configured';
41
+ const DEVBRIDGE_BAD_GATEWAY_MARKER = 'bad gateway';
31
42
 
32
43
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
44
  export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
@@ -989,31 +1000,49 @@ export function buildDeployCheckScript({ port, project, outputPath, isCrossPlatf
989
1000
  `fi`,
990
1001
  ``,
991
1002
  `TOTAL=$((TOTAL+1))`,
992
- `TUNNEL_ID=$(devbridge list -j 2>/dev/null | grep -oP '"tunnelId":\\s*"\\K[^"]+' | head -1)`,
993
- `if [ -z "$TUNNEL_ID" ]; then`,
994
- ` TUNNEL_ID=$(devbridge list 2>/dev/null | grep -E '^[a-z2-7]{8}[[:space:]]' | awk '{print $1}' | head -1)`,
1003
+ `TUNNEL_IDS=$(devbridge list -j 2>/dev/null | grep -oP '"tunnelId":\\s*"\\K[^"]+' | head -5)`,
1004
+ `if [ -z "$TUNNEL_IDS" ]; then`,
1005
+ ` TUNNEL_IDS=$(devbridge list 2>/dev/null | grep -E '^[a-z2-7]{8}[[:space:]]' | awk '{print $1}' | head -5)`,
995
1006
  `fi`,
996
- `TUNNEL_URL="https://\${TUNNEL_ID}-${port}.${DEVBRIDGE_TUNNEL_DOMAIN}"`,
1007
+ `# Gateway-level tunnel errors have exact text/plain bodies (verified on the s2`,
1008
+ `# gateway): 404 "tunnel not found" = no live host session (the tunnel RECORD may`,
1009
+ `# still be in \`list\`); 404 "port not configured" = URL port not registered; 502`,
1010
+ `# "bad gateway" = tunnel alive, local service down. grep -x (exact-line match) +`,
1011
+ `# status gating avoids flagging app-generated 404/502 pages.`,
997
1012
  `probe_tunnel() {`,
998
1013
  ` local url="$1" code`,
999
- ` code=$(curl -s -o /tmp/.dc_tunnel_body -w "%{http_code}" --max-time 10 "$url" 2>/dev/null)`,
1000
- ` [ "$code" = "000" ] && return 1`,
1001
- ` # A migrated gateway serves a placeholder page with HTTP 200 — treat it as unreachable.`,
1002
- ` if grep -q "${DEVBRIDGE_MIGRATION_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then return 1; fi`,
1003
- ` # Any non-000 HTTP code means the tunnel is connected and nginx is serving`,
1004
- ` # (404/502 = content issue, not a connectivity issue — consistent with nginx_serving).`,
1014
+ ` code=$(curl -s -o /tmp/.dc_tunnel_body -w "%{http_code}" --max-time 5 "$url" 2>/dev/null)`,
1015
+ ` PROBE_FAIL_REASON=""`,
1016
+ ` if [ "$code" = "000" ]; then PROBE_FAIL_REASON="unreachable"; rm -f /tmp/.dc_tunnel_body; return 1; fi`,
1017
+ ` if grep -q "${DEVBRIDGE_MIGRATION_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then PROBE_FAIL_REASON="migration placeholder page"; rm -f /tmp/.dc_tunnel_body; return 1; fi`,
1018
+ ` if [ "$code" = "404" ] && grep -qx "${DEVBRIDGE_TUNNEL_NOT_FOUND_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then PROBE_FAIL_REASON="tunnel not found (no live host session on the gateway)"; rm -f /tmp/.dc_tunnel_body; return 1; fi`,
1019
+ ` if [ "$code" = "404" ] && grep -qx "${DEVBRIDGE_PORT_NOT_CONFIGURED_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then PROBE_FAIL_REASON="port not configured (URL port not registered on the tunnel)"; rm -f /tmp/.dc_tunnel_body; return 1; fi`,
1020
+ ` if [ "$code" = "502" ] && grep -qx "${DEVBRIDGE_BAD_GATEWAY_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then PROBE_FAIL_REASON="bad gateway (tunnel alive, local service not serving)"; rm -f /tmp/.dc_tunnel_body; return 1; fi`,
1005
1021
  ` rm -f /tmp/.dc_tunnel_body`,
1006
1022
  ` return 0`,
1007
1023
  `}`,
1008
- `if [ -n "$TUNNEL_ID" ] && probe_tunnel "$TUNNEL_URL"; then`,
1009
- ` echo "tunnel_url_accessible:PASS ($TUNNEL_URL)"`,
1024
+ `# Probe each listed tunnel (bounded to 5) and select the first one that actually`,
1025
+ `# serves this port — \`devbridge list\` order is arbitrary, so a stale tunnel listed`,
1026
+ `# first must not shadow the live one.`,
1027
+ `SELECTED_URL=""`,
1028
+ `FIRST_ID=""`,
1029
+ `for CAND in $TUNNEL_IDS; do`,
1030
+ ` if [ -z "$FIRST_ID" ]; then FIRST_ID="$CAND"; fi`,
1031
+ ` CAND_URL="https://\${CAND}-${port}.${DEVBRIDGE_TUNNEL_DOMAIN}"`,
1032
+ ` if probe_tunnel "$CAND_URL"; then`,
1033
+ ` SELECTED_URL="$CAND_URL"`,
1034
+ ` break`,
1035
+ ` fi`,
1036
+ `done`,
1037
+ `TUNNEL_URL="$SELECTED_URL"`,
1038
+ `if [ -n "$SELECTED_URL" ]; then`,
1039
+ ` echo "tunnel_url_accessible:PASS ($SELECTED_URL)"`,
1010
1040
  ` PASS=$((PASS+1))`,
1011
- `elif [ -n "$TUNNEL_ID" ]; then`,
1012
- ` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> unreachable, tunnel not found, or migration placeholder page)"`,
1041
+ `elif [ -n "$FIRST_ID" ]; then`,
1042
+ ` echo "tunnel_url_accessible:FAIL (https://\${FIRST_ID}-${port}.${DEVBRIDGE_TUNNEL_DOMAIN} -> $PROBE_FAIL_REASON)"`,
1013
1043
  `else`,
1014
1044
  ` echo "tunnel_url_accessible:FAIL (no tunnel)"`,
1015
1045
  `fi`,
1016
- `rm -f /tmp/.dc_tunnel_body 2>/dev/null || true`,
1017
1046
  ``,
1018
1047
  `${`
1019
1048
  TOTAL=$((TOTAL+1))
@@ -1279,13 +1308,19 @@ export function buildDevbridgeExposeScript(port, expiryHours = DEVBRIDGE_HOST_EX
1279
1308
  `if command -v setsid > /dev/null 2>&1; then setsid nohup devbridge host -p ${port} -e ${expiryHours} > ${hostLog} 2>&1 < /dev/null & else nohup devbridge host -p ${port} -e ${expiryHours} > ${hostLog} 2>&1 < /dev/null & fi`,
1280
1309
  `disown 2>/dev/null || true`,
1281
1310
  `sleep 12`,
1282
- `TUNNEL_URL=$(sed -n 's/.*Tunnel URL: *//p' ${hostLog} | tail -1)`,
1283
- `HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
1311
+ // Port-anchored extraction: a multi-port host prints one "Tunnel URL:" line per
1312
+ // port, so a port-blind `sed | tail -1` can pick another port's URL.
1313
+ `TUNNEL_URL=$(grep -oE "https://[A-Za-z0-9_-]+-${port}\\.devbridge-s2\\.hwtunnel\\.com" ${hostLog} | head -1)`,
1314
+ `HTTP_CODE=$(curl -s -o /tmp/.db_probe_body -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
1284
1315
  `echo "DB_HOST_LOG="`,
1285
1316
  `tail -n 5 ${hostLog}`,
1286
1317
  `echo "DB_TUNNEL_URL=$TUNNEL_URL"`,
1287
1318
  `echo "DB_HTTP_CODE=$HTTP_CODE"`,
1288
1319
  `echo "DB_EXPIRY_HOURS=${expiryHours}"`,
1320
+ // First 64 bytes of the probe body (non-sensitive) so gateway-error markers
1321
+ // like "tunnel not found" can be distinguished from app-generated 404/502 pages.
1322
+ `echo "DB_TUNNEL_BODY=$(head -c 64 /tmp/.db_probe_body 2>/dev/null)"`,
1323
+ `rm -f /tmp/.db_probe_body`,
1289
1324
  `echo "DB_QUOTA_ERROR=$(grep -c '10006' ${hostLog} 2>/dev/null || echo 0)"`,
1290
1325
  ].join('\n');
1291
1326
  }
@@ -1317,6 +1352,10 @@ export function buildDevbridgeVerifyScript(tunnelUrl) {
1317
1352
  }
1318
1353
 
1319
1354
  // Parse the raw stdout of buildDevbridgeExposeScript into structured fields.
1355
+ // `gatewayTunnelError` marks a URL the s2 gateway itself reports as dead
1356
+ // (404 "tunnel not found" / "port not configured") — it can never serve content
1357
+ // regardless of the app. `badGateway` (502 "bad gateway") means the tunnel is
1358
+ // alive but the local service on the exposed port is not serving.
1320
1359
  export function parseDevbridgeExposeOutput(stdout) {
1321
1360
  const text = String(stdout || '');
1322
1361
  const getField = (key) => {
@@ -1325,10 +1364,15 @@ export function parseDevbridgeExposeOutput(stdout) {
1325
1364
  };
1326
1365
  const tunnelUrl = getField('DB_TUNNEL_URL') || (text.match(/TUNNEL_URL:(https:\/\/[^\s]+)/) || [])[1] || '';
1327
1366
  const httpCode = getField('DB_HTTP_CODE');
1367
+ const body = getField('DB_TUNNEL_BODY');
1328
1368
  const quotaError = getField('DB_QUOTA_ERROR') === '1' || /10006/.test(text);
1329
1369
  const tunnelId = (tunnelUrl.match(/^https:\/\/([A-Za-z0-9_-]+)-\d+\./) || [])[1] || '';
1330
1370
  const expiryHours = Number(getField('DB_EXPIRY_HOURS')) || undefined;
1331
- return { tunnelUrl, httpCode, quotaError, tunnelId, expiryHours };
1371
+ // Exact-body match gated on the status code: an app's own 404/502 page is never misjudged.
1372
+ const gatewayTunnelError =
1373
+ httpCode === '404' && (body === DEVBRIDGE_TUNNEL_NOT_FOUND_MARKER || body === DEVBRIDGE_PORT_NOT_CONFIGURED_MARKER);
1374
+ const badGateway = httpCode === '502' && body === DEVBRIDGE_BAD_GATEWAY_MARKER;
1375
+ return { tunnelUrl, httpCode, quotaError, tunnelId, expiryHours, gatewayTunnelError, badGateway };
1332
1376
  }
1333
1377
 
1334
1378
  // Parse the raw stdout of buildDevbridgeVerifyScript into structured fields.
@@ -1411,10 +1455,15 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
1411
1455
  if (parsed.quotaError && attempt < maxAttempts) {
1412
1456
  continue;
1413
1457
  }
1414
- // Unreachable (000) or a migrated-gateway placeholder: one retry.
1458
+ // Unreachable (000), migrated-gateway placeholder, or gateway-level tunnel errors
1459
+ // (404 "tunnel not found" / "port not configured"): one cleanup + rebuild retry.
1460
+ // 502 "bad gateway" (host alive, local service down) also gets one retry to cover
1461
+ // slow app starts.
1415
1462
  const unreachable = parsed.httpCode === '000' || parsed.httpCode === '';
1416
1463
  const migration = /服务已迁移/.test(String(result.stdout || ''));
1417
- if (unreachable || migration) {
1464
+ const gatewayTunnelError = parsed.gatewayTunnelError === true;
1465
+ const backendDown = parsed.badGateway === true;
1466
+ if (unreachable || migration || gatewayTunnelError || backendDown) {
1418
1467
  if (attempt === 1) {
1419
1468
  try {
1420
1469
  await execOneShot(
@@ -1426,6 +1475,20 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
1426
1475
  } catch {}
1427
1476
  continue;
1428
1477
  }
1478
+ // Retry exhausted: a URL the gateway itself reports as dead (or one pointing at
1479
+ // the retired pre-migration gateway) must NOT be handed over as a working
1480
+ // deployment link — previously this returned ok:true with a soft warning.
1481
+ if (gatewayTunnelError || migration) {
1482
+ return {
1483
+ ok: false,
1484
+ error: gatewayTunnelError
1485
+ ? 'DevBridge gateway reports the fresh tunnel URL as dead (404 "tunnel not found" / "port not configured") — the host session is not registered on the gateway. A rebuild was already attempted.'
1486
+ : 'DevBridge gateway serves a migration placeholder page (服务已迁移) — the URL points at the retired pre-migration gateway and cannot work.',
1487
+ authMode,
1488
+ httpCode: parsed.httpCode,
1489
+ hostLog: String(result.stdout || '').slice(0, 400),
1490
+ };
1491
+ }
1429
1492
  }
1430
1493
 
1431
1494
  const warnings = [];
@@ -1474,6 +1537,10 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
1474
1537
  }
1475
1538
  }
1476
1539
 
1540
+ if (parsed.badGateway)
1541
+ warnings.push(
1542
+ 'Tunnel reachable but the gateway reports "bad gateway" — the local service on the exposed port is not serving.',
1543
+ );
1477
1544
  if (parsed.httpCode !== '200' && parsed.httpCode !== '304') {
1478
1545
  warnings.push(
1479
1546
  `Tunnel reachable check returned HTTP ${parsed.httpCode} (expected 200/304). The URL may not be serving yet.`,
@@ -961,21 +961,6 @@ async function installOpenCode() {
961
961
  mkdirSync(opcPlugins, { recursive: true });
962
962
  copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
963
963
  console.log(` Plugin -> ${opcPlugins}`);
964
- // OpenCode native plugin (#905 OpenCode performance): bridges all 42 MCP tools
965
- // into OpenCode in-process custom tools (zod schemas + callTool), plus safety
966
- // gate and telemetry. Local plugins are auto-loaded from this directory.
967
- const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
968
- if (existsSync(nativePluginSrc)) {
969
- copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
970
- // Deps for the native plugin (zod) live next to the OpenCode config so the
971
- // plugin can require them at load time.
972
- const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
973
- const nativePkgDst = join(configRoot('opencode'), 'package.json');
974
- if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
975
- copyFileSync(nativePkgSrc, nativePkgDst);
976
- }
977
- console.log(` Native plugin -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
978
- }
979
964
  updateOpenCodeConfig(pluginDest);
980
965
  installRuntimeDeps(pluginDest);
981
966
  }
@@ -1009,11 +994,6 @@ function uninstallOpenCode() {
1009
994
  removeIfExists(pluginFile);
1010
995
  console.log(' Removed plugin');
1011
996
  }
1012
- const nativePluginFile = join(configRoot('opencode'), 'plugins', 'huaweicloud-devkit.js');
1013
- if (existsSync(nativePluginFile)) {
1014
- removeIfExists(nativePluginFile);
1015
- console.log(' Removed native plugin');
1016
- }
1017
997
 
1018
998
  if (removeIfExists(opencodePluginsDir())) {
1019
999
  console.log(' Removed MCP server and safety policy');
@@ -1061,17 +1041,6 @@ async function updateOpenCode() {
1061
1041
  mkdirSync(opcPlugins, { recursive: true });
1062
1042
  copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
1063
1043
  console.log(` Plugin updated -> ${opcPlugins}`);
1064
- // Native plugin refresh (#905 OpenCode performance) — overwrite in place.
1065
- const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
1066
- if (existsSync(nativePluginSrc)) {
1067
- copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
1068
- const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
1069
- const nativePkgDst = join(configRoot('opencode'), 'package.json');
1070
- if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
1071
- copyFileSync(nativePkgSrc, nativePkgDst);
1072
- }
1073
- console.log(` Native plugin updated -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
1074
- }
1075
1044
  updateOpenCodeConfig(pluginDest);
1076
1045
  mkdirSync(pluginDest, { recursive: true });
1077
1046
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
@@ -1,24 +0,0 @@
1
- {
2
- "name": "@huaweicloud-devkit/opencode-integration",
3
- "version": "0.0.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "@huaweicloud-devkit/opencode-integration",
9
- "version": "0.0.0",
10
- "dependencies": {
11
- "zod": "^3.11.0"
12
- }
13
- },
14
- "node_modules/zod": {
15
- "version": "3.25.76",
16
- "resolved": "https://repo.huaweicloud.com/repository/npm/zod/-/zod-3.25.76.tgz",
17
- "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
18
- "license": "MIT",
19
- "funding": {
20
- "url": "https://github.com/sponsors/colinhacks"
21
- }
22
- }
23
- }
24
- }
@@ -1,10 +0,0 @@
1
- {
2
- "name": "@huaweicloud-devkit/opencode-integration",
3
- "version": "0.0.0",
4
- "private": true,
5
- "type": "module",
6
- "description": "Local deps for the huaweicloud-devkit OpenCode plugin (zod for tool schema conversion). Installed into ~/.config/opencode by 'npx huaweicloud-devkit install --target opencode'.",
7
- "dependencies": {
8
- "zod": "^3.11.0"
9
- }
10
- }
@@ -1,194 +0,0 @@
1
- // huaweicloud-devkit OpenCode native plugin
2
- //
3
- // Bridges all 42 MCP tools into OpenCode in-process custom tools via the shared
4
- // callTool() business layer, plus:
5
- // - tool.execute.before safety gate (classifyHcloudArgs / classifyTextCommand)
6
- // - skill-tracker telemetry (superset of the legacy skill-tracker.js plugin)
7
- // - STS command-level injection is inherited from callTool -> runHcloud chain
8
- //
9
- // Install: npm i -g huaweicloud-devkit && npx huaweicloud-devkit install --target opencode
10
- // (this file is copied to ~/.config/opencode/plugins/huaweicloud-devkit.js and
11
- // auto-loaded by OpenCode at startup — local plugins need no config entry)
12
- //
13
- // Compatibility contract (unchanged from MCP mode):
14
- // - tool names stay `huaweicloud_*` (skills/hook matchers keep working)
15
- // - approval gate: run_approved_command still requires args+approvalToken+approvedByUser
16
- // - credentials: resolveCredentials reads env (HW_*) then S1 — same priority as MCP
17
-
18
- import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
19
- import { join, dirname } from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
- import { createRequire } from 'node:module';
22
-
23
- const require = createRequire(import.meta.url);
24
- const z = require('zod');
25
-
26
- // devkit core — resolved from the installed plugin tree next to this file
27
- const selfDir = dirname(fileURLToPath(import.meta.url));
28
- const DEVKIT_SRC = join(selfDir, '..', 'huaweicloud-plugins', 'src');
29
-
30
- let devkit = null;
31
- let loadError = null;
32
- try {
33
- devkit = await import(`file://${join(DEVKIT_SRC, 'tools.mjs')}`);
34
- } catch (error) {
35
- loadError = error;
36
- }
37
-
38
- // ── telemetry (superset of legacy skill-tracker.js) ─────────────────────────
39
-
40
- const TELEMETRY_DIR = join(DEVKIT_SRC, '..', 'telemetry');
41
- function writeEvent(key, value, extra = {}) {
42
- try {
43
- if (!existsSync(TELEMETRY_DIR)) mkdirSync(TELEMETRY_DIR, { recursive: true });
44
- appendFileSync(join(TELEMETRY_DIR, 'hook-events.jsonl'), JSON.stringify({ key, value, ...extra }) + '\n');
45
- } catch {
46
- /* telemetry must never break the host */
47
- }
48
- }
49
-
50
- function isHuaweiCloudSkill(name) {
51
- return typeof name === 'string' && /^huawei/i.test(name);
52
- }
53
-
54
- // ── JSON Schema -> Zod converter (covers the devkit inputSchema subset) ─────
55
-
56
- function jsonSchemaToZod(schema) {
57
- // schema is { type:'object', properties:{...}, required:[...] }
58
- const shape = {};
59
- const required = new Set(schema?.required || []);
60
- for (const [name, prop] of Object.entries(schema?.properties || {})) {
61
- let node;
62
- switch (prop?.type) {
63
- case 'string':
64
- node = z.string();
65
- if (prop.enum) {
66
- const vals = prop.enum.filter((v) => typeof v === 'string');
67
- if (vals.length > 0) node = z.enum(vals);
68
- }
69
- break;
70
- case 'number':
71
- case 'integer':
72
- node = z.number();
73
- break;
74
- case 'boolean':
75
- node = z.boolean();
76
- break;
77
- case 'array':
78
- node = z.array(z.any());
79
- break;
80
- case 'object':
81
- default:
82
- node = z.any(); // nested objects (e.g. git{repo_url}) stay permissive
83
- }
84
- if (prop?.description) node = node.describe(prop.description);
85
- shape[name] = required.has(name) ? node : node.optional();
86
- }
87
- return shape;
88
- }
89
-
90
- // ── safety gate (mirrors the MCP/hook deny semantics) ──────────────────────
91
-
92
- const DENY_TOOLS = new Set([
93
- 'huaweicloud_run_approved_command', // has its own in-tool approval gate (token check)
94
- ]);
95
-
96
- async function safetyGate(toolName, args) {
97
- if (!devkit) return; // devkit failed to load -> tools absent, nothing to gate
98
- // read-only and planning tools pass through: callTool re-runs the full policy
99
- // layer anyway (classifyHcloudArgs inside plan/read paths). The gate below is
100
- // a fast pre-check for the raw hcloud-ish tools only.
101
- if (DENY_TOOLS.has(toolName)) return;
102
- }
103
-
104
- // ── tool bridging ───────────────────────────────────────────────────────────
105
-
106
- function buildBridgedTools() {
107
- if (!devkit) return {};
108
- const tools = {};
109
- const definitions = devkit.TOOL_DEFINITIONS || [];
110
- for (const def of definitions) {
111
- tools[def.name] = {
112
- description: def.description,
113
- args: jsonSchemaToZod(def.inputSchema || { type: 'object', properties: {} }),
114
- async execute(args, context) {
115
- const started = Date.now();
116
- try {
117
- await safetyGate(def.name, args);
118
- const result = await devkit.callTool(def.name, args, { cwd: context?.directory });
119
- writeEvent('tool:invoke', def.name, { durationMs: Date.now() - started });
120
- return typeof result === 'string' ? result : { output: JSON.stringify(result, null, 2) };
121
- } catch (error) {
122
- // crash containment: never take the host down with a tool error
123
- const payload = {
124
- error: error?.message || String(error),
125
- code: error?.code,
126
- action: error?.action || error?.nextStep,
127
- };
128
- writeEvent('tool:error', def.name, { message: payload.error });
129
- return { output: JSON.stringify(payload, null, 2) };
130
- }
131
- },
132
- };
133
- }
134
- return tools;
135
- }
136
-
137
- // ── plugin export (v1 Hooks contract, IDE + CLI dual form) ─────────────────
138
-
139
- function getHooks() {
140
- return {
141
- 'tool.execute.before': function (input, output) {
142
- try {
143
- if (input?.tool === 'skill') {
144
- const name = output?.args?.name;
145
- if (isHuaweiCloudSkill(name)) writeEvent('skill:retrieve', name);
146
- return;
147
- }
148
- if (input?.tool === 'bash') {
149
- const cmd = output?.args?.command || '';
150
- if (!cmd) return;
151
- // CLI read/write classification (legacy skill-tracker behavior)
152
- const isHcloud = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+/i.test(cmd);
153
- if (isHcloud) {
154
- const isRead = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i.test(cmd);
155
- writeEvent(isRead ? 'cli:read' : 'cli:write', cmd.slice(0, 120), { capability: 'cli' });
156
- }
157
- }
158
- } catch {
159
- /* hooks must never throw */
160
- }
161
- },
162
- event: function ({ event }) {
163
- try {
164
- if (event?.type === 'message.part.updated') {
165
- const text = event?.properties?.part?.text;
166
- if (typeof text === 'string') {
167
- const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
168
- if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
169
- }
170
- }
171
- } catch {
172
- /* ignore */
173
- }
174
- },
175
- };
176
- }
177
-
178
- function plugin() {
179
- const hooks = getHooks();
180
- if (loadError) {
181
- // Surface a single actionable line instead of breaking startup
182
- console.error(`[huaweicloud-devkit] core failed to load: ${loadError.message}`);
183
- console.error(`[huaweicloud-devkit] expected devkit src at ${DEVKIT_SRC}`);
184
- console.error(`[huaweicloud-devkit] run: npx huaweicloud-devkit install --target opencode`);
185
- return hooks; // telemetry still works; tools absent until install is re-run
186
- }
187
- hooks.tool = buildBridgedTools();
188
- return hooks;
189
- }
190
-
191
- plugin.id = 'huaweicloud-devkit';
192
- plugin.server = async () => plugin();
193
-
194
- export default plugin;