# Path to contract 1.0 This page is an honest read of how stable the plugin contract is today, for a sister site deciding how much to build on it. It is derived from the ADRs and the live code, not aspiration. It does NOT define the contract - what each hook takes and returns is in [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md), and the versioning rules are in [ADR-002](adr/ADR-002-plugin-versioning.md). What it adds is a judgement about how much weight each part will bear. ## Current version The plugin contract version is declared in `shopdb/__init__.py` as `__contract_version__`, and the current value is in [PROJECT-MAP.md](PROJECT-MAP.md), which is generated. It is pre-1.0, which under semver means any 0.x minor bump is allowed to break the contract, and this project has used that latitude - see 0.19.0 below. The product release version (`__version__`) is a separate series with its own bump rules; see [ADR-007](adr/ADR-007-product-versioning-and-releases.md). Do not pin against it for compatibility - pin against `__contract_version__`. ### 0.x history Recorded in the comment block in `shopdb/__init__.py`: | Version | Change | Kind | |---------|--------|------| | 0.3.0 | `shopdb.api` expanded to the full plugin import surface (db, cache, model bases, core models, response + pagination helpers, `employee_connection`) so plugins stop importing internal core paths | additive (minor) | | 0.4.0 | Removed the never-implemented `get_searchable_fields` hook (search is a core concern over the asset model) and wired `get_dashboard_widgets` to a real consumer (`/api/dashboard/widgets`) | pre-1.0 contract reduction | | 0.6.0 | Added the `get_reports` hook, consumed by `GET /api/reports` to merge plugin report cards into the Reports hub | additive optional hook (minor) | | 0.7.0 | Added the four ADR-010 frontend-contribution hooks (`get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`), consumed by the `GET /api/pluginui/*` endpoints | additive optional hooks (minor) | | 0.9.0 | Exposed the dualpath pair-resolution helpers on `shopdb.api` for the machines plugin | additive surface (minor) | | 0.10.0 | Added the `get_permissions` hook so plugins declare their own RBAC permissions; the catalog is resolved dynamically from core + enabled plugins | additive optional hook (minor) | | 0.11.0 | Added `service_token_authorized(scope)` to `shopdb.api` so a plugin's unattended endpoints can authorize a scoped managed service token without importing core token internals | additive surface (minor) | | 0.12.0 | Added the mailer helpers (`send_email`, `send_alert`) to `shopdb.api` | additive surface (minor) | | 0.13.0 | Added the `User` model to the `shopdb.api` surface | additive surface (minor) | | 0.14.0 | Added `send_webhook` to the `shopdb.api` surface | additive surface (minor) | | 0.15.0 | Added `authorized_service_token` / the `SupportTeam` model to the `shopdb.api` surface | additive surface (minor) | | 0.16.0 | Added the `get_settings_defaults` hook so a plugin declares the Setting rows it owns; the framework seeds them at install, at enable, and on `flask plugin upgrade-all`, files a first-time write under the declared category, and honours `public: True` for pages that render before login | additive optional hook (minor) | | 0.17.0 | Added `DashboardDefault` to the `shopdb.api` surface, so a plugin can resolve a display without reaching into core | additive surface (minor) | | 0.18.0 | Added `DISPLAY_ROLES`, `DISPLAY_ROLE_PATHS` and `normalize_display_role`, and finally exported `DashboardDefault`, which 0.17.0 imported but left out of `__all__`. The role vocabulary became the kiosk's own - `Dashboard`, `Lobby`, `3DPrintRoom` - so a plugin holding its own copy of that map reads core's instead of drifting from it | additive surface (minor) | | 0.19.0 | **BREAKING.** `get_dashboard_widgets` returns DATA AND SHAPE, not a component name. The old shape (`name` + `component` + `size`) named a Vue component per widget, which cannot survive a lean build - a plugin's component may never be staged into the frontend bundle (ADR-013) - and five plugins were declaring widgets that pointed at components nobody had written. A card now declares `id` / `title` / `endpoint` / `render` / `severity` / `permission` / `empty` / `position`; see PLUGIN-HOOKS.md. Also added `send_upload` so a plugin serving user-supplied bytes gets the headers that keep an SVG from running as script | **contract change (minor, pre-1.0)** | | 0.20.0 | The `Asset` model gained `levelid` (which floor plan its `mapx`/`mapy` are pixels of) and `mapverifiedat`; `Location` gained `levelid` too, and `resolve_asset_position` now returns the `levelid` belonging to whichever source supplied the coordinates. A plugin that writes a map position MUST write the level with it - a position without one renders as "level unknown" rather than being drawn on the default drawing, because a marker on the wrong floor plan looks correct (ADR-017) | additive surface (minor) | | 0.21.0 | Added the `get_guest_grantable_permissions` hook: a plugin names the WRITE permissions a site may grant to the seeded `guest` role, which is who anyone without a token now holds. Reads (`.view`/`.read`) are grantable without a plugin saying anything; anything else is refused. Also exposed `require_any_permission` on `shopdb.api` for a read two different rights should reach | additive optional hook + surface (minor) | | 0.22.0 | Added the `register_import_hook` surface: a plugin owns a VIRTUAL COLUMN on an importable CSV table - a header the bulk importer accepts but never writes - plus a post-write row hook that turns the value into a plugin concept. The computers plugin uses it for `machineassetnumber` on `computers.csv`, which builds the PC->machine controls link, so a site writes the machine number in the PC's own row instead of hand-building a separate relationships sheet. Core never learns what the column means. `RowWarning` is exported with it so a hook can report a soft failure without aborting the import | additive surface (minor) | | 0.23.0 | Added `mint_service_token`, `list_service_tokens` and `revoke_service_tokens` so a plugin can provision, list and revoke a single-scope managed service token without importing core token internals. The endpoints plugin uses them for a UI-managed, regenerable agent key scoped to `collector.ingest.endpoints` alone - narrow on purpose, so rotating or revoking it cannot disturb the computers collector, and revocation is an instant kill switch on agent ingest | additive surface (minor) | | 0.24.0 | Added `collector_authorized(pluginname)` so a plugin can gate an agent-facing route that lives OUTSIDE the generic collector blueprint on the same per-plugin key and kill switch the ingest route uses. The endpoints command channel needs it: the agent long-polls `/api/endpoints/commands/next` and posts results back, and those are agent callbacks rather than JWT routes. Without it a plugin would reach into core collector internals to authorize them | additive surface (minor) | | 0.25.0 | Added `caller_holds(permission)`, which answers whether the current caller holds a permission WITHOUT refusing the request - the complement to `require_permission` for a route that serves everyone but should only offer what the caller can act on. The endpoints services panel is the case: it emitted Start/Stop row actions to every reader, because the emission never consulted the caller. Clicking them was always refused server-side, so nothing was exploitable, but offering a read-only viewer controls over a shop-floor PC is the wrong signal, and it becomes a real problem once a site grants that panel to guests. Guests are included in the answer, since a signed-out visitor holds whatever the guest role was granted | additive surface (minor) | | 0.26.0 | Dashboard cards may declare `shortname`. The dashboard merged its per-source cards into one worst-first queue, and a card's `title` is written as a heading ("Endpoints with crashes or shutdowns") which as a row badge ran wider than the hostname it qualified. Optional, defaults to the title, so a plugin that does not declare one is unaffected | additive field (minor) | | 0.27.0 | Added `Script` so a plugin can read the ShopDB-hosted script library that backs endpoint `run_script` delivery without importing core model internals. The endpoints plugin resolves a delivered script by name and serves its body by digest to the agent | additive surface (minor) | | 0.29.0 | Added an optional `group` to `get_asset_panels`, naming which detail-page tab a panel belongs to: `details` (the default) / `monitors` / `software` / `activity`. The detail pages are tabbed, and a page that placed panels by knowing their ids would defeat the hook. Additive: an absent or unrecognised group resolves to `details` server-side, so every existing plugin keeps working with no change | additive surface (minor) | | 0.28.0 | Added `client_ip` and `strip_port` so a plugin does not hand-roll caller-IP extraction. Two plugins had, and both read the first `X-Forwarded-For` hop, which the caller supplies - so a rate limit keyed on it was defeated by rotating the header. The helper resolves from `remote_addr`, which ProxyFix has already filled from the header when `TRUSTED_PROXY_COUNT` says a proxy is really in front | additive surface (minor) | | 0.31.0 | The asset-panel vocabulary a plugin may declare grew four optional things: two more tab groups, `serviceprocess` and `reports`, beside the 0.29.0 set; a `format: 'datetime'` on a column or field, so a panel stops shipping pre-formatted date strings the browser cannot re-localise; a `map.body` key, so a keyvalue field can name where its value lives in a nested payload; and a `tabs` section that carries `columns`/`rows`/`empty` and renders as a table, so one card can hold two tables that share no key (volume capacity and physical drive health are the case). All optional and all resolved server-side when absent, so a plugin declaring none of them is unaffected | additive surface (minor) | The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, 0.10.0, 0.11.0, 0.16.0, 0.18.0 and 0.19.0; 0.12.0 through 0.15.0 and 0.17.0 are recorded in this table only. **0.19.0 is the one to read before pinning.** It is the only entry in this series that took something away, and it shipped before it was recorded here - which is precisely the reason to pin tight rather than to trust that a minor bump is safe. Earlier points (0.1.x / 0.2.x) predate that recorded rationale; `PluginMeta`'s fallback `core_version` default of `>=0.2.0,<1.0.0` is the only remaining trace of the 0.2 baseline. ## Settled surface These are unlikely to break before 1.0. A plugin can depend on them with reasonable confidence; a breaking change to any would be a major bump and would land with a new or amended ADR. What each one does is in [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md); this is only the stability judgement. - The required hooks and the manifest schema: `meta` / `PluginMeta`, `get_blueprint`, `get_models`. - Plugin plumbing: `init_app`, `get_cli_commands`, `get_services`, the settings helpers (`get_setting` / `set_setting`), and the four lifecycle hooks. - The UI-contribution hooks: `get_navigation_items`, `get_dashboard_widgets`, `get_reports`, `get_permissions`, and the ADR-010 set (`get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`). - The collector pair, `get_collector_schema` + `apply_collector_payload`, per [ADR-006](adr/ADR-006-collector-contract.md). - The setup-wizard declarations: `get_provisioning_note`, `get_config_schema`. - The `shopdb.api` import surface itself: adding a name is minor, removing one is major. The asset model itself - `Asset`, `AssetType`, `AssetStatus`, `AssetRelationship`, and the shared reference models - is the platform contract locked in [ADR-001](adr/ADR-001-asset-as-platform-contract.md). ## Expected churn before 1.0 Known-unstable areas. Building on these means expecting rework. | Area | Status | Reference | |------|--------|-----------| | Frontend renderers (residual) | The four data-only hooks and their `/api/pluginui/*` consumers are settled (0.7.0). The generic core renderers are landing incrementally: the settings-cards rail/landing renderer ships with 0.7.0; the asset-panel, map-overlay, and search-presentation renderers are wired opt-in per the ADR adoption plan. Real component-backed panels (bespoke charts, custom overlays) remain deferred to Option C (build-time glob discovery). | [ADR-010](adr/ADR-010-frontend-plugin-hooks.md) (ACCEPTED) | | Per-plugin migrations | Brand new. The per-plugin Alembic engine exists and every bundled plugin now carries a chain, but the pattern has one release of production mileage, not years. | [ADR-008](adr/ADR-008-plugin-migration-ownership.md) (2026-07-10) | | Pip distribution | Deferred to v2. External plugins install by clone / submodule / symlink; there is no entry-point discovery and no automatic update path yet. | [ADR-003](adr/ADR-003-plugin-distribution.md) | ## Bump rules From [ADR-002](adr/ADR-002-plugin-versioning.md), applied to `__contract_version__`: | Bump | Trigger | |------|---------| | major | Breaking change to the `BasePlugin` ABC, the `PluginMeta` schema, or any model in the platform contract (`Asset`, `AssetType`, `AssetStatus`, `AssetRelationship`, `Vendor`, `Location`, `BusinessUnit`, `Model`, `OperatingSystem`). Removing a name from `shopdb.api` is major. | | minor | Additive change: a new optional hook, a new field on a contract model with a default, a new name added to `shopdb.api`. | | patch | Bug fix with no change to the contract surface. | ### Deprecation policy ADR-002 defines the bump classification above but is **silent** on any deprecation window or notice period for pre-1.0 removals. In practice a removal is simply a major (or, pre-1.0, a breaking minor) bump: the hook or name is gone, and the loader fails loud in dev or excludes the mismatched plugin in prod (see the load-time table in [PLUGIN-EXTERNAL-REPO.md](PLUGIN-EXTERNAL-REPO.md)). Because the ADR says nothing about a grace period, this document proposes none; the honest guidance for sister sites is the mitigation that already exists - pin a tight `core_version` range and re-test before widening it. ## Criteria for declaring 1.0 Provisional. This is the maintainer's working list, not a committed checklist, and it will move. ADR-002's own open question ("When does the framework declare 1.0.0?") ties 1.0 to the `Machine` retirement from ADR-001 and the framework being "ready for sister sites"; the items below expand that intent. - Frontend hook contract defined via its own ADR (asset-detail panels, map markers, search results), closing the biggest churn item above. - `Machine` / legacy-model retirement complete per ADR-001, so the asset model is the only contract. - At least two external plugins running in production at a second site - the bar ADR-003 already sets before pip distribution and sister-site readiness are considered justified. - The per-plugin migration pattern (ADR-008) proven across real upgrades, not just fresh installs. - No contract bump needed for N consecutive product releases (N to be fixed when the list is firmed up), showing the surface has actually settled. Until then: pre-1.0, pin tight, re-test on every framework bump.