Plugin manifest and certification contract

The experimental @honua/sdk-js/plugin entrypoint is the versioned plugin SDK tracked by issue #392. It lets a third-party package describe its compatibility and authority boundary as inert JSON, produces a deterministic report for a specific host, and runs an application-local lifecycle plus behavioral conformance without granting the plugin ambient authority.

The SDK does not import plugin code, install packages, or maintain a global registry. A certified manifest means only that its declaration is well formed and compatible with the supplied host snapshot. Applications explicitly import factories and may run them through the application-local lifecycle described below. A separate, deterministic behavioral-conformance harness then proves a certified plugin's runtime behavior (retries, performance bounds, and bundle metadata) against committed golden reports, and a machine-readable support-status program records each plugin's lifecycle stage.

Authoring a manifest

import {
  HONUA_PLUGIN_API_VERSION,
  HONUA_PLUGIN_MANIFEST_VERSION,
  certifyHonuaPluginManifest,
  type HonuaPluginManifest,
} from "@honua/sdk-js/plugin";

const manifest = {
  manifestVersion: HONUA_PLUGIN_MANIFEST_VERSION,
  id: "com.example.cloud-tiles",
  version: "1.0.0",
  kind: "protocol",
  package: { name: "@example/honua-cloud-tiles", entrypoint: "./plugin.js" },
  compatibility: {
    pluginApi: HONUA_PLUGIN_API_VERSION,
    minimumSdk: "0.1.0-beta.0",
    maximumSdkExclusive: "0.2.0",
    environments: ["browser", "worker"],
  },
  capabilities: ["tiles"],
  requestedGrants: { networkOrigins: ["https://tiles.example.com"] },
  data: {
    cache: "memory",
    freshness: "ttl",
    authentication: "none",
    provenance: "preserved",
    mutation: "none",
    realtime: "none",
  },
  lifecycle: { initialization: "explicit", disposal: "required" },
  support: "community",
} as const satisfies HonuaPluginManifest<"protocol">;

const host = {
  pluginApi: HONUA_PLUGIN_API_VERSION,
  sdkVersion: "0.1.0-beta.0",
  environment: "browser",
  grants: { networkOrigins: ["https://tiles.example.com"] },
};

// The trust boundary accepts JSON text, never executable object values.
const report = certifyHonuaPluginManifest(JSON.stringify(manifest), JSON.stringify(host));

JSON.stringify is appropriate for trusted application-owned literals like the example above. Do not import an untrusted plugin module and stringify its exports: serialization itself can run getters or Proxy traps before the SDK is called. Read third-party manifest bytes as text (for example, from the package file or an HTTP response) and pass that text directly to the validator.

The report uses the exported HONUA_PLUGIN_CERTIFICATION_REPORT_VERSION schema version. The verifier fails closed on unsupported versions, missing required fields, unknown fields, invalid digest shapes, or malformed check/diagnostic records before it accepts any integrity receipt. The report contains no time, machine path, or random identifier, so the same manifest and host snapshot serialize identically. Its manifest and host blocks contain the complete canonical snapshots plus SHA-256 fingerprints; changing an entrypoint, capability, data semantic, requested authority, peer, grant, environment, API version, or SDK version changes the corresponding fingerprint. CI can reject when status is "rejected" and archive the deeply frozen JSON as certification evidence without permitting manifest/host swaps. The top-level sha256 covers every other report field, including both complete snapshots, their hashes, status, checks, and diagnostics. It is an integrity receipt for externally archived evidence, not a signature or proof of issuer.

Compatibility policy

Security boundary

Extension inventory

The closed HONUA_PLUGIN_KINDS and HONUA_PLUGIN_CAPABILITIES registries cover protocol, source/format, renderer, auth, geocoder/routing, analysis, style, cache, and realtime declarations. Capability names are kind-specific so a manifest cannot inflate its advertised surface with unknown strings. The manifest also records support status and cache, freshness, auth, provenance, mutation, realtime, peer, environment, and disposal semantics for inventory tools.

Application-local registry and lifecycle

HonuaPluginRegistry turns certified declarations into explicit application instances without adding plugins to root SDK workflows:

import {
  HonuaPluginRegistry,
  type HonuaPluginFactory,
  type HonuaPluginManifest,
} from "@honua/sdk-js/plugin";

declare const manifest: HonuaPluginManifest<"style">;
declare const host: unknown;

const stylePlugin: HonuaPluginFactory<"style"> = {
  manifest: JSON.stringify(manifest),
  initialize(context) {
    return {
      extension: { id: context.manifest.id, kind: "style" },
      start() {},
      stop() {},
      dispose() {},
    };
  },
};

const registry = new HonuaPluginRegistry({
  host: JSON.stringify(host),
  services: {},
});
await registry.register([stylePlugin]);
const extension = registry.get("style", manifest.id);
await registry.dispose();

Plugin modules remain external to SDK core. Merely importing the root SDK or an unrelated subpath does not import a plugin factory, initialize a registry, or pull mapping/database peers into the bundle.

Reference plugins for every kind

Every declared kind ships a minimal, external-style reference implementation so authors can copy a working shape rather than a bare interface. Each is a small factory carrying inert manifest JSON, a typed extension, and lifecycle hooks; none of them are imported by SDK core. They live beside the tests at test/fixtures/plugins/ (external-style.ts plus reference/) and are exercised end to end — certification, registration, extension call, and disposal — in test/plugin-reference-samples.test.ts.

Kind Reference plugin Capabilities What it demonstrates
protocol referenceProtocolPlugin query Bounding-box feature count over the origin-restricted network service
source-format referenceSourceFormatPlugin read Read-only lng,lat text parsing with no requested authority
renderer referenceRendererPlugin 2d Pure point-to-draw-command translation
auth referenceAuthPlugin authorize Header resolution from a scope-restricted credential service
geocoder-routing referenceGeocoderPlugin geocode Offline gazetteer lookup
analysis referenceAnalysisPlugin execute, cancel Cancellable reduction honouring an AbortSignal
cache referenceCachePlugin read, write, invalidate Persistent cache over granted scoped storage
realtime referenceRealtimePlugin subscribe Push subscription through the realtime service
style externalStylePlugin validate The original external-style sample (issue #424/#466)

Each manifest certifies against one shared host that grants exactly the authorities the samples request, so the security boundary stays honest: the auth sample only ever receives the reference.read scope identifier, the cache sample only writes through scoped storage, and the protocol sample's network calls are restricted to its declared origin.

First-party protocol dogfooding (issues #538 and #655)

The renderer kind proved the plugin seam is real for an out-of-tree module (OpenLayers, issue #566): third-party and first-party (maplibreRenderer) renderers both satisfy the same plain RendererAdapter contract (src/kernel/renderer.ts) without the kernel importing HonuaPluginRegistry at all. protocol did not yet have an equivalent story — every built-in protocol adapter was constructed by privileged code paths inside src/contract/source.ts.

ProtocolModule (src/contract/protocol-module.ts) is the minimal discovery/capability/diagnostics/disposal seam for a Source.protocol(...) escape-hatch adapter, mirroring RendererAdapter. QueryCapableProtocolModule is its atomic query extension: query-capable modules must implement both typed compile / execute hooks, while discovery-only modules implement neither. Compiler input uses a credential-free identity and deterministic query representation; runtime authority stays on the discovered handle and explicitly injected dependencies.

The first bounded built-in migrated onto the seam was PMTiles (tiles-only, so it honestly omits the query pair):

Issue #655 adds the first query-capable built-in:

Issue #823 migrates WFS 2.0 through that proven query seam:

Remaining protocol-module migration assessment

Issue #655 proves the versioned query seam; it does not turn the remainder into one mechanical migration. Future work should stay in bounded Specifica children of the adapter-extensibility epic:

Recommended child scope Remaining adapters Why it stays separate
HTTP feature query adapters OGC API Features; GeoServices feature/map/image Pagination, aggregation, edits, and capability negotiation are wider than the OData proof
Opaque/local execution adapters GeoParquet Resource-handle authority, optional DuckDB peer loading, worker lifecycle, and v1/v2 compiled artifacts must move together
RPC query adapter gRPC FeatureService Generated protobuf/connect peers and transport disposal have distinct bundle and authority constraints
Discovery/render/catalog adapters OGC Tiles/Maps/Records/Processes, WMS, WMTS, STAC These need discovery or render/search hooks rather than reusing the query executor blindly
Utility-only adapters GeoServices Geometry Service and GP Service Job/utility lifecycles are not feature-query execution and require their own module capability contract

Each child should migrate one coherent adapter family, retain the current escape-hatch identity, and carry its existing protocol conformance suite plus bundle-budget evidence. This avoids reopening the public hook shape while also avoiding a high-risk all-protocol rewrite.

Running the certification kit independently

The certification logic is also exposed as a runnable kit so a third party can validate their own plugin outside this repository. Installing the SDK provides the honua-plugin-certify bin, which reads a manifest and a host snapshot as inert JSON text, certifies one against the other, prints the deterministic report to stdout (or --out), and resolves an exit code:

# 0 = certified, 1 = rejected, 2 = usage/input error
npx honua-plugin-certify --manifest ./manifest.json --host ./host.json --pretty

The bin never resolves or executes the plugin entrypoint; it only reads the two JSON documents. The same logic is available programmatically through the certifyHonuaPluginManifest public API shown above, and the CLI core (runPluginCertificationCli) takes an injected I/O boundary so it can run in any host, not just Node. This makes the harness suitable for a plugin author's own CI: fail the build when status is "rejected", and archive the frozen report — bound to both canonical snapshots by SHA-256 — as certification evidence.

Signed reports and tamper-evident verification

Every certification report carries a top-level sha256 receipt over all its other fields (including both canonical snapshots and their fingerprints). The kit re-checks that receipt so an archived report is verifiably tamper-evident:

# 0 = verified intact, 1 = tampered, 2 = usage/input error
npx honua-plugin-certify --verify ./report.json

The same check is available programmatically as verifyHonuaPluginCertificationReport(reportText). It reads the report as inert JSON text and recomputes every stored digest — the top-level receipt plus the manifest.sha256 and host.sha256 snapshot fingerprints — from the report's own content. Any altered field, swapped snapshot, or edited diagnostic changes a recomputed digest and yields a structured REPORT_SIGNATURE_MISMATCH or REPORT_FINGERPRINT_MISMATCH diagnostic. This is a deterministic in-repo integrity proof, not a public-key signature: it proves a report is internally self-consistent and unmodified since issue, not which authority issued it. A trusted-issuer signing authority (which key, which registry) is a product decision and is deliberately out of scope here.

Host-mediated signing envelope (SDK-owned seam)

The SDK also exports HONUA_PLUGIN_CERTIFICATION_SIGNING_ENVELOPE_VERSION and the versioned HonuaPluginCertificationSigningEnvelope shape. An envelope contains the report, an opaque application-selected keyId, the fixed algorithm: "external" marker, and a signature string. Hosts sign the exact canonical payload returned by createHonuaPluginCertificationSigningPayload.

verifyHonuaPluginCertificationSigningEnvelope accepts an explicit HonuaPluginCertificationSignatureVerifier callback. It parses inert JSON, validates the envelope and embedded report, rejects non-certified or altered reports, and only then calls the callback. A missing verifier, callback error, unsupported version, malformed key identifier, or false result fails closed. The callback owns key lookup, cryptographic algorithms, issuer policy, key rotation, and distribution; the SDK stores no keys and makes no trust claim about the opaque identifier. This is an integration seam, not a governance or public-key-distribution implementation.

Support-status program

support records who backs a plugin (community, partner, or honua). The optional, machine-readable supportStatus attestation records the plugin's lifecycle stage so applications and agents can act on it without loading code:

The state vocabulary is exported as HONUA_PLUGIN_SUPPORT_STATES. The broader governance program (who may set partner/honua tiers, security-review sign-off, naming policy) is a process decision layered on top of this schema.

Behavioral conformance and golden reports

runHonuaPluginConformance(spec, host) runs a certified plugin through a deterministic behavioral suite and returns a frozen, digest-sealed HonuaPluginConformanceReport. The harness registers the plugin in an application-local registry with instrumented host services and counts integer observations only — no wall-clock time, randomness, or host path — so the report serializes identically on every run and is committed as a golden report (test/fixtures/plugins/golden/conformance-report.json) and asserted byte-for-byte in test/plugin-conformance.test.ts.

Three scenarios are covered, each an observation compared against a declared bound:

Scenario What it proves
retries The plugin recovers from injected transient host failures within its declared attempt budget (hostAttempts <= maxAttempts, recovered).
performance One operation's host-interaction cost stays within bound and is identical across runs (serviceCalls <= maxServiceCalls, deterministic).
cleanup The registry completes the probe and records exactly one successful disposal for the initialized plugin (disposeCalls == 1).
bundle-metadata The declared bundle footprint stays within budget with complete inventory metadata (minifiedBytes, gzipBytes, metadataComplete).

The report binds the certification digest it was run against, so a conformance result cannot be replayed against a different manifest or host. The reference retry-capable plugin exercised end to end lives at test/fixtures/plugins/reference/conformance.ts.

Certification governance policy

Honua certification is a portable evidence format, not a central plugin registry or certificate authority. The SDK validates inert declarations, produces digest-bound reports, and verifies a host-mediated signing envelope through an application-supplied callback. The consuming application remains the policy authority: it selects trusted issuers and keys, decides which support tier is acceptable, and may reject a schema-valid plugin for local policy reasons. With no configured verifier or matching trust record, signed evidence is untrusted and verification fails closed.

Identity and naming

Compatibility, peers, and deprecation

Security review and support tiers

Support tier and lifecycle state are independent. community means the author is the support authority and carries no Honua endorsement. partner requires a current commercial/technical relationship, a partner-controlled identity and signer, and recorded security approval by the consuming program. honua is reserved for Honua-owned code with approval from a Honua maintainer who did not author the reviewed change. Downgrading a tier or lifecycle state is permitted immediately; upgrading requires fresh evidence.

Before partner or honua approval, the reviewer records all of the following against the exact plugin version and certification digest:

  1. A certified inert manifest with reviewed package ownership, entrypoint, compatibility bounds, capabilities, peers, environment, support status, lifecycle, and complete cache/freshness/auth/provenance/mutation/realtime semantics.
  2. Least-privilege grants: exact HTTPS origins, scope identifiers rather than secrets, bounded storage, and explicit mutation/realtime authority. Reports, review records, diagnostics, and fixtures must contain no credentials, private keys, tokens, copied authorization headers, or customer payloads.
  3. Reproducible semantic and adversarial fixtures covering invalid input, structured errors, cancellation, bounded retries, deterministic cleanup, data/secret boundaries, and any declared mutation or realtime behavior.
  4. A digest-bound behavioral report showing the declared performance and bundle-metadata budgets, plus an installed-consumer/tree-shake check proving that an unused plugin adds no root runtime or optional peer.
  5. Dependency and license review, the owner and response path for security findings, the approved signer key id, and a re-review trigger. A new grant, capability, required peer, entrypoint, ownership change, or incompatible major version always triggers re-review.

Community plugins may self-attest, but applications should apply the same checklist when their risk warrants it. Certification status alone never upgrades a support tier.

Issuers, key rotation, revocation, and expiry

This policy completes the SDK-owned governance contract without creating a hosted marketplace, global key service, or implicit endorsement. A future catalog may distribute inventories and trust records, but applications still opt in to those roots explicitly.