Plugin manifest and certification contract

The experimental @honua/sdk-js/plugin entrypoint is the first bounded slice of the plugin SDK tracked by issue #392. It lets a third-party package describe its compatibility and authority boundary as inert JSON, then produces a deterministic report for a specific host.

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 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.

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.

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).
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.

Remaining governance decisions in #392

The code-buildable core of every named criterion is delivered above. What remains is process, not code: a trusted third-party signing authority and key distribution for reports, and the certification governance policy (who grants partner/honua support tiers, security-review sign-off, and naming rules).