huaweicloud-devkit 1.0.2-dev.9 → 1.0.2-next.10

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 (99) hide show
  1. package/.agents/plugins/marketplace.json +20 -20
  2. package/LICENSE +201 -201
  3. package/README.md +178 -138
  4. package/README.zh-CN.md +176 -138
  5. package/bin/setup.cjs +10 -8
  6. package/integrations/opencode/commands/huaweicloud-doctor.md +14 -14
  7. package/integrations/opencode/opencode.json +11 -14
  8. package/integrations/opencode/skills/huaweicloud-core/SKILL.md +32 -32
  9. package/package.json +60 -44
  10. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +31 -44
  11. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +30 -43
  12. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +31 -44
  13. package/plugins/huaweicloud-core/.mcp.json +13 -15
  14. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +31 -44
  15. package/plugins/huaweicloud-core/hooks/hooks.json +26 -26
  16. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +136 -136
  17. package/plugins/huaweicloud-core/safety/policy.json +76 -100
  18. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +227 -199
  19. package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +164 -164
  20. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +48 -48
  21. package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +50 -50
  22. package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +103 -101
  23. package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +90 -90
  24. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +191 -190
  25. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +171 -124
  26. package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +55 -55
  27. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +70 -70
  28. package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +57 -56
  29. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +106 -97
  30. package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +36 -29
  31. package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +29 -23
  32. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +188 -185
  33. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +144 -133
  34. package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +31 -31
  35. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +57 -55
  36. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +52 -52
  37. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +132 -131
  38. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +60 -58
  39. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +72 -71
  40. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +119 -119
  41. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +142 -142
  42. package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +56 -56
  43. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +69 -65
  44. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +111 -111
  45. package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +148 -122
  46. package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +56 -56
  47. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +65 -65
  48. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +134 -127
  49. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +49 -49
  50. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +31 -26
  51. package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +101 -100
  52. package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +40 -40
  53. package/plugins/huaweicloud-core/skills/huawei-obs/references/static-website.md +56 -56
  54. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +229 -224
  55. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +173 -134
  56. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +69 -69
  57. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +84 -84
  58. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +77 -76
  59. package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +60 -59
  60. package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +50 -50
  61. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +63 -63
  62. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +56 -56
  63. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +4 -4
  64. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +56 -47
  65. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +4 -4
  66. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +138 -128
  67. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +4 -4
  68. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +111 -96
  69. package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +4 -4
  70. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +20 -20
  71. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +30 -28
  72. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +59 -60
  73. package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +4 -4
  74. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +83 -83
  75. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +4 -4
  76. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +54 -1
  77. package/plugins/huaweicloud-core/src/auth/credentials.mjs +103 -95
  78. package/plugins/huaweicloud-core/src/auth/service.mjs +63 -63
  79. package/plugins/huaweicloud-core/src/data/icons-manifest.v1.json +3517 -0
  80. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +283 -265
  81. package/plugins/huaweicloud-core/src/icon-library.mjs +171 -0
  82. package/plugins/huaweicloud-core/src/mcp-server.mjs +130 -129
  83. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +68 -0
  84. package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +94 -0
  85. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +140 -137
  86. package/plugins/huaweicloud-core/src/safety-policy.mjs +356 -302
  87. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +86 -87
  88. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +150 -153
  89. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +196 -105
  90. package/plugins/huaweicloud-core/src/search-market.mjs +208 -169
  91. package/plugins/huaweicloud-core/src/setup-cli.mjs +2622 -1594
  92. package/plugins/huaweicloud-core/src/tools.mjs +463 -111
  93. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +447 -427
  94. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +128 -132
  95. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +215 -227
  96. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +189 -202
  97. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +164 -158
  98. package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -19
  99. package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -338
@@ -1,302 +1,356 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { evaluateCommandRisk, mergeRiskDecision } from './risk-rule-engine.mjs';
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const policyPath = join(__dirname, '..', 'safety', 'policy.json');
8
-
9
- export function loadPolicy() {
10
- return JSON.parse(readFileSync(policyPath, 'utf8'));
11
- }
12
-
13
- const DEFAULT_POLICY = loadPolicy();
14
-
15
- function regexFrom(pattern) {
16
- return new RegExp(pattern, 'i');
17
- }
18
-
19
- function isSecretKeyName(key, policy = DEFAULT_POLICY) {
20
- const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
21
- if (
22
- /access.*key|secret.*key|security.*token|xauth.*token|authorization|password|passwd|adminpass|credential|private.*key/.test(normalized)
23
- ) {
24
- return true;
25
- }
26
- return policy.secretKeyNamePatterns.some((pattern) => regexFrom(`^(${pattern})$`).test(key));
27
- }
28
-
29
- function redactString(text) {
30
- return String(text)
31
- .replace(/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1<redacted>')
32
- .replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
33
- }
34
-
35
- export function redactSecrets(value, policy = DEFAULT_POLICY) {
36
- if (Array.isArray(value)) {
37
- return value.map((item) => redactSecrets(item, policy));
38
- }
39
- if (value && typeof value === 'object') {
40
- return Object.fromEntries(
41
- Object.entries(value).map(([key, val]) => [
42
- key,
43
- isSecretKeyName(key, policy) ? '<redacted>' : redactSecrets(val, policy),
44
- ]),
45
- );
46
- }
47
- if (typeof value === 'string') {
48
- return redactString(value);
49
- }
50
- return value;
51
- }
52
-
53
- function stripExecutable(args) {
54
- if (!args.length) return [];
55
- const first = String(args[0]).toLowerCase();
56
- if (first === 'hcloud' || first.endsWith('/hcloud') || first.endsWith('\\hcloud') || first === 'hcloud.exe') {
57
- return args.slice(1);
58
- }
59
- return args;
60
- }
61
-
62
- function commandOperation(args) {
63
- const stripped = stripExecutable(args).map(String).filter(Boolean);
64
- if (stripped[0]?.toLowerCase() === 'configure') {
65
- return { service: 'configure', operation: stripped[1] || '', args: stripped };
66
- }
67
- const nonFlags = stripped.filter((arg) => !arg.startsWith('-'));
68
- return {
69
- service: nonFlags[0] || '',
70
- operation: nonFlags[1] || nonFlags[0] || '',
71
- args: stripped,
72
- };
73
- }
74
-
75
- function matchesAny(value, patterns) {
76
- return patterns.some((pattern) => regexFrom(pattern).test(value));
77
- }
78
-
79
- function hasWritePrefix(operation, policy) {
80
- const normalized = String(operation);
81
- return policy.writeOperationPrefixes.some((prefix) =>
82
- new RegExp(`(^|[A-Za-z0-9])${prefix}`, 'i').test(normalized),
83
- );
84
- }
85
-
86
- function hasReadPrefix(operation, policy) {
87
- return policy.readOperationPrefixes.some((prefix) =>
88
- new RegExp(`^${prefix}`, 'i').test(operation),
89
- );
90
- }
91
-
92
- function isLocalMetadataCommand(args) {
93
- return args.some((arg) => /^(--help|-h|help|version|--version)$/i.test(String(arg)));
94
- }
95
-
96
- function commandRiskText(normalizedArgs, options = {}) {
97
- return options.rawCommand || ['hcloud', ...normalizedArgs].join(' ');
98
- }
99
-
100
- function applyCommandRiskRules(base, normalizedArgs, options = {}) {
101
- if (base.decision === 'deny' || options.skipRiskRules === true) {
102
- return base;
103
- }
104
- const risk = evaluateCommandRisk(commandRiskText(normalizedArgs, options));
105
- return mergeRiskDecision(base, risk);
106
- }
107
-
108
- export function classifyHcloudArgs(args, options = {}) {
109
- const policy = options.policy || DEFAULT_POLICY;
110
- const { service, operation, args: normalizedArgs } = commandOperation(args);
111
- const joined = normalizedArgs.join(' ');
112
-
113
- if (!normalizedArgs.length) {
114
- return {
115
- decision: 'deny',
116
- risk: 'invalid',
117
- reason: 'Empty hcloud command arguments are not executable.',
118
- };
119
- }
120
-
121
- if (isLocalMetadataCommand(normalizedArgs)) {
122
- return applyCommandRiskRules({
123
- decision: 'allow',
124
- risk: 'local_metadata',
125
- reason: 'KooCLI local help and version commands are read-only and do not call Huawei Cloud resource APIs.',
126
- service,
127
- operation,
128
- args: normalizedArgs,
129
- }, normalizedArgs, options);
130
- }
131
-
132
- if (service.toLowerCase() === 'configure') {
133
- const subcommand = operation.toLowerCase();
134
- if (
135
- policy.blockedConfigureSubcommands.map((cmd) => cmd.toLowerCase()).includes(subcommand)
136
- && options.allowCredentialRead !== true
137
- ) {
138
- return {
139
- decision: 'deny',
140
- risk: 'credential',
141
- reason: 'Direct hcloud configure inspection may expose profile credentials. Use the redacted toolkit tools instead.',
142
- };
143
- }
144
- }
145
-
146
- if (policy.blockedSecretOperations.some((op) => op.toLowerCase() === operation.toLowerCase())) {
147
- return {
148
- decision: 'deny',
149
- risk: 'secret',
150
- reason: 'Direct secret value reads are blocked so plaintext secrets do not enter the agent context.',
151
- };
152
- }
153
-
154
- if (/secret[_-]?string|secret[_-]?binary|showsecretversion|getsecretvalue/i.test(joined)) {
155
- return {
156
- decision: 'deny',
157
- risk: 'secret',
158
- reason: 'The command appears to retrieve a secret value. Use a runtime secret reference pattern instead.',
159
- };
160
- }
161
-
162
- const readOnly = hasReadPrefix(operation, policy);
163
- const executionOps = /(^|\.)(Invoke|SyncInvoke|AsyncInvoke|Send|Trigger|Execute|Start|Reboot|Restart|Stop|Publish|Deploy)/i;
164
- const isExecution = executionOps.test(operation) && !readOnly;
165
- const isWrite = !readOnly && hasWritePrefix(operation, policy);
166
-
167
- if (isExecution && !options.allowWrites) {
168
- return {
169
- decision: 'deny',
170
- risk: 'execution',
171
- reason: 'Huawei Cloud execution/trigger operation blocked until approved.',
172
- };
173
- }
174
-
175
- if (isWrite && !options.allowWrites) {
176
- return {
177
- decision: 'deny',
178
- risk: 'write',
179
- reason: 'Huawei Cloud write operation blocked until the agent presents a plan and receives explicit user approval.',
180
- };
181
- }
182
-
183
- const obsutilWrites = ['mb', 'cp', 'mv', 'rm', 'delete', 'mkdir', 'sync', 'restore', 'chattri', 'bucketpolicy', 'lifecycle', 'cors', 'website', 'sign', 'share-add', 'share-update', 'share-rm'];
184
- const obsutilReads = ['ls', 'stat', 'cat', 'help', 'version'];
185
- const isObs = service.toLowerCase() === 'obs' || service.toLowerCase() === 'hcloud obs';
186
- const isObsWrite = isObs && obsutilWrites.includes(operation);
187
- const isObsRead = isObs && obsutilReads.includes(operation);
188
- if (isObsWrite && !options.allowWrites) {
189
- return {
190
- decision: 'deny',
191
- risk: 'write',
192
- reason: 'OBS write operation blocked until the agent presents a plan and receives explicit user approval.',
193
- };
194
- }
195
- if (isObsRead) {
196
- return applyCommandRiskRules({
197
- decision: 'allow',
198
- risk: 'read_only',
199
- reason: 'OBS read-only operation.',
200
- service,
201
- operation,
202
- args: normalizedArgs,
203
- }, normalizedArgs, options);
204
- }
205
- if (isObsWrite && options.allowWrites) {
206
- return applyCommandRiskRules({
207
- decision: 'allow',
208
- risk: 'write',
209
- reason: 'OBS write operation approved by user.',
210
- service,
211
- operation,
212
- args: normalizedArgs,
213
- }, normalizedArgs, options);
214
- }
215
-
216
- if (isExecution && options.allowWrites) {
217
- return applyCommandRiskRules({
218
- decision: 'allow',
219
- risk: 'execution',
220
- reason: 'Huawei Cloud execution/trigger operation approved by user.',
221
- service,
222
- operation,
223
- args: normalizedArgs,
224
- }, normalizedArgs, options);
225
- }
226
-
227
- if (isWrite && options.allowWrites) {
228
- return applyCommandRiskRules({
229
- decision: 'allow',
230
- risk: 'write',
231
- reason: 'Huawei Cloud write operation approved by user.',
232
- service,
233
- operation,
234
- args: normalizedArgs,
235
- }, normalizedArgs, options);
236
- }
237
-
238
- return applyCommandRiskRules({
239
- decision: 'allow',
240
- risk: readOnly ? 'read_only' : 'unknown_read',
241
- reason: readOnly
242
- ? 'Command appears to be a read-only Huawei Cloud operation.'
243
- : 'Command does not match a known write or secret operation; treat output as untrusted and redact it.',
244
- service,
245
- operation,
246
- args: normalizedArgs,
247
- }, normalizedArgs, options);
248
- }
249
-
250
- function splitSimpleCommand(command) {
251
- return String(command)
252
- .match(/"[^"]*"|'[^']*'|\S+/g)
253
- ?.map((part) => part.replace(/^['"]|['"]$/g, '')) || [];
254
- }
255
-
256
- export function classifyTextCommand(command, options = {}) {
257
- const policy = options.policy || DEFAULT_POLICY;
258
- const text = String(command || '');
259
-
260
- if (matchesAny(text, policy.credentialFilePatterns)) {
261
- return {
262
- decision: 'deny',
263
- risk: 'credential',
264
- reason: 'Reading Huawei Cloud credential or profile files is blocked. Use redacted profile inspection tools instead.',
265
- };
266
- }
267
-
268
- if (/(^|\s)(env|printenv|Get-ChildItem\s+Env:|gci\s+Env:|dir\s+Env:)/i.test(text) && /HUAWEICLOUD|HWC_|HCLOUD|OS_/i.test(text)) {
269
- return {
270
- decision: 'deny',
271
- risk: 'credential',
272
- reason: 'Dumping cloud credential environment variables is blocked.',
273
- };
274
- }
275
-
276
- if (/(^|\s)hcloud(\.exe)?\s+/i.test(text)) {
277
- return classifyHcloudArgs(splitSimpleCommand(text), { ...options, rawCommand: text });
278
- }
279
-
280
- if (/ShowSecretVersion|GetSecretValue|secret_string|secret_binary/i.test(text)) {
281
- return {
282
- decision: 'deny',
283
- risk: 'secret',
284
- reason: 'Direct secret value retrieval patterns are blocked.',
285
- };
286
- }
287
-
288
- return {
289
- decision: 'allow',
290
- risk: 'not_huaweicloud',
291
- reason: 'No Huawei Cloud safety rule matched.',
292
- };
293
- }
294
-
295
- export function assertAllowed(result) {
296
- if (result.decision === 'deny') {
297
- const error = new Error(result.reason);
298
- error.policy = result;
299
- throw error;
300
- }
301
- return result;
302
- }
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { evaluateCommandRisk, mergeRiskDecision } from './risk-rule-engine.mjs';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const policyPath = join(__dirname, '..', 'safety', 'policy.json');
8
+
9
+ export function loadPolicy() {
10
+ return JSON.parse(readFileSync(policyPath, 'utf8'));
11
+ }
12
+
13
+ const DEFAULT_POLICY = loadPolicy();
14
+
15
+ function regexFrom(pattern) {
16
+ return new RegExp(pattern, 'i');
17
+ }
18
+
19
+ function isSecretKeyName(key, policy = DEFAULT_POLICY) {
20
+ const normalized = String(key)
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]/g, '');
23
+ if (
24
+ /access.*key|secret.*key|security.*token|xauth.*token|authorization|password|passwd|adminpass|credential|private.*key/.test(
25
+ normalized,
26
+ )
27
+ ) {
28
+ return true;
29
+ }
30
+ return policy.secretKeyNamePatterns.some((pattern) => regexFrom(`^(${pattern})$`).test(key));
31
+ }
32
+
33
+ function redactString(text) {
34
+ return String(text)
35
+ .replace(
36
+ /((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
37
+ '$1<redacted>',
38
+ )
39
+ .replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
40
+ }
41
+
42
+ export function redactSecrets(value, policy = DEFAULT_POLICY) {
43
+ if (Array.isArray(value)) {
44
+ return value.map((item) => redactSecrets(item, policy));
45
+ }
46
+ if (value && typeof value === 'object') {
47
+ return Object.fromEntries(
48
+ Object.entries(value).map(([key, val]) => [
49
+ key,
50
+ isSecretKeyName(key, policy) ? '<redacted>' : redactSecrets(val, policy),
51
+ ]),
52
+ );
53
+ }
54
+ if (typeof value === 'string') {
55
+ return redactString(value);
56
+ }
57
+ return value;
58
+ }
59
+
60
+ function stripExecutable(args) {
61
+ if (!args.length) return [];
62
+ const first = String(args[0]).toLowerCase();
63
+ if (first === 'hcloud' || first.endsWith('/hcloud') || first.endsWith('\\hcloud') || first === 'hcloud.exe') {
64
+ return args.slice(1);
65
+ }
66
+ return args;
67
+ }
68
+
69
+ function commandOperation(args) {
70
+ const stripped = stripExecutable(args).map(String).filter(Boolean);
71
+ if (stripped[0]?.toLowerCase() === 'configure') {
72
+ return { service: 'configure', operation: stripped[1] || '', args: stripped };
73
+ }
74
+ const nonFlags = stripped.filter((arg) => !arg.startsWith('-'));
75
+ return {
76
+ service: nonFlags[0] || '',
77
+ operation: nonFlags[1] || nonFlags[0] || '',
78
+ args: stripped,
79
+ };
80
+ }
81
+
82
+ function matchesAny(value, patterns) {
83
+ return patterns.some((pattern) => regexFrom(pattern).test(value));
84
+ }
85
+
86
+ function hasWritePrefix(operation, policy) {
87
+ const normalized = String(operation);
88
+ return policy.writeOperationPrefixes.some((prefix) => new RegExp(`(^|[A-Za-z0-9])${prefix}`, 'i').test(normalized));
89
+ }
90
+
91
+ function hasReadPrefix(operation, policy) {
92
+ return policy.readOperationPrefixes.some((prefix) => new RegExp(`^${prefix}`, 'i').test(operation));
93
+ }
94
+
95
+ function isLocalMetadataCommand(args) {
96
+ return args.some((arg) => /^(--help|-h|help|version|--version)$/i.test(String(arg)));
97
+ }
98
+
99
+ function commandRiskText(normalizedArgs, options = {}) {
100
+ return options.rawCommand || ['hcloud', ...normalizedArgs].join(' ');
101
+ }
102
+
103
+ function applyCommandRiskRules(base, normalizedArgs, options = {}) {
104
+ if (base.decision === 'deny' || options.skipRiskRules === true) {
105
+ return base;
106
+ }
107
+ const risk = evaluateCommandRisk(commandRiskText(normalizedArgs, options));
108
+ return mergeRiskDecision(base, risk);
109
+ }
110
+
111
+ export function classifyHcloudArgs(args, options = {}) {
112
+ const policy = options.policy || DEFAULT_POLICY;
113
+ const { service, operation, args: normalizedArgs } = commandOperation(args);
114
+ const joined = normalizedArgs.join(' ');
115
+
116
+ if (!normalizedArgs.length) {
117
+ return {
118
+ decision: 'deny',
119
+ risk: 'invalid',
120
+ reason: 'Empty hcloud command arguments are not executable.',
121
+ };
122
+ }
123
+
124
+ if (isLocalMetadataCommand(normalizedArgs)) {
125
+ return applyCommandRiskRules(
126
+ {
127
+ decision: 'allow',
128
+ risk: 'local_metadata',
129
+ reason: 'KooCLI local help and version commands are read-only and do not call Huawei Cloud resource APIs.',
130
+ service,
131
+ operation,
132
+ args: normalizedArgs,
133
+ },
134
+ normalizedArgs,
135
+ options,
136
+ );
137
+ }
138
+
139
+ if (service.toLowerCase() === 'configure') {
140
+ const subcommand = operation.toLowerCase();
141
+ if (
142
+ policy.blockedConfigureSubcommands.map((cmd) => cmd.toLowerCase()).includes(subcommand) &&
143
+ options.allowCredentialRead !== true
144
+ ) {
145
+ return {
146
+ decision: 'deny',
147
+ risk: 'credential',
148
+ reason:
149
+ 'Direct hcloud configure inspection may expose profile credentials. Use the redacted toolkit tools instead.',
150
+ };
151
+ }
152
+ }
153
+
154
+ if (policy.blockedSecretOperations.some((op) => op.toLowerCase() === operation.toLowerCase())) {
155
+ return {
156
+ decision: 'deny',
157
+ risk: 'secret',
158
+ reason: 'Direct secret value reads are blocked so plaintext secrets do not enter the agent context.',
159
+ };
160
+ }
161
+
162
+ if (/secret[_-]?string|secret[_-]?binary|showsecretversion|getsecretvalue/i.test(joined)) {
163
+ return {
164
+ decision: 'deny',
165
+ risk: 'secret',
166
+ reason: 'The command appears to retrieve a secret value. Use a runtime secret reference pattern instead.',
167
+ };
168
+ }
169
+
170
+ const readOnly = hasReadPrefix(operation, policy);
171
+ const executionOps =
172
+ /(^|\.)(Invoke|SyncInvoke|AsyncInvoke|Send|Trigger|Execute|Start|Reboot|Restart|Stop|Publish|Deploy)/i;
173
+ const isExecution = executionOps.test(operation) && !readOnly;
174
+ const isWrite = !readOnly && hasWritePrefix(operation, policy);
175
+
176
+ if (isExecution && !options.allowWrites) {
177
+ return {
178
+ decision: 'deny',
179
+ risk: 'execution',
180
+ reason: 'Huawei Cloud execution/trigger operation blocked until approved.',
181
+ };
182
+ }
183
+
184
+ if (isWrite && !options.allowWrites) {
185
+ return {
186
+ decision: 'deny',
187
+ risk: 'write',
188
+ reason:
189
+ 'Huawei Cloud write operation blocked until the agent presents a plan and receives explicit user approval.',
190
+ };
191
+ }
192
+
193
+ const obsutilWrites = [
194
+ 'mb',
195
+ 'cp',
196
+ 'mv',
197
+ 'rm',
198
+ 'delete',
199
+ 'mkdir',
200
+ 'sync',
201
+ 'restore',
202
+ 'chattri',
203
+ 'bucketpolicy',
204
+ 'lifecycle',
205
+ 'cors',
206
+ 'website',
207
+ 'sign',
208
+ 'share-add',
209
+ 'share-update',
210
+ 'share-rm',
211
+ ];
212
+ const obsutilReads = ['ls', 'stat', 'cat', 'help', 'version'];
213
+ const isObs = service.toLowerCase() === 'obs' || service.toLowerCase() === 'hcloud obs';
214
+ const isObsWrite = isObs && obsutilWrites.includes(operation);
215
+ const isObsRead = isObs && obsutilReads.includes(operation);
216
+ if (isObsWrite && !options.allowWrites) {
217
+ return {
218
+ decision: 'deny',
219
+ risk: 'write',
220
+ reason: 'OBS write operation blocked until the agent presents a plan and receives explicit user approval.',
221
+ };
222
+ }
223
+ if (isObsRead) {
224
+ return applyCommandRiskRules(
225
+ {
226
+ decision: 'allow',
227
+ risk: 'read_only',
228
+ reason: 'OBS read-only operation.',
229
+ service,
230
+ operation,
231
+ args: normalizedArgs,
232
+ },
233
+ normalizedArgs,
234
+ options,
235
+ );
236
+ }
237
+ if (isObsWrite && options.allowWrites) {
238
+ return applyCommandRiskRules(
239
+ {
240
+ decision: 'allow',
241
+ risk: 'write',
242
+ reason: 'OBS write operation approved by user.',
243
+ service,
244
+ operation,
245
+ args: normalizedArgs,
246
+ },
247
+ normalizedArgs,
248
+ options,
249
+ );
250
+ }
251
+
252
+ if (isExecution && options.allowWrites) {
253
+ return applyCommandRiskRules(
254
+ {
255
+ decision: 'allow',
256
+ risk: 'execution',
257
+ reason: 'Huawei Cloud execution/trigger operation approved by user.',
258
+ service,
259
+ operation,
260
+ args: normalizedArgs,
261
+ },
262
+ normalizedArgs,
263
+ options,
264
+ );
265
+ }
266
+
267
+ if (isWrite && options.allowWrites) {
268
+ return applyCommandRiskRules(
269
+ {
270
+ decision: 'allow',
271
+ risk: 'write',
272
+ reason: 'Huawei Cloud write operation approved by user.',
273
+ service,
274
+ operation,
275
+ args: normalizedArgs,
276
+ },
277
+ normalizedArgs,
278
+ options,
279
+ );
280
+ }
281
+
282
+ return applyCommandRiskRules(
283
+ {
284
+ decision: 'allow',
285
+ risk: readOnly ? 'read_only' : 'unknown_read',
286
+ reason: readOnly
287
+ ? 'Command appears to be a read-only Huawei Cloud operation.'
288
+ : 'Command does not match a known write or secret operation; treat output as untrusted and redact it.',
289
+ service,
290
+ operation,
291
+ args: normalizedArgs,
292
+ },
293
+ normalizedArgs,
294
+ options,
295
+ );
296
+ }
297
+
298
+ function splitSimpleCommand(command) {
299
+ return (
300
+ String(command)
301
+ .match(/"[^"]*"|'[^']*'|\S+/g)
302
+ ?.map((part) => part.replace(/^['"]|['"]$/g, '')) || []
303
+ );
304
+ }
305
+
306
+ export function classifyTextCommand(command, options = {}) {
307
+ const policy = options.policy || DEFAULT_POLICY;
308
+ const text = String(command || '');
309
+
310
+ if (matchesAny(text, policy.credentialFilePatterns)) {
311
+ return {
312
+ decision: 'deny',
313
+ risk: 'credential',
314
+ reason:
315
+ 'Reading Huawei Cloud credential or profile files is blocked. Use redacted profile inspection tools instead.',
316
+ };
317
+ }
318
+
319
+ if (
320
+ /(^|\s)(env|printenv|Get-ChildItem\s+Env:|gci\s+Env:|dir\s+Env:)/i.test(text) &&
321
+ /HUAWEICLOUD|HWC_|HCLOUD|OS_/i.test(text)
322
+ ) {
323
+ return {
324
+ decision: 'deny',
325
+ risk: 'credential',
326
+ reason: 'Dumping cloud credential environment variables is blocked.',
327
+ };
328
+ }
329
+
330
+ if (/(^|\s)hcloud(\.exe)?\s+/i.test(text)) {
331
+ return classifyHcloudArgs(splitSimpleCommand(text), { ...options, rawCommand: text });
332
+ }
333
+
334
+ if (/ShowSecretVersion|GetSecretValue|secret_string|secret_binary/i.test(text)) {
335
+ return {
336
+ decision: 'deny',
337
+ risk: 'secret',
338
+ reason: 'Direct secret value retrieval patterns are blocked.',
339
+ };
340
+ }
341
+
342
+ return {
343
+ decision: 'allow',
344
+ risk: 'not_huaweicloud',
345
+ reason: 'No Huawei Cloud safety rule matched.',
346
+ };
347
+ }
348
+
349
+ export function assertAllowed(result) {
350
+ if (result.decision === 'deny') {
351
+ const error = new Error(result.reason);
352
+ error.policy = result;
353
+ throw error;
354
+ }
355
+ return result;
356
+ }