diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e779da79..f9c05757 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- docs(huawei-ecs): large-user_data JSON-file path (--skeleton / --cli-jsonInput / --dryrun) + anti-pattern warning against abandoning the MCP approval channel (#964) + ## 1.1.10-next.0 (2026-10-20) - chore: sync README beta badge to 1.1.10 diff --git a/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md b/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md index a4d3b1e2..cd0437cd 100644 --- a/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +++ b/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md @@ -129,6 +129,33 @@ SCRIPT hcloud ECS CreateServers ... --server.user_data=$user_data ``` +#### Large user_data: pass via JSON file, do NOT abandon the MCP channel + +The inline form above works only while the base64 blob fits the tool-call parameter limit (a few KB). The API itself accepts user_data up to **32KB before encoding** (`--server.user_data` in `hcloud ECS CreateServers --help`) — so a full bootstrap script (22KB base64 observed in the wild) is well within the API limit but **cannot be handed through a single MCP tool call**. When that happens, keep the MCP approval channel (plan → approve → run) and move the parameters to a local JSON file instead: + +```bash +# 1. Generate the API parameter skeleton (writes a JSON file into the CURRENT directory): +hcloud ECS CreateServers --skeleton +# -> ECS_CreateServers_-.json, structured as header / path / body. +# user_data lives at body.server.user_data (base64-encoded, same as inline). + +# 2. Fill in the real values (name/flavorRef/imageRef/nics/... and body.server.user_data), +# keep only the fields you actually use, then execute with the file: + +hcloud ECS CreateServers --cli-region= --cli-jsonInput=./ECS_CreateServers_-.json + +# 3. Optional pre-flight: --dryrun validates and prints the request payload WITHOUT creating anything: +hcloud ECS CreateServers --cli-region= --cli-jsonInput=./.json --dryrun +``` + +`--cli-jsonInput` / `--skeleton` / `--dryrun` are **global flags — they are NOT listed in operation-level `--help` output** (only in `hcloud --help`), so agents routinely miss them. + +> **Anti-pattern (observed in the wild, #964)**: when the base64 blob exceeded the tool parameter limit, the agent dropped the MCP tools entirely and ran `hcloud` directly in a shell — which also silently bypassed the plan → approve → run approval layer. The JSON-file path above keeps oversized parameters on the approved channel. Never abandon the MCP wrapper just because one parameter is too long. +> +> **Skeleton side effect**: `--skeleton` writes its JSON file to the current working directory (it does not print to stdout). Clean it up or keep it as the record of what was created. + +The security and recovery notes below apply to user_data in general (inline or JSON-file form): + > **Security**: Never embed secrets (passwords, AK/SK, tokens) in user_data. It is stored unencrypted and readable from within the instance via IMDS. Fetch secrets at boot from DEW/CSMS instead. > > **Debugging**: If the script didn't run, check `/var/log/cloud-init-output.log` on the instance. diff --git a/test/structure.test.mjs b/test/structure.test.mjs index f6fd9dbe..c58af83e 100644 --- a/test/structure.test.mjs +++ b/test/structure.test.mjs @@ -195,6 +195,18 @@ test('huawei-iam skill documents the validated primary-account judgment rule (#9 assert.match(quickRef, /assumed-agency/); }); +test('huawei-ecs create-instance documents the large-user_data JSON-file path (#964)', () => { + const body = readFileSync(join(pluginRoot, 'skills', 'huawei-ecs', 'references', 'create-instance.md'), 'utf8'); + // The oversized-parameter escape hatch: file-based params keep the MCP approval channel. + assert.match(body, /--cli-jsonInput/); + assert.match(body, /body\.server\.user_data/); + // The three global flags are absent from operation-level --help, so naming them is HELPFUL knowledge. + assert.match(body, /--skeleton/); + assert.match(body, /--dryrun/); + // The observed anti-pattern (abandoning MCP tools for a direct shell call) must be called out. + assert.match(body, /do NOT abandon the MCP channel/i); +}); + test('skills with references have non-empty reference files', () => { const skillsDir = join(pluginRoot, 'skills'); const skillNames = readdirSync(skillsDir).filter((name) => existsSync(join(skillsDir, name, 'SKILL.md')));