huaweicloud-devkit 1.1.0-next.6 → 1.1.0-next.7
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 +19 -1
- package/README.zh-CN.md +19 -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 +34 -2
- package/plugins/huaweicloud-core/src/setup-cli.mjs +89 -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,24 @@ 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
|
+
> **Windows**: Hermes on Windows stores data under `%LOCALAPPDATA%\hermes` instead of `~/.hermes`. The installer detects this automatically.
|
|
118
|
+
|
|
101
119
|
### OpenClaw
|
|
102
120
|
|
|
103
121
|
```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,24 @@ 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
|
+
> **Windows**:Hermes 在 Windows 上数据存储在 `%LOCALAPPDATA%\hermes` 而非 `~/.hermes`,安装器会自动检测适配。
|
|
118
|
+
|
|
101
119
|
### OpenClaw
|
|
102
120
|
|
|
103
121
|
```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.7",
|
|
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.7",
|
|
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.7",
|
|
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.7",
|
|
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.7",
|
|
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.7",
|
|
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.7",
|
|
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
|
}
|
|
@@ -557,6 +577,8 @@ export async function uploadProjectWithSession(
|
|
|
557
577
|
throw new Error(`sandbox upload project: path is not a directory: ${localDir}`);
|
|
558
578
|
}
|
|
559
579
|
|
|
580
|
+
rotateUploadLog();
|
|
581
|
+
|
|
560
582
|
const projectName = basename(localDir);
|
|
561
583
|
const targetParentDir = remoteDir || '/workspace';
|
|
562
584
|
const archiveRemotePath = `${targetParentDir}/${projectName}.tar.gz`;
|
|
@@ -567,6 +589,15 @@ export async function uploadProjectWithSession(
|
|
|
567
589
|
|
|
568
590
|
uploadLog(`uploadProject: ${localDir} -> ${archiveRemotePath} (archive=${archiveSize} bytes, md5=${expectedMd5})`);
|
|
569
591
|
|
|
592
|
+
const SIZE_50MB = 50 * 1024 * 1024;
|
|
593
|
+
if (archiveSize > SIZE_50MB) {
|
|
594
|
+
uploadLog(
|
|
595
|
+
`uploadProject: archive size ${(archiveSize / (1024 * 1024)).toFixed(1)}MB exceeds 50MB. ` +
|
|
596
|
+
`Dependencies or platform binaries may have been included. ` +
|
|
597
|
+
`Ensure exclude list contains "**/node_modules" to match all nesting levels.`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
570
601
|
let result;
|
|
571
602
|
let tunnelError;
|
|
572
603
|
for (let attempt = 0; attempt < UPLOAD_MAX_RETRIES; attempt++) {
|
|
@@ -577,7 +608,8 @@ export async function uploadProjectWithSession(
|
|
|
577
608
|
break;
|
|
578
609
|
} catch (error) {
|
|
579
610
|
tunnelError = error;
|
|
580
|
-
|
|
611
|
+
const errorType = error.code || error.name || 'unknown';
|
|
612
|
+
uploadLog(`uploadProject: attempt ${attempt + 1}/${UPLOAD_MAX_RETRIES} failed [${errorType}]: ${error.message}`);
|
|
581
613
|
await new Promise((r) => setTimeout(r, 5000));
|
|
582
614
|
}
|
|
583
615
|
}
|
|
@@ -2035,28 +2035,99 @@ 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
|
+
|
|
2038
2101
|
async function installHermes() {
|
|
2039
2102
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
2040
2103
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
2041
2104
|
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
2105
|
+
const hooksDir = join(PLUGIN_ROOT, 'hooks');
|
|
2042
2106
|
const pluginDest = hermesPluginsDir();
|
|
2107
|
+
const skipMcp = process.argv.includes('--skip-mcp-server');
|
|
2043
2108
|
|
|
2044
2109
|
copyDir(skillsSrc, hermesSkillsDir());
|
|
2045
2110
|
console.log(` Skills -> ${hermesSkillsDir()}`);
|
|
2046
2111
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2112
|
+
if (!skipMcp) {
|
|
2113
|
+
copyDir(srcDir, join(pluginDest, 'src'));
|
|
2114
|
+
console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
|
|
2115
|
+
}
|
|
2049
2116
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2050
2117
|
console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
|
|
2118
|
+
copyDir(hooksDir, join(pluginDest, 'hooks'));
|
|
2119
|
+
console.log(` Safety Hooks -> ${join(pluginDest, 'hooks')}`);
|
|
2051
2120
|
|
|
2052
|
-
ensureHermesMcpConfig();
|
|
2053
|
-
|
|
2121
|
+
if (!skipMcp) ensureHermesMcpConfig();
|
|
2122
|
+
ensureHermesHooksConfig();
|
|
2123
|
+
if (!skipMcp) installRuntimeDeps(pluginDest);
|
|
2054
2124
|
}
|
|
2055
2125
|
|
|
2056
2126
|
async function updateHermes() {
|
|
2057
2127
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
2058
2128
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
2059
2129
|
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
2130
|
+
const hooksDir = join(PLUGIN_ROOT, 'hooks');
|
|
2060
2131
|
const pluginDest = hermesPluginsDir();
|
|
2061
2132
|
|
|
2062
2133
|
copyDir(skillsSrc, hermesSkillsDir());
|
|
@@ -2066,7 +2137,10 @@ async function updateHermes() {
|
|
|
2066
2137
|
console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
|
|
2067
2138
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2068
2139
|
console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
|
|
2140
|
+
copyDir(hooksDir, join(pluginDest, 'hooks'));
|
|
2141
|
+
console.log(` Safety Hooks updated -> ${join(pluginDest, 'hooks')}`);
|
|
2069
2142
|
ensureHermesMcpConfig();
|
|
2143
|
+
ensureHermesHooksConfig();
|
|
2070
2144
|
mkdirSync(pluginDest, { recursive: true });
|
|
2071
2145
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
2072
2146
|
installRuntimeDeps(pluginDest);
|
|
@@ -2092,9 +2166,10 @@ function uninstallHermes() {
|
|
|
2092
2166
|
}
|
|
2093
2167
|
|
|
2094
2168
|
if (removeIfExists(hermesPluginsDir())) {
|
|
2095
|
-
console.log(' Removed MCP server
|
|
2169
|
+
console.log(' Removed MCP server, safety policy and hooks');
|
|
2096
2170
|
}
|
|
2097
2171
|
removeHermesMcpConfigBlock();
|
|
2172
|
+
removeHermesHooksConfigBlock();
|
|
2098
2173
|
}
|
|
2099
2174
|
|
|
2100
2175
|
function hermesStatus() {
|
|
@@ -2106,6 +2181,9 @@ function hermesStatus() {
|
|
|
2106
2181
|
console.log(
|
|
2107
2182
|
` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2108
2183
|
);
|
|
2184
|
+
console.log(
|
|
2185
|
+
` Safety Hooks: ${existsSync(join(pluginDir, 'hooks', 'huaweicloud-safety.py')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2186
|
+
);
|
|
2109
2187
|
let skillCount = 0;
|
|
2110
2188
|
if (existsSync(skillsDir)) {
|
|
2111
2189
|
skillCount = readdirSync(skillsDir, { withFileTypes: true }).filter(
|
|
@@ -2119,13 +2197,17 @@ function hermesStatus() {
|
|
|
2119
2197
|
if (existsSync(configPath)) {
|
|
2120
2198
|
try {
|
|
2121
2199
|
const config = readFileSync(configPath, 'utf8');
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2200
|
+
const mcpConfigured = config.includes('mcp_servers:') && config.includes('huaweicloud-devkit');
|
|
2201
|
+
const hooksConfigured = config.includes('hooks:') && config.includes('huaweicloud-safety.py');
|
|
2202
|
+
console.log(` MCP config: ${mcpConfigured ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
2203
|
+
console.log(` Hooks config: ${hooksConfigured ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
2124
2204
|
} catch {
|
|
2125
2205
|
console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
|
|
2206
|
+
console.log(` Hooks config: \x1b[31mInvalid\x1b[0m`);
|
|
2126
2207
|
}
|
|
2127
2208
|
} else {
|
|
2128
2209
|
console.log(` MCP config: \x1b[31mNot found\x1b[0m`);
|
|
2210
|
+
console.log(` Hooks config: \x1b[31mNot found\x1b[0m`);
|
|
2129
2211
|
}
|
|
2130
2212
|
}
|
|
2131
2213
|
|
|
@@ -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')) {
|