huaweicloud-devkit 1.1.1-next.13 → 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 +88 -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 {
|
|
@@ -2743,11 +2746,32 @@ function parseTarget() {
|
|
|
2743
2746
|
process.exit(1);
|
|
2744
2747
|
}
|
|
2745
2748
|
|
|
2749
|
+
function checkForUpdate() {
|
|
2750
|
+
if (pkgVersion === '0.0.0') return;
|
|
2751
|
+
const tag = /-next\.\d+/.test(pkgVersion) ? 'next' : 'latest';
|
|
2752
|
+
let latest = null;
|
|
2753
|
+
try {
|
|
2754
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
2755
|
+
const r = spawnSync(npmBin, ['view', `huaweicloud-devkit@${tag}`, 'version'], {
|
|
2756
|
+
encoding: 'utf8',
|
|
2757
|
+
timeout: 5000,
|
|
2758
|
+
windowsHide: true,
|
|
2759
|
+
});
|
|
2760
|
+
if (r.status === 0) latest = (r.stdout || '').trim().split('\n').pop().trim();
|
|
2761
|
+
} catch {}
|
|
2762
|
+
if (latest && latest !== pkgVersion) {
|
|
2763
|
+
console.log(
|
|
2764
|
+
`\n\x1b[33mℹ️ 检测到新版本:${latest}(当前 ${pkgVersion},${tag} 频道)。建议运行 \`npx huaweicloud-devkit@${tag} update\` 更新。\x1b[0m`,
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2746
2769
|
async function cmdInstall() {
|
|
2747
2770
|
const target = parseTarget();
|
|
2748
2771
|
console.log(BANNER);
|
|
2749
2772
|
console.log(`Installing HuaweiCloud DevKit${target !== 'opencode' ? ` for ${target}` : ''}...\n`);
|
|
2750
2773
|
checkNode();
|
|
2774
|
+
checkForUpdate();
|
|
2751
2775
|
|
|
2752
2776
|
if (target === 'opencode' || target === 'all') {
|
|
2753
2777
|
console.log('[OpenCode]');
|
|
@@ -3450,6 +3474,7 @@ async function cmdDoctor() {
|
|
|
3450
3474
|
async function cmdUpdate() {
|
|
3451
3475
|
console.log(BANNER);
|
|
3452
3476
|
const target = parseTarget();
|
|
3477
|
+
checkForUpdate();
|
|
3453
3478
|
|
|
3454
3479
|
if (target === 'opencode') {
|
|
3455
3480
|
if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
|
|
@@ -3814,7 +3839,6 @@ async function cmdInstallHcloud() {
|
|
|
3814
3839
|
console.log('\nAfter install, set HCLOUD_BIN if hcloud is not on PATH.');
|
|
3815
3840
|
console.log('\n\x1b[1m\x1b[33m=== Configure credentials SAFELY ===\x1b[0m');
|
|
3816
3841
|
console.log(' Unified credentials (recommended): npx huaweicloud-devkit auth init');
|
|
3817
|
-
console.log(' KooCLI only (alternative): hcloud configure init');
|
|
3818
3842
|
console.log(' NEVER: hcloud configure set --cli-access-key=xxx (AK/SK in shell history!)');
|
|
3819
3843
|
console.log('\nThen run: npx huaweicloud-devkit doctor');
|
|
3820
3844
|
}
|
|
@@ -3871,11 +3895,17 @@ async function readSecret(prompt) {
|
|
|
3871
3895
|
});
|
|
3872
3896
|
}
|
|
3873
3897
|
|
|
3898
|
+
function configuredProfileName() {
|
|
3899
|
+
const name = resolveManagedProfile();
|
|
3900
|
+
return name || 'default';
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3874
3903
|
function configureHcloud(credentials) {
|
|
3875
3904
|
const hcloudBin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
|
|
3876
3905
|
const args = [
|
|
3877
3906
|
'configure',
|
|
3878
3907
|
'set',
|
|
3908
|
+
`--cli-profile=${configuredProfileName()}`,
|
|
3879
3909
|
`--cli-access-key=${credentials.ak}`,
|
|
3880
3910
|
`--cli-secret-key=${credentials.sk}`,
|
|
3881
3911
|
`--cli-region=${credentials.region || ''}`,
|
|
@@ -3889,9 +3919,11 @@ function configureHcloud(credentials) {
|
|
|
3889
3919
|
return {
|
|
3890
3920
|
ok: r.status === 0,
|
|
3891
3921
|
code: r.status,
|
|
3892
|
-
error:
|
|
3893
|
-
.
|
|
3894
|
-
|
|
3922
|
+
error: redactSecrets(
|
|
3923
|
+
String(r.stderr || '')
|
|
3924
|
+
.trim()
|
|
3925
|
+
.slice(0, 240),
|
|
3926
|
+
),
|
|
3895
3927
|
};
|
|
3896
3928
|
}
|
|
3897
3929
|
|
|
@@ -3970,6 +4002,56 @@ async function cmdAuthInit() {
|
|
|
3970
4002
|
console.log(' Restart your agent sessions.');
|
|
3971
4003
|
}
|
|
3972
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
|
+
|
|
3973
4055
|
async function cmdAuthSync() {
|
|
3974
4056
|
const target = parseTarget();
|
|
3975
4057
|
console.log(BANNER);
|
|
@@ -4001,6 +4083,7 @@ async function cmdAuth() {
|
|
|
4001
4083
|
const sub = (process.argv[3] || 'status').toLowerCase();
|
|
4002
4084
|
if (sub === 'init' || sub === 'setup') return cmdAuthInit();
|
|
4003
4085
|
if (sub === 'sync' || sub === 'refresh') return cmdAuthSync();
|
|
4086
|
+
if (sub === 'reconcile') return cmdAuthReconcile();
|
|
4004
4087
|
return cmdAuthStatus();
|
|
4005
4088
|
}
|
|
4006
4089
|
|
|
@@ -4187,7 +4270,7 @@ async function main() {
|
|
|
4187
4270
|
console.log(' status Show installation status');
|
|
4188
4271
|
console.log(' doctor Self-check: hcloud, MCP, skills, auth');
|
|
4189
4272
|
console.log(' install-hcloud Show KooCLI install commands for your OS');
|
|
4190
|
-
console.log(' auth Manage unified auth: init | sync | status');
|
|
4273
|
+
console.log(' auth Manage unified auth: init | sync | status | reconcile');
|
|
4191
4274
|
console.log(' proxy Manage proxy config: init | show | clear');
|
|
4192
4275
|
console.log(' version Print installed plugin version per agent');
|
|
4193
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
|
|