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,
Key topics
- Semantic Systems / Language / Glyphs
- Semantic Systems
- Language
- Glyphs
- .NET
- C#
- Python
- Runtime
- Privacy
Research provenance
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.
| Stack | Schema rigor | C#/Python tooling | Forward compatibility | Streaming | Cancellation | Browser path later | Local IPC suitability | Binary support | Canonicalization | Inspectability | Operational complexity | Overall fit |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| JSON Schema 2020-12 + custom framed I-JSON | 5 | 4 | 4 | 4 | 4 | 5 | 5 | 2 | 5 | 5 | 4 | 4.5 |
| JSON Schema 2020-12 + JSON-RPC 2.0 over framed JSON | 4 | 4 | 3 | 2 | 2 | 5 | 5 | 2 | 5 | 5 | 4 | 3.8 |
| Protobuf + gRPC | 4 | 5 | 5 in binary, 2 in ProtoJSON | 5 | 5 | 2 | 3 | 5 | 2 | 2 | 3 | 4.0 |
| Protobuf + custom framed protobuf | 4 | 5 | 5 | 5 | 4 | 1 | 5 | 5 | 3 | 1 | 4 | 3.9 |
| JSON Schema + CBOR over custom frames | 4 | 3 | 4 | 4 | 4 | 2 | 5 | 5 | 4 | 2 | 4 | 3.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:
| Field | Meaning | Mutability | Format | Compatibility rule |
|---|---|---|---|---|
pluginId | Stable security identifier for the logical plugin | Immutable | reverse-DNS ASCII string | Never reused |
publisherId | Stable identifier for publisher trust domain | Immutable | reverse-DNS ASCII string | Never reused |
packageVersion | Version of distributable artifact | Mutable | SemVer | Distribution semantics only |
manifestSchemaVersion | Version of manifest schema dialect/profile | Slow-moving | date or major.minor | Host validates manifest against supported versions |
protocolVersion | Runtime wire/invocation version | Negotiated | major.minor | Major break requires negotiation failure or downgrade |
capabilityVersion | Version of each declared capability contract | Mutable | SemVer | Capability-level compatibility |
settingsSchemaVersion | Version of settings schema | Mutable | SemVer plus content digest | Migration may be required |
operationVersion | Version of each operation payload/result contract | Mutable | major.minor | Determined 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.catalogpublisherId:org.examplepackageVersion: SemVer stringprotocolVersion:1.0,1.1,2.0manifestSchemaVersion: URI or profile token such asurn:example:plugin-manifest:1settingsSchemaVersion: SemVer plus digest tie-downmessageId,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:
- The initiator sends supported
protocol.minandprotocol.max, required feature tokens, optional feature tokens, maximum frame size, and supported codecs. - The responder returns the chosen protocol version, enabled features, selected codec, limits, and the running plugin’s manifest digest.
- If no common major version exists, the responder returns
unsupported_protocol. - If a required feature is missing, the responder returns
missing_required_feature. - If a lower compatible version exists, the responder SHOULD include
recommendedProtocolVersionin 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.pluginIdis invalid because it is not reverse-DNS ASCII. - Sample
packageVersionis invalid because it is not SemVer. - Sample
protocol.minVersionis invalid because it lacksmajor.minor. - Sample
entryPointsis invalid because at least one language entry point is required. - Sample
capabilitiesis invalid because at least one capability is required. - Sample
manifestDigestis invalid because it does not match thesha256:<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.secretApiKeyis 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:
| Family | Purpose | Terminal | Notes |
|---|---|---|---|
hello / helloAck | version, limits, feature negotiation | no | required |
init / initAck | runtime init with local config and grants | no | required |
request | start or continue an operation | no | required |
progress | bounded progress update | no | optional |
streamItem | one streamed item or chunk | no | optional |
streamEnd | logical end of response stream | yes | terminal for stream body |
response | final successful non-streaming result or stream trailer | yes | terminal |
error | terminal failure | yes | terminal |
cancel / cancelAck | request cancellation / acknowledge receipt | no | required when cancel negotiated |
cancelled | terminal cancellation outcome | yes | terminal |
flow | update stream credits | no | required when streaming negotiated |
heartbeat | liveness | no | required when heartbeat negotiated |
healthQuery / healthReport | pull health | no | required |
healthWatch / healthEvent | push health mode | no | optional |
event | non-requested informational event | no | optional |
goAway | connection draining and shutdown intent | no | required |
Proposal The request envelope MUST distinguish identifiers with different semantics:
| Field | Semantics |
|---|---|
messageId | unique frame identity |
requestId | correlates frames that belong to one attempt on one connection |
operationId | stable logical operation identity across retries or resume |
idempotencyKey | stable dedupe key for effectful operations |
attempt | caller attempt counter starting at 1 |
deadlineUtc | absolute caller deadline |
traceId | distributed trace identity |
causationId | immediate parent message/event |
sequence | transport 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
idempotencyKeyplus 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:
| Topic | Rule |
|---|---|
| Unknown core fields | reject |
Unknown extension namespaces under extensions | ignore unless required by negotiation |
| Defaults | omitted means absent; defaults are local API conveniences, not wire semantics |
| Numbers | JSON numeric fields MUST stay inside interoperable range; larger integers and exact decimals use strings |
| Timestamps | RFC 3339 UTC strings only |
| Enums | use lowercase strings, not integers |
| Nullability | null only where explicitly allowed; absence is preferred for “not provided” |
| Maps | string-keyed only |
| Bytes | base64url strings or host-local blob handles |
| Unicode | reject 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:
cancelrequests that work stop as soon as practical.cancelAckmeans the peer received the cancellation request, not that the operation is already cancelled.- The terminal outcome MAY still be
response,error, orcancelled, depending on timing. - The first valid terminal frame for an operation wins.
- 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, orcancelled. streamEndends the stream body, not the operation, until followed byresponseorerror.- Shutdown MUST stop accepting new requests before it cancels old ones.
- A peer MAY send
goAwaywith alastAcceptedSequenceso 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:
| Code | Meaning | Retryable | Blame | Severity | Safe default message |
|---|---|---|---|---|---|
invalid_request | malformed envelope or schema-invalid payload | no | caller | error | The request format was invalid. |
unsupported_protocol | no compatible protocol version | no | peer mismatch | error | No compatible protocol version was found. |
missing_required_feature | negotiated features insufficient | no | peer mismatch | error | A required protocol feature is unavailable. |
unsupported_capability | capability or operation not declared | no | caller | error | The requested capability is not supported. |
permission_denied | permission missing or grant insufficient | no | caller/host policy | warning | Permission was not granted. |
payload_class_denied | payload classification exceeds authorization | no | caller/host policy | warning | The payload class is not authorized. |
deadline_exceeded | deadline reached | maybe | environment | warning | The operation exceeded its deadline. |
conflict | idempotency or state conflict | maybe | caller/environment | warning | The operation conflicts with current state. |
rate_limited | local throttling or policy rate limit | yes | environment | warning | The operation was rate limited. |
transient_failure | recoverable dependency or IPC failure | yes | environment/plugin | error | A transient failure occurred. |
plugin_fault | uncaught plugin exception or invariant break | maybe | plugin | error | The plugin encountered an internal fault. |
host_shutdown | host is draining or shutting down | yes | host | info | The host is shutting down. |
flow_control_violation | sender exceeded stream credit | no | sender | error | Stream flow-control limits were violated. |
frame.too_large | frame exceeds negotiated size | no | sender | error | The 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:
| Field | Purpose |
|---|---|
receiptId | unique durable receipt identifier |
pluginId / publisherId | trust binding |
manifestDigest | code identity tie-down |
capabilityId / operationName / operationVersion | contract identity |
operationId / idempotencyKey / attempt | dedupe and retry trace |
requestDigest | canonical digest of redacted request |
resultDigest | optional digest of redacted result |
effectState | none, started, partial, completed, rejected, compensated, unknown |
terminalState | completed, failed, cancelled |
issuedAtUtc / expiresAtUtc | time bounds |
privacyClass | default minimum handling |
retention | none, ephemeral, short, audit |
signatureRef | optional 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
conflictwithexistingOperationId; 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,regulateddiagnosticPolicy:safe-default,safe-plus-hashes,explicit-debugretention:none,ephemeral,short,auditredaction: field-level directives in schema, such asx-redact: full,x-redact: hash, orx-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 Pythonawait-returning coroutine. - C#
IAsyncEnumerable<T>maps to Python async iterator / async generator. - C#
CancellationTokenmaps 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 class | Examples |
|---|---|
| Canonical JSON | object key ordering, escaped Unicode, number normalization, no extra whitespace |
| Schema validity | required/optional/null distinctions, forbidden unknown fields, extension namespaces |
| Numeric edges | 0, -0, 9007199254740991, 9007199254740992 as string, decimal string canonical forms |
| Time edges | UTC Z, fractional seconds, expired deadlines |
| Unicode edges | combining characters, astral-plane characters, invalid surrogate pairs |
| Duplicate keys | same key repeated, same key via escaped form, extension collisions |
| Streaming | empty stream, single-item, multi-item, late streamEnd, out-of-credit sender |
| Cancellation races | cancel before start, during progress, after final response emitted, after terminal sent |
| Version skew | unknown optional feature, missing required feature, downgraded protocol |
| Faults | malformed 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:
- start the implementation under test as a subprocess;
- drive handshake and initialization fixtures;
- send normative frames from golden files;
- compare received frames against strict expectations;
- inject malformed frames and transport faults;
- verify terminal conditions, receipts, and privacy guarantees; and
- 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
messageTypeand 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
extensionsremains 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
deprecatedSinceandsunsetAfter; - 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:
- Freeze the abstract contract vocabulary for identities, envelopes, errors, receipts, and privacy classes.
- Publish bundled schemas for manifest, settings, envelopes, and operation examples.
- Build the conformance runner first, before either public SDK reaches feature completeness.
- Implement the C# SDK and Python SDK against the same fixtures, not against each other.
- 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.