Forum teuk.org

🪄 The Marauder’s Map Opens: Mediabot v3 Gains a Safe Perl / Python / Tcl Plugin Bridge

in Mediabot · started by TeuK · 1w ago

TeuK · 1w ago

🪄 The Marauder’s Map Opens: Mediabot v3 Gains a Safe Perl / Python / Tcl Plugin Bridge

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


🧙 Commit spell

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.

🏰 What changed?

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.


🦉 Why this matters for users

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.


🧪 The script protocol: not magic, a contract

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.


🛡️ Safety model: Protego first, fireworks later

The bridge is designed around several defensive layers.

1. Disabled by default

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.

2. Dry-run by default

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.

3. Explicit apply mode

Real application requires:

ACTION_MODE=apply

Without that, actions remain planned, not applied.

4. IRC output is gated separately

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.

5. Scope guard

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.

6. No shell execution

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.

7. Path validation

Script paths are validated:

  • no absolute paths;
  • no .. traversal;
  • no backslashes;
  • no NUL bytes;
  • only known extensions: .pl, .py, .tcl;
  • symlink containment checked with real paths.

The symlink containment check matters because a path can look safe textually while pointing outside the script directory. That door is now closed.

8. Bounded execution

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:

  • invalid JSON;
  • unsupported actions;
  • non-zero exits;
  • scripts that time out;
  • scripts that close stdout/stderr and keep running;
  • scripts that do not read stdin;
  • symlinks escaping the script directory;
  • declared script failure with hidden actions.

That is exactly what you want before introducing external script execution inside a long-running IRC bot.


🐍 Why Perl, Python, and Tcl?

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

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

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

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.


🧭 How command routing works

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.


🧯 Failure behavior

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.


🔍 Observability

The bridge now logs useful runtime information around routed commands:

  • accepted command;
  • selected script;
  • action mode;
  • IRC gate status;
  • script result summary;
  • action plan summary;
  • elapsed execution time.

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.


🧙 Example configuration

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.


🦁 What users should understand before enabling it

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:

  1. Keep the plugin disabled unless needed.
  2. Prefer explicit ROUTES over broad fallback scripts.
  3. Keep APPLY_REQUIRE_SCOPE=yes.
  4. Test first in dry-run mode.
  5. Enable ALLOW_IRC=yes only after verifying the returned actions.
  6. Keep scripts small and readable.
  7. Treat script changes like code changes: review, test, commit.
  8. Never route an existing important legacy command without checking the impact.

That is how you get flexibility without regression.


🧱 Why this is a foundation, not just a feature

This work opens the door to future Mediabot features that do not need to be baked directly into the core:

  • experimental IRC commands;
  • API integrations;
  • per-channel scripted utilities;
  • migration of old Tcl/Eggdrop logic;
  • safer prototypes before native Perl implementation;
  • optional plugin packs;
  • scripted diagnostics;
  • controlled automation around existing bot state.

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.


🧹 What was intentionally not changed

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.


🌌 Final word

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.