huaweicloud-devkit 1.1.3-next.2 → 1.1.3-next.4
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/package.json +4 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +25 -2
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +36 -36
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +3 -1
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +7 -4
- package/plugins/huaweicloud-core/src/setup-cli.mjs +5 -5
- package/plugins/huaweicloud-core/src/telemetry/telemetry.mjs +5 -1
- package/plugins/huaweicloud-core/src/update-check.mjs +57 -18
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"mcpName": "io.github.huaweicloud/huaweicloud-devkit",
|
|
4
|
-
"version": "1.1.3-next.
|
|
4
|
+
"version": "1.1.3-next.4",
|
|
5
5
|
"kooCliVersion": "7.2.12",
|
|
6
6
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
7
7
|
"type": "module",
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
"markdownlint-cli2": "^0.23.2",
|
|
56
56
|
"prettier": "^3.9.6"
|
|
57
57
|
},
|
|
58
|
+
"overrides": {
|
|
59
|
+
"smol-toml": "1.8.0"
|
|
60
|
+
},
|
|
58
61
|
"keywords": [
|
|
59
62
|
"huaweicloud",
|
|
60
63
|
"huawei-cloud",
|
|
@@ -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.3-next.
|
|
20
|
+
"version": "1.1.3-next.4",
|
|
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.3-next.
|
|
23
|
+
"version": "1.1.3-next.4",
|
|
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.3-next.
|
|
20
|
+
"version": "1.1.3-next.4",
|
|
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.3-next.
|
|
3
|
+
"version": "1.1.3-next.4",
|
|
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.3-next.
|
|
3
|
+
"version": "1.1.3-next.4",
|
|
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",
|
|
@@ -43,7 +43,7 @@ def load_policy():
|
|
|
43
43
|
SECRET_READ_RE = re.compile("|".join(re.escape(op) for op in blocked_secrets), re.I)
|
|
44
44
|
write_prefixes = policy.get("writeOperationPrefixes", [])
|
|
45
45
|
if write_prefixes:
|
|
46
|
-
WRITE_OPERATION_RE = re.compile(r"
|
|
46
|
+
WRITE_OPERATION_RE = re.compile(r"(^|[A-Za-z0-9])(" + "|".join(write_prefixes) + r")\w*", re.I)
|
|
47
47
|
except Exception:
|
|
48
48
|
pass
|
|
49
49
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.3-next.
|
|
5
|
+
"version": "1.1.3-next.4",
|
|
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.",
|
|
@@ -231,17 +231,40 @@
|
|
|
231
231
|
"any": [
|
|
232
232
|
{
|
|
233
233
|
"field": "text",
|
|
234
|
-
"regex": "hcloud\\s+\\w+\\s+(Delete\\w
|
|
234
|
+
"regex": "hcloud\\s+\\w+\\s+(\\w*Delete\\w*|\\w*Detach\\w*|\\w*Remove\\w*|Dissociate\\w*|Unassign\\w*|Revoke\\w*)\\b"
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
237
|
"field": "text",
|
|
238
|
-
"regex": "hcloud\\s+\\w+\\s+(BatchDelete\\w
|
|
238
|
+
"regex": "hcloud\\s+\\w+\\s+(\\w*BatchDelete\\w*|\\w*BatchRemove\\w*)\\b"
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
"none": [
|
|
242
|
+
{
|
|
243
|
+
"field": "text",
|
|
244
|
+
"regex": "DeleteProtection"
|
|
239
245
|
}
|
|
240
246
|
]
|
|
241
247
|
},
|
|
242
248
|
"message": "This hcloud command will delete, detach, or remove cloud resources. The operation may be irreversible.",
|
|
243
249
|
"remediation": "List the resources to be affected first, confirm with the user, and require explicit approval before executing any destructive operation."
|
|
244
250
|
},
|
|
251
|
+
{
|
|
252
|
+
"id": "hwc-destructive-reset-operation",
|
|
253
|
+
"title": "Destructive resource reset operation",
|
|
254
|
+
"category": "destructive",
|
|
255
|
+
"severity": "warn",
|
|
256
|
+
"stages": ["command"],
|
|
257
|
+
"match": {
|
|
258
|
+
"any": [
|
|
259
|
+
{
|
|
260
|
+
"field": "text",
|
|
261
|
+
"regex": "hcloud\\s+\\w+\\s+(Batch)?Reset\\w*\\b"
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
},
|
|
265
|
+
"message": "This hcloud command resets a cloud resource (password, metadata, or instance state). This may disrupt access or mutate the resource.",
|
|
266
|
+
"remediation": "Confirm the reset target with the user and require explicit approval before executing."
|
|
267
|
+
},
|
|
245
268
|
{
|
|
246
269
|
"id": "hwc-destructive-delete-cascade",
|
|
247
270
|
"title": "Cascading cloud resource deletion",
|
|
@@ -21,16 +21,16 @@ Always run `hcloud <Service> <Operation> --help` before constructing commands to
|
|
|
21
21
|
|
|
22
22
|
**Proactive by design**: any deployment intent ("部署一个博客", "搭一套环境", "买个服务器") automatically triggers stages 1-5 BEFORE anything is created. The user NEVER needs to ask for an architecture proposal, cost estimate, or risk warnings - they are built into the flow. Only stages 6-8 (create / verify / manage) wait for explicit user confirmation at the approval gates.
|
|
23
23
|
|
|
24
|
-
| Stage | What happens | Hard rule
|
|
25
|
-
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
-
| 1. Scale assessment | **FIRST inspect the project code** (clone remote repos to a temp dir, or read the local path): tech stack, data services (Redis/MySQL/SQLite...), build artifacts, existing Dockerfile/k8s manifests → then classify or ask scale | **NEVER present deployment options (sandbox/ECS/OBS...) before the codebase has been inspected** - options without code analysis are guesses. If scale is not stated or clearly inferable, ASK - present the 3 tiers as options with what each includes and its monthly cost band (see `references/architectures.md`), and let the user choose. Never assume a tier. The scale question and the hosting-target question are SEPARATE questions on separate axes. FunctionGraph filesystem is ephemeral - never recommend SQLite/local files for persistent data
|
|
27
|
-
| 2. Frontend hosting choice | Present BOTH options: (a) OBS+CDN+DNS custom domain, (b) ECS+EIP | For SPA/static frontends (Vue/React dist), **OBS hosting must always appear as an option** with cost comparison against same-host Nginx - do not silently merge the frontend into a compute node. For option (a) three prerequisites: domain ownership verification (CDN requires a DNS record or file upload first - `CDN.00010185` otherwise), ICP filing check, DNS hosting check; domain registration link: https://www.huaweicloud.com/product/domain.html
|
|
28
|
-
| 3. Architecture proposal | ASCII topology tree with dependencies | User confirms before continuing. **Architecture decision points must be surfaced as explicit choices, never silently made**: (a) app depends on Redis/MySQL/Kafka etc. → managed service (DCS/RDS/DMS, billed) vs same-host install (cheap, ops burden) - state the trade-off and ask; (b) repo ships a Dockerfile / SWR image / k8s manifests → offer container path (Docker on ECS or CCE) alongside source-build path; (c) frontend hosting layer (Nginx same-host vs OBS+CDN). Silent defaults here are a correctness failure
|
|
29
|
-
| 4. Parameter discovery | `ListFlavors` / `ListImages` / `huaweicloud_list_regions` | Never hardcode flavor/image names
|
|
30
|
-
| 5. Cost + balance gate | Price every resource via BSS `ListOnDemandResourceRatings`, then query balance | Balance = 0 → the FIRST message must state: "您的余额为 0,按时长计费的资源(ECS/EIP/RDS
|
|
31
|
-
| 6. Provision | deployment_id, session state, batch approval, ordered creation | See "Provisioning Rules" below
|
|
32
|
-
| 7. Verify | `curl -I` the site / API; expect 200 | DNS CNAME may take minutes to propagate - not an error
|
|
33
|
-
| 8. Manage | Session-scoped status query and destroy | See "Destroy Rules" below
|
|
24
|
+
| Stage | What happens | Hard rule |
|
|
25
|
+
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| 1. Scale assessment | **FIRST inspect the project code** (clone remote repos to a temp dir, or read the local path): tech stack, data services (Redis/MySQL/SQLite...), build artifacts, existing Dockerfile/k8s manifests → then classify or ask scale | **NEVER present deployment options (sandbox/ECS/OBS...) before the codebase has been inspected** - options without code analysis are guesses. If scale is not stated or clearly inferable, ASK - present the 3 tiers as options with what each includes and its monthly cost band (see `references/architectures.md`), and let the user choose. Never assume a tier. The scale question and the hosting-target question are SEPARATE questions on separate axes. FunctionGraph filesystem is ephemeral - never recommend SQLite/local files for persistent data |
|
|
27
|
+
| 2. Frontend hosting choice | Present BOTH options: (a) OBS+CDN+DNS custom domain, (b) ECS+EIP | For SPA/static frontends (Vue/React dist), **OBS hosting must always appear as an option** with cost comparison against same-host Nginx - do not silently merge the frontend into a compute node. For option (a) three prerequisites: domain ownership verification (CDN requires a DNS record or file upload first - `CDN.00010185` otherwise), ICP filing check, DNS hosting check; domain registration link: https://www.huaweicloud.com/product/domain.html |
|
|
28
|
+
| 3. Architecture proposal | ASCII topology tree with dependencies | User confirms before continuing. **Architecture decision points must be surfaced as explicit choices, never silently made**: (a) app depends on Redis/MySQL/Kafka etc. → managed service (DCS/RDS/DMS, billed) vs same-host install (cheap, ops burden) - state the trade-off and ask; (b) repo ships a Dockerfile / SWR image / k8s manifests → offer container path (Docker on ECS or CCE) alongside source-build path; (c) frontend hosting layer (Nginx same-host vs OBS+CDN). Silent defaults here are a correctness failure |
|
|
29
|
+
| 4. Parameter discovery | `ListFlavors` / `ListImages` / `huaweicloud_list_regions` | Never hardcode flavor/image names |
|
|
30
|
+
| 5. Cost + balance gate | Price every resource via BSS `ListOnDemandResourceRatings`, then query balance | Balance = 0 → the FIRST message must state: "您的余额为 0,按时长计费的资源(ECS/EIP/RDS 等)下单将失败(Ecs.7000),请先充值或使用免费额度"。充值入口:https://account.huaweicloud.com/usercenter/?region=cn-north-4#/userindex/balanceRecharge 。Remedy priority: (1) FIRST claim the one-time incentive voucher (huaweicloud_voucher_status → huaweicloud_voucher_claim) to offset usage fees; (2) then instruct the user to top up balance (via the link above) if pay-per-use orders are needed, otherwise orders fail with `Ecs.7000`; (3) free-tier resources (FunctionGraph, small OBS) proceed without any balance. Balance > 0 → report how long it can sustain (balance ÷ monthly estimate). **权限降级(实测)**:IAM 用户无 `billing:balance:view` 时余额查询报 `CBC.0151` → 不阻塞流程,明确告知"余额无法程序化查询,请自行核对;若下单报 Ecs.7000 即余额不足",领券后继续。定价编码逐服务查证,见 `references/resource-catalog.md` § Pricing |
|
|
31
|
+
| 6. Provision | deployment_id, session state, batch approval, ordered creation | See "Provisioning Rules" below |
|
|
32
|
+
| 7. Verify | `curl -I` the site / API; expect 200 | DNS CNAME may take minutes to propagate - not an error |
|
|
33
|
+
| 8. Manage | Session-scoped status query and destroy | See "Destroy Rules" below |
|
|
34
34
|
|
|
35
35
|
## Provisioning Rules (Stage 6)
|
|
36
36
|
|
|
@@ -52,32 +52,32 @@ Execution: show the to-be-deleted list for final confirmation → delete in **re
|
|
|
52
52
|
|
|
53
53
|
## Critical Warnings
|
|
54
54
|
|
|
55
|
-
| Trap | Why / Rule
|
|
56
|
-
| ---------------------------------------------------- |
|
|
57
|
-
| Stale `securityToken` poisons every hcloud call | Symptom: every API returns `APIGW.0301 Incorrect IAM authentication information` while OBS/obsutil still works. `configure set` refuses empty values and auth-sync never clears the field. Fix: `hcloud configure delete --cli-profile=default`, then re-run auth init with AK/SK only (no token)
|
|
58
|
-
| OBS writes bypass the REST write-prefix policy | `mb` / `cp` / `rm` / `chattri` are obsutil-style commands. ALWAYS route them through `huaweicloud_plan_cli_command` → approval → `huaweicloud_run_approved_command`. Never treat them as read-only
|
|
59
|
-
| `OBS mb` flag rules differ from cp/rm | `mb` does NOT accept `-f` (flag parse error) and REQUIRES explicit `-location=<region>` even with a regional endpoint (else `IllegalLocationConstraintException`)
|
|
60
|
-
| Duplicate `OBS mb` is idempotent success | Same-account same-name bucket creation returns success, not an error - never treat a second create as failure. OBS deletes are also idempotent (deleting a nonexistent object succeeds)
|
|
61
|
-
| Bucket ACL does not cascade to objects | Every uploaded object needs `-acl=public-read` at upload time (`-f` IS valid on cp/rm)
|
|
62
|
-
| OBS static website bucket name must equal the domain | e.g. bucket `www.example.com` for domain `www.example.com`
|
|
63
|
-
| VPC v3 API differs from older docs | v3 `CreateSecurityGroup` has NO `vpc_id` (account-scoped); `CreateSecurityGroupRule` uses `multiport=80` not `port_range_min/max`; `DeleteSubnet` requires `vpc_id` too. Always `--help` first
|
|
64
|
-
| Public exposure is auto-blocked | Risk rules deny `0.0.0.0/0` port rules at plan time. For test/dev use a narrower `remote_ip_prefix` (e.g. the VPC CIDR) or get user consent for a documented exception
|
|
65
|
-
| ECS CreateServers hidden required params | Besides flavor/image/nics/az it also needs `--server.vpcid` and `--server.root_volume.volumetype` - order submission fails without them
|
|
66
|
-
| FunctionGraph runtime/code enums are strict | runtime must be exact (`Node.js18.15`, `Python3.9`, `Go1.x`, ...); inline code is the PAIR `--package=default --code_type=inline` (`--package=inline` → `FSS.1006`); TIMER triggers use `--event_data.schedule_type=Rate --event_data.schedule=1m` (`@every` style → `FSS.1109`); DeleteFunction takes `--function_urn` WITHOUT `:latest`
|
|
67
|
-
| Keypairs: `NovaCreateKeypair`, not CreateKeypair | `ECS CreateKeypair` does NOT exist - use `hcloud ECS NovaCreateKeypair`; delete takes `--keypair_name` (not `--keypair.name`). The API-generated `private_key` is MASKED in tool output - for SSH access create the keypair in the console or import the user's own public key, then bind with `--server.key_name=<n>`
|
|
68
|
-
| Long inline args break MCP JSON | Values >~2KB inline (e.g. `--server.user_data=<base64 cloud-init>`) fail JSON parsing silently. Write a local JSON file and pass `--cli-jsonInput=<file>` (see `references/resource-catalog.md` header note)
|
|
69
|
-
| EIP quota fails at RUN stage, not at plan | `EIP.7905 Quota exceeded` appears only after plan+approval - pre-check quota during stage 5 (and count precisely: `ListPublicips --limit=50` truncates, a "50/50" read on a 59-EIP account is wrong). Released quotas may lag minutes before `EIP.7905` clears
|
|
70
|
-
| Zero balance blocks order submission | ECS creation fails with `Ecs.7000 Insufficient account balance` when cash + voucher accounts are 0. This is the stage-5 gate materialized: stop, mark `partial`, tell the user pay-by-duration resources need a
|
|
71
|
-
| CDN mainland acceleration requires ICP filing | Unregistered domains cannot go live on CDN - check before stage 3. CDN create ALSO requires domain ownership verification first (`CDN.00010185 Verify domain ownership failed` - user must add a DNS record or upload a verification file)
|
|
72
|
-
| CDN is a global service in KooCLI | CDN operations only accept `--cli-region=cn-north-1` or `ap-southeast-1` - never the resource region
|
|
73
|
-
| OBS static website hosting has NO CLI path | `chattri` only sets ACL/storage-class; obsutil has no website command. Options: console setup, or CDN OBS-origin with `--domain.sources.1.enable_obs_web_hosting=1`. Without website hosting, verify via direct object URLs (still public HTTP 200)
|
|
74
|
-
| DNS hosting location | `hcloud DNS ListPublicZones` decides: hosted in Huawei Cloud → auto-create CNAME record set; hosted elsewhere → output the CNAME value for manual setup at the user's DNS provider
|
|
75
|
-
| EIP bills when idle; stopped ECS still bills | Include in cost estimate; destroy unattached EIPs
|
|
76
|
-
| ECS delete defaults leak resources | `--delete_publicip` and `--delete_volume` are false by default - set `true` when tearing down
|
|
77
|
-
| EIP binding type is non-obvious | `--publicip.associate_instance_type=PORT` (not INSTANCE)
|
|
78
|
-
| State is session-only | Temp-dir file, dies with the session. Never promise cross-session deploy management
|
|
79
|
-
| State file must be UTF-8 WITHOUT BOM | PowerShell `Set-Content -Encoding utf8` adds a BOM that breaks JSON parsers - write state with Node or another BOM-less writer
|
|
80
|
-
| HTTPS needs a certificate | CDN serves plain HTTP by default; SSL cert (upload or SCM) required for HTTPS
|
|
55
|
+
| Trap | Why / Rule |
|
|
56
|
+
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| Stale `securityToken` poisons every hcloud call | Symptom: every API returns `APIGW.0301 Incorrect IAM authentication information` while OBS/obsutil still works. `configure set` refuses empty values and auth-sync never clears the field. Fix: `hcloud configure delete --cli-profile=default`, then re-run auth init with AK/SK only (no token) |
|
|
58
|
+
| OBS writes bypass the REST write-prefix policy | `mb` / `cp` / `rm` / `chattri` are obsutil-style commands. ALWAYS route them through `huaweicloud_plan_cli_command` → approval → `huaweicloud_run_approved_command`. Never treat them as read-only |
|
|
59
|
+
| `OBS mb` flag rules differ from cp/rm | `mb` does NOT accept `-f` (flag parse error) and REQUIRES explicit `-location=<region>` even with a regional endpoint (else `IllegalLocationConstraintException`) |
|
|
60
|
+
| Duplicate `OBS mb` is idempotent success | Same-account same-name bucket creation returns success, not an error - never treat a second create as failure. OBS deletes are also idempotent (deleting a nonexistent object succeeds) |
|
|
61
|
+
| Bucket ACL does not cascade to objects | Every uploaded object needs `-acl=public-read` at upload time (`-f` IS valid on cp/rm) |
|
|
62
|
+
| OBS static website bucket name must equal the domain | e.g. bucket `www.example.com` for domain `www.example.com` |
|
|
63
|
+
| VPC v3 API differs from older docs | v3 `CreateSecurityGroup` has NO `vpc_id` (account-scoped); `CreateSecurityGroupRule` uses `multiport=80` not `port_range_min/max`; `DeleteSubnet` requires `vpc_id` too. Always `--help` first |
|
|
64
|
+
| Public exposure is auto-blocked | Risk rules deny `0.0.0.0/0` port rules at plan time. For test/dev use a narrower `remote_ip_prefix` (e.g. the VPC CIDR) or get user consent for a documented exception |
|
|
65
|
+
| ECS CreateServers hidden required params | Besides flavor/image/nics/az it also needs `--server.vpcid` and `--server.root_volume.volumetype` - order submission fails without them |
|
|
66
|
+
| FunctionGraph runtime/code enums are strict | runtime must be exact (`Node.js18.15`, `Python3.9`, `Go1.x`, ...); inline code is the PAIR `--package=default --code_type=inline` (`--package=inline` → `FSS.1006`); TIMER triggers use `--event_data.schedule_type=Rate --event_data.schedule=1m` (`@every` style → `FSS.1109`); DeleteFunction takes `--function_urn` WITHOUT `:latest` |
|
|
67
|
+
| Keypairs: `NovaCreateKeypair`, not CreateKeypair | `ECS CreateKeypair` does NOT exist - use `hcloud ECS NovaCreateKeypair`; delete takes `--keypair_name` (not `--keypair.name`). The API-generated `private_key` is MASKED in tool output - for SSH access create the keypair in the console or import the user's own public key, then bind with `--server.key_name=<n>` |
|
|
68
|
+
| Long inline args break MCP JSON | Values >~2KB inline (e.g. `--server.user_data=<base64 cloud-init>`) fail JSON parsing silently. Write a local JSON file and pass `--cli-jsonInput=<file>` (see `references/resource-catalog.md` header note) |
|
|
69
|
+
| EIP quota fails at RUN stage, not at plan | `EIP.7905 Quota exceeded` appears only after plan+approval - pre-check quota during stage 5 (and count precisely: `ListPublicips --limit=50` truncates, a "50/50" read on a 59-EIP account is wrong). Released quotas may lag minutes before `EIP.7905` clears |
|
|
70
|
+
| Zero balance blocks order submission | ECS creation fails with `Ecs.7000 Insufficient account balance` when cash + voucher accounts are 0. This is the stage-5 gate materialized: stop, mark `partial`, tell the user pay-by-duration resources need a balance top-up. Free-tier resources (FunctionGraph, small OBS) DO work at 0 balance - the gate applies to order-submission resources only |
|
|
71
|
+
| CDN mainland acceleration requires ICP filing | Unregistered domains cannot go live on CDN - check before stage 3. CDN create ALSO requires domain ownership verification first (`CDN.00010185 Verify domain ownership failed` - user must add a DNS record or upload a verification file) |
|
|
72
|
+
| CDN is a global service in KooCLI | CDN operations only accept `--cli-region=cn-north-1` or `ap-southeast-1` - never the resource region |
|
|
73
|
+
| OBS static website hosting has NO CLI path | `chattri` only sets ACL/storage-class; obsutil has no website command. Options: console setup, or CDN OBS-origin with `--domain.sources.1.enable_obs_web_hosting=1`. Without website hosting, verify via direct object URLs (still public HTTP 200) |
|
|
74
|
+
| DNS hosting location | `hcloud DNS ListPublicZones` decides: hosted in Huawei Cloud → auto-create CNAME record set; hosted elsewhere → output the CNAME value for manual setup at the user's DNS provider |
|
|
75
|
+
| EIP bills when idle; stopped ECS still bills | Include in cost estimate; destroy unattached EIPs |
|
|
76
|
+
| ECS delete defaults leak resources | `--delete_publicip` and `--delete_volume` are false by default - set `true` when tearing down |
|
|
77
|
+
| EIP binding type is non-obvious | `--publicip.associate_instance_type=PORT` (not INSTANCE) |
|
|
78
|
+
| State is session-only | Temp-dir file, dies with the session. Never promise cross-session deploy management |
|
|
79
|
+
| State file must be UTF-8 WITHOUT BOM | PowerShell `Set-Content -Encoding utf8` adds a BOM that breaks JSON parsers - write state with Node or another BOM-less writer |
|
|
80
|
+
| HTTPS needs a certificate | CDN serves plain HTTP by default; SSL cert (upload or SCM) required for HTTPS |
|
|
81
81
|
|
|
82
82
|
## MCP Tools
|
|
83
83
|
|
|
@@ -43,7 +43,9 @@ export function globalCredentialsPath() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function obsConfigPath() {
|
|
46
|
-
|
|
46
|
+
// obsutil reads its config from a fixed location (~/.obsutilconfig), independent
|
|
47
|
+
// of HUAWEICLOUD_HOME. HCLOUD_OBS_CONFIG_PATH exists solely for hermetic test injection.
|
|
48
|
+
return process.env.HCLOUD_OBS_CONFIG_PATH || join(homedir(), '.obsutilconfig');
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function readGlobalCredentials() {
|
|
@@ -16,8 +16,11 @@ import { redactSecrets } from '../safety-policy.mjs';
|
|
|
16
16
|
|
|
17
17
|
export { hasRuntimeCredentials };
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
|
|
19
|
+
export function kooCliConfigPath() {
|
|
20
|
+
// KooCLI keeps its config at a fixed location (~/.hcloud/config.json),
|
|
21
|
+
// independent of HUAWEICLOUD_HOME (that env only relocates devkit's own S1/S3).
|
|
22
|
+
// HCLOUD_CONFIG_PATH exists solely for hermetic test injection.
|
|
23
|
+
return process.env.HCLOUD_CONFIG_PATH || join(homedir(), '.hcloud', 'config.json');
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function fingerprint(ak, sk) {
|
|
@@ -38,7 +41,7 @@ export function isManualModified(path) {
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export function readKooCliProfiles() {
|
|
41
|
-
const configPath =
|
|
44
|
+
const configPath = kooCliConfigPath();
|
|
42
45
|
if (!existsSync(configPath)) return { error: 'KooCLI config not found' };
|
|
43
46
|
try {
|
|
44
47
|
const raw = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
@@ -129,7 +132,7 @@ export function scanState() {
|
|
|
129
132
|
store: 'S2-current',
|
|
130
133
|
source: 'KooCLI current profile',
|
|
131
134
|
fingerprint: currentFp,
|
|
132
|
-
manualModified: isManualModified(kooCli.configPath ||
|
|
135
|
+
manualModified: isManualModified(kooCli.configPath || kooCliConfigPath()),
|
|
133
136
|
});
|
|
134
137
|
}
|
|
135
138
|
if (s1Fingerprint && s3Fingerprint && s1Fingerprint !== s3Fingerprint) {
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
getProxySettings,
|
|
37
37
|
} from './proxy/proxy-config.mjs';
|
|
38
38
|
import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
|
|
39
|
-
import {
|
|
39
|
+
import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
|
|
40
40
|
import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
|
|
41
41
|
import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
|
|
42
42
|
|
|
@@ -3255,9 +3255,9 @@ function parseTarget() {
|
|
|
3255
3255
|
process.exit(1);
|
|
3256
3256
|
}
|
|
3257
3257
|
|
|
3258
|
-
function checkForUpdate() {
|
|
3258
|
+
async function checkForUpdate() {
|
|
3259
3259
|
if (pkgVersion === '0.0.0') return;
|
|
3260
|
-
const distTags =
|
|
3260
|
+
const distTags = await queryDistTagsFetch({ timeoutMs: 15000 });
|
|
3261
3261
|
const target = determineTarget(pkgVersion, distTags ?? {});
|
|
3262
3262
|
if (target && semverCompare(target, pkgVersion) > 0) {
|
|
3263
3263
|
const tag = distTags.next && semverCompare(target, distTags.next) === 0 ? 'next' : 'latest';
|
|
@@ -3312,7 +3312,7 @@ async function cmdInstall() {
|
|
|
3312
3312
|
}
|
|
3313
3313
|
|
|
3314
3314
|
checkNode();
|
|
3315
|
-
checkForUpdate();
|
|
3315
|
+
await checkForUpdate();
|
|
3316
3316
|
const installFailures = [];
|
|
3317
3317
|
|
|
3318
3318
|
function shouldInstall(name) {
|
|
@@ -4077,7 +4077,7 @@ async function cmdDoctor() {
|
|
|
4077
4077
|
async function cmdUpdate() {
|
|
4078
4078
|
console.log(BANNER);
|
|
4079
4079
|
const target = parseTarget();
|
|
4080
|
-
checkForUpdate();
|
|
4080
|
+
await checkForUpdate();
|
|
4081
4081
|
|
|
4082
4082
|
if (target === 'opencode') {
|
|
4083
4083
|
if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
|
|
@@ -19,7 +19,11 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
19
19
|
const __dirname = dirname(__filename);
|
|
20
20
|
const PLUGIN_DIR = join(__dirname, '..', '..');
|
|
21
21
|
const AGENT_TELEMETRY_DIR = join(PLUGIN_DIR, 'telemetry');
|
|
22
|
-
const GLOBAL_TELEMETRY_DIR = join(
|
|
22
|
+
const GLOBAL_TELEMETRY_DIR = join(
|
|
23
|
+
(process.env.HUAWEICLOUD_DEVKIT_HOME || '').trim() || homedir(),
|
|
24
|
+
'.huaweicloud-devkit',
|
|
25
|
+
'telemetry',
|
|
26
|
+
);
|
|
23
27
|
|
|
24
28
|
let PLUGIN_VERSION = '0.0.0';
|
|
25
29
|
try {
|
|
@@ -6,6 +6,8 @@ import { join, dirname, resolve } from 'node:path';
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
|
+
import { fetchWithProxy } from './proxy/proxy-agent.mjs';
|
|
10
|
+
|
|
9
11
|
const IS_WINDOWS = process.platform === 'win32';
|
|
10
12
|
const NPM_BIN = IS_WINDOWS ? 'npm.cmd' : 'npm';
|
|
11
13
|
const NPX_BIN = IS_WINDOWS ? 'npx.cmd' : 'npx';
|
|
@@ -187,7 +189,33 @@ export function writeSkipState(file, dismissedVersion, { at = Date.now(), days =
|
|
|
187
189
|
return state;
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
|
|
192
|
+
function debugLog(message) {
|
|
193
|
+
if (process.env.HUAWEICLOUD_DEVKIT_DEBUG === '1' || process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true') {
|
|
194
|
+
console.error(`[debug] ${message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function queryDistTagsFetch({ timeoutMs = 15000 } = {}) {
|
|
199
|
+
let registry = 'https://registry.npmjs.org';
|
|
200
|
+
if (process.env.HUAWEICLOUD_NPM_REGISTRY) {
|
|
201
|
+
registry = process.env.HUAWEICLOUD_NPM_REGISTRY.replace(/\/+$/, '');
|
|
202
|
+
}
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
205
|
+
return fetchWithProxy(`${registry}/-/package/huaweicloud-devkit/dist-tags`)
|
|
206
|
+
.then((resp) => {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
if (!resp || !resp.ok) return null;
|
|
209
|
+
return resp.json().catch(() => null);
|
|
210
|
+
})
|
|
211
|
+
.catch((error) => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
debugLog(`queryDistTagsFetch: ${error?.message || error}`);
|
|
214
|
+
return null;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function queryDistTagsSync({ timeoutMs = 15000, cwd } = {}) {
|
|
191
219
|
try {
|
|
192
220
|
const result = spawnSync(NPM_BIN, ['view', 'huaweicloud-devkit', 'dist-tags', '--json'], {
|
|
193
221
|
encoding: 'utf8',
|
|
@@ -195,14 +223,18 @@ export function queryDistTagsSync({ timeoutMs = 5000, cwd } = {}) {
|
|
|
195
223
|
windowsHide: true,
|
|
196
224
|
cwd,
|
|
197
225
|
});
|
|
198
|
-
if (result.status !== 0)
|
|
226
|
+
if (result.status !== 0) {
|
|
227
|
+
debugLog(`queryDistTagsSync: npm view exited with status ${result.status}`);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
199
230
|
return parseDistTagsOutput(result.stdout);
|
|
200
|
-
} catch {
|
|
231
|
+
} catch (error) {
|
|
232
|
+
debugLog(`queryDistTagsSync: ${error?.message || error}`);
|
|
201
233
|
return null;
|
|
202
234
|
}
|
|
203
235
|
}
|
|
204
236
|
|
|
205
|
-
export function queryDistTags({ timeoutMs =
|
|
237
|
+
export function queryDistTags({ timeoutMs = 15000, cwd } = {}) {
|
|
206
238
|
return new Promise((resolve) => {
|
|
207
239
|
let child;
|
|
208
240
|
try {
|
|
@@ -210,7 +242,8 @@ export function queryDistTags({ timeoutMs = 5000, cwd } = {}) {
|
|
|
210
242
|
windowsHide: true,
|
|
211
243
|
cwd,
|
|
212
244
|
});
|
|
213
|
-
} catch {
|
|
245
|
+
} catch (error) {
|
|
246
|
+
debugLog(`queryDistTags: ${error?.message || error}`);
|
|
214
247
|
resolve(null);
|
|
215
248
|
return;
|
|
216
249
|
}
|
|
@@ -218,20 +251,26 @@ export function queryDistTags({ timeoutMs = 5000, cwd } = {}) {
|
|
|
218
251
|
try {
|
|
219
252
|
child.kill();
|
|
220
253
|
} catch {}
|
|
254
|
+
debugLog(`queryDistTags: timed out after ${timeoutMs}ms`);
|
|
221
255
|
resolve(null);
|
|
222
256
|
}, timeoutMs);
|
|
223
257
|
let stdout = '';
|
|
224
258
|
child.stdout.on('data', (d) => {
|
|
225
259
|
stdout += String(d);
|
|
226
260
|
});
|
|
227
|
-
child.on('error', () => {
|
|
261
|
+
child.on('error', (error) => {
|
|
228
262
|
clearTimeout(timer);
|
|
263
|
+
debugLog(`queryDistTags: ${error?.message || error}`);
|
|
229
264
|
resolve(null);
|
|
230
265
|
});
|
|
231
266
|
child.on('close', (code) => {
|
|
232
267
|
clearTimeout(timer);
|
|
233
|
-
if (code === 0)
|
|
234
|
-
|
|
268
|
+
if (code === 0) {
|
|
269
|
+
resolve(parseDistTagsOutput(stdout));
|
|
270
|
+
} else {
|
|
271
|
+
debugLog(`queryDistTags: npm view exited with code ${code}`);
|
|
272
|
+
resolve(null);
|
|
273
|
+
}
|
|
235
274
|
});
|
|
236
275
|
});
|
|
237
276
|
}
|
|
@@ -250,19 +289,19 @@ export function invalidateUpdateCache() {
|
|
|
250
289
|
lastHint = null;
|
|
251
290
|
}
|
|
252
291
|
|
|
253
|
-
function cacheValid() {
|
|
254
|
-
return Boolean(cachedDistTags) &&
|
|
292
|
+
function cacheValid(now = Date.now()) {
|
|
293
|
+
return Boolean(cachedDistTags) && now - cachedAt <= TTL_MS;
|
|
255
294
|
}
|
|
256
295
|
|
|
257
|
-
export async function getCachedUpdateInfo(current, { doQuery = queryDistTags } = {}) {
|
|
296
|
+
export async function getCachedUpdateInfo(current, { doQuery = queryDistTags, now = Date.now() } = {}) {
|
|
258
297
|
if (process.env.HUAWEICLOUD_DEVKIT_SKIP_UPDATE === '1') {
|
|
259
|
-
lastHint = judgeUpdate(current, null);
|
|
298
|
+
lastHint = judgeUpdate(current, null, undefined, now);
|
|
260
299
|
return lastHint;
|
|
261
300
|
}
|
|
262
301
|
const skipState = readSkipState(resolveSkipFilePath());
|
|
263
|
-
if (!cacheValid()) {
|
|
264
|
-
if (!cachedDistTags &&
|
|
265
|
-
lastHint = judgeUpdate(current, null, skipState);
|
|
302
|
+
if (!cacheValid(now)) {
|
|
303
|
+
if (!cachedDistTags && now - failedAt < FAIL_THROTTLE_MS) {
|
|
304
|
+
lastHint = judgeUpdate(current, null, skipState, now);
|
|
266
305
|
return lastHint;
|
|
267
306
|
}
|
|
268
307
|
if (!inflightQuery) {
|
|
@@ -270,9 +309,9 @@ export async function getCachedUpdateInfo(current, { doQuery = queryDistTags } =
|
|
|
270
309
|
.then((distTags) => {
|
|
271
310
|
if (distTags) {
|
|
272
311
|
cachedDistTags = distTags;
|
|
273
|
-
cachedAt =
|
|
312
|
+
cachedAt = now;
|
|
274
313
|
} else {
|
|
275
|
-
failedAt =
|
|
314
|
+
failedAt = now;
|
|
276
315
|
}
|
|
277
316
|
return distTags;
|
|
278
317
|
})
|
|
@@ -281,7 +320,7 @@ export async function getCachedUpdateInfo(current, { doQuery = queryDistTags } =
|
|
|
281
320
|
});
|
|
282
321
|
}
|
|
283
322
|
const distTags = await inflightQuery;
|
|
284
|
-
lastHint = judgeUpdate(current, distTags, skipState);
|
|
323
|
+
lastHint = judgeUpdate(current, distTags, skipState, now);
|
|
285
324
|
return lastHint;
|
|
286
325
|
}
|
|
287
326
|
lastHint = judgeUpdate(current, cachedDistTags, skipState);
|