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

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/README.md CHANGED
@@ -57,14 +57,36 @@ npx --yes huaweicloud-devkit install --target codex
57
57
  **Restart the Codex session** after installation.
58
58
 
59
59
  ```bash
60
+ codex plugin list # verify huaweicloud-devkit@huaweicloud-devkit is installed and enabled
60
61
  npx --yes huaweicloud-devkit doctor --target codex
61
62
  npx --yes huaweicloud-devkit status --target codex
62
63
  npx --yes huaweicloud-devkit update --target codex
63
64
  npx --yes huaweicloud-devkit uninstall --target codex
64
65
  ```
65
66
 
67
+ Then mention `@huaweicloud-devkit` in Codex or describe your Huawei Cloud task directly.
68
+
66
69
  > **Requires Codex CLI** — the `codex` command must be in PATH. If Codex is installed via WindowsApps (Microsoft Store), use `--target codex-desktop` instead. Run `codex --version` to verify CLI availability.
67
70
 
71
+ ### Codex Desktop
72
+
73
+ Use this target when the Codex CLI is unavailable or when Codex is installed through WindowsApps on Windows.
74
+
75
+ ```bash
76
+ npx --yes huaweicloud-devkit install --target codex-desktop
77
+ ```
78
+
79
+ **Restart the Codex Desktop session** after installation.
80
+
81
+ ```bash
82
+ npx --yes huaweicloud-devkit doctor --target codex-desktop
83
+ npx --yes huaweicloud-devkit status --target codex-desktop
84
+ npx --yes huaweicloud-devkit update --target codex-desktop
85
+ npx --yes huaweicloud-devkit uninstall --target codex-desktop
86
+ ```
87
+
88
+ Then mention `@huaweicloud-devkit` in a new Codex Desktop task or describe your Huawei Cloud task directly.
89
+
68
90
  ### CodeArts Agent
69
91
 
70
92
  ```bash
package/README.zh-CN.md CHANGED
@@ -57,14 +57,36 @@ npx --yes huaweicloud-devkit install --target codex
57
57
  安装后**重启 Codex 会话**。
58
58
 
59
59
  ```bash
60
+ codex plugin list # 验证 huaweicloud-devkit@huaweicloud-devkit 已安装并启用
60
61
  npx --yes huaweicloud-devkit doctor --target codex
61
62
  npx --yes huaweicloud-devkit status --target codex
62
63
  npx --yes huaweicloud-devkit update --target codex
63
64
  npx --yes huaweicloud-devkit uninstall --target codex
64
65
  ```
65
66
 
67
+ 随后在 Codex 中提及 `@huaweicloud-devkit`,或直接描述华为云任务。
68
+
66
69
  > **需要 Codex CLI** — `codex` 命令必须在 PATH 中。若 Codex 通过 WindowsApps(Microsoft Store)安装,请使用 `--target codex-desktop` 替代。运行 `codex --version` 验证 CLI 可用性。
67
70
 
71
+ ### Codex Desktop
72
+
73
+ 当 Codex CLI 不可用,或 Windows 上通过 WindowsApps 安装 Codex 时,使用此目标。
74
+
75
+ ```bash
76
+ npx --yes huaweicloud-devkit install --target codex-desktop
77
+ ```
78
+
79
+ 安装后**重启 Codex Desktop 会话**。
80
+
81
+ ```bash
82
+ npx --yes huaweicloud-devkit doctor --target codex-desktop
83
+ npx --yes huaweicloud-devkit status --target codex-desktop
84
+ npx --yes huaweicloud-devkit update --target codex-desktop
85
+ npx --yes huaweicloud-devkit uninstall --target codex-desktop
86
+ ```
87
+
88
+ 随后在新的 Codex Desktop 任务中提及 `@huaweicloud-devkit`,或直接描述华为云任务。
89
+
68
90
  ### CodeArts Agent(码道)
69
91
 
70
92
  ```bash
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.2-next.3",
4
+ "version": "1.1.2-next.4",
5
5
  "kooCliVersion": "7.2.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.2-next.3",
20
+ "version": "1.1.2-next.4",
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.2-next.3",
23
+ "version": "1.1.2-next.4",
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.2-next.3",
20
+ "version": "1.1.2-next.4",
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.2-next.3",
3
+ "version": "1.1.2-next.4",
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.2-next.3",
3
+ "version": "1.1.2-next.4",
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",
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/huaweicloud-safety.py\"",
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/huaweicloud-safety.mjs\"",
10
10
  "timeout": 5
11
11
  }
12
12
  ]
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/huaweicloud-safety.py\"",
19
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/huaweicloud-safety.mjs\"",
20
20
  "timeout": 5
21
21
  }
22
22
  ]
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { classifyTextCommand } from '../src/safety-policy.mjs';
4
+
5
+ const DENY_PREFIX = 'Huawei Cloud safety hook blocked this action: ';
6
+
7
+ function commandText(toolInput) {
8
+ if (typeof toolInput === 'string') return toolInput;
9
+ if (toolInput && typeof toolInput === 'object') {
10
+ const values = [];
11
+ for (const key of ['command', 'cmd', 'script', 'args', 'arguments']) {
12
+ const value = toolInput[key];
13
+ if (Array.isArray(value)) values.push(value.map(String).join(' '));
14
+ else if (value !== undefined && value !== null) values.push(String(value));
15
+ }
16
+ if (values.length > 0) return values.join('\n');
17
+ return JSON.stringify(toolInput);
18
+ }
19
+ return JSON.stringify(toolInput);
20
+ }
21
+
22
+ function deny(reason) {
23
+ process.stdout.write(
24
+ JSON.stringify({
25
+ hookSpecificOutput: {
26
+ hookEventName: 'PreToolUse',
27
+ permissionDecision: 'deny',
28
+ permissionDecisionReason: DENY_PREFIX + reason,
29
+ },
30
+ }) + '\n',
31
+ );
32
+ }
33
+
34
+ function readStdin() {
35
+ try {
36
+ return readFileSync(0, 'utf8');
37
+ } catch {
38
+ return '';
39
+ }
40
+ }
41
+
42
+ function main() {
43
+ const input = readStdin();
44
+ let data;
45
+ try {
46
+ data = JSON.parse(input);
47
+ } catch {
48
+ return;
49
+ }
50
+
51
+ const text = commandText(data.tool_input ?? {});
52
+ const result = classifyTextCommand(text);
53
+ if (result.decision === 'deny') deny(result.reason);
54
+ }
55
+
56
+ main();
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.2-next.3",
5
+ "version": "1.1.2-next.4",
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.",
@@ -27,7 +27,7 @@ Use this skill before any Huawei Cloud action that may expose secrets, change re
27
27
 
28
28
  ## Enforcement Layers
29
29
 
30
- - Hooks: `hooks/huaweicloud-safety.py` can block risky tool calls on platforms that support plugin hooks.
30
+ - Hooks: `hooks/huaweicloud-safety.mjs` blocks risky Codex native hook calls without requiring Python; `hooks/huaweicloud-safety.py` remains for Hermes/Python hook compatibility.
31
31
  - MCP wrapper: the Node MCP server applies the same safety policy for Codex/OpenCode paths that do not enforce hooks.
32
32
  - Skills: this document teaches agents the rule before they act.
33
33
 
@@ -272,10 +272,14 @@ export function readLastSync() {
272
272
  }
273
273
  }
274
274
 
275
- export function writeLastSync() {
275
+ export function writeLastSync(metadata = {}) {
276
276
  const path = lastSyncPath();
277
277
  mkdirSync(dirname(path), { recursive: true });
278
- const payload = { ts: Date.now() };
278
+ const payload = {
279
+ ts: Date.now(),
280
+ ...(metadata.kooCliProfile ? { kooCliProfile: String(metadata.kooCliProfile) } : {}),
281
+ ...(metadata.s1Fingerprint ? { s1Fingerprint: String(metadata.s1Fingerprint) } : {}),
282
+ };
279
283
  writeFileSync(path, JSON.stringify(payload), { encoding: 'utf8', mode: 0o600 });
280
284
  ensurePrivateMode(path);
281
285
  }
@@ -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,5 +1,4 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { spawnSync } from 'node:child_process';
3
2
 
4
3
  import { getAgentRegistrationStatuses } from './agent-registration.mjs';
5
4
  import {
@@ -10,38 +9,27 @@ import {
10
9
  writeObsConfig,
11
10
  } from './credentials.mjs';
12
11
  import {
12
+ fingerprint,
13
13
  exportStateForStatus,
14
14
  hasRuntimeCredentials,
15
15
  resolveManagedProfile,
16
16
  runHcloudConfigure,
17
17
  } 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
- }
18
+ import { hcloudProbeNextStep, probeHcloud } from '../hcloud-probe.mjs';
34
19
 
35
20
  export function getAuthStatus(target = 'all') {
36
21
  const credentials = readGlobalCredentials();
37
22
  const reconciled = { ...exportStateForStatus(), runtimeActive: hasRuntimeCredentials() };
23
+ const hcloud = probeHcloud();
38
24
  return {
39
25
  target,
40
26
  credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
41
27
  credentialsPath: globalCredentialsPath(),
42
28
  obsConfigured: existsSync(obsConfigPath()),
43
29
  obsConfigPath: obsConfigPath(),
44
- kooCliInstalled: hcloudInstalled(),
30
+ kooCliInstalled: hcloud.installed,
31
+ kooCliStatus: hcloud.status,
32
+ kooCliNextStep: hcloudProbeNextStep(hcloud),
45
33
  reconciled,
46
34
  agents: getAgentRegistrationStatuses(target).agents,
47
35
  };
@@ -85,11 +73,17 @@ export function syncAuth(target = 'all') {
85
73
  };
86
74
  }
87
75
 
88
- if (!hcloudInstalled()) {
76
+ const hcloud = probeHcloud();
77
+ if (!hcloud.ok) {
89
78
  return {
90
79
  ok: false,
91
- error: 'KooCLI not installed.',
92
- nextStep: 'Install KooCLI or point HCLOUD_BIN at the hcloud executable.',
80
+ error:
81
+ hcloud.status === 'sandbox_home_failure'
82
+ ? 'KooCLI detected but cannot resolve the user home directory in this agent sandbox.'
83
+ : hcloud.status === 'privacy_pending'
84
+ ? 'KooCLI privacy agreement is pending.'
85
+ : 'KooCLI not ready.',
86
+ nextStep: hcloudProbeNextStep(hcloud),
93
87
  obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
94
88
  };
95
89
  }
@@ -104,7 +98,7 @@ export function syncAuth(target = 'all') {
104
98
  };
105
99
  }
106
100
 
107
- writeLastSync();
101
+ writeLastSync({ kooCliProfile: profile, s1Fingerprint: fingerprint(credentials.ak, credentials.sk) });
108
102
 
109
103
  return {
110
104
  ok: true,
@@ -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
+ }
@@ -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) {
@@ -18,7 +18,7 @@ import { createRequire } from 'node:module';
18
18
 
19
19
  import { getAuthStatus, syncAuth } from './auth/service.mjs';
20
20
  import { SUPPORTED_AGENT_TARGETS } from './auth/agent-registration.mjs';
21
- import { resolveManagedProfile } from './auth/reconcile.mjs';
21
+ import { fingerprint, resolveManagedProfile } from './auth/reconcile.mjs';
22
22
  import { redactSecrets } from './safety-policy.mjs';
23
23
  import {
24
24
  globalCredentialsPath,
@@ -36,13 +36,8 @@ import {
36
36
  } from './proxy/proxy-config.mjs';
37
37
  import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
38
38
  import { queryDistTagsSync, determineTarget, semverCompare } from './update-check.mjs';
39
- import {
40
- getKooCliVersion,
41
- parseHcloudVersion,
42
- compareVersion,
43
- kooCliDownloadBase,
44
- KOO_CLI_BASE,
45
- } from './koocli-version.mjs';
39
+ import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
40
+ import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
46
41
 
47
42
  const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite');
48
43
 
@@ -420,23 +415,6 @@ function detectCodeartsSandbox() {
420
415
  }
421
416
  }
422
417
 
423
- // Locate an existing hcloud executable (HCLOUD_BIN, ~/hcloud on Windows, ~/.local/bin elsewhere).
424
- function findHcloudBin() {
425
- if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
426
- const isWin = platform() === 'win32';
427
- const candidates = isWin
428
- ? [join(homedir(), 'hcloud', 'hcloud.exe')]
429
- : [
430
- join(homedir(), '.local', 'bin', 'hcloud'),
431
- join(homedir(), 'hcloud', 'hcloud'),
432
- join(homedir(), 'hcloud', 'hcloud.exe'),
433
- ];
434
- for (const c of candidates) {
435
- if (existsSync(c)) return c;
436
- }
437
- return null;
438
- }
439
-
440
418
  function printSandboxWarning(reason) {
441
419
  console.log(`\n\x1b[1m\x1b[31m⚠ 检测到码道沙箱模式 (bash_mode: sandbox)\x1b[0m`);
442
420
  console.log(`\x1b[31m ${reason}\x1b[0m`);
@@ -630,27 +608,25 @@ function hasCodexCLI() {
630
608
  return false;
631
609
  }
632
610
 
633
- function checkHcloud() {
634
- const bin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
635
- if (!existsSync(bin)) return false;
636
- try {
637
- if (statSync(bin).size < 1024) return false;
638
- } catch {
639
- return false;
640
- }
611
+ function getMarketplaceName() {
612
+ const marketplacePath = join(PACKAGE_ROOT, '.agents', 'plugins', 'marketplace.json');
641
613
  try {
642
- const r = spawnSync(`"${bin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
643
- const out = (r.stdout ? r.stdout.toString() : '') + (r.stderr ? r.stderr.toString() : '');
644
- return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
645
- } catch {
646
- return false;
647
- }
614
+ const manifest = JSON.parse(readFileSync(marketplacePath, 'utf8'));
615
+ if (manifest.name) return manifest.name;
616
+ } catch {}
617
+ return 'huaweicloud-devkit';
648
618
  }
649
619
 
650
- function getMarketplaceName() {
620
+ function getCodexPluginName() {
651
621
  const marketplacePath = join(PACKAGE_ROOT, '.agents', 'plugins', 'marketplace.json');
652
622
  try {
653
- const manifest = JSON.parse(readFileSync(marketplacePath, 'utf8'));
623
+ const marketplace = JSON.parse(readFileSync(marketplacePath, 'utf8'));
624
+ const pluginName = marketplace.plugins?.[0]?.name;
625
+ if (pluginName) return pluginName;
626
+ } catch {}
627
+ const codexManifestPath = join(PLUGIN_ROOT, '.codex-plugin', 'plugin.json');
628
+ try {
629
+ const manifest = JSON.parse(readFileSync(codexManifestPath, 'utf8'));
654
630
  if (manifest.name) return manifest.name;
655
631
  } catch {}
656
632
  return 'huaweicloud-devkit';
@@ -658,7 +634,7 @@ function getMarketplaceName() {
658
634
 
659
635
  function installCodex() {
660
636
  const marketplaceRoot = PACKAGE_ROOT;
661
- const pluginName = 'huaweicloud-core';
637
+ const pluginName = getCodexPluginName();
662
638
  const marketplaceName = getMarketplaceName();
663
639
 
664
640
  console.log(` Registering Codex marketplace: ${marketplaceRoot}`);
@@ -674,6 +650,10 @@ function installCodex() {
674
650
  console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
675
651
  return false;
676
652
  }
653
+ if (r1.status !== 0) {
654
+ console.log(` \x1b[31mCodex marketplace registration failed.\x1b[0m`);
655
+ return false;
656
+ }
677
657
 
678
658
  console.log(` Installing plugin: ${pluginName}@${marketplaceName}`);
679
659
  const r2 = spawnSync(`codex plugin add "${pluginName}@${marketplaceName}"`, [], {
@@ -688,20 +668,26 @@ function installCodex() {
688
668
  console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
689
669
  return false;
690
670
  }
671
+ if (r2.status !== 0) {
672
+ console.log(` \x1b[31mCodex plugin installation failed.\x1b[0m`);
673
+ return false;
674
+ }
691
675
 
692
676
  return true;
693
677
  }
694
678
 
695
679
  function uninstallCodex() {
696
- const pluginName = 'huaweicloud-core';
680
+ const pluginName = getCodexPluginName();
697
681
  const marketplaceName = getMarketplaceName();
698
- console.log(` Removing Codex plugin: ${pluginName}@${marketplaceName}`);
699
- const r = spawnSync(`codex plugin remove "${pluginName}@${marketplaceName}"`, [], {
700
- shell: true,
701
- windowsHide: true,
702
- stdio: 'pipe',
703
- });
704
- console.log(` ${r.stdout ? r.stdout.toString().trim() : r.stderr.toString().trim()}`);
682
+ for (const name of new Set([pluginName, 'huaweicloud-core'])) {
683
+ console.log(` Removing Codex plugin: ${name}@${marketplaceName}`);
684
+ const r = spawnSync(`codex plugin remove "${name}@${marketplaceName}"`, [], {
685
+ shell: true,
686
+ windowsHide: true,
687
+ stdio: 'pipe',
688
+ });
689
+ console.log(` ${r.stdout ? r.stdout.toString().trim() : r.stderr.toString().trim()}`);
690
+ }
705
691
 
706
692
  for (const name of new Set([marketplaceName, 'HuaweiCloud-Devkit'])) {
707
693
  console.log(` Removing Codex marketplace: ${name}`);
@@ -717,7 +703,7 @@ function uninstallCodex() {
717
703
  function codexStatus() {
718
704
  const r = spawnSync('codex plugin list', [], { shell: true, windowsHide: true, stdio: 'pipe' });
719
705
  const out = r.stdout ? r.stdout.toString() : '';
720
- return out.includes('huaweicloud-core');
706
+ return out.includes(getCodexPluginName()) || out.includes('huaweicloud-core');
721
707
  }
722
708
 
723
709
  async function installOpenCode() {
@@ -3074,75 +3060,116 @@ function checkForUpdate() {
3074
3060
  }
3075
3061
  }
3076
3062
 
3063
+ function installMarkerDirForTarget(target) {
3064
+ if (target === 'codex') return null;
3065
+ if (target === 'dsh') return dshPluginsDir();
3066
+ if (target === 'codearts') return codeartsPluginsDir();
3067
+ if (target === 'codearts-work') return codeartsWorkPluginsDir();
3068
+ if (target === 'workbuddy') return workbuddyPluginsDir();
3069
+ if (target === 'officeace') return officeacePluginsDir();
3070
+ if (target === 'hermes') return hermesPluginsDir();
3071
+ if (target === 'openclaw') return openclawPluginsDir();
3072
+ if (target === 'atomcode') return atomcodePluginsDir();
3073
+ if (target === 'codex-desktop') return codexDesktopPluginsDir();
3074
+ if (target === 'opencode') return opencodePluginsDir();
3075
+ return null;
3076
+ }
3077
+
3078
+ function writeInstallMarker(target) {
3079
+ const markerDir = installMarkerDirForTarget(target);
3080
+ if (!markerDir) return;
3081
+ mkdirSync(markerDir, { recursive: true });
3082
+ writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
3083
+ }
3084
+
3077
3085
  async function cmdInstall() {
3078
3086
  const target = parseTarget();
3079
3087
  console.log(BANNER);
3080
3088
  console.log(`Installing HuaweiCloud DevKit${target !== 'opencode' ? ` for ${target}` : ''}...\n`);
3081
3089
  checkNode();
3082
3090
  checkForUpdate();
3091
+ const installFailures = [];
3092
+
3093
+ async function runInstallStep(stepTarget, title, fn) {
3094
+ console.log(title);
3095
+ try {
3096
+ const result = await fn();
3097
+ if (result === false) {
3098
+ installFailures.push(stepTarget);
3099
+ return false;
3100
+ }
3101
+ writeInstallMarker(stepTarget);
3102
+ return true;
3103
+ } catch (error) {
3104
+ installFailures.push(stepTarget);
3105
+ console.log(` \x1b[31m${stepTarget} install failed: ${error.message}\x1b[0m`);
3106
+ return false;
3107
+ }
3108
+ }
3083
3109
 
3084
3110
  if (target === 'opencode' || target === 'all') {
3085
- console.log('[OpenCode]');
3086
- await installOpenCode();
3111
+ await runInstallStep('opencode', '[OpenCode]', installOpenCode);
3087
3112
  }
3088
3113
  if (target === 'codex-desktop' || target === 'all') {
3089
- console.log('\n[Codex Desktop]');
3090
- await installCodexDesktop();
3114
+ await runInstallStep('codex-desktop', '\n[Codex Desktop]', installCodexDesktop);
3091
3115
  }
3092
3116
  if (target === 'codearts' || target === 'all') {
3093
- console.log('\n[CodeArts]');
3094
- await installCodeArts();
3117
+ await runInstallStep('codearts', '\n[CodeArts]', installCodeArts);
3095
3118
  }
3096
3119
  if (target === 'codearts-work' || target === 'all') {
3097
- console.log('\n[CodeArts Work]');
3098
- await installCodeArtsWork();
3120
+ await runInstallStep('codearts-work', '\n[CodeArts Work]', installCodeArtsWork);
3099
3121
  }
3100
3122
  if (target === 'workbuddy' || target === 'all') {
3101
- console.log('\n[WorkBuddy]');
3102
- await installWorkBuddy();
3123
+ await runInstallStep('workbuddy', '\n[WorkBuddy]', installWorkBuddy);
3103
3124
  }
3104
3125
  if (target === 'dsh' || target === 'all') {
3105
- console.log('\n[DSH]');
3106
- await installDsh();
3126
+ await runInstallStep('dsh', '\n[DSH]', installDsh);
3107
3127
  }
3108
3128
  if (target === 'officeace' || target === 'all') {
3109
- console.log('\n[OfficeAce]');
3110
- await installOfficeAce();
3129
+ await runInstallStep('officeace', '\n[OfficeAce]', installOfficeAce);
3111
3130
  }
3112
3131
  if (target === 'hermes' || target === 'all') {
3113
- console.log('\n[Hermes Agent]');
3114
- await installHermes();
3132
+ await runInstallStep('hermes', '\n[Hermes Agent]', installHermes);
3115
3133
  }
3116
3134
  if (target === 'openclaw' || target === 'all') {
3117
- console.log('\n[OpenClaw]');
3118
- await installOpenClaw();
3135
+ await runInstallStep('openclaw', '\n[OpenClaw]', installOpenClaw);
3119
3136
  }
3120
3137
  if (target === 'atomcode' || target === 'all') {
3121
- console.log('\n[AtomCode]');
3122
- await installAtomCode();
3138
+ await runInstallStep('atomcode', '\n[AtomCode]', installAtomCode);
3123
3139
  }
3124
3140
  if (target === 'codex' || target === 'all') {
3125
- console.log('\n[Codex]');
3126
- if (!hasCodexCLI()) {
3127
- if (target === 'codex') {
3128
- console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
3141
+ await runInstallStep('codex', '\n[Codex]', () => {
3142
+ if (!hasCodexCLI()) {
3143
+ if (target === 'codex') {
3144
+ console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
3145
+ if (process.platform === 'win32') {
3146
+ console.log(` \x1b[33mTip: Codex Desktop on Windows installs codex.exe under WindowsApps,\x1b[0m`);
3147
+ console.log(` \x1b[33m which may fail with "Access is denied". Try instead:\x1b[0m`);
3148
+ console.log(` \x1b[33m npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
3149
+ }
3150
+ console.log(` \x1b[31mOr install Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
3151
+ return false;
3152
+ }
3153
+ console.log(` \x1b[33mCodex CLI not found. Skipping Codex.\x1b[0m`);
3129
3154
  if (process.platform === 'win32') {
3130
- console.log(` \x1b[33mTip: Codex Desktop on Windows installs codex.exe under WindowsApps,\x1b[0m`);
3131
- console.log(` \x1b[33m which may fail with "Access is denied". Try instead:\x1b[0m`);
3132
- console.log(` \x1b[33m npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
3155
+ console.log(' \x1b[33mTip: try --target codex-desktop for Codex Desktop on Windows\x1b[0m');
3156
+ } else {
3157
+ console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
3133
3158
  }
3134
- console.log(` \x1b[31mOr install Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
3135
- process.exit(1);
3136
- }
3137
- console.log(` \x1b[33mCodex CLI not found. Skipping Codex.\x1b[0m`);
3138
- if (process.platform === 'win32') {
3139
- console.log(' \x1b[33mTip: try --target codex-desktop for Codex Desktop on Windows\x1b[0m');
3140
- } else {
3141
- console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
3159
+ return true;
3142
3160
  }
3143
- } else {
3144
- installCodex();
3161
+ return installCodex();
3162
+ });
3163
+ }
3164
+
3165
+ if (installFailures.length > 0) {
3166
+ console.log(`\n\x1b[31mInstallation failed for: ${installFailures.join(', ')}\x1b[0m`);
3167
+ if (target === 'all') {
3168
+ console.log(
3169
+ '\x1b[33mSome targets may have been installed before the failure. Re-run status/doctor per target.\x1b[0m',
3170
+ );
3145
3171
  }
3172
+ process.exit(1);
3146
3173
  }
3147
3174
 
3148
3175
  console.log(`\n\x1b[32mInstallation complete!\x1b[0m`);
@@ -3197,10 +3224,15 @@ async function cmdInstall() {
3197
3224
  console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
3198
3225
  }
3199
3226
 
3200
- const hcloudOk = checkHcloud();
3201
- if (!hcloudOk) {
3202
- console.log(`\n\x1b[33mKooCLI (hcloud) is not installed.`);
3203
- console.log(` Run: npx huaweicloud-devkit install-hcloud\x1b[0m`);
3227
+ const hcloudProbe = probeHcloud();
3228
+ if (!hcloudProbe.ok) {
3229
+ if (hcloudProbe.status === 'sandbox_home_failure') {
3230
+ console.log(`\n\x1b[33mKooCLI (hcloud) detected, but cannot run in this sandbox.`);
3231
+ console.log(` ${hcloudProbeNextStep(hcloudProbe)}\x1b[0m`);
3232
+ } else {
3233
+ console.log(`\n\x1b[33mKooCLI (hcloud) is not ready.`);
3234
+ console.log(` ${hcloudProbeNextStep(hcloudProbe)}\x1b[0m`);
3235
+ }
3204
3236
  } else {
3205
3237
  console.log(`\nKooCLI (hcloud) detected.`);
3206
3238
  }
@@ -3219,27 +3251,6 @@ async function cmdInstall() {
3219
3251
  }
3220
3252
  console.log(` 4. 运行自检:npx huaweicloud-devkit doctor`);
3221
3253
 
3222
- // Write install marker for doctor to detect
3223
- const markerDir =
3224
- target === 'dsh'
3225
- ? dshPluginsDir()
3226
- : target === 'codearts'
3227
- ? codeartsPluginsDir()
3228
- : target === 'codearts-work'
3229
- ? codeartsWorkPluginsDir()
3230
- : target === 'workbuddy'
3231
- ? workbuddyPluginsDir()
3232
- : target === 'officeace'
3233
- ? officeacePluginsDir()
3234
- : target === 'openclaw'
3235
- ? codexDesktopPluginsDir()
3236
- : target === 'atomcode'
3237
- ? atomcodePluginsDir()
3238
- : target === 'codex-desktop'
3239
- ? codexDesktopPluginsDir()
3240
- : opencodePluginsDir();
3241
- mkdirSync(markerDir, { recursive: true });
3242
- writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
3243
3254
  if (target === 'opencode' || target === 'all') {
3244
3255
  console.log('Or describe your Huawei Cloud task in OpenCode');
3245
3256
  }
@@ -3250,7 +3261,7 @@ async function cmdInstall() {
3250
3261
  console.log('Or describe your Huawei Cloud task in CodeArts Work');
3251
3262
  }
3252
3263
  if (target === 'codex' || target === 'all') {
3253
- console.log('Or mention @huaweicloud-core in Codex');
3264
+ console.log('Or mention @huaweicloud-devkit in Codex');
3254
3265
  }
3255
3266
  if (target === 'workbuddy' || target === 'all') {
3256
3267
  console.log('Or describe your Huawei Cloud task in WorkBuddy');
@@ -3553,6 +3564,21 @@ async function cmdDoctor() {
3553
3564
  existsSync(join(atomcodePluginDir, 'safety', 'policy.json'));
3554
3565
  check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit install');
3555
3566
 
3567
+ const hookConfigPath = join(PLUGIN_ROOT, 'hooks', 'hooks.json');
3568
+ const hookNodePath = join(PLUGIN_ROOT, 'hooks', 'huaweicloud-safety.mjs');
3569
+ let hookConfigUsesNode = false;
3570
+ let hookConfigUsesPython = false;
3571
+ try {
3572
+ const hookConfigText = readFileSync(hookConfigPath, 'utf8');
3573
+ hookConfigUsesNode = hookConfigText.includes('huaweicloud-safety.mjs') && /\bnode\b/.test(hookConfigText);
3574
+ hookConfigUsesPython = /\bpython3?\b/.test(hookConfigText);
3575
+ } catch {}
3576
+ check(
3577
+ 'Safety hook runtime (Node)',
3578
+ existsSync(hookNodePath) && hookConfigUsesNode && !hookConfigUsesPython,
3579
+ 'hooks/hooks.json should invoke node hooks/huaweicloud-safety.mjs, not python3',
3580
+ );
3581
+
3556
3582
  // MCP config — check OpenCode, Codex Desktop, CodeArts, WorkBuddy, and DSH
3557
3583
  let mcpConfigured = false;
3558
3584
  let mcpCfgTarget = '';
@@ -3664,20 +3690,23 @@ async function cmdDoctor() {
3664
3690
  }
3665
3691
 
3666
3692
  // hcloud CLI
3667
- const hcloudBin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
3668
- const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], {
3669
- shell: true,
3670
- windowsHide: true,
3671
- stdio: 'pipe',
3672
- timeout: 5000,
3673
- });
3674
- const hcloudOut = (hcloudCheck.stdout || '').toString() + (hcloudCheck.stderr || '').toString();
3675
- const hcloudOk = hcloudCheck.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(hcloudOut);
3676
- check('hcloud CLI installed', hcloudOk, 'Run: npx huaweicloud-devkit install-hcloud');
3693
+ const hcloudProbe = probeHcloud();
3694
+ const hcloudBin = hcloudProbe.executable;
3695
+ const hcloudOk = hcloudProbe.ok;
3696
+ check('hcloud CLI installed', hcloudProbe.installed, hcloudProbeNextStep(hcloudProbe));
3697
+ if (hcloudProbe.status === 'sandbox_home_failure') {
3698
+ warn++;
3699
+ console.log(` \x1b[33m[WARN]\x1b[0m hcloud cannot resolve the Windows user home in this agent sandbox.`);
3700
+ console.log(` ${hcloudProbeNextStep(hcloudProbe)}`);
3701
+ } else if (hcloudProbe.status === 'privacy_pending') {
3702
+ warn++;
3703
+ console.log(` \x1b[33m[WARN]\x1b[0m KooCLI privacy agreement is pending.`);
3704
+ console.log(` ${hcloudProbeNextStep(hcloudProbe)}`);
3705
+ }
3677
3706
 
3678
3707
  const kooCliTarget = getKooCliVersion();
3679
3708
  if (hcloudOk && kooCliTarget) {
3680
- const installed = parseHcloudVersion(hcloudOut);
3709
+ const installed = hcloudProbe.installedVersion;
3681
3710
  if (installed && compareVersion(installed, kooCliTarget) !== 0) {
3682
3711
  warn++;
3683
3712
  console.log(
@@ -3701,7 +3730,7 @@ async function cmdDoctor() {
3701
3730
  }
3702
3731
 
3703
3732
  if (hcloudOk) {
3704
- const ver = (hcloudCheck.stdout.toString().match(/(\d+\.\d+\.\d+)/) || [])[1] || 'unknown';
3733
+ const ver = hcloudProbe.installedVersion || 'unknown';
3705
3734
  console.log(` Version: ${ver}`);
3706
3735
 
3707
3736
  // Check auth
@@ -4373,12 +4402,18 @@ async function cmdAuthReconcile() {
4373
4402
  console.log(`OBS sync failed: ${error.message}`);
4374
4403
  }
4375
4404
  const profile = resolveManagedProfile();
4405
+ let hcloudSynced = false;
4376
4406
  if (profile) {
4377
4407
  const r = runHcloudConfigure(profile, credentials.ak, credentials.sk, credentials.region);
4408
+ hcloudSynced = r.ok;
4378
4409
  console.log(` KooCLI ${r.ok ? 'synced' : 'sync failed'}: profile=${profile} ${r.error || ''}`);
4379
4410
  }
4380
- writeLastSync();
4381
- console.log('Done. .last_sync refreshed.');
4411
+ if (hcloudSynced) {
4412
+ writeLastSync({ kooCliProfile: profile, s1Fingerprint: fingerprint(credentials.ak, credentials.sk) });
4413
+ console.log('Done. .last_sync refreshed.');
4414
+ } else {
4415
+ console.log('Done. .last_sync was not refreshed because KooCLI sync did not complete.');
4416
+ }
4382
4417
  }
4383
4418
 
4384
4419
  async function cmdAuthSync() {
@@ -57,7 +57,7 @@ import {
57
57
  resolveSkipFilePath,
58
58
  upgradePackage,
59
59
  } from './update-check.mjs';
60
- import { getKooCliVersion, parseHcloudVersion, compareVersion } from './koocli-version.mjs';
60
+ import { hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
61
61
 
62
62
  const __dirname = dirname(fileURLToPath(import.meta.url));
63
63
  const SKILLS_ROOT_DEV = join(__dirname, '..', 'skills');
@@ -984,7 +984,9 @@ function persistCredentials(ak, sk, securityToken, region) {
984
984
  } else {
985
985
  hcloud = runHcloudConfigure(profile, ak, sk, region);
986
986
  }
987
- writeLastSync();
987
+ if (hcloud.ok) {
988
+ writeLastSync({ kooCliProfile: profile, s1Fingerprint: fingerprint(ak, sk) });
989
+ }
988
990
  return {
989
991
  status: 'ok',
990
992
  scope: 'persist',
@@ -1382,35 +1384,17 @@ function hookResult(result) {
1382
1384
  }
1383
1385
 
1384
1386
  export async function runVersionCheck(options = {}) {
1385
- const result = await runHcloud(['version'], {
1386
- ...options,
1387
- maxRetries: options.maxRetries ?? 0,
1388
- });
1389
- const errorText = result.error || result.stderr || '';
1390
- const isSpawnError = /ENOENT|SPAWN_ERROR/i.test(errorText) || result.code === 'SPAWN_ERROR';
1391
- const requiredVersion = getKooCliVersion();
1392
- const installedVersion = parseHcloudVersion(result.stdout || '');
1393
- const versionMismatch = !!(
1394
- result.ok &&
1395
- requiredVersion &&
1396
- installedVersion &&
1397
- compareVersion(installedVersion, requiredVersion) !== 0
1398
- );
1387
+ const result = probeHcloud(options);
1399
1388
  return {
1400
- installed: result.ok,
1401
- authenticated: result.ok && !/配置文件中不存在配置项|USE_ERROR.*配置/i.test(result.stdout || ''),
1402
- errorCode: isSpawnError ? 'HCLOUD_NOT_FOUND' : undefined,
1403
- output: result.ok ? result.stdout : errorText,
1404
- kooCliVersion: requiredVersion || undefined,
1405
- installedVersion: installedVersion || undefined,
1406
- versionMismatch,
1407
- nextStep: result.ok
1408
- ? versionMismatch
1409
- ? `KooCLI version mismatch: installed ${installedVersion}, this plugin is paired with ${requiredVersion}. Reinstall the pinned version (see huaweicloud-cli-and-auth skill) and restart the agent.`
1410
- : 'Use huaweicloud_show_profile_redacted to inspect the active KooCLI profile safely.'
1411
- : isSpawnError
1412
- ? 'hcloud executable not found. Set HCLOUD_BIN to the full hcloud path, or install KooCLI: npx huaweicloud-devkit install-hcloud. Then restart the agent.'
1413
- : 'Install Huawei Cloud KooCLI: npx huaweicloud-devkit install-hcloud. Configure credentials outside the agent conversation.',
1389
+ installed: result.installed,
1390
+ authenticated: result.ok && !/配置文件中不存在配置项|USE_ERROR.*配置/i.test(result.output || ''),
1391
+ errorCode: result.errorCode,
1392
+ status: result.status,
1393
+ output: result.output,
1394
+ kooCliVersion: result.requiredVersion || undefined,
1395
+ installedVersion: result.installedVersion || undefined,
1396
+ versionMismatch: Boolean(result.versionMismatch),
1397
+ nextStep: hcloudProbeNextStep(result),
1414
1398
  authHint:
1415
1399
  'If hcloud is installed but commands fail with "配置文件中不存在配置项", run `npx huaweicloud-devkit auth init` outside agent chat to configure credentials.',
1416
1400
  };