Runtime

Python Plugin Packaging and Runtime Architecture for a Windows C Host

Report summary

Architecture synthesis. The default architecture should be a host-managed, per-plugin isolated sidecar process running on a host-owned CPython runtime , with each plugin installed into its own virtual environment and invoked by absolute interpreter path , never by whatever python or py happens to re

Status
Research archive item
Category
Runtime
Length
5,276 words
Reading time
24 minutes
Report type
strategy

Key topics

  • Runtime
  • .NET
  • Python
  • Semantic Systems
  • Research Archive
  • Strategy
  • Audit
  • Architecture

Research provenance

Archive status
Research archive item
Content identity
sha256:86432a1dcee777b30a464a45b39bff1917c0592d27f956a4733d84b271656e60

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

Source availability: 50 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

Architecture synthesis. The default architecture should be a host-managed, per-plugin isolated sidecar process running on a host-owned CPython runtime, with each plugin installed into its own virtual environment and invoked by absolute interpreter path, never by whatever python or py happens to resolve on the machine. On Windows, the sidecar should communicate with the C# host over a dedicated IPC channel that is separate from human-readable logs; for a Windows-first host, the best default is named pipes with an explicit framing protocol and a separate stderr/log channel. This design draws a clean line between package installation, dependency resolution, runtime launch, protocol communication, cancellation, and recovery, while avoiding the reliability and trust problems of in-process embedding.

Proposal. The submitted artifact should not be a raw sdist and should not default to a bare wheel by itself. The most robust production submission is a signed plugin bundle containing: one primary plugin wheel, a host-owned manifest with identity/capabilities/permissions/runtime contract, a lock file and hash metadata, a vetted wheelhouse of approved dependency wheels, and supply-chain evidence such as SBOMs and provenance/attestations. Wheels are the standard binary distribution format and carry metadata cleanly; the host manifest fills the gaps that Python packaging intentionally does not standardize, such as plugin ID, publisher ID, permissions, health policy, and host contract version.

Architecture synthesis. Embedding CPython in the .NET UI process, including via Python.NET, should be treated as a special-case exception rather than the default. Embedded execution and mixed-runtime bridges provide convenience, but they do not create a true crash boundary, do not provide a security boundary by themselves, and make interpreter selection, GIL/runtime lifetime, shutdown ordering, and native-extension behavior materially harder to reason about. By contrast, a sidecar process gives a clear reliability boundary, clearer update ownership, simpler cancellation and restart semantics, and a much better place to apply Windows controls such as Job Objects, per-process mitigations, ACLs, and optional AppContainer-style containment.

Proposal. Production submissions should be wheel-only for Python dependencies and should prohibit sdists by default. Any build-from-source path executes arbitrary build logic through the packaging toolchain and adds a second trust boundary around compilers, build backends, and transient dependencies. Where native stacks, GPU libraries, or platform-specific vendor runtimes make wheel-only delivery impractical, the exception path should be a contract-compatible packaged executable sidecar or another prebuilt, host-vetted runtime package, not “build it on the customer machine.”

Scope, method, and applicability

Scope declaration. This report is based on public external sources only. It does not assume access to any private product, website, repository, manifest, environment, logs, interpreter inventory, endpoint, binary, or internal document. All examples are synthetic and do not claim compatibility with any private product.

Source method. The load-bearing sources are the Python Packaging Authority specifications for pyproject.toml, core metadata, wheels, sdists, entry points, platform tags, installed-project recording, and pylock.toml; CPython standard-library and Windows documentation for importlib.metadata, venv, ensurepip, Windows embedding, stable ABI, and process creation; Microsoft documentation for Job Objects, named-pipe security, long-path behavior, AppContainer, process mitigations, handle inheritance, firewall controls, and Visual C++ redistributables; plus primary supply-chain guidance from PyPI, CISA, and NIST. Where guidance comes from specific tools such as uv, pipx, PyInstaller, Nuitka, or conda, it is treated as External guidance or tool-vendor guidance, not as ecosystem specification.

Python-version applicability. The packaging guidance in this report applies broadly to modern CPython packaging based on pyproject.toml, wheels, core metadata, entry points, and virtual environments. For native compatibility, the host should treat the runtime as a contract over implementation, version line, ABI, and architecture, not merely “Python installed somewhere.” The stable ABI (abi3) helps some extensions span multiple CPython 3.x releases, and abi3t is emerging for free-threaded builds, but free-threaded Python remains a transition area and should be treated as a separate runtime family until deliberately adopted and tested by the host.

Proposal. A practical applicability rule is: support one or two host-owned CPython minor lines at a time, declare them explicitly in the host runtime catalog, and force plugins to target one of those runtime families. Never infer runtime support from machine state alone. This is especially important on Windows, where the install manager and multiple Python installations can create selection surprises unless the host always launches a specific interpreter path.

Packaging and discovery

Packaging roles and metadata boundaries

Specification. pyproject.toml is the standard project configuration file for packaging-related tools. Its [build-system] table declares build-system dependencies, and its [project] table provides the project’s core metadata. That is the correct place for distribution-level package metadata such as name, version, dependencies, and requires-python.

Specification. A wheel is the standard binary distribution format. It is a ZIP archive whose filename encodes {distribution}-{version}-{python tag}-{abi tag}-{platform tag}.whl, and it contains a .dist-info directory with at least METADATA, WHEEL, and RECORD. RECORD carries secure hashes for all files except itself, and wheels intentionally omit setup.py and setup.cfg.

Specification. A source distribution is a .tar.gz source archive whose standard form is defined around pyproject.toml, PKG-INFO, and a single top-level {name}-{version} directory. An sdist may contain whatever the build system needs to produce the package, which is exactly why it is useful for authors and risky for production admission.

Specification. Entry points are metadata records in entry_points.txt inside the distribution’s .dist-info directory. They let installed distributions advertise named components without requiring discovery-time imports. The standard library’s importlib.metadata reads installed metadata, versions, files, requirements, and entry points from distributions with discoverable metadata, and it explicitly distinguishes distribution packages from top-level import packages, which are not always a 1:1 mapping.

Specification. Dependencies are expressed through core metadata fields such as Requires-Dist and Requires-Python, using version specifiers and optional environment markers. Platform compatibility tags encode the Python implementation/version, ABI, and platform. The new Import-Name field in core metadata gives tools a standardized way to declare import names that a project exclusively provides, and tools are expected to reject overlapping import names to prevent accidental shadowing.

Architecture synthesis. For a host plugin system, the standard Python artifacts are necessary but not sufficient. Python packaging standardizes distributions, metadata, and installation behavior; it does not standardize plugin identity, publisher identity, permissions, capabilities, health policy, host-protocol compatibility, disable/quarantine semantics, or enterprise approval state. Those belong in a host-owned manifest that the host can validate without importing plugin code.

Proposal. The production submission format should be a plugin bundle such as a signed ZIP or MSIX-like envelope that contains:

sample-plugin-bundle/
  manifest/host-plugin.json
  dist/sample_plugin-1.2.0-py3-none-any.whl
  wheelhouse/
    dep_a-4.1.0-py3-none-any.whl
    dep_b-2.7.3-cp312-cp312-win_amd64.whl
  locks/pylock.samplehost.toml
  sbom/plugin.cdx.json
  provenance/publish.attestation.json
  signatures/bundle.sig

This keeps standard Python payloads inside a host-governed admission envelope. The host can inspect all metadata before installation, enforce wheel-only policy, and install offline from approved artifacts.

Architecture synthesis. A plain wheel can still be useful for local development and early SDK ergonomics, but it is weaker for enterprise release management because the lock file, wheelhouse, permissions, and provenance generally live outside the wheel. A standalone executable sidecar can be allowed as an exception artifact where the Python ecosystem cannot practically carry the native stack, but that should preserve the same runtime contract and IPC protocol as normal plugins.

Identity model

Proposal. The host should keep these identities separate:

IdentifierPurposeMutabilityAuthority
Distribution namePython package identity used by packaging toolsCan differ from plugin IDPython metadata
Plugin IDStable host-level plugin identityImmutable once publishedHost manifest
Publisher IDStable issuer identityImmutableHost trust system
Distribution versionPython package release versionChanges each releasePython metadata
Contract versionHost protocol/API compatibility lineChanges less oftenHost manifest
Runtime versionInterpreter family targeted by the pluginChanges with ABI/runtime needsHost manifest + wheel tags

Architecture synthesis. This separation matters because distribution names are normalized and compared case-insensitively in PyPA tooling, while host identity needs stronger semantics around publisher trust, permissions, and compatibility. Treat the distribution name as packaging metadata, not as the canonical plugin identifier.

Discovery without importing code

Proposal. Catalog discovery should be pure metadata inspection and should not import plugin modules. The recommended algorithm is:

  1. Read the host manifest from the bundle.
  2. Open the wheel as a ZIP archive.
  3. Validate wheel filename tags and .dist-info presence.
  4. Read METADATA, WHEEL, RECORD, and entry_points.txt.
  5. Cross-check the manifest’s declared entry point against the wheel metadata.
  6. Reject on mismatch, overlap, or unsupported tags.

This gives deterministic discovery and avoids import side effects during indexing.

Architecture synthesis. For installed environments, importlib.metadata is the right runtime API to inspect versions, files, requirements, entry points, and import/distribution mappings without loading the plugin implementation. For pre-install inspection, the host should parse the wheel archive directly instead of installing first and asking Python later.

Windows filesystem considerations

Platform fact. Windows long-path behavior is not automatic. Modern Win32 long-path support requires both system configuration and an application manifest with longPathAware; otherwise many APIs still observe MAX_PATH-related limits. NTFS stores filenames in Unicode, preserves case, and is usually case-insensitive by default. Developers should not assume case-sensitive path behavior on Windows.

Proposal. Install plugin bundles under short, deterministic roots such as %ProgramData%\<Host>\plugins\<plugin-id>\<version>\... or the per-user equivalent, normalize and compare package names using PyPA normalization rules, and never use case-sensitive comparisons for Windows paths. Use staged installation on the same volume followed by atomic directory promotion where possible, and plan for retriable cleanup on uninstall because Windows file deletion is sensitive to open handles from the plugin process or its descendants.

Interpreter, installation, and environment management

Interpreter strategy comparison

Architecture synthesis. The table below uses a 1–5 score where 5 is best for the host and 1 is worst. Scores reflect packaging specifications, CPython behavior, Windows controls, and each tool’s official documentation. For criteria like streaming and cancellation, the score captures how naturally the interpreter strategy supports a robust sidecar-style host architecture rather than an in-process scripting convenience model.

Interpreter strategySecurity boundaryDependency isolationNative package supportStartup costStreamingCancellationCrash containmentPackaging complexityUpdate ownership clarityOffline deploymentDiagnosticsDeveloper burden
System Python124444421234
Bundled CPython shared by all plugins234444435544
Bundled CPython + per-plugin venv254355545554
Shared managed environments224444434433
Embeddable distribution vendored per plugin253322154522
pipx-style per-app envs243344533334
conda-managed envs255255553442
PyInstaller sidecar exe254444552523
Nuitka sidecar exe254444552522
External service or container-like boundary454155554341

Recommendation. The best general-purpose choice is bundled CPython plus per-plugin virtual environments. venv gives environment-local site-packages and a per-environment interpreter path; the host retains update ownership over the interpreter baseline; native wheels remain usable; and the operational complexity is much lower than conda or packaging every plugin as its own frozen executable.

Tooling recommendations and rejected defaults

Platform fact. venv creates a lightweight environment with its own Python executable, pyvenv.cfg, and site-packages, optionally isolated from the system environment. On Windows, symlinks are supported but not recommended because normal shell and Explorer behavior can bypass the virtual-environment illusion.

External guidance. uv strongly prefers virtual environments and can even download Python automatically; pipx creates isolated venvs for command-line applications; conda provides package, dependency, and environment management for multiple languages. Those tools can be valuable in developer workflows, but their behavior is not Python packaging specification and should not define the plugin contract unless the host intentionally standardizes on them.

Proposal. Use host-owned CPython and venv as the default install substrate. Use pip invoked as python -m pip as the baseline installer because it is standard, well-documented, and bootstrapped via ensurepip; permit uv only as an implementation optimization if the host also owns and pins that choice. Do not use pipx for plugins, because plugins are not ordinary CLI apps with PATH-exposed entry points. Do not use conda as the default unless the plugin ecosystem is explicitly scientific/ML-heavy and the organization is willing to absorb the operational complexity of a second packaging universe.

Rejected default. The Windows embeddable distribution is a useful building block, but CPython explicitly documents it as a minimal, app-vendoring-oriented package where ordinary pip-style dependency management is not supported and third-party packages should typically be treated as vendored application content. That makes it a poor default for an extensible plugin ecosystem that needs predictable admission, upgrades, and dependency inspection.

Update ownership, compatibility, native code, and offline install

Proposal. The host platform team should own interpreter security updates. Plugins should declare compatibility targets; they should not bring their own “mystery Python” by default. That keeps CVE response, runtime rollout, and rollback under host control, and it avoids the ambiguity that arises when Windows has multiple managed and unmanaged Python installs.

Specification. Compatibility must be checked across wheel tags, Requires-Python, and any host manifest runtime fields. The wheel tag encodes Python implementation/version, ABI, and platform, and Requires-Python expresses supported Python versions for installers. Extensions targeting the CPython Stable ABI can use abi3, which lowers host upgrade friction; newer free-threaded compatibility introduces abi3t, which should be handled as a deliberate compatibility choice rather than an implicit assumption.

Specification and Platform fact. Native wheels and compiled extensions are fully compatible with the wheel model, but they are sensitive to ABI and platform tags, and they may depend on the Microsoft Visual C++ runtime or other external native libraries. Microsoft documents that modern Visual Studio releases share the v14 redistributable line, and Python documents Windows extension-module building around MSVC-compatible toolchains. The metadata field Requires-External exists, but it is only a hint and has no standardized installer semantics.

Proposal. Treat GPU, CUDA, accelerator libraries, and large vendor DLL stacks as exceptional native dependencies. If they are supported, require them to arrive as prebuilt, host-vetted artifacts with explicit compatibility declarations and preflight checks; do not resolve them from the public internet at install time. When the Python packaging path becomes too brittle, permit a standalone executable sidecar that preserves the same host manifest and IPC contract.

Specification. Reproducible and offline-capable installation is well supported with hash-checking mode, per-requirement hashes, pinned versions, a wheelhouse, and --no-index --find-links. pip download can fetch artifacts for a target interpreter/platform/ABI ahead of time, and pip documents wheelhouse-based installation bundles for offline or locked-down deployment. pylock.toml is intended to specify dependencies for reproducible installation, although tool support is still maturing.

Proposal. Rollback and garbage collection rules should be strict:

  • install into a new immutable environment directory keyed by plugin ID, plugin version, runtime family, and lock digest;
  • promote atomically only after validation;
  • keep the previous known-good environment until the new one passes startup and health checks;
  • reference-count interpreter runtimes and shared caches;
  • delete only environments not referenced by any installed plugin version or rollback slot.

This avoids in-place drift and makes quarantine/rollback cheap and deterministic.

Process-model comparison

Architecture synthesis. The host should prefer a sidecar process because it is the cleanest reliability boundary and the most practical place to attach Windows controls. The table below uses the same 1–5 scoring convention as the interpreter matrix.

Process modelSecurity boundaryDependency isolationNative package supportStartup costStreamingCancellationCrash containmentPackaging complexityUpdate ownership clarityOffline deploymentDiagnosticsDeveloper burden
Embed CPython in .NET host114522123423
Python.NET in host process113422133423
Isolated Python process254355545554
Packaged executable sidecar254455552533
External brokered service / container-like boundary454155554341

Architecture synthesis. In-process models are neither sound reliability boundaries nor meaningful security boundaries. A separate process is a reliability boundary by default; it becomes a stronger security boundary only when paired with OS-enforced controls such as ACLs, restricted tokens, network policy, AppContainer, or a stronger external sandbox.

IPC strategy comparison

Architecture synthesis. For a Windows-first desktop host, the strongest default is named pipes with explicit framing. Standard I/O is attractive for local child-process control but is fragile when plugin authors print() to stdout, and HTTP loopback expands the attack surface more than a local pipe. gRPC and JSON-RPC are useful protocol layers, but they are not transports by themselves; gRPC usually rides HTTP/2 and JSON-RPC is an envelope specification.

IPC strategySecurity boundaryDependency isolationNative package supportStartup costStreamingCancellationCrash containmentPackaging complexityUpdate ownership clarityOffline deploymentDiagnosticsDeveloper burden
stdio with framed protocol255534525525
Named pipes + framed protocol455455535554
Unix domain sockets on Windows355455545543
gRPC over named pipe/UDS adapter355255555553
JSON-RPC over named pipes455444535554
HTTP loopback255354545544

Specification and Platform fact. Windows named pipes support access-control descriptors and access checks. They are a first-class local IPC mechanism on Windows. Unix domain sockets are also available on modern Windows platforms, and .NET explicitly documents gRPC over UDS as a same-machine IPC option, but for a Windows-only host named pipes integrate more naturally with Windows ACLs, sessions, and service/process security primitives.

Proposal. The default runtime shape should be:

  1. C# host launches a plugin runner process by absolute path inside the plugin’s own virtual environment.
  2. The process joins a Job Object immediately.
  3. The host provides a dedicated control channel over a named pipe.
  4. The plugin writes human logs only to stderr or to a separate log/event pipe.
  5. All control/data messages use an explicit frame format.

This eliminates ambiguous interpreter selection, isolates dependencies, prevents stdout corruption of the protocol, and makes cancellation and restart deterministic.

Proposal. Use a simple framed envelope for the control pipe:

Frame:
  uint32_le total_length
  utf8 json envelope

Envelope:
  {
    "protocol_version": "1.0",
    "session_id": "uuid",
    "message_id": "uuid",
    "correlation_id": "uuid|null",
    "sequence": 17,
    "kind": "request|response|event|error|heartbeat|cancel",
    "method": "plugin.start|tool.invoke|stream.chunk|plugin.stop|...",
    "deadline_utc": "2026-07-11T20:15:30Z|null",
    "stream_id": "uuid|null",
    "more": false,
    "content_type": "application/json",
    "payload": { ... }
  }

Binary payloads should not be inlined into random logs or mixed with stderr. For large binaries, send metadata on the control pipe and move bytes through an attachment pipe or file-handle transfer path.

Proposal. Startup and control semantics should be explicit:

  • Handshake deadline: the plugin must connect and send plugin.hello within a short startup timeout.
  • Capability negotiation: the plugin declares protocol version, contract version, supported methods, stream support, optional subprocess/network capabilities, and health endpoints.
  • Heartbeat: the plugin emits a heartbeat event on a monotonic cadence during idle and during long-running calls.
  • Per-request deadlines: all requests carry deadlines; the plugin must stop work when cancellation or deadline expiration arrives.
  • Graceful stop: host sends plugin.stop, stops issuing new work, and waits a bounded grace period.
  • Forced termination: if the plugin misses grace or becomes unresponsive, the host kills the Job Object.

Architecture synthesis. If the host wants a standards-based message model without pulling in HTTP/2 and protobuf, JSON-RPC 2.0 over named pipes is a strong option for unary control calls, but streaming will still need host-specific conventions. JSON-RPC already provides request IDs, notifications, and structured errors; the host can extend it with stream.chunk, progress, and heartbeat event methods. gRPC is excellent when both sides are willing to standardize on protobuf and richer toolchains, but it is a heavier starting point for a desktop plugin platform.

Orphan prevention and async interop

Platform fact. Windows Job Objects support kill-on-close behavior and hierarchical termination. TerminateJobObject terminates all associated processes, and Microsoft documents how breakaway behavior affects process trees. Windows also supports child-process restriction and explicit handle inheritance lists through process-thread attributes.

Proposal. The host should:

  • create a Job Object with KILL_ON_JOB_CLOSE;
  • avoid breakaway permissions by default;
  • use explicit handle inheritance rather than blanket inheritance;
  • optionally deny child-process creation unless the plugin has a declared subprocess capability.

This is the practical way to prevent orphan trees and descendant-process escape on Windows.

Architecture synthesis. Async Python plugins should own their own event loop. The C# host should not couple itself to Python’s loop; it should see the plugin as an RPC/stream endpoint and let the plugin map protocol messages onto asyncio, threads, or worker processes internally. That keeps failure domains separate and makes restart simpler.

Security, supply chain, and Windows containment

Supply-chain admission policy

Specification and External guidance. pip documents that ordinary installation can involve running arbitrary code from distributions, and it recommends a more secure posture through hash-checking mode and wheel-only installation (--only-binary :all:). Hash-checking mode requires all requirements and all dependencies to be pinned and hashed. pylock.toml exists to support reproducible installation, while PyPI now supports attestations tied to Trusted Publishing and provenance scenarios. CISA and NIST both treat hashes, signatures, provenance, and SBOMs as core software-supply-chain controls.

Proposal. Production submission rules should be:

  • No sdists in production submissions.
  • No install-time builds on customer machines.
  • No unpinned transitive resolution during install.
  • All wheels hashed and pre-approved.
  • Bundle signature required by host trust policy.
  • SBOM required for the bundle and dependency set.
  • Vulnerability scan required against a curated advisory source such as OSV and internal policy.
  • License policy review required using SPDX-capable metadata where available.

Architecture synthesis. The main supply-chain risks are not subtle: build hooks in sdists, arbitrary backend code via pyproject-declared build systems, dependency confusion, typosquatting, malicious or replaced artifacts, unsafe native extensions, accidental import-name shadowing, and drift between approved and actually installed environments. The admission pipeline must therefore validate names, hashes, signatures, entry points, import-name overlap, supported tags, and lock congruence before any install begins.

Why a virtual environment is not a sandbox

Specification and Platform fact. A venv isolates Python packages by giving the environment its own interpreter path and site-packages. It does not change the Windows user token, filesystem rights, registry rights, network rights, process privileges, or device access. By contrast, AppContainer is an OS-enforced restrictive execution environment that limits file, registry, network, device, window, and process interactions unless explicit permissions are granted.

Conclusion. A virtual environment is a dependency-isolation mechanism, not a sandbox. Treating it as a security boundary would be a category error.

Windows containment plan

Feasible controls. The practical controls a desktop host can apply today are:

ControlFeasibilityWhat it helps withPractical constraint
Job Object with kill-on-closeHighOrphans, crash cleanup, forced stopDoes not by itself sandbox file/network access
Job Object memory / CPU / active-process limitsHighRunaway resource useTune to avoid false positives for native workloads
Explicit handle inheritance listHighPrevent leaked privileged handlesRequires careful launcher implementation
Named-pipe ACLs and session scopingHighRestrict who can connect to control channelMust avoid permissive default descriptors
Dedicated plugin directories with locked-down ACLsHighPrevent cross-plugin file tamperingSame-user write access outside those folders still exists
Windows Firewall app rulesMediumBlock or scope plugin network accessOften requires admin or enterprise policy cooperation
Restricted tokenMediumReduce ambient privilegesCompatibility testing needed for native extensions
AppContainer / LPACMediumStronger OS-enforced containmentMore packaging/compatibility complexity, especially for unpackaged native-heavy sidecars
Win32 app isolationMedium / evolvingStronger brokered isolation on recent WindowsNewer OS dependency and packaging/tooling validation required

Platform fact. Microsoft documents named-pipe security descriptors and access checks, per-process mitigation policies, Job Objects, AppContainer file/network/process isolation, and firewall rules. AppContainer specifically limits file, registry, network, and process interaction and gives the process a distinct application identity and capabilities model. Windows also documents Win32 app isolation as a newer security feature built on AppContainer for recent Windows releases.

Proposal. Use a tiered containment model:

  • Default tier: Job Object, explicit handle inheritance, named-pipe ACLs, dedicated ACL-locked directories, optional firewall policy.
  • Elevated-risk tier: add restricted token and child-process restriction.
  • High-risk or hostile-plugin tier: move to AppContainer/LPAC or a brokered helper/service boundary, and assume some native Python packages may become unsupported.

This is the realistic balance between compatibility and containment on Windows.

Secret handling

Proposal. Do not pass secrets on the command line, because command lines are routinely visible to debuggers, crash reports, and management tooling. Avoid long-lived environment variables for secrets for the same reason. Prefer one of these patterns:

  • brokered “fetch secret” calls from the host over the authenticated control pipe;
  • short-lived opaque handles or references rather than secret values;
  • host-owned decryption and signing services so the plugin never sees root credentials;
  • ephemeral in-memory delivery with no persistence in plugin files.

Architecture synthesis. Endpoint names, session IDs, and correlation IDs are not the same as secrets; they can live on the command line if needed. Secret material should not.

Lifecycle, examples, algorithms, and failure catalog

Lifecycle model

Proposal. The host lifecycle should have explicit states:

StateMeaningHost action
SubmittedArtifact received but not trustedStore in quarantine
VerifiedSignature, hashes, manifest, wheel metadata, policy checks passedPermit install
InstalledEnvironment created and promoted atomicallyRecord immutable environment ID
ConfiguredHost-side config storedDo not write into plugin package files
StartingProcess launched, handshake pendingEnforce startup timeout
HealthyHandshake complete, heartbeat flowingAccept work
DegradedHeartbeats late or resource thresholds crossedReduce traffic, surface diagnostics
QuarantinedPlugin disabled due to policy or healthDeny launch
UpdatingNew environment staged alongside oldPreserve rollback slot
DisabledAdministrator or policy disabledSkip launch
UninstallingRemoval in progressStop process, remove references, then delete

Proposal. Suspend should mean “stop routing new work, optionally keep process warm if policy allows” rather than OS suspend semantics. Quarantine should preserve install evidence and diagnostic metadata but sever launch eligibility.

Generic package layout and synthetic examples

Sample. A Python plugin source tree, before bundling, can be organized like this:

sample-echo-plugin/
  pyproject.toml
  README.md
  src/
    sample_echo_plugin/
      __init__.py
      plugin.py
      protocol/
        __init__.py
        models.py
        codec.py
  tests/
    test_contract.py
    fixtures/
      malformed_message.json
      slow_start_config.json
  host/
    host-plugin.json
    pylock.samplehost.toml
    approved-wheels.txt

Sample. pyproject.toml should carry distribution metadata and a host-specific entry point group, but it should not try to represent host permissions or trust policy:

[build-system]
requires = ["setuptools>=70", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "sample-echo-plugin"
version = "1.2.0"
description = "Synthetic example plugin"
requires-python = ">=3.12,<3.13"
dependencies = [
  "pydantic>=2.8,<3",
  "orjson>=3.10,<4; platform_system == 'Windows'"
]

[project.entry-points."samplehost.plugins"]
echo = "sample_echo_plugin.plugin:Plugin"

This aligns with the standard pyproject.toml and entry-point specifications.

Sample. A separate host-owned manifest should carry the plugin contract and governance fields that Python packaging does not standardize:

{
  "plugin_id": "com.example.echo",
  "publisher_id": "publisher-example",
  "display_name": "Echo Plugin",
  "distribution_name": "sample-echo-plugin",
  "distribution_version": "1.2.0",
  "contract_version": "1.0",
  "runtime": {
    "family": "cpython",
    "variant": "gil",
    "version": "3.12",
    "architecture": "x64"
  },
  "entry_point": {
    "group": "samplehost.plugins",
    "name": "echo"
  },
  "permissions": {
    "filesystem": ["plugin-data-readwrite"],
    "network": "none",
    "subprocess": "deny"
  },
  "health": {
    "startup_timeout_seconds": 15,
    "heartbeat_interval_seconds": 10,
    "request_timeout_default_seconds": 60
  },
  "artifacts": {
    "primary_wheel": "dist/sample_echo_plugin-1.2.0-py3-none-any.whl",
    "lock_file": "host/pylock.samplehost.toml"
  }
}

Sample. Lock and hash metadata can use pylock.toml where supported, with a host policy that also records bundle-level hashes and approved wheel filenames:

lock-version = "1.0"
environments = [
  "sys_platform == 'win32' and python_version == '3.12'"
]

[[packages]]
name = "sample-echo-plugin"
version = "1.2.0"

[[packages.wheels]]
name = "sample_echo_plugin-1.2.0-py3-none-any.whl"
hashes = [{ name = "sha256", value = "aaaaaaaa..." }]

[[packages]]
name = "pydantic"
version = "2.9.1"

[[packages.wheels]]
name = "pydantic-2.9.1-py3-none-any.whl"
hashes = [{ name = "sha256", value = "bbbbbbbb..." }]

Sample. Keep protocol models and implementation logic separate inside the plugin package:

# src/sample_echo_plugin/protocol/models.py
from pydantic import BaseModel

class EchoRequest(BaseModel):
    message: str

class EchoResponse(BaseModel):
    echoed: str
# src/sample_echo_plugin/plugin.py
from sample_echo_plugin.protocol.models import EchoRequest, EchoResponse

class Plugin:
    contract_version = "1.0"

    async def invoke(self, method: str, payload: dict) -> dict:
        if method == "echo":
            req = EchoRequest.model_validate(payload)
            return EchoResponse(echoed=req.message).model_dump()
        raise ValueError(f"Unknown method: {method}")

Install, launch, and rollback algorithms

Proposal. Artifact verification and install should look like this:

function install_plugin(bundle_path):
    verify_bundle_signature(bundle_path)
    extract_manifest(bundle_path)
    validate_manifest_schema()
    require_exactly_one_primary_wheel()
    reject_if_contains_sdist()
    verify_all_bundle_hashes()

    wheel = open_wheel_zip(primary_wheel)
    read_dist_info_files(wheel, ["METADATA", "WHEEL", "RECORD", "entry_points.txt"])
    validate_distribution_name_version_match(manifest, wheel_metadata)
    validate_entry_point_match(manifest.entry_point, entry_points_txt)
    validate_requires_python(wheel_metadata, host_runtime_catalog)
    validate_supported_tags(wheel_filename_tags, host_runtime_catalog)
    validate_import_name_conflicts(wheel_metadata, installed_plugins)
    validate_lock_file_against_wheelhouse()

    runtime = resolve_host_runtime(manifest.runtime)
    env_id = compute_env_id(plugin_id, distribution_version, runtime, lock_digest)
    staging_dir = create_staging_dir(env_id)

    create_venv(runtime.python_exe, staging_dir.env)
    installer = [staging_dir.env.python, "-m", "pip"]
    run(installer + [
        "install",
        "--no-index",
        "--find-links", bundle_wheelhouse_dir,
        "--require-hashes",
        "--only-binary", ":all:",
        "-r", lock_file
    ])

    run_validation_probe(staging_dir.env.python, validation_script)
    atomically_promote(staging_dir, live_dir_for(plugin_id, distribution_version))
    record_install(plugin_id, env_id, manifest, hashes, health_policy)
    keep_previous_known_good(plugin_id)

Proposal. Launch, health, cancellation, and forced stop should look like this:

function start_plugin(plugin_id):
    install_record = read_active_install(plugin_id)
    create_named_pipe_pair_with_restrictive_acl()
    create_job_object(kill_on_close=true, memory_limit=?, active_process_limit=?)
    create_log_capture(stderr=true, stdout=discard_or_capture_separately)

    proc = create_process(
        exe=install_record.env_python,
        args=["-m", "samplehost_runner", "--session", session_id],
        cwd=install_record.working_dir,
        env=minimal_environment(),
        inherit_only=approved_handles,
        mitigation_policy=configured_policy
    )

    assign_process_to_job(proc, job)
    wait_for_handshake(timeout=startup_timeout)
    if handshake_fails:
        terminate_job(job)
        mark_start_failure(plugin_id)
        maybe_quarantine(plugin_id)
        return failure

    mark_healthy(plugin_id)
function cancel_request(plugin_id, request_id):
    send_control({
        kind: "cancel",
        correlation_id: request_id
    })
    wait cancel_grace
    if request_still_running:
        if plugin_process_is_healthy:
            continue waiting until request_deadline
        else:
            force_stop_plugin(plugin_id)
function force_stop_plugin(plugin_id):
    send_control({ kind: "request", method: "plugin.stop" })
    wait graceful_stop_timeout
    if process_still_alive:
        terminate_job_object(plugin_job)
    capture_exit_code_and_trailing_logs()
    update_health_counters()
function rollback_plugin(plugin_id):
    select_previous_known_good_env(plugin_id)
    repoint_active_version(plugin_id)
    mark_failed_env_ineligible()

Failure-mode catalog

Proposal. The host should classify and react to common failures like this:

Failure modeDetection pointRecommended reaction
Broken wheel archiveBundle verificationReject submission
Unsupported tag / wrong ABI / wrong archPre-install inspectionReject submission
Missing runtime familyInstall planningReject or defer until runtime installed
Dependency conflict with lock mismatchPre-installReject bundle as inconsistent
Native-load failure / missing DLL / VC++ runtimeValidation probe or startupFail install or quarantine version
Import side effect during startupStartup phaseFail startup; do not import during discovery
Startup hangHandshake timeoutKill job; increment crash-loop counter
Malformed messageProtocol codecNACK once; disconnect on repeated framing errors
Stdout contaminationRuntime log policyIgnore stdout for protocol; treat as noisy plugin
Crash loopHealth managerExponential backoff, then quarantine
Orphan child attemptJob/mitigation policyKeep within job or deny child creation
Disk exhaustionInstall or runtime monitoringStop install; do not corrupt active slot
Update failure after stagingPromotion phaseKeep old version active
Uninstall blocked by file lockUninstallStop process, retry delete, then deferred cleanup
Environment driftPeriodic auditReconcile against recorded hashes; quarantine on mismatch

SDK, testing, open questions, limitations, and sources

SDK and developer experience

Proposal. A Python plugin SDK should provide:

SDK componentWhy it matters
Typed protocol modelsPrevent schema drift between Python and C#
Entry-point helperReduce packaging mistakes
Decorators or registration helpersStandardize method exposure
Local runner CLIReproduce host launch and handshake locally
Bundle validator CLIFail fast on manifest/tag/hash mistakes
Test fakes for host transportLet authors simulate cancellation, streaming, and restart
Sample plugin + templateNormalize structure and metadata
Conformance fixturesMake failure reasons reproducible

Architecture synthesis. Type hints, JSON Schema, and a validation library such as Pydantic fit well together here: Python authors get clear runtime validation and generated schema shapes, while the host team gets explicit contracts to mirror in C# models and integration tests. Static analysis should be advisory for plugin code quality, but contract tests should be mandatory for admission.

Automated-test strategy

Proposal. Test coverage should span the full matrix of packaging, runtime, protocol, lifecycle, containment, and uninstall behavior. A practical matrix is:

Test familyExample cases
Package inspectionmalformed wheel, missing entry_points.txt, bad RECORD, duplicate import names
Resolver / lockunhashed dependency, extra wheel not in lock, wrong wheel filename tag
Runtime compatibilityx64 vs ARM64, pure-Python vs native wheel, abi3 vs CPython-minor-specific wheel
Lifecycleinstall, start, health, cancel, stop, update, rollback, disable, uninstall
Protocolbad JSON, bad frame length, duplicate request ID, out-of-order chunk sequence
Cancellationcooperative cancel, ignored cancel, cancel during stream, deadline expiration
Containmentchild-process denial, breakaway attempt, network-denied plugin, handle-leak attempt
Chaoshard crash, startup sleep, infinite stdout spam, disk flood, high memory, shutdown refusal
Drift / recoverydeleted runtime, altered environment file, missing DLL after install
Uninstallfile lock held, orphan child alive, partially removed environment

Proposal. Maintain intentionally bad fixture plugins and hard-code the expected rejection reason. Examples:

FixtureExpected rejection
bad_tag_nativeUnsupported wheel tag
has_sdist_dependencysdist not permitted
entrypoint_mismatchManifest and wheel disagree
prints_to_stdout_protocolProtocol contamination risk; runner policy violation
slow_startStartup timeout
fork_bomb_capabilitylessSubprocess capability denied
native_missing_runtimeNative dependency preflight failed
import_name_overlapImport shadowing risk
malformed_record_hashIntegrity failure
quarantine_on_crashloopExceeded crash policy threshold

Open implementation questions

Unknown. The biggest product-level choices that remain open are not Python-spec questions; they are platform-governance questions:

  • whether the host will support one CPython line or multiple lines at once;
  • whether free-threaded Python will be excluded for now;
  • whether high-risk plugins require AppContainer/LPAC or an even stronger broker/service boundary;
  • whether networked plugins are allowed at all, and if so under what policy model;
  • whether packaged executable sidecars are a normal exception path or a separate class of plugin.

Limitations

Limitations. Python packaging standards do not provide a complete standardized answer for plugin permissions, enterprise trust policy, or accelerator-library compatibility. pylock.toml is standardized, but support across installers is still evolving, and tool behaviors around locks, configuration files, and resolution remain partly tool-specific. Windows containment options beyond Job Objects and ACL basics can involve OS-version and packaging constraints that must be validated empirically against the chosen plugin runner and native-extension set.

Sources

Primary specifications and platform documentation used throughout this report include the PyPA specifications for pyproject.toml, core metadata, dependency specifiers, wheels, sdists, entry points, platform tags, installed-project recording, name normalization, and pylock.toml; CPython documentation for importlib.metadata, venv, ensurepip, Windows embedding, the embeddable distribution, and the Stable ABI; pip documentation for secure installs, repeatable installs, lock/download behavior, and offline wheelhouses; Microsoft documentation for long paths, Unicode and case behavior, CreateProcess and handle inheritance, Job Objects, process mitigations, named-pipe security, AppContainer, firewall rules, and Visual C++ redistributables; gRPC and JSON-RPC official protocol guidance; and supply-chain guidance from PyPI, CISA, and NIST.