huaweicloud-devkit 1.1.9-next.4 → 1.1.9-next.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.4-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.6-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **[中文](README.zh-CN.md) | English**
10
10
 
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.4-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.6-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **中文 | [English](README.md)**
10
10
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
3
  "mcpName": "io.github.huaweicloud/huaweicloud-devkit",
4
- "version": "1.1.9-next.4",
4
+ "version": "1.1.9-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.9-next.4",
20
+ "version": "1.1.9-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.9-next.4",
23
+ "version": "1.1.9-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.9-next.4",
20
+ "version": "1.1.9-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.9-next.4",
3
+ "version": "1.1.9-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.9-next.4",
3
+ "version": "1.1.9-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.9-next.4",
5
+ "version": "1.1.9-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.",
@@ -429,6 +429,12 @@
429
429
  "field": "text",
430
430
  "regex": "\\bfdisk\\b"
431
431
  }
432
+ ],
433
+ "none": [
434
+ {
435
+ "field": "text",
436
+ "regex": "\\bhcloud\\b"
437
+ }
432
438
  ]
433
439
  },
434
440
  "message": "The sandbox terminal command is destructively dangerous and could destroy the workspace.",
@@ -393,6 +393,49 @@ 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()) return null;
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
+ return Number.isFinite(t) ? t : null;
437
+ }
438
+
396
439
  export function lastSyncPath() {
397
440
  return join(baseHome(), '.config', 'huaweicloud', '.last_sync');
398
441
  }
@@ -0,0 +1,100 @@
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
+ }
@@ -7,7 +7,13 @@ 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 { parseStsExpiry, resolveCredentialsWithRuntime } from './auth/credentials.mjs';
10
+ import {
11
+ parseStsExpiry,
12
+ resolveCredentialsWithRuntime,
13
+ resolveXAuthToken,
14
+ resolveXAuthTokenExpiry,
15
+ } from './auth/credentials.mjs';
16
+ import { getTokenCredentials } from './auth/token-exchange.mjs';
11
17
 
12
18
  const DEFAULT_TIMEOUT_MS = 60_000;
13
19
  const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
@@ -297,7 +303,7 @@ export function redactArgsWithObs(rawArgs) {
297
303
  return redactSecrets(out);
298
304
  }
299
305
 
300
- export function resolveStsInjectArgs(rawArgs) {
306
+ export async function resolveStsInjectArgs(rawArgs) {
301
307
  const normalized = Array.isArray(rawArgs) ? rawArgs.map(String) : [];
302
308
  if (normalized.length === 0) return [];
303
309
  const flat = normalized.map(String);
@@ -350,7 +356,25 @@ export function resolveStsInjectArgs(rawArgs) {
350
356
  } catch {
351
357
  return [];
352
358
  }
353
- if (!creds || !creds.ak || !creds.sk || !creds.securityToken) return [];
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 [];
354
378
 
355
379
  // R3: if we can derive an expiry and the token is already at/within 60s of
356
380
  // expiring, skip injection — using a dead token would make a doomed IAM round
@@ -468,7 +492,22 @@ export async function runHcloud(args, options = {}) {
468
492
  // and R3 forbids writing them to disk). Inject per-command so hcloud actually
469
493
  // authenticates with the platform-provided STS. Injection args are appended to
470
494
  // the EXECUTED args only; classification/approval used the original args.
471
- const stsInject = resolveStsInjectArgs(normalizedArgs);
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
+ }
472
511
  if (stsInject.length > 0) {
473
512
  plan.rawArgs = [...plan.rawArgs, ...stsInject];
474
513
  }
@@ -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|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
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,
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|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
42
+ /((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|x[_-]?subject[_-]?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>')