huaweicloud-devkit 1.1.13-next.0 → 1.1.13-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.13-next.0",
4
+ "version": "1.1.13-next.2",
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.0",
20
+ "version": "1.1.13-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.13-next.0",
23
+ "version": "1.1.13-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.13-next.0",
20
+ "version": "1.1.13-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.13-next.0",
3
+ "version": "1.1.13-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.13-next.0",
3
+ "version": "1.1.13-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.13-next.0",
5
+ "version": "1.1.13-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.",
@@ -47,6 +47,36 @@ Hard rules:
47
47
 
48
48
  Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace terminal execution. Covers sandbox lifecycle via hdkitservice API and remote terminal command execution via hwlink protocol.
49
49
 
50
+ ## 两池沙箱(pool_type)
51
+
52
+ > 口径基调:一切以实测为准,口头支持不作数(下表生命周期为实测/平台侧现状,平台调参后本节随之更新)。
53
+
54
+ `huaweicloud_sandbox_connect` 用 `pool_type` 选择资源池(服务端权威映射模板;**没有 persist_mode 参数**——上游参数面仅 template_id/flavor_id):
55
+
56
+ | pool_type | 语义 | 生命周期(已实测) | 适用 |
57
+ | ------------------- | ------------------ | ---------------------------------------------------------- | --------------------------- |
58
+ | `ephemeral`(默认) | 非持久沙箱 | 闲置 30 分钟自动关机 + 闲置 1 小时自动删除(平台定时任务) | 临时运行/预览,勿存重要数据 |
59
+ | `persistent` | 持久沙箱(EVS 盘) | 数据跨关机保留;闲置回收保留期上限 7 天 | 需要跨会话保留数据 |
60
+
61
+ Selection rules(agent 按意图自动选池,开发者可覆盖;意图不明确时呈现两类供选择):
62
+
63
+ | 意图 / 场景 | pool_type | 理由 |
64
+ | ------------------------- | ---------- | ----------------------------------- |
65
+ | 预览 / 试用 / demo | ephemeral | 用完即弃,无需保留状态 |
66
+ | 一次性任务(跑完即弃) | ephemeral | 结果带出沙箱;实例随闲置回收 |
67
+ | 敏感内容(即焚) | ephemeral | 闲置 1 小时自动删除,数据随实例消亡 |
68
+ | 开发工作台 / workbench | persistent | 会话跨天;状态需跨关机保留 |
69
+ | 带数据库项目(DB-backed) | persistent | 数据落在挂载的 EVS 盘(32G) |
70
+ | 跨日评审 / 展示 | persistent | 评审者后续日期回访;实例保留 |
71
+
72
+ - **配额**:每用户每类型 1 台(1+1 可并存);同池 connect 自动复用已有实例
73
+ - **429 双错误码**:`HDKIT_QUOTA_EXCEEDED`(账号配额满,重试无效→引导释放/复用);`HDKIT_CAPACITY_INSUFFICIENT`(池容量不足→建议错峰重试)。响应带 `Retry-After`
74
+ - **TTL 播报**:connect 与执行类工具返回值带池模式/生命周期提示——**ephemeral 池部署的公网地址会随沙箱闲置关机失效**,向开发者交付 URL 时必须附带该提示
75
+ - **烧核播报口径**:沙箱运行消耗核时,播报用「平台赠送 + 活动赚取」口径(非直接计费);账号核时耗尽(`HD.97520422`)时工具返回值给出友好引导
76
+ - **删除纪律**:不向开发者暴露生命周期删除 API(回收走平台闲置策略 G1);开发者主动删除时必须二次确认(确认要删除?数据不可恢复);绝不做延后代删
77
+ - 显式 `template_id` 优先于 `pool_type`(自定义模板场景)
78
+ - 完整口径、容量规划与 G 清单:[references/resource-pool.md](references/resource-pool.md)
79
+
50
80
  ## Activation
51
81
 
52
82
  - **Proactive offering**: when the developer's task needs a temporary runtime (e.g. "deploy this web app", "run this app and preview it"), offer the sandbox proactively — the developer never has to say "use sandbox". Prompt: "This task can be satisfied by a sandbox — use it?"
@@ -74,10 +104,10 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
74
104
 
75
105
  ### Sandbox Lifecycle
76
106
 
77
- | Tool | Purpose |
78
- | --------------------------------- | ------------------------------------------------------------------------ |
79
- | `huaweicloud_sandbox_connect` | Connect to sandbox (one user one instance, reuses existing if available) |
80
- | `huaweicloud_sandbox_credentials` | Inject temporary AK/SK into a running sandbox |
107
+ | Tool | Purpose |
108
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
109
+ | `huaweicloud_sandbox_connect` | Connect to sandbox (one instance per user per pool; `pool_type` selects ephemeral/persistent, default ephemeral) |
110
+ | `huaweicloud_sandbox_credentials` | Inject temporary AK/SK into a running sandbox |
81
111
 
82
112
  ### Tunnel Persistence (public URL lifetime)
83
113
 
@@ -133,6 +163,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
133
163
 
134
164
  **Step 0 — Target-Selection Gate (MANDATORY)**: before any sandbox lifecycle tool, confirm the target. If the developer asked to deploy/host/publish a site without naming a target, STOP and run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) first; do NOT call `huaweicloud_sandbox_connect` until it is resolved. Proceed with this workflow only if the target is already confirmed as sandbox (or a temporary runtime).
135
165
 
166
+ **Step 0.3 — Resource pool selection**: once the target is sandbox, pick the pool type per the [两池沙箱(pool_type)](#两池沙箱pool_type) selection rules (auto by intent, developer may override; 1 instance per type, same type reuses). Check the task survives the chosen type's lifecycle window (ephemeral: idle 30min shutdown + 1h delete; persistent: 7-day idle reclaim cap) before connecting.
167
+
136
168
  **Step 0.5 — Stack-Selection Gate (MANDATORY)**: before uploading or writing any application code, confirm the backend stack via the [Stack-Selection Gate](#stack-selection-gate-mandatory--do-not-skip). 工作台/workbench/全栈/full-stack/CRUD/持久化 ⇒ full-stack (PostgreSQL + PostgREST default) — do NOT start an SPA-only deployment for a workbench request.
137
169
 
138
170
  Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
@@ -1049,6 +1081,7 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
1049
1081
 
1050
1082
  - [Framework Commands](references/framework-commands.md) — command mapping for all supported frameworks
1051
1083
  - [Nginx Templates](references/nginx-templates.md) — nginx configuration templates and fallback
1084
+ - [Resource Pool](references/resource-pool.md) — 资源池定稿口径:两沙箱类型、申请/限流参数(持久 2U4G×300 + 4U8G×200、非持久 2U4G×1000、overlayer 5GB/实例、EVS 300TB)、配额 1/类型 与 429+Retry-After 语义
1052
1085
 
1053
1086
  ## Critical Warnings
1054
1087
 
@@ -0,0 +1,77 @@
1
+ # 两池沙箱资源池口径(v3,2026-11 定稿)
2
+
3
+ > 权威实现:hdkitservice(pool_type 服务端权威解析 + 池感知复用 + 429 双错误码)+ 本插件 `sandbox-pool.mjs`。
4
+ > 本文件是口径快照,与实现冲突时以最新实测为准(基调:一切以实测为准,口头支持不作数)。
5
+ > 本版合并并取代 #1071 旧口径(非持久 3h 固定 / 持久 14 天保留 / 三信号 / 关机前通知均已作废)。
6
+
7
+ ## 机制
8
+
9
+ - **池 = 平台预置模板**:非持久 `9a108ab1bc354bc4979b949b36e3c8c1` / 持久 `05860f507ee94930844907a3a136d1a7` / flavor 3U6G `23ebdfe3b2d34cc4b34a1aaf03d27c54`(服务端 `POOL_TEMPLATE_*` 环境变量维护)
10
+ - 客户端传 `pool_type`(ephemeral/persistent),**服务端权威映射** template_id;显式 `template_id` 优先(自定义模板)
11
+ - **无 persist_mode 请求参数**(需求方两次纠正);闲置语义由平台按模板配置,上游参数面仅 name/template_id/flavor_id/source/env/git
12
+ - 实例名前缀即池归属:`hcdke*`(非持久)/ `hcdkp*`(持久);同池才复用。遗留 `hcdk*` 老实例不参与池复用(老模板后续可能无对应资源池,处置待定)
13
+
14
+ ## 两类沙箱
15
+
16
+ | 维度 | 非持久化 (ephemeral) | 持久化 (persistent) |
17
+ | -------- | --------------------------------------- | -------------------------------------------- |
18
+ | 定位 | 预览/试用/一次性任务/敏感即焚 | 开发工作台/带数据库项目/跨日评审展示 |
19
+ | 生命周期 | 闲置 30min 自动关机 + 1h 删除(已实测) | 闲置回收保留期上限 7 天(资源组 DB 硬约束) |
20
+ | 存储 | 无持久盘 | 挂 EVS 32G |
21
+ | 配额 | 每用户 1 台(同类复用) | 每用户 1 台(同类复用;可与 1 台非持久并存) |
22
+
23
+ ## 生命周期(实测口径)
24
+
25
+ | 项 | 口径 | 状态 |
26
+ | --------------- | ---------------------------------------------- | ------------- |
27
+ | 非持久闲置策略 | 30min 自动关机 + 1h 删除(平台定时任务) | ✅ 已验证生效 |
28
+ | 持久闲置回收 | 保留期上限 7 天(原 14 天口径作废) | 平台侧 |
29
+ | 活性检测 | 当前仅 connect 单信号(三信号为后续诉求 G2) | 现状 |
30
+ | 删除/关机前通知 | 不支持(G6 诉求中)→ 客户端 TTL 播报为补偿方案 | 现状 |
31
+ | 两池实际差异 | 仅 EVS 32GiB 数据盘(非持久池无 disks 字段) | ✅ 已实证 |
32
+
33
+ ## 容量规划(原始申请口径)
34
+
35
+ | 资源 | 申请容量 |
36
+ | ----------------- | ----------- |
37
+ | 非持久化 2U4G | 1000 实例 |
38
+ | 持久化 2U4G | 300 实例 |
39
+ | 持久化 4U8G | 200 实例 |
40
+ | overlayer | 5 GB / 实例 |
41
+ | EVS(持久化挂盘) | 300 TB 总量 |
42
+
43
+ > 注意:实际池 flavor 为 **3U6G**(需求方 2026-11-02 指定,尾号 54),上述 2U4G/4U8G 为原始申请口径,容量数字需按 3U6G 重新核算。
44
+
45
+ ## 配额与错误码
46
+
47
+ - 每用户每类型 1 台(同类复用,ephemeral + persistent 可并存 1+1);账号并发上限 5
48
+ - 429 双错误码(均带 `Retry-After: 60`):
49
+ - `HDKIT_QUOTA_EXCEEDED`:用户配额满(上游 `HD.98320022`)→ 重试无效,引导释放/复用已有实例
50
+ - `HDKIT_CAPACITY_INSUFFICIENT`:池容量不足(上游 `HD.99003001`/`HD.99008001`/`HD.98330021`)→ 建议错峰重试
51
+ - 烧核:`HD.97520422` = 账号核心时长耗尽(沙箱运行消耗核时;客户端对含该码的错误输出友好播报)
52
+
53
+ ## 烧核播报口径
54
+
55
+ 沙箱消耗核时(烧核)时向用户播报:核时来源于**平台赠送 + 活动赚取**(非直接计费口径)。
56
+
57
+ ## 客户端行为(本插件)
58
+
59
+ - `sandbox_connect` 传 `pool_type`;非法值本地即报错(不发无效请求);未传由服务端默认 ephemeral
60
+ - connect 返回值富化 `pool` 块(TTL 播报,仅服务端回显 pool_type 时);exec/upload/deploy/expose 返回值带 `poolMode`/`persistenceHint`
61
+ - 429 结构化 `remediation`(配额满/容量不足差异化引导步骤)+ `retryAfter`
62
+ - 钉扎(可选,运维场景):`HW_POOL_TEMPLATE_EPHEMERAL`/`HW_POOL_TEMPLATE_PERSISTENT` 环境变量——设置后客户端附带映射 template_id 上送;日常部署无需配置(模板 ID 由服务端权威维护)
63
+
64
+ ## 平台侧关键依赖(G 清单,非 devkit 交付)
65
+
66
+ - G1 闲置删除策略配置(资源组,驱动 7 天上限回收)
67
+ - G2 活性检测(当前仅 connect 单信号;三信号为后续诉求)
68
+ - G3 状态查询 API(未落地,继续提)
69
+ - G4 销户开机处置(已完成待测试)
70
+ - G5 销户实例回收(已完成待测试)
71
+ - G6 删除/关机前通知(诉求中)
72
+
73
+ ## 已知边界
74
+
75
+ - 池模板当前为**白名单条件实测**(仅对测试账号加白),公开上线后需复测模板 ID 与口径
76
+ - 上游 429 真实响应结构未实测:服务端映射先按已知错误码实现,联调实测后校准
77
+ - 沙箱实例状态机外的过渡态(`cde.1000/1002` 排队中)由服务端等待超时兜底
@@ -3,6 +3,7 @@ import { getProxyDispatcher } from '../proxy/proxy-agent.mjs';
3
3
  import { cacheUserHash } from '../telemetry/telemetry.mjs';
4
4
  import { detectAgent } from '../telemetry/agent-detect.mjs';
5
5
  import { readInstalledVersion } from '../update-check.mjs';
6
+ import { pinnedTemplateFor } from './sandbox-pool.mjs';
6
7
 
7
8
  function getHdkitBaseUrl() {
8
9
  return process.env.HDKITSERVICE_ENDPOINT || 'https://devkit.huaweicloud.com/rest/developer/server/hdkitservice/';
@@ -78,6 +79,43 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
78
79
  ],
79
80
  };
80
81
  }
82
+ // 429 双错误码结构化处理(两池配额契约):配额满(用户侧,重试无效)与
83
+ // 池容量不足(平台侧,错峰重试)给出差异化引导;携带服务端 Retry-After
84
+ if (code === 'HDKIT_QUOTA_EXCEEDED') {
85
+ err.retryAfter = resp.headers.get('Retry-After') || undefined;
86
+ err.remediation = {
87
+ hint: '沙箱并发配额已满(每账号最多 5 台、每类型 1 台),重试无效:',
88
+ steps: [
89
+ '确认已有实例是否够用:connect 对同池实例自动复用,无需重复创建',
90
+ '释放不再使用的沙箱(控制台或平台回收)后重试',
91
+ '如需两种生命周期语义并存:ephemeral 与 persistent 各可持有 1 台',
92
+ ],
93
+ };
94
+ }
95
+ if (code === 'HDKIT_CAPACITY_INSUFFICIENT') {
96
+ err.retryAfter = resp.headers.get('Retry-After') || undefined;
97
+ err.remediation = {
98
+ hint: '平台池容量暂时不足(非账号问题):',
99
+ steps: [
100
+ '稍后错峰重试(devkit提供的免费资源紧张,建议错峰使用)',
101
+ '急需使用时可尝试另一 pool_type(另一池可能有余量)',
102
+ ],
103
+ };
104
+ }
105
+ // 烧核播报(缺口#5 代码层):账号核心时长耗尽——上游码 HD.97520422 经
106
+ // hdkitservice 以 HDKIT_UPSTREAM_ERROR 包装透传,客户端按消息内错误码识别。
107
+ // 仅在前面的分支未设置 remediation 时生效(else-if 语义:烧核不覆盖 429 双码/凭证指引)
108
+ if (!err.remediation && String(data.message || '').includes('HD.97520422')) {
109
+ err.coreHoursExhausted = true;
110
+ err.remediation = {
111
+ hint: '账号核心时长(核时)已耗尽,沙箱无法继续运行:',
112
+ steps: [
113
+ '核时按沙箱运行时长消耗,当前账号余额为 0',
114
+ '更换有核时余额的账号,或等待/申请核时补充',
115
+ '详见华为云开发者空间核心时长说明',
116
+ ],
117
+ };
118
+ }
81
119
  throw err;
82
120
  }
83
121
 
@@ -107,6 +145,13 @@ export async function hdkitConnect(options = {}) {
107
145
  if (options.git) body.git = options.git;
108
146
  if (options.template_id) body.template_id = options.template_id;
109
147
  if (options.flavor_id) body.flavor_id = options.flavor_id;
148
+ if (options.pool_type) {
149
+ // 两池:pool_type 由服务端权威映射(不传默认 ephemeral)。
150
+ // 可选运维钉扎:配置 HW_POOL_TEMPLATE_* 时附带映射模板(服务端与老服务端均按显式模板处理)
151
+ body.pool_type = options.pool_type;
152
+ const pinned = pinnedTemplateFor(options.pool_type);
153
+ if (pinned && !body.template_id) body.template_id = pinned;
154
+ }
110
155
 
111
156
  return await hdkitRequest('POST', 'connect', body);
112
157
  }
@@ -0,0 +1,104 @@
1
+ // 两池沙箱(pool_type)客户端支持。
2
+ // 机制(定稿):池 = 平台预置模板,客户端传 pool_type(ephemeral/persistent),
3
+ // 服务端权威映射 template_id;显式 template_id 优先(运维钉扎场景)。
4
+ // - 非持久池(默认):闲置 30min 自动关机 + 1h 删除(平台定时任务,已实测生效)
5
+ // - 持久池:EVS 数据盘跨关机保留,闲置回收保留期上限 7 天
6
+ // - 配额:每用户每类型 1 台(同类复用),账号并发上限 5
7
+
8
+ const POOL_TYPES = new Set(['ephemeral', 'persistent']);
9
+
10
+ /** workspaceId → poolType 缓存:exec/upload/deploy 等工具返回值据此附带池上下文 */
11
+ const workspacePool = new Map();
12
+
13
+ /**
14
+ * 校验并归一化 pool_type;未传时返回 null(交由服务端默认 ephemeral,不在客户端伪造语义)。
15
+ * @throws Error 非法值(ephemeral/persistent 之外)
16
+ */
17
+ export function validatePoolType(raw) {
18
+ if (raw === undefined || raw === null || raw === '') return null;
19
+ const normalized = String(raw).trim().toLowerCase();
20
+ if (!POOL_TYPES.has(normalized)) {
21
+ throw new Error(
22
+ `invalid pool_type "${raw}" (expected ephemeral or persistent). ` +
23
+ 'ephemeral = 非持久沙箱(闲置 30min 自动关机、1h 删除);persistent = 持久沙箱(数据盘保留,闲置回收上限 7 天)',
24
+ );
25
+ }
26
+ return normalized;
27
+ }
28
+
29
+ /**
30
+ * 运维钉扎(可选):设置 HW_POOL_TEMPLATE_EPHEMERAL/PERSISTENT 后,客户端把映射出的
31
+ * template_id 与 pool_type 一并上送——服务端(及未升级的老服务端)按显式模板处理。
32
+ * 日常部署无需配置:模板 ID 由服务端 POOL_TEMPLATE_* 环境变量权威维护。
33
+ */
34
+ export function pinnedTemplateFor(poolType) {
35
+ if (poolType === 'ephemeral') return process.env.HW_POOL_TEMPLATE_EPHEMERAL || '';
36
+ if (poolType === 'persistent') return process.env.HW_POOL_TEMPLATE_PERSISTENT || '';
37
+ return '';
38
+ }
39
+
40
+ /** 记录 workspace 的池归属(来自服务端 connect 响应的 pool_type 回显) */
41
+ export function rememberWorkspacePool(workspaceId, poolType) {
42
+ if (workspaceId && poolType) workspacePool.set(workspaceId, poolType);
43
+ }
44
+
45
+ /** 清理 workspace 池缓存(测试用) */
46
+ export function forgetWorkspacePool(workspaceId) {
47
+ if (workspaceId === undefined) workspacePool.clear();
48
+ else workspacePool.delete(workspaceId);
49
+ }
50
+
51
+ /**
52
+ * exec/upload/deploy 等工具返回值的池上下文(缺口#2 模式提示)。
53
+ * 未记录池归属(老服务端/自定义模板)时返回 undefined,不附加任何字段。
54
+ */
55
+ export function poolContextFor(workspaceId) {
56
+ const poolType = workspacePool.get(workspaceId);
57
+ if (poolType === 'ephemeral') {
58
+ return {
59
+ poolMode: 'ephemeral',
60
+ persistenceHint: '非持久沙箱:闲置 30 分钟自动关机、1 小时自动删除,重要数据请及时保存或迁出',
61
+ };
62
+ }
63
+ if (poolType === 'persistent') {
64
+ return {
65
+ poolMode: 'persistent',
66
+ persistenceHint: '持久沙箱:数据盘跨关机保留,闲置回收保留期最长 7 天',
67
+ };
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ /**
73
+ * connect 结果池富化(缺口#1 TTL 播报)。
74
+ * 仅当服务端回显 pool_type 才播报(两种状态制:服务端未确认池语义不编造)。
75
+ * 钉扎/自定义模板(服务端回显 null)不播报。
76
+ * 读取做 pool_type/poolType 双读兜底(与 devStageId 双读同型,防 wire 命名漂移)。
77
+ */
78
+ export function enrichConnectResult(result) {
79
+ if (!result || typeof result !== 'object') return result;
80
+ const poolType = result.pool_type || result.poolType;
81
+ if (poolType === 'ephemeral') {
82
+ result.pool = {
83
+ pool_type: 'ephemeral',
84
+ lifecycle: 'idle 30min auto-shutdown + 1h auto-delete (platform scheduled task, verified)',
85
+ ttl_hint:
86
+ '非持久沙箱:闲置 30 分钟将自动关机、闲置 1 小时将自动删除(平台定时任务已生效)。' +
87
+ '重要数据请及时保存或迁出;需要跨会话保留数据请改用 pool_type=persistent。',
88
+ };
89
+ } else if (poolType === 'persistent') {
90
+ result.pool = {
91
+ pool_type: 'persistent',
92
+ lifecycle: 'EVS data disk retained; idle reclaim within 7 days max',
93
+ ttl_hint: '持久沙箱:数据盘跨关机保留,闲置回收保留期最长 7 天;长期不用请自行导出数据。',
94
+ };
95
+ }
96
+ return result;
97
+ }
98
+
99
+ /** 池信息字段并入既有对象结果(非对象结果原样返回) */
100
+ export function withPoolContext(workspaceId, result) {
101
+ const ctx = poolContextFor(workspaceId);
102
+ if (!ctx || !result || typeof result !== 'object') return result;
103
+ return { ...result, ...ctx };
104
+ }
@@ -27,6 +27,13 @@ import {
27
27
  getCurrentWorkspaceId,
28
28
  setWorkspaceId,
29
29
  } from './sandbox/session-manager.mjs';
30
+ import {
31
+ validatePoolType,
32
+ rememberWorkspacePool,
33
+ enrichConnectResult,
34
+ withPoolContext,
35
+ forgetWorkspacePool,
36
+ } from './sandbox/sandbox-pool.mjs';
30
37
  import {
31
38
  hdkitCheckUser,
32
39
  hdkitSignAgreement,
@@ -623,7 +630,8 @@ export const TOOL_DEFINITIONS = [
623
630
  },
624
631
  {
625
632
  name: 'huaweicloud_sandbox_close_session',
626
- description: 'Close the persistent terminal session for a workspace.',
633
+ description:
634
+ 'Close the persistent terminal session for a workspace. This closes the agent-side terminal session only — it does NOT delete the sandbox instance. Resource-pool sandbox deletion is developer-initiated (always ask for a second confirmation before guiding a delete) or handled by the platform resource-group idle-reclaim policy within the 7-day cap (v3 caliber); devkit never defers or performs deletion on the user behalf.',
627
635
  inputSchema: {
628
636
  type: 'object',
629
637
  properties: {
@@ -876,7 +884,7 @@ export const TOOL_DEFINITIONS = [
876
884
  {
877
885
  name: 'huaweicloud_sandbox_connect',
878
886
  description:
879
- 'Connect to a sandbox via hdkitservice. One user one instance - reuses existing sandbox if available, otherwise creates a new one. Returns session_id, dev_stage_id, connection_id, connection_address, and expiresAt (STS credential expiry).',
887
+ 'Connect to a sandbox via hdkitservice. One instance per user per pool — reuses the same-pool sandbox if available, otherwise creates a new one. Quota: 1 instance per pool type (a user may hold 1 ephemeral AND 1 persistent simultaneously; same-type requests reuse the existing instance). On quota exhaustion the API returns HTTP 429 with a Retry-After header and a dual error code: HDKIT_QUOTA_EXCEEDED (retry useless — release or reuse the existing instance) vs HDKIT_CAPACITY_INSUFFICIENT (pool capacity; retry later / off-peak). Returns session_id, dev_stage_id, connection_id, connection_address, expiresAt (STS credential expiry), and pool_type/template_id echo. Ephemeral pool (default): idle auto-shutdown after 30min, auto-delete after 1h — do not keep important data there. Persistent pool: data disk retained (EVS 32G), idle reclaim capped at 7 days.',
880
888
  inputSchema: {
881
889
  type: 'object',
882
890
  properties: {
@@ -885,6 +893,12 @@ export const TOOL_DEFINITIONS = [
885
893
  description:
886
894
  'Source identifier (default: WEB). Options: VSCODE, CLI, WEB, WEBVNC, WEBPTY, WEBIDE, CURSOR, etc.',
887
895
  },
896
+ pool_type: {
897
+ type: 'string',
898
+ enum: ['ephemeral', 'persistent'],
899
+ description:
900
+ 'Sandbox pool type (default ephemeral). ephemeral = non-persistent: idle auto-shutdown 30min + auto-delete 1h (platform scheduled task, verified) — save important data elsewhere; persistent = data disk retained, idle reclaim within 7 days. Each user may hold one instance per pool type (1+1). Explicit template_id overrides pool_type.',
901
+ },
888
902
  template_id: { type: 'string', description: 'Template ID; overrides server default (only for new sandbox)' },
889
903
  flavor_id: { type: 'string', description: 'Flavor ID; overrides server default (only for new sandbox)' },
890
904
  env: { type: 'object', description: 'Environment variables to set in the sandbox (only for new sandbox)' },
@@ -1483,7 +1497,10 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1483
1497
  const sandboxUser2 = args.username || 'root';
1484
1498
  const sandboxTimeout2 = args.timeout_ms || 120000;
1485
1499
  const sandboxResult2 = await execWithSession(sandboxWsId2, args.command, sandboxUser2, sandboxTimeout2);
1486
- return { stdout: sandboxResult2.stdout, exitCode: sandboxResult2.exitCode };
1500
+ return withPoolContext(sandboxWsId2, {
1501
+ stdout: sandboxResult2.stdout,
1502
+ exitCode: sandboxResult2.exitCode,
1503
+ });
1487
1504
  }
1488
1505
  case 'huaweicloud_sandbox_exec_one_shot': {
1489
1506
  const sandboxWsId3 = args.workspace_id || getCurrentWorkspaceId();
@@ -1496,7 +1513,10 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1496
1513
  const sandboxUser3 = args.username || 'root';
1497
1514
  const sandboxTimeout3 = args.timeout_ms || 120000;
1498
1515
  const sandboxResult3 = await execOneShot(sandboxWsId3, args.command, sandboxUser3, sandboxTimeout3);
1499
- return { stdout: sandboxResult3.stdout, exitCode: sandboxResult3.exitCode };
1516
+ return withPoolContext(sandboxWsId3, {
1517
+ stdout: sandboxResult3.stdout,
1518
+ exitCode: sandboxResult3.exitCode,
1519
+ });
1500
1520
  }
1501
1521
  case 'huaweicloud_sandbox_close_session': {
1502
1522
  const sandboxWsId4 = args.workspace_id || getCurrentWorkspaceId();
@@ -1508,6 +1528,10 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1508
1528
  }
1509
1529
  const sandboxUser4 = args.username || 'root';
1510
1530
  const closed = await closeSession(sandboxWsId4, sandboxUser4);
1531
+ if (closed) {
1532
+ // 池归属缓存随会话关闭清理:长驻进程防只增不减/陈旧池归属
1533
+ forgetWorkspacePool(sandboxWsId4);
1534
+ }
1511
1535
  return closed ? 'ok' : 'not_connected';
1512
1536
  }
1513
1537
  case 'huaweicloud_sandbox_upload_file': {
@@ -1523,12 +1547,9 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1523
1547
  }
1524
1548
  const sandboxUser5 = args.username || 'root';
1525
1549
  const sandboxTimeout5 = args.timeout_ms || 120000;
1526
- return await uploadFileWithSession(
1550
+ return withPoolContext(
1527
1551
  sandboxWsId5,
1528
- args.local_path,
1529
- args.remote_path,
1530
- sandboxUser5,
1531
- sandboxTimeout5,
1552
+ await uploadFileWithSession(sandboxWsId5, args.local_path, args.remote_path, sandboxUser5, sandboxTimeout5),
1532
1553
  );
1533
1554
  }
1534
1555
  case 'huaweicloud_sandbox_upload_project': {
@@ -1544,16 +1565,12 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1544
1565
  }
1545
1566
  const sandboxUser6 = args.username || 'root';
1546
1567
  const sandboxTimeout6 = args.timeout_ms || 120000;
1547
- return await uploadProjectWithSession(
1568
+ return withPoolContext(
1548
1569
  sandboxWsId6,
1549
- args.local_dir,
1550
- args.remote_dir,
1551
- sandboxUser6,
1552
- sandboxTimeout6,
1553
- {
1570
+ await uploadProjectWithSession(sandboxWsId6, args.local_dir, args.remote_dir, sandboxUser6, sandboxTimeout6, {
1554
1571
  exclude: args.exclude,
1555
1572
  extract: args.extract,
1556
- },
1573
+ }),
1557
1574
  );
1558
1575
  }
1559
1576
  case 'huaweicloud_sandbox_deploy_nginx': {
@@ -1569,19 +1586,22 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1569
1586
  }
1570
1587
  const sandboxUser7 = args.username || 'root';
1571
1588
  const sandboxTimeout7 = args.timeout_ms || 60000;
1572
- return await deployNginx(
1589
+ return withPoolContext(
1573
1590
  sandboxWsId7,
1574
- {
1575
- nginxType: args.nginx_type,
1576
- port: args.port,
1577
- project: args.project,
1578
- outputDir: args.output_dir,
1579
- nodePort: args.node_port,
1580
- publicPort: args.public_port,
1581
- configName: args.config_name,
1582
- },
1583
- sandboxUser7,
1584
- sandboxTimeout7,
1591
+ await deployNginx(
1592
+ sandboxWsId7,
1593
+ {
1594
+ nginxType: args.nginx_type,
1595
+ port: args.port,
1596
+ project: args.project,
1597
+ outputDir: args.output_dir,
1598
+ nodePort: args.node_port,
1599
+ publicPort: args.public_port,
1600
+ configName: args.config_name,
1601
+ },
1602
+ sandboxUser7,
1603
+ sandboxTimeout7,
1604
+ ),
1585
1605
  );
1586
1606
  }
1587
1607
  case 'huaweicloud_sandbox_deploy_check': {
@@ -1597,16 +1617,19 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1597
1617
  }
1598
1618
  const sandboxUser8 = args.username || 'root';
1599
1619
  const sandboxTimeout8 = args.timeout_ms || 30000;
1600
- return await deployCheck(
1620
+ return withPoolContext(
1601
1621
  sandboxWsId8,
1602
- {
1603
- port: args.port,
1604
- project: args.project,
1605
- outputDir: args.output_dir,
1606
- frameworkType: args.framework_type,
1607
- },
1608
- sandboxUser8,
1609
- sandboxTimeout8,
1622
+ await deployCheck(
1623
+ sandboxWsId8,
1624
+ {
1625
+ port: args.port,
1626
+ project: args.project,
1627
+ outputDir: args.output_dir,
1628
+ frameworkType: args.framework_type,
1629
+ },
1630
+ sandboxUser8,
1631
+ sandboxTimeout8,
1632
+ ),
1610
1633
  );
1611
1634
  }
1612
1635
  case 'huaweicloud_sandbox_expose_tunnel': {
@@ -1624,11 +1647,14 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1624
1647
  }
1625
1648
  const sandboxUser9 = args.username || 'root';
1626
1649
  const sandboxTimeout9 = args.timeout_ms || 90000;
1627
- return await exposeTunnel(
1650
+ return withPoolContext(
1628
1651
  sandboxWsId9,
1629
- { port: args.port, expiryHours: args.expiry_hours, keepalive: args.keepalive },
1630
- sandboxUser9,
1631
- sandboxTimeout9,
1652
+ await exposeTunnel(
1653
+ sandboxWsId9,
1654
+ { port: args.port, expiryHours: args.expiry_hours, keepalive: args.keepalive },
1655
+ sandboxUser9,
1656
+ sandboxTimeout9,
1657
+ ),
1632
1658
  );
1633
1659
  }
1634
1660
  case 'huaweicloud_sandbox_renew_tunnel': {
@@ -1644,11 +1670,14 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1644
1670
  'or set HW_WORKSPACE_ID environment variable before starting the agent.',
1645
1671
  );
1646
1672
  }
1647
- return await renewTunnel(
1673
+ return withPoolContext(
1648
1674
  sandboxWsId10,
1649
- { tunnelId: args.tunnel_id, expiryHours: args.expiry_hours },
1650
- args.username || 'root',
1651
- args.timeout_ms || 60000,
1675
+ await renewTunnel(
1676
+ sandboxWsId10,
1677
+ { tunnelId: args.tunnel_id, expiryHours: args.expiry_hours },
1678
+ args.username || 'root',
1679
+ args.timeout_ms || 60000,
1680
+ ),
1652
1681
  );
1653
1682
  }
1654
1683
  case 'huaweicloud_sandbox_keepalive_watchdog': {
@@ -1665,11 +1694,14 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1665
1694
  );
1666
1695
  }
1667
1696
  const tunnels11 = args.tunnels.map((t) => ({ tunnelId: t.tunnel_id || t.tunnelId, url: t.url }));
1668
- return await installKeepaliveWatchdog(
1697
+ return withPoolContext(
1669
1698
  sandboxWsId11,
1670
- { tunnels: tunnels11 },
1671
- args.username || 'root',
1672
- args.timeout_ms || 60000,
1699
+ await installKeepaliveWatchdog(
1700
+ sandboxWsId11,
1701
+ { tunnels: tunnels11 },
1702
+ args.username || 'root',
1703
+ args.timeout_ms || 60000,
1704
+ ),
1673
1705
  );
1674
1706
  }
1675
1707
  case 'huaweicloud_sandbox_check_user':
@@ -1677,10 +1709,45 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1677
1709
  case 'huaweicloud_sandbox_sign_agreement':
1678
1710
  return await hdkitSignAgreement();
1679
1711
  case 'huaweicloud_sandbox_connect': {
1680
- const connectResult = await hdkitConnect(args);
1712
+ // 两池:客户端校验 pool_type(非法值在本地即报错,不发无效请求);未传交由服务端默认 ephemeral
1713
+ const connectArgs = { ...args };
1714
+ let poolType;
1715
+ try {
1716
+ poolType = connectArgs.pool_type !== undefined ? validatePoolType(connectArgs.pool_type) : null;
1717
+ if (poolType) connectArgs.pool_type = poolType;
1718
+ } catch (error) {
1719
+ // 非法 pool_type 结构化返回:MCP 错误信封只携带 code+message,指引字段在 throw 路径会丢失(参照 voucher 模式)
1720
+ return { ok: false, code: 'HDKIT_INVALID_POOL_TYPE', message: error.message };
1721
+ }
1722
+ let connectResult;
1723
+ try {
1724
+ connectResult = await hdkitConnect(connectArgs);
1725
+ } catch (error) {
1726
+ // 业务预期失败(429 双码 / 烧核 / 凭证失效)转结构化返回,remediation 才能穿透 MCP 边界;
1727
+ // 其余错误维持抛出(MCP 层透传 code+message)
1728
+ if (
1729
+ error?.code === 'HDKIT_QUOTA_EXCEEDED' ||
1730
+ error?.code === 'HDKIT_CAPACITY_INSUFFICIENT' ||
1731
+ error?.code === 'HDKIT_CRED_INVALID' ||
1732
+ error?.coreHoursExhausted
1733
+ ) {
1734
+ return {
1735
+ ok: false,
1736
+ code: error.code,
1737
+ message: error.message,
1738
+ ...(error.retryAfter !== undefined ? { retryAfter: error.retryAfter } : {}),
1739
+ ...(error.coreHoursExhausted ? { coreHoursExhausted: true } : {}),
1740
+ ...(error.remediation ? { remediation: error.remediation } : {}),
1741
+ };
1742
+ }
1743
+ throw error;
1744
+ }
1681
1745
  const devStageId = connectResult?.dev_stage_id || connectResult?.devStageId;
1682
1746
  if (devStageId) {
1683
1747
  setWorkspaceId(devStageId);
1748
+ // 池归属缓存:exec/upload/deploy 等工具返回值据此附带池模式提示(缺口#2)
1749
+ // 双读兜底与 devStageId 同型(防服务端 wire 命名漂移,QA #1082 配对复审教训)
1750
+ rememberWorkspacePool(devStageId, connectResult?.pool_type || connectResult?.poolType);
1684
1751
  try {
1685
1752
  await execOneShot(devStageId, 'devbridge delete-all 2>/dev/null || true', 'root', 15000);
1686
1753
  } catch {}
@@ -1694,7 +1761,8 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1694
1761
  } catch {}
1695
1762
  await transferGitRepo(args, devStageId, connectResult);
1696
1763
  }
1697
- return connectResult;
1764
+ // TTL 播报(缺口#1):服务端回显 pool_type 才附带生命周期提示
1765
+ return enrichConnectResult(connectResult);
1698
1766
  }
1699
1767
  case 'huaweicloud_sandbox_credentials': {
1700
1768
  const devStageId = args.dev_stage_id || getCurrentWorkspaceId();