huaweicloud-devkit 1.1.0-next.6 → 1.1.0-next.8
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 +20 -1
- package/README.zh-CN.md +20 -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/hooks/huaweicloud-safety.py +23 -15
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +41 -21
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +48 -10
- package/plugins/huaweicloud-core/src/setup-cli.mjs +135 -7
- package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +29 -8
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Help AI coding agents use Huawei Cloud safely and accurately — a single integration that gives agents cloud knowledge, CLI tooling, and safety guardrails.
|
|
10
10
|
|
|
11
|
-
Supports OpenCode, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, OpenClaw, and AtomCode.
|
|
11
|
+
Supports OpenCode, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, Hermes, OpenClaw, and AtomCode.
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
@@ -98,6 +98,25 @@ npx --yes huaweicloud-devkit update --target officeace
|
|
|
98
98
|
npx --yes huaweicloud-devkit uninstall --target officeace
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Hermes
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx --yes huaweicloud-devkit install --target hermes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Restart the Hermes session** after installation.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx --yes huaweicloud-devkit doctor --target hermes
|
|
111
|
+
npx --yes huaweicloud-devkit status --target hermes
|
|
112
|
+
npx --yes huaweicloud-devkit update --target hermes
|
|
113
|
+
npx --yes huaweicloud-devkit uninstall --target hermes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **Safety hooks**: The installer configures Hermes shell hooks (`config.yaml` → `hooks.pre_tool_call`) to intercept unsafe terminal commands such as credential file reads, environment variable dumps, and unapproved `hcloud` write operations. Hermes shows a consent prompt the first time; approve it or set `hooks_auto_accept: true` in `config.yaml` to auto-accept.
|
|
117
|
+
> **MCP Python SDK**: The installer automatically installs the `mcp` Python package required by Hermes for MCP tool discovery. If you see `[FAIL] Hermes MCP Python SDK` in `doctor`, run `pip3 install mcp` manually.
|
|
118
|
+
> **Windows**: Hermes on Windows stores data under `%LOCALAPPDATA%\hermes` instead of `~/.hermes`. The installer detects this automatically.
|
|
119
|
+
|
|
101
120
|
### OpenClaw
|
|
102
121
|
|
|
103
122
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
帮助 AI 编码助手安全、准确地使用华为云——一站式集成云知识、CLI 工具和安全护栏。
|
|
10
10
|
|
|
11
|
-
支持 OpenCode、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、OpenClaw、AtomCode。
|
|
11
|
+
支持 OpenCode、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、Hermes、OpenClaw、AtomCode。
|
|
12
12
|
|
|
13
13
|
## 前置条件
|
|
14
14
|
|
|
@@ -98,6 +98,25 @@ npx --yes huaweicloud-devkit update --target officeace
|
|
|
98
98
|
npx --yes huaweicloud-devkit uninstall --target officeace
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Hermes
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx --yes huaweicloud-devkit install --target hermes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
安装后**重启 Hermes 会话**。
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx --yes huaweicloud-devkit doctor --target hermes
|
|
111
|
+
npx --yes huaweicloud-devkit status --target hermes
|
|
112
|
+
npx --yes huaweicloud-devkit update --target hermes
|
|
113
|
+
npx --yes huaweicloud-devkit uninstall --target hermes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **安全钩子(Safety hooks)**:安装器会在 `config.yaml` 中写入 shell hooks 配置(`hooks.pre_tool_call`),拦截不安全的终端命令,如读取凭据文件、导出环境变量、未审批的 `hcloud` 写操作。Hermes 首次使用时会弹出同意提示,可批准或设置 `hooks_auto_accept: true` 自动批准。
|
|
117
|
+
> **MCP Python SDK**:安装器会自动安装 Hermes 所需的 `mcp` Python 包。如果 doctor 显示 `[FAIL] Hermes MCP Python SDK`,手动执行 `pip3 install mcp`。
|
|
118
|
+
> **Windows**:Hermes 在 Windows 上数据存储在 `%LOCALAPPDATA%\hermes` 而非 `~/.hermes`,安装器会自动检测适配。
|
|
119
|
+
|
|
101
120
|
### OpenClaw
|
|
102
121
|
|
|
103
122
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.8",
|
|
4
4
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.0-next.
|
|
20
|
+
"version": "1.1.0-next.8",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.0-next.
|
|
23
|
+
"version": "1.1.0-next.8",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.0-next.
|
|
20
|
+
"version": "1.1.0-next.8",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.8",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.8",
|
|
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",
|
|
@@ -53,16 +53,23 @@ READ_OPERATION_RE = re.compile(r"\b(List|Show|Get|Describe|NovaList|NovaShow)\w*
|
|
|
53
53
|
READ_OPERATION_RE = re.compile(r"\b(List|Show|Get|Describe|NovaList|NovaShow)\w*", re.I)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def deny(reason):
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"permissionDecisionReason": DENY_PREFIX + reason,
|
|
56
|
+
def deny(reason, hermes=False):
|
|
57
|
+
sys.stdout.write(
|
|
58
|
+
json.dumps(
|
|
59
|
+
{
|
|
60
|
+
"action": "block",
|
|
61
|
+
"message": DENY_PREFIX + reason,
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if hermes
|
|
64
|
+
else {
|
|
65
|
+
"hookSpecificOutput": {
|
|
66
|
+
"hookEventName": "PreToolUse",
|
|
67
|
+
"permissionDecision": "deny",
|
|
68
|
+
"permissionDecisionReason": DENY_PREFIX + reason,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
+ "\n"
|
|
66
73
|
)
|
|
67
74
|
sys.exit(0)
|
|
68
75
|
|
|
@@ -139,18 +146,19 @@ def main():
|
|
|
139
146
|
|
|
140
147
|
tool_name = data.get("tool_name", "")
|
|
141
148
|
text = command_text(data.get("tool_input", {}))
|
|
149
|
+
hermes = "hook_event_name" in data
|
|
142
150
|
|
|
143
151
|
if CONFIG_FILE_RE and CONFIG_FILE_RE.search(text):
|
|
144
|
-
deny("reading Huawei Cloud credential/profile files can expose AK/SK or tokens. Use redacted toolkit tools.")
|
|
152
|
+
deny("reading Huawei Cloud credential/profile files can expose AK/SK or tokens. Use redacted toolkit tools.", hermes)
|
|
145
153
|
if ENV_DUMP_RE.search(text):
|
|
146
|
-
deny("dumping cloud credential environment variables is not allowed.")
|
|
154
|
+
deny("dumping cloud credential environment variables is not allowed.", hermes)
|
|
147
155
|
if SECRET_READ_RE and SECRET_READ_RE.search(text):
|
|
148
|
-
deny("direct secret value retrieval would put plaintext secrets into the agent context.")
|
|
156
|
+
deny("direct secret value retrieval would put plaintext secrets into the agent context.", hermes)
|
|
149
157
|
denied_rule = first_denied_command_rule(text)
|
|
150
158
|
if denied_rule:
|
|
151
|
-
deny(f"{denied_rule.get('message')} Remediation: {denied_rule.get('remediation')}")
|
|
152
|
-
if tool_name
|
|
153
|
-
deny("unapproved Huawei Cloud write operations must be planned first and explicitly approved by the user.")
|
|
159
|
+
deny(f"{denied_rule.get('message')} Remediation: {denied_rule.get('remediation')}", hermes)
|
|
160
|
+
if tool_name in ("Bash", "terminal") and HCLOUD_RE.search(text) and WRITE_OPERATION_RE.search(text) and not READ_OPERATION_RE.search(text):
|
|
161
|
+
deny("unapproved Huawei Cloud write operations must be planned first and explicitly approved by the user.", hermes)
|
|
154
162
|
|
|
155
163
|
allow()
|
|
156
164
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.0-next.
|
|
5
|
+
"version": "1.1.0-next.8",
|
|
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.",
|
|
@@ -72,6 +72,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
72
72
|
{ "timeout_ms": 300000 }
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
**Session recovery**: if `exec_with_session` returns `session is not ready`, the WebSocket connection has dropped. Do NOT retry the same session — fall back to `exec_one_shot` for that command instead. To recover state (cd, env vars), reconstruct them explicitly in the one-shot command.
|
|
76
|
+
|
|
75
77
|
## Workflow
|
|
76
78
|
|
|
77
79
|
Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
|
|
@@ -84,10 +86,21 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
84
86
|
3. **Sign agreement only** (`HDKIT_NOT_AGREEMENT`): **STOP and do NOT sign on your own.** Ask the developer: "Huawei Cloud sandbox requires signing the latest developer service agreement. May I sign it for you?" Then **wait for the developer to explicitly agree** (e.g. "签署" / "确认" / "sign it"). Only after explicit consent call `huaweicloud_sandbox_sign_agreement` and return its result (`signed`/`signedCount`) to the developer. **Never sign a legal agreement on the developer's behalf without their explicit, unambiguous consent.** Do not expose the underlying sandbox/DevBridge service as a separate entity the developer must understand or sign up for
|
|
85
87
|
4. **Both missing** (`HDKIT_NOT_REALNAME_AND_AGREEMENT`): present **both** requirements together in one message — the real-name verification steps (console, step 2) **and** the agreement-signing request (step 3, wait for explicit consent) — so the developer can complete both at once
|
|
86
88
|
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`
|
|
87
|
-
6. **
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs and DevBridge tunnels from previous deployments can cause port conflicts and quota errors. Run cleanup immediately after connect:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Remove stale nginx configs from previous deployments
|
|
93
|
+
sudo rm -f /etc/nginx/conf.d/app.conf /etc/nginx/conf.d/*.conf.bak 2>/dev/null
|
|
94
|
+
# Remove stale DevBridge tunnels
|
|
95
|
+
devbridge delete-all 2>/dev/null || true
|
|
96
|
+
# Reload nginx to apply
|
|
97
|
+
sudo nginx -s reload 2>/dev/null || true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
7. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox
|
|
101
|
+
8. **Execute commands**: use `huaweicloud_sandbox_exec_with_session` for interactive work (cd, env setup), or `huaweicloud_sandbox_exec_one_shot` for long-running builds/installs. See [Tool Selection Guide](#tool-selection-guide).
|
|
102
|
+
9. **Upload project** (if deploying local code): `huaweicloud_sandbox_upload_project` to transfer a local project directory into `/workspace/<name>` — uses HTTP tunnel, much faster than base64 for multi-file projects. Use `huaweicloud_sandbox_upload_file` for single files.
|
|
103
|
+
10. **Expose the deployed app and return the public URL**: set up a public access link for the app port and give the URL to the developer as the final result (see "Expose the deployed app (public URL)") — this step uses an internal tunnel mechanism that must never be mentioned to the developer
|
|
91
104
|
|
|
92
105
|
## File Transfer (local → sandbox)
|
|
93
106
|
|
|
@@ -103,7 +116,7 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
103
116
|
"remote_dir": "/workspace",
|
|
104
117
|
"extract": true,
|
|
105
118
|
"exclude": [
|
|
106
|
-
"node_modules",
|
|
119
|
+
"**/node_modules",
|
|
107
120
|
".git",
|
|
108
121
|
"__pycache__",
|
|
109
122
|
".next",
|
|
@@ -240,7 +253,7 @@ Follow the standard [Workflow](#workflow) steps 1-6 to connect to the sandbox, t
|
|
|
240
253
|
"local_dir": "<projectPath>",
|
|
241
254
|
"remote_dir": "/workspace",
|
|
242
255
|
"exclude": [
|
|
243
|
-
"node_modules",
|
|
256
|
+
"**/node_modules",
|
|
244
257
|
".git",
|
|
245
258
|
"__pycache__",
|
|
246
259
|
".next",
|
|
@@ -258,20 +271,20 @@ Follow the standard [Workflow](#workflow) steps 1-6 to connect to the sandbox, t
|
|
|
258
271
|
|
|
259
272
|
**Always exclude build artifacts and dependency directories** — they will be re-installed/built inside the sandbox:
|
|
260
273
|
|
|
261
|
-
| Pattern
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
| `.git`
|
|
265
|
-
| `__pycache__`
|
|
266
|
-
| `.next`
|
|
267
|
-
| `.nuxt`
|
|
268
|
-
| `.output`
|
|
269
|
-
| `.turbo`
|
|
270
|
-
| `.cache`
|
|
271
|
-
| `.swc`
|
|
272
|
-
| `dist`
|
|
273
|
-
| `coverage`
|
|
274
|
-
| `*.pyc`
|
|
274
|
+
| Pattern | Why excluded |
|
|
275
|
+
| ----------------- | ---------------------------------------------- |
|
|
276
|
+
| `**/node_modules` | Dependencies — reinstall in sandbox |
|
|
277
|
+
| `.git` | Version control — not needed for deployment |
|
|
278
|
+
| `__pycache__` | Python bytecode cache |
|
|
279
|
+
| `.next` | Next.js build output — rebuild in sandbox |
|
|
280
|
+
| `.nuxt` | Nuxt build cache — rebuild in sandbox |
|
|
281
|
+
| `.output` | Nuxt production output — rebuild in sandbox |
|
|
282
|
+
| `.turbo` | Turborepo cache — re-run in sandbox |
|
|
283
|
+
| `.cache` | Generic tool cache (Parcel, Storybook, etc.) |
|
|
284
|
+
| `.swc` | Taro/Webpack SWC cache — regenerate in sandbox |
|
|
285
|
+
| `dist` | Build output — rebuild in sandbox |
|
|
286
|
+
| `coverage` | Test coverage reports — not needed for deploy |
|
|
287
|
+
| `*.pyc` | Python compiled files |
|
|
275
288
|
|
|
276
289
|
**Post-upload permission fix**: after `upload_project` extracts the project, fix file permissions lost during transfer (native binaries from other platforms, .bin symlinks):
|
|
277
290
|
|
|
@@ -420,7 +433,14 @@ if timeout_error; then
|
|
|
420
433
|
tail -30 /tmp/build.log 2>/dev/null
|
|
421
434
|
echo "=== Checking output ==="
|
|
422
435
|
if [ -d <outputDir> ] && [ "$(ls -A <outputDir> 2>/dev/null)" ]; then
|
|
423
|
-
|
|
436
|
+
# Verify at least one key output file exists (not just empty dir from broken build)
|
|
437
|
+
if [ -f <outputDir>/index.html ] || [ -f <outputDir>/server.js ] || [ -f <outputDir>/app.js ]; then
|
|
438
|
+
echo "Build output detected despite timeout — continuing with deployment"
|
|
439
|
+
else
|
|
440
|
+
echo "ERROR: Output directory exists but missing expected files (index.html/server.js). Build may have failed silently."
|
|
441
|
+
echo "Full log: /tmp/build.log"
|
|
442
|
+
exit 1
|
|
443
|
+
fi
|
|
424
444
|
else
|
|
425
445
|
echo "ERROR: Build did not complete. Output directory empty or missing."
|
|
426
446
|
echo "Full log: /tmp/build.log"
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { spawn, execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { createConnection as netConnect } from 'node:net';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
statSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
createReadStream,
|
|
11
|
+
appendFileSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
} from 'node:fs';
|
|
5
14
|
import { createHash, randomBytes } from 'node:crypto';
|
|
6
15
|
import { join, dirname, basename } from 'node:path';
|
|
7
16
|
import { tmpdir } from 'node:os';
|
|
@@ -269,6 +278,17 @@ function uploadLog(message) {
|
|
|
269
278
|
} catch {}
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
function rotateUploadLog(maxBytes = 100 * 1024) {
|
|
282
|
+
try {
|
|
283
|
+
if (existsSync(UPLOAD_LOG_PATH)) {
|
|
284
|
+
const stat = statSync(UPLOAD_LOG_PATH);
|
|
285
|
+
if (stat.size > maxBytes) {
|
|
286
|
+
unlinkSync(UPLOAD_LOG_PATH);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
|
|
272
292
|
function generateUploadToken() {
|
|
273
293
|
return randomBytes(16).toString('hex');
|
|
274
294
|
}
|
|
@@ -279,14 +299,20 @@ async function createTarGz(localDir, exclude = []) {
|
|
|
279
299
|
mkdirSync(archiveDir, { recursive: true });
|
|
280
300
|
const archivePath = join(archiveDir, archiveName);
|
|
281
301
|
|
|
282
|
-
const args = [
|
|
283
|
-
|
|
284
|
-
'
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
302
|
+
const args = [];
|
|
303
|
+
for (const pattern of exclude) {
|
|
304
|
+
if (pattern.startsWith('**/')) {
|
|
305
|
+
const base = pattern.slice(3);
|
|
306
|
+
for (let depth = 0; depth <= 4; depth++) {
|
|
307
|
+
const prefix = depth === 0 ? '' : '*/'.repeat(depth);
|
|
308
|
+
args.push('--exclude', `${prefix}${base}`);
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
args.push('--exclude', pattern);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
args.push('-czf', archivePath, '-C', dirname(localDir), basename(localDir));
|
|
315
|
+
|
|
290
316
|
await execFileAsync('tar', args);
|
|
291
317
|
return archivePath;
|
|
292
318
|
}
|
|
@@ -557,6 +583,8 @@ export async function uploadProjectWithSession(
|
|
|
557
583
|
throw new Error(`sandbox upload project: path is not a directory: ${localDir}`);
|
|
558
584
|
}
|
|
559
585
|
|
|
586
|
+
rotateUploadLog();
|
|
587
|
+
|
|
560
588
|
const projectName = basename(localDir);
|
|
561
589
|
const targetParentDir = remoteDir || '/workspace';
|
|
562
590
|
const archiveRemotePath = `${targetParentDir}/${projectName}.tar.gz`;
|
|
@@ -567,6 +595,15 @@ export async function uploadProjectWithSession(
|
|
|
567
595
|
|
|
568
596
|
uploadLog(`uploadProject: ${localDir} -> ${archiveRemotePath} (archive=${archiveSize} bytes, md5=${expectedMd5})`);
|
|
569
597
|
|
|
598
|
+
const SIZE_50MB = 50 * 1024 * 1024;
|
|
599
|
+
if (archiveSize > SIZE_50MB) {
|
|
600
|
+
uploadLog(
|
|
601
|
+
`uploadProject: archive size ${(archiveSize / (1024 * 1024)).toFixed(1)}MB exceeds 50MB. ` +
|
|
602
|
+
`Dependencies or platform binaries may have been included. ` +
|
|
603
|
+
`Ensure exclude list contains "**/node_modules" to match all nesting levels.`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
570
607
|
let result;
|
|
571
608
|
let tunnelError;
|
|
572
609
|
for (let attempt = 0; attempt < UPLOAD_MAX_RETRIES; attempt++) {
|
|
@@ -577,7 +614,8 @@ export async function uploadProjectWithSession(
|
|
|
577
614
|
break;
|
|
578
615
|
} catch (error) {
|
|
579
616
|
tunnelError = error;
|
|
580
|
-
|
|
617
|
+
const errorType = error.code || error.name || 'unknown';
|
|
618
|
+
uploadLog(`uploadProject: attempt ${attempt + 1}/${UPLOAD_MAX_RETRIES} failed [${errorType}]: ${error.message}`);
|
|
581
619
|
await new Promise((r) => setTimeout(r, 5000));
|
|
582
620
|
}
|
|
583
621
|
}
|
|
@@ -2035,28 +2035,135 @@ function removeHermesMcpConfigBlock() {
|
|
|
2035
2035
|
console.log(' MCP config cleaned');
|
|
2036
2036
|
}
|
|
2037
2037
|
|
|
2038
|
+
function ensureHermesHooksConfig() {
|
|
2039
|
+
const configPath = hermesConfigFile();
|
|
2040
|
+
const pluginDest = hermesPluginsDir();
|
|
2041
|
+
const hookScript = join(pluginDest, 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
|
|
2042
|
+
|
|
2043
|
+
const blockLines = [
|
|
2044
|
+
'hooks:',
|
|
2045
|
+
' pre_tool_call:',
|
|
2046
|
+
' - matcher: "terminal"',
|
|
2047
|
+
` command: "python3 ${hookScript}"`,
|
|
2048
|
+
' timeout: 5',
|
|
2049
|
+
];
|
|
2050
|
+
const block = blockLines.join('\n');
|
|
2051
|
+
|
|
2052
|
+
let existing = '';
|
|
2053
|
+
if (existsSync(configPath)) {
|
|
2054
|
+
try {
|
|
2055
|
+
existing = readFileSync(configPath, 'utf8');
|
|
2056
|
+
} catch {}
|
|
2057
|
+
if (existing.includes('hooks:') && existing.includes('huaweicloud-safety.py')) {
|
|
2058
|
+
console.log(` Hooks config unchanged: ${configPath}`);
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
2063
|
+
const newContent = existing ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
|
|
2064
|
+
writeFileSync(configPath, newContent);
|
|
2065
|
+
console.log(` Hooks config updated: ${configPath}`);
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function removeHermesHooksConfigBlock() {
|
|
2070
|
+
const configPath = hermesConfigFile();
|
|
2071
|
+
if (!existsSync(configPath)) return;
|
|
2072
|
+
const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
|
|
2073
|
+
const out = [];
|
|
2074
|
+
let skip = false;
|
|
2075
|
+
for (const line of lines) {
|
|
2076
|
+
if (/^\s*huaweicloud-safety\.py/.test(line.trim())) {
|
|
2077
|
+
skip = true;
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
if (skip) {
|
|
2081
|
+
if (line.trim() === '') continue;
|
|
2082
|
+
if (/^\s{2,}/.test(line) && line.trim() !== '') continue;
|
|
2083
|
+
skip = false;
|
|
2084
|
+
}
|
|
2085
|
+
if (line.trim() === 'hooks:') {
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
if (line.trim() === 'pre_tool_call:') {
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
if (/^\s*- matcher:\s*"terminal"/.test(line)) {
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
out.push(line);
|
|
2095
|
+
}
|
|
2096
|
+
const cleaned = out.join('\n').trimEnd();
|
|
2097
|
+
writeFileSync(configPath, cleaned ? `${cleaned}\n` : '');
|
|
2098
|
+
console.log(' Hooks config cleaned');
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function ensureHermesMcpSdk() {
|
|
2102
|
+
const pythonBin = process.platform === 'win32' ? 'python' : 'python3';
|
|
2103
|
+
try {
|
|
2104
|
+
const r = spawnSync(pythonBin, ['-c', 'import mcp; print("ok")'], { encoding: 'utf8', timeout: 5000 });
|
|
2105
|
+
if (r.status === 0 && r.stdout.trim() === 'ok') {
|
|
2106
|
+
console.log(' MCP Python SDK: \x1b[32mAlready installed\x1b[0m');
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
} catch {}
|
|
2110
|
+
console.log(' Installing Hermes MCP Python SDK...');
|
|
2111
|
+
try {
|
|
2112
|
+
const pip = spawnSync(pythonBin, ['-m', 'pip', 'install', 'mcp', '--quiet'], { encoding: 'utf8', timeout: 60000 });
|
|
2113
|
+
if (pip.status === 0) {
|
|
2114
|
+
console.log(' MCP Python SDK: \x1b[32mInstalled\x1b[0m');
|
|
2115
|
+
return true;
|
|
2116
|
+
}
|
|
2117
|
+
console.log(` MCP Python SDK: \x1b[33mInstall failed\x1b[0m`);
|
|
2118
|
+
console.log(` \x1b[33m Run manually: ${pythonBin} -m pip install mcp\x1b[0m`);
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
console.log(` MCP Python SDK: \x1b[33m${error.message}\x1b[0m`);
|
|
2121
|
+
console.log(` \x1b[33m Run manually: ${pythonBin} -m pip install mcp\x1b[0m`);
|
|
2122
|
+
}
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function hermesMcpSdkOk() {
|
|
2127
|
+
const pythonBin = process.platform === 'win32' ? 'python' : 'python3';
|
|
2128
|
+
try {
|
|
2129
|
+
const r = spawnSync(pythonBin, ['-c', 'import mcp; print("ok")'], { encoding: 'utf8', timeout: 5000 });
|
|
2130
|
+
return r.status === 0 && r.stdout.trim() === 'ok';
|
|
2131
|
+
} catch {
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2038
2136
|
async function installHermes() {
|
|
2039
2137
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
2040
2138
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
2041
2139
|
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
2140
|
+
const hooksDir = join(PLUGIN_ROOT, 'hooks');
|
|
2042
2141
|
const pluginDest = hermesPluginsDir();
|
|
2142
|
+
const skipMcp = process.argv.includes('--skip-mcp-server');
|
|
2043
2143
|
|
|
2044
2144
|
copyDir(skillsSrc, hermesSkillsDir());
|
|
2045
2145
|
console.log(` Skills -> ${hermesSkillsDir()}`);
|
|
2046
2146
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2147
|
+
if (!skipMcp) {
|
|
2148
|
+
copyDir(srcDir, join(pluginDest, 'src'));
|
|
2149
|
+
console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
|
|
2150
|
+
}
|
|
2049
2151
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2050
2152
|
console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
|
|
2153
|
+
copyDir(hooksDir, join(pluginDest, 'hooks'));
|
|
2154
|
+
console.log(` Safety Hooks -> ${join(pluginDest, 'hooks')}`);
|
|
2051
2155
|
|
|
2052
|
-
ensureHermesMcpConfig();
|
|
2053
|
-
|
|
2156
|
+
if (!skipMcp) ensureHermesMcpConfig();
|
|
2157
|
+
ensureHermesHooksConfig();
|
|
2158
|
+
if (!skipMcp) installRuntimeDeps(pluginDest);
|
|
2159
|
+
if (!skipMcp) ensureHermesMcpSdk();
|
|
2054
2160
|
}
|
|
2055
2161
|
|
|
2056
2162
|
async function updateHermes() {
|
|
2057
2163
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
2058
2164
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
2059
2165
|
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
2166
|
+
const hooksDir = join(PLUGIN_ROOT, 'hooks');
|
|
2060
2167
|
const pluginDest = hermesPluginsDir();
|
|
2061
2168
|
|
|
2062
2169
|
copyDir(skillsSrc, hermesSkillsDir());
|
|
@@ -2066,7 +2173,11 @@ async function updateHermes() {
|
|
|
2066
2173
|
console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
|
|
2067
2174
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2068
2175
|
console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
|
|
2176
|
+
copyDir(hooksDir, join(pluginDest, 'hooks'));
|
|
2177
|
+
console.log(` Safety Hooks updated -> ${join(pluginDest, 'hooks')}`);
|
|
2069
2178
|
ensureHermesMcpConfig();
|
|
2179
|
+
ensureHermesHooksConfig();
|
|
2180
|
+
ensureHermesMcpSdk();
|
|
2070
2181
|
mkdirSync(pluginDest, { recursive: true });
|
|
2071
2182
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
2072
2183
|
installRuntimeDeps(pluginDest);
|
|
@@ -2092,9 +2203,10 @@ function uninstallHermes() {
|
|
|
2092
2203
|
}
|
|
2093
2204
|
|
|
2094
2205
|
if (removeIfExists(hermesPluginsDir())) {
|
|
2095
|
-
console.log(' Removed MCP server
|
|
2206
|
+
console.log(' Removed MCP server, safety policy and hooks');
|
|
2096
2207
|
}
|
|
2097
2208
|
removeHermesMcpConfigBlock();
|
|
2209
|
+
removeHermesHooksConfigBlock();
|
|
2098
2210
|
}
|
|
2099
2211
|
|
|
2100
2212
|
function hermesStatus() {
|
|
@@ -2106,6 +2218,10 @@ function hermesStatus() {
|
|
|
2106
2218
|
console.log(
|
|
2107
2219
|
` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2108
2220
|
);
|
|
2221
|
+
console.log(
|
|
2222
|
+
` Safety Hooks: ${existsSync(join(pluginDir, 'hooks', 'huaweicloud-safety.py')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2223
|
+
);
|
|
2224
|
+
console.log(` MCP Python SDK: ${hermesMcpSdkOk() ? '\x1b[32mReady\x1b[0m' : '\x1b[31mMissing\x1b[0m'}`);
|
|
2109
2225
|
let skillCount = 0;
|
|
2110
2226
|
if (existsSync(skillsDir)) {
|
|
2111
2227
|
skillCount = readdirSync(skillsDir, { withFileTypes: true }).filter(
|
|
@@ -2119,13 +2235,17 @@ function hermesStatus() {
|
|
|
2119
2235
|
if (existsSync(configPath)) {
|
|
2120
2236
|
try {
|
|
2121
2237
|
const config = readFileSync(configPath, 'utf8');
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2238
|
+
const mcpConfigured = config.includes('mcp_servers:') && config.includes('huaweicloud-devkit');
|
|
2239
|
+
const hooksConfigured = config.includes('hooks:') && config.includes('huaweicloud-safety.py');
|
|
2240
|
+
console.log(` MCP config: ${mcpConfigured ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
2241
|
+
console.log(` Hooks config: ${hooksConfigured ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
2124
2242
|
} catch {
|
|
2125
2243
|
console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
|
|
2244
|
+
console.log(` Hooks config: \x1b[31mInvalid\x1b[0m`);
|
|
2126
2245
|
}
|
|
2127
2246
|
} else {
|
|
2128
2247
|
console.log(` MCP config: \x1b[31mNot found\x1b[0m`);
|
|
2248
|
+
console.log(` Hooks config: \x1b[31mNot found\x1b[0m`);
|
|
2129
2249
|
}
|
|
2130
2250
|
}
|
|
2131
2251
|
|
|
@@ -2684,6 +2804,14 @@ async function cmdDoctor() {
|
|
|
2684
2804
|
mcpCfgTarget ? `Found in ${mcpCfgTarget} config` : 'Run: npx huaweicloud-devkit install',
|
|
2685
2805
|
);
|
|
2686
2806
|
|
|
2807
|
+
// Hermes MCP Python SDK (only checked when Hermes config is found)
|
|
2808
|
+
if (
|
|
2809
|
+
mcpCfgTarget === 'Hermes Agent' ||
|
|
2810
|
+
(existsSync(hermesConfigFile()) && readFileSync(hermesConfigFile(), 'utf8').includes('mcp_servers'))
|
|
2811
|
+
) {
|
|
2812
|
+
check('Hermes MCP Python SDK', hermesMcpSdkOk(), 'Run: pip3 install mcp');
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2687
2815
|
// hcloud CLI
|
|
2688
2816
|
const hcloudBin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
|
|
2689
2817
|
const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], {
|
|
@@ -38,15 +38,36 @@ function closeQuietly(ws) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function escapeRegExp(text) {
|
|
42
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
function stripEchoedLine(text, line, options = {}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
if (text.startsWith(line)) {
|
|
43
|
+
text = text.slice(line.length);
|
|
44
|
+
text = text.replace(/^\r?\n/, '');
|
|
45
|
+
if (!options.allowAttached) return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const nl of ['\r\n', '\n']) {
|
|
49
|
+
const idx = text.indexOf(nl + line);
|
|
50
|
+
if (idx !== -1) {
|
|
51
|
+
const before = text.slice(0, idx + nl.length);
|
|
52
|
+
const after = text.slice(idx + nl.length + line.length);
|
|
53
|
+
text = before + after.replace(/^\r?\n/, '');
|
|
54
|
+
if (!options.allowAttached) return text;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.allowAttached) {
|
|
60
|
+
for (let trail = 2; trail >= 0; trail--) {
|
|
61
|
+
for (const nl of ['\r\n', '\n']) {
|
|
62
|
+
const suffix = nl.repeat(trail);
|
|
63
|
+
if (text.endsWith(line + suffix)) {
|
|
64
|
+
return text.slice(0, text.length - line.length - suffix.length);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return text;
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
function buildMarkers(nonce = randomBytes(8).toString('hex')) {
|