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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.1-next.13",
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.13",
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.13",
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.13",
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.13",
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.13",
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.13",
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
- - **Unified credentials** (preferred): `npx huaweicloud-devkit auth init`. This is the DevKit's primary auth path; `hcloud configure init` only covers KooCLI.
74
- - **KooCLI only, interactive** (SAFE): `hcloud configure init` — prompts for AK/SK via terminal input. Values do NOT enter shell history.
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 | Mechanism | Persistence |
129
- | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------- |
130
- | 1 | Runtime credentials | `huaweicloud_auth_init` tool | Memory (cleared on MCP restart) |
131
- | 2 | Environment variables | `HW_ACCESS_KEY` / `HW_SECRET_KEY` | MCP process lifetime |
132
- | 3 | CodeArts / CodeArts Work | `.codeartsdoer/mcp/mcp_settings.json` (project → user) or `.codeartswork/mcp/mcp_settings.json` (user) | File |
133
- | 4 | Global config file | `~/.config/huaweicloud/credentials.json` | Permanent |
134
- | 5 | KooCLI profile | `~/.hcloud/config.json` (KooCLI only) | Permanent |
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. This overrides all other sources for the current MCP process.
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 { chmodSync, existsSync, mkdirSync, readFileSync, readlinkSync, statSync, writeFileSync } from 'node:fs';
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 { globalCredentialsPath, obsConfigPath, readGlobalCredentials, writeObsConfig } from './credentials.mjs';
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
- let hcloud = { ok: false, message: 'KooCLI not installed' };
58
- if (hcloudInstalled()) {
59
- const bin = process.env.HCLOUD_BIN || 'hcloud';
60
- const r = spawnSync(
61
- bin,
62
- [
63
- 'configure',
64
- 'set',
65
- `--cli-access-key=${credentials.ak}`,
66
- `--cli-secret-key=${credentials.sk}`,
67
- `--cli-region=${credentials.region || ''}`,
68
- ],
69
- {
70
- shell: false,
71
- windowsHide: true,
72
- stdio: 'pipe',
73
- timeout: 30000,
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
- return {
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: String(r.stderr || '')
3893
- .trim()
3894
- .slice(0, 240),
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 `hcloud configure set --cli-access-key=<AK> --cli-secret-key=<SK> --cli-region=<region>` outside agent chat to configure credentials.',
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 "hcloud configure set --cli-region=<region>" outside agent chat to set a default region.',
1412
+ nextStep: 'Run "npx huaweicloud-devkit auth init" outside agent chat to configure credentials and region.',
1247
1413
  };
1248
1414
  }
1249
1415