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
Key topics
- Runtime
- .NET
- Python
- Semantic Systems
- Research Archive
- Strategy
- Audit
- Architecture
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: 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.
Recommended artifact shape
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:
| Identifier | Purpose | Mutability | Authority |
|---|---|---|---|
| Distribution name | Python package identity used by packaging tools | Can differ from plugin ID | Python metadata |
| Plugin ID | Stable host-level plugin identity | Immutable once published | Host manifest |
| Publisher ID | Stable issuer identity | Immutable | Host trust system |
| Distribution version | Python package release version | Changes each release | Python metadata |
| Contract version | Host protocol/API compatibility line | Changes less often | Host manifest |
| Runtime version | Interpreter family targeted by the plugin | Changes with ABI/runtime needs | Host 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:
- Read the host manifest from the bundle.
- Open the wheel as a ZIP archive.
- Validate wheel filename tags and
.dist-infopresence. - Read
METADATA,WHEEL,RECORD, andentry_points.txt. - Cross-check the manifest’s declared entry point against the wheel metadata.
- 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 strategy | Security boundary | Dependency isolation | Native package support | Startup cost | Streaming | Cancellation | Crash containment | Packaging complexity | Update ownership clarity | Offline deployment | Diagnostics | Developer burden |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| System Python | 1 | 2 | 4 | 4 | 4 | 4 | 4 | 2 | 1 | 2 | 3 | 4 |
| Bundled CPython shared by all plugins | 2 | 3 | 4 | 4 | 4 | 4 | 4 | 3 | 5 | 5 | 4 | 4 |
Bundled CPython + per-plugin venv | 2 | 5 | 4 | 3 | 5 | 5 | 5 | 4 | 5 | 5 | 5 | 4 |
| Shared managed environments | 2 | 2 | 4 | 4 | 4 | 4 | 4 | 3 | 4 | 4 | 3 | 3 |
| Embeddable distribution vendored per plugin | 2 | 5 | 3 | 3 | 2 | 2 | 1 | 5 | 4 | 5 | 2 | 2 |
pipx-style per-app envs | 2 | 4 | 3 | 3 | 4 | 4 | 5 | 3 | 3 | 3 | 3 | 4 |
| conda-managed envs | 2 | 5 | 5 | 2 | 5 | 5 | 5 | 5 | 3 | 4 | 4 | 2 |
| PyInstaller sidecar exe | 2 | 5 | 4 | 4 | 4 | 4 | 5 | 5 | 2 | 5 | 2 | 3 |
| Nuitka sidecar exe | 2 | 5 | 4 | 4 | 4 | 4 | 5 | 5 | 2 | 5 | 2 | 2 |
| External service or container-like boundary | 4 | 5 | 4 | 1 | 5 | 5 | 5 | 5 | 4 | 3 | 4 | 1 |
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, IPC, and recommended architecture
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 model | Security boundary | Dependency isolation | Native package support | Startup cost | Streaming | Cancellation | Crash containment | Packaging complexity | Update ownership clarity | Offline deployment | Diagnostics | Developer burden |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Embed CPython in .NET host | 1 | 1 | 4 | 5 | 2 | 2 | 1 | 2 | 3 | 4 | 2 | 3 |
| Python.NET in host process | 1 | 1 | 3 | 4 | 2 | 2 | 1 | 3 | 3 | 4 | 2 | 3 |
| Isolated Python process | 2 | 5 | 4 | 3 | 5 | 5 | 5 | 4 | 5 | 5 | 5 | 4 |
| Packaged executable sidecar | 2 | 5 | 4 | 4 | 5 | 5 | 5 | 5 | 2 | 5 | 3 | 3 |
| External brokered service / container-like boundary | 4 | 5 | 4 | 1 | 5 | 5 | 5 | 5 | 4 | 3 | 4 | 1 |
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 strategy | Security boundary | Dependency isolation | Native package support | Startup cost | Streaming | Cancellation | Crash containment | Packaging complexity | Update ownership clarity | Offline deployment | Diagnostics | Developer burden |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| stdio with framed protocol | 2 | 5 | 5 | 5 | 3 | 4 | 5 | 2 | 5 | 5 | 2 | 5 |
| Named pipes + framed protocol | 4 | 5 | 5 | 4 | 5 | 5 | 5 | 3 | 5 | 5 | 5 | 4 |
| Unix domain sockets on Windows | 3 | 5 | 5 | 4 | 5 | 5 | 5 | 4 | 5 | 5 | 4 | 3 |
| gRPC over named pipe/UDS adapter | 3 | 5 | 5 | 2 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 3 |
| JSON-RPC over named pipes | 4 | 5 | 5 | 4 | 4 | 4 | 5 | 3 | 5 | 5 | 5 | 4 |
| HTTP loopback | 2 | 5 | 5 | 3 | 5 | 4 | 5 | 4 | 5 | 5 | 4 | 4 |
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.
Recommended process and protocol design
Proposal. The default runtime shape should be:
- C# host launches a plugin runner process by absolute path inside the plugin’s own virtual environment.
- The process joins a Job Object immediately.
- The host provides a dedicated control channel over a named pipe.
- The plugin writes human logs only to stderr or to a separate log/event pipe.
- 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.hellowithin 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:
| Control | Feasibility | What it helps with | Practical constraint |
|---|---|---|---|
| Job Object with kill-on-close | High | Orphans, crash cleanup, forced stop | Does not by itself sandbox file/network access |
| Job Object memory / CPU / active-process limits | High | Runaway resource use | Tune to avoid false positives for native workloads |
| Explicit handle inheritance list | High | Prevent leaked privileged handles | Requires careful launcher implementation |
| Named-pipe ACLs and session scoping | High | Restrict who can connect to control channel | Must avoid permissive default descriptors |
| Dedicated plugin directories with locked-down ACLs | High | Prevent cross-plugin file tampering | Same-user write access outside those folders still exists |
| Windows Firewall app rules | Medium | Block or scope plugin network access | Often requires admin or enterprise policy cooperation |
| Restricted token | Medium | Reduce ambient privileges | Compatibility testing needed for native extensions |
| AppContainer / LPAC | Medium | Stronger OS-enforced containment | More packaging/compatibility complexity, especially for unpackaged native-heavy sidecars |
| Win32 app isolation | Medium / evolving | Stronger brokered isolation on recent Windows | Newer 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:
| State | Meaning | Host action |
|---|---|---|
| Submitted | Artifact received but not trusted | Store in quarantine |
| Verified | Signature, hashes, manifest, wheel metadata, policy checks passed | Permit install |
| Installed | Environment created and promoted atomically | Record immutable environment ID |
| Configured | Host-side config stored | Do not write into plugin package files |
| Starting | Process launched, handshake pending | Enforce startup timeout |
| Healthy | Handshake complete, heartbeat flowing | Accept work |
| Degraded | Heartbeats late or resource thresholds crossed | Reduce traffic, surface diagnostics |
| Quarantined | Plugin disabled due to policy or health | Deny launch |
| Updating | New environment staged alongside old | Preserve rollback slot |
| Disabled | Administrator or policy disabled | Skip launch |
| Uninstalling | Removal in progress | Stop 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 mode | Detection point | Recommended reaction |
|---|---|---|
| Broken wheel archive | Bundle verification | Reject submission |
| Unsupported tag / wrong ABI / wrong arch | Pre-install inspection | Reject submission |
| Missing runtime family | Install planning | Reject or defer until runtime installed |
| Dependency conflict with lock mismatch | Pre-install | Reject bundle as inconsistent |
| Native-load failure / missing DLL / VC++ runtime | Validation probe or startup | Fail install or quarantine version |
| Import side effect during startup | Startup phase | Fail startup; do not import during discovery |
| Startup hang | Handshake timeout | Kill job; increment crash-loop counter |
| Malformed message | Protocol codec | NACK once; disconnect on repeated framing errors |
| Stdout contamination | Runtime log policy | Ignore stdout for protocol; treat as noisy plugin |
| Crash loop | Health manager | Exponential backoff, then quarantine |
| Orphan child attempt | Job/mitigation policy | Keep within job or deny child creation |
| Disk exhaustion | Install or runtime monitoring | Stop install; do not corrupt active slot |
| Update failure after staging | Promotion phase | Keep old version active |
| Uninstall blocked by file lock | Uninstall | Stop process, retry delete, then deferred cleanup |
| Environment drift | Periodic audit | Reconcile 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 component | Why it matters |
|---|---|
| Typed protocol models | Prevent schema drift between Python and C# |
| Entry-point helper | Reduce packaging mistakes |
| Decorators or registration helpers | Standardize method exposure |
| Local runner CLI | Reproduce host launch and handshake locally |
| Bundle validator CLI | Fail fast on manifest/tag/hash mistakes |
| Test fakes for host transport | Let authors simulate cancellation, streaming, and restart |
| Sample plugin + template | Normalize structure and metadata |
| Conformance fixtures | Make 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 family | Example cases |
|---|---|
| Package inspection | malformed wheel, missing entry_points.txt, bad RECORD, duplicate import names |
| Resolver / lock | unhashed dependency, extra wheel not in lock, wrong wheel filename tag |
| Runtime compatibility | x64 vs ARM64, pure-Python vs native wheel, abi3 vs CPython-minor-specific wheel |
| Lifecycle | install, start, health, cancel, stop, update, rollback, disable, uninstall |
| Protocol | bad JSON, bad frame length, duplicate request ID, out-of-order chunk sequence |
| Cancellation | cooperative cancel, ignored cancel, cancel during stream, deadline expiration |
| Containment | child-process denial, breakaway attempt, network-denied plugin, handle-leak attempt |
| Chaos | hard crash, startup sleep, infinite stdout spam, disk flood, high memory, shutdown refusal |
| Drift / recovery | deleted runtime, altered environment file, missing DLL after install |
| Uninstall | file lock held, orphan child alive, partially removed environment |
Proposal. Maintain intentionally bad fixture plugins and hard-code the expected rejection reason. Examples:
| Fixture | Expected rejection |
|---|---|
bad_tag_native | Unsupported wheel tag |
has_sdist_dependency | sdist not permitted |
entrypoint_mismatch | Manifest and wheel disagree |
prints_to_stdout_protocol | Protocol contamination risk; runner policy violation |
slow_start | Startup timeout |
fork_bomb_capabilityless | Subprocess capability denied |
native_missing_runtime | Native dependency preflight failed |
import_name_overlap | Import shadowing risk |
malformed_record_hash | Integrity failure |
quarantine_on_crashloop | Exceeded 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.