huaweicloud-devkit 0.1.25-dev.0 → 0.1.26-dev.0

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
@@ -76,7 +76,7 @@ npx --yes huaweicloud-devkit uninstall --target codearts # 卸载
76
76
  ```json
77
77
  {
78
78
  "mcp": {
79
- "huaweicloud": {
79
+ "huaweicloud-devkit": {
80
80
  "type": "local",
81
81
  "command": ["node", "<路径>/plugins/huaweicloud-core/src/mcp-server.mjs"],
82
82
  "enabled": true
@@ -123,7 +123,7 @@ Agent 技能是经过整理的指令和参考材料包,帮助 Agent 完成特
123
123
 
124
124
  - **安全优先执行** — 所有 `hcloud` 命令执行前自动分类(读/写/密钥),写操作需用户明确批准。
125
125
  - **输出脱敏** — 凭证形态的值(AK/SK、Token、密码)自动替换为 `***REDACTED***`。
126
- - **12 个结构化工具** — 技能搜索、CLI 检查、只读命令、区域发现、错误解释等。
126
+ - **16 个结构化工具** — 技能搜索、CLI 检查、只读命令、区域发现、错误解释、Hook 风险检查等。
127
127
  - **零运行时依赖** — 纯 Node.js(>= 20),无需 npm install。
128
128
 
129
129
  详见 [MCP 工具表](#mcp-工具)。
@@ -154,6 +154,9 @@ Agent 技能是经过整理的指令和参考材料包,帮助 Agent 完成特
154
154
  | CLI | `huaweicloud_run_readonly_command` | 执行只读命令并脱敏输出 |
155
155
  | CLI | `huaweicloud_run_approved_command` | 经用户明确批准后执行写命令 |
156
156
  | 安全 | `huaweicloud_show_profile_redacted` | 安全查看 KooCLI 配置(凭证脱敏) |
157
+ | 安全 | `huaweicloud_hook_check_command` | 执行前检查 Shell/KooCLI 命令风险 |
158
+ | 安全 | `huaweicloud_hook_check_artifacts` | 检查生成的代码、IaC、IAM/OBS 策略和配置文件风险 |
159
+ | 安全 | `huaweicloud_hook_check_deploy_plan` | 检查沙箱、预览环境和云资源部署计划风险 |
157
160
  | 路由 | `huaweicloud_service_catalog` | 返回推荐的能力来源排序 |
158
161
  | 排错 | `huaweicloud_explain_error` | 解释错误码并建议诊断步骤 |
159
162
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://opencode.ai/config.json",
3
3
  "_note": "Merge this into ~/.config/opencode/opencode.jsonc. Replace the path with the absolute path to your mcp-server.mjs installation.",
4
4
  "mcp": {
5
- "huaweicloud": {
5
+ "huaweicloud-devkit": {
6
6
  "type": "local",
7
7
  "command": [
8
8
  "node",
@@ -20,6 +20,8 @@ Use this skill as the OpenCode entry point. Prefer the full skill set under `plu
20
20
 
21
21
  Never ask the user to paste credentials. Never read `.hcloud` or `.huaweicloud` files into context. Plan writes first, ask for explicit approval, then verify with read-only checks.
22
22
 
23
+ For commands that assemble Huawei Cloud parameters through variables, string concatenation, subshells, encoded payloads, or generated scripts, inspect the final expanded command text before execution. If OpenCode cannot determine the final values, ask for explicit review or run the planning/check tool on the expanded command.
24
+
23
25
  ## KooCLI Basics
24
26
 
25
27
  - Install guide: `https://support.huaweicloud.com/qs-hcli/hcli_02_003.html`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "0.1.25-dev.0",
3
+ "version": "0.1.26-dev.0",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "mcpServers": {
3
- "huaweicloud": {
3
+ "huaweicloud-devkit": {
4
4
  "command": "node",
5
5
  "args": [
6
6
  "./src/mcp-server.mjs"
@@ -14,8 +14,10 @@ plugin hooks.
14
14
  import json
15
15
  import re
16
16
  import sys
17
+ from pathlib import Path
17
18
 
18
19
  DENY_PREFIX = "Huawei Cloud safety hook blocked this action: "
20
+ RULES_PATH = Path(__file__).resolve().parents[1] / "safety" / "rules" / "cloud-risk-rules.json"
19
21
 
20
22
  CONFIG_FILE_RE = re.compile(r"(\.hcloud|\.huaweicloud|hcloud[/\\](config|credentials)|huaweicloud[/\\](config|credentials))", re.I)
21
23
  ENV_DUMP_RE = re.compile(r"(env|printenv|Get-ChildItem\s+Env:|gci\s+Env:|dir\s+Env:).*(HUAWEICLOUD|HWC_|HCLOUD|OS_)", re.I)
@@ -63,6 +65,49 @@ def command_text(tool_input):
63
65
  return json.dumps(tool_input)
64
66
 
65
67
 
68
+ def load_cloud_risk_rules():
69
+ try:
70
+ with RULES_PATH.open("r", encoding="utf-8") as file_obj:
71
+ catalog = json.load(file_obj)
72
+ return catalog.get("rules", [])
73
+ except Exception:
74
+ return []
75
+
76
+
77
+ def condition_matches(condition, text):
78
+ return re.search(condition.get("regex", r"a^"), text, re.I | re.M | re.S) is not None
79
+
80
+
81
+ def rule_matches(rule, text):
82
+ match = rule.get("match") or {}
83
+ all_conditions = match.get("all")
84
+ any_conditions = match.get("any")
85
+ none_conditions = match.get("none")
86
+
87
+ if isinstance(all_conditions, list):
88
+ for condition in all_conditions:
89
+ if not condition_matches(condition, text):
90
+ return False
91
+ if isinstance(any_conditions, list):
92
+ if not any(condition_matches(condition, text) for condition in any_conditions):
93
+ return False
94
+ if isinstance(none_conditions, list):
95
+ if any(condition_matches(condition, text) for condition in none_conditions):
96
+ return False
97
+ return isinstance(all_conditions, list) or isinstance(any_conditions, list)
98
+
99
+
100
+ def first_denied_command_rule(text):
101
+ for rule in load_cloud_risk_rules():
102
+ if "command" not in rule.get("stages", []):
103
+ continue
104
+ if rule.get("severity") != "deny":
105
+ continue
106
+ if rule_matches(rule, text):
107
+ return rule
108
+ return None
109
+
110
+
66
111
  def main():
67
112
  try:
68
113
  data = json.load(sys.stdin)
@@ -78,6 +123,9 @@ def main():
78
123
  deny("dumping cloud credential environment variables is not allowed.")
79
124
  if SECRET_READ_RE.search(text):
80
125
  deny("direct secret value retrieval would put plaintext secrets into the agent context.")
126
+ denied_rule = first_denied_command_rule(text)
127
+ if denied_rule:
128
+ deny(f"{denied_rule.get('message')} Remediation: {denied_rule.get('remediation')}")
81
129
  if tool_name == "Bash" and HCLOUD_RE.search(text) and WRITE_OPERATION_RE.search(text) and not READ_OPERATION_RE.search(text):
82
130
  deny("unapproved Huawei Cloud write operations must be planned first and explicitly approved by the user.")
83
131
 
@@ -0,0 +1,178 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "rules": [
4
+ {
5
+ "id": "hwc-command-credential-file",
6
+ "title": "Credential file read",
7
+ "category": "credential",
8
+ "severity": "deny",
9
+ "stages": ["command"],
10
+ "match": {
11
+ "any": [
12
+ { "field": "text", "regex": "(^|\\s)(cat|type|Get-Content|gc|less|more)\\s+[^\\n]*(\\.hcloud|\\.huaweicloud)" },
13
+ { "field": "text", "regex": "(hcloud|huaweicloud)[/\\\\](config|credentials)" }
14
+ ]
15
+ },
16
+ "message": "The command may read local Huawei Cloud credential or profile files into the agent context.",
17
+ "remediation": "Use huaweicloud_show_profile_redacted or user-side credential configuration instead of reading credential files."
18
+ },
19
+ {
20
+ "id": "hwc-command-env-dump",
21
+ "title": "Cloud credential environment dump",
22
+ "category": "credential",
23
+ "severity": "deny",
24
+ "stages": ["command"],
25
+ "match": {
26
+ "all": [
27
+ { "field": "text", "regex": "(^|\\s)(env|printenv|Get-ChildItem\\s+Env:|gci\\s+Env:|dir\\s+Env:)" },
28
+ { "field": "text", "regex": "(HUAWEICLOUD|HWC_|HCLOUD|OS_)" }
29
+ ]
30
+ },
31
+ "message": "The command may print cloud credential environment variables.",
32
+ "remediation": "Inspect only required non-secret settings and redact secret-like values before returning output."
33
+ },
34
+ {
35
+ "id": "hwc-command-secret-value-read",
36
+ "title": "Plaintext secret read",
37
+ "category": "secret",
38
+ "severity": "deny",
39
+ "stages": ["command"],
40
+ "match": {
41
+ "any": [
42
+ { "field": "text", "regex": "(ShowSecretVersion|DownloadSecret|GetSecretValue)" },
43
+ { "field": "text", "regex": "(secret_string|secret_binary|secretString|secretBinary)" }
44
+ ]
45
+ },
46
+ "message": "The command appears to retrieve plaintext secret values.",
47
+ "remediation": "Use runtime secret references, user-side inspection, or redacted metadata checks instead of returning secret values to the agent."
48
+ },
49
+ {
50
+ "id": "hwc-command-encoded-shell-exec",
51
+ "title": "Encoded payload piped to shell",
52
+ "category": "execution",
53
+ "severity": "deny",
54
+ "stages": ["command"],
55
+ "match": {
56
+ "all": [
57
+ { "field": "text", "regex": "(base64\\s+(-d|--decode)|xxd\\s+-r|certutil\\s+-decode|FromBase64String|hex\\s*decode)" },
58
+ { "field": "text", "regex": "(\\||;|&&)" },
59
+ { "field": "text", "regex": "\\b(bash|sh|zsh|powershell|pwsh|cmd|python|node)\\b" }
60
+ ]
61
+ },
62
+ "message": "The command decodes an encoded payload and pipes it into an interpreter.",
63
+ "remediation": "Decode payloads into a reviewable file first, inspect the content, then execute only explicit reviewed commands."
64
+ },
65
+ {
66
+ "id": "hwc-network-public-admin-port",
67
+ "title": "Public admin port exposure",
68
+ "category": "public_exposure",
69
+ "severity": "deny",
70
+ "stages": ["command", "artifact", "deploy_plan"],
71
+ "match": {
72
+ "all": [
73
+ { "field": "text", "regex": "(0\\.0\\.0\\.0\\s*/\\s*0|::\\s*/\\s*0|remote_ip_prefix\\s*[=:]\\s*0\\.0\\.0\\.0\\s*/\\s*0|cidr\\s*[=:]\\s*0\\.0\\.0\\.0\\s*/\\s*0)" },
74
+ { "field": "text", "regex": "(port_range_min|port_range_max|from_port|to_port|port|\\b22\\b|\\b3389\\b|\\b3306\\b|\\b5432\\b|\\b6379\\b|\\b9200\\b)" }
75
+ ]
76
+ },
77
+ "message": "The change appears to expose an administrative or database port to the public internet.",
78
+ "remediation": "Restrict the source CIDR to a trusted range, use a bastion or VPN, and keep public preview endpoints behind authenticated HTTP services."
79
+ },
80
+ {
81
+ "id": "hwc-obs-anonymous-write",
82
+ "title": "OBS anonymous write",
83
+ "category": "public_exposure",
84
+ "severity": "deny",
85
+ "stages": ["command", "artifact", "deploy_plan"],
86
+ "match": {
87
+ "all": [
88
+ { "field": "text", "regex": "(OBS|obs://|bucket|object|Statement|Principal)" }
89
+ ],
90
+ "any": [
91
+ { "field": "text", "regex": "((Anonymous|Everyone|\\\"Principal\\\"\\s*:\\s*\\\"\\*\\\"|AllUsers).*(PutObject|DeleteObject|PutBucketPolicy|FULL_CONTROL|WRITE|obs:object:PutObject|obs:bucket:PutBucketPolicy)|(PutObject|DeleteObject|PutBucketPolicy|FULL_CONTROL|WRITE|obs:object:PutObject|obs:bucket:PutBucketPolicy).*(Anonymous|Everyone|\\\"Principal\\\"\\s*:\\s*\\\"\\*\\\"|AllUsers))" },
92
+ { "field": "text", "regex": "(-acl\\s*=?\\s*public-read-write|acl\\s*[=:]\\s*[\\\"']?public-read-write|public-write)" }
93
+ ]
94
+ },
95
+ "message": "The change may grant anonymous write or full-control access to OBS data.",
96
+ "remediation": "Use private buckets by default. Grant object read only when required, and avoid anonymous write permissions."
97
+ },
98
+ {
99
+ "id": "hwc-functiongraph-public-no-auth",
100
+ "title": "Public FunctionGraph trigger without auth",
101
+ "category": "public_exposure",
102
+ "severity": "warn",
103
+ "stages": ["command", "artifact", "deploy_plan"],
104
+ "match": {
105
+ "all": [
106
+ { "field": "text", "regex": "(FunctionGraph|APIG|CreateTrigger|CreateApi|DEDICATEDGATEWAY|API Gateway)" },
107
+ { "field": "text", "regex": "(public|PUBLIC|0\\.0\\.0\\.0\\s*/\\s*0|auth[\\\"']?\\s*[=:]\\s*[\\\"']?(NONE|none|false)|security_authentication[\\\"']?\\s*[=:]\\s*[\\\"']?(NONE|none))" }
108
+ ]
109
+ },
110
+ "message": "The generated FunctionGraph or API Gateway path may be publicly reachable without authentication.",
111
+ "remediation": "Require IAM, app authentication, JWT, or another explicit auth layer before exposing generated application endpoints."
112
+ },
113
+ {
114
+ "id": "hwc-iam-admin-policy",
115
+ "title": "Broad IAM administrator grant",
116
+ "category": "iam",
117
+ "severity": "deny",
118
+ "stages": ["command", "artifact", "deploy_plan"],
119
+ "match": {
120
+ "all": [
121
+ { "field": "text", "regex": "(IAM|policy|role|agency|Statement|Action)" },
122
+ { "field": "text", "regex": "(\\\"Action\\\"\\s*:\\s*(\\\"(\\*|\\*:\\*)\\\"|\\[\\s*\\\"(\\*|\\*:\\*)\\\")|Action\\s*[=:]\\s*(\\*|\\*:\\*)|AdministratorAccess|FullAccess)" },
123
+ { "field": "text", "regex": "(\\\"Effect\\\"\\s*:\\s*\\\"Allow\\\"|Effect\\s*[=:]\\s*Allow)" }
124
+ ]
125
+ },
126
+ "message": "The change appears to grant broad administrator permissions.",
127
+ "remediation": "Use least-privilege service actions, resource constraints, and short-lived credentials for generated application workflows."
128
+ },
129
+ {
130
+ "id": "hwc-destructive-delete-force",
131
+ "title": "Forced destructive operation",
132
+ "category": "destructive",
133
+ "severity": "deny",
134
+ "stages": ["command"],
135
+ "match": {
136
+ "all": [
137
+ { "field": "text", "regex": "(Delete|BatchDelete|Remove|\\brm\\b|delete)" },
138
+ { "field": "text", "regex": "(--force|-f|--recursive|-r|delete_publicip\\s*[=:]\\s*true)" }
139
+ ]
140
+ },
141
+ "message": "The command combines deletion with force, recursive, or cascading deletion behavior.",
142
+ "remediation": "Show the exact resources to be deleted, confirm backups or recovery path, and require explicit user approval for the exact command."
143
+ },
144
+ {
145
+ "id": "hwc-sandbox-missing-ttl",
146
+ "title": "Sandbox deployment missing cleanup metadata",
147
+ "category": "sandbox",
148
+ "severity": "warn",
149
+ "stages": ["artifact", "deploy_plan"],
150
+ "match": {
151
+ "all": [
152
+ { "field": "text", "regex": "(sandbox|preview|temporary|ephemeral|dev environment|FunctionGraph|ECS|CCE|APIG)" },
153
+ { "field": "text", "regex": "(Create|Deploy|Provision|resource|stack|environment)" }
154
+ ],
155
+ "none": [
156
+ { "field": "text", "regex": "(ttl|expires_at|expire_at|cleanup|owner|cost_center|auto_delete|auto-delete)" }
157
+ ]
158
+ },
159
+ "message": "The preview or sandbox deployment does not show cleanup or ownership metadata.",
160
+ "remediation": "Add owner, purpose, expiration time, and cleanup command before creating preview cloud resources."
161
+ },
162
+ {
163
+ "id": "hwc-cost-unbounded-scale",
164
+ "title": "Unbounded scale or high-cost resource",
165
+ "category": "cost",
166
+ "severity": "warn",
167
+ "stages": ["command", "artifact", "deploy_plan"],
168
+ "match": {
169
+ "any": [
170
+ { "field": "text", "regex": "(max_instances|max_node_count|max_replica|desired\\s*[=:]\\s*[5-9][0-9]|replicas\\s*[=:]\\s*[5-9][0-9])" },
171
+ { "field": "text", "regex": "(GPU|gpu|large|xlarge|charging_mode\\s*[=:]\\s*prePaid|period_type|period_num)" }
172
+ ]
173
+ },
174
+ "message": "The generated plan may create high-cost or unbounded capacity.",
175
+ "remediation": "Use small preview defaults, explicit quotas, budget labels, and user approval before creating high-cost resources."
176
+ }
177
+ ]
178
+ }
@@ -84,6 +84,7 @@ Abort if the result set is larger than `--limit` and ask the user to narrow the
84
84
  | Insufficient resources | Stock depleted -> Change flavor or AZ |
85
85
  | AuthFailure | Expired AK/SK -> hcloud configure init |
86
86
  | APIGW.0802 / region permission | IAM user has no access to this region -> IAM console → User → Permissions → add region, or switch to another region |
87
+ | Cannot SSH (port 22 open) | SCP policy may be blocking SSH. Check `SYS.0403` errors in command output -> Use cloud-init/user_data for initial setup instead. See `references/create-instance.md` §Bootstrap |
87
88
 
88
89
  ## Security Considerations
89
90
 
@@ -98,9 +99,12 @@ Prefer these tools over raw hcloud CLI — they enforce safety policies:
98
99
 
99
100
  - huaweicloud_list_operations service=ECS
100
101
  - huaweicloud_run_readonly_command for discovery (auto-redacts output)
102
+ - huaweicloud_plan_cli_command for command planning (returns command text + safety classification)
101
103
  - huaweicloud_run_approved_command for writes (requires exact command approval)
102
104
  - huaweicloud_check_cli to verify hcloud is available
103
105
 
106
+ > **approvedCommand trap**: `huaweicloud_run_approved_command` validates that `approvedCommand` matches the planned command EXACTLY (including `<redacted>` placeholders). Always use the `command` field value returned by `huaweicloud_plan_cli_command` verbatim — never reconstruct or retype it. Mismatches cause rejection with "approvedCommand must exactly match the planned hcloud command."
107
+
104
108
  ## Without MCP (Fallback)
105
109
 
106
110
  If MCP tools are NOT available (new install, session not restarted):
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## 1. Discover flavors
6
6
  hcloud ECS ListFlavors --cli-region=<region> --cli-output=json
7
+ Filter for `os_extra_specs.cond:operation:status == normal` — most results are abandoned. See references/flavors.md.
7
8
 
8
9
  ## 2. Find availability zones
9
10
  hcloud ECS NovaListAvailabilityZones --cli-region=<region>
@@ -30,7 +31,7 @@ hcloud VPC ListSubnets --vpc_id=<vpc-id> --cli-region=<region>
30
31
  If no VPC/subnet exists: load `huawei-vpc` skill → create VPC → create subnet (with DNS) → create security group → return here.
31
32
 
32
33
  ## 5. Create keypair (recommended over adminPass)
33
- hcloud ECS NovaCreateKeypair --keypair_name=<name>
34
+ hcloud ECS NovaCreateKeypair --keypair.name=<name>
34
35
  Save the returned private key to a local file. The public key is auto-injected.
35
36
 
36
37
  Password alternative:
@@ -43,7 +44,7 @@ hcloud ECS CreateServers --cli-region=<region> --server.name=<name> --server.fla
43
44
 
44
45
  ### Bootstrap with user_data (cloud-init)
45
46
 
46
- Use `--server.user_data` to run a cloud-init script at first boot. The value must be **base64-encoded**:
47
+ Use `--server.user_data` to run a cloud-init script at first boot. The value must be **base64-encoded**. This is also the recommended bootstrap path when SCP policies block SSH access — user_data serves as the full deployment path, no SSH needed.
47
48
 
48
49
  ```bash
49
50
  # Encode the script
@@ -61,7 +62,23 @@ hcloud ECS CreateServers ... --server.user_data=$user_data
61
62
 
62
63
  > **Debugging**: If the script didn't run, check `/var/log/cloud-init-output.log` on the instance.
63
64
 
64
- ## 7. EIP (optional)
65
+ ## 7. EIP (two methods)
66
+
67
+ ### Method A: Inline with CreateServers (Recommended)
68
+ Add EIP parameters to the `CreateServers` command in step 6:
69
+
70
+ ```bash
71
+ hcloud ECS CreateServers \
72
+ --server.publicip.eip.iptype=<type> \
73
+ --server.publicip.eip.bandwidth.sharetype=<share-type> \
74
+ --server.publicip.eip.bandwidth.size=<size> \
75
+ --server.publicip.eip.bandwidth.chargemode=traffic \
76
+ ...
77
+ ```
78
+
79
+ > **Trap**: Parameter names differ from `EIP CreatePublicip`. Use `iptype` (not `type`), `sharetype` (not `share_type`), and `chargemode` (not `charging_mode`). Always verify with `hcloud ECS CreateServers --help`.
80
+
81
+ ### Method B: Create and bind separately
65
82
  hcloud EIP CreatePublicip --publicip.type=<type> --bandwidth.size=<size> --bandwidth.share_type=<share-type> --bandwidth.name=<name>
66
83
 
67
84
  # Get the ECS network port ID
@@ -3,7 +3,20 @@
3
3
  **Always discover flavors dynamically before recommending a specific flavor name.** Flavor availability varies by region and changes over time.
4
4
 
5
5
  ## Step 1: List available flavors
6
- hcloud ECS ListFlavors --cli-region=<region> --cli-output=json
6
+
7
+ Use JMESPath to filter in-line — raw output returns hundreds of records and floods context:
8
+
9
+ ```bash
10
+ # Filter by family prefix (e.g. ac7), return name + specs only
11
+ hcloud ECS ListFlavors --cli-region=<region> --cli-output=json \
12
+ --cli-query="flavors[?contains(name, 'ac7')].{name:name, vcpus:vcpus, ram:ram}"
13
+
14
+ # Filter by vCPU range
15
+ hcloud ECS ListFlavors --cli-region=<region> --cli-output=json \
16
+ --cli-query="flavors[?vcpus >= '2' && vcpus <= '4'].{name:name, vcpus:vcpus, ram:ram}"
17
+ ```
18
+
19
+ > Always use `--cli-query` with JMESPath to narrow results. Never run bare `ListFlavors` without filtering.
7
20
 
8
21
  ## Step 2: Filter by scenario
9
22
 
@@ -26,3 +39,17 @@ Flavor family names are region-dependent. Example discrepancies seen in testing:
26
39
  - Other regions may have s6/m6/g6 families
27
40
 
28
41
  Always run ListFlavors and pick from actual results.
42
+
43
+ ## Step 3: Filter out abandoned / sold-out specs
44
+
45
+ `ListFlavors` returns ALL specs including abandoned ones. Before selecting a spec, check the `os_extra_specs` field in the JSON response:
46
+
47
+ | Field | Values | Meaning |
48
+ |-------|--------|---------|
49
+ | `os_extra_specs.cond:operation:status` | `normal`, `abandon`, `sellout` | Only `normal` specs can be created. `abandon` = deprecated, `sellout` = out of stock |
50
+ | `os_extra_specs.cond:operation:az` | e.g. `cn-north-4g(normal)` | Spec is available in this AZ. Multiple entries = multiple AZ support |
51
+
52
+ A flavor can be `normal` globally but `abandon` in specific AZs. Selecting an `abandon` or `sellout` spec will fail with **`Ecs.0019`** at creation time — there is no pre-flight validation in `ListFlavors`. If creation fails:
53
+
54
+ 1. Switch to a different AZ: `hcloud ECS NovaListAvailabilityZones --cli-region=<region>`
55
+ 2. Or switch to a different flavor family (e.g., `at7` → `ac7`)
@@ -63,7 +63,6 @@ Always discover parameters with `--help` before executing. These are the correct
63
63
  | Create function | `CreateFunction` | references/create-function.md |
64
64
  | Delete function | `DeleteFunction` | Strip `:latest` from URN |
65
65
  | Invoke function | `InvokeFunction` | Requires body param (`--name=<value>` becomes event body). Use `--x_cff_request_version=v0` for raw output, `v1` for APIG-wrapped. |
66
- | List runtimes | `ListRuntimes` | |
67
66
  | Create trigger | `CreateFunctionTrigger` | references/triggers.md |
68
67
  | List triggers | `ListFunctionTriggers` | |
69
68
  | Delete trigger | `DeleteFunctionTrigger` | |
@@ -18,7 +18,7 @@ version: 1
18
18
 
19
19
  ## First-Time Setup
20
20
  1. **Install KooCLI** using command above
21
- 2. **Accept privacy policy** (first run only): KooCLI requires one-time privacy agreement. Run `hcloud version` and respond `y` to the prompt. For non-interactive terminals, use `echo "y" | hcloud version`
21
+ 2. **Accept privacy policy** (first run only): KooCLI requires one-time privacy agreement. Run `hcloud version` to read the agreement, then respond `y` to accept. Do not pipe `echo "y" |` you must review the terms first.
22
22
  3. **Configure credentials**: `hcloud configure init` (interactive, prompts for AK/SK/region safely)
23
23
  4. **Verify**: `hcloud configure list` to confirm profile, then `hcloud ECS ListServersDetails --cli-region=cn-north-4`
24
24
  5. For detailed auth guidance, see `huaweicloud-cli-and-auth` skill
@@ -43,7 +43,7 @@ hcloud configure list
43
43
  | AK/SK must be kept secret | Never commit to git or share |
44
44
  | Default region applies to all commands | Override with --cli-region= per command |
45
45
  | Some services region-specific | Not all services available in all regions |
46
- | Privacy policy blocks first run | KooCLI requires one-time `y` confirmation. Non-interactive terminals need `echo "y" \| hcloud <cmd>` |
46
+ | Privacy policy blocks first run | Run `hcloud version` to read and accept the agreement. Review the terms before responding `y` |
47
47
 
48
48
  ## What Can I Do? (Quick Index)
49
49
  | Goal | Skill |
@@ -36,22 +36,13 @@ Domain expertise for Huawei Cloud Object Storage Service (OBS). Covers bucket/ob
36
36
  | Versioning is irreversible | Once enabled, cannot be disabled, only suspended |
37
37
  | OBS uses AK/SK directly | NOT IAM tokens. Auth errors mean check AK/SK validity |
38
38
  | Static website via CLI missing | KooCLI OBS lacks website config. Use REST API or console |
39
- | **OBS needs separate cred config** | `hcloud configure` is NOT enough for OBS. Run `hcloud OBS config -i` (interactive) to create `~/.obsutilconfig`. This must be done OUTSIDE agent chat. |
39
+ | **OBS needs separate cred config** | `hcloud configure` is NOT enough for OBS. Before any OBS operation, call `huaweicloud_setup_obs_config` to sync credentials from hcloud profile. |
40
40
  | **obsutil interactive prompts** | `cp`/`rm` without `-f` causes "Please input (y/n)" → Agent hangs (TIMEOUT). Always use `-f` for non-interactive. |
41
41
  | **Directory upload adds prefix** | `cp <dir>/ obs://<bucket>/ -r` puts files under `bucket/<dir>/...`. Use `-flat` for root-level files (static sites). Preview with `-dryRun` first. |
42
42
 
43
43
  ## OBS Credential Setup (Required Before First Use)
44
44
 
45
- KooCLI OBS uses a separate config file (`~/.obsutilconfig`), NOT `~/.hcloud/config.json`. Use the **same AK/SK** that you configured for hcloud. Run once outside agent chat:
46
-
47
- ```bash
48
- # Preferred: interactive (safe, no AK/SK in shell history)
49
- hcloud OBS config -i
50
- # Follow prompts: AK, SK, endpoint (e.g. obs.cn-north-4.myhuaweicloud.com)
51
-
52
- # Alternative: non-interactive (use with caution — AK/SK in shell history)
53
- hcloud OBS config -e=<endpoint> -i=<AK> -k=<SK>
54
- ```
45
+ KooCLI OBS uses a separate config file (`~/.obsutilconfig`), NOT `~/.hcloud/config.json`. Call `huaweicloud_setup_obs_config` to automatically sync credentials from the active hcloud profile. No manual AK/SK entry needed.
55
46
 
56
47
  ## Common Workflows
57
48
 
@@ -10,6 +10,8 @@ version: 1
10
10
 
11
11
  Always run `hcloud <Service> <Operation> --help` before constructing commands to discover exact parameter names and requirements.
12
12
 
13
+ > **Multi-version APIs**: KooCLI may print a warning like "ListVpcs有多个版本,默认使用该API版本v3" before the actual response. The text BEFORE the first `{` is the version selection notice — parse JSON starting from `{` only. This is normal behavior, not an error.
14
+
13
15
  ## Overview
14
16
 
15
17
  Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet lifecycle, security groups, EIP management, NAT gateways, VPN, and network ACLs.
@@ -27,6 +29,7 @@ Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet
27
29
  | **VPC params need nested prefix** | KooCLI 7.x VPC API uses `--vpc.<param>`, `--subnet.<param>`, `--security_group.<param>`. Example: `--vpc.name=xxx` NOT `--name=xxx` |
28
30
  | **Security group needs no vpc_id** | VPC v3 API `CreateSecurityGroup` does NOT accept `vpc_id`. Security groups are region-level, not VPC-bound |
29
31
  | Subnet DNS empty → ECS no DNS | DNS params (`--subnet.primary_dns`, `--subnet.secondary_dns`) marked optional but empty default breaks cloud-init domain resolution — `yum`/`apt` installs fail silently. Always set both. See `--help` for region-specific DNS IPs |
32
+ | SCP blocks 0.0.0.0/0 SG rules | If `CreateSecurityGroupRule` with `--remote_ip_prefix=0.0.0.0/0` fails with `SYS.0403`, an org-level SCP policy is denying wide-open rules. Narrow to a specific CIDR range (e.g., your office IP) instead |
30
33
 
31
34
  ## Common Workflows
32
35
 
@@ -55,7 +58,7 @@ Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet
55
58
  | VPC.0301: Bandwidth name invalid | PER type requires `--bandwidth.name`, even though `--help` marks it optional |
56
59
  | EIP has no public IP after binding | May need AddIngressEipV2 for ELB-type resources (see huawei-apig) |
57
60
  | ECS cloud-init fails silently (port 80/443 closed) | Subnet likely has no DNS. Check `hcloud VPC ShowSubnet --subnet_id=<id>` → `dnsList` empty? Rebuild subnet with `--subnet.primary_dns=<dns1> --subnet.secondary_dns=<dns2>`. DNS addresses per region: `hcloud VPC CreateSubnet --help` |
58
- | SYS.0403 / SCP deny | Service Control Policy explicitly denies this operation — contact org admin to adjust SCP, or use an account/region without the restriction |
61
+ | SYS.0403 / SCP deny | Service Control Policy explicitly denies this operation — contact org admin to adjust SCP, or use an account/region without the restriction. If SSH is blocked, bootstrap via cloud-init user_data instead: see `huawei-ecs` — no SSH needed |
59
62
 
60
63
  ## Security Considerations
61
64
 
@@ -38,6 +38,20 @@ Use this skill before any Huawei Cloud action that may expose secrets, change re
38
38
  - Approved tool path: use `huaweicloud_run_approved_command` only when the exact planned command has been shown and the user explicitly approved that exact command.
39
39
  - After a write, verify with `huaweicloud_run_readonly_command` or another read-only check.
40
40
 
41
+ ## Proactive Hook Checks
42
+
43
+ Before executing, deploying, or handing generated cloud artifacts to the user, run the matching check when available:
44
+
45
+ - `huaweicloud_hook_check_command` for shell or KooCLI command text.
46
+ - `huaweicloud_hook_check_artifacts` for generated policy, IaC, config, workflow, or deployment files.
47
+ - `huaweicloud_hook_check_deploy_plan` for sandbox, preview, FunctionGraph, ECS, CCE, APIG, OBS, IAM, or cost-affecting plans.
48
+
49
+ If the result is `deny`, repair the command, artifact, or deployment plan before execution. If the result is `warn`, show the warning to the user and repair or ask for explicit confirmation.
50
+
51
+ ## Static Analysis Boundary
52
+
53
+ Hook checks inspect the command or plan text that is available before execution. If a command builds cloud parameters through shell variables, string concatenation, subshells, encoded payloads, or generated scripts, first expand the final command into reviewable text and run `huaweicloud_hook_check_command` on that final form. When the final values cannot be determined, stop and ask the user to review the expanded command or use `huaweicloud_plan_cli_command` before execution.
54
+
41
55
  ## Safe Alternatives
42
56
 
43
57
  - Use redacted profile inspection instead of raw `hcloud configure show`.
@@ -15,7 +15,8 @@ function readFrames() {
15
15
  const headerEnd = buffer.indexOf('\r\n\r\n');
16
16
  if (headerEnd !== -1) {
17
17
  useContentLengthFraming = true;
18
- parseContentLengthFrame(headerEnd);
18
+ const consumed = parseContentLengthFrame(headerEnd);
19
+ if (!consumed) return;
19
20
  continue;
20
21
  }
21
22
 
@@ -37,15 +38,16 @@ function parseContentLengthFrame(headerEnd) {
37
38
  const match = header.match(/Content-Length:\s*(\d+)/i);
38
39
  if (!match) {
39
40
  buffer = Buffer.alloc(0);
40
- return;
41
+ return true;
41
42
  }
42
43
  const length = Number(match[1]);
43
44
  const bodyStart = headerEnd + 4;
44
45
  const bodyEnd = bodyStart + length;
45
- if (buffer.length < bodyEnd) return;
46
+ if (buffer.length < bodyEnd) return false;
46
47
  const body = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
47
48
  buffer = buffer.subarray(bodyEnd);
48
49
  void handleMessage(JSON.parse(body));
50
+ return true;
49
51
  }
50
52
 
51
53
  async function handleMessage(message) {