Forum teuk.org

🧭🗝️ The Marauder’s Map Opens to Perl, Python and Tcl

in Mediabot · started by TeuK · 6d ago

TeuK · 6d ago

Mediabot v3 has gained a new kind of magic: trusted external plugins can now be written in Perl, Python or Tcl, routed to selected IRC commands, validated through a strict JSON contract, and either observed safely in dry-run mode or applied through explicit security gates.

This work started as a small experimental bridge. It is now a documented, tested and guarded plugin runtime.


🏰 What has changed?

Mediabot already had a large collection of built-in IRC commands. The goal was not to replace them, nor to turn the bot into an unrestricted script launcher.

The new bridge adds a controlled extension point:

IRC command
    ↓
CommandRegistry / EventBus
    ↓
Mediabot::Plugin::ScriptDryRun
    ↓
Mediabot::ScriptRunner
    ↓
Perl / Python / Tcl script
    ↓
mediabot-script-v1 JSON response
    ↓
Mediabot::ScriptActionRunner
    ↓
validated reply / notice / log action

The historical module name remains:

Mediabot::Plugin::ScriptDryRun

Despite that name, it now supports both dry-run and explicitly gated apply modes.


🜁 A trusted script boundary — not a shell

External scripts are launched out of process with IPC::Open3 and an argv array.

No shell command line is constructed.

The runner rejects:

  • absolute paths;
  • parent-directory traversal;
  • backslash path tricks;
  • unsupported extensions;
  • symlink escapes outside plugins/scripts;
  • mismatched interpreter or execution-plan identities;
  • malformed or oversized JSON input;
  • timeouts and excessive output;
  • too many returned actions.

Supported extensions are:

.pl   Perl
.py   Python
.tcl  Tcl

These scripts are trusted project extensions. They are not a sandbox for untrusted code uploaded by IRC users.


🧵 The mediabot-script-v1 protocol

Every script receives one JSON object on standard input.

A simplified request looks like this:

{
  "protocol": "mediabot-script-v1",
  "event": "public_command",
  "data": {
    "command": "proll",
    "channel": "#teuk",
    "nick": "Te[u]K",
    "args": ["2d6"]
  }
}

The script returns one JSON object on standard output:

{
  "protocol": "mediabot-script-v1",
  "ok": true,
  "actions": [
    {
      "type": "reply",
      "text": "🎲 2d6 → 9"
    },
    {
      "type": "log",
      "level": "info",
      "text": "roll.py completed"
    }
  ]
}

The response is decoded and validated before any action can be applied.


🧿 Actions supported by the bridge

The current protocol understands four action types:

reply
notice
log
timer

reply, notice and log can be applied today.

timer is validated and reserved by the protocol, but deliberately remains non-applied until a dedicated timer execution layer is implemented.

IRC actions are protected against:

  • CR/LF and NUL injection;
  • whitespace or multi-recipient target injection;
  • oversized text;
  • malformed scalar fields;
  • invalid or excessive action lists.

An invalid plan is rejected as a whole before IRC output.


🛡️ Safe by default

The sample configuration keeps the plugin system disabled:

#[plugins]
#AUTOLOAD=0
#ENABLED=Mediabot::Plugin::ScriptDryRun

A safe route-only dry-run example is:

#[plugins.ScriptDryRun]
#COMMANDS=hello
#ROUTES=hello=examples/hello_perl.pl
#ACTION_MODE=dry-run
#ALLOW_IRC=no
#APPLY_REQUIRE_SCOPE=yes

In this mode, scripts execute and their actions are validated, but nothing is sent to IRC.

Real IRC output requires both gates:

ACTION_MODE=apply
ALLOW_IRC=yes

Apply mode also keeps this protection enabled:

APPLY_REQUIRE_SCOPE=yes

That means the current command must be explicitly listed in COMMANDS or ROUTES. A broad fallback script cannot silently take ownership of unrelated commands.


🪶 Six reference commands

The project now ships useful examples in all three languages:

IRC command Language Script Purpose
hello Perl hello_perl.pl Basic reply and log action
pyhello Python hello_python.py Python bridge example
tclhello Tcl hello_tcl.tcl Tcl bridge example
proll Python roll.py Dice expressions such as 2d6+1
p8ball Tcl eightball.tcl Magic 8-Ball answers
pchoose Perl choose.pl Pick one option from a list

The p aliases are intentional.

Mediabot already has richer built-in commands named roll, 8ball and choose. The external examples therefore use proll, p8ball and pchoose instead of shadowing the existing handlers.

Example configuration:

[plugins]
AUTOLOAD=1
ENABLED=Mediabot::Plugin::ScriptDryRun

[plugins.ScriptDryRun]
COMMANDS=hello,pyhello,tclhello,proll,p8ball,pchoose
ROUTES=hello=examples/hello_perl.pl, pyhello=examples/hello_python.py, tclhello=examples/hello_tcl.tcl, proll=examples/roll.py, p8ball=examples/eightball.tcl, pchoose=examples/choose.pl
ACTION_MODE=apply
ALLOW_IRC=yes
APPLY_REQUIRE_SCOPE=yes

🔭 Partyline observability

The bridge can be inspected without executing or changing scripts:

.plugins
.plugins config
.scriptdryrun status
.scriptdryrun last
.scriptdryrun config

These views expose:

  • the autoload gate;
  • loaded and enabled plugins;
  • command filters;
  • route maps;
  • current action mode;
  • IRC permission state;
  • apply-scope protection;
  • the last script result;
  • planned and applied actions;
  • bounded error diagnostics;
  • execution time in milliseconds.

This makes the plugin runtime observable without exposing credentials or requiring database access.


🧪 Hardening work

The bridge went through a long defensive pass covering, among other things:

  • subprocess timeout and process cleanup;
  • bounded stdin, stdout and stderr;
  • symlink containment, including broken symlinks;
  • scalar-only command, path, event and context contracts;
  • strict JSON object envelopes;
  • protocol and ok field validation;
  • bounded errors without Perl reference stringification;
  • action count and text limits;
  • IRC target and line-injection guards;
  • safe plugin replacement and listener cleanup;
  • EventBus listener identity and mutation safety;
  • CommandRegistry alias collision and atomic replacement;
  • explicit apply ownership and scope rules;
  • rejection of malformed false scalar action lists such as 0, "0" and "".

The repository also includes a complete pre-commit suite covering the plugin manager, EventBus, CommandRegistry, script runner, action runner, Partyline visibility and all reference scripts.


🗺️ Configuration documentation

mediabot.sample.conf now documents:

  • canonical plugin keys;
  • compatibility aliases;
  • dry-run and apply behavior;
  • the optional SCRIPT fallback;
  • route ownership;
  • the security implications of broad fallback execution;
  • all six example routes;
  • the JSON protocol;
  • supported actions;
  • Partyline inspection commands.

The sample stays disabled and credential-free by default.


🧰 Safer commits

The project commit helper now refuses to stage live or development configuration files, private keys, environment files, credential stores and known token formats.

It protects files such as:

mediabot.conf
mediabot.conf_YYYYMMDD_HHMM
mediabot_dev_*.conf
.env
*.key
*.pem

Public templates such as mediabot.sample.conf remain committable, while their content is scanned for accidental real credentials.

Generated MP3 files, caches, snapshots, logs and local follow-up documents also remain outside the commit.


🌒 Compatibility and database impact

This work does not require a database schema migration.

Existing built-in commands remain available, and the plugin bridge is disabled unless explicitly enabled.

The implementation preserves the historical dispatcher while allowing the CommandRegistry and EventBus architecture to grow progressively.


🦉 Final result

Mediabot v3 can now be extended with small, readable Perl, Python and Tcl programs without giving those scripts an unrestricted route to IRC.

The result is a bridge that is:

  • multilingual;
  • observable;
  • route-scoped;
  • shell-free;
  • fail-closed;
  • backward-compatible;
  • safe by default.

The castle doors are open — but every spell still passes through the wards.

You must be logged in to reply.