huaweicloud-devkit 1.1.9-next.1 → 1.1.9-next.3
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +32 -31
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +113 -30
- package/plugins/huaweicloud-core/src/setup-cli.mjs +0 -31
- package/integrations/opencode/package-lock.json +0 -24
- package/integrations/opencode/package.json +0 -10
- package/integrations/opencode/plugins/huaweicloud-devkit.js +0 -194
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](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)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](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.
|
|
4
|
+
"version": "1.1.9-next.3",
|
|
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.
|
|
20
|
+
"version": "1.1.9-next.3",
|
|
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.
|
|
23
|
+
"version": "1.1.9-next.3",
|
|
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.
|
|
20
|
+
"version": "1.1.9-next.3",
|
|
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.
|
|
3
|
+
"version": "1.1.9-next.3",
|
|
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.
|
|
3
|
+
"version": "1.1.9-next.3",
|
|
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.
|
|
5
|
+
"version": "1.1.9-next.3",
|
|
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)
|
|
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
|
|
969
|
-
|
|
|
970
|
-
| Target not confirmed
|
|
971
|
-
| Agreement required first
|
|
972
|
-
| Real-name required
|
|
973
|
-
| Never expose tunnel details
|
|
974
|
-
| devbridge auth: probe the build
|
|
975
|
-
| devbridge 0.1.x is dead
|
|
976
|
-
| CLI PATH
|
|
977
|
-
| Never install tunnel tooling locally
|
|
978
|
-
| Return the deployment URL
|
|
979
|
-
| Deploy is not just nginx
|
|
980
|
-
| Call deploy_check before success
|
|
981
|
-
| Session state persists
|
|
982
|
-
| Long commands prefer one-shot
|
|
983
|
-
| SSR nginx/Node ports must differ
|
|
984
|
-
| HTTP 200 ≠ correct content
|
|
985
|
-
| Destructive commands blocked
|
|
986
|
-
| Workspace ID = dev_stage_id
|
|
987
|
-
| Projects live in `/workspace`
|
|
988
|
-
| Upload project for local code
|
|
989
|
-
| Upload file for single files
|
|
990
|
-
| Node.js >= 22 required
|
|
991
|
-
| Sandbox restart kills processes
|
|
992
|
-
| Cross-platform binaries incompatible
|
|
993
|
-
| Cross-platform needs QR code
|
|
994
|
-
| Build fails do NOT auto-fix
|
|
995
|
-
| Tunnel description: no spaces/hyphens
|
|
996
|
-
| Use `host`, never `connect`
|
|
997
|
-
| Port drift detaches the tunnel
|
|
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
|
-
`
|
|
993
|
-
`if [ -z "$
|
|
994
|
-
`
|
|
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
|
-
|
|
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
|
|
1000
|
-
`
|
|
1001
|
-
`
|
|
1002
|
-
` if grep -q "${DEVBRIDGE_MIGRATION_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then return 1; fi`,
|
|
1003
|
-
`
|
|
1004
|
-
`
|
|
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
|
-
|
|
1009
|
-
|
|
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 "$
|
|
1012
|
-
` echo "tunnel_url_accessible:FAIL (
|
|
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))
|
|
@@ -1058,18 +1087,27 @@ fi
|
|
|
1058
1087
|
.join('\n');
|
|
1059
1088
|
}
|
|
1060
1089
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1090
|
+
// Strip terminal escape sequences from sandbox command output (#946): CSI
|
|
1091
|
+
// color/cursor codes and OSC shell-integration markers (shells with integration
|
|
1092
|
+
// enabled wrap EVERY line in \x1b]133;C\x07, which blinds anchored ^DB_...=
|
|
1093
|
+
// parsers), then normalize \r\n / \r line endings to \n.
|
|
1094
|
+
// Use new RegExp to avoid ESLint no-control-regex on literal control chars.
|
|
1095
|
+
export function stripTerminalEscapes(text) {
|
|
1065
1096
|
const ESC = '\x1b';
|
|
1066
1097
|
const csiRe = new RegExp(ESC + '\\[[0-9;]*[a-zA-Z]', 'g');
|
|
1067
1098
|
const oscRe = new RegExp(ESC + '\\][^' + ESC + '\x07]*(?:\x07|' + ESC + '\\\\)', 'g');
|
|
1068
|
-
|
|
1099
|
+
return String(text || '')
|
|
1069
1100
|
.replace(csiRe, '')
|
|
1070
|
-
.replace(oscRe, '')
|
|
1101
|
+
.replace(oscRe, '')
|
|
1102
|
+
.replace(/\r\n|\r/g, '\n');
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
export function parseDeployCheckOutput(stdout, { port, isCrossPlatform } = {}) {
|
|
1106
|
+
// Strip ANSI escape sequences (CSI color/cursor codes and OSC shell-integration markers)
|
|
1107
|
+
// and split on any line-ending style (\r\n, \r, or \n) to handle all terminal outputs.
|
|
1108
|
+
const cleanStdout = stripTerminalEscapes(stdout);
|
|
1071
1109
|
const checks = {};
|
|
1072
|
-
const lines = cleanStdout.split(
|
|
1110
|
+
const lines = cleanStdout.split('\n');
|
|
1073
1111
|
for (const line of lines) {
|
|
1074
1112
|
const trimmed = line.trim();
|
|
1075
1113
|
const m = trimmed.match(/^(\w+):(PASS|FAIL|SKIP)\b(.*)/);
|
|
@@ -1241,7 +1279,9 @@ export function buildDevbridgeAuthProbe() {
|
|
|
1241
1279
|
// authMode is the LAST DB_AUTH_MODE line; attempts collects the non-sensitive
|
|
1242
1280
|
// DB_ATTEMPT_* diagnostics emitted by tiers that failed and fell through.
|
|
1243
1281
|
export function parseDevbridgeAuthOutput(stdout) {
|
|
1244
|
-
|
|
1282
|
+
// #946: OSC 133 markers precede every line on integration-enabled shells; the
|
|
1283
|
+
// anchored ^DB_ATTEMPT_ regexes are blind to them without stripping first.
|
|
1284
|
+
const text = stripTerminalEscapes(stdout);
|
|
1245
1285
|
const modes = text.match(/DB_AUTH_MODE=([A-Z_]+)/g) || [];
|
|
1246
1286
|
const authMode = modes.length > 0 ? modes[modes.length - 1].split('=')[1] : '';
|
|
1247
1287
|
const attempts = {};
|
|
@@ -1279,13 +1319,19 @@ export function buildDevbridgeExposeScript(port, expiryHours = DEVBRIDGE_HOST_EX
|
|
|
1279
1319
|
`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
1320
|
`disown 2>/dev/null || true`,
|
|
1281
1321
|
`sleep 12`,
|
|
1282
|
-
|
|
1283
|
-
|
|
1322
|
+
// Port-anchored extraction: a multi-port host prints one "Tunnel URL:" line per
|
|
1323
|
+
// port, so a port-blind `sed | tail -1` can pick another port's URL.
|
|
1324
|
+
`TUNNEL_URL=$(grep -oE "https://[A-Za-z0-9_-]+-${port}\\.devbridge-s2\\.hwtunnel\\.com" ${hostLog} | head -1)`,
|
|
1325
|
+
`HTTP_CODE=$(curl -s -o /tmp/.db_probe_body -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
|
|
1284
1326
|
`echo "DB_HOST_LOG="`,
|
|
1285
1327
|
`tail -n 5 ${hostLog}`,
|
|
1286
1328
|
`echo "DB_TUNNEL_URL=$TUNNEL_URL"`,
|
|
1287
1329
|
`echo "DB_HTTP_CODE=$HTTP_CODE"`,
|
|
1288
1330
|
`echo "DB_EXPIRY_HOURS=${expiryHours}"`,
|
|
1331
|
+
// First 64 bytes of the probe body (non-sensitive) so gateway-error markers
|
|
1332
|
+
// like "tunnel not found" can be distinguished from app-generated 404/502 pages.
|
|
1333
|
+
`echo "DB_TUNNEL_BODY=$(head -c 64 /tmp/.db_probe_body 2>/dev/null)"`,
|
|
1334
|
+
`rm -f /tmp/.db_probe_body`,
|
|
1289
1335
|
`echo "DB_QUOTA_ERROR=$(grep -c '10006' ${hostLog} 2>/dev/null || echo 0)"`,
|
|
1290
1336
|
].join('\n');
|
|
1291
1337
|
}
|
|
@@ -1317,18 +1363,30 @@ export function buildDevbridgeVerifyScript(tunnelUrl) {
|
|
|
1317
1363
|
}
|
|
1318
1364
|
|
|
1319
1365
|
// Parse the raw stdout of buildDevbridgeExposeScript into structured fields.
|
|
1366
|
+
// `gatewayTunnelError` marks a URL the s2 gateway itself reports as dead
|
|
1367
|
+
// (404 "tunnel not found" / "port not configured") — it can never serve content
|
|
1368
|
+
// regardless of the app. `badGateway` (502 "bad gateway") means the tunnel is
|
|
1369
|
+
// alive but the local service on the exposed port is not serving.
|
|
1320
1370
|
export function parseDevbridgeExposeOutput(stdout) {
|
|
1321
|
-
|
|
1371
|
+
// #946: strip OSC/CSI escapes + \r BEFORE the anchored ^DB_ lookups — otherwise
|
|
1372
|
+
// every field is lost on integration-enabled shells and a perfectly good tunnel
|
|
1373
|
+
// comes back as ok:true with publicUrl missing (and pointlessly re-built once).
|
|
1374
|
+
const text = stripTerminalEscapes(stdout);
|
|
1322
1375
|
const getField = (key) => {
|
|
1323
1376
|
const m = text.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
1324
1377
|
return m ? m[1].trim() : '';
|
|
1325
1378
|
};
|
|
1326
1379
|
const tunnelUrl = getField('DB_TUNNEL_URL') || (text.match(/TUNNEL_URL:(https:\/\/[^\s]+)/) || [])[1] || '';
|
|
1327
1380
|
const httpCode = getField('DB_HTTP_CODE');
|
|
1381
|
+
const body = getField('DB_TUNNEL_BODY');
|
|
1328
1382
|
const quotaError = getField('DB_QUOTA_ERROR') === '1' || /10006/.test(text);
|
|
1329
1383
|
const tunnelId = (tunnelUrl.match(/^https:\/\/([A-Za-z0-9_-]+)-\d+\./) || [])[1] || '';
|
|
1330
1384
|
const expiryHours = Number(getField('DB_EXPIRY_HOURS')) || undefined;
|
|
1331
|
-
|
|
1385
|
+
// Exact-body match gated on the status code: an app's own 404/502 page is never misjudged.
|
|
1386
|
+
const gatewayTunnelError =
|
|
1387
|
+
httpCode === '404' && (body === DEVBRIDGE_TUNNEL_NOT_FOUND_MARKER || body === DEVBRIDGE_PORT_NOT_CONFIGURED_MARKER);
|
|
1388
|
+
const badGateway = httpCode === '502' && body === DEVBRIDGE_BAD_GATEWAY_MARKER;
|
|
1389
|
+
return { tunnelUrl, httpCode, quotaError, tunnelId, expiryHours, gatewayTunnelError, badGateway };
|
|
1332
1390
|
}
|
|
1333
1391
|
|
|
1334
1392
|
// Parse the raw stdout of buildDevbridgeVerifyScript into structured fields.
|
|
@@ -1337,7 +1395,9 @@ export function parseDevbridgeExposeOutput(stdout) {
|
|
|
1337
1395
|
// parseDevbridgeExposeOutput never matches them, which used to leave
|
|
1338
1396
|
// verifyHttpCode empty and persistent always false.
|
|
1339
1397
|
export function parseDevbridgeVerifyOutput(stdout) {
|
|
1340
|
-
|
|
1398
|
+
// #946: strip OSC/CSI escapes + \r — the anchored DB_VERIFY_* lookups are blind
|
|
1399
|
+
// to them, leaving verifyHttpCode empty and persistent permanently false.
|
|
1400
|
+
const text = stripTerminalEscapes(stdout);
|
|
1341
1401
|
const getField = (key) => {
|
|
1342
1402
|
const m = text.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
1343
1403
|
return m ? m[1].trim() : '';
|
|
@@ -1411,10 +1471,15 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1411
1471
|
if (parsed.quotaError && attempt < maxAttempts) {
|
|
1412
1472
|
continue;
|
|
1413
1473
|
}
|
|
1414
|
-
// Unreachable (000)
|
|
1474
|
+
// Unreachable (000), migrated-gateway placeholder, or gateway-level tunnel errors
|
|
1475
|
+
// (404 "tunnel not found" / "port not configured"): one cleanup + rebuild retry.
|
|
1476
|
+
// 502 "bad gateway" (host alive, local service down) also gets one retry to cover
|
|
1477
|
+
// slow app starts.
|
|
1415
1478
|
const unreachable = parsed.httpCode === '000' || parsed.httpCode === '';
|
|
1416
1479
|
const migration = /服务已迁移/.test(String(result.stdout || ''));
|
|
1417
|
-
|
|
1480
|
+
const gatewayTunnelError = parsed.gatewayTunnelError === true;
|
|
1481
|
+
const backendDown = parsed.badGateway === true;
|
|
1482
|
+
if (unreachable || migration || gatewayTunnelError || backendDown) {
|
|
1418
1483
|
if (attempt === 1) {
|
|
1419
1484
|
try {
|
|
1420
1485
|
await execOneShot(
|
|
@@ -1426,6 +1491,20 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1426
1491
|
} catch {}
|
|
1427
1492
|
continue;
|
|
1428
1493
|
}
|
|
1494
|
+
// Retry exhausted: a URL the gateway itself reports as dead (or one pointing at
|
|
1495
|
+
// the retired pre-migration gateway) must NOT be handed over as a working
|
|
1496
|
+
// deployment link — previously this returned ok:true with a soft warning.
|
|
1497
|
+
if (gatewayTunnelError || migration) {
|
|
1498
|
+
return {
|
|
1499
|
+
ok: false,
|
|
1500
|
+
error: gatewayTunnelError
|
|
1501
|
+
? '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.'
|
|
1502
|
+
: 'DevBridge gateway serves a migration placeholder page (服务已迁移) — the URL points at the retired pre-migration gateway and cannot work.',
|
|
1503
|
+
authMode,
|
|
1504
|
+
httpCode: parsed.httpCode,
|
|
1505
|
+
hostLog: String(result.stdout || '').slice(0, 400),
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1429
1508
|
}
|
|
1430
1509
|
|
|
1431
1510
|
const warnings = [];
|
|
@@ -1474,6 +1553,10 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1474
1553
|
}
|
|
1475
1554
|
}
|
|
1476
1555
|
|
|
1556
|
+
if (parsed.badGateway)
|
|
1557
|
+
warnings.push(
|
|
1558
|
+
'Tunnel reachable but the gateway reports "bad gateway" — the local service on the exposed port is not serving.',
|
|
1559
|
+
);
|
|
1477
1560
|
if (parsed.httpCode !== '200' && parsed.httpCode !== '304') {
|
|
1478
1561
|
warnings.push(
|
|
1479
1562
|
`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;
|