huaweicloud-devkit 1.1.0-next.16 → 1.1.0-next.17

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.17",
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.17",
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.17",
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.17",
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.17",
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.17",
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.17",
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.",
@@ -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
@@ -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.
@@ -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
@@ -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)"`,
@@ -2108,6 +2108,14 @@ function hermesPluginsDir() {
2108
2108
  return join(hermesHomeDir(), 'huaweicloud-plugins');
2109
2109
  }
2110
2110
 
2111
+ function hermesPythonPluginsDir() {
2112
+ return join(hermesHomeDir(), 'plugins');
2113
+ }
2114
+
2115
+ function hermesSafetyPluginDir() {
2116
+ return join(hermesPythonPluginsDir(), 'huaweicloud-safety');
2117
+ }
2118
+
2111
2119
  function hermesConfigFile() {
2112
2120
  return join(hermesHomeDir(), 'config.yaml');
2113
2121
  }
@@ -2186,17 +2194,26 @@ function removeHermesMcpConfigBlock() {
2186
2194
  console.log(' MCP config cleaned');
2187
2195
  }
2188
2196
 
2197
+ function hermesHookScript() {
2198
+ return join(hermesPluginsDir(), 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
2199
+ }
2200
+
2201
+ function hermesHookCommand() {
2202
+ const pythonBin = process.platform === 'win32' ? 'python' : 'python3';
2203
+ return `${pythonBin} ${hermesHookScript()}`;
2204
+ }
2205
+
2189
2206
  function ensureHermesHooksConfig() {
2190
2207
  const configPath = hermesConfigFile();
2191
- const pluginDest = hermesPluginsDir();
2192
- const hookScript = join(pluginDest, 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
2208
+ const hookCommand = hermesHookCommand();
2193
2209
 
2194
2210
  const blockLines = [
2195
2211
  'hooks:',
2196
2212
  ' pre_tool_call:',
2197
2213
  ' - matcher: "terminal"',
2198
- ` command: "python3 ${hookScript}"`,
2214
+ ` command: "${hookCommand}"`,
2199
2215
  ' timeout: 5',
2216
+ ' fail_closed: true',
2200
2217
  ];
2201
2218
  const block = blockLines.join('\n');
2202
2219
 
@@ -2205,10 +2222,21 @@ function ensureHermesHooksConfig() {
2205
2222
  try {
2206
2223
  existing = readFileSync(configPath, 'utf8');
2207
2224
  } catch {}
2208
- if (existing.includes('hooks:') && existing.includes('huaweicloud-safety.py')) {
2225
+ if (
2226
+ existing.includes('hooks:') &&
2227
+ existing.includes(`command: "${hookCommand}"`) &&
2228
+ existing.includes('fail_closed: true')
2229
+ ) {
2209
2230
  console.log(` Hooks config unchanged: ${configPath}`);
2210
2231
  return false;
2211
2232
  }
2233
+ removeHermesHooksConfigBlock();
2234
+ existing = '';
2235
+ if (existsSync(configPath)) {
2236
+ try {
2237
+ existing = readFileSync(configPath, 'utf8');
2238
+ } catch {}
2239
+ }
2212
2240
  }
2213
2241
  mkdirSync(dirname(configPath), { recursive: true });
2214
2242
  const newContent = existing ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
@@ -2217,6 +2245,154 @@ function ensureHermesHooksConfig() {
2217
2245
  return true;
2218
2246
  }
2219
2247
 
2248
+ function hermesHookScriptMtime() {
2249
+ const scriptPath = hermesHookScript();
2250
+ try {
2251
+ return statSync(scriptPath).mtime.toISOString();
2252
+ } catch {
2253
+ return null;
2254
+ }
2255
+ }
2256
+
2257
+ function hermesAllowlistPath() {
2258
+ return join(hermesHomeDir(), 'shell-hooks-allowlist.json');
2259
+ }
2260
+
2261
+ function ensureHermesHookAllowlist() {
2262
+ const hookCommand = hermesHookCommand();
2263
+ const allowlistPath = hermesAllowlistPath();
2264
+
2265
+ let data = { approvals: [] };
2266
+ if (existsSync(allowlistPath)) {
2267
+ try {
2268
+ const parsed = JSON.parse(readFileSync(allowlistPath, 'utf8'));
2269
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
2270
+ data = parsed;
2271
+ }
2272
+ } catch {}
2273
+ }
2274
+ const approvals = Array.isArray(data.approvals) ? data.approvals : [];
2275
+ const already = approvals.some(
2276
+ (a) => a && typeof a === 'object' && a.event === 'pre_tool_call' && a.command === hookCommand,
2277
+ );
2278
+ if (already) {
2279
+ console.log(` Hook allowlist unchanged: ${allowlistPath}`);
2280
+ return false;
2281
+ }
2282
+
2283
+ const entry = {
2284
+ event: 'pre_tool_call',
2285
+ command: hookCommand,
2286
+ approved_at: new Date().toISOString(),
2287
+ script_mtime_at_approval: hermesHookScriptMtime(),
2288
+ };
2289
+ data.approvals = [...approvals, entry];
2290
+ mkdirSync(dirname(allowlistPath), { recursive: true });
2291
+ writeFileSync(allowlistPath, JSON.stringify(data, null, 2));
2292
+ console.log(` Hook allowlist updated: ${allowlistPath}`);
2293
+ return true;
2294
+ }
2295
+
2296
+ function hermesHookAllowlisted() {
2297
+ const allowlistPath = hermesAllowlistPath();
2298
+ if (!existsSync(allowlistPath)) return false;
2299
+ try {
2300
+ const data = JSON.parse(readFileSync(allowlistPath, 'utf8'));
2301
+ const cmd = hermesHookCommand();
2302
+ return (
2303
+ Array.isArray(data?.approvals) &&
2304
+ data.approvals.some((a) => a && typeof a === 'object' && a.event === 'pre_tool_call' && a.command === cmd)
2305
+ );
2306
+ } catch {
2307
+ return false;
2308
+ }
2309
+ }
2310
+
2311
+ function hermesHookPluginInit() {
2312
+ const script = hermesHookScript();
2313
+ return [
2314
+ 'import importlib.util',
2315
+ '',
2316
+ `SAFETY_SCRIPT = ${JSON.stringify(script)}`,
2317
+ '',
2318
+ '',
2319
+ 'def _load_safety():',
2320
+ ' spec = importlib.util.spec_from_file_location("huaweicloud_safety_hook", SAFETY_SCRIPT)',
2321
+ ' module = importlib.util.module_from_spec(spec)',
2322
+ ' spec.loader.exec_module(module)',
2323
+ ' return module',
2324
+ '',
2325
+ '',
2326
+ '_safety = _load_safety()',
2327
+ '',
2328
+ '',
2329
+ 'def register(ctx):',
2330
+ ' ctx.register_hook("pre_tool_call", _check)',
2331
+ '',
2332
+ '',
2333
+ 'def _check(tool_name=None, args=None, **kwargs):',
2334
+ ' if tool_name != "terminal":',
2335
+ ' return None',
2336
+ ' if not (isinstance(args, dict) and "command" in args):',
2337
+ ' return None',
2338
+ ' reason = _safety.evaluate(tool_name, args)',
2339
+ ' if reason:',
2340
+ ' return {"action": "block", "message": _safety.DENY_PREFIX + reason}',
2341
+ ' return None',
2342
+ '',
2343
+ ].join('\n');
2344
+ }
2345
+
2346
+ function ensureHermesHookPlugin() {
2347
+ const pluginDir = hermesSafetyPluginDir();
2348
+ const manifestPath = join(pluginDir, 'plugin.yaml');
2349
+ const initPath = join(pluginDir, '__init__.py');
2350
+
2351
+ const manifest = [
2352
+ 'name: huaweicloud-safety',
2353
+ `version: "${pkgVersion}"`,
2354
+ 'description: Huawei Cloud safety pre_tool_call hook',
2355
+ 'author: HuaweiCloud Mate',
2356
+ 'provides_hooks:',
2357
+ ' - pre_tool_call',
2358
+ '',
2359
+ ].join('\n');
2360
+ const init = hermesHookPluginInit();
2361
+
2362
+ const manifestCurrent = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : '';
2363
+ const initCurrent = existsSync(initPath) ? readFileSync(initPath, 'utf8') : '';
2364
+ if (manifestCurrent === manifest && initCurrent === init) {
2365
+ console.log(` Hook plugin unchanged: ${pluginDir}`);
2366
+ return false;
2367
+ }
2368
+
2369
+ mkdirSync(pluginDir, { recursive: true });
2370
+ writeFileSync(manifestPath, manifest);
2371
+ writeFileSync(initPath, init);
2372
+ console.log(` Hook plugin installed: ${pluginDir}`);
2373
+ return true;
2374
+ }
2375
+
2376
+ function removeHermesHookPlugin() {
2377
+ const pluginDir = hermesSafetyPluginDir();
2378
+ if (!existsSync(pluginDir)) return false;
2379
+ rmSync(pluginDir, { recursive: true, force: true });
2380
+ console.log(` Removed hook plugin: ${pluginDir}`);
2381
+ const parent = hermesPythonPluginsDir();
2382
+ try {
2383
+ if (existsSync(parent) && readdirSync(parent).length === 0) {
2384
+ rmSync(parent, { recursive: true, force: true });
2385
+ }
2386
+ } catch {}
2387
+ return true;
2388
+ }
2389
+
2390
+ function hermesHookPluginInstalled() {
2391
+ return (
2392
+ existsSync(join(hermesSafetyPluginDir(), 'plugin.yaml')) && existsSync(join(hermesSafetyPluginDir(), '__init__.py'))
2393
+ );
2394
+ }
2395
+
2220
2396
  function removeHermesHooksConfigBlock() {
2221
2397
  const configPath = hermesConfigFile();
2222
2398
  if (!existsSync(configPath)) return;
@@ -2299,6 +2475,8 @@ async function installHermes() {
2299
2475
 
2300
2476
  if (!skipMcp) ensureHermesMcpConfig();
2301
2477
  ensureHermesHooksConfig();
2478
+ ensureHermesHookAllowlist();
2479
+ ensureHermesHookPlugin();
2302
2480
  if (!skipMcp) installRuntimeDeps(pluginDest);
2303
2481
  if (!skipMcp) ensureHermesMcpSdk();
2304
2482
  }
@@ -2321,6 +2499,8 @@ async function updateHermes() {
2321
2499
  console.log(` Safety Hooks updated -> ${join(pluginDest, 'hooks')}`);
2322
2500
  ensureHermesMcpConfig();
2323
2501
  ensureHermesHooksConfig();
2502
+ ensureHermesHookAllowlist();
2503
+ ensureHermesHookPlugin();
2324
2504
  ensureHermesMcpSdk();
2325
2505
  mkdirSync(pluginDest, { recursive: true });
2326
2506
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
@@ -2335,6 +2515,9 @@ function uninstallHermes() {
2335
2515
  removeHermesHooksConfigBlock();
2336
2516
  console.log(' Hooks config removed');
2337
2517
 
2518
+ // 1b. Remove the Python safety hook plugin (v0.9.0+ enforcement path)
2519
+ removeHermesHookPlugin();
2520
+
2338
2521
  // 2. Clean shell-hooks-allowlist.json (remove approved hook references)
2339
2522
  const hermesHome = hermesHomeDir();
2340
2523
  const allowlistPath = join(hermesHome, 'shell-hooks-allowlist.json');
@@ -2342,9 +2525,10 @@ function uninstallHermes() {
2342
2525
  try {
2343
2526
  const allowlist = JSON.parse(readFileSync(allowlistPath, 'utf8'));
2344
2527
  const before = (allowlist.approvals || []).length;
2345
- allowlist.approvals = (allowlist.approvals || []).filter((a) =>
2346
- typeof a === 'string' ? !a.includes('huaweicloud-safety.py') : true,
2347
- );
2528
+ allowlist.approvals = (allowlist.approvals || []).filter((a) => {
2529
+ const cmd = typeof a === 'string' ? a : a?.command;
2530
+ return !(typeof cmd === 'string' && cmd.includes('huaweicloud-safety.py'));
2531
+ });
2348
2532
  if (allowlist.approvals.length < before) {
2349
2533
  writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2));
2350
2534
  console.log(` Removed ${before - allowlist.approvals.length} hook approvals from allowlist`);
@@ -2394,6 +2578,12 @@ function hermesStatus() {
2394
2578
  console.log(
2395
2579
  ` Safety Hooks: ${existsSync(join(pluginDir, 'hooks', 'huaweicloud-safety.py')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
2396
2580
  );
2581
+ console.log(
2582
+ ` Hook allowlist: ${hermesHookAllowlisted() ? '\x1b[32mApproved\x1b[0m' : '\x1b[31mNot allowlisted\x1b[0m'}`,
2583
+ );
2584
+ console.log(
2585
+ ` Hook plugin: ${hermesHookPluginInstalled() ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
2586
+ );
2397
2587
  console.log(` MCP Python SDK: ${hermesMcpSdkOk() ? '\x1b[32mReady\x1b[0m' : '\x1b[31mMissing\x1b[0m'}`);
2398
2588
  let skillCount = 0;
2399
2589
  if (existsSync(skillsDir)) {