huaweicloud-devkit 1.1.10-next.3 → 1.1.10

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.3",
4
+ "version": "1.1.10",
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.3",
20
+ "version": "1.1.10",
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.3",
23
+ "version": "1.1.10",
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.3",
20
+ "version": "1.1.10",
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.3",
3
+ "version": "1.1.10",
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.3",
3
+ "version": "1.1.10",
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.3",
5
+ "version": "1.1.10",
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.",
@@ -142,7 +142,7 @@
142
142
  ]
143
143
  },
144
144
  "message": "The change appears to expose an administrative or database port to the public internet. Web ports (80/443) are allowed - this rule only covers admin/database ports.",
145
- "remediation": "Restrict the source CIDR to a trusted range, use a bastion or VPN, and keep public preview endpoints behind authenticated HTTP services."
145
+ "remediation": "Restrict the source CIDR to a trusted range, use a bastion or VPN, and keep public preview endpoints behind authenticated HTTP services. For SSH troubleshooting, do NOT widen to 0.0.0.0/0: open a /32 rule for your real direct egress IP instead (detect it with Invoke-RestMethod -Uri https://api.ipify.org -NoProxy on Windows / curl --noproxy '*' on Linux — a proxy egress is NOT the direct egress; see huawei-ecs references/windows-ssh.md §0.1), or skip SSH entirely via cloud-init user_data deployment (references/create-instance.md §Bootstrap). Full SSH failure diagnosis chain: huawei-ecs references/troubleshooting.md."
146
146
  },
147
147
  {
148
148
  "id": "hwc-obs-anonymous-write",
@@ -429,12 +429,6 @@
429
429
  "field": "text",
430
430
  "regex": "\\bfdisk\\b"
431
431
  }
432
- ],
433
- "none": [
434
- {
435
- "field": "text",
436
- "regex": "\\bhcloud\\b"
437
- }
438
432
  ]
439
433
  },
440
434
  "message": "The sandbox terminal command is destructively dangerous and could destroy the workspace.",
@@ -4,20 +4,21 @@
4
4
 
5
5
  ## 错误码映射
6
6
 
7
- | 错误 | 根因 | 修复 |
8
- | --------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
9
- | `Ecs.0005` flavor-image 不匹配 | 镜像虚拟化类型与规格不兼容 | 检查 image `__support_amd`/`__support_xen` 与 flavor `os_extra_specs`。BareMetal 镜像不兼容标准 ECS 规格 |
10
- | `[USE_ERROR]不正确的参数:imagetype` | 参数名错误 | KooCLI 使用双下划线前缀:`--__imagetype=gold`(不是 `--imagetype`) |
11
- | `[USE_ERROR]不正确的参数:chargingMode` | 参数路径错误 | 计费模式参数路径:`--server.extendparam.chargingMode=postPaid`。不传默认按需付费 |
12
- | `[USE_ERROR]不正确的参数:__support_amd` | 参数无效 | `IMS ListImages` 某些版本不支持此过滤。去掉该参数,使用 client-side 过滤 |
13
- | Cannot SSH | 安全组未开放22端口 或 未绑定EIP | 1) 添加入方向规则 tcp 22。2) `hcloud EIP AssociatePublicips --publicip_id=<id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT` |
14
- | Flavor unavailable | 区域不支持该规格 | `hcloud ECS ListFlavors --cli-region=<r>` 先查。不硬编码 s6/m6 等规格名 |
15
- | Insufficient resources | AZ 库存不足 | 换规格、换 AZ、或等待资源释放 |
16
- | `Ecs.0019` flavor 已下线 | 规格已下架 | `hcloud ECS ListFlavors --cli-region=<r>` 查当前在售规格并切换替代规格(状态字段规则见 `references/flavors.md`) |
17
- | `Ecs.0319` 可用区容量不足 | 目标 AZ 库存售罄 | 更换可用区或更换规格后重试 |
18
- | `Ecs.0067` 余额受限 | 现金余额不足/历史欠费 | 查 BSS 现金余额并充值(`hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1`);代金券不参与按量付费下单风控,历史欠费需先结清 |
19
- | `KPS.9004` 密钥对创建受限 | 现金余额不足/历史欠费 | 同上:查 BSS 现金余额并充值 + 历史欠费结清后重试 `hcloud ECS NovaCreateKeypair` |
20
- | AuthFailure | AK/SK 过期或无效 | `npx huaweicloud-devkit auth init` 重新配置统一凭据 |
7
+ | 错误 | 根因 | 修复 |
8
+ | --------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `Ecs.0005` flavor-image 不匹配 | 镜像虚拟化类型与规格不兼容 | 检查 image `__support_amd`/`__support_xen` 与 flavor `os_extra_specs`。BareMetal 镜像不兼容标准 ECS 规格 |
10
+ | `[USE_ERROR]不正确的参数:imagetype` | 参数名错误 | KooCLI 使用双下划线前缀:`--__imagetype=gold`(不是 `--imagetype`) |
11
+ | `[USE_ERROR]不正确的参数:chargingMode` | 参数路径错误 | 计费模式参数路径:`--server.extendparam.chargingMode=postPaid`。不传默认按需付费 |
12
+ | `[USE_ERROR]不正确的参数:__support_amd` | 参数无效 | `IMS ListImages` 某些版本不支持此过滤。去掉该参数,使用 client-side 过滤 |
13
+ | Cannot SSH | 安全组未开放22端口 或 未绑定EIP | 1) 添加入方向规则 tcp 22。2) `hcloud EIP AssociatePublicips --publicip_id=<id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT` |
14
+ | Flavor unavailable | 区域不支持该规格 | `hcloud ECS ListFlavors --cli-region=<r>` 先查。不硬编码 s6/m6 等规格名 |
15
+ | Insufficient resources | AZ 库存不足 | 换规格、换 AZ、或等待资源释放 |
16
+ | `Ecs.0019` flavor 已下线 | 规格已下架 | `hcloud ECS ListFlavors --cli-region=<r>` 查当前在售规格并切换替代规格(状态字段规则见 `references/flavors.md`) |
17
+ | `Ecs.0319` 可用区容量不足 | 目标 AZ 规格库存售罄 | 更换可用区或更换规格后重试 |
18
+ | `Ecs.0044` 磁盘类型售罄 | 目标 AZ 的 root volume 类型售罄 | 换 `--server.root_volume.volumetype`(SAS/SSD/GPSSD)或换 AZ(`hcloud ECS NovaListAvailabilityZones --cli-region=<r>`);与 Ecs.0319(规格容量)是不同根因,ListFlavors 不预告磁盘售罄 |
19
+ | `Ecs.0067` 余额受限 | 现金余额不足/历史欠费 | 查 BSS 现金余额并充值(`hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1`);代金券不参与按量付费下单风控,历史欠费需先结清 |
20
+ | `KPS.9004` 密钥对创建受限 | 现金余额不足/历史欠费 | 同上:查 BSS 现金余额并充值 + 历史欠费结清后重试 `hcloud ECS NovaCreateKeypair` |
21
+ | AuthFailure | AK/SK 过期或无效 | `npx huaweicloud-devkit auth init` 重新配置统一凭据 |
21
22
 
22
23
  ## 创建失败诊断流程
23
24
 
@@ -40,16 +41,16 @@ hcloud IMS ListImages --cli-region=<r> --__imagetype=gold --__isregistered=true
40
41
  # 不支持的参数不要传,用 client-side 过滤
41
42
  ```
42
43
 
43
- ## EulerOS 3.0 firewalld:安全组放行后仍不可达(实测 #935 报告4 问题2)
44
+ ## RHEL 系镜像 firewalld:安全组放行后 SSH 仍不可达(实测 #935 EulerOS / #971 Rocky Linux)
44
45
 
45
- EulerOS 3.0 镜像内置 firewalld 且**默认拒绝 SSH 入站** —— 安全组已放行 22 端口后 SSH 仍超时,是实例内防火墙在拦截,不是安全组问题。修复:
46
+ EulerOS 3.0 镜像内置 firewalld 且**默认拒绝 SSH 入站** —— 安全组已放行 22 端口后 SSH 仍超时,是实例内防火墙在拦截,不是安全组问题(实测 #935 报告4 问题2)。Rocky Linux 9.8 也实测出现同样症状(安全组正确 + 出口 IP 一致 + HTTP 80 正常,但 SSH 22 全超时,#971)——**根因未验证**(SSH 不通无法登录查看,疑似同为 firewalld/镜像默认配置),但症状表明该问题不限于 EulerOS,凡 RHEL 系镜像(EulerOS / Rocky / 麒麟等)安全组正确却 SSH 超时时,都应优先怀疑实例内防火墙。修复:
46
47
 
47
48
  ```bash
48
49
  # 镜像内放行 SSH(SSH 可用时)
49
50
  firewall-cmd --permanent --add-service=ssh && firewall-cmd --reload
50
51
  ```
51
52
 
52
- 或改用 cloud-init user_data 自动部署(创建时写入,无需 SSH):
53
+ **SSH 完全不可达时的恢复路径(无需登录实例)**:改用 cloud-init user_data,创建时放行或直接完成部署(见 `references/create-instance.md` §Bootstrap —— 也是 SSH 不可靠场景的推荐部署路径):
53
54
 
54
55
  ```bash
55
56
  # user_data(base64 后传 CreateServers)
@@ -393,87 +393,6 @@ export function resolveCredentialsWithRuntime(options = {}) {
393
393
  return resolveCredentials(options);
394
394
  }
395
395
 
396
- export function resolveXAuthToken() {
397
- const envToken = process.env.HW_X_AUTH_TOKEN;
398
- if (present(envToken)) return envToken;
399
-
400
- const parentCwd = getParentCwd();
401
- const searchDirs = [process.env.CODEARTS_PROJECT_DIR, parentCwd, process.cwd(), homedir()];
402
- for (const dir of searchDirs) {
403
- if (!dir) continue;
404
- const path = join(dir, '.codeartsdoer', 'mcp', 'mcp_settings.json');
405
- try {
406
- if (!existsSync(path)) continue;
407
- const config = JSON.parse(readFileSync(path, 'utf8'));
408
- const server = pickDevkitMcpServer(config?.mcpServers);
409
- const token = server?.env?.HW_X_AUTH_TOKEN;
410
- if (present(token)) return token;
411
- } catch {}
412
- }
413
-
414
- const workPaths = [
415
- join(homedir(), '.codearts', 'mcp', 'mcp_settings.json'),
416
- join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json'),
417
- ];
418
- for (const path of workPaths) {
419
- try {
420
- if (!existsSync(path)) continue;
421
- const config = JSON.parse(readFileSync(path, 'utf8'));
422
- const server = pickDevkitMcpServer(config?.mcp);
423
- const token = server?.environment?.HW_X_AUTH_TOKEN;
424
- if (present(token)) return token;
425
- } catch {}
426
- }
427
- return null;
428
- }
429
-
430
- export function resolveXAuthTokenExpiry() {
431
- const v = process.env.HW_X_AUTH_TOKEN_EXPIRES_AT;
432
- if (v && typeof v === 'string' && v.trim()) {
433
- const asNum = Number(v.trim());
434
- if (Number.isFinite(asNum) && asNum > 0) return asNum > 1e12 ? asNum : asNum * 1000;
435
- const t = Date.parse(v.trim());
436
- if (Number.isFinite(t)) return t;
437
- }
438
-
439
- // Fallback: 未注入 HW_X_AUTH_TOKEN_EXPIRES_AT 时,从 token 字符串自解析过期时间。
440
- // X-Auth-Token 是 CMS/PKCS#7 SignedData(base64 DER),其 eContent 内嵌明文 JSON
441
- // (含 token.expires_at);base64 解码 + 提取 {"token" JSON 即可读出,无需依赖注入方显式给过期时间。
442
- const token = resolveXAuthToken();
443
- if (!token) return null;
444
- return parseXAuthTokenExpiry(token);
445
- }
446
-
447
- function parseXAuthTokenExpiry(token) {
448
- try {
449
- const pad = token + '='.repeat((4 - (token.length % 4)) % 4);
450
- const text = Buffer.from(pad.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
451
- const idx = text.indexOf('{"token"');
452
- if (idx < 0) return null;
453
- const tail = text.slice(idx);
454
- let depth = 0;
455
- let end = -1;
456
- for (let i = 0; i < tail.length; i += 1) {
457
- if (tail[i] === '{') depth += 1;
458
- else if (tail[i] === '}') {
459
- depth -= 1;
460
- if (depth === 0) {
461
- end = i + 1;
462
- break;
463
- }
464
- }
465
- }
466
- if (end < 0) return null;
467
- const obj = JSON.parse(tail.slice(0, end));
468
- const expiresAt = obj?.token?.expires_at;
469
- if (!expiresAt || typeof expiresAt !== 'string') return null;
470
- const t = Date.parse(expiresAt);
471
- return Number.isFinite(t) ? t : null;
472
- } catch {
473
- return null;
474
- }
475
- }
476
-
477
396
  export function lastSyncPath() {
478
397
  return join(baseHome(), '.config', 'huaweicloud', '.last_sync');
479
398
  }
@@ -11,7 +11,6 @@ import {
11
11
  readCodeArtsCredentials,
12
12
  readGlobalCredentials,
13
13
  resolveCredentialsWithRuntime,
14
- resolveXAuthToken,
15
14
  writeLastSync,
16
15
  writeObsConfig,
17
16
  } from './credentials.mjs';
@@ -80,14 +79,6 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
80
79
  message = '已从码道 MCP settings 读取到临时凭证,MCP 工具可直接使用。';
81
80
  return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
82
81
  }
83
- if (resolveXAuthToken() && !s1Has && !envHasCreds) {
84
- // X-Auth-Token authentication: no AK/SK injected but a token is present —
85
- // devkit exchanges it for temporary AK/SK on demand, so no manual setup.
86
- scenario = 0;
87
- reason = 'token-auth';
88
- message = '已使用 X-Auth-Token 认证,MCP 工具可直接使用,无需配置 AK/SK。';
89
- return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
90
- }
91
82
  if (s1Has && !envHasCreds) {
92
83
  scenario = 1;
93
84
  reason = 's1-only';
@@ -176,7 +167,6 @@ export function getAuthStatus(target = 'all') {
176
167
  const s4Creds = readCodeArtsCredentials();
177
168
  const onboarding = computeOnboarding({ credentials, reconciled });
178
169
  const stsExpiry = computeStsExpiry();
179
- const tokenAuth = resolveXAuthToken();
180
170
  const credentialPanel = {
181
171
  sts: stsExpiry, // 临时 STS(MCP==沙箱同源)
182
172
  s1: {
@@ -184,12 +174,11 @@ export function getAuthStatus(target = 'all') {
184
174
  fingerprint: credentials?.ak ? fingerprint(credentials.ak, credentials.sk) : null,
185
175
  persistent: true, // 永久,无过期
186
176
  },
187
- token: tokenAuth ? { active: true } : { active: false },
188
- activeSource: stsExpiry ? 'temporary-sts' : credentials?.ak ? 's1-persistent' : tokenAuth ? 'x-auth-token' : 'none',
177
+ activeSource: stsExpiry ? 'temporary-sts' : credentials?.ak ? 's1-persistent' : 'none',
189
178
  };
190
179
  return {
191
180
  target,
192
- credentialsConfigured: Boolean(credentials?.ak && credentials?.sk) || Boolean(tokenAuth),
181
+ credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
193
182
  credentialsPath: globalCredentialsPath(),
194
183
  mcpSettingsConfigured: Boolean(s4Creds?.ak && s4Creds?.sk),
195
184
  mcpSettingsSource: s4Creds ? 'codearts' : null,
@@ -7,13 +7,7 @@ import { dirname, join } from 'node:path';
7
7
  import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
8
8
  import { getProxySettings } from './proxy/proxy-config.mjs';
9
9
  import { resolveHcloudCommand } from './hcloud-probe.mjs';
10
- import {
11
- parseStsExpiry,
12
- resolveCredentialsWithRuntime,
13
- resolveXAuthToken,
14
- resolveXAuthTokenExpiry,
15
- } from './auth/credentials.mjs';
16
- import { getTokenCredentials } from './auth/token-exchange.mjs';
10
+ import { parseStsExpiry, resolveCredentialsWithRuntime } from './auth/credentials.mjs';
17
11
 
18
12
  const DEFAULT_TIMEOUT_MS = 60_000;
19
13
  const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
@@ -303,7 +297,7 @@ export function redactArgsWithObs(rawArgs) {
303
297
  return redactSecrets(out);
304
298
  }
305
299
 
306
- export async function resolveStsInjectArgs(rawArgs) {
300
+ export function resolveStsInjectArgs(rawArgs) {
307
301
  const normalized = Array.isArray(rawArgs) ? rawArgs.map(String) : [];
308
302
  if (normalized.length === 0) return [];
309
303
  const flat = normalized.map(String);
@@ -356,25 +350,7 @@ export async function resolveStsInjectArgs(rawArgs) {
356
350
  } catch {
357
351
  return [];
358
352
  }
359
- if (!creds || !creds.ak || !creds.sk) {
360
- const token = resolveXAuthToken();
361
- if (token) {
362
- const expiry = resolveXAuthTokenExpiry();
363
- if (expiry !== null && expiry - 60000 <= Date.now()) {
364
- const err = new Error('X-Auth-Token 已过期,请重启进程重新注入凭证。');
365
- err.code = 'TOKEN_EXPIRED';
366
- throw err;
367
- }
368
- const temp = await getTokenCredentials(token);
369
- return [
370
- '--cli-access-key=' + temp.ak,
371
- '--cli-secret-key=' + temp.sk,
372
- '--cli-security-token=' + temp.securityToken,
373
- ];
374
- }
375
- return [];
376
- }
377
- if (!creds.securityToken) return [];
353
+ if (!creds || !creds.ak || !creds.sk || !creds.securityToken) return [];
378
354
 
379
355
  // R3: if we can derive an expiry and the token is already at/within 60s of
380
356
  // expiring, skip injection — using a dead token would make a doomed IAM round
@@ -492,22 +468,7 @@ export async function runHcloud(args, options = {}) {
492
468
  // and R3 forbids writing them to disk). Inject per-command so hcloud actually
493
469
  // authenticates with the platform-provided STS. Injection args are appended to
494
470
  // the EXECUTED args only; classification/approval used the original args.
495
- let stsInject;
496
- try {
497
- stsInject = await resolveStsInjectArgs(normalizedArgs);
498
- } catch (error) {
499
- if (error?.code === 'TOKEN_INVALID' || error?.code === 'TOKEN_EXCHANGE_FAILED' || error?.code === 'TOKEN_EXPIRED') {
500
- return {
501
- ok: false,
502
- code: error.code,
503
- error: error.message,
504
- stderr: '',
505
- stdout: '',
506
- plan,
507
- };
508
- }
509
- throw error;
510
- }
471
+ const stsInject = resolveStsInjectArgs(normalizedArgs);
511
472
  if (stsInject.length > 0) {
512
473
  plan.rawArgs = [...plan.rawArgs, ...stsInject];
513
474
  }
@@ -3,7 +3,7 @@
3
3
  // Program-owned fields (the node executable + mcp-server.mjs path, required env
4
4
  // keys) are corrected by the installer; everything else belongs to the user.
5
5
 
6
- export function isPlainObject(value) {
6
+ function isPlainObject(value) {
7
7
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
8
8
  }
9
9
 
@@ -19,7 +19,7 @@ export function loadRiskRules(options = {}) {
19
19
  function redactEvidence(text) {
20
20
  return String(text)
21
21
  .replace(
22
- /((?:access[_-]?key|secret[_-]?key|secret|security[_-]?token|x[_-]?auth[_-]?token|x[_-]?subject[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
22
+ /((?:access[_-]?key|secret[_-]?key|secret|security[_-]?token|x[_-]?auth[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
23
23
  '$1<redacted>',
24
24
  )
25
25
  .replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
@@ -39,7 +39,7 @@ function redactString(text) {
39
39
  // Redact the ENTIRE value of this arg — not just the first whitespace token.
40
40
  .replace(/((?:user[_-]?data|metadata|private[_-]?key)\s*[:=]\s*).*/gi, '$1<redacted>')
41
41
  .replace(
42
- /((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|x[_-]?subject[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
42
+ /((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
43
43
  '$1<redacted>',
44
44
  )
45
45
  .replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>')
@@ -44,7 +44,6 @@ import {
44
44
  applyUserDelta,
45
45
  inheritPeerUserEnv,
46
46
  detectOpenCodeEra,
47
- isPlainObject,
48
47
  mergeOpenCodeEntry,
49
48
  } from './mcp-config-merge.mjs';
50
49
  import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
@@ -704,7 +703,7 @@ function resolveOpenCodeEra() {
704
703
  return (resolvedOpenCodeEra = 'v2');
705
704
  }
706
705
 
707
- function updateOpenCodeConfig(pluginDir, { nativePluginInstalled = false } = {}) {
706
+ function updateOpenCodeConfig(pluginDir) {
708
707
  const configPath = opencodeConfigFile();
709
708
  const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
710
709
  const era = resolveOpenCodeEra();
@@ -723,25 +722,6 @@ function updateOpenCodeConfig(pluginDir, { nativePluginInstalled = false } = {})
723
722
 
724
723
  const { config: next, existing, changed } = mergeOpenCodeEntry(config, { mcpPath, era });
725
724
 
726
- // Native-plugin mode: the in-process plugin already bridges all 42 tools with
727
- // the shared callTool layer (names, approval gate, telemetry unchanged). Keep
728
- // the MCP entry for manual fallback but leave it disabled so OpenCode does
729
- // not register 42 duplicate `huaweicloud-devkit_*` copies (#940 follow-up).
730
- let mcpDisabled = false;
731
- if (nativePluginInstalled) {
732
- const key = 'huaweicloud-devkit';
733
- if (era === 'v2') {
734
- const servers = isPlainObject(next.mcp?.servers) ? next.mcp.servers : null;
735
- if (servers && servers[key] && servers[key].disabled !== true) {
736
- servers[key] = { ...servers[key], disabled: true };
737
- mcpDisabled = true;
738
- }
739
- } else if (next.mcp?.[key] && next.mcp[key].enabled !== false) {
740
- next.mcp[key] = { ...next.mcp[key], enabled: false };
741
- mcpDisabled = true;
742
- }
743
- }
744
-
745
725
  // Restore user fields saved by a previous uninstall (issue #615), on fresh install only.
746
726
  if (!existing) {
747
727
  const delta = takeAgentDelta('opencode');
@@ -754,15 +734,12 @@ function updateOpenCodeConfig(pluginDir, { nativePluginInstalled = false } = {})
754
734
  }
755
735
  }
756
736
 
757
- if (existing && !changed && !mcpDisabled) {
737
+ if (existing && !changed) {
758
738
  console.log(` OpenCode MCP config unchanged: ${configPath}`);
759
739
  return;
760
740
  }
761
741
 
762
742
  writeMcpSettingsFile(configPath, next);
763
- if (mcpDisabled) {
764
- console.log(` OpenCode MCP entry disabled (native plugin active — no duplicate tools) -> ${configPath}`);
765
- }
766
743
  console.log(
767
744
  existing
768
745
  ? ` OpenCode MCP config merged (user fields preserved, ${era}): ${configPath}`
@@ -984,24 +961,7 @@ async function installOpenCode() {
984
961
  mkdirSync(opcPlugins, { recursive: true });
985
962
  copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
986
963
  console.log(` Plugin -> ${opcPlugins}`);
987
- // OpenCode native plugin (#905 OpenCode performance): bridges all 42 MCP tools
988
- // into OpenCode in-process custom tools (zod schemas + callTool), plus safety
989
- // gate and telemetry. Local plugins are auto-loaded from this directory.
990
- const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
991
- let nativePluginInstalled = false;
992
- if (existsSync(nativePluginSrc)) {
993
- copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
994
- // Deps for the native plugin (zod) live next to the OpenCode config so the
995
- // plugin can require them at load time.
996
- const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
997
- const nativePkgDst = join(configRoot('opencode'), 'package.json');
998
- if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
999
- copyFileSync(nativePkgSrc, nativePkgDst);
1000
- }
1001
- nativePluginInstalled = true;
1002
- console.log(` Native plugin -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
1003
- }
1004
- updateOpenCodeConfig(pluginDest, { nativePluginInstalled });
964
+ updateOpenCodeConfig(pluginDest);
1005
965
  installRuntimeDeps(pluginDest);
1006
966
  }
1007
967
 
@@ -1034,11 +994,6 @@ function uninstallOpenCode() {
1034
994
  removeIfExists(pluginFile);
1035
995
  console.log(' Removed plugin');
1036
996
  }
1037
- const nativePluginFile = join(configRoot('opencode'), 'plugins', 'huaweicloud-devkit.js');
1038
- if (existsSync(nativePluginFile)) {
1039
- removeIfExists(nativePluginFile);
1040
- console.log(' Removed native plugin');
1041
- }
1042
997
 
1043
998
  if (removeIfExists(opencodePluginsDir())) {
1044
999
  console.log(' Removed MCP server and safety policy');
@@ -1086,20 +1041,7 @@ async function updateOpenCode() {
1086
1041
  mkdirSync(opcPlugins, { recursive: true });
1087
1042
  copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
1088
1043
  console.log(` Plugin updated -> ${opcPlugins}`);
1089
- // Native plugin refresh (#905 OpenCode performance) — overwrite in place.
1090
- const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
1091
- let nativePluginInstalled = false;
1092
- if (existsSync(nativePluginSrc)) {
1093
- copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
1094
- const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
1095
- const nativePkgDst = join(configRoot('opencode'), 'package.json');
1096
- if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
1097
- copyFileSync(nativePkgSrc, nativePkgDst);
1098
- }
1099
- nativePluginInstalled = true;
1100
- console.log(` Native plugin updated -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
1101
- }
1102
- updateOpenCodeConfig(pluginDest, { nativePluginInstalled });
1044
+ updateOpenCodeConfig(pluginDest);
1103
1045
  mkdirSync(pluginDest, { recursive: true });
1104
1046
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1105
1047
  installRuntimeDeps(pluginDest);
@@ -2351,6 +2351,8 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
2351
2351
  'The flavor has been taken offline (Ecs.0019). Run `hcloud ECS ListFlavors --cli-region=<region>` to query currently on-sale flavors and switch to a replacement flavor (see the huawei-ecs skill references/flavors.md for the cond:operation:status field rules — a flavor marked 卖光/下线 keeps failing until you pick an on-sale one), then recreate.',
2352
2352
  'Ecs.0319':
2353
2353
  'The requested flavor has insufficient capacity in the selected availability zone (Ecs.0319). Fix: retry in a different availability zone, or switch to a replacement flavor (list on-sale flavors with `hcloud ECS ListFlavors --cli-region=<region>`), then recreate.',
2354
+ 'Ecs.0044':
2355
+ 'The requested volume type is sold out in the selected availability zone (Ecs.0044, e.g. "the volumeType [SATA] is sellout in az[cn-north-4a]"). This is the ROOT VOLUME/DISK type being unavailable — a different failure from Ecs.0319 (flavor capacity). Fix: switch `--server.root_volume.volumetype` to an available type (SAS/SSD/GPSSD — query availability first) or retry in a different availability zone (`hcloud ECS NovaListAvailabilityZones --cli-region=<region>`). ListFlavors does NOT pre-warn about volume-type sellouts — you only find out at order time.',
2354
2356
  'Ecs.0067':
2355
2357
  'Order submission rejected for balance reasons (Ecs.0067). Check the cash balance with `hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1` and top up if needed — pay-per-use order risk control checks the CASH balance only (代金券/vouchers do NOT participate in order risk control; they only offset the bill at settlement). Also settle any historical arrears, however small, before retrying.',
2356
2358
  },
@@ -1,24 +0,0 @@
1
- {
2
- "name": "@huaweicloud-devkit/opencode-integration",
3
- "version": "0.0.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "@huaweicloud-devkit/opencode-integration",
9
- "version": "0.0.0",
10
- "dependencies": {
11
- "zod": "^3.11.0"
12
- }
13
- },
14
- "node_modules/zod": {
15
- "version": "3.25.76",
16
- "resolved": "https://repo.huaweicloud.com/repository/npm/zod/-/zod-3.25.76.tgz",
17
- "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
18
- "license": "MIT",
19
- "funding": {
20
- "url": "https://github.com/sponsors/colinhacks"
21
- }
22
- }
23
- }
24
- }
@@ -1,10 +0,0 @@
1
- {
2
- "name": "@huaweicloud-devkit/opencode-integration",
3
- "version": "0.0.0",
4
- "private": true,
5
- "type": "module",
6
- "description": "Local deps for the huaweicloud-devkit OpenCode plugin (zod for tool schema conversion). Installed into ~/.config/opencode by 'npx huaweicloud-devkit install --target opencode'.",
7
- "dependencies": {
8
- "zod": "^3.11.0"
9
- }
10
- }
@@ -1,194 +0,0 @@
1
- // huaweicloud-devkit OpenCode native plugin
2
- //
3
- // Bridges all 42 MCP tools into OpenCode in-process custom tools via the shared
4
- // callTool() business layer, plus:
5
- // - tool.execute.before safety gate (classifyHcloudArgs / classifyTextCommand)
6
- // - skill-tracker telemetry (superset of the legacy skill-tracker.js plugin)
7
- // - STS command-level injection is inherited from callTool -> runHcloud chain
8
- //
9
- // Install: npm i -g huaweicloud-devkit && npx huaweicloud-devkit install --target opencode
10
- // (this file is copied to ~/.config/opencode/plugins/huaweicloud-devkit.js and
11
- // auto-loaded by OpenCode at startup — local plugins need no config entry)
12
- //
13
- // Compatibility contract (unchanged from MCP mode):
14
- // - tool names stay `huaweicloud_*` (skills/hook matchers keep working)
15
- // - approval gate: run_approved_command still requires args+approvalToken+approvedByUser
16
- // - credentials: resolveCredentials reads env (HW_*) then S1 — same priority as MCP
17
-
18
- import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
19
- import { join, dirname } from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
- import { createRequire } from 'node:module';
22
-
23
- const require = createRequire(import.meta.url);
24
- const z = require('zod');
25
-
26
- // devkit core — resolved from the installed plugin tree next to this file
27
- const selfDir = dirname(fileURLToPath(import.meta.url));
28
- const DEVKIT_SRC = join(selfDir, '..', 'huaweicloud-plugins', 'src');
29
-
30
- let devkit = null;
31
- let loadError = null;
32
- try {
33
- devkit = await import(`file://${join(DEVKIT_SRC, 'tools.mjs')}`);
34
- } catch (error) {
35
- loadError = error;
36
- }
37
-
38
- // ── telemetry (superset of legacy skill-tracker.js) ─────────────────────────
39
-
40
- const TELEMETRY_DIR = join(DEVKIT_SRC, '..', 'telemetry');
41
- function writeEvent(key, value, extra = {}) {
42
- try {
43
- if (!existsSync(TELEMETRY_DIR)) mkdirSync(TELEMETRY_DIR, { recursive: true });
44
- appendFileSync(join(TELEMETRY_DIR, 'hook-events.jsonl'), JSON.stringify({ key, value, ...extra }) + '\n');
45
- } catch {
46
- /* telemetry must never break the host */
47
- }
48
- }
49
-
50
- function isHuaweiCloudSkill(name) {
51
- return typeof name === 'string' && /^huawei/i.test(name);
52
- }
53
-
54
- // ── JSON Schema -> Zod converter (covers the devkit inputSchema subset) ─────
55
-
56
- function jsonSchemaToZod(schema) {
57
- // schema is { type:'object', properties:{...}, required:[...] }
58
- const shape = {};
59
- const required = new Set(schema?.required || []);
60
- for (const [name, prop] of Object.entries(schema?.properties || {})) {
61
- let node;
62
- switch (prop?.type) {
63
- case 'string':
64
- node = z.string();
65
- if (prop.enum) {
66
- const vals = prop.enum.filter((v) => typeof v === 'string');
67
- if (vals.length > 0) node = z.enum(vals);
68
- }
69
- break;
70
- case 'number':
71
- case 'integer':
72
- node = z.number();
73
- break;
74
- case 'boolean':
75
- node = z.boolean();
76
- break;
77
- case 'array':
78
- node = z.array(z.any());
79
- break;
80
- case 'object':
81
- default:
82
- node = z.any(); // nested objects (e.g. git{repo_url}) stay permissive
83
- }
84
- if (prop?.description) node = node.describe(prop.description);
85
- shape[name] = required.has(name) ? node : node.optional();
86
- }
87
- return shape;
88
- }
89
-
90
- // ── safety gate (mirrors the MCP/hook deny semantics) ──────────────────────
91
-
92
- const DENY_TOOLS = new Set([
93
- 'huaweicloud_run_approved_command', // has its own in-tool approval gate (token check)
94
- ]);
95
-
96
- async function safetyGate(toolName, args) {
97
- if (!devkit) return; // devkit failed to load -> tools absent, nothing to gate
98
- // read-only and planning tools pass through: callTool re-runs the full policy
99
- // layer anyway (classifyHcloudArgs inside plan/read paths). The gate below is
100
- // a fast pre-check for the raw hcloud-ish tools only.
101
- if (DENY_TOOLS.has(toolName)) return;
102
- }
103
-
104
- // ── tool bridging ───────────────────────────────────────────────────────────
105
-
106
- function buildBridgedTools() {
107
- if (!devkit) return {};
108
- const tools = {};
109
- const definitions = devkit.TOOL_DEFINITIONS || [];
110
- for (const def of definitions) {
111
- tools[def.name] = {
112
- description: def.description,
113
- args: jsonSchemaToZod(def.inputSchema || { type: 'object', properties: {} }),
114
- async execute(args, context) {
115
- const started = Date.now();
116
- try {
117
- await safetyGate(def.name, args);
118
- const result = await devkit.callTool(def.name, args, { cwd: context?.directory });
119
- writeEvent('tool:invoke', def.name, { durationMs: Date.now() - started });
120
- return typeof result === 'string' ? result : { output: JSON.stringify(result, null, 2) };
121
- } catch (error) {
122
- // crash containment: never take the host down with a tool error
123
- const payload = {
124
- error: error?.message || String(error),
125
- code: error?.code,
126
- action: error?.action || error?.nextStep,
127
- };
128
- writeEvent('tool:error', def.name, { message: payload.error });
129
- return { output: JSON.stringify(payload, null, 2) };
130
- }
131
- },
132
- };
133
- }
134
- return tools;
135
- }
136
-
137
- // ── plugin export (v1 Hooks contract, IDE + CLI dual form) ─────────────────
138
-
139
- function getHooks() {
140
- return {
141
- 'tool.execute.before': function (input, output) {
142
- try {
143
- if (input?.tool === 'skill') {
144
- const name = output?.args?.name;
145
- if (isHuaweiCloudSkill(name)) writeEvent('skill:retrieve', name);
146
- return;
147
- }
148
- if (input?.tool === 'bash') {
149
- const cmd = output?.args?.command || '';
150
- if (!cmd) return;
151
- // CLI read/write classification (legacy skill-tracker behavior)
152
- const isHcloud = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+/i.test(cmd);
153
- if (isHcloud) {
154
- const isRead = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i.test(cmd);
155
- writeEvent(isRead ? 'cli:read' : 'cli:write', cmd.slice(0, 120), { capability: 'cli' });
156
- }
157
- }
158
- } catch {
159
- /* hooks must never throw */
160
- }
161
- },
162
- event: function ({ event }) {
163
- try {
164
- if (event?.type === 'message.part.updated') {
165
- const text = event?.properties?.part?.text;
166
- if (typeof text === 'string') {
167
- const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
168
- if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
169
- }
170
- }
171
- } catch {
172
- /* ignore */
173
- }
174
- },
175
- };
176
- }
177
-
178
- function plugin() {
179
- const hooks = getHooks();
180
- if (loadError) {
181
- // Surface a single actionable line instead of breaking startup
182
- console.error(`[huaweicloud-devkit] core failed to load: ${loadError.message}`);
183
- console.error(`[huaweicloud-devkit] expected devkit src at ${DEVKIT_SRC}`);
184
- console.error(`[huaweicloud-devkit] run: npx huaweicloud-devkit install --target opencode`);
185
- return hooks; // telemetry still works; tools absent until install is re-run
186
- }
187
- hooks.tool = buildBridgedTools();
188
- return hooks;
189
- }
190
-
191
- plugin.id = 'huaweicloud-devkit';
192
- plugin.server = async () => plugin();
193
-
194
- export default plugin;
@@ -1,100 +0,0 @@
1
- import { fetchWithProxy } from '../proxy/proxy-agent.mjs';
2
-
3
- const DEFAULT_DURATION_SECONDS = 900;
4
- const DEFAULT_IAM_ENDPOINT = 'https://iam.myhuaweicloud.com';
5
-
6
- export async function exchangeTokenForCredentials(token, options = {}) {
7
- const durationSeconds = options.durationSeconds ?? DEFAULT_DURATION_SECONDS;
8
- const iamEndpoint = options.iamEndpoint || process.env.HW_TOKEN_IAM_ENDPOINT || DEFAULT_IAM_ENDPOINT;
9
- const fetchImpl = options.fetchImpl;
10
-
11
- const body = {
12
- auth: {
13
- identity: {
14
- methods: ['token'],
15
- token: { id: token, duration_seconds: durationSeconds },
16
- },
17
- },
18
- };
19
-
20
- const fetchOpts = {
21
- method: 'POST',
22
- headers: { 'Content-Type': 'application/json;charset=utf8' },
23
- body: JSON.stringify(body),
24
- };
25
-
26
- const url = `${iamEndpoint}/v3.0/OS-CREDENTIAL/securitytokens`;
27
-
28
- let resp;
29
- try {
30
- resp = fetchImpl ? await fetchImpl(url, fetchOpts) : await fetchWithProxy(url, fetchOpts);
31
- } catch (error) {
32
- const err = new Error(`Token exchange request failed: ${error.message}`);
33
- err.code = 'TOKEN_EXCHANGE_FAILED';
34
- throw err;
35
- }
36
-
37
- if (resp.status === 401 || resp.status === 403) {
38
- const err = new Error('X-Auth-Token is invalid or expired.');
39
- err.code = 'TOKEN_INVALID';
40
- throw err;
41
- }
42
- if (resp.status !== 201) {
43
- const err = new Error(`Token exchange returned unexpected status ${resp.status}.`);
44
- err.code = 'TOKEN_EXCHANGE_FAILED';
45
- throw err;
46
- }
47
-
48
- let data;
49
- try {
50
- data = await resp.json();
51
- } catch (error) {
52
- const err = new Error(`Token exchange returned malformed JSON: ${error.message}`);
53
- err.code = 'TOKEN_EXCHANGE_FAILED';
54
- throw err;
55
- }
56
- const cred = data?.credential;
57
- if (!cred?.access || !cred?.secret || !cred?.securitytoken) {
58
- const err = new Error('Token exchange response missing credential fields.');
59
- err.code = 'TOKEN_EXCHANGE_FAILED';
60
- throw err;
61
- }
62
- // IAM always returns expires_at; if it is absent (or unparseable) we store
63
- // expiresAt:null, which never satisfies the `cached.expiresAt && …` guard in
64
- // getTokenCredentials — so the cache is always a miss and we deliberately
65
- // re-exchange on every call rather than risk caching credentials forever.
66
- const expiresAt = cred.expires_at ? Date.parse(cred.expires_at) : NaN;
67
- return {
68
- ak: cred.access,
69
- sk: cred.secret,
70
- securityToken: cred.securitytoken,
71
- expiresAt: Number.isFinite(expiresAt) ? expiresAt : null,
72
- };
73
- }
74
-
75
- const GRACE_MS = 60 * 1000;
76
-
77
- let cached = null; // { ak, sk, securityToken, expiresAt }
78
- let inflight = null; // Promise 单飞
79
-
80
- export function __resetTokenCache() {
81
- cached = null;
82
- inflight = null;
83
- }
84
-
85
- export async function getTokenCredentials(token, options = {}) {
86
- const now = Date.now();
87
- if (cached && cached.expiresAt && cached.expiresAt - GRACE_MS > now) {
88
- return cached;
89
- }
90
- if (inflight) return inflight;
91
- inflight = (async () => {
92
- try {
93
- cached = await exchangeTokenForCredentials(token, options);
94
- return cached;
95
- } finally {
96
- inflight = null;
97
- }
98
- })();
99
- return inflight;
100
- }