huaweicloud-devkit 1.0.2-dev.0 → 1.0.2-dev.2
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 +1 -1
- package/plugins/huaweicloud-core/.mcp.json +3 -1
- package/plugins/huaweicloud-core/safety/policy.json +16 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +21 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +8 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +40 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +69 -0
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +74 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +63 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +97 -0
- package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -0
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +230 -27
- package/plugins/huaweicloud-core/src/tools.mjs +187 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -0
- package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -0
- package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.0.2-dev.
|
|
3
|
+
"version": "1.0.2-dev.2",
|
|
4
4
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -82,5 +82,20 @@
|
|
|
82
82
|
"ls",
|
|
83
83
|
"dir",
|
|
84
84
|
"Get-ChildItem"
|
|
85
|
-
]
|
|
85
|
+
],
|
|
86
|
+
"blockedSandboxCommands": [
|
|
87
|
+
"rm\\s+-rf\\s+/",
|
|
88
|
+
"mkfs",
|
|
89
|
+
"dd\\s+if=",
|
|
90
|
+
":\\(\\)\\{:\\|:&\\};:",
|
|
91
|
+
"shutdown",
|
|
92
|
+
"reboot",
|
|
93
|
+
"init\\s+[06]",
|
|
94
|
+
"fdisk"
|
|
95
|
+
],
|
|
96
|
+
"sandboxWriteTools": [
|
|
97
|
+
"huaweicloud_sandbox_connect",
|
|
98
|
+
"huaweicloud_sandbox_credentials",
|
|
99
|
+
"huaweicloud_sandbox_release"
|
|
100
|
+
]
|
|
86
101
|
}
|
|
@@ -173,6 +173,27 @@
|
|
|
173
173
|
},
|
|
174
174
|
"message": "The generated plan may create high-cost or unbounded capacity.",
|
|
175
175
|
"remediation": "Use small preview defaults, explicit quotas, budget labels, and user approval before creating high-cost resources."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"id": "hwc-sandbox-destructive-command",
|
|
179
|
+
"title": "Destructive command in sandbox terminal",
|
|
180
|
+
"category": "destructive",
|
|
181
|
+
"severity": "deny",
|
|
182
|
+
"stages": ["command"],
|
|
183
|
+
"match": {
|
|
184
|
+
"any": [
|
|
185
|
+
{ "field": "text", "regex": "rm\\s+-rf\\s+/" },
|
|
186
|
+
{ "field": "text", "regex": "mkfs" },
|
|
187
|
+
{ "field": "text", "regex": "dd\\s+if=" },
|
|
188
|
+
{ "field": "text", "regex": ":\\\\(\\\\)\\\\{[^}]*\\\\|[^}]*&[^}]*\\\\}[^;]*;:" },
|
|
189
|
+
{ "field": "text", "regex": "\\bshutdown\\b" },
|
|
190
|
+
{ "field": "text", "regex": "\\breboot\\b" },
|
|
191
|
+
{ "field": "text", "regex": "\\binit\\s+[06]\\b" },
|
|
192
|
+
{ "field": "text", "regex": "\\bfdisk\\b" }
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
"message": "The sandbox terminal command is destructively dangerous and could destroy the workspace.",
|
|
196
|
+
"remediation": "Use targeted deletions instead of recursive root removal. Avoid disk format, fork bombs, and system shutdown commands in sandbox terminals."
|
|
176
197
|
}
|
|
177
198
|
]
|
|
178
199
|
}
|
|
@@ -79,6 +79,13 @@ Build → Create bucket → Upload → Set bucket ACL → Set object ACL → Con
|
|
|
79
79
|
|
|
80
80
|
> KooCLI OBS does NOT support `SetBucketWebsite`. Configure static website hosting via REST API (`PUT /?website`) or the Huawei Cloud console.
|
|
81
81
|
|
|
82
|
+
## Single-File Quick Share
|
|
83
|
+
|
|
84
|
+
See `references/single-file-share.md` for the full workflow to host one file and get a shareable link in seconds:
|
|
85
|
+
|
|
86
|
+
- **Private, time-limited**: `hcloud OBS sign obs://<bucket>/<key> -e=<seconds>` (max 7 days)
|
|
87
|
+
- **Public, permanent**: `hcloud OBS cp <file> obs://<bucket>/<key> -f` + `hcloud OBS chattri obs://<bucket>/<key> -acl=public-read`, then share `https://<bucket>.obs.<region>.myhuaweicloud.com/<key>`
|
|
88
|
+
|
|
82
89
|
## Storage Classes
|
|
83
90
|
|
|
84
91
|
| Class | Use Case | Min Storage | Retrieval Fee |
|
|
@@ -115,5 +122,6 @@ Build → Create bucket → Upload → Set bucket ACL → Set object ACL → Con
|
|
|
115
122
|
|
|
116
123
|
- OBS Docs: https://support.huaweicloud.com/obs/
|
|
117
124
|
- Static website: references/static-website.md
|
|
125
|
+
- Single-file share: references/single-file-share.md
|
|
118
126
|
- Lifecycle: references/bucket-lifecycle.md
|
|
119
127
|
- Replication: references/replication.md
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# OBS Single-File Quick Share
|
|
2
|
+
|
|
3
|
+
Host one file and get a shareable link in seconds. Two options:
|
|
4
|
+
|
|
5
|
+
| Option | Link type | Bucket/object visibility | Expiry |
|
|
6
|
+
|--------|-----------|--------------------------|--------|
|
|
7
|
+
| Presigned URL | Time-limited private link | Keep private | `-e=<seconds>`, max 7 days |
|
|
8
|
+
| Public URL | Permanent public link | Set object `-acl=public-read` | None |
|
|
9
|
+
|
|
10
|
+
## Option 1: Presigned URL (private, time-limited)
|
|
11
|
+
|
|
12
|
+
No ACL change needed. The object stays private; the link grants temporary access.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
hcloud OBS cp <file> obs://<bucket>/<key> -f
|
|
16
|
+
hcloud OBS sign obs://<bucket>/<key> -e=3600 # valid 1 hour, max 604800 (7 days)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `sign` command prints the full presigned URL directly — share it as-is.
|
|
20
|
+
|
|
21
|
+
## Option 2: Public URL (permanent, public-read)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
hcloud OBS cp <file> obs://<bucket>/<key> -f
|
|
25
|
+
hcloud OBS chattri obs://<bucket>/<key> -acl=public-read
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The shareable public URL follows the OBS endpoint format (NOT the `obs-website` static-site endpoint):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
https://<bucket>.obs.<region>.myhuaweicloud.com/<key>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Key Gotchas
|
|
35
|
+
|
|
36
|
+
- **Bucket ACL does NOT cascade**: `chattri obs://<bucket> -acl=public-read` alone does not make objects public. Set the object-level ACL explicitly.
|
|
37
|
+
- **`-f` is mandatory**: `cp` without `-f` prompts "Please input (y/n)" on overwrite → agent hangs (TIMEOUT).
|
|
38
|
+
- **Public-read means anyone with the link can access**: use presigned URLs when the file is sensitive.
|
|
39
|
+
- **Presigned URL max expiry is 7 days** (`-e=604800`). For longer-lived public access, use the public-read option.
|
|
40
|
+
- **Credential setup**: OBS uses a separate config (`~/.obsutilconfig`). Call `huaweicloud_setup_obs_config` before first use.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: huawei-sandbox
|
|
3
|
+
description: "Use when creating, connecting, or managing Huawei Cloud Sandbox instances and workspace terminals. Covers sandbox lifecycle (check-user, sign-agreement, connect, release), terminal execution (one-shot and session-based), and credential injection. Triggers on: sandbox, workspace, terminal, hwlink, devstation, hdkitservice, remote exec. NOT for: ECS instances (use huawei-ecs), CCE clusters (use huawei-cce)."
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Huawei Cloud Sandbox
|
|
8
|
+
|
|
9
|
+
**STOP - Do not answer from general knowledge.** Follow the procedure below.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## MCP Tools
|
|
16
|
+
|
|
17
|
+
### User Verification (Prerequisites)
|
|
18
|
+
|
|
19
|
+
| Tool | Purpose |
|
|
20
|
+
|------|---------|
|
|
21
|
+
| `huaweicloud_sandbox_check_user` | Check real-name verification and agreement signing status |
|
|
22
|
+
| `huaweicloud_sandbox_sign_agreement` | Sign unsigned/outdated agreements (required before connect) |
|
|
23
|
+
|
|
24
|
+
### Sandbox Lifecycle
|
|
25
|
+
|
|
26
|
+
| Tool | Purpose |
|
|
27
|
+
|------|---------|
|
|
28
|
+
| `huaweicloud_sandbox_connect` | Connect to sandbox (one user one instance, reuses existing if available) |
|
|
29
|
+
| `huaweicloud_sandbox_credentials` | Inject temporary AK/SK into a running sandbox |
|
|
30
|
+
| `huaweicloud_sandbox_release` | Shut down and delete a sandbox (idempotent) |
|
|
31
|
+
|
|
32
|
+
### Terminal Execution
|
|
33
|
+
|
|
34
|
+
| Tool | Purpose |
|
|
35
|
+
|------|---------|
|
|
36
|
+
| `huaweicloud_sandbox_exec` | One-shot command execution (no session reuse) |
|
|
37
|
+
| `huaweicloud_sandbox_exec_with_session` | Session-based execution (state persists) |
|
|
38
|
+
| `huaweicloud_sandbox_close_session` | Close a persistent terminal session |
|
|
39
|
+
|
|
40
|
+
## Workflow
|
|
41
|
+
|
|
42
|
+
1. **Check user**: `huaweicloud_sandbox_check_user` — verify `realname_verified` and `agreement_signed`
|
|
43
|
+
2. **Sign agreement** (if needed): `huaweicloud_sandbox_sign_agreement` — when `agreement_signed=false`
|
|
44
|
+
3. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`
|
|
45
|
+
4. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox
|
|
46
|
+
5. **Execute commands**: `huaweicloud_sandbox_exec_with_session` for interactive work, `huaweicloud_sandbox_exec` for one-shot
|
|
47
|
+
6. **Release**: `huaweicloud_sandbox_release` — cleans up sandbox and session
|
|
48
|
+
|
|
49
|
+
## Critical Warnings
|
|
50
|
+
|
|
51
|
+
| Trap | Why |
|
|
52
|
+
|------|-----|
|
|
53
|
+
| Agreement required first | `sandbox_connect` fails if user hasn't signed agreements; run `sandbox_check_user` first |
|
|
54
|
+
| Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
|
|
55
|
+
| No session reuse in `exec` | Each `exec` call creates a new connection; previous state is lost |
|
|
56
|
+
| Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
|
|
57
|
+
| Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
|
|
58
|
+
| Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket) |
|
|
59
|
+
|
|
60
|
+
## Environment Variables
|
|
61
|
+
|
|
62
|
+
| Variable | Required | Description |
|
|
63
|
+
|----------|----------|-------------|
|
|
64
|
+
| `HW_ACCESS_KEY` | Yes | Huawei Cloud AK |
|
|
65
|
+
| `HW_SECRET_KEY` | Yes | Huawei Cloud SK |
|
|
66
|
+
| `HW_SECURITY_TOKEN` | No | STS security token |
|
|
67
|
+
| `HW_WORKSPACE_ID` | No | Default workspace ID |
|
|
68
|
+
| `HDKITSERVICE_ENDPOINT` | No | hdkitservice API endpoint |
|
|
69
|
+
| `HWLINK_ENDPOINT` | No | DevStation API endpoint |
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export const SUPPORTED_AGENT_TARGETS = ['opencode', 'codex', 'codex-desktop', 'codearts', 'workbuddy'];
|
|
7
|
+
|
|
8
|
+
function baseHome() {
|
|
9
|
+
return process.env.HUAWEICLOUD_HOME || homedir();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function opencodeConfigFile() {
|
|
13
|
+
const jsonc = join(baseHome(), '.config', 'opencode', 'opencode.jsonc');
|
|
14
|
+
if (existsSync(jsonc)) return jsonc;
|
|
15
|
+
return join(baseHome(), '.config', 'opencode', 'opencode.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJsonSafe(path) {
|
|
19
|
+
if (!existsSync(path)) return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function opencodeRegistered() {
|
|
28
|
+
const path = opencodeConfigFile();
|
|
29
|
+
const cfg = readJsonSafe(path);
|
|
30
|
+
return Boolean(cfg?.mcp?.['huaweicloud-devkit']);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function codexDesktopRegistered() {
|
|
34
|
+
const path = join(baseHome(), '.codex', 'config.toml');
|
|
35
|
+
if (!existsSync(path)) return false;
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(path, 'utf8').includes('[mcp_servers.huaweicloud-devkit]');
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function codexCliRegistered() {
|
|
44
|
+
try {
|
|
45
|
+
const r = spawnSync('codex', ['plugin', 'list'], {
|
|
46
|
+
shell: false,
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
stdio: 'pipe',
|
|
49
|
+
timeout: 10000,
|
|
50
|
+
});
|
|
51
|
+
const out = `${r.stdout || ''}${r.stderr || ''}`;
|
|
52
|
+
return out.includes('huaweicloud-core');
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function codeartsRegistered() {
|
|
59
|
+
const paths = [
|
|
60
|
+
join(baseHome(), '.codeartsdoer', 'mcp', 'mcp_settings.json'),
|
|
61
|
+
join(process.cwd(), '.codeartsdoer', 'mcp', 'mcp_settings.json'),
|
|
62
|
+
];
|
|
63
|
+
return paths.some((path) => {
|
|
64
|
+
const cfg = readJsonSafe(path);
|
|
65
|
+
return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function workbuddyRegistered() {
|
|
70
|
+
const cfg = readJsonSafe(join(baseHome(), '.workbuddy', 'mcp.json'));
|
|
71
|
+
return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getAgentRegistrationStatuses(target = 'all') {
|
|
75
|
+
const requested = target === 'all' ? SUPPORTED_AGENT_TARGETS : [target];
|
|
76
|
+
const result = { target, agents: {} };
|
|
77
|
+
for (const agent of requested) {
|
|
78
|
+
let configured = false;
|
|
79
|
+
if (agent === 'opencode') configured = opencodeRegistered();
|
|
80
|
+
if (agent === 'codex-desktop') configured = codexDesktopRegistered();
|
|
81
|
+
if (agent === 'codex') configured = codexCliRegistered();
|
|
82
|
+
if (agent === 'codearts') configured = codeartsRegistered();
|
|
83
|
+
if (agent === 'workbuddy') configured = workbuddyRegistered();
|
|
84
|
+
result.agents[agent] = { configured };
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
function baseHome() {
|
|
6
|
+
return process.env.HUAWEICLOUD_HOME || homedir();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function globalCredentialsPath() {
|
|
10
|
+
return join(baseHome(), '.config', 'huaweicloud', 'credentials.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function obsConfigPath() {
|
|
14
|
+
return join(baseHome(), '.obsutilconfig');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readGlobalCredentials() {
|
|
18
|
+
const path = globalCredentialsPath();
|
|
19
|
+
if (!existsSync(path)) return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writeGlobalCredentials(credentials = {}) {
|
|
28
|
+
const path = globalCredentialsPath();
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
const payload = {
|
|
31
|
+
ak: String(credentials.ak || ''),
|
|
32
|
+
sk: String(credentials.sk || ''),
|
|
33
|
+
securityToken: String(credentials.securityToken || ''),
|
|
34
|
+
region: String(credentials.region || ''),
|
|
35
|
+
};
|
|
36
|
+
writeFileSync(path, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function writeObsConfig(credentials = {}) {
|
|
41
|
+
const region = String(credentials.region || '');
|
|
42
|
+
const ak = String(credentials.ak || '');
|
|
43
|
+
const sk = String(credentials.sk || '');
|
|
44
|
+
if (!region || !ak || !sk) {
|
|
45
|
+
throw new Error('region, ak, and sk are required to write OBS config');
|
|
46
|
+
}
|
|
47
|
+
const path = obsConfigPath();
|
|
48
|
+
const endpoint = credentials.endpoint || `https://obs.${region}.myhuaweicloud.com`;
|
|
49
|
+
const content = `[default]\r\nendpoint=${endpoint}\r\nak=${ak}\r\nsk=${sk}\r\n`;
|
|
50
|
+
writeFileSync(path, content, { encoding: 'utf8', mode: 0o600 });
|
|
51
|
+
return { path, endpoint };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveCredentials(options = {}) {
|
|
55
|
+
let ak = process.env.HW_ACCESS_KEY;
|
|
56
|
+
let sk = process.env.HW_SECRET_KEY;
|
|
57
|
+
let securityToken = process.env.HW_SECURITY_TOKEN;
|
|
58
|
+
let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
|
|
59
|
+
|
|
60
|
+
const stored = readGlobalCredentials();
|
|
61
|
+
if (stored) {
|
|
62
|
+
if (!ak && stored.ak) ak = stored.ak;
|
|
63
|
+
if (!sk && stored.sk) sk = stored.sk;
|
|
64
|
+
if (!securityToken && stored.securityToken) securityToken = stored.securityToken;
|
|
65
|
+
if (!region && stored.region) region = stored.region;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!ak || !sk) {
|
|
69
|
+
if (options.allowMissing) return null;
|
|
70
|
+
throw new Error('Huawei Cloud credentials are not configured. Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { ak, sk, securityToken, region };
|
|
74
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { getAgentRegistrationStatuses } from './agent-registration.mjs';
|
|
4
|
+
import { globalCredentialsPath, obsConfigPath, readGlobalCredentials, writeObsConfig } from './credentials.mjs';
|
|
5
|
+
|
|
6
|
+
function hcloudInstalled() {
|
|
7
|
+
const bin = process.env.HCLOUD_BIN || 'hcloud';
|
|
8
|
+
try {
|
|
9
|
+
const r = spawnSync(`"${bin}" version`, [], {
|
|
10
|
+
shell: true,
|
|
11
|
+
windowsHide: true,
|
|
12
|
+
stdio: 'pipe',
|
|
13
|
+
timeout: 5000,
|
|
14
|
+
});
|
|
15
|
+
const out = `${r.stdout || ''}${r.stderr || ''}`;
|
|
16
|
+
return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getAuthStatus(target = 'all') {
|
|
23
|
+
const credentials = readGlobalCredentials();
|
|
24
|
+
return {
|
|
25
|
+
target,
|
|
26
|
+
credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
|
|
27
|
+
credentialsPath: globalCredentialsPath(),
|
|
28
|
+
obsConfigured: existsSync(obsConfigPath()),
|
|
29
|
+
obsConfigPath: obsConfigPath(),
|
|
30
|
+
kooCliInstalled: hcloudInstalled(),
|
|
31
|
+
agents: getAgentRegistrationStatuses(target).agents,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function syncAuth(target = 'all') {
|
|
36
|
+
const credentials = readGlobalCredentials();
|
|
37
|
+
if (!credentials?.ak || !credentials?.sk) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'Global credentials are not configured.',
|
|
41
|
+
nextStep: 'Run "npx huaweicloud-devkit auth init" first.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let obs = null;
|
|
46
|
+
try {
|
|
47
|
+
obs = writeObsConfig(credentials);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: error.message,
|
|
52
|
+
nextStep: 'Run "npx huaweicloud-devkit auth init" to refresh credentials and region.',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
59
|
+
credentialsConfigured: true,
|
|
60
|
+
agents: getAgentRegistrationStatuses(target).agents,
|
|
61
|
+
note: 'OBS credentials were synced from the global credential vault. Agent MCP registration is managed by "npx huaweicloud-devkit install --target <agent>".',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getCredentials } from './hwlink-api.mjs';
|
|
2
|
+
|
|
3
|
+
const HDKIT_BASE_URL =
|
|
4
|
+
process.env.HDKITSERVICE_ENDPOINT ||
|
|
5
|
+
'http://113.44.143.91:3002/rest/developer/server/hdkitservice/';
|
|
6
|
+
|
|
7
|
+
async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
8
|
+
const { ak, sk, securitytoken } = getCredentials();
|
|
9
|
+
|
|
10
|
+
const headers = {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
'X-HW-AK': ak,
|
|
13
|
+
'X-HW-SK': sk,
|
|
14
|
+
};
|
|
15
|
+
if (securitytoken) {
|
|
16
|
+
headers['X-HW-Security-Token'] = securitytoken;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const url = `${HDKIT_BASE_URL}${path}`;
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
+
|
|
23
|
+
let resp;
|
|
24
|
+
try {
|
|
25
|
+
resp = await fetch(url, {
|
|
26
|
+
method,
|
|
27
|
+
headers,
|
|
28
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const text = await resp.text();
|
|
36
|
+
let data;
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(text);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`hdkitservice returned non-JSON (status ${resp.status}): ${text.slice(0, 200)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!resp.ok) {
|
|
44
|
+
const err = new Error(
|
|
45
|
+
data.message || `hdkitservice error: ${data.code || resp.status}`
|
|
46
|
+
);
|
|
47
|
+
err.code = data.code;
|
|
48
|
+
err.status = resp.status;
|
|
49
|
+
err.traceId = data.trace_id;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function hdkitCheckUser() {
|
|
57
|
+
return await hdkitRequest('GET', 'check-user', undefined, 30000);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function hdkitSignAgreement() {
|
|
61
|
+
return await hdkitRequest('POST', 'sign-agreement', {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function hdkitConnect(options = {}) {
|
|
65
|
+
const body = {};
|
|
66
|
+
if (options.source) body.source = options.source;
|
|
67
|
+
if (options.env) body.env = options.env;
|
|
68
|
+
if (options.git) body.git = options.git;
|
|
69
|
+
if (options.template_id) body.template_id = options.template_id;
|
|
70
|
+
if (options.flavor_id) body.flavor_id = options.flavor_id;
|
|
71
|
+
|
|
72
|
+
return await hdkitRequest('POST', 'connect', body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function hdkitCredentials(sessionId, devStageId, enableSts = true) {
|
|
76
|
+
const body = { enable_sts: enableSts };
|
|
77
|
+
if (sessionId) body.session_id = sessionId;
|
|
78
|
+
if (devStageId) body.dev_stage_id = devStageId;
|
|
79
|
+
|
|
80
|
+
if (!sessionId && !devStageId) {
|
|
81
|
+
throw new Error('session_id or dev_stage_id is required');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return await hdkitRequest('POST', 'credentials', body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function hdkitRelease(sessionId, devStageId) {
|
|
88
|
+
const body = {};
|
|
89
|
+
if (sessionId) body.session_id = sessionId;
|
|
90
|
+
if (devStageId) body.dev_stage_id = devStageId;
|
|
91
|
+
|
|
92
|
+
if (!sessionId && !devStageId) {
|
|
93
|
+
throw new Error('session_id or dev_stage_id is required');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return await hdkitRequest('POST', 'release', body);
|
|
97
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { resolveCredentials } from '../auth/credentials.mjs';
|
|
3
|
+
|
|
4
|
+
const BASE_URL = process.env.HWLINK_ENDPOINT || 'https://devstation.myhuaweicloud.com';
|
|
5
|
+
|
|
6
|
+
function sha256Hex(data) {
|
|
7
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hmacSha256(key, data) {
|
|
11
|
+
return crypto.createHmac('sha256', key).update(data).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function urlEncode(str) {
|
|
15
|
+
const hex = (c) => '%' + (c < 16 ? '0' : '') + c.toString(16).toUpperCase();
|
|
16
|
+
const noEscape = new Set(
|
|
17
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~'.split('')
|
|
18
|
+
);
|
|
19
|
+
let out = '';
|
|
20
|
+
for (const ch of str) {
|
|
21
|
+
const c = ch.codePointAt(0);
|
|
22
|
+
out += noEscape.has(ch) && c < 0x80 ? ch : c < 0x80 ? hex(c) : encodeURIComponent(ch);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function timestamp() {
|
|
28
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '') + 'Z';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sortedQs(query) {
|
|
32
|
+
return Object.entries(query)
|
|
33
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
34
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
35
|
+
.join('&');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function signRequest(method, path, query, body, ak, sk, securitytoken) {
|
|
39
|
+
const ts = timestamp();
|
|
40
|
+
const host = new URL(BASE_URL).host;
|
|
41
|
+
|
|
42
|
+
const cqs = Object.entries(query)
|
|
43
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
44
|
+
.map(([k, v]) => `${urlEncode(k)}=${urlEncode(v)}`)
|
|
45
|
+
.join('&');
|
|
46
|
+
|
|
47
|
+
const curi =
|
|
48
|
+
'/' +
|
|
49
|
+
path
|
|
50
|
+
.split('/')
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.map((s) => urlEncode(s))
|
|
53
|
+
.join('/') +
|
|
54
|
+
'/';
|
|
55
|
+
|
|
56
|
+
const signedHeaders = securitytoken
|
|
57
|
+
? 'host;x-sdk-date;x-security-token'
|
|
58
|
+
: 'host;x-sdk-date';
|
|
59
|
+
const canonicalHeaders = securitytoken
|
|
60
|
+
? `host:${host}\nx-sdk-date:${ts}\nx-security-token:${securitytoken}\n`
|
|
61
|
+
: `host:${host}\nx-sdk-date:${ts}\n`;
|
|
62
|
+
|
|
63
|
+
const bodyStr = body ? JSON.stringify(body) : '';
|
|
64
|
+
const payloadHash = sha256Hex(bodyStr);
|
|
65
|
+
|
|
66
|
+
const canonicalRequest = [
|
|
67
|
+
method,
|
|
68
|
+
curi,
|
|
69
|
+
cqs,
|
|
70
|
+
canonicalHeaders,
|
|
71
|
+
signedHeaders,
|
|
72
|
+
payloadHash,
|
|
73
|
+
].join('\n');
|
|
74
|
+
|
|
75
|
+
const stringToSign = `SDK-HMAC-SHA256\n${ts}\n${sha256Hex(canonicalRequest)}`;
|
|
76
|
+
const signature = hmacSha256(sk, stringToSign);
|
|
77
|
+
|
|
78
|
+
const headers = {
|
|
79
|
+
host,
|
|
80
|
+
'x-sdk-date': ts,
|
|
81
|
+
Authorization: `SDK-HMAC-SHA256 Access=${ak}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
82
|
+
};
|
|
83
|
+
if (securitytoken) {
|
|
84
|
+
headers['x-security-token'] = securitytoken;
|
|
85
|
+
}
|
|
86
|
+
return headers;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getCredentials() {
|
|
90
|
+
const credentials = resolveCredentials();
|
|
91
|
+
return { ak: credentials.ak, sk: credentials.sk, securitytoken: credentials.securityToken };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function apiGet(path, query, ak, sk, securitytoken) {
|
|
95
|
+
query = query || {};
|
|
96
|
+
const qs = sortedQs(query);
|
|
97
|
+
const fullPath = qs ? `${path}?${qs}` : path;
|
|
98
|
+
const headers = signRequest('GET', path, query, undefined, ak, sk, securitytoken);
|
|
99
|
+
const resp = await fetch(`${BASE_URL}${fullPath}`, { headers });
|
|
100
|
+
return { status: resp.status, data: await resp.json() };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function apiPost(path, body, ak, sk, securitytoken) {
|
|
104
|
+
const headers = signRequest('POST', path, {}, body, ak, sk, securitytoken);
|
|
105
|
+
const resp = await fetch(`${BASE_URL}${path}`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
});
|
|
110
|
+
return { status: resp.status, data: await resp.json() };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function createConnection(envId, ak, sk, securitytoken) {
|
|
114
|
+
const { status, data } = await apiPost(
|
|
115
|
+
`/open-api-public/v1/devenvs/${envId}/connections`,
|
|
116
|
+
{ source: 'CLI' },
|
|
117
|
+
ak, sk, securitytoken
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (status !== 200 || data?.error_code !== '0000' || !data?.result?.connection_id) {
|
|
121
|
+
throw new Error(`Failed to create connection: ${JSON.stringify(data)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const connectionId = data.result.connection_id;
|
|
125
|
+
const maxAttempts = 60;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
128
|
+
process.stderr.write(`\rWaiting for connection ${connectionId}... (${i}s)`);
|
|
129
|
+
const { data: getData } = await apiGet(
|
|
130
|
+
`/open-api-public/v1/devenvs/${envId}/connections/${connectionId}`,
|
|
131
|
+
{},
|
|
132
|
+
ak, sk, securitytoken
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
getData?.result?.connection_info?.url &&
|
|
137
|
+
getData.result.connection_info.extensions?.source != null
|
|
138
|
+
) {
|
|
139
|
+
const u = new URL(getData.result.connection_info.url);
|
|
140
|
+
u.searchParams.set('source', String(getData.result.connection_info.extensions.source));
|
|
141
|
+
process.stderr.write(`\rConnection ${connectionId} established (${i}s).\n`);
|
|
142
|
+
return {
|
|
143
|
+
wsUrl: u.toString(),
|
|
144
|
+
source: getData.result.connection_info.extensions.source,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw new Error(`Timed out waiting for connection ${connectionId}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|