Skip to content

Contribution and realm contract #5

Closed

Description

@dvcolomban

Accepted two-layer selection amendment, 2026-09-27

The owner chose typed command/results with native state observation, and implementation-owned applicability. Contract migration and authenticated native proof are on main. The architecture diagram and API examples supersede the earlier universal target contract and A1/A2/B mapping questions.

Client selectors choose realm/provider recipients. Selected action/service implementations inspect schema-defined domain/resource input and return their own declared result, such as applied or not-applicable. There is no SDK target envelope, target resolver, accepts hook or new topic/event bus. A not-applicable result is fulfilled; it neither makes the provider unavailable nor triggers fallback/replay. If multiple selected implementations match, all may act.

Provider incarnation, contribution activation generation, exact versions, native authorization/serialization and dispatch ownership remain unchanged. Browser/debugger capabilities still validate actual resources and permissions.

Part of Design a portable contribution SDK and WebExtension runtime.

The accepted resolution and canonical contract files supersede the historical proposals below. This issue resolves the design contract, not runtime implementation.

Question

What is the smallest complete contribution and provider contract that lets one framework-neutral feature run across devframe, Vite DevTools and browser extensions while exposing typed local native contexts and accurately declaring capabilities?

Context and current behavior

The project will ship generic packages in a pnpm/Turbo monorepo under dvcol/devkit-extension. Consumers assemble contributions at build time. A contribution can contain UI, actions, state, transforms, native integrations, shared behavior, or any combination; it does not need a panel to be useful.

At devframe commit a3f967755af99a0a0d8dd82b7b5d4f3dd4aec0bc, DevframeNodeContext contains RPC and server-oriented services. At DevTools commit 121656dfc5866b2f8de5baa8e0b6166ec28599c8, KitNodeContext extends the same hub context with Vite primitives. These inspected branches show layered contexts, not mutually exclusive devframe/devtools runtimes. Their release availability must follow Upstream reuse audit.

The common contract must distinguish a provider instance, its declared realm, an execution context and a UI surface. A browser-extension provider can execute across a worker, content script, page script and extension documents, while its UI appears in several places. Raw native objects stay in the process or document that owns them.

Requirements and scope

  • Specify contribution identity, build-time registration, entry-point discovery, dependencies, duplicate handling, activation and disposal.
  • Make realms and capabilities extensible for downstream adapters. The initial supported hosts are devframe, Vite DevTools, Chromium and Firefox; any optional CDB adapter requires independent proof before claiming support.
  • Define common behavior separately from provider-native behavior. Common availability distinguishes implementation/execution readiness and missing permissions. Request-specific domain/resource applicability belongs to the implementation and its input/result schemas.
  • Type native context access through a discriminant, while preserving the layered relationship of server contexts. Remote calls carry serializable identifiers and data, never raw context objects or callbacks.
  • Keep provider selection, separate provider state and per-provider outcomes compatible with their dedicated decisions. This ticket defines their integration points rather than inventing competing routing or state rules.
  • Require a runnable example for every package role and complete behavioral coverage of every public SDK API, hook and feature across supported hosts.

Options and recommendation

A single large context can expose every possible native API as optional fields. It is approachable initially but makes invalid combinations and partial support difficult to explain. Separate provider-specific SDKs have clearer native typing but fragment shared features.

Resolve with the user a shared contribution definition, capability contracts and extensible provider descriptors with typed local native access. Adapters implement those contracts and retain native APIs. Decide between explicit contract objects and declaration merging, recording consequences for dependency order and downstream authors.

Proposed API or experiment

Illustrative pseudocode only; names and signatures remain decision outputs:

const inspectionContribution = defineContribution({
  id: 'example.inspection',
  entries: {
    provider: './provider.ts',
    page: './page.ts',
  },
  capabilities: ['state', 'actions', 'json-view'],
});

export function setupProvider(context: ContributionContext) {
  const registration = context.actions.register('read-title', readTitle);
  context.lifecycle.onDispose(registration.dispose);

  if (context.native.realm !== 'webext') return;
  if (context.native.execution !== 'background') return;

  const installedEvent = context.native.browser.runtime.onInstalled;
  installedEvent.addListener(onInstalled);
  context.lifecycle.onDispose(() => installedEvent.removeListener(onInstalled));
}

Use this sketch to settle entry locations, targets, registration timing and cleanup. Replace capability strings with the chosen typed model. The illustrative execution discriminant narrows privileged background access separately from the realm; page and content contexts must not acquire that privilege through realm narrowing alone.

Walk through one contribution with UI and actions, one transform-only contribution, one page-only integration and one provider-native integration. Publish a contract inventory linking every exported type and hook to observable behavior. The example layout must contain runnable contribution, renderer, devframe-host, devtools-host, Chromium-host and Firefox-host applications. Shared fixtures may reduce duplication, but each host must launch and exercise its integration.

Scenarios and acceptance criteria

  • A UI-free contribution activates and disposes without a renderer or open panel.
  • The same shared contribution executes under two providers without merging their native contexts or state.
  • Adding a downstream realm and capability requires no modification to a closed central union; incompatible capability versions fail explicitly.
  • A worker restart, page navigation, panel close and contribution replacement each have defined ownership and cleanup outcomes. Repeated activation leaves no duplicate listener or registration.
  • Missing permission and unsupported capability produce distinguishable outcomes; domain-specific resource failures use their capability contract, including per-provider results when routing fans out.
  • Every public API and hook has a named runnable example and real-host assertion covering success where supported, unsupported behavior where not, failure and relevant lifecycle transitions. Compile checks and browser mocks supplement this proof; they cannot replace it.

Dependencies

Blocked by Upstream reuse audit and Browser capability audit using native issue-tracker blocking relationships. The first establishes reusable contracts; the second establishes actual browser and execution-context limits. This ticket unblocks Server adapter contract and other provider implementations. Its parent-map relationship is organizational, not its dependency representation.

Definition of Ready

  • Both blocking audit tickets are resolved with accessible evidence and an explicit supported-host baseline.
  • Contribution scope, build-time assembly, extensibility and local native context requirements are accepted.
  • A concrete shared feature and a native-only feature are available as examples for the live design discussion.

Definition of Done

  • A user-reviewed resolution fixes vocabulary, public interfaces, extension mechanisms, registration lifecycle, capability availability semantics and native-context boundaries.

  • The resolution includes complete representative pseudocode with no unexplained lifecycle owner or execution location.

  • Public exports and hooks are enumerated with exact example and test obligations for every supported host, including failure and unsupported outcomes.

  • Routing and state integration points are explicit; unresolved behavior becomes a linked decision dependency rather than an implementer choice.

  • Record rejected alternatives and compatibility consequences, close the issue, and add its named decision pointer to the map. Completion settles the contract, not implementation delivery.

  • The human has confirmed the resolution through discussion; the agent has not supplied the human side of the decision.

Resolution record

Post the agreed contract and terminology, native-context typing examples, lifecycle table, capability behavior, example/test inventory, alternatives considered and follow-on decisions in a resolution comment. The map retains only a named link and a short gist.

Activity

  1. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Upstream reuse audit resolution supports reusing published RPC/state/JSON contracts through local adapters. The inspected kit context layers Vite fields onto hub and Devframe contexts; these are not mutually exclusive provider identities. The common contract must distinguish provider identity, execution context, UI surface and layered native primitives, with raw handles remaining local.

    Acceptance: show valid UI-only, behavior-only and mixed contribution declarations; define minimal action/state/view/lifecycle interfaces and extensible capability outcomes without requiring a complete Node context from extension providers. Renderer action forwarding does not establish routing or authorization policy. Kit context source.

  2. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Research handoff and acceptance gates

    Sources: Browser capability audit.

    Accountable owner: dvcol, until reassigned. These observations inform this open contract; they do not resolve it.

    The browser audit resolves the semantic availability baseline: unsupported implementation, wrong execution context, missing permission, restricted target, disconnected backend, conflicting owner and stale target are distinct outcomes. Public names and serialization remain this contract's decision.

    Acceptance additions:

    • Narrow native access by execution context as well as provider realm: MAIN has no extension messaging, isolated scripts have messaging but no debugger, and Firefox has no equivalent native extension debugger.
    • Preserve those distinctions through capability descriptions, operation failures and observable transitions. Do not serialize native namespaces/listeners/handles.
    • An unavailable optional capability must leave the contribution's other supported behavior usable. Prove this through real host examples after the contract is resolved.
  3. self-assigned this
    on Sep 22, 2026
  4. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Contribution and realm contract: review packet

    Status: proposed, awaiting the project owner's architectural choices. This is preparation for Contribution and realm contract, not an accepted public API or SDK implementation. The canonical decision will live in the issue's eventual resolution comment.

    Grounding and settled constraints

    The map already fixes build-time executable contributions, framework-neutral authoring, extensible capabilities and realms, separate provider state, and native resources local to their owner. A contribution can contain UI alone, behavior alone, or both. These requirements do not need another approval round.

    Upstream reuse audit establishes reusable released RPC/state/JSON contracts and layered Devframe/hub/DevTools contexts. A Vite DevTools provider may expose several native server contexts at once. It is not three competing providers solely because three library contexts are present.

    Browser capability audit establishes local execution boundaries and distinct unavailable outcomes. MAIN scripts have no extension messaging, isolated scripts cannot access the debugger, and Firefox lacks the Chromium native extension debugger. An unavailable capability must not silently disable unrelated contribution behavior.

    Toolchain and reload audit proves a portable TS7/Vite/Oxc pipeline. Its WXT declaration/reload gates remain owned by later contracts. This ticket does not choose a packager or equate source edits with successful runtime replacement.

    The glossary records only the settled distinctions. The Feature grouping below is a proposal and therefore is not added to that glossary yet.

    First decision: how packages extend the API

    Recommend explicit imported contract descriptors. A downstream package exports a descriptor carrying its stable identity/version and TypeScript operation types. Providers register implementations against it. Consumers import that same contract to obtain a typed binding. No central realm union or global TypeScript augmentation is required.

    Illustrative shape only:

    interface PageTitleApi {
      read(input: { target: TargetReference }): Promise<{ title: string }>;
    }
    
    export const pageTitle = defineCapability<PageTitleApi>({
      id: 'example.page-title',
      version: 1,
    });
    
    const binding = await context.capabilities.connect(pageTitle, {
      target: context.target,
    });
    
    if (binding.status !== 'available') {
      return showUnavailable(binding.reason);
    }
    
    const result = await binding.api.read({ target: context.target });

    TargetReference, defineCapability, connect, and the result shape are proposed core exports. showUnavailable is example UI behavior, not an SDK hook. The read operation executes in the selected provider's eligible execution context; its consumer receives a serializable result. TypeScript operation types alone are not runtime validation. Wire validation and authorization must be specified with Permissions and trust before implementation admission.

    The alternative is declaration merging: downstream packages augment a global capability map, and consumers use typed string keys such as context.capabilities.connect('example.page-title'). This is convenient for a fluent context API, but type visibility then depends on installed augmentations. Both choices still need runtime registration and capability negotiation; declaration merging does not provide those by itself.

    Choice Downstream author experience Tradeoff
    Explicit descriptors, recommended Import the capability/realm contract being used; register implementations explicitly More visible imports; local types and runtime declarations stay together
    Declaration merging Extend a shared type map; call APIs through known string keys Convenient completion; ambient extensions can collide or appear available in compilation without a runtime registration

    Proposed identity rule: compare the declared contract identity/version at runtime, never JavaScript object identity across separately bundled copies. Reject ambiguous duplicate implementations within the same provider registration scope. Version compatibility and schema consistency still require an explicit policy after the authoring model is chosen.

    Second decision: who owns optional feature lifetimes

    Recommend independently managed features within a contribution. A feature is the smallest group of registrations with shared execution requirements and a shared cleanup lifetime. A simple contribution has one feature. A mixed contribution can have a view feature and a debugger feature.

    The host activates a feature only when its required capabilities and execution context are available. Failure or loss of a required capability ends that feature's registrations. Other independently eligible features remain active. A JSON view can still explain why its debugger controls are unavailable.

    Illustrative build-time declaration, not a finalized manifest:

    export const inspectionContribution = defineContribution({
      id: 'example.inspection',
      features: [
        {
          id: 'title-view',
          entry: './title-view.ts',
          execution: presentationContext,
          requires: [jsonViews, actions, providerState],
        },
        {
          id: 'debugger-tools',
          entry: './debugger-tools.ts',
          execution: providerContext,
          requires: [debuggerCommands],
        },
      ],
    });

    The entry locations are resolved at build time and contain packaged executable code. presentationContext and providerContext describe roles satisfied by eligible host executions; they do not magically move a callback to another process. The adapter maps roles to concrete entry bundles. The debugger implementation can live in Chromium background code or an eligible server provider; the presentation consumes its transported operations. No debugger declaration makes Firefox's missing native API available.

    The alternative is one setup function per contribution execution. Authors check optional capabilities, register available pieces and manage their availability subscriptions and cleanup themselves. This has fewer manifest concepts but puts partial-failure and reconnection rules in every contribution author's code.

    Situation Independently managed features, recommended One setup function
    Firefox has no debugger implementation Debugger feature unavailable; independent view/actions still active Author branches correctly and keeps unaffected registrations alive
    Permission disappears while a feature is active Host ends the affected activation and its owned registrations Author watches availability and removes affected registrations
    Setup throws after registering an action Host unwinds that feature's scope Contribution setup must use the host scope correctly; partial grouping is authored manually
    Simple UI-free contribution One feature, no renderer required One setup function, no renderer required

    This recommendation does not settle automatic retry or reactivation when capability availability returns. Those choices depend on the activation unit and are the next lifecycle questions, together with async teardown and dependency failure propagation.

    Shared and native examples for the discussion

    Shared title inspector

    One contribution describes a JSON title view and a read-title action. Devframe, Vite DevTools and WebExtension providers implement the title capability. The common view invokes the same typed operation; it never imports Node, Chrome or Firefox APIs.

    Two provider instances may return different titles and retain different state. A provider binding records its provider identity and target generation. Selection, broadcast and per-provider outcomes use the integration point owned by Provider discovery and routing. Persistence and synchronization use the integration point owned by State scope and recovery. This sketch chooses neither policy.

    Native background integration

    Only an eligible local background entry may access the native extension event below. Acquiring a realm descriptor alone does not prove background access.

    export function activate(context: FeatureContext): void {
      const native = context.native.find(webExtensionBackground);
      if (!native) return;
    
      const installedEvent = native.browser.runtime.onInstalled;
      const onInstalled = (): void => {
        console.info('Example extension installed');
      };
    
      installedEvent.addListener(onInstalled);
      context.scope.onDispose(() => {
        installedEvent.removeListener(onInstalled);
      });
    }

    FeatureContext, native.find, the webExtensionBackground descriptor and scope.onDispose are proposed core/adapter APIs. This example logs an installation event locally. The callback and native event stay in the background execution. If the single-setup alternative is chosen, the corresponding context/scope belongs to that contribution execution instead.

    A server-native equivalent can expose Devframe, hub, DevTools kit and Vite resources together, where actually present. A downstream realm provides its own local descriptor and native type without modifying the core.

    Transform-only and page-only contributions

    A transform-only feature registers an HTTP transform and cleanup with the eligible provider. It has no view and requires the corresponding transform capability. Chromium debugger Fetch and Firefox filtering satisfy different implementations with explicitly declared semantic limits. CDB integration remains conditional on its managed Fetch configuration gate.

    A page-only feature runs a packaged entry against a specific document generation. It can access page-owned resources and the validated page bridge but receives no background native namespace. Document replacement ends its activation. Parser timing, CSP and frame coverage remain owned by Injection and transform contract.

    Context exposed by every capability binding

    Propose a context with separate provider, realm, execution, target and presentation descriptors. It also discriminates local implementation access from transported access.

    Binding Context information Native access
    Local implementation Provider identity/realm, actual execution, target generation, optional UI placement Typed resources owned by that execution, narrowed through an imported native-context descriptor
    Remote consumer Serializable provider/execution/target descriptors and operation availability No remote native resource property; use transported operations
    Presentation Its own local execution and UI surface; selected provider bindings remain separate Only native resources of the presentation document itself

    A provider realm such as webext never grants background APIs to its MAIN or content entries. A remote provider describing itself as webext never gives the caller a local browser namespace. Third-party realm registration extends typed access without requiring a central exhaustive union.

    Unavailable semantics already resolved by research remain distinct: unsupported implementation, wrong execution context, missing permission, restricted target, disconnected backend, conflicting owner and stale target. Snapshot availability can change before an operation executes, so invocation failures must carry the same distinctions. Public names and data shapes are still candidate contract decisions.

    Proposed ownership and failure table

    Event Owner and intended consequence Still to settle
    Host begins activation New scope for the chosen activation unit; register only after requirements are checked Feature grouping versus one setup
    Registration succeeds Scope owns its unregister callback; optional explicit disposal removes it once Exact registration/disposable API
    Setup fails partway Scope unwinds resources already registered Failure propagation to dependent features/contributions
    Popup or panel closes Presentation scope ends; independent provider work continues Native UI lifecycle proof in the renderer contract
    Target document is replaced Old document registrations end; stale operations cannot affect the replacement Cancellation and retry policy in target/routing/state contracts
    Required capability becomes unavailable Affected activation stops owning privileged operations Automatic reactivation versus explicit restart
    Worker terminates abruptly No guarantee that JavaScript cleanup runs; next activation reconciles durable ownership/state Recovery protocol in the state contract
    Contribution code is replaced Dispose the old generation where possible; identify a fresh activation Cleanup deadline, overlap and last-good behavior in the reload contract

    The host must reject cycles and missing required contribution dependencies before activation. Duplicate contribution identity must produce an explicit diagnostic, not a last-registration-wins overwrite. These are proposed defaults for the final resolution, not completed runtime behavior.

    Candidate public inventory and acceptance obligations

    This is the complete candidate inventory for this ticket's contribution/context/lifecycle layer. Action, state, routing, rendering and transform protocols remain owned by their linked decisions. Exact signatures depend on the two choices above.

    Candidate API/type/hook Example obligation Required assertions
    defineContribution, contribution identity and entry/dependency declarations Shared inspector, UI-free transform and page-only contribution Build-time entry discovery, duplicate/cycle/missing dependency diagnostics, no renderer requirement
    defineCapability, contract identity/version and operation types Downstream page-title contract Third-party registration, incompatible versions, duplicate bundled descriptors, actual runtime availability
    Realm/native-context descriptor registration Downstream test realm and WebExtension/Devframe adapters No core union edit, correct typed local narrowing, no native resources in transport payloads
    Provider/execution/target/presentation descriptors Same contribution under two provider instances Distinct identity/state, correct execution, replaced-target rejection
    Capability describe, watch, connect and invocation binding context Shared inspector plus optional debugger All unavailable distinctions; availability race during call; watch disposal; remaining features stay usable
    Activation hook and activation outcome Shared, transform-only, page-only and native features Successful activation, partial setup failure, required capability loss, no duplicate active registration
    Scope registration, onDispose, cancellation signal and disposable registration Native event listener and action/state subscriptions Exactly-once explicit cleanup, late completion behavior, abrupt termination without assuming cleanup ran
    Host registration/activation/disposal entrypoints Each host example through public packages Real host execution, independent presentation/provider lifetimes, packaging boundaries

    Each applicable row must execute on standalone Devframe, Vite DevTools, Chromium and Firefox, using public package exports. Add a downstream realm example and a replaceable renderer example. Unsupported combinations must assert unavailability while unrelated functionality continues. Mocks, type checks and parser validation alone cannot satisfy real-host claims.

    These are obligations for Examples and API coverage contract, not claims that those examples exist. Its independent inventory/registry checker must reject missing links or unsupported cells mislabeled as supported.

    Decisions needed now

    1. Use explicit imported capability/realm descriptors, or a globally augmented TypeScript registry? Recommend explicit descriptors.
    2. Let the host manage independent feature lifetimes within a contribution, or leave partial activation and optional-capability cleanup inside one contribution setup function? Recommend independent feature lifetimes, with one feature for simple contributions.

    After those answers, settle concrete signatures, version compatibility, dependency failure propagation, async disposal and reactivation rules. Final user review still precedes closing the contract. No dependent contract is silently resolved by this packet.

    Review artifact: local, unpushed branch dvcol/wayfinder/contribution-realm, commit 01d9c0a, file docs/planning/005-contribution-realm.md. The same commit contains the glossary derived from already agreed map requirements. This comment is a proposed design round, not the contract resolution.

  5. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Contribution activation: two authoring styles

    Part of Contribution and realm contract. The owner selected explicit capability and realm descriptors. Activation style remains open. All API names below are illustrative, not implemented exports.

    Compare the same inspector: it publishes a JSON view and adds debugger actions when a debugger provider is available. Both snippets run in a provider entry. The renderer mounts the published JSON in its own UI entry. Content, page and native implementation entries remain separately packaged in either design.

    Shared code in both versions

    jsonViews, actionRegistry and debuggerCommands are imported explicit descriptors. Both versions use exactly the same installInspector and installDebuggerTools functions.

    function installDebuggerTools({ actions, debuggerApi }, scope) {
      scope.use(actions.register(captureSnapshot, async (input) => {
        return debuggerApi.captureSnapshot({
          target: input.target,
          signal: scope.signal,
        });
      }));
    }

    captureSnapshot is the example's imported action descriptor. The action registry returns a disposable registration, adopted by scope.use. The scope's cancellation signal covers that activation's calls. captureSnapshot on the debugger API is an illustrative operation to make the example concrete; this does not settle the debugger contract's command model. installInspector similarly publishes the common JSON view and adopts its registrations into its supplied scope.

    Both versions use these same proposed runtime guarantees:

    • A scope owns registrations and child activations, cancels its signal and disposes resources idempotently.
    • Starting a child activation handles setup failure by unwinding its resources and returning a failed activation outcome for reporting. It does not silently terminate unrelated children.
    • Capability observation supplies an initial snapshot without a subscribe/read gap, then serialized changes including binding replacement. It is scoped to the current provider and target generation.
    • A live binding represents an eligible implementation, not an automatically attached debugger session. User-triggered privileged work still occurs through actions and the permission/ownership rules.

    These are comparison assumptions for both designs. Async disposal ordering, retry policy and permission behavior still require contract decisions. Abrupt worker termination cannot guarantee JavaScript cleanup.

    A. Host-managed features

    The author declares each feature's requirements. The host connects availability to activation and disposal.

    export default defineProviderEntry({
      features: {
        inspector: {
          requires: { views: jsonViews, actions: actionRegistry },
          activate({ capabilities, scope }) {
            installInspector(capabilities, scope);
          },
        },
    
        debuggerTools: {
          requires: {
            debuggerApi: debuggerCommands,
            actions: actionRegistry,
          },
          whenAvailableAgain: 'reactivate',
          activate({ capabilities, scope }) {
            installDebuggerTools(capabilities, scope);
          },
        },
      },
    });

    The host resolves every required descriptor before calling activate, supplies typed capability bindings, and creates the feature's scope. whenAvailableAgain makes this example's chosen behavior explicit; the API name and default remain open.

    If debugger availability disappears, the host disposes only debuggerTools. Its actions are unregistered and its operation signals are cancelled. The inspector stays active. When a valid binding returns, the host starts a fresh debugger-tools scope under the explicit reactivation policy. Re-registering an action does not invoke it or request permission.

    Feature requirements and activation outcomes are inspectable by the host. activate is still ordinary code: authors can branch, register native listeners and manage resources inside their feature scope.

    B. One setup function per provider execution

    The contribution owns the availability-to-lifetime connection. It still receives the same typed descriptors and scope helpers; it does not need to reinvent resource disposal.

    export default defineProviderEntry({
      requires: { views: jsonViews, actions: actionRegistry },
    
      activate(context) {
        installInspector(context.capabilities, context.scope);
    
        let debuggerActivation;
    
        context.scope.use(
          context.observeCapability(debuggerCommands, (binding) => {
            debuggerActivation?.dispose();
            debuggerActivation = undefined;
    
            if (binding.status !== 'available') return;
    
            debuggerActivation = context.scope.startChild((scope) => {
              installDebuggerTools({
                actions: context.capabilities.actions,
                debuggerApi: binding.api,
              }, scope);
            });
    
            reportActivationOutcome('debuggerTools', debuggerActivation);
          }),
        );
      },
    });

    reportActivationOutcome is the example's reporting helper, not an implicit SDK feature registry. startChild parents the child activation to the entry scope, so closing the entry disposes both its observer and any active child. The observer's sequence implements the same reactivation behavior as A. Production types would name the activation handle; the sketch omits type boilerplate to focus on ownership.

    If debugger availability disappears, the author's callback disposes the debugger child while leaving the inspector active. If availability returns, the callback creates a fresh child. The host knows the child resources, but the semantic label, requirements and relationship to debugger availability live in the author's code unless separately declared.

    As optional capabilities grow, the author adds observers and combines their readiness. Grouping them into one activation and propagating dependent failures also belongs to this setup code. The host still enforces authorization and stale-target rejection at invocation time in either design.

    Behavior and tradeoffs

    Situation A: host-managed features B: one setup function
    Firefox has no applicable debugger provider Host leaves debugger feature unavailable Observer receives unavailable; callback returns
    Debugger binding disappears Host disposes the affected feature Author callback disposes the child
    A compatible binding returns Declared policy starts a new feature scope Author callback starts a new child
    Several capabilities are required together Host resolves the declared requirement group Author combines observations and readiness
    Show why functionality is inactive Host can expose declared requirements and outcomes Contribution supplies semantic status/reporting
    Custom lifecycle conditions Put imperative logic inside a feature Express conditions directly in setup
    Small contribution without optional behavior One feature with one activation One setup function

    I recommend A as the normal authoring model, while retaining scopes and imperative APIs inside activate. The host owns repetitive availability/lifecycle wiring and diagnostics; contributions keep control of their behavior. B is reasonable if minimizing declarative concepts matters more than uniform lifecycle behavior across third-party contributions.

    Open choice: A or B as the primary authoring model. Selecting A does not yet choose automatic reactivation as the default; both sketches explicitly demonstrate reactivation only to make their behavior comparable.

    Recorded owner decision: explicit imported capability and realm descriptors. Global TypeScript declaration merging is not the primary extension mechanism. The owner requested these activation sketches before choosing the second model, so the contract remains open.

    Local review commit: 6ef6de2 on dvcol/wayfinder/contribution-realm, scoped to this ticket. Markdown structure, local links, snippet syntax and the diff were checked. These are design sketches, not implemented or type-validated SDK APIs.

  6. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Plugin definitions, contributions and host runtime

    Proposal for Contribution and realm contract. The owner selected explicit imported descriptors and requires definition modules to stay separate from the core runtime. Host-managed lifecycle is their preference, pending a concrete contract. The names and signatures below remain proposed.

    Separate the things we are naming

    The earlier defineContribution sketch described an entire installable tool, its entry modules and its activation. Calling all of that a contribution obscures what the tool adds to its host. Recommend the following vocabulary:

    Term Responsibility Example
    Plugin definition Identifies a reusable, installable tool and its packaged entries A page inspector
    Contribution definition Describes something the plugin adds to the host An action, JSON view, state declaration or HTTP transform
    Capability descriptor Identifies a typed service contract that a provider can implement Read a page title, perform debugger operations
    Capability implementation Supplies that contract in an eligible execution context A browser-backed page service
    Host Composes plugins, capability implementations and contribution registries WebExtension or Devframe host
    Plugin handle Observes and controls one live plugin installation Enable, disable, inspect status and dispose

    A plugin can contribute behavior alone, UI alone, or both. A package may export several plugins or contribution definitions. A plugin is not necessarily an npm package or a separate browser extension. The browser extension itself can host many plugins.

    Reserve handler for an actual callback, such as an action handler. Remove the earlier generic features/handlers layer. A contribution can have dependencies and an owned registration lifetime without every author inventing another feature grouping.

    Integration is a reasonable alternative for the installable unit, especially in host documentation, but it is also used for host adapters and external connections. Module clashes with JavaScript entry modules. Capability is already needed for the common service contract. Recommend Plugin for the installable unit and Contribution for what it adds.

    The existing glossary still records the earlier whole-feature meaning of contribution. This packet explicitly proposes revising that meaning; it does not silently treat the rename as accepted.

    Architectural basis

    The inspected public Devframe definition helper returns a typed definition. Host installation separately resolves assets, queues services and invokes setup. Vite DevTools adds a host adapter around that definition. These are the useful boundaries to retain:

    These links identify the inspected checkout revisions, not a new claim about the latest released API. Release reuse constraints remain in Upstream reuse audit.

    The owner also asked us to follow the composition pattern of their existing tooling. The generic lesson is that feature authors supply typed definitions or contribution results, and a separate composition root installs them, projects their metadata and owns disposal. This proposal carries that pattern forward without importing downstream application code or domain contracts. Some existing definition factories already allocate resources, so inert definition helpers are a deliberate strengthening of that boundary. Complete unloading is also a new obligation; existing subscription cleanup alone does not prove that actions, views and every native registration can be removed.

    Package and import boundaries

    Illustrative package names, not a naming decision:

    Package or module Contains Allowed dependencies
    definitions Plugin, capability, execution and native-context descriptors; common context and lifecycle types Portable types and small definition helpers
    Capability contracts Typed operations, schemas and explicit descriptors Definitions and portable schema dependencies
    Contribution definitions Action, JSON view, transform and other typed declarations Definitions and relevant capability contracts
    runtime Installation, binding resolution, registries, ownership, cancellation and status Definitions and portable runtime utilities
    Host adapters Native implementations, entry loading and transports Runtime, contracts and eligible platform libraries
    Renderer implementation Maps a JSON view and actions to an actual UI View contracts and the renderer's chosen UI implementation
    Example plugins Feature definitions and packaged entry code Definition/contract modules; local native modules only in eligible entries

    A definition helper constructs a typed descriptor. Importing it does not start a host, open a connection, install a listener or import the core runtime. Executable callbacks are packaged code; a definition containing callbacks is not a JSON wire payload. Only serializable metadata and validated operation arguments/results cross contexts.

    Keeping definitions and runtime separate is a dependency boundary. A setup function can be declared in a definition and invoked later by the runtime without the definition package depending on that runtime.

    Concrete authoring sketch

    Define a capability contract

    // contracts/page.ts
    import { defineCapability } from '@portable-sdk/definitions';
    
    export interface PageApi {
      readTitle(
        input: { target: TargetReference },
        options: { signal: AbortSignal },
      ): Promise<{ title: string }>;
    }
    
    export const pageCapability = defineCapability<PageApi>({
      id: 'example.page',
      version: 1,
    });

    TargetReference is a common contract type. Real operation declarations also require runtime schemas and the agreed unavailable-outcome shape. TypeScript types alone cannot validate a message. This example isolates the capability identity from any implementation; it does not settle its wire schema.

    Define what the plugin contributes

    // contributions/read-title.ts
    import { defineAction } from '@portable-sdk/actions/definitions';
    import { pageCapability } from '../contracts/page.js';
    import { readTitleInput, readTitleOutput } from './schemas.js';
    
    export const readTitleAction = defineAction({
      id: 'example.inspector.read-title',
      input: readTitleInput,
      output: readTitleOutput,
      requires: { page: pageCapability },
    
      handler({ input, services, signal }) {
        return services.page.api.readTitle(input, { signal });
      },
    });

    readTitleInput and readTitleOutput are imported portable runtime schemas. requires maps local names to explicit capability descriptors. It supplies the typed services keys to this action's callback. It does not request browser permission, select a provider or target, attach a debugger or invoke the action.

    A separate JSON view definition can reference this action's identity and bind its input/result using the renderer contract. It does not import the action implementation's platform backend. The renderer catalog and exact view schema remain in Renderer and surface contract.

    // provider.ts
    import { definePluginEntry } from '@portable-sdk/definitions';
    import { readTitleAction } from './contributions/read-title.js';
    import { inspectorView } from './contributions/inspector-view.js';
    
    export default definePluginEntry({
      setup() {
        return [readTitleAction, inspectorView];
      },
    });

    The entry returns contribution definitions. The host validates and installs them in their respective registries. An action callback runs when invoked; returning its definition does not execute it. A view contribution publishes the JSON view; rendering belongs to a UI execution with its own lifetime.

    For context-dependent definitions, setup(context) receives the entry's local execution context and resource scope. It can construct contributions from configuration and register cleanup for resources it owns. Returning static definitions is the simplest case. A static contributions shorthand can be added later if it reduces real repetition; it is not necessary to establish this contract.

    Package the entries as one plugin

    // plugin.ts
    import { definePlugin, providerExecution } from '@portable-sdk/definitions';
    
    export const inspectorPlugin = definePlugin({
      id: 'example.inspector',
      entries: {
        provider: {
          execution: providerExecution,
          module: './provider.ts',
        },
      },
    });

    The build integration resolves the entry relative to its defining module and packages it for eligible hosts. The WebExtension adapter maps this role to background execution; a server adapter maps it to its provider process. Content, page and presentation executions use separate declared entry modules where needed. Executable entry discovery, module resolution and host-role compatibility must be checked by the packaging contract. Runtime installation only uses packaged entries.

    Install through the runtime

    // extension host composition
    import { createWebExtensionHost } from '@portable-sdk/host-webext';
    import { inspectorPlugin } from './plugin.js';
    import { pageCapability } from './contracts/page.js';
    import { pageImplementation } from './adapters/page.webext.js';
    
    const host = createWebExtensionHost({
      providerId: 'example.extension',
    });
    
    host.provide(pageCapability, pageImplementation);
    const installation = await host.install(inspectorPlugin);
    // development-server host composition
    import { createDevframeHost } from '@portable-sdk/host-devframe';
    import { inspectorPlugin } from './plugin.js';
    import { pageCapability } from './contracts/page.js';
    import { pageImplementation } from './adapters/page.devframe.js';
    
    const host = createDevframeHost({
      providerId: 'example.development-server',
      devframe: devframeContext,
    });
    
    host.provide(pageCapability, pageImplementation);
    const installation = await host.install(inspectorPlugin);

    Factories install their supported standard services and contribution registries. The custom example.page contract still needs the explicit implementation registration shown in each composition root. The two imported implementations illustrate the adapter boundary; their platform operations are not implemented by this planning packet. A composition root can retain that registration for explicit disposal:

    const implementationRegistration = host.provide(
      pageCapability,
      pageImplementation,
    );

    pageImplementation contains the API and its eligible local context. It is implemented in the adapter, separately from pageCapability. Registration returns a disposable owned by the host. Removing or replacing it invalidates existing bindings. Missing implementations are represented explicitly; no adapter provides a fake successful implementation for an unsupported browser API.

    install validates/adopts the definition and returns a handle even if some contributions are unavailable. Invalid manifests or duplicate plugin identities reject installation. Contribution setup failures and missing implementations appear as diagnostics in the handle. Exact dependency/version rules still require review.

    What host-managed means

    It means the runtime manages the contributions returned by an entry. The author defines the action/view/transform once and supplies any executable callback. The host owns validation, capability binding, registration, cancellation and teardown.

    Use two distinct layers of status:

    1. The host retains the declared contribution's identity and metadata while its plugin is enabled. A UI can therefore explain an unavailable action.
    2. The host makes executable registrations eligible when their required bindings are valid. A required binding loss cancels affected work and removes any active registration that relies on it. Restoring bindings recreates that registration, never a completed user operation.

    For an action, execution only happens on invocation. For a transform, its active interception registration can exist between requests. These contribution kinds share dependency and ownership rules, while their registries implement the actual registration mechanics.

    Event Proposed consequence
    No implementation exists for an action's required capability Action metadata remains known, action unavailable, independent view remains usable
    A required implementation is removed/replaced Cancel affected invocations and dispose affected active registrations; retain declaration/status
    Compatible implementation returns Recreate eligible registrations; never replay a prior action invocation
    Service rejects an operation for a permission, target or owner conflict Return its specific outcome; do not automatically tear down unrelated provider work
    User invokes an action again Resolve/check the current eligible binding and validate the target generation at execution time
    Plugin entry setup fails Unwind its owned resources; expose failure; explicit retry controls setup retry
    Plugin disabled or disposed End its owned activations and registrations; disposal is terminal
    Worker dies abruptly Cleanup callbacks may not run; recovery reconciles durable ownership under the recovery contract

    Remove whenAvailableAgain. Restoration is a documented runtime lifecycle rule, while a failed setup requires explicit retry. Ordinary service restoration does not rerun the plugin entry's setup just to restore an action. A fresh execution or code generation does run its entry setup again.

    Setup establishes definitions and reversible owned resources. Side-effecting user commands belong in action callbacks. Completed external effects cannot be undone by disposal. Target permission loss can invalidate a target-scoped binding when that binding itself depends on the permission, but it does not automatically destroy a provider-wide service or plugin.

    Context and runtime controls

    Every required service binding exposes .api and .context. The binding context identifies provider, realm, owning execution, optional target generation and whether access is local or transported. The callback also has its own execution context. Those contexts can differ.

    An entry setup context contains local provider/execution identity, target only for target-scoped execution, and a scope with signal, use(disposable) and onDispose(cleanup). native.get(importedDescriptor) returns typed native resources only when that local context owns them. A server context can expose Devframe, hub and Vite descriptors together; a WebExtension context exposes only its actual browser execution's primitives. Native objects and functions never cross RPC.

    interface PluginHandle {
      readonly id: string;
      readonly providerId: string;
    
      snapshot(): PluginSnapshot;
      subscribe(listener: (snapshot: PluginSnapshot) => void): Disposable;
      setEnabled(enabled: boolean): Promise<void>;
      retry(entryId: string): Promise<void>;
      dispose(): Promise<void>;
    }
    const observation = installation.subscribe(renderPluginStatus);
    await installation.setEnabled(false);
    await installation.setEnabled(true);
    await installation.retry('provider');
    observation.dispose();
    await installation.dispose();

    The handle is a local control object. Remote clients receive serializable snapshots and transported control operations. Snapshots contain entry lifecycle status and separate per-contribution availability, identity, generation and diagnostics. Subscription must deliver current state and changes without a read/subscribe race.

    retry retries failed entry setup. Enabling alone does not clear failure into a retry loop. It never retries a user action. Async teardown ordering, transition completion deadlines, snapshot wire types and failure isolation for a malformed returned contribution remain open contract details.

    Extensibility and core API inventory

    The contribution result must be extensible through imported descriptors just like capability contracts. Built-in helpers such as defineAction and the JSON view helpers produce definitions tagged with their contribution-type descriptor. The runtime dispatches them through registered contribution-type implementations. Adding a downstream kind must not require editing a central TypeScript union, a global augmentation or the core runtime's switch statement.

    A contribution-type implementation owns validation and installation for that kind and returns scoped cleanup. Runtime registration of those implementations belongs in host composition. The public authoring API never imports that registry. The exact descriptor/installer generic signatures need review before claiming a complete public contract.

    The proposed core inventory is:

    • Definition contracts and helpers for plugins, entries, capability descriptors, contribution types, execution roles and local native-context descriptors.
    • Separately exported typed contribution definitions for actions, JSON views and other supported kinds.
    • Host provide, contribution-type registration, install and dispose operations.
    • Typed capability binding and local/remote context contracts.
    • Entry setup context and resource scope.
    • Plugin handle, entry/contribution snapshots, enable/disable, explicit setup retry and disposal.

    The full action/state/renderer/transform/debugger operation contracts are still owned by their dedicated map tickets. This sketch makes the core authoring/runtime boundary concrete without presenting those operation schemas as already resolved.

    Acceptance obligations for the final contract

    These are required evidence for later implementation, not claims that this proposal already runs:

    • Import a plugin/capability/contribution definition without importing runtime, native platform libraries or a UI framework. Enforce the dependency boundary from emitted bundles and declarations.
    • Use the same plugin definitions through standalone Devframe, Vite DevTools, Chromium and Firefox host examples.
    • Add a downstream capability and contribution kind entirely through public imported descriptors and runtime registration.
    • Prove UI-only, behavior-only, mixed and target-scoped entries, including raw local context narrowing and serialization rejection of native resources.
    • Keep a view and action metadata available while the action's implementation is absent. Restore the registration without invoking the action.
    • Exercise binding replacement, permission/target races, cancellation, partial setup/installation failure, explicit retry, disable, disposal and execution replacement.
    • Tie every public API and hook to the required example/test/host matrix under Examples and API coverage contract.

    Decision frontier

    Definition/runtime separation and explicit imported descriptors are requirements. The next owner decision is the naming and composition model: plugin for the installable unit, contribution for each thing it adds, capability for each service contract it consumes or provides. The revised host-managed model operates on these contributions, without a generic feature/handler grouping.

    If accepted, update the glossary and settle exact contribution-type registration, dependency binding, setup failure and disposal signatures. Keep this decision ticket open until its full definition of done is satisfied.

    Review artifact and validation

    Local ticket-scoped commit: 89e7591 on dvcol/wayfinder/contribution-realm. The branch is not pushed; the complete proposal is reproduced in this comment for review.

    Documentation checks passed for Markdown fence balance, local document links, whitespace and the TypeScript syntax of all nine new code blocks. The sketch uses proposed packages and interfaces; it is not SDK type-check or runtime evidence. No production source changed, so application lint/type checks are not applicable to this documentation update. The issue remains open, with its definition of done unchanged.

  7. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Architecture, glossary and declaration contract

    Working proposal for Contribution and realm contract. The owner requires a clear glossary, dependency/interaction diagrams, type safety and declaration API before runtime implementation. Explicit imported descriptors and separation of definition modules from core runtime are settled requirements. Names, signatures and the lifecycle policies below remain under review.

    This packet supersedes the earlier two-step definePluginEntry/definePlugin authoring sketch. There is one plugin definition. Executable script references remain explicit where a real execution boundary requires separate packaging.

    Correct the relationships first

    A capability is a service contract. A provider instance offers an implementation of that contract. A realm describes the environment family; it does not execute or implement anything. A provider only advertises capabilities it can actually implement in eligible contexts.

    A contribution is something a plugin adds, including views, actions, state declarations, transforms and packaged scripts. Its implementation can require zero or more capabilities. A view may also reference an action contribution through its public action descriptor. Keep those references distinct from service dependencies.

    A plugin groups contributions under one identity and installation lifetime. Its package can also export shared capability contracts and optional implementation definitions. Exporting or listing a contract does not supply an implementation. Several plugins can share the same contract without owning one another.

    An action describes a use case, such as capture a snapshot. A capability describes a service that can support that use case, such as debugger operations. An action handler may orchestrate several capabilities; a capability may support several actions or transforms.

    Proposed glossary

    This is the complete proposed replacement vocabulary for the current discussion. Copy agreed terms into the root glossary after owner review; the earlier glossary is not silently renamed by this packet.

    Term Definition Example or boundary
    Capability descriptor Stable, versioned, typed service contract with runtime operation schemas Page inspection contract, independent of Chrome or Vite
    Capability implementation Executable code satisfying one capability contract in an eligible execution A browser adapter's page inspection service
    Capability binding A consumer's currently resolved implementation, exposing api and context Page service from provider A for document generation B
    Contribution definition Typed description of one addition to a host Action, JSON view, transform, state declaration or packaged script
    Action descriptor Shared input/output contract and identity for an invocable action Imported by both UI and the action contribution's handler
    Plugin definition Named collection of contributions installed and controlled together Inspector plugin, useful with or without UI
    Plugin installation One provider's running instance of a plugin Same definition installed independently in two providers
    Plugin handle Local control and observation object for one installation Status, enable, disable, retry setup and dispose
    Provider Logical identity offering capabilities and installed plugin behavior One development server or one extension instance
    Realm Extensible environment family of a provider WebExtension or development server
    Host Application composition that chooses runtimes, adapters and installed plugins Browser extension application or a development tool application
    Runtime Process-local engine that manages registrations, execution and lifecycle Provider runtime or client runtime
    Provider runtime Runtime responsible for service implementations, plugin contributions and authoritative operation execution Runs in extension background or a server process
    Client runtime Runtime responsible for discovery, selection, subscriptions, action calls and local presentation Runs in a popup or panel document
    Execution agent Target-scoped code that performs delegated work in another execution for a provider Isolated content script or page MAIN script
    Adapter Integration code that connects the portable runtime to a specific environment WebExtension provider adapter or Devframe client adapter
    Transport Channel carrying validated protocol messages Extension messaging or a server connection
    Renderer Implementation that turns JSON views and bindings into UI Default renderer or downstream replacement
    Execution context Actual worker, process or document where code and native resources live Background worker, Node process, popup document
    UI surface Placement of a rendered interface Popup, options, side panel, DevTools panel
    Target Subject of an operation, including identity and generation A particular inspected document, independent of selected UI
    Native context Raw resources owned by a local execution, accessed through explicit descriptors Browser events, Devframe/hub/Vite context objects

    A process may contain both client and provider roles when a host deliberately composes them. The default extension popup is a client; that does not erase the browser APIs that its own document legitimately owns.

    Axes that must remain separate

    Axis Answers Proposed examples
    Provider identity Which instance owns this operation/state? extension:local, server:project-a
    Realm What environment family does that provider belong to? webext, devserver
    Adapter How is this runtime connected to that environment? Chromium/Firefox integration, standalone Devframe, Vite DevTools
    Execution Where is this particular code running? Background worker, content script, page document, server process, UI document
    Surface Where does its UI appear? Popup, options, panel, side panel
    Target Which inspected subject does this call address? Tab plus document generation, frame
    Mode Which build and reload policy applies? Development, watched production preview, packaged release

    Recommend one development-server realm for standalone Devframe and Vite DevTools. They use different adapters and can expose different native context descriptors. A Vite DevTools server may expose Devframe, hub and Vite resources together. Those libraries do not create three independent provider identities. Realm IDs remain extensible imported descriptors; the example names are not a closed core union.

    An options document can have no selected target. A popup's selected target is not automatically a background service's lifetime. Closing a panel ends that client document's subscriptions and presentation, while independent provider work continues.

    Definition dependencies

    Arrows in this diagram mean imports or typed references, not runtime messages.

    flowchart BT
      Contracts["Shared descriptors, schemas and context types"]
      Contributions["Contribution definitions and packaged handlers"] --> Contracts
      Plugin["One plugin definition"] --> Contributions
      ProviderRuntime["Provider runtime"] --> Contracts
      ClientRuntime["Client runtime"] --> Contracts
      ProviderAdapter["Provider adapter and capability implementations"] --> ProviderRuntime
      ClientAdapter["Client adapter"] --> ClientRuntime
      Renderer["Renderer implementation"] --> Contracts
      ProviderApp["Provider composition root"] --> Plugin
      ProviderApp --> ProviderAdapter
      ClientApp["Client composition root"] --> ClientAdapter
      ClientApp --> Renderer
      ClientApp --> Contracts
    
    Loading

    Definitions do not import either runtime. Shared action/view/capability contracts do not import their executable implementations. A client can import an action descriptor without importing the plugin's provider handlers or native server code. Adapter modules and renderer implementations may depend on native APIs or a UI framework within their own eligible bundles.

    A definition may contain a packaged function. That is executable source known at build time, not JSON sent to a browser. The compiler/packager resolves the declared execution assignments and script modules; the wire protocol publishes only validated metadata, state and operation messages.

    Runtime interactions

    Arrows in this diagram mean runtime interactions. One client may connect to several providers; selecting and broadcasting across them is owned by Provider discovery and routing.

    flowchart LR
      subgraph Presentation["Client execution: popup, options or panel"]
        Renderer["JSON renderer"]
        Client["Client runtime + client adapter"]
        Renderer -->|"action request"| Client
        Client -->|"view, state and availability"| Renderer
      end
      Client <-->|"validated messages"| Transport["Transport"]
      Transport <-->|"calls, results and subscriptions"| Provider["Provider runtime + provider adapter"]
      Provider -->|"invokes packaged handler"| Action["Action contribution"]
      Action -->|"typed capability binding"| Service["Capability implementation"]
      Service -->|"local eligible operation"| Native["Browser or server APIs"]
      Service <-->|"validated target-scoped requests"| Agent["Content or page execution agent"]
      Agent -->|"local operation"| Target["Inspected document"]
      Provider -->|"owns declarations and registrations"| Registry["Views, actions, state and transforms"]
    
    Loading

    The provider runtime remains the authority for its owned operations. A MAIN script does not receive background permissions or become a trusted provider simply because it can send a message. An execution agent reports into a provider under explicit target/session validation. If a future host advertises an agent as an independent provider, it must give it its own identity and authority explicitly.

    Placement by host

    Deployment piece SDK role Adapter/implementation responsibility Lifecycle owner
    Extension background worker/document Provider runtime Extension messaging, persistence, eligible browser APIs, delegation to tab executions Extension provider; survives UI closures, recovers from worker replacement
    Isolated content script Execution agent DOM operations and the extension-side page bridge in that document Document/frame generation
    Injected MAIN script Execution agent Page-owned APIs through the validated page bridge Page document generation
    Extension DevTools page Client bootstrap and local native integration DevTools lifecycle, inspected target and panel creation hooks DevTools document
    Extension popup/options/side panel/panel Client runtime plus renderer Local UI lifecycle, connection to providers, permitted local native hooks Each UI document
    Vite DevTools server Provider runtime Adapter around Devframe/hub/DevTools and Vite resources Server provider
    Standalone Devframe server Provider runtime Adapter around the available Devframe and optional hub resources Server provider
    Vite DevTools panel Client runtime plus renderer DevTools mount/connection integration Panel mount/document
    Standalone Devframe panel Client runtime plus renderer Devframe mount/connection integration Panel mount/document

    The server provider can also rely on a browser execution agent for page-side operations. The shared capability contract exposes the operation and its actual availability; it does not require every platform to perform it through the same native API. Detailed injection timing, browser permissions and transport/recovery behavior remain the linked map contracts' work.

    An adapter is broader than a transport. It may provide lifecycle hooks, target discovery, capability implementations, entry packaging and local context access. A transport alone does not implement page inspection, debugger operations or rendering.

    One plugin declaration

    All names below are proposed exports. The snippets define an authoring contract, not implemented SDK packages.

    Shared contracts

    Schema-derived types keep TypeScript inference and wire validation tied to the same declarations. The imported schemas below are portable runtime schemas for the target and result; they are application examples, not magic SDK globals.

    // contracts.ts: safe for provider and client bundles
    import { defineAction, defineCapability, defineOperation } from '@portable-sdk/definitions';
    import { targetInputSchema, titleOutputSchema } from './schemas.js';
    
    export const pageCapability = defineCapability({
      id: 'example.page',
      version: 1,
      operations: {
        readTitle: defineOperation({
          input: targetInputSchema,
          output: titleOutputSchema,
        }),
      },
    });
    
    export const readTitleAction = defineAction({
      id: 'example.inspector.read-title',
      version: 1,
      input: targetInputSchema,
      output: titleOutputSchema,
    });

    Capability operations are reusable services. The action is the plugin's invocable use case. Their schemas happen to match in this small example; a more complex action can combine several operations and return a different result.

    Contribution implementation and plugin definition

    // plugin.ts: packaged for provider execution
    import {
      defineActionContribution,
      definePlugin,
      providerExecution,
    } from '@portable-sdk/definitions';
    import { pageCapability, readTitleAction } from './contracts.js';
    import { inspectorView } from './view.js';
    
    const readTitleContribution = defineActionContribution(readTitleAction, {
      execution: providerExecution,
      requires: { page: pageCapability },
    
      handler({ input, services, signal }) {
        return services.page.api.readTitle(input, { signal });
      },
    });
    
    export const inspectorPlugin = definePlugin({
      id: 'example.inspector',
      contributions: [readTitleContribution, inspectorView],
    });

    defineAction creates the public contract. defineActionContribution attaches a handler, service dependencies and execution assignment to that contract. Both are inert definition helpers. This is an actual action callback, not a new generic feature/handler grouping.

    inspectorView is a separately defined JSON view contribution that references readTitleAction through its public descriptor. Its JSON catalog and component bindings belong to the renderer contract. The client imports public contracts and views, never this provider implementation module.

    There is no definePluginEntry, no mandatory setup wrapper returning an unchanged array, and no second plugin identity. A contribution kind that owns startup resources supplies a typed activation hook and cleanup scope through that kind's definition contract. It shares the runtime ownership protocol rather than forcing all plugins into an additional feature container.

    Platform implementations and provider composition

    The custom page capability has separate implementation modules, for example page.webext.ts and page.devframe.ts. They both implement the operation schemas above, receive eligible local contexts and obey the same outcome contract. A common contribution uses the descriptor without importing either module.

    // provider.webext.ts: host composition, the runtime dependency starts here
    import { createProviderRuntime } from '@portable-sdk/runtime/provider';
    import { createWebExtensionProviderAdapter } from '@portable-sdk/adapter-webext/provider';
    import { pageCapability } from './contracts.js';
    import { inspectorPlugin } from './plugin.js';
    import { pageImplementation } from './page.webext.js';
    
    const provider = createProviderRuntime({
      adapter: createWebExtensionProviderAdapter({
        providerId: 'example.extension',
      }),
    });
    
    provider.provide(pageCapability, pageImplementation);
    const installation = await provider.install(inspectorPlugin);

    The Devframe composition substitutes its provider adapter and page implementation. The Vite DevTools adapter can build on the Devframe adapter while exposing the additional eligible Vite context. These are factories for adapters around the same provider-runtime contract; they do not redefine plugins.

    provide validates and registers an implementation definition for later eligible activation. install starts that plugin's eligible registrations and returns a handle. The adapter's environment bootstrap owns channel and native listener initialization, including any synchronous registration requirements. The exact start/readiness API and worker recovery sequence must be fixed before implementation, not inferred from this sketch.

    A plugin package may export optional native implementations, but host composition selects the eligible ones. Contract-only exports do not make those native implementations appear automatically. The first proof can use explicit composition; automatic implementation selection would need its own reviewed build-time declaration contract.

    Client composition

    // popup.ts: client runtime, no provider handler imports
    import { createClientRuntime } from '@portable-sdk/runtime/client';
    import { createWebExtensionClientAdapter } from '@portable-sdk/adapter-webext/client';
    import { renderer } from './renderer.js';
    import { readTitleAction } from './contracts.js';
    
    const client = createClientRuntime({
      adapter: createWebExtensionClientAdapter({ surface: popupSurface }),
      renderer,
    });
    
    await client.connect();
    const presentation = client.mountView(inspectorViewReference, {
      container,
      provider: selectedProvider,
    });
    
    const result = await client.actions.invoke(
      readTitleAction,
      { target: selectedTarget },
      { provider: selectedProvider },
    );

    popupSurface is an imported surface descriptor. inspectorViewReference is a typed public reference to the published view. container, selectedProvider and selectedTarget come from the client application. The client knows its own local execution and selected remote provider separately. Mount/subscription handles belong to the client document and are disposed with it. Exact view mounting and renderer integration signatures remain part of Renderer and surface contract.

    The Devframe/DevTools client compositions substitute their mount/connection adapters. The view and action contract stay the same. A renderer can also handle a local presentation interaction without creating a provider action; the renderer contract must distinguish those local interactions from authoritative backend commands.

    Scripts retain real execution boundaries

    Some contributions genuinely need a separate packaged module:

    const pageProbe = defineScriptContribution({
      id: 'example.inspector.page-probe',
      execution: mainWorldExecution,
      module: new URL('./page-probe.ts', import.meta.url),
    });

    This definition identifies a script contribution. It is not another plugin declaration. Its activation target, match rules, permissions, timing and bridge contract must be specified by Injection and transform contract. The script runs in its declared execution. It cannot inherit a provider's raw native context by importing a common type.

    Dependency edges have different meanings

    Edge Meaning What it does not imply
    Contribution requires capability Bind the declared service before making the dependent behavior executable Grant permission, choose arbitrary providers or execute an operation
    View references action Bind a UI interaction to the action's public contract and availability Import its handler or require its service to render the whole view
    Contribution references shared state Address a typed state contract with explicit provider/scope ownership Merge state across providers
    Plugin contains contributions Install/control these definitions as one named unit Make all contributions fail together or expose every capability
    Capability implementation delegates to agent Execute an eligible part of the operation in a target context Transfer all provider authority to page code
    Host selects adapter Connect the portable runtime to this environment Make unsupported platform features available

    Default recommendation: no arbitrary plugin-to-plugin or contribution-to-contribution activation dependencies. Share behavior through capability and action contracts, with explicit references for views/state. If a real ordering dependency remains, name and specify it before adding a general dependency graph API. This revises earlier speculative cycle handling in the first review packet; that older sketch is not an accepted promise of a dependency graph.

    An action using several services resolves them under its selected provider by default. Cross-provider orchestration must be explicit and preserve per-provider outcomes. Binding resolution must not silently combine state from one provider with target operations from another.

    Type-safety contract

    Boundary Required guarantee Rejection/evidence
    Descriptor identity Literal identity/version preserved in types; runtime compares declared identity/version across bundles Duplicate copies of the same compatible contract work without object identity
    Operation schema Infer input/output types from runtime schemas Reject wrong input types statically and malformed messages at runtime
    Capability implementation Every declared operation has the correct signature and outcome type Compile failures for missing operations, wrong results and accidental extra operations
    requires inference Each declared local key maps to a typed binding for exactly its descriptor services.page.api inferred; undeclared services rejected
    Action caller Import the public action descriptor and infer its input/result Client builds without provider handler or native dependencies
    Local native context Descriptor access returns only correctly typed resources owned by this execution Unsupported context returns absent; a realm label alone cannot grant access
    Remote context Serializable provider/execution/target metadata, no native object field Compile-time narrowing and wire rejection for native handles/functions
    Target lifecycle Every target-bearing call validates current identity and generation A reused tab/frame identity cannot authorize a stale document operation
    Availability Typed bindings do not imply permanent permission or connectivity Runtime rejects races with explicit outcomes
    Downstream extension Imported capability, realm and contribution-kind descriptors extend the SDK Third-party example compiles without global augmentation or core union edits
    Packaging Execution declarations become actual eligible bundles Emitted client graph contains no provider handlers or Node/backend-only dependencies

    Proposed operation outcome shape:

    export type OperationResult<Output> =
      | { status: 'success'; value: Output }
      | { status: 'unavailable'; reason: UnavailableReason }
      | { status: 'cancelled' }
      | { status: 'failed'; error: OperationError };

    UnavailableReason retains distinct unsupported implementation, wrong execution, missing permission, restricted target, disconnected provider, conflicting owner, stale target and incompatible contract reasons. OperationError is serializable and contains no native exception object. Capability bindings and action calls use the same envelope; the declared output schema validates the successful value. The concrete reason/error fields and authorization policy still need review.

    An implementation is typed against the descriptor's inferred operations rather than a separately hand-maintained API interface. Optionality is explicit in schema and contract declarations. Avoid any, assertions that pretend a browser Port is a Devframe transport, and skipLibCheck as evidence of compatibility.

    Recommend exact declared contract-version matching for the first proof. Semver-range negotiation and schema compatibility inference can be added only with an explicit policy. Exporting a changed schema under the same version is a contract violation; version equality alone is not proof that the author preserved the schema. Runtime schema validation remains mandatory.

    Context and ownership contract

    Every bound API exposes api and context. Its context describes the service owner. The action callback separately receives its own execution identity and cancellation scope.

    export type BindingContext<LocalNativeContext> =
      | {
          access: 'local';
          provider: ProviderDescriptor;
          execution: ExecutionDescriptor;
          target?: TargetReference;
          native: LocalNativeContext;
        }
      | {
          access: 'remote';
          provider: ProviderDescriptor;
          execution: ExecutionDescriptor;
          target?: TargetReference;
        };

    ProviderDescriptor carries provider identity and the serializable realm descriptor. LocalNativeContext is a descriptor-indexed accessor restricted to the owning execution, not a union of every host namespace. A local Vite server can expose several native descriptors; a remote popup sees only their declared metadata. Permission and trust remain runtime checks even after TypeScript narrows a local context.

    A plugin installation owns provider contributions and their activation generations. A client owns its renderer mounts and subscriptions. An agent owns document-scoped resources. A capability implementation owns its native service resources within its eligible execution. The runtime records those owners and unwinds resources on failure, replacement, disable and disposal. Abrupt worker termination requires durable recovery rather than assuming cleanup callbacks ran.

    Missing required services keep contribution metadata observable while executable behavior is unavailable. Losing a binding cancels dependent work and active registrations. Compatible restoration recreates registrations and never replays a user action. Permission/target failure on one operation does not automatically dispose a provider-wide plugin.

    Public inventory and decision boundaries

    Area Proposed exports/hooks Owner of unresolved details
    Definitions defineCapability, defineOperation, defineAction, defineActionContribution, definePlugin; execution/realm/native-context/contribution-kind descriptors This contract
    Provider runtime createProviderRuntime, provide, contribution-kind registration, install, runtime disposal This contract; adapter lifecycle in server/state contracts
    Installation handle Snapshot, subscribe, enable/disable, explicit failed-activation retry, dispose This contract; exact retry target revised after removing entry wrappers
    Capability binding Typed API/context, resolution and observable availability This contract; routing/trust behavior in their dedicated contracts
    Client runtime Connect/disconnect, provider discovery/selection, action invocation, state subscriptions Routing/state contracts
    Presentation View references, mount/unmount, renderer adapter, local UI actions Renderer/surface contract
    Native implementations Eligible local contexts and capability implementations Server/debugger/injection contracts
    Script definitions Packaged execution reference and lifecycle hooks Injection/transform contract
    Build integration Discover declarations, produce per-execution bundles and preserve ownership through reload Toolchain/reload contracts

    This packet supplies the architecture and proposed declaration API. It does not claim every domain signature is finished. Before implementation admission, each public method and hook must have an exact type, execution owner, failure behavior and example/test obligation. The map must make remaining decisions explicit rather than leaving them to an implementer.

    Required contract evidence before implementation is considered complete

    • Compile positive and negative authoring fixtures for inferred schemas, dependency bindings, implementation conformance, local/remote narrowing and downstream descriptors.
    • Prove a contract-only client import cannot pull in a provider handler, native adapter or UI framework unintentionally.
    • Execute the same inspector definitions through all supported host/client examples, with real success, unsupported, failure and lifecycle assertions.
    • Exercise multiple providers and simultaneous client documents, including panel closure, document replacement and background recovery.
    • Verify that capability restoration cannot replay a user action and that unavailable behavior does not remove unrelated views.
    • Enumerate every public export, hook and behavior-changing option in the API/example/test/host matrix.

    These are future implementation obligations. This planning change adds no SDK runtime. Documentation syntax checks are not evidence of any of these guarantees.

    Owner review frontier

    1. Accept the corrected naming and relationships: provider implements capabilities; plugin contains contributions; contributions consume capabilities and may reference action/view/state contracts.
    2. Accept one definePlugin({ contributions }), separate shared action contracts from handlers, and keep native script modules as explicit contribution packaging references.
    3. Choose realm classification. Recommend webext and devserver, with standalone Devframe versus Vite DevTools represented by adapters and native-context descriptors.
    4. Decide whether explicit host composition of capability implementations is sufficient for the first contract. Recommend yes; plugins can export optional implementations without introducing automatic realm-based implementation discovery yet.

    After this review, record agreed vocabulary and settle the remaining exact core descriptor, contribution-kind installer, availability, retry/disposal and context signatures. The ticket remains open until the user-reviewed definition of done is satisfied.

    Review artifact and validation

    Local ticket-scoped commit: 35aa498 on dvcol/wayfinder/contribution-realm. The branch is not pushed; this comment contains the complete proposal for review.

    Documentation checks passed for Markdown fence balance, local links, whitespace and TypeScript syntax in all seven new code blocks. The two Mermaid diagrams were inspected as source; no rendered-diagram or SDK runtime/type-conformance claim is made. No application source changed, so application lint/type checks are not applicable. All final contract acceptance items remain open.

  8. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Dedicated declarations, service installation and upstream alignment

    Proposal for Contribution and realm contract, responding to the owner's three challenges about declaration grouping, declarative versus imperative service installation, and the amount of abstraction above existing tooling. This is a revision for review, not a resolved contract or SDK implementation.

    This packet supersedes the heterogeneous contributions array as the recommended built-in authoring shape. It also supersedes imperative-only capability installation and the assumption that every host must expose a new generic provider runtime constructor. The earlier architecture diagrams remain useful descriptions of ownership and execution roles.

    Recommendations

    Question Recommendation Reason
    One mixed contribution list or dedicated properties? Dedicated typed actions, views, transforms, scripts and services properties Readers and TypeScript know the expected definition kind from its property
    Startup declarations or imperative installation? Both, consuming the same service implementation definition Static composition is the default; runtime changes use the same lifecycle and validation
    How much abstraction? A small portable declaration/context contract, integrated into native host bootstrapping Reuse Devframe's existing mechanisms instead of introducing parallel RPC, state and rendering engines

    These recommendations preserve the accepted requirements for explicit imported descriptors, separate definition modules, framework-neutral authoring and browser-specific implementations.

    Released API grounding

    A bounded follow-up to Upstream reuse audit rechecked the relevant public declarations and selected implementation paths in the SHA-512-verified devframe@1.0.0, @devframes/hub@1.0.0 and @vitejs/devtools-kit@0.7.5 artifacts. These are verified pinned contracts, not a claim that they are the latest available releases or that every source file matches a release.

    • Devframe uses dedicated definition fields and the released DevTools bridge installs a Devframe definition into an existing context. Definition contract, DevTools bridge.
    • Service declarations, wire-service installation and publication of an existing local object are separate APIs. Package resolution uses Node facilities and therefore needs bundled-factory resolution in a browser adapter. Service contracts, service host.
    • Released lower-level RPC accepts channel callbacks, while the assembled reference renderer expects the full client context and its high-level transport model. A Port integration must satisfy an actual supported seam rather than cast itself into the larger context. RPC channel, renderer interface.

    JSON views, command handles and renderer registrations have removal mechanisms; installed services and the server dock host lack equivalent public uninstall methods. Service revocation alone does not undo wire-service installation. The released hub also starts declared service installations without awaiting each returned promise, then awaits a memoized readiness barrier. After startup that barrier may already be fulfilled, so late setup can race asynchronous service construction. This last point is a source-supported risk, not a reproduced runtime failure. It requires a targeted proof before promising startup/dynamic equivalence. Hub installation, service host.

    1. Contributions describe a relationship, not a mandatory container

    A plugin contributes actions, views, transforms and services. The word contribution is useful for documentation, ownership, diagnostics and common typing. It does not require all of those declarations to live in the same array or expose identical lifecycle hooks.

    export const inspectorPlugin = definePlugin({
      id: 'example.inspector',
      actions: [captureSnapshotAction],
      views: [inspectorView],
      transforms: [responseTransform],
      scripts: [pageProbe],
    });

    Each item is an imported definition of the appropriate kind. Omitted properties mean no declarations of that kind. A service-only plugin can instead declare services: [portableService], provided the implementation definition is eligible for that plugin's installed execution. A host can supply shared services independently of any plugin.

    All kinds can share identity, ownership, dependency diagnostics and disposal conventions where those concepts apply. Their specific contracts remain distinct:

    • An action has an invocation handler and invocation lifetime.
    • A JSON view has publication state, while its renderer mount has a separate client lifetime.
    • A transform owns interception registrations and in-flight request work.
    • A packaged script has execution, document generation and injection timing.
    • A service definition describes construction of an implementation and its owned resources.

    Do not force a view's action reference to behave like a transform's required execution binding. Do not use array position as identity or implicitly derive cross-kind execution order from it.

    Authoring choice Benefit Cost
    contributions: [action, view, transform] Uniform iteration and an open list of kinds More tagging/narrowing; less direct documentation and per-property diagnostics
    actions, views, transforms, etc. Discoverable declarations, precise element types, familiar composition Needs an explicit policy for adding a genuinely new kind
    Both public shapes for the same built-ins Two ways to express every plugin Duplicate docs, precedence and lifecycle rules without additional capability

    Recommend only the dedicated built-in shape. Internal normalization is an implementation choice; it must preserve each kind's schema and semantics. It should not become an extra public execution language.

    Downstream extension without weakening the built-ins

    Capability and realm extensibility remain based on imported descriptors. A new service capability normally fits services plus existing action/view/transform declarations; it does not require a new contribution kind.

    For a genuinely new contribution kind, reserve an explicit descriptor-tagged extension collection rather than accepting arbitrary top-level keys:

    export const diagnosticsPlugin = definePlugin({
      id: 'example.diagnostics',
      actions: [collectDiagnostics],
      extensions: [diagnosticReportDefinition],
    });

    diagnosticReportDefinition carries its imported kind descriptor and validated typed payload. Host composition supplies that kind's installer explicitly. This is a proposed extension path, not a requirement to reimplement every built-in through a universal registry. Its exact helper and installer types must be proved with a concrete downstream example before the contract closes. Unknown top-level fields must remain errors so a typo such as actons cannot silently become an extension.

    Definition helpers should earn their place

    A define* helper constructs and types an inert definition. It can infer schema types, preserve literal descriptor identity and validate malformed declarations. It does not register listeners or start a service. Plain objects using satisfies remain reasonable where they retain the required types and invariants; do not introduce a factory solely to return the same object under a new name.

    The separation between an action's client-safe contract and its executable implementation remains necessary. Dedicated properties do not authorize importing provider handlers into a popup. The helper count should follow actual inference and execution requirements, not the number of glossary nouns.

    2. One service definition, two installation entry points

    The earlier provide example belonged to the provider side, not the UI client. It changed a live service registry. That is different from a definition helper constructing a recipe.

    Use the following distinction:

    Object or operation Meaning Starts work?
    Capability descriptor Contract the implementation must satisfy No
    Service implementation definition Packaged construction/setup recipe for that contract No
    Startup services list Definitions the host must install during startup Installation begins when the host starts
    Runtime services.install(definition) Install a definition into an already running host Yes, through the same installer
    Registration handle Observe/release that installation's owned resources Disposal changes the live registry

    A service implementation definition is the existing capability-implementation concept made explicit as a recipe; it is not another provider identity. Its setup receives the eligible native context and resource scope. The exact helper name below is proposed.

    export const pageService = defineService(pageCapability, {
      execution: extensionBackgroundExecution,
    
      setup(context) {
        return createPageApi(context);
      },
    });

    createPageApi is local implementation code satisfying the operation schemas of pageCapability. Resources it creates belong to context.scope. The definition helper does not invoke it. The module belongs to the eligible extension provider bundle; a server host supplies a different implementation definition for the same capability.

    Startup composition:

    const host = await createWebExtensionHost({
      services: [pageService],
      plugins: [inspectorPlugin],
    });

    Runtime composition, in a separate host where this service is not already installed:

    const host = await createWebExtensionHost({
      plugins: [inspectorPlugin],
    });
    
    const registration = await host.services.install(pageService);
    await registration.dispose();

    createWebExtensionHost is an illustrative host bootstrap, not a new finalized export. A Devframe host uses its native initialization and adapter's startup service declarations. The guarantee is the shared definition and installation semantics; constructor names need not be identical across hosts.

    flowchart LR
      Definition["Same service implementation definition"] --> Startup["Host startup services list"]
      Definition --> Dynamic["Runtime services.install"]
      Startup --> Installer["One installer and ownership policy"]
      Dynamic --> Installer
      Installer --> Registry["Capability bindings and status"]
    
    Loading

    Naming and policy

    Prefer services.install(definition) for constructing a service from its definition. Upstream Devframe uses provide(id, object) for a different operation: publishing an already-created in-process object. Do not make these names aliases and silently inherit different transport or ownership semantics.

    A raw-object provide escape hatch can remain available through an eligible native Devframe context. Add it to the portable API only if a concrete use case establishes how its resources, validation, remote publication and lifetime are owned. The normal authoring interface needs the startup list and runtime installation, not three overlapping methods.

    Both installation paths must share:

    • Descriptor and runtime schema validation, execution eligibility and authorization.
    • Duplicate/conflict behavior and contract-version checking.
    • Construction, partial-failure cleanup and cancellation.
    • Dependency availability publication and removal.
    • Ownership, disposal and replacement rules.

    Proposed first-contract defaults: an implementation is installed once in its provider/registration scope; an ambiguous duplicate fails explicitly. Shared host services are installed by host composition and consumed by plugins through requirements. A plugin-declared service belongs to that plugin's installation and is disposed with it. Do not silently merge ownership or configuration when two plugins claim the same implementation slot. If shared installation leases are needed later, specify them explicitly before allowing duplicates.

    A failed dynamic installation must leave the previous valid registry state intact and unwind its own partial resources. Required startup failure must report which service prevented readiness; optional unsupported services must report availability without fabricating success. Those policies must be fixed in the exact lifecycle contract, with the same service-level behavior in both paths.

    The runtime method supports installation from already-packaged code, including user-enabled integrations and replacement during development. It does not permit fetching arbitrary executable plugin code into a packaged extension.

    Upstream semantics are precedent, not automatically our policy

    Devframe's existing service APIs distinguish startup and later option handling. Startup definitions participate in a readiness barrier and option merging; late installation of an existing package can return its existing API and ignore new options. Its wire-service install returns an API, while in-process provide returns a revoke function. A proposed disposable installation handle is therefore an additional ownership contract, not an existing upstream guarantee.

    Our adapter must specify which semantics it preserves and how additional ownership requirements are implemented. Renaming the calls does not establish equivalence.

    3. Align the implementation with Devframe and the declarations with typed contribution composition

    The earlier proposals were accumulating an abstract service registry, action registry, state layer, view layer, transport layer and host factory above a stack that already implements many of those mechanisms. That would create competing representations and more places to enforce the same rules.

    Recommend a small portable authoring contract and direct adapters into existing host contexts. The server should continue using Devframe/Vite bootstrapping. The WebExtension host should use its own browser packaging and lifecycle integration. Shared UI and contribution code use the same contracts across them.

    flowchart TB
      Definitions["Shared plugin, service and operation definitions"]
      Definitions --> ServerAdapter["Install into existing Devframe / DevTools context"]
      Definitions --> ExtensionAdapter["Install into WebExtension host"]
      ServerAdapter --> Upstream["Existing RPC, services, shared state and JSON render"]
      ExtensionAdapter --> Portable["Reusable portable upstream pieces"]
      ExtensionAdapter --> Browser["Browser messaging, permissions, targets and lifecycle"]
    
    Loading

    This diagram expresses a reuse target, not proof that the whole Devframe Node context runs in a browser. The previous audits still control which released components are actually reusable.

    Concern Preferred alignment Additional contract still needed
    Operation contracts and dispatch Preserve compatible upstream RPC schemas, metadata and registration semantics Explicit imported descriptors, browser-safe bindings and target/permission outcomes
    Service definitions Use the definition/setup plus declarative-list/dynamic-install pattern Provider/execution identity, exact portable version and ownership policy
    Shared state Reuse compatible Devframe shared-state representations and synchronization Browser persistence, scope, worker recovery and separate-provider ownership
    JSON rendering Reuse supported JSON spec, action binding and view/state contracts Replaceable renderer, extension client connection and UI mount lifecycle
    Dock/panel integration Reuse hub docks and DevTools mount hooks where they apply Map extension popup/options/panels and standalone presentation appropriately
    Injection/transforms Reuse compatible server hooks and definitions Explicit browser mechanisms and differing semantic limits
    Native access Expose real local Devframe/hub/Vite or browser contexts through typed descriptors No fabricated full context or remote native objects
    Plugin unload/reload Reuse existing disposable registrations where available Prove complete cleanup, async ordering and durable recovery; add missing upstream lifecycle hooks if needed

    Do not confuse upstream ctx.views, which hosts static assets, with portable JSON view declarations. Mapping must follow actual semantics rather than reusing the word views for unrelated operations.

    Avoid a mandatory action around every service method

    Capability operations already describe callable behavior. A separate action is useful when it adds an invocable use case, orchestration, command/UI metadata or a distinct input/output contract. It need not be a second wrapper around every RPC function. Reuse the original schemas and metadata rather than hand-maintaining an equivalent descriptor at each layer. Client invocation must still use validated, authorized bindings whether it invokes a service operation or a composed action. Only explicitly published operation contracts are remotely callable; registering an in-process service does not expose its raw methods to clients.

    The earlier read-title action demonstrated dependency injection, but it is too small to justify a universal action-over-operation requirement. The complete API must allow a deliberate direct service operation path where no extra action semantics are needed.

    Upstream adoption

    Technical alignment can make upstream review easier; maintainer interest is unknown until discussed. A large generic plugin framework is a much larger adoption request than improvements useful to existing Devframe users.

    Potential upstream changes should each solve a concrete missing seam, such as a narrower renderer-host interface, portable service resolution, complete disposable registration ownership, or an optional explicit-descriptor API. The combined portable declaration package can remain a companion integration while those pieces are evaluated. Vite-specific integration belongs in the Vite adapter; browser packaging and permissions belong in the WebExtension adapter.

    Do not couple the generic SDK to application-specific contribution or renderer types. The reusable lesson from downstream tooling is typed dedicated contribution properties, direct use of compatible upstream types and centralized registration/teardown. The generic package must remain independent of that tooling's business context.

    Exact decision and validation obligations

    Before implementation admission, this ticket must settle:

    1. Dedicated built-in properties, their exact readonly types and a descriptor-based extension path with no arbitrary top-level keys.
    2. Capability descriptor versus service implementation definition, including inferred implementation signatures and eligible execution contexts.
    3. One startup/dynamic installer contract, ownership/duplicates, readiness, failure cleanup and disposal outcomes.
    4. A public-export mapping showing which types are reused, which are adapted and which are new. A wrapper must name the guarantee it adds.
    5. The smallest server adapter that installs these definitions into an existing context, with no duplicate state/RPC authority.
    6. The client-safe contract import path, including direct service calls and composed actions without backend imports.

    Acceptance evidence must cover positive and negative declaration typing; wrong-kind property entries; misspelled properties; startup/dynamic equivalence; duplicate/conflict behavior; partial installation failure; plugin-owned versus host-owned disposal; actual emitted bundle dependencies; and real host success/unsupported/lifecycle behavior. Preserve the API/example/test/host coverage obligation already recorded by the map.

    The owner has not yet accepted these recommendations. The contract remains open. This document is a planning deliverable and contains no runtime implementation.

    Review artifact and validation

    Local ticket-scoped commit: 1600ea2 on dvcol/wayfinder/contribution-realm. The branch is not pushed; this comment contains the full proposal for review.

    Documentation checks passed for whitespace, fence balance, local document links and TypeScript syntax in all five new code blocks. Two Mermaid diagrams were inspected as source. The pinned released-API review is described above; the late-install ordering concern is not a reproduced runtime failure. No SDK source changed, and no application lint/type or runtime-conformance claim is made. The contract remains open with final acceptance pending owner review.

  9. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Contribution contract decision ledger

    Accepted decisions from the live review of Contribution and realm contract. This ledger distinguishes owner decisions from earlier proposal packets. It does not close the contract or claim an implemented SDK.

    Previously settled requirements

    • Explicit imported capability and realm descriptors provide downstream extension without global declaration merging or a closed central realm switch.
    • Contribution/capability definitions stay separate from core runtime and eligible native implementation modules.
    • Authoring and public contracts are framework-neutral. JSON rendering is replaceable.
    • The host can use multiple providers, routing defaults and per-operation overrides. Broadcast, callbacks, provider/realm preferences and UI-driven selection are already in the map's agreed scope.
    • Provider state remains distinct. Native resources stay local to their owning execution.

    Interview round 1: owner decisions

    Question Owner answer Accepted consequence
    Plugin declaration shape 1A Use dedicated typed properties such as services, actions, views, transforms and scripts. Contribution is shared terminology/typing; a mixed built-in collection is not the canonical declaration shape.
    Portable service registration inputs 2A Use service implementation definitions for both startup lists and runtime installation. A separate portable operation accepting arbitrary already-created service objects is not required; eligible native contexts retain their native APIs.
    Implementation selection and routing Corrected the question A host must support multiple realms/providers and dynamic backend selection, including target-origin/URL policies and default fallback. Installing service implementations must not fix the host to one backend.
    Wire type authority Agreed Runtime schemas are the source of public wire input/output types. The schema format/library and descriptor generics remain to be selected.
    Realm classification Agreed, with easy extensibility required Begin with webext and devserver environment families. Devframe/Vite integration differences belong in adapters/native contexts. Adding or changing realm families must remain straightforward through descriptors and adapters.

    The owner explicitly requires the agreed architecture to be recorded in GLOSSARY.md and ARCHITECTURE.md, with detailed definitions, tables, schemas and diagrams, after the architecture/declaration interview settles it. The current root glossary records agreed vocabulary in the meantime; the canonical documents must not present remaining proposals as accepted.

    Correction to the earlier implementation-selection question

    The previous question conflated two decisions:

    1. Installation determines which implementations a particular provider can offer in its eligible executions.
    2. Routing selects which available provider or providers execute a requested operation.

    Both are required. A host may have a WebExtension provider and one or more development-server providers available simultaneously. A shared UI can route a development target to its development server and a deployed staging/production target to the extension provider. The same host may also apply capability-specific preferences, caller overrides, callbacks and broadcast.

    The earlier recommendation for explicit host installation did not and must not narrow the original runtime routing scope. Provider installation declaration details remain subject to the packaging/lifecycle contract; dynamic provider routing is an accepted requirement, not a new optional feature.

    Proposed identity and routing relationship

    The following diagram makes the accepted multi-realm host requirement concrete. The model uses separately identifiable providers with their own realm metadata; exact connection and dispatch rules remain under review.

    flowchart TB
      UI["Shared JSON UI and client API"] --> Router["Host routing policy"]
      Router -->|"development target preference"| Development["Development provider: devserver realm"]
      Router -->|"deployed target preference or eligible fallback"| Extension["Extension provider: webext realm"]
      Router -->|"additional registered policy"| Custom["Additional provider: custom realm"]
      Development --> DevelopmentServices["Installed development-server implementations"]
      Extension --> ExtensionServices["Installed browser implementations"]
      Custom --> CustomServices["Installed custom implementations"]
    
    Loading

    URL-sensitive policy refers to the inspected target, not the popup's own extension URL. The generic SDK must not contain company domains, hard-coded staging/production classifications or assumptions that every development origin is localhost. The host supplies target policies and provider metadata.

    Adding a realm requires an exported descriptor and an adapter/integration that advertises eligible implementations. It must not require modifying an exhaustive core realm union or teaching the JSON renderer a new backend. A new realm can initially support only some capabilities, with explicit unsupported outcomes for the remainder.

    Still open at the core/routing boundary

    Provider discovery and routing already owns detailed selection, ambiguity, cancellation, broadcast and reconnect behavior. The current contribution contract must expose compatible definition/context integration points rather than settle that ticket implicitly.

    The next interview must clarify:

    • How host URL policies combine with plugin/contribution defaults and explicit invocation overrides.
    • Whether an action's required services remain on one provider by default, and how intentional cross-provider orchestration is declared.
    • Whether fallback is only pre-dispatch, and how uncertain execution after disconnect is represented without repeating a mutation.
    • How custom contribution kinds extend dedicated properties without weakening type checking.
    • How multiple declarations of the same service interact with ownership and disposal.

    Later declaration questions include schema representation, descriptor/version compatibility, local/remote context narrowing, activation failure and async teardown. Accepted answers must be consolidated into the canonical architecture documents once the core review is complete.

    Status

    The contract remains open. No SDK runtime, canonical architecture declaration or routing implementation is delivered by this decision ledger. This commit records the owner's accepted choices and corrects the previous installation/routing conflation.

    Review artifact

    Local ticket-scoped commit: 98f1fe3 on dvcol/wayfinder/contribution-realm. The branch is not pushed; this comment contains the complete accepted-decision ledger. Documentation whitespace, fence and local-link checks passed. No SDK source changed; application lint/type checks are not applicable. The remaining core and routing questions are still open.

  10. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Accepted routing and duplicate decisions

    Continuation of the live Contribution and realm contract review. These answers refine the contract; they do not complete its Definition of Done.

    Question Owner answer Accepted consequence
    Q6: Invocation overrides Agreed An explicit invocation policy replaces lower-priority defaults. An exact provider pin has no inherited fallback; the invocation must explicitly permit any fallback.
    Q7: Action provider affinity Agreed An action uses its selected provider's required capabilities by default. Cross-provider orchestration must be explicit.
    Q8: Fallback boundary A, with the clarification "once dispatched nor more routing" Routing freezes at dispatch. Fallback can select an eligible alternative before dispatch only. No response, timeout, disconnect or report that execution never began allows that dispatched invocation to move to another provider.
    Q9: Custom contribution kinds Agreed Genuinely new kinds use a typed, descriptor-tagged extensions collection. Built-ins retain dedicated properties; arbitrary top-level plugin keys remain invalid.
    Q10: Duplicate services Initially reject every duplicate; then allow exact instance identity Reject conflicting duplicate service registrations within the owning provider's registry. Permit an exception for the exact same object by reference. Equal IDs, versions, schemas or object contents do not establish this exception. The compared object and ownership after repeated registration still need clarification.

    The Q8 clarification is stricter than the offered recommendation, which also allowed fallback after confirmation that execution never started. The accepted rule removes that exception. A separately requested invocation starts a new routing decision; the SDK must not silently create one to replay the dispatched call. The detailed definition of dispatch, connection races and outcomes remains in Provider discovery and routing, subject to this boundary.

    The Q10 clarification supersedes unconditional rejection, but does not yet select reference counting, shared disposal handles or ownership transfer.

    Two different identity rules

    Identity Comparison Purpose
    Capability contract Declared stable identity/version, with compatibility policy still open Match consumers and providers across separately bundled modules and remote connections
    Duplicate service exception Exact local object reference Recognize the same installation object without treating separately constructed definitions as equivalent

    Different providers can implement the same capability. The duplicate check is local to the provider's owning registry, not a global prohibition across the host. A serialized remote description cannot prove local pointer equality.

    Portable installation already accepts service definitions. The next decision is whether the duplicate exception compares that definition object before setup, or the running API object produced by setup. Comparing the definition is recommended because a registry can reject a different recipe before it creates resources. This recommendation is not yet accepted.

    Allowing two owners to install the same definition also requires explicit disposal semantics. A recommended proposal is one constructed service with a separate ownership lease per installation; disposing one lease leaves the other installation active. A stricter alternative permits repeated references only within one owner. Neither policy follows automatically from reference equality. HMR creates a new object and therefore needs an explicit replacement lifecycle; structural equality must not bypass duplicate checks.

    Schema evidence for the next decision

    A read-only follow-up inspected SHA-512-verified devframe@1.0.0 and @devframes/json-render@1.0.0 artifacts. There were no new runtime experiments.

    Devframe already accepts Standard Schema validators, supports asynchronous RPC validation, and infers schema-backed arguments and returns from the schema input type. Its validation discards transformed values. JSON view custom validators must be synchronous, and patchState() does not rerun the view schema. Public JSON Schema export is a separate operation, with a permissive fallback when conversion is unavailable. Our contract must not mistake that fallback for faithful metadata.

    Sources: RPC definitions, RPC validation, view validation, schema metadata conversion.

    Next review round, recommendations only

    Q11: Which service object does the identity exception compare?

    • A: The exact service-definition object supplied to installation, checked before setup runs.
    • B: The running API object returned by setup, even when the recipes differ.

    Recommend A. The portable API already accepts definitions. B requires executing a second recipe before discovering whether it conflicts, and must account for any resources that recipe created.

    Illustrative interpretation under A, pending approval:

    const firstDefinition = pageService;
    const sameDefinition = pageService;
    const copiedDefinition = { ...pageService };
    
    // Same reference qualifies for the exception.
    firstDefinition === sameDefinition;
    // A separate object with the same contents does not qualify.
    firstDefinition !== copiedDefinition;

    Q12: May different plugins own the same accepted service instance?

    • A: Yes. Construct once, give each installation a separate ownership lease, and tear down the service after the final lease is released.
    • B: The exception applies only within one owner. Different plugins must consume an explicitly host-owned service.

    Recommend A. This permits the same shared definition in two plugin service lists without allowing either plugin's disposal to break the other. Returning one shared disposable handle would not provide that guarantee.

    Q13: Which runtime-schema interface should declarations accept?

    • A: Standard Schema, with a compatible validator chosen by the author.
    • B: JSON Schema as the primary authoring format, with a validator/compiler adapter.

    Recommend A for upstream alignment. The following round must still decide normalization and metadata export; choosing Standard Schema does not silently settle either.

    Q14: How should capability contract versions match?

    • A: Exact declared contract version; no inferred compatibility across versions.
    • B: Declared compatible version ranges with negotiation.

    Recommend A initially. Contract versions are separate from package release versions. Compatibility adapters can explicitly satisfy another contract version.

    Q15: After declaration validation succeeds, how far should a setup failure propagate?

    • A: Roll back the whole plugin.
    • B: Clean up the failed contribution, make its dependent contributions unavailable, and keep independent contributions running.

    Recommend B. A debugger setup error should leave an independent status view usable. Malformed declarations and duplicate conflicts remain registration errors; this question concerns setup failure after admission.

    Deliverable status

    Local ticket-scoped commit: d6b0cd0, not pushed. Updated docs/planning/005-decisions.md and the schema evidence in docs/planning/005-declaration-alignment.md. Markdown structure, local links and whitespace checks pass. Application lint and type checks do not cover this Markdown-only change.

    The issue remains open. After the declaration review settles, consolidate the agreed vocabulary and full API into GLOSSARY.md and ARCHITECTURE.md, including the requested tables, diagrams and example/test obligations. No SDK implementation is delivered by this update.

  11. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Standard Schema, numeric contracts and observable setup failures

    Continuation of the live Contribution and realm contract review. The owner accepted the schema interface, numeric exact versions and isolated setup failure behavior. The owner also proposed replacing the pointer-identity exception with simpler duplicate handling controlled by a shared strictness toggle.

    Question Owner answer Consequence and status
    Q11: Object identity Suggested dropping the identity check for simplicity; duplicate definitions should fail at setup, or warn and drop the later registration under a strictness option Revise the working proposal to reject duplicate definition registrations without comparing definition or running API references. The common strict/warn policy is the next confirmation point.
    Q12: Shared ownership See Q11 Do not add shared installation leases while pursuing the simpler duplicate policy. A skipped registration must not gain ownership of the existing service.
    Q13: Schema interface Standard Schema Accepted. Public wire declarations use Standard Schema. Transformation semantics and serializable schema export remain open.
    Q14: Contract versions Numeric contract version, exact match for now; use the same strictness toggle to warn when relaxed Accepted numeric versions and exact matching. Shared strictness behavior is requested. Whether warning means skipping an incompatible candidate or allowing its use needs explicit clarification.
    Q15: Setup failure B, with appropriate errors in logs and, where possible, the UI Accepted. Clean up the failed contribution and make its dependents unavailable. Keep independent contributions active. Report the failure in logs and expose it for UI presentation where a client is available.

    Proposed common strictness policy

    Use a host option such as strict: true, enabled by default, for duplicate registration and contract mismatch diagnostics. This table is a recommendation awaiting owner confirmation, not permission to silently redefine relaxed behavior.

    Condition Strict mode Relaxed mode Registry or execution effect in both modes
    A service slot is already registered in this provider Fail the incoming registration with a typed error Warn and return an explicit skipped result Preserve the existing registration and owner; do not run the incoming setup or return an owning handle for the existing service
    A candidate capability contract version differs from the requested numeric version Report the mismatch as an error to the affected resolution attempt Warn and expose an incompatible/unavailable candidate Do not bind or invoke the incompatible implementation

    Apply duplicate checks during startup registration as well as runtime installation, before the incoming setup callback creates resources. The proposed rule rejects the same definition object too; it needs no pointer comparison. One provider's duplicate rule does not prevent a different provider from implementing the same capability.

    The existing registration wins only within a known registration order. Concurrent startup declarations require deterministic admission rather than choosing whichever asynchronous setup finishes first. The exact startup conflict grouping and failure result belong in the complete registration API review.

    Only inspect a version mismatch when resolving or installing the relevant contract; advertising an unrelated version must not fail the entire host. A host may select another compatible provider before dispatch when the existing routing policy permits it. An exact provider pin and the prohibition on routing after dispatch still apply. Relaxed mode changes error handling, not the exact-match rule.

    This proposal introduces no implicit service sharing, ownership transfer, replacement or use of an incompatible contract. Actual setup failures retain Q15's failure behavior in either mode. HMR replacement and async cleanup remain explicit lifecycle decisions.

    Failure reporting into logs and UI

    The accepted requirement is observable failure without removing independent contributions. The proposed integration records the failed contribution's status and a serializable diagnostic identifying its provider, plugin, contribution and lifecycle phase. Runtime logs report the failure; an attached client can render the same diagnostic. A later UI mount reads current failure status, so an error before panel creation is not lost merely because no UI listener existed.

    Keep the original native exception in its owning execution. Pass a serializable diagnostic through the existing status/transport integration, rather than requiring a separate diagnostics backend or a renderer to make setup work. The exact status, error and subscription signatures remain part of the complete API sketch. Lifecycle failure reporting does not itself settle how individual operation calls return or throw errors.

    flowchart LR
      Failure["Contribution setup fails"] --> Cleanup["Clean up failed contribution"]
      Cleanup --> Dependents["Dependent contributions unavailable"]
      Failure --> Status["Contribution failure status and diagnostic"]
      Status --> Logs["Runtime error log"]
      Status --> Client["Current or later attached UI"]
    
    Loading

    Independent contributions keep running. A status view that itself requires the failed service is a dependent contribution and cannot be used as the only failure display. The host's generic failure presentation must remain independent of that service.

    Next review round, recommendations only

    Q16: Accept one shared strictness policy with strict mode enabled by default?

    The table above proposes strict: true by default. Duplicate definitions always lose the incoming registration, including repeated references to the same object. Strict mode reports an error; relaxed mode warns and returns a skipped result. A version mismatch remains unavailable in either mode. The toggle changes severity/handling, not compatibility or ownership. Recommend this policy. It removes pointer checks and shared leases while preserving numeric exact matching.

    Q17: Should schemas only validate, or also transform values?

    • A: Check original values. Perform defaults/coercion/normalization explicitly in implementation code when needed.
    • B: Use successful schema output, applying schema defaults/coercion/transforms to handler inputs and returned values.

    Recommend A for alignment with the audited Devframe RPC path, which discards transformed output. The tradeoff must be explicit: schema transforms do not run as application normalization under A, and handler/result types must not pretend they do. B requires consistent parsed-value handling and input/output inference across adapters.

    Q18: Must every public definition export JSON Schema metadata?

    • A: Require a JSON Schema exporter for every public definition.
    • B: Require export only for integrations that need schema inspection, such as schema-driven forms or an MCP adapter.

    Recommend B. Standard Schema validation remains mandatory for public wire operations. Basic identity/version discovery does not require exporting a validator as JSON Schema. An integration requiring export must report missing or unsupported export explicitly; it must not invent a permissive schema. Metadata is a structural description and does not replace local validation or authorization.

    Q19: When a required capability returns, should the host reactivate the dependent contribution automatically?

    • A: Yes, after ending the previous activation. Setup exceptions require an explicit retry; restoration never replays an operation.
    • B: Require an explicit retry for both capability restoration and setup exceptions.

    Recommend A. This makes the host responsible for capability availability changes while keeping programming failures visible instead of retrying them repeatedly. The exact async cleanup and replacement sequence follows this decision.

    Evidence and deliverable status

    The schema recommendation uses previously inspected, SHA-512-verified devframe@1.0.0 release code. This update adds no new runtime experiment. RPC validation, schema inference, Standard Schema, Standard JSON Schema.

    Local ticket-scoped commit: a573d4f, not pushed. Updated the decision ledger, declaration-alignment evidence and the glossary's contract-version term. Markdown structure, local links and whitespace checks pass. Application lint/type checks do not cover this Markdown-only change.

    The issue and its Definition of Done remain open. Registration/skip results, operation error signatures, schema typing, diagnostics, local/remote contexts and lifecycle replacement still need a complete API review. Consolidate the settled contract into GLOSSARY.md and ARCHITECTURE.md, with the requested definitions, tables, diagrams and example/test obligations, after that review. No SDK implementation is included.

  12. dvcolomban commented on Sep 22, 2026

    @dvcolomban
    CollaboratorAuthor

    Accepted strictness, schema and restoration decisions

    Continuation of the live Contribution and realm contract review. The owner accepted Q16 through Q19. These answers replace the earlier pointer-identity exception and settle schema behavior.

    Question Owner answer Accepted consequence
    Q16: Shared strictness Agreed Strict mode is the default. A duplicate fails the incoming registration; relaxed mode warns and skips it. No object-identity exception or shared ownership leases. A mismatched numeric contract stays unavailable in both modes, with error or warning reporting respectively.
    Q17: Schema transformations Agreed; consumers can transform handlers if needed Standard Schema validates original values. Consumers perform normalization, defaults and transformations explicitly in their implementation or a handler wrapper. Successful validator output does not replace the original input or return value.
    Q18: JSON Schema export Agreed Export is required only for integrations needing schema inspection. Standard Schema validation remains mandatory for public wire operations. An integration requiring export must report unavailable/unsupported export instead of fabricating permissive metadata.
    Q19: Capability restoration Agreed Automatically reactivate dependent declarations when required capability availability returns, after ending the previous activation. Setup exceptions require explicit retry. Restoration recreates registrations and never replays an operation.

    The Q16 answer supersedes the historical Q10 pointer-identity exception. The latest policy compares registration identities for conflicts and does not compare definition or running-service pointers to permit duplicates.

    Q17 also fixes a typing constraint: public handler inputs and successful return types must describe the values actually used. A guard-only adapter cannot advertise InferOutput as proof of a schema transformation it discards. The complete declaration sketch must derive those types from StandardSchemaV1.InferInput for both the input and return schema. Explicit normalization inside an implementation must still produce a return value accepted by the declared wire return schema.

    Capability restoration and setup retry remain separate triggers. Normal readiness loss can end an activation and later recreate it; a thrown setup error must not enter an automatic retry loop merely because its dependencies remain available. A permission or target error from one operation does not by itself remove a provider-wide service registration.

    Consolidated API direction

    Dedicated plugin properties, definition-only installation, multiple providers/realms, explicit descriptors and native host bootstrapping remain the core structure. The new review packet connects registration ownership, activation generations, schema-derived methods, local/remote native contexts and the existing domain tickets. It does not introduce a new generic host runtime or settle the pending alternatives silently.

    Standard Schema checks original values. Its successful parsed output does not replace a handler argument or result. Consumer implementations may normalize explicitly, provided their returned wire values satisfy the declared return schema.

    The following candidate illustrates type derivation for ordinary value-returning calls, pending Q22. These are proposed types, not published exports. target placement remains Q23; it is intentionally absent from this minimal derivation.

    import type { StandardSchemaV1 } from '@standard-schema/spec';
    
    export interface OperationDefinition {
      readonly input: StandardSchemaV1;
      readonly output: StandardSchemaV1;
    }
    
    export interface CallOptions {
      readonly signal?: AbortSignal;
    }
    
    export type OperationInput<Definition extends OperationDefinition> =
      StandardSchemaV1.InferInput<Definition['input']>;
    
    export type OperationValue<Definition extends OperationDefinition> =
      StandardSchemaV1.InferInput<Definition['output']>;
    
    export type CapabilityApi<
      Operations extends Readonly<Record<string, OperationDefinition>>,
    > = {
      readonly [OperationName in keyof Operations]: (
        input: OperationInput<Operations[OperationName]>,
        options?: CallOptions,
      ) => Promise<OperationValue<Operations[OperationName]>>;
    };

    Definition helpers must preserve each supplied schema's precise type instead of widening it to the base OperationDefinition. Input and output names refer to operation direction; InferInput on the return schema describes the original return value that this contract validates. Advertising InferOutput would promise transformations the runtime does not apply.

    JSON Schema export is required by integrations that need schema inspection, rather than every public definition. An integration must report missing or unsupported conversion. Exported metadata never replaces the validator or permission checks.

    Local and remote context sketch

    Use an explicit access discriminant, alongside provider/realm/execution metadata:

    Context Native access Typing consequence
    Locally owned service binding Descriptor-indexed access to the actual native objects owned by this execution Import a native descriptor and narrow presence before using its typed value
    Remote service binding Serializable provider, realm, execution and target metadata only No native field after narrowing to remote access
    Client's own execution Its own local native access where available A popup's browser APIs do not become the remote background worker's APIs

    The proposed accessor is context.native.get(importedDescriptor), returning its native type or undefined. A realm label alone must not imply privileged background APIs or a running Vite server. A local Vite adapter may expose Devframe, hub and kit contexts together. The contract must not turn those layered contexts into mutually exclusive realms.

    The complete signatures must prove literal descriptor inference, custom realm/native descriptors, remote exclusion and absence handling. Native resources and executable setup functions never travel in a serialized descriptor.

    Lifecycle and diagnostics

    Each service registration has one owner. Consumers receive bindings, not ownership leases. Strict duplicates fail the incoming registration; relaxed duplicates warn and skip it before setup. An installation handle is proposed to expose current status, subscriptions, enable/disable, explicit setup retry and async disposal. A plugin handle observes child contribution status, including partial readiness.

    Required capability loss ends the affected activation; restoration recreates it after cleanup. Setup failure cleans up that contribution and invalidates dependents while independent contributions remain active. Current status and serializable diagnostics support logging and UI display, including a panel opened after the failure. The exact admission and cleanup failure boundaries are still questions below.

    Remaining core review questions

    Q20: Strict errors during batch admission

    Q15 fixed isolation after setup begins; it did not settle invalid startup declarations discovered before setup. Consider two service definitions claiming the same slot in one startup batch with strict mode enabled.

    • A: Reject that admission batch before running any of its setup callbacks. A startup batch fails to start; a later plugin batch fails to install. Previously running installations remain intact.
    • B: Reject only the conflicting declaration, activate independent declarations and return a partial admission report.

    Recommend A. It separates invalid composition from runtime setup failure and gives strict mode a predictable admission boundary. Relaxed duplicates still warn/skip. This does not change Q15's isolation for setup failures in an admitted batch.

    Q21: Service versions in one provider

    Should one provider be able to offer both version 1 and version 2 of the same capability ID at the same time?

    • A: Yes. Conflicts use provider plus capability ID plus exact numeric version; each version has its own registration and owner.
    • B: No. One service slot per capability ID in a provider; changing its version requires explicit replacement.

    Recommend A. It supports explicit compatibility implementations while preserving exact matching and no negotiation. Registering the same ID/version twice still conflicts, including repeated object references. Different provider instances remain independent under either option.

    Q22: Individual call results

    • A: Ordinary calls return Promise<Value> and reject on failure. Publish stable error codes and a runtime narrowing helper. Availability and lifecycle remain observable separately.
    • B: Every call returns a tagged result such as success, unavailable, cancelled or failed. Callers must unwrap the successful value.

    Recommend A to preserve upstream call/return-schema semantics. TypeScript does not encode a rejection type in Promise<Value>, so this is a runtime-classified error contract, not statically typed exceptions. The already-required broadcast routing API still needs separate per-provider outcomes; that does not require wrapping every service method.

    The earlier architecture packet proposed B. This revision makes the compatibility tradeoff explicit instead of adopting that wrapper by default.

    Q23: Target placement

    • A: Put the inspected target in common invocation options/context; operation input contains business data. Routing and authorization consume that same target.
    • B: Put the target in each operation's input schema; routing needs a declared way to extract it.

    Recommend A. A target-bearing call must not carry two independently editable target identities in routing options and business input. The operation descriptor must specify whether a target is required or absent; the exact typing follows this choice. Browser target data remains a validated transport value, and targetless calls such as provider configuration remain possible. A business operation can still reference other subjects in its payload; those do not silently change its execution target.

    Q24: Cleanup failure before replacement

    If cleanup rejects or never completes during reactivation or HMR replacement:

    • A: Keep the new activation blocked. Report the failure; recovery requires successful cleanup or an explicit adapter reset/reload that proves the old execution is gone.
    • B: After a configurable limit, warn and allow the new activation even though old registrations may remain.

    Recommend A. This preserves the accepted requirement to end the previous activation before starting another. Async teardown should be awaited and repeated disposal should refer to the same teardown attempt. An abort signal requests cooperative cancellation; it cannot prove that old native work stopped or undo external effects.

    The concrete reset/reload choice, timeout and recovery from browser worker termination belong to the reload and state tickets. The core contract must state what qualifies as the old activation ending.

    Released API evidence

    A read-only follow-up checked SHA-512-verified devframe@1.0.0 and @vitejs/devtools-kit@0.7.5 artifacts. No new runtime experiment or latest-release claim is made here.

    • Ordinary RPC calls return values through promises and reject on failure. Scoped calls and local invocation preserve the awaited return type. A universal result envelope would change this shape. Client RPC, scoped calls.
    • Connection errors expose runtime kind values for connection, authentication and timeout. Status/events and named RPC diagnostics are separate observability mechanisms. Preservation of native exception prototypes/codes through every future adapter is not established. Connection errors, RPC diagnostics.
    • Definition wrappers accept an existing typed context, and kit contexts extend hub contexts. The released Devframe bridge installs into the existing host. Context wrapper, kit context, bridge.

    Validation and deliverable status

    Local ticket-scoped commit: c7ad39e, not pushed. Added docs/planning/005-core-api-review.md and updated the accepted decision ledger and schema-alignment notes.

    The exact method-type snippet above passed isolated TypeScript 7.0.2 checks with @standard-schema/spec@1.1.0 and Zod 4.6.5 fixtures. Strict mode, unchecked-index checks, exact optional properties and library checking were enabled. Positive fixtures preserve original string input/return types for schemas declaring string-to-number transforms. Four negative fixtures reject transformed input/result types, undeclared methods and missing required input. The same fixture passed Oxlint correctness, suspicious and pedantic categories with warnings denied. Markdown structure, local links and whitespace checks also pass.

    This proves the small type derivation, not a running SDK, adapter, helper-inference API or error-transport implementation. No full-repository validation was run.

    The issue remains open. After the remaining core choices and complete signature review, consolidate the agreed glossary and architecture into GLOSSARY.md and ARCHITECTURE.md. The new packet links all existing domain tickets, including the exact example/API-coverage obligations, so their unresolved signatures remain explicit planning dependencies. Do not mark the Definition of Done complete or begin SDK implementation on the basis of this partial type proof.

  13. dvcolomban commented on Sep 23, 2026

    @dvcolomban
    CollaboratorAuthor

    Resolution: core architecture and declaration contract

    The owner accepted the final core decisions, Q20–24, after the earlier four review rounds. This resolves Contribution and realm contract as a design decision. Runtime packages and full host conformance remain implementation work.

    Download the canonical contract and review history.

    The archive contains GLOSSARY.md, ARCHITECTURE.md, CONTEXT.md, the complete declaration specification in docs/contracts/core.d.ts, compile-only positive/negative fixtures, reproduction instructions, the API/example/test matrix and the historical review packets. Its source is local ticket commit 195e066, following c7ad39e, on dvcol/wayfinder/contribution-realm. The branch is not pushed. Archive SHA-256: dc7e8fc69929cac5d371383952902fe446695462875b92850eb1b132851f120f.

    Final owner decisions

    Decision Accepted contract
    Strict admission Reject the invalid incoming batch before any of its setup runs. Preserve existing installations.
    Service versions Identify slots by provider, capability ID and exact numeric version. Multiple versions may coexist, each with a mandatory explicit contract.
    Individual calls Return Promise<Value> and reject on failure, with portable diagnostics and runtime narrowing. Broadcast outcomes belong to the routing contract.
    Targets Put the validated execution target in common invocation options/context, separate from business data. Operations declare required or absent target.
    Failed cleanup Block replacement until cleanup succeeds or a verified adapter reset proves the previous execution ended. Timeouts do not authorize overlap.

    Vocabulary and declaration boundaries

    A capability describes a versioned backend service contract; a service definition supplies its implementation recipe. A contribution is an owned addition such as a service, action, view, transform or script. A plugin groups dedicated typed lists of contributions under one installation. A plugin package may also export contracts; exporting a contract does not implement it.

    A host composes providers, clients, adapters and policies. A provider is a backend identity. Its realm describes an extensible environment family, initially webext or devserver. An execution identifies the actual worker, process or document; a surface identifies presentation. These are separate axes. Local Devframe, hub, kit and Vite contexts can coexist within one server provider.

    Use explicit imported descriptors and one definePlugin. Built-ins have dedicated properties; custom kinds use validated descriptor-tagged extensions and an explicitly installed kind handler. Definitions remain inert and separate from runtime, native implementations and rendering. Startup and dynamic installation accept the same service recipe. No generic plugin-entry wrapper, implicit raw-object provider, global augmentation, pointer-identity duplicate exemption or shared ownership lease is required.

    Representative authoring

    The full types and compile-checked fixture are in the archive. This abbreviated example uses imported schemas and an execution descriptor:

    const page = defineCapability({
      id: 'example.page', version: 1,
      operations: {
        readTitle: defineOperation({
          input: titleInputSchema, output: titleResultSchema, target: 'required',
        }),
      },
    });
    const readTitle = defineAction({
      id: 'example.read-title', version: 1, operation: page.operations.readTitle,
    });
    const handler = defineActionContribution(readTitle, {
      id: 'example.read-title-handler', execution: providerExecution,
      requires: { page },
      handler({ input, services, target, signal }) {
        return services.page.api.readTitle(input, { target, signal });
      },
    });
    const plugin = definePlugin({ id: 'example.inspector', actions: [handler] });

    The action descriptor is client-safe; its packaged handler executes in the provider. A JSON view references the action contract without importing the handler. A UI-free plugin needs no renderer. A server or browser recipe implements page with defineService, obtains its actual native objects through context.native.get(importedDescriptor), and registers owned cleanup with context.scope.onDispose.

    A local binding exposes api and context with access: 'local' and typed native lookup. A remote binding has access: 'remote' and no native member. Native resources never cross messages. A bound API remains pinned to its provider; a fresh capabilities.invoke, actions.invoke or resolution can select another provider before dispatch. Once dispatched, the call never reroutes or replays automatically.

    Standard Schema validates original wire inputs and results. Both types derive from the respective schema's InferInput; transformations belong in consumer handlers. Schema export is required only by integrations that need it, with explicit unsupported export instead of permissive fabricated metadata.

    Ownership and lifecycle

    Operation/event Contract
    Install Preflight and reserve identities, then perform the initial activation pass. Missing dependencies leave an observed waiting contribution; they do not hang install indefinitely.
    Duplicate service Strict default rejects the incoming batch. Relaxed mode warns/skips before setup and grants no ownership of the existing service.
    Contract mismatch Remains unavailable in both modes; strictness changes diagnostic severity, never compatibility.
    Setup failure Clean up the failed contribution, invalidate dependents, keep independent contributions active, expose logs and current/later UI diagnostics.
    Requirement restoration Reactivate after the previous activation ends. A setup exception instead needs explicit retry.
    Observe/control Handle exposes snapshot, subscribe, enable, disable, retry and async dispose. Admission is distinct from readiness.
    Cleanup Cancel cooperatively, fence stale generations, await reverse-order cleanup, attempt every callback and report all failures.
    Replace Validate successor before stopping predecessor; successor cannot activate until old resources have ended.
    Worker termination Does not prove callbacks ran. Adapter recovery must establish ownership and target generation. Closing a panel does not dispose independent provider work.

    The architecture includes dependency, execution, lifecycle and invocation sequence diagrams, installation-state definitions, admission boundaries, replacement rules and error requirements. Raw native APIs remain available in their eligible context. Adapters reuse upstream host RPC/state/JSON contracts; they do not replace those systems with a second generic host engine.

    Validation and complete coverage obligations

    All 61 public exports appear in CORE-API-MATRIX.md, mapped to named example/fixture obligations for standalone Devframe, Vite DevTools, Chromium and Firefox. It includes custom realms/native contexts/kinds, UI-free contributions, version coexistence, target errors, duplicate admission, partial failure, subscriptions, cleanup, replacement and client bundle boundaries. Domain-specific APIs must extend that inventory before implementation admission. Planned runtime cells are clearly marked as obligations, not completed tests.

    The full declaration and fixture compile with TypeScript 7.0.2, Standard Schema 1.1.0 and Zod 4.6.5 under strict mode, unchecked-index checks, exact optional properties and skipLibCheck: false. 25 expected-error fixtures check missing contracts/targets/methods, invalid return types, guard-only schema inference, undeclared requirements, remote native access, incorrect plugin fields/kinds and extension payloads. Oxlint 1.85.0 correctness, suspicious and pedantic categories pass with warnings denied. Document links, fences and whitespace checks pass. No full-repository validation or SDK runtime-conformance claim is made.

    Follow-on ownership

    Exact native host interfaces belong to Server adapter contract. Dynamic selection, cancellation and broadcast remain in Provider discovery and routing; persistence/recovery in State scope and recovery; authorization in Permissions and trust. Debugger and CDB contract, Injection and transform contract, Renderer and surface contract and Live preview and reload contract own their domain operations. Complete execution evidence belongs to Examples and API coverage contract, Portable contribution proof and Release and conformance contract.

    The owner explicitly authorized continuing the map and implementation after the architecture settles until another material choice needs input, with commits separated by ticket. This overrides Wayfinder's default one-ticket-per-session stopping rule; it does not let the agent answer unresolved owner decisions.

  14. dvcolomban commented on Sep 23, 2026

    @dvcolomban
    CollaboratorAuthor

    Core implementation progress

    Local commit 5c3469b implements the reviewed declarations and the first owned runtime components on dvcol/implementation/foundation. This does not reopen or change the resolved core decisions. The branch remains local and no upstream PR has been opened.

    Reviewable source archive through 76b09e4, SHA-256 aaaec5d63ce427ed4c85b57eeb062c4b29d4f72053b52cfdf42fba9a26ac3d4a.

    Implemented

    packages/core exposes all 61 agreed exports, including the 12 executable factories/error guard. Declarations are inert and validate IDs, numeric contract versions, schema protocols, explicit properties and contribution kinds. Factories snapshot and freeze their owned declaration records and collections without freezing application schemas, payloads or functions. This prevents mutable aliases from changing a capability's identity or requirements after registration. Shared contract imports contain no backend or renderer dependency.

    packages/runtime is a private adapter implementation package. It owns:

    • Atomic startup/later reservation, strict duplicate rejection, relaxed service skips without ownership, exact-version coexistence, and cycle detection across new and already waiting services.
    • Guard-only Standard Schema invocation. Input/output transforms never replace values; invalid input never reaches the handler. Target identity is captured before asynchronous validation so caller mutation cannot switch the authorized target. The adapter still owns target authorization and freshness.
    • Cancellation, local classified errors and no replay. Once dispatched, a handler is never invoked again automatically. Cancellation waits for actual in-flight work to settle before ownership can be considered ended.
    • One-generation setup/call/cleanup ownership. Stop fences new work, cancels immediately, awaits setup and calls, then attempts every cleanup in reverse order. Pending or failed cleanup cannot become successful disposal. Repeated stop/dispose observes one attempt. Late setup completion cannot reactivate the generation.

    Executed checks

    • Core: 53 runtime tests and the original 25 expected compiler errors against the real package implementation.
    • Runtime: 32 tests covering admission, guarded calls, cancellation, mutable alias/target regressions, cleanup and delayed settlement.
    • Both packages: configured strict type-aware Oxlint, Oxfmt checks, TypeScript 7 with library checking retained, Vite bundle builds and declaration emission.
    • 76b09e4 adds real tarball installation outside the workspace, Bundler and NodeNext strict declaration consumers, runtime execution and a browser bundle dependency audit. It is a separate issue-16 check, not real extension-host evidence.

    Remaining work

    Installation handles, dependency reconciliation, provider-local bindings and action execution are being composed next, together with a maintained runnable contribution example. Native server/extension adapters, dynamic routing, state, permissions, debugger, transforms, renderer surfaces and HMR still belong to their domain contracts and full host tests. The API proof matrix records these remaining obligations; unit tests and declarations do not mark its four-host cells complete.

  15. dvcolomban commented on Sep 23, 2026

    @dvcolomban
    CollaboratorAuthor

    Local provider lifecycle implemented

    Separate commit 8304803 implements the accepted core ownership contract in the private @devkit/runtime package. The earlier declaration/activation commit is 5c3469b. Both are local and unpushed. Committed source checkpoint through 57f3381 includes the complete implementation, canonical architecture/glossary, tests and example.

    Archive SHA-256: 83a7e3788718bb15b3a1f97f98d341c1da41e800033f3673ba199a0df46818d6.

    The private adapter entry point now composes real installation handles, dependency activation, status and local calls:

    const provider = createProviderLifecycle({ provider: descriptor, execution, native, report });
    const startup = await provider.startup({ services, plugins });
    const service = await provider.services.install(serviceDefinition);
    const resolution = await provider.resolve(capability);
    const value = await provider.invoke(action, input);
    // Admitted handles support snapshot/subscribe/enable/disable/retry/dispose.
    await provider.dispose();

    Strict startup preflights the complete batch before setup. Relaxed duplicates return explicit skipped results. Waiting contributions activate when dependencies return, explicit disable survives restoration, and setup failures require explicit retry. Independent failures remain isolated. Status subscriptions deliver the current snapshot and isolate failing listeners.

    Replacement validates the successor before retiring the current owner. Old and successor identities remain reserved during cleanup. Unrelated installation and calls continue while cleanup waits; dependency cycles are checked against both possible graphs. Replacements themselves serialize per provider. Failed cleanup retains ownership and blocks a successor. A relaxed skipped service replacement leaves the old service active.

    Regression tests cover synchronous abort-listener reentry, dependency teardown when a nested call omits signal forwarding, late setup completion, cleanup order/failure, invalid replacement preserving the old owner, conflicting replacement claims, provider-owned schema validation and prototype-shaped operation names. Native causes reach the local diagnostic sink separately and never appear in portable status records.

    Validation passed for this package: 55 runtime tests, strict TypeScript 7, type-aware Oxlint with warnings denied, Oxfmt, Vite build and declaration emit. The final core/runtime tarballs also pass clean Bundler/NodeNext consumer checks and a portable browser bundle check. Core's separate existing suite has 53 passing tests and 25 negative type fixtures.

    This completes another local implementation slice of the already-settled decision. It does not complete provider routing, transport, shared state, renderer, scripts/transforms, real server/extension adapters or their host matrix. packages/runtime/README.md and docs/contracts/CORE-API-MATRIX.md record the exact local evidence and remaining obligations. The maintained contribution example is reported under issue 14.

  16. dvcolomban commented on Sep 27, 2026

    @dvcolomban
    CollaboratorAuthor

    Implemented the owner-approved amendment in c374198: schema-defined operation inputs now own resource references; client selectors retain realm/provider routing. Removed universal TargetReference/TargetRequirement, target modes, common target context/options and catalog target metadata. Runtime, client, server exposure, examples and packed-consumer fixtures are migrated together.

    The canonical architecture includes the multi-client/backend diagram, ownership table, code sketch and result/state distinction. Glossary and maintained package documentation agree; earlier target-mapping proposals are explicitly superseded.

    Validation: 75 core, 79 runtime and 31 client tests; affected builds, TypeScript 7 and strict Oxlint/Oxfmt; isolated Bundler/NodeNext consumers and browser bundles. CI initially caught formatter-moved @ts-expect-error directives; adc48d7 puts them on the exact invalid properties. Full repository CI passes.

    The native two-client/two-backend applicability proof is a separate ticket #6 commit. No generic acceptance hook, topic bus, new authentication layer or codec was added. This is an amendment to the closed design contract, not a claim of complete host conformance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

wayfinder:grillingDecision requiring discussion with the project owner

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions