huaweicloud-devkit 1.1.7-next.1 → 1.1.7

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
@@ -332,6 +332,18 @@ npx --yes huaweicloud-devkit@latest update --target all
332
332
  config. Always keep `@latest` so npm fetches the newest version instead of a
333
333
  locally cached older one.
334
334
 
335
+ ### Configure Proxy
336
+
337
+ ```bash
338
+ npx --yes huaweicloud-devkit proxy init # configure proxy (interactive)
339
+ npx --yes huaweicloud-devkit proxy show # show effective proxy settings
340
+ npx --yes huaweicloud-devkit proxy clear # remove proxy configuration
341
+ ```
342
+
343
+ Configures HTTP/HTTPS proxy for connections to Huawei Cloud services (e.g. enterprise intranet environments). Settings are saved to `~/.config/huaweicloud/proxy.json`.
344
+
345
+ Environment variables `HTTPS_PROXY`, `HTTP_PROXY`, and `NO_PROXY` take precedence over the file configuration — use `proxy show` to inspect the effective settings.
346
+
335
347
  ## What It Does
336
348
 
337
349
  - **Guided cloud operations** — agents get step-by-step guidance for 20+ commonly used Huawei Cloud services (ECS, OBS, VPC, RDS, GaussDB, FunctionGraph, APIG, CCE, and more)
package/README.zh-CN.md CHANGED
@@ -326,6 +326,18 @@ npx --yes huaweicloud-devkit@latest update --target all
326
326
 
327
327
  `update` 是增量更新——只刷新已安装的文件,不动配置文件。请务必保留 `@latest`,确保 npm 获取最新版本而非本地缓存的旧版本。
328
328
 
329
+ ### 配置代理
330
+
331
+ ```bash
332
+ npx --yes huaweicloud-devkit proxy init # 配置代理(交互式)
333
+ npx --yes huaweicloud-devkit proxy show # 查看生效的代理设置
334
+ npx --yes huaweicloud-devkit proxy clear # 删除代理配置
335
+ ```
336
+
337
+ 为连接华为云服务配置 HTTP/HTTPS 代理(如企业内网环境)。配置保存在 `~/.config/huaweicloud/proxy.json`。
338
+
339
+ 环境变量 `HTTPS_PROXY`、`HTTP_PROXY`、`NO_PROXY` 优先级高于文件配置——可使用 `proxy show` 查看最终生效的设置。
340
+
329
341
  ## 功能特性
330
342
 
331
343
  - **引导式云操作** — Agent 获得 20+ 常用华为云服务的分步操作指引(ECS、OBS、VPC、RDS、GaussDB、FunctionGraph、APIG、CCE 等)
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.7-next.1",
4
+ "version": "1.1.7",
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.7-next.1",
20
+ "version": "1.1.7",
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.7-next.1",
23
+ "version": "1.1.7",
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.7-next.1",
20
+ "version": "1.1.7",
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.7-next.1",
3
+ "version": "1.1.7",
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.7-next.1",
3
+ "version": "1.1.7",
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.7-next.1",
5
+ "version": "1.1.7",
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,6 +85,95 @@ function present(v) {
85
85
  return typeof v === 'string' && v.length > 0 && !isPlaceholder(v);
86
86
  }
87
87
 
88
+ // Pick the Huawei Cloud DevKit MCP server entry from a map using prefix matching.
89
+ // CodeArts Work marketplace presets keyed entries like `huaweicloud-devkit_1`
90
+ // (suffix per installed instance); match by prefix so `_1`/`_2`/`HuaweiCloud DevKit`
91
+ // all resolve without hardcoding an instance number.
92
+ // Deterministically choose the Huawei Cloud DevKit MCP server entry from a map.
93
+ // Priority (stable, independent of object key order):
94
+ // 1. exact `huaweicloud-devkit` (installer-managed key)
95
+ // 2. `HuaweiCloud DevKit`
96
+ // 3. prefixed instances `huaweicloud-devkit_N` sorted by ascending N
97
+ export function pickDevkitMcpServer(mcpMap) {
98
+ if (!mcpMap || typeof mcpMap !== 'object') return null;
99
+ const keys = Object.keys(mcpMap);
100
+ const rank = (k) => {
101
+ if (k === 'huaweicloud-devkit') return -2;
102
+ if (k === 'HuaweiCloud DevKit') return -1;
103
+ const m = /^huaweicloud-devkit_(\d+)$/i.exec(k);
104
+ return m ? Number(m[1]) : Number.MAX_SAFE_INTEGER;
105
+ };
106
+ const candidates = keys
107
+ .filter((k) => /^huaweicloud-devkit(?:_|$)/i.test(k) || k === 'HuaweiCloud DevKit')
108
+ .sort((a, b) => rank(a) - rank(b));
109
+ for (const k of candidates) {
110
+ if (mcpMap[k]) return mcpMap[k];
111
+ }
112
+ return null;
113
+ }
114
+
115
+ // Derive the expiry (epoch ms) of a temporary STS credential set.
116
+ // Priority: 1) HW_STS_EXPIRES_AT env (ISO8601 or epoch seconds); 2) decode the
117
+ // security token (JWT payload or bare URL-safe base64 JSON) reading common
118
+ // expiry fields (exp, timeout_at, expires_at, id_expires_at; issued_at+duration).
119
+ // Returns null when unknown/unparseable. Never throws.
120
+ export function parseStsExpiry({ securityToken, expiresAtEnv = process.env.HW_STS_EXPIRES_AT } = {}) {
121
+ if (expiresAtEnv) {
122
+ const v = String(expiresAtEnv).trim();
123
+ if (!v) return null;
124
+ const asNum = Number(v);
125
+ if (Number.isFinite(asNum) && asNum > 0) return asNum > 1e12 ? asNum : asNum * 1000;
126
+ const t = Date.parse(v);
127
+ if (Number.isFinite(t)) return t;
128
+ return null;
129
+ }
130
+ if (!securityToken || typeof securityToken !== 'string') return null;
131
+ const token = String(securityToken).trim();
132
+ if (!token) return null;
133
+
134
+ const payload = (() => {
135
+ try {
136
+ if (token.includes('.')) {
137
+ const parts = token.split('.');
138
+ const b64 = parts.length >= 2 ? parts[1] : null;
139
+ if (!b64) return null;
140
+ const pad = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
141
+ return JSON.parse(Buffer.from(pad.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
142
+ }
143
+ const pad = token + '='.repeat((4 - (token.length % 4)) % 4);
144
+ return JSON.parse(Buffer.from(pad.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
145
+ } catch {
146
+ return null;
147
+ }
148
+ })();
149
+ if (!payload || typeof payload !== 'object') return null;
150
+
151
+ const exp = Number(payload.exp ?? payload.expires_at ?? payload.timeout_at ?? Number.NaN);
152
+ if (Number.isFinite(exp) && exp > 0) return exp * 1000;
153
+ const issued = Number(payload.issued_at ?? payload.iat ?? Number.NaN);
154
+ const duration = Number(payload.duration ?? payload.expires_in ?? payload.lifetime ?? Number.NaN);
155
+ if (Number.isFinite(issued) && issued > 0 && Number.isFinite(duration) && duration > 0) {
156
+ return (issued + duration) * 1000;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ function readWorkEnvironmentEntry(config) {
162
+ const server = pickDevkitMcpServer(config?.mcp);
163
+ if (!server?.environment) return null;
164
+ const ak = server.environment.HW_ACCESS_KEY;
165
+ const sk = server.environment.HW_SECRET_KEY;
166
+ if (present(ak) && present(sk)) {
167
+ return {
168
+ ak,
169
+ sk,
170
+ securityToken: present(server.environment.HW_SECURITY_TOKEN) ? server.environment.HW_SECURITY_TOKEN : '',
171
+ region: server.environment.HW_REGION || server.environment.HUAWEICLOUD_REGION || '',
172
+ };
173
+ }
174
+ return null;
175
+ }
176
+
88
177
  export function writeGlobalCredentials(credentials = {}) {
89
178
  const path = globalCredentialsPath();
90
179
  mkdirSync(dirname(path), { recursive: true });
@@ -136,6 +225,7 @@ export function resolveCredentials(options = {}) {
136
225
  if (codeartsCreds) {
137
226
  if (!ak && codeartsCreds.ak) ak = codeartsCreds.ak;
138
227
  if (!sk && codeartsCreds.sk) sk = codeartsCreds.sk;
228
+ if (!securityToken && codeartsCreds.securityToken) securityToken = codeartsCreds.securityToken;
139
229
  if (!region && codeartsCreds.region) region = codeartsCreds.region;
140
230
  }
141
231
 
@@ -234,7 +324,8 @@ function isCodeArtsContext() {
234
324
  return (
235
325
  existsSync(join(process.cwd(), '.codeartsdoer')) ||
236
326
  existsSync(join(homedir(), '.codeartsdoer')) ||
237
- existsSync(join(homedir(), '.codeartswork'))
327
+ existsSync(join(homedir(), '.codeartswork')) ||
328
+ existsSync(join(homedir(), '.codearts'))
238
329
  );
239
330
  }
240
331
 
@@ -248,7 +339,7 @@ export function readCodeArtsCredentials() {
248
339
  try {
249
340
  if (!existsSync(path)) continue;
250
341
  const config = JSON.parse(readFileSync(path, 'utf8'));
251
- const server = config?.mcpServers?.['huaweicloud-devkit'] || config?.mcpServers?.['HuaweiCloud DevKit'];
342
+ const server = pickDevkitMcpServer(config?.mcpServers);
252
343
  if (!server?.env) continue;
253
344
 
254
345
  const ak = server.env.HW_ACCESS_KEY;
@@ -266,26 +357,20 @@ export function readCodeArtsCredentials() {
266
357
  }
267
358
  }
268
359
 
269
- // CodeArts Work — user-level only
270
- {
271
- const path = join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json');
360
+ // CodeArts Work — user-level only.
361
+ // New layout (post platform migration): ~/.codearts/mcp/mcp_settings.json with
362
+ // prefixed keys like `huaweicloud-devkit_1`; legacy ~/.codeartswork kept as
363
+ // fallback while still in service.
364
+ const workPaths = [
365
+ join(homedir(), '.codearts', 'mcp', 'mcp_settings.json'),
366
+ join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json'),
367
+ ];
368
+ for (const path of workPaths) {
272
369
  try {
273
- if (existsSync(path)) {
274
- const config = JSON.parse(readFileSync(path, 'utf8'));
275
- const server = config?.mcp?.['huaweicloud-devkit'] || config?.mcp?.['HuaweiCloud DevKit'];
276
- if (server?.environment) {
277
- const ak = server.environment.HW_ACCESS_KEY;
278
- const sk = server.environment.HW_SECRET_KEY;
279
- if (present(ak) && present(sk)) {
280
- return {
281
- ak,
282
- sk,
283
- securityToken: present(server.environment.HW_SECURITY_TOKEN) ? server.environment.HW_SECURITY_TOKEN : '',
284
- region: server.environment.HW_REGION || server.environment.HUAWEICLOUD_REGION || '',
285
- };
286
- }
287
- }
288
- }
370
+ if (!existsSync(path)) continue;
371
+ const config = JSON.parse(readFileSync(path, 'utf8'));
372
+ const entry = readWorkEnvironmentEntry(config);
373
+ if (entry) return entry;
289
374
  } catch {
290
375
  // mcp_settings.json missing or invalid — skip
291
376
  }
@@ -8,6 +8,7 @@ import {
8
8
  globalCredentialsPath,
9
9
  isPlaceholder,
10
10
  obsConfigPath,
11
+ readCodeArtsCredentials,
11
12
  readGlobalCredentials,
12
13
  writeLastSync,
13
14
  writeObsConfig,
@@ -47,6 +48,8 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
47
48
  const creds = credentials ?? readGlobalCredentials();
48
49
  const scan = reconciled ?? exportStateForStatus();
49
50
  const s1Has = Boolean(creds?.ak && creds?.sk && !isPlaceholder(creds.ak) && !isPlaceholder(creds.sk));
51
+ const s4Creds = readCodeArtsCredentials();
52
+ const s4Has = Boolean(s4Creds?.ak && s4Creds?.sk);
50
53
  const injected = envHasRealTriplet();
51
54
  // env has real non-triplet creds that are NOT placeholders (e.g. devspace AK/SK w/o token)
52
55
  const envRealAk = !isPlaceholder(process.env.HW_ACCESS_KEY) && Boolean(process.env.HW_ACCESS_KEY);
@@ -72,6 +75,14 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
72
75
  message = 'Platform credentials are active; nothing to configure.';
73
76
  return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
74
77
  }
78
+ if (s4Has && !s1Has && !envHasCreds) {
79
+ // CodeArts Work/IDE carries the user's temporary STS credentials in the MCP
80
+ // settings file only (not surfaced in the UI, not in S1/env).
81
+ scenario = 0;
82
+ reason = 'mcp-settings-injected';
83
+ message = '已从码道 MCP settings 读取到临时凭证,MCP 工具可直接使用。';
84
+ return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
85
+ }
75
86
  if (s1Has && !envHasCreds) {
76
87
  scenario = 1;
77
88
  reason = 's1-only';
@@ -155,11 +166,14 @@ export function getAuthStatus(target = 'all') {
155
166
  const credentials = readGlobalCredentials();
156
167
  const reconciled = { ...exportStateForStatus(), runtimeActive: hasRuntimeCredentials() };
157
168
  const hcloud = probeHcloud();
169
+ const s4Creds = readCodeArtsCredentials();
158
170
  const onboarding = computeOnboarding({ credentials, reconciled });
159
171
  return {
160
172
  target,
161
173
  credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
162
174
  credentialsPath: globalCredentialsPath(),
175
+ mcpSettingsConfigured: Boolean(s4Creds?.ak && s4Creds?.sk),
176
+ mcpSettingsSource: s4Creds ? 'codearts' : null,
163
177
  obsConfigured: existsSync(obsConfigPath()),
164
178
  obsConfigPath: obsConfigPath(),
165
179
  kooCliInstalled: hcloud.installed,
@@ -7,6 +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 { findHcloudBin, resolveHcloudCommand } from './hcloud-probe.mjs';
10
+ import { parseStsExpiry, resolveCredentialsWithRuntime } from './auth/credentials.mjs';
10
11
 
11
12
  const DEFAULT_TIMEOUT_MS = 60_000;
12
13
  const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
@@ -237,6 +238,105 @@ export function classifyUnsupported(service, metaDir) {
237
238
  return 'unknown';
238
239
  }
239
240
 
241
+ // Build command-line credential-injection args when the current credentials are
242
+ // temporary STS (carry a non-empty security token). KooCLI/obsutil do not read the
243
+ // platform-injected HW_* env (link B reads only S2/S3 config), so for a live STS
244
+ // credential set that R3 forbids writing to disk, we pass it per-command instead.
245
+ // Zero-persist: nothing touches S1/S2/S3 and every invocation re-reads the current
246
+ // value (stale as soon as the token is).
247
+ // Redact executed args for MCP-visible output. Besides the generic key=value
248
+ // redaction (--cli-access-key=...), obsutil-style standalone credential flags
249
+ // (-i AK / -k SK / -t TOKEN) carry the temporary STS as following array elements,
250
+ // which redactSecrets can't match. We scrub those explicitly.
251
+ export function redactArgsWithObs(rawArgs) {
252
+ const arr = Array.isArray(rawArgs) ? rawArgs.map(String) : [];
253
+ const out = [...arr];
254
+ for (let i = 0; i < out.length; i += 1) {
255
+ if (out[i] === '-i' || out[i] === '-k' || out[i] === '-t') {
256
+ if (i + 1 < out.length) out[i + 1] = '<redacted>';
257
+ i += 1;
258
+ } else if (/^-i[A-Za-z0-9]/.test(out[i])) {
259
+ out[i] = '-i<redacted>';
260
+ } else if (/^-k[A-Za-z0-9]/.test(out[i])) {
261
+ out[i] = '-k<redacted>';
262
+ } else if (/^-t[A-Za-z0-9]/.test(out[i])) {
263
+ out[i] = '-t<redacted>';
264
+ }
265
+ }
266
+ return redactSecrets(out);
267
+ }
268
+
269
+ export function resolveStsInjectArgs(rawArgs) {
270
+ const normalized = Array.isArray(rawArgs) ? rawArgs.map(String) : [];
271
+ if (normalized.length === 0) return [];
272
+ const flat = normalized.map(String);
273
+
274
+ // Explicit kill-switch (R2): CI/ops can disable argv-injection entirely so the
275
+ // temporary STS never appears in a process list.
276
+ const flag = process.env.HUAWEICLOUD_INJECT_STS_CMD;
277
+ if (flag === '0' || flag === 'false') return [];
278
+
279
+ // Never inject for help / metadata subcommands — the flags are meaningless there.
280
+ if (flat.some((a) => a === '--help' || a === '-h' || a === 'help')) return [];
281
+
282
+ // Wrapper invocation (bash -c / sudo / sh ...): our extra args would land in the
283
+ // wrapper's argv, not hcloud's — either ineffective or misleading. Skip them.
284
+ const WRAP = new Set([
285
+ 'bash',
286
+ 'sh',
287
+ 'zsh',
288
+ 'dash',
289
+ 'bash.exe',
290
+ 'sh.exe',
291
+ '/bin/bash',
292
+ '/bin/sh',
293
+ '/bin/zsh',
294
+ '/bin/dash',
295
+ 'sudo',
296
+ ]);
297
+ const first = String(flat[0] || '').toLowerCase();
298
+ if (WRAP.has(first)) return [];
299
+
300
+ // KooCLI profile-management subcommands don't take --cli-security-token.
301
+ if (first === 'configure' || first === 'config') return [];
302
+
303
+ // If the caller already passed explicit credential flags, do not override them.
304
+ if (
305
+ flat.some(
306
+ (a) =>
307
+ a.startsWith('--cli-access-key=') || a.startsWith('--cli-secret-key=') || a.startsWith('--cli-security-token='),
308
+ )
309
+ ) {
310
+ return [];
311
+ }
312
+ // Explicit obsutil-style credentials already present — standalone (-i AK) or
313
+ // attached (-iAK) — should not be overridden by an extra injection.
314
+ if (flat.some((a) => a === '-i' || a === '-k' || a === '-t' || /^-[ikt][A-Za-z0-9]/.test(a))) return [];
315
+
316
+ let creds;
317
+ try {
318
+ creds = resolveCredentialsWithRuntime({ allowMissing: true });
319
+ } catch {
320
+ return [];
321
+ }
322
+ if (!creds || !creds.ak || !creds.sk || !creds.securityToken) return [];
323
+
324
+ // R3: if we can derive an expiry and the token is already at/within 60s of
325
+ // expiring, skip injection — using a dead token would make a doomed IAM round
326
+ // trip. If expiry is unknown/unparseable we keep the existing "inject anyway"
327
+ // behavior (can't prove it's stale).
328
+ const expiry = parseStsExpiry({ securityToken: creds.securityToken });
329
+ if (expiry !== null) {
330
+ const grace = 60 * 1000;
331
+ if (expiry <= Date.now() + grace) return [];
332
+ }
333
+
334
+ const isObs = flat[0].toUpperCase() === 'OBS';
335
+ return isObs
336
+ ? ['-i', creds.ak, '-k', creds.sk, '-t', creds.securityToken]
337
+ : ['--cli-access-key=' + creds.ak, '--cli-secret-key=' + creds.sk, '--cli-security-token=' + creds.securityToken];
338
+ }
339
+
240
340
  export function planHcloudCommand(args, options = {}) {
241
341
  const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
242
342
  const classification = classifyHcloudArgs(normalizedArgs, options);
@@ -299,6 +399,16 @@ export async function runHcloud(args, options = {}) {
299
399
  };
300
400
  assertAllowed(plan.classification);
301
401
 
402
+ // Link B: when the runtime/environment carries live temporary STS credentials
403
+ // (security token set), KooCLI/obsutil would not see them (they read S2/S3 only
404
+ // and R3 forbids writing them to disk). Inject per-command so hcloud actually
405
+ // authenticates with the platform-provided STS. Injection args are appended to
406
+ // the EXECUTED args only; classification/approval used the original args.
407
+ const stsInject = resolveStsInjectArgs(normalizedArgs);
408
+ if (stsInject.length > 0) {
409
+ plan.rawArgs = [...plan.rawArgs, ...stsInject];
410
+ }
411
+
302
412
  const metaDir = options.metaDir;
303
413
 
304
414
  // KooCLI only switches language globally (`hcloud configure set --cli-lang=cn`); there is no
@@ -411,6 +521,13 @@ function runHcloudOnce(plan, options) {
411
521
  result.stdout = String(result.stdout).slice(0, 2000) + `\n...(truncated, full output saved to ${outputFile})`;
412
522
  }
413
523
  }
524
+ // Never surface raw args (may include the injected temporary STS) to MCP
525
+ // clients / agent conversation — redact before resolve. The live `plan`
526
+ // object itself is left untouched so retries keep executing the real args.
527
+ if (result.plan && Array.isArray(result.plan.rawArgs)) {
528
+ const redactedPlan = { ...result.plan, rawArgs: redactArgsWithObs(result.plan.rawArgs) };
529
+ result.plan = redactedPlan;
530
+ }
414
531
  resolve(result);
415
532
  }
416
533
 
@@ -509,7 +626,7 @@ function quoteShellArg(value) {
509
626
  function planningWarnings(args) {
510
627
  const joined = args.join(' ');
511
628
  const warnings = [];
512
- if (/adminPass|password|passwd|secret|token/i.test(joined)) {
629
+ if (/admin[_-]?pass|password|passwd|secret|token/i.test(joined)) {
513
630
  warnings.push(
514
631
  'This command appears to contain a password or secret field. Do not leave plaintext secrets in shell history; prefer local-only input or a runtime injection pattern.',
515
632
  );
@@ -130,3 +130,26 @@ export function applyUserDelta(entry, delta, style) {
130
130
  if (delta.enabled === false) merged.enabled = false;
131
131
  return merged;
132
132
  }
133
+
134
+ // Collect user-owned environment variables from peer DevKit server entries in the
135
+ // same MCP map (e.g. market-preset keys like `huaweicloud-devkit_1` that carry the
136
+ // user's temporary STS credentials HW_ACCESS_KEY / HW_SECRET_KEY / HW_SECURITY_TOKEN).
137
+ // When the installer writes a NEW `huaweicloud-devkit` entry these must be inherited
138
+ // so the freshly installed key also has the user's credentials (the CodeArts Work
139
+ // UI never surfaces them; they live only in mcp_settings.json).
140
+ export function inheritPeerUserEnv(mcpMap) {
141
+ if (!isPlainObject(mcpMap)) return null;
142
+ const collected = {};
143
+ for (const [key, entry] of Object.entries(mcpMap)) {
144
+ if (key === 'huaweicloud-devkit') continue;
145
+ if (!/^huaweicloud-devkit(?:_|$)/i.test(key) && key !== 'HuaweiCloud DevKit') continue;
146
+ const env = isPlainObject(entry?.environment) ? entry.environment : isPlainObject(entry?.env) ? entry.env : null;
147
+ if (!env) continue;
148
+ for (const [k, v] of Object.entries(env)) {
149
+ if (REQUIRED_ENV_KEYS.has(k)) continue;
150
+ if (typeof v !== 'string' || v === '') continue;
151
+ if (!(k in collected)) collected[k] = v;
152
+ }
153
+ }
154
+ return Object.keys(collected).length > 0 ? collected : null;
155
+ }
@@ -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|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
22
+ /((?:access[_-]?key|secret[_-]?key|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|authorization|password|passwd|adminPass|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>')
@@ -65,6 +65,16 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
65
65
  err.code = code;
66
66
  err.status = resp.status;
67
67
  err.traceId = data.traceId;
68
+ if (code === 'HDKIT_CRED_INVALID') {
69
+ err.remediation = {
70
+ hint: '已保存的凭证(S1)可能已失效。请执行以下操作之一:',
71
+ steps: [
72
+ '运行 huaweicloud_auth_status 查看当前凭证状态(S1 指纹 vs 环境注入指纹)',
73
+ '若环境注入了有效凭证:运行 huaweicloud_auth_switch action=clear 清除 runtime 凭证,或删除 ~/.config/huaweicloud/credentials.json 让平台凭证接管',
74
+ '若需更新 S1:运行 npx huaweicloud-devkit auth init 重新配置有效 AK/SK',
75
+ ],
76
+ };
77
+ }
68
78
  throw err;
69
79
  }
70
80
 
@@ -119,6 +129,7 @@ export async function hdkitVoucherStatus(domainId) {
119
129
  claimed: false,
120
130
  message: error?.message || 'Incentive service unavailable, please try again later',
121
131
  code: error?.code,
132
+ ...(error?.remediation ? { remediation: error.remediation } : {}),
122
133
  };
123
134
  }
124
135
  }
@@ -132,6 +143,7 @@ export async function hdkitVoucherClaim(domainId) {
132
143
  claimed: false,
133
144
  message: error?.message || 'Incentive service unavailable, please try again later',
134
145
  code: error?.code,
146
+ ...(error?.remediation ? { remediation: error.remediation } : {}),
135
147
  };
136
148
  }
137
149
  }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ chmodSync,
2
3
  copyFileSync,
3
4
  existsSync,
4
5
  mkdirSync,
@@ -36,7 +37,13 @@ import {
36
37
  getProxySettings,
37
38
  } from './proxy/proxy-config.mjs';
38
39
  import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
39
- import { mergeCommandStyle, mergeArgsStyle, extractUserDelta, applyUserDelta } from './mcp-config-merge.mjs';
40
+ import {
41
+ mergeCommandStyle,
42
+ mergeArgsStyle,
43
+ extractUserDelta,
44
+ applyUserDelta,
45
+ inheritPeerUserEnv,
46
+ } from './mcp-config-merge.mjs';
40
47
  import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
41
48
  import { isUsableOfficeaceRoot, readOfficeaceRootMarker, writeOfficeaceRootMarker } from './officeace-paths.mjs';
42
49
  import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
@@ -123,7 +130,12 @@ function codeartsWorkSkillsDir() {
123
130
  return join(homedir(), '.codeartswork', 'skills');
124
131
  }
125
132
  function codeartsWorkMcpSettingsFile() {
126
- return join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json');
133
+ // Post platform migration the marketplace presets the plugin under ~/.codearts
134
+ // (new layout); legacy ~/.codeartswork is kept as fallback while still in
135
+ // service. Default to the new dir when it exists, otherwise legacy.
136
+ const newDir = join(homedir(), '.codearts', 'mcp');
137
+ const legacy = join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json');
138
+ return existsSync(newDir) ? join(newDir, 'mcp_settings.json') : legacy;
127
139
  }
128
140
  function codeartsWorkPluginsDir() {
129
141
  return join(homedir(), '.codeartswork', 'huaweicloud-plugins');
@@ -524,6 +536,22 @@ function copyFileVerified(src, dest) {
524
536
  );
525
537
  }
526
538
 
539
+ // R5: write an MCP settings JSON file with least-privilege permissions
540
+ // (parent dir 0700, file 0600) so the platform-injected temporary credentials
541
+ // (HW_ACCESS_KEY/HW_SECRET_KEY/HW_SECURITY_TOKEN) are not world-readable.
542
+ function writeMcpSettingsFile(configPath, config) {
543
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
544
+ writeFileSync(configPath, JSON.stringify(config, null, 2), { encoding: 'utf8', mode: 0o600 });
545
+ try {
546
+ chmodSync(configPath, 0o600);
547
+ // Re-assert the parent dir mode too — later un-mode'd mkdirSync calls can
548
+ // produce a broader default (e.g. 0775) on existing trees.
549
+ chmodSync(dirname(configPath), 0o700);
550
+ } catch {
551
+ // best-effort; unsupported on some platforms
552
+ }
553
+ }
554
+
527
555
  function copyDir(src, dest) {
528
556
  if (!existsSync(src)) return;
529
557
  mkdirSync(dest, { recursive: true });
@@ -625,7 +653,7 @@ function updateOpenCodeConfig(pluginDir) {
625
653
  }
626
654
  if (existing && changed) {
627
655
  config.mcp['huaweicloud-devkit'] = entry;
628
- writeFileSync(configPath, JSON.stringify(config, null, 2));
656
+ writeMcpSettingsFile(configPath, config);
629
657
  console.log(` OpenCode MCP config merged (user fields preserved): ${configPath}`);
630
658
  return;
631
659
  }
@@ -635,7 +663,7 @@ function updateOpenCodeConfig(pluginDir) {
635
663
  // Restore user fields saved by a previous uninstall (issue #615).
636
664
  const delta = takeAgentDelta('opencode');
637
665
  if (delta) config.mcp['huaweicloud-devkit'] = applyUserDelta(config.mcp['huaweicloud-devkit'], delta, 'command');
638
- writeFileSync(configPath, JSON.stringify(config, null, 2));
666
+ writeMcpSettingsFile(configPath, config);
639
667
  console.log(` OpenCode config updated: ${configPath}`);
640
668
  }
641
669
 
@@ -666,7 +694,7 @@ function writeMcpServersFile(pluginDest, mcpPath, agentKey) {
666
694
  if (delta)
667
695
  next.mcpServers['huaweicloud-devkit'] = applyUserDelta(next.mcpServers['huaweicloud-devkit'], delta, 'args');
668
696
  }
669
- writeFileSync(configPath, JSON.stringify(next, null, 2));
697
+ writeMcpSettingsFile(configPath, next);
670
698
  console.log(` MCP Config -> ${configPath}`);
671
699
  }
672
700
 
@@ -695,7 +723,7 @@ function removeOpenCodeConfig() {
695
723
  if (delta) saveAgentDelta('opencode', delta);
696
724
  delete config.mcp['huaweicloud-devkit'];
697
725
  if (Object.keys(config.mcp).length === 0) delete config.mcp;
698
- writeFileSync(configPath, JSON.stringify(config, null, 2));
726
+ writeMcpSettingsFile(configPath, config);
699
727
  console.log(` OpenCode MCP config cleaned: ${configPath}`);
700
728
  }
701
729
 
@@ -1267,7 +1295,7 @@ function registerCodeartsMcp(configPath, agentKey = 'codearts') {
1267
1295
  }
1268
1296
  config.mcpServers['huaweicloud-devkit'] = entry;
1269
1297
  mkdirSync(dirname(configPath), { recursive: true });
1270
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1298
+ writeMcpSettingsFile(configPath, config);
1271
1299
  console.log(` MCP config merged (user fields preserved): ${configPath}`);
1272
1300
  return;
1273
1301
  }
@@ -1280,7 +1308,7 @@ function registerCodeartsMcp(configPath, agentKey = 'codearts') {
1280
1308
  if (delta) entry = applyUserDelta(entry, delta, 'args');
1281
1309
  config.mcpServers['huaweicloud-devkit'] = entry;
1282
1310
  mkdirSync(dirname(configPath), { recursive: true });
1283
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1311
+ writeMcpSettingsFile(configPath, config);
1284
1312
  console.log(` MCP config updated: ${configPath}`);
1285
1313
  }
1286
1314
 
@@ -1375,7 +1403,7 @@ function uninstallCodeArts() {
1375
1403
  if (delta) saveAgentDelta('codearts', delta);
1376
1404
  delete config.mcpServers['huaweicloud-devkit'];
1377
1405
  if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
1378
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1406
+ writeMcpSettingsFile(configPath, config);
1379
1407
  console.log(` Config cleaned: ${configPath}`);
1380
1408
  }
1381
1409
  }
@@ -1446,7 +1474,7 @@ function registerCodeartsWorkMcp() {
1446
1474
  }
1447
1475
  config.mcp['huaweicloud-devkit'] = merged;
1448
1476
  mkdirSync(dirname(configPath), { recursive: true });
1449
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1477
+ writeMcpSettingsFile(configPath, config);
1450
1478
  console.log(` MCP config merged (user fields preserved): ${configPath}`);
1451
1479
  return;
1452
1480
  }
@@ -1454,12 +1482,16 @@ function registerCodeartsWorkMcp() {
1454
1482
  config.mcp = config.mcp || {};
1455
1483
  let entry = mergeCommandStyle(undefined, { mcpPath }).entry;
1456
1484
  entry.environment = { ...environment };
1485
+ // Inherit user-owned env (e.g. market-preset `huaweicloud-devkit_1` carrying the
1486
+ // user's temporary STS credentials) so a freshly installed key also has them.
1487
+ const peerEnv = inheritPeerUserEnv(config.mcp);
1488
+ if (peerEnv) entry.environment = { ...peerEnv, ...entry.environment };
1457
1489
  // Restore user fields saved by a previous uninstall (issue #615).
1458
1490
  const delta = takeAgentDelta('codearts-work');
1459
1491
  if (delta) entry = applyUserDelta(entry, delta, 'command');
1460
1492
  config.mcp['huaweicloud-devkit'] = entry;
1461
1493
  mkdirSync(dirname(configPath), { recursive: true });
1462
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1494
+ writeMcpSettingsFile(configPath, config);
1463
1495
  console.log(` MCP config updated: ${configPath}`);
1464
1496
  }
1465
1497
 
@@ -1530,7 +1562,7 @@ function uninstallCodeArtsWork() {
1530
1562
  if (delta) saveAgentDelta('codearts-work', delta);
1531
1563
  delete config.mcp['huaweicloud-devkit'];
1532
1564
  if (Object.keys(config.mcp).length === 0) delete config.mcp;
1533
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1565
+ writeMcpSettingsFile(configPath, config);
1534
1566
  console.log(` Config cleaned: ${configPath}`);
1535
1567
  }
1536
1568
  }
@@ -1591,7 +1623,7 @@ function ensureWorkbuddyMcpConfig() {
1591
1623
  }
1592
1624
  config.mcpServers['huaweicloud-devkit'] = entry;
1593
1625
  mkdirSync(dirname(configPath), { recursive: true });
1594
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1626
+ writeMcpSettingsFile(configPath, config);
1595
1627
  console.log(` MCP config merged (user fields preserved): ${configPath}`);
1596
1628
  return true;
1597
1629
  }
@@ -1603,7 +1635,7 @@ function ensureWorkbuddyMcpConfig() {
1603
1635
  if (delta) entry = applyUserDelta(entry, delta, 'args');
1604
1636
  config.mcpServers['huaweicloud-devkit'] = entry;
1605
1637
  mkdirSync(dirname(configPath), { recursive: true });
1606
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1638
+ writeMcpSettingsFile(configPath, config);
1607
1639
  console.log(` MCP config updated: ${configPath}`);
1608
1640
  return true;
1609
1641
  }
@@ -1817,7 +1849,7 @@ function uninstallWorkBuddy() {
1817
1849
  if (delta) saveAgentDelta('workbuddy', delta);
1818
1850
  delete config.mcpServers['huaweicloud-devkit'];
1819
1851
  if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
1820
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1852
+ writeMcpSettingsFile(configPath, config);
1821
1853
  console.log(` MCP config cleaned: ${configPath}`);
1822
1854
  }
1823
1855
  }
@@ -1923,7 +1955,7 @@ function ensureAtomcodeMcpConfig() {
1923
1955
  }
1924
1956
  config.mcpServers['huaweicloud-devkit'] = entry;
1925
1957
  mkdirSync(dirname(configPath), { recursive: true });
1926
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1958
+ writeMcpSettingsFile(configPath, config);
1927
1959
  console.log(` MCP config merged (user fields preserved): ${configPath}`);
1928
1960
  return true;
1929
1961
  }
@@ -1935,7 +1967,7 @@ function ensureAtomcodeMcpConfig() {
1935
1967
  if (delta) entry = applyUserDelta(entry, delta, 'args');
1936
1968
  config.mcpServers['huaweicloud-devkit'] = entry;
1937
1969
  mkdirSync(dirname(configPath), { recursive: true });
1938
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1970
+ writeMcpSettingsFile(configPath, config);
1939
1971
  console.log(` MCP config updated: ${configPath}`);
1940
1972
  return true;
1941
1973
  }
@@ -2009,7 +2041,7 @@ function uninstallAtomCode() {
2009
2041
  if (delta) saveAgentDelta('atomcode', delta);
2010
2042
  delete config.mcpServers['huaweicloud-devkit'];
2011
2043
  if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
2012
- writeFileSync(configPath, JSON.stringify(config, null, 2));
2044
+ writeMcpSettingsFile(configPath, config);
2013
2045
  console.log(` MCP config cleaned: ${configPath}`);
2014
2046
  }
2015
2047
  }