Skip to content

v0.4.10: settings an install can default, and a config that arrives whole

Choose a tag to compare

@matAtWork matAtWork released this 27 Aug 09:05
· 123 commits to main since this release
b00bc49

What's Changed

  • #52 — install defaults for plugin settings, from the config (#51) by @matAtWork
  • A patch release: one new config surface, plus a parser defect found while sizing it that could silently drop half a config file.

Full Changelog: v0.4.9...v0.4.10


What it's for

A plugin keeps its own runtime settings in matbot's store — a classifier provider, a list of tools to ignore, a tuning knob — and until now the only way to ship an install with those already set was to wrap the plugin in a package of your own that initialises them. That is worse than it looks. Plugin identity is loader-derived from the package name, and the settings namespace is that name, so a wrapper moves the namespace: whatever the install had already stored is orphaned, both copies collide on every tool name if both load, and the wrapper has to track upstream for ever. One wrapper per plugin you wanted an opinion about.

The shape

default_settings:
  @matatbread/matbot-triggers:
    classifierProvider: fast-haiku
  @matatbread/matbot-cognition:
    innerVoiceProvider: fast-haiku
    dream:
      maxItems: 5

Keys are plugin package names — what plugin list reports, which need not be the specifier you wrote under plugins: (a plugin loaded as ./plugins/triggers is still named @matatbread/matbot-triggers). A key naming no loaded plugin is warned about at boot, because it would otherwise look like it had worked. Values are opaque: matbot does not interpret what a plugin stores. BrowserConfig.defaultSettings is the browser analogue, baked into the bundle.

Nothing in plugin-api changed and no plugin changed. Every consumer already spelled (await settings.get(k)) ?? codeDefault, so the new layer slots in below the store and above the code default — which is how every existing knob became config-defaultable at once, and the test for whether this belonged in matbot at all rather than in a wrapper.

The semantics

One rule: reads are layered, writes are not.

Why not the alternative
Read only when the store has no value yes
Seeded into the store on install no Once the document exists, editing the yaml does nothing, for ever, silently
Seeded when settings are next written no set('A') would freeze B's default: whether a yaml edit takes effect would depend on unrelated write history
Merge granularity per key A key is the only addressable unit PluginSettings has; deep merge has no bounded semantics

get returns the stored key if present — in, so a stored null is an override a plugin gets to interpret — else the install's default, else undefined. The compare-and-swap write path reads the stored document only, so set persists exactly the key it was given: a probe reading three defaulted keys leaves data: {} on disk.

delete therefore means revert to the configured default, which is what every existing clear action already meant. Precedence reads store → config → the plugin's own code default.

Both seeding options were also wrong about ownership: they write a per-principal document for what is install-wide configuration, so under storage/profiles a boot-time seed lands under whichever principal booted and no other one ever gets it. A default is config rather than data, so it applies to everyone, cannot be destroyed by a plugin or provider update, and survives a StorageBackend swap.

Also: the config parser no longer returns half a file

Found while sizing the format options, and the more serious of the two changes.

An unparseable construct made the parser break its enclosing loop, which returned what had been read so far and left the rest of the document silently discarded. A stray - in plugins: — the dash alone on its line, a plausible hand-editing artifact — dropped every plugin after it and every top-level section below it, providers: included, with no error. An install that boots and behaves as though half its configuration had never been written. A config parser returning a subset of the file is worse than one that fails: the failure is one message, the subset is a running install.

It now throws, naming the line. Two constructs it could not read are now read:

  • A bare - takes the block indented beneath it as its item value. The branch for that existed but was unreachable — the dash test required a trailing space, so - alone matched nothing and then failed the mapping test too, which is precisely how the truncation arose.
  • A quoted mapping key is unquoted like any other scalar. '@scope/pkg': addressed a key literally spelled with its quotes; nothing hit it before because provider names are written bare.

YAML's compact mapping in a sequence entry (- key: value) stays unsupported and is now rejected rather than mis-read. Telling it from a plugin specifier needs the spec's rule that a key separator is a colon followed by space or end-of-line, without which - https://host/p.ts parses as a mapping keyed https — so http specifiers make this permanent rather than incidental. Verified against every yaml in the repo: identical parses, bar the intended key unquoting.

Deliberately not built

  • No tool to author a default. The runtime write path is the override — CAS'd, per-principal, notifiable — and for a single-principal install "set the override" is observationally identical. A tool for the floor would be a second way to do one thing, with restart semantics and no CAS.
  • No ${NAME} resolution in these values. Secrets are the vault's; resolving at get time would give the settings facade a Vault dependency it does not have.
  • No override (top-precedence) layer. Two layers is a lookup; three is a policy engine.
  • No separate defaults file — but the seam is open at near-zero cost, because core takes a defaults map, never a path. Which file supplies it is a host detail, so a matbot.defaults.yaml merged before injection is a later ~10-line host change. Note that extends: is not that mechanism today: the CLI chdirs to the base's directory and rewrites configPath, so a shared base becomes the project — .data, .env and every yaml write land beside the base rather than the install.

One consequence to know: nothing distinguishes a defaulted read from a stored one, so a *_config get action reports a configured default where it used to say pinned: null. That is the value in effect, which is what the model needs; the wording in those descriptions has yet to catch up.

Versioning

core, plugin-api, cli and web-bundle move together as a changesets fixed group. plugin-api is unchanged in this release and bumped anyway: the boot banner reads any difference between the CLI and the resolved core/plugin-api versions as two physical copies of a host singleton, and about_matbot reports the app's own version.

Published: core, plugin-api, cli, web-bundle at 0.4.10. No plugin packages changed.