huaweicloud-devkit 1.1.10-next.4 → 1.1.10-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/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.10-next.4",
4
+ "version": "1.1.10-next.6",
5
5
  "kooCliVersion": "7.3.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.10-next.4",
20
+ "version": "1.1.10-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.10-next.4",
23
+ "version": "1.1.10-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.10-next.4",
20
+ "version": "1.1.10-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.10-next.4",
3
+ "version": "1.1.10-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.10-next.4",
3
+ "version": "1.1.10-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.10-next.4",
5
+ "version": "1.1.10-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.",
@@ -85,7 +85,8 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
85
85
  // devkit exchanges it for temporary AK/SK on demand, so no manual setup.
86
86
  scenario = 0;
87
87
  reason = 'token-auth';
88
- message = '已使用 X-Auth-Token 认证,MCP 工具可直接使用,无需配置 AK/SK。';
88
+ message =
89
+ '已使用 X-Auth-Token 认证。管理面与数据面(沙箱部署/exec)都会在需要时用 token 自动换取临时 AK/SK;请确保可访问 IAM 端点。';
89
90
  return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
90
91
  }
91
92
  if (s1Has && !envHasCreds) {
@@ -111,6 +111,18 @@ export function classifyHcloudProbe(result, requiredVersion = getKooCliVersion()
111
111
  };
112
112
  }
113
113
 
114
+ // Probe timed out / was signal-killed: we can't tell whether hcloud is truly
115
+ // broken or just slow on this box — report unknown instead of unavailable so
116
+ // the caller doesn't falsely conclude "CLI not installed" (#P0-2).
117
+ if (result.signal || result.status === null || result.error?.code === 'ETIMEDOUT') {
118
+ return {
119
+ status: 'unknown',
120
+ installed: false,
121
+ ok: false,
122
+ errorCode: 'HCLOUD_UNKNOWN',
123
+ };
124
+ }
125
+
114
126
  return {
115
127
  status: 'unavailable',
116
128
  installed: false,
@@ -0,0 +1,57 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { getKooCliVersion, kooCliDownloadBase } from './koocli-version.mjs';
7
+
8
+ // Programmatic KooCLI (hcloud) install — downloadable from inside the MCP server
9
+ // process via Node (fetch + tar via child_process), so it does NOT depend on the
10
+ // agent's bash tool (which is unavailable in CodeArts with OPENCODE_CONFIG
11
+ // unset — experience-report #3.5). Returns a structured result for the
12
+ // `huaweicloud_install_hcloud` MCP tool.
13
+ export async function installKooCli(options = {}) {
14
+ const os = process.platform;
15
+ const arch = process.arch;
16
+
17
+ // Windows auto-install needs PowerShell specifics — keep it on the CLI path.
18
+ if (os === 'win32') {
19
+ return { ok: false, error: 'Windows 环境请使用 CLI: npx huaweicloud-devkit install-hcloud' };
20
+ }
21
+ if (os !== 'linux' && os !== 'darwin') {
22
+ return { ok: false, error: `不支持的操作系统: ${os}` };
23
+ }
24
+
25
+ const installDir = options.binDir || (os === 'linux' ? join(homedir(), '.local', 'bin') : '/usr/local/bin');
26
+ const pkg =
27
+ os === 'linux' ? (arch === 'arm64' ? 'linux-arm64' : 'linux-amd64') : arch === 'arm64' ? 'mac-arm64' : 'mac-amd64';
28
+ const url = `${kooCliDownloadBase()}/huaweicloud-cli-${pkg}.tar.gz`;
29
+ const binPath = join(installDir, 'hcloud');
30
+
31
+ try {
32
+ mkdirSync(installDir, { recursive: true });
33
+
34
+ const tmpTar = join(installDir, `huaweicloud-cli-${pkg}.tar.gz`);
35
+ const resp = await fetch(url);
36
+ if (!resp.ok) {
37
+ return { ok: false, error: `下载失败 (HTTP ${resp.status}): ${url}` };
38
+ }
39
+ const buf = Buffer.from(await resp.arrayBuffer());
40
+ writeFileSync(tmpTar, buf);
41
+
42
+ const ex = spawnSync('tar', ['-xzf', tmpTar, '-C', installDir], { stdio: 'pipe', timeout: 60000 });
43
+ rmSync(tmpTar, { force: true });
44
+ if (ex.status !== 0) {
45
+ return { ok: false, error: `解压失败 (tar exit ${ex.status}): ${(ex.stderr || '').toString().slice(0, 200)}` };
46
+ }
47
+
48
+ if (!existsSync(binPath)) {
49
+ return { ok: false, error: `hcloud 二进制未生成于 ${binPath}` };
50
+ }
51
+ chmodSync(binPath, 0o755);
52
+
53
+ return { ok: true, binPath, version: getKooCliVersion() };
54
+ } catch (error) {
55
+ return { ok: false, error: error?.message || String(error) };
56
+ }
57
+ }
@@ -1,6 +1,8 @@
1
1
  import crypto from 'node:crypto';
2
2
 
3
- import { resolveCredentialsWithRuntime } from '../auth/credentials.mjs';
3
+ import { resolveCredentialsWithRuntime, resolveXAuthToken } from '../auth/credentials.mjs';
4
+ import { getTokenCredentials } from '../auth/token-exchange.mjs';
5
+ import { makeError } from '../auth/guidance.mjs';
4
6
  import { getProxyDispatcher } from '../proxy/proxy-agent.mjs';
5
7
 
6
8
  const BASE_URL = process.env.HWLINK_ENDPOINT || 'https://devstation.myhuaweicloud.com';
@@ -82,6 +84,39 @@ export function getCredentials() {
82
84
  return { ak: credentials.ak, sk: credentials.sk, securitytoken: credentials.securityToken };
83
85
  }
84
86
 
87
+ // Data-plane (sandbox exec/upload/tunnel) credentials: prefer AK/SK, else fall
88
+ // back to X-Auth-Token → temporary AK/SK, so token-auth users can drive the data
89
+ // plane without a permanent AK/SK (experience-report #3.3/#3.7 dead-loop fix).
90
+ // NOTE: returns `securitytoken` (lowercase t) — the call sites (session-manager,
91
+ // tools.mjs) destructure that exact spelling; returning `securityToken` here
92
+ // silently drops the STS token from the hwlink signature (APIGW.0301).
93
+ export async function resolveBackendCredentials() {
94
+ try {
95
+ const creds = resolveCredentialsWithRuntime({ allowMissing: true });
96
+ if (creds && creds.ak && creds.sk) {
97
+ return { ak: creds.ak, sk: creds.sk, securitytoken: creds.securityToken || '' };
98
+ }
99
+ } catch {
100
+ // no AK/SK — fall through to token
101
+ }
102
+ const token = resolveXAuthToken();
103
+ if (token) {
104
+ try {
105
+ const temp = await getTokenCredentials(token);
106
+ return { ak: temp.ak, sk: temp.sk, securitytoken: temp.securityToken || '' };
107
+ } catch (error) {
108
+ // Distinguish "token exchange failed" from "no credential at all" (#3.4).
109
+ const err = makeError('HDKIT_CRED_MISSING');
110
+ err.code = error?.code || 'TOKEN_EXCHANGE_FAILED';
111
+ err.messageZh =
112
+ `X-Auth-Token 换取临时 AK/SK 失败(${err.code}):` +
113
+ `${error?.message || '网络异常或 token 无效'},请检查网络连通性与 token 有效性。`;
114
+ throw err;
115
+ }
116
+ }
117
+ throw makeError('HDKIT_CRED_MISSING');
118
+ }
119
+
85
120
  async function apiGet(path, query, ak, sk, securitytoken) {
86
121
  query = query || {};
87
122
  const qs = sortedQs(query);
@@ -16,7 +16,7 @@ import { join, dirname, basename } from 'node:path';
16
16
  import { tmpdir } from 'node:os';
17
17
  import { fileURLToPath, pathToFileURL } from 'node:url';
18
18
 
19
- import { createConnection, getCredentials } from './hwlink-api.mjs';
19
+ import { createConnection, resolveBackendCredentials } from './hwlink-api.mjs';
20
20
  import { getWebSocketImpl } from '../proxy/proxy-agent.mjs';
21
21
  import { trackSandboxConnect, trackSandboxDisconnect } from '../telemetry/telemetry.mjs';
22
22
 
@@ -107,7 +107,7 @@ async function getSession(workspaceId, username, timeoutMs) {
107
107
  const key = `${workspaceId}:${username}`;
108
108
  if (sessions.has(key)) return sessions.get(key);
109
109
 
110
- const { ak, sk, securitytoken } = getCredentials();
110
+ const { ak, sk, securitytoken } = await resolveBackendCredentials();
111
111
  const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
112
112
 
113
113
  const WebSocketImpl = await getWebSocketImpl(wsUrl);
@@ -126,7 +126,7 @@ async function getSession(workspaceId, username, timeoutMs) {
126
126
  }
127
127
 
128
128
  async function createTunnelSession(workspaceId, username, timeoutMs = 30000) {
129
- const { ak, sk, securitytoken } = getCredentials();
129
+ const { ak, sk, securitytoken } = await resolveBackendCredentials();
130
130
  const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
131
131
  const WebSocketImpl = await getWebSocketImpl(wsUrl);
132
132
 
@@ -165,7 +165,7 @@ async function createTunnelSession(workspaceId, username, timeoutMs = 30000) {
165
165
  }
166
166
 
167
167
  export async function execOneShot(workspaceId, command, username, timeoutMs) {
168
- const { ak, sk, securitytoken } = getCredentials();
168
+ const { ak, sk, securitytoken } = await resolveBackendCredentials();
169
169
  const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
170
170
 
171
171
  const WebSocketImpl = await getWebSocketImpl(wsUrl);
@@ -13,6 +13,7 @@ import { checkAccountType } from './account-type.mjs';
13
13
  import { searchMarketplace } from './search-market.mjs';
14
14
  import { getServiceIcon } from './icon-library.mjs';
15
15
  import { detectFramework } from './detect-framework.mjs';
16
+ import { installKooCli } from './koocli-install.mjs';
16
17
  import {
17
18
  execWithSession,
18
19
  execOneShot,
@@ -35,7 +36,7 @@ import {
35
36
  hdkitCompetitionVoucherClaim,
36
37
  hdkitGenerateUserHash,
37
38
  } from './sandbox/hdkitservice-api.mjs';
38
- import { getCredentials } from './sandbox/hwlink-api.mjs';
39
+ import { resolveBackendCredentials } from './sandbox/hwlink-api.mjs';
39
40
  import { getAuthStatus, syncAuth } from './auth/service.mjs';
40
41
  import { validateIamCredentials } from './auth/credential-validator.mjs';
41
42
  import {
@@ -199,6 +200,15 @@ export const TOOL_DEFINITIONS = [
199
200
  properties: {},
200
201
  },
201
202
  },
203
+ {
204
+ name: 'huaweicloud_install_hcloud',
205
+ description:
206
+ 'Install KooCLI (hcloud) from inside the MCP server process via Node (no agent bash tool required). Downloads and extracts the pinned hcloud binary to ~/.local/bin/hcloud. Use when huaweicloud_check_cli reports HCLOUD_NOT_FOUND and the agent bash tool is unavailable (e.g. CodeArts with OPENCODE_CONFIG unset).',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: {},
210
+ },
211
+ },
202
212
  {
203
213
  name: 'huaweicloud_plan_cli_command',
204
214
  description: 'Classify and plan a Huawei Cloud hcloud command without executing it.',
@@ -1063,7 +1073,9 @@ function readImportFile() {
1063
1073
  const path = join(dirname(globalCredentialsPath()), 'creds-import.json');
1064
1074
  try {
1065
1075
  if (!existsSync(path)) return null;
1066
- const data = JSON.parse(readFileSync(path, 'utf8'));
1076
+ // Strip a leading UTF-8 BOM: PowerShell `Set-Content -Encoding UTF8` writes
1077
+ // one, which plain JSON.parse rejects (#devkit-体验报告 P1-4).
1078
+ const data = JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''));
1067
1079
  return {
1068
1080
  ak: String(data.ak || ''),
1069
1081
  sk: String(data.sk || ''),
@@ -1180,6 +1192,8 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1180
1192
  switch (name) {
1181
1193
  case 'huaweicloud_check_cli':
1182
1194
  return runVersionCheck();
1195
+ case 'huaweicloud_install_hcloud':
1196
+ return installKooCli();
1183
1197
  case 'huaweicloud_plan_cli_command':
1184
1198
  return planHcloudCommand(args.args || [], { allowWrites: args.allowWrites === true });
1185
1199
  case 'huaweicloud_run_readonly_command':
@@ -1283,6 +1297,11 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1283
1297
  }
1284
1298
 
1285
1299
  if (!ak || !sk) {
1300
+ if (sourceChannel === 'import') {
1301
+ throw new Error(
1302
+ 'ak and sk are required. creds-import.json 缺失或无法解析(可能带 UTF-8 BOM 或非法 JSON),解析失败时文件已被清除。请用无 BOM 的 UTF-8 JSON 重新写入后重试 mode=import(PowerShell 推荐 [IO.File]::WriteAllText 配 UTF8Encoding($false))。',
1303
+ );
1304
+ }
1286
1305
  throw new Error('ak and sk are required (or provide creds-import.json for mode=import).');
1287
1306
  }
1288
1307
 
@@ -1572,7 +1591,7 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1572
1591
  const apiKeyFile = '/tmp/hw_api_key';
1573
1592
  if (sandboxWsIdCred) {
1574
1593
  try {
1575
- const { ak, sk, securitytoken } = getCredentials();
1594
+ const { ak, sk, securitytoken } = await resolveBackendCredentials();
1576
1595
  const credsScript = [
1577
1596
  `export HW_ACCESS_KEY='${ak}'`,
1578
1597
  `export HW_SECRET_KEY='${sk}'`,
@@ -2318,6 +2337,26 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
2318
2337
  }
2319
2338
  const patternKey = SERVICE_ALIASES[svc] || resolvedService;
2320
2339
 
2340
+ // devkit internal credential/environment error codes (experience-report #3.9)
2341
+ // — these come from devkit's own guidance/credential layer, not Huawei Cloud API.
2342
+ const devkitErrorPatterns = {
2343
+ HDKIT_CRED_MISSING:
2344
+ '未配置华为云凭证。运行 npx huaweicloud-devkit auth init 配置 AK/SK,或注入 HW_X_AUTH_TOKEN(devkit 会自动用 token 换取临时 AK/SK)。',
2345
+ HDKIT_CRED_INVALID: '凭证已被华为云拒绝(AK/SK 无效或已过期)。重新生成 AK/SK 后 auth init。',
2346
+ HDKIT_CRED_FORBIDDEN: '凭证有效但无权限列出项目。检查 IAM 授权(可用 huaweicloud_check_account_type 判断账号类型)。',
2347
+ HDKIT_CRED_REGION_MISSING: '缺少 region 配置。运行 hcloud configure set --cli-region=<region>。',
2348
+ HDKIT_KOOCLI_NOT_READY: 'KooCLI(hcloud)未安装或未就绪。运行 npx huaweicloud-devkit install-hcloud。',
2349
+ HDKIT_PROJECT_ID_MISSING: '缺少 project_id。运行 npx huaweicloud-devkit auth init 自动配置。',
2350
+ TOKEN_INVALID: 'X-Auth-Token 无效或已过期。重新注入有效的 HW_X_AUTH_TOKEN。',
2351
+ TOKEN_EXPIRED: 'X-Auth-Token 已过期。重启进程重新注入凭证。',
2352
+ TOKEN_EXCHANGE_FAILED:
2353
+ 'X-Auth-Token 换取临时 AK/SK 失败,通常是网络无法访问 IAM 端点。检查网络连通性与 token 有效性。',
2354
+ HDKIT_SUB_ACCOUNT_RESTRICTED: '沙箱使用限制:当前子账号无法使用沙箱。切换主账号认证(auth_init)后重试。',
2355
+ };
2356
+ if (devkitErrorPatterns[errorCode]) {
2357
+ suggestions.push(`${errorCode}: ${devkitErrorPatterns[errorCode]}`);
2358
+ }
2359
+
2321
2360
  const hwErrorPatterns = {
2322
2361
  OBS: {
2323
2362
  InvalidAccessKeyId: