Skip to content

Latest commit

 

History

History
495 lines (354 loc) · 65.6 KB

ARCHITECTURE.md

File metadata and controls

495 lines (354 loc) · 65.6 KB

Portable contribution architecture

This is the canonical core architecture settled through the owner review of Contribution and realm contract. GLOSSARY.md defines the vocabulary. Archived declarations preserve the original review; maintained package source and generated exports define the current API. The core package implements its shared types and inert declaration factories. Private adapter runtime internals implement admission, guarded invocation and generation ownership. Complete provider adapters and host conformance remain in progress.

The core decisions are settled. The linked domain tickets still own the exact server, routing, state, security, debugger, injection, renderer and reload integrations. Settling this document does not claim those implementations or their real-host tests exist.

Purpose and constraints

Build a generic, modular client and contribution ecosystem shared by browser extensions and development tools. A host can select a development-server provider for a development target and an extension provider for a deployed target while retaining the same contribution contracts and JSON UI. Hosts supply domain/origin policies; the SDK contains no company-specific code or domain classifications.

Executable contributions are assembled from packaged code at build time. Startup and later installation accept the same definitions; runtime installation does not mean downloading executable plugins. Public authoring is framework-neutral. Renderer implementations can use a framework and remain replaceable.

The monorepo uses pnpm/Turbo, Vite/Oxc, strict type-aware Oxlint and enforced Oxfmt formatting with TypeScript 7. The old frontend scaffold and active ESLint, Stylelint and Prettier integrations have been replaced. Tooling conventions record enforced checks and narrow exceptions; migration gates retain the required acceptance criteria. Default-rule feasibility probes do not satisfy these gates. Chromium and Firefox are the initial browser hosts. Development HMR and watched production builds with live preview have separate, explicit update/restart/reload behavior.

Upstream compatibility and minimal adapters

Use Devframe's public APIs and terminology directly wherever they express the required behavior. Reuse its authentication/authorization, RPC, serialization, shared state, JSON model and renderer contract; use Vite for compilation, watching, HMR and server lifecycle. An adapter supplies missing realm integration, not an alternative implementation of those systems.

The native reference renderer stays unchanged when dispatching portable contribution actions. Its client integration binds declared action references and inputs to the existing router, including outgoing recipient selection and provider-owned applicability. Do not add a parallel per-button handler API or put routing inside the renderer. Native view reads, built-ins and state subscriptions retain their own behavior. Custom renderers remain optional implementations of the native renderer contract. Action binding implementation record.

Before adding an SDK API or mechanism, identify the public upstream entry point, the concrete missing behavior, and why direct composition cannot supply it. Keep any necessary difference confined to the adapter or the agreed portable contribution contract. Prefer deleting a local mechanism when upstream covers its behavior. Avoid renaming facades, copied vendor types, private imports, independent wire formats and generic extension points without a demonstrated consumer.

Native connections retain their own authentication and authorization. Ordinary native calls need no mandatory second SDK authorization callback. A page-to-extension bridge still checks its actual sender and target because the trusted extension connection does not authenticate the originating page. Reuse Devframe serialization for operation values; JSON-authored UI does not imply JSON-only business payloads. Unsupported-value rejection must be tested at each actual transport boundary.

The portability layer preserves native state lifetime and write behavior. Replacing a portable provider does not reset host-owned state, and an SDK action is not a mandatory gateway for native state writes. Contributions and hosts choose keys, resource scope, validation, persistence and conflict policy through native APIs. Add an adapter only for a demonstrated transport or lifecycle gap; do not impose a second state policy to make hosts appear identical.

The accepted numeric contract versions, explicit dependency lifetimes, replacement barriers and multi-provider routing remain SDK responsibilities where native APIs do not provide equivalent behavior. The implementation and map review records the comparison and the remaining integration gaps. Preserve existing behavioral tests when replacing a local mechanism with an upstream API.

Host-managed build and reload

The owner confirmed that normal server development follows Vite and the existing Devframe/DevTools host lifecycle. Vite owns compilation, module updates, watching and its server restart behavior. Its current pipeline uses Oxc transformations and Rolldown bundling. Experimental bundled development remains a development-server mode; it does not replace the separate production-build watcher and live preview workflow.

Adapters install and dispose their own contributions through the host's supported lifecycle. Core does not supervise processes or automatically restart an embedding host after failed cleanup. A host-specific restart mechanism requires an actual integration need and an explicit ownership policy. Vite's in-process server restart is not proof that arbitrary plugin timers, callbacks or in-flight work stopped.

The owner accepts Vite's current failed-restart replacement-cleanup gap while the dependency remains unpatched. A separate upstream proposal closes an abandoned replacement after old shutdown rejects. Normal SDK implementation does not depend on that proposal or add compensating restart machinery.

Workflow Host owns Adapter owns
Vite development, including supported bundled dev Module graph, HMR, HTTP server and normal server restart Portable registrations, local native contexts and their cleanup
Vite production build watch Source watching and completed bundle output Build completion/status handoff where required
Vite live preview HTTP server and serving completed built assets Attached live provider and owned subscriptions/middleware
Existing standalone Devframe/DevTools host Existing process, RPC/state services and host lifecycle Only the portable contributions installed into that host

Last-successful-build publication concerns asset availability. It does not restore backend state or replay interrupted actions. The server adapter contract and reload contract retain the integration and verification obligations. Vite build watching, Vite lifecycle hooks.

Contracts, contributions and plugins

Element Declaration Runtime meaning
Capability Imported ID, mandatory positive integer contract version, operation schemas A service contract; does not imply availability
Service Capability descriptor plus execution assignment, requirements and setup recipe Constructs an owned implementation of exactly that contract
Action Shared versioned descriptor plus a separate handler contribution An invocable use case that can consume capabilities
View contribution Execution assignment, requirements and setup recipe Owns native publication and subscriptions through its activation scope
Transform Execution assignment, requirements and native registration setup recipe Owns native transformation registration through its activation scope
Script Execution assignment, requirements and native registration setup recipe Owns registration through its activation scope; native tooling packages page code
Plugin Named lists of services, actions, views, transforms, scripts and custom extensions Installed and controlled together, with independent contribution activation/failure
Custom kind Imported kind descriptor, payload schema and explicitly installed kind handler Allows new kinds without arbitrary plugin fields or a closed core switch

A service always declares a capability contract and its version. There is no unversioned service, implicit latest version or required portable raw-object provide. Native APIs remain available in eligible native contexts.

Definitions are inert. Helpers preserve schema/literal inference and validate declarative invariants; they do not execute setup or register resources. Use dedicated plugin properties. Unknown properties such as actons, wrong-kind list entries and missing contracts are errors.

Declaration helpers

Every declaration helper takes one object. Contracts can be imported by a UI without importing backend handlers.

Helper Input Result and placement
defineCapability { id, version, operations } Shared service contract
defineService { capability, id, execution, requires?, setup } Implementation in plugin.services
defineActionContract { id, version, operation } Shared action contract
defineAction { contract, id, execution, requires?, handler } Implementation in plugin.actions
defineView { id, execution, requires?, setup } Native setup recipe in plugin.views
defineScript { id, execution, requires?, setup } Native setup recipe in plugin.scripts
defineTransform { id, execution, requires?, setup } Native setup recipe in plugin.transforms
defineExtension { descriptor, id, execution, payload } Custom contribution in plugin.extensions

An operation describes one callable input/result pair. A capability names several operations; an action contract describes one invocable use case. “Contribution” remains the shared terminology and typing for owned additions. It does not require a generic defineContribution factory. defineActionContract replaces the former contract helper named defineAction; defineAction replaces defineActionContribution.

Portable view contributions

The owner accepted the native setup recipe on 2026-10-02. plugin.views contains ViewDeclaration entries created by defineView({ id, execution, requires?, setup }). Setup receives the existing SetupContext<Requirements> and returns Awaitable<void>. The declaration is inert; activation runs the recipe in its assigned execution with its declared capability bindings.

A view contribution owns a recipe and its activation scope. The native published view is the resource that recipe creates with createJsonRenderView. Authors register view.dispose immediately after creation and register subscription cleanup through scope.onDispose. Dependency loss, disable, enable and replacement use the existing contribution lifecycle. Cleanup runs in reverse acquisition order; failures retain the cleanup barrier and block a successor.

Core and runtime remain native-independent. The author imports the native publisher; the host supplies its actual publishing context through the existing native descriptor lookup. Reuse one stable context object per native shared-state host so native duplicate detection and index ownership remain consistent. Native options, state, index discovery and renderer contracts remain unchanged; this adds no JSON schema, metadata catalog, state protocol, adapter API or framework dependency.

The host owns renderer mounting and surface/dock placement. Native server dock registration has no unregister handle, so publication cleanup does not imply server-dock removal. The shared recipe is exercised by the server, Vite and WebExtension examples. The decision record and executable API inventory distinguish those checks from the remaining surface/HMR obligations.

Portable script contributions

The owner accepted native declarations plus contribution setup on 2026-10-02, provided one common contribution API can register a feature on either provider realm. defineScript({ id, execution, requires?, setup }) supplies that registration lifecycle in plugin.scripts. Like a view, it receives the existing typed SetupContext<Requirements>, returns Awaitable<void> and owns cleanup through scope.onDispose. The realm implementation performs native registration; core has no realm-specific option union or source loader. Decision and native examples.

The declared execution owns registration, not necessarily the injected code's execution. A background recipe can register a native MAIN-world content script. The browser owns document matching, permissions, timing and world semantics. A server recipe can own whether an installed native Vite HTML hook contributes a script to future HTML responses. Native declaration/build configuration packages or serves executable code and remains separate from runtime activation.

Devframe host-page imports, Vite application HTML transformation and browser content-script registration retain their actual stages. Unregistration does not undo effects in existing documents. Built HTML remains built HTML in preview; disabling a live backend does not rewrite it. No common declaration promises an unsupported timing or rollback guarantee. Arbitrary HTTP/response transform contracts remain issue #11 work.

Portable transform contributions

defineTransform({ id, execution, requires?, setup }) applies the accepted native setup pattern to plugin.transforms. Its typed SetupContext<Requirements>, activation scope and explicit cleanup match view/script recipes. Definitions remain inert; dependency loss, disable, retry and replacement use the existing contribution lifecycle. Returned resources are not adopted. Implementation contract and native limits.

The recipe registers transformation through the actual native host or capability. Native APIs retain their stages, matching, ordering and failure behavior. The SDK adds no callback pipeline, priority model, response buffering, paused-request controller or cross-realm order guarantee. Capability implementations can compose handlers when their native mechanism needs a single resource owner.

The Vite example installs two native HTML hooks in reversed configuration order and uses native pre/post processing. Contributions own whether those hooks act on future development responses. Independent builds use native configuration, and preview serves their completed output. The WebExtension example registers two native DNR session header rules with explicit IDs/priorities and independent cleanup on both browsers. Native rejection and atomicity remain visible as setup outcomes. Redirect/body capability implementations and the full permissions, frames, streaming and failure matrix remain issue #11 work.

Dependency and execution diagrams

flowchart BT
  Contracts["Shared descriptors, schemas and context types"]
  Declarations["Contribution definitions and packaged handlers"] --> Contracts
  Plugin["Plugin composition"] --> Declarations
  Adapter["Provider adapter"] --> Contracts
  NativeHost["Existing Devframe/Vite host or extension host"] --> Adapter
  Adapter --> Activation["Owned admission and activation"]
  Plugin --> Activation
  Client["Client integration"] --> Contracts
  Renderer["Replaceable JSON renderer"] --> Client
Loading
flowchart LR
  UI["Popup, options, panel or server UI"] --> Client["Client and routing policy"]
  Client -->|"select before dispatch"| Server["devserver provider"]
  Client -->|"select before dispatch"| Extension["webext provider"]
  Server --> ServerNative["Actual Devframe, hub, kit and Vite contexts"]
  Extension --> Background["Background browser APIs"]
  Extension --> Agents["Isolated content and MAIN-world agents"]
  Agents --> Target["Inspected document generation"]
  Server -->|"where its adapter supports delegation"| Agents
Loading

An adapter integrates environment lifecycle, communication and implementations. A transport only carries messages. Reuse upstream RPC/state/JSON contracts through supported interfaces; do not build a competing generic host, RPC engine or state authority merely to unify constructor names.

Placement and ownership

Piece Role Owns
Extension background worker/document Provider Browser capability implementations, provider registrations and recovery coordination
Isolated content script Execution agent Its document/frame resources and extension-side page bridge
Injected MAIN-world script Execution agent Page-owned resources and a validated target-scoped bridge
Extension DevTools page Client bootstrap/local integration Panel creation, inspected target integration and its native hooks
Popup, options, panel, sidebar Client and renderer That document's connections, subscriptions and mounts
Standalone Devframe server Provider through adapter Existing host integration and server contribution lifetimes
Vite DevTools server Provider through adapter Existing Devframe/hub/kit context and eligible Vite integration
Devframe/DevTools panel Client and renderer Panel mount and client subscriptions

Closing a UI document does not dispose independent provider work. A content/page script is not a trusted provider merely because it can send a message. A worker termination cannot be treated as proof that JavaScript cleanup callbacks ran; recovery must re-establish ownership and target generation.

Provider identity, realm, execution, UI surface and target are separate axes. Initial realm descriptors are webext and devserver. Adding a realm requires a descriptor and adapter, not a core union or renderer rewrite. Devframe, hub and Vite contexts may coexist within one server provider.

Routing identity

A route selector has a required realm: string and an optional provider: string. A provider selector constrains both fields. Provider-only selectors are unsupported. Identifiers are non-empty strings; callers choose stable unique names. Core performs no symbol or number coercion and maintains no alias registry. Registry collisions are checked within the (realm, provider) namespace; incarnation identifies a particular backend lifetime within that logical selection.

// Exported by @devkit/core and consumed by @devkit/client.
type RouteSelector = {
  readonly realm: string;
  readonly provider?: string;
};

The router belongs to the client application's composition. It can run in a panel or the extension's existing background execution and does not require another server or daemon. Each connection retains its own native authentication. Registry metadata and routing selectors never carry credentials.

Stable identity and client-owned discovery

The owner confirmed stable configured provider IDs and fresh backend incarnations on 2026-09-23. The provider descriptor carries id, incarnation and realm. An adapter creates the incarnation once for its backend lifetime. A recreated backend retains its configured ID and gets a fresh incarnation. Reconnecting to the same live backend or updating only a UI module does not create a new backend identity.

Identity Example Changes when
Provider ID example:project-server Host configuration selects a different logical provider
Provider incarnation Opaque runtime token The provider backend is recreated, including complete disposal and reinstallation
Contribution generation Local activation counter A contribution's owned activation is replaced
Resource generation Capability-specific document/session token A capability needs to distinguish successive lifetimes of the same resource

These identities do not substitute for authentication, permissions or target validation. A binding remains attached to the provider incarnation that created it. A successor with the same logical ID cannot acquire the old binding or an already dispatched call. Durable state and resynchronization policy remain in the state contract.

The client application's composition root owns discovery and supplies its registry to consumers. It composes explicit endpoint adapters, verified host-page handoffs and extension-local connections where supported. UI views consume provider metadata and route choices. They do not each invent discovery policy. This ownership creates no global daemon or singleton across unrelated applications and origins.

flowchart LR
  Server["Devframe / DevTools backend"] --> ServerAdapter["Native server connection adapter"]
  Extension["Extension backend"] --> ExtensionAdapter["Native extension connection adapter"]
  ServerAdapter --> Registry["Client composition root: discovery registry"]
  ExtensionAdapter --> Registry
  Registry --> Client["Shared client routing and subscriptions"]
  Client --> UI["JSON views and UI-free consumers"]
Loading

An extension background runtime can coordinate shared discovery for popup/panel consumers through native ports. Local DevTools-only hooks remain in the document that owns them. A web client can compose the registry in its own application runtime. Adapters retain native transports and provider state; registry snapshots contain only permitted portable metadata. The client implements local catalog readiness, routing defaults and broadcast. Native remote adapters synchronize an explicitly exposed catalog through authorized RPC queries and payload-free invalidations. Endpoint discovery and extension trust filtering remain adapter work. Capability implementations own resource selection, permissions and freshness checks. Routing ownership record.

Host-owned native RPC methods

The owner selected explicit host-owned remote method lifetime in Server adapter contract. The host declares the exposed action and capability contracts. Their named native RPC definitions remain registered for the host lifetime; contribution installation, disabling and disposal change implementation availability. An unavailable or disposed implementation rejects invocation. Native method registration alone does not advertise an available capability or grant authorization.

Remote exposure and local implementation ownership remain distinct within one startup call: createDevframeProvider({ context, providerId, services, plugins, expose? }), or createDevToolsProvider for a kit context. The owner accepted this authoring shape. expose lists imported action and capability descriptors; omitted contracts remain local. Startup and later installation/replacement return handles directly in default strict mode, and admitted/skipped outcomes with strict: false. A runtime boolean strictness produces the corresponding union type. Admission does not guarantee successful activation.

This choice reuses native registration without requiring an upstream unregister API. It preserves native authentication, schemas and serialization, expected-incarnation checks, and no rerouting after dispatch. The implemented exposure supports schema-validated unary operations. Capability implementations own domain/resource applicability and native resource checks. The browser-safe @devkit/server/client entry adapts an existing native RPC client into the shared router. Its read-only catalog query uses native authorization, and catalog changes trigger a fresh authorized query through a payload-free notification. It does not establish server-side unary cancellation on caller abort or disconnect.

The shared implementation lives in @devkit/devframe. Its createRpcProvider({ context: { rpc, realm, execution, native }, ...composition }) consumes actual native registration/broadcast members. createRpcProviderConnection({ rpc, realm, providerId, signal?, report? }) accepts native call, collector and connection-event members, checks the configured expected realm, and retains optional native cache checks. The existing server factories supply their real contexts; the extension example supplies its native Port composition. Both use the same exposure, catalog, incarnation and lifecycle logic. Native RPC objects remain host-owned and must keep stable identity across provider replacement. No full server client is fabricated for a Port.

Identity, admission and strictness

Every registration has an owner. Capability slots use (provider ID, capability ID, exact contract version). A provider can offer multiple versions concurrently, each backed by an explicit contract and implementation. A definition's contribution ID identifies its lifecycle within the installation and must be unique there. Version equality is not proof that an author preserved a schema: publishing incompatible content under an unchanged contract version violates the contract.

Contract IDs are non-empty, case-sensitive identifiers. Contract versions are positive safe integers. Package versions do not select contracts. Pointer identity is irrelevant to duplicate admission, including when the very same definition object appears twice.

Condition Default strict: true Relaxed strictness
Duplicate service slot Reject incoming admission batch Warn and skip the incoming service definition
Requested contract version unavailable Error diagnostic; incompatible binding remains unavailable Warning diagnostic; incompatible binding remains unavailable
Invalid declaration, unknown kind/field or dependency cycle Reject admission batch Still reject; strictness is not a bypass for declaration validation
Setup throws after admission Isolate failed contribution, invalidate dependents, report diagnostics Same failure and cleanup behavior
Cleanup fails or does not finish Block replacement Still block replacement

A startup batch is the host's complete startup composition. A later plugin installation is one batch; installing a service directly is a one-definition batch. In strict mode, reject an invalid batch before any of its setup runs. Previously running installations remain intact. Reserve identities deterministically before asynchronous setup; completion speed never selects a duplicate winner. Within a declared startup plan, host service declarations precede plugin lists, and declaration-list order supplies conflict diagnostics. Activation follows dependency order, not incidental array or promise completion order.

Relaxed duplicate results contain an explicit skipped outcome and diagnostic, with no ownership handle for the old service. A plugin containing skipped services may still admit its other declarations; its snapshot records the skips. Requiring an already host-owned service is allowed, but registering it again does not acquire ownership or merge options.

Schemas, values and calls

Standard Schema is mandatory for public wire inputs and returns. Validation checks original values; it discards successful transformed output. Derive both argument and return value types from the corresponding schema's InferInput. Authors can normalize in handlers or wrap handlers explicitly. The final returned value must still satisfy the wire return schema.

Public JSON Schema export is needed only by integrations requiring schema inspection, such as generated forms or an MCP adapter. Missing or unsupported export is explicit; never invent a permissive schema. Validation, serialization and authorization remain separate checks. Native objects and functions are never made transportable by a successful schema check.

Debugger and interception implementations are optional capability integrations. CDB is one independent library an implementation can use; its targets, leases, domain demand, Fetch configuration and recovery belong inside that integration. The generic packages must not depend on CDB, interpret its commands or require its configuration choices before supporting other capabilities. Native integration tests and dependency patches in the debugger example do not change this ownership. CDB integration scope.

Ordinary operation/action calls return Promise<Value> and reject on failure. Stable error codes and isOperationError allow runtime narrowing; TypeScript does not specify a promise's rejection type. Availability and lifecycle status remain separately observable. Broadcast uses per-provider outcomes in the routing contract.

Call path Provider behavior Target and context
capabilities.invoke({ capability, operation, input, signal?, routing? }) Select for this invocation before dispatch Request carries schema-defined input and an optional routing policy
capabilities.resolve({ capability, signal?, routing? }) Resolve a particular provider binding Availability is a snapshot; an operation can still fail afterward
binding.api.operation(input, options) Use that binding's provider No routing override that silently changes the binding's owner/context
actions.invoke({ action, input, signal?, routing? }) Select the action provider; its requirements bind there by default Handler receives schema-validated input, signal and its local execution context

A bound API stays pinned to its provider. A fresh routed invocation can select another provider before dispatch. Cross-provider orchestration is explicit. After dispatch, no timeout, disconnect or report that execution did not begin permits rerouting or automatic replay. A separately requested invocation starts a new decision.

The 2026-09-24 routing review confirms that equally eligible providers produce an ambiguity error requiring an explicit caller/UI/agent discriminant. Ordinary invocations use current availability at dispatch and do not implicitly wait for a connecting provider. A drop during execution is an error; a caller may explicitly request a new invocation, but the router cannot infer that the previous mutation did not run.

Core client interfaces and local provider handles now use request objects. Local handles accept resolve({ capability }) and invoke({ action, input, signal? }); their provider is already fixed, so they expose no routing override. Bound capability methods retain the two arguments input, options. Request types preserve operation/input correlation, including unions of operation requests. @devkit/client implements these routed interfaces over adapter-owned connections; both local server handles satisfy its ProviderConnection contract.

Core exports RouteSelector, RoutingDirective, RoutingCallback and RoutingPolicy, and validates optional routing defaults on public action contracts. The directive contains one selector or a non-empty ordered fallback list. Factories copy and freeze this metadata, including selectors inside structural plugin declarations. An explicit invocation policy replaces the action default, which replaces the client default. Local callback functions retain their identity and are not executed by declaration factories. Backend implementation handlers remain separate. Selectors require a string realm and optional string provider scoped to that realm. This declaration support does not implement remote discovery or selection.

Broadcast has separate capability/action methods with per-provider outcomes and a required non-empty selection list describing the union of recipients. It inherits no ordinary-invocation defaults. Before dispatch, every selector must match a known provider; an unmatched selector rejects the whole broadcast with unmatched-selection, listing all unmatched selectors. Known unavailable providers instead produce individual failed outcomes. Overlapping selectors dispatch once per attached incarnation.

An asynchronous routing callback observes fixed candidate owners. It rechecks current readiness for those original live attachments before dispatch. A selected attachment that disappeared or changed incarnation rejects as stale; new providers cannot enter the waiting callback's candidate set. A fresh invocation may select a successor. Cancellation stops waiting and prevents late callback dispatch. These choices are accepted in Provider discovery and routing.

The client registry rejects every duplicate (realm, provider ID) attachment and owns subscriptions/cancellation, not backend resources. detach() and client.dispose() do not close native hosts. A binding stays pinned to its attachment even if another connection later advertises the same logical ID. Selection errors expose local RoutingError codes, candidate metadata and unmatched selectors. The client delegates authenticated transport, connection authorization to its adapters; capability implementations retain resource-specific checks.

Local providers expose catalog.snapshot() and catalog.subscribe(listener). This read-only projection includes admitted capability/action IDs, exact versions, operation names, execution and contribution status. It contains no schemas, executable handlers, native context or diagnostic causes. It observes the existing installation lifecycle, including late admission, dependency changes, replacement and cleanup failure. An active entry is not proof of permission or target availability. Consumers also require an open provider. The native remote adapter separately projects explicitly exposed contracts through an authorized query; it does not put this catalog in client-writable shared state. Endpoint discovery remains unfinished. Browser capability authority is domain-specific work.

Operation declarations contain input and output schemas. There is no universal target field, target mode or target reference type. A capability that operates on domains, documents or debugger sessions defines those values in its own input schema and checks their applicability, permissions and freshness in its implementation. Provider incarnation and contribution activation generation remain SDK ownership concepts. Abort signals remain local. For native remote unary RPC, caller abort or disconnect stops waiting but does not cancel already-dispatched backend work. The owner accepted that native limitation; adapters never retry or replay calls automatically. Local contribution lifecycle cancellation retains its existing ownership guarantees.

Recipient selection and request applicability

The owner accepted two selection layers on 2026-09-27. The client chooses recipients using realm/provider selectors. Each selected action or service implementation decides which of its resources the input concerns. Core and transport do not interpret domains, URLs, feature flags, document IDs or debugger sessions.

flowchart LR
  ViewA["Client A: shared view"] --> RouterA["Client A recipient selection"]
  ViewB["Client B: another panel"] --> RouterB["Client B recipient selection"]
  RouterA -->|"native authenticated connection"| ServerA["devserver / frontend"]
  RouterA -->|"native authenticated connection"| ServerB["devserver / tools"]
  RouterB --> ServerA
  RouterB --> Extension["webext / browser"]
  ServerA --> ActionA["Action contribution"]
  ActionA --> ServiceA["Service: input applicability and mutation"]
  ServerB --> ServiceB["Same contract, own resources and applicability"]
  Extension --> ServiceC["Same contract, browser-owned resources"]
  ServiceA -->|"typed command result"| ViewA
  ServiceA -.->|"native state observation, separate integration"| ViewB
Loading

Connections are per client/backend pair. Closing Client A does not dispose Server A or Client B's independent connection. An action delegates to capability bindings in its selected provider unless it explicitly orchestrates across providers. If callers may invoke a capability directly, the service must enforce its own applicability and permissions; an action-only check is insufficient.

Responsibility Owner Example
Choose one recipient or broadcast recipient union Client policy selection: [{ realm: 'devserver' }]
Decide which resources the command concerns Action/service implementation Compare input.domain with the resources this service owns
Authenticate a connection and authorize native methods Native host Existing Devframe session and method policy
Validate resource permissions and freshness Capability implementation and its native integration Check an actual document/session before mutation
Report command completion Typed operation result or rejection Schema-defined applied / not-applicable value
Observe changes from other clients Native state integration Read current state and subscribe within each provider
// The action's input schema defines domain, key and value.
const outcomes = await client.actions.broadcast({
  action: setFlag,
  selection: [{ realm: 'devserver' }],
  input: { domain: 'dev.example.test', key: 'feature', value: true },
});

// Inside the service implementing this operation. This is ordinary author code.
if (!ownedDomains.has(input.domain)) return { status: 'not-applicable' };
return applyFlag(input);

not-applicable is an example business result declared by that operation's output schema. The SDK adds no accepts callback, domain matching DSL or special skip result. A successful not-applicable response is a fulfilled broadcast outcome. It does not remove the provider, trigger fallback or replay the request. If two selected providers both match, both can act; this is explicit broadcast, not a competing-consumer topic.

Single-recipient calls return a typed promise. Broadcast runs recipients concurrently and returns their individual outcomes after they settle; it does not stream partial progress. A direct response gives the initiating UI a completion/error path without a second event-correlation API. It cannot prove that a mutation did not happen if the response is lost. Existing cancellation and no-replay rules still apply.

Other clients observe changes through native state. The browser counter subscribes directly to native state and shows command completion separately. Live checks on both server hosts prove peer updates and explicit page-reload recovery through a fresh client. They do not prove automatic reconnect, atomic snapshot/stream ordering or persistence. State evidence.

Native state ownership

State follows the native host's key space and lifetime. Separate hosts keep separate values under the same key. Portable providers sharing one host share its native key space; contributions choose distinct keys when they need separate records. Provider incarnation pins operation ownership, not state storage.

Concern Owner and behavior
Value, updates, mutation and patches Native shared-state API and synchronization. Both native writes and typed actions remain usable under the host's authorization policy.
Keys and resource scope Contribution/host. A document, tab or origin can be part of an application-owned key when needed. No automatic provider-incarnation prefix.
Provider disposal/replacement Adapter removes its owned contributions and subscriptions. Existing host state retains its native lifetime.
Surface closure/disconnection Surface removes its listeners and owned connection. A displayed last value is stale; a new client reads the current native snapshot.
Native host restart Native host decides retention. The counter fixture is in-memory and a fresh host starts at its initial value.
Validation, persistence, migration and edit conflicts Contribution/host policy using native facilities. The SDK adds no mandatory revision, storage, single-writer or replication protocol.
HMR and extension reload Actual host lifecycle determines which owners survive. Verify each supported integration; a retained asset build alone promises no state restoration.

The example uses sharedState.get, state.value(), state.on('updated', ...) and native mutation directly. TypeScript state types are not runtime validation, and choosing actions in a UI does not prohibit native client writes. Authoritative permissions and provider catalogs stay in their owning APIs. Cross-provider synchronization requires explicit contribution logic; broadcast does not merge or replicate state.

A backend connection owns its transport, state mirrors and associated mounts. Losing that connection leaves independently authenticated connections to other providers usable in the same surface. A disconnected attachment that remains registered produces its own rejected broadcast outcome alongside successful recipients. Explicit surface disconnect, closure or replacement disposes all connections that surface owns. These events do not add automatic reconnect or replay.

The optional server persistence example supplies native createStorage under the existing state key before installing its service. Both native hosts restore observed disk writes under a fresh provider incarnation. Native debounced writes and diagnostics remain unchanged; action completion does not confirm persistence. The example owns saved-data validation, and the caller owns the file.

The opt-in extension example restores one counter record through native storage.local. Its native RPC resolver waits for background initialization, and native shared-state changes trigger asynchronous writes. Actual Chromium tests cover restoration after a confirmed write and forced worker termination, visible quota failure without rollback, and invalid saved data without overwrite. Firefox verifies explicit native extension reload closes the old pages and fresh pages restore the confirmed saved value under a new provider incarnation. The default example remains ephemeral.

The state contract tracks remaining host integration evidence. Native receipts now cover confirmed-write restoration after Chromium browser restart, Firefox restart with explicit temporary fixture loading, and natural Chromium/Firefox background idle. Fresh clients perform recovery explicitly; these checks do not establish automatic reconnect or replay. Interrupted writes, crashes and broader native failure cases remain open. Those gaps do not justify a generic recovery or enforcement layer.

Representative shared and native declarations

The following pseudocode follows the maintained core package. The accepted view recipe is described above; its browser integration proof remains domain-ticket work. titleInputSchema and titleResultSchema are application-owned Standard Schema validators; titleResultSchema accepts { title: string }.

const pageCapability = defineCapability({
  id: 'example.page',
  version: 1,
  operations: {
    readTitle: defineOperation({
      input: titleInputSchema,
      output: titleResultSchema,
    }),
  },
});

const readTitleAction = defineActionContract({
  id: 'example.inspector.read-title',
  version: 1,
  operation: pageCapability.operations.readTitle,
});

const readTitleContribution = defineAction({
  contract: readTitleAction,
  id: 'example.inspector.read-title',
  execution: providerExecution,
  requires: { page: pageCapability },
  handler({ input, services, signal }) {
    return services.page.api.readTitle(input, { signal });
  },
});

const inspectorPlugin = definePlugin({
  id: 'example.inspector',
  actions: [readTitleContribution],
  views: [inspectorView],
});

The action handler is packaged for provider execution and does not run in the renderer. inspectorView is a view contribution recipe that publishes a native JSON spec referencing the action's public descriptor; the spec does not import that handler. A UI-free plugin can omit views. Direct capability calls need no action wrapper.

const browserPageService = defineService({
  capability: pageCapability,
  id: 'example.browser-page-service',
  execution: extensionBackgroundExecution,
  setup(context) {
    const browserContext = context.native.get(browserBackgroundContext);
    if (!browserContext) throw new Error('Browser background context unavailable');

    const resource = createBrowserPageImplementation(browserContext);
    context.scope.onDispose(() => resource.dispose());
    return resource.operations;
  },
});

const admission = await provider.services.install(browserPageService);
if (admission.status === 'admitted') {
  const current = admission.handle.snapshot();
  showInstallationStatus(current);
}

createBrowserPageImplementation is application/adapter code returning operations satisfying pageCapability and an asynchronous cleanup function. It resolves/authorizes target references through the browser adapter before native work. This is not a claim that a browser API accepts the generic target reference directly. A server implementation supplies a different recipe for the same contract and receives its actual native server contexts.

Transforms and scripts use the native setup recipes described above. Registration runs in the declared execution; packaged page code receives only its actual page/world APIs. A transform-only plugin owns native registration through its activation scope, while its native host or capability owns the actual transformation. Exact response-transform operations and remaining script-stage guarantees belong to Injection and transform contract.

A custom kind uses defineContributionKind({ id, schema }), defineExtension({ descriptor: kind, id, execution, payload }) and plugin.extensions. The host explicitly supplies a ContributionKindInstaller for that descriptor. Its activate receives the validated definition and owned local setup scope. An unknown kind is an admission error; no arbitrary plugin key is treated as an extension. The renderer needs no change merely because another non-UI kind exists.

Local native contexts

A binding exposes api and context. The context includes provider and realm identity plus execution identity, with access: 'local' | 'remote'. Local contexts expose native.get(importedDescriptor), yielding the declared native type or undefined. Remote contexts have no native member. Calls receive their target separately in invocation context.

A realm label does not grant a native API. A background context, content-script context and popup context have different resources. A Vite server context may expose Devframe, hub and kit values simultaneously; optional resources remain optional. The client can also use its own locally owned browser context without confusing it with a remote provider's context.

Native descriptors are explicit imports. The runtime stores actual native values locally and transports only validated metadata. Definition modules and client-safe contracts cannot import provider handlers, Node libraries or renderer frameworks. Bundle checks must prove this separation.

Activation, retry and replacement

stateDiagram-v2
  [*] --> Waiting: admitted
  Waiting --> Starting: enabled and requirements available
  Starting --> Active: setup completes
  Starting --> Stopping: setup fails or cancellation
  Active --> Stopping: dependency loss, disable or replacement
  Stopping --> Waiting: clean after dependency loss
  Stopping --> Failed: clean after setup failure
  Stopping --> Disabled: clean after disable
  Stopping --> Disposed: clean after disposal
  Stopping --> CleanupBlocked: cleanup fails or cannot complete
  CleanupBlocked --> Disposed: verified cleanup or execution reset
  Disabled --> Waiting: enable
  Failed --> Waiting: explicit retry
Loading

Readiness loss and setup failure are different. Compatible capability restoration can reactivate a waiting contribution. A setup exception stays failed until explicit retry; repeated availability notifications must not create a retry loop. A per-call permission/target failure does not automatically dispose a provider-wide service.

API/hook Required behavior
install(definition) Preflight/reserve, then complete the initial activation pass. Return admitted handle or relaxed skipped result. Waiting dependencies do not hang installation indefinitely. Strict admission errors reject.
handle.snapshot() Return current installation/child states and diagnostics; admitted is not a claim that every child is active.
handle.subscribe(listener) Deliver current snapshot on subscription and subsequent changes; return an idempotent unsubscribe. Listener failure is diagnosed without breaking runtime ownership.
handle.enable() Clear explicit disable and reconcile availability; return the settled current snapshot.
handle.disable() Stop new work, request cancellation and await owned teardown; keep declarations registered but inactive.
handle.retry(contributionId) Retry that failed setup explicitly if enabled; missing requirements leave it waiting. It cannot bypass cleanup-blocked status.
handle.dispose() Terminal, asynchronous teardown. Repeated calls observe the same teardown attempt; no duplicate cleanup. Reject/report incomplete cleanup and retain blocked state.
scope.onDispose(cleanup) Register activation-owned cleanup while the scope is open. Run in reverse acquisition order, attempt every cleanup, and report all failures.
replace(handle, definition) Preflight replacement first, keeping the old installation on invalid input. Stop old work, await cleanup, then activate the admitted successor. Ordinary install never replaces.

Initial setup failures are reflected in admitted handles, logs and diagnostics so callers can observe and retry them; they do not turn a partially active plugin into a successful all-active result. Aggregate installation state is ready only when every enabled, admitted child is active and there are no failure/skipped diagnostics requiring attention. partial reports mixed active/inactive or failed/skipped outcomes; inactive reports a clean installation with no active children.

Cancellation is cooperative. Disposal must account for in-progress setup and owned work before claiming the activation ended. Fence generation-sensitive registrations so late completions cannot be published into a successor. Resources created outside the supplied ownership scope remain the author's responsibility and cannot be used as proof of successful conformance.

Cleanup failure or non-completion blocks the successor even in relaxed mode. An adapter may recover through an explicit reset/reload only when it establishes that the old execution and registrations are gone. Normal updates use the native host lifecycle; there is no generic automatic whole-provider recovery controller. Resetting a whole shared provider may affect unrelated contributions and requires a separately justified host policy. A timer is not cleanup proof.

Diagnostics and error contract

Admission, setup, call, cleanup and transport failures have stable codes and serializable diagnostics carrying provider and relevant plugin/contribution identity. Original native exceptions remain in the owning execution. Expected capability unavailability distinguishes unsupported implementation, wrong execution, missing permission, target absence/restriction/staleness, disconnect, incompatible contract and dependency unavailability.

The implementation must publish core codes for invalid definition, duplicate registration, dependency cycle, incompatible contract, setup failure, invalid input, invalid return, unavailable capability, cancellation, transport failure and cleanup failure. An adapter preserves a supported underlying diagnostic as additional local context without depending on native prototype transport. isOperationError validates the portable error shape before narrowing.

Log failures at their owning execution and retain current lifecycle diagnostic status for clients. A later UI mount can show the current failure. A generic status view must not depend on the failed service. Individual rejected calls and lifecycle diagnostics serve different consumers; neither replaces the other. The trust contract owns wire error-detail exposure.

Public API and proof obligations

Core package exports define the current core types and methods; archived declaration probes are historical evidence, not a second maintained API. The API proof matrix maps their behaviors to named fixtures/examples and host assertions. Those are implementation obligations; declaration checks alone do not satisfy runtime conformance.

The monorepo must contain runnable contribution, custom renderer, standalone Devframe host, Vite DevTools host, Chromium host and Firefox host examples. Each supported API/hook requires success, failure, unavailable, disposal and applicable navigation/permission/disconnect/recovery assertions. Unsupported combinations must prove explicit unavailability rather than disappear from coverage.

The native JSON renderer example composes upstream view publication, native state/action dispatch and the published renderer asset with a real native browser runtime on both server backends. Its custom DOM renderer replaces the reference renderer through native local registration while retaining the same contribution and JSON view. Six integration/build tests and maintained Chromium acceptance cover both renderers, action/state/error behavior, replacement, disposal and host disconnect. This adds no SDK rendering contract. The WebExtension example implements mixed-provider JSON action dispatch and owns Port/browser-surface HMR evidence. It also mounts connected servers' native view indexes with separate state and per-mount action lifetimes. Both Chromium and Firefox prove discovery, separate state/actions, removal and isolated disconnect; detached-button and fresh-connection view checks currently run in Chromium. Custom-renderer module HMR and complete host conformance remain separate work.

Domain boundaries and implementation sequence

Contract Owns remaining decisions
Server adapter contract Exact public host factories, development/preview bindings, native contexts and middleware/transport cleanup
Provider discovery and routing Routing-directive construction, callbacks/preferences/broadcast, discovery identity, dispatch races and cancellation outcomes
State scope and recovery State declarations, scope, persistence, transport reconciliation and abrupt worker recovery
Permissions and trust Authorization, wire validation/serialization, bridge trust and diagnostic exposure
Debugger and CDB contract Debugger operations, session/target ownership and supported adapters
Injection and transform contract Script modules, injection stages and HTML/HTTP transform contracts
Renderer and surface contract JSON view/state/action integration, renderer replacement and surface mount APIs
Live preview and reload contract Change matrix, build generations, cleanup/reset recovery and retained state across modes
Examples and API coverage contract Complete monorepo example layout, every domain API/hook cell and real-host test enforcement
Portable contribution proof End-to-end cross-host proof of the resolved contracts
Release and conformance contract Package publication, compatibility baselines and release gates

Implement the settled declaration/core lifecycle contract first, preserving the existing upstream boundaries. Continue each adapter/domain as its required decisions and evidence become available. Keep commits and issue deliverables separated by ticket. The owner authorized continuing after architecture settlement until another decision requires input; that overrides the original single-ticket planning-session limit without inventing answers to later human-led decisions.

Rejected alternatives

Alternative Reason
One heterogeneous built-in contributions array Dedicated properties provide clearer declaration typing and diagnostics
Separate plugin-entry wrapper Adds another identity/factory without a distinct ownership requirement
Global declaration merging as the primary extension API Imported descriptors keep contracts explicit and portable across bundles
Pointer-equality duplicate exemption or shared install leases Replaced by predictable conflict handling and one owner per registration
Implicit contract version ranges Exact numeric versions and explicit compatibility implementations are sufficient now
Automatic fallback after dispatch Can replay work or change its owner after execution may have begun
Universal operation-result wrapper Changes upstream ordinary call/return-schema semantics; use per-provider outcomes where routing needs them
Schema transformations applied implicitly Conflicts with the selected upstream guard-only behavior; authors normalize explicitly
Mandatory JSON Schema conversion everywhere Only schema-inspection integrations require export
Parallel activation after cleanup timeout Cannot establish that old registrations or native work ended

The decision history and pinned upstream evidence remain in the planning packets. Canonical semantics in this document supersede their unselected alternatives.