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 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.16",
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.16",
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.16",
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.16",
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.16",
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.16",
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
- text = command_text(data.get("tool_input", {}))
173
+ tool_input = data.get("tool_input", {})
149
174
  hermes = "hook_event_name" in data
150
175
 
151
- if CONFIG_FILE_RE and CONFIG_FILE_RE.search(text):
152
- deny("reading Huawei Cloud credential/profile files can expose AK/SK or tokens. Use redacted toolkit tools.", hermes)
153
- if ENV_DUMP_RE.search(text):
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.16",
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|-f|--recursive|-r|delete_publicip\\s*[=:]\\s*true)" }
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 | Steps |
46
- | ------------------------ | ---------------------------------------------------------------------------------------------- | ----------------------------- |
47
- | List users | hcloud IAM ListUsers | references/iam-ops.md |
48
- | Create group | hcloud IAM CreateGroup --group.name=<name> | references/iam-ops.md |
49
- | Create custom policy | hcloud IAM CreateCustomPolicy --policy.name=<n> --policy.document=<json> | references/policy-examples.md |
50
- | Create agency | hcloud IAM CreateAgency --agency.name=<n> --agency.domain_id=<id> --agency.trust_policy=<json> | references/agency.md |
51
- | Get temporary credential | hcloud STS GetTemporaryCredential --agency_name=<n> --domain_id=<id> --duration_seconds=3600 | references/sts.md |
52
- | Attach policy | hcloud IAM AttachGroupPolicy --group_id=<id> --policy_id=<id> | references/iam-ops.md |
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 --agency_name=<name> --trust_domain_name=functiongraph
91
+ hcloud IAM CreateAgency --agency.name=<name> --agency.trust_domain_name=functiongraph
92
92
 
93
- # 2. Attach VPC Administrator role
94
- hcloud IAM GrantRoleToAgency --agency_name=<name> --role_id=<role-id>
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
- hcloud RDS CreateReadReplica --replica_of_id=<primary-id> --name=<name> --flavor_ref=<id> --volume.type=<vol-type> --volume.size=<size> --cli-region=<r>
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: `'CLI'`, `'WEb'`, `'VSCODE'`, `'CURSO'`, `'WEBIDE'`, etc. — case-sensitive). The `git` parameter (with `repo_url`, `repo_name`, `target_path`) is accepted but does NOT auto-clone the repository — always clone manually.
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 ls --all
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 config --global http.sslVerify false
363
+ git -c http.sslVerify=false clone <repo-url>
363
364
  ```
364
365
 
365
- Then retry the clone. This is a known sandbox environment limitation.
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 ls --all`, delete stale ones, and retry.
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 | `ListInstances --cli-region=<r> --project_id=<p>` |
33
- | List policies | `ListPolicies --cli-region=<r> --project_id=<p>` |
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
- hcloud AAD ListInstances --cli-region=<r> --project_id=<p>
43
- hcloud AAD CreateInstance --cli-region=<r> --project_id=<p>
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 (options.stdin) {
94
- if (typeof options.stdin === 'function') {
95
- options.stdin(child.stdin);
94
+ if (stdin) {
95
+ if (typeof stdin === 'function') {
96
+ stdin(child.stdin);
96
97
  } else {
97
- child.stdin.write(String(options.stdin));
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 ls 2>/dev/null | grep -q "Tunnel URL" && echo "ACTIVE" || echo "INACTIVE"',
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 ls 2>/dev/null | grep -q "Tunnel URL"; then`,
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
- `TUNNEL_URL=$(devbridge ls 2>/dev/null | grep -oP "Tunnel URL: \\Khttps://[^ ]+")`,
924
- `if [ -n "$TUNNEL_URL" ]; then`,
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+):(PASS|FAIL)\b(.*)/);
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
- console.log(` \x1b[33m[WARN]\x1b[0m npm install failed in ${pluginsDir}${err ? `: ${err}` : ''}${hint}`);
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 pluginDest = hermesPluginsDir();
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: "python3 ${hookScript}"`,
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 (existing.includes('hooks:') && existing.includes('huaweicloud-safety.py')) {
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' ? !a.includes('huaweicloud-safety.py') : true,
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
- if (String(args.approvedCommand || '') !== plan.command) {
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 || [], {