huaweicloud-devkit 1.1.8 → 1.1.9-next.1
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/integrations/opencode/package-lock.json +24 -0
- package/integrations/opencode/package.json +10 -0
- package/integrations/opencode/plugins/huaweicloud-devkit.js +194 -0
- package/package.json +2 -2
- 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/huawei-ecs/SKILL.md +5 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +16 -2
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +22 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/windows-ssh.md +118 -0
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +3 -3
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +36 -36
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +30 -3
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +35 -6
- package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +1 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +6 -6
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +16 -9
- package/plugins/huaweicloud-core/src/auth/credential-validator.mjs +37 -2
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +14 -27
- package/plugins/huaweicloud-core/src/auth/guidance.mjs +112 -0
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +36 -2
- package/plugins/huaweicloud-core/src/auth/service.mjs +54 -46
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +32 -4
- package/plugins/huaweicloud-core/src/hcloud-probe.mjs +26 -6
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +120 -4
- package/plugins/huaweicloud-core/src/setup-cli.mjs +111 -8
- package/plugins/huaweicloud-core/src/tools.mjs +23 -3
|
@@ -1251,17 +1251,33 @@ export function parseDevbridgeAuthOutput(stdout) {
|
|
|
1251
1251
|
return { authMode, attempts };
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
1254
|
+
// Tunnel lifetime for expose-created hosts, in hours. The report that motivated
|
|
1255
|
+
// #919 uses -e 72 so the shared link survives long after the deploying session
|
|
1256
|
+
// ends; the value is surfaced to callers via exposeTunnel's expiresAt field.
|
|
1257
|
+
export const DEVBRIDGE_HOST_EXPIRY_HOURS = 72;
|
|
1258
|
+
|
|
1254
1259
|
// Start (or replace) a devbridge host bound to a single port, extract the public
|
|
1255
1260
|
// URL, and health-check it. Mirrors the huaweicloud-sandbox skill's Expose flow:
|
|
1256
1261
|
// pre-clean stale tunnels (quota 10006), start host in background, read
|
|
1257
1262
|
// "Tunnel URL: ..." from /tmp/host.log, curl-check, retry once on unreachable.
|
|
1258
|
-
|
|
1263
|
+
// The host MUST survive the exec session that started it (#919): setsid detaches
|
|
1264
|
+
// it from the session's process group, `< /dev/null` drops the controlling tty
|
|
1265
|
+
// stdin, and `disown` removes it from the shell's job table — without all three
|
|
1266
|
+
// the host is reaped when the one-shot session ends and the tunnel dies
|
|
1267
|
+
// immediately after a green in-session health check (the "假成功" bug).
|
|
1268
|
+
export function buildDevbridgeExposeScript(port, expiryHours = DEVBRIDGE_HOST_EXPIRY_HOURS) {
|
|
1259
1269
|
const hostLog = '/tmp/host.log';
|
|
1260
1270
|
return [
|
|
1261
1271
|
`pkill -f "devbridge host" 2>/dev/null || true`,
|
|
1262
1272
|
`sleep 2`,
|
|
1263
1273
|
`devbridge delete-all > /dev/null 2>&1 || true`,
|
|
1264
|
-
|
|
1274
|
+
// #922 (S8): setsid is unavailable on some sandbox images — fall back to a
|
|
1275
|
+
// bare nohup launch so the host still starts; persistence is then judged by
|
|
1276
|
+
// the independent re-verify below, which emits the 请勿分享 warning in the
|
|
1277
|
+
// non-persistent case. disown is guarded: shells without the builtin must
|
|
1278
|
+
// not abort the script.
|
|
1279
|
+
`if command -v setsid > /dev/null 2>&1; then setsid nohup devbridge host -p ${port} -e ${expiryHours} > ${hostLog} 2>&1 < /dev/null & else nohup devbridge host -p ${port} -e ${expiryHours} > ${hostLog} 2>&1 < /dev/null & fi`,
|
|
1280
|
+
`disown 2>/dev/null || true`,
|
|
1265
1281
|
`sleep 12`,
|
|
1266
1282
|
`TUNNEL_URL=$(sed -n 's/.*Tunnel URL: *//p' ${hostLog} | tail -1)`,
|
|
1267
1283
|
`HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
|
|
@@ -1269,10 +1285,37 @@ export function buildDevbridgeExposeScript(port) {
|
|
|
1269
1285
|
`tail -n 5 ${hostLog}`,
|
|
1270
1286
|
`echo "DB_TUNNEL_URL=$TUNNEL_URL"`,
|
|
1271
1287
|
`echo "DB_HTTP_CODE=$HTTP_CODE"`,
|
|
1288
|
+
`echo "DB_EXPIRY_HOURS=${expiryHours}"`,
|
|
1272
1289
|
`echo "DB_QUOTA_ERROR=$(grep -c '10006' ${hostLog} 2>/dev/null || echo 0)"`,
|
|
1273
1290
|
].join('\n');
|
|
1274
1291
|
}
|
|
1275
1292
|
|
|
1293
|
+
// Build the script for the SECOND, independent exec session that re-verifies the
|
|
1294
|
+
// tunnel with a real GET (#919): the in-session health check inside
|
|
1295
|
+
// buildDevbridgeExposeScript cannot prove the host survived session teardown —
|
|
1296
|
+
// a GET from a fresh session can. #922 (B2): the response body is kept and
|
|
1297
|
+
// checked for the native false-success form — a 200 whose body is "tunnel not
|
|
1298
|
+
// found" — and the verdict is emitted as DB_VERIFY_NOT_FOUND so
|
|
1299
|
+
// parseDevbridgeVerifyOutput can catch it. The URL is validated (https,
|
|
1300
|
+
// URL-safe chars only) before being spliced into the shell command.
|
|
1301
|
+
export function buildDevbridgeVerifyScript(tunnelUrl) {
|
|
1302
|
+
const url = String(tunnelUrl || '');
|
|
1303
|
+
if (!/^https:\/\/[A-Za-z0-9:._/-]+$/.test(url)) {
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
`sandbox expose tunnel: invalid tunnel URL ${JSON.stringify(url.slice(0, 80))}. Expected an https:// URL with URL-safe characters (from the expose script's DB_TUNNEL_URL output).`,
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
const bodyFile = '/tmp/devbridge_verify_body';
|
|
1309
|
+
return [
|
|
1310
|
+
`VERIFY_CODE=$(curl -s -o ${bodyFile} -w "%{http_code}" --max-time 10 "${url}" 2>/dev/null || echo "000")`,
|
|
1311
|
+
`VERIFY_NOT_FOUND=$(grep -ci 'tunnel not found' ${bodyFile} 2>/dev/null || true)`,
|
|
1312
|
+
`if [ -z "$VERIFY_NOT_FOUND" ]; then VERIFY_NOT_FOUND=0; fi`,
|
|
1313
|
+
`rm -f ${bodyFile} 2>/dev/null || true`,
|
|
1314
|
+
`echo "DB_VERIFY_HTTP_CODE=$VERIFY_CODE"`,
|
|
1315
|
+
`echo "DB_VERIFY_NOT_FOUND=$VERIFY_NOT_FOUND"`,
|
|
1316
|
+
].join('\n');
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1276
1319
|
// Parse the raw stdout of buildDevbridgeExposeScript into structured fields.
|
|
1277
1320
|
export function parseDevbridgeExposeOutput(stdout) {
|
|
1278
1321
|
const text = String(stdout || '');
|
|
@@ -1284,7 +1327,25 @@ export function parseDevbridgeExposeOutput(stdout) {
|
|
|
1284
1327
|
const httpCode = getField('DB_HTTP_CODE');
|
|
1285
1328
|
const quotaError = getField('DB_QUOTA_ERROR') === '1' || /10006/.test(text);
|
|
1286
1329
|
const tunnelId = (tunnelUrl.match(/^https:\/\/([A-Za-z0-9_-]+)-\d+\./) || [])[1] || '';
|
|
1287
|
-
|
|
1330
|
+
const expiryHours = Number(getField('DB_EXPIRY_HOURS')) || undefined;
|
|
1331
|
+
return { tunnelUrl, httpCode, quotaError, tunnelId, expiryHours };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Parse the raw stdout of buildDevbridgeVerifyScript into structured fields.
|
|
1335
|
+
// Dedicated parser (#922 B1): the verify script emits DB_VERIFY_HTTP_CODE /
|
|
1336
|
+
// DB_VERIFY_NOT_FOUND keys — the anchored ^DB_HTTP_CODE= lookup in
|
|
1337
|
+
// parseDevbridgeExposeOutput never matches them, which used to leave
|
|
1338
|
+
// verifyHttpCode empty and persistent always false.
|
|
1339
|
+
export function parseDevbridgeVerifyOutput(stdout) {
|
|
1340
|
+
const text = String(stdout || '');
|
|
1341
|
+
const getField = (key) => {
|
|
1342
|
+
const m = text.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
1343
|
+
return m ? m[1].trim() : '';
|
|
1344
|
+
};
|
|
1345
|
+
const httpCode = getField('DB_VERIFY_HTTP_CODE');
|
|
1346
|
+
const notFoundCount = Number(getField('DB_VERIFY_NOT_FOUND')) || 0;
|
|
1347
|
+
const notFound = notFoundCount > 0;
|
|
1348
|
+
return { httpCode, notFound };
|
|
1288
1349
|
}
|
|
1289
1350
|
|
|
1290
1351
|
// Parse and validate a TCP port number destined for shell interpolation. Returns
|
|
@@ -1303,7 +1364,10 @@ export function validateTunnelPort(port) {
|
|
|
1303
1364
|
// Expose a sandbox port to a public DevBridge tunnel URL. `port` MUST be the
|
|
1304
1365
|
// ACTUAL port reported by deployNginx (deploy_nginx auto-increments on conflict),
|
|
1305
1366
|
// otherwise the tunnel is bound to a dead port. Returns ok, publicUrl, port,
|
|
1306
|
-
// tunnelId, auth mode, and warnings
|
|
1367
|
+
// tunnelId, auth mode, expiresAt (tunnel lifetime from -e), and warnings; the
|
|
1368
|
+
// tunnel is re-verified from a second independent exec session (#919) and the
|
|
1369
|
+
// result carries verifyHttpCode + persistent so callers can tell a
|
|
1370
|
+
// session-surviving URL from one that dies with the deploying session.
|
|
1307
1371
|
export async function exposeTunnel(workspaceId, { port }, username = 'root', timeoutMs = 90000) {
|
|
1308
1372
|
if (!workspaceId) {
|
|
1309
1373
|
throw new Error('sandbox expose tunnel: workspace_id is required.');
|
|
@@ -1367,11 +1431,58 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1367
1431
|
const warnings = [];
|
|
1368
1432
|
if (parsed.quotaError)
|
|
1369
1433
|
warnings.push('DevBridge tunnel quota (10006) hit — stale tunnels were cleaned up and the host was retried.');
|
|
1434
|
+
|
|
1435
|
+
// #919: re-verify with a real GET from a SECOND, independent exec session.
|
|
1436
|
+
// The in-session health check above cannot prove the host survived session
|
|
1437
|
+
// teardown (setsid/nohup/disown make that likely, not certain). When the
|
|
1438
|
+
// fresh-session GET fails, the URL will die with the deploying session —
|
|
1439
|
+
// say so explicitly instead of returning a link the visitor cannot open.
|
|
1440
|
+
let verifyHttpCode = '';
|
|
1441
|
+
let verifyNotFound = false;
|
|
1442
|
+
let persistent = undefined;
|
|
1443
|
+
if (parsed.tunnelUrl) {
|
|
1444
|
+
try {
|
|
1445
|
+
const verifyResult = await execOneShot(
|
|
1446
|
+
workspaceId,
|
|
1447
|
+
buildDevbridgeVerifyScript(parsed.tunnelUrl),
|
|
1448
|
+
username,
|
|
1449
|
+
30000,
|
|
1450
|
+
);
|
|
1451
|
+
// #922 (B1): parse with the dedicated verify parser — the
|
|
1452
|
+
// DB_VERIFY_HTTP_CODE key is not visible to parseDevbridgeExposeOutput.
|
|
1453
|
+
const verify = parseDevbridgeVerifyOutput(verifyResult.stdout);
|
|
1454
|
+
verifyHttpCode = verify.httpCode || '';
|
|
1455
|
+
// #922 (B2): a native false-success form answers HTTP 200 with a
|
|
1456
|
+
// "tunnel not found" body — treat it as non-persistent.
|
|
1457
|
+
verifyNotFound = verify.notFound;
|
|
1458
|
+
} catch {}
|
|
1459
|
+
// 000 = the fresh session could not reach the tunnel (host already dead
|
|
1460
|
+
// or gateway not serving); 200 with a "tunnel not found" body = the
|
|
1461
|
+
// gateway answered but the tunnel is gone; anything non-200/304 is
|
|
1462
|
+
// surfaced as-is below.
|
|
1463
|
+
persistent = verifyHttpCode !== '' && verifyHttpCode !== '000' && !verifyNotFound;
|
|
1464
|
+
if (!persistent) {
|
|
1465
|
+
if (verifyNotFound) {
|
|
1466
|
+
warnings.push(
|
|
1467
|
+
'复测返回 HTTP 200 但 body 为 "tunnel not found"(原生假成功形态),隧道实际未生效,请勿分享 (the recheck answered HTTP 200 with a "tunnel not found" body — the tunnel is NOT serving; do NOT share this URL).',
|
|
1468
|
+
);
|
|
1469
|
+
} else {
|
|
1470
|
+
warnings.push(
|
|
1471
|
+
'隧道将在会话结束后失效,请勿分享 (the tunnel did NOT survive an independent exec-session recheck — do NOT share this URL; the host process is not persistent).',
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1370
1477
|
if (parsed.httpCode !== '200' && parsed.httpCode !== '304') {
|
|
1371
1478
|
warnings.push(
|
|
1372
1479
|
`Tunnel reachable check returned HTTP ${parsed.httpCode} (expected 200/304). The URL may not be serving yet.`,
|
|
1373
1480
|
);
|
|
1374
1481
|
}
|
|
1482
|
+
const expiresAt =
|
|
1483
|
+
parsed.expiryHours !== undefined
|
|
1484
|
+
? new Date(Date.now() + parsed.expiryHours * 3_600_000).toISOString()
|
|
1485
|
+
: undefined;
|
|
1375
1486
|
return {
|
|
1376
1487
|
ok: true,
|
|
1377
1488
|
publicUrl: parsed.tunnelUrl || undefined,
|
|
@@ -1379,6 +1490,11 @@ export async function exposeTunnel(workspaceId, { port }, username = 'root', tim
|
|
|
1379
1490
|
tunnelId: parsed.tunnelId || undefined,
|
|
1380
1491
|
authMode,
|
|
1381
1492
|
httpCode: parsed.httpCode,
|
|
1493
|
+
verifyHttpCode: verifyHttpCode || undefined,
|
|
1494
|
+
verifyNotFound: verifyNotFound || undefined,
|
|
1495
|
+
persistent,
|
|
1496
|
+
expiresAt,
|
|
1497
|
+
expiresInHours: parsed.expiryHours,
|
|
1382
1498
|
warnings: warnings.length > 0 ? warnings.join(' ') : undefined,
|
|
1383
1499
|
hostLog: String(result.stdout || '').slice(0, 400),
|
|
1384
1500
|
};
|
|
@@ -961,6 +961,21 @@ async function installOpenCode() {
|
|
|
961
961
|
mkdirSync(opcPlugins, { recursive: true });
|
|
962
962
|
copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
|
|
963
963
|
console.log(` Plugin -> ${opcPlugins}`);
|
|
964
|
+
// OpenCode native plugin (#905 OpenCode performance): bridges all 42 MCP tools
|
|
965
|
+
// into OpenCode in-process custom tools (zod schemas + callTool), plus safety
|
|
966
|
+
// gate and telemetry. Local plugins are auto-loaded from this directory.
|
|
967
|
+
const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
|
|
968
|
+
if (existsSync(nativePluginSrc)) {
|
|
969
|
+
copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
|
|
970
|
+
// Deps for the native plugin (zod) live next to the OpenCode config so the
|
|
971
|
+
// plugin can require them at load time.
|
|
972
|
+
const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
|
|
973
|
+
const nativePkgDst = join(configRoot('opencode'), 'package.json');
|
|
974
|
+
if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
|
|
975
|
+
copyFileSync(nativePkgSrc, nativePkgDst);
|
|
976
|
+
}
|
|
977
|
+
console.log(` Native plugin -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
|
|
978
|
+
}
|
|
964
979
|
updateOpenCodeConfig(pluginDest);
|
|
965
980
|
installRuntimeDeps(pluginDest);
|
|
966
981
|
}
|
|
@@ -994,6 +1009,11 @@ function uninstallOpenCode() {
|
|
|
994
1009
|
removeIfExists(pluginFile);
|
|
995
1010
|
console.log(' Removed plugin');
|
|
996
1011
|
}
|
|
1012
|
+
const nativePluginFile = join(configRoot('opencode'), 'plugins', 'huaweicloud-devkit.js');
|
|
1013
|
+
if (existsSync(nativePluginFile)) {
|
|
1014
|
+
removeIfExists(nativePluginFile);
|
|
1015
|
+
console.log(' Removed native plugin');
|
|
1016
|
+
}
|
|
997
1017
|
|
|
998
1018
|
if (removeIfExists(opencodePluginsDir())) {
|
|
999
1019
|
console.log(' Removed MCP server and safety policy');
|
|
@@ -1041,6 +1061,17 @@ async function updateOpenCode() {
|
|
|
1041
1061
|
mkdirSync(opcPlugins, { recursive: true });
|
|
1042
1062
|
copyFileSync(pluginSrc, join(opcPlugins, 'skill-tracker.js'));
|
|
1043
1063
|
console.log(` Plugin updated -> ${opcPlugins}`);
|
|
1064
|
+
// Native plugin refresh (#905 OpenCode performance) — overwrite in place.
|
|
1065
|
+
const nativePluginSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'plugins', 'huaweicloud-devkit.js');
|
|
1066
|
+
if (existsSync(nativePluginSrc)) {
|
|
1067
|
+
copyFileSync(nativePluginSrc, join(opcPlugins, 'huaweicloud-devkit.js'));
|
|
1068
|
+
const nativePkgSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'package.json');
|
|
1069
|
+
const nativePkgDst = join(configRoot('opencode'), 'package.json');
|
|
1070
|
+
if (existsSync(nativePkgSrc) && !existsSync(nativePkgDst)) {
|
|
1071
|
+
copyFileSync(nativePkgSrc, nativePkgDst);
|
|
1072
|
+
}
|
|
1073
|
+
console.log(` Native plugin updated -> ${join(opcPlugins, 'huaweicloud-devkit.js')}`);
|
|
1074
|
+
}
|
|
1044
1075
|
updateOpenCodeConfig(pluginDest);
|
|
1045
1076
|
mkdirSync(pluginDest, { recursive: true });
|
|
1046
1077
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
@@ -4873,6 +4904,53 @@ function configureHcloud(credentials) {
|
|
|
4873
4904
|
};
|
|
4874
4905
|
}
|
|
4875
4906
|
|
|
4907
|
+
function resetHcloudProfile() {
|
|
4908
|
+
const hcloudBin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
|
|
4909
|
+
const r = spawnSync(hcloudBin, ['configure', 'delete', `--cli-profile=${configuredProfileName()}`], {
|
|
4910
|
+
shell: false,
|
|
4911
|
+
windowsHide: true,
|
|
4912
|
+
stdio: 'pipe',
|
|
4913
|
+
timeout: 30000,
|
|
4914
|
+
});
|
|
4915
|
+
return {
|
|
4916
|
+
ok: r.status === 0,
|
|
4917
|
+
error: redactSecrets(
|
|
4918
|
+
String(r.stderr || '')
|
|
4919
|
+
.trim()
|
|
4920
|
+
.slice(0, 240),
|
|
4921
|
+
),
|
|
4922
|
+
};
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
// Verify that the freshly written KooCLI profile can actually authenticate
|
|
4926
|
+
// (#920): a stale/expired security token in the profile survives
|
|
4927
|
+
// `configure set` (it refuses empty values) and poisons every hcloud call with
|
|
4928
|
+
// APIGW.0301 — while the setup output still claims success. Probe with a real
|
|
4929
|
+
// `hcloud STS GetCallerIdentity` and only report synchronized when it passes.
|
|
4930
|
+
function verifyHcloudIdentity(region) {
|
|
4931
|
+
const hcloudBin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
|
|
4932
|
+
// #922 (S2): an empty region must be omitted entirely — `--cli-region=`
|
|
4933
|
+
// sends a literal empty value that KooCLI rejects as an invalid region.
|
|
4934
|
+
const args = ['STS', 'GetCallerIdentity'];
|
|
4935
|
+
if (region) args.push(`--cli-region=${region}`);
|
|
4936
|
+
const r = spawnSync(hcloudBin, args, {
|
|
4937
|
+
shell: false,
|
|
4938
|
+
windowsHide: true,
|
|
4939
|
+
stdio: 'pipe',
|
|
4940
|
+
timeout: 30000,
|
|
4941
|
+
});
|
|
4942
|
+
const output = `${r.stdout || ''}${r.stderr || ''}`;
|
|
4943
|
+
return {
|
|
4944
|
+
ok: r.status === 0 && !/APIGW\.0301|Incorrect IAM authentication/i.test(output),
|
|
4945
|
+
code: r.status,
|
|
4946
|
+
error: redactSecrets(
|
|
4947
|
+
String(r.stderr || r.stdout || '')
|
|
4948
|
+
.trim()
|
|
4949
|
+
.slice(0, 240),
|
|
4950
|
+
),
|
|
4951
|
+
};
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4876
4954
|
function printAuthAgents(agents = {}) {
|
|
4877
4955
|
for (const [agent, info] of Object.entries(agents)) {
|
|
4878
4956
|
console.log(` ${agent}: ${info.configured ? '[OK]' : '[MISSING]'}`);
|
|
@@ -4951,6 +5029,12 @@ async function cmdAuthInit() {
|
|
|
4951
5029
|
}
|
|
4952
5030
|
|
|
4953
5031
|
if (findHcloudBin()) {
|
|
5032
|
+
// #920: `configure set` refuses empty --cli-security-token values, so a
|
|
5033
|
+
// stale token already in the profile survives the write. When auth init
|
|
5034
|
+
// runs WITHOUT a token, delete the profile first (documented manual
|
|
5035
|
+
// bypass) so the recreated profile cannot keep poisoning calls with
|
|
5036
|
+
// APIGW.0301. A missing profile ("nothing to clean") is not an error.
|
|
5037
|
+
if (!securityToken) resetHcloudProfile();
|
|
4954
5038
|
const result = configureHcloud({ ak, sk, region });
|
|
4955
5039
|
if (!result.ok) console.log(`KooCLI update failed: ${result.error || result.code}`);
|
|
4956
5040
|
else {
|
|
@@ -4958,21 +5042,40 @@ async function cmdAuthInit() {
|
|
|
4958
5042
|
if (proj.ok) {
|
|
4959
5043
|
console.log(` Project ID auto-set: ${proj.projectId}`);
|
|
4960
5044
|
} else {
|
|
4961
|
-
|
|
4962
|
-
|
|
5045
|
+
// #920: an unconfigured project_id is an ERROR, not a warning — most
|
|
5046
|
+
// hcloud calls fail with APIGW.0301 until it is set.
|
|
5047
|
+
console.error(
|
|
5048
|
+
`\x1b[31m ERROR: Project ID not auto-set (${proj.reason}). hcloud API calls (ECS/VPC/OBS API paths, sandbox exec) will fail with APIGW.0301 Incorrect IAM authentication information until it is configured. Recover with:\x1b[0m`,
|
|
4963
5049
|
);
|
|
4964
|
-
console.
|
|
4965
|
-
console.
|
|
5050
|
+
console.error(` hcloud IAM KeystoneListProjects --cli-region=${region} --name=${region}`);
|
|
5051
|
+
console.error(` hcloud configure set --cli-project-id=<project_id>`);
|
|
5052
|
+
console.error(` (or re-run "npx huaweicloud-devkit auth init", which auto-sets it)`);
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
// #920: verify the profile can actually authenticate before claiming
|
|
5056
|
+
// success — a poisoned profile must never print "synchronized".
|
|
5057
|
+
const identity = verifyHcloudIdentity(region);
|
|
5058
|
+
if (identity.ok) {
|
|
5059
|
+
console.log('\nCredentials synchronized.');
|
|
5060
|
+
} else {
|
|
5061
|
+
console.error(
|
|
5062
|
+
`\x1b[31m ERROR: credentials were written but the KooCLI profile FAILED the hcloud STS GetCallerIdentity verification (exit ${identity.code}). hcloud calls will fail with APIGW.0301 — "Credentials synchronized" is NOT confirmed. Typical cause: a stale/expired security token in the profile or wrong AK/SK. Recover with:\x1b[0m`,
|
|
5063
|
+
);
|
|
5064
|
+
console.error(` hcloud configure delete --cli-profile=${configuredProfileName()}`);
|
|
5065
|
+
console.error(` then re-run "npx huaweicloud-devkit auth init" with AK/SK only (no token)`);
|
|
5066
|
+
console.error(` Last error: ${identity.error || 'unknown'}`);
|
|
5067
|
+
process.exitCode = 1;
|
|
4966
5068
|
}
|
|
4967
5069
|
}
|
|
4968
5070
|
} else {
|
|
4969
5071
|
console.log('KooCLI not found. Run "npx huaweicloud-devkit install-hcloud" and then "auth sync".');
|
|
4970
5072
|
}
|
|
4971
5073
|
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
5074
|
+
if (process.exitCode !== 1) {
|
|
5075
|
+
console.log('\nNext steps:');
|
|
5076
|
+
console.log(' npx huaweicloud-devkit install --target all');
|
|
5077
|
+
console.log(' Restart your agent sessions.');
|
|
5078
|
+
}
|
|
4976
5079
|
}
|
|
4977
5080
|
|
|
4978
5081
|
async function cmdAuthReconcile() {
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
} from './auth/credentials.mjs';
|
|
54
54
|
import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
|
|
55
55
|
import { fetchWithProxy } from './proxy/proxy-agent.mjs';
|
|
56
|
-
import { fingerprint, runHcloudConfigure, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
56
|
+
import { fingerprint, runHcloudConfigure, runHcloudProfileDelete, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
57
57
|
import {
|
|
58
58
|
getCachedUpdateInfo,
|
|
59
59
|
getUpdateDistTags,
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
resolveSkipFilePath,
|
|
66
66
|
upgradePackage,
|
|
67
67
|
} from './update-check.mjs';
|
|
68
|
-
import { hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
|
|
68
|
+
import { hcloudProbeNextStep, hcloudProbeRaw, probeHcloud } from './hcloud-probe.mjs';
|
|
69
69
|
import { isUsableOfficeaceRoot, readOfficeaceRootMarker } from './officeace-paths.mjs';
|
|
70
70
|
|
|
71
71
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1090,7 +1090,20 @@ function persistCredentials(ak, sk, securityToken, region) {
|
|
|
1090
1090
|
if (!profile) {
|
|
1091
1091
|
hcloud = { ok: false, reason: 'KooCLI current profile unresolved' };
|
|
1092
1092
|
} else {
|
|
1093
|
+
// #920: `configure set` refuses an empty --cli-security-token, so a stale
|
|
1094
|
+
// token already in the profile survives the write and poisons every hcloud
|
|
1095
|
+
// call with APIGW.0301 — while the tool still reports "synchronized". When
|
|
1096
|
+
// persisting WITHOUT a token, walk the documented manual bypass
|
|
1097
|
+
// (delete the profile, then re-configure) so the on-disk profile truly
|
|
1098
|
+
// matches the incoming credentials. The delete is best-effort: a missing
|
|
1099
|
+
// profile ("nothing to clean") must not fail the persist.
|
|
1100
|
+
let profileReset = false;
|
|
1101
|
+
if (!String(securityToken || '')) {
|
|
1102
|
+
const deleted = runHcloudProfileDelete(profile);
|
|
1103
|
+
profileReset = deleted.ok || /not (exist|found)/i.test(deleted.error || '');
|
|
1104
|
+
}
|
|
1093
1105
|
hcloud = runHcloudConfigure(profile, ak, sk, region);
|
|
1106
|
+
hcloud.profileReset = profileReset;
|
|
1094
1107
|
}
|
|
1095
1108
|
if (hcloud.ok) {
|
|
1096
1109
|
writeLastSync({ kooCliProfile: profile, s1Fingerprint: fingerprint(ak, sk) });
|
|
@@ -1101,7 +1114,7 @@ function persistCredentials(ak, sk, securityToken, region) {
|
|
|
1101
1114
|
backedUp: Boolean(before),
|
|
1102
1115
|
obs: obs.ok ? { configured: true } : { configured: false, error: obs.error },
|
|
1103
1116
|
hcloud,
|
|
1104
|
-
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.',
|
|
1117
|
+
note: 'S1 written with configuredBySession (R9), which now takes priority over env-injected credentials; S2(current profile) and S3 synced. Persisting without a security token resets the target profile first (delete + recreate) so a stale token cannot survive the write (#920). Note: running `auth init` later clears the configuredBySession flag and env credentials regain priority.',
|
|
1105
1118
|
};
|
|
1106
1119
|
}
|
|
1107
1120
|
|
|
@@ -1662,6 +1675,7 @@ export async function runVersionCheck(options = {}) {
|
|
|
1662
1675
|
installedVersion: result.installedVersion || undefined,
|
|
1663
1676
|
versionMismatch: Boolean(result.versionMismatch),
|
|
1664
1677
|
nextStep: hcloudProbeNextStep(result),
|
|
1678
|
+
raw: hcloudProbeRaw(result),
|
|
1665
1679
|
authHint:
|
|
1666
1680
|
'If hcloud is installed but commands fail with "配置文件中不存在配置项", run `npx huaweicloud-devkit auth init` outside agent chat to configure credentials.',
|
|
1667
1681
|
};
|
|
@@ -2263,6 +2277,8 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
|
|
|
2263
2277
|
const SERVICE_ALIASES = {
|
|
2264
2278
|
functiongraph: 'FSS',
|
|
2265
2279
|
fgs: 'FSS',
|
|
2280
|
+
ecs: 'Ecs',
|
|
2281
|
+
ECS: 'Ecs',
|
|
2266
2282
|
};
|
|
2267
2283
|
const patternKey = SERVICE_ALIASES[svc] || service;
|
|
2268
2284
|
|
|
@@ -2290,6 +2306,10 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
|
|
|
2290
2306
|
VPC: {
|
|
2291
2307
|
'VPC.0301': 'Bandwidth name is required for PER type EIPs, even though --help marks it optional.',
|
|
2292
2308
|
},
|
|
2309
|
+
Ecs: {
|
|
2310
|
+
'Ecs.0707':
|
|
2311
|
+
'The flavor + image-type combination does not exist ("the product does not exist") — the error does not say which side is wrong, so check BOTH (#922): (1) the flavor is abandoned/sellout in this region/AZ (check os_extra_specs.cond:operation:status and cond:operation:az; see the huawei-ecs skill references/flavors.md for the AZ-vs-global-status conflict rule); (2) the image TYPE is incompatible with the flavor — market-type images fail with KunPeng flavors (kc1/kc2 etc.). Fix: switch the image to a gold-type image or switch to a compatible general-purpose flavor (verified: kc2.small.2 + market Ubuntu → Ecs.0707; s6.medium.2 + gold Ubuntu works), re-listing with hcloud ECS ListFlavors / hcloud IMS ListImages --__imagetype=gold first.',
|
|
2312
|
+
},
|
|
2293
2313
|
APIGW: {
|
|
2294
2314
|
'APIGW.0301':
|
|
2295
2315
|
'Incorrect IAM authentication information. The AK/SK is invalid (check the SK for typos), the security token is missing or expired, or the profile lacks project_id. Fix: re-run "npx huaweicloud-devkit auth init" (it auto-sets project_id), or set it manually: hcloud configure set --cli-project-id=<project_id> after finding it via hcloud IAM KeystoneListProjects --cli-region=<region> --name=<region>.',
|