huaweicloud-devkit 0.1.25-dev.0 → 1.0.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.
Files changed (27) hide show
  1. package/README.md +9 -22
  2. package/integrations/opencode/opencode.json +1 -1
  3. package/integrations/opencode/skills/huaweicloud-core/SKILL.md +2 -0
  4. package/package.json +2 -2
  5. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +43 -43
  6. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +42 -42
  7. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +43 -43
  8. package/plugins/huaweicloud-core/.mcp.json +1 -1
  9. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +48 -0
  10. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +178 -0
  11. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +1 -1
  12. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +2 -2
  13. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +4 -0
  14. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +23 -4
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +28 -1
  16. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +0 -1
  17. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +1 -1
  18. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +2 -2
  19. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +2 -11
  20. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +4 -1
  21. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +5 -5
  22. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +14 -0
  23. package/plugins/huaweicloud-core/src/mcp-server.mjs +5 -3
  24. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +137 -0
  25. package/plugins/huaweicloud-core/src/safety-policy.mjs +26 -13
  26. package/plugins/huaweicloud-core/src/setup-cli.mjs +127 -66
  27. package/plugins/huaweicloud-core/src/tools.mjs +156 -1
@@ -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
+ }
@@ -12,7 +12,7 @@ description: |
12
12
 
13
13
  # Huawei Cloud Agent Skills Search and Discovery
14
14
 
15
- This skill enables users to efficiently search, discover, and install Huawei Cloud skills.
15
+ This skill enables users to efficiently search, discover, and install Huawei Cloud skills.
16
16
 
17
17
  ## Scenario Description
18
18
 
@@ -12,7 +12,7 @@ Always run `hcloud DDS <Operation> --help` or `hcloud DCS <Operation> --help` be
12
12
 
13
13
  ## DDS (Document Database Service — MongoDB Compatible)
14
14
 
15
- ## Critical Warnings
15
+ ## DDS Critical Warnings
16
16
 
17
17
  | Trap | Why |
18
18
  |------|-----|
@@ -35,7 +35,7 @@ Discover exact parameters with `--help` before executing any command.
35
35
 
36
36
  ## DCS (Distributed Cache Service — Redis/Memcached)
37
37
 
38
- ## Critical Warnings
38
+ ## DCS Critical Warnings
39
39
 
40
40
  | Trap | Why |
41
41
  |------|-----|
@@ -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
@@ -58,18 +59,36 @@ hcloud ECS CreateServers ... --server.user_data=$user_data
58
59
  ```
59
60
 
60
61
  > **Security**: Never embed secrets (passwords, AK/SK, tokens) in user_data. It is stored unencrypted and readable from within the instance via IMDS. Fetch secrets at boot from DEW/CSMS instead.
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
 
84
+ ```bash
67
85
  # Get the ECS network port ID
68
86
  hcloud ECS ListServersDetails --cli-region=<region> --server_id=<instance-id>
69
87
  # → addresses.<vpc-id>[].OS-EXT-IPS:port_id
70
88
 
71
89
  # Bind EIP via port
72
90
  hcloud EIP AssociatePublicips --publicip_id=<eip-id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT
91
+ ```
73
92
 
74
93
  ## 8. Verify
75
94
  hcloud ECS ListServersDetails --cli-region=<region> --server_id=<instance-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` | |
@@ -138,5 +138,5 @@ hcloud FunctionGraph DeleteFunctionTrigger --function_urn=<urn> --trigger_type_c
138
138
  ```
139
139
 
140
140
  > Deleting a trigger does NOT cascade-delete the associated APIG API. After `DeleteFunctionTrigger`, also run `hcloud APIG ListApisV2 --instance_id=<id>` and delete orphaned APIs to avoid resource residue.
141
-
141
+ >
142
142
  > **APIG event format**: When using DEDICATEDGATEWAY, the event body is Base64 encoded and uses a non-standard structure. See `apig-event-format.md` for handler templates.
@@ -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
 
@@ -72,14 +72,14 @@ Agent processes find executables through `PATH`. If OpenCode/Codex cannot find `
72
72
  2. **KooCLI first-run privacy agreement**: On a fresh KooCLI install, `hcloud` blocks with `同意并继续使用(y)/不同意并退出(N)` and fails with `[USE_ERROR]您输入的是无效字符` in non-interactive mode. Detection: check command output for these strings. Ask the user: "KooCLI needs to accept its privacy agreement. May I accept it on your behalf?" If the user agrees, run `huaweicloud_run_readonly_command` with `args=["version"]` and `stdin="y\n"`. This accepts the agreement once, after which hcloud works normally.
73
73
  3. Ask the user to configure credentials outside the agent conversation when setup is needed.
74
74
  4. Inspect profile and region only through redacted tooling.
75
- 4. Discover exact operation names with `hcloud <Service> --help` before guessing. Example: ECS instance listing is commonly `ECS ListServersDetails`; ECS creation is commonly `ECS CreateServers`; image lookup may be under `IMS GlanceShowImage`.
76
- 5. Use `--cli-output=json` for machine-readable responses when supported.
77
- 6. For resource operations, include `--cli-region`, `--cli-profile`, and service-specific project information when required.
78
- 7. Classify every command before running it:
75
+ 5. Discover exact operation names with `hcloud <Service> --help` before guessing. Example: ECS instance listing is commonly `ECS ListServersDetails`; ECS creation is commonly `ECS CreateServers`; image lookup may be under `IMS GlanceShowImage`.
76
+ 6. Use `--cli-output=json` for machine-readable responses when supported.
77
+ 7. For resource operations, include `--cli-region`, `--cli-profile`, and service-specific project information when required.
78
+ 8. Classify every command before running it:
79
79
  - Read-only: `List*`, `Show*`, `Get*`, `Describe*`.
80
80
  - Write: `Create*`, `Delete*`, `Update*`, `Resize*`, `Start*`, `Stop*`, `Authorize*`, and similar.
81
81
  - Secret: any operation returning secret string, binary secret, token, or password.
82
- 8. For write operations, show the exact command and ask for explicit approval.
82
+ 9. For write operations, show the exact command and ask for explicit approval.
83
83
 
84
84
  ## KooCLI Syntax Notes
85
85
 
@@ -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) {
@@ -0,0 +1,137 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const defaultRulesPath = join(__dirname, '..', 'safety', 'rules', 'cloud-risk-rules.json');
7
+
8
+ const SEVERITY_RANK = {
9
+ deny: 3,
10
+ warn: 2,
11
+ info: 1,
12
+ };
13
+
14
+ export function loadRiskRules(options = {}) {
15
+ const path = options.path || defaultRulesPath;
16
+ return JSON.parse(readFileSync(path, 'utf8'));
17
+ }
18
+
19
+ function redactEvidence(text) {
20
+ return String(text)
21
+ .replace(/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1<redacted>')
22
+ .replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
23
+ }
24
+
25
+ function normalizeText(value) {
26
+ if (typeof value === 'string') return value;
27
+ return JSON.stringify(value, null, 2);
28
+ }
29
+
30
+ function evaluationContext(stage, input) {
31
+ if (stage === 'command') {
32
+ const command = normalizeText(input.command || input.text || '');
33
+ return { text: command, command };
34
+ }
35
+ if (stage === 'artifact') {
36
+ const path = String(input.path || '');
37
+ const content = normalizeText(input.content || '');
38
+ return { text: `${path}\n${content}`, path, content };
39
+ }
40
+ if (stage === 'deploy_plan') {
41
+ const plan = normalizeText(input.plan || input.text || input);
42
+ return { text: plan, plan };
43
+ }
44
+ return { text: normalizeText(input) };
45
+ }
46
+
47
+ function conditionMatches(condition, context) {
48
+ const field = condition.field || 'text';
49
+ const value = Object.hasOwn(context, field) ? context[field] : context.text;
50
+ return new RegExp(condition.regex, 'ims').test(String(value || ''));
51
+ }
52
+
53
+ function ruleMatches(rule, context) {
54
+ const match = rule.match || {};
55
+ const all = match.all;
56
+ const any = match.any;
57
+ const none = match.none;
58
+ if (Array.isArray(all) && !all.every((condition) => conditionMatches(condition, context))) {
59
+ return false;
60
+ }
61
+ if (Array.isArray(any) && !any.some((condition) => conditionMatches(condition, context))) {
62
+ return false;
63
+ }
64
+ if (Array.isArray(none) && none.some((condition) => conditionMatches(condition, context))) {
65
+ return false;
66
+ }
67
+ return Array.isArray(all) || Array.isArray(any);
68
+ }
69
+
70
+ function excerpt(text) {
71
+ const compact = redactEvidence(String(text).replace(/\s+/g, ' ').trim());
72
+ if (compact.length <= 240) return compact;
73
+ return `${compact.slice(0, 237)}...`;
74
+ }
75
+
76
+ function evaluate(stage, inputs, options = {}) {
77
+ const catalog = options.catalog || loadRiskRules(options);
78
+ const items = Array.isArray(inputs) ? inputs : [inputs];
79
+ const findings = [];
80
+
81
+ for (const input of items) {
82
+ const context = evaluationContext(stage, input || {});
83
+ for (const rule of catalog.rules) {
84
+ if (!rule.stages.includes(stage)) continue;
85
+ if (!ruleMatches(rule, context)) continue;
86
+ findings.push({
87
+ ruleId: rule.id,
88
+ title: rule.title,
89
+ category: rule.category,
90
+ severity: rule.severity,
91
+ message: rule.message,
92
+ remediation: rule.remediation,
93
+ source: input?.path || stage,
94
+ evidence: excerpt(context.text),
95
+ });
96
+ }
97
+ }
98
+
99
+ findings.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
100
+ const hasDeny = findings.some((finding) => finding.severity === 'deny');
101
+ const hasWarn = findings.some((finding) => finding.severity === 'warn');
102
+ return {
103
+ decision: hasDeny ? 'deny' : hasWarn ? 'warn' : 'allow',
104
+ findings,
105
+ };
106
+ }
107
+
108
+ export function evaluateCommandRisk(command, options = {}) {
109
+ return evaluate('command', { command }, options);
110
+ }
111
+
112
+ export function evaluateArtifacts(artifacts, options = {}) {
113
+ return evaluate('artifact', Array.isArray(artifacts) ? artifacts : [], options);
114
+ }
115
+
116
+ export function evaluateDeployPlan(plan, options = {}) {
117
+ return evaluate('deploy_plan', { plan }, options);
118
+ }
119
+
120
+ export function mergeRiskDecision(base, risk) {
121
+ if (!risk || !risk.findings?.length) return base;
122
+ if (risk.decision === 'deny') {
123
+ const topFinding = risk.findings[0];
124
+ return {
125
+ ...base,
126
+ decision: 'deny',
127
+ risk: topFinding.category,
128
+ reason: topFinding.message,
129
+ blockedByRiskRule: true,
130
+ findings: risk.findings,
131
+ };
132
+ }
133
+ return {
134
+ ...base,
135
+ warnings: [...(base.warnings || []), ...risk.findings],
136
+ };
137
+ }