Forum teuk.org

🪄 Mediabot v3 — The Plugin Gallery Opens Its Doors

in Mediabot · started by TeuK · 3w ago

TeuK · 3w ago

Development chronicle — mb586 to mb591 · Plugins v2

A new wing has opened inside Mediabot’s castle.

Plugins are no longer mysterious boxes that must be opened and read before anyone can understand what they provide. They can now declare their identity, commands, access requirements and public surface through a versioned manifest — then join the live command registry through a controlled, reversible lifecycle.

The same contract now covers trusted in-process Perl plugins and external Perl, Python or Tcl scripts.

No database schema was changed during this arc.


📜 mb586 — Every plugin receives a manifest

Plugin API v2 begins with a declarative contract:

sub manifest {
    return {
        api         => 2,
        name        => 'demo',
        version     => '0.002',
        description => 'A small demonstration plugin.',
        commands    => {
            hello => {
                help  => 'Say hello.',
                level => 0,
            },
        },
        events => [ 'public_command_observed' ],
    };
}

The manifest is validated before register() can create any side effect.

The validation protects the registry from:

  • malformed API versions;
  • invalid names;
  • plugin identity impersonation;
  • malformed command descriptions;
  • collisions with native Mediabot commands;
  • collisions with commands already owned by another plugin.

Legacy plugins remain compatible. A plugin without a v2 manifest continues to load through the historical API v1 path.


✨ mb587 — Declared commands become live commands

A command declared by a v2 plugin is mounted automatically into Mediabot’s CommandRegistry.

There is no manual wiring and no second command table to maintain.

The registered wrapper checks the plugin’s current state on every dispatch, then calls:

$plugin->command_<name>($ctx)

The lifecycle is atomic:

  • a declared command without its matching method is refused;
  • partial command mounting is rolled back;
  • unregister removes commands and aliases;
  • replace removes the old instance’s command surface;
  • disabling a plugin silences it without unloading it;
  • no ghost command remains after removal.

This extends the same discipline Mediabot already applies to event listeners: once an object leaves the castle, none of its portraits should keep speaking.


🏰 mb588 — The partyline becomes the control room

The .plugins partyline command now operates the v2 lifecycle:

.plugins loaded
.plugins load My::Plugin [name]
.plugins reload <name>
.plugins unload <name>
.plugins enable <name>
.plugins disable <name>

Read-only inspection remains available, while mutations are protected by Mediabot’s existing partyline access model.

Reload is real: the module is removed from %INC, loaded again from disk and validated as a new candidate.

If the new code is invalid, the previous instance remains active. A broken replacement cannot leave the bot with half a plugin and half a command registry.


🔐 mb589 — Manifest permissions meet USER_LEVEL

The word level once risked representing two incompatible scales.

Mediabot’s database uses named access levels where a lower stored value is more powerful. A plugin manifest now avoids that ambiguity:

commands => {
    hello => { help => 'Public command.', level => 0        },
    vault => { help => 'Master only.',    level => 'Master' },
}

The rules are explicit:

  • 0 means public;
  • a privileged command uses a named USER_LEVEL description;
  • unsupported positive numeric declarations are rejected with a migration explanation.

Authorization is checked at every dispatch, not frozen when the plugin loads.

The wrapper verifies:

  • a real command context exists;
  • the user can be resolved;
  • authentication is valid;
  • the user’s current level grants access.

An Owner can invoke a Master command. An ordinary User cannot. Every refusal is fail-closed, concise to the caller and traceable in the log.


🌉 mb590 — External scripts join the same contract

Perl, Python and Tcl scripts can now become plugin v2 participants through a JSON sidecar:

plugins/greet.tcl
plugins/greet.tcl.manifest.json
{
  "api": 2,
  "name": "greet",
  "version": "1.0",
  "description": "Greet users from Tcl.",
  "commands": {
    "greet": {
      "help": "Say hello.",
      "level": 0
    },
    "vip": {
      "help": "Master only.",
      "level": "Master"
    }
  }
}

They enter through:

.plugins loadscript greet.tcl

The sidecar is validated by the same manifest rules as an in-process plugin, while execution still uses the hardened script bridge:

  • path validation;
  • execution timeout;
  • bounded output;
  • validated action documents;
  • dry-run support;
  • IRC actions applied only through the existing gates;
  • intrusive topic, kick and ban actions still disabled unless explicitly allowed.

Reload reads the sidecar again. An invalid replacement leaves the previous script instance active.


🛡️ mb591 — The final pre-commit guard

The last inspection found several edge cases where the implementation did not yet fully match the guarantees promised by the arc.

They are now closed.

Complete rollback during reload

A failure while mounting the replacement command surface restores the previous plugin and its commands instead of merely removing the failed candidate.

Disabled state survives reload

Reloading a disabled plugin no longer silently re-enables it.

Sidecars are bounded before full ingestion

The 8 KiB sidecar limit is enforced before the whole document is accepted into memory.

Script and sidecar paths remain inside the trusted root

The script must exist when it is registered, and the sidecar receives its own containment check. Symbolic-link tricks cannot silently point the manifest outside the approved script directory.

Action failures are real failures

A script command is not announced as successful when ScriptActionRunner reports that its actions could not be applied.

Command-bearing manifests require a real mounting environment

A manifest declaring commands cannot load without a plugin object and a usable CommandRegistry.

The partyline help was also completed so that loadscript is visible to operators.


🧪 Validation

The complete local test suite passed after the mb591 hardening.

The dedicated arc covers:

  • manifest validation and impersonation protection;
  • native and plugin command collisions;
  • automatic command mounting;
  • aliases and full unmount;
  • true reload with rollback;
  • enabled and disabled state;
  • authorization through named USER_LEVEL entries;
  • external script sidecars;
  • Perl, Python and Tcl dispatch;
  • path containment and sidecar size limits;
  • action-application failures;
  • reload state restoration;
  • final pre-commit security guards.

🧙 What plugin authors gain

A plugin author can now describe a command once and let Mediabot handle the rest:

  • discovery;
  • help metadata;
  • registration;
  • access control;
  • lifecycle;
  • collision protection;
  • reload;
  • removal.

External script authors receive the same operational model without moving their code into the bot process.

This gives Mediabot a coherent extension architecture while preserving the hard-earned compatibility of the legacy plugin system.


✨ Final parchment

Every portrait in the gallery now carries a proper nameplate.

The castle knows which spells it offers, who may cast them, how to replace them safely, and how to remove them without leaving a whisper in the corridor.

Even visitors from other castles — Perl, Python and Tcl scripts — now pass through the same guarded doors.

Plugins v2 is no longer a proposal. The full arc is alive.

You must be logged in to reply.