fgpl_tool
fgpl_tool is the command-line tool of the plugin system. With it you can build a .fgpl package, check it the way the app does, create a signing key, and run a plugin against a fake app without installing anything in the client. It lives in Telegram/plugins_host and uses the same code as the app (manifest parser, package validator, plugin host), so what it accepts, the app accepts.
| Command | Purpose | For |
|---|---|---|
keygen <name> |
create a signing key | authors |
pack <directory> <output.fgpl> [--key <name>.key] |
build (and sign) a package | authors |
verify <package.fgpl> |
validate a package and print what it asks for | authors, CI |
run <directory or .fgpl> [options] |
run a plugin against a fake app | authors |
hash <file> |
print the SHA-256 and size of a file | maintainers |
pack-runtime <directory> <output.zip> |
pack a runtime bundle | maintainers |
sign-runtime <index.json> <name>.key <output.json> |
sign a runtime index | maintainers |
Run it with no arguments, with an unknown command or with the wrong number of arguments and it prints its usage and exits with code 64.
Getting the tool
Section titled “Getting the tool”There is no prebuilt fgpl_tool: you build it from the client sources. The CI workflow publishes only the signed plugin host binaries. You need a C++20 compiler, CMake 3.20 or newer, OpenSSL and zlib development files, and the Wasmtime C API unpacked in Telegram/plugins_host/third_party (the directory is wasmtime-v50.0.0-<arch>-<os>-c-api; change it with -D FGPL_WASMTIME_DIR=...).
cd Telegram/plugins_hostcmake -S . -B build -D CMAKE_BUILD_TYPE=Releasecmake --build build --target fgpl_tool fgpl_plugin_host fgpl_plugin_host_unsandboxed| Target | What it is |
|---|---|
fgpl_tool |
the tool itself |
fgpl_plugin_host |
the real plugin host, with the OS sandbox. run starts it |
fgpl_plugin_host_unsandboxed |
a test build of the host without the OS sandbox. run --no-sandbox starts it. It is built only while FGPL_BUILD_TESTS is ON (the default) |
-D FGPL_HOST_ONLY=ON builds only the host and does not build fgpl_tool. run looks for the host next to the fgpl_tool binary, so keep the files of the build directory together (or pass --host).
Typical workflow
Section titled “Typical workflow”- Write
manifest.jsonand the code in one directory. Do not writefilesorsignatureby hand. - Try it:
fgpl_tool run ./hello --no-sandboxand type commands, or put them in a script file. - Package it:
fgpl_tool pack ./hello hello.fgpl. The tool re-opens the result with the app’s validator and refuses to write a package the app would reject. - Check it:
fgpl_tool verify hello.fgpl. Read the permission list and the risk notes the same way a user will. - Sign it (optional but recommended for releases):
fgpl_tool keygen mykeyonce, thenfgpl_tool pack ./hello hello.fgpl --key mykey.key. - Smoke-test the package:
fgpl_tool run hello.fgpl --script smoke.txt.
Run pack and verify in CI before publishing.
keygen
Section titled “keygen”fgpl_tool keygen mykeyWrites mykey.key (the 32-byte Ed25519 seed as one Base64 line) and prints:
private key: mykey.key (keep it secret)public key: <base64 of the 32-byte key>fingerprint: 1a2b 3c4d 5e6f 7a8b 9c0d 1e2f 3a4b 5c6dThe fingerprint is what users see as the publisher. Keep the .key file secret and backed up: you cannot publish updates under the same publisher without it. Exit code 0, or 1 if the file cannot be written. Details of signing are in Packaging.
fgpl_tool pack ./hello hello.fgplfgpl_tool pack ./hello hello.fgpl --key mykey.keyThe only accepted forms are the three or five arguments above. --key must be the fifth argument and be followed by the key file; any other shape prints the usage (64). The command takes manifest.json, computes the files hashes, signs when --key is given, writes the archive, re-validates it, and then prints:
wrote hello.fgpl (3421 bytes)com.example.hello 1.0.0 (Hello)runtime: pythonworks with freshGram >=7.0.0 <8.0.0signature: valid, publisher 1a2b 3c4d 5e6f 7a8b 9c0d 1e2f 3a4b 5c6dpermission: messages.read (required) - Counts the messages it sees.risk: <a risk note, if the permissions call for one>files: 3Everything in the directory is packed, so keep it clean (no .git, __pycache__, editor files). Exit codes: 0 ok, 1 I/O or key error (the message says cannot use the key ... or a file path), 2 the package would not be accepted (the problems are listed), 64 usage.
verify
Section titled “verify”fgpl_tool verify hello.fgplOpens the package with the same checks the app runs and prints the summary shown above (without the wrote line). signature: is valid, publisher <fingerprint> or none (unverified publisher). Exit codes: 0 valid, 1 the file cannot be read, 2 invalid (the problems follow the package is not valid:).
fgpl_tool run ./hello --no-sandboxfgpl_tool run ./hello --script test.txt --state ./.dev-state --grant messages.read,ui.commandLoads a directory (packed in memory like pack, unsigned) or a .fgpl into the real plugin host and drives it with a fake app: three chats, an in-memory message store, a clock you can advance, a mock HTTP layer. The effects of the plugin are printed, and commands are read from standard input or from --script. The options, the REPL command list, the output prefixes and the exit codes are described in Packaging: the dev runner. In short:
| Option | Meaning |
|---|---|
--script <file> |
read commands from a file |
--no-sandbox |
use fgpl_plugin_host_unsandboxed |
--host <path> |
use another host binary (or set FGPL_HOST) |
--runtime <dir> |
a runtime directory with python/ and sdk/ (or set FGPL_RUNTIME_DIR) |
--state <dir> |
keep the plugin’s data between runs |
--folder <dir> |
the folder granted for fs.folder |
--grant a,b / --deny a,b |
grant only these permissions / withhold these |
--app X.Y.Z / --account <id> |
the client version and account the fake app reports |
Where the Python runtime comes from. If you pass --runtime (or set FGPL_RUNTIME_DIR), python/python.wasm, python/lib and sdk/ are taken from there, as in a downloaded runtime bundle. Without it, a tool built from the client sources uses the CPython in Telegram/plugins_host/third_party/cpython-wasi and the SDK in Telegram/plugins_sdk/python, so a Python plugin runs without any extra setup. A copy of the tool moved elsewhere has those paths baked in at build time; if they no longer exist you get no Python runtime: pass --runtime ... or set FGPL_RUNTIME_DIR. Wasm plugins need no runtime at all.
hash, pack-runtime, sign-runtime
Section titled “hash, pack-runtime, sign-runtime”These three are for people who build runtime bundles, not for plugin authors.
fgpl_tool hash file.wasm# "sha256": "sha256-...", "size": 12345pack-runtime <directory> <output.zip> zips every regular file of a directory (sorted by path) and prints one line with "sha256", "size", "hostSha256" and "pythonSha256" (the hashes of the host binary and of python/python.wasm in the directory) ready to paste into a runtime index. sign-runtime <index.json> <name>.key <output.json> removes any signature from a JSON index, signs the canonical JSON with the key and writes the result with signature: {"alg": "ed25519", "value": ...}, then prints the public key. All three exit with 0, or 1 when a file cannot be read or written.
Environment
Section titled “Environment”| Variable | Used by | Meaning |
|---|---|---|
FGPL_HOST |
run |
path of the plugin host binary (overridden by --host) |
FGPL_RUNTIME_DIR |
run |
runtime directory (overridden by --runtime) |
Troubleshooting
Section titled “Troubleshooting”| Message | What to do |
|---|---|
no plugin host at <path> (use --host or --no-sandbox) |
build fgpl_plugin_host (or fgpl_plugin_host_unsandboxed for --no-sandbox) and keep it next to fgpl_tool, or pass --host |
no Python runtime: pass --runtime <dir ...> or set FGPL_RUNTIME_DIR |
the built-in CPython path does not exist; pass a runtime directory |
the package would not be accepted: / the package is not valid: |
fix the listed manifest or file problems; see the manifest reference |
cannot use the key <path> |
the file is not a 44-character Base64 seed made by keygen |
<option> needs a value / unknown option <option> |
usage error (64) |
the run ends with still busy after 10 seconds |
the plugin is slow or blocked; see Runtime and sandbox |
