huaweicloud-devkit 1.1.9-next.3 → 1.1.9-next.5
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/integrations/opencode/package-lock.json +24 -0
- package/integrations/opencode/package.json +10 -0
- package/integrations/opencode/plugins/huaweicloud-devkit.js +194 -0
- 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/safety/rules/cloud-risk-rules.json +6 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +31 -32
- package/plugins/huaweicloud-core/src/mcp-config-merge.mjs +1 -1
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +30 -113
- package/plugins/huaweicloud-core/src/setup-cli.mjs +62 -4
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)
|
|
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)
|
|
7
|
+
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**中文 | [English](README.md)**
|
|
10
10
|
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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;
|
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.5",
|
|
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.5",
|
|
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.5",
|
|
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.5",
|
|
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.5",
|
|
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.5",
|
|
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.5",
|
|
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.",
|
|
@@ -429,6 +429,12 @@
|
|
|
429
429
|
"field": "text",
|
|
430
430
|
"regex": "\\bfdisk\\b"
|
|
431
431
|
}
|
|
432
|
+
],
|
|
433
|
+
"none": [
|
|
434
|
+
{
|
|
435
|
+
"field": "text",
|
|
436
|
+
"regex": "\\bhcloud\\b"
|
|
437
|
+
}
|
|
432
438
|
]
|
|
433
439
|
},
|
|
434
440
|
"message": "The sandbox terminal command is destructively dangerous and could destroy the workspace.",
|
|
@@ -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;
|
|
945
|
+
- **tunnel_url_accessible** — tunnel URL returns 200/304 with real content; a gateway migration placeholder page (「服务已迁移」 with HTTP 200) counts as FAIL
|
|
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,37 +965,36 @@ 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
|
|
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. |
|
|
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. |
|
|
999
998
|
|
|
1000
999
|
## Node.js in the sandbox
|
|
1001
1000
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Program-owned fields (the node executable + mcp-server.mjs path, required env
|
|
4
4
|
// keys) are corrected by the installer; everything else belongs to the user.
|
|
5
5
|
|
|
6
|
-
function isPlainObject(value) {
|
|
6
|
+
export function isPlainObject(value) {
|
|
7
7
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -28,17 +28,6 @@ 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';
|
|
42
31
|
|
|
43
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
44
33
|
export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
|
|
@@ -1000,49 +989,31 @@ export function buildDeployCheckScript({ port, project, outputPath, isCrossPlatf
|
|
|
1000
989
|
`fi`,
|
|
1001
990
|
``,
|
|
1002
991
|
`TOTAL=$((TOTAL+1))`,
|
|
1003
|
-
`
|
|
1004
|
-
`if [ -z "$
|
|
1005
|
-
`
|
|
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)`,
|
|
1006
995
|
`fi`,
|
|
1007
|
-
|
|
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.`,
|
|
996
|
+
`TUNNEL_URL="https://\${TUNNEL_ID}-${port}.${DEVBRIDGE_TUNNEL_DOMAIN}"`,
|
|
1012
997
|
`probe_tunnel() {`,
|
|
1013
998
|
` local url="$1" code`,
|
|
1014
|
-
` code=$(curl -s -o /tmp/.dc_tunnel_body -w "%{http_code}" --max-time
|
|
1015
|
-
`
|
|
1016
|
-
`
|
|
1017
|
-
` if grep -q "${DEVBRIDGE_MIGRATION_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then
|
|
1018
|
-
`
|
|
1019
|
-
`
|
|
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`,
|
|
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).`,
|
|
1021
1005
|
` rm -f /tmp/.dc_tunnel_body`,
|
|
1022
1006
|
` return 0`,
|
|
1023
1007
|
`}`,
|
|
1024
|
-
|
|
1025
|
-
|
|
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)"`,
|
|
1008
|
+
`if [ -n "$TUNNEL_ID" ] && probe_tunnel "$TUNNEL_URL"; then`,
|
|
1009
|
+
` echo "tunnel_url_accessible:PASS ($TUNNEL_URL)"`,
|
|
1040
1010
|
` PASS=$((PASS+1))`,
|
|
1041
|
-
`elif [ -n "$
|
|
1042
|
-
` echo "tunnel_url_accessible:FAIL (
|
|
1011
|
+
`elif [ -n "$TUNNEL_ID" ]; then`,
|
|
1012
|
+
` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> unreachable, tunnel not found, or migration placeholder page)"`,
|
|
1043
1013
|
`else`,
|
|
1044
1014
|
` echo "tunnel_url_accessible:FAIL (no tunnel)"`,
|
|
1045
1015
|
`fi`,
|
|
1016
|
+
`rm -f /tmp/.dc_tunnel_body 2>/dev/null || true`,
|
|
1046
1017
|
``,
|
|
1047
1018
|
`${`
|
|
1048
1019
|
TOTAL=$((TOTAL+1))
|
|
@@ -1087,27 +1058,18 @@ fi
|
|
|
1087
1058
|
.join('\n');
|
|
1088
1059
|
}
|
|
1089
1060
|
|
|
1090
|
-
|
|
1091
|
-
// color/cursor codes and OSC shell-integration markers
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
// Use new RegExp to avoid ESLint no-control-regex on literal control chars.
|
|
1095
|
-
export function stripTerminalEscapes(text) {
|
|
1061
|
+
export function parseDeployCheckOutput(stdout, { port, isCrossPlatform } = {}) {
|
|
1062
|
+
// Strip ANSI escape sequences (CSI color/cursor codes and OSC shell-integration markers)
|
|
1063
|
+
// and split on any line-ending style (\r\n, \r, or \n) to handle all terminal outputs.
|
|
1064
|
+
// Use new RegExp to avoid ESLint no-control-regex on literal control chars in regex.
|
|
1096
1065
|
const ESC = '\x1b';
|
|
1097
1066
|
const csiRe = new RegExp(ESC + '\\[[0-9;]*[a-zA-Z]', 'g');
|
|
1098
1067
|
const oscRe = new RegExp(ESC + '\\][^' + ESC + '\x07]*(?:\x07|' + ESC + '\\\\)', 'g');
|
|
1099
|
-
|
|
1068
|
+
const cleanStdout = String(stdout || '')
|
|
1100
1069
|
.replace(csiRe, '')
|
|
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);
|
|
1070
|
+
.replace(oscRe, '');
|
|
1109
1071
|
const checks = {};
|
|
1110
|
-
const lines = cleanStdout.split('
|
|
1072
|
+
const lines = cleanStdout.split(new RegExp('\\r\\n|\\r|\\n'));
|
|
1111
1073
|
for (const line of lines) {
|
|
1112
1074
|
const trimmed = line.trim();
|
|
1113
1075
|
const m = trimmed.match(/^(\w+):(PASS|FAIL|SKIP)\b(.*)/);
|
|
@@ -1279,9 +1241,7 @@ export function buildDevbridgeAuthProbe() {
|
|
|
1279
1241
|
// authMode is the LAST DB_AUTH_MODE line; attempts collects the non-sensitive
|
|
1280
1242
|
// DB_ATTEMPT_* diagnostics emitted by tiers that failed and fell through.
|
|
1281
1243
|
export function parseDevbridgeAuthOutput(stdout) {
|
|
1282
|
-
|
|
1283
|
-
// anchored ^DB_ATTEMPT_ regexes are blind to them without stripping first.
|
|
1284
|
-
const text = stripTerminalEscapes(stdout);
|
|
1244
|
+
const text = String(stdout || '');
|
|
1285
1245
|
const modes = text.match(/DB_AUTH_MODE=([A-Z_]+)/g) || [];
|
|
1286
1246
|
const authMode = modes.length > 0 ? modes[modes.length - 1].split('=')[1] : '';
|
|
1287
1247
|
const attempts = {};
|
|
@@ -1319,19 +1279,13 @@ export function buildDevbridgeExposeScript(port, expiryHours = DEVBRIDGE_HOST_EX
|
|
|
1319
1279
|
`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`,
|
|
1320
1280
|
`disown 2>/dev/null || true`,
|
|
1321
1281
|
`sleep 12`,
|
|
1322
|
-
|
|
1323
|
-
|
|
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")`,
|
|
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")`,
|
|
1326
1284
|
`echo "DB_HOST_LOG="`,
|
|
1327
1285
|
`tail -n 5 ${hostLog}`,
|
|
1328
1286
|
`echo "DB_TUNNEL_URL=$TUNNEL_URL"`,
|
|
1329
1287
|
`echo "DB_HTTP_CODE=$HTTP_CODE"`,
|
|
1330
1288
|
`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`,
|
|
1335
1289
|
`echo "DB_QUOTA_ERROR=$(grep -c '10006' ${hostLog} 2>/dev/null || echo 0)"`,
|
|
1336
1290
|
].join('\n');
|
|
1337
1291
|
}
|
|
@@ -1363,30 +1317,18 @@ export function buildDevbridgeVerifyScript(tunnelUrl) {
|
|
|
1363
1317
|
}
|
|
1364
1318
|
|
|
1365
1319
|
// 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.
|
|
1370
1320
|
export function parseDevbridgeExposeOutput(stdout) {
|
|
1371
|
-
|
|
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);
|
|
1321
|
+
const text = String(stdout || '');
|
|
1375
1322
|
const getField = (key) => {
|
|
1376
1323
|
const m = text.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
1377
1324
|
return m ? m[1].trim() : '';
|
|
1378
1325
|
};
|
|
1379
1326
|
const tunnelUrl = getField('DB_TUNNEL_URL') || (text.match(/TUNNEL_URL:(https:\/\/[^\s]+)/) || [])[1] || '';
|
|
1380
1327
|
const httpCode = getField('DB_HTTP_CODE');
|
|
1381
|
-
const body = getField('DB_TUNNEL_BODY');
|
|
1382
1328
|
const quotaError = getField('DB_QUOTA_ERROR') === '1' || /10006/.test(text);
|
|
1383
1329
|
const tunnelId = (tunnelUrl.match(/^https:\/\/([A-Za-z0-9_-]+)-\d+\./) || [])[1] || '';
|
|
1384
1330
|
const expiryHours = Number(getField('DB_EXPIRY_HOURS')) || undefined;
|
|
1385
|
-
|
|
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 };
|
|
1331
|
+
return { tunnelUrl, httpCode, quotaError, tunnelId, expiryHours };
|
|
1390
1332
|
}
|
|
1391
1333
|
|
|
1392
1334
|
// Parse the raw stdout of buildDevbridgeVerifyScript into structured fields.
|
|
@@ -1395,9 +1337,7 @@ export function parseDevbridgeExposeOutput(stdout) {
|
|
|
1395
1337
|
// parseDevbridgeExposeOutput never matches them, which used to leave
|
|
1396
1338
|
// verifyHttpCode empty and persistent always false.
|
|
1397
1339
|
export function parseDevbridgeVerifyOutput(stdout) {
|
|
1398
|
-
|
|
1399
|
-
// to them, leaving verifyHttpCode empty and persistent permanently false.
|
|
1400
|
-
const text = stripTerminalEscapes(stdout);
|
|
1340
|
+
const text = String(stdout || '');
|
|
1401
1341
|
const getField = (key) => {
|
|
1402
1342
|
const m = text.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
1403
1343
|
return m ? m[1].trim() : '';
|
|
@@ -1471,15 +1411,10 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1471
1411
|
if (parsed.quotaError && attempt < maxAttempts) {
|
|
1472
1412
|
continue;
|
|
1473
1413
|
}
|
|
1474
|
-
// Unreachable (000)
|
|
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.
|
|
1414
|
+
// Unreachable (000) or a migrated-gateway placeholder: one retry.
|
|
1478
1415
|
const unreachable = parsed.httpCode === '000' || parsed.httpCode === '';
|
|
1479
1416
|
const migration = /服务已迁移/.test(String(result.stdout || ''));
|
|
1480
|
-
|
|
1481
|
-
const backendDown = parsed.badGateway === true;
|
|
1482
|
-
if (unreachable || migration || gatewayTunnelError || backendDown) {
|
|
1417
|
+
if (unreachable || migration) {
|
|
1483
1418
|
if (attempt === 1) {
|
|
1484
1419
|
try {
|
|
1485
1420
|
await execOneShot(
|
|
@@ -1491,20 +1426,6 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1491
1426
|
} catch {}
|
|
1492
1427
|
continue;
|
|
1493
1428
|
}
|
|
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
|
-
}
|
|
1508
1429
|
}
|
|
1509
1430
|
|
|
1510
1431
|
const warnings = [];
|
|
@@ -1553,10 +1474,6 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1553
1474
|
}
|
|
1554
1475
|
}
|
|
1555
1476
|
|
|
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
|
-
);
|
|
1560
1477
|
if (parsed.httpCode !== '200' && parsed.httpCode !== '304') {
|
|
1561
1478
|
warnings.push(
|
|
1562
1479
|
`Tunnel reachable check returned HTTP ${parsed.httpCode} (expected 200/304). The URL may not be serving yet.`,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
applyUserDelta,
|
|
45
45
|
inheritPeerUserEnv,
|
|
46
46
|
detectOpenCodeEra,
|
|
47
|
+
isPlainObject,
|
|
47
48
|
mergeOpenCodeEntry,
|
|
48
49
|
} from './mcp-config-merge.mjs';
|
|
49
50
|
import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
|
|
@@ -703,7 +704,7 @@ function resolveOpenCodeEra() {
|
|
|
703
704
|
return (resolvedOpenCodeEra = 'v2');
|
|
704
705
|
}
|
|
705
706
|
|
|
706
|
-
function updateOpenCodeConfig(pluginDir) {
|
|
707
|
+
function updateOpenCodeConfig(pluginDir, { nativePluginInstalled = false } = {}) {
|
|
707
708
|
const configPath = opencodeConfigFile();
|
|
708
709
|
const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
709
710
|
const era = resolveOpenCodeEra();
|
|
@@ -722,6 +723,25 @@ function updateOpenCodeConfig(pluginDir) {
|
|
|
722
723
|
|
|
723
724
|
const { config: next, existing, changed } = mergeOpenCodeEntry(config, { mcpPath, era });
|
|
724
725
|
|
|
726
|
+
// Native-plugin mode: the in-process plugin already bridges all 42 tools with
|
|
727
|
+
// the shared callTool layer (names, approval gate, telemetry unchanged). Keep
|
|
728
|
+
// the MCP entry for manual fallback but leave it disabled so OpenCode does
|
|
729
|
+
// not register 42 duplicate `huaweicloud-devkit_*` copies (#940 follow-up).
|
|
730
|
+
let mcpDisabled = false;
|
|
731
|
+
if (nativePluginInstalled) {
|
|
732
|
+
const key = 'huaweicloud-devkit';
|
|
733
|
+
if (era === 'v2') {
|
|
734
|
+
const servers = isPlainObject(next.mcp?.servers) ? next.mcp.servers : null;
|
|
735
|
+
if (servers && servers[key] && servers[key].disabled !== true) {
|
|
736
|
+
servers[key] = { ...servers[key], disabled: true };
|
|
737
|
+
mcpDisabled = true;
|
|
738
|
+
}
|
|
739
|
+
} else if (next.mcp?.[key] && next.mcp[key].enabled !== false) {
|
|
740
|
+
next.mcp[key] = { ...next.mcp[key], enabled: false };
|
|
741
|
+
mcpDisabled = true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
725
745
|
// Restore user fields saved by a previous uninstall (issue #615), on fresh install only.
|
|
726
746
|
if (!existing) {
|
|
727
747
|
const delta = takeAgentDelta('opencode');
|
|
@@ -734,12 +754,15 @@ function updateOpenCodeConfig(pluginDir) {
|
|
|
734
754
|
}
|
|
735
755
|
}
|
|
736
756
|
|
|
737
|
-
if (existing && !changed) {
|
|
757
|
+
if (existing && !changed && !mcpDisabled) {
|
|
738
758
|
console.log(` OpenCode MCP config unchanged: ${configPath}`);
|
|
739
759
|
return;
|
|
740
760
|
}
|
|
741
761
|
|
|
742
762
|
writeMcpSettingsFile(configPath, next);
|
|
763
|
+
if (mcpDisabled) {
|
|
764
|
+
console.log(` OpenCode MCP entry disabled (native plugin active — no duplicate tools) -> ${configPath}`);
|
|
765
|
+
}
|
|
743
766
|
console.log(
|
|
744
767
|
existing
|
|
745
768
|
? ` OpenCode MCP config merged (user fields preserved, ${era}): ${configPath}`
|
|
@@ -961,7 +984,24 @@ async function installOpenCode() {
|
|
|
961
984
|
mkdirSync(opcPlugins, { recursive: true });
|
|
962
985
|
copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
|
|
963
986
|
console.log(` Plugin -> ${opcPlugins}`);
|
|
964
|
-
|
|
987
|
+
// OpenCode native plugin (#905 OpenCode performance): bridges all 42 MCP tools
|
|
988
|
+
// into OpenCode in-process custom tools (zod schemas + callTool), plus safety
|
|
989
|
+
// gate and telemetry. Local plugins are auto-loaded from this directory.
|
|
990
|
+
const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
|
|
991
|
+
let nativePluginInstalled = false;
|
|
992
|
+
if (existsSync(nativePluginSrc)) {
|
|
993
|
+
copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
|
|
994
|
+
// Deps for the native plugin (zod) live next to the OpenCode config so the
|
|
995
|
+
// plugin can require them at load time.
|
|
996
|
+
const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
|
|
997
|
+
const nativePkgDst = join(configRoot('opencode'), 'package.json');
|
|
998
|
+
if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
|
|
999
|
+
copyFileSync(nativePkgSrc, nativePkgDst);
|
|
1000
|
+
}
|
|
1001
|
+
nativePluginInstalled = true;
|
|
1002
|
+
console.log(` Native plugin -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
|
|
1003
|
+
}
|
|
1004
|
+
updateOpenCodeConfig(pluginDest, { nativePluginInstalled });
|
|
965
1005
|
installRuntimeDeps(pluginDest);
|
|
966
1006
|
}
|
|
967
1007
|
|
|
@@ -994,6 +1034,11 @@ function uninstallOpenCode() {
|
|
|
994
1034
|
removeIfExists(pluginFile);
|
|
995
1035
|
console.log(' Removed plugin');
|
|
996
1036
|
}
|
|
1037
|
+
const nativePluginFile = join(configRoot('opencode'), 'plugins', 'huaweicloud-devkit.js');
|
|
1038
|
+
if (existsSync(nativePluginFile)) {
|
|
1039
|
+
removeIfExists(nativePluginFile);
|
|
1040
|
+
console.log(' Removed native plugin');
|
|
1041
|
+
}
|
|
997
1042
|
|
|
998
1043
|
if (removeIfExists(opencodePluginsDir())) {
|
|
999
1044
|
console.log(' Removed MCP server and safety policy');
|
|
@@ -1041,7 +1086,20 @@ async function updateOpenCode() {
|
|
|
1041
1086
|
mkdirSync(opcPlugins, { recursive: true });
|
|
1042
1087
|
copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
|
|
1043
1088
|
console.log(` Plugin updated -> ${opcPlugins}`);
|
|
1044
|
-
|
|
1089
|
+
// Native plugin refresh (#905 OpenCode performance) — overwrite in place.
|
|
1090
|
+
const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
|
|
1091
|
+
let nativePluginInstalled = false;
|
|
1092
|
+
if (existsSync(nativePluginSrc)) {
|
|
1093
|
+
copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
|
|
1094
|
+
const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
|
|
1095
|
+
const nativePkgDst = join(configRoot('opencode'), 'package.json');
|
|
1096
|
+
if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
|
|
1097
|
+
copyFileSync(nativePkgSrc, nativePkgDst);
|
|
1098
|
+
}
|
|
1099
|
+
nativePluginInstalled = true;
|
|
1100
|
+
console.log(` Native plugin updated -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
|
|
1101
|
+
}
|
|
1102
|
+
updateOpenCodeConfig(pluginDest, { nativePluginInstalled });
|
|
1045
1103
|
mkdirSync(pluginDest, { recursive: true });
|
|
1046
1104
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
1047
1105
|
installRuntimeDeps(pluginDest);
|