huaweicloud-devkit 1.1.10-next.1 → 1.1.10-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.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/safety/rules/cloud-risk-rules.json +0 -6
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +0 -81
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +4 -43
- package/plugins/huaweicloud-core/src/mcp-config-merge.mjs +1 -1
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +1 -1
- package/plugins/huaweicloud-core/src/safety-policy.mjs +1 -1
- package/plugins/huaweicloud-core/src/setup-cli.mjs +4 -62
- package/integrations/opencode/package-lock.json +0 -24
- package/integrations/opencode/package.json +0 -10
- package/integrations/opencode/plugins/huaweicloud-devkit.js +0 -194
- package/plugins/huaweicloud-core/src/auth/token-exchange.mjs +0 -100
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"mcpName": "io.github.huaweicloud/huaweicloud-devkit",
|
|
4
|
-
"version": "1.1.10-next.
|
|
4
|
+
"version": "1.1.10-next.2",
|
|
5
5
|
"kooCliVersion": "7.3.12",
|
|
6
6
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
7
7
|
"type": "module",
|
|
@@ -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.10-next.
|
|
20
|
+
"version": "1.1.10-next.2",
|
|
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.10-next.
|
|
23
|
+
"version": "1.1.10-next.2",
|
|
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.10-next.
|
|
20
|
+
"version": "1.1.10-next.2",
|
|
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.10-next.
|
|
3
|
+
"version": "1.1.10-next.2",
|
|
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.10-next.
|
|
3
|
+
"version": "1.1.10-next.2",
|
|
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.10-next.
|
|
5
|
+
"version": "1.1.10-next.2",
|
|
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.",
|
|
@@ -429,12 +429,6 @@
|
|
|
429
429
|
"field": "text",
|
|
430
430
|
"regex": "\\bfdisk\\b"
|
|
431
431
|
}
|
|
432
|
-
],
|
|
433
|
-
"none": [
|
|
434
|
-
{
|
|
435
|
-
"field": "text",
|
|
436
|
-
"regex": "\\bhcloud\\b"
|
|
437
|
-
}
|
|
438
432
|
]
|
|
439
433
|
},
|
|
440
434
|
"message": "The sandbox terminal command is destructively dangerous and could destroy the workspace.",
|
|
@@ -393,87 +393,6 @@ export function resolveCredentialsWithRuntime(options = {}) {
|
|
|
393
393
|
return resolveCredentials(options);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
export function resolveXAuthToken() {
|
|
397
|
-
const envToken = process.env.HW_X_AUTH_TOKEN;
|
|
398
|
-
if (present(envToken)) return envToken;
|
|
399
|
-
|
|
400
|
-
const parentCwd = getParentCwd();
|
|
401
|
-
const searchDirs = [process.env.CODEARTS_PROJECT_DIR, parentCwd, process.cwd(), homedir()];
|
|
402
|
-
for (const dir of searchDirs) {
|
|
403
|
-
if (!dir) continue;
|
|
404
|
-
const path = join(dir, '.codeartsdoer', 'mcp', 'mcp_settings.json');
|
|
405
|
-
try {
|
|
406
|
-
if (!existsSync(path)) continue;
|
|
407
|
-
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
408
|
-
const server = pickDevkitMcpServer(config?.mcpServers);
|
|
409
|
-
const token = server?.env?.HW_X_AUTH_TOKEN;
|
|
410
|
-
if (present(token)) return token;
|
|
411
|
-
} catch {}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const workPaths = [
|
|
415
|
-
join(homedir(), '.codearts', 'mcp', 'mcp_settings.json'),
|
|
416
|
-
join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json'),
|
|
417
|
-
];
|
|
418
|
-
for (const path of workPaths) {
|
|
419
|
-
try {
|
|
420
|
-
if (!existsSync(path)) continue;
|
|
421
|
-
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
422
|
-
const server = pickDevkitMcpServer(config?.mcp);
|
|
423
|
-
const token = server?.environment?.HW_X_AUTH_TOKEN;
|
|
424
|
-
if (present(token)) return token;
|
|
425
|
-
} catch {}
|
|
426
|
-
}
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
export function resolveXAuthTokenExpiry() {
|
|
431
|
-
const v = process.env.HW_X_AUTH_TOKEN_EXPIRES_AT;
|
|
432
|
-
if (v && typeof v === 'string' && v.trim()) {
|
|
433
|
-
const asNum = Number(v.trim());
|
|
434
|
-
if (Number.isFinite(asNum) && asNum > 0) return asNum > 1e12 ? asNum : asNum * 1000;
|
|
435
|
-
const t = Date.parse(v.trim());
|
|
436
|
-
if (Number.isFinite(t)) return t;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Fallback: 未注入 HW_X_AUTH_TOKEN_EXPIRES_AT 时,从 token 字符串自解析过期时间。
|
|
440
|
-
// X-Auth-Token 是 CMS/PKCS#7 SignedData(base64 DER),其 eContent 内嵌明文 JSON
|
|
441
|
-
// (含 token.expires_at);base64 解码 + 提取 {"token" JSON 即可读出,无需依赖注入方显式给过期时间。
|
|
442
|
-
const token = resolveXAuthToken();
|
|
443
|
-
if (!token) return null;
|
|
444
|
-
return parseXAuthTokenExpiry(token);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function parseXAuthTokenExpiry(token) {
|
|
448
|
-
try {
|
|
449
|
-
const pad = token + '='.repeat((4 - (token.length % 4)) % 4);
|
|
450
|
-
const text = Buffer.from(pad.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
|
|
451
|
-
const idx = text.indexOf('{"token"');
|
|
452
|
-
if (idx < 0) return null;
|
|
453
|
-
const tail = text.slice(idx);
|
|
454
|
-
let depth = 0;
|
|
455
|
-
let end = -1;
|
|
456
|
-
for (let i = 0; i < tail.length; i += 1) {
|
|
457
|
-
if (tail[i] === '{') depth += 1;
|
|
458
|
-
else if (tail[i] === '}') {
|
|
459
|
-
depth -= 1;
|
|
460
|
-
if (depth === 0) {
|
|
461
|
-
end = i + 1;
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (end < 0) return null;
|
|
467
|
-
const obj = JSON.parse(tail.slice(0, end));
|
|
468
|
-
const expiresAt = obj?.token?.expires_at;
|
|
469
|
-
if (!expiresAt || typeof expiresAt !== 'string') return null;
|
|
470
|
-
const t = Date.parse(expiresAt);
|
|
471
|
-
return Number.isFinite(t) ? t : null;
|
|
472
|
-
} catch {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
396
|
export function lastSyncPath() {
|
|
478
397
|
return join(baseHome(), '.config', 'huaweicloud', '.last_sync');
|
|
479
398
|
}
|
|
@@ -7,13 +7,7 @@ import { dirname, join } from 'node:path';
|
|
|
7
7
|
import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
|
|
8
8
|
import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
9
9
|
import { resolveHcloudCommand } from './hcloud-probe.mjs';
|
|
10
|
-
import {
|
|
11
|
-
parseStsExpiry,
|
|
12
|
-
resolveCredentialsWithRuntime,
|
|
13
|
-
resolveXAuthToken,
|
|
14
|
-
resolveXAuthTokenExpiry,
|
|
15
|
-
} from './auth/credentials.mjs';
|
|
16
|
-
import { getTokenCredentials } from './auth/token-exchange.mjs';
|
|
10
|
+
import { parseStsExpiry, resolveCredentialsWithRuntime } from './auth/credentials.mjs';
|
|
17
11
|
|
|
18
12
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
19
13
|
const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
|
|
@@ -303,7 +297,7 @@ export function redactArgsWithObs(rawArgs) {
|
|
|
303
297
|
return redactSecrets(out);
|
|
304
298
|
}
|
|
305
299
|
|
|
306
|
-
export
|
|
300
|
+
export function resolveStsInjectArgs(rawArgs) {
|
|
307
301
|
const normalized = Array.isArray(rawArgs) ? rawArgs.map(String) : [];
|
|
308
302
|
if (normalized.length === 0) return [];
|
|
309
303
|
const flat = normalized.map(String);
|
|
@@ -356,25 +350,7 @@ export async function resolveStsInjectArgs(rawArgs) {
|
|
|
356
350
|
} catch {
|
|
357
351
|
return [];
|
|
358
352
|
}
|
|
359
|
-
if (!creds || !creds.ak || !creds.sk)
|
|
360
|
-
const token = resolveXAuthToken();
|
|
361
|
-
if (token) {
|
|
362
|
-
const expiry = resolveXAuthTokenExpiry();
|
|
363
|
-
if (expiry !== null && expiry - 60000 <= Date.now()) {
|
|
364
|
-
const err = new Error('X-Auth-Token 已过期,请重启进程重新注入凭证。');
|
|
365
|
-
err.code = 'TOKEN_EXPIRED';
|
|
366
|
-
throw err;
|
|
367
|
-
}
|
|
368
|
-
const temp = await getTokenCredentials(token);
|
|
369
|
-
return [
|
|
370
|
-
'--cli-access-key=' + temp.ak,
|
|
371
|
-
'--cli-secret-key=' + temp.sk,
|
|
372
|
-
'--cli-security-token=' + temp.securityToken,
|
|
373
|
-
];
|
|
374
|
-
}
|
|
375
|
-
return [];
|
|
376
|
-
}
|
|
377
|
-
if (!creds.securityToken) return [];
|
|
353
|
+
if (!creds || !creds.ak || !creds.sk || !creds.securityToken) return [];
|
|
378
354
|
|
|
379
355
|
// R3: if we can derive an expiry and the token is already at/within 60s of
|
|
380
356
|
// expiring, skip injection — using a dead token would make a doomed IAM round
|
|
@@ -492,22 +468,7 @@ export async function runHcloud(args, options = {}) {
|
|
|
492
468
|
// and R3 forbids writing them to disk). Inject per-command so hcloud actually
|
|
493
469
|
// authenticates with the platform-provided STS. Injection args are appended to
|
|
494
470
|
// the EXECUTED args only; classification/approval used the original args.
|
|
495
|
-
|
|
496
|
-
try {
|
|
497
|
-
stsInject = await resolveStsInjectArgs(normalizedArgs);
|
|
498
|
-
} catch (error) {
|
|
499
|
-
if (error?.code === 'TOKEN_INVALID' || error?.code === 'TOKEN_EXCHANGE_FAILED' || error?.code === 'TOKEN_EXPIRED') {
|
|
500
|
-
return {
|
|
501
|
-
ok: false,
|
|
502
|
-
code: error.code,
|
|
503
|
-
error: error.message,
|
|
504
|
-
stderr: '',
|
|
505
|
-
stdout: '',
|
|
506
|
-
plan,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
throw error;
|
|
510
|
-
}
|
|
471
|
+
const stsInject = resolveStsInjectArgs(normalizedArgs);
|
|
511
472
|
if (stsInject.length > 0) {
|
|
512
473
|
plan.rawArgs = [...plan.rawArgs, ...stsInject];
|
|
513
474
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Program-owned fields (the node executable + mcp-server.mjs path, required env
|
|
4
4
|
// keys) are corrected by the installer; everything else belongs to the user.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function isPlainObject(value) {
|
|
7
7
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -19,7 +19,7 @@ export function loadRiskRules(options = {}) {
|
|
|
19
19
|
function redactEvidence(text) {
|
|
20
20
|
return String(text)
|
|
21
21
|
.replace(
|
|
22
|
-
/((?:access[_-]?key|secret[_-]?key|secret|security[_-]?token|x[_-]?auth[_-]?token|
|
|
22
|
+
/((?:access[_-]?key|secret[_-]?key|secret|security[_-]?token|x[_-]?auth[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
|
|
23
23
|
'$1<redacted>',
|
|
24
24
|
)
|
|
25
25
|
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
|
|
@@ -39,7 +39,7 @@ function redactString(text) {
|
|
|
39
39
|
// Redact the ENTIRE value of this arg — not just the first whitespace token.
|
|
40
40
|
.replace(/((?:user[_-]?data|metadata|private[_-]?key)\s*[:=]\s*).*/gi, '$1<redacted>')
|
|
41
41
|
.replace(
|
|
42
|
-
/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|
|
|
42
|
+
/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
|
|
43
43
|
'$1<redacted>',
|
|
44
44
|
)
|
|
45
45
|
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>')
|
|
@@ -44,7 +44,6 @@ import {
|
|
|
44
44
|
applyUserDelta,
|
|
45
45
|
inheritPeerUserEnv,
|
|
46
46
|
detectOpenCodeEra,
|
|
47
|
-
isPlainObject,
|
|
48
47
|
mergeOpenCodeEntry,
|
|
49
48
|
} from './mcp-config-merge.mjs';
|
|
50
49
|
import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
|
|
@@ -704,7 +703,7 @@ function resolveOpenCodeEra() {
|
|
|
704
703
|
return (resolvedOpenCodeEra = 'v2');
|
|
705
704
|
}
|
|
706
705
|
|
|
707
|
-
function updateOpenCodeConfig(pluginDir
|
|
706
|
+
function updateOpenCodeConfig(pluginDir) {
|
|
708
707
|
const configPath = opencodeConfigFile();
|
|
709
708
|
const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
710
709
|
const era = resolveOpenCodeEra();
|
|
@@ -723,25 +722,6 @@ function updateOpenCodeConfig(pluginDir, { nativePluginInstalled = false } = {})
|
|
|
723
722
|
|
|
724
723
|
const { config: next, existing, changed } = mergeOpenCodeEntry(config, { mcpPath, era });
|
|
725
724
|
|
|
726
|
-
// Native-plugin mode: the in-process plugin already bridges all 42 tools with
|
|
727
|
-
// the shared callTool layer (names, approval gate, telemetry unchanged). Keep
|
|
728
|
-
// the MCP entry for manual fallback but leave it disabled so OpenCode does
|
|
729
|
-
// not register 42 duplicate `huaweicloud-devkit_*` copies (#940 follow-up).
|
|
730
|
-
let mcpDisabled = false;
|
|
731
|
-
if (nativePluginInstalled) {
|
|
732
|
-
const key = 'huaweicloud-devkit';
|
|
733
|
-
if (era === 'v2') {
|
|
734
|
-
const servers = isPlainObject(next.mcp?.servers) ? next.mcp.servers : null;
|
|
735
|
-
if (servers && servers[key] && servers[key].disabled !== true) {
|
|
736
|
-
servers[key] = { ...servers[key], disabled: true };
|
|
737
|
-
mcpDisabled = true;
|
|
738
|
-
}
|
|
739
|
-
} else if (next.mcp?.[key] && next.mcp[key].enabled !== false) {
|
|
740
|
-
next.mcp[key] = { ...next.mcp[key], enabled: false };
|
|
741
|
-
mcpDisabled = true;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
725
|
// Restore user fields saved by a previous uninstall (issue #615), on fresh install only.
|
|
746
726
|
if (!existing) {
|
|
747
727
|
const delta = takeAgentDelta('opencode');
|
|
@@ -754,15 +734,12 @@ function updateOpenCodeConfig(pluginDir, { nativePluginInstalled = false } = {})
|
|
|
754
734
|
}
|
|
755
735
|
}
|
|
756
736
|
|
|
757
|
-
if (existing && !changed
|
|
737
|
+
if (existing && !changed) {
|
|
758
738
|
console.log(` OpenCode MCP config unchanged: ${configPath}`);
|
|
759
739
|
return;
|
|
760
740
|
}
|
|
761
741
|
|
|
762
742
|
writeMcpSettingsFile(configPath, next);
|
|
763
|
-
if (mcpDisabled) {
|
|
764
|
-
console.log(` OpenCode MCP entry disabled (native plugin active — no duplicate tools) -> ${configPath}`);
|
|
765
|
-
}
|
|
766
743
|
console.log(
|
|
767
744
|
existing
|
|
768
745
|
? ` OpenCode MCP config merged (user fields preserved, ${era}): ${configPath}`
|
|
@@ -984,24 +961,7 @@ async function installOpenCode() {
|
|
|
984
961
|
mkdirSync(opcPlugins, { recursive: true });
|
|
985
962
|
copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
|
|
986
963
|
console.log(` Plugin -> ${opcPlugins}`);
|
|
987
|
-
|
|
988
|
-
// into OpenCode in-process custom tools (zod schemas + callTool), plus safety
|
|
989
|
-
// gate and telemetry. Local plugins are auto-loaded from this directory.
|
|
990
|
-
const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
|
|
991
|
-
let nativePluginInstalled = false;
|
|
992
|
-
if (existsSync(nativePluginSrc)) {
|
|
993
|
-
copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
|
|
994
|
-
// Deps for the native plugin (zod) live next to the OpenCode config so the
|
|
995
|
-
// plugin can require them at load time.
|
|
996
|
-
const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
|
|
997
|
-
const nativePkgDst = join(configRoot('opencode'), 'package.json');
|
|
998
|
-
if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
|
|
999
|
-
copyFileSync(nativePkgSrc, nativePkgDst);
|
|
1000
|
-
}
|
|
1001
|
-
nativePluginInstalled = true;
|
|
1002
|
-
console.log(` Native plugin -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
|
|
1003
|
-
}
|
|
1004
|
-
updateOpenCodeConfig(pluginDest, { nativePluginInstalled });
|
|
964
|
+
updateOpenCodeConfig(pluginDest);
|
|
1005
965
|
installRuntimeDeps(pluginDest);
|
|
1006
966
|
}
|
|
1007
967
|
|
|
@@ -1034,11 +994,6 @@ function uninstallOpenCode() {
|
|
|
1034
994
|
removeIfExists(pluginFile);
|
|
1035
995
|
console.log(' Removed plugin');
|
|
1036
996
|
}
|
|
1037
|
-
const nativePluginFile = join(configRoot('opencode'), 'plugins', 'huaweicloud-devkit.js');
|
|
1038
|
-
if (existsSync(nativePluginFile)) {
|
|
1039
|
-
removeIfExists(nativePluginFile);
|
|
1040
|
-
console.log(' Removed native plugin');
|
|
1041
|
-
}
|
|
1042
997
|
|
|
1043
998
|
if (removeIfExists(opencodePluginsDir())) {
|
|
1044
999
|
console.log(' Removed MCP server and safety policy');
|
|
@@ -1086,20 +1041,7 @@ async function updateOpenCode() {
|
|
|
1086
1041
|
mkdirSync(opcPlugins, { recursive: true });
|
|
1087
1042
|
copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
|
|
1088
1043
|
console.log(` Plugin updated -> ${opcPlugins}`);
|
|
1089
|
-
|
|
1090
|
-
const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
|
|
1091
|
-
let nativePluginInstalled = false;
|
|
1092
|
-
if (existsSync(nativePluginSrc)) {
|
|
1093
|
-
copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
|
|
1094
|
-
const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
|
|
1095
|
-
const nativePkgDst = join(configRoot('opencode'), 'package.json');
|
|
1096
|
-
if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
|
|
1097
|
-
copyFileSync(nativePkgSrc, nativePkgDst);
|
|
1098
|
-
}
|
|
1099
|
-
nativePluginInstalled = true;
|
|
1100
|
-
console.log(` Native plugin updated -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
|
|
1101
|
-
}
|
|
1102
|
-
updateOpenCodeConfig(pluginDest, { nativePluginInstalled });
|
|
1044
|
+
updateOpenCodeConfig(pluginDest);
|
|
1103
1045
|
mkdirSync(pluginDest, { recursive: true });
|
|
1104
1046
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
1105
1047
|
installRuntimeDeps(pluginDest);
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@huaweicloud-devkit/opencode-integration",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"lockfileVersion": 3,
|
|
5
|
-
"requires": true,
|
|
6
|
-
"packages": {
|
|
7
|
-
"": {
|
|
8
|
-
"name": "@huaweicloud-devkit/opencode-integration",
|
|
9
|
-
"version": "0.0.0",
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"zod": "^3.11.0"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"node_modules/zod": {
|
|
15
|
-
"version": "3.25.76",
|
|
16
|
-
"resolved": "https://repo.huaweicloud.com/repository/npm/zod/-/zod-3.25.76.tgz",
|
|
17
|
-
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"funding": {
|
|
20
|
-
"url": "https://github.com/sponsors/colinhacks"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@huaweicloud-devkit/opencode-integration",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "Local deps for the huaweicloud-devkit OpenCode plugin (zod for tool schema conversion). Installed into ~/.config/opencode by 'npx huaweicloud-devkit install --target opencode'.",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"zod": "^3.11.0"
|
|
9
|
-
}
|
|
10
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
// huaweicloud-devkit OpenCode native plugin
|
|
2
|
-
//
|
|
3
|
-
// Bridges all 42 MCP tools into OpenCode in-process custom tools via the shared
|
|
4
|
-
// callTool() business layer, plus:
|
|
5
|
-
// - tool.execute.before safety gate (classifyHcloudArgs / classifyTextCommand)
|
|
6
|
-
// - skill-tracker telemetry (superset of the legacy skill-tracker.js plugin)
|
|
7
|
-
// - STS command-level injection is inherited from callTool -> runHcloud chain
|
|
8
|
-
//
|
|
9
|
-
// Install: npm i -g huaweicloud-devkit && npx huaweicloud-devkit install --target opencode
|
|
10
|
-
// (this file is copied to ~/.config/opencode/plugins/huaweicloud-devkit.js and
|
|
11
|
-
// auto-loaded by OpenCode at startup — local plugins need no config entry)
|
|
12
|
-
//
|
|
13
|
-
// Compatibility contract (unchanged from MCP mode):
|
|
14
|
-
// - tool names stay `huaweicloud_*` (skills/hook matchers keep working)
|
|
15
|
-
// - approval gate: run_approved_command still requires args+approvalToken+approvedByUser
|
|
16
|
-
// - credentials: resolveCredentials reads env (HW_*) then S1 — same priority as MCP
|
|
17
|
-
|
|
18
|
-
import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
19
|
-
import { join, dirname } from 'node:path';
|
|
20
|
-
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import { createRequire } from 'node:module';
|
|
22
|
-
|
|
23
|
-
const require = createRequire(import.meta.url);
|
|
24
|
-
const z = require('zod');
|
|
25
|
-
|
|
26
|
-
// devkit core — resolved from the installed plugin tree next to this file
|
|
27
|
-
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
const DEVKIT_SRC = join(selfDir, '..', 'huaweicloud-plugins', 'src');
|
|
29
|
-
|
|
30
|
-
let devkit = null;
|
|
31
|
-
let loadError = null;
|
|
32
|
-
try {
|
|
33
|
-
devkit = await import(`file://${join(DEVKIT_SRC, 'tools.mjs')}`);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
loadError = error;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── telemetry (superset of legacy skill-tracker.js) ─────────────────────────
|
|
39
|
-
|
|
40
|
-
const TELEMETRY_DIR = join(DEVKIT_SRC, '..', 'telemetry');
|
|
41
|
-
function writeEvent(key, value, extra = {}) {
|
|
42
|
-
try {
|
|
43
|
-
if (!existsSync(TELEMETRY_DIR)) mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
44
|
-
appendFileSync(join(TELEMETRY_DIR, 'hook-events.jsonl'), JSON.stringify({ key, value, ...extra }) + '\n');
|
|
45
|
-
} catch {
|
|
46
|
-
/* telemetry must never break the host */
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isHuaweiCloudSkill(name) {
|
|
51
|
-
return typeof name === 'string' && /^huawei/i.test(name);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── JSON Schema -> Zod converter (covers the devkit inputSchema subset) ─────
|
|
55
|
-
|
|
56
|
-
function jsonSchemaToZod(schema) {
|
|
57
|
-
// schema is { type:'object', properties:{...}, required:[...] }
|
|
58
|
-
const shape = {};
|
|
59
|
-
const required = new Set(schema?.required || []);
|
|
60
|
-
for (const [name, prop] of Object.entries(schema?.properties || {})) {
|
|
61
|
-
let node;
|
|
62
|
-
switch (prop?.type) {
|
|
63
|
-
case 'string':
|
|
64
|
-
node = z.string();
|
|
65
|
-
if (prop.enum) {
|
|
66
|
-
const vals = prop.enum.filter((v) => typeof v === 'string');
|
|
67
|
-
if (vals.length > 0) node = z.enum(vals);
|
|
68
|
-
}
|
|
69
|
-
break;
|
|
70
|
-
case 'number':
|
|
71
|
-
case 'integer':
|
|
72
|
-
node = z.number();
|
|
73
|
-
break;
|
|
74
|
-
case 'boolean':
|
|
75
|
-
node = z.boolean();
|
|
76
|
-
break;
|
|
77
|
-
case 'array':
|
|
78
|
-
node = z.array(z.any());
|
|
79
|
-
break;
|
|
80
|
-
case 'object':
|
|
81
|
-
default:
|
|
82
|
-
node = z.any(); // nested objects (e.g. git{repo_url}) stay permissive
|
|
83
|
-
}
|
|
84
|
-
if (prop?.description) node = node.describe(prop.description);
|
|
85
|
-
shape[name] = required.has(name) ? node : node.optional();
|
|
86
|
-
}
|
|
87
|
-
return shape;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── safety gate (mirrors the MCP/hook deny semantics) ──────────────────────
|
|
91
|
-
|
|
92
|
-
const DENY_TOOLS = new Set([
|
|
93
|
-
'huaweicloud_run_approved_command', // has its own in-tool approval gate (token check)
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
|
-
async function safetyGate(toolName, args) {
|
|
97
|
-
if (!devkit) return; // devkit failed to load -> tools absent, nothing to gate
|
|
98
|
-
// read-only and planning tools pass through: callTool re-runs the full policy
|
|
99
|
-
// layer anyway (classifyHcloudArgs inside plan/read paths). The gate below is
|
|
100
|
-
// a fast pre-check for the raw hcloud-ish tools only.
|
|
101
|
-
if (DENY_TOOLS.has(toolName)) return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── tool bridging ───────────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
function buildBridgedTools() {
|
|
107
|
-
if (!devkit) return {};
|
|
108
|
-
const tools = {};
|
|
109
|
-
const definitions = devkit.TOOL_DEFINITIONS || [];
|
|
110
|
-
for (const def of definitions) {
|
|
111
|
-
tools[def.name] = {
|
|
112
|
-
description: def.description,
|
|
113
|
-
args: jsonSchemaToZod(def.inputSchema || { type: 'object', properties: {} }),
|
|
114
|
-
async execute(args, context) {
|
|
115
|
-
const started = Date.now();
|
|
116
|
-
try {
|
|
117
|
-
await safetyGate(def.name, args);
|
|
118
|
-
const result = await devkit.callTool(def.name, args, { cwd: context?.directory });
|
|
119
|
-
writeEvent('tool:invoke', def.name, { durationMs: Date.now() - started });
|
|
120
|
-
return typeof result === 'string' ? result : { output: JSON.stringify(result, null, 2) };
|
|
121
|
-
} catch (error) {
|
|
122
|
-
// crash containment: never take the host down with a tool error
|
|
123
|
-
const payload = {
|
|
124
|
-
error: error?.message || String(error),
|
|
125
|
-
code: error?.code,
|
|
126
|
-
action: error?.action || error?.nextStep,
|
|
127
|
-
};
|
|
128
|
-
writeEvent('tool:error', def.name, { message: payload.error });
|
|
129
|
-
return { output: JSON.stringify(payload, null, 2) };
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
return tools;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── plugin export (v1 Hooks contract, IDE + CLI dual form) ─────────────────
|
|
138
|
-
|
|
139
|
-
function getHooks() {
|
|
140
|
-
return {
|
|
141
|
-
'tool.execute.before': function (input, output) {
|
|
142
|
-
try {
|
|
143
|
-
if (input?.tool === 'skill') {
|
|
144
|
-
const name = output?.args?.name;
|
|
145
|
-
if (isHuaweiCloudSkill(name)) writeEvent('skill:retrieve', name);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (input?.tool === 'bash') {
|
|
149
|
-
const cmd = output?.args?.command || '';
|
|
150
|
-
if (!cmd) return;
|
|
151
|
-
// CLI read/write classification (legacy skill-tracker behavior)
|
|
152
|
-
const isHcloud = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+/i.test(cmd);
|
|
153
|
-
if (isHcloud) {
|
|
154
|
-
const isRead = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i.test(cmd);
|
|
155
|
-
writeEvent(isRead ? 'cli:read' : 'cli:write', cmd.slice(0, 120), { capability: 'cli' });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
} catch {
|
|
159
|
-
/* hooks must never throw */
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
event: function ({ event }) {
|
|
163
|
-
try {
|
|
164
|
-
if (event?.type === 'message.part.updated') {
|
|
165
|
-
const text = event?.properties?.part?.text;
|
|
166
|
-
if (typeof text === 'string') {
|
|
167
|
-
const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
|
|
168
|
-
if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
} catch {
|
|
172
|
-
/* ignore */
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function plugin() {
|
|
179
|
-
const hooks = getHooks();
|
|
180
|
-
if (loadError) {
|
|
181
|
-
// Surface a single actionable line instead of breaking startup
|
|
182
|
-
console.error(`[huaweicloud-devkit] core failed to load: ${loadError.message}`);
|
|
183
|
-
console.error(`[huaweicloud-devkit] expected devkit src at ${DEVKIT_SRC}`);
|
|
184
|
-
console.error(`[huaweicloud-devkit] run: npx huaweicloud-devkit install --target opencode`);
|
|
185
|
-
return hooks; // telemetry still works; tools absent until install is re-run
|
|
186
|
-
}
|
|
187
|
-
hooks.tool = buildBridgedTools();
|
|
188
|
-
return hooks;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
plugin.id = 'huaweicloud-devkit';
|
|
192
|
-
plugin.server = async () => plugin();
|
|
193
|
-
|
|
194
|
-
export default plugin;
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { fetchWithProxy } from '../proxy/proxy-agent.mjs';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_DURATION_SECONDS = 900;
|
|
4
|
-
const DEFAULT_IAM_ENDPOINT = 'https://iam.myhuaweicloud.com';
|
|
5
|
-
|
|
6
|
-
export async function exchangeTokenForCredentials(token, options = {}) {
|
|
7
|
-
const durationSeconds = options.durationSeconds ?? DEFAULT_DURATION_SECONDS;
|
|
8
|
-
const iamEndpoint = options.iamEndpoint || process.env.HW_TOKEN_IAM_ENDPOINT || DEFAULT_IAM_ENDPOINT;
|
|
9
|
-
const fetchImpl = options.fetchImpl;
|
|
10
|
-
|
|
11
|
-
const body = {
|
|
12
|
-
auth: {
|
|
13
|
-
identity: {
|
|
14
|
-
methods: ['token'],
|
|
15
|
-
token: { id: token, duration_seconds: durationSeconds },
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const fetchOpts = {
|
|
21
|
-
method: 'POST',
|
|
22
|
-
headers: { 'Content-Type': 'application/json;charset=utf8' },
|
|
23
|
-
body: JSON.stringify(body),
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const url = `${iamEndpoint}/v3.0/OS-CREDENTIAL/securitytokens`;
|
|
27
|
-
|
|
28
|
-
let resp;
|
|
29
|
-
try {
|
|
30
|
-
resp = fetchImpl ? await fetchImpl(url, fetchOpts) : await fetchWithProxy(url, fetchOpts);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
const err = new Error(`Token exchange request failed: ${error.message}`);
|
|
33
|
-
err.code = 'TOKEN_EXCHANGE_FAILED';
|
|
34
|
-
throw err;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
38
|
-
const err = new Error('X-Auth-Token is invalid or expired.');
|
|
39
|
-
err.code = 'TOKEN_INVALID';
|
|
40
|
-
throw err;
|
|
41
|
-
}
|
|
42
|
-
if (resp.status !== 201) {
|
|
43
|
-
const err = new Error(`Token exchange returned unexpected status ${resp.status}.`);
|
|
44
|
-
err.code = 'TOKEN_EXCHANGE_FAILED';
|
|
45
|
-
throw err;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let data;
|
|
49
|
-
try {
|
|
50
|
-
data = await resp.json();
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const err = new Error(`Token exchange returned malformed JSON: ${error.message}`);
|
|
53
|
-
err.code = 'TOKEN_EXCHANGE_FAILED';
|
|
54
|
-
throw err;
|
|
55
|
-
}
|
|
56
|
-
const cred = data?.credential;
|
|
57
|
-
if (!cred?.access || !cred?.secret || !cred?.securitytoken) {
|
|
58
|
-
const err = new Error('Token exchange response missing credential fields.');
|
|
59
|
-
err.code = 'TOKEN_EXCHANGE_FAILED';
|
|
60
|
-
throw err;
|
|
61
|
-
}
|
|
62
|
-
// IAM always returns expires_at; if it is absent (or unparseable) we store
|
|
63
|
-
// expiresAt:null, which never satisfies the `cached.expiresAt && …` guard in
|
|
64
|
-
// getTokenCredentials — so the cache is always a miss and we deliberately
|
|
65
|
-
// re-exchange on every call rather than risk caching credentials forever.
|
|
66
|
-
const expiresAt = cred.expires_at ? Date.parse(cred.expires_at) : NaN;
|
|
67
|
-
return {
|
|
68
|
-
ak: cred.access,
|
|
69
|
-
sk: cred.secret,
|
|
70
|
-
securityToken: cred.securitytoken,
|
|
71
|
-
expiresAt: Number.isFinite(expiresAt) ? expiresAt : null,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const GRACE_MS = 60 * 1000;
|
|
76
|
-
|
|
77
|
-
let cached = null; // { ak, sk, securityToken, expiresAt }
|
|
78
|
-
let inflight = null; // Promise 单飞
|
|
79
|
-
|
|
80
|
-
export function __resetTokenCache() {
|
|
81
|
-
cached = null;
|
|
82
|
-
inflight = null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function getTokenCredentials(token, options = {}) {
|
|
86
|
-
const now = Date.now();
|
|
87
|
-
if (cached && cached.expiresAt && cached.expiresAt - GRACE_MS > now) {
|
|
88
|
-
return cached;
|
|
89
|
-
}
|
|
90
|
-
if (inflight) return inflight;
|
|
91
|
-
inflight = (async () => {
|
|
92
|
-
try {
|
|
93
|
-
cached = await exchangeTokenForCredentials(token, options);
|
|
94
|
-
return cached;
|
|
95
|
-
} finally {
|
|
96
|
-
inflight = null;
|
|
97
|
-
}
|
|
98
|
-
})();
|
|
99
|
-
return inflight;
|
|
100
|
-
}
|