Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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_<region>-<timestamp>.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=<region> --cli-jsonInput=./ECS_CreateServers_<region>-<timestamp>.json

# 3. Optional pre-flight: --dryrun validates and prints the request payload WITHOUT creating anything:
hcloud ECS CreateServers --cli-region=<region> --cli-jsonInput=./<file>.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.
Expand Down
12 changes: 12 additions & 0 deletions test/structure.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')));
Expand Down
Loading