huaweicloud-devkit 1.1.7 → 1.1.8-next.1

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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +2 -1
  4. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  5. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  6. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +19 -1
  10. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  11. package/plugins/huaweicloud-core/safety/policy.json +1 -1
  12. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +57 -2
  13. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +19 -16
  14. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +1 -1
  15. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +9 -3
  16. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +1 -1
  17. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +15 -2
  18. package/plugins/huaweicloud-core/src/auth/service.mjs +36 -0
  19. package/plugins/huaweicloud-core/src/data/icons-manifest.v1.json +23 -5
  20. package/plugins/huaweicloud-core/src/detect-framework.mjs +41 -5
  21. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +78 -8
  22. package/plugins/huaweicloud-core/src/hcloud-probe.mjs +10 -2
  23. package/plugins/huaweicloud-core/src/mcp-server.mjs +9 -0
  24. package/plugins/huaweicloud-core/src/preflight.mjs +125 -0
  25. package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +94 -1
  26. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +1 -1
  27. package/plugins/huaweicloud-core/src/safety-policy.mjs +165 -33
  28. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +214 -27
  29. package/plugins/huaweicloud-core/src/search-market.mjs +40 -1
  30. package/plugins/huaweicloud-core/src/setup-cli.mjs +57 -17
  31. package/plugins/huaweicloud-core/src/tools.mjs +307 -20
  32. package/rules/huawei-agent-rules.mdc +78 -0
@@ -8,7 +8,7 @@ import { createHmac, createHash } from 'node:crypto';
8
8
 
9
9
  import { evaluateArtifacts, evaluateCommandRisk, evaluateDeployPlan } from './risk-rule-engine.mjs';
10
10
  import { classifyTextCommand, redactSecrets } from './safety-policy.mjs';
11
- import { planHcloudCommand, runHcloud, consumeApprovalToken, hashArgs } from './hcloud-cli.mjs';
11
+ import { planHcloudCommand, runHcloud, consumeApprovalToken, inspectApprovalToken, hashArgs } from './hcloud-cli.mjs';
12
12
  import { searchMarketplace } from './search-market.mjs';
13
13
  import { getServiceIcon } from './icon-library.mjs';
14
14
  import { detectFramework } from './detect-framework.mjs';
@@ -20,6 +20,7 @@ import {
20
20
  uploadProjectWithSession,
21
21
  deployNginx,
22
22
  deployCheck,
23
+ exposeTunnel,
23
24
  getCurrentWorkspaceId,
24
25
  setWorkspaceId,
25
26
  } from './sandbox/session-manager.mjs';
@@ -46,6 +47,7 @@ import {
46
47
  writeLastSync,
47
48
  readCodeArtsCredentials,
48
49
  globalCredentialsPath,
50
+ parseStsExpiry,
49
51
  resolveCredentialsWithRuntime,
50
52
  } from './auth/credentials.mjs';
51
53
  import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
@@ -734,6 +736,29 @@ export const TOOL_DEFINITIONS = [
734
736
  },
735
737
  },
736
738
  },
739
+ {
740
+ name: 'huaweicloud_sandbox_expose_tunnel',
741
+ description:
742
+ 'Expose a port on the sandbox to a public DevBridge tunnel URL (devbridge 0.2.x). Authenticates automatically (probing AK/SK vs API-Key builds, using /tmp/hw_creds.sh and /tmp/hw_api_key as injected by huaweicloud_sandbox_credentials), pre-cleans stale tunnels to avoid quota errors, starts the host, extracts the public URL (https://<id>-<port>.devbridge-s2.hwtunnel.com), and health-checks it before returning. Use the ACTUAL port reported by huaweicloud_sandbox_deploy_nginx (its "port" field auto-increments on conflict) — never the originally requested port. This automates the manual "Expose via DevBridge" 5-step flow. Returns ok, publicUrl, port, tunnelId, and warnings. If the API Key is missing on a release build it returns ok:false with instructions to create one.',
743
+ inputSchema: {
744
+ type: 'object',
745
+ required: ['port'],
746
+ properties: {
747
+ port: {
748
+ type: 'number',
749
+ description:
750
+ 'Actual port nginx is listening on — use the "port" value returned by huaweicloud_sandbox_deploy_nginx, not the originally requested port.',
751
+ },
752
+ workspace_id: {
753
+ type: 'string',
754
+ description:
755
+ 'Workspace ID from huaweicloud_sandbox_connect return value. Required - must be passed explicitly when HW_WORKSPACE_ID is not set.',
756
+ },
757
+ username: { type: 'string', description: 'Login username (default: root)' },
758
+ timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 90000)' },
759
+ },
760
+ },
761
+ },
737
762
  {
738
763
  name: 'huaweicloud_sandbox_check_user',
739
764
  description:
@@ -989,6 +1014,11 @@ async function transferGitRepo(args, devStageId, connectResult) {
989
1014
  }
990
1015
 
991
1016
  const pendingConfirms = new Map();
1017
+ // Tombstones for confirm tokens already consumed, so a repeat
1018
+ // huaweicloud_auth_confirm call can be classified as already_processed
1019
+ // instead of not_found (D4-24, #745). Kept in-memory only: confirm tokens
1020
+ // are session-scoped and never persisted across processes.
1021
+ const consumedConfirms = new Map();
992
1022
 
993
1023
  function readImportFile() {
994
1024
  const path = join(dirname(globalCredentialsPath()), 'creds-import.json');
@@ -1256,9 +1286,18 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1256
1286
  return persisted;
1257
1287
  }
1258
1288
  case 'huaweicloud_auth_confirm': {
1289
+ // D4-24 precise JSON contract (#745): a missing or already-consumed
1290
+ // confirm token returns a structured result instead of throwing a
1291
+ // generic Error string, mirroring runApprovedCommand's contract.
1259
1292
  const pending = pendingConfirms.get(args.token);
1260
- if (!pending) throw new Error('confirmToken not found or expired.');
1293
+ if (!pending) {
1294
+ if (args.token && consumedConfirms.has(args.token)) {
1295
+ return { status: 'ok', outcome: 'already_processed' };
1296
+ }
1297
+ return { status: 'rejected', code: 'CONFIRM_TOKEN_NOT_FOUND' };
1298
+ }
1261
1299
  pendingConfirms.delete(args.token);
1300
+ consumedConfirms.set(args.token, { consumedAt: Date.now() });
1262
1301
  if (args.decision === 's1') {
1263
1302
  return { status: 'ok', outcome: 'aborted', message: '保持 S1 现有账号,未覆盖。' };
1264
1303
  }
@@ -1404,6 +1443,23 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1404
1443
  sandboxTimeout8,
1405
1444
  );
1406
1445
  }
1446
+ case 'huaweicloud_sandbox_expose_tunnel': {
1447
+ if (!args.port) {
1448
+ throw new Error(
1449
+ 'port is required. Use the ACTUAL "port" value returned by huaweicloud_sandbox_deploy_nginx (it may have been auto-incremented on a port conflict).',
1450
+ );
1451
+ }
1452
+ const sandboxWsId9 = args.workspace_id || getCurrentWorkspaceId();
1453
+ if (!sandboxWsId9) {
1454
+ throw new Error(
1455
+ 'workspace_id is required. No sandbox connected — call huaweicloud_sandbox_connect first, ' +
1456
+ 'or set HW_WORKSPACE_ID environment variable before starting the agent.',
1457
+ );
1458
+ }
1459
+ const sandboxUser9 = args.username || 'root';
1460
+ const sandboxTimeout9 = args.timeout_ms || 90000;
1461
+ return await exposeTunnel(sandboxWsId9, { port: args.port }, sandboxUser9, sandboxTimeout9);
1462
+ }
1407
1463
  case 'huaweicloud_sandbox_check_user':
1408
1464
  return await hdkitCheckUser();
1409
1465
  case 'huaweicloud_sandbox_sign_agreement':
@@ -1750,11 +1806,80 @@ const SERVICE_EXAMPLES = {
1750
1806
  DCS: { list: 'DCS ListInstances', create: 'DCS CreateInstance', show: 'DCS ShowInstance' },
1751
1807
  };
1752
1808
 
1809
+ // DMS and DEW are Huawei Cloud aggregate service names that do not exist as
1810
+ // single KooCLI service identifiers. Each maps to real KooCLI sub-services.
1811
+ const AGGREGATE_SERVICE_MAP = {
1812
+ DMS: ['Kafka', 'RabbitMQ', 'RocketMQ'],
1813
+ DEW: ['KMS', 'CSMS'],
1814
+ };
1815
+
1816
+ // Identity-class notes per service. BSS leads: its operations are split between
1817
+ // Customer-level APIs (work with a normal IAM user) and Partner-level APIs
1818
+ // (only valid for a partner/dealer identity). Calling a Partner-level API with a
1819
+ // customer credential returns APIGW.0301 even when AK/SK are perfectly valid —
1820
+ // the common false-positive diagnosis is "cli-domain-id missing".
1821
+ const SERVICE_IDENTITY_NOTES = {
1822
+ BSS: [
1823
+ {
1824
+ identity: 'Customer-level (normal IAM user)',
1825
+ apis: [
1826
+ 'ShowCustomerAccountBalances',
1827
+ 'ListCustomerBillsFeeRecords',
1828
+ 'ListCustomerCouponChangeRecords',
1829
+ 'ListResourceUsage',
1830
+ 'ListCosts',
1831
+ ],
1832
+ note: 'Work with a normal IAM user that has BSS Administrator or Finance role.',
1833
+ },
1834
+ {
1835
+ identity: 'Partner-level (partner/dealer only)',
1836
+ apis: ['ListQuotaCoupons', 'ListSubCustomerCoupons', 'ListSubCustomerBillDetail'],
1837
+ note: 'APIGW.0301 here means the caller identity is not a partner — NOT bad AK/SK or a missing cli-domain-id.',
1838
+ },
1839
+ ],
1840
+ };
1841
+
1753
1842
  async function listOperations(service, options = {}) {
1754
1843
  const serviceName = String(service || '').trim();
1755
1844
  if (!/^[A-Za-z][A-Za-z0-9-]{1,63}$/.test(serviceName)) {
1756
1845
  throw new Error('service must be a KooCLI service name such as ECS, VPC, IMS, OBS, RDS, or CDN.');
1757
1846
  }
1847
+ const upperName = serviceName.toUpperCase();
1848
+ const subServices = AGGREGATE_SERVICE_MAP[upperName];
1849
+ if (subServices && subServices.length) {
1850
+ const results = await Promise.all(
1851
+ subServices.map(async (sub) => {
1852
+ const r = await runHcloud([sub, '--help'], {
1853
+ timeoutMs: options.timeoutMs,
1854
+ maxRetries: 0,
1855
+ });
1856
+ let result = r;
1857
+ if (!result.ok) {
1858
+ result = await runHcloud([sub, 'help'], {
1859
+ timeoutMs: options.timeoutMs,
1860
+ maxRetries: 0,
1861
+ });
1862
+ }
1863
+ return {
1864
+ subService: sub,
1865
+ command: `hcloud ${sub} --help`,
1866
+ examples: SERVICE_EXAMPLES[sub.toUpperCase()] || {
1867
+ note: `No cached examples for ${sub}. Use the help text above to discover available operations.`,
1868
+ },
1869
+ result,
1870
+ };
1871
+ }),
1872
+ );
1873
+ return {
1874
+ service: serviceName,
1875
+ aggregate: true,
1876
+ aggregatedFrom: subServices,
1877
+ selectionRule:
1878
+ 'DMS/DEW are aggregate service names. Use each sub-service help text below to select the exact KooCLI operation name.',
1879
+ identityNotes: SERVICE_IDENTITY_NOTES[upperName] || null,
1880
+ subServices: results,
1881
+ };
1882
+ }
1758
1883
  const isObs = /^obs$/i.test(serviceName);
1759
1884
  const svc = isObs ? 'obs' : serviceName;
1760
1885
  const args = isObs ? ['obs', 'help'] : [svc, '--help'];
@@ -1771,8 +1896,10 @@ async function listOperations(service, options = {}) {
1771
1896
  return {
1772
1897
  service: serviceName,
1773
1898
  command: isObs ? 'hcloud obs help' : `hcloud ${svc} --help`,
1774
- selectionRule: 'Use this help text to select the exact KooCLI operation name before planning any service command.',
1775
- examples: SERVICE_EXAMPLES[serviceName.toUpperCase()] || {
1899
+ selectionRule:
1900
+ 'Use this help text to select the exact KooCLI operation name before planning any service command. After selecting an operation, run "hcloud <Service> <Operation> --help" to confirm exact parameter names before constructing the command.',
1901
+ identityNotes: SERVICE_IDENTITY_NOTES[upperName] || null,
1902
+ examples: SERVICE_EXAMPLES[upperName] || {
1776
1903
  note: `No cached examples for ${serviceName}. Use the help text above to discover available operations.`,
1777
1904
  },
1778
1905
  result,
@@ -1784,9 +1911,33 @@ async function runApprovedCommand(args = {}) {
1784
1911
  throw new Error('approvedByUser must be true after explicit user approval for this exact command.');
1785
1912
  }
1786
1913
  const token = String(args.approvalToken || '');
1914
+
1915
+ // D4-24 precise JSON contract (#745): expired and already-consumed tokens
1916
+ // return a structured result instead of throwing a generic Error string, so
1917
+ // clients can machine-assert the two non-success states.
1918
+ const inspection = inspectApprovalToken(token);
1919
+ if (inspection.state === 'already_consumed') {
1920
+ return { status: 'ok', outcome: 'already_processed' };
1921
+ }
1922
+ if (inspection.state === 'expired') {
1923
+ return { status: 'rejected', code: 'CONFIRM_TOKEN_EXPIRED' };
1924
+ }
1925
+ if (inspection.state === 'not_found') {
1926
+ // A token that was never created (typo / unknown / pruned beyond the
1927
+ // tombstone window) is still a rejection, but carries no expired/already
1928
+ // semantics — surface a distinct code so clients can tell the cases apart.
1929
+ return { status: 'rejected', code: 'CONFIRM_TOKEN_NOT_FOUND' };
1930
+ }
1931
+
1787
1932
  const stored = consumeApprovalToken(token);
1788
1933
  if (!stored) {
1789
- throw new Error('Invalid or expired approval token. Please re-plan the command.');
1934
+ // Defensive: inspectApprovalToken said 'valid' but consume returned null
1935
+ // (e.g. raced and pruned between the two reads). Re-classify so we never
1936
+ // return a misleading success.
1937
+ const recheck = inspectApprovalToken(token);
1938
+ if (recheck.state === 'expired') return { status: 'rejected', code: 'CONFIRM_TOKEN_EXPIRED' };
1939
+ if (recheck.state === 'already_consumed') return { status: 'ok', outcome: 'already_processed' };
1940
+ return { status: 'rejected', code: 'CONFIRM_TOKEN_NOT_FOUND' };
1790
1941
  }
1791
1942
  const providedArgs = Array.isArray(args.args) ? args.args.map(String) : [];
1792
1943
  if (hashArgs(providedArgs) !== stored.argsHash) {
@@ -1816,22 +1967,79 @@ function serviceCatalog(intent = '') {
1816
1967
  const it = String(intent).toLowerCase();
1817
1968
  const routeMap = [
1818
1969
  {
1819
- keywords: ['ecs', 'server', 'vm', 'instance', 'compute', 'flavor', 'image'],
1970
+ keywords: [
1971
+ 'ecs',
1972
+ 'server',
1973
+ 'vm',
1974
+ 'instance',
1975
+ 'compute',
1976
+ 'flavor',
1977
+ 'image',
1978
+ '弹性云服务器',
1979
+ '云服务器',
1980
+ '服务器',
1981
+ '虚拟机',
1982
+ '镜像',
1983
+ ],
1820
1984
  skills: ['huawei-ecs'],
1821
1985
  services: ['ECS'],
1822
1986
  },
1823
1987
  {
1824
- keywords: ['vpc', 'subnet', 'network', 'security group', 'eip', 'nat', 'vpn', 'bandwidth'],
1988
+ keywords: [
1989
+ 'vpc',
1990
+ 'subnet',
1991
+ 'network',
1992
+ 'security group',
1993
+ 'eip',
1994
+ 'nat',
1995
+ 'vpn',
1996
+ 'bandwidth',
1997
+ '虚拟私有云',
1998
+ '子网',
1999
+ '网络',
2000
+ '安全组',
2001
+ '弹性公网IP',
2002
+ 'NAT网关',
2003
+ 'VPN网关',
2004
+ '带宽',
2005
+ ],
1825
2006
  skills: ['huawei-vpc'],
1826
2007
  services: ['VPC', 'EIP'],
1827
2008
  },
1828
2009
  {
1829
- keywords: ['obs', 'bucket', 'storage', 'object', 'static website', 'static site', 'hosting'],
2010
+ keywords: [
2011
+ 'obs',
2012
+ 'bucket',
2013
+ 'storage',
2014
+ 'object',
2015
+ 'static website',
2016
+ 'static site',
2017
+ 'hosting',
2018
+ '对象存储',
2019
+ '存储桶',
2020
+ '桶',
2021
+ '存储',
2022
+ '对象',
2023
+ '静态网站',
2024
+ '静态托管',
2025
+ ],
1830
2026
  skills: ['huawei-obs'],
1831
2027
  services: ['OBS'],
1832
2028
  },
1833
2029
  {
1834
- keywords: ['functiongraph', 'serverless', 'function', 'lambda', 'trigger', 'faas'],
2030
+ keywords: [
2031
+ 'functiongraph',
2032
+ 'serverless',
2033
+ 'function',
2034
+ 'lambda',
2035
+ 'trigger',
2036
+ 'faas',
2037
+ '函数工作流',
2038
+ '函数',
2039
+ '触发器',
2040
+ '无服务器',
2041
+ '函数计算',
2042
+ ],
1835
2043
  skills: ['huawei-functiongraph'],
1836
2044
  services: ['FunctionGraph'],
1837
2045
  },
@@ -1840,20 +2048,60 @@ function serviceCatalog(intent = '') {
1840
2048
  skills: ['huawei-cce'],
1841
2049
  services: ['CCE', 'SWR'],
1842
2050
  },
1843
- { keywords: ['apig', 'api gateway', 'publish', 'throttle'], skills: ['huawei-apig'], services: ['APIG'] },
1844
- { keywords: ['rds', 'mysql', 'postgresql', 'database', 'db'], skills: ['huawei-rds'], services: ['RDS'] },
1845
2051
  {
1846
- keywords: ['gaussdb', 'distributed', 'sharding', 'opengauss'],
2052
+ keywords: ['apig', 'api gateway', 'publish', 'throttle', 'API网关', '限流'],
2053
+ skills: ['huawei-apig'],
2054
+ services: ['APIG'],
2055
+ },
2056
+ {
2057
+ keywords: ['rds', 'mysql', 'postgresql', 'database', 'db', '关系型数据库', '数据库'],
2058
+ skills: ['huawei-rds'],
2059
+ services: ['RDS'],
2060
+ },
2061
+ {
2062
+ keywords: ['gaussdb', 'distributed', 'sharding', 'opengauss', '高斯数据库', '分布式数据库'],
1847
2063
  skills: ['huawei-gaussdb'],
1848
2064
  services: ['GaussDB'],
1849
2065
  },
1850
2066
  {
1851
- keywords: ['iam', 'permission', 'policy', 'role', 'user', 'ak/sk', 'access key', 'agency'],
2067
+ keywords: [
2068
+ 'iam',
2069
+ 'permission',
2070
+ 'policy',
2071
+ 'role',
2072
+ 'user',
2073
+ 'ak/sk',
2074
+ 'access key',
2075
+ 'agency',
2076
+ '身份认证',
2077
+ '身份管理',
2078
+ '权限',
2079
+ '策略',
2080
+ '角色',
2081
+ '用户',
2082
+ '访问密钥',
2083
+ '委托',
2084
+ ],
1852
2085
  skills: ['huawei-iam'],
1853
2086
  services: ['IAM'],
1854
2087
  },
1855
2088
  {
1856
- keywords: ['dew', 'secret', 'kms', 'encrypt', 'decrypt', 'certificate', 'csms'],
2089
+ keywords: [
2090
+ 'dew',
2091
+ 'secret',
2092
+ 'kms',
2093
+ 'encrypt',
2094
+ 'decrypt',
2095
+ 'certificate',
2096
+ 'csms',
2097
+ '数据加密服务',
2098
+ '密钥管理',
2099
+ '密钥',
2100
+ '加密',
2101
+ '解密',
2102
+ '证书',
2103
+ '凭据管理',
2104
+ ],
1857
2105
  skills: ['huawei-dew'],
1858
2106
  services: ['CSMS', 'KMS'],
1859
2107
  },
@@ -1863,7 +2111,7 @@ function serviceCatalog(intent = '') {
1863
2111
  services: ['ModelArts'],
1864
2112
  },
1865
2113
  {
1866
- keywords: ['billing', 'cost', 'bill', 'budget', 'expense', 'bss'],
2114
+ keywords: ['billing', 'cost', 'bill', 'budget', 'expense', 'bss', '计费', '费用', '账单', '预算', '开销'],
1867
2115
  skills: ['huawei-billing'],
1868
2116
  services: ['BSS'],
1869
2117
  },
@@ -1878,12 +2126,16 @@ function serviceCatalog(intent = '') {
1878
2126
  services: ['SMN', 'DMS'],
1879
2127
  },
1880
2128
  {
1881
- keywords: ['ces', 'monitor', 'alarm', 'metric', 'dashboard', 'cloud eye'],
2129
+ keywords: ['ces', 'monitor', 'alarm', 'metric', 'dashboard', 'cloud eye', '监控', '告警', '云监控'],
1882
2130
  skills: ['huawei-cloud-eye'],
1883
2131
  services: ['CES'],
1884
2132
  },
1885
- { keywords: ['cts', 'audit', 'trace', 'tracker'], skills: ['huawei-cts'], services: ['CTS'] },
1886
- { keywords: ['cbr', 'backup', 'restore', 'vault', 'snapshot'], skills: ['huawei-cbr'], services: ['CBR'] },
2133
+ { keywords: ['cts', 'audit', 'trace', 'tracker', '审计', '追踪'], skills: ['huawei-cts'], services: ['CTS'] },
2134
+ {
2135
+ keywords: ['cbr', 'backup', 'restore', 'vault', 'snapshot', '备份', '恢复', '快照', '存储库'],
2136
+ skills: ['huawei-cbr'],
2137
+ services: ['CBR'],
2138
+ },
1887
2139
  {
1888
2140
  keywords: ['deployment', 'deploy', 'ci/cd', 'pipeline', 'release'],
1889
2141
  skills: ['huawei-deployment'],
@@ -1909,10 +2161,26 @@ function serviceCatalog(intent = '') {
1909
2161
  services: ['Sandbox', 'DevStation'],
1910
2162
  },
1911
2163
  {
1912
- keywords: ['dds', 'dcs', 'mongodb', 'redis', 'memcached', 'cache', 'document db'],
2164
+ keywords: [
2165
+ 'dds',
2166
+ 'dcs',
2167
+ 'mongodb',
2168
+ 'redis',
2169
+ 'memcached',
2170
+ 'cache',
2171
+ 'document db',
2172
+ '文档数据库',
2173
+ '缓存',
2174
+ '内存数据库',
2175
+ ],
1913
2176
  skills: ['huawei-dds-dcs'],
1914
2177
  services: ['DDS', 'DCS'],
1915
2178
  },
2179
+ {
2180
+ keywords: ['elb', 'load balancer', '负载均衡', '弹性负载均衡', '监听器', '证书'],
2181
+ skills: ['huawei-vpc'],
2182
+ services: ['ELB'],
2183
+ },
1916
2184
  {
1917
2185
  keywords: ['voucher', 'coupon', 'incentive', 'credit', '领券', '代金券', '优惠券', '激励金', '领取'],
1918
2186
  skills: ['huawei-voucher'],
@@ -1923,7 +2191,9 @@ function serviceCatalog(intent = '') {
1923
2191
  const tokens = new Set(it.split(/[\s,./-]+/).filter((t) => t.length > 0));
1924
2192
  const cjk = /[\u4e00-\u9fff]/;
1925
2193
  for (const route of routeMap) {
1926
- if (route.keywords.some((kw) => (kw.includes(' ') || cjk.test(kw) ? it.includes(kw) : tokens.has(kw)))) {
2194
+ if (
2195
+ route.keywords.some((kw) => (kw.includes(' ') || cjk.test(kw) ? it.includes(kw.toLowerCase()) : tokens.has(kw)))
2196
+ ) {
1927
2197
  matched.push(route);
1928
2198
  }
1929
2199
  }
@@ -2016,6 +2286,23 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
2016
2286
  }
2017
2287
  }
2018
2288
 
2289
+ // G: when the active credential set is a temporary STS token that has already
2290
+ // expired, surface that as the primary cause before generic auth guidance.
2291
+ let activeCreds;
2292
+ try {
2293
+ activeCreds = resolveCredentialsWithRuntime({ allowMissing: true });
2294
+ } catch {
2295
+ activeCreds = null;
2296
+ }
2297
+ if (activeCreds?.ak && activeCreds?.sk && activeCreds.securityToken) {
2298
+ const expiry = parseStsExpiry({ securityToken: activeCreds.securityToken });
2299
+ if (expiry !== null && expiry <= Date.now()) {
2300
+ suggestions.push(
2301
+ 'CREDENTIAL_EXPIRED: the temporary STS security token has expired. Re-authenticate to obtain fresh credentials (e.g. run "npx huaweicloud-devkit auth init" or re-login / restart the session).',
2302
+ );
2303
+ }
2304
+ }
2305
+
2019
2306
  if (/auth|token|credential|ak|sk|401|403|unauthorized|forbidden|Incorrect IAM/i.test(combined)) {
2020
2307
  if (svc === 'obs') {
2021
2308
  suggestions.push(
@@ -0,0 +1,78 @@
1
+ # HuaweiCloud DevKit — Agent Rules
2
+
3
+ > Agent 加载插件时自动读取,定义全局行为约束。
4
+
5
+ ---
6
+
7
+ ## Core Principles
8
+
9
+ - **Always use the Huawei MCP server first** (sandboxed, observable, auditable). Fall back to `hcloud` CLI only when MCP is unavailable.
10
+ - **Before any task, check for available skills.** Use `huaweicloud_search_docs` or `huaweicloud_retrieve_skill` via MCP. Follow skill procedures over general knowledge.
11
+ - **When uncertain** about API parameters, quotas, limits, or error codes — search documentation via MCP. Do not guess.
12
+ - **Prefer IaC over direct CLI calls** for infrastructure creation (Terraform for Huawei Cloud, or CloudFormation equivalent).
13
+ - **Never use em dashes in resource names/tags.** Use hyphens only.
14
+ - **Match user language.** Respond in the same language the user writes in.
15
+
16
+ ## Secret Safety
17
+
18
+ - **MUST load the `huawei-dew` skill** first for any secret, credential, API key, token, or password task.
19
+ - **MUST NOT call** `hcloud csms download-secret` or `hcloud csms show-secret` directly.
20
+ - **MUST NOT call** `hcloud kms decrypt` directly.
21
+ - **MUST use** `{{resolve:csms:secret-id:SecretString:key}}` with the MCP proxy — secrets resolve at runtime, never enter agent context.
22
+ - **NEVER** echo AK/SK values in conversation or logs.
23
+
24
+ ## IAM Security
25
+
26
+ - **Least privilege by default.** Start with no permissions, add only what is needed.
27
+ - **MUST NOT create IAM users** — use IAM Identity Center (OneAccess) or temporary credentials.
28
+ - **MUST NOT create long-term AK/SK** — use temporary STS tokens.
29
+ - **MUST scope resource ARNs** — no `*` wildcards.
30
+ - **Confused deputy protection:** Add `g:SourceAccount` and `g:SourceUrn` conditions to trust policies.
31
+
32
+ ## Network Security
33
+
34
+ - **MUST block public access by default** on OBS buckets.
35
+ - **MUST use security groups** (not iptables) for ECS/CCE access control.
36
+ - **SHOULD use VPC endpoints** for internal service access (no public internet).
37
+ - **MUST enable HTTPS-only** on OBS buckets and API endpoints.
38
+
39
+ ## Observability
40
+
41
+ - **SHOULD enable CTS audit logging** on all accounts.
42
+ - **SHOULD configure Cloud Eye alarms** for production workloads (CPU > 80%, memory > 85%, disk > 90%).
43
+ - **SHOULD enable OBS access logging** for audit compliance.
44
+
45
+ ## Cost Awareness
46
+
47
+ - **Before creating resources**, inform the user of estimated cost.
48
+ - **SHOULD suggest billing budgets** for production accounts.
49
+ - **SHOULD recommend CBR backup** for stateful resources (RDS, ECS system disks).
50
+
51
+ ## Naming Conventions
52
+
53
+ - Resource names: lowercase, hyphens, max 64 chars
54
+ - Tags: include Environment (dev/staging/prod), Owner, Project
55
+ - OBS bucket names: globally unique, lowercase, no underscores
56
+
57
+ ## MCP Usage Rules
58
+
59
+ - **Prefer MCP for ALL Huawei Cloud operations** — sandbox, audit, enterprise controls.
60
+ - **When MCP is unavailable:** Fall back to `hcloud` CLI. State clearly: "MCP unavailable, using local hcloud CLI."
61
+ - **`huaweicloud_run_approved_command`:** Only use when the exact planned command has been shown and the user explicitly approved that exact command.
62
+ - **`huaweicloud_run_script`:** Use for deterministic calculations (cost math, data processing). Never do arithmetic in LLM reasoning.
63
+
64
+ ## Skill Discovery Priority
65
+
66
+ 1. **`huaweicloud-core`** — always check first for routing
67
+ 2. **Service-specific skills** — load after routing via `huaweicloud_retrieve_skill`
68
+ 3. **Knowledge cards** (`references/`) — quick facts without full skill load
69
+
70
+ ## Red Flags — STOP and Re-evaluate
71
+
72
+ - Creating resources without user confirmation
73
+ - Using `hcloud csms download-secret` or `hcloud kms decrypt` directly
74
+ - Guessing API parameters without searching docs
75
+ - Creating IAM users or long-term AK/SK
76
+ - Opening OBS buckets to public access
77
+ - Skipping CTS/Cloud Eye on production workloads
78
+ - Echoing AK/SK or secret values in conversation