huaweicloud-devkit 1.1.7-next.0 → 1.1.7-next.2

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.7-next.0",
4
+ "version": "1.1.7-next.2",
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.0",
20
+ "version": "1.1.7-next.2",
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.0",
23
+ "version": "1.1.7-next.2",
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.0",
20
+ "version": "1.1.7-next.2",
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.0",
3
+ "version": "1.1.7-next.2",
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.0",
3
+ "version": "1.1.7-next.2",
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.0",
5
+ "version": "1.1.7-next.2",
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.",
@@ -264,32 +264,45 @@ fi
264
264
  - **Old tunnels do not survive the upgrade**: 0.1.x-created tunnels are not registered on the s2 gateway — always rebuild the tunnel after upgrading (the expose flow below does this via pre-cleanup).
265
265
  - If `10006: quota exceeded` appears after upgrading, stale pre-upgrade tunnels still count against the quota — `devbridge delete-all` and retry.
266
266
 
267
- ### Step 1: Authenticate with an API Key (0.2.x removed AK/SK login)
267
+ ### Step 1: Authenticate — probe the build's auth capability first
268
268
 
269
- devbridge 0.2.x removed `--access-key/--secret-key/--huaweicloud`. The only non-interactive auth is a DevBridge API Key. AK/SK (`/tmp/hw_creds.sh`) remains valid for hcloud, but not for devbridge.
269
+ `0.2.2-release` ships as **two different builds** with identical version strings:
270
270
 
271
- **Check, then login if the key exists** (the API Key is a long-lived account-level credential stored in its own file `/tmp/hw_api_key`, separate from the temporary AK/SK in `/tmp/hw_creds.sh` — never echo its value):
271
+ - **Image builds** (sandboxes created 2026-09+): retain AK/SK login (`--access-key/--secret-key`) AND auto-read `HW_ACCESS_KEY`/`HW_SECRET_KEY` env vars — fully automatic, no API Key needed.
272
+ - **Release builds** (GitHub/GitCode downloads, incl. the Step 0 upgrade artifact): AK/SK removed, only API Key works.
273
+
274
+ Version numbers cannot distinguish them — **probe the binary's capability at runtime** and branch:
272
275
 
273
276
  ```bash
277
+ export PATH="$HOME/.huawei/bin:$PATH"
274
278
  source /tmp/hw_creds.sh 2>/dev/null
275
- source /tmp/hw_api_key 2>/dev/null
276
- if [ -n "$HW_API_KEY" ]; then
277
- devbridge auth login --api-key "$HW_API_KEY" && devbridge auth status
279
+ if devbridge auth login --help 2>&1 | grep -q -- '--access-key'; then
280
+ echo "AUTH_MODE=AKSK_SUPPORTED"
281
+ devbridge auth login --access-key "$HW_ACCESS_KEY" --secret-key "$HW_SECRET_KEY"
282
+ devbridge auth status # separate step: a status failure must not mask the login result
278
283
  else
279
- echo "NO_API_KEY"
284
+ echo "AUTH_MODE=API_KEY_ONLY"
285
+ source /tmp/hw_api_key 2>/dev/null
286
+ if [ -n "$HW_API_KEY" ]; then
287
+ devbridge auth login --api-key "$HW_API_KEY"
288
+ devbridge auth status # separate step: a status failure must not mask the login result
289
+ else
290
+ echo "NO_API_KEY"
291
+ fi
280
292
  fi
281
293
  ```
282
294
 
283
- **If `NO_API_KEY`** — STOP and guide the developer through creating one (wait for the key before continuing):
295
+ - **`AUTH_MODE=AKSK_SUPPORTED`** → done. The temporary AK/SK injected by `huaweicloud_sandbox_credentials` is used directly (validated against IAM before injection). If login fails, surface the CLI error — AK/SK was IAM-validated at injection time, so failures here are rare (expired STS token → re-run `huaweicloud_sandbox_credentials`).
296
+ - **`AUTH_MODE=API_KEY_ONLY`** → the API Key is a long-lived account-level credential stored in its own file `/tmp/hw_api_key`, separate from the temporary AK/SK in `/tmp/hw_creds.sh` — never echo its value. On `NO_API_KEY`, STOP and guide the developer through creating one (wait for the key before continuing):
284
297
 
285
- 1. **Why (one sentence)**: "沙箱的地址生成服务已升级到 0.2.x,新版本仅支持 API Key 登录(官方变更)。API Key 按账号管理,创建一次长期可用、所有沙箱通用。"
298
+ 1. **Why (one sentence)**: "沙箱的地址生成服务发布版构建已移除 AK/SK 登录,仅支持 API Key(镜像内置构建仍支持 AK/SK,Agent 已自动探测)。API Key 按账号管理,创建一次长期可用、所有沙箱通用。"
286
299
  2. **Where (exact steps)**: open https://devstation.connect.huaweicloud.com/space/devbridge/apikey → 登录控制台 → 选择 DevBridge 场景 → 点击"创建"。**完整值仅在创建时展示一次,立即复制**(`devbridge_` 开头)。
287
300
  3. **How to hand it over** (present both, recommend the first):
288
301
  - **Recommended**: 在本地终端执行 `export HW_API_KEY=<粘贴Key>`(或写入 shell profile),然后告知"已设置" — Key 不经过对话记录;随后 agent 重新调用 `huaweicloud_sandbox_credentials`(本地 `HW_API_KEY` 会被自动透传注入沙箱)。
289
302
  - **Alternative**: 直接把 Key 粘贴在对话中 — agent 通过 `huaweicloud_sandbox_credentials` 的 `api_key` 参数注入,**绝不回显、不复述、不写入日志**。
290
303
  4. **Security**: 不要提交到代码库或分享;怀疑泄露时在同页面删除并重建(1 分钟)。
291
304
 
292
- **Failure paths**:
305
+ **Failure paths (API Key branch)**:
293
306
 
294
307
  | Symptom | Guidance |
295
308
  | -------------------------------------- | ---------------------------------------------------- |
@@ -913,36 +926,36 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
913
926
 
914
927
  ## Critical Warnings
915
928
 
916
- | Trap | Why |
917
- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
918
- | Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
919
- | Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
920
- | Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
921
- | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
922
- | devbridge 0.2.x needs an API Key | 0.2.x removed AK/SK login (`--access-key/--secret-key/--huaweicloud` are gone). Login with `--api-key "$HW_API_KEY"` from `/tmp/hw_api_key` (long-lived credential, stored separately from `/tmp/hw_creds.sh`). If missing, guide the developer to create one (see Step 1 of "Expose the deployed app") |
923
- | devbridge 0.1.x is dead | Sandboxes created before Sep 2026 ship 0.1.13, which connects to a migrated gateway serving a 「服务已迁移」 placeholder with HTTP 200. Check `devbridge version` first and upgrade in place (Step 0) — old tunnels never survive the upgrade |
924
- | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win |
925
- | Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment |
926
- | Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
927
- | Deploy is not just nginx | Configuring nginx does NOT complete the deployment. Steps 7 (DevBridge expose) and deploy_check are REQUIRED — `deploy_nginx` returns `nextStep: expose_via_devbridge` as a reminder. Do not stop after nginx. |
928
- | Call deploy_check before success | Always call `huaweicloud_sandbox_deploy_check` before reporting deployment success. A green nginx status does not mean the tunnel is accessible — verify end-to-end with the tool. |
929
- | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
930
- | Long commands prefer one-shot | `exec_one_shot` creates a fresh connection per call — more stable for builds, installs, and scripts >30s. See [Tool Selection Guide](#tool-selection-guide). |
931
- | SSR nginx/Node ports must differ | nginx `proxy_pass` targets `<nodePort>`, not `<port>`. `deploy_nginx` auto-defaults `nodePort` to `<port>+1` — always start the Node process with `PORT=<nodePort>` to match. Same-port = EADDRINUSE. |
932
- | HTTP 200 ≠ correct content | A green HTTP check does not guarantee the right project is serving — old processes from a previous session bound to the same port will still return 200. `deploy_check` verifies the deployment fingerprint to catch this. |
933
- | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
934
- | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
935
- | Projects live in `/workspace` | Clone/install project code under `/workspace/<repo-name>` (filesystem-root workspace mount, not `$HOME/workspace`), never in `/tmp` — ephemeral locations lose the project when the sandbox session restarts |
936
- | Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects |
937
- | Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB) |
938
- | Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket); if Node.js is missing, install it from the Huawei Cloud mirror (see "Node.js in the sandbox") |
939
- | Sandbox restart kills processes | After sandbox restarts, all user processes (nginx, Node.js, Python servers) are stopped. Re-run startup commands and verify ports are listening before proceeding. |
940
- | Cross-platform binaries incompatible | The sandbox runs Linux. Native binaries built on Windows/macOS (e.g., Prisma client, `node_modules/.prisma/`, platform-specific native addons) will not execute. Always install and build dependencies inside the sandbox, not locally. |
941
- | Cross-platform needs QR code | When `detect_framework` returns `type: "cross-platform"` (Taro, uni-app), generating a QR code image is **mandatory** — the deployment is incomplete without it. Check the Deployment Completion Check table in Step 7. |
942
- | Build fails do NOT auto-fix | When a build exits with non-zero exit code, STOP and present the error + fix options to the developer. Do not silently retry, modify configs, or change source files without explicit approval. See 4c-aux. |
943
- | Tunnel description: no spaces/hyphens | `devbridge create <name> -d 'hello world'` fails with `Invalid tunnel description: only Chinese characters, digits, letters, length 0-64`. The message is misleading — the real rule is no spaces/hyphens. Omit `-d` or use bare letters/digits. |
944
- | Use `host`, never `connect` | `devbridge connect` is the sender side (for on-prem machines dialing out). For public preview it loops `Connection failed, retrying...` forever with no hint. Always use `devbridge host <tunnelId> -p <port>`. |
945
- | Port drift detaches the tunnel | When deploy_nginx auto-increments to a free port, an existing tunnel stays bound to the old port — re-bind: `devbridge port create <tunnelId> -p <newPort> --protocol http -a` and restart `devbridge host`. deploy_nginx emits a warning when this happens. |
929
+ | Trap | Why |
930
+ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
931
+ | Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
932
+ | Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
933
+ | Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
934
+ | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
935
+ | devbridge auth: probe the build | `0.2.2-release` has two builds: image builds retain AK/SK login + env auto-read (fully automatic via `/tmp/hw_creds.sh`); release builds accept only API Key (`/tmp/hw_api_key`). Never assume — probe with `devbridge auth login --help 2>&1 \| grep -q -- '--access-key'` (Step 1 of "Expose the deployed app") |
936
+ | devbridge 0.1.x is dead | Sandboxes created before Sep 2026 ship 0.1.13, which connects to a migrated gateway serving a 「服务已迁移」 placeholder with HTTP 200. Check `devbridge version` first and upgrade in place (Step 0) — old tunnels never survive the upgrade |
937
+ | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win |
938
+ | Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment |
939
+ | Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
940
+ | Deploy is not just nginx | Configuring nginx does NOT complete the deployment. Steps 7 (DevBridge expose) and deploy_check are REQUIRED — `deploy_nginx` returns `nextStep: expose_via_devbridge` as a reminder. Do not stop after nginx. |
941
+ | Call deploy_check before success | Always call `huaweicloud_sandbox_deploy_check` before reporting deployment success. A green nginx status does not mean the tunnel is accessible — verify end-to-end with the tool. |
942
+ | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
943
+ | Long commands prefer one-shot | `exec_one_shot` creates a fresh connection per call — more stable for builds, installs, and scripts >30s. See [Tool Selection Guide](#tool-selection-guide). |
944
+ | SSR nginx/Node ports must differ | nginx `proxy_pass` targets `<nodePort>`, not `<port>`. `deploy_nginx` auto-defaults `nodePort` to `<port>+1` — always start the Node process with `PORT=<nodePort>` to match. Same-port = EADDRINUSE. |
945
+ | HTTP 200 ≠ correct content | A green HTTP check does not guarantee the right project is serving — old processes from a previous session bound to the same port will still return 200. `deploy_check` verifies the deployment fingerprint to catch this. |
946
+ | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
947
+ | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
948
+ | Projects live in `/workspace` | Clone/install project code under `/workspace/<repo-name>` (filesystem-root workspace mount, not `$HOME/workspace`), never in `/tmp` — ephemeral locations lose the project when the sandbox session restarts |
949
+ | Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects |
950
+ | Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB) |
951
+ | Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket); if Node.js is missing, install it from the Huawei Cloud mirror (see "Node.js in the sandbox") |
952
+ | Sandbox restart kills processes | After sandbox restarts, all user processes (nginx, Node.js, Python servers) are stopped. Re-run startup commands and verify ports are listening before proceeding. |
953
+ | Cross-platform binaries incompatible | The sandbox runs Linux. Native binaries built on Windows/macOS (e.g., Prisma client, `node_modules/.prisma/`, platform-specific native addons) will not execute. Always install and build dependencies inside the sandbox, not locally. |
954
+ | Cross-platform needs QR code | When `detect_framework` returns `type: "cross-platform"` (Taro, uni-app), generating a QR code image is **mandatory** — the deployment is incomplete without it. Check the Deployment Completion Check table in Step 7. |
955
+ | Build fails do NOT auto-fix | When a build exits with non-zero exit code, STOP and present the error + fix options to the developer. Do not silently retry, modify configs, or change source files without explicit approval. See 4c-aux. |
956
+ | Tunnel description: no spaces/hyphens | `devbridge create <name> -d 'hello world'` fails with `Invalid tunnel description: only Chinese characters, digits, letters, length 0-64`. The message is misleading — the real rule is no spaces/hyphens. Omit `-d` or use bare letters/digits. |
957
+ | Use `host`, never `connect` | `devbridge connect` is the sender side (for on-prem machines dialing out). For public preview it loops `Connection failed, retrying...` forever with no hint. Always use `devbridge host <tunnelId> -p <port>`. |
958
+ | Port drift detaches the tunnel | When deploy_nginx auto-increments to a free port, an existing tunnel stays bound to the old port — re-bind: `devbridge port create <tunnelId> -p <newPort> --protocol http -a` and restart `devbridge host`. deploy_nginx emits a warning when this happens. |
946
959
 
947
960
  ## Node.js in the sandbox
948
961
 
@@ -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
  }
@@ -1500,10 +1500,10 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1500
1500
  if (sandboxWsIdCred) result.apiKeyInjected = Boolean(apiKey);
1501
1501
  if (apiKey) {
1502
1502
  result.apiKeyHint =
1503
- 'DevBridge API Key written to /tmp/hw_api_key (0600, kept separate from the temporary AK/SK in /tmp/hw_creds.sh — it is a long-lived account-level credential). devbridge 0.2.x uses it via: source /tmp/hw_api_key && devbridge auth login --api-key "$HW_API_KEY". Never echo it into logs.';
1503
+ 'DevBridge API Key written to /tmp/hw_api_key (0600, kept separate from the temporary AK/SK in /tmp/hw_creds.sh — it is a long-lived account-level credential). Release builds of devbridge 0.2.x use it via: source /tmp/hw_api_key && devbridge auth login --api-key "$HW_API_KEY". Image builds retain AK/SK login — the huawei-sandbox skill probes the capability at expose time. Never echo the key into logs.';
1504
1504
  } else {
1505
1505
  result.apiKeyHint =
1506
- 'No DevBridge API Key provided — devbridge 0.2.x cannot log in with AK/SK. To expose web apps, ask the user for an API Key (created at https://devstation.connect.huaweicloud.com/space/devbridge/apikey) and re-run with api_key, or set the local HW_API_KEY environment variable (preferred — keeps the key out of the conversation).';
1506
+ 'No DevBridge API Key provided — release builds of devbridge 0.2.x cannot log in with AK/SK (image builds retain AK/SK; the huawei-sandbox skill probes the build at expose time and uses the injected AK/SK directly when supported). For release builds, ask the user for an API Key (created at https://devstation.connect.huaweicloud.com/space/devbridge/apikey) and re-run with api_key, or set the local HW_API_KEY environment variable (preferred — keeps the key out of the conversation).';
1507
1507
  }
1508
1508
  if (validation.projectId) result.projectId = validation.projectId;
1509
1509
  if (validation.warning) result.warning = validation.warning;