huaweicloud-devkit 1.1.3 → 1.1.4-next.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -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/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/policy.json +2 -0
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +8 -7
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +6 -6
- package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +36 -1
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +0 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +9 -7
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +4 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +1 -1
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +3 -1
- package/plugins/huaweicloud-core/src/auth/service.mjs +4 -1
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +223 -17
- package/plugins/huaweicloud-core/src/safety-policy.mjs +13 -7
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +10 -2
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +24 -6
- package/plugins/huaweicloud-core/src/setup-cli.mjs +21 -6
- package/plugins/huaweicloud-core/src/tools.mjs +45 -11
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**[中文](README.zh-CN.md) | English**
|
|
10
10
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**中文 | [English](README.md)**
|
|
10
10
|
|
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.
|
|
4
|
+
"version": "1.1.4-next.1",
|
|
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",
|
|
@@ -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.
|
|
20
|
+
"version": "1.1.4-next.1",
|
|
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.
|
|
23
|
+
"version": "1.1.4-next.1",
|
|
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.
|
|
20
|
+
"version": "1.1.4-next.1",
|
|
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
|
+
"version": "1.1.4-next.1",
|
|
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
|
+
"version": "1.1.4-next.1",
|
|
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",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.4-next.1",
|
|
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.",
|
|
@@ -16,13 +16,14 @@ Domain expertise for billing queries (BSS). Covers cost tracking, bill details,
|
|
|
16
16
|
|
|
17
17
|
## Critical Warnings
|
|
18
18
|
|
|
19
|
-
| Trap | Why
|
|
20
|
-
| ----------------------------------------- |
|
|
21
|
-
| Bills delayed ~24h | Yesterday's costs may not appear until the next day
|
|
22
|
-
| BSS Admin role needed | IAM user must have BSS Administrator or Finance role
|
|
23
|
-
| Currency conversion varies | Cross-region costs use daily exchange rates
|
|
24
|
-
| Region fixed to cn-north-1 | BSS operations only support `--cli-region=cn-north-1` in KooCLI. This is a KooCLI metadata limitation — the billing data itself covers all regions.
|
|
25
|
-
|
|
|
19
|
+
| Trap | Why |
|
|
20
|
+
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| Bills delayed ~24h | Yesterday's costs may not appear until the next day |
|
|
22
|
+
| BSS Admin role needed | IAM user must have BSS Administrator or Finance role |
|
|
23
|
+
| Currency conversion varies | Cross-region costs use daily exchange rates |
|
|
24
|
+
| Region fixed to cn-north-1 | BSS operations only support `--cli-region=cn-north-1` in KooCLI. This is a KooCLI metadata limitation — the billing data itself covers all regions. |
|
|
25
|
+
| English catalog missing BSS | `Unsupported service: BSS` under the default `en` language is a KooCLI catalog gap, not a missing service. devkit auto-injects `--cli-lang=cn`; manual fallback: add `--cli-lang=cn` or `hcloud configure set --cli-lang=cn` — prefer `--cli-lang=cn` on the command itself so the global output language is untouched. |
|
|
26
|
+
| `缺少必填参数 cli-domain-id` / APIGW.0301 | Invalid credentials — KooCLI can't resolve the account-id | Re-run `npx huaweicloud-devkit auth init` |
|
|
26
27
|
|
|
27
28
|
## Common Workflows
|
|
28
29
|
|
|
@@ -58,7 +58,7 @@ Flavor families are **region-dependent**. Always run `hcloud ECS ListFlavors --c
|
|
|
58
58
|
| Create instance | hcloud ECS CreateServers --server.name=<n> --server.flavorRef=<id> --server.imageRef=<id> --server.nics.1.subnet_id=<id> --server.availability_zone=<az> | references/create-instance.md |
|
|
59
59
|
| Find by name | See "How to search for instances" below | |
|
|
60
60
|
| Bind EIP | hcloud EIP AssociatePublicips --publicip_id=<id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT | Get port ID from `hcloud ECS ListServersDetails --server_id=<id>` → `OS-EXT-IPS:port_id` |
|
|
61
|
-
| Security group rule | hcloud VPC CreateSecurityGroupRule --security_group_id=<id> --direction=<direction> --protocol=<protocol> |
|
|
61
|
+
| Security group rule | hcloud VPC CreateSecurityGroupRule --security_group_id=<id> --direction=<direction> --protocol=<protocol> | see huawei-vpc |
|
|
62
62
|
| Attach disk | hcloud ECS AttachServerVolume --server_id=<id> --volumeAttachment.volumeId=<id> | references/evs.md |
|
|
63
63
|
| Delete instance | hcloud ECS DeleteServers --servers.1.id=<id> --delete_publicip=true --delete_volume=true | references/create-instance.md |
|
|
64
64
|
| Reboot instance | hcloud ECS BatchRebootServers --reboot.servers.1.id=<id> --reboot.type=SOFT | NOT `RebootServer` — that operation does not exist |
|
|
@@ -45,12 +45,12 @@ Domain expertise for Huawei Cloud Identity and Access Management (IAM). Covers u
|
|
|
45
45
|
|
|
46
46
|
| Task | Command | Steps |
|
|
47
47
|
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
48
|
-
| List users | hcloud IAM KeystoneListUsers --cli-region=<r> | references/
|
|
49
|
-
| Create group | hcloud IAM CreateGroupV5 --group_name=<name> | references/
|
|
48
|
+
| List users | hcloud IAM KeystoneListUsers --cli-region=<r> | references/quick-ref.md |
|
|
49
|
+
| Create group | hcloud IAM CreateGroupV5 --group_name=<name> | references/quick-ref.md |
|
|
50
50
|
| Create custom policy | hcloud IAM CreateCloudServiceCustomPolicy --role.display_name=<n> --role.type=<type> --role.policy.Statement.1.Effect=Allow | references/policy-examples.md |
|
|
51
|
-
| Create agency | hcloud IAM CreateAgency --agency.name=<n> --agency.domain_id=<id> --agency.trust_domain_name=<domain> | references/
|
|
52
|
-
| Get temporary credential | hcloud STS AssumeAgency --agency_urn=<urn> --agency_session_name=<n> --duration_seconds=3600 | references/
|
|
53
|
-
| Attach policy to group | hcloud IAM AttachGroupPolicyV5 --group_id=<id> --policy_id=<id> | references/
|
|
51
|
+
| Create agency | hcloud IAM CreateAgency --agency.name=<n> --agency.domain_id=<id> --agency.trust_domain_name=<domain> | references/quick-ref.md |
|
|
52
|
+
| Get temporary credential | hcloud STS AssumeAgency --agency_urn=<urn> --agency_session_name=<n> --duration_seconds=3600 | references/quick-ref.md |
|
|
53
|
+
| Attach policy to group | hcloud IAM AttachGroupPolicyV5 --group_id=<id> --policy_id=<id> | references/quick-ref.md |
|
|
54
54
|
|
|
55
55
|
## Policy Examples (Least Privilege)
|
|
56
56
|
|
|
@@ -109,4 +109,4 @@ Common roles for FunctionGraph: VPC Administrator, RDS Administrator, DCS Admini
|
|
|
109
109
|
|
|
110
110
|
- IAM Docs: https://support.huaweicloud.com/iam/
|
|
111
111
|
- Policy examples: references/policy-examples.md
|
|
112
|
-
- Agency setup: references/
|
|
112
|
+
- Agency setup: references/quick-ref.md
|
|
@@ -7,11 +7,46 @@ hcloud IAM KeystoneListProjects # list projects
|
|
|
7
7
|
hcloud IAM KeystoneListUsers # list users
|
|
8
8
|
hcloud IAM KeystoneListGroups # list groups
|
|
9
9
|
hcloud IAM ListCustomPolicies # list custom policies
|
|
10
|
-
hcloud IAM ListAgencies
|
|
10
|
+
hcloud IAM ListAgencies --domain_id=<domain-id> # list agencies
|
|
11
11
|
hcloud IAM KeystoneListAuthDomains # list domains
|
|
12
12
|
hcloud IAM KeystoneShowUser # show user detail
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
> **Group lifecycle uses the V5 API, not Keystone.** `CreateGroupV5` / `AttachGroupPolicyV5` take `group_id`/`policy_id`; `KeystoneListGroups` is the older read API. The V5 group `description` forbids `@ # % & < > \ $ ^ *`.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
hcloud IAM CreateGroupV5 --group_name=<name>
|
|
19
|
+
hcloud IAM AttachGroupPolicyV5 --group_id=<id> --policy_id=<id>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Agencies & Temporary Credentials (STS)
|
|
23
|
+
|
|
24
|
+
An agency + `STS AssumeAgency` is the only safe way to mint temporary credentials (never store long-term AK/SK):
|
|
25
|
+
|
|
26
|
+
1. Create the delegation:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
hcloud IAM CreateAgency --agency.name=<name> --agency.domain_id=<domain-id> --agency.trust_domain_name=<trust-domain>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--agency.trust_domain_name` and `--agency.trust_domain_id` are two ways to name the entrusted account — provide at least one; when both are present, `trust_domain_name` wins validation.
|
|
33
|
+
|
|
34
|
+
2. Grant a role to the agency (optional):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
hcloud IAM AssociateAgencyWithDomainPermission --agency_id=<id> --role_id=<role-id>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. Exchange for temporary credentials:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
hcloud STS AssumeAgency --agency_urn=<urn> --agency_session_name=<name> --duration_seconds=3600
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `--agency_urn` is the agency **URN**, not its name or id — query it with `hcloud IAM ListAgencies --domain_id=<domain-id>` first.
|
|
47
|
+
- `--duration_seconds` range is `[900, 43200]`; it must not exceed the agency's own session duration, and is capped at `3600` when `--X-Security-Token` is present.
|
|
48
|
+
- The returned credential includes a `security_token` — pass it as `--X-Security-Token` for subsequent calls.
|
|
49
|
+
|
|
15
50
|
## Credential Management
|
|
16
51
|
|
|
17
52
|
```bash
|
|
@@ -131,4 +131,3 @@ See `references/single-file-share.md` for the full workflow to host one file and
|
|
|
131
131
|
- Static website: references/static-website.md
|
|
132
132
|
- Single-file share: references/single-file-share.md
|
|
133
133
|
- Lifecycle: references/bucket-lifecycle.md
|
|
134
|
-
- Replication: references/replication.md
|
|
@@ -95,7 +95,7 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
95
95
|
2. **Real-name verification only** (`HDKIT_NOT_REALNAME`): tell the developer once, "Huawei Cloud requires real-name verification before using the sandbox — please complete it in the Huawei Cloud console (实名认证)." and stop — do not retry `connect` in a loop
|
|
96
96
|
3. **Sign agreement only** (`HDKIT_NOT_AGREEMENT`): **STOP and do NOT sign on your own.** Ask the developer: "Huawei Cloud sandbox requires signing the latest developer service agreement. May I sign it for you?" Then **wait for the developer to explicitly agree** (e.g. "签署" / "确认" / "sign it"). Only after explicit consent call `huaweicloud_sandbox_sign_agreement` and return its result (`signed`/`signedCount`) to the developer. **Never sign a legal agreement on the developer's behalf without their explicit, unambiguous consent.** Do not expose the underlying sandbox/DevBridge service as a separate entity the developer must understand or sign up for
|
|
97
97
|
4. **Both missing** (`HDKIT_NOT_REALNAME_AND_AGREEMENT`): present **both** requirements together in one message — the real-name verification steps (console, step 2) **and** the agreement-signing request (step 3, wait for explicit consent) — so the developer can complete both at once
|
|
98
|
-
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address
|
|
98
|
+
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`, `expiresAt` (STS credential expiry). The `source` parameter identifies the calling agent (valid values: `CLI`, `WEB`, `VSCODE`, `WEBVNC`, `WEBPTY`, `WEBIDE`, `CURSOR`, etc. — case-sensitive, all uppercase). When the `git` parameter is provided (with `repo_url`, `target_path`, optional `repo_branch`), code is automatically transferred to the sandbox: local `git clone` followed by `uploadProject` (falls back to direct sandbox clone when local git is unavailable). Check `_repoStatus` in the response: `uploaded_from_local`, `cloned_in_sandbox`, or `already_exists` (reconnect).
|
|
99
99
|
6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs, DevBridge tunnels, and stale web processes from previous deployments can cause port conflicts and quota errors. Run cleanup immediately after connect:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
@@ -14,6 +14,7 @@ version: 1
|
|
|
14
14
|
2. 按返回严格判定:
|
|
15
15
|
- `claimed=true` → 静默处理,不再提及代金券,不打扰用户
|
|
16
16
|
- `claimed=false` 且 `message="未领取"` → **主动提示**,用下方话术并询问是否领取
|
|
17
|
+
- 返回含凭证错误码(`code=HDKIT_CRED_MISSING` / `HDKIT_CRED_INVALID` / `HDKIT_CRED_RATE_LIMIT`),或 `message` 含「未配置 / not configured / AK/SK 无效 / 限额」→ **提示用户检查或配置华为云凭证**(不要误报为「服务不可用」)
|
|
17
18
|
- 其余情况(如 `message="查询失败"`、状态不明确、调用异常)→ **严格不提示**
|
|
18
19
|
3. 提示话术(可直接使用):
|
|
19
20
|
> "你还可以领取一张华为云激励代金券(一人一次),需要我现在帮你领取吗?"
|
|
@@ -38,13 +39,14 @@ version: 1
|
|
|
38
39
|
|
|
39
40
|
### 3. 结果解读(错误码 UX)
|
|
40
41
|
|
|
41
|
-
| 返回 message 关键特征
|
|
42
|
-
|
|
|
43
|
-
| `领取成功`
|
|
44
|
-
| `已领取过` / `已领取`
|
|
45
|
-
| `本月代金券总额度已用完,所有账号均无法领取`
|
|
46
|
-
| `请先完成实名认证`
|
|
47
|
-
| `发券失败: <原文>`
|
|
42
|
+
| 返回 message 关键特征 | 含义 | 对用户表述 |
|
|
43
|
+
| ----------------------------------------------------------------------- | --------------------------- | ------------------------ |
|
|
44
|
+
| `领取成功` | 发券成功 | 告知券已到账 |
|
|
45
|
+
| `已领取过` / `已领取` | 该账号已领过 | 告知已领取,不打扰 |
|
|
46
|
+
| `本月代金券总额度已用完,所有账号均无法领取` | 全局额度用尽(HD.60630042) | 告知下月再试 |
|
|
47
|
+
| `请先完成实名认证` | 账号未实名(HD.60630022) | 引导去控制台实名认证 |
|
|
48
|
+
| `发券失败: <原文>` | 其他错误 | 原样转达,不展开 |
|
|
49
|
+
| `code=HDKIT_CRED_*` / 含「未配置 / not configured / AK/SK 无效 / 限额」 | 凭证未配置、无效或达限额 | 引导用户检查并配置 AK/SK |
|
|
48
50
|
|
|
49
51
|
## 环境注意
|
|
50
52
|
|
|
@@ -43,8 +43,8 @@ Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet
|
|
|
43
43
|
| SG rule | hcloud VPC CreateSecurityGroupRule --security_group_rule.security_group_id=<id> --security_group_rule.direction=<direction> --security_group_rule.protocol=<protocol> --security_group_rule.multiport=<port> --security_group_rule.remote_ip_prefix=<cidr> | references/security-group.md |
|
|
44
44
|
| Create EIP | hcloud EIP CreatePublicip --publicip.type=<type> --bandwidth.size=<size> --bandwidth.share_type=<share-type> --bandwidth.name=<name> | Run `hcloud EIP CreatePublicip --help` to confirm valid type values per region |
|
|
45
45
|
| Bind EIP to ECS | hcloud EIP AssociatePublicips --publicip_id=<id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT | Get port ID from `hcloud ECS ListServersDetails --server_id=<id>` → `OS-EXT-IPS:port_id` |
|
|
46
|
-
| Unbind EIP | hcloud EIP DisassociatePublicips --publicip_id=<id> | references/
|
|
47
|
-
| Delete EIP | hcloud EIP DeletePublicip --publicip_id=<id> | references/
|
|
46
|
+
| Unbind EIP | hcloud EIP DisassociatePublicips --publicip_id=<id> | references/network.md |
|
|
47
|
+
| Delete EIP | hcloud EIP DeletePublicip --publicip_id=<id> | references/network.md |
|
|
48
48
|
| List EIPs | hcloud EIP ListPublicips | |
|
|
49
49
|
| NAT gateway | hcloud NAT CreateNatGateway --nat.name=<name> --nat.spec=<spec> --router_id=<vpc-id> --internal_network_id=<subnet-id> | Run `hcloud NAT CreateNatGateway --help` for available spec values |
|
|
50
50
|
|
|
@@ -29,10 +29,10 @@ Use this skill to turn vague developer intent into a precise Huawei Cloud capabi
|
|
|
29
29
|
|
|
30
30
|
Match the scenario before picking a service. For "deploy a web app" requests, layer the recommendation instead of defaulting to a production service:
|
|
31
31
|
|
|
32
|
-
| Scenario | Recommended path
|
|
33
|
-
| -------------------------------------------------------------------- |
|
|
34
|
-
| hello world / prototype / demo / temporary preview (free, quick try) | `huawei-sandbox` first — temporary runtime + public URL
|
|
35
|
-
| production / long-term / custom domain / high availability | `huawei-functiongraph` / `huawei-ecs` (or `huawei-cce` for containers)
|
|
32
|
+
| Scenario | Recommended path |
|
|
33
|
+
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| hello world / prototype / demo / temporary preview (free, quick try) | `huawei-sandbox` first — temporary runtime + public URL (runtime lifetime not exposed via API); credential expiry see `expiresAt` from `connect`/`credentials`, zero billed resources |
|
|
35
|
+
| production / long-term / custom domain / high availability | `huawei-functiongraph` / `huawei-ecs` (or `huawei-cce` for containers) |
|
|
36
36
|
|
|
37
37
|
Route to the sandbox when the developer signals free/quick preview intent ("免费", "快速", "预览", "hello world", "原型", "演示"); route to production services only when production-grade hosting is explicitly required.
|
|
38
38
|
|
|
@@ -106,6 +106,13 @@ hcloud ECS CreateServers --cli-region=<region> --server.name=<name> --server.fla
|
|
|
106
106
|
|
|
107
107
|
If a command needs an `adminPass` or other password field, do not leave plaintext secrets in shell history. Prefer local-only input or runtime injection.
|
|
108
108
|
|
|
109
|
+
## Language / Service Catalog Trap
|
|
110
|
+
|
|
111
|
+
- KooCLI loads the service list from a per-language catalog: `~/.hcloud/metaRepo/services_{cn,en}.json`. The **English catalog is incomplete** (149 services vs 220 in Chinese; ~70 services — BSS, DevStar, CloudTable, FRS, ASM, ... — are missing). A command against a service that only exists in the Chinese catalog fails with a misleading `Unsupported service: X`.
|
|
112
|
+
- `huaweicloud-devkit` auto-injects `--cli-lang=cn` (proactive when the catalog shows `X ∈ cn ∧ X ∉ en`, reactive once if `Unsupported service` still appears) for read and write commands; approved write commands keep their approved argument list (injection happens inside the runner).
|
|
113
|
+
- Manual fallback (do not rely on the auto path alone): append `--cli-lang=cn` to the failing command, or set globally with `hcloud configure set --cli-lang=cn` (this changes ALL CLI output language).
|
|
114
|
+
- Distinguish from **region** metadata limits (e.g., BSS only supports `cn-north-1`): a region error is NOT solved by switching language.
|
|
115
|
+
|
|
109
116
|
## Output Formatting
|
|
110
117
|
|
|
111
118
|
```bash
|
|
@@ -82,7 +82,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
|
|
|
82
82
|
| Getting started | Account setup | huaweicloud-cli-and-auth |
|
|
83
83
|
| Incentive voucher | Voucher | huawei-voucher |
|
|
84
84
|
|
|
85
|
-
**Web app scenario layering**: for "deploy a web app", prefer `huawei-sandbox` for free/quick try (hello world, prototype, demo, temporary preview — temporary runtime + public URL
|
|
85
|
+
**Web app scenario layering**: for "deploy a web app", prefer `huawei-sandbox` for free/quick try (hello world, prototype, demo, temporary preview — temporary runtime + public URL (runtime lifetime not exposed via API); credential expiry see `expiresAt` from `connect`/`credentials`, zero billed resources); route to `huawei-functiongraph` / `huawei-ecs` (or `huawei-cce`) for production / long-term / custom-domain / high-availability hosting. When the chosen target requires **multiple purchased resources** (e.g. VPC + subnet + ECS + EIP + RDS, or OBS + CDN + DNS) or the user asks for an architecture/cost proposal first, hand off to `huawei-iac` for orchestrated provisioning with a mandatory cost gate.
|
|
86
86
|
|
|
87
87
|
## Deployment Target Options
|
|
88
88
|
|
|
@@ -38,5 +38,5 @@ Ask:
|
|
|
38
38
|
|
|
39
39
|
Route by scenario:
|
|
40
40
|
|
|
41
|
-
- Free / quick try / prototype / demo / temporary preview -> huawei-sandbox (temporary runtime + public URL
|
|
41
|
+
- Free / quick try / prototype / demo / temporary preview -> huawei-sandbox (temporary runtime + public URL (runtime lifetime not exposed via API); credential expiry see `expiresAt` from `connect`/`credentials`, zero billed resources)
|
|
42
42
|
- Production / long-term / custom domain / high availability -> huawei-functiongraph / huawei-ecs (or huawei-cce for containers)
|
|
@@ -148,9 +148,11 @@ export function resolveCredentials(options = {}) {
|
|
|
148
148
|
|
|
149
149
|
if (!ak || !sk) {
|
|
150
150
|
if (options.allowMissing) return null;
|
|
151
|
-
|
|
151
|
+
const err = new Error(
|
|
152
152
|
'Huawei Cloud credentials are not configured. Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
|
|
153
153
|
);
|
|
154
|
+
err.code = 'HDKIT_CRED_MISSING';
|
|
155
|
+
throw err;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
return { ak, sk, securityToken, region };
|
|
@@ -57,10 +57,13 @@ export function syncAuth(target = 'all') {
|
|
|
57
57
|
try {
|
|
58
58
|
obs = writeObsConfig(credentials);
|
|
59
59
|
} catch (error) {
|
|
60
|
+
const missingRegion = !String(credentials?.region || '').trim();
|
|
60
61
|
return {
|
|
61
62
|
ok: false,
|
|
62
63
|
error: error.message,
|
|
63
|
-
nextStep:
|
|
64
|
+
nextStep: missingRegion
|
|
65
|
+
? 'Credential region is missing — run huaweicloud_auth_switch action=persist with --region, or add "region" to creds-import.json and re-import.'
|
|
66
|
+
: 'Run "npx huaweicloud-devkit auth init" to refresh credentials and region.',
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import {
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
5
6
|
|
|
6
7
|
import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
|
|
7
8
|
import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
@@ -14,29 +15,84 @@ const APPROVAL_TTL_MS = 5 * 60_000;
|
|
|
14
15
|
const LARGE_OUTPUT_THRESHOLD = 50_000;
|
|
15
16
|
const OUTPUT_DIR = join('/tmp', 'huaweicloud-devkit');
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
// Approval tokens must survive a process boundary: plan and run land in
|
|
19
|
+
// different MCP sessions/processes (and headless subprocesses), so the old
|
|
20
|
+
// in-memory Map lost the token and plan→approve→run became unreachable (#578).
|
|
21
|
+
// Persist to a per-user JSON file as the single source of truth. Only the
|
|
22
|
+
// sha256 of the args plus the redacted form are stored — never the raw args,
|
|
23
|
+
// which may carry --adminPass / --server.user_data secrets.
|
|
24
|
+
function approvalFilePath() {
|
|
25
|
+
return join(process.env.HUAWEICLOUD_HOME || homedir(), '.config', 'huaweicloud', 'approvals.json');
|
|
26
|
+
}
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
function sha256Hex(data) {
|
|
29
|
+
return createHash('sha256').update(data).digest('hex');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hashArgs(args) {
|
|
33
|
+
return sha256Hex(JSON.stringify(args));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readApprovals() {
|
|
37
|
+
try {
|
|
38
|
+
const path = approvalFilePath();
|
|
39
|
+
if (!existsSync(path)) return {};
|
|
40
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeApprovals(map) {
|
|
48
|
+
try {
|
|
49
|
+
const path = approvalFilePath();
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
52
|
+
writeFileSync(tmp, JSON.stringify(map), { encoding: 'utf8', mode: 0o600 });
|
|
53
|
+
renameSync(tmp, path);
|
|
54
|
+
} catch {
|
|
55
|
+
// Best-effort: an in-file failure degrades cross-process persistence but a
|
|
56
|
+
// token created/consumed within one process still works.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pruneStale(map, now = Date.now()) {
|
|
61
|
+
let changed = false;
|
|
62
|
+
for (const [key, value] of Object.entries(map)) {
|
|
63
|
+
if (now - value.createdAt > APPROVAL_TTL_MS) {
|
|
64
|
+
delete map[key];
|
|
65
|
+
changed = true;
|
|
26
66
|
}
|
|
27
67
|
}
|
|
68
|
+
return changed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createApprovalToken(rawArgs) {
|
|
72
|
+
const token = randomUUID();
|
|
73
|
+
const map = readApprovals();
|
|
74
|
+
map[token] = {
|
|
75
|
+
argsHash: hashArgs(rawArgs),
|
|
76
|
+
argsRedacted: redactSecrets(rawArgs),
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
pruneStale(map);
|
|
80
|
+
writeApprovals(map);
|
|
28
81
|
return token;
|
|
29
82
|
}
|
|
30
83
|
|
|
31
84
|
export function consumeApprovalToken(token) {
|
|
32
|
-
const
|
|
85
|
+
const map = readApprovals();
|
|
86
|
+
const entry = map[token];
|
|
33
87
|
if (!entry) return null;
|
|
34
88
|
if (Date.now() - entry.createdAt > APPROVAL_TTL_MS) {
|
|
35
|
-
|
|
89
|
+
delete map[token];
|
|
90
|
+
writeApprovals(map);
|
|
36
91
|
return null;
|
|
37
92
|
}
|
|
38
|
-
|
|
39
|
-
|
|
93
|
+
delete map[token];
|
|
94
|
+
writeApprovals(map);
|
|
95
|
+
return entry;
|
|
40
96
|
}
|
|
41
97
|
|
|
42
98
|
function saveLargeOutput(rawStdout) {
|
|
@@ -140,6 +196,75 @@ function obsWriteHint(args) {
|
|
|
140
196
|
return 'OBS write operations are obsutil-style and always write-class. Before executing, present the full resource manifest (bucket/object list) to the user for ONE batch approval, then run each command through plan → approve (see huawei-iac skill, Provisioning Rules).';
|
|
141
197
|
}
|
|
142
198
|
|
|
199
|
+
const OBS_SUBCOMMANDS = new Set([
|
|
200
|
+
'help',
|
|
201
|
+
'ls',
|
|
202
|
+
'mb',
|
|
203
|
+
'cp',
|
|
204
|
+
'mv',
|
|
205
|
+
'rm',
|
|
206
|
+
'chattri',
|
|
207
|
+
'config',
|
|
208
|
+
'cors',
|
|
209
|
+
'lifecycle',
|
|
210
|
+
'policy',
|
|
211
|
+
'share',
|
|
212
|
+
'sync',
|
|
213
|
+
'url',
|
|
214
|
+
'sign',
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
const CATALOG_TTL_MS = 5 * 60 * 1000;
|
|
218
|
+
let _catalogCache = { dir: null, t: 0, cn: new Set(), en: new Set() };
|
|
219
|
+
|
|
220
|
+
export function readServiceCatalogs(metaDir = join(homedir(), '.hcloud', 'metaRepo')) {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
if (_catalogCache.dir === metaDir && now - _catalogCache.t < CATALOG_TTL_MS) return _catalogCache;
|
|
223
|
+
const load = (f) => {
|
|
224
|
+
try {
|
|
225
|
+
const p = join(metaDir, f);
|
|
226
|
+
if (!existsSync(p)) return { present: false, set: new Set() };
|
|
227
|
+
const d = JSON.parse(readFileSync(p, 'utf8'));
|
|
228
|
+
// Normalize to uppercase on load: catalog entries are mixed-case
|
|
229
|
+
// (DevStar, CloudTable, MapDS, ...) while lookups use uppercase.
|
|
230
|
+
return {
|
|
231
|
+
present: true,
|
|
232
|
+
set: new Set((d.items || []).map((i) => i?.Service?.Text?.toUpperCase()).filter(Boolean)),
|
|
233
|
+
};
|
|
234
|
+
} catch {
|
|
235
|
+
return { present: false, set: new Set() };
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const cn = load('services_cn.json');
|
|
239
|
+
const en = load('services_en.json');
|
|
240
|
+
_catalogCache = { dir: metaDir, t: now, cn: cn.set, en: en.set, cnPresent: cn.present, enPresent: en.present };
|
|
241
|
+
return _catalogCache;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function classifyUnsupported(service, metaDir) {
|
|
245
|
+
const { cn, en, cnPresent, enPresent } = readServiceCatalogs(metaDir);
|
|
246
|
+
const s = String(service || '').toUpperCase();
|
|
247
|
+
if (!s) return 'other';
|
|
248
|
+
if (cnPresent && enPresent) {
|
|
249
|
+
if (cn.has(s) && !en.has(s)) return 'lang-missing';
|
|
250
|
+
if (!cn.has(s) && !en.has(s)) return 'not-found';
|
|
251
|
+
return 'other';
|
|
252
|
+
}
|
|
253
|
+
// One or both catalog files are missing locally — we cannot distinguish
|
|
254
|
+
// "service missing from the en catalog" from "en catalog not downloaded".
|
|
255
|
+
// Only the reactive fallback may attempt --cli-lang=cn for this case.
|
|
256
|
+
return 'unknown';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function shouldInjectLang(args, metaDir) {
|
|
260
|
+
const arr = Array.isArray(args) ? args.map(String) : [];
|
|
261
|
+
if (arr.some((a) => /^--cli-lang=/.test(a))) return false;
|
|
262
|
+
const s = (arr[0] || '').toUpperCase();
|
|
263
|
+
if (!s) return false;
|
|
264
|
+
if (s === 'OBS' && OBS_SUBCOMMANDS.has((arr[1] || '').toLowerCase())) return false;
|
|
265
|
+
return classifyUnsupported(s, metaDir) === 'lang-missing';
|
|
266
|
+
}
|
|
267
|
+
|
|
143
268
|
export function planHcloudCommand(args, options = {}) {
|
|
144
269
|
const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
|
|
145
270
|
const classification = classifyHcloudArgs(normalizedArgs, options);
|
|
@@ -171,6 +296,38 @@ export function planHcloudCommand(args, options = {}) {
|
|
|
171
296
|
};
|
|
172
297
|
}
|
|
173
298
|
|
|
299
|
+
const UNSUPPORTED_SERVICE_RE = /Unsupported service:\s*([A-Za-z0-9_-]+)/i;
|
|
300
|
+
|
|
301
|
+
function appendLangHint(result, service, metaDir) {
|
|
302
|
+
const cause = classifyUnsupported(service, metaDir);
|
|
303
|
+
if (cause === 'lang-missing' || cause === 'unknown') {
|
|
304
|
+
const nextStep = 'Re-run with --cli-lang=cn, or set hcloud configure set --cli-lang=cn.';
|
|
305
|
+
return {
|
|
306
|
+
...result,
|
|
307
|
+
langCause: cause,
|
|
308
|
+
langHint: `Unsupported service: ${service}. ${nextStep}`,
|
|
309
|
+
langNextStep: nextStep,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const nextStep = 'Check the service name, or refresh KooCLI metadata (hcloud upgrade / configure).';
|
|
313
|
+
return {
|
|
314
|
+
...result,
|
|
315
|
+
langCause: cause,
|
|
316
|
+
langHint: `Unsupported service: ${service}. ${nextStep}`,
|
|
317
|
+
langNextStep: nextStep,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isObsUtilStyle(args) {
|
|
322
|
+
const s = String(args[0] || '').toUpperCase();
|
|
323
|
+
return s === 'OBS' && OBS_SUBCOMMANDS.has(String(args[1] || '').toLowerCase());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function canRetryLang(service, metaDir) {
|
|
327
|
+
const cause = classifyUnsupported(service, metaDir);
|
|
328
|
+
return cause === 'lang-missing' || cause === 'unknown';
|
|
329
|
+
}
|
|
330
|
+
|
|
174
331
|
export async function runHcloud(args, options = {}) {
|
|
175
332
|
const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
|
|
176
333
|
const plan = {
|
|
@@ -179,6 +336,52 @@ export async function runHcloud(args, options = {}) {
|
|
|
179
336
|
};
|
|
180
337
|
assertAllowed(plan.classification);
|
|
181
338
|
|
|
339
|
+
const metaDir = options.metaDir;
|
|
340
|
+
const langGiven = normalizedArgs.some((a) => /^--cli-lang=/.test(a));
|
|
341
|
+
const injectedArgs =
|
|
342
|
+
!langGiven && shouldInjectLang(normalizedArgs, metaDir) ? [...normalizedArgs, '--cli-lang=cn'] : null;
|
|
343
|
+
|
|
344
|
+
if (injectedArgs) {
|
|
345
|
+
const result = await runHcloudOnceWithRetries(
|
|
346
|
+
{
|
|
347
|
+
...plan,
|
|
348
|
+
rawArgs: injectedArgs,
|
|
349
|
+
},
|
|
350
|
+
options,
|
|
351
|
+
);
|
|
352
|
+
const tagged = {
|
|
353
|
+
...result,
|
|
354
|
+
autoRetried: true,
|
|
355
|
+
injection: 'proactive',
|
|
356
|
+
injectedLang: 'cn',
|
|
357
|
+
};
|
|
358
|
+
if (tagged.ok) return tagged;
|
|
359
|
+
const match = `${result.stderr || ''}\n${result.stdout || ''}`.match(UNSUPPORTED_SERVICE_RE);
|
|
360
|
+
return match ? appendLangHint(tagged, match[1], metaDir) : tagged;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const result = await runHcloudOnceWithRetries(plan, options);
|
|
364
|
+
const text = `${result.stderr || ''}\n${result.stdout || ''}`;
|
|
365
|
+
const match = text.match(UNSUPPORTED_SERVICE_RE);
|
|
366
|
+
if (match && !langGiven && !result.ok && canRetryLang(match[1], metaDir) && !isObsUtilStyle(normalizedArgs)) {
|
|
367
|
+
const retry = await runHcloudOnceWithRetries(
|
|
368
|
+
{
|
|
369
|
+
...plan,
|
|
370
|
+
rawArgs: [...normalizedArgs, '--cli-lang=cn'],
|
|
371
|
+
},
|
|
372
|
+
options,
|
|
373
|
+
);
|
|
374
|
+
return retry.ok
|
|
375
|
+
? { ...retry, autoRetried: true, reactiveFallback: true, injection: 'reactive', injectedLang: 'cn' }
|
|
376
|
+
: appendLangHint(retry, match[1], metaDir);
|
|
377
|
+
}
|
|
378
|
+
if (match && !result.ok) {
|
|
379
|
+
return appendLangHint(result, match[1], metaDir);
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function runHcloudOnceWithRetries(plan, options) {
|
|
182
385
|
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
183
386
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
184
387
|
const result = await runHcloudOnce(plan, options);
|
|
@@ -226,6 +429,9 @@ function runHcloudOnce(plan, options) {
|
|
|
226
429
|
const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
|
|
227
430
|
const cwd = options.cwd || undefined;
|
|
228
431
|
const stdin = options.stdin ?? 'y\n';
|
|
432
|
+
const childOptions = { ...options };
|
|
433
|
+
delete childOptions.metaDir;
|
|
434
|
+
const childEnv = { ...process.env, ...childOptions.env };
|
|
229
435
|
|
|
230
436
|
return new Promise((resolve) => {
|
|
231
437
|
const proxySettings = getProxySettings();
|
|
@@ -242,7 +448,7 @@ function runHcloudOnce(plan, options) {
|
|
|
242
448
|
env: {
|
|
243
449
|
...process.env,
|
|
244
450
|
...proxyEnv,
|
|
245
|
-
...
|
|
451
|
+
...childEnv,
|
|
246
452
|
},
|
|
247
453
|
});
|
|
248
454
|
if (stdin) {
|
|
@@ -418,7 +624,7 @@ function validateRequiredParams(args) {
|
|
|
418
624
|
return { valid: missing.length === 0, missing, hints };
|
|
419
625
|
}
|
|
420
626
|
|
|
421
|
-
function extractApiError(stdout) {
|
|
627
|
+
export function extractApiError(stdout) {
|
|
422
628
|
let text = String(stdout || '');
|
|
423
629
|
// Strip KooCLI multi-version prefix lines (e.g. "ListVpcs有多个版本,默认使用该API版本v3…")
|
|
424
630
|
const bracketIdx = text.indexOf('{');
|
|
@@ -22,7 +22,7 @@ function isSecretKeyName(key, policy = DEFAULT_POLICY) {
|
|
|
22
22
|
.toLowerCase()
|
|
23
23
|
.replace(/[^a-z0-9]/g, '');
|
|
24
24
|
if (
|
|
25
|
-
/access.*key|secret.*key|security.*token|xauth.*token|authorization|password|passwd|adminpass|credential|private.*key/.test(
|
|
25
|
+
/access.*key|secret.*key|security.*token|xauth.*token|authorization|password|passwd|adminpass|credential|private.*key|userdata|metadata/.test(
|
|
26
26
|
normalized,
|
|
27
27
|
)
|
|
28
28
|
) {
|
|
@@ -32,12 +32,18 @@ function isSecretKeyName(key, policy = DEFAULT_POLICY) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function redactString(text) {
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
return (
|
|
36
|
+
String(text)
|
|
37
|
+
// Opaque blob keys (cloud-init user_data, metadata, private_key) carry
|
|
38
|
+
// base64/scripts that may embed SSH keys, DB passwords, bootstrap tokens.
|
|
39
|
+
// Redact the ENTIRE value of this arg — not just the first whitespace token.
|
|
40
|
+
.replace(/((?:user[_-]?data|metadata|private[_-]?key)\s*[:=]\s*).*/gi, '$1<redacted>')
|
|
41
|
+
.replace(
|
|
42
|
+
/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
|
|
43
|
+
'$1<redacted>',
|
|
44
|
+
)
|
|
45
|
+
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>')
|
|
46
|
+
);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
export function redactSecrets(value, policy = DEFAULT_POLICY) {
|
|
@@ -115,7 +115,11 @@ export async function hdkitVoucherStatus(domainId) {
|
|
|
115
115
|
const path = domainId ? `voucher/status?domain_id=${encodeURIComponent(domainId)}` : 'voucher/status';
|
|
116
116
|
return await hdkitRequest('GET', path, undefined, 30000);
|
|
117
117
|
} catch (error) {
|
|
118
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
claimed: false,
|
|
120
|
+
message: error?.message || 'Incentive service unavailable, please try again later',
|
|
121
|
+
code: error?.code,
|
|
122
|
+
};
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -124,6 +128,10 @@ export async function hdkitVoucherClaim(domainId) {
|
|
|
124
128
|
const body = domainId ? { domain_id: domainId } : {};
|
|
125
129
|
return await hdkitRequest('POST', 'voucher/claim', body);
|
|
126
130
|
} catch (error) {
|
|
127
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
claimed: false,
|
|
133
|
+
message: error?.message || 'Incentive service unavailable, please try again later',
|
|
134
|
+
code: error?.code,
|
|
135
|
+
};
|
|
128
136
|
}
|
|
129
137
|
}
|
|
@@ -984,15 +984,23 @@ fi
|
|
|
984
984
|
|
|
985
985
|
const result = await execOneShot(workspaceId, checkScript, username, timeoutMs);
|
|
986
986
|
const stdout = String(result.stdout || '');
|
|
987
|
+
// Strip ANSI escape sequences (CSI color/cursor codes and OSC shell-integration markers)
|
|
988
|
+
// and split on any line-ending style (\r\n, \r, or \n) to handle all terminal outputs.
|
|
989
|
+
// Use new RegExp to avoid ESLint no-control-regex on literal control chars in regex.
|
|
990
|
+
const ESC = '\x1b';
|
|
991
|
+
const csiRe = new RegExp(ESC + '\\[[0-9;]*[a-zA-Z]', 'g');
|
|
992
|
+
const oscRe = new RegExp(ESC + '\\][^' + ESC + '\x07]*(?:\x07|' + ESC + '\\\\)', 'g');
|
|
993
|
+
const cleanStdout = stdout.replace(csiRe, '').replace(oscRe, '');
|
|
987
994
|
const checks = {};
|
|
988
|
-
const lines =
|
|
995
|
+
const lines = cleanStdout.split(new RegExp('\\r\\n|\\r|\\n'));
|
|
989
996
|
for (const line of lines) {
|
|
990
|
-
const
|
|
997
|
+
const trimmed = line.trim();
|
|
998
|
+
const m = trimmed.match(/^(\w+):(PASS|FAIL|SKIP)\b(.*)/);
|
|
991
999
|
if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
|
|
992
1000
|
}
|
|
993
|
-
const scoreMatch =
|
|
994
|
-
const tunnelMatch =
|
|
995
|
-
const complete = /VERDICT:COMPLETE/.test(
|
|
1001
|
+
const scoreMatch = cleanStdout.match(/SCORE:(\d+)\/(\d+)/);
|
|
1002
|
+
const tunnelMatch = cleanStdout.match(/TUNNEL_URL:(https:\/\/[^\s]+)/);
|
|
1003
|
+
const complete = /VERDICT:COMPLETE/.test(cleanStdout);
|
|
996
1004
|
|
|
997
1005
|
const missing = [];
|
|
998
1006
|
if (!complete) {
|
|
@@ -1001,6 +1009,12 @@ fi
|
|
|
1001
1009
|
}
|
|
1002
1010
|
}
|
|
1003
1011
|
|
|
1012
|
+
// If checks is empty but score was found, parsing failed — return raw output for debugging
|
|
1013
|
+
const parseWarning =
|
|
1014
|
+
Object.keys(checks).length === 0 && scoreMatch
|
|
1015
|
+
? 'Check output parsing failed — individual check results could not be extracted. See rawOutput for details.'
|
|
1016
|
+
: undefined;
|
|
1017
|
+
|
|
1004
1018
|
return {
|
|
1005
1019
|
ok: true,
|
|
1006
1020
|
complete,
|
|
@@ -1009,6 +1023,8 @@ fi
|
|
|
1009
1023
|
score: scoreMatch ? { pass: parseInt(scoreMatch[1], 10), total: parseInt(scoreMatch[2], 10) } : null,
|
|
1010
1024
|
publicUrl: tunnelMatch ? tunnelMatch[1] : undefined,
|
|
1011
1025
|
missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
|
|
1026
|
+
parseWarning,
|
|
1027
|
+
rawOutput: parseWarning ? stdout.trim() : undefined,
|
|
1012
1028
|
nextStep: !complete
|
|
1013
1029
|
? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
|
|
1014
1030
|
? 'expose_via_devbridge'
|
|
@@ -1016,7 +1032,9 @@ fi
|
|
|
1016
1032
|
? 'configure_nginx'
|
|
1017
1033
|
: missing.includes('qr_code')
|
|
1018
1034
|
? 'generate_qr_code'
|
|
1019
|
-
:
|
|
1035
|
+
: parseWarning
|
|
1036
|
+
? 'review_raw_output'
|
|
1037
|
+
: 'review_checks'
|
|
1020
1038
|
: 'complete',
|
|
1021
1039
|
};
|
|
1022
1040
|
}
|
|
@@ -702,9 +702,22 @@ function uninstallCodex() {
|
|
|
702
702
|
}
|
|
703
703
|
|
|
704
704
|
function codexStatus() {
|
|
705
|
-
|
|
705
|
+
// Parse the structured output instead of substring-matching the table: the
|
|
706
|
+
// plugin's SOURCE path (…/plugins/huaweicloud-devkit) also contains the name,
|
|
707
|
+
// so a naive includes() reports "installed" even when the status column says
|
|
708
|
+
// "not installed".
|
|
709
|
+
const r = spawnSync('codex plugin list --json', [], { shell: true, windowsHide: true, stdio: 'pipe' });
|
|
706
710
|
const out = r.stdout ? r.stdout.toString() : '';
|
|
707
|
-
|
|
711
|
+
const name = getCodexPluginName();
|
|
712
|
+
try {
|
|
713
|
+
const data = JSON.parse(out);
|
|
714
|
+
const installed = Array.isArray(data?.installed) ? data.installed : [];
|
|
715
|
+
return installed.some(
|
|
716
|
+
(p) => p && (p.name === name || (typeof p.pluginId === 'string' && p.pluginId.startsWith(`${name}@`))),
|
|
717
|
+
);
|
|
718
|
+
} catch {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
708
721
|
}
|
|
709
722
|
|
|
710
723
|
async function installOpenCode() {
|
|
@@ -2417,12 +2430,14 @@ function officeaceStatus() {
|
|
|
2417
2430
|
|
|
2418
2431
|
// ── Hermes Agent ──
|
|
2419
2432
|
|
|
2433
|
+
// Home directory resolution precedence:
|
|
2434
|
+
// 1. HERMES_HOME env var — explicit override, highest priority
|
|
2435
|
+
// 2. homedir() fallback — respects USERPROFILE (Windows) / HOME (Unix)
|
|
2436
|
+
// Default: ~/.hermes (i.e., join(homedir(), '.hermes'))
|
|
2437
|
+
// No --home CLI option exists; home redirection is via env vars only.
|
|
2438
|
+
// This matches the pattern used by other agents (ATOMCODE_HOME, DSH_HOME, etc.)
|
|
2420
2439
|
function hermesHomeDir() {
|
|
2421
2440
|
if (process.env.HERMES_HOME) return process.env.HERMES_HOME;
|
|
2422
|
-
// Hermes on Windows stores under LOCALAPPDATA, not ~/.hermes
|
|
2423
|
-
if (platform() === 'win32' && process.env.LOCALAPPDATA) {
|
|
2424
|
-
return join(process.env.LOCALAPPDATA, 'hermes');
|
|
2425
|
-
}
|
|
2426
2441
|
return join(homedir(), '.hermes');
|
|
2427
2442
|
}
|
|
2428
2443
|
|
|
@@ -7,7 +7,7 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
|
|
8
8
|
import { evaluateArtifacts, evaluateCommandRisk, evaluateDeployPlan } from './risk-rule-engine.mjs';
|
|
9
9
|
import { classifyTextCommand, redactSecrets } from './safety-policy.mjs';
|
|
10
|
-
import { planHcloudCommand, runHcloud, consumeApprovalToken } from './hcloud-cli.mjs';
|
|
10
|
+
import { planHcloudCommand, runHcloud, consumeApprovalToken, hashArgs } from './hcloud-cli.mjs';
|
|
11
11
|
import { searchMarketplace } from './search-market.mjs';
|
|
12
12
|
import { getServiceIcon } from './icon-library.mjs';
|
|
13
13
|
import { detectFramework } from './detect-framework.mjs';
|
|
@@ -745,7 +745,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
745
745
|
{
|
|
746
746
|
name: 'huaweicloud_sandbox_connect',
|
|
747
747
|
description:
|
|
748
|
-
'Connect to a sandbox via hdkitservice. One user one instance - reuses existing sandbox if available, otherwise creates a new one. Returns session_id, dev_stage_id, connection_id, and
|
|
748
|
+
'Connect to a sandbox via hdkitservice. One user one instance - reuses existing sandbox if available, otherwise creates a new one. Returns session_id, dev_stage_id, connection_id, connection_address, and expiresAt (STS credential expiry).',
|
|
749
749
|
inputSchema: {
|
|
750
750
|
type: 'object',
|
|
751
751
|
properties: {
|
|
@@ -961,9 +961,23 @@ function readImportFile() {
|
|
|
961
961
|
region: String(data.region || ''),
|
|
962
962
|
};
|
|
963
963
|
} catch {
|
|
964
|
+
// Malformed/undecodable import file is un-replayable — wipe it. A VALID
|
|
965
|
+
// file is kept so a rejected persist can be replayed (see #502).
|
|
966
|
+
try {
|
|
967
|
+
rmSync(path, { force: true });
|
|
968
|
+
} catch {
|
|
969
|
+
// ignore
|
|
970
|
+
}
|
|
964
971
|
return null;
|
|
965
|
-
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function clearImportFile() {
|
|
976
|
+
const path = join(dirname(globalCredentialsPath()), 'creds-import.json');
|
|
977
|
+
try {
|
|
966
978
|
rmSync(path, { force: true });
|
|
979
|
+
} catch {
|
|
980
|
+
// best-effort: an absent or locked file is not an error
|
|
967
981
|
}
|
|
968
982
|
}
|
|
969
983
|
|
|
@@ -996,7 +1010,7 @@ function persistCredentials(ak, sk, securityToken, region) {
|
|
|
996
1010
|
writeLastSync({ kooCliProfile: profile, s1Fingerprint: fingerprint(ak, sk) });
|
|
997
1011
|
}
|
|
998
1012
|
return {
|
|
999
|
-
status: 'ok',
|
|
1013
|
+
status: obs.ok && hcloud.ok ? 'ok' : 'partial',
|
|
1000
1014
|
scope: 'persist',
|
|
1001
1015
|
backedUp: Boolean(before),
|
|
1002
1016
|
obs: obs.ok ? { configured: true } : { configured: false, error: obs.error },
|
|
@@ -1124,10 +1138,14 @@ export async function callTool(name, rawArgs = {}) {
|
|
|
1124
1138
|
let securityToken = args.securityToken || '';
|
|
1125
1139
|
let region = args.region || '';
|
|
1126
1140
|
const sourceChannel = args.mode || 'memory';
|
|
1141
|
+
let importedFromFile = false;
|
|
1127
1142
|
|
|
1128
1143
|
if (sourceChannel === 'import' && (!ak || !sk)) {
|
|
1129
1144
|
const imported = readImportFile();
|
|
1130
|
-
if (imported)
|
|
1145
|
+
if (imported) {
|
|
1146
|
+
({ ak, sk, securityToken, region } = imported);
|
|
1147
|
+
importedFromFile = true;
|
|
1148
|
+
}
|
|
1131
1149
|
}
|
|
1132
1150
|
if (sourceChannel === 'mcp-config' && (!ak || !sk)) {
|
|
1133
1151
|
const cc = readCodeArtsCredentials();
|
|
@@ -1143,9 +1161,19 @@ export async function callTool(name, rawArgs = {}) {
|
|
|
1143
1161
|
throw new Error('ak and sk are required (or provide creds-import.json for mode=import).');
|
|
1144
1162
|
}
|
|
1145
1163
|
|
|
1164
|
+
if (action === 'persist' && !String(region || '').trim()) {
|
|
1165
|
+
return {
|
|
1166
|
+
status: 'error',
|
|
1167
|
+
scope: 'invalid_region',
|
|
1168
|
+
error:
|
|
1169
|
+
'region is required to persist credentials. Pass --region, or include "region" in creds-import.json (mode=import).',
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1146
1173
|
if (action === 'temporary') {
|
|
1147
1174
|
setRuntimeCredentials(ak, sk, securityToken || undefined, region);
|
|
1148
1175
|
refreshUserHashAfterAuthChange();
|
|
1176
|
+
if (importedFromFile) clearImportFile();
|
|
1149
1177
|
return {
|
|
1150
1178
|
status: 'ok',
|
|
1151
1179
|
scope: 'temporary',
|
|
@@ -1166,6 +1194,7 @@ export async function callTool(name, rawArgs = {}) {
|
|
|
1166
1194
|
newRegion: region,
|
|
1167
1195
|
oldFingerprint: fingerprint(prev.ak, prev.sk),
|
|
1168
1196
|
newFingerprint: fingerprint(ak, sk),
|
|
1197
|
+
fromImport: importedFromFile,
|
|
1169
1198
|
});
|
|
1170
1199
|
return {
|
|
1171
1200
|
status: 'needs_confirmation',
|
|
@@ -1178,6 +1207,10 @@ export async function callTool(name, rawArgs = {}) {
|
|
|
1178
1207
|
}
|
|
1179
1208
|
|
|
1180
1209
|
const persisted = persistCredentials(ak, sk, securityToken, region);
|
|
1210
|
+
// Clear the import file for non-replayable outcomes (success, or an
|
|
1211
|
+
// unfixable rejection such as STS R3). Keep it only for a retryable
|
|
1212
|
+
// 'partial' (S1 written but a mirror failed).
|
|
1213
|
+
if (importedFromFile && persisted.status !== 'partial') clearImportFile();
|
|
1181
1214
|
refreshUserHashAfterAuthChange();
|
|
1182
1215
|
return persisted;
|
|
1183
1216
|
}
|
|
@@ -1189,6 +1222,7 @@ export async function callTool(name, rawArgs = {}) {
|
|
|
1189
1222
|
return { status: 'ok', outcome: 'aborted', message: '保持 S1 现有账号,未覆盖。' };
|
|
1190
1223
|
}
|
|
1191
1224
|
const confirmed = persistCredentials(pending.newAk, pending.newSk, pending.newSecurityToken, pending.newRegion);
|
|
1225
|
+
if (pending.fromImport && confirmed.status !== 'partial') clearImportFile();
|
|
1192
1226
|
refreshUserHashAfterAuthChange();
|
|
1193
1227
|
return confirmed;
|
|
1194
1228
|
}
|
|
@@ -1674,15 +1708,15 @@ async function runApprovedCommand(args = {}) {
|
|
|
1674
1708
|
throw new Error('approvedByUser must be true after explicit user approval for this exact command.');
|
|
1675
1709
|
}
|
|
1676
1710
|
const token = String(args.approvalToken || '');
|
|
1677
|
-
const
|
|
1678
|
-
if (!
|
|
1711
|
+
const stored = consumeApprovalToken(token);
|
|
1712
|
+
if (!stored) {
|
|
1679
1713
|
throw new Error('Invalid or expired approval token. Please re-plan the command.');
|
|
1680
1714
|
}
|
|
1681
1715
|
const providedArgs = Array.isArray(args.args) ? args.args.map(String) : [];
|
|
1682
|
-
if (
|
|
1683
|
-
const redactedStored =
|
|
1684
|
-
const redactedProvided = redactSecrets(providedArgs);
|
|
1685
|
-
if (
|
|
1716
|
+
if (hashArgs(providedArgs) !== stored.argsHash) {
|
|
1717
|
+
const redactedStored = JSON.stringify(stored.argsRedacted);
|
|
1718
|
+
const redactedProvided = JSON.stringify(redactSecrets(providedArgs));
|
|
1719
|
+
if (redactedStored !== redactedProvided) {
|
|
1686
1720
|
throw new Error(
|
|
1687
1721
|
'Provided args do not match the approved plan. Use the exact args from the plan. ' +
|
|
1688
1722
|
'If the plan shows <redacted> for passwords or secrets, replace <redacted> with the actual values in approvedCommand.',
|