Mediabot v3 just received another careful round of hardening around its plugin system and the external script bridge.
This work focuses on a very specific part of the bot: the boundary where Mediabot lets external Perl, Python and Tcl scripts communicate through JSON. The goal is not to add shiny new user-facing commands, but to make the bridge safer, clearer, and less likely to behave strangely when a script returns malformed data.
No database schema was changed. No SQL migration was added. Existing valid scripts should keep working.
A plugin/script bridge is a powerful thing. It lets the bot grow beyond its Perl core and makes it possible to plug in small external helpers written in different languages.
But with great scripting power comes great responsibility — and occasionally, a suspicious goblin carrying a malformed JSON object.
The main risk here was not remote code execution. The real issue was protocol cleanliness: Perl can treat references as truthy, stringify arrays and hashes into values like ARRAY(...) or HASH(...), and quietly accept values that should not cross a trust boundary.
That is fine for a quick prototype. It is not fine for a bot that has been alive long enough to deserve its own portrait in the Hogwarts common room.
ok field is now explicitThe script response field ok is now treated as a proper protocol field.
Valid:
{ "ok": true, "actions": [] }
Valid legacy behavior is still preserved: older scripts may omit ok.
Invalid values such as objects, arrays, or strings like "true" are rejected instead of being interpreted through Perl truthiness.
This closes a subtle but important gap: a malformed value should never accidentally open the action layer.
Scripts may now explicitly declare:
{ "protocol": "mediabot-script-v1" }
Legacy scripts without a protocol field still work.
Unknown protocols or structured protocol values are rejected. This gives the bridge room to evolve later without silently accepting a future or foreign contract by accident.
action.type is now validated before being normalized and exposed to the action runner.
A clean action still works:
{ "type": "reply", "target": "#channel", "text": "hello" }
A broken action like this is rejected:
{ "type": ["reply"], "target": "#channel", "text": "hello" }
Mediabot no longer lets malformed JSON objects or arrays sneak into the action classifier disguised as Perl stringification.
Script failures can still return useful diagnostics:
{
"ok": false,
"errors": ["refused by script"],
"actions": []
}
But nested JSON diagnostics are now dropped cleanly instead of being rendered as HASH(...) or ARRAY(...).
If no usable scalar diagnostic remains, Mediabot falls back to a stable generic error.
That keeps logs, Partyline output and .scriptdryrun last readable instead of turning them into a Divination exam.
stdin is now scalar-onlyScriptRunner::run_plan() now refuses non-scalar stdin before spawning an external process.
That means a future internal caller cannot accidentally feed a stringified Perl reference to a Python, Tcl or Perl script.
The script bridge expects text or bytes. Not a cursed HASH(0x...) pretending to be a message.
Plugin replacement lifecycle checks were also tightened.
Instead of comparing objects through stringification, PluginManager now uses Scalar::Util::refaddr().
That matters because Perl objects can overload stringification. Two different plugin objects may look identical as strings, but they are not the same object.
With this change:
Nearly Headless Nick may tolerate ghosts. PluginManager should not.
The ScriptDryRun plugin was tightened around command ownership and apply mode.
A bare global script should not accidentally swallow every legacy public command. The safer behavior is now to only “own” commands that are explicitly scoped through COMMANDS or ROUTES.
Apply mode is also safer by default: applying script actions now requires an explicit scope unless the operator opts out deliberately.
This is a small behavioral hardening, but it closes a real footgun.
The hardening keeps the important compatibility promises:
The only intentional behavior change is the safer ScriptDryRun scope/apply behavior. If someone was relying on an unscoped apply-mode script catching everything, they now need to explicitly opt out or define the scope properly.
That is a good trade. A bot should not cast spells in every room just because someone left the door open.
New regression coverage was added across the bridge and plugin lifecycle:
ok contract;protocol contract;action.type;stdin;ok contract;The commit helper preflight has also been updated to include the new tests.
This is not a flashy feature release. It is the kind of maintenance work that keeps a long-running IRC bot healthy: fewer ambiguous contracts, fewer accidental truthiness traps, fewer ghost listeners, and clearer failure behavior.
In other words: less “why did the cauldron explode?” and more “yes, Professor, the charm behaves exactly as documented.”
Mediabot v3 remains old-school at heart, but the bridge under the hood is getting much more disciplined.
You must be logged in to reply.