Forum teuk.org

MB747 — A Read-Only Pensieve for Plugin Data 🧠🔒

in Mediabot · started by TeuK · yesterday

TeuK · yesterday

Mediabot’s Plugin API v3 can now consult real application data without being given the keys to the database.

MB747 introduces the first approved domain-data capability: data.quotes.read. It gives future plugins a small, explicit and guarded view of the quote archive while keeping SQL, database handles, bot internals and cross-channel access firmly on the other side of the castle door.

The change was committed and pushed as:

2542001cdcaa70b05cb4a247771a75860db33874
MB747: let plugins consult the read-only Pensieve of quotes 🧠🔒

This is an architectural milestone rather than a visible IRC feature. No quote command has moved yet, no plugin has been activated, and no channel policy has changed. What MB747 provides is the safe foundation on which that migration can now be built.

The problem: useful plugins eventually need real data

The first API v3 plugins proved the runtime itself:

  • strict manifests and explicit capabilities;
  • bounded IRC output;
  • versioned events and shared scheduled jobs;
  • typed per-channel configuration with off, observe and on policies;
  • reversible command migration;
  • shared HTTPS access and namespaced compare-and-swap storage.

That was enough for self-contained features, but not for plugins that need to work with Mediabot’s established community memory. Quotes are stored in the main MariaDB database and belong to a specific IRC channel.

The tempting shortcut would have been to pass a database handle to plugin code and let each plugin execute its own queries. That would immediately weaken the whole API v3 model. A plugin could accidentally read another channel, issue an unbounded query, depend on private schema details, retain a stale connection or perform a write that was never part of its advertised permissions.

MB747 deliberately refuses that shortcut.

The solution: a domain facade, not a database door

Plugins may request the capability data.quotes.read. If the manifest requests it and the operator explicitly grants it, PluginContext exposes six named operations:

Operation Purpose
quote_by_id Retrieve one quote by its numeric identifier
quote_random Retrieve one random quote from the current channel
quote_search Search quote text using bounded literal words
quotes_by_author Retrieve recent quotes attributed to one nickname
quote_count Count all quotes, or those from one author
top_quotes Retrieve the most recalled quotes for the channel

These are application-level operations, not generic query primitives. The plugin cannot submit SQL, choose a table, supply an arbitrary channel or obtain the underlying MariaDB handle.

The core remains the only component that understands the database schema.

The channel comes from the invocation

Channel confinement is one of the most important parts of this milestone.

Every quote read receives the current API v3 invocation. Mediabot derives the channel from that invocation’s current policy and injects it into the approved operation. There is no plugin argument that can replace it with another channel.

In practical terms, a command invoked on one channel cannot use this capability to inspect the quote archive of another channel. The boundary is enforced by the core rather than left to plugin authors to remember.

The service also resolves Mediabot’s current database handle for every operation. If the core reconnects to MariaDB, plugins do not remain attached to an obsolete connection.

Bounded and prepared reads

All six operations use core-owned prepared statements. Inputs are validated before a query is prepared or executed:

  • result lists are limited to between 1 and 20 records;
  • search text and author names are limited to 256 encoded bytes;
  • searches accept at most eight words;
  • %, _ and the escape character are treated literally in searches;
  • quote identifiers must be positive integers;
  • channel names must pass the IRC-channel validation boundary;
  • unexpected result growth is rejected rather than silently accepted.

The random operation first obtains a bounded channel-local count, chooses an offset, and then fetches exactly one ordered record. The service never uses an unbounded result set merely to select a random item.

Database errors are contained by the core. A plugin receives a neutral result:

{ ok => 0, error => "unavailable" }

Raw SQL, driver diagnostics and connection details do not cross the boundary. The failure is still logged and counted by Mediabot for operators.

Immutable memories from the Pensieve

Successful reads return Mediabot::Plugin::QuoteRecordV3 objects containing only these fields:

id
text
author
author_id
created_at
hits

Each value is validated, bounded and detached from the database row. The record exposes readers and can produce a copied hash, but plugin code cannot modify the internal value or use it as a hidden path back to the database.

This makes the contract stable even if the physical schema evolves later. Plugins depend on a small domain object, not on column names and joins.

Read-only means read-only

MB747 contains no operation to:

  • add a quote;
  • edit a quote;
  • delete a quote;
  • increment its recall counter;
  • run arbitrary SQL;
  • obtain a database handle.

Even recalling a quote through the new facade leaves its hits value unchanged. That detail matters: observe mode must be able to compare a future plugin with historical behaviour without creating side effects merely because the shadow implementation ran.

Reads are therefore permitted in observe, while off remains completely inert. Visible output is still suppressed by the existing channel-policy boundary.

Nothing was silently activated

MB747 does not ship a new quote plugin and does not migrate an existing quote command. It only introduces the capability and the core service.

There is consequently no Partyline pilot to perform for this milestone:

  • no API v3 package was loaded;
  • no package was enabled;
  • no channel was switched to observe or on;
  • no private configuration was changed;
  • no database schema or data was modified.

Historical quote commands continue to work through their existing path.

That separation is intentional. The data boundary can be reviewed and tested independently before any user-visible command starts relying on it.

Operational proof on the development instance

The development deployment ran a live, read-only MariaDB smoke test against #test:

QUOTE_DATA_SMOKE=PASS count=0 top_records=0

An empty archive is a valid result, so this also exercised the neutral zero-record path. Two real reads completed without an insert, update, delete or recall-counter change.

The validation sequence then completed successfully:

Gate Result
Targeted MB747 suite 1,257 / 1,257 tests passed
Fast validation lane 7,411 / 7,411 tests passed
Final full suite 19,721 / 19,721 tests passed
Full-suite duration 141 seconds
Files committed 20
Diff 839 insertions, 26 deletions

The final gate staged exactly the expected files, recorded the staged diff digest, ran the full suite once with visible progress, and confirmed afterward that neither source bytes nor the running process had changed during the test.

The service remained active with zero restarts, the development repository was clean after the push, and local master matched origin/master.

A useful catch before the final gate

The first MB747 application round exposed a stale architectural sentinel. The machine contract had correctly advanced to MB747 and eight executable capabilities, while one older test still expected MB746 and seven.

The deployment script stopped during targeted tests and restored the complete MB746 source automatically. No service restart, configuration change or database change had occurred. The sentinel was then updated, added to the protected MB747 surface and validated again in the R2 package.

That is exactly what the layered validation process is meant to do: targeted tests catch the local contract drift early, the fast lane checks a broad slice of the system, and the full suite remains the single final gate before commit.

Why MB747 matters beyond quotes

Quotes are the first domain chosen because they are useful, channel-scoped and easy to express through a small read contract. The more important result is the pattern now established for future application data:

plugin request
    → capability check
    → current channel policy
    → named domain operation
    → validated inputs
    → core-owned prepared query
    → detached bounded result

Future plugins should not need direct database access. They can receive the specific operations they need, with the same controls for scope, limits, policy, observability and failure handling.

This keeps Plugin API v3 extensible without turning “extensible” into “trusted with everything.”

What comes next

The next logical step is a separate quote plugin built on this facade. It can migrate selected historical commands through the reversible fallback already proved by MB745:

  1. load the package disabled;
  2. configure one development channel;
  3. run it silently in observe and compare results with the historical path;
  4. switch that single channel to on only after parity is demonstrated;
  5. retain immediate disable, unload and legacy restoration as rollback paths.

Write operations should remain a different milestone with their own explicit capabilities, policy and audit rules. MB747 does not quietly smuggle them in under a read permission.

Closing the book — for now ✨

MB747 is not a flashy IRC command. It is the spellwork behind the spellwork: the moment Mediabot’s new plugin platform learned how to consult its own memories without handing every visitor the keys to the Restricted Section.

The Pensieve is open, but only through a guarded index. 🧠🔒

Commit: 2542001cdcaa70b05cb4a247771a75860db33874

You must be logged in to reply.