From 60eddc6eb56de2738eeb3330a44bedc369427031 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:55:13 -0500 Subject: [PATCH 01/11] Add Python adapter RFC --- rfc/draft/0001-python-adapter.md | 486 +++++++++++++++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 rfc/draft/0001-python-adapter.md diff --git a/rfc/draft/0001-python-adapter.md b/rfc/draft/0001-python-adapter.md new file mode 100644 index 000000000..e63a79bc9 --- /dev/null +++ b/rfc/draft/0001-python-adapter.md @@ -0,0 +1,486 @@ +--- +RFC: 0001 +Author: ThomasNieto +Sponsor: null +Status: Draft +SupercededBy: null +Version: 1.0 +Area: Adapter +CommentsDue: null +--- + +# Microsoft.Adapter/Python: Python DSC Resource Adapter + +This RFC describes the design of the `Microsoft.Adapter/Python` DSC v3 adapter, +the `ms-dsc` Python SDK for resource authors, and the `Microsoft.Python/Discover` +discovery extension. Together these components allow DSC resources to be written +in Python and discovered/invoked through the standard DSC engine pipeline. + +## Motivation + +> As a system administrator or developer, +> I want to write DSC resources in Python using familiar language patterns, +> so that I can manage system state with DSC without learning Rust or PowerShell. + +Python is a widely-used language for system automation, and a first-class Python +adapter lowers the barrier to writing portable DSC resources. Key goals are: + +1. **Zero friction** — resource authors install one package (`ms-dsc`) and follow + familiar Python patterns (dataclasses, typing, logging). +2. **No mandatory Rust or PowerShell dependency** — the entire adapter runtime is pure + Python, stdlib only; it ships alongside the DSC binary. +3. **Discoverable by default** — resources are auto-discovered without needing to + maintain hand-written manifest files. +4. **Idiomatic Python** — the SDK leverages dataclasses, type hints, structural + protocols, and entry points. + +## Proposed experience + +A resource author creates a Python package with `ms-dsc` as a build-time +dependency: + +```toml +[build-system] +requires = ["hatchling", "ms-dsc"] +build-backend = "hatchling.build" + +[tool.hatch.build.hooks.dsc] +# Generates *.dsc.adaptedResource.json manifests at wheel-build time + +[project] +dependencies = [] # ms-dsc is provided at runtime by DSC +``` + +They implement their resource by inheriting from `DscResource[T]` and implementing +the capability protocols they need: + +```python +from dataclasses import dataclass, field +from collections.abc import Iterator +from ms_dsc import DscResource, dsc_resource, SetResult, TestResult +from ms_dsc.metadata import SetReturn, TestReturn +from ms_dsc.schema import DataclassSchemaProvider + +@dataclass +class GreetingSchema: + name: str = field(metadata={"description": "Name to greet."}) + message: str = field(default="", metadata={"description": "Greeting message."}) + +@dsc_resource( + type="Example/Greeting", + version="1.0.0", + description="A resource that manages greeting messages.", + tags=["example"], + set_return=SetReturn.STATE_AND_DIFF, + test_return=TestReturn.STATE_AND_DIFF, +) +class GreetingResource(DscResource[GreetingSchema]): + schema_provider = DataclassSchemaProvider(GreetingSchema) + + def get(self, instance: GreetingSchema) -> GreetingSchema: + return GreetingSchema(name=instance.name, message=f"Hello, {instance.name}!") + + def set(self, instance: GreetingSchema) -> SetResult[GreetingSchema]: + actual = self.get(instance) + changed = [f for f in ("message",) if getattr(actual, f) != getattr(instance, f)] + return SetResult(actual_state=actual, changed_properties=changed) + + def test(self, instance: GreetingSchema) -> TestResult[GreetingSchema]: + actual = self.get(instance) + diffs = [f for f in ("message",) if getattr(actual, f) != getattr(instance, f)] + return TestResult(actual_state=actual, differing_properties=diffs) + + def export(self, instance: GreetingSchema | None) -> Iterator[GreetingSchema]: + for name in ("Alice", "Bob"): + yield self.get(GreetingSchema(name=name)) +``` + +After the package is built and installed, DSC automatically discovers the resource +through the `Microsoft.Python/Discover` extension. Manifests can also be generated +manually: + +```bash +dsc-gen manifest +``` + +## Specification + +### Components + +Three cooperating components implement the Python adapter: + +| Component | Shipped as | Purpose | +|-----------|------------|---------| +| `pyadapter` | Bundled with DSC | Adapter runtime invoked by DSC per operation | +| `ms-dsc` SDK | PyPI + bundled with DSC | Used by resource authors; provides `DscResource`, protocols, and schema generation | +| `Microsoft.Python/Discover` | Bundled with DSC | Discovery extension; scans Python distributions at DSC startup | + +The `ms-dsc` SDK is bundled alongside `pyadapter` in the DSC install directory, +providing the SDK at runtime so resource packages do not need to declare it as a +runtime dependency. + +### Platform manifests + +Two adapter manifests provide cross-platform support: + +| Manifest | Platform(s) | Executable | +|----------|-------------|-----------| +| `python.dsc.resource.json` | Windows | `python` | +| `python3.dsc.resource.json` | Linux, macOS | `python3` | + +Both declare the resource type `Microsoft.Adapter/Python`. Only the appropriate +manifest is included in each platform's package. + +### SDK public API + +#### `DscResource[T]` + +Base class for all Python DSC resources. `T` is the schema type (dataclass or +Pydantic model) that defines the resource's state. + +#### Capability protocols + +Capabilities are declared by implementing the corresponding methods. No explicit +interface inheritance is required. + +| Protocol | Method signature | DSC capability | +|----------|-----------------|----------------| +| `Gettable` | `get(self, instance: T) -> T` | `get` | +| `Settable` | `set(self, instance: T) -> SetResult[T]` | `set` | +| `Testable` | `test(self, instance: T) -> TestResult[T]` | `test` | +| `Deletable` | `delete(self, instance: T) -> None` | `delete` | +| `Exportable` | `export(self, instance: T \| None) -> Iterator[T]` | `export` | + +#### `@dsc_resource` decorator + +Annotates a `DscResource` subclass with its DSC type identifier and behavioural +metadata: + +```python +@dsc_resource( + type="Vendor/ResourceName", # Required: DSC resource type identifier + version="1.0.0", # Required: semver string + description="...", # Optional: resource description + tags=["tag1", "tag2"], # Optional: list of tags for discovery filtering + set_return=SetReturn.STATE, # Optional: STATE (default) or STATE_AND_DIFF + test_return=TestReturn.STATE, # Optional: STATE (default) or STATE_AND_DIFF +) +``` + +#### Return types + +```python +@dataclass +class SetResult(Generic[T]): + actual_state: T + changed_properties: list[str] # Required when set_return=STATE_AND_DIFF + +@dataclass +class TestResult(Generic[T]): + actual_state: T + differing_properties: list[str] # Required when test_return=STATE_AND_DIFF +``` + +#### Schema providers + +| Provider | Schema source | Additional requirement | +|----------|--------------|----------------------| +| `DataclassSchemaProvider` | Python dataclass | None (stdlib only) | +| `PydanticSchemaProvider` | Pydantic model | `pydantic` package | + +#### Field metadata specification + +**DataclassSchemaProvider:** Supports the following field metadata keys in the `metadata` dict: + +| Key | Type | JSON schema target | Description | +|-----|------|-------------------|-------------| +| `description` | `str` | `description` | Human-readable description of the field for documentation. | +| `title` | `str` | `title` | Short display title for the field. | +| `examples` | `list` | `examples` | Array of example values for the field. | + +**Example with dataclass:** + +```python +from dataclasses import dataclass, field + +@dataclass +class HostConnectionSchema: + hostname: str = field( + metadata={ + "description": "The target hostname or IP address.", + "title": "Host", + "examples": ["example.com", "192.168.1.1"] + } + ) + port: int = field( + default=22, + metadata={"description": "The SSH port to use.", "title": "Port"} + ) +``` + +**Generated JSON schema:** + +```json +{ + "type": "object", + "properties": { + "hostname": { + "type": "string", + "description": "The target hostname or IP address.", + "title": "Host", + "examples": ["example.com", "192.168.1.1"] + }, + "port": { + "type": "integer", + "description": "The SSH port to use.", + "title": "Port", + "default": 22 + } + }, + "required": ["hostname"] +} +``` + +**PydanticSchemaProvider:** Delegates to Pydantic's `model_json_schema()` and supports all +Pydantic v2 field metadata and configuration options. See the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/json_schema/) +for complete reference. + +Unknown metadata keys are silently ignored by `DataclassSchemaProvider` during schema generation. + +### Adapted resource manifest format + +Manifests are generated by `dsc-gen manifest` and packaged as package data at +`/dsc/*.dsc.adaptedResource.json`. The `content` field is a JSON +object that encodes the Python module and class used for operation dispatch: + +```json +{ + "manifestVersion": "1.0", + "type": "Vendor/ResourceName", + "version": "1.0.0", + "description": "A resource that manages greeting messages.", + "tags": ["example"], + "adapter": { + "type": "Microsoft.Adapter/Python" + }, + "content": { + "module": "vendor_resource.resource", + "class": "ResourceClass" + }, + "get": { "input": "stdin" }, + "set": { "input": "stdin", "return": "stateAndDiff" }, + "test": { "input": "stdin", "return": "stateAndDiff" }, + "schema": { + "embedded": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name to greet." + }, + "message": { + "type": "string", + "description": "Greeting message.", + "default": "" + } + }, + "required": ["name"] + } + } +} +``` + +#### Runtime-aware cached manifests + +When resources are discovered at runtime, the adapter adds two additional fields +to the `content` object before caching: + +- `pythonExecutable` — the Python executable used to discover and invoke this resource (e.g., `python3.11`, `/usr/bin/python3.12`) +- `venv` — the virtual environment path where this resource was found (e.g., `/opt/venv1`) + +These fields are **only present in cached adapted resource manifests** and are never +included in shipped manifests. They are added during discovery and enable the adapter +to invoke the resource in the correct Python runtime and virtual environment context. + +**Cached manifest example:** + +```json +{ + "manifestVersion": "1.0", + "type": "Example/Greeting", + "version": "1.0.0", + "adapter": { "type": "Microsoft.Adapter/Python" }, + "content": { + "module": "dsc_example_resource.resources", + "class": "GreetingResource", + "pythonExecutable": "python3.11", + "venv": "/opt/venv1" + }, + "get": { "input": "stdin" }, + "schema": { "embedded": { } } +} +``` + +### Stdin/stdout contract + +The adapter reads a JSON object from stdin and writes results to stdout as +newline-delimited JSON (NDJSON). Unknown input fields are silently ignored. + +| Operation | stdin | stdout lines | +|-----------|-------|-------------| +| `get` | Desired state | 1 — actual state | +| `set` (STATE) | Desired state | 1 — actual state | +| `set` (STATE_AND_DIFF) | Desired state | 2 — actual state, then `["changedProp", ...]` | +| `test` (STATE) | Desired state | 1 — actual state | +| `test` (STATE_AND_DIFF) | Desired state | 2 — actual state, then `["differingProp", ...]` | +| `delete` | Desired state | 0 | +| `export` | Filter or `{}` | 0..N — one object per instance | + +### Discovery mechanism + +Two discovery paths are supported: + +**Extension-based (preferred):** The `Microsoft.Python/Discover` extension scans +installed Python distributions for `*.dsc.adaptedResource.json` files packaged +as data in a `/dsc/` directory. This requires the resource to ship +manifests generated by `dsc-gen manifest`. + +**List command (fallback):** The adapter's `list` command enumerates Python +distributions that declare a `microsoft.dsc.resources` entry point group, and +returns the resource list to DSC. This supports development installs and resources +without pre-built manifests. + +### Logging contract + +Resource authors use Python's standard `logging` module. The adapter translates +log records to DSC's structured JSON stderr format before dispatching any +operation: + +```json +{"info": "vendor_resource.resource: Getting /tmp/hello.txt"} +``` + +Log verbosity is controlled by the `DSC_TRACE_LEVEL` environment variable +(`trace` / `debug` / `info` / `warn` / `error`). + +### Discovery and invocation flow + +```mermaid +flowchart TD + A["DSC Engine
requests resource list"] --> B["Python Adapter
pyadapter"] + + B --> C{DSC_PYTHON_EXECUTABLE
set?} + C -->|Yes| D["Use specified
Python exe"] + C -->|No| E["Use system python
python3 Unix / python Windows"] + + D --> F["Resolve python_exe"] + E --> F + + F --> G{DSC_VENV_PATHS
set?} + + G -->|No| H["Scan system site-packages"] + H --> I["Scan for *.dsc.adaptedResource.json
and entry points"] + I --> J["Generate adapted resource manifests"] + J --> K["Deduplicate by resource type"] + + G -->|Yes| L["Parse VENV paths
Windows: ; Unix: :"] + L --> M["Initialize VENV loop"] + M --> N["Get next VENV path"] + + N --> O{VENV
exists?} + O -->|No| P["Skip, log warning"] + P --> Q{More
VENVs?} + + O -->|Yes| R["Scan for *.dsc.adaptedResource.json
and entry points"] + R --> S["Generate adapted resource manifests"] + S --> Q + + Q -->|Yes| N + Q -->|No| K + + K --> T["Add pythonExecutable
and venv to content"] + T --> U["Cache all manifests"] + + U --> V["Return adapted resource manifests
to DSC"] + V --> W["DSC invokes resource operation"] + + W --> X["Adapter reads pythonExecutable
and venv from manifest"] + X --> Y{venv
defined?} + + Y -->|Yes| Z["Spawn subprocess with python_exe
activated in venv"] + Y -->|No| AA["Spawn subprocess with python_exe"] + + Z --> AB["Execute resource operation"] + AA --> AB + AB --> AC["Return results to DSC"] +``` + +### Multi-runtime and virtual environment support + +The adapter supports resource discovery and execution across multiple Python runtimes +and virtual environments, enabling operators to manage resource placement and isolation +independently of package installation. + +#### Operator configuration + +Operators control runtime and VENV behavior via environment variables: + +| Variable | Platform | Purpose | Example | +|----------|----------|---------|----------| +| `DSC_PYTHON_EXECUTABLE` | All | Specifies the Python executable to use | `python3.11`, `/usr/bin/python3.12` | +| `DSC_VENV_PATHS` | All | Platform-delimited list of virtual environment paths | Windows: `C:\venv1;C:\venv2` / Unix: `/opt/venv1:/opt/venv2` | + +**Defaults:** +- If `DSC_PYTHON_EXECUTABLE` is not set, the adapter uses the system Python (`python` on Windows, `python3` on Unix) +- If `DSC_VENV_PATHS` is not set, resource discovery searches only system site-packages + +**Path delimiters:** +- Windows: semicolon (`;`) +- Unix (Linux, macOS): colon (`:`) + +#### Discovery flow + +1. Resolve the Python executable from `DSC_PYTHON_EXECUTABLE` env var; fallback to system default +2. If `DSC_VENV_PATHS` is set, parse it into a list of paths using the platform-specific delimiter +3. For each VENV path (in order): + - Validate the VENV exists; skip with a warning if not + - Scan for pre-built `*.dsc.adaptedResource.json` manifests in `/lib/pythonX.Y/site-packages/*/dsc/` + - If no manifests found, run the adapter's `list` command in a subprocess with that VENV activated + - Collect all discovered resources and tag them with the source VENV path +4. If no VENVs specified, scan system site-packages using the same process +5. Deduplicate resources by type and version; last VENV wins (priority order respects list order) +6. Cache all discovered manifests with the `pythonExecutable` and `venv` fields added to each +7. Cache key includes the hash of (pythonExecutable, VENV paths) to invalidate when configuration changes + +#### Runtime invocation + +When DSC invokes a resource operation: + +1. The adapter loads the cached adapted resource manifest and reads `pythonExecutable` and `venv` +2. Spawns a subprocess using the specified `pythonExecutable` +3. If `venv` is present, activates the VENV by setting `VIRTUAL_ENV` and adjusting `PATH` in the subprocess environment +4. Executes the resource operation (get, set, test, etc.) in that Python context + +#### Error handling + +| Scenario | Behavior | +|----------|----------| +| VENV path in `DSC_VENV_PATHS` doesn't exist | Skip with warning; continue to next VENV | +| Python executable not found | Fallback to system default; log warning | +| Permission denied on VENV | Skip with warning; continue | +| No resources found in any VENV | Return empty list | +| Invalid `DSC_PYTHON_EXECUTABLE` path | Raise error; operator must fix configuration | +| Multiple VENVs have the same resource type | Use first match (respects priority order) | + +## Alternate Proposals and Considerations + +### Alternative A: Single Python file adapter + +A single-file adapter is simpler to ship but limits testability and extensibility. +Rejected in favour of the package-based adapter structure. + +### Alternative B: Require Pydantic for all resources + +Pydantic provides excellent runtime validation. Rejected as a hard requirement +because many resources are simple and don't need Pydantic's overhead. Pydantic +remains an optional, fully-supported schema backend. From 28baa2e526976405caf63329428f3c1d5f3f040f Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:56:25 -0500 Subject: [PATCH 02/11] Update RFC --- rfc/draft/0001-python-adapter.md | 229 ++++++++++++++++++------------- 1 file changed, 136 insertions(+), 93 deletions(-) diff --git a/rfc/draft/0001-python-adapter.md b/rfc/draft/0001-python-adapter.md index e63a79bc9..79aac0a6d 100644 --- a/rfc/draft/0001-python-adapter.md +++ b/rfc/draft/0001-python-adapter.md @@ -1,7 +1,7 @@ --- RFC: 0001 -Author: ThomasNieto -Sponsor: null +Author: "@ThomasNieto" +Sponsor: "@ThomasNieto" Status: Draft SupercededBy: null Version: 1.0 @@ -290,37 +290,82 @@ object that encodes the Python module and class used for operation dispatch: } ``` -#### Runtime-aware cached manifests +#### Adapter-generated manifest cache -When resources are discovered at runtime, the adapter adds two additional fields -to the `content` object before caching: +Manifests discovered via the extension (shipped with packages) are NOT cached; they +are referenced in-place and remain immutable for signature verification. -- `pythonExecutable` — the Python executable used to discover and invoke this resource (e.g., `python3.11`, `/usr/bin/python3.12`) -- `venv` — the virtual environment path where this resource was found (e.g., `/opt/venv1`) +Manifests generated at runtime by the adapter's `list` command (for resources +discovered via entry points) are cached to avoid regeneration on every discovery. -These fields are **only present in cached adapted resource manifests** and are never -included in shipped manifests. They are added during discovery and enable the adapter -to invoke the resource in the correct Python runtime and virtual environment context. +Runtime venv bindings for all discovered resources (both shipped and generated) are +stored in a separate venv index file (see below), enabling DSC to invoke resources +in the correct virtual environment context while keeping shipped manifests immutable +and signature-safe. -**Cached manifest example:** +**Adapter-generated manifest cache locations:** +- **Windows:** `%LOCALAPPDATA%\dsc\python-adapter-manifest-cache.json` +- **Linux/macOS:** `$HOME/.dsc/python-adapter-manifest-cache.json` + +**Cache format:** +A JSON object mapping resource identifiers (`Type@Version`) to their generated manifests: ```json { - "manifestVersion": "1.0", - "type": "Example/Greeting", - "version": "1.0.0", - "adapter": { "type": "Microsoft.Adapter/Python" }, - "content": { - "module": "dsc_example_resource.resources", - "class": "GreetingResource", - "pythonExecutable": "python3.11", - "venv": "/opt/venv1" - }, - "get": { "input": "stdin" }, - "schema": { "embedded": { } } + "version": "1.0", + "manifests": { + "Example/Greeting@1.0.0": { /* full adapted resource manifest */ }, + "Example/Service@1.0.0": { /* full adapted resource manifest */ } + } } ``` +#### Virtual environment index cache + +Virtual environment bindings are stored in a separate index file to keep manifests +immutable and signature-safe. The index maps resource types to their virtual environment +paths. Cache is stored per-user (consistent with the PowerShell adapter pattern). + +**Index file locations:** +- **Windows:** `%LOCALAPPDATA%\dsc\python-venv-index.json` +- **Linux/macOS:** `$HOME/.dsc/python-venv-index.json` + +**Index file format:** + +The index maps each discovered resource (identified by type and version) to its +source virtual environment(s) using the canonical `Type@Version` format. This allows +multiple versions of the same resource to be discovered across different VENVs, and +multiple locations for the same resource version (for DSC engine to select); +DSC engine handles deduplication and selection. + +```json +{ + "version": "1.0", + "index": { + "Example/Greeting@1.0.0": ["/opt/venv1", "/opt/venv2"], + "Example/Greeting@2.0.0": ["/opt/venv2"], + "Example/Service@1.0.0": ["/opt/venv2"], + "Example/Config@1.0.0": null + } +} +``` + +| Field | Description | +|-------|-------------| +| `version` | Index format version | +| `index` | Object mapping resource identifiers to venv path(s). Format: `Type@Version` → string[] or null (e.g., `"Microsoft.Windows/Service@1.0.0"` → `["/path/to/venv"]` or `["/venv1", "/venv2"]` for multiple locations; `null` for system Python). Multiple versions and multiple locations of the same resource can coexist. | + +**Venv index generation:** +- Created during discovery after all resources are found (both via extension and adapter list command) +- Never shipped with packages; runtime-only cache artifact + +**Index invalidation:** +The venv index is maintained across discovery cycles, but validated each time: +- On each discovery cycle, verify all venv paths in the cached index still exist on the filesystem +- Remove any venv entries where paths no longer exist +- If new venvs appear in `DSC_PYTHON_VIRTUAL_ENV_PATH`, discover resources from those venvs and add to index +- If all venvs are gone or the index is empty, fall back to system Python only and log a warning + ### Stdin/stdout contract The adapter reads a JSON object from stdin and writes results to stdout as @@ -347,8 +392,8 @@ manifests generated by `dsc-gen manifest`. **List command (fallback):** The adapter's `list` command enumerates Python distributions that declare a `microsoft.dsc.resources` entry point group, and -returns the resource list to DSC. This supports development installs and resources -without pre-built manifests. +returns the resource list to DSC. This generates manifests at runtime for resources +discovered via entry points. ### Logging contract @@ -363,56 +408,35 @@ operation: Log verbosity is controlled by the `DSC_TRACE_LEVEL` environment variable (`trace` / `debug` / `info` / `warn` / `error`). -### Discovery and invocation flow +### Adapter-generated manifest discovery flow + +The following flow describes discovery of resources via the adapter's `list` command +(entry-point discovered resources without pre-built manifests). Shipped manifests +from the discovery extension follow the standard DSC discovery priority. ```mermaid flowchart TD - A["DSC Engine
requests resource list"] --> B["Python Adapter
pyadapter"] - - B --> C{DSC_PYTHON_EXECUTABLE
set?} - C -->|Yes| D["Use specified
Python exe"] - C -->|No| E["Use system python
python3 Unix / python Windows"] - - D --> F["Resolve python_exe"] - E --> F - - F --> G{DSC_VENV_PATHS
set?} + A["DSC Engine
adapter resource list"] --> B["Python Adapter
pyadapter"] - G -->|No| H["Scan system site-packages"] - H --> I["Scan for *.dsc.adaptedResource.json
and entry points"] - I --> J["Generate adapted resource manifests"] - J --> K["Deduplicate by resource type"] + B --> C{DSC_PYTHON_VIRTUAL_ENV_PATH
set?} - G -->|Yes| L["Parse VENV paths
Windows: ; Unix: :"] - L --> M["Initialize VENV loop"] - M --> N["Get next VENV path"] + C -->|No| E["Discover resources via entry points"] + E --> F["Generate adapted resource manifests"] + F --> Q["Cache manifests"] - N --> O{VENV
exists?} - O -->|No| P["Skip, log warning"] - P --> Q{More
VENVs?} + C -->|Yes| K{VENV
exists?} + K -->|No| L["Skip, log warning"] + L --> M{More
VENVs?} - O -->|Yes| R["Scan for *.dsc.adaptedResource.json
and entry points"] - R --> S["Generate adapted resource manifests"] - S --> Q + K -->|Yes| N["Discover resources via entry points"] + N --> O["Generate adapted resource manifests"] + O --> M - Q -->|Yes| N - Q -->|No| K + M -->|Yes| K + M -->|No| P["Build venv index
mapping types to venvs"] + P --> Q - K --> T["Add pythonExecutable
and venv to content"] - T --> U["Cache all manifests"] - - U --> V["Return adapted resource manifests
to DSC"] - V --> W["DSC invokes resource operation"] - - W --> X["Adapter reads pythonExecutable
and venv from manifest"] - X --> Y{venv
defined?} - - Y -->|Yes| Z["Spawn subprocess with python_exe
activated in venv"] - Y -->|No| AA["Spawn subprocess with python_exe"] - - Z --> AB["Execute resource operation"] - AA --> AB - AB --> AC["Return results to DSC"] + Q --> R["Return generated manifests
to DSC"] ``` ### Multi-runtime and virtual environment support @@ -423,54 +447,73 @@ independently of package installation. #### Operator configuration -Operators control runtime and VENV behavior via environment variables: +Operators control VENV behavior via environment variables: | Variable | Platform | Purpose | Example | |----------|----------|---------|----------| -| `DSC_PYTHON_EXECUTABLE` | All | Specifies the Python executable to use | `python3.11`, `/usr/bin/python3.12` | -| `DSC_VENV_PATHS` | All | Platform-delimited list of virtual environment paths | Windows: `C:\venv1;C:\venv2` / Unix: `/opt/venv1:/opt/venv2` | +| `DSC_PYTHON_VIRTUAL_ENV_PATH` | All | Platform-delimited list of virtual environment paths | Windows: `C:\venv1;C:\venv2` / Unix: `/opt/venv1:/opt/venv2` | **Defaults:** -- If `DSC_PYTHON_EXECUTABLE` is not set, the adapter uses the system Python (`python` on Windows, `python3` on Unix) -- If `DSC_VENV_PATHS` is not set, resource discovery searches only system site-packages +- If `DSC_PYTHON_VIRTUAL_ENV_PATH` is not set, resource discovery searches only system site-packages **Path delimiters:** -- Windows: semicolon (`;`) -- Unix (Linux, macOS): colon (`:`) +- Uses the platform's standard path separator (e.g., `;` on Windows, `:` on Unix) -#### Discovery flow +#### Adapter list command discovery flow -1. Resolve the Python executable from `DSC_PYTHON_EXECUTABLE` env var; fallback to system default -2. If `DSC_VENV_PATHS` is set, parse it into a list of paths using the platform-specific delimiter -3. For each VENV path (in order): +When DSC engine falls back to the adapter's `list` command for discovery: + +1. If `DSC_PYTHON_VIRTUAL_ENV_PATH` is set, parse it into a list of paths using the platform-specific delimiter +2. For each VENV path (in order): - Validate the VENV exists; skip with a warning if not - - Scan for pre-built `*.dsc.adaptedResource.json` manifests in `/lib/pythonX.Y/site-packages/*/dsc/` - - If no manifests found, run the adapter's `list` command in a subprocess with that VENV activated - - Collect all discovered resources and tag them with the source VENV path -4. If no VENVs specified, scan system site-packages using the same process -5. Deduplicate resources by type and version; last VENV wins (priority order respects list order) -6. Cache all discovered manifests with the `pythonExecutable` and `venv` fields added to each -7. Cache key includes the hash of (pythonExecutable, VENV paths) to invalidate when configuration changes + - Run the adapter's `list` command in a subprocess with that VENV activated to discover resources via entry points + - Collect all discovered resources and track their source VENV path +3. If no VENVs specified, run the adapter's `list` command to discover resources via entry points in system site-packages (venv=`null`) +4. Generate adapted resource manifests for all discovered resources (no deduplication) +5. Cache generated manifests to avoid regeneration on next discovery +6. Build a venv index mapping each resource's canonical identifier `Type@Version` to its source VENV path +7. Validate cached venv index: remove entries for venvs that no longer exist on the filesystem +8. Cache both generated manifests and updated venv index +9. Return generated manifests to DSC engine #### Runtime invocation -When DSC invokes a resource operation: +When DSC engine invokes a Python resource operation: + +1. DSC has the adapted resource manifest (from shipped or generated sources) +2. DSC constructs the canonical resource identifier `Type@Version` from the manifest +3. DSC looks up this identifier in the venv index to determine the target Python environment +4. DSC invokes the adapter with: + - The manifest + - The venv path (if found in index) or `null` for system Python + - The operation (get, set, test, delete, export) + - The desired state (stdin) +5. The adapter: + - Selects the appropriate Python executable based on venv path + - Spawns a subprocess with that executable if venv is specified + - Loads the resource module/class from manifest + - Invokes the operation and returns results (stdout/stderr) +6. DSC processes the results + +#### Adapter-generated manifest cache invalidation + +The manifest cache is invalidated when: +- Cached venv index contains stale paths (venvs no longer exist on filesystem) +- New venvs appear in `DSC_PYTHON_VIRTUAL_ENV_PATH` (resources need to be rediscovered from new locations) -1. The adapter loads the cached adapted resource manifest and reads `pythonExecutable` and `venv` -2. Spawns a subprocess using the specified `pythonExecutable` -3. If `venv` is present, activates the VENV by setting `VIRTUAL_ENV` and adjusting `PATH` in the subprocess environment -4. Executes the resource operation (get, set, test, etc.) in that Python context +If the manifest cache is missing or corrupted, the adapter regenerates manifests on next discovery. #### Error handling | Scenario | Behavior | |----------|----------| -| VENV path in `DSC_VENV_PATHS` doesn't exist | Skip with warning; continue to next VENV | -| Python executable not found | Fallback to system default; log warning | +| VENV path in `DSC_PYTHON_VIRTUAL_ENV_PATH` doesn't exist | Skip with warning; continue to next VENV | | Permission denied on VENV | Skip with warning; continue | | No resources found in any VENV | Return empty list | -| Invalid `DSC_PYTHON_EXECUTABLE` path | Raise error; operator must fix configuration | -| Multiple VENVs have the same resource type | Use first match (respects priority order) | +| Multiple VENVs have the same resource (`Type@Version`) | All are discovered; DSC engine selects which to use | +| Adapter-generated manifest cache missing or corrupted | Regenerate manifests on next discovery | +| Venv index missing or corrupted | Log warning; DSC uses system Python for invocation | +| Resource identifier (`Type@Version`) not found in index | Log warning; DSC falls back to system Python for that resource | ## Alternate Proposals and Considerations From 1d1982d9c81512fe89628068149c70c68dc9400e Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:32:39 -0500 Subject: [PATCH 03/11] Remove standard dsc input/output --- rfc/draft/0001-python-adapter.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/rfc/draft/0001-python-adapter.md b/rfc/draft/0001-python-adapter.md index 79aac0a6d..8e87687c6 100644 --- a/rfc/draft/0001-python-adapter.md +++ b/rfc/draft/0001-python-adapter.md @@ -366,21 +366,6 @@ The venv index is maintained across discovery cycles, but validated each time: - If new venvs appear in `DSC_PYTHON_VIRTUAL_ENV_PATH`, discover resources from those venvs and add to index - If all venvs are gone or the index is empty, fall back to system Python only and log a warning -### Stdin/stdout contract - -The adapter reads a JSON object from stdin and writes results to stdout as -newline-delimited JSON (NDJSON). Unknown input fields are silently ignored. - -| Operation | stdin | stdout lines | -|-----------|-------|-------------| -| `get` | Desired state | 1 — actual state | -| `set` (STATE) | Desired state | 1 — actual state | -| `set` (STATE_AND_DIFF) | Desired state | 2 — actual state, then `["changedProp", ...]` | -| `test` (STATE) | Desired state | 1 — actual state | -| `test` (STATE_AND_DIFF) | Desired state | 2 — actual state, then `["differingProp", ...]` | -| `delete` | Desired state | 0 | -| `export` | Filter or `{}` | 0..N — one object per instance | - ### Discovery mechanism Two discovery paths are supported: From abcae1e4b7d2525fecaffb7455ed7779d89bf95c Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:36:58 -0500 Subject: [PATCH 04/11] Fix github copilot review comments --- rfc/draft/0001-python-adapter.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfc/draft/0001-python-adapter.md b/rfc/draft/0001-python-adapter.md index 8e87687c6..16ff13339 100644 --- a/rfc/draft/0001-python-adapter.md +++ b/rfc/draft/0001-python-adapter.md @@ -1,12 +1,12 @@ --- -RFC: 0001 +RFC: RFC0001 Author: "@ThomasNieto" Sponsor: "@ThomasNieto" Status: Draft -SupercededBy: null +SupersededBy: null Version: 1.0 Area: Adapter -CommentsDue: null +CommentsDue: 2026-08-31 --- # Microsoft.Adapter/Python: Python DSC Resource Adapter From 8d07fb664c391701e0968ce3e9864de36fb6cac9 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:42:38 -0500 Subject: [PATCH 05/11] Fix typo --- rfc/draft/0001-python-adapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc/draft/0001-python-adapter.md b/rfc/draft/0001-python-adapter.md index 16ff13339..c4244d025 100644 --- a/rfc/draft/0001-python-adapter.md +++ b/rfc/draft/0001-python-adapter.md @@ -505,7 +505,7 @@ If the manifest cache is missing or corrupted, the adapter regenerates manifests ### Alternative A: Single Python file adapter A single-file adapter is simpler to ship but limits testability and extensibility. -Rejected in favour of the package-based adapter structure. +Rejected in favor of the package-based adapter structure. ### Alternative B: Require Pydantic for all resources From 457111a9b50d5213f18a00877e4205b5d2884fb3 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:03:39 -0500 Subject: [PATCH 06/11] Fix adapted resource manifest example --- rfc/draft/0001-python-adapter.md | 38 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/rfc/draft/0001-python-adapter.md b/rfc/draft/0001-python-adapter.md index c4244d025..d21a0ea4f 100644 --- a/rfc/draft/0001-python-adapter.md +++ b/rfc/draft/0001-python-adapter.md @@ -250,28 +250,24 @@ Unknown metadata keys are silently ignored by `DataclassSchemaProvider` during s ### Adapted resource manifest format Manifests are generated by `dsc-gen manifest` and packaged as package data at -`/dsc/*.dsc.adaptedResource.json`. The `content` field is a JSON -object that encodes the Python module and class used for operation dispatch: +`/dsc/*.dsc.adaptedResource.json`. Manifests follow the DSC adapted +resource manifest schema. The `path` property references a Python module metadata file +that encodes the module and class used for operation dispatch: ```json { - "manifestVersion": "1.0", + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", "type": "Vendor/ResourceName", + "kind": "resource", "version": "1.0.0", "description": "A resource that manages greeting messages.", - "tags": ["example"], - "adapter": { - "type": "Microsoft.Adapter/Python" - }, - "content": { - "module": "vendor_resource.resource", - "class": "ResourceClass" - }, - "get": { "input": "stdin" }, - "set": { "input": "stdin", "return": "stateAndDiff" }, - "test": { "input": "stdin", "return": "stateAndDiff" }, + "author": "Example Corp", + "capabilities": ["get", "set", "test", "export"], + "requireAdapter": "Microsoft.Adapter/Python", + "path": "resource.json", "schema": { "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "name": { @@ -290,6 +286,20 @@ object that encodes the Python module and class used for operation dispatch: } ``` +The `path` property (relative to the manifest file) references an adapter-specific metadata file +that contains the Python module and class information. This file is typically named `resource.json` +and contains: + +```json +{ + "module": "vendor_resource.resource", + "class": "ResourceClass" +} +``` + +Capabilities are derived from the resource's implemented methods at manifest-generation time +(e.g., if the class has a `set()` method, `"set"` is included in `capabilities`). + #### Adapter-generated manifest cache Manifests discovered via the extension (shipped with packages) are NOT cached; they From 622e5be456bad732a983c68f157fb435ac774fd0 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:36:49 -0500 Subject: [PATCH 07/11] Rename RFC --- rfc/draft/{0001-python-adapter.md => RFC0001-python-adapter.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rfc/draft/{0001-python-adapter.md => RFC0001-python-adapter.md} (100%) diff --git a/rfc/draft/0001-python-adapter.md b/rfc/draft/RFC0001-python-adapter.md similarity index 100% rename from rfc/draft/0001-python-adapter.md rename to rfc/draft/RFC0001-python-adapter.md From b0a811bf473d3083da9de4b1fcebfdae66eb81b2 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:44:57 -0500 Subject: [PATCH 08/11] Fix rfc name --- .../{RFC0001-python-adapter.md => RFCNNNN-python-adapter.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rfc/draft/{RFC0001-python-adapter.md => RFCNNNN-python-adapter.md} (100%) diff --git a/rfc/draft/RFC0001-python-adapter.md b/rfc/draft/RFCNNNN-python-adapter.md similarity index 100% rename from rfc/draft/RFC0001-python-adapter.md rename to rfc/draft/RFCNNNN-python-adapter.md From 9163498c2147b59777a2ba08a52dc85ef30c1da9 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:48:36 -0500 Subject: [PATCH 09/11] Remove RFC number --- rfc/RFC0000-rfc-process.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc/RFC0000-rfc-process.md b/rfc/RFC0000-rfc-process.md index d74965d90..b840ddc23 100644 --- a/rfc/RFC0000-rfc-process.md +++ b/rfc/RFC0000-rfc-process.md @@ -1,5 +1,5 @@ --- -RFC: RFC0000 +RFC: RFCNNNN Author: @michaeltlombardi Sponsor: @michaeltlombardi Status: Draft From 4b71c0563e18457cc4892650a04fdd53b103ff13 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:06:22 -0500 Subject: [PATCH 10/11] Revert RFC number for unrelated file --- rfc/RFC0000-rfc-process.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc/RFC0000-rfc-process.md b/rfc/RFC0000-rfc-process.md index b840ddc23..d74965d90 100644 --- a/rfc/RFC0000-rfc-process.md +++ b/rfc/RFC0000-rfc-process.md @@ -1,5 +1,5 @@ --- -RFC: RFCNNNN +RFC: RFC0000 Author: @michaeltlombardi Sponsor: @michaeltlombardi Status: Draft From 989d5464c9bea6c07f5cd685dc71221e8067d557 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:19:14 -0500 Subject: [PATCH 11/11] Fix RFC number to placeholder --- rfc/draft/RFCNNNN-python-adapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc/draft/RFCNNNN-python-adapter.md b/rfc/draft/RFCNNNN-python-adapter.md index d21a0ea4f..0691d29c6 100644 --- a/rfc/draft/RFCNNNN-python-adapter.md +++ b/rfc/draft/RFCNNNN-python-adapter.md @@ -1,5 +1,5 @@ --- -RFC: RFC0001 +RFC: RFCNNNN Author: "@ThomasNieto" Sponsor: "@ThomasNieto" Status: Draft