huaweicloud-devkit 1.1.13-next.2 → 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 +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +34 -42
- package/plugins/huaweicloud-core/src/sandbox/devbridge-key.mjs +187 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +22 -0
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +91 -46
- package/plugins/huaweicloud-core/src/tools.mjs +12 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
"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
|
+
"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.
|
|
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--
|
|
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 —
|
|
374
|
+
### Step 1: Authenticate — API Key only (AK/SK removed, auto-create fallback)
|
|
375
375
|
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
393
|
-
source /tmp/
|
|
394
|
-
if
|
|
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
|
|
411
|
+
devbridge auth status
|
|
403
412
|
```
|
|
404
413
|
|
|
405
|
-
- **`API_KEY_KEYRING`** → done
|
|
406
|
-
- **`
|
|
407
|
-
- **`
|
|
408
|
-
|
|
409
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
1291
|
-
//
|
|
1292
|
-
// (
|
|
1293
|
-
//
|
|
1294
|
-
// Tier 2
|
|
1295
|
-
// from /
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
// Every tier performs a real `devbridge auth login` and verifies with
|
|
1300
|
-
// (must print "Logged in"); on failure the waterfall falls through
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1916
|
-
'1) Re-run huaweicloud_sandbox_credentials
|
|
1917
|
-
'
|
|
1918
|
-
'
|
|
1919
|
-
'
|
|
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
|
-
{
|
|
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
|
),
|