huaweicloud-devkit 1.1.12 → 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.
@@ -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,
@@ -54,7 +61,7 @@ import {
54
61
  parseStsExpiry,
55
62
  resolveCredentialsWithRuntime,
56
63
  } from './auth/credentials.mjs';
57
- import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
64
+ import { trackToolInvoke, trackSkillRetrieve, clearUserHash, cacheUserHash } from './telemetry/telemetry.mjs';
58
65
  import { fetchWithProxy } from './proxy/proxy-agent.mjs';
59
66
  import { fingerprint, runHcloudConfigure, runHcloudProfileDelete, resolveManagedProfile } from './auth/reconcile.mjs';
60
67
  import {
@@ -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();
@@ -1785,10 +1853,17 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1785
1853
  }
1786
1854
  case 'huaweicloud_voucher_status':
1787
1855
  return await hdkitVoucherStatus(args.domain_id);
1788
- case 'huaweicloud_voucher_claim':
1789
- return await hdkitVoucherClaim(args.domain_id);
1790
- case 'huaweicloud_competition_voucher_claim':
1791
- return await hdkitCompetitionVoucherClaim(args.domain_id);
1856
+ case 'huaweicloud_voucher_claim': {
1857
+ const result = await hdkitVoucherClaim(args.domain_id);
1858
+ // #1014 P0: 领券成功后补缓存 userHash(响应携带时),flush 前兜底补齐队列缺失字段。
1859
+ if (result?.userHash) cacheUserHash(result.userHash);
1860
+ return result;
1861
+ }
1862
+ case 'huaweicloud_competition_voucher_claim': {
1863
+ const result = await hdkitCompetitionVoucherClaim(args.domain_id);
1864
+ if (result?.userHash) cacheUserHash(result.userHash);
1865
+ return result;
1866
+ }
1792
1867
  case 'huaweicloud_check_update':
1793
1868
  return await handleCheckUpdate(args, { sessionId: opts?.sessionId, doQuery: opts?.doQuery });
1794
1869
  case 'huaweicloud_upgrade':