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.
- package/README.md +22 -0
- package/README.zh-CN.md +22 -0
- package/integrations/hermes/skills-snapshot.json +175 -0
- package/package.json +3 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/hooks/hooks.json +2 -2
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.mjs +56 -0
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +6 -7
- package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +7 -6
- package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +3 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +10 -10
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +2 -2
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +15 -9
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-iac/references/resource-catalog.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +3 -3
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +15 -15
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +1 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +18 -16
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +1 -1
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +6 -2
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +30 -6
- package/plugins/huaweicloud-core/src/auth/service.mjs +16 -22
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +16 -10
- package/plugins/huaweicloud-core/src/hcloud-probe.mjs +154 -0
- package/plugins/huaweicloud-core/src/koocli-version.mjs +47 -0
- package/plugins/huaweicloud-core/src/safety-policy.mjs +17 -5
- package/plugins/huaweicloud-core/src/setup-cli.mjs +196 -134
- 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
|
|
19
|
-
hcloud IAM CreateLoginToken
|
|
20
|
-
hcloud IAM
|
|
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`
|
|
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
|
|
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
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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.
|
|
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 = {
|
|
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 !==
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
76
|
+
const hcloud = probeHcloud();
|
|
77
|
+
if (!hcloud.ok) {
|
|
89
78
|
return {
|
|
90
79
|
ok: false,
|
|
91
|
-
error:
|
|
92
|
-
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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) {
|