Semantic Systems / Language / Glyphs

Cross-Language Plugin Contracts and SDK Parity

Report summary

Proposal The strongest baseline contract for a Windows desktop host that must support equivalent C and Python plugins, preserve human auditability, avoid remote schema resolution, and keep browser options open later is a text-first stack composed of JSON Schema Draft 2020-12 for all static schemas,

Status
Research archive item
Category
Semantic Systems / Language / Glyphs
Length
5,048 words
Reading time
23 minutes
Report type
evaluation

Key topics

  • Semantic Systems / Language / Glyphs
  • Semantic Systems
  • Language
  • Glyphs
  • .NET
  • C#
  • Python
  • Runtime
  • Privacy

Research provenance

Archive status
Research archive item
Content identity
sha256:a759a411d6f252b33f2471450cc501fd666c0db9b5e59c102ba11a1ec2fb7b5e

For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.

Source availability: 34 citation markers in the source export have no recoverable source links. Those markers are omitted from this reader; any supplied bibliography and ordinary links remain. Check the original sources before relying on the cited claims.

This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.

Full report

On this page

Executive recommendation

Proposal The strongest baseline contract for a Windows desktop host that must support equivalent C# and Python plugins, preserve human auditability, avoid remote schema resolution, and keep browser options open later is a text-first stack composed of JSON Schema Draft 2020-12 for all static schemas, a restricted I-JSON profile for on-wire values, RFC 8785 JCS canonicalization for hashing and signing of selected artifacts, and a custom length-prefixed UTF-8 framed message protocol over local IPC transports such as stdio, named pipes, or Unix domain sockets. The runtime contract should reserve a single explicit extension container rather than relying on permissive unknown top-level fields, and it should pair that strict shape with negotiated feature flags, stable machine error codes, durable idempotency receipts, and a cross-language conformance runner. This choice is not the most codegen-friendly option, but it is the best balance of validation rigor, inspectability, privacy reviewability, streaming support, operational simplicity, and future web portability.

Protocol synthesis Protobuf and gRPC remain the strongest alternative when raw throughput, generated DTOs, and built-in bidi streaming dominate the decision. However, gRPC’s primary model is HTTP/2 RPC, browser use typically requires gRPC-Web or a proxy layer, ProtoJSON has materially weaker evolution guarantees than protobuf binary, and .proto is a poor fit for settings UI metadata, permission declarations, localization metadata, and human-reviewed manifests. Those trade-offs matter more in a plugin ecosystem than they do in homogeneous service-to-service systems.

Proposal The contract below therefore uses this split: immutable manifests, settings schemas, operation payload schemas, and receipts are defined in JSON Schema; runtime envelopes are JSON objects framed by a transport-neutral byte prefix; canonical hashes and signatures apply only to selected artifacts and receipts, not arbitrary transient frames; and an optional future CBOR codec profile MAY be added later through negotiation, using the same logical envelope fields and deterministic encoding rules, without changing semantics.

Scope, assumptions, and specification method

Assumption This report treats the owner-supplied context as unverified requirements context and makes no claim about any existing product implementation, private package, internal manifest, local endpoint, or runtime behavior. All identifiers, schemas, message names, and examples below are synthetic proposals. No local source, service, or runtime was inspected.

Specification The normative requirement words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are used in the sense of BCP 14, as defined by RFC 2119 and clarified by RFC 8174.

Specification The core source set for this design is the authoritative standards surface most relevant to cross-language interchange: RFC 8259 JSON, RFC 7493 I-JSON, RFC 3339 timestamps, RFC 9562 UUIDs, RFC 8785 JCS, JSON Schema Draft 2020-12, OpenAPI, JSON-RPC 2.0, Protocol Buffers documentation, gRPC documentation, RFC 8949 CBOR, RFC 8927 JTD, the Unicode Standard, and selected security guidance from OWASP and NIST. JSON requires UTF-8 for JSON exchanged outside a closed ecosystem, warns that duplicate object names are unpredictable, and identifies the IEEE-754-safe integer range as the interoperable exact range. I-JSON further forbids duplicate names, requires UTF-8, tightens Unicode handling, recommends RFC 3339 timestamps, and recommends base64url for arbitrary binary data.

Protocol synthesis To satisfy the user’s “label every claim” requirement without making the report unreadable, each substantive paragraph, table note, code preface, or example below is explicitly tagged as Specification, External guidance, Protocol synthesis, Proposal, Sample, Assumption, or Unknown. Where a paragraph contains both a factual standards statement and a design recommendation, the recommendation is introduced separately.

Technology decision matrix

Proposal The table below scores five viable stacks on a five-point scale, where five is strongest for this plugin-contract problem. The scores are design judgments, not measurements. They are grounded in the cited specifications and ecosystem properties discussed after the table.

StackSchema rigorC#/Python toolingForward compatibilityStreamingCancellationBrowser path laterLocal IPC suitabilityBinary supportCanonicalizationInspectabilityOperational complexityOverall fit
JSON Schema 2020-12 + custom framed I-JSON544445525544.5
JSON Schema 2020-12 + JSON-RPC 2.0 over framed JSON443225525543.8
Protobuf + gRPC455 in binary, 2 in ProtoJSON552352234.0
Protobuf + custom framed protobuf455541553143.9
JSON Schema + CBOR over custom frames434442554243.9

Specification JSON Schema Draft 2020-12 is strong for validation and bundling. It explicitly supports embedded schemas and compound schema documents, separates format annotation from format assertion, and defines how bundled schemas retain stable $id-based references without requiring URI rewriting. That makes it particularly well suited for offline plugin packages that must not resolve schemas remotely at install time.

Specification OpenAPI is explicitly an interface description format for HTTP APIs, which is useful for future web gateways but is too HTTP-shaped to be the canonical source of truth for a host-to-plugin IPC contract. JSON-RPC 2.0 is transport-agnostic and simple, but it is fundamentally a stateless request/response protocol; notifications have no reply, batch responses may be returned in any order, and the spec has no native vocabulary for heartbeats, health watches, progress streams, flow control, receipts, capability negotiation, or durable idempotency semantics.

Specification Protocol Buffers are designed for language-neutral, extensible structured data with generated code, and protobuf binary is highly evolution-friendly. The protobuf wire format does not guarantee field order, and parsers must be able to accept fields in any order. However, ProtoJSON explicitly does not support unknown fields and has weaker wire-safety guarantees than protobuf binary, which matters if JSON is your canonical interchange rather than merely a compatibility view.

Specification gRPC provides unary, client-streaming, server-streaming, and bidirectional-streaming RPCs, supports deadlines and cancellation, and documents that cancellation does not roll back effects already produced on the server side. It also provides a standard health-check service with both unary and watch modes. Those are excellent properties for service RPC, but they come with the assumptions of HTTP/2 and service-style deployment. Browser compatibility for core gRPC is not direct; official gRPC-Web documentation and gRPC guidance show that browser use generally depends on gRPC-Web and often a proxy.

Specification CBOR is an Internet Standard designed for compactness and extensibility, explicitly including the possibility of extensibility without version negotiation. RFC 8949 also defines preferred serialization and deterministic encoding requirements, including shortest-form encoding and deterministic map-key ordering. Those are powerful traits for signed or hashed binary payloads, but CBOR is materially less inspectable for ordinary plugin authors and operators than JSON.

Specification MessagePack is an efficient binary serialization format, but its official specification is just that: a serialization format. It does not provide a first-class schema and validation story comparable to JSON Schema or protobuf.

Specification JTD exists specifically to make code generation and portable validation easier by intentionally staying within the expressive range of mainstream programming-language type systems, but it is Experimental rather than Standards Track, and it intentionally omits 64-bit integer types because interoperable JSON handling of those values is weak.

Proposal For this plugin ecosystem, the cleanest separation of concerns is:

This avoids overloading JSON-RPC with many ecosystem-specific extensions while still preserving the readability and ubiquity of JSON.

  • Schema layer: JSON Schema 2020-12, bundled into package-local compound documents.
  • Wire value profile: I-JSON restrictions plus explicit project rules for decimals, enums, nullability, and bytes.
  • Canonicalization layer: JCS for signed manifests and receipts only.
  • Transport layer: length-prefixed frames over stdio, named pipes, Unix sockets, or WebSocket later.
  • Invocation layer: custom envelope semantics, not JSON-RPC.

Proposal The most important anti-drift rule is that schema, transport, and invocation are separate contracts. A serializer change must not imply a semantic change; an invocation feature flag must not mutate the meaning of validated payload fields; and a transport swap from stdio to named pipes or WebSocket must not change envelope semantics.

Identity and version model

Proposal The contract needs multiple independent version axes, because SemVer works only when the “public API” is singular and behaviorally clear. A plugin ecosystem has at least eight materially different compatibility surfaces:

FieldMeaningMutabilityFormatCompatibility rule
pluginIdStable security identifier for the logical pluginImmutablereverse-DNS ASCII stringNever reused
publisherIdStable identifier for publisher trust domainImmutablereverse-DNS ASCII stringNever reused
packageVersionVersion of distributable artifactMutableSemVerDistribution semantics only
manifestSchemaVersionVersion of manifest schema dialect/profileSlow-movingdate or major.minorHost validates manifest against supported versions
protocolVersionRuntime wire/invocation versionNegotiatedmajor.minorMajor break requires negotiation failure or downgrade
capabilityVersionVersion of each declared capability contractMutableSemVerCapability-level compatibility
settingsSchemaVersionVersion of settings schemaMutableSemVer plus content digestMigration may be required
operationVersionVersion of each operation payload/result contractMutablemajor.minorDetermined per capability/operation

Specification Semantic Versioning requires a declared public API and defines major/minor/patch semantics around incompatible changes, compatible additions, and backward-compatible fixes. That is useful, but insufficient by itself for distributed contracts where the same package contains multiple schemas, capability contracts, and runtime behavior planes.

Protocol synthesis SemVer is especially insufficient for: behavioral changes that leave shape unchanged, default-value changes, privacy-policy changes, new enum values in otherwise “compatible” shapes, capability-renaming, shifting timeout behavior, and changes in idempotency scope. Those cases require separate compatibility statements, feature negotiation, or explicit operation-version increments even if package and protocol versions do not major-bump.

Proposal Use the following identifier formats:

  • pluginId: com.example.image.catalog
  • publisherId: org.example
  • packageVersion: SemVer string
  • protocolVersion: 1.0, 1.1, 2.0
  • manifestSchemaVersion: URI or profile token such as urn:example:plugin-manifest:1
  • settingsSchemaVersion: SemVer plus digest tie-down
  • messageId, operationId, receiptId: RFC 9562 UUIDs, preferably UUIDv7 for lexical sortability and operational debugging if both SDKs have solid support. UUIDv7 is time-ordered and RFC 9562 explicitly recommends it over UUIDv1 and UUIDv6 where possible.

Proposal Handshake negotiation MUST follow this model:

  1. The initiator sends supported protocol.min and protocol.max, required feature tokens, optional feature tokens, maximum frame size, and supported codecs.
  2. The responder returns the chosen protocol version, enabled features, selected codec, limits, and the running plugin’s manifest digest.
  3. If no common major version exists, the responder returns unsupported_protocol.
  4. If a required feature is missing, the responder returns missing_required_feature.
  5. If a lower compatible version exists, the responder SHOULD include recommendedProtocolVersion in the error to enable downgrade.

This model supports rolling compatibility because a host and plugin only need one common protocol version and a satisfiable required-feature set to interoperate.

Proposal Required versus optional features MUST be represented separately:

{
  "requires": ["flow.credit.v1", "cancel.v1"],
  "supports": ["codec.cbor.v1", "receipt.signature.v1", "health.watch.v1"]
}

A peer MUST reject missing requires, MAY ignore unrecognized supports, and MUST NOT silently enable an unrecognized required feature.

Proposal Deprecation, sunset, downgrade, and incompatibility MUST be explicit machine data, not prose comments. Every deprecated field, feature, capability, or operation SHOULD carry: deprecatedSince, sunsetAfter, replacement, and behaviorAfterSunset. Incompatible-version errors SHOULD return a stable machine code, the peer’s supported version range, and a safe human message. Downgrade is valid only if the downgraded version preserves the declared capability and permission semantics.

Normative manifest draft

Proposal The manifest is the signed immutable identity and capability declaration. It MUST NOT contain resolved secret values, user-entered private prompts, private files, screenshots, or logs. Mutable installation choices belong in separate local configuration. This split is the only way to keep signatures stable while still allowing host-local policy, permission grants, secret binding, and enablement state.

Proposal The following fields belong in the signed immutable manifest:

  • logical identity and publisher;
  • package and contract version surfaces;
  • supported entry points by language/runtime;
  • capabilities and requested permissions;
  • settings schema reference and digest;
  • platform/runtime constraints;
  • declared network destinations and data classes;
  • lifecycle/health requirements;
  • hashes, signatures, and provenance references.

Proposal The following fields belong in mutable local configuration instead:

  • enabled/disabled state;
  • granted-permission subset;
  • environment-specific resource limits;
  • secret references bound by the host;
  • install path;
  • host-local overrides such as trust policy or proxy settings.

Proposal Entry points for .NET and Python SHOULD differ only inside a typed entryPoints object so the common security and lifecycle model stays unified:

"entryPoints": {
  "dotnet": {
    "assembly": "plugins/Example.Plugin.dll",
    "type": "Example.Plugin.EntryPoint, Example.Plugin",
    "targetFramework": "net8.0"
  },
  "python": {
    "module": "example_plugin.main",
    "callable": "create_plugin",
    "pythonVersion": "3.12"
  }
}

Proposal Localization metadata MUST remain separate from security identifiers. Authorization, trust, receipts, and logs MUST use pluginId, publisherId, capability.id, and permission.id, never localized display strings. Human-facing names belong in display or localization sections only.

Proposal Schema references MUST avoid remote dereferencing at install time. JSON Schema compound documents and $id-based bundling are the right fit: references stay as stable URIs, while package tooling embeds all referenced schema resources into package-local compound documents that preserve canonical identifiers.

Sample Normative manifest schema draft:

{
  "$id": "urn:example:schemas:plugin-manifest:1",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Generic Plugin Manifest",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "manifestSchemaVersion",
    "plugin",
    "publisher",
    "packageVersion",
    "protocol",
    "entryPoints",
    "capabilities",
    "permissions",
    "runtime",
    "provenance"
  ],
  "properties": {
    "manifestSchemaVersion": {
      "type": "string",
      "const": "urn:example:plugin-manifest:1"
    },
    "plugin": {
      "type": "object",
      "additionalProperties": false,
      "required": ["pluginId", "displayName"],
      "properties": {
        "pluginId": {
          "type": "string",
          "pattern": "^[a-z0-9]+(\\.[a-z0-9-]+)+$"
        },
        "displayName": { "type": "string", "minLength": 1, "maxLength": 128 },
        "description": { "type": "string", "maxLength": 4096 },
        "categories": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9._-]{1,63}$"
          },
          "uniqueItems": true,
          "maxItems": 32
        },
        "localization": {
          "type": "object",
          "propertyNames": {
            "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$"
          },
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "displayName": { "type": "string", "maxLength": 128 },
              "description": { "type": "string", "maxLength": 4096 }
            }
          }
        }
      }
    },
    "publisher": {
      "type": "object",
      "additionalProperties": false,
      "required": ["publisherId", "displayName"],
      "properties": {
        "publisherId": {
          "type": "string",
          "pattern": "^[a-z0-9]+(\\.[a-z0-9-]+)+$"
        },
        "displayName": { "type": "string", "minLength": 1, "maxLength": 128 },
        "website": { "type": "string", "format": "uri" }
      }
    },
    "packageVersion": {
      "type": "string",
      "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
    },
    "protocol": {
      "type": "object",
      "additionalProperties": false,
      "required": ["minVersion", "maxVersion", "features"],
      "properties": {
        "minVersion": { "type": "string", "pattern": "^\\d+\\.\\d+$" },
        "maxVersion": { "type": "string", "pattern": "^\\d+\\.\\d+$" },
        "features": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9._-]{1,63}$"
          },
          "uniqueItems": true
        }
      }
    },
    "entryPoints": {
      "type": "object",
      "additionalProperties": false,
      "minProperties": 1,
      "properties": {
        "dotnet": {
          "type": "object",
          "additionalProperties": false,
          "required": ["assembly", "type", "targetFramework"],
          "properties": {
            "assembly": { "type": "string", "minLength": 1 },
            "type": { "type": "string", "minLength": 1 },
            "targetFramework": { "type": "string", "minLength": 1 }
          }
        },
        "python": {
          "type": "object",
          "additionalProperties": false,
          "required": ["module", "callable", "pythonVersion"],
          "properties": {
            "module": { "type": "string", "minLength": 1 },
            "callable": { "type": "string", "minLength": 1 },
            "pythonVersion": { "type": "string", "pattern": "^\\d+\\.\\d+$" }
          }
        }
      }
    },
    "capabilities": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "version", "operations"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9._-]{1,63}$"
          },
          "version": {
            "type": "string",
            "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
          },
          "operations": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": ["name", "operationVersion", "requestSchema", "responseSchema", "streaming", "idempotent"],
              "properties": {
                "name": {
                  "type": "string",
                  "pattern": "^[a-z][a-z0-9._-]{1,63}$"
                },
                "operationVersion": { "type": "string", "pattern": "^\\d+\\.\\d+$" },
                "requestSchema": { "$ref": "#/$defs/schemaRef" },
                "responseSchema": { "$ref": "#/$defs/schemaRef" },
                "streaming": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": ["request", "response"],
                  "properties": {
                    "request": { "type": "boolean" },
                    "response": { "type": "boolean" }
                  }
                },
                "idempotent": { "type": "boolean" },
                "receipts": { "type": "boolean", "default": true }
              }
            }
          }
        }
      }
    },
    "permissions": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "required", "scope"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9._-]{1,63}$"
          },
          "required": { "type": "boolean" },
          "scope": {
            "type": "string",
            "enum": [
              "settings.read",
              "settings.write",
              "filesystem.read",
              "filesystem.write",
              "network.connect",
              "clipboard.read",
              "clipboard.write",
              "ui.contribute",
              "background.run",
              "bounded.private-data"
            ]
          },
          "justification": { "type": "string", "maxLength": 1024 }
        }
      }
    },
    "settings": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "schema": { "$ref": "#/$defs/schemaRef" }
      }
    },
    "runtime": {
      "type": "object",
      "additionalProperties": false,
      "required": ["os", "architectures"],
      "properties": {
        "os": {
          "type": "array",
          "items": { "type": "string", "enum": ["windows", "linux", "macos"] },
          "uniqueItems": true
        },
        "architectures": {
          "type": "array",
          "items": { "type": "string", "enum": ["x64", "arm64"] },
          "uniqueItems": true
        },
        "minHostVersion": { "type": "string", "pattern": "^\\d+\\.\\d+$" }
      }
    },
    "network": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "destinations": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["scheme", "hostPattern"],
            "properties": {
              "scheme": { "type": "string", "enum": ["https", "wss"] },
              "hostPattern": { "type": "string", "minLength": 1 },
              "port": { "type": "integer", "minimum": 1, "maximum": 65535 }
            }
          }
        }
      }
    },
    "dataDeclarations": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "acceptedClasses": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": ["public", "internal", "sensitive", "secret", "regulated"]
          },
          "uniqueItems": true
        },
        "defaultInboundClass": {
          "type": "string",
          "enum": ["public", "internal", "sensitive", "secret", "regulated"]
        },
        "defaultReceiptClass": {
          "type": "string",
          "enum": ["public", "internal", "sensitive"]
        }
      }
    },
    "lifecycle": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "supportsHealthWatch": { "type": "boolean" },
        "requiresHeartbeat": { "type": "boolean" },
        "backgroundBehavior": {
          "type": "string",
          "enum": ["none", "on-demand", "scheduled", "continuous"]
        }
      }
    },
    "provenance": {
      "type": "object",
      "additionalProperties": false,
      "required": ["manifestDigest"],
      "properties": {
        "manifestDigest": {
          "type": "string",
          "pattern": "^sha256:[A-Fa-f0-9]{64}$"
        },
        "signatureRefs": {
          "type": "array",
          "items": { "type": "string", "format": "uri" }
        },
        "sbomRef": { "type": "string", "format": "uri" },
        "provenanceRef": { "type": "string", "format": "uri" }
      }
    }
  },
  "$defs": {
    "schemaRef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["uri", "digest"],
      "properties": {
        "uri": { "type": "string", "format": "uri" },
        "digest": {
          "type": "string",
          "pattern": "^sha256:[A-Fa-f0-9]{64}$"
        }
      }
    }
  }
}

Specification Bundled JSON Schema resources SHOULD preserve stable $id references, and JSON Schema 2020-12 recommends compound documents with embedded resources identified by their own $id.

Sample Valid synthetic manifest example:

{
  "manifestSchemaVersion": "urn:example:plugin-manifest:1",
  "plugin": {
    "pluginId": "com.example.catalog.indexer",
    "displayName": "Catalog Indexer",
    "description": "Indexes public catalog metadata from bounded local inputs.",
    "categories": ["catalog", "indexing"]
  },
  "publisher": {
    "publisherId": "org.example",
    "displayName": "Example Org"
  },
  "packageVersion": "1.4.2",
  "protocol": {
    "minVersion": "1.0",
    "maxVersion": "1.2",
    "features": ["cancel.v1", "progress.v1", "flow.credit.v1", "receipt.v1"]
  },
  "entryPoints": {
    "dotnet": {
      "assembly": "plugins/CatalogIndexer.dll",
      "type": "Example.CatalogIndexer.EntryPoint, Example.CatalogIndexer",
      "targetFramework": "net8.0"
    },
    "python": {
      "module": "catalog_indexer.main",
      "callable": "create_plugin",
      "pythonVersion": "3.12"
    }
  },
  "capabilities": [
    {
      "id": "catalog.index",
      "version": "1.0.0",
      "operations": [
        {
          "name": "start",
          "operationVersion": "1.0",
          "requestSchema": {
            "uri": "urn:example:schemas:catalog-index-start-request:1",
            "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
          },
          "responseSchema": {
            "uri": "urn:example:schemas:catalog-index-start-response:1",
            "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222"
          },
          "streaming": { "request": false, "response": true },
          "idempotent": true,
          "receipts": true
        }
      ]
    }
  ],
  "permissions": [
    {
      "id": "perm-files-read",
      "required": true,
      "scope": "filesystem.read",
      "justification": "Reads bounded local input files selected by the host."
    }
  ],
  "settings": {
    "schema": {
      "uri": "urn:example:schemas:catalog-indexer-settings:2",
      "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333"
    }
  },
  "runtime": {
    "os": ["windows"],
    "architectures": ["x64", "arm64"],
    "minHostVersion": "1.2"
  },
  "network": {
    "destinations": []
  },
  "dataDeclarations": {
    "acceptedClasses": ["public", "internal"],
    "defaultInboundClass": "internal",
    "defaultReceiptClass": "internal"
  },
  "lifecycle": {
    "supportsHealthWatch": true,
    "requiresHeartbeat": true,
    "backgroundBehavior": "on-demand"
  },
  "provenance": {
    "manifestDigest": "sha256:4444444444444444444444444444444444444444444444444444444444444444",
    "sbomRef": "urn:example:spdx:catalog-indexer:1",
    "provenanceRef": "urn:example:slsa:catalog-indexer:1"
  }
}

Sample Invalid synthetic manifest examples and why they fail:

{
  "manifestSchemaVersion": "urn:example:plugin-manifest:1",
  "plugin": {
    "pluginId": "Catalog Indexer",
    "displayName": "Catalog Indexer"
  },
  "publisher": {
    "publisherId": "org.example",
    "displayName": "Example Org"
  },
  "packageVersion": "1.0",
  "protocol": {
    "minVersion": "1",
    "maxVersion": "1.0",
    "features": []
  },
  "entryPoints": {},
  "capabilities": [],
  "permissions": [],
  "runtime": {
    "os": ["windows"],
    "architectures": ["x64"]
  },
  "provenance": {
    "manifestDigest": "sha256:not-a-real-digest"
  }
}
  • Sample plugin.pluginId is invalid because it is not reverse-DNS ASCII.
  • Sample packageVersion is invalid because it is not SemVer.
  • Sample protocol.minVersion is invalid because it lacks major.minor.
  • Sample entryPoints is invalid because at least one language entry point is required.
  • Sample capabilities is invalid because at least one capability is required.
  • Sample manifestDigest is invalid because it does not match the sha256:<64 hex> pattern.
{
  "manifestSchemaVersion": "urn:example:plugin-manifest:1",
  "plugin": {
    "pluginId": "com.example.badplugin",
    "displayName": "Bad Plugin",
    "secretApiKey": "super-secret"
  },
  "publisher": {
    "publisherId": "org.example",
    "displayName": "Example Org"
  },
  "packageVersion": "1.0.0",
  "protocol": { "minVersion": "1.0", "maxVersion": "1.0", "features": [] },
  "entryPoints": {
    "python": {
      "module": "bad.main",
      "callable": "create_plugin",
      "pythonVersion": "3.12"
    }
  },
  "capabilities": [],
  "permissions": [],
  "runtime": { "os": ["windows"], "architectures": ["x64"] },
  "provenance": { "manifestDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }
}
  • Sample plugin.secretApiKey is invalid because the schema disallows undeclared properties.
  • Sample putting a secret in the manifest violates the privacy model even if a looser schema allowed it.

External guidance SPDX 3.0.1 is an open standard for BOM data, and Sigstore bundles are designed to contain the verification material required to verify a signed artifact. Using URN or package-local references to those artifacts is therefore a sound provenance strategy without embedding large signatures in the manifest body itself.

Wire protocol and state machines

Proposal The runtime protocol MUST be defined independently of any single IPC substrate. The baseline frame format is:

+----------------------+-----------------------------+
| uint32_be length N   | N bytes UTF-8 JSON payload  |
+----------------------+-----------------------------+

Maximum N MUST be negotiated during handshake and MUST default to a conservative limit such as 8 MiB. Receivers MUST reject negative, truncated, or oversized frames with frame.invalid or frame.too_large. Senders MUST encode payload JSON in UTF-8.

Proposal Every frame payload MUST conform to this abstract base envelope:

{
  "protocolVersion": "1.1",
  "messageType": "hello",
  "messageId": "0197f9d2-6f31-7cb1-a612-5f5b7f7b8d19",
  "sequence": 1,
  "timestampUtc": "2026-07-11T16:20:00Z",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "causationId": null,
  "extensions": {}
}

Proposal messageId MUST be unique within a connection and SHOULD remain globally unique. sequence MUST be strictly increasing per sender per connection, not per operation. timestampUtc MUST be RFC 3339 UTC with uppercase Z. Large exact integers and decimals that cannot be safely represented in I-JSON MUST be encoded as strings. Arbitrary bytes MUST be base64url strings when inline transport is unavoidable. Duplicate property names are forbidden.

Proposal The protocol MUST define these message families:

FamilyPurposeTerminalNotes
hello / helloAckversion, limits, feature negotiationnorequired
init / initAckruntime init with local config and grantsnorequired
requeststart or continue an operationnorequired
progressbounded progress updatenooptional
streamItemone streamed item or chunknooptional
streamEndlogical end of response streamyesterminal for stream body
responsefinal successful non-streaming result or stream traileryesterminal
errorterminal failureyesterminal
cancel / cancelAckrequest cancellation / acknowledge receiptnorequired when cancel negotiated
cancelledterminal cancellation outcomeyesterminal
flowupdate stream creditsnorequired when streaming negotiated
heartbeatlivenessnorequired when heartbeat negotiated
healthQuery / healthReportpull healthnorequired
healthWatch / healthEventpush health modenooptional
eventnon-requested informational eventnooptional
goAwayconnection draining and shutdown intentnorequired

Proposal The request envelope MUST distinguish identifiers with different semantics:

FieldSemantics
messageIdunique frame identity
requestIdcorrelates frames that belong to one attempt on one connection
operationIdstable logical operation identity across retries or resume
idempotencyKeystable dedupe key for effectful operations
attemptcaller attempt counter starting at 1
deadlineUtcabsolute caller deadline
traceIddistributed trace identity
causationIdimmediate parent message/event
sequencetransport ordering per sender

Proposal These fields MUST NOT be conflated. requestId answers “which in-flight call is this frame part of?”; operationId answers “which logical operation across retries is this?”; and idempotencyKey answers “which effect should be deduplicated?” Conflating them is a common source of replay bugs, duplicate effects, and unusable diagnostics.

Proposal A request SHOULD look like this:

{
  "protocolVersion": "1.1",
  "messageType": "request",
  "messageId": "0197f9d2-6f31-7cb1-a612-5f5b7f7b8d19",
  "sequence": 22,
  "timestampUtc": "2026-07-11T16:21:00Z",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "causationId": null,
  "request": {
    "requestId": "0197f9d2-6f32-7ef4-9ad8-6a0d2dabe3ae",
    "operationId": "0197f9d2-6f33-75f7-bf1d-1c8451c879b8",
    "idempotencyKey": "8d7a53b4-5c9f-40c1-b9dc-56e8e9b6a7cf",
    "attempt": 1,
    "deadlineUtc": "2026-07-11T16:22:00Z",
    "capabilityId": "catalog.index",
    "operationName": "start",
    "operationVersion": "1.0",
    "permissionsGrantId": "grant-7ba86bff",
    "payloadClass": "internal",
    "payload": {
      "inputUris": ["hostlocal://selection/42"],
      "mode": "incremental"
    },
    "stream": {
      "response": true,
      "initialCreditItems": 16,
      "initialCreditBytes": 262144
    }
  },
  "extensions": {}
}

Proposal Delivery semantics MUST be stated honestly:

  • Over a live local IPC connection, frames are in-order and either accepted or lost with the connection.
  • Request execution is at-most-once per accepted frame on a given live connection.
  • Across process crash, reconnect, or caller retry, the protocol can provide only at-least-once submission unless the operation is idempotent and deduplicated.
  • Exactly-once effects are approximated through idempotencyKey plus durable receipts, not promised by transport.

This is both more accurate and safer than claiming exactly-once across crashes. gRPC’s own documentation makes the same broad conceptual point: cancellation and local completion decisions can diverge between peers, and cancellation does not roll back prior effects.

Proposal Unknown fields, defaults, numbers, timestamps, enums, nullability, maps, bytes, and Unicode MUST follow these cross-language rules:

TopicRule
Unknown core fieldsreject
Unknown extension namespaces under extensionsignore unless required by negotiation
Defaultsomitted means absent; defaults are local API conveniences, not wire semantics
NumbersJSON numeric fields MUST stay inside interoperable range; larger integers and exact decimals use strings
TimestampsRFC 3339 UTC strings only
Enumsuse lowercase strings, not integers
Nullabilitynull only where explicitly allowed; absence is preferred for “not provided”
Mapsstring-keyed only
Bytesbase64url strings or host-local blob handles
Unicodereject unpaired surrogates and duplicate keys

Specification JSON and I-JSON justify these choices directly: duplicate names are unpredictable, UTF-8 is required, I-JSON forbids duplicate names and surrogate/noncharacter code points, RFC 3339 is recommended for timestamps, and exact values outside the IEEE-754-safe range should be encoded as strings.

Proposal Hashing and signing MUST canonicalize only well-defined artifacts. For JSON artifacts, canonicalization MUST use JCS. For any future CBOR profile, deterministic encoding MUST be used and the protocol must specify all numeric and tag choices explicitly. Hashing arbitrary, transient, non-canonical runtime frames SHOULD be avoided.

Proposal Backpressure MUST be explicit. Streaming senders MUST obey byte and item credits. When credits reach zero, a sender MUST pause streamItem emission except for one already-buffered item. Receivers MAY replenish credits with flow frames. Exceeding credit is a protocol violation and MAY cause flow_control_violation cancellation. This keeps host memory bounded and gives both C# async streams and Python async iterators equivalent semantics.

Proposal Cancellation MUST be cooperative and race-aware:

  1. cancel requests that work stop as soon as practical.
  2. cancelAck means the peer received the cancellation request, not that the operation is already cancelled.
  3. The terminal outcome MAY still be response, error, or cancelled, depending on timing.
  4. The first valid terminal frame for an operation wins.
  5. Later frames for the same request MUST be ignored and SHOULD be logged as late protocol noise.

This mirrors real cancellation behavior in .NET and Python: cancellation is cooperative, not preemptive. In Python asyncio, cancellation raises CancelledError at the next opportunity and cleanup should generally propagate the cancellation after cleanup. In .NET, cancellation tokens are a cooperative model in which cancellation is requested by the source and observed by listeners.

Proposal Connection, lifecycle, operation, cancellation, and shutdown state machines:

Connection State Machine
------------------------
DISCONNECTED
  -> CONNECTING
  -> NEGOTIATING (hello / helloAck)
  -> INITIALIZING (init / initAck)
  -> READY
READY
  -> DRAINING (goAway received or sent)
  -> FAILED
  -> CLOSED
DRAINING
  -> CLOSED
FAILED
  -> CLOSED
Plugin Lifecycle State Machine
------------------------------
DISCOVERED
  -> LOADED
  -> STARTING
  -> READY
READY
  -> BUSY
BUSY
  -> READY
READY/BUSY
  -> DEGRADED
DEGRADED
  -> READY | BUSY | STOPPING
READY/BUSY/DEGRADED
  -> STOPPING
STOPPING
  -> STOPPED
STOPPED
  -> TERMINATED
Operation State Machine
-----------------------
ACCEPTED
  -> VALIDATING
  -> RUNNING
RUNNING
  -> STREAMING
RUNNING/STREAMING
  -> COMPLETED
  -> FAILED
  -> CANCELLING
CANCELLING
  -> CANCELLED
  -> COMPLETED
  -> FAILED
Any terminal state
  -> TERMINAL
Host Shutdown State Machine
---------------------------
RUNNING
  -> DRAIN_ANNOUNCED (send goAway, stop new requests)
  -> CANCEL_INFLIGHT (optional by policy after grace)
  -> FLUSH_TERMINALS
  -> CLOSE_TRANSPORT
  -> EXITED

Proposal Terminal-state rules:

  • An operation has exactly one authoritative terminal state: completed, failed, or cancelled.
  • streamEnd ends the stream body, not the operation, until followed by response or error.
  • Shutdown MUST stop accepting new requests before it cancels old ones.
  • A peer MAY send goAway with a lastAcceptedSequence so the other side can distinguish “not accepted” from “accepted but incomplete,” analogous in spirit to HTTP/2 GOAWAY boundaries.

Error, receipt, privacy, secret, and SDK parity model

Proposal Machine errors MUST be stable string codes, with safe human messages and explicit retryability. Borrowing conceptual clarity from gRPC status code families is useful, but the plugin protocol should define its own code space so it can express plugin-specific blame, privacy, and receipt semantics. gRPC’s status set is well defined and distinguishes, for example, CANCELLED, INVALID_ARGUMENT, ALREADY_EXISTS, and FAILED_PRECONDITION; those distinctions are a good pattern to emulate.

Proposal Error taxonomy:

CodeMeaningRetryableBlameSeveritySafe default message
invalid_requestmalformed envelope or schema-invalid payloadnocallererrorThe request format was invalid.
unsupported_protocolno compatible protocol versionnopeer mismatcherrorNo compatible protocol version was found.
missing_required_featurenegotiated features insufficientnopeer mismatcherrorA required protocol feature is unavailable.
unsupported_capabilitycapability or operation not declarednocallererrorThe requested capability is not supported.
permission_deniedpermission missing or grant insufficientnocaller/host policywarningPermission was not granted.
payload_class_deniedpayload classification exceeds authorizationnocaller/host policywarningThe payload class is not authorized.
deadline_exceededdeadline reachedmaybeenvironmentwarningThe operation exceeded its deadline.
conflictidempotency or state conflictmaybecaller/environmentwarningThe operation conflicts with current state.
rate_limitedlocal throttling or policy rate limityesenvironmentwarningThe operation was rate limited.
transient_failurerecoverable dependency or IPC failureyesenvironment/pluginerrorA transient failure occurred.
plugin_faultuncaught plugin exception or invariant breakmaybepluginerrorThe plugin encountered an internal fault.
host_shutdownhost is draining or shutting downyeshostinfoThe host is shutting down.
flow_control_violationsender exceeded stream creditnosendererrorStream flow-control limits were violated.
frame.too_largeframe exceeds negotiated sizenosendererrorThe message exceeded transport limits.

Proposal Every error frame MUST include: code, message, retryable, blameDomain, severity, requestId, operationId, and MAY include details only if those details are privacy-safe under the payload classification.

Proposal Receipt taxonomy MUST distinguish execution state from external effect state. A useful minimal receipt model is:

FieldPurpose
receiptIdunique durable receipt identifier
pluginId / publisherIdtrust binding
manifestDigestcode identity tie-down
capabilityId / operationName / operationVersioncontract identity
operationId / idempotencyKey / attemptdedupe and retry trace
requestDigestcanonical digest of redacted request
resultDigestoptional digest of redacted result
effectStatenone, started, partial, completed, rejected, compensated, unknown
terminalStatecompleted, failed, cancelled
issuedAtUtc / expiresAtUtctime bounds
privacyClassdefault minimum handling
retentionnone, ephemeral, short, audit
signatureRefoptional verification material reference

Proposal Exactly-once effect is approximated as follows:

  • caller supplies idempotencyKey;
  • plugin persists a durable receipt keyed by (pluginId, capabilityId, operationName, idempotencyKey);
  • retries with the same dedupe tuple return the existing receipt and prior terminal result if safe;
  • if the earlier attempt is still in progress, return conflict with existingOperationId; and
  • if a prior attempt ended with unknown effect disposition, return the prior receipt rather than rerunning automatically.

This is the most honest portable approximation of exactly-once available over crash-prone IPC.

Proposal Settings UI MUST be declarative. Use JSON Schema plus a narrow annotation vocabulary such as x-ui:widget, x-ui:group, x-ui:order, x-ui:placeholder, x-ui:multiline, and x-secretRefAllowed. Plugins MUST NOT ship arbitrary settings UI code if parity and reviewability matter. JSON Schema is expressly designed to support documentation and interaction control in addition to validation, which makes this use natural.

Proposal Secret values MUST NOT appear in manifests or exported configuration. They SHOULD be represented by opaque host-resolved references, for example:

{
  "apiToken": {
    "secretRef": {
      "provider": "host-vault",
      "name": "example/catalog-indexer/token",
      "version": "42"
    }
  }
}

The host resolves the reference at runtime and injects only the minimum live secret material needed for the authorized operation. Exported configuration MUST preserve only the secretRef, never its resolved value.

Proposal Payload classification, redaction, retention, and receipt minimization MUST be explicit:

  • payloadClass: public, internal, sensitive, secret, regulated
  • diagnosticPolicy: safe-default, safe-plus-hashes, explicit-debug
  • retention: none, ephemeral, short, audit
  • redaction: field-level directives in schema, such as x-redact: full, x-redact: hash, or x-redact: prefix

If no explicit bounded-data capability is granted, requests MUST NOT include credentials, private prompts, generated model text, private files, screenshots, or private logs. Receipts SHOULD default to digests, counts, sizes, and bounded machine messages only.

External guidance OWASP’s logging guidance is directly aligned with this minimization approach: logging should be deliberate, security-relevant, and structured, while sensitive data handling must be controlled. NIST’s secure software guidance likewise emphasizes secure development practices rather than accidental leakage through diagnostics.

Proposal Safe diagnostic fields by default are: code, message, retryable, blameDomain, severity, requestId, operationId, attempt, timing metrics, item counts, byte counts, request and result digests, manifest digest, and negotiated protocol/feature versions. Raw payload fragments, stack traces, environment variables, file contents, prompts, generated model text, screenshots, and secret values MUST NOT be emitted unless an explicitly authorized diagnostic capability and retention policy allow them.

Proposal C# and Python SDK parity rules:

  • C# Task<T> maps to Python await-returning coroutine.
  • C# IAsyncEnumerable<T> maps to Python async iterator / async generator.
  • C# CancellationToken maps to explicit cancellation context plus task cancellation in Python.
  • C# immutable records map to frozen dataclasses or immutable validation models in Python.
  • C# typed exceptions map to Python typed exceptions with equivalent machine-code properties.
  • Both SDKs MUST expose the same logical error, receipt, and health models even if their host-language idioms differ.

Specification .NET supports immutable types and records with System.Text.Json, supports cooperative cancellation via CancellationToken, and supports async streams with IAsyncEnumerable<T>. Python asyncio uses CancelledError for cooperative task cancellation, and Python’s async syntax supports asynchronous iteration.

Sample C# SDK sketch:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace GenericPluginContract;

/// <summary>
/// Represents a validated plugin manifest.
/// </summary>
public sealed record PluginManifest
{
    [Display(Name = "manifest schema version")]
    public required string ManifestSchemaVersion { get; init; }

    [Display(Name = "plugin id")]
    public required string PluginId { get; init; }

    [Display(Name = "publisher")]
    public required string PublisherId { get; init; }

    [Display(Name = "package version")]
    public required string PackageVersion { get; init; }

    [Display(Name = "protocol min version")]
    public required string ProtocolMinVersion { get; init; }

    [Display(Name = "protocol max version")]
    public required string ProtocolMaxVersion { get; init; }
}

/// <summary>
/// Describes the execution context for a single operation.
/// </summary>
public sealed record PluginOperationContext
{
    [Display(Name = "request")]
    public required string RequestId { get; init; }

    [Display(Name = "operation")]
    public required string OperationId { get; init; }

    [Display(Name = "idempotency key")]
    public string? IdempotencyKey { get; init; }

    [Display(Name = "deadline utc")]
    public DateTimeOffset? DeadlineUtc { get; init; }

    [Display(Name = "attempt")]
    public required int Attempt { get; init; }

    [Display(Name = "trace")]
    public string? TraceId { get; init; }
}

/// <summary>
/// Represents a progress notification emitted during an operation.
/// </summary>
public sealed record ProgressUpdate
{
    [Display(Name = "message")]
    public required string Message { get; init; }

    [Display(Name = "completed")]
    public double? PercentCompleted { get; init; }

    [Display(Name = "current")]
    public long? Current { get; init; }

    [Display(Name = "total")]
    public long? Total { get; init; }
}

/// <summary>
/// Represents a machine-readable plugin error.
/// </summary>
public sealed class PluginException : Exception
{
    /// <summary>
    /// Initializes a new plugin exception.
    /// </summary>
    /// <param name="code">Stable machine error code.</param>
    /// <param name="message">Human-safe error message.</param>
    /// <param name="retryable">Indicates whether retry is safe.</param>
    public PluginException(string code, string message, bool retryable)
        : base(message)
    {
        Code = code;
        Retryable = retryable;
    }

    [Display(Name = "code")]
    public string Code { get; }

    [Display(Name = "retryable")]
    public bool Retryable { get; }
}

/// <summary>
/// Represents a durable receipt describing the operation effect.
/// </summary>
public sealed record OperationReceipt
{
    [Display(Name = "receipt")]
    public required string ReceiptId { get; init; }

    [Display(Name = "operation")]
    public required string OperationId { get; init; }

    [Display(Name = "idempotency key")]
    public string? IdempotencyKey { get; init; }

    [Display(Name = "terminal state")]
    public required string TerminalState { get; init; }

    [Display(Name = "effect state")]
    public required string EffectState { get; init; }

    [Display(Name = "issued at utc")]
    public required DateTimeOffset IssuedAtUtc { get; init; }
}

/// <summary>
/// Represents the plugin health signal.
/// </summary>
public sealed record HealthReport
{
    [Display(Name = "status")]
    public required string Status { get; init; }

    [Display(Name = "message")]
    public string? Message { get; init; }

    [Display(Name = "updated at utc")]
    public required DateTimeOffset UpdatedAtUtc { get; init; }
}

/// <summary>
/// Provides an abstraction for reporting progress.
/// </summary>
public interface IProgressSink
{
    /// <summary>
    /// Reports operation progress.
    /// </summary>
    /// <param name="update">Progress update payload.</param>
    /// <param name="cancellationToken">Cancellation for the reporting call.</param>
    ValueTask ReportAsync(ProgressUpdate update, CancellationToken cancellationToken);
}

/// <summary>
/// Defines the common plugin contract surface.
/// </summary>
public interface IPluginContract
{
    /// <summary>
    /// Gets the immutable manifest for the plugin.
    /// </summary>
    /// <param name="cancellationToken">Cancellation for the retrieval.</param>
    Task<PluginManifest> GetManifestAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Validates host-provided settings and returns a normalized view if valid.
    /// </summary>
    /// <param name="settingsJson">Raw settings JSON.</param>
    /// <param name="cancellationToken">Cancellation for validation.</param>
    Task<string> ValidateSettingsAsync(string settingsJson, CancellationToken cancellationToken);

    /// <summary>
    /// Executes a unary operation and returns the final JSON result and receipt.
    /// </summary>
    /// <param name="context">Operation execution context.</param>
    /// <param name="requestJson">Validated request JSON.</param>
    /// <param name="progress">Optional progress sink.</param>
    /// <param name="cancellationToken">Cooperative cancellation token.</param>
    Task<(string ResponseJson, OperationReceipt Receipt)> ExecuteAsync(
        PluginOperationContext context,
        string requestJson,
        IProgressSink? progress,
        CancellationToken cancellationToken);

    /// <summary>
    /// Executes a streaming operation and yields each response item.
    /// </summary>
    /// <param name="context">Operation execution context.</param>
    /// <param name="requestJson">Validated request JSON.</param>
    /// <param name="progress">Optional progress sink.</param>
    /// <param name="cancellationToken">Cooperative cancellation token.</param>
    IAsyncEnumerable<string> StreamAsync(
        PluginOperationContext context,
        string requestJson,
        IProgressSink? progress,
        [EnumeratorCancellation] CancellationToken cancellationToken);

    /// <summary>
    /// Returns the current plugin health.
    /// </summary>
    /// <param name="cancellationToken">Cancellation for the health query.</param>
    Task<HealthReport> GetHealthAsync(CancellationToken cancellationToken);
}

Sample Python SDK sketch:

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import AsyncIterator, Optional, Protocol


@dataclass(frozen=True)
class PluginManifest:
    manifest_schema_version: str
    plugin_id: str
    publisher_id: str
    package_version: str
    protocol_min_version: str
    protocol_max_version: str


@dataclass(frozen=True)
class PluginOperationContext:
    request_id: str
    operation_id: str
    idempotency_key: Optional[str]
    deadline_utc: Optional[datetime]
    attempt: int
    trace_id: Optional[str]


@dataclass(frozen=True)
class ProgressUpdate:
    message: str
    percent_completed: Optional[float] = None
    current: Optional[int] = None
    total: Optional[int] = None


@dataclass(frozen=True)
class OperationReceipt:
    receipt_id: str
    operation_id: str
    idempotency_key: Optional[str]
    terminal_state: str
    effect_state: str
    issued_at_utc: datetime


@dataclass(frozen=True)
class HealthReport:
    status: str
    updated_at_utc: datetime
    message: Optional[str] = None


class PluginError(Exception):
    def __init__(self, code: str, message: str, retryable: bool) -> None:
        super().__init__(message)
        self.code = code
        self.retryable = retryable


class ProgressSink(Protocol):
    async def report(self, update: ProgressUpdate) -> None:
        """Report operation progress."""


class PluginContract(Protocol):
    async def get_manifest(self) -> PluginManifest:
        """Return the immutable plugin manifest."""

    async def validate_settings(self, settings_json: str) -> str:
        """Validate host-provided settings and return normalized JSON."""

    async def execute(
        self,
        context: PluginOperationContext,
        request_json: str,
        progress: Optional[ProgressSink] = None,
    ) -> tuple[str, OperationReceipt]:
        """Execute a unary operation and return final JSON plus receipt."""

    async def stream(
        self,
        context: PluginOperationContext,
        request_json: str,
        progress: Optional[ProgressSink] = None,
    ) -> AsyncIterator[str]:
        """Execute a streaming operation and yield response items."""

    async def get_health(self) -> HealthReport:
        """Return the current plugin health."""

Proposal The public SDK surface SHOULD expose domain-meaningful records and protocols, while generated or schema-bound transport DTOs stay in an internal namespace. That separation is essential if code generation is introduced later. Generated code is valuable for low-level envelope DTOs when the message catalog is large and stable; handwritten domain models remain preferable where language ergonomics, richer invariants, or privacy-preserving wrappers matter. JTD and protobuf are specifically designed with code generation in mind, while JSON Schema is more expressive and better for validation than for perfectly portable idiomatic model generation.

Conformance, evolution policy, migration, open questions, limitations, and sources

Proposal The conformance suite MUST be the main anti-drift mechanism. Every changed or added SDK function in either language SHOULD map to at least one automated behavior test and one serialization or error fixture where applicable.

Proposal Golden serialization vectors MUST cover at least these cases:

Vector classExamples
Canonical JSONobject key ordering, escaped Unicode, number normalization, no extra whitespace
Schema validityrequired/optional/null distinctions, forbidden unknown fields, extension namespaces
Numeric edges0, -0, 9007199254740991, 9007199254740992 as string, decimal string canonical forms
Time edgesUTC Z, fractional seconds, expired deadlines
Unicode edgescombining characters, astral-plane characters, invalid surrogate pairs
Duplicate keyssame key repeated, same key via escaped form, extension collisions
Streamingempty stream, single-item, multi-item, late streamEnd, out-of-credit sender
Cancellation racescancel before start, during progress, after final response emitted, after terminal sent
Version skewunknown optional feature, missing required feature, downgraded protocol
Faultsmalformed length prefix, truncated frame, oversized frame, broken UTF-8, broken JSON

Specification Protobuf’s own documentation notes that some conformance edge cases live in the conformance suite rather than prose documentation. That is an important design lesson here: prose alone is not enough for interoperable parsers.

Proposal The conformance runner SHOULD operate as a black-box harness:

  1. start the implementation under test as a subprocess;
  2. drive handshake and initialization fixtures;
  3. send normative frames from golden files;
  4. compare received frames against strict expectations;
  5. inject malformed frames and transport faults;
  6. verify terminal conditions, receipts, and privacy guarantees; and
  7. repeat the exact same corpus against C# and Python implementations.

The harness SHOULD validate both behavior and bytes for canonical artifacts.

Proposal Fuzzing requirements:

  • schema fuzzing against every JSON Schema resource;
  • duplicate-key generation and parser differential tests;
  • randomized field reordering;
  • oversized string and array payloads;
  • invalid UTF-8 byte sequences;
  • base64url edge cases for byte strings;
  • deadline underflow/overflow cases;
  • flow-control abuse cases;
  • unknown messageType and unknown extension namespace cases;
  • idempotency-key replay storms;
  • shutdown races while streams are active.

Proposal Privacy assertions MUST be first-class conformance tests. The harness SHOULD assert that: raw secret values never appear in errors, receipts, or default logs; blocked payload classes are rejected before delivery to plugin code; and configuration export paths preserve secretRef while omitting resolved values.

Proposal Compatibility promises:

  • protocol major versions are mutually incompatible unless a bridge is explicitly declared;
  • protocol minor versions MUST be backward-compatible and additive only;
  • core envelope objects reserve unknown top-level fields for future use and therefore reject them, while extensions remains the sanctioned must-ignore space;
  • capability and operation changes that alter semantics MUST increment capability or operation versions even if payload shape is unchanged;
  • removal of declared permissions, capabilities, or request fields is breaking unless retained as deprecated aliases through the published window.

Proposal Deprecation policy:

  • publish deprecations in schema annotations and negotiation responses;
  • provide at least one minor release of overlap for routine changes;
  • encode deprecatedSince and sunsetAfter;
  • after sunset, the old element SHOULD produce a deterministic incompatibility error rather than silent coercion.

Proposal Emergency revocation policy MUST exist outside SemVer. Hosts SHOULD be able to deny specific (publisherId, pluginId, manifestDigest) tuples regardless of package version, using signed revocation metadata distributed independently of plugin updates.

Proposal Migration and adoption sequence:

  1. Freeze the abstract contract vocabulary for identities, envelopes, errors, receipts, and privacy classes.
  2. Publish bundled schemas for manifest, settings, envelopes, and operation examples.
  3. Build the conformance runner first, before either public SDK reaches feature completeness.
  4. Implement the C# SDK and Python SDK against the same fixtures, not against each other.
  5. Add optional codecs or transports later only after the abstract contract is stable and fully covered.

That sequence minimizes the risk that the first SDK becomes the de facto spec.

Unknown A few implementation questions remain open because they are host-policy questions rather than pure protocol questions: whether the host wants per-user or per-machine secret stores; whether local configuration grants are scoped to machine, user, or workspace; whether background behavior is globally schedulable or host-mediated only; and whether receipts should be signed by the plugin, the host, or both.

Limitations This proposal deliberately favors explicitness and reviewability over absolute minimal payload size. A protobuf-based runtime could outperform it on hot-path throughput and could reduce handcrafted DTO drift. Also, the settings-annotation subset proposed here needs governance: if each team adds ad hoc x-ui fields, drift simply moves from runtime messages to UI metadata. Finally, any receipt-based exactly-once approximation depends on correct durable storage and policy around expiry; transport design alone cannot solve that.

Sources The most load-bearing references for interoperable implementation are RFC 8259 JSON, RFC 7493 I-JSON, RFC 3339 timestamps, RFC 9562 UUIDs, RFC 8785 JCS, JSON Schema Draft 2020-12 and its core specification on bundling and compound documents, OpenAPI, JSON-RPC 2.0, Protocol Buffers language and encoding guides, ProtoJSON format notes, gRPC core concepts and health/status guidance, RFC 8949 CBOR, RFC 8927 JTD, the Unicode Standard current release page, Semantic Versioning 2.0.0, and OWASP/NIST logging and secure software guidance.