huaweicloud-devkit 1.1.1-next.14 → 1.1.1-next.15
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/.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/skills/huaweicloud-cli-and-auth/SKILL.md +11 -11
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +82 -2
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +174 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +53 -32
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +22 -1
- package/plugins/huaweicloud-core/src/setup-cli.mjs +66 -5
- package/plugins/huaweicloud-core/src/tools.mjs +168 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.1-next.
|
|
3
|
+
"version": "1.1.1-next.15",
|
|
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": [
|
|
@@ -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.1-next.
|
|
20
|
+
"version": "1.1.1-next.15",
|
|
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.1-next.
|
|
23
|
+
"version": "1.1.1-next.15",
|
|
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.1-next.
|
|
20
|
+
"version": "1.1.1-next.15",
|
|
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.1-next.
|
|
3
|
+
"version": "1.1.1-next.15",
|
|
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.1-next.
|
|
3
|
+
"version": "1.1.1-next.15",
|
|
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.1-next.
|
|
5
|
+
"version": "1.1.1-next.15",
|
|
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.",
|
|
@@ -70,9 +70,8 @@ Agent processes find executables through `PATH`. If OpenCode/Codex cannot find `
|
|
|
70
70
|
**NEVER let AK/SK enter shell history. This is the #1 credential leak vector.**
|
|
71
71
|
|
|
72
72
|
- Create AK/SK in the Huawei Cloud console under `My Credentials -> Access Keys`.
|
|
73
|
-
- **
|
|
74
|
-
-
|
|
75
|
-
- **Non-interactive** (DANGEROUS — AK/SK in shell history): `hcloud configure set --cli-access-key=<AK> --cli-secret-key=<SK> --cli-region=<region>`. Only use in ephemeral CI/CD shells. User must execute outside agent chat.
|
|
73
|
+
- **Configure credentials ONLY via `npx huaweicloud-devkit auth init`** — the single entry point. It writes the unified `~/.config/huaweicloud/credentials.json` and mirrors it to KooCLI (`~/.hcloud/config.json`) and OBS (`~/.obsutilconfig`), without AK/SK entering shell history.
|
|
74
|
+
- Never configure KooCLI on its own: do not ask the user to run `hcloud configure init` or `hcloud configure set --cli-access-key=... --cli-secret-key=... --cli-region=...` (AK/SK would leak into shell history). If KooCLI lacks credentials, tell the user to run `npx huaweicloud-devkit auth init`.
|
|
76
75
|
- If MCP is available, use `huaweicloud_show_profile_redacted` to check status without ever seeing credentials.
|
|
77
76
|
- Never paste AK/SK, passwords, tokens, or profile files into the agent conversation.
|
|
78
77
|
- KooCLI stores credentials in `~/.hcloud/config.json`, NOT environment variables. `HCLOUD_ACCESS_KEY` / `HCLOUD_SECRET_KEY` / `HCLOUD_REGION` env vars are NOT read by KooCLI 7.x.
|
|
@@ -125,15 +124,16 @@ hcloud <Service> <Op> --cli-debug=true
|
|
|
125
124
|
|
|
126
125
|
Credentials are resolved in this order (highest priority first):
|
|
127
126
|
|
|
128
|
-
| Priority | Source
|
|
129
|
-
| -------- |
|
|
130
|
-
| 1 | Runtime credentials
|
|
131
|
-
| 2 |
|
|
132
|
-
| 3 |
|
|
133
|
-
| 4 |
|
|
134
|
-
| 5 |
|
|
127
|
+
| Priority | Source | When active | Persistence |
|
|
128
|
+
| -------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- |
|
|
129
|
+
| 1 | Runtime (session) credentials | `huaweicloud_auth_init` / `huaweicloud_auth_switch action=temporary` set them | Memory (MCP restart clears) |
|
|
130
|
+
| 2 | S1 global file with `configuredBySession: true` | set by `huaweicloud_auth_switch action=persist` (session-configured account wins over env) | Permanent (S1) |
|
|
131
|
+
| 3 | Environment variables | `HW_ACCESS_KEY` / `HW_SECRET_KEY` (platform/devspace-injected default account) | MCP process lifetime |
|
|
132
|
+
| 4 | CodeArts / CodeArts Work | `.codeartsdoer/mcp/mcp_settings.json` / `.codeartswork/mcp/mcp_settings.json` | File |
|
|
133
|
+
| 5 | S1 global file (no session flag) | `auth init` permanent credentials | Permanent (S1) |
|
|
134
|
+
| 6 | KooCLI profile | `~/.hcloud/config.json` (KooCLI commands only) | Permanent |
|
|
135
135
|
|
|
136
|
-
When switching accounts within the same Agent session, use `huaweicloud_auth_init` to set runtime credentials
|
|
136
|
+
When switching accounts within the same Agent session, use `huaweicloud_auth_init` to set runtime credentials (overrides all sources for the current MCP process), or `huaweicloud_auth_switch action=persist`, which writes S1 with `configuredBySession: true` so the session-configured account outranks `HW_ACCESS_KEY` / `HW_SECRET_KEY`. Note: running `auth init` clears the configuredBySession flag.
|
|
137
137
|
|
|
138
138
|
## Preferred Toolkit Tools
|
|
139
139
|
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readlinkSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
2
11
|
import { dirname, join } from 'node:path';
|
|
3
12
|
import { homedir } from 'node:os';
|
|
4
13
|
|
|
@@ -55,12 +64,21 @@ export function writeGlobalCredentials(credentials = {}) {
|
|
|
55
64
|
sk: String(credentials.sk || ''),
|
|
56
65
|
securityToken: String(credentials.securityToken || ''),
|
|
57
66
|
region: String(credentials.region || ''),
|
|
67
|
+
...(credentials.configuredBySession === undefined
|
|
68
|
+
? {}
|
|
69
|
+
: { configuredBySession: Boolean(credentials.configuredBySession) }),
|
|
58
70
|
};
|
|
59
71
|
writeFileSync(path, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
60
72
|
ensurePrivateMode(path);
|
|
61
73
|
return path;
|
|
62
74
|
}
|
|
63
75
|
|
|
76
|
+
export function setConfiguredBySession(flag) {
|
|
77
|
+
const stored = readGlobalCredentials();
|
|
78
|
+
const next = { ...(stored || {}), configuredBySession: Boolean(flag) };
|
|
79
|
+
writeGlobalCredentials(next);
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
export function writeObsConfig(credentials = {}) {
|
|
65
83
|
const region = String(credentials.region || '');
|
|
66
84
|
const ak = String(credentials.ak || '');
|
|
@@ -99,6 +117,16 @@ export function resolveCredentials(options = {}) {
|
|
|
99
117
|
if (!region && stored.region) region = stored.region;
|
|
100
118
|
}
|
|
101
119
|
|
|
120
|
+
// R9: S1 written by `auth_switch persist` (configuredBySession) is the session's
|
|
121
|
+
// source of truth and wins over env-injected defaults. Plain S1 (`auth init`)
|
|
122
|
+
// still yields to env so devspace-style env injection keeps working.
|
|
123
|
+
if (stored && stored.configuredBySession === true && stored.ak && stored.sk) {
|
|
124
|
+
ak = stored.ak;
|
|
125
|
+
sk = stored.sk;
|
|
126
|
+
if (!securityToken) securityToken = stored.securityToken || '';
|
|
127
|
+
if (!region) region = stored.region || '';
|
|
128
|
+
}
|
|
129
|
+
|
|
102
130
|
// Sandbox/platform-injected temporary STS credentials (env vars carrying a
|
|
103
131
|
// security token) must not shadow the user's explicit permanent credentials
|
|
104
132
|
// from `auth init`. Prefer the stored file when both exist.
|
|
@@ -147,7 +175,7 @@ function isCodeArtsContext() {
|
|
|
147
175
|
);
|
|
148
176
|
}
|
|
149
177
|
|
|
150
|
-
function readCodeArtsCredentials() {
|
|
178
|
+
export function readCodeArtsCredentials() {
|
|
151
179
|
const parentCwd = getParentCwd();
|
|
152
180
|
const searchDirs = [process.env.CODEARTS_PROJECT_DIR, parentCwd, process.cwd(), homedir()];
|
|
153
181
|
|
|
@@ -213,6 +241,10 @@ export function clearRuntimeCredentials() {
|
|
|
213
241
|
runtimeCredentials = null;
|
|
214
242
|
}
|
|
215
243
|
|
|
244
|
+
export function hasRuntimeCredentials() {
|
|
245
|
+
return runtimeCredentials !== null;
|
|
246
|
+
}
|
|
247
|
+
|
|
216
248
|
export function resolveCredentialsWithRuntime(options = {}) {
|
|
217
249
|
if (runtimeCredentials) {
|
|
218
250
|
return {
|
|
@@ -225,3 +257,51 @@ export function resolveCredentialsWithRuntime(options = {}) {
|
|
|
225
257
|
|
|
226
258
|
return resolveCredentials(options);
|
|
227
259
|
}
|
|
260
|
+
|
|
261
|
+
export function lastSyncPath() {
|
|
262
|
+
return join(baseHome(), '.config', 'huaweicloud', '.last_sync');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function readLastSync() {
|
|
266
|
+
const path = lastSyncPath();
|
|
267
|
+
if (!existsSync(path)) return null;
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function writeLastSync() {
|
|
276
|
+
const path = lastSyncPath();
|
|
277
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
278
|
+
const payload = { ts: Date.now() };
|
|
279
|
+
writeFileSync(path, JSON.stringify(payload), { encoding: 'utf8', mode: 0o600 });
|
|
280
|
+
ensurePrivateMode(path);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function backupGlobalCredentials() {
|
|
284
|
+
const path = globalCredentialsPath();
|
|
285
|
+
if (!existsSync(path)) return null;
|
|
286
|
+
const bakPath = `${path}.bak`;
|
|
287
|
+
try {
|
|
288
|
+
copyFileSync(path, bakPath);
|
|
289
|
+
ensurePrivateMode(bakPath);
|
|
290
|
+
return bakPath;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function restoreGlobalCredentialsBackup() {
|
|
297
|
+
const path = globalCredentialsPath();
|
|
298
|
+
const bakPath = `${path}.bak`;
|
|
299
|
+
if (!existsSync(bakPath)) return false;
|
|
300
|
+
try {
|
|
301
|
+
copyFileSync(bakPath, path);
|
|
302
|
+
ensurePrivateMode(path);
|
|
303
|
+
return true;
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
hasRuntimeCredentials,
|
|
9
|
+
obsConfigPath,
|
|
10
|
+
readGlobalCredentials,
|
|
11
|
+
readLastSync,
|
|
12
|
+
resolveCredentialsWithRuntime,
|
|
13
|
+
} from './credentials.mjs';
|
|
14
|
+
import { redactSecrets } from '../safety-policy.mjs';
|
|
15
|
+
|
|
16
|
+
export { hasRuntimeCredentials };
|
|
17
|
+
|
|
18
|
+
function baseHome() {
|
|
19
|
+
return process.env.HUAWEICLOUD_HOME || homedir();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fingerprint(ak, sk) {
|
|
23
|
+
if (!ak || !sk) return '';
|
|
24
|
+
return createHash('sha256').update(`${ak}${sk}`).digest('hex').slice(0, 8);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isManualModified(path) {
|
|
28
|
+
if (!existsSync(path)) return false;
|
|
29
|
+
const lastSync = readLastSync();
|
|
30
|
+
try {
|
|
31
|
+
const fts = statSync(path).mtimeMs;
|
|
32
|
+
if (!lastSync) return true; // no marker → treat as manual
|
|
33
|
+
return fts > lastSync.ts;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readKooCliProfiles() {
|
|
40
|
+
const configPath = join(baseHome(), '.hcloud', 'config.json');
|
|
41
|
+
if (!existsSync(configPath)) return { error: 'KooCLI config not found' };
|
|
42
|
+
try {
|
|
43
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
44
|
+
const current = String(raw.current || 'default');
|
|
45
|
+
const profiles = Array.isArray(raw.profiles) ? raw.profiles : [];
|
|
46
|
+
const mtimeMs = statSync(configPath).mtimeMs;
|
|
47
|
+
return {
|
|
48
|
+
current,
|
|
49
|
+
mtimeMs,
|
|
50
|
+
profiles: profiles.map((p) => ({
|
|
51
|
+
name: String(p.name || ''),
|
|
52
|
+
fingerprint: fingerprint(p.accessKeyId, p.secretAccessKey),
|
|
53
|
+
accessKeyId: p.accessKeyId || '',
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return { error: 'KooCLI config parse failed' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveManagedProfile() {
|
|
62
|
+
const res = readKooCliProfiles();
|
|
63
|
+
if (res.error) return null;
|
|
64
|
+
return res.current;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function currentFingerprintFromHcloud(res) {
|
|
68
|
+
if (res.error) return null;
|
|
69
|
+
const cur = res.profiles.find((p) => p.name === res.current);
|
|
70
|
+
return cur ? cur.fingerprint : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function scanState() {
|
|
74
|
+
const s1 = readGlobalCredentials() || {};
|
|
75
|
+
const envAk = process.env.HW_ACCESS_KEY || '';
|
|
76
|
+
const envSk = process.env.HW_SECRET_KEY || '';
|
|
77
|
+
const kooCli = readKooCliProfiles();
|
|
78
|
+
const currentFp = currentFingerprintFromHcloud(kooCli);
|
|
79
|
+
const inconsistencies = [];
|
|
80
|
+
|
|
81
|
+
const s1Fingerprint = fingerprint(s1.ak, s1.sk);
|
|
82
|
+
const envFingerprint = fingerprint(envAk, envSk);
|
|
83
|
+
const s3 = existsSync(obsConfigPath()) ? parseS3ObsConfig(obsConfigPath()) : null;
|
|
84
|
+
const s3Fingerprint = s3 ? fingerprint(s3.ak, s3.sk) : null;
|
|
85
|
+
|
|
86
|
+
let hasRuntime = false;
|
|
87
|
+
let runtimeFingerprint = null;
|
|
88
|
+
try {
|
|
89
|
+
const rt = resolveCredentialsWithRuntime();
|
|
90
|
+
hasRuntime = hasRuntimeCredentials();
|
|
91
|
+
if (hasRuntime) runtimeFingerprint = fingerprint(rt.ak, rt.sk);
|
|
92
|
+
} catch {
|
|
93
|
+
// nothing resolvable → runtime store inactive
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (s1Fingerprint && currentFp && s1Fingerprint !== currentFp) {
|
|
97
|
+
inconsistencies.push({
|
|
98
|
+
store: 'S2-current',
|
|
99
|
+
source: 'KooCLI current profile',
|
|
100
|
+
fingerprint: currentFp,
|
|
101
|
+
manualModified: isManualModified(join(baseHome(), '.hcloud', 'config.json')),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (s1Fingerprint && s3Fingerprint && s1Fingerprint !== s3Fingerprint) {
|
|
105
|
+
inconsistencies.push({
|
|
106
|
+
store: 'S3',
|
|
107
|
+
source: 'obsutilconfig',
|
|
108
|
+
fingerprint: s3Fingerprint,
|
|
109
|
+
manualModified: isManualModified(obsConfigPath()),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
stores: {
|
|
115
|
+
s1Fingerprint,
|
|
116
|
+
envFingerprint,
|
|
117
|
+
currentFingerprint: currentFp,
|
|
118
|
+
s3Fingerprint,
|
|
119
|
+
runtimeFingerprint,
|
|
120
|
+
},
|
|
121
|
+
kooCliCurrent: kooCli.error ? null : kooCli.current,
|
|
122
|
+
inconsistencies,
|
|
123
|
+
hasRuntime,
|
|
124
|
+
runtimeFingerprint,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseS3ObsConfig(path) {
|
|
129
|
+
try {
|
|
130
|
+
const text = readFileSync(path, 'utf8');
|
|
131
|
+
const get = (k) => {
|
|
132
|
+
const m = text.match(new RegExp(`^${k}=(.+)$`, 'm'));
|
|
133
|
+
return m ? m[1].trim() : '';
|
|
134
|
+
};
|
|
135
|
+
return { ak: get('ak'), sk: get('sk'), region: inferRegion(text) };
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function inferRegion(text) {
|
|
142
|
+
const m = text.match(/endpoint=https:\/\/obs\.([^.]+)\./);
|
|
143
|
+
return m ? m[1] : '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function exportStateForStatus(_jailed) {
|
|
147
|
+
const scan = scanState();
|
|
148
|
+
return {
|
|
149
|
+
...scan,
|
|
150
|
+
// fields below are consumed by getAuthStatus in service.mjs
|
|
151
|
+
inconsistent: scan.inconsistencies.length > 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function runHcloudConfigure(profile, ak, sk, region) {
|
|
156
|
+
const bin = process.env.HCLOUD_BIN || 'hcloud';
|
|
157
|
+
const args = [
|
|
158
|
+
'configure',
|
|
159
|
+
'set',
|
|
160
|
+
`--cli-profile=${profile}`,
|
|
161
|
+
`--cli-access-key=${ak}`,
|
|
162
|
+
`--cli-secret-key=${sk}`,
|
|
163
|
+
`--cli-region=${region || ''}`,
|
|
164
|
+
];
|
|
165
|
+
const r = spawnSync(bin, args, { shell: false, windowsHide: true, stdio: 'pipe', timeout: 30000 });
|
|
166
|
+
return {
|
|
167
|
+
ok: r.status === 0,
|
|
168
|
+
error: redactSecrets(
|
|
169
|
+
String(r.stderr || '')
|
|
170
|
+
.trim()
|
|
171
|
+
.slice(0, 240),
|
|
172
|
+
),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -2,7 +2,19 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
|
|
4
4
|
import { getAgentRegistrationStatuses } from './agent-registration.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
globalCredentialsPath,
|
|
7
|
+
obsConfigPath,
|
|
8
|
+
readGlobalCredentials,
|
|
9
|
+
writeLastSync,
|
|
10
|
+
writeObsConfig,
|
|
11
|
+
} from './credentials.mjs';
|
|
12
|
+
import {
|
|
13
|
+
exportStateForStatus,
|
|
14
|
+
hasRuntimeCredentials,
|
|
15
|
+
resolveManagedProfile,
|
|
16
|
+
runHcloudConfigure,
|
|
17
|
+
} from './reconcile.mjs';
|
|
6
18
|
|
|
7
19
|
function hcloudInstalled() {
|
|
8
20
|
const bin = process.env.HCLOUD_BIN || 'hcloud';
|
|
@@ -22,6 +34,7 @@ function hcloudInstalled() {
|
|
|
22
34
|
|
|
23
35
|
export function getAuthStatus(target = 'all') {
|
|
24
36
|
const credentials = readGlobalCredentials();
|
|
37
|
+
const reconciled = { ...exportStateForStatus(), runtimeActive: hasRuntimeCredentials() };
|
|
25
38
|
return {
|
|
26
39
|
target,
|
|
27
40
|
credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
|
|
@@ -29,6 +42,7 @@ export function getAuthStatus(target = 'all') {
|
|
|
29
42
|
obsConfigured: existsSync(obsConfigPath()),
|
|
30
43
|
obsConfigPath: obsConfigPath(),
|
|
31
44
|
kooCliInstalled: hcloudInstalled(),
|
|
45
|
+
reconciled,
|
|
32
46
|
agents: getAgentRegistrationStatuses(target).agents,
|
|
33
47
|
};
|
|
34
48
|
}
|
|
@@ -42,6 +56,13 @@ export function syncAuth(target = 'all') {
|
|
|
42
56
|
nextStep: 'Run "npx huaweicloud-devkit auth init" first.',
|
|
43
57
|
};
|
|
44
58
|
}
|
|
59
|
+
if (hasRuntimeCredentials()) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: 'Runtime credentials are active; auto-sync suppressed (R10).',
|
|
63
|
+
nextStep: 'Run huaweicloud_auth_switch action=clear or action=persist first.',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
45
66
|
|
|
46
67
|
let obs;
|
|
47
68
|
try {
|
|
@@ -54,42 +75,42 @@ export function syncAuth(target = 'all') {
|
|
|
54
75
|
};
|
|
55
76
|
}
|
|
56
77
|
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
);
|
|
76
|
-
if (r.status === 0) {
|
|
77
|
-
hcloud = { ok: true, message: 'KooCLI config synced' };
|
|
78
|
-
} else {
|
|
79
|
-
hcloud = {
|
|
80
|
-
ok: false,
|
|
81
|
-
message: 'KooCLI config sync failed',
|
|
82
|
-
error: String(r.stderr || '')
|
|
83
|
-
.trim()
|
|
84
|
-
.slice(0, 240),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
78
|
+
const profile = resolveManagedProfile();
|
|
79
|
+
if (!profile) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: 'KooCLI current profile unresolved; run "npx huaweicloud-devkit auth init" in a real terminal.',
|
|
83
|
+
nextStep: 'Run "npx huaweicloud-devkit auth init" in a real terminal to create the KooCLI current profile.',
|
|
84
|
+
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!hcloudInstalled()) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: 'KooCLI not installed.',
|
|
92
|
+
nextStep: 'Install KooCLI or point HCLOUD_BIN at the hcloud executable.',
|
|
93
|
+
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
94
|
+
};
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
const { ok, error } = runHcloudConfigure(profile, credentials.ak, credentials.sk, credentials.region);
|
|
98
|
+
if (!ok) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: error || 'KooCLI config sync failed',
|
|
102
|
+
nextStep: 'Run "npx huaweicloud-devkit auth init" in a real terminal to refresh the KooCLI config.',
|
|
103
|
+
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writeLastSync();
|
|
108
|
+
|
|
89
109
|
return {
|
|
90
110
|
ok: true,
|
|
111
|
+
profile,
|
|
91
112
|
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
92
|
-
hcloud,
|
|
113
|
+
hcloud: { ok: true, message: `KooCLI config synced to profile=${profile}` },
|
|
93
114
|
credentialsConfigured: true,
|
|
94
115
|
agents: getAgentRegistrationStatuses(target).agents,
|
|
95
116
|
note: 'OBS credentials were synced from the global credential vault. Agent MCP registration is managed by "npx huaweicloud-devkit install --target <agent>".',
|
|
@@ -172,17 +172,38 @@ export async function runHcloud(args, options = {}) {
|
|
|
172
172
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
173
173
|
const result = await runHcloudOnce(plan, options);
|
|
174
174
|
if (result.ok || attempt >= maxRetries || !isRetryableNetworkError(result)) {
|
|
175
|
-
|
|
175
|
+
const merged = {
|
|
176
176
|
...result,
|
|
177
177
|
retries: attempt,
|
|
178
178
|
attempts: attempt + 1,
|
|
179
179
|
};
|
|
180
|
+
if (merged.ok) {
|
|
181
|
+
const warning = await runtimeCurrentMismatchWarning();
|
|
182
|
+
return warning ? { ...merged, authWarning: warning } : merged;
|
|
183
|
+
}
|
|
184
|
+
return merged;
|
|
180
185
|
}
|
|
181
186
|
await wait((options.retryBaseDelayMs ?? 500) * 2 ** attempt);
|
|
182
187
|
}
|
|
183
188
|
throw new Error('Unreachable retry state.');
|
|
184
189
|
}
|
|
185
190
|
|
|
191
|
+
async function runtimeCurrentMismatchWarning() {
|
|
192
|
+
try {
|
|
193
|
+
const { hasRuntimeCredentials, scanState } = await import('./auth/reconcile.mjs');
|
|
194
|
+
if (!hasRuntimeCredentials()) return null;
|
|
195
|
+
const scan = scanState();
|
|
196
|
+
const { runtimeFingerprint, currentFingerprint } = scan.stores;
|
|
197
|
+
if (runtimeFingerprint && currentFingerprint && runtimeFingerprint !== currentFingerprint) {
|
|
198
|
+
return '会话内临时账号与 KooCLI current 档不一致:hcloud 命令仍使用 current 档账号。如需对齐请用 huaweicloud_auth_switch action=persist。';
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
} catch {
|
|
202
|
+
// reconcile unavailable → no warning, never block the command
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
186
207
|
function discoverHcloudPath() {
|
|
187
208
|
if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
|
|
188
209
|
const candidates =
|
|
@@ -18,10 +18,13 @@ import { createRequire } from 'node:module';
|
|
|
18
18
|
|
|
19
19
|
import { getAuthStatus, syncAuth } from './auth/service.mjs';
|
|
20
20
|
import { SUPPORTED_AGENT_TARGETS } from './auth/agent-registration.mjs';
|
|
21
|
+
import { resolveManagedProfile } from './auth/reconcile.mjs';
|
|
22
|
+
import { redactSecrets } from './safety-policy.mjs';
|
|
21
23
|
import {
|
|
22
24
|
globalCredentialsPath,
|
|
23
25
|
readGlobalCredentials,
|
|
24
26
|
writeGlobalCredentials,
|
|
27
|
+
writeLastSync,
|
|
25
28
|
writeObsConfig,
|
|
26
29
|
} from './auth/credentials.mjs';
|
|
27
30
|
import {
|
|
@@ -3836,7 +3839,6 @@ async function cmdInstallHcloud() {
|
|
|
3836
3839
|
console.log('\nAfter install, set HCLOUD_BIN if hcloud is not on PATH.');
|
|
3837
3840
|
console.log('\n\x1b[1m\x1b[33m=== Configure credentials SAFELY ===\x1b[0m');
|
|
3838
3841
|
console.log(' Unified credentials (recommended): npx huaweicloud-devkit auth init');
|
|
3839
|
-
console.log(' KooCLI only (alternative): hcloud configure init');
|
|
3840
3842
|
console.log(' NEVER: hcloud configure set --cli-access-key=xxx (AK/SK in shell history!)');
|
|
3841
3843
|
console.log('\nThen run: npx huaweicloud-devkit doctor');
|
|
3842
3844
|
}
|
|
@@ -3893,11 +3895,17 @@ async function readSecret(prompt) {
|
|
|
3893
3895
|
});
|
|
3894
3896
|
}
|
|
3895
3897
|
|
|
3898
|
+
function configuredProfileName() {
|
|
3899
|
+
const name = resolveManagedProfile();
|
|
3900
|
+
return name || 'default';
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3896
3903
|
function configureHcloud(credentials) {
|
|
3897
3904
|
const hcloudBin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
|
|
3898
3905
|
const args = [
|
|
3899
3906
|
'configure',
|
|
3900
3907
|
'set',
|
|
3908
|
+
`--cli-profile=${configuredProfileName()}`,
|
|
3901
3909
|
`--cli-access-key=${credentials.ak}`,
|
|
3902
3910
|
`--cli-secret-key=${credentials.sk}`,
|
|
3903
3911
|
`--cli-region=${credentials.region || ''}`,
|
|
@@ -3911,9 +3919,11 @@ function configureHcloud(credentials) {
|
|
|
3911
3919
|
return {
|
|
3912
3920
|
ok: r.status === 0,
|
|
3913
3921
|
code: r.status,
|
|
3914
|
-
error:
|
|
3915
|
-
.
|
|
3916
|
-
|
|
3922
|
+
error: redactSecrets(
|
|
3923
|
+
String(r.stderr || '')
|
|
3924
|
+
.trim()
|
|
3925
|
+
.slice(0, 240),
|
|
3926
|
+
),
|
|
3917
3927
|
};
|
|
3918
3928
|
}
|
|
3919
3929
|
|
|
@@ -3992,6 +4002,56 @@ async function cmdAuthInit() {
|
|
|
3992
4002
|
console.log(' Restart your agent sessions.');
|
|
3993
4003
|
}
|
|
3994
4004
|
|
|
4005
|
+
async function cmdAuthReconcile() {
|
|
4006
|
+
console.log(BANNER);
|
|
4007
|
+
console.log('HuaweiCloud DevKit Credential Reconciliation\n');
|
|
4008
|
+
|
|
4009
|
+
const { scanState, runHcloudConfigure, resolveManagedProfile } = await import('./auth/reconcile.mjs');
|
|
4010
|
+
const state = scanState();
|
|
4011
|
+
if (state.inconsistencies.length === 0) {
|
|
4012
|
+
console.log('All credential files are consistent. ✓');
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
4017
|
+
if (!interactive) {
|
|
4018
|
+
console.error(
|
|
4019
|
+
'\x1b[31mNon-interactive session. Cannot run interactive reconciliation. Use "npx huaweicloud-devkit auth sync" or run reconciliation in a real terminal.\x1b[0m',
|
|
4020
|
+
);
|
|
4021
|
+
process.exitCode = 1;
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
for (const inc of state.inconsistencies) {
|
|
4026
|
+
console.log(
|
|
4027
|
+
` [${inc.store}] fingerprint ${inc.fingerprint} differs from S1 ${state.stores.s1Fingerprint}${inc.manualModified ? ' (manual modified)' : ''}`,
|
|
4028
|
+
);
|
|
4029
|
+
}
|
|
4030
|
+
const ask = await readLineQuestion('以 S1 为准同步到不一致文件? (y/N) ');
|
|
4031
|
+
if (!['y', 'Y', 'yes'].includes(ask.trim())) {
|
|
4032
|
+
console.log('Aborted.');
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
const credentials = readGlobalCredentials();
|
|
4036
|
+
if (!credentials?.ak || !credentials?.sk) {
|
|
4037
|
+
console.error('No global credentials found after confirmation; aborting.');
|
|
4038
|
+
process.exitCode = 1;
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
try {
|
|
4042
|
+
writeObsConfig(credentials);
|
|
4043
|
+
} catch (error) {
|
|
4044
|
+
console.log(`OBS sync failed: ${error.message}`);
|
|
4045
|
+
}
|
|
4046
|
+
const profile = resolveManagedProfile();
|
|
4047
|
+
if (profile) {
|
|
4048
|
+
const r = runHcloudConfigure(profile, credentials.ak, credentials.sk, credentials.region);
|
|
4049
|
+
console.log(` KooCLI ${r.ok ? 'synced' : 'sync failed'}: profile=${profile} ${r.error || ''}`);
|
|
4050
|
+
}
|
|
4051
|
+
writeLastSync();
|
|
4052
|
+
console.log('Done. .last_sync refreshed.');
|
|
4053
|
+
}
|
|
4054
|
+
|
|
3995
4055
|
async function cmdAuthSync() {
|
|
3996
4056
|
const target = parseTarget();
|
|
3997
4057
|
console.log(BANNER);
|
|
@@ -4023,6 +4083,7 @@ async function cmdAuth() {
|
|
|
4023
4083
|
const sub = (process.argv[3] || 'status').toLowerCase();
|
|
4024
4084
|
if (sub === 'init' || sub === 'setup') return cmdAuthInit();
|
|
4025
4085
|
if (sub === 'sync' || sub === 'refresh') return cmdAuthSync();
|
|
4086
|
+
if (sub === 'reconcile') return cmdAuthReconcile();
|
|
4026
4087
|
return cmdAuthStatus();
|
|
4027
4088
|
}
|
|
4028
4089
|
|
|
@@ -4209,7 +4270,7 @@ async function main() {
|
|
|
4209
4270
|
console.log(' status Show installation status');
|
|
4210
4271
|
console.log(' doctor Self-check: hcloud, MCP, skills, auth');
|
|
4211
4272
|
console.log(' install-hcloud Show KooCLI install commands for your OS');
|
|
4212
|
-
console.log(' auth Manage unified auth: init | sync | status');
|
|
4273
|
+
console.log(' auth Manage unified auth: init | sync | status | reconcile');
|
|
4213
4274
|
console.log(' proxy Manage proxy config: init | show | clear');
|
|
4214
4275
|
console.log(' version Print installed plugin version per agent');
|
|
4215
4276
|
console.log(' help Show this help');
|
|
@@ -34,10 +34,17 @@ import { getCredentials } from './sandbox/hwlink-api.mjs';
|
|
|
34
34
|
import { getAuthStatus, syncAuth } from './auth/service.mjs';
|
|
35
35
|
import {
|
|
36
36
|
readGlobalCredentials,
|
|
37
|
+
writeGlobalCredentials,
|
|
37
38
|
writeObsConfig as writeObsConfigFile,
|
|
38
39
|
setRuntimeCredentials,
|
|
39
40
|
clearRuntimeCredentials,
|
|
41
|
+
setConfiguredBySession,
|
|
42
|
+
backupGlobalCredentials,
|
|
43
|
+
writeLastSync,
|
|
44
|
+
readCodeArtsCredentials,
|
|
45
|
+
globalCredentialsPath,
|
|
40
46
|
} from './auth/credentials.mjs';
|
|
47
|
+
import { fingerprint, runHcloudConfigure, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
41
48
|
|
|
42
49
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
43
50
|
const SKILLS_ROOT_DEV = join(__dirname, '..', 'skills');
|
|
@@ -500,6 +507,34 @@ export const TOOL_DEFINITIONS = [
|
|
|
500
507
|
},
|
|
501
508
|
},
|
|
502
509
|
},
|
|
510
|
+
{
|
|
511
|
+
name: 'huaweicloud_auth_switch',
|
|
512
|
+
description:
|
|
513
|
+
'Switch Huawei Cloud credentials within the current session. action=temporary keeps AK/SK in memory only (restart loses them; hcloud commands are unaffected and an A+C warning will be raised). action=persist writes S1 (single source of truth) with configuredBySession flag and propagates to S2 (KooCLI current profile) and S3 (OBS). action=clear resets runtime credentials. mode=import reads AK/SK from ~/.config/huaweicloud/creds-import.json then wipes it (SK never enters conversation). mode=memory passes AK/SK as tool arguments (SK is visible to model - prefer import). mode=mcp-config reads the injected mcp_settings environment.',
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
mode: { type: 'string', enum: ['import', 'memory', 'mcp-config'], description: 'Credential source channel' },
|
|
518
|
+
action: { type: 'string', enum: ['persist', 'temporary', 'clear'], description: 'Apply scope' },
|
|
519
|
+
ak: { type: 'string' },
|
|
520
|
+
sk: { type: 'string' },
|
|
521
|
+
securityToken: { type: 'string' },
|
|
522
|
+
region: { type: 'string' },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: 'huaweicloud_auth_confirm',
|
|
528
|
+
description:
|
|
529
|
+
'Confirm a pending credential reconciliation choice returned by auth_switch persist when S1 already holds a different account (R2). decision=s1 keeps S1 as source of truth and propagates it; decision=newImported propagates the newly imported account into S1 and mirrors.',
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
properties: {
|
|
533
|
+
token: { type: 'string', description: 'confirmation token from the needs_confirmation response' },
|
|
534
|
+
decision: { type: 'string', enum: ['s1', 'newImported'] },
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
503
538
|
{
|
|
504
539
|
name: 'huaweicloud_sandbox_exec_with_session',
|
|
505
540
|
description:
|
|
@@ -840,6 +875,62 @@ async function transferGitRepo(args, devStageId, connectResult) {
|
|
|
840
875
|
connectResult._repoStatus = 'cloned_in_sandbox';
|
|
841
876
|
}
|
|
842
877
|
|
|
878
|
+
const pendingConfirms = new Map();
|
|
879
|
+
|
|
880
|
+
function readImportFile() {
|
|
881
|
+
const path = join(dirname(globalCredentialsPath()), 'creds-import.json');
|
|
882
|
+
try {
|
|
883
|
+
if (!existsSync(path)) return null;
|
|
884
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
885
|
+
return {
|
|
886
|
+
ak: String(data.ak || ''),
|
|
887
|
+
sk: String(data.sk || ''),
|
|
888
|
+
securityToken: String(data.securityToken || ''),
|
|
889
|
+
region: String(data.region || ''),
|
|
890
|
+
};
|
|
891
|
+
} catch {
|
|
892
|
+
return null;
|
|
893
|
+
} finally {
|
|
894
|
+
rmSync(path, { force: true });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function persistCredentials(ak, sk, securityToken, region) {
|
|
899
|
+
if (String(securityToken || '')) {
|
|
900
|
+
return {
|
|
901
|
+
status: 'error',
|
|
902
|
+
error: 'Temporary STS credentials cannot be persisted (R3). Use action=temporary.',
|
|
903
|
+
scope: 'rejected',
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const before = backupGlobalCredentials();
|
|
907
|
+
writeGlobalCredentials({ ak, sk: String(sk), securityToken: '', region, configuredBySession: true });
|
|
908
|
+
setConfiguredBySession(true);
|
|
909
|
+
let obs;
|
|
910
|
+
try {
|
|
911
|
+
writeObsConfigFile({ ak, sk, securityToken, region });
|
|
912
|
+
obs = { ok: true };
|
|
913
|
+
} catch (error) {
|
|
914
|
+
obs = { ok: false, error: error.message };
|
|
915
|
+
}
|
|
916
|
+
const profile = resolveManagedProfile();
|
|
917
|
+
let hcloud;
|
|
918
|
+
if (!profile) {
|
|
919
|
+
hcloud = { ok: false, reason: 'KooCLI current profile unresolved' };
|
|
920
|
+
} else {
|
|
921
|
+
hcloud = runHcloudConfigure(profile, ak, sk, region);
|
|
922
|
+
}
|
|
923
|
+
writeLastSync();
|
|
924
|
+
return {
|
|
925
|
+
status: 'ok',
|
|
926
|
+
scope: 'persist',
|
|
927
|
+
backedUp: Boolean(before),
|
|
928
|
+
obs: obs.ok ? { configured: true } : { configured: false, error: obs.error },
|
|
929
|
+
hcloud,
|
|
930
|
+
note: 'S1 written with configuredBySession (R9), which now takes priority over env-injected credentials; S2(current profile) and S3 synced. Note: running `auth init` later clears the configuredBySession flag and env credentials regain priority.',
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
843
934
|
export async function callTool(name, args = {}) {
|
|
844
935
|
switch (name) {
|
|
845
936
|
case 'huaweicloud_check_cli':
|
|
@@ -906,6 +997,81 @@ export async function callTool(name, args = {}) {
|
|
|
906
997
|
}
|
|
907
998
|
setRuntimeCredentials(args.ak, args.sk, undefined, args.region);
|
|
908
999
|
return { status: 'ok', message: 'Runtime credentials set for this MCP session.' };
|
|
1000
|
+
case 'huaweicloud_auth_switch': {
|
|
1001
|
+
const action = args.action || 'temporary';
|
|
1002
|
+
if (action === 'clear') {
|
|
1003
|
+
clearRuntimeCredentials();
|
|
1004
|
+
return { status: 'cleared', message: 'Runtime credentials cleared. Fallback to env/file/S1.' };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
let ak = args.ak || '';
|
|
1008
|
+
let sk = args.sk || '';
|
|
1009
|
+
let securityToken = args.securityToken || '';
|
|
1010
|
+
let region = args.region || '';
|
|
1011
|
+
const sourceChannel = args.mode || 'memory';
|
|
1012
|
+
|
|
1013
|
+
if (sourceChannel === 'import' && (!ak || !sk)) {
|
|
1014
|
+
const imported = readImportFile();
|
|
1015
|
+
if (imported) ({ ak, sk, securityToken, region } = imported);
|
|
1016
|
+
}
|
|
1017
|
+
if (sourceChannel === 'mcp-config' && (!ak || !sk)) {
|
|
1018
|
+
const cc = readCodeArtsCredentials();
|
|
1019
|
+
if (cc) {
|
|
1020
|
+
ak = cc.ak;
|
|
1021
|
+
sk = cc.sk;
|
|
1022
|
+
securityToken = cc.securityToken || '';
|
|
1023
|
+
region = cc.region || region;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (!ak || !sk) {
|
|
1028
|
+
throw new Error('ak and sk are required (or provide creds-import.json for mode=import).');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (action === 'temporary') {
|
|
1032
|
+
setRuntimeCredentials(ak, sk, securityToken || undefined, region);
|
|
1033
|
+
return {
|
|
1034
|
+
status: 'ok',
|
|
1035
|
+
scope: 'temporary',
|
|
1036
|
+
note: 'Runtime credentials active for this MCP process. hcloud commands still use the KooCLI current profile; use action=persist to align files.',
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// action === 'persist'
|
|
1041
|
+
const prev = readGlobalCredentials();
|
|
1042
|
+
const conflict = prev?.ak && prev.ak !== ak;
|
|
1043
|
+
if (conflict) {
|
|
1044
|
+
backupGlobalCredentials();
|
|
1045
|
+
const token = `switch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1046
|
+
pendingConfirms.set(token, {
|
|
1047
|
+
newAk: ak,
|
|
1048
|
+
newSk: sk,
|
|
1049
|
+
newSecurityToken: securityToken,
|
|
1050
|
+
newRegion: region,
|
|
1051
|
+
oldFingerprint: fingerprint(prev.ak, prev.sk),
|
|
1052
|
+
newFingerprint: fingerprint(ak, sk),
|
|
1053
|
+
});
|
|
1054
|
+
return {
|
|
1055
|
+
status: 'needs_confirmation',
|
|
1056
|
+
confirmToken: token,
|
|
1057
|
+
options: [
|
|
1058
|
+
{ key: 's1', label: '以 S1 现有账号为准(不切换,恢复 backup)' },
|
|
1059
|
+
{ key: 'newImported', label: `以新账号(${fingerprint(ak, sk)})为准,覆盖 S1 并同步全部凭证文件` },
|
|
1060
|
+
],
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
return persistCredentials(ak, sk, securityToken, region);
|
|
1065
|
+
}
|
|
1066
|
+
case 'huaweicloud_auth_confirm': {
|
|
1067
|
+
const pending = pendingConfirms.get(args.token);
|
|
1068
|
+
if (!pending) throw new Error('confirmToken not found or expired.');
|
|
1069
|
+
pendingConfirms.delete(args.token);
|
|
1070
|
+
if (args.decision === 's1') {
|
|
1071
|
+
return { status: 'ok', outcome: 'aborted', message: '保持 S1 现有账号,未覆盖。' };
|
|
1072
|
+
}
|
|
1073
|
+
return persistCredentials(pending.newAk, pending.newSk, pending.newSecurityToken, pending.newRegion);
|
|
1074
|
+
}
|
|
909
1075
|
case 'huaweicloud_sandbox_exec_with_session': {
|
|
910
1076
|
const sandboxWsId2 = args.workspace_id || getCurrentWorkspaceId();
|
|
911
1077
|
if (!sandboxWsId2) {
|
|
@@ -1130,7 +1296,7 @@ export async function runVersionCheck(options = {}) {
|
|
|
1130
1296
|
? 'hcloud executable not found. Set HCLOUD_BIN to the full hcloud path, or install KooCLI: npx huaweicloud-devkit install-hcloud. Then restart the agent.'
|
|
1131
1297
|
: 'Install Huawei Cloud KooCLI: npx huaweicloud-devkit install-hcloud. Configure credentials outside the agent conversation.',
|
|
1132
1298
|
authHint:
|
|
1133
|
-
'If hcloud is installed but commands fail with "配置文件中不存在配置项", run `
|
|
1299
|
+
'If hcloud is installed but commands fail with "配置文件中不存在配置项", run `npx huaweicloud-devkit auth init` outside agent chat to configure credentials.',
|
|
1134
1300
|
};
|
|
1135
1301
|
}
|
|
1136
1302
|
|
|
@@ -1243,7 +1409,7 @@ async function setupObsConfigFromHcloud(profile) {
|
|
|
1243
1409
|
return {
|
|
1244
1410
|
ok: false,
|
|
1245
1411
|
error: 'No region found in hcloud profile.',
|
|
1246
|
-
nextStep: 'Run "
|
|
1412
|
+
nextStep: 'Run "npx huaweicloud-devkit auth init" outside agent chat to configure credentials and region.',
|
|
1247
1413
|
};
|
|
1248
1414
|
}
|
|
1249
1415
|
|