huaweicloud-devkit 1.1.0-next.16 → 1.1.0-next.18
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 +8 -0
- package/README.zh-CN.md +8 -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/hooks/huaweicloud-safety.py +29 -12
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +11 -11
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +4 -3
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +7 -6
- package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +7 -4
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +5 -4
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +9 -5
- package/plugins/huaweicloud-core/src/setup-cli.mjs +206 -8
- package/plugins/huaweicloud-core/src/tools.mjs +9 -1
package/README.md
CHANGED
|
@@ -14,6 +14,14 @@ Supports OpenCode, Codex, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), Off
|
|
|
14
14
|
|
|
15
15
|
- Node.js >= 22
|
|
16
16
|
|
|
17
|
+
> **China mainland users**: If you experience slow downloads or connection issues with the default npm registry, configure the Huawei Cloud npm mirror:
|
|
18
|
+
>
|
|
19
|
+
> ```bash
|
|
20
|
+
> npm config set registry https://mirrors.huaweicloud.com/repository/npm/
|
|
21
|
+
> ```
|
|
22
|
+
>
|
|
23
|
+
> Restore the default registry: `npm config delete registry`
|
|
24
|
+
|
|
17
25
|
## Quick Start
|
|
18
26
|
|
|
19
27
|
> If `--target` is omitted, the installer auto-detects agents on your machine. When multiple agents are detected, **all of them** will be installed. Specify `--target` to control which agent receives the install.
|
package/README.zh-CN.md
CHANGED
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
- Node.js >= 22
|
|
16
16
|
|
|
17
|
+
> **国内用户**:使用默认 npm 镜像时可能遇到下载缓慢或连接失败的问题,建议配置华为云 npm 镜像:
|
|
18
|
+
>
|
|
19
|
+
> ```bash
|
|
20
|
+
> npm config set registry https://mirrors.huaweicloud.com/repository/npm/
|
|
21
|
+
> ```
|
|
22
|
+
>
|
|
23
|
+
> 恢复默认镜像:`npm config delete registry`
|
|
24
|
+
|
|
17
25
|
## 快速开始
|
|
18
26
|
|
|
19
27
|
> 省略 `--target` 时,安装器会自动检测机器上的 agent,检测到多个时**全部安装**。建议始终指定 `--target` 以明确安装目标。
|
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.18",
|
|
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.18",
|
|
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.18",
|
|
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.18",
|
|
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.18",
|
|
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.18",
|
|
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",
|
|
@@ -138,6 +138,31 @@ def first_denied_command_rule(text):
|
|
|
138
138
|
return None
|
|
139
139
|
|
|
140
140
|
|
|
141
|
+
def evaluate(tool_name, tool_input):
|
|
142
|
+
"""Run the full safety check and return a raw block reason, or None to allow.
|
|
143
|
+
|
|
144
|
+
Shared by the CLI shell (``main``) and the Hermes Python-plugin hook, which
|
|
145
|
+
reuses this module via ``importlib`` so a single implementation enforces the
|
|
146
|
+
policy everywhere. The returned reason does NOT include ``DENY_PREFIX`` —
|
|
147
|
+
callers decide the exact output shape.
|
|
148
|
+
"""
|
|
149
|
+
text = command_text(tool_input)
|
|
150
|
+
|
|
151
|
+
if CONFIG_FILE_RE and CONFIG_FILE_RE.search(text):
|
|
152
|
+
return "reading Huawei Cloud credential/profile files can expose AK/SK or tokens. Use redacted toolkit tools."
|
|
153
|
+
if ENV_DUMP_RE.search(text):
|
|
154
|
+
return "dumping cloud credential environment variables is not allowed."
|
|
155
|
+
if SECRET_READ_RE and SECRET_READ_RE.search(text):
|
|
156
|
+
return "direct secret value retrieval would put plaintext secrets into the agent context."
|
|
157
|
+
denied_rule = first_denied_command_rule(text)
|
|
158
|
+
if denied_rule:
|
|
159
|
+
return f"{denied_rule.get('message')} Remediation: {denied_rule.get('remediation')}"
|
|
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
|
+
return "unapproved Huawei Cloud write operations must be planned first and explicitly approved by the user."
|
|
162
|
+
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
141
166
|
def main():
|
|
142
167
|
try:
|
|
143
168
|
data = json.load(sys.stdin)
|
|
@@ -145,20 +170,12 @@ def main():
|
|
|
145
170
|
allow()
|
|
146
171
|
|
|
147
172
|
tool_name = data.get("tool_name", "")
|
|
148
|
-
|
|
173
|
+
tool_input = data.get("tool_input", {})
|
|
149
174
|
hermes = "hook_event_name" in data
|
|
150
175
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
deny("dumping cloud credential environment variables is not allowed.", hermes)
|
|
155
|
-
if SECRET_READ_RE and SECRET_READ_RE.search(text):
|
|
156
|
-
deny("direct secret value retrieval would put plaintext secrets into the agent context.", hermes)
|
|
157
|
-
denied_rule = first_denied_command_rule(text)
|
|
158
|
-
if denied_rule:
|
|
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)
|
|
176
|
+
reason = evaluate(tool_name, tool_input)
|
|
177
|
+
if reason:
|
|
178
|
+
deny(reason, hermes)
|
|
162
179
|
|
|
163
180
|
allow()
|
|
164
181
|
|
|
@@ -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.18",
|
|
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.",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"match": {
|
|
158
158
|
"all": [
|
|
159
159
|
{ "field": "text", "regex": "(Delete|BatchDelete|Remove|\\brm\\b|delete)" },
|
|
160
|
-
{ "field": "text", "regex": "(--force
|
|
160
|
+
{ "field": "text", "regex": "(\\s--force|\\s-f\\s|\\s--recursive|\\s-r\\s|delete_publicip\\s*[=:]\\s*true)" }
|
|
161
161
|
]
|
|
162
162
|
},
|
|
163
163
|
"message": "The command combines deletion with force, recursive, or cascading deletion behavior.",
|
|
@@ -42,14 +42,14 @@ Domain expertise for Huawei Cloud Identity and Access Management (IAM). Covers u
|
|
|
42
42
|
|
|
43
43
|
## Common Workflows
|
|
44
44
|
|
|
45
|
-
| Task | Command
|
|
46
|
-
| ------------------------ |
|
|
47
|
-
| List users | hcloud IAM
|
|
48
|
-
| Create group | hcloud IAM
|
|
49
|
-
| Create custom policy | hcloud IAM
|
|
50
|
-
| Create agency | hcloud IAM CreateAgency --agency.name=<n> --agency.domain_id=<id> --agency.
|
|
51
|
-
| Get temporary credential | hcloud STS
|
|
52
|
-
| Attach policy
|
|
45
|
+
| Task | Command | Steps |
|
|
46
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
47
|
+
| List users | hcloud IAM KeystoneListUsers --cli-region=<r> | references/iam-ops.md |
|
|
48
|
+
| Create group | hcloud IAM CreateGroupV5 --group_name=<name> | references/iam-ops.md |
|
|
49
|
+
| Create custom policy | hcloud IAM CreateCloudServiceCustomPolicy --role.display_name=<n> --role.type=<type> --role.policy.Statement.1.Effect=Allow | references/policy-examples.md |
|
|
50
|
+
| Create agency | hcloud IAM CreateAgency --agency.name=<n> --agency.domain_id=<id> --agency.trust_domain_name=<domain> | references/agency.md |
|
|
51
|
+
| Get temporary credential | hcloud STS AssumeAgency --agency_urn=<urn> --agency_session_name=<n> --duration_seconds=3600 | references/sts.md |
|
|
52
|
+
| Attach policy to group | hcloud IAM AttachGroupPolicyV5 --group_id=<id> --policy_id=<id> | references/iam-ops.md |
|
|
53
53
|
|
|
54
54
|
## Policy Examples (Least Privilege)
|
|
55
55
|
|
|
@@ -88,10 +88,10 @@ FunctionGraph needs an agency to access VPC resources (RDS, DCS, etc.):
|
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
90
|
# 1. Create agency with FunctionGraph trust
|
|
91
|
-
hcloud IAM CreateAgency --
|
|
91
|
+
hcloud IAM CreateAgency --agency.name=<name> --agency.trust_domain_name=functiongraph
|
|
92
92
|
|
|
93
|
-
# 2.
|
|
94
|
-
hcloud IAM
|
|
93
|
+
# 2. Grant role to agency
|
|
94
|
+
hcloud IAM AssociateAgencyWithDomainPermission --agency_id=<id> --role_id=<role-id>
|
|
95
95
|
|
|
96
96
|
# 3. Use in CreateFunction
|
|
97
97
|
hcloud FunctionGraph CreateFunction ... --app_xrole=<agency-name>
|
|
@@ -55,7 +55,7 @@ hcloud RDS <Operation> --cli-region=<region> [--key=value ...]
|
|
|
55
55
|
hcloud RDS ListInstances --cli-region=<r>
|
|
56
56
|
hcloud RDS ListFlavors --database_name=<engine> --cli-region=<r>
|
|
57
57
|
hcloud RDS ListDatastores --database_name=<engine> --cli-region=<r>
|
|
58
|
-
hcloud RDS ListEngineFlavors --instance_id=<id> --cli-region=<r>
|
|
58
|
+
hcloud RDS ListEngineFlavors --instance_id=<id> --availability_zone_ids=<az> --ha_mode=<mode> --cli-region=<r>
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
### Create Instance
|
|
@@ -150,7 +150,8 @@ hcloud RDS CreateRestoreInstance --instance_id=<id> --backup_id=<id> --name=<new
|
|
|
150
150
|
## Read Replicas
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
|
-
|
|
153
|
+
# Create a read replica via CreateInstance with --replica_of_id
|
|
154
|
+
hcloud RDS CreateInstance --replica_of_id=<primary-id> --name=<name> --flavor_ref=<id> --volume.type=<vol-type> --volume.size=<size> --cli-region=<r>
|
|
154
155
|
```
|
|
155
156
|
|
|
156
157
|
## Fault Diagnosis
|
|
@@ -159,7 +160,7 @@ hcloud RDS CreateReadReplica --replica_of_id=<primary-id> --name=<name> --flavor
|
|
|
159
160
|
hcloud RDS ListErrorLogs --instance_id=<id> --start_date=2024-01-01T00:00:00Z --end_date=2024-01-31T23:59:59Z --cli-region=<r>
|
|
160
161
|
hcloud RDS ListSlowLogs --instance_id=<id> --start_date=... --end_date=... --cli-region=<r>
|
|
161
162
|
hcloud RDS ShowReplicationStatus --instance_id=<id> --cli-region=<r>
|
|
162
|
-
hcloud RDS ListInstanceDiagnosis --cli-region=<r>
|
|
163
|
+
hcloud RDS ListInstanceDiagnosis --engine=<engine> --cli-region=<r>
|
|
163
164
|
```
|
|
164
165
|
|
|
165
166
|
## Connecting
|
|
@@ -95,7 +95,7 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
95
95
|
2. **Real-name verification only** (`HDKIT_NOT_REALNAME`): tell the developer once, "Huawei Cloud requires real-name verification before using the sandbox — please complete it in the Huawei Cloud console (实名认证)." and stop — do not retry `connect` in a loop
|
|
96
96
|
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
|
|
97
97
|
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
|
|
98
|
-
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`. The `source` parameter identifies the calling agent (valid values: `
|
|
98
|
+
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`. The `source` parameter identifies the calling agent (valid values: `CLI`, `WEB`, `VSCODE`, `WEBVNC`, `WEBPTY`, `WEBIDE`, `CURSOR`, etc. — case-sensitive, all uppercase). The `git` parameter (with `repo_url`, `repo_name`, `target_path`) is accepted but does NOT auto-clone the repository — always clone manually.
|
|
99
99
|
6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs, DevBridge tunnels, and stale web processes from previous deployments can cause port conflicts and quota errors. Run cleanup immediately after connect:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
@@ -253,7 +253,7 @@ fi
|
|
|
253
253
|
|
|
254
254
|
```bash
|
|
255
255
|
# Step A: List all tunnels (both active and stale)
|
|
256
|
-
devbridge
|
|
256
|
+
devbridge list -j
|
|
257
257
|
# Step B: Remove all stale tunnels
|
|
258
258
|
devbridge delete-all
|
|
259
259
|
# Step C: Retry tunnel creation
|
|
@@ -265,6 +265,7 @@ This eliminates the most common deployment failure — historical tunnels from p
|
|
|
265
265
|
|
|
266
266
|
- The public URL has the form `https://<id>-<port>.cn-north-4-bridge.myhuaweicloud.com` (from the `Tunnel URL:` line).
|
|
267
267
|
- **Return this URL to the developer as the deployment result link.** Keep the host process running (do not close the session before handing over the URL).
|
|
268
|
+
- Tunnel `description` (`-d`) accepts only Chinese characters, letters, and digits (0-64). Symbols such as `-`/`_`/spaces are rejected (`Invalid tunnel description`).
|
|
268
269
|
- Internal docs: https://huaweicloud.github.io/devspace-devbridge/
|
|
269
270
|
|
|
270
271
|
**No local downgrade**: if the tunnel tooling cannot be installed in the sandbox, STOP and report a generic error ("无法生成访问地址") without technical detail. Never install it on the developer's local machine — a local install would defeat the purpose of sandbox deployment.
|
|
@@ -356,13 +357,13 @@ Use the detected `PKG_MGR` for all package installations below.
|
|
|
356
357
|
|
|
357
358
|
**Architecture awareness**: the sandbox runs Linux aarch64 (ARM64). Native binaries built on x64 (Windows/macOS Intel) will not execute. Always install dependencies and build inside the sandbox. For projects with native addons (Taro `@swc/core`, Prisma, `esbuild`, `node-gyp`), local x64 pre-build + upload of `dist/` output is a viable alternative when sandbox builds fail.
|
|
358
359
|
|
|
359
|
-
**GitCode SSL**: if `git clone` from GitCode fails with SSL certificate errors:
|
|
360
|
+
**GitCode SSL**: if `git clone` from GitCode fails with SSL certificate errors, use a one-shot override (do NOT set it globally — that would disable cert verification for every repo):
|
|
360
361
|
|
|
361
362
|
```bash
|
|
362
|
-
git
|
|
363
|
+
git -c http.sslVerify=false clone <repo-url>
|
|
363
364
|
```
|
|
364
365
|
|
|
365
|
-
Then retry the clone. This
|
|
366
|
+
Then retry the clone. This bypasses SSL verification only for this single clone.
|
|
366
367
|
|
|
367
368
|
#### 3b: Install nginx (before project upload)
|
|
368
369
|
|
|
@@ -751,7 +752,7 @@ Follow the standard [Expose the deployed app](#expose-the-deployed-app-public-ur
|
|
|
751
752
|
|
|
752
753
|
Use `exec_with_session` to background DevBridge. For SSR, DevBridge tunnels the nginx public port (not the Node port directly).
|
|
753
754
|
|
|
754
|
-
**Pre-flight**: always run `devbridge delete-all` before creating a new tunnel to prevent `10006: quota exceeded` from accumulated stale tunnels. If you still get quota error, list tunnels with `devbridge
|
|
755
|
+
**Pre-flight**: always run `devbridge delete-all` before creating a new tunnel to prevent `10006: quota exceeded` from accumulated stale tunnels. If you still get quota error, list tunnels with `devbridge list -j`, delete stale ones, and retry.
|
|
755
756
|
|
|
756
757
|
Extract the tunnel URL from DevBridge output. The public URL has the form `https://<id>-<port>.cn-north-4-bridge.myhuaweicloud.com`. **Return this URL to the developer as the deployment result.**
|
|
757
758
|
|
|
@@ -29,8 +29,8 @@ Domain expertise for WAF and Anti-DDoS. Covers WAF policy/rules, AAD protection,
|
|
|
29
29
|
|
|
30
30
|
| Task | Operation |
|
|
31
31
|
| ------------------- | --------------------------------------------------------------- |
|
|
32
|
-
| List WAF instances | `
|
|
33
|
-
| List policies | `
|
|
32
|
+
| List WAF instances | `ListCompositeHosts --cli-region=<r> --project_id=<p>` |
|
|
33
|
+
| List policies | `ListPolicy --cli-region=<r> --project_id=<p>` |
|
|
34
34
|
| Create custom rule | `BatchCreateCustomRule --cli-region=<r> --project_id=<p>` |
|
|
35
35
|
| Create IP blacklist | `BatchCreateWhiteblackipRule --cli-region=<r> --project_id=<p>` |
|
|
36
36
|
| Create CC rule | `BatchCreateCcRule --cli-region=<r> --project_id=<p>` |
|
|
@@ -39,8 +39,11 @@ Domain expertise for WAF and Anti-DDoS. Covers WAF policy/rules, AAD protection,
|
|
|
39
39
|
### AAD
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
|
|
43
|
-
hcloud AAD
|
|
42
|
+
# List AAD instances (requires --cli-region=cn-north-4 or ap-southeast-1)
|
|
43
|
+
hcloud AAD ListInstance --cli-region=<r> --project_id=<p>
|
|
44
|
+
|
|
45
|
+
# List DDoS attack events
|
|
46
|
+
hcloud AAD ListDDoSAttackEvent --cli-region=<r> --project_id=<p>
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
## Troubleshooting
|
|
@@ -71,6 +71,7 @@ function runHcloudOnce(plan, options) {
|
|
|
71
71
|
const executable = options.executable || options.env?.HCLOUD_BIN || discoverHcloudPath() || 'hcloud';
|
|
72
72
|
const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
|
|
73
73
|
const cwd = options.cwd || undefined;
|
|
74
|
+
const stdin = options.stdin ?? 'y\n';
|
|
74
75
|
|
|
75
76
|
return new Promise((resolve) => {
|
|
76
77
|
const proxySettings = getProxySettings();
|
|
@@ -90,11 +91,11 @@ function runHcloudOnce(plan, options) {
|
|
|
90
91
|
...options.env,
|
|
91
92
|
},
|
|
92
93
|
});
|
|
93
|
-
if (
|
|
94
|
-
if (typeof
|
|
95
|
-
|
|
94
|
+
if (stdin) {
|
|
95
|
+
if (typeof stdin === 'function') {
|
|
96
|
+
stdin(child.stdin);
|
|
96
97
|
} else {
|
|
97
|
-
child.stdin.write(String(
|
|
98
|
+
child.stdin.write(String(stdin));
|
|
98
99
|
child.stdin.end();
|
|
99
100
|
}
|
|
100
101
|
}
|
|
@@ -836,7 +836,7 @@ fi`;
|
|
|
836
836
|
try {
|
|
837
837
|
const tunnelCheck = await execOneShot(
|
|
838
838
|
workspaceId,
|
|
839
|
-
'devbridge
|
|
839
|
+
'devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' && echo "ACTIVE" || echo "INACTIVE"',
|
|
840
840
|
username,
|
|
841
841
|
10000,
|
|
842
842
|
);
|
|
@@ -912,7 +912,7 @@ export async function deployCheck(
|
|
|
912
912
|
`fi`,
|
|
913
913
|
``,
|
|
914
914
|
`TOTAL=$((TOTAL+1))`,
|
|
915
|
-
`if devbridge
|
|
915
|
+
`if devbridge list -j 2>/dev/null | grep -q '"tunnelId"'; then`,
|
|
916
916
|
` echo "devbridge_tunnel:PASS"`,
|
|
917
917
|
` PASS=$((PASS+1))`,
|
|
918
918
|
`else`,
|
|
@@ -920,8 +920,9 @@ export async function deployCheck(
|
|
|
920
920
|
`fi`,
|
|
921
921
|
``,
|
|
922
922
|
`TOTAL=$((TOTAL+1))`,
|
|
923
|
-
`
|
|
924
|
-
`
|
|
923
|
+
`TUNNEL_ID=$(devbridge list -j 2>/dev/null | grep -oP '"tunnelId":\\s*"\\K[^"]+' | head -1)`,
|
|
924
|
+
`TUNNEL_URL="https://\${TUNNEL_ID}-${port}.cn-north-4-bridge.myhuaweicloud.com"`,
|
|
925
|
+
`if [ -n "$TUNNEL_ID" ] && [ -n "$TUNNEL_URL" ]; then`,
|
|
925
926
|
` HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
|
|
926
927
|
` if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then`,
|
|
927
928
|
` echo "tunnel_url_accessible:PASS ($TUNNEL_URL -> $HTTP_CODE)"`,
|
|
@@ -947,6 +948,7 @@ fi
|
|
|
947
948
|
: ''
|
|
948
949
|
}`,
|
|
949
950
|
`echo "SCORE:\${PASS}/\${TOTAL}"`,
|
|
951
|
+
`echo "TUNNEL_URL:\${TUNNEL_URL:-}"`,
|
|
950
952
|
`[ "\${PASS}" = "\${TOTAL}" ] && echo "VERDICT:COMPLETE" || echo "VERDICT:INCOMPLETE"`,
|
|
951
953
|
]
|
|
952
954
|
.filter(Boolean)
|
|
@@ -957,10 +959,11 @@ fi
|
|
|
957
959
|
const checks = {};
|
|
958
960
|
const lines = stdout.split('\n');
|
|
959
961
|
for (const line of lines) {
|
|
960
|
-
const m = line.match(/^(\w+):(
|
|
962
|
+
const m = line.match(/^(\w+):(\w+)\b(.*)/);
|
|
961
963
|
if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
|
|
962
964
|
}
|
|
963
965
|
const scoreMatch = stdout.match(/SCORE:(\d+)\/(\d+)/);
|
|
966
|
+
const tunnelMatch = stdout.match(/TUNNEL_URL:(https:\/\/[^\s]+)/);
|
|
964
967
|
const complete = /VERDICT:COMPLETE/.test(stdout);
|
|
965
968
|
|
|
966
969
|
const missing = [];
|
|
@@ -976,6 +979,7 @@ fi
|
|
|
976
979
|
checkType: isCrossPlatform ? 'cross-platform' : 'standard',
|
|
977
980
|
checks,
|
|
978
981
|
score: scoreMatch ? { pass: parseInt(scoreMatch[1], 10), total: parseInt(scoreMatch[2], 10) } : null,
|
|
982
|
+
publicUrl: tunnelMatch ? tunnelMatch[1] : undefined,
|
|
979
983
|
missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
|
|
980
984
|
nextStep: !complete
|
|
981
985
|
? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
|
|
@@ -474,6 +474,7 @@ function installRuntimeDeps(pluginsDir) {
|
|
|
474
474
|
};
|
|
475
475
|
let r = spawnSync('npm', ['install', '--omit=dev'], spawnOpts);
|
|
476
476
|
const isRetryable = (res) => {
|
|
477
|
+
if (res.status === 0) return false;
|
|
477
478
|
const stderr = (res.stderr || '').toString();
|
|
478
479
|
return res.error?.code === 'EPERM' || res.error?.code === 'EBUSY' || /EPERM|EBUSY/.test(stderr);
|
|
479
480
|
};
|
|
@@ -482,6 +483,11 @@ function installRuntimeDeps(pluginsDir) {
|
|
|
482
483
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
483
484
|
r = spawnSync('npm', ['install', '--omit=dev'], spawnOpts);
|
|
484
485
|
}
|
|
486
|
+
if (r.status !== 0 && !isRetryable(r)) {
|
|
487
|
+
console.log(` \x1b[33m[RETRY]\x1b[0m npm install failed (exit ${r.status}), retrying in 3s...`);
|
|
488
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
|
|
489
|
+
r = spawnSync('npm', ['install', '--omit=dev'], spawnOpts);
|
|
490
|
+
}
|
|
485
491
|
const undiciDir = join(pluginsDir, 'node_modules', 'undici');
|
|
486
492
|
if (r.status === 0 && existsSync(undiciDir)) {
|
|
487
493
|
console.log(` Runtime deps installed -> ${join(pluginsDir, 'node_modules')}`);
|
|
@@ -489,12 +495,14 @@ function installRuntimeDeps(pluginsDir) {
|
|
|
489
495
|
const errCode = r.error?.code;
|
|
490
496
|
const err = (r.stderr || '').toString().trim().split(/\r?\n/).slice(-2).join(' ');
|
|
491
497
|
const hint = errCode === 'ENOENT' ? ' (npm not found — ensure Node.js/npm is in PATH)' : '';
|
|
492
|
-
|
|
498
|
+
const msg = `npm install failed in ${pluginsDir}${err ? `: ${err}` : ''}${hint}`;
|
|
499
|
+
console.log(` \x1b[31m[ERROR]\x1b[0m ${msg}`);
|
|
493
500
|
console.log(' Manual fix: cd %s && npm install', pluginsDir);
|
|
494
501
|
if (!existsSync(undiciDir)) {
|
|
495
502
|
console.log(` \x1b[31m[ERROR]\x1b[0m undici is NOT installed. MCP server will fail to start.`);
|
|
496
503
|
console.log(` Run manually: cd "${pluginsDir}" && npm install undici@^8.10.0`);
|
|
497
504
|
}
|
|
505
|
+
throw new Error(msg);
|
|
498
506
|
}
|
|
499
507
|
}
|
|
500
508
|
|
|
@@ -2108,6 +2116,14 @@ function hermesPluginsDir() {
|
|
|
2108
2116
|
return join(hermesHomeDir(), 'huaweicloud-plugins');
|
|
2109
2117
|
}
|
|
2110
2118
|
|
|
2119
|
+
function hermesPythonPluginsDir() {
|
|
2120
|
+
return join(hermesHomeDir(), 'plugins');
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function hermesSafetyPluginDir() {
|
|
2124
|
+
return join(hermesPythonPluginsDir(), 'huaweicloud-safety');
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2111
2127
|
function hermesConfigFile() {
|
|
2112
2128
|
return join(hermesHomeDir(), 'config.yaml');
|
|
2113
2129
|
}
|
|
@@ -2186,17 +2202,26 @@ function removeHermesMcpConfigBlock() {
|
|
|
2186
2202
|
console.log(' MCP config cleaned');
|
|
2187
2203
|
}
|
|
2188
2204
|
|
|
2205
|
+
function hermesHookScript() {
|
|
2206
|
+
return join(hermesPluginsDir(), 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function hermesHookCommand() {
|
|
2210
|
+
const pythonBin = process.platform === 'win32' ? 'python' : 'python3';
|
|
2211
|
+
return `${pythonBin} ${hermesHookScript()}`;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2189
2214
|
function ensureHermesHooksConfig() {
|
|
2190
2215
|
const configPath = hermesConfigFile();
|
|
2191
|
-
const
|
|
2192
|
-
const hookScript = join(pluginDest, 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
|
|
2216
|
+
const hookCommand = hermesHookCommand();
|
|
2193
2217
|
|
|
2194
2218
|
const blockLines = [
|
|
2195
2219
|
'hooks:',
|
|
2196
2220
|
' pre_tool_call:',
|
|
2197
2221
|
' - matcher: "terminal"',
|
|
2198
|
-
` command: "
|
|
2222
|
+
` command: "${hookCommand}"`,
|
|
2199
2223
|
' timeout: 5',
|
|
2224
|
+
' fail_closed: true',
|
|
2200
2225
|
];
|
|
2201
2226
|
const block = blockLines.join('\n');
|
|
2202
2227
|
|
|
@@ -2205,10 +2230,21 @@ function ensureHermesHooksConfig() {
|
|
|
2205
2230
|
try {
|
|
2206
2231
|
existing = readFileSync(configPath, 'utf8');
|
|
2207
2232
|
} catch {}
|
|
2208
|
-
if (
|
|
2233
|
+
if (
|
|
2234
|
+
existing.includes('hooks:') &&
|
|
2235
|
+
existing.includes(`command: "${hookCommand}"`) &&
|
|
2236
|
+
existing.includes('fail_closed: true')
|
|
2237
|
+
) {
|
|
2209
2238
|
console.log(` Hooks config unchanged: ${configPath}`);
|
|
2210
2239
|
return false;
|
|
2211
2240
|
}
|
|
2241
|
+
removeHermesHooksConfigBlock();
|
|
2242
|
+
existing = '';
|
|
2243
|
+
if (existsSync(configPath)) {
|
|
2244
|
+
try {
|
|
2245
|
+
existing = readFileSync(configPath, 'utf8');
|
|
2246
|
+
} catch {}
|
|
2247
|
+
}
|
|
2212
2248
|
}
|
|
2213
2249
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
2214
2250
|
const newContent = existing ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
|
|
@@ -2217,6 +2253,154 @@ function ensureHermesHooksConfig() {
|
|
|
2217
2253
|
return true;
|
|
2218
2254
|
}
|
|
2219
2255
|
|
|
2256
|
+
function hermesHookScriptMtime() {
|
|
2257
|
+
const scriptPath = hermesHookScript();
|
|
2258
|
+
try {
|
|
2259
|
+
return statSync(scriptPath).mtime.toISOString();
|
|
2260
|
+
} catch {
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function hermesAllowlistPath() {
|
|
2266
|
+
return join(hermesHomeDir(), 'shell-hooks-allowlist.json');
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
function ensureHermesHookAllowlist() {
|
|
2270
|
+
const hookCommand = hermesHookCommand();
|
|
2271
|
+
const allowlistPath = hermesAllowlistPath();
|
|
2272
|
+
|
|
2273
|
+
let data = { approvals: [] };
|
|
2274
|
+
if (existsSync(allowlistPath)) {
|
|
2275
|
+
try {
|
|
2276
|
+
const parsed = JSON.parse(readFileSync(allowlistPath, 'utf8'));
|
|
2277
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
2278
|
+
data = parsed;
|
|
2279
|
+
}
|
|
2280
|
+
} catch {}
|
|
2281
|
+
}
|
|
2282
|
+
const approvals = Array.isArray(data.approvals) ? data.approvals : [];
|
|
2283
|
+
const already = approvals.some(
|
|
2284
|
+
(a) => a && typeof a === 'object' && a.event === 'pre_tool_call' && a.command === hookCommand,
|
|
2285
|
+
);
|
|
2286
|
+
if (already) {
|
|
2287
|
+
console.log(` Hook allowlist unchanged: ${allowlistPath}`);
|
|
2288
|
+
return false;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
const entry = {
|
|
2292
|
+
event: 'pre_tool_call',
|
|
2293
|
+
command: hookCommand,
|
|
2294
|
+
approved_at: new Date().toISOString(),
|
|
2295
|
+
script_mtime_at_approval: hermesHookScriptMtime(),
|
|
2296
|
+
};
|
|
2297
|
+
data.approvals = [...approvals, entry];
|
|
2298
|
+
mkdirSync(dirname(allowlistPath), { recursive: true });
|
|
2299
|
+
writeFileSync(allowlistPath, JSON.stringify(data, null, 2));
|
|
2300
|
+
console.log(` Hook allowlist updated: ${allowlistPath}`);
|
|
2301
|
+
return true;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function hermesHookAllowlisted() {
|
|
2305
|
+
const allowlistPath = hermesAllowlistPath();
|
|
2306
|
+
if (!existsSync(allowlistPath)) return false;
|
|
2307
|
+
try {
|
|
2308
|
+
const data = JSON.parse(readFileSync(allowlistPath, 'utf8'));
|
|
2309
|
+
const cmd = hermesHookCommand();
|
|
2310
|
+
return (
|
|
2311
|
+
Array.isArray(data?.approvals) &&
|
|
2312
|
+
data.approvals.some((a) => a && typeof a === 'object' && a.event === 'pre_tool_call' && a.command === cmd)
|
|
2313
|
+
);
|
|
2314
|
+
} catch {
|
|
2315
|
+
return false;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function hermesHookPluginInit() {
|
|
2320
|
+
const script = hermesHookScript();
|
|
2321
|
+
return [
|
|
2322
|
+
'import importlib.util',
|
|
2323
|
+
'',
|
|
2324
|
+
`SAFETY_SCRIPT = ${JSON.stringify(script)}`,
|
|
2325
|
+
'',
|
|
2326
|
+
'',
|
|
2327
|
+
'def _load_safety():',
|
|
2328
|
+
' spec = importlib.util.spec_from_file_location("huaweicloud_safety_hook", SAFETY_SCRIPT)',
|
|
2329
|
+
' module = importlib.util.module_from_spec(spec)',
|
|
2330
|
+
' spec.loader.exec_module(module)',
|
|
2331
|
+
' return module',
|
|
2332
|
+
'',
|
|
2333
|
+
'',
|
|
2334
|
+
'_safety = _load_safety()',
|
|
2335
|
+
'',
|
|
2336
|
+
'',
|
|
2337
|
+
'def register(ctx):',
|
|
2338
|
+
' ctx.register_hook("pre_tool_call", _check)',
|
|
2339
|
+
'',
|
|
2340
|
+
'',
|
|
2341
|
+
'def _check(tool_name=None, args=None, **kwargs):',
|
|
2342
|
+
' if tool_name != "terminal":',
|
|
2343
|
+
' return None',
|
|
2344
|
+
' if not (isinstance(args, dict) and "command" in args):',
|
|
2345
|
+
' return None',
|
|
2346
|
+
' reason = _safety.evaluate(tool_name, args)',
|
|
2347
|
+
' if reason:',
|
|
2348
|
+
' return {"action": "block", "message": _safety.DENY_PREFIX + reason}',
|
|
2349
|
+
' return None',
|
|
2350
|
+
'',
|
|
2351
|
+
].join('\n');
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function ensureHermesHookPlugin() {
|
|
2355
|
+
const pluginDir = hermesSafetyPluginDir();
|
|
2356
|
+
const manifestPath = join(pluginDir, 'plugin.yaml');
|
|
2357
|
+
const initPath = join(pluginDir, '__init__.py');
|
|
2358
|
+
|
|
2359
|
+
const manifest = [
|
|
2360
|
+
'name: huaweicloud-safety',
|
|
2361
|
+
`version: "${pkgVersion}"`,
|
|
2362
|
+
'description: Huawei Cloud safety pre_tool_call hook',
|
|
2363
|
+
'author: HuaweiCloud Mate',
|
|
2364
|
+
'provides_hooks:',
|
|
2365
|
+
' - pre_tool_call',
|
|
2366
|
+
'',
|
|
2367
|
+
].join('\n');
|
|
2368
|
+
const init = hermesHookPluginInit();
|
|
2369
|
+
|
|
2370
|
+
const manifestCurrent = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : '';
|
|
2371
|
+
const initCurrent = existsSync(initPath) ? readFileSync(initPath, 'utf8') : '';
|
|
2372
|
+
if (manifestCurrent === manifest && initCurrent === init) {
|
|
2373
|
+
console.log(` Hook plugin unchanged: ${pluginDir}`);
|
|
2374
|
+
return false;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
2378
|
+
writeFileSync(manifestPath, manifest);
|
|
2379
|
+
writeFileSync(initPath, init);
|
|
2380
|
+
console.log(` Hook plugin installed: ${pluginDir}`);
|
|
2381
|
+
return true;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function removeHermesHookPlugin() {
|
|
2385
|
+
const pluginDir = hermesSafetyPluginDir();
|
|
2386
|
+
if (!existsSync(pluginDir)) return false;
|
|
2387
|
+
rmSync(pluginDir, { recursive: true, force: true });
|
|
2388
|
+
console.log(` Removed hook plugin: ${pluginDir}`);
|
|
2389
|
+
const parent = hermesPythonPluginsDir();
|
|
2390
|
+
try {
|
|
2391
|
+
if (existsSync(parent) && readdirSync(parent).length === 0) {
|
|
2392
|
+
rmSync(parent, { recursive: true, force: true });
|
|
2393
|
+
}
|
|
2394
|
+
} catch {}
|
|
2395
|
+
return true;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function hermesHookPluginInstalled() {
|
|
2399
|
+
return (
|
|
2400
|
+
existsSync(join(hermesSafetyPluginDir(), 'plugin.yaml')) && existsSync(join(hermesSafetyPluginDir(), '__init__.py'))
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2220
2404
|
function removeHermesHooksConfigBlock() {
|
|
2221
2405
|
const configPath = hermesConfigFile();
|
|
2222
2406
|
if (!existsSync(configPath)) return;
|
|
@@ -2299,6 +2483,8 @@ async function installHermes() {
|
|
|
2299
2483
|
|
|
2300
2484
|
if (!skipMcp) ensureHermesMcpConfig();
|
|
2301
2485
|
ensureHermesHooksConfig();
|
|
2486
|
+
ensureHermesHookAllowlist();
|
|
2487
|
+
ensureHermesHookPlugin();
|
|
2302
2488
|
if (!skipMcp) installRuntimeDeps(pluginDest);
|
|
2303
2489
|
if (!skipMcp) ensureHermesMcpSdk();
|
|
2304
2490
|
}
|
|
@@ -2321,6 +2507,8 @@ async function updateHermes() {
|
|
|
2321
2507
|
console.log(` Safety Hooks updated -> ${join(pluginDest, 'hooks')}`);
|
|
2322
2508
|
ensureHermesMcpConfig();
|
|
2323
2509
|
ensureHermesHooksConfig();
|
|
2510
|
+
ensureHermesHookAllowlist();
|
|
2511
|
+
ensureHermesHookPlugin();
|
|
2324
2512
|
ensureHermesMcpSdk();
|
|
2325
2513
|
mkdirSync(pluginDest, { recursive: true });
|
|
2326
2514
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
@@ -2335,6 +2523,9 @@ function uninstallHermes() {
|
|
|
2335
2523
|
removeHermesHooksConfigBlock();
|
|
2336
2524
|
console.log(' Hooks config removed');
|
|
2337
2525
|
|
|
2526
|
+
// 1b. Remove the Python safety hook plugin (v0.9.0+ enforcement path)
|
|
2527
|
+
removeHermesHookPlugin();
|
|
2528
|
+
|
|
2338
2529
|
// 2. Clean shell-hooks-allowlist.json (remove approved hook references)
|
|
2339
2530
|
const hermesHome = hermesHomeDir();
|
|
2340
2531
|
const allowlistPath = join(hermesHome, 'shell-hooks-allowlist.json');
|
|
@@ -2342,9 +2533,10 @@ function uninstallHermes() {
|
|
|
2342
2533
|
try {
|
|
2343
2534
|
const allowlist = JSON.parse(readFileSync(allowlistPath, 'utf8'));
|
|
2344
2535
|
const before = (allowlist.approvals || []).length;
|
|
2345
|
-
allowlist.approvals = (allowlist.approvals || []).filter((a) =>
|
|
2346
|
-
typeof a === 'string' ?
|
|
2347
|
-
|
|
2536
|
+
allowlist.approvals = (allowlist.approvals || []).filter((a) => {
|
|
2537
|
+
const cmd = typeof a === 'string' ? a : a?.command;
|
|
2538
|
+
return !(typeof cmd === 'string' && cmd.includes('huaweicloud-safety.py'));
|
|
2539
|
+
});
|
|
2348
2540
|
if (allowlist.approvals.length < before) {
|
|
2349
2541
|
writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2));
|
|
2350
2542
|
console.log(` Removed ${before - allowlist.approvals.length} hook approvals from allowlist`);
|
|
@@ -2394,6 +2586,12 @@ function hermesStatus() {
|
|
|
2394
2586
|
console.log(
|
|
2395
2587
|
` Safety Hooks: ${existsSync(join(pluginDir, 'hooks', 'huaweicloud-safety.py')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2396
2588
|
);
|
|
2589
|
+
console.log(
|
|
2590
|
+
` Hook allowlist: ${hermesHookAllowlisted() ? '\x1b[32mApproved\x1b[0m' : '\x1b[31mNot allowlisted\x1b[0m'}`,
|
|
2591
|
+
);
|
|
2592
|
+
console.log(
|
|
2593
|
+
` Hook plugin: ${hermesHookPluginInstalled() ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2594
|
+
);
|
|
2397
2595
|
console.log(` MCP Python SDK: ${hermesMcpSdkOk() ? '\x1b[32mReady\x1b[0m' : '\x1b[31mMissing\x1b[0m'}`);
|
|
2398
2596
|
let skillCount = 0;
|
|
2399
2597
|
if (existsSync(skillsDir)) {
|
|
@@ -106,6 +106,10 @@ function hermesSkillsDir() {
|
|
|
106
106
|
return join(home, '.hermes', 'skills');
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function codexDesktopSkillsDir() {
|
|
110
|
+
return join(homedir(), '.agents', 'skills');
|
|
111
|
+
}
|
|
112
|
+
|
|
109
113
|
export function listSkillDirs(root) {
|
|
110
114
|
if (!existsSync(root)) return [];
|
|
111
115
|
try {
|
|
@@ -135,6 +139,7 @@ function resolveSkillsRoot() {
|
|
|
135
139
|
workbuddySkillsDir(),
|
|
136
140
|
officeaceSkillsRoot(),
|
|
137
141
|
hermesSkillsDir(),
|
|
142
|
+
codexDesktopSkillsDir(),
|
|
138
143
|
]) || SKILLS_ROOT_DEV
|
|
139
144
|
);
|
|
140
145
|
}
|
|
@@ -1228,7 +1233,10 @@ async function runApprovedCommand(args = {}) {
|
|
|
1228
1233
|
}
|
|
1229
1234
|
const strictPlan = planHcloudCommand(args.args || [], { allowWrites: false });
|
|
1230
1235
|
const plan = planHcloudCommand(args.args || [], { allowWrites: true });
|
|
1231
|
-
|
|
1236
|
+
const approved = String(args.approvedCommand || '');
|
|
1237
|
+
const planned = String(plan.command || '');
|
|
1238
|
+
const normalized = (s) => s.replace(/--\S+[= ]<redacted>/g, '--<placeholder>');
|
|
1239
|
+
if (normalized(approved) !== normalized(planned)) {
|
|
1232
1240
|
throw new Error('approvedCommand must exactly match the planned hcloud command.');
|
|
1233
1241
|
}
|
|
1234
1242
|
const result = await runHcloud(args.args || [], {
|