feat(integration): cross-app leaf provider registration (ADR-066)#2108
Merged
Conversation
Add RegisterLeafProvidersEvent — a typed collect-event mirroring
RegisterMcpToolProvidersEvent — so a sibling Nextcloud app can register
itself as a leaf on OpenRegister objects (render surface + app-local
read/append data) by writing one IEventListener. Render-and-read only by
construction: descriptor and provider carry no verb; cross-app commands
stay ADR-041 typed events.
- LeafDescriptor value object: id, label, icon, requiredApp, group,
surfaces, referenceType, requiresPermission, kinds set
{render-surface, data-provider, agent-runner (reserved)}.
- LeafRegistry: dispatches the event once lazily on first read, validates
(non-empty/known kinds, kebab-case id, data-provider-requires-provider),
first-wins on duplicate id (ADR-013), swallows a throwing listener so it
costs only its own leaf, and lands data-provider leaves on the shared
IntegrationRegistry via a lazy loader hook so existing per-object routing
reaches them unchanged.
- app-local storage strategy documented on the IntegrationProvider
contract: data lives in the sibling app's own store; OpenRegister routes
list()/optional create() and persists nothing.
- Discovery: openregister.integrations.leaves OCS capabilities block
(id, label, requiredApp, surfaces, kinds, usability) — discoverable
without loading any leaf app's JS.
- Tests: 22 PHPUnit tests (event registration, app-local list/create,
kinds, first-wins collision, throwing-listener isolation, OCS usability,
render-and-read boundary).
- Docs: docs/Integrations/leaf-system.md cross-app section + worked notes
leaf example, cross-referencing ADR-019/041/013/066.
Spec: openspec/changes/app-leaf-provider-registration (PR #2102).
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 555/555 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-07-25 09:42 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Gives any sibling Nextcloud app a sanctioned server-side seam to register itself as a leaf on OpenRegister objects — contributing render surfaces and app-local read/append data — through a typed collect-event, the same idiom OpenRegister already uses for MCP tool providers and flow nodes.
Implements spec #2102 (
spec/app-leaf-provider-registration) and ADR-066 (cross-app leaf registration — the render-and-read boundary; lifts the ADR-041 moratorium strictly for the collect/render/read case). Render-and-read only by construction: descriptor + provider carry no verb; cross-app commands stay ADR-041 typed events.What changed
New
lib/Event/RegisterLeafProvidersEvent.php— typed collect-event mirroringRegisterMcpToolProvidersEventline for line (registerLeaf()+getLeaves()).lib/Service/Integration/LeafDescriptor.php— value object: id, label, icon, requiredApp, group, surfaces, referenceType, requiresPermission, and a non-emptykindsset{render-surface, data-provider, agent-runner}(agent-runner reserved — hermiq's change consumes it).lib/Service/Integration/LeafRegistry.php— dispatches the event once, lazily, on first read; validates (non-empty/known kinds, kebab-case id, data-provider-requires-provider); first-wins on duplicate id (ADR-013); swallows a throwing listener so it costs only its own leaf; lands data-provider leaves on the sharedIntegrationRegistry. ExposesdescribeForCapabilities()for discovery.LeafDescriptorTest+LeafRegistryTest(22 tests / 174 assertions).Changed
IntegrationRegistry— lazysetLeafLoader()hook (guarded, once-per-request) so a data-provider leaf lands beforeObjectIntegrationsControllerresolves it, without eager dispatch.IntegrationProvider— documents the newapp-localstorage strategy (data lives in the sibling app's own store; OpenRegister routeslist()/optionalcreate()and persists nothing). Existing strategies untouched.IntegrationsCapability— newopenregister.integrations.leavesOCS block (id, label, requiredApp, surfaces, kinds, usability) — discoverable without loading any leaf's JS.Application::bootLeafRegistry()— installs the lazy loader; registers theLeafRegistryservice.docs/Integrations/leaf-system.md— cross-app section + worked notes-leaf example (ADR-019/041/013/066).app-local routing
The existing
ObjectIntegrationsControllerdispatch already calls provider methods directly, soapp-localneeds no controller branch: the provider'slist()/create()run in the sibling app's DI context; a read-only leaf lets theAbstractIntegrationProviderdefault throwNotImplementedExceptionwhilelist()stays usable.Verification
nextcloud:34container (174 assertions): announced leaf reaches catalogue; built-ins survive; throwing listener isolated; empty/unknown kinds rejected; data-provider-requires-provider; first-wins collision; namespaced ids coexist; app-local list/empty/create + read-only-throws; OCS reports leaves + usability (disabled required app → unusable); render-and-read boundary (contract exposes no command verb).RegisterLeafProvidersEventdispatcher appears indescribeForCapabilities(), its data-provider is routable onIntegrationRegistry, and app-local list→create appends only in the provider's own store. Probe removed (no test provider shipped).php -l+composer phpcsclean on all changedlib/files; spec-coverage (gate-16) passes vsorigin/development.Out of scope / follow-up
@conduction/nextcloud-vue(registerIntegration()/check-integration-parity.js) and the canonical gate-24 in hydrarun-hydra-gates.sh. The openregister wrapper only delegates to the JS check, so the cross-app correlation is a coordinated follow-up in those repos — not edited here.agent-runnerkind is a reserved forward-reference only.Spec: #2102 · ADR-066 (hydra).