huaweicloud-devkit 1.1.2-next.2 → 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.
Files changed (38) hide show
  1. package/README.md +22 -0
  2. package/README.zh-CN.md +22 -0
  3. package/integrations/hermes/skills-snapshot.json +175 -0
  4. package/package.json +3 -1
  5. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  6. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  10. package/plugins/huaweicloud-core/hooks/hooks.json +2 -2
  11. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.mjs +56 -0
  12. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  13. package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +1 -1
  14. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +6 -7
  15. package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +7 -6
  16. package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +1 -1
  17. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +1 -1
  18. package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +3 -1
  19. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +10 -10
  20. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +2 -2
  21. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +1 -1
  22. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +15 -9
  23. package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +1 -1
  24. package/plugins/huaweicloud-core/skills/huawei-iac/references/resource-catalog.md +1 -1
  25. package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +3 -3
  26. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +15 -15
  27. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +1 -1
  28. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +18 -16
  29. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +1 -1
  30. package/plugins/huaweicloud-core/src/auth/credentials.mjs +6 -2
  31. package/plugins/huaweicloud-core/src/auth/reconcile.mjs +30 -6
  32. package/plugins/huaweicloud-core/src/auth/service.mjs +16 -22
  33. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +16 -10
  34. package/plugins/huaweicloud-core/src/hcloud-probe.mjs +154 -0
  35. package/plugins/huaweicloud-core/src/koocli-version.mjs +47 -0
  36. package/plugins/huaweicloud-core/src/safety-policy.mjs +17 -5
  37. package/plugins/huaweicloud-core/src/setup-cli.mjs +196 -134
  38. package/plugins/huaweicloud-core/src/tools.mjs +16 -17
@@ -15,9 +15,9 @@ hcloud IAM KeystoneShowUser # show user detail
15
15
  ## Credential Management
16
16
 
17
17
  ```bash
18
- hcloud IAM KeystoneListAccessKeys --user_id=<id> # list AK/SK for user
19
- hcloud IAM CreateLoginToken # create login token
20
- hcloud IAM GetAccountSummary # account summary
18
+ hcloud IAM ListAccessKeysV5 --user_id=<id> # list AK/SK for user
19
+ hcloud IAM CreateLoginToken # create login token
20
+ hcloud IAM GetAccountSummaryV5 # account summary
21
21
  ```
22
22
 
23
23
  ## Security Best Practices
@@ -62,21 +62,21 @@ hcloud OBS config -e=<endpoint> -i=<AK> -k=<SK> -t=token
62
62
 
63
63
  ## Common Workflows
64
64
 
65
- | Task | Command |
66
- | ---------------------------- | ---------------------------------------------------------------------- |
67
- | Create bucket | `hcloud OBS mb obs://<bucket> -location=<region>` |
68
- | List buckets/objects | `hcloud OBS ls [obs://<bucket>]` |
69
- | Upload file | `hcloud OBS cp <file> obs://<bucket>/<key>` |
70
- | Upload directory (recursive) | `hcloud OBS cp <dir>/ obs://<bucket>/ -r -f -flat` |
71
- | Download object | `hcloud OBS cp obs://<bucket>/<key> <local-path>` |
72
- | Set bucket ACL | `hcloud OBS chattri obs://<bucket> -acl=public-read` |
73
- | Set object ACL | `hcloud OBS chattri obs://<bucket>/<key> -acl=public-read` | Bucket ACL does NOT cascade — anonymous reads need both |
74
- | Set lifecycle | `hcloud OBS lifecycle obs://<bucket> -method=put -localfile=<json>` |
75
- | Set bucket policy | `hcloud OBS bucketpolicy obs://<bucket> -method=put -localfile=<json>` |
76
- | Set CORS | `hcloud OBS cors obs://<bucket> -method=put -localfile=<json>` |
77
- | Delete bucket | `hcloud OBS rm obs://<bucket> -r` (must be empty) |
78
- | Presigned URL | `hcloud OBS sign obs://<bucket>/<key> -e=<seconds>` |
79
- | Object metadata | `hcloud OBS stat obs://<bucket>/<key>` |
65
+ | Task | Command |
66
+ | ---------------------------- | ------------------------------------------------------------------------- |
67
+ | Create bucket | `hcloud OBS mb obs://<bucket> -location=<region>` |
68
+ | List buckets/objects | `hcloud OBS ls [obs://<bucket>]` |
69
+ | Upload file | `hcloud OBS cp <file> obs://<bucket>/<key>` |
70
+ | Upload directory (recursive) | `hcloud OBS cp <dir>/ obs://<bucket>/ -r -f -flat` |
71
+ | Download object | `hcloud OBS cp obs://<bucket>/<key> <local-path>` |
72
+ | Set bucket ACL | `hcloud OBS chattri obs://<bucket> -acl=public-read` |
73
+ | Set object ACL | `hcloud OBS chattri obs://<bucket>/<key> -acl=public-read` | Bucket ACL does NOT cascade — anonymous reads need both |
74
+ | Set lifecycle | `hcloud OBS lifecycle obs://<bucket> -method=put -localfile=<json>` |
75
+ | Set bucket policy | `hcloud OBS bucketpolicy obs://<bucket> -method=put -localfile=<json>` |
76
+ | Set CORS | Not available via `hcloud OBS` — use API/SDK (`SetBucketCors`) or console |
77
+ | Delete bucket | `hcloud OBS rm obs://<bucket> -r` (must be empty) |
78
+ | Presigned URL | `hcloud OBS sign obs://<bucket>/<key> -e=<seconds>` |
79
+ | Object metadata | `hcloud OBS stat obs://<bucket>/<key>` |
80
80
 
81
81
  ## Static Website Deployment Workflow
82
82
 
@@ -54,7 +54,7 @@ hcloud EIP AssociatePublicips --publicip_id=<id> \
54
54
  --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT
55
55
 
56
56
  # Unbind
57
- hcloud EIP DisassociatePublicip --publicip_id=<id>
57
+ hcloud EIP DisassociatePublicips --publicip_id=<id>
58
58
 
59
59
  # List
60
60
  hcloud EIP ListPublicips
@@ -13,56 +13,58 @@ Use KooCLI `hcloud` for local inspection and reviewed operations. Never ask the
13
13
 
14
14
  Official guide: `https://support.huaweicloud.com/qs-hcli/hcli_02_003.html`.
15
15
 
16
+ > **Version pairing**: this plugin is paired with **KooCLI v7.2.12** (`kooCliVersion` in `package.json`). Prefer the pinned download below. The one-liner script `hcloud_install.sh` always installs the `latest` version and cannot be pinned — if you use it, `check_cli`/`doctor` will warn when the installed version does not match v7.2.12.
17
+
16
18
  ### Windows
17
19
 
18
- 1. Download and unzip: `https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-windows-amd64.zip`
20
+ 1. Download and unzip: `https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.2.12/huaweicloud-cli-windows-amd64.zip`
19
21
  2. Extract to `%USERPROFILE%\hcloud`, add to user `PATH`
20
22
  3. Verify: `hcloud version`
21
23
 
22
24
  ### Linux (amd64 / arm64)
23
25
 
24
- One-liner (recommended):
25
-
26
- ```bash
27
- curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y
28
- ```
29
-
30
- Or manual download:
26
+ Fixed download (recommended):
31
27
 
32
28
  ```bash
33
29
  # amd64
34
- curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-linux-amd64.tar.gz"
30
+ curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.2.12/huaweicloud-cli-linux-amd64.tar.gz"
35
31
  tar -zxvf huaweicloud-cli-linux-amd64.tar.gz
36
32
  # arm64
37
- curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-linux-arm64.tar.gz"
33
+ curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.2.12/huaweicloud-cli-linux-arm64.tar.gz"
38
34
  tar -zxvf huaweicloud-cli-linux-arm64.tar.gz
39
35
  ```
40
36
 
41
37
  Move to PATH: `mv $(pwd)/hcloud ~/.local/bin/`
42
38
  Verify: `hcloud version`
43
39
 
44
- ### macOS (amd64 / arm64)
45
-
46
- One-liner (recommended):
40
+ One-liner (installs `latest`, may drift from v7.2.12):
47
41
 
48
42
  ```bash
49
43
  curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y
50
44
  ```
51
45
 
52
- Or manual download:
46
+ ### macOS (amd64 / arm64)
47
+
48
+ Fixed download (recommended):
53
49
 
54
50
  ```bash
55
51
  # amd64
56
- curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-mac-amd64.tar.gz"
52
+ curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.2.12/huaweicloud-cli-mac-amd64.tar.gz"
57
53
  tar -zxvf huaweicloud-cli-mac-amd64.tar.gz
58
54
  # arm64 (Apple Silicon)
59
- curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-mac-arm64.tar.gz"
55
+ curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.2.12/huaweicloud-cli-mac-arm64.tar.gz"
60
56
  tar -zxvf huaweicloud-cli-mac-arm64.tar.gz
61
57
  ```
62
58
 
63
59
  Move to PATH: `mv $(pwd)/hcloud /usr/local/bin/`
64
60
  Verify: `hcloud version`
65
61
 
62
+ One-liner (installs `latest`, may drift from v7.2.12):
63
+
64
+ ```bash
65
+ curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y
66
+ ```
67
+
66
68
  Agent processes find executables through `PATH`. If OpenCode/Codex cannot find `hcloud`, restart after updating `PATH`, or set `HCLOUD_BIN`.
67
69
 
68
70
  ## Configure Credentials Outside Chat
@@ -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',
@@ -131,11 +131,22 @@ function applyPreflightFindings(classification, sgFindings) {
131
131
  return classification;
132
132
  }
133
133
 
134
+ const OBS_WRITE_SUBCOMMANDS = new Set(['mb', 'cp', 'mv', 'rm', 'chattri', 'restore']);
135
+
136
+ function obsWriteHint(args) {
137
+ if (!Array.isArray(args) || args.length < 2) return null;
138
+ if (String(args[0]).toUpperCase() !== 'OBS') return null;
139
+ if (!OBS_WRITE_SUBCOMMANDS.has(String(args[1]).toLowerCase())) return null;
140
+ return 'OBS write operations are obsutil-style and always write-class. Before executing, present the full resource manifest (bucket/object list) to the user for ONE batch approval, then run each command through plan → approve (see huawei-iac skill, Provisioning Rules).';
141
+ }
142
+
134
143
  export function planHcloudCommand(args, options = {}) {
135
144
  const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
136
145
  const classification = classifyHcloudArgs(normalizedArgs, options);
137
146
  const command = ['hcloud', ...normalizedArgs].map((arg) => quoteShellArg(arg)).join(' ');
138
147
  const warnings = planningWarnings(normalizedArgs);
148
+ const obsHint = obsWriteHint(normalizedArgs);
149
+ if (obsHint) warnings.push(obsHint);
139
150
  const sgFindings = preflightSecurityGroupCheck(normalizedArgs);
140
151
  if (sgFindings.length > 0) {
141
152
  for (const f of sgFindings) warnings.push(f);
@@ -205,12 +216,7 @@ async function runtimeCurrentMismatchWarning() {
205
216
  }
206
217
 
207
218
  function discoverHcloudPath() {
208
- if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
209
- const candidates =
210
- process.platform === 'win32'
211
- ? [join(homedir(), 'hcloud', 'hcloud.exe')]
212
- : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
213
- return candidates.find((c) => existsSync(c)) || null;
219
+ return findHcloudBin();
214
220
  }
215
221
 
216
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
+ }
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const PACKAGE_JSON = resolve(__dirname, '..', '..', '..', 'package.json');
7
+
8
+ export const KOO_CLI_BASE = 'https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli';
9
+
10
+ const VERSION_RE = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
11
+
12
+ export function getKooCliVersion() {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf8'));
15
+ return pkg.kooCliVersion || null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export function parseHcloudVersion(out) {
22
+ if (!out) return null;
23
+ const match = String(out).match(VERSION_RE);
24
+ return match ? match[0] : null;
25
+ }
26
+
27
+ export function compareVersion(a, b) {
28
+ const pa = String(a || '')
29
+ .split(/[-+]/)[0]
30
+ .split('.')
31
+ .map((n) => Number.parseInt(n, 10) || 0);
32
+ const pb = String(b || '')
33
+ .split(/[-+]/)[0]
34
+ .split('.')
35
+ .map((n) => Number.parseInt(n, 10) || 0);
36
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
37
+ const va = pa[i] || 0;
38
+ const vb = pb[i] || 0;
39
+ if (va > vb) return 1;
40
+ if (va < vb) return -1;
41
+ }
42
+ return 0;
43
+ }
44
+
45
+ export function kooCliDownloadBase() {
46
+ return `${KOO_CLI_BASE}/${getKooCliVersion() || 'latest'}`;
47
+ }
@@ -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) {