Skip to content

v0.35.2 — Capability manifest + marketplace polish

Choose a tag to compare

@dmellok dmellok released this 07 Jun 08:59

First major release since v0.34.1. Headline is the widget capability
manifest
that closes the marketplace phase 2 work
(#2) — widgets now
declare which network hosts, settings sections, and filesystem paths
they need, and the host enforces network egress at the socket layer.
Plus a topbar battery indicator + a clutch of marketplace card polish.

Highlights

  • Widget capability manifest + enforcement. A widget can declare
    what it needs in a requires: block in plugin.json:

    {
      "requires": [
        "network:api.open-meteo.com",
        "settings:app"
      ]
    }

    The host installs a hook over socket.create_connection +
    socket.socket.connect and gates every outbound connect against
    the active widget's allowlist. A widget trying to phone home to
    an undeclared host raises `CapabilityDenied` and the composer
    surfaces it as a cell-level error rather than a render failure.
    Settings + filesystem entries are review-only in v1 (Python's
    open() and current_app.config are reached through too many
    paths to interpose cleanly without process isolation, which is
    #3). Widgets
    without a requires: block load with no enforcement (the
    pre-#2 behaviour) so existing bundled + catalog widgets keep
    working. New catalog submissions are expected to declare.

  • Topbar battery indicator. When any registered device instance
    reports a battery_pct heartbeat, a small Phosphor battery glyph
    appears next to the theme toggle. Single-device installs show the
    percentage inline; multi-device installs show a count badge with
    a hover/click popover listing every battery + level. The trigger
    paints in the tone of the worst battery so a single critical
    device catches the eye when others are fine.

  • Low-battery push overlay cleanup. Dropped the black border
    (was picking up dither artifacts on some panel gamuts) and
    re-anchored the Phosphor glyph to share a baseline with the
    percentage text — they now read as on the same line instead of
    the icon visibly floating above the digits.

  • Marketplace card thumbnails fill the preview cleanly. After
    two iterations on aspect-ratio + object-fit, settled on
    object-fit: fill so the image always fills the card regardless
    of source shape. Cards are thumbnails; the contributor's source
    repo link is one click away if a user wants the true ratio.

  • Catalog dogfooded with requires:. All three live catalog
    entries (community_demo, devref bundle, public_holiday_countdown)
    bumped to declare their capability surface — the catalog no
    longer relies on the legacy "unenforced" path, even though the
    host loader keeps it for in-tree compat.

Under the hood

  • app/capabilities.py (new): Capabilities dataclass,
    parse(), capability_scope() context manager, idempotent
    install() / uninstall() socket hooks. ContextVar-backed so
    concurrent renders don't cross-contaminate. The hook fires before
    DNS so hard-coded IPs can't dodge the gate.
  • app/plugin_loader.py parses requires: during discovery
    and hangs the snapshot on Plugin.capabilities. Malformed
    entries log + drop rather than failing the load.
  • app/composer.py _fetch_plugin_data enters
    capability_scope(plugin.capabilities) around every widget
    fetch() call. `CapabilityDenied` surfaces as
    {"error": "Capability denied: ..."} in the cell.
  • app/app_factory.py installs the hooks once after the
    registry is built. Idempotent under the dev reloader.
  • Docs: new "Capabilities, requires:" section in
    docs/widgets.md
    with the vocabulary table, enforcement sketch, backward compat,
    and an honest threat-model section (what this catches:
    manifest-vs-behaviour drift; what it doesn't catch: a determined
    attacker with ctypes — real isolation is #3). Reviewer checklist
    in docs/dev/publishing-a-widget.md
    expanded to demand requires: declarations on new submissions.
  • 3 bundled widgets ported as worked examples: weather_now
    • clock_sunrise_sunset declare network:api.open-meteo.com;
      clock_analog declares requires: [] (the explicit "no
      capabilities" form).
  • 19 new tests covering parse / scope nesting / urllib
    enforcement / legacy bypass / install idempotence. 853 total
    pass
    ; mypy --strict + ruff + mkdocs --strict all clean.
  • Catalog refreshed: all three live entries got fresh
    1200×800 tight screenshots so the marketplace thumbs render
    cleanly under the new fill behaviour.

See CHANGELOG.md
for the full breakdown.