huaweicloud-devkit 1.1.2-next.4 → 1.1.2-next.6
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 +7 -1
- package/README.zh-CN.md +7 -1
- package/integrations/dsh/hook-plugin.mjs +11 -6
- package/integrations/opencode/hooks/skill-tracker.js +10 -9
- 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/skills/huawei-billing/SKILL.md +15 -14
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +2 -0
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +6 -2
- package/plugins/huaweicloud-core/skills/huawei-iac/references/resource-catalog.md +19 -12
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +8 -7
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +8 -0
- package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +16 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +13 -12
- package/plugins/huaweicloud-core/src/auth/credential-validator.mjs +163 -0
- package/plugins/huaweicloud-core/src/auth/project-id.mjs +68 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +6 -1
- package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +7 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +46 -8
- package/plugins/huaweicloud-core/src/telemetry/agent-detect.mjs +2 -2
- package/plugins/huaweicloud-core/src/telemetry/agent-registry.mjs +9 -1
- package/plugins/huaweicloud-core/src/telemetry/telemetry.mjs +41 -5
- package/plugins/huaweicloud-core/src/tools.mjs +72 -8
package/README.md
CHANGED
|
@@ -22,13 +22,19 @@ Supports OpenCode, Codex, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), Off
|
|
|
22
22
|
> ```
|
|
23
23
|
>
|
|
24
24
|
> Restore the default registry: `npm config delete registry`
|
|
25
|
+
>
|
|
26
|
+
> **Mirror lag**: npm mirrors (npmmirror, mirrors.huaweicloud.com) may lag behind the official registry for hours after a new release. If install fails with `ETARGET` or you get an older version, install via the official registry instead:
|
|
27
|
+
>
|
|
28
|
+
> ```bash
|
|
29
|
+
> npx --yes --registry=https://registry.npmjs.org huaweicloud-devkit install --target <target>
|
|
30
|
+
> ```
|
|
25
31
|
|
|
26
32
|
## Quick Start
|
|
27
33
|
|
|
28
34
|
> If `--target` is omitted, the installer auto-detects agents on your machine. When multiple agents are detected, **all of them** will be installed. Specify `--target` to control which agent receives the install.
|
|
29
35
|
|
|
30
36
|
```bash
|
|
31
|
-
npx --yes huaweicloud-devkit version # print
|
|
37
|
+
npx --yes huaweicloud-devkit version # print CLI version and installed plugin versions per agent
|
|
32
38
|
npx --yes huaweicloud-devkit uninstall --target all --clean-global # also remove KooCLI + OBS config
|
|
33
39
|
```
|
|
34
40
|
|
package/README.zh-CN.md
CHANGED
|
@@ -22,13 +22,19 @@
|
|
|
22
22
|
> ```
|
|
23
23
|
>
|
|
24
24
|
> 恢复默认镜像:`npm config delete registry`
|
|
25
|
+
>
|
|
26
|
+
> **镜像滞后**:npm 镜像(npmmirror、mirrors.huaweicloud.com)在新版本发布后可能滞后官方源数小时。若安装报 `ETARGET` 或拿到旧版本,改用官方源安装:
|
|
27
|
+
>
|
|
28
|
+
> ```bash
|
|
29
|
+
> npx --yes --registry=https://registry.npmjs.org huaweicloud-devkit install --target <target>
|
|
30
|
+
> ```
|
|
25
31
|
|
|
26
32
|
## 快速开始
|
|
27
33
|
|
|
28
34
|
> 省略 `--target` 时,安装器会自动检测机器上的 agent,检测到多个时**全部安装**。建议始终指定 `--target` 以明确安装目标。
|
|
29
35
|
|
|
30
36
|
```bash
|
|
31
|
-
npx --yes huaweicloud-devkit version #
|
|
37
|
+
npx --yes huaweicloud-devkit version # 查看 CLI 版本和各 agent 已安装的插件版本
|
|
32
38
|
npx --yes huaweicloud-devkit uninstall --target all --clean-global # 一并删除 KooCLI 与 OBS 配置
|
|
33
39
|
```
|
|
34
40
|
|
|
@@ -65,18 +65,23 @@ function isHuaweiCloudSkill(name) {
|
|
|
65
65
|
// hcloud command classification (mirrors skill-tracker.js)
|
|
66
66
|
// ══════════════════════════════════════════════════════════════════
|
|
67
67
|
|
|
68
|
-
const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(
|
|
68
|
+
const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+([^\s;&|"<>]+(?:\s+[^\s;&|"<>]+){0,3})/i;
|
|
69
69
|
const READ_VERBS = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i;
|
|
70
70
|
const WRITE_VERBS =
|
|
71
71
|
/\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*/i;
|
|
72
72
|
|
|
73
|
-
function classifyHcloud(text) {
|
|
73
|
+
export function classifyHcloud(text) {
|
|
74
74
|
const m = HCLOUD_RE.exec(text);
|
|
75
75
|
if (!m) return null;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
76
|
+
const raw = m[1].trim();
|
|
77
|
+
if (!raw) return null;
|
|
78
|
+
const cmdTokens = [];
|
|
79
|
+
for (const t of raw.split(/\s+/)) {
|
|
80
|
+
if (t.startsWith('--')) break;
|
|
81
|
+
cmdTokens.push(t);
|
|
82
|
+
}
|
|
83
|
+
if (cmdTokens.length === 0) return null;
|
|
84
|
+
const cmd = cmdTokens.join(' ');
|
|
80
85
|
if (READ_VERBS.test(cmd)) return { key: 'cli:read', value: `hcloud ${cmd}`, capability: 'cli' };
|
|
81
86
|
if (WRITE_VERBS.test(cmd)) return { key: 'cli:write', value: `hcloud ${cmd}`, capability: 'cli' };
|
|
82
87
|
return { key: 'cli:invoke', value: `hcloud ${cmd}`, capability: 'cli' };
|
|
@@ -28,7 +28,7 @@ function writeEvent(key, value, extra = {}) {
|
|
|
28
28
|
|
|
29
29
|
// ── CLI command classification ────────────────────────────────
|
|
30
30
|
|
|
31
|
-
const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(
|
|
31
|
+
const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+([^\s;&|"<>]+(?:\s+[^\s;&|"<>]+){0,3})/i;
|
|
32
32
|
const READ_VERBS = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i;
|
|
33
33
|
const WRITE_VERBS = new RegExp(
|
|
34
34
|
'\\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|' +
|
|
@@ -42,14 +42,15 @@ const WRITE_VERBS = new RegExp(
|
|
|
42
42
|
function classifyHcloud(text) {
|
|
43
43
|
const m = HCLOUD_RE.exec(text);
|
|
44
44
|
if (!m) return null;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
if (
|
|
45
|
+
const raw = m[1].trim();
|
|
46
|
+
if (!raw) return null;
|
|
47
|
+
const cmdTokens = [];
|
|
48
|
+
for (const t of raw.split(/\s+/)) {
|
|
49
|
+
if (t.startsWith('--')) break;
|
|
50
|
+
cmdTokens.push(t);
|
|
51
|
+
}
|
|
52
|
+
if (cmdTokens.length === 0) return null;
|
|
53
|
+
const cmd = cmdTokens.join(' ');
|
|
53
54
|
if (READ_VERBS.test(cmd)) return { key: 'cli:read', value: `hcloud ${cmd}` };
|
|
54
55
|
if (WRITE_VERBS.test(cmd)) return { key: 'cli:write', value: `hcloud ${cmd}` };
|
|
55
56
|
return { key: 'cli:invoke', value: `hcloud ${cmd}` };
|
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.2-next.
|
|
4
|
+
"version": "1.1.2-next.6",
|
|
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.2-next.
|
|
20
|
+
"version": "1.1.2-next.6",
|
|
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.2-next.
|
|
23
|
+
"version": "1.1.2-next.6",
|
|
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.2-next.
|
|
20
|
+
"version": "1.1.2-next.6",
|
|
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.2-next.
|
|
3
|
+
"version": "1.1.2-next.6",
|
|
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.2-next.
|
|
3
|
+
"version": "1.1.2-next.6",
|
|
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.2-next.
|
|
5
|
+
"version": "1.1.2-next.6",
|
|
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,23 +16,24 @@ Domain expertise for billing queries (BSS). Covers cost tracking, bill details,
|
|
|
16
16
|
|
|
17
17
|
## Critical Warnings
|
|
18
18
|
|
|
19
|
-
| Trap
|
|
20
|
-
|
|
|
21
|
-
| Bills delayed ~24h
|
|
22
|
-
| BSS Admin role needed
|
|
23
|
-
| Currency conversion varies
|
|
24
|
-
| Region fixed to cn-north-1
|
|
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
|
+
| Global service needs domain-id | BSS is a global service: AK/SK mode requires `--cli-domain-id`. If unknown, run `hcloud STS GetCallerIdentity --cli-region=<region>` and use `account_id` (see `huaweicloud-cli-and-auth`). |
|
|
25
26
|
|
|
26
27
|
## Common Workflows
|
|
27
28
|
|
|
28
|
-
| Task | Operation
|
|
29
|
-
| -------------------- |
|
|
30
|
-
| List costs | `ListCosts --cli-region=cn-north-1 --project_id=<p>`
|
|
31
|
-
| List customer bills | `ListCustomerBillsFeeRecords --cli-region=cn-north-1 --project_id=<p>`
|
|
32
|
-
| List resource usage | `ListResourceUsage --cli-region=cn-north-1 --project_id=<p>`
|
|
33
|
-
| List sub-customers | `ListConsumeSubCustomers --cli-region=cn-north-1 --project_id=<p>`
|
|
34
|
-
| Show account balance | `ShowCustomerAccountBalances --cli-region=cn-north-1 --
|
|
35
|
-
| List conversions | `ListConversions --cli-region=cn-north-1 --project_id=<p>`
|
|
29
|
+
| Task | Operation |
|
|
30
|
+
| -------------------- | --------------------------------------------------------------------------------- |
|
|
31
|
+
| List costs | `ListCosts --cli-region=cn-north-1 --project_id=<p>` |
|
|
32
|
+
| List customer bills | `ListCustomerBillsFeeRecords --cli-region=cn-north-1 --project_id=<p>` |
|
|
33
|
+
| List resource usage | `ListResourceUsage --cli-region=cn-north-1 --project_id=<p>` |
|
|
34
|
+
| List sub-customers | `ListConsumeSubCustomers --cli-region=cn-north-1 --project_id=<p>` |
|
|
35
|
+
| Show account balance | `ShowCustomerAccountBalances --cli-region=cn-north-1 --cli-domain-id=<domain_id>` |
|
|
36
|
+
| List conversions | `ListConversions --cli-region=cn-north-1 --project_id=<p>` |
|
|
36
37
|
|
|
37
38
|
Discover exact parameters with `--help` before executing any command. All BSS operations are read-only.
|
|
38
39
|
|
|
@@ -69,4 +69,6 @@ hcloud IAM AssociateAgencyWithDomainPermission --agency_id=<id> --domain_id=<id>
|
|
|
69
69
|
hcloud FunctionGraph CreateFunction --func_vpc.vpc_id=<vpc> --func_vpc.subnet_id=<subnet> --app_xrole=<name> ...
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
> IAM is a global service in KooCLI. If `CreateAgency` / `AssociateAgencyWithDomainPermission` fail with `缺少必填参数 cli-domain-id`, prefer a concrete `--cli-region` (e.g. `cn-north-4`), or discover the domain-id via `hcloud STS GetCallerIdentity --cli-region=<region>` (see `huaweicloud-cli-and-auth`).
|
|
73
|
+
|
|
72
74
|
| QuotaExceeded | Max 10 functions per project per region |
|
|
@@ -15,6 +15,7 @@ Always run `hcloud <Service> <Operation> --help` before constructing commands to
|
|
|
15
15
|
- This skill orchestrates the **purchase lifecycle of ALL supported resources**: proposal → pricing → provisioning → destroy, across every service in the plugin catalog.
|
|
16
16
|
- **Web app deployment keeps sandbox as the first choice** (see `huaweicloud-core` Deployment Target Options). Route here only when the user wants billed, long-term, or multi-resource cloud infrastructure, or explicitly asks for an architecture + cost proposal.
|
|
17
17
|
- Single-resource tasks go directly to the service skill. This skill adds value for **multi-resource orchestration, mandatory cost gating, and ordered destroy**.
|
|
18
|
+
- **A user-specified hosting target (e.g. "部署到 ECS") does NOT exempt the flow**: stages 1-5 (code inspection, cost estimate, balance check, risk warnings) still run in full - the user's target choice only narrows stage 2/3 options, it never skips the gates.
|
|
18
19
|
|
|
19
20
|
## Workflow (strict order)
|
|
20
21
|
|
|
@@ -47,7 +48,7 @@ Preconditions - all required:
|
|
|
47
48
|
3. The user explicitly names the target deployment. Ambiguous "delete it" → list deployments and ask. Never guess.
|
|
48
49
|
4. Every delete command still goes through plan → approve → run.
|
|
49
50
|
|
|
50
|
-
Execution: show the to-be-deleted list for final confirmation → delete in **reverse creation order** → a "resource not found" error counts as already deleted, continue → mark `destroyed`. OBS:
|
|
51
|
+
Execution: show the to-be-deleted list for final confirmation → delete in **reverse creation order** → a "resource not found" error counts as already deleted, continue → mark `destroyed`. OBS: recursive sweeps (`OBS rm obs://<bucket>/ -r -f`) are DENIED by the safety rule `hwc-destructive-delete-force` even after manifest approval - do not plan them. Instead list objects (`hcloud OBS ls obs://<bucket>/ -s`) and delete per-object (watch for folder-marker objects like `assets/` that outlive the files), then delete the bucket (a non-empty bucket fails with 409 `BucketNotEmpty`). CDN: disable then delete. DNS: delete the record set (only if hosted in Huawei Cloud DNS).
|
|
51
52
|
|
|
52
53
|
## Critical Warnings
|
|
53
54
|
|
|
@@ -63,6 +64,9 @@ Execution: show the to-be-deleted list for final confirmation → delete in **re
|
|
|
63
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 |
|
|
64
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 |
|
|
65
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 |
|
|
66
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 deposit. Free-tier resources (FunctionGraph, small OBS) DO work at 0 balance - the gate applies to order-submission resources only |
|
|
67
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) |
|
|
68
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 |
|
|
@@ -82,7 +86,7 @@ Execution: show the to-be-deleted list for final confirmation → delete in **re
|
|
|
82
86
|
- `huaweicloud_run_readonly_command` → discovery (flavors, images, zones, prices)
|
|
83
87
|
- `huaweicloud_list_regions` / `huaweicloud_get_regional_availability` → region intent
|
|
84
88
|
- `huaweicloud_hook_check_deploy_plan` → risk-check the plan before approval
|
|
85
|
-
- Balance and pricing: `huawei-billing` skill. Balance operation (BSS is pinned to cn-north-1 in KooCLI): `hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1 --cli-domain-id=<domain_id>`. Global services (BSS/IAM) require `--cli-domain-id` per call or in the profile
|
|
89
|
+
- Balance and pricing: `huawei-billing` skill. Balance operation (BSS is pinned to cn-north-1 in KooCLI): `hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1 --cli-domain-id=<domain_id>`. Global services (BSS/IAM) require `--cli-domain-id` per call or in the profile. If unknown, discover it with `hcloud STS GetCallerIdentity --cli-region=<region>` (`account_id` = domain-id); use `tenant_id` from a resource response only as a fallback
|
|
86
90
|
|
|
87
91
|
## Cross-Skill References
|
|
88
92
|
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
Every resource type the orchestration flow can purchase, bind, and destroy. This is the authoritative checklist when composing a deployment plan.
|
|
4
4
|
|
|
5
5
|
> **Operation names below are entry points, not gospel.** Always confirm exact parameters with `hcloud <Service> <Operation> --help` before planning a command. When an operation name is uncertain, run `hcloud <Service> --help` first.
|
|
6
|
+
>
|
|
7
|
+
> **Long parameter values MUST go through a file.** Inline args longer than ~2KB (e.g. `--server.user_data=<base64 cloud-init>`) break MCP JSON parsing and the command silently fails. Write a local JSON file and pass `--cli-jsonInput=<file>` instead (KooCLI-native; the approval gate matches the arg verbatim, so use the same file path in plan and run).
|
|
6
8
|
|
|
7
9
|
## Networking (foundation - create first, destroy last)
|
|
8
10
|
|
|
9
|
-
> **v3 API notes (verified by E2E)**: `CreateSecurityGroup` is account-scoped - NO `vpc_id` param. `CreateSecurityGroupRule` uses `multiport=80` (not `port_range_min/max`). `DeleteSubnet` requires `--vpc_id` in addition to `--subnet_id`. Deletion APIs return empty output on success (async 202/204) - confirm via list, not stdout.
|
|
11
|
+
> **v3 API notes (verified by E2E)**: `CreateSecurityGroup` is account-scoped - NO `vpc_id` param. `CreateSecurityGroupRule` uses `multiport=80` (not `port_range_min/max`). `DeleteSubnet` requires `--vpc_id` in addition to `--subnet_id`. Deletion APIs return empty output on success (async 202/204) - confirm via list, not stdout. `ListPorts` uses array-style params (`--device_id.1=<uuid>`, `--network_id.1=<id>`) - flat `--device_id`/`--network_id` fail with USE_ERROR, and `--limit` is not supported.
|
|
10
12
|
|
|
11
13
|
| Resource | Create | Delete | Skill | Depends on |
|
|
12
14
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -19,18 +21,21 @@ Every resource type the orchestration flow can purchase, bind, and destroy. This
|
|
|
19
21
|
|
|
20
22
|
## Compute
|
|
21
23
|
|
|
22
|
-
| Resource | Create | Delete
|
|
23
|
-
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
24
|
-
| ECS instance | `hcloud ECS CreateServers --server.name=<n> --server.flavorRef=<id> --server.imageRef=<id> --server.vpcid=<id> --server.nics.1.subnet_id=<id> --server.security_groups.1.id=<id> --server.availability_zone=<az> --server.root_volume.volumetype=<type>` (hidden required: `vpcid` + `root_volume.volumetype`; check flavor family vs image arch, e.g. kc1 = ARM needs an ARM image) | `hcloud ECS DeleteServers --servers.1.id=<id> --delete_publicip=true --delete_volume=true`
|
|
25
|
-
|
|
|
24
|
+
| Resource | Create | Delete | Skill | Depends on |
|
|
25
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------- |
|
|
26
|
+
| ECS instance | `hcloud ECS CreateServers --server.name=<n> --server.flavorRef=<id> --server.imageRef=<id> --server.vpcid=<id> --server.nics.1.subnet_id=<id> --server.security_groups.1.id=<id> --server.availability_zone=<az> --server.root_volume.volumetype=<type>` (hidden required: `vpcid` + `root_volume.volumetype`; check flavor family vs image arch, e.g. kc1 = ARM needs an ARM image) | `hcloud ECS DeleteServers --servers.1.id=<id> --delete_publicip=true --delete_volume=true` | huawei-ecs | VPC + Subnet + SG. Set both delete flags true on teardown or EIP/disk leak |
|
|
27
|
+
| ECS keypair | `hcloud ECS NovaCreateKeypair --keypair.name=<n>` (⚠️ `ECS CreateKeypair` does NOT exist; `NovaCreateKeypair` with a `public_key` field imports your own pubkey, otherwise the API generates one) | `hcloud ECS NovaDeleteKeypair --keypair_name=<n>` (param is `--keypair_name`, NOT `--keypair.name`) | huawei-ecs | - |
|
|
28
|
+
| EVS data disk | `hcloud EVS CreateVolume` | `hcloud EVS DeleteVolume` | huawei-ecs | - (attach with `hcloud ECS AttachServerVolume`) |
|
|
29
|
+
|
|
30
|
+
> **Keypair private_key is NOT retrievable via CLI**: the API-created keypair returns `private_key` once, but the tool output redaction masks it - the agent can never read it back. For SSH access, either guide the user to create the keypair in the console, or have them bring their own public key (import via `NovaCreateKeypair` `public_key` field, then reference with `--server.key_name=<n>` at ECS create).
|
|
26
31
|
|
|
27
32
|
## Databases
|
|
28
33
|
|
|
29
|
-
| Resource | Create
|
|
30
|
-
| --------------------------- |
|
|
31
|
-
| RDS (MySQL/PG/SQLServer) |
|
|
32
|
-
| GaussDB | discover with `hcloud GaussDB --help`
|
|
33
|
-
| DDS (MongoDB) / DCS (Redis) |
|
|
34
|
+
| Resource | Create | Delete | Skill | Depends on |
|
|
35
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ------------ |
|
|
36
|
+
| RDS (MySQL/PG/SQLServer) | `hcloud RDS CreateInstance` — `flavor_ref` MUST be the **spec-code string** (e.g. `rds.mysql.n1.large.2`), NOT the UUID from `ListFlavors` (UUID → `DBS.280434 Invalid specification code`); single-instance mode may omit the `ha` block; x1 series are dedicated-ish flavors - prefer n1 for general accounts | discover with `hcloud RDS --help` | huawei-rds | VPC + Subnet |
|
|
37
|
+
| GaussDB | discover with `hcloud GaussDB --help` | discover with `hcloud GaussDB --help` | huawei-gaussdb | VPC + Subnet |
|
|
38
|
+
| DDS (MongoDB) / DCS (Redis) | DCS `CreateInstance`: `available_zones` takes **AZ IDs** (passing AZ codes fails with `DCS.4042`) | DCS delete: `hcloud DCS BatchDeleteInstances --instances.1=<id>` (`DeleteInstances` does NOT exist); DDS discover with `hcloud DDS --help` | huawei-dds-dcs | VPC + Subnet |
|
|
34
39
|
|
|
35
40
|
## Storage / Delivery
|
|
36
41
|
|
|
@@ -90,18 +95,20 @@ hcloud BSS ListOnDemandResourceRatings --project_id=<pid> \
|
|
|
90
95
|
- `usage_measure_id=4` = hour; `usage_value=730` = one month. Response: `amount` (CNY) per product label
|
|
91
96
|
- **ECS codes verified**: service `hws.service.type.ec2`, resource `hws.resource.type.vm`, spec = `<flavor>.linux` (or `.win`); monthly price for kc1.small.1 = 87.60 CNY (official, cn-north-4)
|
|
92
97
|
- **EIP codes NOT usable (verified)**: `hws.service.type.vpc` + `19_bgp` fails with `CBC.6074` - use official website prices as static estimates and label them 估算
|
|
98
|
+
- **RDS / DCS codes NOT usable (verified 2026-09-08)**: RDS combos fail with `CBC.6074`, DCS with `CBC.6006`; the metadata discovery path is unusable - `ListServiceTypes` returns 895 entries with no filter param and `ListResourceTypes` has no service filter either. Degrade to official website prices as static estimates and label them 估算
|
|
93
99
|
- **Other services**: codes are per-service and MUST be looked up via BSS metadata APIs (`ListServiceTypes` / `ListResourceTypes` / `ListUsageTypes`) or docs - a wrong combination fails with `CBC.6074`. Never guess
|
|
94
100
|
- **Balance query may be permission-blocked**: `CBC.0151` = IAM user lacks `billing:balance:view` → degrade to "user self-checks balance" and continue (do not block; `Ecs.7000` at order time is recoverable). domain_id 可从报错信息 `iam::<domain_id>:user:<name>` 提取
|
|
95
101
|
|
|
96
102
|
- Account balance: `hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1 --cli-domain-id=<domain_id>`
|
|
97
103
|
- Global services (BSS/IAM/CDN) demand `--cli-domain-id` per call (or in the profile) under AK/SK auth
|
|
98
|
-
- If the domain_id is unknown:
|
|
104
|
+
- If the domain_id is unknown: discover it via `hcloud STS GetCallerIdentity --cli-region=<region>` (`account_id` = domain-id); fall back to `tenant_id` from a resource response or the console
|
|
99
105
|
- Response shape: `account_balances[].amount` (cash account type=1, voucher account type=5), `debt_amount`, `currency`
|
|
100
106
|
- Cost gate rule: no deployment without a per-resource cost estimate AND a balance check. Verified failure mode when skipped: ECS creation dies with `Ecs.7000 Insufficient account balance` at order submission
|
|
107
|
+
- **Quota pre-check (EIP and friends)**: quota errors surface only at RUN stage (`EIP.7905 Quota exceeded` AFTER plan+approval), wasting an approval cycle. Before provisioning, pre-check counts with `hcloud EIP ListPublicips` (⚠️ beware pagination truncation: `--limit=50` on a 59-EIP account showed "50" - the first "50/50 full" verdict was wrong; count precisely, e.g. loop pages or use the console quota page). If quota is exhausted, surface it at the cost gate instead of after approval. Note: after releasing an EIP, quota counting may lag several minutes - `EIP.7905` can persist briefly even under the limit
|
|
101
108
|
|
|
102
109
|
## Auth Bootstrapping (prerequisite for every command above)
|
|
103
110
|
|
|
104
111
|
- Symptom of a broken profile: every hcloud API returns `APIGW.0301 Incorrect IAM authentication information` while OBS/obsutil keeps working - caused by a stale `securityToken` in the profile
|
|
105
112
|
- Fix: `hcloud configure delete --cli-profile=default`, then re-run `npx huaweicloud-devkit auth init` with permanent AK/SK only (no token). `configure set` refuses empty values, so the token cannot be cleared in place
|
|
106
113
|
- project_id is auto-discovered once signing works - no need to configure it manually
|
|
107
|
-
- VPC/OBS create responses expose `tenant_id` (= domain_id) -
|
|
114
|
+
- VPC/OBS create responses expose `tenant_id` (= domain_id) - a fallback when `STS GetCallerIdentity` discovery is unavailable
|
|
@@ -16,13 +16,14 @@ Domain expertise for Huawei Cloud Identity and Access Management (IAM). Covers u
|
|
|
16
16
|
|
|
17
17
|
## Critical Warnings
|
|
18
18
|
|
|
19
|
-
| Trap | Why
|
|
20
|
-
| --------------------------------- |
|
|
21
|
-
| NEVER create IAM users for humans | Use OneAccess (IAM Identity Center) or federated SSO
|
|
22
|
-
| NEVER create long-term AK/SK | Use temporary STS tokens via agencies
|
|
23
|
-
| Wildcard policies dangerous | Effect:Allow + Resource:* = full access. Always scope resources
|
|
24
|
-
| Agency trust is powerful | Agencies let services assume roles. Always add conditions
|
|
25
|
-
| Root account must have MFA | Root AK/SK is all-powerful. Enable MFA immediately
|
|
19
|
+
| Trap | Why |
|
|
20
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| NEVER create IAM users for humans | Use OneAccess (IAM Identity Center) or federated SSO |
|
|
22
|
+
| NEVER create long-term AK/SK | Use temporary STS tokens via agencies |
|
|
23
|
+
| Wildcard policies dangerous | Effect:Allow + Resource:* = full access. Always scope resources |
|
|
24
|
+
| Agency trust is powerful | Agencies let services assume roles. Always add conditions |
|
|
25
|
+
| Root account must have MFA | Root AK/SK is all-powerful. Enable MFA immediately |
|
|
26
|
+
| IAM is a global service in KooCLI | `--cli-region=cn-north-1` routes to the global endpoint and then AK/SK mode needs `--cli-domain-id`. Prefer a concrete region (e.g. `cn-north-4`); if you hit `缺少必填参数 cli-domain-id`, discover it via `hcloud STS GetCallerIdentity --cli-region=<region>` |
|
|
26
27
|
|
|
27
28
|
## Policy Structure
|
|
28
29
|
|
|
@@ -365,6 +365,14 @@ git -c http.sslVerify=false clone <repo-url>
|
|
|
365
365
|
|
|
366
366
|
Then retry the clone. This bypasses SSL verification only for this single clone.
|
|
367
367
|
|
|
368
|
+
**index.html overwritten by the platform landing page**: when a repo is cloned **inside** the sandbox (fallback path, used only when the operator machine has no git), the DevStation portal-template init may asynchronously write a default GitCode/AtomGit landing page over the repo's `index.html`, leaving a blank page after deploy. Symptoms: `git show HEAD:index.html` shows the user's original page but `/workspace/<repo>/index.html` is a Vue SPA shell referencing `cdn-static.gitcode.com`. Recovery:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
cd /workspace/<repo> && git checkout HEAD -- index.html
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Prevention: `sandbox_connect` clones locally and uploads the finished tree by default (`_repoStatus: 'uploaded_from_local'`), which never triggers this race — keep local git available so the fallback in-sandbox clone is not used. The overwrite itself is DevStation platform behavior; report persistent occurrences to the platform team.
|
|
375
|
+
|
|
368
376
|
#### 3b: Install nginx (before project upload)
|
|
369
377
|
|
|
370
378
|
```bash
|
|
@@ -49,7 +49,7 @@ version: 1
|
|
|
49
49
|
## 环境注意
|
|
50
50
|
|
|
51
51
|
- 生产环境:`voucher_status` / `voucher_claim` 无需传 `domain_id`,后端从 IAM 自动解析账号。
|
|
52
|
-
- 测试环境:需传 `domain_id`(华为云账号 ID);未传时返回 `测试环境需提供 domain_id
|
|
52
|
+
- 测试环境:需传 `domain_id`(华为云账号 ID);未传时返回 `测试环境需提供 domain_id`。
|
|
53
53
|
|
|
54
54
|
## 注意事项
|
|
55
55
|
|
|
@@ -137,6 +137,22 @@ Credentials are resolved in this order (highest priority first):
|
|
|
137
137
|
|
|
138
138
|
When switching accounts within the same Agent session, use `huaweicloud_auth_init` to set runtime credentials (overrides all sources for the current MCP process), or `huaweicloud_auth_switch action=persist`, which writes S1 with `configuredBySession: true` so the session-configured account outranks `HW_ACCESS_KEY` / `HW_SECRET_KEY`. Note: running `auth init` clears the configuredBySession flag.
|
|
139
139
|
|
|
140
|
+
## Global Services & domain-id
|
|
141
|
+
|
|
142
|
+
Global services (BSS, and IAM routed via the `cn-north-1` global endpoint) require `--cli-domain-id` under AK/SK auth. When it is missing, KooCLI fails with `[USE_ERROR]...缺少必填参数cli-domain-id`.
|
|
143
|
+
|
|
144
|
+
Do **not** ask the user to run `hcloud configure set --cli-domain-id=...`. Discover the domain-id yourself:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
hcloud STS GetCallerIdentity --cli-region=<region>
|
|
148
|
+
# → account_id IS the domain-id; retry the original call with --cli-domain-id=<account_id>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `<region>` is the profile's current region. **STS is not deployed in `cn-north-1`** — if the profile region is `cn-north-1`, use another region (e.g. `cn-north-4`).
|
|
152
|
+
- Temporary credentials (AK/SK + security token) work too: add `--cli-security-token=<token>`.
|
|
153
|
+
- Prefer `STS GetCallerIdentity` over `IAM KeystoneListAuthDomains` — the latter only works with permanent credentials and outside `cn-north-1`; it fails under temporary credentials.
|
|
154
|
+
- `cli-domain-id` is a KooCLI CLI requirement, not a Huawei Cloud API requirement: the raw `GET /v3/auth/domains` can be signed with just AK/SK.
|
|
155
|
+
|
|
140
156
|
## Preferred Toolkit Tools
|
|
141
157
|
|
|
142
158
|
- `huaweicloud_auth_init`
|
|
@@ -50,18 +50,19 @@ Use evidence before fixes. Do not guess service behavior when request IDs, regio
|
|
|
50
50
|
|
|
51
51
|
## Common Error Codes
|
|
52
52
|
|
|
53
|
-
| Error
|
|
54
|
-
|
|
|
55
|
-
| AuthFailure / 401
|
|
56
|
-
| AccessDenied / 403
|
|
57
|
-
| NoSuchKey / 404
|
|
58
|
-
| QuotaExceeded
|
|
59
|
-
| [USE_ERROR] 不正确的参数
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| FSS.
|
|
63
|
-
|
|
|
64
|
-
|
|
|
53
|
+
| Error | Likely Cause | Fix |
|
|
54
|
+
| --------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| AuthFailure / 401 | AK/SK invalid or expired | Regenerate AK/SK, re-run `npx huaweicloud-devkit auth init` |
|
|
56
|
+
| AccessDenied / 403 | IAM permission missing | Check `huawei-iam` skill, add required policy action |
|
|
57
|
+
| NoSuchKey / 404 | Resource not found | Verify resource ID, region, and project_id |
|
|
58
|
+
| QuotaExceeded | Account limit reached | Request quota increase in console |
|
|
59
|
+
| [USE_ERROR] 不正确的参数 | Wrong param name | Run `--help`, check `--param=value` format and nested prefix |
|
|
60
|
+
| `[USE_ERROR]缺少必填参数 cli-domain-id` | Global service (BSS / IAM global endpoint) under AK/SK | Run `hcloud STS GetCallerIdentity --cli-region=<region>` → `account_id`, retry with `--cli-domain-id=<account_id>` (see `huaweicloud-cli-and-auth`) |
|
|
61
|
+
| Ecs.0005 | Flavor-image mismatch | Check image `__support_*` against flavor virtualization type |
|
|
62
|
+
| FSS.0400 | FunctionGraph latest version error | Strip `:latest` from function URN |
|
|
63
|
+
| FSS.1417 | DEDICATEDGATEWAY missing params | Add instance_id, group_id, protocol, env_name, env_id |
|
|
64
|
+
| APIC.7201 | Missing security_group_id | Add `--security_group_id` param for APIG CreateInstanceV2 |
|
|
65
|
+
| [NETWORK_ERROR] | Transient network failure | Retry with `maxRetries` param or wait and retry |
|
|
65
66
|
|
|
66
67
|
## KooCLI Error Types
|
|
67
68
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { getProxyDispatcher } from '../proxy/proxy-agent.mjs';
|
|
4
|
+
|
|
5
|
+
function iamBaseUrl() {
|
|
6
|
+
return process.env.HW_IAM_ENDPOINT || 'https://iam.myhuaweicloud.com';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sha256Hex(data) {
|
|
10
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hmacSha256(key, data) {
|
|
14
|
+
return crypto.createHmac('sha256', key).update(data).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function urlEncode(str) {
|
|
18
|
+
const hex = (c) => '%' + (c < 16 ? '0' : '') + c.toString(16).toUpperCase();
|
|
19
|
+
const noEscape = new Set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~'.split(''));
|
|
20
|
+
let out = '';
|
|
21
|
+
for (const ch of str) {
|
|
22
|
+
const c = ch.codePointAt(0);
|
|
23
|
+
out += noEscape.has(ch) && c < 0x80 ? ch : c < 0x80 ? hex(c) : encodeURIComponent(ch);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function timestamp() {
|
|
29
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '') + 'Z';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function signIamRequest(path, query, ak, sk, securitytoken) {
|
|
33
|
+
const ts = timestamp();
|
|
34
|
+
const host = new URL(iamBaseUrl()).host;
|
|
35
|
+
|
|
36
|
+
const cqs = Object.entries(query)
|
|
37
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
38
|
+
.map(([k, v]) => `${urlEncode(k)}=${urlEncode(v)}`)
|
|
39
|
+
.join('&');
|
|
40
|
+
|
|
41
|
+
const curi =
|
|
42
|
+
'/' +
|
|
43
|
+
path
|
|
44
|
+
.split('/')
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.map((s) => urlEncode(s))
|
|
47
|
+
.join('/') +
|
|
48
|
+
'/';
|
|
49
|
+
|
|
50
|
+
const signedHeaders = securitytoken ? 'host;x-sdk-date;x-security-token' : 'host;x-sdk-date';
|
|
51
|
+
const canonicalHeaders = securitytoken
|
|
52
|
+
? `host:${host}\nx-sdk-date:${ts}\nx-security-token:${securitytoken}\n`
|
|
53
|
+
: `host:${host}\nx-sdk-date:${ts}\n`;
|
|
54
|
+
|
|
55
|
+
const payloadHash = sha256Hex('');
|
|
56
|
+
const canonicalRequest = ['GET', curi, cqs, canonicalHeaders, signedHeaders, payloadHash].join('\n');
|
|
57
|
+
const stringToSign = `SDK-HMAC-SHA256\n${ts}\n${sha256Hex(canonicalRequest)}`;
|
|
58
|
+
const signature = hmacSha256(sk, stringToSign);
|
|
59
|
+
|
|
60
|
+
const headers = {
|
|
61
|
+
host,
|
|
62
|
+
'x-sdk-date': ts,
|
|
63
|
+
Authorization: `SDK-HMAC-SHA256 Access=${ak}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
64
|
+
};
|
|
65
|
+
if (securitytoken) {
|
|
66
|
+
headers['x-security-token'] = securitytoken;
|
|
67
|
+
}
|
|
68
|
+
return headers;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function projectForRegion(projects, region) {
|
|
72
|
+
if (region) {
|
|
73
|
+
const match = projects.find((p) => p && p.name === region && p.id);
|
|
74
|
+
if (match) return match.id;
|
|
75
|
+
}
|
|
76
|
+
return projects.find((p) => p && p.id)?.id || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate AK/SK by calling IAM KeystoneListProjects (read-only) with
|
|
81
|
+
* SDK-HMAC-SHA256 request signing. A wrong SK produces an invalid signature,
|
|
82
|
+
* which IAM rejects with HTTP 401 before any project data is returned.
|
|
83
|
+
*
|
|
84
|
+
* Returns { valid, projectId, error, warning }:
|
|
85
|
+
* - valid: true when IAM verified the signature (or the rejection is not
|
|
86
|
+
* authentication-related), false when the credentials are unusable.
|
|
87
|
+
* - projectId: first project matching `region`, else the first visible project.
|
|
88
|
+
* - warning: set when credentials passed but project discovery was denied.
|
|
89
|
+
*/
|
|
90
|
+
export async function validateIamCredentials({ ak, sk, securityToken, region, timeoutMs = 15000 } = {}) {
|
|
91
|
+
if (!ak || !sk) {
|
|
92
|
+
return { valid: false, projectId: null, error: 'AK and SK are both required for credential validation.' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const base = iamBaseUrl();
|
|
96
|
+
const path = '/v3/projects';
|
|
97
|
+
const query = region ? { name: region } : {};
|
|
98
|
+
const qs = Object.entries(query)
|
|
99
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
100
|
+
.join('&');
|
|
101
|
+
const url = `${base}${path}${qs ? `?${qs}` : ''}`;
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
|
+
|
|
105
|
+
let resp;
|
|
106
|
+
try {
|
|
107
|
+
const headers = signIamRequest(path, query, ak, sk, securityToken);
|
|
108
|
+
const fetchOpts = { headers, signal: controller.signal };
|
|
109
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
110
|
+
if (dispatcher) {
|
|
111
|
+
fetchOpts.dispatcher = dispatcher;
|
|
112
|
+
const { fetch: undiciFetch } = await import('undici');
|
|
113
|
+
resp = await undiciFetch(url, fetchOpts);
|
|
114
|
+
} else {
|
|
115
|
+
resp = await fetch(url, fetchOpts);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
valid: false,
|
|
120
|
+
projectId: null,
|
|
121
|
+
skipped: true,
|
|
122
|
+
error: `IAM validation request failed (treating credentials as unverified): ${error.message}`,
|
|
123
|
+
};
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const text = await resp.text();
|
|
129
|
+
let data = null;
|
|
130
|
+
try {
|
|
131
|
+
data = JSON.parse(text);
|
|
132
|
+
} catch {}
|
|
133
|
+
|
|
134
|
+
if (resp.status === 200) {
|
|
135
|
+
const projects = Array.isArray(data?.projects) ? data.projects : [];
|
|
136
|
+
return { valid: true, projectId: projectForRegion(projects, region), error: null, warning: null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (resp.status === 401) {
|
|
140
|
+
const msg = data?.error?.message || text.slice(0, 200);
|
|
141
|
+
return {
|
|
142
|
+
valid: false,
|
|
143
|
+
projectId: null,
|
|
144
|
+
error: `IAM rejected the credentials (HTTP 401: ${msg}). The AK/SK is invalid - check the SK for typos or expired security tokens.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (resp.status === 403) {
|
|
149
|
+
return {
|
|
150
|
+
valid: true,
|
|
151
|
+
projectId: null,
|
|
152
|
+
error: null,
|
|
153
|
+
warning: `Credentials signed successfully but project listing was denied (HTTP 403). Continuing without project_id.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
valid: false,
|
|
159
|
+
projectId: null,
|
|
160
|
+
skipped: true,
|
|
161
|
+
error: `Unexpected IAM response (HTTP ${resp.status}): ${text.slice(0, 200)}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
function hcloudCommand(args) {
|
|
5
|
+
const bin = process.env.HCLOUD_BIN || 'hcloud';
|
|
6
|
+
// Test doubles (and exotic setups) may point HCLOUD_BIN at a Node script;
|
|
7
|
+
// those must be launched through node instead of the shell.
|
|
8
|
+
if (/\.(mjs|cjs|js)$/i.test(bin) && existsSync(bin)) {
|
|
9
|
+
return { file: process.execPath, args: [bin, ...args] };
|
|
10
|
+
}
|
|
11
|
+
return { file: bin, args };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function runHcloud(args, timeoutMs = 20000) {
|
|
15
|
+
const { file, args: spawnArgs } = hcloudCommand(args);
|
|
16
|
+
return spawnSync(file, spawnArgs, {
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
stdio: 'pipe',
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the project_id for `region` via IAM KeystoneListProjects using the
|
|
25
|
+
* credentials already stored in the KooCLI profile, then write it back with
|
|
26
|
+
* `hcloud configure set --cli-project-id=<id>`. Read-only discovery + local
|
|
27
|
+
* config write only - no secrets appear in process arguments because the
|
|
28
|
+
* profile carries the credentials.
|
|
29
|
+
*
|
|
30
|
+
* Best-effort by design: any failure returns { ok: false, reason } and never
|
|
31
|
+
* throws, so callers can stay non-fatal.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveAndApplyProjectId({ region, profile } = {}) {
|
|
34
|
+
if (!region) return { ok: false, reason: 'region is required' };
|
|
35
|
+
const profileArgs = profile ? [`--cli-profile=${profile}`] : [];
|
|
36
|
+
try {
|
|
37
|
+
const list = runHcloud([
|
|
38
|
+
'IAM',
|
|
39
|
+
'KeystoneListProjects',
|
|
40
|
+
...profileArgs,
|
|
41
|
+
`--cli-region=${region}`,
|
|
42
|
+
`--name=${region}`,
|
|
43
|
+
]);
|
|
44
|
+
if (list.status !== 0) {
|
|
45
|
+
return { ok: false, reason: `KeystoneListProjects failed (exit ${list.status})` };
|
|
46
|
+
}
|
|
47
|
+
let projects = null;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(String(list.stdout || ''));
|
|
50
|
+
projects = Array.isArray(parsed?.projects) ? parsed.projects : Array.isArray(parsed) ? parsed : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return { ok: false, reason: 'could not parse KeystoneListProjects output' };
|
|
53
|
+
}
|
|
54
|
+
if (!projects || projects.length === 0) {
|
|
55
|
+
return { ok: false, reason: `no projects found for region ${region}` };
|
|
56
|
+
}
|
|
57
|
+
const match = projects.find((p) => p && p.name === region && p.id) || projects.find((p) => p && p.id);
|
|
58
|
+
if (!match) return { ok: false, reason: 'project list contained no usable ids' };
|
|
59
|
+
|
|
60
|
+
const set = runHcloud(['configure', 'set', ...profileArgs, `--cli-project-id=${match.id}`]);
|
|
61
|
+
if (set.status !== 0) {
|
|
62
|
+
return { ok: false, reason: `configure set --cli-project-id failed (exit ${set.status})` };
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, projectId: match.id };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return { ok: false, reason: error.message };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
|
|
3
3
|
import { getAgentRegistrationStatuses } from './agent-registration.mjs';
|
|
4
|
+
import { resolveAndApplyProjectId } from './project-id.mjs';
|
|
4
5
|
import {
|
|
5
6
|
globalCredentialsPath,
|
|
6
7
|
obsConfigPath,
|
|
@@ -98,9 +99,11 @@ export function syncAuth(target = 'all') {
|
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
const project = resolveAndApplyProjectId({ region: credentials.region, profile });
|
|
103
|
+
|
|
101
104
|
writeLastSync({ kooCliProfile: profile, s1Fingerprint: fingerprint(credentials.ak, credentials.sk) });
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
const result = {
|
|
104
107
|
ok: true,
|
|
105
108
|
profile,
|
|
106
109
|
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
@@ -109,4 +112,6 @@ export function syncAuth(target = 'all') {
|
|
|
109
112
|
agents: getAgentRegistrationStatuses(target).agents,
|
|
110
113
|
note: 'OBS credentials were synced from the global credential vault. Agent MCP registration is managed by "npx huaweicloud-devkit install --target <agent>".',
|
|
111
114
|
};
|
|
115
|
+
if (project.ok) result.projectId = project.projectId;
|
|
116
|
+
return result;
|
|
112
117
|
}
|
|
@@ -26,6 +26,13 @@ export async function getProxyDispatcher(targetUrl) {
|
|
|
26
26
|
return cachedDispatcher;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export async function fetchWithProxy(url, options = {}) {
|
|
30
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
31
|
+
if (!dispatcher) return fetch(url, options);
|
|
32
|
+
const { fetch: undiciFetch } = await import('undici');
|
|
33
|
+
return undiciFetch(url, { ...options, dispatcher });
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
export function clearProxyDispatcherCache() {
|
|
30
37
|
cachedDispatcher = undefined;
|
|
31
38
|
cachedDispatcherProxyUrl = null;
|
|
@@ -17,8 +17,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
|
|
19
19
|
import { getAuthStatus, syncAuth } from './auth/service.mjs';
|
|
20
|
+
import { resolveAndApplyProjectId } from './auth/project-id.mjs';
|
|
20
21
|
import { SUPPORTED_AGENT_TARGETS } from './auth/agent-registration.mjs';
|
|
21
|
-
import { fingerprint, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
22
|
+
import { fingerprint, readKooCliProfiles, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
22
23
|
import { redactSecrets } from './safety-policy.mjs';
|
|
23
24
|
import {
|
|
24
25
|
globalCredentialsPath,
|
|
@@ -4311,6 +4312,16 @@ async function cmdAuthInit() {
|
|
|
4311
4312
|
console.log(' 3. 下载凭证文件(内含 AK 和 SK)。');
|
|
4312
4313
|
console.log(' 注意:SK 只在创建密钥时显示一次,请妥善保存该文件。\n');
|
|
4313
4314
|
|
|
4315
|
+
try {
|
|
4316
|
+
const kooCli = readKooCliProfiles();
|
|
4317
|
+
const currentProfile = kooCli.profiles?.find((p) => p.name === kooCli.current);
|
|
4318
|
+
if (currentProfile?.accessKeyId) {
|
|
4319
|
+
console.log(
|
|
4320
|
+
`\x1b[33m检测到 KooCLI 已配置 profile "${currentProfile.name}"(AK 已存储)。SK 为加密存储无法导出,请直接输入或通过环境变量提供凭据。\x1b[0m\n`,
|
|
4321
|
+
);
|
|
4322
|
+
}
|
|
4323
|
+
} catch {}
|
|
4324
|
+
|
|
4314
4325
|
let ak = process.env.HW_ACCESS_KEY || '';
|
|
4315
4326
|
let sk = process.env.HW_SECRET_KEY || '';
|
|
4316
4327
|
let securityToken = process.env.HW_SECURITY_TOKEN || '';
|
|
@@ -4321,7 +4332,12 @@ async function cmdAuthInit() {
|
|
|
4321
4332
|
console.error(
|
|
4322
4333
|
'\x1b[31mNon-interactive session detected. Provide credentials via environment variables instead:\x1b[0m',
|
|
4323
4334
|
);
|
|
4324
|
-
console.error(
|
|
4335
|
+
console.error(
|
|
4336
|
+
' export HW_ACCESS_KEY=<ak> HW_SECRET_KEY=<sk> then re-run "npx huaweicloud-devkit auth init" to continue',
|
|
4337
|
+
);
|
|
4338
|
+
console.error(
|
|
4339
|
+
' (Agents: prefer huaweicloud_auth_switch mode=import — reads ~/.config/huaweicloud/creds-import.json and wipes it, so the SK never enters the conversation.)',
|
|
4340
|
+
);
|
|
4325
4341
|
console.error(' (Or run "npx huaweicloud-devkit auth init" in a real terminal.)');
|
|
4326
4342
|
process.exitCode = 1;
|
|
4327
4343
|
return;
|
|
@@ -4350,6 +4366,18 @@ async function cmdAuthInit() {
|
|
|
4350
4366
|
if (findHcloudBin()) {
|
|
4351
4367
|
const result = configureHcloud({ ak, sk, region });
|
|
4352
4368
|
if (!result.ok) console.log(`KooCLI update failed: ${result.error || result.code}`);
|
|
4369
|
+
else {
|
|
4370
|
+
const proj = resolveAndApplyProjectId({ region });
|
|
4371
|
+
if (proj.ok) {
|
|
4372
|
+
console.log(` Project ID auto-set: ${proj.projectId}`);
|
|
4373
|
+
} else {
|
|
4374
|
+
console.log(
|
|
4375
|
+
`\x1b[33m Project ID not auto-set (${proj.reason}). Sandbox exec may fail with APIGW.0301 until it is configured:\x1b[0m`,
|
|
4376
|
+
);
|
|
4377
|
+
console.log(` hcloud IAM KeystoneListProjects --cli-region=${region} --name=${region}`);
|
|
4378
|
+
console.log(` hcloud configure set --cli-project-id=<project_id>`);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4353
4381
|
} else {
|
|
4354
4382
|
console.log('KooCLI not found. Run "npx huaweicloud-devkit install-hcloud" and then "auth sync".');
|
|
4355
4383
|
}
|
|
@@ -4552,6 +4580,8 @@ function readInstalledVersion(pluginsDir) {
|
|
|
4552
4580
|
}
|
|
4553
4581
|
|
|
4554
4582
|
function cmdVersion() {
|
|
4583
|
+
console.log(`HuaweiCloud DevKit CLI: ${pkgVersion}`);
|
|
4584
|
+
|
|
4555
4585
|
const agents = [
|
|
4556
4586
|
['OpenCode', opencodePluginsDir()],
|
|
4557
4587
|
['Codex Desktop', codexDesktopPluginsDir()],
|
|
@@ -4564,16 +4594,24 @@ function cmdVersion() {
|
|
|
4564
4594
|
['Hermes', hermesPluginsDir()],
|
|
4565
4595
|
['AtomCode', atomcodePluginsDir()],
|
|
4566
4596
|
];
|
|
4567
|
-
|
|
4597
|
+
const installed = [];
|
|
4568
4598
|
for (const [label, dir] of agents) {
|
|
4569
4599
|
const v = dir ? readInstalledVersion(dir) : null;
|
|
4570
4600
|
if (!v) continue;
|
|
4571
|
-
|
|
4572
|
-
found += 1;
|
|
4601
|
+
installed.push([label, v]);
|
|
4573
4602
|
}
|
|
4574
|
-
|
|
4603
|
+
|
|
4604
|
+
if (installed.length === 0) {
|
|
4605
|
+
console.log('');
|
|
4575
4606
|
console.log('No Huawei Cloud DevKit plugin installed. Run `npx huaweicloud-devkit install --target <agent>`.');
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4610
|
+
console.log('\nInstalled agent plugins:');
|
|
4611
|
+
for (const [label, version] of installed) {
|
|
4612
|
+
console.log(`${label}: ${version}`);
|
|
4576
4613
|
}
|
|
4614
|
+
console.log('\nRun `npx huaweicloud-devkit update --target <agent>` to refresh installed agent plugins.');
|
|
4577
4615
|
}
|
|
4578
4616
|
|
|
4579
4617
|
async function main() {
|
|
@@ -4636,13 +4674,13 @@ async function main() {
|
|
|
4636
4674
|
console.log(' install-hcloud Show KooCLI install commands for your OS');
|
|
4637
4675
|
console.log(' auth Manage unified auth: init | sync | status | reconcile');
|
|
4638
4676
|
console.log(' proxy Manage proxy config: init | show | clear');
|
|
4639
|
-
console.log(' version Print installed plugin version per agent');
|
|
4677
|
+
console.log(' version Print CLI version and installed plugin version per agent');
|
|
4640
4678
|
console.log(' help Show this help');
|
|
4641
4679
|
console.log('\nOptions:');
|
|
4642
4680
|
console.log(
|
|
4643
4681
|
' --target Target agent: opencode (default), codex, codearts, codearts-work, workbuddy, dsh, officeace, hermes, openclaw, atomcode, all',
|
|
4644
4682
|
);
|
|
4645
|
-
console.log(' --version Print installed plugin version per agent');
|
|
4683
|
+
console.log(' --version Print CLI version and installed plugin version per agent');
|
|
4646
4684
|
console.log(' --clean-kocli (with: uninstall --target all) also remove KooCLI');
|
|
4647
4685
|
console.log(' --clean-obs (with: uninstall --target all) also remove OBS config');
|
|
4648
4686
|
console.log(' --clean-global (with: uninstall --target all) also remove KooCLI + OBS config');
|
|
@@ -7,7 +7,7 @@ import { AGENTS, matchAgent, detectVersion, installSegment } from './agent-regis
|
|
|
7
7
|
export function detectAgentHarness(clientInfo = {}) {
|
|
8
8
|
if (process.env.AGENT_HARNESS) return process.env.AGENT_HARNESS;
|
|
9
9
|
for (const agent of AGENTS) {
|
|
10
|
-
if (matchAgent(agent)) return agent.id;
|
|
10
|
+
if (matchAgent(agent, clientInfo)) return agent.id;
|
|
11
11
|
}
|
|
12
12
|
return clientInfo.name || null;
|
|
13
13
|
}
|
|
@@ -22,7 +22,7 @@ export function detectAgent(clientInfo = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
for (const agent of AGENTS) {
|
|
25
|
-
if (matchAgent(agent)) {
|
|
25
|
+
if (matchAgent(agent, clientInfo)) {
|
|
26
26
|
return {
|
|
27
27
|
harness: agent.id,
|
|
28
28
|
version: detectVersion(agent.version) || clientInfo.version || '0.0.0',
|
|
@@ -43,6 +43,7 @@ export const AGENTS = [
|
|
|
43
43
|
id: 'codex',
|
|
44
44
|
pathPatterns: null,
|
|
45
45
|
envVars: ['CODEX_SESSION_ID', 'CODEX_CLI_VERSION', 'CODEX_SANDBOX', 'CODEX_THREAD_ID'],
|
|
46
|
+
clientNames: ['codex-mcp-client'],
|
|
46
47
|
version: null,
|
|
47
48
|
},
|
|
48
49
|
{
|
|
@@ -67,6 +68,7 @@ export const AGENTS = [
|
|
|
67
68
|
id: 'officeace',
|
|
68
69
|
pathPatterns: ['/.office-claw/', '/.officeace/'],
|
|
69
70
|
envVars: ['OFFICEACE_SESSION_ID', 'OFFICE_CLAW_CONFIG_ROOT'],
|
|
71
|
+
clientNames: ['office-claw-mcp-connector-probe'],
|
|
70
72
|
version: { type: 'officeace' },
|
|
71
73
|
},
|
|
72
74
|
{
|
|
@@ -85,6 +87,7 @@ export const AGENTS = [
|
|
|
85
87
|
id: 'openclaw',
|
|
86
88
|
pathPatterns: ['/.openclaw/', '/.agents/huaweicloud-plugins/'],
|
|
87
89
|
envVars: ['OPENCLAW_SESSION_ID', 'OPENCLAW_CONFIG_ROOT'],
|
|
90
|
+
clientNames: ['openclaw-bundle-mcp'],
|
|
88
91
|
version: null,
|
|
89
92
|
},
|
|
90
93
|
{
|
|
@@ -161,9 +164,14 @@ export const AGENTS = [
|
|
|
161
164
|
},
|
|
162
165
|
];
|
|
163
166
|
|
|
164
|
-
export function matchAgent(agent) {
|
|
167
|
+
export function matchAgent(agent, clientInfo = {}) {
|
|
165
168
|
if (agent.pathPatterns && agent.pathPatterns.some((p) => selfPath.includes(p))) return true;
|
|
166
169
|
if (agent.envVars && agent.envVars.some((v) => process.env[v])) return true;
|
|
170
|
+
const name = clientInfo.name;
|
|
171
|
+
if (!name) return false;
|
|
172
|
+
const lower = String(name).toLowerCase();
|
|
173
|
+
if (agent.id.toLowerCase() === lower) return true;
|
|
174
|
+
if (agent.clientNames && agent.clientNames.some((n) => n.toLowerCase() === lower)) return true;
|
|
167
175
|
return false;
|
|
168
176
|
}
|
|
169
177
|
|
|
@@ -13,6 +13,8 @@ import { homedir, hostname, type as osType, networkInterfaces, release as osRele
|
|
|
13
13
|
import { createHash, randomUUID } from 'node:crypto';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
|
|
16
|
+
import { fetchWithProxy } from '../proxy/proxy-agent.mjs';
|
|
17
|
+
|
|
16
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
19
|
const __dirname = dirname(__filename);
|
|
18
20
|
const PLUGIN_DIR = join(__dirname, '..', '..');
|
|
@@ -56,6 +58,8 @@ const MAX_QUEUE_SIZE = 500;
|
|
|
56
58
|
const FLUSH_INTERVAL_MS = 60_000;
|
|
57
59
|
const BATCH_SIZE = 100;
|
|
58
60
|
const FETCH_TIMEOUT_MS = 5000;
|
|
61
|
+
const MAX_VALUE_LENGTH = 255;
|
|
62
|
+
const MAX_RETRIES = 3;
|
|
59
63
|
|
|
60
64
|
const DEFAULT_ENDPOINT = 'https://devkit.huaweicloud.com/rest/developer/server/hdkitservice/telemetry/events';
|
|
61
65
|
|
|
@@ -178,10 +182,19 @@ function capabilityFromKey(key) {
|
|
|
178
182
|
return undefined;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
export function sanitizeValue(value) {
|
|
186
|
+
if (typeof value !== 'string') value = value == null ? '' : String(value);
|
|
187
|
+
value = value.replace(/[\r\n\t]+/g, ' ').trim();
|
|
188
|
+
if (value.length > MAX_VALUE_LENGTH) {
|
|
189
|
+
value = value.slice(0, MAX_VALUE_LENGTH - 3) + '...';
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
181
194
|
function buildEvent(raw) {
|
|
182
195
|
const event = {
|
|
183
196
|
key: raw.key,
|
|
184
|
-
value: raw.value,
|
|
197
|
+
value: sanitizeValue(raw.value),
|
|
185
198
|
installId: installId,
|
|
186
199
|
userHash: userHash,
|
|
187
200
|
version: PLUGIN_VERSION,
|
|
@@ -300,6 +313,13 @@ export function cacheUserHash(hash) {
|
|
|
300
313
|
writeTextFile(USER_HASH_PATH, hash);
|
|
301
314
|
}
|
|
302
315
|
|
|
316
|
+
export function clearUserHash() {
|
|
317
|
+
userHash = null;
|
|
318
|
+
try {
|
|
319
|
+
unlinkSync(USER_HASH_PATH);
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
|
|
303
323
|
function flushEvents() {
|
|
304
324
|
if (isFlushing) return;
|
|
305
325
|
if (eventQueue.length === 0) return;
|
|
@@ -315,10 +335,10 @@ function flushEvents() {
|
|
|
315
335
|
const controller = new AbortController();
|
|
316
336
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
317
337
|
|
|
318
|
-
|
|
338
|
+
fetchWithProxy(endpoint, {
|
|
319
339
|
method: 'POST',
|
|
320
340
|
headers: { 'Content-Type': 'application/json' },
|
|
321
|
-
body: JSON.stringify(batch),
|
|
341
|
+
body: JSON.stringify(batch.map(({ _retries, ...rest }) => rest)),
|
|
322
342
|
signal: controller.signal,
|
|
323
343
|
})
|
|
324
344
|
.then((resp) => {
|
|
@@ -330,19 +350,35 @@ function flushEvents() {
|
|
|
330
350
|
if (event.key === 'plugin:install') touchFile(installStampPath());
|
|
331
351
|
if (event.key === 'plugin:first_use') touchFile(firstUseStampPath());
|
|
332
352
|
}
|
|
353
|
+
} else if (resp.status >= 400 && resp.status < 500) {
|
|
354
|
+
debugLog(`POST status=${resp.status} dropping ${batch.length} events (client error)`);
|
|
333
355
|
} else {
|
|
334
|
-
|
|
356
|
+
requeueEvents(batch);
|
|
335
357
|
}
|
|
336
358
|
isFlushing = false;
|
|
337
359
|
})
|
|
338
360
|
.catch((error) => {
|
|
339
361
|
clearTimeout(timer);
|
|
340
362
|
debugLog(`POST FAIL err=${error.message} events=${batch.length}`);
|
|
341
|
-
|
|
363
|
+
requeueEvents(batch);
|
|
342
364
|
isFlushing = false;
|
|
343
365
|
});
|
|
344
366
|
}
|
|
345
367
|
|
|
368
|
+
function requeueEvents(batch) {
|
|
369
|
+
const kept = [];
|
|
370
|
+
for (const event of batch) {
|
|
371
|
+
const retries = event._retries || 0;
|
|
372
|
+
if (retries >= MAX_RETRIES) {
|
|
373
|
+
debugLog(`DROP event key=${event.key} after ${MAX_RETRIES} retries`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
event._retries = retries + 1;
|
|
377
|
+
kept.push(event);
|
|
378
|
+
}
|
|
379
|
+
if (kept.length > 0) eventQueue = [...kept, ...eventQueue];
|
|
380
|
+
}
|
|
381
|
+
|
|
346
382
|
export function initTelemetry({ harness, version }) {
|
|
347
383
|
installId = generateOrRecoverInstallId();
|
|
348
384
|
agentHarness = harness || 'unknown';
|
|
@@ -29,9 +29,11 @@ import {
|
|
|
29
29
|
hdkitCredentials,
|
|
30
30
|
hdkitVoucherStatus,
|
|
31
31
|
hdkitVoucherClaim,
|
|
32
|
+
hdkitGenerateUserHash,
|
|
32
33
|
} from './sandbox/hdkitservice-api.mjs';
|
|
33
34
|
import { getCredentials } from './sandbox/hwlink-api.mjs';
|
|
34
35
|
import { getAuthStatus, syncAuth } from './auth/service.mjs';
|
|
36
|
+
import { validateIamCredentials } from './auth/credential-validator.mjs';
|
|
35
37
|
import {
|
|
36
38
|
readGlobalCredentials,
|
|
37
39
|
writeGlobalCredentials,
|
|
@@ -43,8 +45,9 @@ import {
|
|
|
43
45
|
writeLastSync,
|
|
44
46
|
readCodeArtsCredentials,
|
|
45
47
|
globalCredentialsPath,
|
|
48
|
+
resolveCredentialsWithRuntime,
|
|
46
49
|
} from './auth/credentials.mjs';
|
|
47
|
-
import { trackToolInvoke, trackSkillRetrieve } from './telemetry/telemetry.mjs';
|
|
50
|
+
import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
|
|
48
51
|
import { fingerprint, runHcloudConfigure, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
49
52
|
import {
|
|
50
53
|
getCachedUpdateInfo,
|
|
@@ -771,13 +774,18 @@ export const TOOL_DEFINITIONS = [
|
|
|
771
774
|
{
|
|
772
775
|
name: 'huaweicloud_sandbox_credentials',
|
|
773
776
|
description:
|
|
774
|
-
'Configure temporary AK/SK for a sandbox via hdkitservice.
|
|
777
|
+
'Configure temporary AK/SK for a sandbox via hdkitservice. Validates the current AK/SK against IAM before injecting (invalid SK is rejected here instead of failing later with APIGW.0301 during exec), then injects temporary credentials into the sandbox. The sandbox must be in RUNNING state.',
|
|
775
778
|
inputSchema: {
|
|
776
779
|
type: 'object',
|
|
777
780
|
properties: {
|
|
778
781
|
session_id: { type: 'string', description: 'Session ID from huaweicloud_sandbox_connect' },
|
|
779
782
|
dev_stage_id: { type: 'string', description: 'DevStation environment ID (alternative to session_id)' },
|
|
780
783
|
enable_sts: { type: 'boolean', description: 'Whether to enable STS temporary AK/SK (default: true)' },
|
|
784
|
+
region: {
|
|
785
|
+
type: 'string',
|
|
786
|
+
description:
|
|
787
|
+
'Region used for IAM credential validation and project_id resolution (defaults to the configured region)',
|
|
788
|
+
},
|
|
781
789
|
},
|
|
782
790
|
},
|
|
783
791
|
},
|
|
@@ -997,6 +1005,13 @@ function persistCredentials(ak, sk, securityToken, region) {
|
|
|
997
1005
|
};
|
|
998
1006
|
}
|
|
999
1007
|
|
|
1008
|
+
function refreshUserHashAfterAuthChange({ regenerate = true } = {}) {
|
|
1009
|
+
clearUserHash();
|
|
1010
|
+
if (regenerate) {
|
|
1011
|
+
hdkitGenerateUserHash().catch(() => {});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1000
1015
|
export async function callTool(name, args = {}) {
|
|
1001
1016
|
const toolValue = toolInvokeValue(name, args);
|
|
1002
1017
|
trackToolInvoke(name, toolValue);
|
|
@@ -1055,22 +1070,28 @@ export async function callTool(name, args = {}) {
|
|
|
1055
1070
|
return setupObsConfig(args.profile);
|
|
1056
1071
|
case 'huaweicloud_auth_status':
|
|
1057
1072
|
return getAuthStatus(args.target || 'all');
|
|
1058
|
-
case 'huaweicloud_auth_sync':
|
|
1059
|
-
|
|
1073
|
+
case 'huaweicloud_auth_sync': {
|
|
1074
|
+
const result = syncAuth(args.target || 'all');
|
|
1075
|
+
refreshUserHashAfterAuthChange();
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1060
1078
|
case 'huaweicloud_auth_init':
|
|
1061
1079
|
if (args.clear) {
|
|
1062
1080
|
clearRuntimeCredentials();
|
|
1081
|
+
refreshUserHashAfterAuthChange({ regenerate: false });
|
|
1063
1082
|
return { status: 'cleared', message: 'Runtime credentials cleared. Fallback to env/file.' };
|
|
1064
1083
|
}
|
|
1065
1084
|
if (!args.ak || !args.sk) {
|
|
1066
1085
|
throw new Error('ak and sk are required. Set clear=true to clear runtime credentials.');
|
|
1067
1086
|
}
|
|
1068
1087
|
setRuntimeCredentials(args.ak, args.sk, undefined, args.region);
|
|
1088
|
+
refreshUserHashAfterAuthChange();
|
|
1069
1089
|
return { status: 'ok', message: 'Runtime credentials set for this MCP session.' };
|
|
1070
1090
|
case 'huaweicloud_auth_switch': {
|
|
1071
1091
|
const action = args.action || 'temporary';
|
|
1072
1092
|
if (action === 'clear') {
|
|
1073
1093
|
clearRuntimeCredentials();
|
|
1094
|
+
refreshUserHashAfterAuthChange({ regenerate: false });
|
|
1074
1095
|
return { status: 'cleared', message: 'Runtime credentials cleared. Fallback to env/file/S1.' };
|
|
1075
1096
|
}
|
|
1076
1097
|
|
|
@@ -1100,6 +1121,7 @@ export async function callTool(name, args = {}) {
|
|
|
1100
1121
|
|
|
1101
1122
|
if (action === 'temporary') {
|
|
1102
1123
|
setRuntimeCredentials(ak, sk, securityToken || undefined, region);
|
|
1124
|
+
refreshUserHashAfterAuthChange();
|
|
1103
1125
|
return {
|
|
1104
1126
|
status: 'ok',
|
|
1105
1127
|
scope: 'temporary',
|
|
@@ -1131,7 +1153,9 @@ export async function callTool(name, args = {}) {
|
|
|
1131
1153
|
};
|
|
1132
1154
|
}
|
|
1133
1155
|
|
|
1134
|
-
|
|
1156
|
+
const persisted = persistCredentials(ak, sk, securityToken, region);
|
|
1157
|
+
refreshUserHashAfterAuthChange();
|
|
1158
|
+
return persisted;
|
|
1135
1159
|
}
|
|
1136
1160
|
case 'huaweicloud_auth_confirm': {
|
|
1137
1161
|
const pending = pendingConfirms.get(args.token);
|
|
@@ -1140,7 +1164,9 @@ export async function callTool(name, args = {}) {
|
|
|
1140
1164
|
if (args.decision === 's1') {
|
|
1141
1165
|
return { status: 'ok', outcome: 'aborted', message: '保持 S1 现有账号,未覆盖。' };
|
|
1142
1166
|
}
|
|
1143
|
-
|
|
1167
|
+
const confirmed = persistCredentials(pending.newAk, pending.newSk, pending.newSecurityToken, pending.newRegion);
|
|
1168
|
+
refreshUserHashAfterAuthChange();
|
|
1169
|
+
return confirmed;
|
|
1144
1170
|
}
|
|
1145
1171
|
case 'huaweicloud_sandbox_exec_with_session': {
|
|
1146
1172
|
const sandboxWsId2 = args.workspace_id || getCurrentWorkspaceId();
|
|
@@ -1300,6 +1326,32 @@ export async function callTool(name, args = {}) {
|
|
|
1300
1326
|
}
|
|
1301
1327
|
case 'huaweicloud_sandbox_credentials': {
|
|
1302
1328
|
const devStageId = args.dev_stage_id || getCurrentWorkspaceId();
|
|
1329
|
+
let resolved;
|
|
1330
|
+
try {
|
|
1331
|
+
resolved = resolveCredentialsWithRuntime();
|
|
1332
|
+
} catch {
|
|
1333
|
+
resolved = null;
|
|
1334
|
+
}
|
|
1335
|
+
if (!resolved?.ak || !resolved?.sk) {
|
|
1336
|
+
return {
|
|
1337
|
+
ok: false,
|
|
1338
|
+
error: 'Huawei Cloud credentials are not configured. Nothing was injected into the sandbox.',
|
|
1339
|
+
hint: 'Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY, then retry.',
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
const validation = await validateIamCredentials({
|
|
1343
|
+
ak: resolved.ak,
|
|
1344
|
+
sk: resolved.sk,
|
|
1345
|
+
securityToken: resolved.securityToken,
|
|
1346
|
+
region: args.region || resolved.region,
|
|
1347
|
+
});
|
|
1348
|
+
if (!validation.valid && !validation.skipped) {
|
|
1349
|
+
return {
|
|
1350
|
+
ok: false,
|
|
1351
|
+
error: 'Credential validation failed before injection: ' + validation.error,
|
|
1352
|
+
hint: 'Credentials were NOT injected into the sandbox. Fix AK/SK first: run "npx huaweicloud-devkit auth init" or correct HW_ACCESS_KEY/HW_SECRET_KEY, then retry.',
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1303
1355
|
const credResult = await hdkitCredentials(args.session_id, devStageId, args.enable_sts !== false);
|
|
1304
1356
|
const sandboxWsIdCred = args.dev_stage_id || getCurrentWorkspaceId();
|
|
1305
1357
|
if (sandboxWsIdCred) {
|
|
@@ -1310,6 +1362,7 @@ export async function callTool(name, args = {}) {
|
|
|
1310
1362
|
`export HW_SECRET_KEY='${sk}'`,
|
|
1311
1363
|
securitytoken ? `export HW_SECURITY_TOKEN='${securitytoken}'` : '',
|
|
1312
1364
|
securitytoken ? `export X_HW_SECURITY_TOKEN='${securitytoken}'` : '',
|
|
1365
|
+
validation.projectId ? `export HW_PROJECT_ID='${validation.projectId}'` : '',
|
|
1313
1366
|
]
|
|
1314
1367
|
.filter(Boolean)
|
|
1315
1368
|
.join('\n');
|
|
@@ -1323,7 +1376,14 @@ export async function callTool(name, args = {}) {
|
|
|
1323
1376
|
await execWithSession(sandboxWsIdCred, `source ${credsFile} && echo "CREDS_SOURCED"`, 'root', 15000);
|
|
1324
1377
|
} catch {}
|
|
1325
1378
|
}
|
|
1326
|
-
|
|
1379
|
+
const result = {
|
|
1380
|
+
...credResult,
|
|
1381
|
+
credentialValidation: validation.warning ? 'passed-with-warning' : 'passed',
|
|
1382
|
+
};
|
|
1383
|
+
if (validation.projectId) result.projectId = validation.projectId;
|
|
1384
|
+
if (validation.warning) result.warning = validation.warning;
|
|
1385
|
+
if (validation.skipped) result.warning = validation.error;
|
|
1386
|
+
return result;
|
|
1327
1387
|
}
|
|
1328
1388
|
case 'huaweicloud_voucher_status':
|
|
1329
1389
|
return await hdkitVoucherStatus(args.domain_id);
|
|
@@ -1806,6 +1866,8 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
|
|
|
1806
1866
|
'VPC.0301': 'Bandwidth name is required for PER type EIPs, even though --help marks it optional.',
|
|
1807
1867
|
},
|
|
1808
1868
|
APIGW: {
|
|
1869
|
+
'APIGW.0301':
|
|
1870
|
+
'Incorrect IAM authentication information. The AK/SK is invalid (check the SK for typos), the security token is missing or expired, or the profile lacks project_id. Fix: re-run "npx huaweicloud-devkit auth init" (it auto-sets project_id), or set it manually: hcloud configure set --cli-project-id=<project_id> after finding it via hcloud IAM KeystoneListProjects --cli-region=<region> --name=<region>.',
|
|
1809
1871
|
'APIGW.0802':
|
|
1810
1872
|
'The current IAM user has no permissions in the requested region. Go to IAM console → Users → Permissions → add the target region, or switch to a different region.',
|
|
1811
1873
|
},
|
|
@@ -1836,7 +1898,9 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
|
|
|
1836
1898
|
': API Gateway layer error. ' +
|
|
1837
1899
|
(errorCode === 'APIGW.0802'
|
|
1838
1900
|
? 'IAM user has no region permissions — check IAM console → User → Permissions → add target region.'
|
|
1839
|
-
:
|
|
1901
|
+
: errorCode === 'APIGW.0301'
|
|
1902
|
+
? 'Incorrect IAM authentication information — verify AK/SK (SK typos are the usual cause), security token expiry, and that project_id is configured (auth init auto-sets it).'
|
|
1903
|
+
: 'Verify the API request, region endpoint, and IAM permissions.'),
|
|
1840
1904
|
);
|
|
1841
1905
|
}
|
|
1842
1906
|
if (/region|endpoint|project/i.test(combined)) {
|