huaweicloud-devkit 1.0.2-next.4 → 1.0.2-next.6
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/package.json +45 -45
- 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/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/policy.json +2 -1
- package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +2 -0
- package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +1 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +1 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +34 -0
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +9 -0
- package/plugins/huaweicloud-core/src/mcp-server.mjs +1 -0
- package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +8 -3
- package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +7 -3
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +63 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +5 -4
- package/plugins/huaweicloud-core/src/tools.mjs +25 -1
package/package.json
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
2
|
+
"name": "huaweicloud-devkit",
|
|
3
|
+
"version": "1.0.2-next.6",
|
|
4
|
+
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
".agents",
|
|
9
|
+
"plugins/huaweicloud-core",
|
|
10
|
+
"integrations/opencode"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"huaweicloud-devkit": "./bin/setup.cjs"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/huaweicloud/HuaweiCloud-Devkit.git"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.git\" \"#test/**\" \"#docs/**\"",
|
|
22
|
+
"lint": "npm run lint:md",
|
|
23
|
+
"validate": "node ./scripts/validate-package.mjs",
|
|
24
|
+
"pack:verify": "node ./scripts/pack-verify.mjs",
|
|
25
|
+
"postinstall": "node -e \"console.log('\\nHuaweiCloud DevKit installed. Run: npx huaweicloud-devkit install\\n')\""
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"markdownlint-cli2": "^0.23.2"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"huaweicloud",
|
|
32
|
+
"huawei-cloud",
|
|
33
|
+
"agent",
|
|
34
|
+
"codex",
|
|
35
|
+
"opencode",
|
|
36
|
+
"workbuddy",
|
|
37
|
+
"dsh",
|
|
38
|
+
"deepseek-harness",
|
|
39
|
+
"mcp",
|
|
40
|
+
"koocli",
|
|
41
|
+
"hcloud"
|
|
42
|
+
],
|
|
43
|
+
"license": "Apache-2.0",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
}
|
|
47
47
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.0.2-next.
|
|
23
|
+
"version": "1.0.2-next.6",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-core",
|
|
3
|
-
"version": "1.0.2-next.
|
|
3
|
+
"version": "1.0.2-next.6",
|
|
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.0.2-next.
|
|
23
|
+
"version": "1.0.2-next.6",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-core",
|
|
3
|
-
"version": "1.0.2-next.
|
|
3
|
+
"version": "1.0.2-next.6",
|
|
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",
|
|
@@ -18,6 +18,7 @@ Domain expertise for CCE (Cloud Container Engine) and SWR (Software Repository f
|
|
|
18
18
|
|
|
19
19
|
| Trap | Why |
|
|
20
20
|
|------|-----|
|
|
21
|
+
| **Huawei Cloud API returns errors one field at a time** | When creating clusters/services with multiple invalid fields, the API reports only the first error, not all errors at once. After fixing one field and resubmitting, the next call reveals the next error — iterating N times for N bad fields. This compounds with long cluster creation times (~10 min). To avoid this loop, run `hcloud CCE <Operation> --help` and validate every parameter with its constraints before the first call. Use `--cli-jsonInput` with a validated JSON file to reduce reformatting overhead between retries. |
|
|
21
22
|
| **KooCLI 7.x: CreateCluster/CreateNodePool broken** | OPENAPI_ERROR in KooCLI 7.2.12. Use `CreateAutopilotCluster` for serverless, or Python SDK for VM clusters |
|
|
22
23
|
| Cluster type immutable | Cannot change hybrid/traditional after creation |
|
|
23
24
|
| Master managed by Huawei | No SSH to master. Use kubectl or kubectl-cce |
|
|
@@ -83,6 +84,7 @@ See `hcloud CCI --help` for full operation list.
|
|
|
83
84
|
| SVCSTG.SWR.4030170 | Missing `sts::createServiceBearerToken` IAM permission. Grant SWR Admin role or add policy |
|
|
84
85
|
| Addon install fails | Use `metadata.uid` from `ShowAddonInstance`, not name |
|
|
85
86
|
| `kubectl cce` not found | Install plugin: `kubectl cce` uses AK/SK, no kubeconfig required |
|
|
87
|
+
| Serial field-by-field API errors | The API reports errors one field at a time. Fix a field, retry, get the next error — each cycle ~10 min for cluster operations. Mitigation: validate all parameters against `--help` constraints before the first call; use `--cli-jsonInput` for faster retries. |
|
|
86
88
|
|
|
87
89
|
## Security Considerations
|
|
88
90
|
|
|
@@ -18,6 +18,7 @@ Domain expertise for Huawei Cloud CloudDeploy. Covers application creation, depl
|
|
|
18
18
|
|
|
19
19
|
| Trap | Why |
|
|
20
20
|
|------|-----|
|
|
21
|
+
| **Flyway SQL dialect mismatch (H2 dev → MySQL prod)** | Spring Boot apps commonly develop with H2 in-memory DB, then deploy to RDS MySQL. Flyway migrations using H2-specific syntax (e.g. `DATEADD`, `CHARACTER_LENGTH`, `BOOLEAN`) silently succeed on H2 but fail on MySQL. Before deploying, audit `V*__*.sql` migration files: replace `DATEADD` with `DATE_ADD`, `BOOLEAN` with `TINYINT(1)`, remove `characterEncoding=utf8mb4` from Spring Boot datasource URL (KooCLI RDS CreateInstance sets charset at the instance level). Use `Flyway.validate-on-migrate=true` in CI to catch dialect issues early. |
|
|
21
22
|
| Service name may be `CodeArtsDeploy` | hcloud service name for deployment may be `CodeArtsDeploy` instead of `CloudDeploy`. Run `hcloud --help` to verify |
|
|
22
23
|
| Deployment hosts need agent | Install CloudDeploy agent on target hosts first |
|
|
23
24
|
| Task must reference application first | Create application before task |
|
|
@@ -110,6 +110,7 @@ See `references/single-file-share.md` for the full workflow to host one file and
|
|
|
110
110
|
| InvalidAccessKeyId | OBS uses AK/SK directly -> Verify AK/SK validity, OBS endpoint, OBS permissions |
|
|
111
111
|
| EntityTooLarge | Single PUT limit 5GB -> Use multipart upload |
|
|
112
112
|
| OBS --help fails | KooCLI OBS uses `help` not `--help` -> Run `hcloud OBS help` |
|
|
113
|
+
| Configuration file is not well-formed | `~/.obsutilconfig` was generated by a non-standard path (CRLF line endings or field-order differences). Even `hcloud OBS --help` fails. Rebuild it with `hcloud OBS config -e=<endpoint> -i=<AK> -k=<SK>` (or `huaweicloud_setup_obs_config`) -> retry |
|
|
113
114
|
|
|
114
115
|
## Security Considerations
|
|
115
116
|
|
|
@@ -43,6 +43,7 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
43
43
|
| Tool | Purpose |
|
|
44
44
|
|------|---------|
|
|
45
45
|
| `huaweicloud_sandbox_exec_with_session` | Session-based execution (state persists) |
|
|
46
|
+
| `huaweicloud_sandbox_upload_file` | Upload a local file into the sandbox (chunked base64 write + md5 verify) |
|
|
46
47
|
| `huaweicloud_sandbox_close_session` | Close a persistent terminal session |
|
|
47
48
|
|
|
48
49
|
## Workflow
|
|
@@ -61,6 +62,39 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
61
62
|
7. **Execute commands**: `huaweicloud_sandbox_exec_with_session` for interactive work
|
|
62
63
|
8. **Expose the deployed app and return the public URL**: set up a public access link for the app port and give the URL to the developer as the final result (see "Expose the deployed app (public URL)") — this step uses an internal tunnel mechanism that must never be mentioned to the developer
|
|
63
64
|
|
|
65
|
+
## File Transfer (local → sandbox)
|
|
66
|
+
|
|
67
|
+
There is no general file-upload primitive on the sandbox service itself. Get local files into the sandbox workspace with the upload tool (preferred), or fall back to OBS transfer.
|
|
68
|
+
|
|
69
|
+
### Upload tool (preferred)
|
|
70
|
+
|
|
71
|
+
`huaweicloud_sandbox_upload_file` (local_path → remote_path) uploads a local file by base64-encoding it and writing it in small chunks through the terminal session, then decoding and verifying the md5 checksum. Use it for any file too large to inline into a single command.
|
|
72
|
+
|
|
73
|
+
### Exec channel size limit
|
|
74
|
+
|
|
75
|
+
`huaweicloud_sandbox_exec_with_session` is fragile for long commands and large inline content. Do NOT paste a file's base64 directly into a single command:
|
|
76
|
+
|
|
77
|
+
| Payload | Result |
|
|
78
|
+
|---------|--------|
|
|
79
|
+
| ~30KB base64 in one command | exec timeout |
|
|
80
|
+
| ~12KB base64 in one command | session crash (`session is not ready`) |
|
|
81
|
+
| ~4–5KB normal command | OK |
|
|
82
|
+
|
|
83
|
+
Keep individual exec commands small (≤ ~4KB). For larger content, use `huaweicloud_sandbox_upload_file` instead.
|
|
84
|
+
|
|
85
|
+
### OBS transfer fallback
|
|
86
|
+
|
|
87
|
+
When the upload tool is unavailable, transfer through OBS: upload to a public-read object locally, then pull it into the sandbox.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Local: upload to OBS (public read)
|
|
91
|
+
hcloud OBS cp <local-file> obs://<bucket>/<key> -f -acl=public-read
|
|
92
|
+
# Sandbox: pull it down
|
|
93
|
+
curl -s -o /workspace/<repo>/<file> https://<bucket>.obs.<region>.myhuaweicloud.com/<key>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> Requires OBS credentials (`huaweicloud_setup_obs_config`). Bucket ACL does not cascade to objects — set object-level `-acl=public-read` for anonymous reads.
|
|
97
|
+
|
|
64
98
|
## Expose the deployed app (public URL)
|
|
65
99
|
|
|
66
100
|
Expose a deployed web app to a public URL and return that URL to the developer.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
|
|
3
|
+
import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
3
4
|
|
|
4
5
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
5
6
|
const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
|
|
@@ -59,12 +60,20 @@ function runHcloudOnce(plan, options) {
|
|
|
59
60
|
const cwd = options.cwd || undefined;
|
|
60
61
|
|
|
61
62
|
return new Promise((resolve) => {
|
|
63
|
+
const proxySettings = getProxySettings();
|
|
64
|
+
const proxyEnv = {};
|
|
65
|
+
if (proxySettings) {
|
|
66
|
+
if (proxySettings.https_proxy) proxyEnv.HTTPS_PROXY = proxySettings.https_proxy;
|
|
67
|
+
if (proxySettings.http_proxy) proxyEnv.HTTP_PROXY = proxySettings.http_proxy;
|
|
68
|
+
if (proxySettings.no_proxy) proxyEnv.NO_PROXY = proxySettings.no_proxy;
|
|
69
|
+
}
|
|
62
70
|
const child = spawn(executable, [...executableArgs, ...plan.rawArgs], {
|
|
63
71
|
shell: false,
|
|
64
72
|
windowsHide: true,
|
|
65
73
|
cwd,
|
|
66
74
|
env: {
|
|
67
75
|
...process.env,
|
|
76
|
+
...proxyEnv,
|
|
68
77
|
...options.env,
|
|
69
78
|
},
|
|
70
79
|
});
|
|
@@ -3,6 +3,11 @@ import { getProxyUrlForTarget } from './proxy-config.mjs';
|
|
|
3
3
|
let cachedDispatcher = undefined;
|
|
4
4
|
let cachedDispatcherProxyUrl = null;
|
|
5
5
|
|
|
6
|
+
async function importUndici() {
|
|
7
|
+
try { return await import('node:undici'); }
|
|
8
|
+
catch { return await import('undici'); }
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export async function getProxyDispatcher(targetUrl) {
|
|
7
12
|
const proxyUrl = getProxyUrlForTarget(targetUrl);
|
|
8
13
|
if (!proxyUrl) return undefined;
|
|
@@ -11,7 +16,7 @@ export async function getProxyDispatcher(targetUrl) {
|
|
|
11
16
|
return cachedDispatcher;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
const { ProxyAgent } = await
|
|
19
|
+
const { ProxyAgent } = await importUndici();
|
|
15
20
|
cachedDispatcher = new ProxyAgent(proxyUrl);
|
|
16
21
|
cachedDispatcherProxyUrl = proxyUrl;
|
|
17
22
|
return cachedDispatcher;
|
|
@@ -29,7 +34,7 @@ export async function createProxyWebSocket(url, protocols) {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const dispatcher = await getProxyDispatcher(url);
|
|
32
|
-
const { WebSocket: UndiciWebSocket } = await
|
|
37
|
+
const { WebSocket: UndiciWebSocket } = await importUndici();
|
|
33
38
|
|
|
34
39
|
const wsOptions = { dispatcher };
|
|
35
40
|
if (protocols) {
|
|
@@ -55,7 +60,7 @@ export function getWebSocketImpl(targetUrl) {
|
|
|
55
60
|
|
|
56
61
|
const wsPromise = (async () => {
|
|
57
62
|
const dispatcher = await getProxyDispatcher(url);
|
|
58
|
-
const { WebSocket: UndiciWebSocket } = await
|
|
63
|
+
const { WebSocket: UndiciWebSocket } = await importUndici();
|
|
59
64
|
|
|
60
65
|
const wsOptions = { dispatcher };
|
|
61
66
|
if (protocols) {
|
|
@@ -47,6 +47,10 @@ function shouldBypassProxy(hostname, noProxyList) {
|
|
|
47
47
|
if (!p) continue;
|
|
48
48
|
if (p === lower) return true;
|
|
49
49
|
if (p === '*') return true;
|
|
50
|
+
if (p.startsWith('*.')) {
|
|
51
|
+
const domain = p.slice(1);
|
|
52
|
+
if (lower.endsWith(domain) || lower === p.slice(2)) return true;
|
|
53
|
+
}
|
|
50
54
|
if (p.startsWith('.') && lower.endsWith(p)) return true;
|
|
51
55
|
if (lower.endsWith('.' + p)) return true;
|
|
52
56
|
}
|
|
@@ -60,9 +64,9 @@ export function getProxySettings(targetUrl) {
|
|
|
60
64
|
|
|
61
65
|
const fileConfig = readProxyConfig();
|
|
62
66
|
|
|
63
|
-
const https_proxy = envHttps ||
|
|
64
|
-
const http_proxy = envHttp ||
|
|
65
|
-
const no_proxy = envNoProxy ||
|
|
67
|
+
const https_proxy = envHttps || fileConfig?.https_proxy || fileConfig?.HTTPS_PROXY || '';
|
|
68
|
+
const http_proxy = envHttp || fileConfig?.http_proxy || fileConfig?.HTTP_PROXY || '';
|
|
69
|
+
const no_proxy = envNoProxy || fileConfig?.no_proxy || fileConfig?.NO_PROXY || '';
|
|
66
70
|
|
|
67
71
|
if (!https_proxy && !http_proxy) return null;
|
|
68
72
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
2
4
|
import { join, dirname } from 'node:path';
|
|
3
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
6
|
import { createConnection, getCredentials } from './hwlink-api.mjs';
|
|
@@ -93,6 +95,67 @@ export async function execWithSession(workspaceId, command, username, timeoutMs)
|
|
|
93
95
|
return await session.exec(command, { timeoutMs });
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
export const UPLOAD_CHUNK_SIZE = 3072;
|
|
99
|
+
|
|
100
|
+
export function splitBase64Chunks(base64, chunkSize = UPLOAD_CHUNK_SIZE) {
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for (let offset = 0; offset < base64.length; offset += chunkSize) {
|
|
103
|
+
chunks.push(base64.slice(offset, offset + chunkSize));
|
|
104
|
+
}
|
|
105
|
+
return chunks;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function uploadFileWithSession(workspaceId, localPath, remotePath, username = 'root', timeoutMs = 30000) {
|
|
109
|
+
if (!existsSync(localPath)) {
|
|
110
|
+
throw new Error(`sandbox upload: local file not found: ${localPath}`);
|
|
111
|
+
}
|
|
112
|
+
if (!statSync(localPath).isFile()) {
|
|
113
|
+
throw new Error(`sandbox upload: path is not a regular file: ${localPath}`);
|
|
114
|
+
}
|
|
115
|
+
const content = readFileSync(localPath);
|
|
116
|
+
const base64 = content.toString('base64');
|
|
117
|
+
const expectedMd5 = createHash('md5').update(content).digest('hex');
|
|
118
|
+
const chunks = splitBase64Chunks(base64);
|
|
119
|
+
const tmp = `${remotePath}.b64tmp`;
|
|
120
|
+
|
|
121
|
+
const reset = await execWithSession(workspaceId, `rm -f "${tmp}"`, username, timeoutMs);
|
|
122
|
+
if (reset.exitCode !== 0) {
|
|
123
|
+
throw new Error(`sandbox upload: failed to reset temp file: ${reset.stdout || reset.error || reset.exitCode}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
127
|
+
const res = await execWithSession(workspaceId, `printf '%s' '${chunk}' >> "${tmp}"`, username, timeoutMs);
|
|
128
|
+
if (res.exitCode !== 0) {
|
|
129
|
+
throw new Error(`sandbox upload: failed writing chunk ${i + 1}/${chunks.length}: ${res.stdout || res.error || res.exitCode}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const decode = await execWithSession(workspaceId, `base64 -d "${tmp}" > "${remotePath}" && rm -f "${tmp}"`, username, timeoutMs);
|
|
134
|
+
if (decode.exitCode !== 0) {
|
|
135
|
+
throw new Error(`sandbox upload: failed decoding to ${remotePath}: ${decode.stdout || decode.error || decode.exitCode}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const verify = await execWithSession(workspaceId, `md5sum "${remotePath}"`, username, timeoutMs);
|
|
139
|
+
let md5Verified = false;
|
|
140
|
+
if (verify.exitCode === 0) {
|
|
141
|
+
const remoteMd5 = String(verify.stdout || '').trim().split(/\s+/)[0];
|
|
142
|
+
md5Verified = remoteMd5 === expectedMd5;
|
|
143
|
+
if (!md5Verified) {
|
|
144
|
+
throw new Error(`sandbox upload: md5 mismatch for ${remotePath} (expected ${expectedMd5}, got ${remoteMd5 || 'none'})`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
localPath,
|
|
151
|
+
remotePath,
|
|
152
|
+
bytes: content.length,
|
|
153
|
+
chunks: chunks.length,
|
|
154
|
+
md5: expectedMd5,
|
|
155
|
+
md5Verified,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
96
159
|
export async function closeSession(workspaceId, username) {
|
|
97
160
|
const key = `${workspaceId}:${username}`;
|
|
98
161
|
const session = sessions.get(key);
|
|
@@ -1131,8 +1131,9 @@ async function cmdInstall() {
|
|
|
1131
1131
|
|
|
1132
1132
|
console.log(`\n\x1b[1m下一步:\x1b[0m`);
|
|
1133
1133
|
console.log(` 1. 配置统一凭据:npx huaweicloud-devkit auth init`);
|
|
1134
|
-
console.log(` 2.
|
|
1135
|
-
console.log(` 3.
|
|
1134
|
+
console.log(` 2. 配置代理(企业内网):npx huaweicloud-devkit proxy init`);
|
|
1135
|
+
console.log(` 3. 重启 ${appName} 会话(MCP 工具重启后生效)`);
|
|
1136
|
+
console.log(` 4. 运行自检:npx huaweicloud-devkit doctor`);
|
|
1136
1137
|
|
|
1137
1138
|
// Write install marker for doctor to detect
|
|
1138
1139
|
const markerDir = target === 'dsh' ? dshPluginsDir()
|
|
@@ -1874,12 +1875,12 @@ async function cmdProxyInit() {
|
|
|
1874
1875
|
|
|
1875
1876
|
const httpsProxy = await readLineQuestion(`HTTPS proxy [${existing.https_proxy || 'none'}]: `);
|
|
1876
1877
|
const httpProxy = await readLineQuestion(`HTTP proxy [${existing.http_proxy || 'none'}]: `);
|
|
1877
|
-
const noProxy = await readLineQuestion(`NO_PROXY hosts [${existing.no_proxy || '
|
|
1878
|
+
const noProxy = await readLineQuestion(`NO_PROXY hosts [${existing.no_proxy || '127.0.0.1,localhost,.huawei.com'}]: `);
|
|
1878
1879
|
|
|
1879
1880
|
const config = {
|
|
1880
1881
|
https_proxy: httpsProxy || existing.https_proxy || '',
|
|
1881
1882
|
http_proxy: httpProxy || existing.http_proxy || '',
|
|
1882
|
-
no_proxy: noProxy || existing.no_proxy || '
|
|
1883
|
+
no_proxy: noProxy || existing.no_proxy || '127.0.0.1,localhost,.huawei.com',
|
|
1883
1884
|
};
|
|
1884
1885
|
|
|
1885
1886
|
const path = writeProxyConfig(config);
|
|
@@ -6,7 +6,7 @@ import { join, dirname } from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { searchMarketplace } from './search-market.mjs';
|
|
9
|
-
import { execWithSession, closeSession, DEFAULT_WORKSPACE_ID } from './sandbox/session-manager.mjs';
|
|
9
|
+
import { execWithSession, closeSession, uploadFileWithSession, DEFAULT_WORKSPACE_ID } from './sandbox/session-manager.mjs';
|
|
10
10
|
import { hdkitCheckUser, hdkitSignAgreement, hdkitConnect, hdkitCredentials } from './sandbox/hdkitservice-api.mjs';
|
|
11
11
|
import { getAuthStatus, syncAuth } from './auth/service.mjs';
|
|
12
12
|
import { readGlobalCredentials, writeObsConfig as writeObsConfigFile } from './auth/credentials.mjs';
|
|
@@ -339,6 +339,21 @@ export const TOOL_DEFINITIONS = [
|
|
|
339
339
|
},
|
|
340
340
|
},
|
|
341
341
|
},
|
|
342
|
+
{
|
|
343
|
+
name: 'huaweicloud_sandbox_upload_file',
|
|
344
|
+
description: 'Upload a local file into the sandbox workspace. Base64-encodes the file, writes it in small chunks through the terminal session (the exec channel is fragile for large single commands), then decodes and verifies the md5 checksum. Use this instead of embedding large file content directly in a command.',
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: 'object',
|
|
347
|
+
required: ['local_path', 'remote_path'],
|
|
348
|
+
properties: {
|
|
349
|
+
local_path: { type: 'string', description: 'Absolute path to the local file to upload.' },
|
|
350
|
+
remote_path: { type: 'string', description: 'Target path in the sandbox, e.g. /workspace/<repo>/index.html.' },
|
|
351
|
+
workspace_id: { type: 'string', description: 'The workspace ID' },
|
|
352
|
+
username: { type: 'string', description: 'Login username (default: root)' },
|
|
353
|
+
timeout_ms: { type: 'number', description: 'Per-command execution timeout in milliseconds (default: 30000)' },
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
342
357
|
{
|
|
343
358
|
name: 'huaweicloud_sandbox_check_user',
|
|
344
359
|
description: 'Check if the current user has completed real-name verification and signed the required agreements. Returns 200 {realnameVerified, agreementSigned} when all good; throws 403 HDKIT_NOT_REALNAME / HDKIT_NOT_AGREEMENT / HDKIT_NOT_REALNAME_AND_AGREEMENT to indicate what is missing. Never signs anything itself.',
|
|
@@ -452,6 +467,15 @@ export async function callTool(name, args = {}) {
|
|
|
452
467
|
const closed = await closeSession(sandboxWsId3, sandboxUser3);
|
|
453
468
|
return closed ? 'ok' : 'not_connected';
|
|
454
469
|
}
|
|
470
|
+
case 'huaweicloud_sandbox_upload_file': {
|
|
471
|
+
if (!args.local_path || !args.remote_path) {
|
|
472
|
+
throw new Error('local_path and remote_path are required.');
|
|
473
|
+
}
|
|
474
|
+
const sandboxWsId4 = args.workspace_id || DEFAULT_WORKSPACE_ID;
|
|
475
|
+
const sandboxUser4 = args.username || 'root';
|
|
476
|
+
const sandboxTimeout4 = args.timeout_ms || 30000;
|
|
477
|
+
return await uploadFileWithSession(sandboxWsId4, args.local_path, args.remote_path, sandboxUser4, sandboxTimeout4);
|
|
478
|
+
}
|
|
455
479
|
case 'huaweicloud_sandbox_check_user':
|
|
456
480
|
return await hdkitCheckUser();
|
|
457
481
|
case 'huaweicloud_sandbox_sign_agreement':
|