huaweicloud-devkit 1.1.2-next.4 → 1.1.2-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 the installed plugin version per agent
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 # 查看各 agent 已安装的插件版本
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+(.+)/i;
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 rest = m[1].trim();
77
- const parts = rest.split(/\s+/).filter((p) => !p.startsWith('--'));
78
- const cmd = parts.join(' ');
79
- if (!cmd) return null;
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+(.+)/i;
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 rest = m[1].trim();
46
- const cmdEnd = rest.search(/\s[|&<>;]/);
47
- const cmdPart = cmdEnd > -1 ? rest.slice(0, cmdEnd) : rest;
48
- const parts = cmdPart
49
- .split(/\s+/)
50
- .filter((p) => !p.startsWith('--') && !/^\d*>(&?\d*|%devnull)/.test(p) && !/^(&\d+)$/.test(p));
51
- const cmd = parts.join(' ');
52
- if (!cmd) return null;
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",
4
+ "version": "1.1.2-next.5",
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.4",
20
+ "version": "1.1.2-next.5",
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.4",
23
+ "version": "1.1.2-next.5",
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.4",
20
+ "version": "1.1.2-next.5",
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.4",
3
+ "version": "1.1.2-next.5",
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.4",
3
+ "version": "1.1.2-next.5",
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.4",
5
+ "version": "1.1.2-next.5",
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.",
@@ -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
- return {
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,6 +17,7 @@ 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
22
  import { fingerprint, resolveManagedProfile } from './auth/reconcile.mjs';
22
23
  import { redactSecrets } from './safety-policy.mjs';
@@ -4350,6 +4351,18 @@ async function cmdAuthInit() {
4350
4351
  if (findHcloudBin()) {
4351
4352
  const result = configureHcloud({ ak, sk, region });
4352
4353
  if (!result.ok) console.log(`KooCLI update failed: ${result.error || result.code}`);
4354
+ else {
4355
+ const proj = resolveAndApplyProjectId({ region });
4356
+ if (proj.ok) {
4357
+ console.log(` Project ID auto-set: ${proj.projectId}`);
4358
+ } else {
4359
+ console.log(
4360
+ `\x1b[33m Project ID not auto-set (${proj.reason}). Sandbox exec may fail with APIGW.0301 until it is configured:\x1b[0m`,
4361
+ );
4362
+ console.log(` hcloud IAM KeystoneListProjects --cli-region=${region} --name=${region}`);
4363
+ console.log(` hcloud configure set --cli-project-id=<project_id>`);
4364
+ }
4365
+ }
4353
4366
  } else {
4354
4367
  console.log('KooCLI not found. Run "npx huaweicloud-devkit install-hcloud" and then "auth sync".');
4355
4368
  }
@@ -4552,6 +4565,8 @@ function readInstalledVersion(pluginsDir) {
4552
4565
  }
4553
4566
 
4554
4567
  function cmdVersion() {
4568
+ console.log(`HuaweiCloud DevKit CLI: ${pkgVersion}`);
4569
+
4555
4570
  const agents = [
4556
4571
  ['OpenCode', opencodePluginsDir()],
4557
4572
  ['Codex Desktop', codexDesktopPluginsDir()],
@@ -4564,16 +4579,24 @@ function cmdVersion() {
4564
4579
  ['Hermes', hermesPluginsDir()],
4565
4580
  ['AtomCode', atomcodePluginsDir()],
4566
4581
  ];
4567
- let found = 0;
4582
+ const installed = [];
4568
4583
  for (const [label, dir] of agents) {
4569
4584
  const v = dir ? readInstalledVersion(dir) : null;
4570
4585
  if (!v) continue;
4571
- console.log(`${label}: ${v}`);
4572
- found += 1;
4586
+ installed.push([label, v]);
4573
4587
  }
4574
- if (found === 0) {
4588
+
4589
+ if (installed.length === 0) {
4590
+ console.log('');
4575
4591
  console.log('No Huawei Cloud DevKit plugin installed. Run `npx huaweicloud-devkit install --target <agent>`.');
4592
+ return;
4593
+ }
4594
+
4595
+ console.log('\nInstalled agent plugins:');
4596
+ for (const [label, version] of installed) {
4597
+ console.log(`${label}: ${version}`);
4576
4598
  }
4599
+ console.log('\nRun `npx huaweicloud-devkit update --target <agent>` to refresh installed agent plugins.');
4577
4600
  }
4578
4601
 
4579
4602
  async function main() {
@@ -4636,13 +4659,13 @@ async function main() {
4636
4659
  console.log(' install-hcloud Show KooCLI install commands for your OS');
4637
4660
  console.log(' auth Manage unified auth: init | sync | status | reconcile');
4638
4661
  console.log(' proxy Manage proxy config: init | show | clear');
4639
- console.log(' version Print installed plugin version per agent');
4662
+ console.log(' version Print CLI version and installed plugin version per agent');
4640
4663
  console.log(' help Show this help');
4641
4664
  console.log('\nOptions:');
4642
4665
  console.log(
4643
4666
  ' --target Target agent: opencode (default), codex, codearts, codearts-work, workbuddy, dsh, officeace, hermes, openclaw, atomcode, all',
4644
4667
  );
4645
- console.log(' --version Print installed plugin version per agent');
4668
+ console.log(' --version Print CLI version and installed plugin version per agent');
4646
4669
  console.log(' --clean-kocli (with: uninstall --target all) also remove KooCLI');
4647
4670
  console.log(' --clean-obs (with: uninstall --target all) also remove OBS config');
4648
4671
  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
- fetch(endpoint, {
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
- eventQueue = [...batch, ...eventQueue];
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
- eventQueue = [...batch, ...eventQueue];
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. Injects temporary credentials into the sandbox. The sandbox must be in RUNNING state.',
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
- return syncAuth(args.target || 'all');
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
- return persistCredentials(ak, sk, securityToken, region);
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
- return persistCredentials(pending.newAk, pending.newSk, pending.newSecurityToken, pending.newRegion);
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
- return credResult;
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
- : 'Verify the API request, region endpoint, and IAM permissions.'),
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)) {