Over the last development pass, Mediabot v3 received a major architectural foundation for its next generation of extensibility.
This was not a flashy “add one command and hope it works” change. It was a careful step-by-step refactoring pass designed to prepare Mediabot for a plugin and script system inspired by Eggdrop, while keeping the current bot stable, predictable, and compatible with existing commands.
The key rule was simple: add the new architecture without breaking the old runtime.
This pass introduced four major internal foundations:
CommandRegistry
EventBus
PluginManager
ScriptRunner
and one safety layer for script-produced actions:
ScriptActionRunner
Together, these pieces prepare Mediabot for a future where internal Perl plugins and controlled external scripts can react to bot events without being hardwired into the main command dispatcher.
The first brick was Mediabot::CommandRegistry.
It gives Mediabot a clean place to register public and private commands with metadata, aliases, levels, categories, and handlers.
The first real integration moved only a tiny safe group of public commands into the registry path:
version
uptime
help
commands
Everything else still falls back to the legacy dispatch path.
This was intentional. The goal was not to rewrite the bot in one dangerous jump. The goal was to prove that new registry-based commands and old legacy commands can coexist safely.
The second brick was Mediabot::EventBus.
It provides a generic internal event system with:
event name normalization
listener registration
listener priority
one-shot listeners
safe emit_report()
structured listener errors
The first real runtime event added was:
public_command_observed
This event is emitted after the public command context and command object are built, but before the registry or legacy dispatch runs.
With no listeners, this changes nothing. With listeners later, plugins can observe commands safely without owning the dispatcher.
Listener failures are reported without breaking normal command execution.
The third brick was Mediabot::PluginManager.
It introduced a structured way to register trusted in-process Perl plugins:
register_plugin()
unregister_plugin()
plugin()
object_for()
enable()
disable()
list()
names()
count()
load_perl_module()
The manager rejects path-like module names and accepts only normal Perl module names such as:
Mediabot::Plugin::Demo
No arbitrary file path loading was added.
A deliberately tiny demo plugin was also introduced:
Mediabot::Plugin::Demo
It listens to public_command_observed and only increments an internal counter. It does not send IRC messages, touch the database, mutate context, or modify dispatch.
Plugin autoload was then added behind an explicit configuration gate:
[plugins]
AUTOLOAD=1
ENABLED=Mediabot::Plugin::Demo
Without AUTOLOAD=1, no plugin is loaded at boot.
This keeps the default runtime unchanged.
Before adding plugin control commands, a read-only partyline command was added:
.plugins
.plugins loaded
.plugins config
This gives operator visibility into:
plugin autoload status
registered plugin count
enabled plugin count
disabled plugin count
configured plugin loading rules
The command is strictly read-only. It does not load, unload, enable, disable, write config, touch the database, or send IRC messages.
That matters because visibility should come before power.
The fourth brick was Mediabot::ScriptRunner.
This prepares Mediabot for controlled external scripts written in:
Perl
Python
Tcl
Supported extensions:
.pl
.py
.tcl
The initial foundation added:
path validation
language detection
JSON event envelope
JSON response validation
safe action type validation
Unsafe paths are rejected:
/tmp/foo.py
../evil.py
foo\bar.py
script.sh
Valid relative paths are resolved under the script directory:
plugins/scripts/games/duckhunt.tcl
The JSON event protocol is versioned:
{
"protocol": "mediabot-script-v1",
"event": "public_command",
"data": {
"channel": "#test",
"nick": "Te[u]K",
"command": "demo",
"args": ["a", "b"]
}
}
Before executing anything, Mediabot gained a dry-run execution plan.
For example:
$runner->build_execution_plan('demo/hello.py', $payload)
returns a plan like:
{
ok => 1,
dry_run => 1,
language => 'python',
script => 'demo/hello.py',
full_path => 'plugins/scripts/demo/hello.py',
command => [ 'python3', 'plugins/scripts/demo/hello.py' ],
stdin => '<JSON envelope>',
timeout => 5,
max_stdout_bytes => 8192,
}
The command is built as an argv array, not a shell string.
That is the right safety model.
After the dry-run contract was validated, real subprocess execution was added using:
open3($child_in, $child_out, $child_err, @cmd)
The important part is @cmd: no shell string is involved.
The runner now supports:
JSON stdin
JSON stdout response
stderr capture
timeout
stdout cap
stderr cap
non-zero exit reporting
timeout killing
structured result objects
The tests cover:
valid script execution
invalid JSON output
non-zero exit with stderr
timeout kill
unsafe path rejection before spawn
This is still not exposed directly to IRC users or partyline operators. It is only a safe internal capability.
The last part of this pass introduced:
Mediabot::ScriptActionRunner
This is the safety layer between a script response and real Mediabot side effects.
It validates and plans script actions, but does not execute them yet.
Accepted initial action types:
reply
notice
log
timer
Rejected action types include:
raw_irc
exec
shell
file_write
db_write
anything unknown
This module is dry-run only. It does not send IRC messages, create timers, touch the database, or mutate dispatch.
That gives us a clean safety checkpoint before allowing external scripts to affect the bot.
This pass added focused tests for each step:
404_mb165_command_registry_foundation.t
405_mb166_registry_public_core_dispatch.t
406_mb167_event_bus_foundation.t
407_mb168_public_command_observed_event.t
408_mb169_plugin_manager_foundation.t
409_mb170_plugin_config_loader_foundation.t
410_mb171_demo_plugin_explicit_load.t
411_mb172_plugin_autoload_gate.t
412_mb173_partyline_plugins_status.t
413_mb174_script_runner_foundation.t
414_mb175_script_runner_execution_plan.t
415_mb176_script_runner_subprocess.t
416_mb177_script_action_runner_dryrun.t
These tests validate the new architecture without requiring risky live IRC behavior.
The bot was restarted successfully after the changes.
A minimal runtime validation on the dev IRC network should include:
m version
m uptime
m help
m commands
m channels
m seen Te[u]K
and partyline checks:
.help
.plugins
.plugins loaded
.plugins config
Expected default plugin state:
autoload: disabled
registered: 0
plugins: none loaded
This pass creates the foundation for a much more extensible Mediabot without sacrificing stability.
The future direction is now clear:
core commands can move gradually into CommandRegistry
plugins can observe events through EventBus
trusted Perl plugins can load through PluginManager
external Perl/Python/Tcl scripts can run through ScriptRunner
script actions must pass through ScriptActionRunner before side effects
That last point is crucial.
Mediabot is getting more powerful, but not by becoming reckless. Each new capability is being placed behind a boundary, tested, and introduced in small safe steps.
The next logical pass is to connect the pieces in a controlled integration test:
external script
-> subprocess execution
-> JSON response
-> ScriptActionRunner dry-run action plan
Only after that should real IRC-side effects be considered.
The architecture is now in place. The door is open, but the wards are up.
You must be logged in to reply.