huaweicloud-devkit 1.1.2-next.3 → 1.1.2-next.5

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 (28) hide show
  1. package/README.md +29 -1
  2. package/README.zh-CN.md +29 -1
  3. package/integrations/dsh/hook-plugin.mjs +11 -6
  4. package/integrations/opencode/hooks/skill-tracker.js +10 -9
  5. package/package.json +1 -1
  6. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  10. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  11. package/plugins/huaweicloud-core/hooks/hooks.json +2 -2
  12. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.mjs +56 -0
  13. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  14. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +1 -1
  15. package/plugins/huaweicloud-core/src/auth/credential-validator.mjs +163 -0
  16. package/plugins/huaweicloud-core/src/auth/credentials.mjs +6 -2
  17. package/plugins/huaweicloud-core/src/auth/project-id.mjs +68 -0
  18. package/plugins/huaweicloud-core/src/auth/reconcile.mjs +30 -6
  19. package/plugins/huaweicloud-core/src/auth/service.mjs +22 -23
  20. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +5 -10
  21. package/plugins/huaweicloud-core/src/hcloud-probe.mjs +154 -0
  22. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +7 -0
  23. package/plugins/huaweicloud-core/src/safety-policy.mjs +17 -5
  24. package/plugins/huaweicloud-core/src/setup-cli.mjs +192 -134
  25. package/plugins/huaweicloud-core/src/telemetry/agent-detect.mjs +2 -2
  26. package/plugins/huaweicloud-core/src/telemetry/agent-registry.mjs +9 -1
  27. package/plugins/huaweicloud-core/src/telemetry/telemetry.mjs +41 -5
  28. package/plugins/huaweicloud-core/src/tools.mjs +86 -38
@@ -11,6 +11,7 @@ import {
11
11
  readLastSync,
12
12
  resolveCredentialsWithRuntime,
13
13
  } from './credentials.mjs';
14
+ import { resolveHcloudCommand } from '../hcloud-probe.mjs';
14
15
  import { redactSecrets } from '../safety-policy.mjs';
15
16
 
16
17
  export { hasRuntimeCredentials };
@@ -47,6 +48,7 @@ export function readKooCliProfiles() {
47
48
  return {
48
49
  current,
49
50
  mtimeMs,
51
+ configPath,
50
52
  profiles: profiles.map((p) => ({
51
53
  name: String(p.name || ''),
52
54
  fingerprint: fingerprint(p.accessKeyId, p.secretAccessKey),
@@ -70,15 +72,32 @@ export function currentFingerprintFromHcloud(res) {
70
72
  return cur ? cur.fingerprint : null;
71
73
  }
72
74
 
75
+ function currentProfileFromHcloud(res) {
76
+ if (res.error) return null;
77
+ return res.profiles.find((p) => p.name === res.current) || null;
78
+ }
79
+
80
+ function s2CurrentMatchesLastDevkitSync(kooCli, s1, s1Fingerprint) {
81
+ const current = currentProfileFromHcloud(kooCli);
82
+ const lastSync = readLastSync();
83
+ if (!current || !lastSync?.ts || !lastSync.kooCliProfile || !lastSync.s1Fingerprint) return false;
84
+ if (!s1?.ak || current.accessKeyId !== s1.ak) return false;
85
+ if (lastSync.kooCliProfile !== kooCli.current) return false;
86
+ if (lastSync.s1Fingerprint !== s1Fingerprint) return false;
87
+ return Number(kooCli.mtimeMs || 0) <= Number(lastSync.ts) + 1000;
88
+ }
89
+
73
90
  export function scanState() {
74
91
  const s1 = readGlobalCredentials() || {};
75
92
  const envAk = process.env.HW_ACCESS_KEY || '';
76
93
  const envSk = process.env.HW_SECRET_KEY || '';
77
94
  const kooCli = readKooCliProfiles();
78
- const currentFp = currentFingerprintFromHcloud(kooCli);
79
95
  const inconsistencies = [];
80
96
 
81
97
  const s1Fingerprint = fingerprint(s1.ak, s1.sk);
98
+ const currentFp = currentFingerprintFromHcloud(kooCli);
99
+ const s2SyncedByDevkit = s1Fingerprint ? s2CurrentMatchesLastDevkitSync(kooCli, s1, s1Fingerprint) : false;
100
+ const effectiveCurrentFp = s2SyncedByDevkit ? s1Fingerprint : currentFp;
82
101
  const envFingerprint = fingerprint(envAk, envSk);
83
102
  const s3 = existsSync(obsConfigPath()) ? parseS3ObsConfig(obsConfigPath()) : null;
84
103
  const s3Fingerprint = s3 ? fingerprint(s3.ak, s3.sk) : null;
@@ -93,12 +112,12 @@ export function scanState() {
93
112
  // nothing resolvable → runtime store inactive
94
113
  }
95
114
 
96
- if (s1Fingerprint && currentFp && s1Fingerprint !== currentFp) {
115
+ if (s1Fingerprint && currentFp && s1Fingerprint !== effectiveCurrentFp) {
97
116
  inconsistencies.push({
98
117
  store: 'S2-current',
99
118
  source: 'KooCLI current profile',
100
119
  fingerprint: currentFp,
101
- manualModified: isManualModified(join(baseHome(), '.hcloud', 'config.json')),
120
+ manualModified: isManualModified(kooCli.configPath || join(baseHome(), '.hcloud', 'config.json')),
102
121
  });
103
122
  }
104
123
  if (s1Fingerprint && s3Fingerprint && s1Fingerprint !== s3Fingerprint) {
@@ -114,7 +133,7 @@ export function scanState() {
114
133
  stores: {
115
134
  s1Fingerprint,
116
135
  envFingerprint,
117
- currentFingerprint: currentFp,
136
+ currentFingerprint: effectiveCurrentFp,
118
137
  s3Fingerprint,
119
138
  runtimeFingerprint,
120
139
  },
@@ -153,7 +172,7 @@ export function exportStateForStatus(_jailed) {
153
172
  }
154
173
 
155
174
  export function runHcloudConfigure(profile, ak, sk, region) {
156
- const bin = process.env.HCLOUD_BIN || 'hcloud';
175
+ const { executable, argsPrefix } = resolveHcloudCommand();
157
176
  const args = [
158
177
  'configure',
159
178
  'set',
@@ -162,7 +181,12 @@ export function runHcloudConfigure(profile, ak, sk, region) {
162
181
  `--cli-secret-key=${sk}`,
163
182
  `--cli-region=${region || ''}`,
164
183
  ];
165
- const r = spawnSync(bin, args, { shell: false, windowsHide: true, stdio: 'pipe', timeout: 30000 });
184
+ const r = spawnSync(executable, [...argsPrefix, ...args], {
185
+ shell: false,
186
+ windowsHide: true,
187
+ stdio: 'pipe',
188
+ timeout: 30000,
189
+ });
166
190
  return {
167
191
  ok: r.status === 0,
168
192
  error: redactSecrets(
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { spawnSync } from 'node:child_process';
3
2
 
4
3
  import { getAgentRegistrationStatuses } from './agent-registration.mjs';
4
+ import { resolveAndApplyProjectId } from './project-id.mjs';
5
5
  import {
6
6
  globalCredentialsPath,
7
7
  obsConfigPath,
@@ -10,38 +10,27 @@ import {
10
10
  writeObsConfig,
11
11
  } from './credentials.mjs';
12
12
  import {
13
+ fingerprint,
13
14
  exportStateForStatus,
14
15
  hasRuntimeCredentials,
15
16
  resolveManagedProfile,
16
17
  runHcloudConfigure,
17
18
  } from './reconcile.mjs';
18
-
19
- function hcloudInstalled() {
20
- const bin = process.env.HCLOUD_BIN || 'hcloud';
21
- try {
22
- const r = spawnSync(`"${bin}" version`, [], {
23
- shell: true,
24
- windowsHide: true,
25
- stdio: 'pipe',
26
- timeout: 5000,
27
- });
28
- const out = `${r.stdout || ''}${r.stderr || ''}`;
29
- return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
30
- } catch {
31
- return false;
32
- }
33
- }
19
+ import { hcloudProbeNextStep, probeHcloud } from '../hcloud-probe.mjs';
34
20
 
35
21
  export function getAuthStatus(target = 'all') {
36
22
  const credentials = readGlobalCredentials();
37
23
  const reconciled = { ...exportStateForStatus(), runtimeActive: hasRuntimeCredentials() };
24
+ const hcloud = probeHcloud();
38
25
  return {
39
26
  target,
40
27
  credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
41
28
  credentialsPath: globalCredentialsPath(),
42
29
  obsConfigured: existsSync(obsConfigPath()),
43
30
  obsConfigPath: obsConfigPath(),
44
- kooCliInstalled: hcloudInstalled(),
31
+ kooCliInstalled: hcloud.installed,
32
+ kooCliStatus: hcloud.status,
33
+ kooCliNextStep: hcloudProbeNextStep(hcloud),
45
34
  reconciled,
46
35
  agents: getAgentRegistrationStatuses(target).agents,
47
36
  };
@@ -85,11 +74,17 @@ export function syncAuth(target = 'all') {
85
74
  };
86
75
  }
87
76
 
88
- if (!hcloudInstalled()) {
77
+ const hcloud = probeHcloud();
78
+ if (!hcloud.ok) {
89
79
  return {
90
80
  ok: false,
91
- error: 'KooCLI not installed.',
92
- nextStep: 'Install KooCLI or point HCLOUD_BIN at the hcloud executable.',
81
+ error:
82
+ hcloud.status === 'sandbox_home_failure'
83
+ ? 'KooCLI detected but cannot resolve the user home directory in this agent sandbox.'
84
+ : hcloud.status === 'privacy_pending'
85
+ ? 'KooCLI privacy agreement is pending.'
86
+ : 'KooCLI not ready.',
87
+ nextStep: hcloudProbeNextStep(hcloud),
93
88
  obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
94
89
  };
95
90
  }
@@ -104,9 +99,11 @@ export function syncAuth(target = 'all') {
104
99
  };
105
100
  }
106
101
 
107
- writeLastSync();
102
+ const project = resolveAndApplyProjectId({ region: credentials.region, profile });
108
103
 
109
- return {
104
+ writeLastSync({ kooCliProfile: profile, s1Fingerprint: fingerprint(credentials.ak, credentials.sk) });
105
+
106
+ const result = {
110
107
  ok: true,
111
108
  profile,
112
109
  obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
@@ -115,4 +112,6 @@ export function syncAuth(target = 'all') {
115
112
  agents: getAgentRegistrationStatuses(target).agents,
116
113
  note: 'OBS credentials were synced from the global credential vault. Agent MCP registration is managed by "npx huaweicloud-devkit install --target <agent>".',
117
114
  };
115
+ if (project.ok) result.projectId = project.projectId;
116
+ return result;
118
117
  }
@@ -1,11 +1,11 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
- import { homedir } from 'node:os';
3
+ import { mkdirSync, writeFileSync } from 'node:fs';
5
4
  import { join } from 'node:path';
6
5
 
7
6
  import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
8
7
  import { getProxySettings } from './proxy/proxy-config.mjs';
8
+ import { findHcloudBin, resolveHcloudCommand } from './hcloud-probe.mjs';
9
9
 
10
10
  const DEFAULT_TIMEOUT_MS = 60_000;
11
11
  const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
@@ -67,10 +67,10 @@ function preflightSecurityGroupCheck(normalizedArgs) {
67
67
  const findings = [];
68
68
  for (const sgId of sgIds) {
69
69
  try {
70
- const hcloudBin = process.env.HCLOUD_BIN || 'hcloud';
70
+ const { executable, argsPrefix } = resolveHcloudCommand();
71
71
  const spawnArgs = ['VPC', 'ListSecurityGroupRules', `--security_group_id.1=${sgId}`];
72
72
  if (regionArg) spawnArgs.push(regionArg);
73
- const r = spawnSync(hcloudBin, spawnArgs, {
73
+ const r = spawnSync(executable, [...argsPrefix, ...spawnArgs], {
74
74
  shell: false,
75
75
  windowsHide: true,
76
76
  stdio: 'pipe',
@@ -216,12 +216,7 @@ async function runtimeCurrentMismatchWarning() {
216
216
  }
217
217
 
218
218
  function discoverHcloudPath() {
219
- if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
220
- const candidates =
221
- process.platform === 'win32'
222
- ? [join(homedir(), 'hcloud', 'hcloud.exe')]
223
- : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
224
- return candidates.find((c) => existsSync(c)) || null;
219
+ return findHcloudBin();
225
220
  }
226
221
 
227
222
  function runHcloudOnce(plan, options) {
@@ -0,0 +1,154 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { compareVersion, getKooCliVersion, parseHcloudVersion } from './koocli-version.mjs';
7
+ import { redactSecrets } from './safety-policy.mjs';
8
+
9
+ const VERSION_RE = /KooCLI|Current.*version|当前KooCLI/i;
10
+
11
+ export function findHcloudBin() {
12
+ if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
13
+ const candidates =
14
+ process.platform === 'win32'
15
+ ? [join(homedir(), 'hcloud', 'hcloud.exe')]
16
+ : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
17
+ const found = candidates.find((candidate) => existsSync(candidate));
18
+ if (found) return found;
19
+
20
+ const locator =
21
+ process.platform === 'win32'
22
+ ? spawnSync('where.exe', ['hcloud'], { windowsHide: true, stdio: 'pipe', encoding: 'utf8', timeout: 3000 })
23
+ : spawnSync('sh', ['-c', 'command -v hcloud'], {
24
+ windowsHide: true,
25
+ stdio: 'pipe',
26
+ encoding: 'utf8',
27
+ timeout: 3000,
28
+ });
29
+ if (locator.status === 0) {
30
+ const first = String(locator.stdout || '')
31
+ .split(/\r?\n/)
32
+ .map((line) => line.trim())
33
+ .find(Boolean);
34
+ if (first) return first;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function envHcloudArgs() {
40
+ if (!process.env.HCLOUD_BIN_ARGS_JSON) return [];
41
+ try {
42
+ const parsed = JSON.parse(process.env.HCLOUD_BIN_ARGS_JSON);
43
+ if (Array.isArray(parsed)) return parsed.map((item) => String(item));
44
+ } catch {}
45
+ return [];
46
+ }
47
+
48
+ export function resolveHcloudCommand(options = {}) {
49
+ return {
50
+ executable: options.executable || findHcloudBin() || 'hcloud',
51
+ argsPrefix: Array.isArray(options.executableArgs) ? options.executableArgs : envHcloudArgs(),
52
+ };
53
+ }
54
+
55
+ export function classifyHcloudProbe(result, requiredVersion = getKooCliVersion()) {
56
+ const stdout = String(result.stdout || '');
57
+ const stderr = String(result.stderr || '');
58
+ const output = `${stdout}${stderr}`;
59
+ const installedVersion = parseHcloudVersion(output);
60
+
61
+ if (result.error?.code === 'ENOENT') {
62
+ return {
63
+ status: 'not_found',
64
+ installed: false,
65
+ ok: false,
66
+ errorCode: 'HCLOUD_NOT_FOUND',
67
+ };
68
+ }
69
+
70
+ if (result.status === 0 && (VERSION_RE.test(output) || installedVersion)) {
71
+ const versionMismatch = !!(
72
+ requiredVersion &&
73
+ installedVersion &&
74
+ compareVersion(installedVersion, requiredVersion) !== 0
75
+ );
76
+ return {
77
+ status: versionMismatch ? 'version_mismatch' : 'ok',
78
+ installed: true,
79
+ ok: true,
80
+ installedVersion,
81
+ requiredVersion: requiredVersion || undefined,
82
+ versionMismatch,
83
+ };
84
+ }
85
+
86
+ if (/获取当前用户家目录失败|home\s+directory|user\s+home|home\s+dir|homedir/i.test(output)) {
87
+ return {
88
+ status: 'sandbox_home_failure',
89
+ installed: true,
90
+ ok: false,
91
+ errorCode: 'HCLOUD_SANDBOX_HOME_FAILURE',
92
+ };
93
+ }
94
+
95
+ if (
96
+ /同意并继续使用|不同意并退出|隐私协议|隐私声明|privacy agreement|privacy statement|invalid character|无效字符/i.test(
97
+ output,
98
+ )
99
+ ) {
100
+ return {
101
+ status: 'privacy_pending',
102
+ installed: true,
103
+ ok: false,
104
+ errorCode: 'HCLOUD_PRIVACY_PENDING',
105
+ };
106
+ }
107
+
108
+ return {
109
+ status: 'unavailable',
110
+ installed: false,
111
+ ok: false,
112
+ errorCode: 'HCLOUD_UNAVAILABLE',
113
+ };
114
+ }
115
+
116
+ export function probeHcloud(options = {}) {
117
+ const { executable, argsPrefix } = resolveHcloudCommand(options);
118
+ const r = spawnSync(executable, [...argsPrefix, 'version'], {
119
+ shell: false,
120
+ windowsHide: true,
121
+ stdio: 'pipe',
122
+ encoding: 'utf8',
123
+ timeout: options.timeoutMs ?? 5000,
124
+ });
125
+ const classified = classifyHcloudProbe(r, options.requiredVersion);
126
+ const stdout = redactSecrets(String(r.stdout || '').trim());
127
+ const stderr = redactSecrets(String(r.stderr || r.error?.message || '').trim());
128
+ return {
129
+ executable,
130
+ exitCode: r.status,
131
+ stdout,
132
+ stderr,
133
+ output: classified.ok ? stdout : stderr || stdout,
134
+ ...classified,
135
+ };
136
+ }
137
+
138
+ export function hcloudProbeNextStep(probe) {
139
+ if (probe.status === 'ok')
140
+ return 'Use huaweicloud_show_profile_redacted to inspect the active KooCLI profile safely.';
141
+ if (probe.status === 'version_mismatch') {
142
+ return `KooCLI version mismatch: installed ${probe.installedVersion}, this plugin is paired with ${probe.requiredVersion}. Reinstall the pinned version (see huaweicloud-cli-and-auth skill) and restart the agent.`;
143
+ }
144
+ if (probe.status === 'sandbox_home_failure') {
145
+ return 'KooCLI was found, but this agent sandbox cannot resolve the Windows user home directory. Set HCLOUD_BIN to the full hcloud path, restart Codex, or verify from a normal terminal.';
146
+ }
147
+ if (probe.status === 'privacy_pending') {
148
+ return 'KooCLI requires accepting its one-time privacy agreement. Run hcloud version in a real terminal and accept the prompt, then restart the agent.';
149
+ }
150
+ if (probe.status === 'not_found') {
151
+ return 'hcloud executable not found. Set HCLOUD_BIN to the full hcloud path, or install KooCLI: npx huaweicloud-devkit install-hcloud. Then restart the agent.';
152
+ }
153
+ return 'Install Huawei Cloud KooCLI: npx huaweicloud-devkit install-hcloud. Configure credentials outside the agent conversation.';
154
+ }
@@ -26,6 +26,13 @@ export async function getProxyDispatcher(targetUrl) {
26
26
  return cachedDispatcher;
27
27
  }
28
28
 
29
+ export async function fetchWithProxy(url, options = {}) {
30
+ const dispatcher = await getProxyDispatcher(url);
31
+ if (!dispatcher) return fetch(url, options);
32
+ const { fetch: undiciFetch } = await import('undici');
33
+ return undiciFetch(url, { ...options, dispatcher });
34
+ }
35
+
29
36
  export function clearProxyDispatcherCache() {
30
37
  cachedDispatcher = undefined;
31
38
  cachedDispatcherProxyUrl = null;
@@ -109,6 +109,14 @@ function applyCommandRiskRules(base, normalizedArgs, options = {}) {
109
109
  return mergeRiskDecision(base, risk);
110
110
  }
111
111
 
112
+ function applyRawCommandRiskRules(base, command, options = {}) {
113
+ if (base.decision === 'deny' || options.skipRiskRules === true) {
114
+ return base;
115
+ }
116
+ const risk = evaluateCommandRisk(command);
117
+ return mergeRiskDecision(base, risk);
118
+ }
119
+
112
120
  export function classifyHcloudArgs(args, options = {}) {
113
121
  const policy = options.policy || DEFAULT_POLICY;
114
122
  const { service, operation, args: normalizedArgs } = commandOperation(args);
@@ -340,11 +348,15 @@ export function classifyTextCommand(command, options = {}) {
340
348
  };
341
349
  }
342
350
 
343
- return {
344
- decision: 'allow',
345
- risk: 'not_huaweicloud',
346
- reason: 'No Huawei Cloud safety rule matched.',
347
- };
351
+ return applyRawCommandRiskRules(
352
+ {
353
+ decision: 'allow',
354
+ risk: 'not_huaweicloud',
355
+ reason: 'No Huawei Cloud safety rule matched.',
356
+ },
357
+ text,
358
+ options,
359
+ );
348
360
  }
349
361
 
350
362
  export function assertAllowed(result) {