Skip to content

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.

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=...).

Terminal window
cd Telegram/plugins_host
cmake -S . -B build -D CMAKE_BUILD_TYPE=Release
cmake --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).

  1. Write manifest.json and the code in one directory. Do not write files or signature by hand.
  2. Try it: fgpl_tool run ./hello --no-sandbox and type commands, or put them in a script file.
  3. 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.
  4. Check it: fgpl_tool verify hello.fgpl. Read the permission list and the risk notes the same way a user will.
  5. Sign it (optional but recommended for releases): fgpl_tool keygen mykey once, then fgpl_tool pack ./hello hello.fgpl --key mykey.key.
  6. Smoke-test the package: fgpl_tool run hello.fgpl --script smoke.txt.

Run pack and verify in CI before publishing.

Terminal window
fgpl_tool keygen mykey

Writes 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 5c6d

The 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.

Terminal window
fgpl_tool pack ./hello hello.fgpl
fgpl_tool pack ./hello hello.fgpl --key mykey.key

The 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: python
works with freshGram >=7.0.0 <8.0.0
signature: valid, publisher 1a2b 3c4d 5e6f 7a8b 9c0d 1e2f 3a4b 5c6d
permission: messages.read (required) - Counts the messages it sees.
risk: <a risk note, if the permissions call for one>
files: 3

Everything 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.

Terminal window
fgpl_tool verify hello.fgpl

Opens 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:).

Terminal window
fgpl_tool run ./hello --no-sandbox
fgpl_tool run ./hello --script test.txt --state ./.dev-state --grant messages.read,ui.command

Loads 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.

These three are for people who build runtime bundles, not for plugin authors.

Terminal window
fgpl_tool hash file.wasm
# "sha256": "sha256-...", "size": 12345

pack-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.

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)
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