“I solemnly swear that I am up to good engineering.”
Mediabot v3 has just crossed an important threshold: it is no longer only a large Perl IRC bot with internal commands. It now has the foundations of a real plugin and external script system, able to delegate command behavior to trusted scripts written in Perl, Python, or Tcl, while keeping the old Mediabot behavior protected by strict guards.
This is not a cosmetic feature. It is a major architectural step.
Until now, adding behavior to Mediabot usually meant touching the Perl core directly, extending dispatch code, and being extremely careful not to disturb long-standing commands. That works, but over time it makes every improvement heavier. The new plugin/script bridge changes that model: selected commands can now be routed to external scripts, tested independently, observed cleanly, and applied through a controlled action layer.
The important part is not only that scripts can run. The important part is that they run through a protocol: validated paths, bounded subprocess execution, explicit dry-run/apply modes, IRC gates, failure isolation, and pre-commit regression tests. In other words: this is not a random spell cast from the Forbidden Forest. It is a wand registered at Ollivanders, inspected by Professor McGonagall, and watched by the Marauder’s Map.
Recommended commit subject:
🪄 Protego Scriptorum: unlock the safe Perl/Python/Tcl plugin bridge
Recommended extended commit text:
Introduce the guarded ScriptDryRun plugin bridge for trusted external Perl, Python and Tcl scripts.
This adds the CommandRegistry/EventBus/PluginManager foundations, a bounded ScriptRunner subprocess layer, a validated ScriptActionRunner apply layer, visible multilingual example scripts, preflight coverage, and commit-time hygiene guards.
The bridge stays disabled by default, dry-run by default, scoped by explicit routes/commands, and IRC output remains gated behind ALLOW_IRC.
The runner was hardened against shell execution, unsafe paths, symlink escapes, invalid JSON, declared script failure, non-zero exits, timeouts, descriptor-closing hangs, oversized output, and blocking stdin writes.
Mischief managed — safely.
This work introduces a structured plugin/script architecture around Mediabot’s existing command system.
The new pieces are:
| Component | Role |
|---|---|
Mediabot::CommandRegistry |
Registers internal commands and aliases cleanly. |
Mediabot::EventBus |
Allows internal events to be observed without hard-wiring everything into the core. |
Mediabot::PluginManager |
Loads enabled Perl plugins from configuration. |
Mediabot::Plugin::ScriptDryRun |
Observes public commands and routes selected ones to trusted scripts. |
Mediabot::ScriptRunner |
Executes external Perl/Python/Tcl scripts safely and returns structured results. |
Mediabot::ScriptActionRunner |
Validates and applies script actions through strict gates. |
plugins/scripts/examples/ |
Provides real working examples for Perl, Python, and Tcl. |
The goal is simple: allow new behavior to be developed outside the core while keeping Mediabot’s historical behavior stable.
That matters because Mediabot is not a toy bot. It has long-lived IRC behavior, old commands, channel-specific logic, database-backed features, and a real production history. Any plugin system that blindly executes scripts would be dangerous. This one is deliberately conservative.
For a normal user, this feature means Mediabot can grow faster without becoming fragile.
Instead of modifying the central Perl command dispatcher for every experimental feature, a developer can create a dedicated script such as:
plugins/scripts/examples/hello_python.py
plugins/scripts/examples/hello_perl.pl
plugins/scripts/examples/hello_tcl.tcl
Then route a command to it:
[plugins]
AUTOLOAD=1
ENABLED=Mediabot::Plugin::ScriptDryRun
[plugins.ScriptDryRun]
ROUTES=hello=examples/hello_perl.pl, pyhello=examples/hello_python.py, tclhello=examples/hello_tcl.tcl
ACTION_MODE=apply
ALLOW_IRC=yes
APPLY_REQUIRE_SCOPE=yes
After that, commands such as these can be handled by external scripts:
m hello
m pyhello
m tclhello
The user sees normal IRC replies, but internally the command is being handled by the new bridge.
The difference is that the bridge does not simply trust the script blindly. The script must return structured JSON actions, and those actions are validated before anything is applied.
A script is expected to return JSON describing what it wants Mediabot to do.
A successful script response can contain actions such as:
{
"ok": true,
"actions": [
{
"type": "reply",
"target": "#teuk",
"text": "Python script bridge OK for command: pyhello"
},
{
"type": "log",
"level": "info",
"text": "Python example script produced an action plan"
}
]
}
Mediabot then decides whether those actions are valid and whether they are allowed to be applied.
A script can also explicitly refuse a command:
{
"ok": false,
"errors": ["script refused this command"],
"actions": [
{
"type": "reply",
"text": "This must not be applied"
}
]
}
That last case is important. A declared failure must stay a failure. Even if the script accidentally includes actions, Mediabot must not expose them to the apply layer. This was hardened before commit: if the script says ok=false, the bridge keeps the action plan closed.
That is the kind of small detail that prevents nasty surprises later.
The bridge is designed around several defensive layers.
The plugin system does not activate itself on existing installations. A user must explicitly configure plugin autoloading and enable the plugin.
This protects existing Mediabot instances: no configuration change means no script execution.
Even when the plugin is loaded, ScriptDryRun defaults to dry-run mode unless configured otherwise.
Dry-run means the script can be executed and an action plan can be produced, but Mediabot does not apply IRC side effects.
Real application requires:
ACTION_MODE=apply
Without that, actions remain planned, not applied.
Even in apply mode, sending IRC messages requires:
ALLOW_IRC=yes
This is intentional. It allows a future setup where non-IRC actions could be applied while IRC output remains locked.
The safer configuration style is explicit routing:
ROUTES=pyhello=examples/hello_python.py
APPLY_REQUIRE_SCOPE=yes
That prevents a broad fallback script from swallowing unrelated legacy commands by accident.
The runner uses argv-style subprocess execution rather than shell strings. That means commands are not built as interpolated shell lines.
This is a major security boundary. The difference between:
open3(..., $interpreter, $script_path)
and a shell command string is the difference between controlled execution and a cursed necklace.
Script paths are validated:
.. traversal;.pl, .py, .tcl;The symlink containment check matters because a path can look safe textually while pointing outside the script directory. That door is now closed.
Scripts are not allowed to run forever.
The runner has timeout handling, stdout/stderr limits, and explicit failure paths. It was hardened against several tricky cases:
That is exactly what you want before introducing external script execution inside a long-running IRC bot.
Mediabot is historically Perl, and Perl remains the core language of the bot. But the IRC ecosystem is full of small scripts, experiments, old Eggdrop habits, and quick automation ideas.
Supporting Perl, Python, and Tcl gives the bot three useful spellbooks:
Perl is the native language of Mediabot. Perl scripts are ideal when you want behavior close to the bot’s existing culture and deployment model.
Python is convenient for quick integrations, APIs, JSON processing, and modern tooling. It lowers the barrier for writing small external features without touching the Perl core.
Tcl matters because IRC bots and Eggdrop history are full of Tcl logic. Supporting Tcl makes it easier to reuse or adapt old scripting habits without rewriting everything immediately.
This does not mean every feature should become an external script. It means Mediabot now has a clean path for features that are better isolated, tested, or developed independently.
The safest routing model is explicit:
ROUTES=hello=examples/hello_perl.pl, pyhello=examples/hello_python.py, tclhello=examples/hello_tcl.tcl
This means:
m hello -> hello_perl.pl
m pyhello -> hello_python.py
m tclhello -> hello_tcl.tcl
Commands not listed in ROUTES should continue through the normal Mediabot legacy dispatch.
That is essential. The goal is not to break old commands. The goal is to give selected commands a new execution path.
There is also a broader SCRIPT fallback design, but it should be used carefully. A global fallback script can be powerful, but it can also swallow commands that should have gone to the legacy dispatcher. The recommended operational model is therefore:
APPLY_REQUIRE_SCOPE=yes
ROUTES=...
The castle gates stay open only for named visitors.
A script bridge is only trustworthy if failure is boring.
The expected behavior is:
| Failure case | Expected behavior |
|---|---|
| Invalid script path | No execution, structured error. |
| Script exits non-zero | Result rejected, no actions applied. |
| Script returns invalid JSON | Result rejected, no actions applied. |
| Script times out | Process killed, no actions applied. |
| Script returns unsupported action | Action rejected, no side effect. |
Script declares ok=false |
Actions are not exposed for application. |
| IRC not allowed | IRC actions are gated, logs may still apply. |
| Dry-run mode | Actions are planned only, not applied. |
This is the heart of the work. Running external scripts is easy. Running them without turning your IRC bot into a Portkey to chaos is the real achievement.
The bridge now logs useful runtime information around routed commands:
That means when a command like m pyhello runs, the operator can see what happened without guessing:
PUBLIC(scriptdryrun): accepted command=pyhello script=examples/hello_python.py mode=apply allow_irc=1
script_result command=pyhello ok=1 timeout=0 exit=0 actions=2 errors=0 elapsed_ms=...
action_plan command=pyhello ok=1 applied_ok=1 planned=2 applied=2 errors=0 apply_errors=0 elapsed_ms=...
This is important for production use. A plugin system without observability becomes a haunted corridor. You hear noises, but you do not know which portrait is screaming.
A conservative example:
[plugins]
AUTOLOAD=0
# ENABLED=Mediabot::Plugin::ScriptDryRun
[plugins.ScriptDryRun]
# Safe default: do not apply actions.
ACTION_MODE=dry-run
# Safe default: do not send IRC messages.
ALLOW_IRC=no
# Require explicit command scope before apply.
APPLY_REQUIRE_SCOPE=yes
# Example explicit routes:
# ROUTES=hello=examples/hello_perl.pl, pyhello=examples/hello_python.py, tclhello=examples/hello_tcl.tcl
A development setup for live testing:
[plugins]
AUTOLOAD=1
ENABLED=Mediabot::Plugin::ScriptDryRun
[plugins.ScriptDryRun]
ROUTES=hello=examples/hello_perl.pl, pyhello=examples/hello_python.py, tclhello=examples/hello_tcl.tcl
ACTION_MODE=apply
ALLOW_IRC=yes
APPLY_REQUIRE_SCOPE=yes
The difference between those two configurations is deliberate. Documentation should show safe defaults first and live apply mode second.
This plugin bridge is powerful, but it should be treated as trusted-code execution.
If someone can modify scripts under plugins/scripts/, they can influence what the bot does for routed commands. The runner has strong boundaries, but it is not a sandbox for hostile code. It is a controlled integration layer for trusted scripts.
Good operational rules:
ROUTES over broad fallback scripts.APPLY_REQUIRE_SCOPE=yes.ALLOW_IRC=yes only after verifying the returned actions.That is how you get flexibility without regression.
This work opens the door to future Mediabot features that do not need to be baked directly into the core:
The long-term value is architectural: Mediabot can now evolve with clearer boundaries.
The Perl core remains the castle. Plugins are classrooms. Scripts are spellbooks. The action runner is Filch checking every corridor pass.
No database schema change was introduced.
No legacy command was removed.
The plugin system remains disabled by default.
The script bridge remains dry-run by default.
IRC output remains gated.
The old runtime behavior is preserved unless the operator explicitly enables and routes the plugin.
That is the right posture for a long-lived IRC bot.
This is a major integration for Mediabot v3.
It brings a new extension model without throwing away the old one. It allows Perl, Python, and Tcl scripts to participate in the bot’s behavior while still passing through a strict validation and application layer. It gives developers flexibility, operators visibility, and users continuity.
The most important part is not the magic itself. It is the discipline around the magic.
Mediabot can now open the Marauder’s Map, see the scripts walking through the corridors, verify their badges, inspect their spells, and decide whether they are allowed to speak in the Great Hall. 🗺️✨
You must be logged in to reply.