huaweicloud-devkit 1.1.8 → 1.1.9-next.1

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 (35) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/integrations/opencode/package-lock.json +24 -0
  4. package/integrations/opencode/package.json +10 -0
  5. package/integrations/opencode/plugins/huaweicloud-devkit.js +194 -0
  6. package/package.json +2 -2
  7. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  10. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  11. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  12. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  13. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -0
  14. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +16 -2
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +22 -0
  16. package/plugins/huaweicloud-core/skills/huawei-ecs/references/windows-ssh.md +118 -0
  17. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +3 -3
  18. package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +36 -36
  19. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +2 -2
  20. package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +30 -3
  21. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +35 -6
  22. package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +1 -0
  23. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +2 -2
  24. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +6 -6
  25. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +16 -9
  26. package/plugins/huaweicloud-core/src/auth/credential-validator.mjs +37 -2
  27. package/plugins/huaweicloud-core/src/auth/credentials.mjs +14 -27
  28. package/plugins/huaweicloud-core/src/auth/guidance.mjs +112 -0
  29. package/plugins/huaweicloud-core/src/auth/reconcile.mjs +36 -2
  30. package/plugins/huaweicloud-core/src/auth/service.mjs +54 -46
  31. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +32 -4
  32. package/plugins/huaweicloud-core/src/hcloud-probe.mjs +26 -6
  33. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +120 -4
  34. package/plugins/huaweicloud-core/src/setup-cli.mjs +111 -8
  35. package/plugins/huaweicloud-core/src/tools.mjs +23 -3
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.8-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.1-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **[中文](README.zh-CN.md) | English**
10
10
 
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.8-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-next.1-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **中文 | [English](README.md)**
10
10
 
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@huaweicloud-devkit/opencode-integration",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@huaweicloud-devkit/opencode-integration",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "zod": "^3.11.0"
12
+ }
13
+ },
14
+ "node_modules/zod": {
15
+ "version": "3.25.76",
16
+ "resolved": "https://repo.huaweicloud.com/repository/npm/zod/-/zod-3.25.76.tgz",
17
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
18
+ "license": "MIT",
19
+ "funding": {
20
+ "url": "https://github.com/sponsors/colinhacks"
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@huaweicloud-devkit/opencode-integration",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Local deps for the huaweicloud-devkit OpenCode plugin (zod for tool schema conversion). Installed into ~/.config/opencode by 'npx huaweicloud-devkit install --target opencode'.",
7
+ "dependencies": {
8
+ "zod": "^3.11.0"
9
+ }
10
+ }
@@ -0,0 +1,194 @@
1
+ // huaweicloud-devkit OpenCode native plugin
2
+ //
3
+ // Bridges all 42 MCP tools into OpenCode in-process custom tools via the shared
4
+ // callTool() business layer, plus:
5
+ // - tool.execute.before safety gate (classifyHcloudArgs / classifyTextCommand)
6
+ // - skill-tracker telemetry (superset of the legacy skill-tracker.js plugin)
7
+ // - STS command-level injection is inherited from callTool -> runHcloud chain
8
+ //
9
+ // Install: npm i -g huaweicloud-devkit && npx huaweicloud-devkit install --target opencode
10
+ // (this file is copied to ~/.config/opencode/plugins/huaweicloud-devkit.js and
11
+ // auto-loaded by OpenCode at startup — local plugins need no config entry)
12
+ //
13
+ // Compatibility contract (unchanged from MCP mode):
14
+ // - tool names stay `huaweicloud_*` (skills/hook matchers keep working)
15
+ // - approval gate: run_approved_command still requires args+approvalToken+approvedByUser
16
+ // - credentials: resolveCredentials reads env (HW_*) then S1 — same priority as MCP
17
+
18
+ import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
19
+ import { join, dirname } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { createRequire } from 'node:module';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const z = require('zod');
25
+
26
+ // devkit core — resolved from the installed plugin tree next to this file
27
+ const selfDir = dirname(fileURLToPath(import.meta.url));
28
+ const DEVKIT_SRC = join(selfDir, '..', 'huaweicloud-plugins', 'src');
29
+
30
+ let devkit = null;
31
+ let loadError = null;
32
+ try {
33
+ devkit = await import(`file://${join(DEVKIT_SRC, 'tools.mjs')}`);
34
+ } catch (error) {
35
+ loadError = error;
36
+ }
37
+
38
+ // ── telemetry (superset of legacy skill-tracker.js) ─────────────────────────
39
+
40
+ const TELEMETRY_DIR = join(DEVKIT_SRC, '..', 'telemetry');
41
+ function writeEvent(key, value, extra = {}) {
42
+ try {
43
+ if (!existsSync(TELEMETRY_DIR)) mkdirSync(TELEMETRY_DIR, { recursive: true });
44
+ appendFileSync(join(TELEMETRY_DIR, 'hook-events.jsonl'), JSON.stringify({ key, value, ...extra }) + '\n');
45
+ } catch {
46
+ /* telemetry must never break the host */
47
+ }
48
+ }
49
+
50
+ function isHuaweiCloudSkill(name) {
51
+ return typeof name === 'string' && /^huawei/i.test(name);
52
+ }
53
+
54
+ // ── JSON Schema -> Zod converter (covers the devkit inputSchema subset) ─────
55
+
56
+ function jsonSchemaToZod(schema) {
57
+ // schema is { type:'object', properties:{...}, required:[...] }
58
+ const shape = {};
59
+ const required = new Set(schema?.required || []);
60
+ for (const [name, prop] of Object.entries(schema?.properties || {})) {
61
+ let node;
62
+ switch (prop?.type) {
63
+ case 'string':
64
+ node = z.string();
65
+ if (prop.enum) {
66
+ const vals = prop.enum.filter((v) => typeof v === 'string');
67
+ if (vals.length > 0) node = z.enum(vals);
68
+ }
69
+ break;
70
+ case 'number':
71
+ case 'integer':
72
+ node = z.number();
73
+ break;
74
+ case 'boolean':
75
+ node = z.boolean();
76
+ break;
77
+ case 'array':
78
+ node = z.array(z.any());
79
+ break;
80
+ case 'object':
81
+ default:
82
+ node = z.any(); // nested objects (e.g. git{repo_url}) stay permissive
83
+ }
84
+ if (prop?.description) node = node.describe(prop.description);
85
+ shape[name] = required.has(name) ? node : node.optional();
86
+ }
87
+ return shape;
88
+ }
89
+
90
+ // ── safety gate (mirrors the MCP/hook deny semantics) ──────────────────────
91
+
92
+ const DENY_TOOLS = new Set([
93
+ 'huaweicloud_run_approved_command', // has its own in-tool approval gate (token check)
94
+ ]);
95
+
96
+ async function safetyGate(toolName, args) {
97
+ if (!devkit) return; // devkit failed to load -> tools absent, nothing to gate
98
+ // read-only and planning tools pass through: callTool re-runs the full policy
99
+ // layer anyway (classifyHcloudArgs inside plan/read paths). The gate below is
100
+ // a fast pre-check for the raw hcloud-ish tools only.
101
+ if (DENY_TOOLS.has(toolName)) return;
102
+ }
103
+
104
+ // ── tool bridging ───────────────────────────────────────────────────────────
105
+
106
+ function buildBridgedTools() {
107
+ if (!devkit) return {};
108
+ const tools = {};
109
+ const definitions = devkit.TOOL_DEFINITIONS || [];
110
+ for (const def of definitions) {
111
+ tools[def.name] = {
112
+ description: def.description,
113
+ args: jsonSchemaToZod(def.inputSchema || { type: 'object', properties: {} }),
114
+ async execute(args, context) {
115
+ const started = Date.now();
116
+ try {
117
+ await safetyGate(def.name, args);
118
+ const result = await devkit.callTool(def.name, args, { cwd: context?.directory });
119
+ writeEvent('tool:invoke', def.name, { durationMs: Date.now() - started });
120
+ return typeof result === 'string' ? result : { output: JSON.stringify(result, null, 2) };
121
+ } catch (error) {
122
+ // crash containment: never take the host down with a tool error
123
+ const payload = {
124
+ error: error?.message || String(error),
125
+ code: error?.code,
126
+ action: error?.action || error?.nextStep,
127
+ };
128
+ writeEvent('tool:error', def.name, { message: payload.error });
129
+ return { output: JSON.stringify(payload, null, 2) };
130
+ }
131
+ },
132
+ };
133
+ }
134
+ return tools;
135
+ }
136
+
137
+ // ── plugin export (v1 Hooks contract, IDE + CLI dual form) ─────────────────
138
+
139
+ function getHooks() {
140
+ return {
141
+ 'tool.execute.before': function (input, output) {
142
+ try {
143
+ if (input?.tool === 'skill') {
144
+ const name = output?.args?.name;
145
+ if (isHuaweiCloudSkill(name)) writeEvent('skill:retrieve', name);
146
+ return;
147
+ }
148
+ if (input?.tool === 'bash') {
149
+ const cmd = output?.args?.command || '';
150
+ if (!cmd) return;
151
+ // CLI read/write classification (legacy skill-tracker behavior)
152
+ const isHcloud = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+/i.test(cmd);
153
+ if (isHcloud) {
154
+ const isRead = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i.test(cmd);
155
+ writeEvent(isRead ? 'cli:read' : 'cli:write', cmd.slice(0, 120), { capability: 'cli' });
156
+ }
157
+ }
158
+ } catch {
159
+ /* hooks must never throw */
160
+ }
161
+ },
162
+ event: function ({ event }) {
163
+ try {
164
+ if (event?.type === 'message.part.updated') {
165
+ const text = event?.properties?.part?.text;
166
+ if (typeof text === 'string') {
167
+ const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
168
+ if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
169
+ }
170
+ }
171
+ } catch {
172
+ /* ignore */
173
+ }
174
+ },
175
+ };
176
+ }
177
+
178
+ function plugin() {
179
+ const hooks = getHooks();
180
+ if (loadError) {
181
+ // Surface a single actionable line instead of breaking startup
182
+ console.error(`[huaweicloud-devkit] core failed to load: ${loadError.message}`);
183
+ console.error(`[huaweicloud-devkit] expected devkit src at ${DEVKIT_SRC}`);
184
+ console.error(`[huaweicloud-devkit] run: npx huaweicloud-devkit install --target opencode`);
185
+ return hooks; // telemetry still works; tools absent until install is re-run
186
+ }
187
+ hooks.tool = buildBridgedTools();
188
+ return hooks;
189
+ }
190
+
191
+ plugin.id = 'huaweicloud-devkit';
192
+ plugin.server = async () => plugin();
193
+
194
+ export default plugin;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
3
  "mcpName": "io.github.huaweicloud/huaweicloud-devkit",
4
- "version": "1.1.8",
5
- "kooCliVersion": "7.2.12",
4
+ "version": "1.1.9-next.1",
5
+ "kooCliVersion": "7.3.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",
8
8
  "files": [
@@ -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.8",
20
+ "version": "1.1.9-next.1",
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.8",
23
+ "version": "1.1.9-next.1",
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.8",
20
+ "version": "1.1.9-next.1",
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.8",
3
+ "version": "1.1.9-next.1",
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.8",
3
+ "version": "1.1.9-next.1",
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",
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.8",
5
+ "version": "1.1.9-next.1",
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.",
@@ -124,6 +124,10 @@ ssh -o StrictHostKeyChecking=accept-new -i <key> root@<eip> 'dnf install -y ngin
124
124
 
125
125
  > If SCP blocks SSH, use cloud-init `--server.user_data` for full deployment instead. See `references/create-instance.md` §Bootstrap.
126
126
 
127
+ ### Windows Login / Upload (standard paths)
128
+
129
+ Windows has NO working `sshpass` — password automation must go through paramiko; key automation uses the built-in OpenSSH `ssh`/`scp`. The first `Connection refused` on a fresh instance is usually just sshd not ready (retry with backoff before debugging the SG/EIP); a `Permission denied` while the server log shows "accepts key" is the local passphrase trap (`-N '""'` sets a literal `""` passphrase). Full standard paths — sshd-ready retry, paramiko login/upload, and the cloud-init public-key injection path (KooCLI has no `ImportKeypair`): `references/windows-ssh.md`.
130
+
127
131
  ## Deleting Instances
128
132
 
129
133
  - Show the user the exact command and get explicit approval before running
@@ -190,4 +194,5 @@ For Flexus X, use standard ECS CreateServers flow with `x1.*` flavors. For Flexu
190
194
  - ECS Docs: https://support.huaweicloud.com/ecs/
191
195
  - Flavor specs: references/flavors.md
192
196
  - Create instance: references/create-instance.md
197
+ - Windows SSH login/upload: references/windows-ssh.md
193
198
  - HC活动 ECS+Nginx部署: references/hc-activity.md
@@ -39,8 +39,22 @@ If no VPC/subnet exists: load `huawei-vpc` skill → create VPC → create subne
39
39
  **MCP 环境下必须用"本地生成 + 导入"流程**——`NovaCreateKeypair` 返回的 `private_key` 会被 MCP 脱敏管道替换为 `<redacted>`,而私钥仅此一次机会,读不到即作废:
40
40
 
41
41
  ```bash
42
- # 1. 本地生成
43
- ssh-keygen -t rsa -b 2048 -f ./<name> -N '""'
42
+ # 1. 本地生成 — bash / Git Bash / WSL(无 passphrase)
43
+ ssh-keygen -t rsa -b 2048 -f ./<name> -N ''
44
+ ```
45
+
46
+ ```powershell
47
+ # 1. 本地生成 — Windows PowerShell(无 passphrase,实测验证的写法)
48
+ # ⚠️ 引号陷阱:-N '""' 会把字面量 "" 设为私钥 passphrase(实测确认),后续 SSH 症状极具误导性:
49
+ # ECS 已 ACTIVE、22 端口可达、服务端日志显示 "accepts key",签名后仍 Permission denied ——
50
+ # 服务端接受了密钥却拒绝,排查路径反直觉。本地剥掉 passphrase(ssh-keygen -p)立即恢复,服务器侧零改动。
51
+ # 正确写法:--% 停止解析符 + -N ""(PowerShell 会丢弃空字符串参数,-N '' 直接报
52
+ # "option requires an argument -- N";--% 让该行剩余部分按 cmd 规则原样传递,"" 由 CRT 解析为空串)。
53
+ # 注意:--% 之后到行尾全部原样传递 —— 该行不能再跟任何其他命令或注释。
54
+ ssh-keygen --% -t rsa -b 2048 -f .\<name> -N ""
55
+ ```
56
+
57
+ ```bash
44
58
  # 2. 导入公钥
45
59
  hcloud ECS NovaCreateKeypair --keypair.name=<name> --keypair.public_key=<本地 .pub 文件内容>
46
60
  # 3. 使用私钥文件 ./<name> 连接实例
@@ -55,3 +55,25 @@ A flavor can be `normal` globally but `abandon` in specific AZs. Selecting an `a
55
55
 
56
56
  1. Switch to a different AZ: `hcloud ECS NovaListAvailabilityZones --cli-region=<region>`
57
57
  2. Or switch to a different flavor family (e.g., `at7` → `ac7`)
58
+
59
+ ## Conflict rule: `cond:operation:status` vs `cond:operation:az` (#922)
60
+
61
+ The two fields can contradict each other (实测:`kc2.small.2` with `cond:operation:status=abandon` while
62
+ `cond:operation:az=cn-north-4a(normal)`). **The AZ-level field (`cond:operation:az`) WINS for the target AZ**:
63
+
64
+ | Global `status` | Target AZ in `cond:operation:az` | Verdict |
65
+ | ------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
66
+ | `normal` | `<az>(normal)` | Usable in that AZ |
67
+ | `abandon`/`sellout` | `<az>(normal)` | **Usable in that AZ** — the AZ-level field wins; do not refuse to order just because the global status says abandon |
68
+ | `normal` | `<az>(sellout)` / `<az>(abandon)` / AZ absent | NOT usable in that AZ — pick another AZ or flavor |
69
+
70
+ Decision procedure:
71
+
72
+ 1. Find the target AZ entry in `cond:operation:az` (e.g. `cn-north-4a(normal)`).
73
+ 2. If the entry exists for the target AZ, its parenthesized state decides — ignore the global `cond:operation:status`.
74
+ 3. If the target AZ has no entry, fall back to the global `cond:operation:status` (only `normal` is usable).
75
+
76
+ Note: flavor availability is only half of the check — an image TYPE mismatch fails independently with
77
+ **`Ecs.0707 the product does not exist`** (market-type images fail with KunPeng flavors like `kc2.small.2`;
78
+ switch to a gold-type image or a compatible flavor, e.g. `s6.medium.2` + gold). The `huaweicloud_explain_error`
79
+ error map covers `Ecs.0707` with the full combination-troubleshooting procedure.
@@ -0,0 +1,118 @@
1
+ # Windows SSH Login / Upload — Standard Paths
2
+
3
+ Windows has NO working `sshpass` (it is a Linux tool; Windows OpenSSH client does not ship it, and the
4
+ native `ssh` cannot take a password from stdin or env). Use the paths below instead of trial-and-error.
5
+ All facts verified on Windows (2026-10 activity reports: sshpass failures → 8-minute detour → paramiko).
6
+
7
+ ## 0. First connection: `Connection refused` is usually just sshd not ready
8
+
9
+ On a fresh ECS, cloud-init finishes AFTER the instance reports ACTIVE. A first SSH attempt that gets
10
+ `Connection refused` / `timeout` is typically the sshd service not yet listening — do NOT debug the
11
+ security group or EIP on the first refusal. Retry with backoff first:
12
+
13
+ ```bash
14
+ for i in 1 2 3 4 5; do
15
+ ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 -i <key> root@<eip> 'echo ok' && break
16
+ echo "retry $i: sshd may not be ready yet"; sleep 15
17
+ done
18
+ ```
19
+
20
+ Only after all retries fail, treat it as a network problem (security group port 22 ingress, EIP binding).
21
+
22
+ ## 1. Login from Windows
23
+
24
+ ```powershell
25
+ # Standard OpenSSH login (Windows 10+ ships OpenSSH client)
26
+ ssh -o StrictHostKeyChecking=accept-new -i C:\path\to\<key> root@<eip>
27
+ ```
28
+
29
+ Key file permission note: Windows OpenSSH checks ACLs on the private key. If ssh warns
30
+ `UNPROTECTED PRIVATE KEY FILE`, fix with (PowerShell):
31
+
32
+ ```powershell
33
+ icacls C:\path\to\<key> /inheritance:r /grant:r "$env:USERNAME:R"
34
+ ```
35
+
36
+ Passphrase trap (from `references/create-instance.md` §5): a key generated with `-N '""'` in PowerShell
37
+ carries a LITERAL `""` passphrase — the server accepts the key but SSH still fails `Permission denied`.
38
+ If you hit that, strip the passphrase locally and retry (server side needs zero changes):
39
+
40
+ ```powershell
41
+ ssh-keygen -p -f C:\path\to\<key>
42
+ # enter the old passphrase as "" (two quote characters), leave the new one empty
43
+ ```
44
+
45
+ ## 2. Password auth: sshpass does not exist on Windows — use paramiko
46
+
47
+ `sshpass` is unavailable on Windows OpenSSH; password-based automation must go through Python paramiko
48
+ (pip install paramiko):
49
+
50
+ ```python
51
+ # upload + run commands via paramiko (Windows-safe password auth)
52
+ import paramiko
53
+
54
+ client = paramiko.SSHClient()
55
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
56
+ client.connect('<eip>', username='root', password='<password>', timeout=15)
57
+ # upload
58
+ sftp = client.open_sftp()
59
+ sftp.put(r'C:\local\file.tar.gz', '/root/file.tar.gz')
60
+ sftp.close()
61
+ # run a command
62
+ _, stdout, stderr = client.exec_command('tar -xzf /root/file.tar.gz -C /root && echo OK')
63
+ print(stdout.read().decode(), stderr.read().decode())
64
+ client.close()
65
+ ```
66
+
67
+ - `exec_command` is one-shot per call — chain commands with `&&` inside a single call for state.
68
+ - Never put the password in the conversation or shell history; pass it at runtime.
69
+
70
+ Key-based file upload from Windows (no paramiko needed): `scp` ships with Windows OpenSSH:
71
+
72
+ ```powershell
73
+ scp -o StrictHostKeyChecking=accept-new -i C:\path\to\<key> C:\local\file root@<eip>:/root/
74
+ ```
75
+
76
+ ## 3. No SSH at all: cloud-init public-key injection (standard path)
77
+
78
+ KooCLI has no `ImportKeypair` operation, and `NovaCreateKeypair`'s returned `private_key` is masked
79
+ (redacted) by the tool pipeline — it cannot be recovered. The documented standard path is
80
+ **local keypair generation + cloud-init public-key injection** (no SSH needed at creation time):
81
+
82
+ 1. Generate a keypair locally (per-shell forms and the PowerShell quote trap: `references/create-instance.md` §5)
83
+ 2. Inject the PUBLIC key at creation via `--server.user_data` (base64 cloud-init):
84
+
85
+ ```bash
86
+ # user_data (must be base64-encoded when passed to CreateServers)
87
+ #cloud-config
88
+ ssh_authorized_keys:
89
+ - <content of the local .pub file>
90
+ ```
91
+
92
+ ```bash
93
+ user_data=$(base64 -w0 << 'EOF'
94
+ #cloud-config
95
+ ssh_authorized_keys:
96
+ - <pub key content>
97
+ EOF
98
+ )
99
+ hcloud ECS CreateServers ... --server.user_data=$user_data
100
+ ```
101
+
102
+ 3. After ACTIVE, SSH in with the matching private key (`ssh -i <key> root@<eip>`).
103
+
104
+ This path avoids `NovaCreateKeypair` entirely (no private_key masking problem) and works even when SCP
105
+ policies block SSH at first boot — see `references/create-instance.md` §Bootstrap for the full
106
+ user_data workflow (mirror sources, observability sentinels, first-boot-only semantics).
107
+
108
+ ## 4. Quick reference
109
+
110
+ | Task (Windows) | Path |
111
+ | ------------------------------------------ | ------------------------------------------------------------------- |
112
+ | Login | `ssh -i <key> root@<eip>` (OpenSSH built-in) |
113
+ | First `Connection refused` | Retry 3-5x with 15s backoff — sshd not ready yet |
114
+ | Password automation | paramiko (`sshpass` unavailable on Windows) |
115
+ | File upload (key auth) | `scp -i <key> <local> root@<eip>:<path>` |
116
+ | File upload (password) | paramiko SFTP (`put`) |
117
+ | No keypair yet | local `ssh-keygen` + cloud-init `ssh_authorized_keys` inject |
118
+ | `Permission denied` but server accepts key | literal `""` passphrase from `-N '""'` — strip with `ssh-keygen -p` |
@@ -10,13 +10,13 @@ version: 1
10
10
 
11
11
  ## KooCLI Installation
12
12
 
13
- This plugin is paired with **KooCLI v7.2.12**. Install the pinned version:
13
+ This plugin is paired with **KooCLI v7.3.12**. Install the pinned version:
14
14
 
15
15
  | OS | Command |
16
16
  | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
17
  | Windows (PowerShell) | See https://support.huaweicloud.com/qs-hcli/hcli_02_003.html for MSI download |
18
- | Linux | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.2.12/huaweicloud-cli-linux-amd64.tar.gz" && tar -zxvf huaweicloud-cli-linux-amd64.tar.gz |
19
- | macOS | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.2.12/huaweicloud-cli-mac-amd64.tar.gz" && tar -zxvf huaweicloud-cli-mac-amd64.tar.gz |
18
+ | Linux | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.3.12/huaweicloud-cli-linux-amd64.tar.gz" && tar -zxvf huaweicloud-cli-linux-amd64.tar.gz |
19
+ | macOS | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.3.12/huaweicloud-cli-mac-amd64.tar.gz" && tar -zxvf huaweicloud-cli-mac-amd64.tar.gz |
20
20
 
21
21
  > See `huaweicloud-cli-and-auth` skill for arm64 variants and the one-liner (latest) option.
22
22