huaweicloud-devkit 1.1.13-next.3 → 1.1.13-next.4

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.13-next.3",
4
+ "version": "1.1.13-next.4",
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.13-next.3",
20
+ "version": "1.1.13-next.4",
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.13-next.3",
23
+ "version": "1.1.13-next.4",
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.13-next.3",
20
+ "version": "1.1.13-next.4",
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.13-next.3",
3
+ "version": "1.1.13-next.4",
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.13-next.3",
3
+ "version": "1.1.13-next.4",
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.13-next.3",
5
+ "version": "1.1.13-next.4",
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.",
@@ -208,7 +208,7 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
208
208
 
209
209
  If `source /tmp/hw_creds.sh` returns empty, the credentials injection may not have completed — re-run `huaweicloud_sandbox_credentials`.
210
210
 
211
- **DevBridge API Key 前置引导(before expose, not mid-expose)**: the address-generation service (DevBridge 0.2.x) requires an API Key. Recent sandbox images auto-acquire one from the platform keyring — but if the sandbox was created on an older image (cannot auto-acquire), prepare the key at THIS stage instead of being interrupted halfway through the expose flow: create it once at https://devstation.connect.huaweicloud.com/space/devbridge/apikey (value shown only once, `devbridge_` prefix), hand it over via `export HW_API_KEY=<key>` locally (preferred) or paste it in the conversation for the `api_key` param, then run `huaweicloud_sandbox_credentials`. Full why/where/how guide: [Step 1, NO_CREDENTIAL branch](#step-1-authenticate--verified-waterfall-keyring-first).
211
+ **DevBridge API Key 前置引导(before expose, not mid-expose)**: the address-generation service (DevBridge 0.2.x) requires an API Key. Recent sandbox images auto-acquire one from the platform keyring — but if the sandbox was created on an older image (cannot auto-acquire), prepare the key at THIS stage instead of being interrupted halfway through the expose flow: create it once at https://devstation.connect.huaweicloud.com/space/devbridge/apikey (value shown only once, `devbridge_` prefix), hand it over via `export HW_API_KEY=<key>` locally (preferred) or paste it in the conversation for the `api_key` param, then run `huaweicloud_sandbox_credentials`. Full why/where/how guide: [Step 1, NO_CREDENTIAL branch](#step-1-authenticate--api-key-only-aksk-removed-auto-create-fallback).
212
212
 
213
213
  8. **Execute commands**: use `huaweicloud_sandbox_exec_with_session` for interactive work (cd, env setup), or `huaweicloud_sandbox_exec_one_shot` for long-running builds/installs. See [Tool Selection Guide](#tool-selection-guide).
214
214
  9. **Upload project** (if deploying local code): `huaweicloud_sandbox_upload_project` to transfer a local project directory into `/workspace/<name>` — uses HTTP tunnel, much faster than base64 for multi-file projects. Use `huaweicloud_sandbox_upload_file` for single files.
@@ -371,60 +371,52 @@ fi
371
371
 
372
372
  Measured (2026-10-22): an overnight reboot reverted `~/.huawei/bin/devbridge` 0.2.2 to the image's 0.1.13 — **yesterday's successful operation replayed as today's unexplained failure**. Recovery after a reboot: re-run the Step 0 install (or just re-export the PATH to the surviving `/workspace/.local/bin`), re-run `huaweicloud_sandbox_credentials` to re-inject `/tmp` credentials, and re-run `huaweicloud_sandbox_keepalive_watchdog` to restore the keepalive loop.
373
373
 
374
- ### Step 1: Authenticate — verified waterfall, keyring first
374
+ ### Step 1: Authenticate — API Key only (AK/SK removed, auto-create fallback)
375
375
 
376
- Authentication tries three credential sources in order, each verified with a real login + `auth status` before use; a failure falls through to the next tier:
376
+ DevBridge authentication is **fixed to API Key** (定稿决策,2026-11-04)。AK/SK login (`--access-key/--secret-key`) has been REMOVED — the auth waterfall no longer reads `/tmp/hw_creds.sh` for devbridge purposes.
377
377
 
378
- - **Tier 1 — keyring API Key** (preferred): recent sandbox images (hd-space-ai-shell line) get `HW_DEVBRIDGE_API_KEY` auto-injected by the platform into the sandbox keyring — **zero user action, long-lived key**. Read it via `hwcloud keyring get HW_DEVBRIDGE_API_KEY 2>&1` (⚠ the value goes to **stderr** — `2>&1` is mandatory or you silently capture an empty string) and accept only values starting with `devbridge_` (`(not found)` and error text are rejected). If the first read fails, a light revive (restart the D-Bus session bus at `unix:path=/run/dbus-session`, replace a locked keyring daemon with `printf '\n' | gnome-keyring-daemon --unlock`) retries once — no package installs.
379
- - **Tier 2 — AK/SK** (image builds, legacy sandboxes): image builds keep `--access-key/--secret-key`. STS credentials **must** include `--security-token "$HW_SECURITY_TOKEN"` or the gateway rejects the request with 401 `APIGW.0301`. **The `--huaweicloud` SSO login flag does NOT exist in sandbox devbridge 0.2.x binaries (e.g. 0.2.2)** — 文档超前于二进制:不要尝试 `--huaweicloud`,直接使用 `devbridge auth login --access-key/--secret-key`(上方脚本以 `auth login --help` 探测为准)。0.1.x binaries additionally advertised `--huaweicloud` (use it only when their help advertises it).
380
- - **Tier 3 — `/tmp/hw_api_key`** (release builds, user-provided one-time): the file is written by `huaweicloud_sandbox_credentials` from the local `HW_API_KEY` env (preferred) or the `api_key` param.
378
+ Authentication tries sources in order, each verified with a real `devbridge auth login --api-key` + `auth status` before use. Note: the probe tests keyring and file in one exec — file hits before auto-create triggers (if a user-provided key exists, it wins over auto-creation):
381
379
 
382
- `huaweicloud_sandbox_expose_tunnel` runs this waterfall automatically and reports the winning mode (`API_KEY_KEYRING` / `AKSK_SUPPORTED` / `API_KEY_FILE`) or `NO_CREDENTIAL` with a non-sensitive per-tier `attempts` diagnostic. Manual equivalent:
380
+ - **Tier 1 — keyring API Key** (preferred): read via `hwcloud keyring get HW_DEVBRIDGE_API_KEY 2>&1` (⚠ stderr). Sources: (a) self-injected by the plugin's auto-create (devbridge-key.mjs, key names `devkit-ephemeral`/`devkit-persistent` per pool); (b) platform legacy injection (compatible, not relied upon). A light revive (D-Bus restart + keyring unlock) retries once.
381
+ - **Tier 2 — `/tmp/hw_api_key` file** (manual channel): written by `huaweicloud_sandbox_credentials` from the local `HW_API_KEY` env or the `api_key` param. If a file exists, it wins over auto-create.
382
+ - **Tier 3 — auto-create fallback** (host-orchestrated): keyring/file both empty → plugin calls the DevStation relay API (`POST /api-key` via hdkitservice proxy) to create a key → injects into sandbox keyring (file fallback if keyring unavailable) → re-runs probe → reports `API_KEY_AUTO`.
383
+ - **Self-healing**: key invalid (login fails) → clear cache → recreate → inject → retry probe.
384
+
385
+ `huaweicloud_sandbox_expose_tunnel` runs this waterfall automatically and reports the winning mode (`API_KEY_KEYRING` / `API_KEY_AUTO` / `API_KEY_FILE`) or `NO_CREDENTIAL`.
386
+
387
+ **Key lifecycle** (per account × pool, fixed name):
388
+
389
+ - ephemeral pool → `devkit-ephemeral`
390
+ - persistent pool → `devkit-persistent`
391
+ - Local cache at `~/.config/huaweicloud/devbridge-api-keys.json` (0600) stores the full key (list API only returns mask — the full key is visible only at creation time)
392
+ - Cache miss → list (delete same-name orphan) → create → cache
393
+ - Key invalid (login fails) → clear cache → recreate
394
+
395
+ **Quota handling** (实测 2026-11-04): 20 key limit per account, error code `13002` → `429 HDKIT_DEVBRIDGE_KEY_QUOTA_EXCEEDED`. The structured fallback includes the current key list (with `last_used_at` for stale detection) and the console cleanup URL: https://devstation.connect.huaweicloud.com/space/devbridge/apikey. `devkit-*` prefixed keys are plugin-managed (auto-recreated after deletion) — advise users to clean OTHER stale keys first.
396
+
397
+ Manual equivalent (for debugging, not for agent use):
383
398
 
384
399
  ```bash
385
400
  export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH"
386
401
  export HOME="${HOME:-/root}"
387
- # Tier 1: platform-injected keyring key (stderr! prefix filter!)
388
402
  export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=/run/dbus-session}"
403
+ # Tier 1: keyring (stderr! prefix filter!)
389
404
  HW_DB_KEY=$(hwcloud keyring get HW_DEVBRIDGE_API_KEY 2>&1 || true)
390
405
  case "$HW_DB_KEY" in
391
406
  devbridge_*) devbridge auth login --api-key "$HW_DB_KEY" ;;
392
- *) # Tier 2: AK/SK (image builds; STS needs --security-token)
393
- source /tmp/hw_creds.sh 2>/dev/null
394
- if devbridge auth login --help 2>&1 | grep -q -- '--access-key'; then
395
- devbridge auth login --access-key "$HW_ACCESS_KEY" --secret-key "$HW_SECRET_KEY" \
396
- ${HW_SECURITY_TOKEN:+--security-token "$HW_SECURITY_TOKEN"}
397
- else # Tier 3: user-provided API Key file
398
- source /tmp/hw_api_key 2>/dev/null
399
- if [ -n "$HW_API_KEY" ]; then devbridge auth login --api-key "$HW_API_KEY"; fi
400
- fi ;;
407
+ *) # Tier 3: file fallback
408
+ source /tmp/hw_api_key 2>/dev/null
409
+ if [ -n "$HW_API_KEY" ]; then devbridge auth login --api-key "$HW_API_KEY"; fi ;;
401
410
  esac
402
- devbridge auth status # separate step: a status failure must not mask the login result
411
+ devbridge auth status
403
412
  ```
404
413
 
405
- - **`API_KEY_KEYRING`** → done, nothing was asked of the user (recent images only; the platform injects and rotates the key).
406
- - **`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 — expired STS token → re-run `huaweicloud_sandbox_credentials`.
407
- - **`NO_CREDENTIAL`** → first re-run `huaweicloud_sandbox_credentials` and retry (refreshes STS, most common quick fix). Still failing means an older sandbox whose image cannot auto-acquire (images are frozen at sandbox creation and cannot be upgraded in place) — guide the developer through a one-time API Key creation (wait for the key before continuing):
408
-
409
- **沙箱凭证与 devbridge 版本差异陷阱(实测 #935 坑2/坑3)**:
410
-
411
- - 沙箱内 devbridge 0.2.2 无技能文档所述 `--huaweicloud` 登录标志(文档超前于二进制)—— 应直接使用 `devbridge auth login --access-key/--secret-key`(`auth login --help` 探测到 `--access-key` 支持时走该分支,见上方手动等价脚本)。
412
- - 沙箱内 MCP 注入的 STS 凭证为 obsutil 加密态存储:devbridge 读到密文凭证会报 `APIGW.0301`。需先上传本地 DevKit 明文凭证库(AK 20 位 / SK 40 位,通过 `huaweicloud_sandbox_credentials` 注入),再创建隧道。
413
-
414
- 1. **Why (one sentence)**: "沙箱的地址生成服务发布版构建已移除 AK/SK 登录,仅支持 API Key(镜像内置构建仍支持 AK/SK,Agent 已自动探测)。API Key 按账号管理,创建一次长期可用、所有沙箱通用。"
415
- 2. **Where (exact steps)**: open https://devstation.connect.huaweicloud.com/space/devbridge/apikey → 登录控制台 → 选择 DevBridge 场景 → 点击"创建"。**完整值仅在创建时展示一次,立即复制**(`devbridge_` 开头)。
416
- 3. **How to hand it over** (present both, recommend the first):
417
- - **Recommended**: 在本地终端执行 `export HW_API_KEY=<粘贴Key>`(或写入 shell profile),然后告知"已设置" — Key 不经过对话记录;随后 agent 重新调用 `huaweicloud_sandbox_credentials`(本地 `HW_API_KEY` 会被自动透传注入沙箱)。
418
- - **Alternative**: 直接把 Key 粘贴在对话中 — agent 通过 `huaweicloud_sandbox_credentials` 的 `api_key` 参数注入,**绝不回显、不复述、不写入日志**。
419
- 4. **Security**: 不要提交到代码库或分享;怀疑泄露时在同页面删除并重建(1 分钟)。
420
-
421
- **Failure paths (API Key branch)**:
422
-
423
- | Symptom | Guidance |
424
- | -------------------------------------- | ---------------------------------------------------- |
425
- | `auth login` returns 401/403 | Key 已被删除或禁用 — 引导在同页面删除旧 Key 重新创建 |
426
- | 无法访问 API Key 管理页面 | 账号无该页面权限 — 引导联系账号管理员处理 |
427
- | Key 带空格/换行/缺失 `devbridge_` 前缀 | 引导重新完整复制粘贴 |
414
+ - **`API_KEY_KEYRING`** → done (zero user action; key is ours or platform legacy).
415
+ - **`API_KEY_AUTO`** → done (plugin auto-created and injected; zero user action).
416
+ - **`API_KEY_FILE`** → done (user-provided or auto-injected file fallback).
417
+ - **`NO_CREDENTIAL`** → auto-create failed. Check the error:
418
+ - **Quota full** (`HDKIT_DEVBRIDGE_KEY_QUOTA_EXCEEDED`): guide the user to clean unused keys at https://devstation.connect.huaweicloud.com/space/devbridge/apikey (the `currentKeys` field shows the list). `devkit-*` keys auto-recreate — clean other stale keys first.
419
+ - **Other errors**: one-time manual fallback — create a key at the console page, `export HW_API_KEY=<key>` locally, re-run `huaweicloud_sandbox_credentials`, then retry.
428
420
 
429
421
  ### Step 2: Expose
430
422
 
@@ -1091,7 +1083,7 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
1091
1083
  | 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 |
1092
1084
  | Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
1093
1085
  | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
1094
- | 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") |
1086
+ | devbridge auth: API Key only | DevBridge auth is fixed to API Key (AK/SK login removed, 2026-11-04). The waterfall: keyring (ours + platform legacy) → auto-create fallback → file. Use `devbridge auth login --api-key` only — never `--access-key/--secret-key`. |
1095
1087
  | 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 |
1096
1088
  | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win. `/workspace/.local/bin` survives sandbox reboots; `~/.huawei/bin` is on the system disk and is wiped on reboot (#999) |
1097
1089
  | 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 |
@@ -0,0 +1,187 @@
1
+ // DevBridge API Key 管理:本地缓存 + 孤儿清理 + 自动创建 + 两池分流。
2
+ //
3
+ // 认证架构(定稿):
4
+ // - devbridge 固定使用 API Key 认证(AK/SK 登录已移除)
5
+ // - key 来源优先级:keyring(我们注入的为主,平台遗留兼容命中)
6
+ // → 自动创建兜底 → /tmp/hw_api_key 文件(手动通道最后)
7
+ // - key 生命周期:每账号 × 每池恰 1 个固定 name key
8
+ // - ephemeral → devkit-ephemeral
9
+ // - persistent → devkit-persistent(未知池归 ephemeral)
10
+ // - 配额上限 20 个/账号(上游错误码 13002)
11
+ //
12
+ // 缓存文件 ~/.config/huaweicloud/devbridge-api-keys.json(0600):
13
+ // { [akFingerprint]: { ephemeral: "devbridge_...", persistent: "devbridge_..." } }
14
+ // 完整 key 只在创建时可见(list 仅 mask),缓存是唯一复用途径。
15
+
16
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
17
+ import { join, dirname } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+ import { createHash } from 'node:crypto';
20
+
21
+ import { hdkitCreateApiKey, hdkitListApiKeys, hdkitDeleteApiKey } from './hdkitservice-api.mjs';
22
+ import { getCredentials } from './hwlink-api.mjs';
23
+
24
+ const POOL_KEY_NAMES = {
25
+ ephemeral: 'devkit-ephemeral',
26
+ persistent: 'devkit-persistent',
27
+ };
28
+ const DEFAULT_POOL = 'ephemeral';
29
+ const CACHE_FILE = join(homedir(), '.config', 'huaweicloud', 'devbridge-api-keys.json');
30
+
31
+ /** pool → 固定 key name */
32
+ export function keyNameForPool(poolType) {
33
+ return POOL_KEY_NAMES[poolType] || POOL_KEY_NAMES[DEFAULT_POOL];
34
+ }
35
+
36
+ /** AK 指纹(缓存键,不落明文 AK) */
37
+ function akFingerprint(ak) {
38
+ return createHash('sha256').update(String(ak)).digest('hex').slice(0, 16);
39
+ }
40
+
41
+ // ── 本地缓存 ──
42
+
43
+ function readCache() {
44
+ try {
45
+ return JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ function writeCache(cache) {
52
+ const dir = dirname(CACHE_FILE);
53
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
54
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), { mode: 0o600 });
55
+ try {
56
+ chmodSync(CACHE_FILE, 0o600);
57
+ } catch {
58
+ // Windows: chmod 可能不可用
59
+ }
60
+ }
61
+
62
+ /** 读缓存中的完整 key(按 AK + 池) */
63
+ export function getCachedKey(ak, poolType) {
64
+ const cache = readCache();
65
+ return cache[akFingerprint(ak)]?.[keyNameForPool(poolType)] || '';
66
+ }
67
+
68
+ /** 写缓存(按 AK + 池) */
69
+ function setCachedKey(ak, poolType, apiKey) {
70
+ const fp = akFingerprint(ak);
71
+ const cache = readCache();
72
+ if (!cache[fp]) cache[fp] = {};
73
+ cache[fp][keyNameForPool(poolType)] = apiKey;
74
+ writeCache(cache);
75
+ }
76
+
77
+ /** 清除缓存(key 失效时自愈用) */
78
+ export function clearCachedKey(ak, poolType) {
79
+ const fp = akFingerprint(ak);
80
+ const cache = readCache();
81
+ if (cache[fp]?.[keyNameForPool(poolType)]) {
82
+ delete cache[fp][keyNameForPool(poolType)];
83
+ writeCache(cache);
84
+ }
85
+ }
86
+
87
+ // ── 自动创建链路 ──
88
+
89
+ /**
90
+ * 确保当前账号在指定池有一个可用的 DevBridge API Key。
91
+ *
92
+ * 流程:缓存命中 → 直接返回
93
+ * 无缓存 → list 删同名孤儿 → 创建 → 缓存 → 返回
94
+ *
95
+ * @param {string} poolType ephemeral/persistent(未知归 ephemeral)
96
+ * @returns {Promise<{apiKey: string, mode: string, ak: string}>}
97
+ * @throws Error 配额满(code=HDKIT_DEVBRIDGE_KEY_QUOTA_EXCEEDED)或上游错误
98
+ */
99
+ export async function ensureDevbridgeKey(poolType) {
100
+ const { ak } = getCredentials();
101
+ const pool = POOL_KEY_NAMES[poolType] ? poolType : DEFAULT_POOL;
102
+ const name = keyNameForPool(pool);
103
+
104
+ // 1. 缓存命中
105
+ const cached = getCachedKey(ak, pool);
106
+ if (cached) {
107
+ return { apiKey: cached, mode: 'CACHE_HIT', ak };
108
+ }
109
+
110
+ // 2. list → 删同名孤儿(配额安全:每池恰 1 个,1+1 配额保证同名 key 不可能被另一台活沙箱使用)
111
+ try {
112
+ const keys = await hdkitListApiKeys();
113
+ const orphan = Array.isArray(keys) ? keys.find((k) => k.name === name) : null;
114
+ if (orphan) {
115
+ await hdkitDeleteApiKey(orphan.id);
116
+ }
117
+ } catch {
118
+ // list/delete 失败不阻断创建(可能权限或网络问题,继续尝试创建)
119
+ }
120
+
121
+ // 3. 创建
122
+ const created = await hdkitCreateApiKey(name, 'devbridge');
123
+
124
+ // 4. 缓存
125
+ setCachedKey(ak, pool, created.api_key);
126
+
127
+ return { apiKey: created.api_key, mode: 'CREATED', ak };
128
+ }
129
+
130
+ /**
131
+ * key 失效自愈:清除缓存 → 走完整重建链路。
132
+ * 由沙箱内 devbridge auth login 失败触发。
133
+ */
134
+ export async function recreateDevbridgeKey(poolType) {
135
+ const { ak } = getCredentials();
136
+ clearCachedKey(ak, poolType);
137
+ return ensureDevbridgeKey(poolType);
138
+ }
139
+
140
+ // ── 配额降级数据 ──
141
+
142
+ /**
143
+ * 获取配额降级所需的 key 清单(list 成功时返回数组,含 last_used_at)。
144
+ * 用于配额满时给用户展示可清理的 key。
145
+ */
146
+ export async function listKeysForQuotaHint() {
147
+ try {
148
+ const keys = await hdkitListApiKeys();
149
+ if (!Array.isArray(keys)) return [];
150
+ return keys.map((k) => ({
151
+ name: k.name,
152
+ id: k.id,
153
+ lastUsed: k.last_used_at || '',
154
+ }));
155
+ } catch {
156
+ return [];
157
+ }
158
+ }
159
+
160
+ // ── keyring 注入命令构建 ──
161
+
162
+ /**
163
+ * 构建沙箱内 keyring 注入命令(exec 到沙箱执行)。
164
+ * keyring 不可用时降级写 /tmp/hw_api_key 文件。
165
+ */
166
+ export function buildKeyringInjectScript(apiKey) {
167
+ return [
168
+ `export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH"`,
169
+ `export HOME="\${HOME:-/root}"`,
170
+ // 尝试 keyring set(hwcloud 支持 set 时优先)
171
+ `KEY_INJECTED=0`,
172
+ `if command -v hwcloud >/dev/null 2>&1; then`,
173
+ ` export DBUS_SESSION_BUS_ADDRESS="\${DBUS_SESSION_BUS_ADDRESS:-unix:path=/run/dbus-session}"`,
174
+ ` if ! pgrep -x dbus-daemon >/dev/null 2>&1 && command -v dbus-daemon >/dev/null 2>&1; then`,
175
+ ` dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --fork --nopidfile >/dev/null 2>&1 || true`,
176
+ ` sleep 1`,
177
+ ` fi`,
178
+ ` hwcloud keyring set HW_DEVBRIDGE_API_KEY '${apiKey}' 2>/dev/null && KEY_INJECTED=1`,
179
+ `fi`,
180
+ // keyring 不可用 → 降级写文件(0600,与手动通道同路径但内容是我们注入的自动 key)
181
+ `if [ "$KEY_INJECTED" -eq 0 ]; then`,
182
+ ` echo "export HW_API_KEY='${apiKey}'" > /tmp/hw_api_key`,
183
+ ` chmod 600 /tmp/hw_api_key`,
184
+ `fi`,
185
+ `echo "DB_KEY_INJECTED=$KEY_INJECTED"`,
186
+ ].join('\n');
187
+ }
@@ -168,6 +168,28 @@ export async function hdkitCredentials(sessionId, devStageId, enableSts = true)
168
168
  return await hdkitRequest('POST', 'credentials', body);
169
169
  }
170
170
 
171
+ // ── DevBridge API Key relay 代理(经 hdkitservice 透传 DevStation relay 接口)──
172
+ // 凭证由 hdkitRequest 内部 getCredentials() 统一解析,调用方无需传
173
+
174
+ /**
175
+ * 创建 DevBridge API Key。响应含完整 api_key(仅创建时可见)。
176
+ * 配额满(13002)时服务端返回 429 HDKIT_DEVBRIDGE_KEY_QUOTA_EXCEEDED,
177
+ * hdkitRequest 抛错 err.code 携带该码。
178
+ */
179
+ export async function hdkitCreateApiKey(name, scope = 'devbridge') {
180
+ return await hdkitRequest('POST', 'api-key', { name, scope });
181
+ }
182
+
183
+ /** 列出当前账号所有 API Key(仅 mask + last_used_at)。 */
184
+ export async function hdkitListApiKeys() {
185
+ return await hdkitRequest('GET', 'api-keys', undefined, 30000);
186
+ }
187
+
188
+ /** 按 keyId 删除 API Key(上游 DELETE 方法)。 */
189
+ export async function hdkitDeleteApiKey(keyId) {
190
+ return await hdkitRequest('DELETE', `api-keys/${encodeURIComponent(keyId)}`, undefined, 30000);
191
+ }
192
+
171
193
  export async function hdkitVoucherStatus(domainId) {
172
194
  try {
173
195
  const path = domainId ? `voucher/status?domain_id=${encodeURIComponent(domainId)}` : 'voucher/status';
@@ -795,8 +795,13 @@ export function resolveNginxConfigPath(configName, project) {
795
795
  // DB_NGINX_CONFIG_MISSING=1 — the target config file does not exist
796
796
  // DB_NGINX_T=PASS|FAIL — nginx -t verdict, with failure output echoed
797
797
  // as `DB_NGINX_T_OUTPUT: ...` lines
798
+ // #946 (regression, 2026-11-03 E2E): strip OSC/CSI escapes + \r BEFORE the
799
+ // anchored ^DB_NGINX_ lookups — integration-enabled shells wrap every line in
800
+ // \x1b]133;C\x07, which used to blind the nginxTest match and turn a PASS into
801
+ // "nginx -t did not pass" (deploy_nginx ok:false false negative; the config was
802
+ // written and nginx -t actually passed — measured twice on real sandboxes).
798
803
  export function parseDeployNginxVerifyOutput(stdout) {
799
- const text = String(stdout || '');
804
+ const text = stripTerminalEscapes(stdout);
800
805
  const configMissing = /DB_NGINX_CONFIG_MISSING=1/.test(text);
801
806
  const nginxTest = (text.match(/^DB_NGINX_T=(\w+)$/m) || [])[1] || '';
802
807
  const nginxTestOutput = (text.match(/^DB_NGINX_T_OUTPUT: .*$/gm) || [])
@@ -1287,18 +1292,21 @@ export async function closeSession(workspaceId, username) {
1287
1292
 
1288
1293
  // Build a shell snippet that authenticates devbridge via a verified waterfall:
1289
1294
  // Tier 1 keyring API Key — hwcloud keyring get HW_DEVBRIDGE_API_KEY (platform-injected
1290
- // on hd-space-ai-shell images; the value goes to STDERR so 2>&1 is mandatory and
1291
- // the devbridge_ prefix filters "(not found)" / error text). A light revive step
1292
- // (restart the D-Bus session bus + re-unlock the keyring daemon, no dnf) retries
1293
- // once when the first read fails. Preferred: devbridge is deprecating AK/SK auth.
1294
- // Tier 2 AK/SK — image builds keep --access-key/--secret-key; STS credentials
1295
- // from /tmp/hw_creds.sh need --security-token or the gateway rejects with 401
1296
- // APIGW.0301; 0.1.x binaries additionally advertise --huaweicloud in help.
1297
- // Tier 3 /tmp/hw_api_key — release builds accept only --api-key; the file is written by
1298
- // huaweicloud_sandbox_credentials from HW_API_KEY / the api_key param.
1299
- // Every tier performs a real `devbridge auth login` and verifies with `auth status`
1300
- // (must print "Logged in"); on failure the waterfall falls through to the next tier.
1295
+ // on hd-space-ai-shell images, or self-injected by devbridge-key.mjs; the value
1296
+ // goes to STDERR so 2>&1 is mandatory and the devbridge_ prefix filters
1297
+ // "(not found)" / error text). A light revive step (restart the D-Bus session
1298
+ // bus + re-unlock the keyring daemon, no dnf) retries once when the first read fails.
1299
+ // Tier 2 /tmp/hw_api_key — file channel: written by huaweicloud_sandbox_credentials
1300
+ // from HW_API_KEY / the api_key param (user-provided), or by the auto-create
1301
+ // fallback (devbridge-key.mjs) when keyring injection is unavailable.
1302
+ // AK/SK login (--access-key/--secret-key) was REMOVED: devbridge authentication is
1303
+ // now fixed to API Key only (定稿决策,2026-11-04).
1304
+ // Every tier performs a real `devbridge auth login --api-key` and verifies with
1305
+ // `auth status` (must print "Logged in"); on failure the waterfall falls through.
1301
1306
  // DB_ATTEMPT_* lines are non-sensitive diagnostics for support escalation.
1307
+ // The HOST orchestrates an auto-create fallback between probe rounds: when the
1308
+ // first probe returns NO_CREDENTIAL, the host calls ensureDevbridgeKey() and
1309
+ // injects the key (keyring preferred, file fallback), then runs a second probe.
1302
1310
  export function buildDevbridgeAuthProbe() {
1303
1311
  return [
1304
1312
  // #999: /workspace/.local/bin FIRST — /workspace is the data disk and
@@ -1355,29 +1363,7 @@ export function buildDevbridgeAuthProbe() {
1355
1363
  ` db_diag KEYRING "no-valid-key"`,
1356
1364
  `fi`,
1357
1365
  ``,
1358
- `# ---- Tier 2: AK/SK from /tmp/hw_creds.sh (image builds; STS needs --security-token) ----`,
1359
- `if [ "$DB_OK" -eq 0 ]; then`,
1360
- ` source /tmp/hw_creds.sh 2>/dev/null`,
1361
- ` if devbridge auth login --help 2>&1 | grep -q -- '--access-key' && [ -n "$HW_ACCESS_KEY" ]; then`,
1362
- ` DB_TOKEN_ARGS=""`,
1363
- ` [ -n "$HW_SECURITY_TOKEN" ] && DB_TOKEN_ARGS="--security-token \\"$HW_SECURITY_TOKEN\\""`,
1364
- ` if devbridge auth login --help 2>&1 | grep -q -- '--huaweicloud'; then`,
1365
- ` devbridge auth login --huaweicloud --access-key "$HW_ACCESS_KEY" --secret-key "$HW_SECRET_KEY" $DB_TOKEN_ARGS >/tmp/db_auth.log 2>&1`,
1366
- ` else`,
1367
- ` devbridge auth login --access-key "$HW_ACCESS_KEY" --secret-key "$HW_SECRET_KEY" $DB_TOKEN_ARGS >/tmp/db_auth.log 2>&1`,
1368
- ` fi`,
1369
- ` if db_login_ok; then`,
1370
- ` echo "DB_AUTH_MODE=AKSK_SUPPORTED"`,
1371
- ` DB_OK=1`,
1372
- ` else`,
1373
- ` db_diag AKSK "login-failed"`,
1374
- ` fi`,
1375
- ` else`,
1376
- ` db_diag AKSK "binary-or-creds-unavailable"`,
1377
- ` fi`,
1378
- `fi`,
1379
- ``,
1380
- `# ---- Tier 3: API Key from /tmp/hw_api_key (release builds, user-provided) ----`,
1366
+ `# ---- Tier 2: API Key from /tmp/hw_api_key (file channel; auto-injected or user-provided) ----`,
1381
1367
  `if [ "$DB_OK" -eq 0 ]; then`,
1382
1368
  ` source /tmp/hw_api_key 2>/dev/null`,
1383
1369
  ` if [ -n "$HW_API_KEY" ]; then`,
@@ -1537,8 +1523,11 @@ export function buildDevbridgeVerifyScript(tunnelUrl) {
1537
1523
  // Each probe echo is `DB_PROBE_<n>=<code>`; reachability is decided by the
1538
1524
  // LAST probe — intermediate 502/000 probes are gateway-not-ready noise, not
1539
1525
  // failures, so they must not leak into the tool's verdict.
1526
+ // #946 (regression): strip OSC/CSI escapes + \r first — integration-enabled
1527
+ // shells wrap every line in \x1b]133;C\x07 and blind the anchored
1528
+ // ^DB_PROBE_ match (probes came back empty, finalHttpCode always '').
1540
1529
  export function parseDevbridgeProbeSequence(stdout) {
1541
- const text = String(stdout || '');
1530
+ const text = stripTerminalEscapes(stdout);
1542
1531
  const probes = [];
1543
1532
  for (const m of text.matchAll(/^DB_PROBE_(\d+)=(\S*)$/gm)) {
1544
1533
  probes.push({ attempt: Number(m[1]), httpCode: m[2] });
@@ -1873,7 +1862,7 @@ export async function installKeepaliveWatchdog(workspaceId, { tunnels }, usernam
1873
1862
  // session-surviving URL from one that dies with the deploying session.
1874
1863
  export async function exposeTunnel(
1875
1864
  workspaceId,
1876
- { port, expiryHours, keepalive },
1865
+ { port, expiryHours, keepalive, poolType, ensureDevbridgeKey, buildKeyringInjectScript, listKeysForQuotaHint },
1877
1866
  username = 'root',
1878
1867
  timeoutMs = 90000,
1879
1868
  ) {
@@ -1904,7 +1893,66 @@ export async function exposeTunnel(
1904
1893
  };
1905
1894
  }
1906
1895
 
1907
- if (authMode !== 'API_KEY_KEYRING' && authMode !== 'AKSK_SUPPORTED' && authMode !== 'API_KEY_FILE') {
1896
+ // Auth waterfall: probe → (auto-create fallback) → expose loop
1897
+ // 失效自愈:probe 报 login-failed(key 被删/过期)→ 清缓存 → 重建 → 注入 → 重跑 probe
1898
+ let effectiveAuthMode = authMode;
1899
+ const isLoginFailed =
1900
+ authMode !== 'API_KEY_KEYRING' && authMode !== 'API_KEY_FILE' && attempts.KEYRING === 'login-failed';
1901
+
1902
+ if (isLoginFailed && ensureDevbridgeKey) {
1903
+ // key 失效(缓存里有 key 但 login 失败)→ 清缓存重建
1904
+ try {
1905
+ const { recreateDevbridgeKey } = await import('./devbridge-key.mjs');
1906
+ const { apiKey } = await recreateDevbridgeKey(poolType || 'ephemeral');
1907
+ const injectScript = buildKeyringInjectScript(apiKey);
1908
+ await execOneShot(workspaceId, injectScript, username, 15000);
1909
+ const healResult = await execOneShot(workspaceId, authScript, username, 30000);
1910
+ const healParsed = parseDevbridgeAuthOutput(healResult.stdout);
1911
+ if (healParsed.authMode === 'API_KEY_KEYRING' || healParsed.authMode === 'API_KEY_FILE') {
1912
+ effectiveAuthMode = 'API_KEY_AUTO';
1913
+ }
1914
+ } catch {
1915
+ // 自愈失败 → 走泛化降级
1916
+ }
1917
+ }
1918
+
1919
+ if (effectiveAuthMode !== 'API_KEY_KEYRING' && effectiveAuthMode !== 'API_KEY_FILE' && ensureDevbridgeKey) {
1920
+ // keyring/文件均无 key → host 调 relay API 自动创建 → 注入沙箱 → 重跑 probe
1921
+ try {
1922
+ const { apiKey } = await ensureDevbridgeKey(poolType || 'ephemeral');
1923
+ const injectScript = buildKeyringInjectScript(apiKey);
1924
+ await execOneShot(workspaceId, injectScript, username, 15000);
1925
+ const authResult2 = await execOneShot(workspaceId, authScript, username, 30000);
1926
+ const parsed2 = parseDevbridgeAuthOutput(authResult2.stdout);
1927
+ if (parsed2.authMode === 'API_KEY_KEYRING' || parsed2.authMode === 'API_KEY_FILE') {
1928
+ effectiveAuthMode = 'API_KEY_AUTO';
1929
+ }
1930
+ } catch (error) {
1931
+ // 自动创建失败(含配额满 13002)→ 结构化降级
1932
+ if (error.code === 'HDKIT_DEVBRIDGE_KEY_QUOTA_EXCEEDED') {
1933
+ const keysHint = listKeysForQuotaHint ? await listKeysForQuotaHint() : [];
1934
+ return {
1935
+ ok: false,
1936
+ error: error.message,
1937
+ error_code: 'HDKIT_DEVBRIDGE_KEY_QUOTA_EXCEEDED',
1938
+ authMode: authMode || 'UNKNOWN',
1939
+ attempts,
1940
+ currentKeys: keysHint,
1941
+ hint:
1942
+ 'API Key 数量已达上限。请到控制台清理不再使用的 key:' +
1943
+ 'https://devstation.connect.huaweicloud.com/space/devbridge/apikey\n' +
1944
+ 'devkit-* 开头的是插件自动管理的 key(删除后会自动重建),可优先清理其他闲置 key。',
1945
+ };
1946
+ }
1947
+ // 其他自动创建失败 → 继续走原有降级路径(可能 keyring/file 后续可用)
1948
+ }
1949
+ }
1950
+
1951
+ if (
1952
+ effectiveAuthMode !== 'API_KEY_KEYRING' &&
1953
+ effectiveAuthMode !== 'API_KEY_FILE' &&
1954
+ effectiveAuthMode !== 'API_KEY_AUTO'
1955
+ ) {
1908
1956
  const loginOutput = String(authResult.stdout || '') + String(authResult.stderr || '');
1909
1957
  return {
1910
1958
  ok: false,
@@ -1912,14 +1960,11 @@ export async function exposeTunnel(
1912
1960
  authMode: authMode || 'UNKNOWN',
1913
1961
  attempts,
1914
1962
  hint:
1915
- 'No DevBridge credential is available in this sandbox. Suggested next steps:\n' +
1916
- '1) Re-run huaweicloud_sandbox_credentials once, then retry huaweicloud_sandbox_expose_tunnel — ' +
1917
- 'this refreshes the temporary AK/SK (most common quick fix when they simply expired) and rewrites /tmp/hw_creds.sh.\n' +
1918
- '2) If it still fails, this sandbox was created on an older image and cannot auto-acquire an API Key ' +
1919
- '(the image cannot be upgraded in place). One-time manual fallback: create an API Key at ' +
1920
- 'https://devstation.connect.huaweicloud.com/space/devbridge/apikey, run `export HW_API_KEY=<key>` locally, ' +
1921
- 're-run huaweicloud_sandbox_credentials, then retry.\n' +
1922
- '3) The "attempts" field shows which tiers were tried and why they failed — include it when escalating to the platform team.',
1963
+ 'No DevBridge API Key is available in this sandbox. Suggested next steps:\n' +
1964
+ '1) Re-run huaweicloud_sandbox_credentials, then retry huaweicloud_sandbox_expose_tunnel.\n' +
1965
+ '2) If auto-creation failed due to quota (20 key limit), clean up unused keys at ' +
1966
+ 'https://devstation.connect.huaweicloud.com/space/devbridge/apikey, then retry.\n' +
1967
+ '3) The "attempts" field shows which tiers were tried and why they failed — include it when escalating.',
1923
1968
  loginOutput,
1924
1969
  };
1925
1970
  }
@@ -31,9 +31,11 @@ import {
31
31
  validatePoolType,
32
32
  rememberWorkspacePool,
33
33
  enrichConnectResult,
34
+ poolContextFor,
34
35
  withPoolContext,
35
36
  forgetWorkspacePool,
36
37
  } from './sandbox/sandbox-pool.mjs';
38
+ import { ensureDevbridgeKey, buildKeyringInjectScript, listKeysForQuotaHint } from './sandbox/devbridge-key.mjs';
37
39
  import {
38
40
  hdkitCheckUser,
39
41
  hdkitSignAgreement,
@@ -1651,7 +1653,16 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1651
1653
  sandboxWsId9,
1652
1654
  await exposeTunnel(
1653
1655
  sandboxWsId9,
1654
- { port: args.port, expiryHours: args.expiry_hours, keepalive: args.keepalive },
1656
+ {
1657
+ port: args.port,
1658
+ expiryHours: args.expiry_hours,
1659
+ keepalive: args.keepalive,
1660
+ // DevBridge API Key 自动创建兜底(认证固定 apikey,AK/SK 已移除)
1661
+ poolType: poolContextFor(sandboxWsId9)?.poolMode || 'ephemeral',
1662
+ ensureDevbridgeKey,
1663
+ buildKeyringInjectScript,
1664
+ listKeysForQuotaHint,
1665
+ },
1655
1666
  sandboxUser9,
1656
1667
  sandboxTimeout9,
1657
1668
  ),