huaweicloud-devkit 1.0.2-dev.0 → 1.0.2-dev.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.
@@ -0,0 +1,105 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createConnection, getCredentials } from './hwlink-api.mjs';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const WS_EXEC_SRC = join(__dirname, '..', 'ws-exec');
8
+
9
+ const DEFAULT_WORKSPACE_ID = process.env.HW_WORKSPACE_ID || '0107bd9997aa4287bd2b4890b49af07d';
10
+
11
+ function resolveEnv() {
12
+ const env = { ...process.env };
13
+ env.PATH = `${env.HOME || '/root'}/.huawei/bin:${env.PATH || ''}`;
14
+ return env;
15
+ }
16
+
17
+ async function runNodeExec(args, timeoutMs = 30000) {
18
+ const env = resolveEnv();
19
+ return new Promise((resolve) => {
20
+ const proc = spawn('node', args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
21
+ let stdout = '';
22
+ let stderr = '';
23
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
24
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
25
+
26
+ const timer = setTimeout(() => {
27
+ proc.kill();
28
+ resolve({ error: 'timed out', exitCode: 124 });
29
+ }, timeoutMs);
30
+
31
+ proc.on('close', (code) => {
32
+ clearTimeout(timer);
33
+ const out = stdout.trim();
34
+ if (out) {
35
+ try {
36
+ resolve({ ...JSON.parse(out), exitCode: code || 0 });
37
+ return;
38
+ } catch {}
39
+ }
40
+ if (code && code !== 0 && !out) {
41
+ resolve({ error: stderr.trim() || `exit code ${code}`, exitCode: code });
42
+ return;
43
+ }
44
+ resolve({ data: out, exitCode: code || 0 });
45
+ });
46
+ });
47
+ }
48
+
49
+ const sessions = new Map();
50
+
51
+ async function getSession(workspaceId, username, timeoutMs) {
52
+ const key = `${workspaceId}:${username}`;
53
+ if (sessions.has(key)) return sessions.get(key);
54
+
55
+ const { ak, sk, securitytoken } = getCredentials();
56
+ const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
57
+
58
+ const { connectHwlinkTerminalSession } = await import(join(WS_EXEC_SRC, 'index.js'));
59
+ const session = await connectHwlinkTerminalSession({
60
+ url: wsUrl,
61
+ source,
62
+ username,
63
+ timeoutMs,
64
+ });
65
+
66
+ sessions.set(key, session);
67
+ return session;
68
+ }
69
+
70
+ export async function execOneShot(workspaceId, command, username, timeoutMs) {
71
+ const { ak, sk, securitytoken } = getCredentials();
72
+ const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
73
+
74
+ const { executeHwlinkCommand } = await import(join(WS_EXEC_SRC, 'index.js'));
75
+ return await executeHwlinkCommand({
76
+ url: wsUrl,
77
+ source,
78
+ username,
79
+ command,
80
+ timeoutMs,
81
+ });
82
+ }
83
+
84
+ export async function execWithSession(workspaceId, command, username, timeoutMs) {
85
+ const session = await getSession(workspaceId, username, timeoutMs);
86
+ return await session.exec(command, { timeoutMs });
87
+ }
88
+
89
+ export async function closeSession(workspaceId, username) {
90
+ const key = `${workspaceId}:${username}`;
91
+ const session = sessions.get(key);
92
+ if (!session) return false;
93
+ sessions.delete(key);
94
+ try { session.close(); } catch {}
95
+ return true;
96
+ }
97
+
98
+ export async function closeAllSessions() {
99
+ for (const [key, session] of sessions) {
100
+ sessions.delete(key);
101
+ try { session.close(); } catch {}
102
+ }
103
+ }
104
+
105
+ export { DEFAULT_WORKSPACE_ID, runNodeExec };
@@ -6,6 +6,8 @@ import { fileURLToPath } from 'node:url';
6
6
  import { homedir, platform } from 'node:os';
7
7
  import { createInterface } from 'node:readline';
8
8
  import { spawnSync } from 'node:child_process';
9
+ import { getAuthStatus, syncAuth } from './auth/service.mjs';
10
+ import { globalCredentialsPath, readGlobalCredentials, writeGlobalCredentials, writeObsConfig } from './auth/credentials.mjs';
9
11
 
10
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
13
  const PLUGIN_ROOT = resolve(__dirname, '..');
@@ -40,7 +42,7 @@ function opencodeConfigFile() {
40
42
  function codexDesktopSkillsDir() { return join(homedir(), '.agents', 'skills'); }
41
43
  function codexDesktopCommandsDir() { return join(homedir(), '.agents', 'commands'); }
42
44
  function codexDesktopPluginsDir() { return join(homedir(), '.agents', 'huaweicloud-plugins'); }
43
- function codexDesktopConfigFile() { return join(homedir(), '.agents', 'opencode.json'); }
45
+ function codexConfigToml() { return join(homedir(), '.codex', 'config.toml'); }
44
46
 
45
47
  function codeartsSkillsDir() { return join(homedir(), '.codeartsdoer', 'skills'); }
46
48
  function codeartsMcpSettingsDir() { return join(homedir(), '.codeartsdoer', 'mcp'); }
@@ -230,6 +232,14 @@ function uninstallCodex() {
230
232
  shell: true, windowsHide: true, stdio: 'pipe',
231
233
  });
232
234
  console.log(` ${r.stdout ? r.stdout.toString().trim() : r.stderr.toString().trim()}`);
235
+
236
+ for (const name of new Set([marketplaceName, 'HuaweiCloud-Devkit'])) {
237
+ console.log(` Removing Codex marketplace: ${name}`);
238
+ const r2 = spawnSync(`codex plugin marketplace remove "${name}"`, [], {
239
+ shell: true, windowsHide: true, stdio: 'pipe',
240
+ });
241
+ console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
242
+ }
233
243
  }
234
244
 
235
245
  function codexStatus() {
@@ -326,18 +336,27 @@ async function installCodexDesktop() {
326
336
  }
327
337
 
328
338
  const mcpPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
329
- const configPath = codexDesktopConfigFile();
330
- let config = {};
339
+ const configPath = codexConfigToml();
340
+ const section = [
341
+ '[mcp_servers.huaweicloud-devkit]',
342
+ 'command = "node"',
343
+ `args = ["${mcpPath}"]`,
344
+ '',
345
+ '[mcp_servers.huaweicloud-devkit.env]',
346
+ 'HUAWEICLOUD_AGENT_TOOLKIT_MODE = "local"',
347
+ '',
348
+ ].join('\n');
349
+ let existing = '';
331
350
  if (existsSync(configPath)) {
332
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
351
+ try { existing = readFileSync(configPath, 'utf8'); } catch {}
352
+ if (existing.includes('[mcp_servers.huaweicloud-devkit]')) {
353
+ console.log(` Config already configured: ${configPath}`);
354
+ return;
355
+ }
333
356
  }
334
- config.mcp = config.mcp || {};
335
- config.mcp['huaweicloud-devkit'] = {
336
- type: 'local',
337
- command: ['node', mcpPath],
338
- enabled: true,
339
- };
340
- writeFileSync(configPath, JSON.stringify(config, null, 2));
357
+ mkdirSync(dirname(configPath), { recursive: true });
358
+ if (existing && !existing.endsWith('\n')) existing += '\n';
359
+ writeFileSync(configPath, existing + section);
341
360
  console.log(` Config updated: ${configPath}`);
342
361
  }
343
362
 
@@ -369,16 +388,19 @@ function uninstallCodexDesktop() {
369
388
  if (removeIfExists(codexDesktopPluginsDir())) {
370
389
  console.log(' Removed MCP server and safety policy');
371
390
  }
372
- const configPath = codexDesktopConfigFile();
391
+ const configPath = codexConfigToml();
373
392
  if (existsSync(configPath)) {
374
- let config = {};
375
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
376
- if (config.mcp?.['huaweicloud-devkit']) {
377
- delete config.mcp['huaweicloud-devkit'];
378
- if (Object.keys(config.mcp).length === 0) delete config.mcp;
379
- writeFileSync(configPath, JSON.stringify(config, null, 2));
380
- console.log(' Config cleaned');
393
+ const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
394
+ const out = [];
395
+ let skip = false;
396
+ for (const line of lines) {
397
+ if (/^\[mcp_servers\.huaweicloud-devkit(\]|\.)/.test(line)) { skip = true; continue; }
398
+ if (skip && line.startsWith('[')) skip = false;
399
+ if (!skip) out.push(line);
381
400
  }
401
+ while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
402
+ writeFileSync(configPath, out.join('\n') + (out.length > 0 ? '\n' : ''));
403
+ console.log(' Config cleaned');
382
404
  }
383
405
  }
384
406
 
@@ -663,7 +685,9 @@ async function cmdInstall() {
663
685
  // Write install marker for doctor to detect
664
686
  const markerDir = target === 'codearts' ? codeartsPluginsDir()
665
687
  : target === 'workbuddy' ? workbuddyPluginsDir()
688
+ : target === 'codex-desktop' ? codexDesktopPluginsDir()
666
689
  : opencodePluginsDir();
690
+ mkdirSync(markerDir, { recursive: true });
667
691
  writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
668
692
  if (target === 'opencode' || target === 'all') {
669
693
  console.log('Or describe your Huawei Cloud task in OpenCode');
@@ -696,13 +720,15 @@ async function cmdUninstall() {
696
720
  console.log('\n[WorkBuddy]');
697
721
  uninstallWorkBuddy();
698
722
  }
699
- if (target === 'codex' || target === 'all') {
723
+ if (target === 'codex-desktop' || target === 'codex' || target === 'all') {
700
724
  console.log('\n[Codex]');
701
725
  uninstallCodexDesktop();
702
- if (!hasCodexCLI()) {
703
- console.log(' \x1b[33mCodex CLI not found. Run "npm uninstall -g codex" to fully remove.\x1b[0m');
704
- } else {
705
- uninstallCodex();
726
+ if (target === 'codex' || target === 'all') {
727
+ if (!hasCodexCLI()) {
728
+ console.log(' \x1b[33mCodex CLI not found. Run "npm uninstall -g codex" to fully remove.\x1b[0m');
729
+ } else {
730
+ uninstallCodex();
731
+ }
706
732
  }
707
733
  }
708
734
  console.log(`\n\x1b[32mUninstall complete.\x1b[0m`);
@@ -783,11 +809,11 @@ async function cmdDoctor() {
783
809
  if (cfg.mcp && cfg.mcp['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'OpenCode'; }
784
810
  } catch {}
785
811
  }
786
- const codexCfg = codexDesktopConfigFile();
812
+ const codexCfg = codexConfigToml();
787
813
  if (!mcpConfigured && existsSync(codexCfg)) {
788
814
  try {
789
- const cfg = JSON.parse(readFileSync(codexCfg, 'utf8'));
790
- if (cfg.mcp && cfg.mcp['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'Codex Desktop'; }
815
+ const cfg = readFileSync(codexCfg, 'utf8');
816
+ if (cfg.includes('[mcp_servers.huaweicloud-devkit]')) { mcpConfigured = true; mcpCfgTarget = 'Codex Desktop'; }
791
817
  } catch {}
792
818
  }
793
819
  const workbuddyCfg = workbuddyMcpConfigFile();
@@ -860,6 +886,7 @@ async function cmdDoctor() {
860
886
  // Detect "installed but not restarted" — check all supported agents
861
887
  const installedMarkers = [
862
888
  { path: join(opencodePluginsDir(), '.installed'), name: 'OpenCode' },
889
+ { path: join(codexDesktopPluginsDir(), '.installed'), name: 'Codex Desktop' },
863
890
  { path: join(workbuddyPluginsDir(), '.installed'), name: 'WorkBuddy' },
864
891
  { path: join(codeartsPluginsDir(), '.installed'), name: 'CodeArts' },
865
892
  ];
@@ -1017,6 +1044,175 @@ async function cmdInstallHcloud() {
1017
1044
  console.log('\nThen run: npx huaweicloud-devkit doctor');
1018
1045
  }
1019
1046
 
1047
+ function readLineQuestion(prompt) {
1048
+ return new Promise((resolve) => {
1049
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1050
+ rl.question(prompt, (answer) => {
1051
+ rl.close();
1052
+ resolve(answer.trim());
1053
+ });
1054
+ });
1055
+ }
1056
+
1057
+ async function readSecret(prompt) {
1058
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1059
+ return readLineQuestion(prompt);
1060
+ }
1061
+
1062
+ process.stdout.write(prompt);
1063
+ const wasRaw = process.stdin.isRaw;
1064
+ process.stdin.setRawMode(true);
1065
+ process.stdin.resume();
1066
+
1067
+ return new Promise((resolve) => {
1068
+ let value = '';
1069
+ const onData = (chunk) => {
1070
+ for (const ch of chunk.toString('utf8')) {
1071
+ if (ch === '\r' || ch === '\n') {
1072
+ cleanup();
1073
+ resolve(value.trim());
1074
+ return;
1075
+ }
1076
+ if (ch === '\u0003') {
1077
+ cleanup();
1078
+ process.exit(130);
1079
+ }
1080
+ if (ch === '\b' || ch === '\u007f') {
1081
+ value = value.slice(0, -1);
1082
+ continue;
1083
+ }
1084
+ value += ch;
1085
+ }
1086
+ };
1087
+ const cleanup = () => {
1088
+ process.stdin.setRawMode(wasRaw);
1089
+ process.stdin.pause();
1090
+ process.stdin.off('data', onData);
1091
+ process.stdout.write('\n');
1092
+ };
1093
+ process.stdin.on('data', onData);
1094
+ });
1095
+ }
1096
+
1097
+ function configureHcloud(credentials) {
1098
+ const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
1099
+ const args = [
1100
+ 'configure',
1101
+ 'set',
1102
+ `--cli-access-key=${credentials.ak}`,
1103
+ `--cli-secret-key=${credentials.sk}`,
1104
+ `--cli-region=${credentials.region || ''}`,
1105
+ ];
1106
+ const r = spawnSync(hcloudBin, args, {
1107
+ shell: false,
1108
+ windowsHide: true,
1109
+ stdio: 'pipe',
1110
+ timeout: 30000,
1111
+ });
1112
+ return {
1113
+ ok: r.status === 0,
1114
+ code: r.status,
1115
+ error: String(r.stderr || '').trim().slice(0, 240),
1116
+ };
1117
+ }
1118
+
1119
+ function printAuthAgents(agents = {}) {
1120
+ for (const [agent, info] of Object.entries(agents)) {
1121
+ console.log(` ${agent}: ${info.configured ? '[OK]' : '[MISSING]'}`);
1122
+ }
1123
+ }
1124
+
1125
+ function printAuthStatus(status) {
1126
+ console.log(`Credentials vault: ${status.credentialsConfigured ? 'configured' : 'missing'} (${status.credentialsPath})`);
1127
+ console.log(`OBS config: ${status.obsConfigured ? 'configured' : 'missing'} (${status.obsConfigPath})`);
1128
+ console.log(`KooCLI: ${status.kooCliInstalled ? 'installed' : 'missing'}`);
1129
+ console.log('Agent MCP registration:');
1130
+ printAuthAgents(status.agents);
1131
+ }
1132
+
1133
+ async function cmdAuthInit() {
1134
+ console.log(BANNER);
1135
+ console.log('HuaweiCloud DevKit Unified Authentication Setup\n');
1136
+
1137
+ let ak = process.env.HW_ACCESS_KEY || '';
1138
+ let sk = process.env.HW_SECRET_KEY || '';
1139
+ let securityToken = process.env.HW_SECURITY_TOKEN || '';
1140
+ let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
1141
+
1142
+ if (!ak) ak = await readSecret('Access Key ID (AK): ');
1143
+ if (!sk) sk = await readSecret('Secret Access Key (SK): ');
1144
+ if (!securityToken) securityToken = await readLineQuestion('Security Token (optional, press Enter to skip): ');
1145
+ if (!region) region = await readLineQuestion('Region (e.g. cn-north-4): ');
1146
+
1147
+ if (!ak || !sk) {
1148
+ console.error('\nAK and SK are required.');
1149
+ process.exitCode = 1;
1150
+ return;
1151
+ }
1152
+ if (!region) {
1153
+ console.error('\nRegion is required to generate the OBS endpoint.');
1154
+ process.exitCode = 1;
1155
+ return;
1156
+ }
1157
+
1158
+ const vaultPath = writeGlobalCredentials({ ak, sk, securityToken, region });
1159
+ console.log(`\nCredentials stored: ${vaultPath}`);
1160
+
1161
+ try {
1162
+ const obs = writeObsConfig({ ak, sk, securityToken, region });
1163
+ console.log(`OBS config synced: ${obs.path} (${obs.endpoint})`);
1164
+ } catch (error) {
1165
+ console.log(`OBS config sync failed: ${error.message}`);
1166
+ }
1167
+
1168
+ if (findHcloudBin()) {
1169
+ const result = configureHcloud({ ak, sk, region });
1170
+ console.log(result.ok ? 'KooCLI profile updated.' : `KooCLI update failed: ${result.error || result.code}`);
1171
+ } else {
1172
+ console.log('KooCLI not found. Run "npx huaweicloud-devkit install-hcloud" and then "auth sync".');
1173
+ }
1174
+
1175
+ console.log('\nNext steps:');
1176
+ console.log(' npx huaweicloud-devkit install --target all');
1177
+ console.log(' Restart your agent sessions.');
1178
+ }
1179
+
1180
+ async function cmdAuthSync() {
1181
+ const target = parseTarget();
1182
+ console.log(BANNER);
1183
+ console.log('Synchronizing Huawei Cloud authentication...\n');
1184
+
1185
+ const credentials = readGlobalCredentials();
1186
+ if (!credentials?.ak || !credentials?.sk) {
1187
+ console.error('No global credentials found. Run "npx huaweicloud-devkit auth init" first.');
1188
+ process.exitCode = 1;
1189
+ return;
1190
+ }
1191
+
1192
+ const result = syncAuth(target);
1193
+ if (result.ok) {
1194
+ console.log(`OBS config synced: ${result.obs.path} (${result.obs.endpoint})`);
1195
+ } else {
1196
+ console.error(result.error);
1197
+ }
1198
+ console.log('Agent MCP registration:');
1199
+ printAuthAgents(result.agents);
1200
+ }
1201
+
1202
+ async function cmdAuthStatus() {
1203
+ const target = parseTarget();
1204
+ console.log(BANNER);
1205
+ console.log('HuaweiCloud DevKit Authentication Status\n');
1206
+ printAuthStatus(getAuthStatus(target));
1207
+ }
1208
+
1209
+ async function cmdAuth() {
1210
+ const sub = (process.argv[3] || 'status').toLowerCase();
1211
+ if (sub === 'init' || sub === 'setup') return cmdAuthInit();
1212
+ if (sub === 'sync' || sub === 'refresh') return cmdAuthSync();
1213
+ return cmdAuthStatus();
1214
+ }
1215
+
1020
1216
  async function main() {
1021
1217
  const cmd = process.argv[2] || 'help';
1022
1218
 
@@ -1047,6 +1243,9 @@ async function main() {
1047
1243
  case 'install-hcloud':
1048
1244
  await cmdInstallHcloud();
1049
1245
  break;
1246
+ case 'auth':
1247
+ await cmdAuth();
1248
+ break;
1050
1249
  case 'help':
1051
1250
  case '--help':
1052
1251
  case '-h':
@@ -1061,6 +1260,7 @@ async function main() {
1061
1260
  console.log(' status Show installation status');
1062
1261
  console.log(' doctor Self-check: hcloud, MCP, skills, auth');
1063
1262
  console.log(' install-hcloud Show KooCLI install commands for your OS');
1263
+ console.log(' auth Manage unified auth: init | sync | status');
1064
1264
  console.log(' help Show this help');
1065
1265
  console.log('\nOptions:');
1066
1266
  console.log(' --target Target agent: opencode (default), codex, codearts, workbuddy, all');
@@ -1070,6 +1270,9 @@ async function main() {
1070
1270
  console.log(' npx huaweicloud-devkit install --target codearts');
1071
1271
  console.log(' npx huaweicloud-devkit install --target workbuddy');
1072
1272
  console.log(' npx huaweicloud-devkit install --target all');
1273
+ console.log(' npx huaweicloud-devkit auth init');
1274
+ console.log(' npx huaweicloud-devkit auth sync --target all');
1275
+ console.log(' npx huaweicloud-devkit auth status --target all');
1073
1276
  break;
1074
1277
  }
1075
1278
  }
@@ -6,6 +6,10 @@ import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { homedir } from 'node:os';
8
8
  import { searchMarketplace } from './search-market.mjs';
9
+ import { execOneShot, execWithSession, closeSession, DEFAULT_WORKSPACE_ID } from './sandbox/session-manager.mjs';
10
+ import { hdkitCheckUser, hdkitSignAgreement, hdkitConnect, hdkitCredentials, hdkitRelease } from './sandbox/hdkitservice-api.mjs';
11
+ import { getAuthStatus, syncAuth } from './auth/service.mjs';
12
+ import { readGlobalCredentials, writeObsConfig as writeObsConfigFile } from './auth/credentials.mjs';
9
13
 
10
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
15
  const SKILLS_ROOT_DEV = join(__dirname, '..', 'skills');
@@ -285,6 +289,128 @@ export const TOOL_DEFINITIONS = [
285
289
  },
286
290
  },
287
291
  },
292
+ {
293
+ name: 'huaweicloud_auth_status',
294
+ description: 'Check unified Huawei Cloud authentication status across the global credential vault, OBS, KooCLI, and all supported agent MCP registrations. Returns only redacted/status information, never credentials.',
295
+ inputSchema: {
296
+ type: 'object',
297
+ properties: {
298
+ target: { type: 'string', description: 'Agent target to check: opencode, codex, codex-desktop, codearts, workbuddy, or all (default).' },
299
+ },
300
+ },
301
+ },
302
+ {
303
+ name: 'huaweicloud_auth_sync',
304
+ description: 'Synchronize credentials from the global Huawei Cloud credential vault to OBS and report agent registration status. Does not write secrets into any agent config.',
305
+ inputSchema: {
306
+ type: 'object',
307
+ properties: {
308
+ target: { type: 'string', description: 'Agent target to report after sync: opencode, codex, codex-desktop, codearts, workbuddy, or all (default).' },
309
+ },
310
+ },
311
+ },
312
+ {
313
+ name: 'huaweicloud_sandbox_exec',
314
+ description: 'Execute a command on a Huawei Cloud workspace terminal via hwlink (one-shot, no session reuse). Each call creates a new connection. Shell state does NOT persist across calls.',
315
+ inputSchema: {
316
+ type: 'object',
317
+ required: ['command'],
318
+ properties: {
319
+ command: { type: 'string', description: 'The shell command to execute on the remote workspace' },
320
+ workspace_id: { type: 'string', description: 'The workspace ID' },
321
+ username: { type: 'string', description: 'Login username for the remote terminal (default: root)' },
322
+ timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 30000)' },
323
+ },
324
+ },
325
+ },
326
+ {
327
+ name: 'huaweicloud_sandbox_exec_with_session',
328
+ description: 'Execute a command on a workspace terminal with session reuse (state persists across calls). Shell state (cd, env vars, aliases) carries over between calls.',
329
+ inputSchema: {
330
+ type: 'object',
331
+ required: ['command'],
332
+ properties: {
333
+ command: { type: 'string', description: 'The shell command to execute on the remote workspace' },
334
+ workspace_id: { type: 'string', description: 'The workspace ID' },
335
+ username: { type: 'string', description: 'Login username for the remote terminal (default: root)' },
336
+ timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 30000)' },
337
+ },
338
+ },
339
+ },
340
+ {
341
+ name: 'huaweicloud_sandbox_close_session',
342
+ description: 'Close the persistent terminal session for a workspace.',
343
+ inputSchema: {
344
+ type: 'object',
345
+ properties: {
346
+ workspace_id: { type: 'string', description: 'The workspace ID' },
347
+ username: { type: 'string', description: 'Login username (default: root)' },
348
+ },
349
+ },
350
+ },
351
+ {
352
+ name: 'huaweicloud_sandbox_check_user',
353
+ description: 'Check if the current user has completed real-name verification and signed the required agreements. Returns realname_verified and agreement_signed status.',
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {},
357
+ },
358
+ },
359
+ {
360
+ name: 'huaweicloud_sandbox_sign_agreement',
361
+ description: 'Sign all unsigned or outdated agreements for the current user. Required before huaweicloud_sandbox_connect if check-user returns agreement_signed=false.',
362
+ inputSchema: {
363
+ type: 'object',
364
+ properties: {},
365
+ },
366
+ },
367
+ {
368
+ name: 'huaweicloud_sandbox_connect',
369
+ description: '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, and connection_address.',
370
+ inputSchema: {
371
+ type: 'object',
372
+ properties: {
373
+ source: { type: 'string', description: 'Source identifier (default: WEB). Options: VSCODE, CLI, WEB, WEBVNC, WEBPTY, WEBIDE, CURSOR, etc.' },
374
+ template_id: { type: 'string', description: 'Template ID; overrides server default (only for new sandbox)' },
375
+ flavor_id: { type: 'string', description: 'Flavor ID; overrides server default (only for new sandbox)' },
376
+ env: { type: 'object', description: 'Environment variables to set in the sandbox (only for new sandbox)' },
377
+ git: {
378
+ type: 'object',
379
+ description: 'Git repo config (only for new sandbox)',
380
+ properties: {
381
+ repo_url: { type: 'string', description: 'Git repository URL' },
382
+ repo_branch: { type: 'string', description: 'Git branch' },
383
+ repo_name: { type: 'string', description: 'Repository name' },
384
+ target_path: { type: 'string', description: 'Clone target path in sandbox' },
385
+ open_type: { type: 'string', description: 'Open type' },
386
+ },
387
+ },
388
+ },
389
+ },
390
+ },
391
+ {
392
+ name: 'huaweicloud_sandbox_credentials',
393
+ description: 'Configure temporary AK/SK for a sandbox via hdkitservice. Injects temporary credentials into the sandbox. The sandbox must be in RUNNING state.',
394
+ inputSchema: {
395
+ type: 'object',
396
+ properties: {
397
+ session_id: { type: 'string', description: 'Session ID from huaweicloud_sandbox_connect' },
398
+ dev_stage_id: { type: 'string', description: 'DevStation environment ID (alternative to session_id)' },
399
+ enable_sts: { type: 'boolean', description: 'Whether to enable STS temporary AK/SK (default: true)' },
400
+ },
401
+ },
402
+ },
403
+ {
404
+ name: 'huaweicloud_sandbox_release',
405
+ description: 'Release a sandbox via hdkitservice. Shuts down and deletes the sandbox, and cleans up the session. Idempotent - releasing a non-existent sandbox returns success.',
406
+ inputSchema: {
407
+ type: 'object',
408
+ properties: {
409
+ session_id: { type: 'string', description: 'Session ID from huaweicloud_sandbox_connect' },
410
+ dev_stage_id: { type: 'string', description: 'DevStation environment ID (alternative to session_id)' },
411
+ },
412
+ },
413
+ },
288
414
  ];
289
415
 
290
416
  export async function callTool(name, args = {}) {
@@ -328,6 +454,40 @@ export async function callTool(name, args = {}) {
328
454
  return searchMarketplace(args.query || '', args.category || '');
329
455
  case 'huaweicloud_setup_obs_config':
330
456
  return setupObsConfig(args.profile);
457
+ case 'huaweicloud_auth_status':
458
+ return getAuthStatus(args.target || 'all');
459
+ case 'huaweicloud_auth_sync':
460
+ return syncAuth(args.target || 'all');
461
+ case 'huaweicloud_sandbox_exec': {
462
+ const sandboxWsId1 = args.workspace_id || DEFAULT_WORKSPACE_ID;
463
+ const sandboxUser1 = args.username || 'root';
464
+ const sandboxTimeout1 = args.timeout_ms || 30000;
465
+ const sandboxResult1 = await execOneShot(sandboxWsId1, args.command, sandboxUser1, sandboxTimeout1);
466
+ return { stdout: sandboxResult1.stdout, exitCode: sandboxResult1.exitCode };
467
+ }
468
+ case 'huaweicloud_sandbox_exec_with_session': {
469
+ const sandboxWsId2 = args.workspace_id || DEFAULT_WORKSPACE_ID;
470
+ const sandboxUser2 = args.username || 'root';
471
+ const sandboxTimeout2 = args.timeout_ms || 30000;
472
+ const sandboxResult2 = await execWithSession(sandboxWsId2, args.command, sandboxUser2, sandboxTimeout2);
473
+ return { stdout: sandboxResult2.stdout, exitCode: sandboxResult2.exitCode };
474
+ }
475
+ case 'huaweicloud_sandbox_close_session': {
476
+ const sandboxWsId3 = args.workspace_id || DEFAULT_WORKSPACE_ID;
477
+ const sandboxUser3 = args.username || 'root';
478
+ const closed = await closeSession(sandboxWsId3, sandboxUser3);
479
+ return closed ? 'ok' : 'not_connected';
480
+ }
481
+ case 'huaweicloud_sandbox_check_user':
482
+ return await hdkitCheckUser();
483
+ case 'huaweicloud_sandbox_sign_agreement':
484
+ return await hdkitSignAgreement();
485
+ case 'huaweicloud_sandbox_connect':
486
+ return await hdkitConnect(args);
487
+ case 'huaweicloud_sandbox_credentials':
488
+ return await hdkitCredentials(args.session_id, args.dev_stage_id, args.enable_sts !== false);
489
+ case 'huaweicloud_sandbox_release':
490
+ return await hdkitRelease(args.session_id, args.dev_stage_id);
331
491
  default:
332
492
  throw new Error(`Unknown tool: ${name}`);
333
493
  }
@@ -395,6 +555,33 @@ async function showProfileRedacted(profile) {
395
555
  }
396
556
 
397
557
  async function setupObsConfig(profile) {
558
+ const stored = readGlobalCredentials();
559
+ if (stored?.ak && stored?.sk) {
560
+ try {
561
+ const obs = writeObsConfigFile(stored);
562
+ return {
563
+ ok: true,
564
+ existed: false,
565
+ created: true,
566
+ path: obs.path,
567
+ region: stored.region,
568
+ endpoint: obs.endpoint,
569
+ source: 'global-credentials',
570
+ note: 'OBS credentials synced from the global credential vault. OBS commands (hcloud OBS ls, mb, cp, etc.) should now work.',
571
+ };
572
+ } catch (error) {
573
+ return {
574
+ ok: false,
575
+ error: error.message,
576
+ nextStep: 'Run "npx huaweicloud-devkit auth init" to refresh credentials and region.',
577
+ };
578
+ }
579
+ }
580
+
581
+ return setupObsConfigFromHcloud(profile);
582
+ }
583
+
584
+ async function setupObsConfigFromHcloud(profile) {
398
585
  const obsConfigPath = join(homedir(), '.obsutilconfig');
399
586
  if (existsSync(obsConfigPath)) {
400
587
  return { ok: true, existed: true, path: obsConfigPath, note: 'OBS config already exists. Delete ~/.obsutilconfig first if you need to re-sync.' };