huaweicloud-devkit 1.1.4-next.6 → 1.1.5
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -2
- 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/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/policy.json +1 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +22 -1
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +34 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +135 -0
- package/plugins/huaweicloud-core/src/mcp-protocol.mjs +5 -1
- package/plugins/huaweicloud-core/src/mcp-server-remote.mjs +1 -1
- package/plugins/huaweicloud-core/src/mcp-server.mjs +1 -1
- package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +12 -1
- package/plugins/huaweicloud-core/src/safety-policy.mjs +86 -3
- package/plugins/huaweicloud-core/src/tools.mjs +8 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](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)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**中文 | [English](README.md)**
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"mcpName": "io.github.huaweicloud/huaweicloud-devkit",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.5",
|
|
5
5
|
"kooCliVersion": "7.2.12",
|
|
6
6
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
7
7
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"test": "node --test \"test/*.test.mjs\"",
|
|
33
|
+
"test": "node --experimental-test-module-mocks --test \"test/*.test.mjs\"",
|
|
34
34
|
"format": "prettier --write .",
|
|
35
35
|
"format:check": "prettier --check .",
|
|
36
36
|
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.git\" \"#test/**\" \"#docs/**\" \"#.superpowers/**\"",
|
|
@@ -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.
|
|
20
|
+
"version": "1.1.5",
|
|
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.
|
|
23
|
+
"version": "1.1.5",
|
|
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.
|
|
20
|
+
"version": "1.1.5",
|
|
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.
|
|
3
|
+
"version": "1.1.5",
|
|
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.
|
|
3
|
+
"version": "1.1.5",
|
|
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.
|
|
5
|
+
"version": "1.1.5",
|
|
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.",
|
|
@@ -8,6 +8,24 @@ version: 1
|
|
|
8
8
|
|
|
9
9
|
**STOP - Do not answer from general knowledge.** Follow the procedure below.
|
|
10
10
|
|
|
11
|
+
## Target-Selection Gate (MANDATORY — do not skip)
|
|
12
|
+
|
|
13
|
+
When the developer asks to **deploy / host / publish / 上线 / 部署 / 发布 / 搭建** a website, web app, or static site WITHOUT naming a specific target, you MUST present the target options and obtain an explicit choice BEFORE calling any sandbox lifecycle tool (`huaweicloud_sandbox_check_user`, `huaweicloud_sandbox_connect`, `huaweicloud_sandbox_upload_project`, `huaweicloud_sandbox_deploy_nginx`, etc.).
|
|
14
|
+
|
|
15
|
+
Use the `question` tool and present, in this exact order:
|
|
16
|
+
|
|
17
|
+
1. **沙箱 (Sandbox)** — temporary runtime, instant preview URL (recommended for quick preview)
|
|
18
|
+
2. **OBS 静态托管** — long-term static hosting + CDN + custom domain
|
|
19
|
+
3. **ECS** — server-based hosting
|
|
20
|
+
4. **CCE** — container-based hosting
|
|
21
|
+
|
|
22
|
+
Hard rules:
|
|
23
|
+
|
|
24
|
+
- **NEVER infer or default the target** when it is unstated. Calling `huaweicloud_sandbox_connect` before this choice is made is a violation.
|
|
25
|
+
- Only skip the question when: (a) the developer already named a target (e.g. "部署到 ECS", "用 OBS"), or (b) the developer explicitly asked for a temporary / sandbox / preview runtime.
|
|
26
|
+
- "部署到华为云" / "deploy to Huawei Cloud" / "上线" alone is NOT a named target — still run the gate.
|
|
27
|
+
- After the choice: sandbox → continue with this skill; OBS → `huawei-obs`; ECS → `huawei-ecs` / `huawei-iac`; CCE → `huawei-cce`.
|
|
28
|
+
|
|
11
29
|
## Overview
|
|
12
30
|
|
|
13
31
|
Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace terminal execution. Covers sandbox lifecycle via hdkitservice API and remote terminal command execution via hwlink protocol.
|
|
@@ -15,7 +33,7 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
15
33
|
## Activation
|
|
16
34
|
|
|
17
35
|
- **Proactive offering**: when the developer's task needs a temporary runtime (e.g. "deploy this web app", "run this app and preview it"), offer the sandbox proactively — the developer never has to say "use sandbox". Prompt: "This task can be satisfied by a sandbox — use it?"
|
|
18
|
-
- **Static website hosting — present options, sandbox first**: when the developer asks to deploy/host/publish a static website or web app without naming a target, do NOT silently pick a service.
|
|
36
|
+
- **Static website hosting — present options, sandbox first (MANDATORY)**: when the developer asks to deploy/host/publish a static website or web app without naming a target, do NOT silently pick a service and do NOT default to the sandbox. Run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) via the `question` tool first. Present, in order: ① 沙箱 (recommended — temporary runtime, instant preview URL) ② OBS 静态托管 (long-term static hosting/CDN) ③ ECS ④ CCE. Recommended prompt: "建议优先部署到沙箱(临时运行环境,可预览访问),也可选择 OBS 静态托管、ECS 或 CCE,你想部署到哪里?" Only if the developer, after being asked, expresses no preference may you proceed with the sandbox.
|
|
19
37
|
- **Detect web apps in deploy-a-repo prompts**: prompts like "部署 GitHub - <owner>/<repo>" or "deploy <github repo>" do not say "web app". Clone or inspect the repository first (package.json with dev/serve/build scripts, index.html, frontend framework dependencies such as vue/react/angular/vite, static sites, Dockerfile running a web server). If it is a web application, offer the sandbox first: "检测到该项目是 Web 应用,建议优先使用沙箱部署(临时运行环境,可预览访问),是否使用?"
|
|
20
38
|
- **Clone into the sandbox workspace directory**: always put project code under `/workspace/<repo-name>` (create the directory if missing) — `/workspace` is the sandbox's dedicated workspace mount at the filesystem root, not `$HOME/workspace`. Never use `/tmp` or other ephemeral locations. This keeps the project with the sandbox session, is easy to reference for serving/exposing, and survives session-level restarts of the shell.
|
|
21
39
|
- **Deployment must end with a public URL**: after deploying and exposing the app with DevBridge, always return the tunnel URL to the developer as the final result — a deployment without an accessible link is incomplete.
|
|
@@ -86,6 +104,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
86
104
|
|
|
87
105
|
## Workflow
|
|
88
106
|
|
|
107
|
+
**Step 0 — Target-Selection Gate (MANDATORY)**: before any sandbox lifecycle tool, confirm the target. If the developer asked to deploy/host/publish a site without naming a target, STOP and run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) first; do NOT call `huaweicloud_sandbox_connect` until it is resolved. Proceed with this workflow only if the target is already confirmed as sandbox (or a temporary runtime).
|
|
108
|
+
|
|
89
109
|
Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
|
|
90
110
|
|
|
91
111
|
1. **Check user** (transparent): `huaweicloud_sandbox_check_user` — returns `realnameVerified`/`agreementSigned` (200) when all good, OR throws a 403 error with one of these codes:
|
|
@@ -836,6 +856,7 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
|
|
|
836
856
|
|
|
837
857
|
| Trap | Why |
|
|
838
858
|
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
859
|
+
| Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
|
|
839
860
|
| Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
|
|
840
861
|
| Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
|
|
841
862
|
| Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
|
|
@@ -177,6 +177,40 @@ export function resolveCredentials(options = {}) {
|
|
|
177
177
|
'Huawei Cloud credentials are not configured. Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
|
|
178
178
|
);
|
|
179
179
|
err.code = 'HDKIT_CRED_MISSING';
|
|
180
|
+
// Lightweight onboarding hint. HDKIT_CRED_MISSING fires when no credential
|
|
181
|
+
// resolved: with env empty/masked and S1 absent, the scenario is 3 by
|
|
182
|
+
// default. Scenario 4 (import a config-provided account) is only reachable
|
|
183
|
+
// in the narrow case where CodeArts S4 offers real creds that the resolver
|
|
184
|
+
// still couldn't adopt (e.g. CODEARTS_PROJECT_DIR set but the marker dir is
|
|
185
|
+
// absent). Full guidance comes from getAuthStatus(); keep this branch cheap.
|
|
186
|
+
const codeartsCreds = readCodeArtsCredentials();
|
|
187
|
+
const injectedAvailable = present(codeartsCreds?.ak) && present(codeartsCreds?.sk);
|
|
188
|
+
err.onboarding = injectedAvailable
|
|
189
|
+
? {
|
|
190
|
+
scenario: 4,
|
|
191
|
+
reason: 'import-injected',
|
|
192
|
+
message: '检测到配置中已有有效账号,可导入为正式凭证。',
|
|
193
|
+
steps: isCodeArtsContext()
|
|
194
|
+
? [{ action: 'auth_switch', args: { mode: 'mcp-config', action: 'persist' }, label: '导入配置中的账号' }]
|
|
195
|
+
: [
|
|
196
|
+
{
|
|
197
|
+
action: 'auth_switch',
|
|
198
|
+
args: { mode: 'import', action: 'persist' },
|
|
199
|
+
label: '通过 creds-import.json 导入',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
scenario: 3,
|
|
205
|
+
reason: 's1-missing',
|
|
206
|
+
message: '未配置华为云凭证,需要完成登录后才能使用云能力。',
|
|
207
|
+
steps: isCodeArtsContext()
|
|
208
|
+
? [
|
|
209
|
+
{ action: 'write-import', target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json') },
|
|
210
|
+
{ action: 'auth_switch', args: { mode: 'import', action: 'persist' } },
|
|
211
|
+
]
|
|
212
|
+
: [{ action: 'auth-init', args: {} }],
|
|
213
|
+
};
|
|
180
214
|
throw err;
|
|
181
215
|
}
|
|
182
216
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
|
|
3
5
|
import { getAgentRegistrationStatuses } from './agent-registration.mjs';
|
|
4
6
|
import { resolveAndApplyProjectId } from './project-id.mjs';
|
|
5
7
|
import {
|
|
6
8
|
globalCredentialsPath,
|
|
9
|
+
isPlaceholder,
|
|
7
10
|
obsConfigPath,
|
|
8
11
|
readGlobalCredentials,
|
|
9
12
|
writeLastSync,
|
|
@@ -18,10 +21,141 @@ import {
|
|
|
18
21
|
} from './reconcile.mjs';
|
|
19
22
|
import { hcloudProbeNextStep, probeHcloud } from '../hcloud-probe.mjs';
|
|
20
23
|
|
|
24
|
+
function isCodeArtsHome() {
|
|
25
|
+
return (
|
|
26
|
+
existsSync(join(process.cwd(), '.codeartsdoer')) ||
|
|
27
|
+
existsSync(join(homedir(), '.codeartsdoer')) ||
|
|
28
|
+
existsSync(join(homedir(), '.codeartswork'))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function envHasRealTriplet() {
|
|
33
|
+
return (
|
|
34
|
+
!isPlaceholder(process.env.HW_ACCESS_KEY) &&
|
|
35
|
+
Boolean(process.env.HW_ACCESS_KEY) &&
|
|
36
|
+
!isPlaceholder(process.env.HW_SECRET_KEY) &&
|
|
37
|
+
Boolean(process.env.HW_SECRET_KEY) &&
|
|
38
|
+
!isPlaceholder(process.env.HW_SECURITY_TOKEN) &&
|
|
39
|
+
Boolean(process.env.HW_SECURITY_TOKEN)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Onboarding guidance (scenarios 1-4) for credential setup.
|
|
44
|
+
// scenario: 1=S1 exists, no env/…; 2=conflict (S1 vs injected); 3=everything
|
|
45
|
+
// missing (fresh user); 4=S1 empty but injected creds exist (import candidate).
|
|
46
|
+
export function computeOnboarding({ credentials, reconciled } = {}) {
|
|
47
|
+
const creds = credentials ?? readGlobalCredentials();
|
|
48
|
+
const scan = reconciled ?? exportStateForStatus();
|
|
49
|
+
const s1Has = Boolean(creds?.ak && creds?.sk && !isPlaceholder(creds.ak) && !isPlaceholder(creds.sk));
|
|
50
|
+
const injected = envHasRealTriplet();
|
|
51
|
+
// env has real non-triplet creds that are NOT placeholders (e.g. devspace AK/SK w/o token)
|
|
52
|
+
const envRealAk = !isPlaceholder(process.env.HW_ACCESS_KEY) && Boolean(process.env.HW_ACCESS_KEY);
|
|
53
|
+
const envRealSk = !isPlaceholder(process.env.HW_SECRET_KEY) && Boolean(process.env.HW_SECRET_KEY);
|
|
54
|
+
const envHasCreds = envRealAk && envRealSk;
|
|
55
|
+
const codeArts = isCodeArtsHome();
|
|
56
|
+
const accountHint = s1Has ? fingerprint(creds.ak, creds.sk) : null;
|
|
57
|
+
|
|
58
|
+
let scenario;
|
|
59
|
+
let reason;
|
|
60
|
+
let message;
|
|
61
|
+
let steps;
|
|
62
|
+
|
|
63
|
+
if (scan.hasRuntime) {
|
|
64
|
+
scenario = 0;
|
|
65
|
+
reason = 'runtime-active';
|
|
66
|
+
message = 'Runtime credentials are active; no setup needed.';
|
|
67
|
+
return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
|
|
68
|
+
}
|
|
69
|
+
if (envHasRealTriplet() && !s1Has) {
|
|
70
|
+
scenario = 0;
|
|
71
|
+
reason = 'platform-injected';
|
|
72
|
+
message = 'Platform credentials are active; nothing to configure.';
|
|
73
|
+
return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
|
|
74
|
+
}
|
|
75
|
+
if (s1Has && !envHasCreds) {
|
|
76
|
+
scenario = 1;
|
|
77
|
+
reason = 's1-only';
|
|
78
|
+
message = `已保存账号(指纹 ${accountHint})可直接使用。`;
|
|
79
|
+
steps = [
|
|
80
|
+
{ order: 1, action: 'use-s1', args: {}, label: '直接使用已保存账号' },
|
|
81
|
+
{ order: 2, action: 'switch-new', args: { mode: 'import', action: 'persist' }, label: '改用新账号(导入)' },
|
|
82
|
+
];
|
|
83
|
+
} else if (s1Has && envHasCreds) {
|
|
84
|
+
// Covers both plain env creds (conflict) AND platform triplet when S1
|
|
85
|
+
// exists. resolveCredentials' STS gate keeps S1 in front of a triplet,
|
|
86
|
+
// so the effective account is S1 → scenario 1 semantics plus a note.
|
|
87
|
+
if (injected) {
|
|
88
|
+
scenario = 1;
|
|
89
|
+
reason = 's1-with-platform-injected';
|
|
90
|
+
message = `已保存账号(指纹 ${accountHint})优先于平台注入,可直接使用。`;
|
|
91
|
+
steps = [
|
|
92
|
+
{ order: 1, action: 'use-s1', args: {}, label: '使用已保存账号' },
|
|
93
|
+
{
|
|
94
|
+
order: 2,
|
|
95
|
+
action: 'switch-platform',
|
|
96
|
+
args: { mode: 'mcp-config', action: 'persist' },
|
|
97
|
+
label: '改用平台注入账号',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
} else {
|
|
101
|
+
scenario = 2;
|
|
102
|
+
reason = 'conflict';
|
|
103
|
+
message = '检测到两套账号:已保存 与 环境注入,请选择其一。';
|
|
104
|
+
steps = [
|
|
105
|
+
{
|
|
106
|
+
order: 1,
|
|
107
|
+
action: 'switch-persist',
|
|
108
|
+
args: { mode: 'memory', action: 'persist' },
|
|
109
|
+
label: '使用已保存账号覆盖',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
order: 2,
|
|
113
|
+
action: 'switch-env',
|
|
114
|
+
args: { mode: 'mcp-config', action: 'persist' },
|
|
115
|
+
label: '使用环境注入账号导入',
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
} else if (!s1Has && envHasCreds && !injected) {
|
|
120
|
+
scenario = 4;
|
|
121
|
+
reason = 'import-injected';
|
|
122
|
+
message = '检测到配置中已有有效账号,可导入为正式凭证。';
|
|
123
|
+
steps = [
|
|
124
|
+
{
|
|
125
|
+
order: 1,
|
|
126
|
+
action: 'switch-persist',
|
|
127
|
+
args: { mode: codeArts ? 'mcp-config' : 'import', action: 'persist' },
|
|
128
|
+
label: '导入该账号',
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
} else {
|
|
132
|
+
scenario = 3;
|
|
133
|
+
reason = 's1-missing';
|
|
134
|
+
message = '未配置华为云凭证,需要完成登录后才能使用云能力。';
|
|
135
|
+
steps = [
|
|
136
|
+
{ order: 1, action: 'obtain-aksk', target: 'console', label: '获取 AK/SK(华为云控制台)' },
|
|
137
|
+
...(codeArts
|
|
138
|
+
? [
|
|
139
|
+
{
|
|
140
|
+
order: 2,
|
|
141
|
+
action: 'write-import',
|
|
142
|
+
target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json'),
|
|
143
|
+
label: '把 AK/SK 写入 creds-import.json',
|
|
144
|
+
},
|
|
145
|
+
{ order: 3, action: 'auth_switch', args: { mode: 'import', action: 'persist' }, label: '执行导入完成配置' },
|
|
146
|
+
]
|
|
147
|
+
: [{ order: 2, action: 'auth-init', args: {}, label: '运行 npx huaweicloud-devkit auth init' }]),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { needsSetup: true, scenario, reason, message, steps, accountHint };
|
|
152
|
+
}
|
|
153
|
+
|
|
21
154
|
export function getAuthStatus(target = 'all') {
|
|
22
155
|
const credentials = readGlobalCredentials();
|
|
23
156
|
const reconciled = { ...exportStateForStatus(), runtimeActive: hasRuntimeCredentials() };
|
|
24
157
|
const hcloud = probeHcloud();
|
|
158
|
+
const onboarding = computeOnboarding({ credentials, reconciled });
|
|
25
159
|
return {
|
|
26
160
|
target,
|
|
27
161
|
credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
|
|
@@ -32,6 +166,7 @@ export function getAuthStatus(target = 'all') {
|
|
|
32
166
|
kooCliStatus: hcloud.status,
|
|
33
167
|
kooCliNextStep: hcloudProbeNextStep(hcloud),
|
|
34
168
|
reconciled,
|
|
169
|
+
onboarding,
|
|
35
170
|
agents: getAgentRegistrationStatuses(target).agents,
|
|
36
171
|
};
|
|
37
172
|
}
|
|
@@ -92,5 +92,9 @@ export async function dispatch(method, params, opts = {}) {
|
|
|
92
92
|
return { resources: [] };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// JSON-RPC 2.0: unknown methods must surface as -32601 (Method not found),
|
|
96
|
+
// not the generic -32603 internal error (#650 D9-2).
|
|
97
|
+
const methodError = new Error(`Method not found: ${method}`);
|
|
98
|
+
methodError.code = -32601;
|
|
99
|
+
throw methodError;
|
|
96
100
|
}
|
|
@@ -60,7 +60,7 @@ export async function startRemoteServer({ port = DEFAULT_PORT, host = DEFAULT_HO
|
|
|
60
60
|
response = {
|
|
61
61
|
jsonrpc: '2.0',
|
|
62
62
|
id: message.id,
|
|
63
|
-
error: { code: -32603, message: error.message },
|
|
63
|
+
error: { code: Number.isSafeInteger(error.code) ? error.code : -32603, message: error.message },
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -3,6 +3,17 @@ import { getProxyUrlForTarget } from './proxy-config.mjs';
|
|
|
3
3
|
let cachedDispatcher = undefined;
|
|
4
4
|
let cachedDispatcherProxyUrl = null;
|
|
5
5
|
|
|
6
|
+
// Enterprise MITM proxies re-sign TLS on BOTH legs: the proxy's own certificate
|
|
7
|
+
// (`proxyTls`) and the tunneled connection to the target server (`requestTls`).
|
|
8
|
+
// Both must be relaxed for such environments. Note `requestTls` is NOT the proxy
|
|
9
|
+
// cert — it is the target-server cert seen through the tunnel. This relaxation is
|
|
10
|
+
// scoped to proxied connections only; direct connections keep strict certificate
|
|
11
|
+
// verification (see the non-proxy branch of `fetchWithProxy`).
|
|
12
|
+
export const PROXY_TLS_OPTIONS = {
|
|
13
|
+
proxyTls: { rejectUnauthorized: false },
|
|
14
|
+
requestTls: { rejectUnauthorized: false },
|
|
15
|
+
};
|
|
16
|
+
|
|
6
17
|
async function importUndici() {
|
|
7
18
|
try {
|
|
8
19
|
/* eslint-disable-next-line n/no-missing-import */
|
|
@@ -21,7 +32,7 @@ export async function getProxyDispatcher(targetUrl) {
|
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
const { ProxyAgent } = await importUndici();
|
|
24
|
-
cachedDispatcher = new ProxyAgent(proxyUrl);
|
|
35
|
+
cachedDispatcher = new ProxyAgent({ uri: proxyUrl, ...PROXY_TLS_OPTIONS });
|
|
25
36
|
cachedDispatcherProxyUrl = proxyUrl;
|
|
26
37
|
return cachedDispatcher;
|
|
27
38
|
}
|
|
@@ -66,11 +66,38 @@ export function redactSecrets(value, policy = DEFAULT_POLICY) {
|
|
|
66
66
|
|
|
67
67
|
function stripExecutable(args) {
|
|
68
68
|
if (!args.length) return [];
|
|
69
|
-
|
|
69
|
+
let current = args;
|
|
70
|
+
// Unwrap shell wrappers (bash -c / sh -c 'hcloud ...', sudo hcloud ...) so
|
|
71
|
+
// wrapped write commands keep their deny classification (#650 D4-16).
|
|
72
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
73
|
+
const first = String(current[0]).toLowerCase();
|
|
74
|
+
const isShell = [
|
|
75
|
+
'bash',
|
|
76
|
+
'sh',
|
|
77
|
+
'zsh',
|
|
78
|
+
'dash',
|
|
79
|
+
'bash.exe',
|
|
80
|
+
'sh.exe',
|
|
81
|
+
'/bin/bash',
|
|
82
|
+
'/bin/sh',
|
|
83
|
+
'/bin/zsh',
|
|
84
|
+
'/bin/dash',
|
|
85
|
+
].includes(first);
|
|
86
|
+
if (isShell && String(current[1]).toLowerCase() === '-c' && current[2]) {
|
|
87
|
+
current = splitSimpleCommand(current[2]);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (first === 'sudo' && current.length > 1) {
|
|
91
|
+
current = current.slice(1);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
const first = String(current[0]).toLowerCase();
|
|
70
97
|
if (first === 'hcloud' || first.endsWith('/hcloud') || first.endsWith('\\hcloud') || first === 'hcloud.exe') {
|
|
71
|
-
return
|
|
98
|
+
return current.slice(1);
|
|
72
99
|
}
|
|
73
|
-
return
|
|
100
|
+
return current;
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
function commandOperation(args) {
|
|
@@ -123,8 +150,44 @@ function applyRawCommandRiskRules(base, command, options = {}) {
|
|
|
123
150
|
return mergeRiskDecision(base, risk);
|
|
124
151
|
}
|
|
125
152
|
|
|
153
|
+
// Find hcloud command segments split by shell operators (; && || |) so a write
|
|
154
|
+
// command in the middle of a concatenated string keeps its deny classification
|
|
155
|
+
// (#650 review edge 1).
|
|
156
|
+
function findHcloudCommandSegments(text) {
|
|
157
|
+
return String(text)
|
|
158
|
+
.split(/(?:\|\||&&|;|\|)/)
|
|
159
|
+
.map((segment) => segment.trim())
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.map((segment) => splitSimpleCommand(segment))
|
|
162
|
+
.filter((tokens) => {
|
|
163
|
+
const first = String(tokens[0] || '').toLowerCase();
|
|
164
|
+
return first === 'hcloud' || first.endsWith('/hcloud') || first.endsWith('\\hcloud') || first === 'hcloud.exe';
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
126
168
|
export function classifyHcloudArgs(args, options = {}) {
|
|
127
169
|
const policy = options.policy || DEFAULT_POLICY;
|
|
170
|
+
// Second pass for shell-wrapped input (#650 D4-16 review edge 1): when the
|
|
171
|
+
// leading command is not hcloud but a concatenated segment contains one,
|
|
172
|
+
// classify every hcloud segment and merge to the most severe decision.
|
|
173
|
+
const unwrappedTokens = stripExecutable(Array.isArray(args) ? args.map(String) : []);
|
|
174
|
+
const unwrappedFirst = String(unwrappedTokens[0] || '').toLowerCase();
|
|
175
|
+
const unwrappedIsHcloud =
|
|
176
|
+
unwrappedFirst === 'hcloud' ||
|
|
177
|
+
unwrappedFirst.endsWith('/hcloud') ||
|
|
178
|
+
unwrappedFirst.endsWith('\\hcloud') ||
|
|
179
|
+
unwrappedFirst === 'hcloud.exe';
|
|
180
|
+
if (!unwrappedIsHcloud && !options._segmentDepth) {
|
|
181
|
+
const hcloudSegments = findHcloudCommandSegments(unwrappedTokens.join(' '));
|
|
182
|
+
if (hcloudSegments.length > 0) {
|
|
183
|
+
const results = hcloudSegments.map((segment) => classifyHcloudArgs(segment, { ...options, _segmentDepth: 1 }));
|
|
184
|
+
return (
|
|
185
|
+
results.find((result) => result.decision === 'deny') ||
|
|
186
|
+
results.find((result) => ['write', 'execution', 'secret', 'credential'].includes(result.risk)) ||
|
|
187
|
+
results[0]
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
128
191
|
const { service, operation, args: normalizedArgs } = commandOperation(args);
|
|
129
192
|
const joined = normalizedArgs.join(' ');
|
|
130
193
|
|
|
@@ -342,6 +405,26 @@ export function classifyTextCommand(command, options = {}) {
|
|
|
342
405
|
};
|
|
343
406
|
}
|
|
344
407
|
|
|
408
|
+
// Credential variable references bypass the env-command gate above: HW_ is
|
|
409
|
+
// the plugin's own documented credential prefix (HW_ACCESS_KEY/HW_SECRET_KEY/
|
|
410
|
+
// HW_SECURITY_TOKEN), and `echo $HW_SECRET_KEY` / `printenv HW_ACCESS_KEY`
|
|
411
|
+
// previously fell through to allow (#650 D4-2).
|
|
412
|
+
//
|
|
413
|
+
// The negative lookbehind exempts literal-NAME references — backslash-escaped
|
|
414
|
+
// (`\$HW_*`) or single-quoted (`'$HW_*'`, which the shell never expands) —
|
|
415
|
+
// while unescaped `$HW_*` is a potential expansion/dump regardless of the
|
|
416
|
+
// command. No command-name whitelist, so no false negative (#650 review).
|
|
417
|
+
if (
|
|
418
|
+
/(?<!['\\])\$\{?(?:HUAWEICLOUD|HWC|HW|OS)_(?:ACCESS_KEY|SECRET_KEY|SECURITY_TOKEN)/i.test(text) ||
|
|
419
|
+
/(?:^|\s)printenv\s+(?:HUAWEICLOUD|HWC|HW|OS)_(?:ACCESS_KEY|SECRET_KEY|SECURITY_TOKEN)/i.test(text)
|
|
420
|
+
) {
|
|
421
|
+
return {
|
|
422
|
+
decision: 'deny',
|
|
423
|
+
risk: 'credential',
|
|
424
|
+
reason: 'Printing cloud credential environment variables is blocked.',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
345
428
|
if (/(^|\s)hcloud(\.exe)?\s+/i.test(text)) {
|
|
346
429
|
return classifyHcloudArgs(splitSimpleCommand(text), { ...options, rawCommand: text });
|
|
347
430
|
}
|
|
@@ -2015,8 +2015,15 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
|
|
|
2015
2015
|
}
|
|
2016
2016
|
if (!suggestions.length) {
|
|
2017
2017
|
suggestions.push(
|
|
2018
|
-
'
|
|
2018
|
+
'No known error-code pattern matched. Re-run the failing command with --debug to capture ' +
|
|
2019
|
+
'X-Request-Id from the response headers, then re-call explain_error with service, operation, ' +
|
|
2020
|
+
'region, the full redacted message, and requestId.',
|
|
2019
2021
|
);
|
|
2022
|
+
if (errorCode) {
|
|
2023
|
+
suggestions.push(
|
|
2024
|
+
`Search the error code "${errorCode}" in the Huawei Cloud error center (support.huaweicloud.com) for the authoritative cause.`,
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2020
2027
|
}
|
|
2021
2028
|
|
|
2022
2029
|
if (requestId) {
|